@khalidadil/commerce-manager (0.4.0)
Installation
@khalidadil:registry=npm install @khalidadil/commerce-manager@0.4.0"@khalidadil/commerce-manager": "0.4.0"About this package
@khalidadil/commerce-manager
Generic shop-logic module for any Node app: multi-currency carts, checkout
orchestration, order state machine, discounts, inventory, emails, order
history, webhooks. Postgres-backed (module-owned commerce schema),
SureCart-powered via @khalidadil/surecart-commerce (≥0.2 for saved methods), live shipping rates via
@khalidadil/shipping-manager. This README is the agent-facing map —
every file is small enough to read whole (largest: express.ts, 320 lines).
Status
| Version | 0.4.0 |
|---|---|
| Verified | 44 db tests (npm run test:db, real Postgres) + 2 live tests vs real SureCart test mode |
| Registry | npm config set @khalidadil:registry https://git.kadil.dev/api/packages/khalidadil/npm/ |
| Host DB | Injected pg Pool; module migrates its own commerce schema (never touches host tables) |
Consumer contract (the single entry point)
import { createCommerce } from '@khalidadil/commerce-manager';
import { createCommerceRouter } from '@khalidadil/commerce-manager/express';
const commerce = createCommerce({
db: pgPool,
surecart: checkoutService, // surecart-commerce CheckoutService
catalog: { resolveItem(ref, currency) { /* → name, unitAmountCents, pricing,
trackInventory?, parcel?, metadata? */ } },
identity: (req) => ({ id, email, isAdmin }) | null, // optional
shipping: { skeletonPriceId, flatRateCents?, freeThresholdCents?,
rates?, shipFrom?, quoteTtlSeconds? }, // optional
email: { sender, from?, templates? }, // optional
webhooks: { signingSecret, paidEvents?, refundEvents? }, // optional
config: { currencies: ['usd'], mode: 'test'|'live',
approvalGate?: 'auto'|'manual', lowStockThreshold?: number },
});
await commerce.migrate();
app.use('/api/shop', createCommerceRouter(commerce, { identity }));
Returns: { carts, discounts, inventory, checkout, shippingQuotes, orders, webhooks, surecart, events, db, migrate, reconcilePendingOrders, config }.
Feature → file map (all paths under src/)
- Contract & types —
types.ts(CatalogAdapter, ResolvedItem+parcel, ItemPricing, RatesAdapter, ShippingConfig, CommerceError, entity shapes) - createCommerce wiring —
index.ts(DI, event bus, email listeners, reconcilePendingOrders) - DB layer —
db.ts(query/one/maybeOne/tx over injected Pool) - Migrations —
migrations.ts(id-tracked, additive-only; 0001 core schema, 0002 shipping_quotes, 0003 discount scope/free_shipping + order_refunds) - Carts —
carts.ts(currency pinned at create; EVERY write re-resolves the catalog; merge/update/claim/freeze) - Checkout orchestration —
checkout.ts(re-resolve → inventory guard → discount quote+distribution → shipping → SureCart mapping → order row → cart freeze;distributeDiscountlargest-remainder, scope-aware) - Discounts —
discounts.ts(percent/fixed/free_shipping; scoping; windows/caps; bulk one-offs; list/get/update; pending-aware + email-aware caps;PENDING_HOLD_WINDOW = '2 hours') - Inventory —
inventory.ts(opt-in guard w/ pending-hold, ledgered set/consume/restock, listStock/history,inventory.lowemission) - Shipping quotes —
shippingQuotes.ts(RatesAdapter → persisted options; checkout trusts ONLY the stored row) - Orders —
ordersService.ts(transition graph, idempotent markPaid pipeline, partial/full refunds + ledger),orders.ts(row shaping) - Webhooks —
webhookProcessor.ts(verify → dedup → tolerant processing;extractCheckoutId) - Emails —
emails.ts(EmailSender iface, defaultTemplates, stubSender, resendSender) - HTTP adapter —
express.ts(whole surface incl. admin API; raw-body webhook route registered BEFORE json())
HTTP surface (mount prefix is the host's)
| Route | Auth | Purpose |
|---|---|---|
POST /carts · GET /carts/:id |
— | create (optional currency) / read |
POST /carts/:id/lines · PATCH /carts/:id/lines/:ref |
— | add {ref, quantity} / set quantity (0 removes) |
POST /carts/:id/shipping-options |
— | live quotes → stored options (needs shipping.rates) |
GET /my/checkout-prefill |
identity | returning-customer saved name/address + reusable methods (own email only; guests get null) |
POST /carts/:id/checkout |
— | {…, surecart_customer_id?, saved_payment_method_id?} → {order, surecart_checkout, saved_method_outcome?} |
GET /my/orders · GET /orders/:id |
identity | history / detail (owner or admin) |
GET /admin/orders |
admin | list by status |
POST /admin/orders/:id/transition |
admin | {to, note} state moves |
POST /admin/orders/:id/refund · GET …/refunds |
admin | full or partial {amount_cents?, lines?, reason?, restock?} / ledger |
GET /admin/orders/:id/shipping-address |
admin | fulfillment address (live from SureCart under 'minimal' PII) |
GET/POST/PATCH/DELETE /admin/discounts[...] |
admin | list?active= · create · update · deactivate |
GET /admin/discounts/:code |
admin | config + redemption count |
POST /admin/discounts/bulk |
admin | {count, prefix?, …config} → N unique single-use codes |
GET /admin/inventory · GET /admin/inventory/:ref · PUT /admin/inventory/:ref |
admin | levels / level+history / set {stock} |
POST /admin/reconcile |
admin | missed-webhook fallback |
POST /webhooks/surecart |
HMAC | raw-body receiver (403 bad signature; dedup by event id) |
Schema (commerce.*)
| Table | Purpose / key columns |
|---|---|
| carts, cart_lines | server carts; lines snapshot ref/name/qty/unit amount/pricing |
| orders, order_lines | totals snapshot, surecart ids, discount_code, addresses; number ≥1001 |
| order_events | append-only status ledger (from→to, actor, note) |
| order_refunds | partial/full refund ledger (amount, lines, reason, restocked) |
| discounts, discount_redemptions | kind percent/fixed/free_shipping, applies_to scope, windows, caps |
| inventory_items, inventory_events | stock + FULL movement ledger (incl. manual_adjustment) |
| shipping_quotes | stored live options (cart-scoped, TTL) |
| webhook_events | (provider, provider_event_id) unique = dedup |
| migrations | applied migration ids |
Events (commerce.events)
checkout.created · order.paid · order.approved · order.fulfilling ·
order.fulfilled · order.completed · order.cancelled · order.refunded ·
order.partially_refunded {order, amountCents, reason} ·
order.payment_failed · inventory.low {catalogRef, stock} ·
webhook.processed · email.failed. Email listeners auto-attach when
email config present (confirmation on paid, refund notice on refunded).
Invariants & traps (violate these and totals go wrong silently)
- Prices are never trusted from storage — every cart write AND checkout
re-resolves
catalog.resolveItem(ref, cart.currency). Sale pricing belongs in the catalog adapter (that IS the store-wide-discount mechanism). - SureCart
ad_hoc_amountsilently no-ops on non-ad-hoc prices — any discounted/custom amount must ride a price created withad_hoc: true(skeleton). The orchestrator errors loudly if a discounted line lackspricing.adHoc.skeletonPriceId. - Discounted lines go to SureCart as quantity-1 ad-hoc LINE TOTALS
(sidesteps unit-vs-total ambiguity); real
surecartPriceIdlines pass through only when undiscounted. - Shipping amounts come from the DB, never the client — checkout accepts
shipping_quote_idonly; the row must belong to the cart and be unexpired. live_mode/ mode gating lives in surecart-commerce — this module never sets it.- SureCart rounds tax PER LINE then sums (multi-line orders differ from total-rounding by cents — verified live).
- One discount code per checkout (no stacking) — deliberate.
- Saved-method checkout is single-auth: prefill is identity-gated (only
the session customer's own email — no account enumeration); a saved-method
charge needs
surecart_customer_id(the checkout must be customer-linked or SureCart won't enforce reusability) and settles the order server-side (idempotent markPaid); a bank 3DS returnssaved_method_outcome: 'requires_action'→ browser confirms the intent. - PII posture defaults to 'minimal' — no customer name/address stored in
commerce.orders (SureCart holds the address;
orders.getShippingAddress()fetches live for fulfillment; email is kept for mail + guest caps); stored webhook payloads are PII-redacted in BOTH modes (redactPii). Opt into snapshots withconfig.storePii: 'full'. - Webhook verification needs the RAW request bytes — the webhook route must stay ahead of any json() middleware (express.ts already orders this).
Race mitigations (the pending-hold pattern)
pending_payment orders younger than PENDING_HOLD_WINDOW (2h) hold their
claims: one-off/global discount caps count them, and the inventory guard
subtracts their quantities ("soft reservation"). Stale unpaid checkouts
release automatically when the window lapses. Guest per-customer discount
caps additionally match on order email, not just customer_ref.
Deliberate boundaries / still open
| Item | State |
|---|---|
| Money movement on refunds | Module records + restocks + transitions; the actual SureCart refund is the host's call (surecart resources.createRefund or dashboard), reconciled by the refund webhook |
| Hard inventory reservations | Soft (pending-hold) only — revisit at volume |
| Async jobs (pg-boss) | v1 is synchronous; queued jobs are the 0.4 upgrade |
| Storefront React components | Payment UI ships in surecart-commerce/react; module wrappers deferred |
| SureCart production webhook event NAMES | Configurable lists (paidEvents/refundEvents) pending a real-delivery confirmation |
| Labels/tracking | shipping-manager v2 |
Dev
npm run build # tsc → dist
npm run test:db # 34 tests vs postgres://postgres:commerce@localhost:5434/commerce_manager_test
npm run test:live # real SureCart test mode; needs SURECART_API_TOKEN in .env
Local Postgres: docker container commerce-pg, port 5434 (5433 belongs to
another project). Test DBs only; suites drop/recreate the commerce schema
and must run with --test-concurrency=1 (already in the npm scripts).
Dependencies
Dependencies
| ID | Version |
|---|---|
| @khalidadil/surecart-commerce | ^0.2.0 |
| pg | ^8.13.1 |
Development Dependencies
| ID | Version |
|---|---|
| @types/express | ^4.17.21 |
| @types/pg | ^8.11.10 |
| express | ^4.21.2 |
| tsx | ^4.19.2 |
| typescript | ^5.7.3 |
Peer Dependencies
| ID | Version |
|---|---|
| express | ^4.18.0 || ^5.0.0 |