khalidadil
  • Joined on 2025-08-25

@khalidadil/commerce-manager (0.3.1)

Published 2026-09-02 03:27:50 +00:00 by khalidadil

Installation

@khalidadil:registry=
npm install @khalidadil/commerce-manager@0.3.1
"@khalidadil/commerce-manager": "0.3.1"

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, 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.3.0
Verified 34 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; distributeDiscount largest-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.low emission)
  • 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)
POST /carts/:id/checkout — {email, name, shipping_address, discount_code, shipping_quote_id, metadata} → {order, surecart_checkout}
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)

  1. 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).
  2. SureCart ad_hoc_amount silently no-ops on non-ad-hoc prices — any discounted/custom amount must ride a price created with ad_hoc: true (skeleton). The orchestrator errors loudly if a discounted line lacks pricing.adHoc.skeletonPriceId.
  3. Discounted lines go to SureCart as quantity-1 ad-hoc LINE TOTALS (sidesteps unit-vs-total ambiguity); real surecartPriceId lines pass through only when undiscounted.
  4. Shipping amounts come from the DB, never the client — checkout accepts shipping_quote_id only; the row must belong to the cart and be unexpired.
  5. live_mode / mode gating lives in surecart-commerce — this module never sets it.
  6. SureCart rounds tax PER LINE then sums (multi-line orders differ from total-rounding by cents — verified live).
  7. One discount code per checkout (no stacking) — deliberate.
  8. 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 with config.storePii: 'full'.
  9. 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.1.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
Details
npm
2026-09-02 03:27:50 +00:00
0
50 KiB
Assets (1)
Versions (5) View all
0.4.0 2026-09-02
0.3.1 2026-09-02
0.3.0 2026-09-02
0.2.0 2026-09-02
0.1.0 2026-09-01