Read PRD.md, architecture.md, and architecture-essentials.md before making structural changes. This file is operational: how to work in the codebase, not what to build.
/app → Next.js App Router pages (storefront)
/(storefront)/[sector]/[product]
/admin/... → admin panel — this is the reliable path for order status, not a fallback UI
/api
/checkout
/webhooks/paystack
/webhooks/whatsapp
/cron/abandoned-sweep
/admin/...
/lib
/db → Prisma client, transactions
/paystack → Paystack SDK wrapper, verify helper
/whatsapp → Cloud API send/receive helpers, template registry
/notify → shared status-change notification function (see rule 6)
/pdf → invoice/receipt generation
/prisma/schema.prisma
- Never treat the browser redirect as payment confirmation. Order status only ever flips to
paidinside the Paystack webhook handler, after signature verification + idempotency check. - Paystack webhook handlers must be idempotent on
paystack_reference. - Stock decrement and order-status-to-paid happen in one DB transaction.
- Invoice/receipt numbers come from a DB sequence inside the confirmation transaction, not
count()+1, not UUIDs. - Never overwrite
OrderItem.product_name_snapshot/unit_price_kobo_snapshotfrom live product data. - All customer-facing status notifications go through one shared function (
/lib/notify), called for every transition topaid,shipped,delivered,returned. Email is sent unconditionally inside this function. WhatsApp is attempted only ifCustomer.whatsapp_opt_inis true, and only after the email send has been kicked off — not instead of it. - A WhatsApp send failure must never throw out of
/lib/notifyor block order processing. Catch it, write aMessageNotificationLogrow withstatus=failed, move on. This function's job is "best-effort notify," not "guarantee WhatsApp delivery." - Never send a WhatsApp message using a template that isn't in the approved-template registry (
/lib/whatsapp/templates.tsor equivalent, mapping template name → Meta template ID + approval status). If a needed template doesn't exist or isn't approved yet, log and skip the WhatsApp attempt — don't invent a freeform business-initiated message as a workaround; that's exactly the policy violation the opt-in/template system exists to prevent. - Check
Customer.whatsapp_opt_inand a validwhatsapp_phone_e164before every WhatsApp send attempt. No opt-in, no attempt, full stop — not even for what feels like an obviously-wanted message (e.g. "your payment failed"). - Inbound WhatsApp webhook messages must be routed by sender number: check against
AdminUser.whatsapp_phone_e164first (admin command handling — status changes, order lookups) before falling through to customer FAQ/support handling. - The admin panel must have a fully independent path for every action a WhatsApp button can trigger (mark shipped, mark delivered, mark returned). Do not build a WhatsApp-button-only action with no panel equivalent — see architecture-essentials.md's fulfillment decision.
abandonedis not terminal. A late Paystack webhook for anabandonedorder must still transition it topaidnormally.returnedorders never auto-refund or auto-restock. Status change only.- Don't add a website live-chat widget. Support is WhatsApp-only by design.
- Don't add stock reservation, RBAC, microservice splits, AI bot replies, or a BSP abstraction layer unless explicitly requested. All documented as deliberately deferred/rejected in architecture-essentials.md.
- Store all amounts as integer kobo (smallest currency unit), never floats.
- Format to Naira only at the display layer.
- All outbound sends go through
/lib/whatsapp, which wraps the Graph APIPOST /{phone-number-id}/messagescall. Don't call the Graph API directly from route handlers. - New message types require: (a) a template drafted and submitted to Meta, (b) approval confirmed, (c) an entry added to the template registry with its approved name/ID, in that order. Code that references a template name should assume it might not be approved yet in a given environment and fail gracefully, not throw.
- Webhook handler must respond to Meta's GET verification handshake (echoing the
hub.challengeparam whenhub.verify_tokenmatches) and must validate theX-Hub-Signature-256header on POST payloads using the app secret. - Test against Meta's provided test phone number and the small set of allowlisted recipient numbers during development — production sending requires the app to be out of development mode.
- Local development: use Paystack test mode keys and documented test cards.
- Any change to the Paystack webhook handler must be manually re-verified against Paystack's "resend webhook" tool for: a fresh success event, a duplicate/replayed event, and a success event for an order already in
abandonedstatus.
- Must be safe to run concurrently with itself.
- Email only — do not add a WhatsApp reminder without an explicit PRD change (see architecture-essentials.md's rejected-alternatives table for why).
- Paystack webhook idempotency.
- Transactional integrity of payment confirmation.
- Invoice/receipt number uniqueness under concurrent orders.
- Notification behavior: email always sent; WhatsApp attempted only when opted-in; a simulated WhatsApp API failure must not affect order state or the email send.
- Admin panel status-change actions work with WhatsApp integration fully disabled/mocked — this is the regression test for rule 11.
- Abandoned-cart sweep idempotency and the late-payment-after-abandonment path.
- Admin-vs-customer message routing in the WhatsApp webhook handler.
- TypeScript strict mode on.
- Prisma migrations committed with every schema change — never hand-edit the database in a deployed environment.
- No new top-level services/deployables without updating architecture.md first.