@ikeboy003/cart 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.d.ts CHANGED
@@ -32,6 +32,40 @@ export interface CreateCartStoreOptions {
32
32
  * without forcing it into a Workers bundle.
33
33
  */
34
34
  export declare function createCartStore<TItem extends CartItem = CartItem>(opts?: CreateCartStoreOptions): Promise<DurableCartStore<TItem>>;
35
+ /** `order_intents` DDL — the immutable per-checkout order snapshot. `createOrderStore`
36
+ * self-applies it (idempotent), so no separate migration step. */
37
+ export declare const ORDER_INTENTS_SQL = "CREATE TABLE IF NOT EXISTS order_intents (\n cart_id TEXT PRIMARY KEY,\n site TEXT NOT NULL,\n items TEXT NOT NULL DEFAULT '[]',\n subtotal_cents INTEGER NOT NULL DEFAULT 0,\n customer_name TEXT,\n customer_email TEXT,\n customer_phone TEXT,\n created_at INTEGER\n)";
38
+ /** A checkout snapshot: the cart's lines + the guest customer, frozen at PaymentIntent
39
+ * time. The payment webhook reads this (not the mutable cart) to build the order. */
40
+ export interface OrderSnapshot<TItem extends CartItem = CartItem> {
41
+ /** Cart id — the PaymentIntent carries it in metadata; the webhook joins on it. */
42
+ cartId: string;
43
+ /** Owning site / namespace (becomes the order's `site`). */
44
+ site: string;
45
+ /** Cart lines, stored VERBATIM — the order pipeline projects them to its own shape. */
46
+ items: TItem[];
47
+ /** Guest contact — only known at checkout, never on the cart. */
48
+ customer?: {
49
+ name?: string;
50
+ email?: string;
51
+ phone?: string;
52
+ };
53
+ }
54
+ /** Writes {@link OrderSnapshot}s. One method — the snapshot is write-once-per-checkout. */
55
+ export interface OrderSnapshotStore<TItem extends CartItem = CartItem> {
56
+ save(snapshot: OrderSnapshot<TItem>): Promise<void>;
57
+ }
58
+ /**
59
+ * Build the order-snapshot store. Same env gating as {@link createCartStore}: a D1 binding
60
+ * is used when present (and self-applies the `order_intents` DDL); production without one
61
+ * throws; local without one falls back to `node:sqlite`.
62
+ *
63
+ * ```ts
64
+ * const orders = await createOrderStore({ d1: env.CART_DB, env: "production" });
65
+ * await orders.save({ cartId, site: "fourth-street", items, customer });
66
+ * ```
67
+ */
68
+ export declare function createOrderStore<TItem extends CartItem = CartItem>(opts?: CreateCartStoreOptions): Promise<OrderSnapshotStore<TItem>>;
35
69
  /**
36
70
  * Web-standard `GET`/`PUT` handlers for `/api/cart`, matching the `httpSync`
37
71
  * wire contract. Framework-agnostic (Request → Response) — re-export them from a
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEzD,wFAAwF;AACxF,MAAM,WAAW,gBAAgB,CAAC,KAAK,SAAS,QAAQ,GAAG,QAAQ;IACjE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,CAAC;IAC7D,KAAK,CAAC,QAAQ,EAAE,YAAY,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD;AAED,kFAAkF;AAClF,MAAM,WAAW,MAAM;IACrB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAAC;CACpC;AACD,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,UAAU,CAAC;IACvC,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACxB,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;CACzD;AAED,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,YAAY,CAAC;AAElD,mFAAmF;AACnF,eAAO,MAAM,eAAe,8QAQ1B,CAAC;AAEH,MAAM,WAAW,sBAAsB;IACrC,0FAA0F;IAC1F,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,6FAA6F;IAC7F,GAAG,CAAC,EAAE,YAAY,CAAC;IACnB,2FAA2F;IAC3F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA4ED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,KAAK,SAAS,QAAQ,GAAG,QAAQ,EACrE,IAAI,GAAE,sBAA2B,GAChC,OAAO,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAUlC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,KAAK,SAAS,QAAQ,GAAG,QAAQ,EAC5D,KAAK,EAAE,gBAAgB,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,GAChE;IACD,GAAG,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACzC,GAAG,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC1C,CA0BA"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEzD,wFAAwF;AACxF,MAAM,WAAW,gBAAgB,CAAC,KAAK,SAAS,QAAQ,GAAG,QAAQ;IACjE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,CAAC;IAC7D,KAAK,CAAC,QAAQ,EAAE,YAAY,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD;AAED,kFAAkF;AAClF,MAAM,WAAW,MAAM;IACrB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAAC;CACpC;AACD,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,UAAU,CAAC;IACvC,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACxB,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;CACzD;AAED,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,YAAY,CAAC;AAElD,mFAAmF;AACnF,eAAO,MAAM,eAAe,8QAQ1B,CAAC;AAEH,MAAM,WAAW,sBAAsB;IACrC,0FAA0F;IAC1F,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,6FAA6F;IAC7F,GAAG,CAAC,EAAE,YAAY,CAAC;IACnB,2FAA2F;IAC3F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA4ED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,KAAK,SAAS,QAAQ,GAAG,QAAQ,EACrE,IAAI,GAAE,sBAA2B,GAChC,OAAO,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAUlC;AAUD;mEACmE;AACnE,eAAO,MAAM,iBAAiB,qTAS5B,CAAC;AAEH;sFACsF;AACtF,MAAM,WAAW,aAAa,CAAC,KAAK,SAAS,QAAQ,GAAG,QAAQ;IAC9D,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,uFAAuF;IACvF,KAAK,EAAE,KAAK,EAAE,CAAC;IACf,iEAAiE;IACjE,QAAQ,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9D;AAED,2FAA2F;AAC3F,MAAM,WAAW,kBAAkB,CAAC,KAAK,SAAS,QAAQ,GAAG,QAAQ;IACnE,IAAI,CAAC,QAAQ,EAAE,aAAa,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD;AA+ED;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,SAAS,QAAQ,GAAG,QAAQ,EACtE,IAAI,GAAE,sBAA2B,GAChC,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAQpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,KAAK,SAAS,QAAQ,GAAG,QAAQ,EAC5D,KAAK,EAAE,gBAAgB,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,GAChE;IACD,GAAG,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACzC,GAAG,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC1C,CA0BA"}
package/dist/server.js CHANGED
@@ -99,6 +99,88 @@ export async function createCartStore(opts = {}) {
99
99
  }
100
100
  return sqliteStore(opts.sqlitePath ?? "cart.sqlite", table);
101
101
  }
102
+ // ── order snapshot ─────────────────────────────────────────────────────────────
103
+ //
104
+ // The live cart is MUTABLE — it's emptied on checkout success. A payment webhook that
105
+ // rebuilt the order by reading the cart therefore raced the clear and got empty/0 orders.
106
+ // So at PaymentIntent time the checkout freezes an IMMUTABLE snapshot of the cart's lines
107
+ // (+ the guest customer, which only exists at checkout, never on the cart) into its own
108
+ // `order_intents` table, keyed by cart_id. The order pipeline reads THIS; carts stay carts.
109
+ /** `order_intents` DDL — the immutable per-checkout order snapshot. `createOrderStore`
110
+ * self-applies it (idempotent), so no separate migration step. */
111
+ export const ORDER_INTENTS_SQL = `CREATE TABLE IF NOT EXISTS order_intents (
112
+ cart_id TEXT PRIMARY KEY,
113
+ site TEXT NOT NULL,
114
+ items TEXT NOT NULL DEFAULT '[]',
115
+ subtotal_cents INTEGER NOT NULL DEFAULT 0,
116
+ customer_name TEXT,
117
+ customer_email TEXT,
118
+ customer_phone TEXT,
119
+ created_at INTEGER
120
+ )`;
121
+ /** Σ priceCents × quantity over the cart lines — the order subtotal, frozen here so the
122
+ * webhook never has to recompute it off a cart that may already be gone. */
123
+ function lineSubtotalCents(items) {
124
+ return items.reduce((n, i) => n + Math.round(i.priceCents) * i.quantity, 0);
125
+ }
126
+ function d1OrderStore(d1) {
127
+ const ready = d1.prepare(ORDER_INTENTS_SQL).run(); // self-bootstrap; idempotent
128
+ return {
129
+ async save(s) {
130
+ await ready;
131
+ await d1
132
+ .prepare(`INSERT INTO order_intents
133
+ (cart_id, site, items, subtotal_cents, customer_name, customer_email, customer_phone, created_at)
134
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
135
+ ON CONFLICT(cart_id) DO UPDATE SET
136
+ items=?3, subtotal_cents=?4, customer_name=?5, customer_email=?6, customer_phone=?7`)
137
+ .bind(s.cartId, s.site, JSON.stringify(s.items), lineSubtotalCents(s.items), s.customer?.name ?? "", s.customer?.email ?? "", s.customer?.phone ?? "", Date.now())
138
+ .run();
139
+ },
140
+ };
141
+ }
142
+ async function sqliteOrderStore(path) {
143
+ let DatabaseSync;
144
+ try {
145
+ const spec = "node:sqlite";
146
+ const mod = (await import(spec));
147
+ DatabaseSync = mod.DatabaseSync;
148
+ }
149
+ catch {
150
+ throw new Error("[cart] local order store needs Node's built-in node:sqlite (Node >= 22.5; stable in 24). Upgrade Node, or pass a d1 binding.");
151
+ }
152
+ const db = new DatabaseSync(path);
153
+ db.exec(ORDER_INTENTS_SQL);
154
+ const ins = db.prepare(`INSERT INTO order_intents
155
+ (cart_id, site, items, subtotal_cents, customer_name, customer_email, customer_phone, created_at)
156
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
157
+ ON CONFLICT(cart_id) DO UPDATE SET
158
+ items=excluded.items, subtotal_cents=excluded.subtotal_cents, customer_name=excluded.customer_name,
159
+ customer_email=excluded.customer_email, customer_phone=excluded.customer_phone`);
160
+ return {
161
+ async save(s) {
162
+ ins.run(s.cartId, s.site, JSON.stringify(s.items), lineSubtotalCents(s.items), s.customer?.name ?? "", s.customer?.email ?? "", s.customer?.phone ?? "", Date.now());
163
+ },
164
+ };
165
+ }
166
+ /**
167
+ * Build the order-snapshot store. Same env gating as {@link createCartStore}: a D1 binding
168
+ * is used when present (and self-applies the `order_intents` DDL); production without one
169
+ * throws; local without one falls back to `node:sqlite`.
170
+ *
171
+ * ```ts
172
+ * const orders = await createOrderStore({ d1: env.CART_DB, env: "production" });
173
+ * await orders.save({ cartId, site: "fourth-street", items, customer });
174
+ * ```
175
+ */
176
+ export async function createOrderStore(opts = {}) {
177
+ if (opts.d1)
178
+ return d1OrderStore(opts.d1);
179
+ if ((opts.env ?? "local") === "production") {
180
+ throw new Error("[cart] production order store requires a D1 binding (pass `d1: env.CART_DB`).");
181
+ }
182
+ return sqliteOrderStore(opts.sqlitePath ?? "cart.sqlite");
183
+ }
102
184
  /**
103
185
  * Web-standard `GET`/`PUT` handlers for `/api/cart`, matching the `httpSync`
104
186
  * wire contract. Framework-agnostic (Request → Response) — re-export them from a
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,+EAA+E;AAC/E,8EAA8E;AAC9E,8BAA8B;AAC9B,EAAE;AACF,iFAAiF;AACjF,4EAA4E;AAC5E,sEAAsE;AACtE,+EAA+E;AAC/E,8DAA8D;AAC9D,EAAE;AACF,iFAAiF;AACjF,gFAAgF;AAChF,qCAAqC;AAsBrC,mFAAmF;AACnF,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;EAQ7B,CAAC;AAaH,SAAS,OAAO,CAAyB,EAAU,EAAE,KAAa;IAChE,OAAO;QACL,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS;YACtB,MAAM,KAAK,GAAG,MAAM,EAAE;iBACnB,OAAO,CAAC,qBAAqB,KAAK,mCAAmC,CAAC;iBACtE,IAAI,CAAC,EAAE,EAAE,SAAS,CAAC;iBACnB,KAAK,CAAS,OAAO,CAAC,CAAC;YAC1B,IAAI,KAAK,IAAI,IAAI;gBAAE,OAAO,IAAI,CAAC;YAC/B,IAAI,CAAC;gBACH,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAY,CAAC;YACtC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QACD,KAAK,CAAC,KAAK,CAAC,CAAC;YACX,MAAM,EAAE;iBACL,OAAO,CACN,eAAe,KAAK;;qEAEuC,CAC5D;iBACA,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;iBAC7D,GAAG,EAAE,CAAC;QACX,CAAC;KACF,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,WAAW,CACxB,IAAY,EACZ,KAAa;IAMb,IAAI,YAAyC,CAAC;IAC9C,IAAI,CAAC;QACH,uEAAuE;QACvE,sEAAsE;QACtE,0CAA0C;QAC1C,MAAM,IAAI,GAAG,aAAa,CAAC;QAC3B,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,CAE9B,CAAC;QACF,YAAY,GAAG,GAAG,CAAC,YAAY,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,+HAA+H,CAChI,CAAC;IACJ,CAAC;IACD,MAAM,EAAE,GAAG,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;IAClC,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CACpB,eAAe,KAAK;;4FAEoE,CACzF,CAAC;IACF,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,qBAAqB,KAAK,iCAAiC,CAAC,CAAC;IACpF,OAAO;QACL,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS;YACtB,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,CAAmC,CAAC;YACrE,IAAI,CAAC,GAAG,EAAE,KAAK;gBAAE,OAAO,IAAI,CAAC;YAC7B,IAAI,CAAC;gBACH,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAY,CAAC;YAC1C,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QACD,KAAK,CAAC,KAAK,CAAC,CAAC;YACX,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC;QAChF,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,OAA+B,EAAE;IAEjC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC;IAChC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,OAAO,CAAC;IACpC,IAAI,IAAI,CAAC,EAAE;QAAE,OAAO,OAAO,CAAQ,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IACnD,IAAI,GAAG,KAAK,YAAY,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,0GAA0G,CAC3G,CAAC;IACJ,CAAC;IACD,OAAO,WAAW,CAAQ,IAAI,CAAC,UAAU,IAAI,aAAa,EAAE,KAAK,CAAC,CAAC;AACrE,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAC1B,KAAiE;IAKjE,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACxC,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,GAAG;YACX,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC7B,MAAM,EAAE,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACtC,MAAM,SAAS,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;YAC1D,IAAI,CAAC,EAAE;gBAAE,OAAO,IAAI,QAAQ,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;YAC5D,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,QAAQ,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;YACzD,IAAI,CAAC,KAAK;gBAAE,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;YACvD,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QAClC,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,GAAG;YACX,IAAI,IAAyB,CAAC;YAC9B,IAAI,CAAC;gBACH,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAwB,CAAC;YACnD,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,IAAI,QAAQ,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;YACvD,CAAC;YACD,IAAI,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC5C,OAAO,IAAI,QAAQ,CAAC,kBAAkB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;YAC3D,CAAC;YACD,MAAM,CAAC,MAAM,QAAQ,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACnC,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QAC7C,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikeboy003/cart",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Headless cart primitive: local-first state + a configurable durable sync path. Framework-agnostic core, optional React binding. No POS/payment/store logic baked in.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,24 +17,41 @@
17
17
  "import": "./dist/server.js"
18
18
  }
19
19
  },
20
- "files": ["dist", "src"],
20
+ "files": [
21
+ "dist",
22
+ "src"
23
+ ],
21
24
  "sideEffects": false,
22
25
  "scripts": {
23
26
  "build": "tsc -p tsconfig.json",
24
27
  "dev": "tsc -p tsconfig.json --watch",
25
28
  "test": "node --test test/*.test.mjs"
26
29
  },
27
- "keywords": ["cart", "ecommerce", "headless", "local-first"],
30
+ "keywords": [
31
+ "cart",
32
+ "ecommerce",
33
+ "headless",
34
+ "local-first"
35
+ ],
28
36
  "license": "MIT",
29
- "publishConfig": { "access": "public" },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
30
40
  "peerDependencies": {
31
41
  "react": ">=18"
32
42
  },
33
43
  "peerDependenciesMeta": {
34
- "react": { "optional": true }
44
+ "react": {
45
+ "optional": true
46
+ }
35
47
  },
36
48
  "devDependencies": {
37
49
  "@types/react": "^19",
38
50
  "typescript": "^5.5.0"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/ikeboy003/ikeboy-npm-packages.git",
55
+ "directory": "packages/cart"
39
56
  }
40
57
  }
package/src/server.ts CHANGED
@@ -148,6 +148,144 @@ export async function createCartStore<TItem extends CartItem = CartItem>(
148
148
  return sqliteStore<TItem>(opts.sqlitePath ?? "cart.sqlite", table);
149
149
  }
150
150
 
151
+ // ── order snapshot ─────────────────────────────────────────────────────────────
152
+ //
153
+ // The live cart is MUTABLE — it's emptied on checkout success. A payment webhook that
154
+ // rebuilt the order by reading the cart therefore raced the clear and got empty/0 orders.
155
+ // So at PaymentIntent time the checkout freezes an IMMUTABLE snapshot of the cart's lines
156
+ // (+ the guest customer, which only exists at checkout, never on the cart) into its own
157
+ // `order_intents` table, keyed by cart_id. The order pipeline reads THIS; carts stay carts.
158
+
159
+ /** `order_intents` DDL — the immutable per-checkout order snapshot. `createOrderStore`
160
+ * self-applies it (idempotent), so no separate migration step. */
161
+ export const ORDER_INTENTS_SQL = `CREATE TABLE IF NOT EXISTS order_intents (
162
+ cart_id TEXT PRIMARY KEY,
163
+ site TEXT NOT NULL,
164
+ items TEXT NOT NULL DEFAULT '[]',
165
+ subtotal_cents INTEGER NOT NULL DEFAULT 0,
166
+ customer_name TEXT,
167
+ customer_email TEXT,
168
+ customer_phone TEXT,
169
+ created_at INTEGER
170
+ )`;
171
+
172
+ /** A checkout snapshot: the cart's lines + the guest customer, frozen at PaymentIntent
173
+ * time. The payment webhook reads this (not the mutable cart) to build the order. */
174
+ export interface OrderSnapshot<TItem extends CartItem = CartItem> {
175
+ /** Cart id — the PaymentIntent carries it in metadata; the webhook joins on it. */
176
+ cartId: string;
177
+ /** Owning site / namespace (becomes the order's `site`). */
178
+ site: string;
179
+ /** Cart lines, stored VERBATIM — the order pipeline projects them to its own shape. */
180
+ items: TItem[];
181
+ /** Guest contact — only known at checkout, never on the cart. */
182
+ customer?: { name?: string; email?: string; phone?: string };
183
+ }
184
+
185
+ /** Writes {@link OrderSnapshot}s. One method — the snapshot is write-once-per-checkout. */
186
+ export interface OrderSnapshotStore<TItem extends CartItem = CartItem> {
187
+ save(snapshot: OrderSnapshot<TItem>): Promise<void>;
188
+ }
189
+
190
+ /** Σ priceCents × quantity over the cart lines — the order subtotal, frozen here so the
191
+ * webhook never has to recompute it off a cart that may already be gone. */
192
+ function lineSubtotalCents<TItem extends CartItem>(items: TItem[]): number {
193
+ return items.reduce((n, i) => n + Math.round(i.priceCents) * i.quantity, 0);
194
+ }
195
+
196
+ function d1OrderStore<TItem extends CartItem>(d1: D1Like): OrderSnapshotStore<TItem> {
197
+ const ready = d1.prepare(ORDER_INTENTS_SQL).run(); // self-bootstrap; idempotent
198
+ return {
199
+ async save(s) {
200
+ await ready;
201
+ await d1
202
+ .prepare(
203
+ `INSERT INTO order_intents
204
+ (cart_id, site, items, subtotal_cents, customer_name, customer_email, customer_phone, created_at)
205
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
206
+ ON CONFLICT(cart_id) DO UPDATE SET
207
+ items=?3, subtotal_cents=?4, customer_name=?5, customer_email=?6, customer_phone=?7`,
208
+ )
209
+ .bind(
210
+ s.cartId,
211
+ s.site,
212
+ JSON.stringify(s.items),
213
+ lineSubtotalCents(s.items),
214
+ s.customer?.name ?? "",
215
+ s.customer?.email ?? "",
216
+ s.customer?.phone ?? "",
217
+ Date.now(),
218
+ )
219
+ .run();
220
+ },
221
+ };
222
+ }
223
+
224
+ async function sqliteOrderStore<TItem extends CartItem>(
225
+ path: string,
226
+ ): Promise<OrderSnapshotStore<TItem>> {
227
+ type SqliteDb = {
228
+ exec(sql: string): void;
229
+ prepare(sql: string): { run(...a: unknown[]): unknown };
230
+ };
231
+ let DatabaseSync: new (p: string) => SqliteDb;
232
+ try {
233
+ const spec = "node:sqlite";
234
+ const mod = (await import(spec)) as unknown as { DatabaseSync: new (p: string) => SqliteDb };
235
+ DatabaseSync = mod.DatabaseSync;
236
+ } catch {
237
+ throw new Error(
238
+ "[cart] local order store needs Node's built-in node:sqlite (Node >= 22.5; stable in 24). Upgrade Node, or pass a d1 binding.",
239
+ );
240
+ }
241
+ const db = new DatabaseSync(path);
242
+ db.exec(ORDER_INTENTS_SQL);
243
+ const ins = db.prepare(
244
+ `INSERT INTO order_intents
245
+ (cart_id, site, items, subtotal_cents, customer_name, customer_email, customer_phone, created_at)
246
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
247
+ ON CONFLICT(cart_id) DO UPDATE SET
248
+ items=excluded.items, subtotal_cents=excluded.subtotal_cents, customer_name=excluded.customer_name,
249
+ customer_email=excluded.customer_email, customer_phone=excluded.customer_phone`,
250
+ );
251
+ return {
252
+ async save(s) {
253
+ ins.run(
254
+ s.cartId,
255
+ s.site,
256
+ JSON.stringify(s.items),
257
+ lineSubtotalCents(s.items),
258
+ s.customer?.name ?? "",
259
+ s.customer?.email ?? "",
260
+ s.customer?.phone ?? "",
261
+ Date.now(),
262
+ );
263
+ },
264
+ };
265
+ }
266
+
267
+ /**
268
+ * Build the order-snapshot store. Same env gating as {@link createCartStore}: a D1 binding
269
+ * is used when present (and self-applies the `order_intents` DDL); production without one
270
+ * throws; local without one falls back to `node:sqlite`.
271
+ *
272
+ * ```ts
273
+ * const orders = await createOrderStore({ d1: env.CART_DB, env: "production" });
274
+ * await orders.save({ cartId, site: "fourth-street", items, customer });
275
+ * ```
276
+ */
277
+ export async function createOrderStore<TItem extends CartItem = CartItem>(
278
+ opts: CreateCartStoreOptions = {},
279
+ ): Promise<OrderSnapshotStore<TItem>> {
280
+ if (opts.d1) return d1OrderStore<TItem>(opts.d1);
281
+ if ((opts.env ?? "local") === "production") {
282
+ throw new Error(
283
+ "[cart] production order store requires a D1 binding (pass `d1: env.CART_DB`).",
284
+ );
285
+ }
286
+ return sqliteOrderStore<TItem>(opts.sqlitePath ?? "cart.sqlite");
287
+ }
288
+
151
289
  /**
152
290
  * Web-standard `GET`/`PUT` handlers for `/api/cart`, matching the `httpSync`
153
291
  * wire contract. Framework-agnostic (Request → Response) — re-export them from a