Skip to content

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.

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=…
  1. Create the session. The route handler POST /api/create-checkout-session receives only { orderId }. It loads the order with the customer’s token, refuses orders that are already paid or cancelled, and creates a Checkout Session for order.total. The order id is saved in the session’s metadata. Customers land on /success when they pay, or on /user/orders/:id when they cancel.
  2. Receive the webhook. POST /api/stripe-webhook checks the Stripe signature and acts on checkout.session.completed and checkout.session.async_payment_succeeded. If the API call fails it answers 502, so Stripe tries again.
  3. Confirm on the API. PUT /api/orders/:id/webhook-status gets the session from Stripe and checks that it is paid, belongs to this order and covers the total. Only then does it set paymentStatus to paid.
  4. Back up the webhook. If the webhook hasn’t arrived yet, the /success page sends the same webhook-status request, then checks the order up to 6 times, 5 seconds apart.
apps/web/lib/server/checkout.ts apps/web/lib/server/stripeWebhook.ts apps/api/src/modules/payments/stripeVerification.ts› confirmStripePaymentForOrder

Unpaid orders show a “Pay Now” button under /user/orders, which starts the same flow.

The mobile app uses Stripe’s PaymentSheet with a PaymentIntent, which the API creates:

POST/api/payments/create-intent POST/api/payments/webhook
  • create-intent uses the total calculated on the server, in usd, for cards only, and puts the orderId in 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_SECRET is not set. It handles checkout.session.completed and payment_intent.succeeded, and ignores all other events.
apps/api/src/modules/payments/payments.service.ts

SSLCommerz is a Bangladeshi payment gateway. The API talks to it from the server side:

  1. 1InitPOST /api/payments/sslcommerz/init
  2. 2Gatewaycustomer pays in BDT
  3. 3Callbacksuccess · fail · cancel · ipn
  4. 4ValidatevalidationserverAPI.php
  5. 5Redirect/success or /checkout?payment=…
SSLCommerz, from init to paid
  • The amount is always in BDT. SSLCOMMERZ_IS_LIVE chooses securepay.sslcommerz.com (live) or sandbox.sslcommerz.com.
  • The callback routes are public, because the gateway calls them. Each one re-checks the val_id with SSLCommerz before it touches the order.
  • The callback URLs are built from SERVER_URL, and the customer is sent back to CLIENT_URL. Both must be set to the public URLs in production.
apps/api/src/modules/payments/sslcommerz.service.ts POST/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:

  1. While the order is delivering, the deliveryman takes the cash and records it (PUT /api/orders/workflow/:id/collect-cod). The order’s paymentStatus becomes paid (older records may still say cod_collected), and a CashCollection is created as collected.
  2. At the end of the day they hand the cash to accounts (PUT /api/cash-collections/submit). The collection becomes submitted.
  3. Accounts counts it and confirms (PUT /api/cash-collections/accounts/confirm). The collection becomes confirmed, and accounts can complete the order.

Who may call each step is listed in Auth and roles.

Stripe’s CLI forwards webhook events to your machine. Run it in its own terminal, pointed at the webhook you want to test:

3000/api/stripe-webhook
pnpm webhook:web

Copy 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).