Payment Flow
Flow Diagram
graph TB
subgraph Checkout
A["POST /order/process\nOrderController::store()"] --> B["CartItem validation\nBuyableRule\nPurchaseLimitRule"]
B --> C["StoreOrderAction\nOrder status: open\nlockForUpdate variations + ckc_codes"]
C --> D{Payment method}
end
subgraph SBPS
D -->|"credit/docomo/au/softbank/paypay/unionpay"| E["PaymentManager::provider('sbps', method)\nSbpsLinkService\nSHA-1 sps_hashcode"]
E --> F["POST to SBPS gateway\nShift_JIS encoded\nHTTP redirect"]
F --> G{SBPS result}
G -->|success| H["POST /order/finish/{order}\nOrderController::complete()"]
G -->|failure| I["POST /order/failure/{order}\nOrderController::fail()"]
H --> J["Order status: success\nCkcCode assigned\nOrderFinished event"]
I --> K["Order status: failure\nOrderRetry created"]
end
subgraph Atone
D -->|"atone BNPL"| L["AtoneChecksumService\nSHA-256 HMAC\nJS SDK on confirm page"]
L --> M["Inline fulfilment\nOrderController::store()\nno redirect"]
M --> N["POST /webhook/atone\nhash_equals Np-Confirmation-Checksum"]
N --> J
end
subgraph Webhook
P["POST /webhook/sbps\nSbpsPaymentController"] --> Q["idempotency: status === OPEN\nno HMAC verification"]
Q --> J
end
J --> R["SendOrderFinishNotification\nemail to customer (sync)"]
Purpose
Orchestrates the complete purchase-to-fulfilment pipeline including payment gateway integration, CKC serial code assignment, and order status reconciliation via webhooks.
Trigger Points
POST /order/process — customer submits order
POST /order/finish/{order} — SBPS success redirect
POST /order/failure/{order} — SBPS failure redirect
POST /webhook/sbps — asynchronous SBPS status update
POST /webhook/atone — Atone authorization result webhook
Actors
User (authenticated, verified, address complete)
PaymentManager — provider factory
- SBPS payment gateway (external)
- Atone BNPL platform (external)
Lifecycle Steps
SBPS Path
OrderController::store(): validate cart items, StoreOrderAction creates Order (status open) inside DB transaction with lockForUpdate on product_variations and ckc_codes
PaymentManager::provider('sbps', $method) returns the appropriate SbpsLinkService subclass
- SHA-1
sps_hashcode computed from params; Shift_JIS encoding applied to item_name
- HTTP POST/redirect to SBPS gateway URL (env-switched prod/test)
- On success:
POST /order/finish/{order} → OrderController::complete()
- On failure:
POST /order/failure/{order} → OrderController::fail(); OrderRetry record created
- SBPS also sends asynchronous webhook
POST /webhook/sbps — idempotency guard: only processes if Order::status === OPEN
Atone Path
- Confirm page renders JS SDK with checksum computed by
AtoneChecksumService (SHA-256 HMAC, ksort+flatten of params, separate prod/test key pairs)
- Customer completes BNPL inline
OrderController::store() handles Atone inline (no redirect) — fulfilment runs synchronously
- Atone sends
POST /webhook/atone — hash_equals on Np-Confirmation-Checksum header for timing-safe verification
Post-Fulfilment
CkcCode records assigned to OrderDetail (if product has CKC codes)
CkcConnect::attach() called inside DB transaction — failure rolls back entire order
OrderFinished event dispatched → SendOrderFinishNotification runs synchronously (no queue)
- Email sent to customer
Laravel Components Involved
| Component |
Path |
| OrderController |
app/Http/Controllers/Web/Order/OrderController.php |
| PaymentManager |
app/Modules/Payment/PaymentManager.php |
| AtoneChecksumService |
app/Modules/Payment/AtoneChecksumService.php |
| SbpsLinkService |
app/Modules/Payment/Providers/Sbps/SbpsLinkService.php |
| SbpsPaymentController |
app/Http/Controllers/Web/Webhook/SbpsPaymentController.php |
| AtoneWebhookController |
app/Http/Controllers/Web/Webhook/AtoneWebhookController.php |
| StoreOrderAction |
app/Modules/Order/Actions/StoreOrderAction.php |
| OrderRetry model |
app/Models/OrderRetry.php |
| Webhook routes |
routes/webhook.php |
Data Mutations
| Step |
Table |
Change |
| Order creation |
orders |
status = open, FK to user |
| Order detail |
order_details |
line items per variation |
| CKC assignment |
ckc_codes |
order_detail_id populated |
| Fulfilment |
orders |
status = success |
| Failure |
orders |
status = failure |
| Retry |
order_retries |
new retry record with token |
| Stock |
product_variations |
stock decremented |
State Transitions
open → success (payment confirmed + fulfilment complete)
open → failure (payment rejected)
failure → open (OrderRetry retry attempt)
success → processing (admin marks processing)
processing → shipped (admin imports shipping CSV)
shipped → canceled (admin action)
Failure Paths
| Scenario |
Behaviour |
| SBPS payment rejected |
order/failure → Order failure, OrderRetry created with token-gated retry URL |
| SBPS webhook duplicate |
Idempotency guard: status === OPEN check prevents double-fulfilment |
| CkcConnect::attach() failure |
DB transaction rollback — order NOT completed |
| Stock exhausted at checkout |
lockForUpdate + BuyableRule validation prevents oversell |
| Atone webhook missing checksum |
hash_equals fails → 400 response |
| SBPS webhook: no HMAC |
No inbound signature check — relies on state-based idempotency only |
Security Considerations
- SBPS webhook has no inbound HMAC verification — any party can POST to
/webhook/sbps and trigger state checks. Idempotency guard (status === OPEN) prevents duplicate fulfilment but doesn't prevent probing.
- Atone webhook:
Np-Confirmation-Checksum validated with hash_equals (timing-safe).
- Webhook routes use
api middleware group — no CSRF, no session auth. SBPS also explicitly excluded from VerifyCsrfToken.
CkcConnect::attach() inside DB transaction: external API failure causes full rollback — network latency directly affects DB lock hold time.
lockForUpdate on product_variations and ckc_codes during order creation — lock duration = network round-trip to payment gateway (SBPS redirect path). Minimise by completing DB work before redirect.
SendOrderFinishNotification fires synchronously — mail send on order complete is blocking.
- Atone fulfilment is inline in
OrderController::store() — no async queue.