Payments
How BabyMart takes money — Stripe, SSLCommerz and cash on delivery — and how an order is marked paid.
The order is created first. The customer pays for it afterwards with one of
three methods (paymentMethod):
| Method | Used by | Currency | Confirmed by |
|---|---|---|---|
Stripe (stripe) |
Storefront (Checkout Session), mobile app (PaymentSheet) | USD | Signed webhook, or the API checking with Stripe |
SSLCommerz (sslcommerz) |
Storefront, mobile app | BDT | SSLCommerz validation API |
Cash on delivery (cod) |
Storefront, mobile app | — | Deliveryman collects, accounts confirms |
One rule holds for all three: only the API decides that an order is paid. It checks every payment claim with the payment provider, so a redirect URL or a client call cannot mark an order paid by itself.
Stripe on the storefront
Section titled “Stripe on the storefront”The storefront uses Stripe Checkout, a hosted payment page. The amount comes from the order saved on the API, never from the browser.
sequenceDiagram
autonumber
participant B as Browser
participant W as Storefront (Next.js)
participant A as API
participant S as Stripe
B->>A: POST /api/orders (cart items)
A-->>B: order (priced on the server)
B->>W: POST /api/create-checkout-session { orderId }
W->>A: GET /api/orders/:id (user token)
W->>S: create Checkout Session (order.total, metadata.orderId)
S-->>B: redirect to hosted checkout
B->>S: pays by card
S->>W: webhook checkout.session.completed
W->>A: PUT /api/orders/:id/webhook-status { stripeSessionId }
A->>S: retrieve session, check paid + amount
A-->>W: order paymentStatus = paid
S-->>B: redirect to /success?orderId=…
- Create the session. The route handler
POST /api/create-checkout-sessionreceives only{ orderId }. It loads the order with the customer’s token, refuses orders that are already paid or cancelled, and creates a Checkout Session fororder.total. The order id is saved in the session’s metadata. Customers land on/successwhen they pay, or on/user/orders/:idwhen they cancel. - Receive the webhook.
POST /api/stripe-webhookchecks the Stripe signature and acts oncheckout.session.completedandcheckout.session.async_payment_succeeded. If the API call fails it answers502, so Stripe tries again. - Confirm on the API.
PUT /api/orders/:id/webhook-statusgets the session from Stripe and checks that it is paid, belongs to this order and covers the total. Only then does it setpaymentStatustopaid. - Back up the webhook. If the webhook hasn’t arrived yet, the
/successpage sends the samewebhook-statusrequest, then checks the order up to 6 times, 5 seconds apart.
Unpaid orders show a “Pay Now” button under /user/orders, which starts the
same flow.
Stripe in the mobile app and on the API
Section titled “Stripe in the mobile app and on the API”The mobile app uses Stripe’s PaymentSheet with a PaymentIntent, which the API creates:
POST/api/payments/create-intent
POST/api/payments/webhook
create-intentuses the total calculated on the server, inusd, for cards only, and puts theorderIdin the metadata.- The API’s own webhook is mounted before the JSON parser so it receives the
raw request body. It refuses every event when
STRIPE_WEBHOOK_SECRETis not set. It handlescheckout.session.completedandpayment_intent.succeeded, and ignores all other events.
SSLCommerz
Section titled “SSLCommerz”SSLCommerz is a Bangladeshi payment gateway. The API talks to it from the server side:
- 1InitPOST /api/payments/sslcommerz/init
- 2Gatewaycustomer pays in BDT
- 3Callbacksuccess · fail · cancel · ipn
- 4ValidatevalidationserverAPI.php
- 5Redirect/success or /checkout?payment=…
- The amount is always in BDT.
SSLCOMMERZ_IS_LIVEchoosessecurepay.sslcommerz.com(live) orsandbox.sslcommerz.com. - The callback routes are public, because the gateway calls them. Each one
re-checks the
val_idwith SSLCommerz before it touches the order. - The callback URLs are built from
SERVER_URL, and the customer is sent back toCLIENT_URL. Both must be set to the public URLs in production.
Cash on delivery
Section titled “Cash on delivery”/api/orders/cod
A COD order skips online payment and goes straight to the staff workflow.
The cash it brings in is tracked as a CashCollection:
- While the order is
delivering, the deliveryman takes the cash and records it (PUT /api/orders/workflow/:id/collect-cod). The order’spaymentStatusbecomespaid(older records may still saycod_collected), and aCashCollectionis created ascollected. - At the end of the day they hand the cash to accounts
(
PUT /api/cash-collections/submit). The collection becomessubmitted. - Accounts counts it and confirms (
PUT /api/cash-collections/accounts/confirm). The collection becomesconfirmed, and accounts can complete the order.
Who may call each step is listed in Auth and roles.
Test payments locally
Section titled “Test payments locally”Stripe’s CLI forwards webhook events to your machine. Run it in its own terminal, pointed at the webhook you want to test:
pnpm webhook:webstripe listen --forward-to localhost:8000/api/payments/webhookCopy the whsec_… secret the CLI prints into STRIPE_WEBHOOK_SECRET for
that app. Use test keys only, and pay with Stripe’s test card
4242 4242 4242 4242 (any future date, any CVC).