D
Engineering

Order Lifecycle

Alur order dari create hingga complete/cancel. Mencakup OrderObserver quirk (mass update tidak trigger), status string literal, race condition, dan bug refund.

Order adalah entitas bisnis utama. Alurnya melibatkan creation, payment, shipping, dan interaction dengan wallet & stock. OrderController.php 2.987 baris dengan banyak inkonsistensi.

Ringkasan alur

text
CREATE:
  POST /api/order/create
    → OrderController@createOrder (:1612)
    → DB::beginTransaction (no-op di MongoDB!)
    → set status awal (pending/process/delivery tergantung shipping)
    → set payment_status='unpaid'
    → TIDAK ada validasi stok
    → TIDAK ada decrement stok

PAYMENT (transisi ke 'paid'):
  Webhook DOKU / manual admin / polling
    → $order->payment_status = 'paid'; $order->save()  ← WAJIB save() agar observer trigger
    → OrderObserver@updated (:18)
      → IF isDirty('payment_status') AND old != 'paid' AND new == 'paid':
        → IF payment_method == 'instant_payment':
            WalletService::processDeposit (kredit wallet)
        → decrementStock (semua order item)
        → DigitalProductService->sendProductLink

STATUS CHANGE:
  Manual via API (mass update, TIDAK trigger observer)
  Atau $order->save() (trigger observer, tapi observer hanya dengar payment_status)

1. Order creation

Endpoint

RouteHandler
POST /api/order/create (routes/api.php:224)OrderController@createOrder (OrderController.php:1612)
GET /orders/create (routes/web.php:222)OrderController@create (render form only)
ShortlinkShortlinkController set payment_status='unpaid' (:307-316)
Engine BotEngineBotPaymentController:300 set payment_method='instant_payment'

Field yang di-set saat create

Dari OrderController.php:1778-1805:

FieldDefaultCatatan
idUUIDGenerated di Order::boot() (Order.php:21-23)
order_numberHelper di :2245
sequenceHelper di :2283 — baca latest() +1 tanpa lock
customer_id, customer_name, customer_waDari model Customer
payment_methodstring requestinstant_payment, cod, bank_transfer, epayment
store_id$userStore->store_idScope multi-tenant (:1787)
total_price(int) $request->total_price
statusdinamisDigital → 'process'; Ada shipping → 'process'; Lain → 'pending'; Ada resi → 'delivery'
payment_status'unpaid'Hardcoded (:1795)
tracking_status'none'Update ke 'ready' saat shipping di-setup
rating0
addressesobjectBentuk flat atau {label, value}tidak seragam
shippingobject{name, code, service, description, etd, cost, resi, courier}
utm_*nullutm_campaing — typo yang terlanjur dipakai

2. Status transisi

Status order

Enum formal ada di app/Enums/StatusOrder.php:5-12: pending, process, cancel, completed, complained, refund. Tapi enum ini tidak dipakai — seluruh transisi pakai string literal.

String tambahan di codebase yang tidak ada di enum:

  • 'delivery' — saat create pakai resi (OrderController.php:1743), update via manual_receipt_input (:3223)
  • 'complete' — typo/inconsistency dengan 'completed' (:1235,2784,2939)
  • 'complete,complained' — compound status (:1237) — menyulitkan query

payment_status values

ValueSumberTrigger
'unpaid'create order (:1795)Default awal
'confirmed'customer upload bukti transfer (PaymentController.php:73,77)Manual
'pending'hapus bukti transfer (PaymentController.php:172-173)Anomali — balik ke pending padahal sebelumnya confirmed
'paid'webhook DOKU / manual admin / pollingTrigger observer

Tidak ada state machine formal

Hanya if/else bertingkat. Tidak ada guard yang melarang transisi dari paidcancel atau lompatan status arbitrari. change_order_status (:1192-1222) menerima $request->status apa pun dan langsung update.

Endpoint yang mengubah status

EndpointMethodTrigger observer?
POST /api/order/change_order_status (:1192)mass updateTIDAK
POST /api/order/change_order_status_multiple (:1224)mass update per itemTIDAK
POST /api/order/bulk_order_status (:951)bulk updateTIDAK
POST /api/order/change_payment_status (:1260)mass updateTIDAK — bug: stock & wallet tidak update
POST /api/order/change_payment_status_multiple (:1280)$order->save()YA — comment eksplisit di :1290
POST /api/order/update_shipping/{id} (:1482)mass updateTIDAK
POST /api/order/manual_receipt_input (:3160)$order->save() per itemYA (tapi tidak ubah payment_status, jadi observer handler tidak jalan)
POST /api/order/upload_resi (:2716)bulk CSV
POST /api/order/update_InformationOrder/{id} (:2088)$order->save()YA

3. OrderObserver

app/Observers/OrderObserver.php. Hanya updated event yang di-handle (:18). Tidak ada handler creating, created, updating, deleting, deleted.

Trigger condition

php
if ($order->isDirty('payment_status') 
    && $order->payment_status === 'paid' 
    && $order->getOriginal('payment_status') !== 'paid') {
    // jalan
}

(OrderObserver.php:21-24)

Yang dilakukan observer

  1. Wallet deposit hanya jika payment_method === 'instant_payment' (:32):
    • Panggil WalletService::processDeposit(storeId, order.id, total_price, ...) (:34-39)
    • Non-blocking — error di-log, order tetap sukses (:40-48)
  2. Decrement stok produk via decrementStock() (:52) — untuk semua order item (variant & non-variant). Juga non-blocking (:98-105)
  3. Kirim link produk digital via DigitalProductService->sendProductLink($order) (:55)

Observer tidak mengubah status order. Pengetusan status='process' untuk digital order dilakukan terpisah di DokuCheckoutController.php:393 (sebelum save), bukan di observer.

Quirk penting

4. Payment integration

Payment method

MethodSumberWallet deposit?
codBankController.php:436-449TIDAK
instant_paymentDOKU VA/QRISYA — lewat observer
bank_transferPaymentController.php:52TIDAK
epaymentPaymentController.php:52TIDAK

Pembayaran gagal/expired

  • Field doku_payment_expired_at disimpan saat create checkout (DokuCheckoutController.php:287,294)
  • Klien polling POST /api/payment/check-status/{orderId} (routes/api.php:813) untuk status real-time
  • Jika expired: EngineBotPaymentController::checkStatus (:398-407) return status='expired' ke Engine Bot, TIDAK mengubah payment_status di DB. Order tetap unpaid.
  • Tidak ada cron auto-cancel order unpaid expired — order tetap unpaid selamanya kecuali admin manual

5. Shipping

Provider

ProviderEndpoint baseFungsi
RajaOngkir Komercehttps://rajaongkir.komerce.id/api/v1/Calculate cost + track waybill (RajaongkirController.php:112,176)
RajaOngkir Prohttps://pro.rajaongkir.com/api/costLegacy (RajaongkirController.php:144)
API Kurirhttps://live.apikurir.idVendor berbeda (ShippingController.php:18)

Resi update

Tiga jalur:

  1. Saat create order dengan input number_receipt → field shipping.resi di-set (:1715) + status='delivery' (:1743)
  2. POST /api/order/manual_receipt_input (:3160) — bulk JSON array, $order->save() per item
  3. POST /api/order/update_shipping/{id} (:1482) — update address + shipping + resi sekaligus

Tracking webhook

Tidak ada. Endpoint /api/rajaongkir/waybill adalah pull (klien minta tracking ke rajaongkir), bukan webhook. Tidak ada route POST dari rajaongkir/apikurir ke aplikasi.

6. Order & wallet interaction

Kapan wallet di-debit/kredit?

AksiTriggerMethod
Kredit (deposit)OrderObserver::updated saat paid + instant_paymentWalletService::processDeposit (OrderObserver.php:32-49)
Debit (withdraw)Manual via WalletService::requestWithdrawalTidak terkait lifecycle order

Refund

Komisi/fee

  • Deposit order: fee = 0 (WalletService.php:358, hardcoded). Full total_price masuk wallet
  • Withdraw: fee dari config doku.wallet.withdrawal_fee (default 5000). Net = amount - fee. Ini fee withdraw, bukan fee per-order

7. Edge case & bug

Race condition

  1. DB::beginTransaction() di OrderController.php:1773MongoDB tidak support ACID transactions. DB::rollBack() di :1949 tidak akan rollback data yang sudah tersimpan
  2. Helper sequence() (:2283) dan order_number() (:2245) baca latest() lalu +1 tanpa lock → dua request bersamaan bisa generate nomor urut sama
  3. change_payment_status tanpa lock, bisa double-trigger (observer punya guard idempotency, tapi OrderPayment bisa di-double-count)

Stok tidak bisa negatif, tapi bisa tidak sinkron

decrementStock pakai max(0, $oldStock - $quantity) (OrderObserver.php:133,179). Tapi:

  • Jika stok awal 0 dan qty > 0, stok tetap 0 tanpa error — order tetap sukses
  • Decrement non-blocking (:98-105) — kalau gagal (mis. product tidak ditemukan), error di-log tapi order tetap paid. Stok bisa tidak sinkron dengan order paid tanpa sinyal error ke user

Order bisa cancel setelah paid

Tidak ada guard. change_order_status (:1192-1222) menerima $request->status apa pun:

  • Order paid + cancel → stok sudah diturunkan (irreversible), wallet sudah di-deposit (tidak ada debit balik)
  • Order paid + refund → sama, wallet tidak dikembalikan
  • Statistik dashboard exclude cancel/refund, tapi wallet & stok sudah tidak konsisten

Referensi file

FileBarisPeran
app/Http/Controllers/OrderController.php2.987Create, update, status change
app/Observers/OrderObserver.php57Trigger wallet + stock + digital link
app/Models/Order.phpModel (no casts, no SoftDeletes)
app/Enums/StatusOrder.php12Enum (tidak dipakai)
app/Http/Controllers/DokuCheckoutController.phpCreate checkout + handle webhook
app/Http/Controllers/PaymentController.phpUpload bukti transfer