@thebes/cadmea-plugin-ecommerce 1.0.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/index.js ADDED
@@ -0,0 +1,554 @@
1
+ import { checkRateLimit } from "@thebes/cadmus/rate-limit";
2
+ //#region src/errors.ts
3
+ /**
4
+ * Thrown by `createCheckoutHandler` on price-mismatch/inventory rejection,
5
+ * and by `PaymentProvider` implementations on charge failure.
6
+ * `createCheckoutHandler` catches it internally (`instanceof
7
+ * CadmeaPaymentError`) and maps it to HTTP 402 itself — checkout/webhook
8
+ * handlers are plain Hono routes, never mounted through
9
+ * `@thebes/cadmus/hono`'s `mountCmsRoutes`, so there's no shared
10
+ * error-to-status pipeline this needs to participate in.
11
+ *
12
+ * Deliberately a plain `Error` subclass, not a `CadmusCmsError` one — this
13
+ * is a Cadmea-plugin-owned error, not a Cadmus-primitive one (every real
14
+ * `CadmusError` subclass is owned by a `packages/cadmus/src/<primitive>/`
15
+ * folder; a payment error belongs to a plugin, not a primitive), and
16
+ * `CadmusCmsError` is only reachable via the root `@thebes/cadmus` package
17
+ * export (not `@thebes/cadmus/cms`) — importing that root barrel here
18
+ * would pull in every other primitive's runtime code (including
19
+ * Workers-only modules like `cloudflare:email`) just for one base class.
20
+ * Keeping this plugin-local and dependency-free is the honest shape.
21
+ */
22
+ var CadmeaPaymentError = class extends Error {
23
+ cause;
24
+ constructor(message, cause) {
25
+ super(message);
26
+ this.cause = cause;
27
+ this.name = "CadmeaPaymentError";
28
+ }
29
+ };
30
+ //#endregion
31
+ //#region src/checkout.ts
32
+ function subtotalCents(lineItems) {
33
+ return lineItems.reduce((sum, item) => sum + item.clientUnitPrice.amount * item.quantity, 0);
34
+ }
35
+ function generateOrderNumber() {
36
+ return `ORD-${crypto.randomUUID().replace(/-/g, "").slice(0, 12).toUpperCase()}`;
37
+ }
38
+ /**
39
+ * Returns a Hono handler implementing the checkout flow against a
40
+ * `PaymentProvider`: rate limit → re-verify cart prices/availability
41
+ * (never trust client-submitted prices) → idempotent customer find-or-
42
+ * create → charge → persist `orders`/`payments` rows. A DB-write failure
43
+ * *after* a successful charge degrades to a 200-with-warning response,
44
+ * never a false "payment failed" — the customer's card was actually
45
+ * charged, telling them otherwise would be worse than a delayed manual
46
+ * reconciliation.
47
+ *
48
+ * Mount it as a plain Hono route alongside `mountCmsRoutes` — checkout
49
+ * isn't part of the generic CMS REST surface that function mounts.
50
+ *
51
+ * ```ts
52
+ * app.post("/api/checkout", createCheckoutHandler({ provider, orders, payments, resolveContext }));
53
+ * ```
54
+ */
55
+ function createCheckoutHandler(options) {
56
+ return async (c) => {
57
+ if (options.rateLimit) {
58
+ const ip = c.req.header("CF-Connecting-IP") ?? "unknown";
59
+ const key = `${options.rateLimit.keyPrefix ?? "checkout"}:${ip}`;
60
+ if (!(await checkRateLimit(options.rateLimit.kv, key, options.rateLimit.limit, options.rateLimit.windowSeconds)).allowed) return c.json({ error: "Rate limit exceeded" }, 429);
61
+ }
62
+ const body = await c.req.json();
63
+ if (!Array.isArray(body.lineItems) || body.lineItems.length === 0) return c.json({ error: "Checkout request must include at least one line item" }, 400);
64
+ if (!body.idempotencyKey) return c.json({ error: "Checkout request must include an idempotencyKey" }, 400);
65
+ try {
66
+ const refs = body.lineItems.map((item) => item.catalogRef);
67
+ const priceChecks = await options.provider.checkCatalogPrices(refs);
68
+ const checkByRef = new Map(priceChecks.map((check) => [check.catalogRef, check]));
69
+ for (const item of body.lineItems) {
70
+ const check = checkByRef.get(item.catalogRef);
71
+ if (!check) throw new CadmeaPaymentError(`Unknown catalog item "${item.catalogRef}"`);
72
+ if (check.serverUnitPrice.amount !== item.clientUnitPrice.amount) throw new CadmeaPaymentError(`Price mismatch for "${item.catalogRef}" — checkout rejected`);
73
+ if (check.availableQuantity !== void 0 && check.availableQuantity < item.quantity) throw new CadmeaPaymentError(`Insufficient inventory for "${item.catalogRef}"`);
74
+ }
75
+ } catch (error) {
76
+ if (error instanceof CadmeaPaymentError) return c.json({ error: error.message }, 402);
77
+ throw error;
78
+ }
79
+ if (body.customerEmail) await options.provider.findOrCreateCustomer(body.customerEmail, body.idempotencyKey);
80
+ const result = await options.provider.checkout({
81
+ lineItems: body.lineItems,
82
+ paymentSourceToken: body.paymentSourceToken,
83
+ customerEmail: body.customerEmail,
84
+ idempotencyKey: body.idempotencyKey,
85
+ metadata: body.metadata
86
+ });
87
+ const context = await options.resolveContext(c);
88
+ const orderData = {
89
+ orderNumber: generateOrderNumber(),
90
+ status: result.status === "succeeded" ? "paid" : "pending",
91
+ totalCents: result.amount.amount,
92
+ subtotalCents: subtotalCents(body.lineItems),
93
+ currency: result.amount.currency,
94
+ provider: options.provider.name,
95
+ providerOrderRef: result.providerOrderRef,
96
+ providerPaymentRef: result.providerPaymentRef,
97
+ guestEmail: body.customerEmail,
98
+ lineItems: body.lineItems.map((item) => ({
99
+ productName: item.catalogRef,
100
+ quantity: item.quantity,
101
+ unitPriceCents: item.clientUnitPrice.amount,
102
+ totalPriceCents: item.clientUnitPrice.amount * item.quantity,
103
+ catalogRef: item.catalogRef
104
+ })),
105
+ shippingAddress: body.shippingAddress
106
+ };
107
+ try {
108
+ const order = await options.orders.create(context, orderData);
109
+ await options.payments.create(context, {
110
+ provider: options.provider.name,
111
+ providerPaymentRef: result.providerPaymentRef,
112
+ providerOrderRef: result.providerOrderRef,
113
+ order: order.id,
114
+ status: result.status,
115
+ amountCents: result.amount.amount,
116
+ currency: result.amount.currency,
117
+ rawResponse: result.raw
118
+ });
119
+ return c.json({ order }, 201);
120
+ } catch (cause) {
121
+ return c.json({
122
+ warning: "Payment succeeded but order record-keeping failed — contact support",
123
+ providerPaymentRef: result.providerPaymentRef,
124
+ providerOrderRef: result.providerOrderRef,
125
+ cause: cause instanceof Error ? cause.message : String(cause)
126
+ }, 200);
127
+ }
128
+ };
129
+ }
130
+ //#endregion
131
+ //#region src/collections.ts
132
+ const DEFAULTS = {
133
+ products: "products",
134
+ orders: "orders",
135
+ customers: "customers",
136
+ payments: "payments",
137
+ webhookEvents: "webhook_events",
138
+ subscriptions: "subscriptions",
139
+ users: "users"
140
+ };
141
+ function buildProductsCollection(slug) {
142
+ return {
143
+ slug,
144
+ fields: {
145
+ id: {
146
+ type: "number",
147
+ autoIncrement: true
148
+ },
149
+ name: {
150
+ type: "text",
151
+ required: true
152
+ },
153
+ description: { type: "text" },
154
+ status: {
155
+ type: "select",
156
+ options: [
157
+ "draft",
158
+ "active",
159
+ "archived"
160
+ ],
161
+ required: true,
162
+ defaultValue: "draft"
163
+ },
164
+ variants: {
165
+ type: "array",
166
+ required: true,
167
+ fields: {
168
+ sku: {
169
+ type: "text",
170
+ required: true
171
+ },
172
+ catalogRef: {
173
+ type: "text",
174
+ required: true
175
+ },
176
+ priceCents: {
177
+ type: "number",
178
+ required: true
179
+ },
180
+ currency: {
181
+ type: "text",
182
+ defaultValue: "USD"
183
+ },
184
+ inventoryCount: { type: "number" }
185
+ }
186
+ },
187
+ createdAt: {
188
+ type: "date",
189
+ mode: "timestamp",
190
+ defaultValue: "now"
191
+ }
192
+ }
193
+ };
194
+ }
195
+ function buildOrdersCollection(slug, customersSlug) {
196
+ return {
197
+ slug,
198
+ fields: {
199
+ id: {
200
+ type: "number",
201
+ autoIncrement: true
202
+ },
203
+ orderNumber: {
204
+ type: "text",
205
+ required: true,
206
+ unique: true
207
+ },
208
+ status: {
209
+ type: "select",
210
+ options: [
211
+ "pending",
212
+ "paid",
213
+ "failed",
214
+ "refunded",
215
+ "partially_refunded"
216
+ ],
217
+ required: true,
218
+ defaultValue: "pending"
219
+ },
220
+ totalCents: {
221
+ type: "number",
222
+ required: true
223
+ },
224
+ subtotalCents: {
225
+ type: "number",
226
+ required: true
227
+ },
228
+ taxCents: { type: "number" },
229
+ currency: {
230
+ type: "text",
231
+ defaultValue: "USD"
232
+ },
233
+ provider: {
234
+ type: "select",
235
+ options: ["square", "stripe"],
236
+ required: true
237
+ },
238
+ providerOrderRef: { type: "text" },
239
+ providerPaymentRef: { type: "text" },
240
+ customer: {
241
+ type: "relationship",
242
+ relationTo: customersSlug
243
+ },
244
+ guestEmail: { type: "text" },
245
+ lineItems: {
246
+ type: "array",
247
+ required: true,
248
+ fields: {
249
+ productName: {
250
+ type: "text",
251
+ required: true
252
+ },
253
+ quantity: {
254
+ type: "number",
255
+ required: true
256
+ },
257
+ unitPriceCents: {
258
+ type: "number",
259
+ required: true
260
+ },
261
+ totalPriceCents: {
262
+ type: "number",
263
+ required: true
264
+ },
265
+ catalogRef: { type: "text" }
266
+ }
267
+ },
268
+ shippingAddress: {
269
+ type: "group",
270
+ fields: {
271
+ firstName: { type: "text" },
272
+ lastName: { type: "text" },
273
+ address1: { type: "text" },
274
+ address2: { type: "text" },
275
+ city: { type: "text" },
276
+ state: { type: "text" },
277
+ zip: { type: "text" },
278
+ country: {
279
+ type: "text",
280
+ defaultValue: "US"
281
+ },
282
+ phone: { type: "text" }
283
+ }
284
+ },
285
+ fulfillmentStatus: {
286
+ type: "select",
287
+ options: [
288
+ "pending",
289
+ "shipped",
290
+ "delivered",
291
+ "failed"
292
+ ]
293
+ },
294
+ trackingNumber: { type: "text" },
295
+ trackingCarrier: { type: "text" },
296
+ trackingUrl: { type: "text" },
297
+ createdAt: {
298
+ type: "date",
299
+ mode: "timestamp",
300
+ defaultValue: "now"
301
+ }
302
+ }
303
+ };
304
+ }
305
+ function buildCustomersCollection(slug, usersSlug) {
306
+ return {
307
+ slug,
308
+ fields: {
309
+ id: {
310
+ type: "number",
311
+ autoIncrement: true
312
+ },
313
+ email: {
314
+ type: "text",
315
+ required: true,
316
+ unique: true
317
+ },
318
+ provider: {
319
+ type: "select",
320
+ options: ["square", "stripe"]
321
+ },
322
+ providerCustomerRef: { type: "text" },
323
+ linkedUser: {
324
+ type: "relationship",
325
+ relationTo: usersSlug
326
+ },
327
+ loyaltyAccountRef: { type: "text" },
328
+ loyaltyPoints: {
329
+ type: "number",
330
+ defaultValue: 0
331
+ },
332
+ createdAt: {
333
+ type: "date",
334
+ mode: "timestamp",
335
+ defaultValue: "now"
336
+ }
337
+ }
338
+ };
339
+ }
340
+ function buildPaymentsCollection(slug, ordersSlug) {
341
+ return {
342
+ slug,
343
+ fields: {
344
+ id: {
345
+ type: "number",
346
+ autoIncrement: true
347
+ },
348
+ provider: {
349
+ type: "select",
350
+ options: ["square", "stripe"],
351
+ required: true
352
+ },
353
+ providerPaymentRef: {
354
+ type: "text",
355
+ required: true
356
+ },
357
+ providerOrderRef: { type: "text" },
358
+ order: {
359
+ type: "relationship",
360
+ relationTo: ordersSlug
361
+ },
362
+ status: { type: "text" },
363
+ amountCents: {
364
+ type: "number",
365
+ required: true
366
+ },
367
+ currency: {
368
+ type: "text",
369
+ defaultValue: "USD"
370
+ },
371
+ rawResponse: { type: "json" },
372
+ createdAt: {
373
+ type: "date",
374
+ mode: "timestamp",
375
+ defaultValue: "now"
376
+ }
377
+ }
378
+ };
379
+ }
380
+ function buildWebhookEventsCollection(slug) {
381
+ return {
382
+ slug,
383
+ fields: {
384
+ id: {
385
+ type: "number",
386
+ autoIncrement: true
387
+ },
388
+ provider: {
389
+ type: "select",
390
+ options: ["square", "stripe"],
391
+ required: true
392
+ },
393
+ eventId: {
394
+ type: "text",
395
+ required: true,
396
+ unique: true
397
+ },
398
+ eventType: { type: "text" },
399
+ receivedAt: {
400
+ type: "date",
401
+ mode: "timestamp",
402
+ defaultValue: "now"
403
+ }
404
+ }
405
+ };
406
+ }
407
+ function buildSubscriptionsCollection(slug, customersSlug) {
408
+ return {
409
+ slug,
410
+ fields: {
411
+ id: {
412
+ type: "number",
413
+ autoIncrement: true
414
+ },
415
+ provider: {
416
+ type: "select",
417
+ options: ["square", "stripe"],
418
+ required: true
419
+ },
420
+ providerSubscriptionRef: {
421
+ type: "text",
422
+ required: true
423
+ },
424
+ customer: {
425
+ type: "relationship",
426
+ relationTo: customersSlug
427
+ },
428
+ status: { type: "text" },
429
+ chargedThroughDate: {
430
+ type: "date",
431
+ mode: "timestamp"
432
+ },
433
+ createdAt: {
434
+ type: "date",
435
+ mode: "timestamp",
436
+ defaultValue: "now"
437
+ }
438
+ }
439
+ };
440
+ }
441
+ /**
442
+ * Returns a Cadmea plugin that adds the provider-agnostic ecommerce
443
+ * collections — a no-op for any collection slug already present, the same
444
+ * idempotent-add convention `cadmea-plugin-redirects`/`cadmea-plugin-crm`
445
+ * use.
446
+ */
447
+ function ecommercePlugin(options = {}) {
448
+ const slugs = {
449
+ products: options.productsSlug ?? DEFAULTS.products,
450
+ orders: options.ordersSlug ?? DEFAULTS.orders,
451
+ customers: options.customersSlug ?? DEFAULTS.customers,
452
+ payments: options.paymentsSlug ?? DEFAULTS.payments,
453
+ webhookEvents: options.webhookEventsSlug ?? DEFAULTS.webhookEvents,
454
+ subscriptions: options.subscriptionsSlug ?? DEFAULTS.subscriptions,
455
+ users: options.usersSlug ?? DEFAULTS.users
456
+ };
457
+ return (config) => {
458
+ const collections = [...config.collections];
459
+ const addIfMissing = (collection) => {
460
+ if (!collections.some((c) => c.slug === collection.slug)) collections.push(collection);
461
+ };
462
+ addIfMissing(buildProductsCollection(slugs.products));
463
+ addIfMissing(buildOrdersCollection(slugs.orders, slugs.customers));
464
+ addIfMissing(buildCustomersCollection(slugs.customers, slugs.users));
465
+ addIfMissing(buildPaymentsCollection(slugs.payments, slugs.orders));
466
+ addIfMissing(buildWebhookEventsCollection(slugs.webhookEvents));
467
+ if (options.includeSubscriptions) addIfMissing(buildSubscriptionsCollection(slugs.subscriptions, slugs.customers));
468
+ return {
469
+ ...config,
470
+ collections
471
+ };
472
+ };
473
+ }
474
+ //#endregion
475
+ //#region src/webhook.ts
476
+ function isUniqueConstraintError(error) {
477
+ return error instanceof Error && error.message.includes("Unique constraint violated");
478
+ }
479
+ async function findOneByField(api, context, field, value) {
480
+ return (await api.find(context)).find((row) => row[field] === value);
481
+ }
482
+ async function dispatchEvent(event, options) {
483
+ switch (event.kind) {
484
+ case "payment.updated": {
485
+ const payment = await findOneByField(options.payments, options.context, "providerPaymentRef", event.providerPaymentRef);
486
+ if (payment) await options.payments.update(options.context, payment.id, { status: event.status });
487
+ const order = await findOneByField(options.orders, options.context, "providerPaymentRef", event.providerPaymentRef);
488
+ if (order) {
489
+ const status = event.status === "succeeded" ? "paid" : event.status === "refunded" ? "refunded" : "failed";
490
+ await options.orders.update(options.context, order.id, { status });
491
+ }
492
+ return;
493
+ }
494
+ case "order.updated": {
495
+ const order = await findOneByField(options.orders, options.context, "providerOrderRef", event.providerOrderRef);
496
+ if (order) await options.orders.update(options.context, order.id, { status: event.status });
497
+ return;
498
+ }
499
+ case "subscription.updated": {
500
+ if (!options.subscriptions) return;
501
+ const subscription = await findOneByField(options.subscriptions, options.context, "providerSubscriptionRef", event.providerSubscriptionRef);
502
+ if (subscription) await options.subscriptions.update(options.context, subscription.id, { status: event.status });
503
+ return;
504
+ }
505
+ case "unhandled": return;
506
+ }
507
+ }
508
+ /**
509
+ * Returns a Hono handler implementing inbound webhook handling against a
510
+ * `PaymentProvider`: verify signature (raw body, before any parsing) →
511
+ * dedup via the `webhook_events` collection's unique `eventId` constraint
512
+ * → dispatch by normalized event kind. Each step isolated so a handler bug
513
+ * never prevents the 200 response a provider needs to stop retrying — the
514
+ * dedup write IS the source of truth for "already processed," checked
515
+ * before dispatch, not after.
516
+ *
517
+ * Mount alongside `mountCmsRoutes`, same as `createCheckoutHandler` — not
518
+ * part of the generic CMS REST surface.
519
+ */
520
+ function createWebhookHandler(options) {
521
+ return async (c) => {
522
+ const rawBody = await c.req.text();
523
+ if (!await options.provider.verifyWebhookSignature({
524
+ rawBody,
525
+ headers: c.req.raw.headers,
526
+ secret: options.secret,
527
+ notificationUrl: options.notificationUrl
528
+ })) return c.json({ error: "Invalid signature" }, 401);
529
+ const { eventId, event } = options.provider.parseWebhookEvent(rawBody);
530
+ try {
531
+ await options.webhookEvents.create(options.context, {
532
+ provider: options.provider.name,
533
+ eventId,
534
+ eventType: event.kind
535
+ });
536
+ } catch (error) {
537
+ if (isUniqueConstraintError(error)) return c.json({
538
+ ok: true,
539
+ duplicate: true
540
+ }, 200);
541
+ throw error;
542
+ }
543
+ try {
544
+ await dispatchEvent(event, options);
545
+ } catch (error) {
546
+ console.error("[cadmea-plugin-ecommerce] webhook dispatch failed", error);
547
+ }
548
+ return c.json({ ok: true }, 200);
549
+ };
550
+ }
551
+ //#endregion
552
+ export { CadmeaPaymentError, createCheckoutHandler, createWebhookHandler, ecommercePlugin };
553
+
554
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/errors.ts","../src/checkout.ts","../src/collections.ts","../src/webhook.ts"],"sourcesContent":["// Copyright (c) 2026 BowenLabs. All rights reserved.\n// MIT licensed. See LICENSE in the repo root.\n\n/**\n * Thrown by `createCheckoutHandler` on price-mismatch/inventory rejection,\n * and by `PaymentProvider` implementations on charge failure.\n * `createCheckoutHandler` catches it internally (`instanceof\n * CadmeaPaymentError`) and maps it to HTTP 402 itself — checkout/webhook\n * handlers are plain Hono routes, never mounted through\n * `@thebes/cadmus/hono`'s `mountCmsRoutes`, so there's no shared\n * error-to-status pipeline this needs to participate in.\n *\n * Deliberately a plain `Error` subclass, not a `CadmusCmsError` one — this\n * is a Cadmea-plugin-owned error, not a Cadmus-primitive one (every real\n * `CadmusError` subclass is owned by a `packages/cadmus/src/<primitive>/`\n * folder; a payment error belongs to a plugin, not a primitive), and\n * `CadmusCmsError` is only reachable via the root `@thebes/cadmus` package\n * export (not `@thebes/cadmus/cms`) — importing that root barrel here\n * would pull in every other primitive's runtime code (including\n * Workers-only modules like `cloudflare:email`) just for one base class.\n * Keeping this plugin-local and dependency-free is the honest shape.\n */\nexport class CadmeaPaymentError extends Error {\n constructor(\n message: string,\n public readonly cause?: unknown,\n ) {\n super(message);\n this.name = \"CadmeaPaymentError\";\n }\n}\n","// Copyright (c) 2026 BowenLabs. All rights reserved.\n// MIT licensed. See LICENSE in the repo root.\n\nimport type { LocalApi } from \"@thebes/cadmus/cms\";\nimport { checkRateLimit } from \"@thebes/cadmus/rate-limit\";\nimport type { Context } from \"hono\";\nimport { CadmeaPaymentError } from \"./errors.js\";\nimport type { CartLineItem, PaymentProvider } from \"./types.js\";\n\n// biome-ignore lint/suspicious/noExplicitAny: LocalApi's table generic is erased at this call boundary — same pattern as @thebes/cadmus/hono's CmsRoutesOptions\ntype AnyLocalApi<TContext> = LocalApi<any, TContext>;\n\nexport interface CheckoutRequestBody {\n lineItems: CartLineItem[];\n paymentSourceToken: string;\n customerEmail?: string;\n idempotencyKey: string;\n shippingAddress?: Record<string, string | undefined>;\n metadata?: Record<string, string>;\n}\n\nexport interface CheckoutHandlerOptions<TContext> {\n provider: PaymentProvider;\n orders: AnyLocalApi<TContext>;\n payments: AnyLocalApi<TContext>;\n /**\n * Resolves the per-request access context passed to `orders`/`payments`\n * — called once per request, the same shape and timing as\n * `@thebes/cadmus/hono`'s `mountCmsRoutes`'s own `resolveContext`. This\n * is a real customer-initiated HTTP request (unlike a `CollectionHooks`\n * hook), so a real per-request context is available here, not a fixed\n * trusted value.\n */\n resolveContext: (c: Context) => Promise<TContext> | TContext;\n rateLimit?: {\n kv: KVNamespace;\n limit: number;\n windowSeconds: number;\n /** Default: \"checkout\". */\n keyPrefix?: string;\n };\n}\n\nfunction subtotalCents(lineItems: CartLineItem[]): number {\n return lineItems.reduce(\n (sum, item) => sum + item.clientUnitPrice.amount * item.quantity,\n 0,\n );\n}\n\nfunction generateOrderNumber(): string {\n return `ORD-${crypto.randomUUID().replace(/-/g, \"\").slice(0, 12).toUpperCase()}`;\n}\n\n/**\n * Returns a Hono handler implementing the checkout flow against a\n * `PaymentProvider`: rate limit → re-verify cart prices/availability\n * (never trust client-submitted prices) → idempotent customer find-or-\n * create → charge → persist `orders`/`payments` rows. A DB-write failure\n * *after* a successful charge degrades to a 200-with-warning response,\n * never a false \"payment failed\" — the customer's card was actually\n * charged, telling them otherwise would be worse than a delayed manual\n * reconciliation.\n *\n * Mount it as a plain Hono route alongside `mountCmsRoutes` — checkout\n * isn't part of the generic CMS REST surface that function mounts.\n *\n * ```ts\n * app.post(\"/api/checkout\", createCheckoutHandler({ provider, orders, payments, resolveContext }));\n * ```\n */\nexport function createCheckoutHandler<TContext>(\n options: CheckoutHandlerOptions<TContext>,\n) {\n return async (c: Context): Promise<Response> => {\n if (options.rateLimit) {\n const ip = c.req.header(\"CF-Connecting-IP\") ?? \"unknown\";\n const key = `${options.rateLimit.keyPrefix ?? \"checkout\"}:${ip}`;\n const result = await checkRateLimit(\n options.rateLimit.kv,\n key,\n options.rateLimit.limit,\n options.rateLimit.windowSeconds,\n );\n if (!result.allowed) {\n return c.json({ error: \"Rate limit exceeded\" }, 429);\n }\n }\n\n const body = await c.req.json<CheckoutRequestBody>();\n // Malformed-request checks return 400 directly, not via\n // CadmeaPaymentError — that class is reserved for \"the request was\n // well-formed but the checkout itself can't proceed\" (price/inventory\n // rejections, charge failure below), which this function maps to 402.\n if (!Array.isArray(body.lineItems) || body.lineItems.length === 0) {\n return c.json(\n { error: \"Checkout request must include at least one line item\" },\n 400,\n );\n }\n if (!body.idempotencyKey) {\n return c.json(\n { error: \"Checkout request must include an idempotencyKey\" },\n 400,\n );\n }\n\n try {\n // Re-verify every line item's price/availability against the live\n // catalog — the client-submitted price is never trusted as-is.\n const refs = body.lineItems.map((item) => item.catalogRef);\n const priceChecks = await options.provider.checkCatalogPrices(refs);\n const checkByRef = new Map(\n priceChecks.map((check) => [check.catalogRef, check]),\n );\n for (const item of body.lineItems) {\n const check = checkByRef.get(item.catalogRef);\n if (!check) {\n throw new CadmeaPaymentError(\n `Unknown catalog item \"${item.catalogRef}\"`,\n );\n }\n if (check.serverUnitPrice.amount !== item.clientUnitPrice.amount) {\n throw new CadmeaPaymentError(\n `Price mismatch for \"${item.catalogRef}\" — checkout rejected`,\n );\n }\n if (\n check.availableQuantity !== undefined &&\n check.availableQuantity < item.quantity\n ) {\n throw new CadmeaPaymentError(\n `Insufficient inventory for \"${item.catalogRef}\"`,\n );\n }\n }\n } catch (error) {\n if (error instanceof CadmeaPaymentError) {\n return c.json({ error: error.message }, 402);\n }\n throw error;\n }\n\n if (body.customerEmail) {\n // Idempotent find-or-create — result isn't used directly below\n // (the provider's own checkout() call resolves the customer again\n // internally via paymentSourceToken/customerEmail), but calling it\n // here ensures a customer record exists in the provider before the\n // charge, matching the reference Square plugin's own step ordering.\n await options.provider.findOrCreateCustomer(\n body.customerEmail,\n body.idempotencyKey,\n );\n }\n\n const result = await options.provider.checkout({\n lineItems: body.lineItems,\n paymentSourceToken: body.paymentSourceToken,\n customerEmail: body.customerEmail,\n idempotencyKey: body.idempotencyKey,\n metadata: body.metadata,\n });\n\n const context = await options.resolveContext(c);\n const orderData = {\n orderNumber: generateOrderNumber(),\n status: result.status === \"succeeded\" ? \"paid\" : \"pending\",\n totalCents: result.amount.amount,\n subtotalCents: subtotalCents(body.lineItems),\n currency: result.amount.currency,\n provider: options.provider.name,\n providerOrderRef: result.providerOrderRef,\n providerPaymentRef: result.providerPaymentRef,\n guestEmail: body.customerEmail,\n lineItems: body.lineItems.map((item) => ({\n productName: item.catalogRef,\n quantity: item.quantity,\n unitPriceCents: item.clientUnitPrice.amount,\n totalPriceCents: item.clientUnitPrice.amount * item.quantity,\n catalogRef: item.catalogRef,\n })),\n shippingAddress: body.shippingAddress,\n };\n\n try {\n const order = await options.orders.create(context, orderData);\n await options.payments.create(context, {\n provider: options.provider.name,\n providerPaymentRef: result.providerPaymentRef,\n providerOrderRef: result.providerOrderRef,\n order: order.id,\n status: result.status,\n amountCents: result.amount.amount,\n currency: result.amount.currency,\n rawResponse: result.raw,\n });\n return c.json({ order }, 201);\n } catch (cause) {\n // The charge already succeeded — never report it as failed because\n // our own record-keeping write failed afterwards.\n return c.json(\n {\n warning:\n \"Payment succeeded but order record-keeping failed — contact support\",\n providerPaymentRef: result.providerPaymentRef,\n providerOrderRef: result.providerOrderRef,\n cause: cause instanceof Error ? cause.message : String(cause),\n },\n 200,\n );\n }\n };\n}\n","// Copyright (c) 2026 BowenLabs. All rights reserved.\n// MIT licensed. See LICENSE in the repo root.\n//\n// Provider-agnostic ecommerce collections — Products/Variants, Orders,\n// Customers, Payments (audit log), WebhookEvents (dedup), and an optional\n// Subscriptions collection. Field types are restricted to what\n// @thebes/cadmus/cms actually supports, including the Section 3 `group`\n// (shippingAddress, flattened to real columns) and `json` (rawResponse)\n// additions.\n\nimport type { CadmeaPlugin, CollectionConfig } from \"@thebes/cadmus/cms\";\n\nexport interface EcommercePluginOptions {\n productsSlug?: string;\n ordersSlug?: string;\n customersSlug?: string;\n paymentsSlug?: string;\n webhookEventsSlug?: string;\n subscriptionsSlug?: string;\n /** What `customers.linkedUser` relates to. Default: \"users\". */\n usersSlug?: string;\n /**\n * Adds the `subscriptions` collection. Default: false — Square and\n * Stripe model recurring billing differently enough\n * (`PaymentProvider.subscriptions` is itself optional) that this\n * collection is opt-in, not assumed needed by every store.\n */\n includeSubscriptions?: boolean;\n}\n\nconst DEFAULTS = {\n products: \"products\",\n orders: \"orders\",\n customers: \"customers\",\n payments: \"payments\",\n webhookEvents: \"webhook_events\",\n subscriptions: \"subscriptions\",\n users: \"users\",\n};\n\nfunction buildProductsCollection(slug: string): CollectionConfig {\n return {\n slug,\n fields: {\n id: { type: \"number\", autoIncrement: true },\n name: { type: \"text\", required: true },\n description: { type: \"text\" },\n status: {\n type: \"select\",\n options: [\"draft\", \"active\", \"archived\"],\n required: true,\n defaultValue: \"draft\",\n },\n // No discriminator needed — every variant has the same shape.\n variants: {\n type: \"array\",\n required: true,\n fields: {\n sku: { type: \"text\", required: true },\n catalogRef: { type: \"text\", required: true },\n priceCents: { type: \"number\", required: true },\n currency: { type: \"text\", defaultValue: \"USD\" },\n inventoryCount: { type: \"number\" },\n },\n },\n createdAt: { type: \"date\", mode: \"timestamp\", defaultValue: \"now\" },\n },\n };\n}\n\nfunction buildOrdersCollection(\n slug: string,\n customersSlug: string,\n): CollectionConfig {\n return {\n slug,\n fields: {\n id: { type: \"number\", autoIncrement: true },\n orderNumber: { type: \"text\", required: true, unique: true },\n status: {\n type: \"select\",\n options: [\n \"pending\",\n \"paid\",\n \"failed\",\n \"refunded\",\n \"partially_refunded\",\n ],\n required: true,\n defaultValue: \"pending\",\n },\n totalCents: { type: \"number\", required: true },\n subtotalCents: { type: \"number\", required: true },\n taxCents: { type: \"number\" },\n currency: { type: \"text\", defaultValue: \"USD\" },\n // Which PaymentProvider created this order — needed so webhook\n // dispatch and any provider-specific lookups (tracking, refunds)\n // know which provider's REST API to call back into.\n provider: {\n type: \"select\",\n options: [\"square\", \"stripe\"],\n required: true,\n },\n providerOrderRef: { type: \"text\" },\n providerPaymentRef: { type: \"text\" },\n customer: { type: \"relationship\", relationTo: customersSlug },\n // No native `email` field type — same as the SMB form-builder's own\n // email-field handling, this is a plain `text` column; validate\n // shape in a beforeChange hook if the operator wants that.\n guestEmail: { type: \"text\" },\n lineItems: {\n type: \"array\",\n required: true,\n fields: {\n productName: { type: \"text\", required: true },\n quantity: { type: \"number\", required: true },\n unitPriceCents: { type: \"number\", required: true },\n totalPriceCents: { type: \"number\", required: true },\n catalogRef: { type: \"text\" },\n },\n },\n // The `group` field type (Section 3) — flattens to real prefixed\n // columns (shipping_address_first_name, etc), not a JSON blob, so\n // SQL-level querying on a subfield still works.\n shippingAddress: {\n type: \"group\",\n fields: {\n firstName: { type: \"text\" },\n lastName: { type: \"text\" },\n address1: { type: \"text\" },\n address2: { type: \"text\" },\n city: { type: \"text\" },\n state: { type: \"text\" },\n zip: { type: \"text\" },\n country: { type: \"text\", defaultValue: \"US\" },\n phone: { type: \"text\" },\n },\n },\n fulfillmentStatus: {\n type: \"select\",\n options: [\"pending\", \"shipped\", \"delivered\", \"failed\"],\n },\n trackingNumber: { type: \"text\" },\n trackingCarrier: { type: \"text\" },\n trackingUrl: { type: \"text\" },\n createdAt: { type: \"date\", mode: \"timestamp\", defaultValue: \"now\" },\n },\n };\n}\n\nfunction buildCustomersCollection(\n slug: string,\n usersSlug: string,\n): CollectionConfig {\n return {\n slug,\n fields: {\n id: { type: \"number\", autoIncrement: true },\n email: { type: \"text\", required: true, unique: true },\n provider: { type: \"select\", options: [\"square\", \"stripe\"] },\n providerCustomerRef: { type: \"text\" },\n linkedUser: { type: \"relationship\", relationTo: usersSlug },\n // Square-specific; null for Stripe customers — fine to keep on the\n // shared collection since unused fields are simply null.\n loyaltyAccountRef: { type: \"text\" },\n loyaltyPoints: { type: \"number\", defaultValue: 0 },\n createdAt: { type: \"date\", mode: \"timestamp\", defaultValue: \"now\" },\n },\n };\n}\n\nfunction buildPaymentsCollection(\n slug: string,\n ordersSlug: string,\n): CollectionConfig {\n return {\n slug,\n fields: {\n id: { type: \"number\", autoIncrement: true },\n provider: {\n type: \"select\",\n options: [\"square\", \"stripe\"],\n required: true,\n },\n providerPaymentRef: { type: \"text\", required: true },\n providerOrderRef: { type: \"text\" },\n order: { type: \"relationship\", relationTo: ordersSlug },\n // The provider's own status string, stored as-is for audit fidelity\n // (not normalized) — a `select` would force enumerating every\n // provider's status vocabulary here, exactly the provider-coupling\n // the core/provider split exists to avoid.\n status: { type: \"text\" },\n amountCents: { type: \"number\", required: true },\n currency: { type: \"text\", defaultValue: \"USD\" },\n // The `json` field type (Section 3) — the full raw provider payload,\n // for audit/debugging. The one place a freeform-blob column is\n // genuinely the right shape, not a workaround.\n rawResponse: { type: \"json\" },\n createdAt: { type: \"date\", mode: \"timestamp\", defaultValue: \"now\" },\n },\n };\n}\n\nfunction buildWebhookEventsCollection(slug: string): CollectionConfig {\n return {\n slug,\n fields: {\n id: { type: \"number\", autoIncrement: true },\n provider: {\n type: \"select\",\n options: [\"square\", \"stripe\"],\n required: true,\n },\n // The unique constraint *is* the concurrency-safe dedup guard — a\n // concurrent duplicate naturally throws a unique-constraint\n // CadmusCmsError from create(), which createWebhookHandler treats as\n // \"already processed,\" not a real error.\n eventId: { type: \"text\", required: true, unique: true },\n eventType: { type: \"text\" },\n receivedAt: { type: \"date\", mode: \"timestamp\", defaultValue: \"now\" },\n },\n };\n}\n\nfunction buildSubscriptionsCollection(\n slug: string,\n customersSlug: string,\n): CollectionConfig {\n return {\n slug,\n fields: {\n id: { type: \"number\", autoIncrement: true },\n provider: {\n type: \"select\",\n options: [\"square\", \"stripe\"],\n required: true,\n },\n providerSubscriptionRef: { type: \"text\", required: true },\n customer: { type: \"relationship\", relationTo: customersSlug },\n status: { type: \"text\" },\n chargedThroughDate: { type: \"date\", mode: \"timestamp\" },\n createdAt: { type: \"date\", mode: \"timestamp\", defaultValue: \"now\" },\n },\n };\n}\n\n/**\n * Returns a Cadmea plugin that adds the provider-agnostic ecommerce\n * collections — a no-op for any collection slug already present, the same\n * idempotent-add convention `cadmea-plugin-redirects`/`cadmea-plugin-crm`\n * use.\n */\nexport function ecommercePlugin(\n options: EcommercePluginOptions = {},\n): CadmeaPlugin {\n const slugs = {\n products: options.productsSlug ?? DEFAULTS.products,\n orders: options.ordersSlug ?? DEFAULTS.orders,\n customers: options.customersSlug ?? DEFAULTS.customers,\n payments: options.paymentsSlug ?? DEFAULTS.payments,\n webhookEvents: options.webhookEventsSlug ?? DEFAULTS.webhookEvents,\n subscriptions: options.subscriptionsSlug ?? DEFAULTS.subscriptions,\n users: options.usersSlug ?? DEFAULTS.users,\n };\n\n return (config) => {\n const collections = [...config.collections];\n const addIfMissing = (collection: CollectionConfig) => {\n if (!collections.some((c) => c.slug === collection.slug)) {\n collections.push(collection);\n }\n };\n\n addIfMissing(buildProductsCollection(slugs.products));\n addIfMissing(buildOrdersCollection(slugs.orders, slugs.customers));\n addIfMissing(buildCustomersCollection(slugs.customers, slugs.users));\n addIfMissing(buildPaymentsCollection(slugs.payments, slugs.orders));\n addIfMissing(buildWebhookEventsCollection(slugs.webhookEvents));\n if (options.includeSubscriptions) {\n addIfMissing(\n buildSubscriptionsCollection(slugs.subscriptions, slugs.customers),\n );\n }\n\n return { ...config, collections };\n };\n}\n","// Copyright (c) 2026 BowenLabs. All rights reserved.\n// MIT licensed. See LICENSE in the repo root.\n\nimport type { LocalApi } from \"@thebes/cadmus/cms\";\nimport type { Context } from \"hono\";\nimport type { NormalizedWebhookEvent, PaymentProvider } from \"./types.js\";\n\n// biome-ignore lint/suspicious/noExplicitAny: see checkout.ts's identical note\ntype AnyLocalApi<TContext> = LocalApi<any, TContext>;\n\nexport interface WebhookHandlerOptions<TContext> {\n provider: PaymentProvider;\n webhookEvents: AnyLocalApi<TContext>;\n orders: AnyLocalApi<TContext>;\n payments: AnyLocalApi<TContext>;\n /** Only needed if `provider.subscriptions` is wired and the consumer included the optional `subscriptions` collection. */\n subscriptions?: AnyLocalApi<TContext>;\n secret: string;\n /** Some providers (Square) sign over the full notification URL. */\n notificationUrl?: string;\n /**\n * Webhooks are server-to-server calls with no real user session behind\n * them — same reasoning as `@thebes/cadmea-plugin-crm`'s\n * `createContactUpsertHook`'s `context` option. Pass whatever trusted\n * context value the `orders`/`payments`/`webhookEvents` collections'\n * own `access` config accepts for system-level writes.\n */\n context: TContext;\n}\n\n// Matches the exact message text `localApi.ts`'s `wrapWriteError` authors\n// for a unique-constraint failure — Cadmus-internal, a contract this\n// plugin controls indirectly (same precedent as `@thebes/cadmus/hono`'s\n// `mountCmsRoutes`'s own `statusForError`, which matches the same way\n// rather than importing `CadmusCmsError` from the root `@thebes/cadmus`\n// package — see errors.ts's doc comment for why that root import is\n// avoided here).\nfunction isUniqueConstraintError(error: unknown): boolean {\n return (\n error instanceof Error &&\n error.message.includes(\"Unique constraint violated\")\n );\n}\n\nasync function findOneByField<TContext>(\n api: AnyLocalApi<TContext>,\n context: TContext,\n field: string,\n value: string,\n): Promise<Record<string, unknown> | undefined> {\n // In-memory filter after a plain find() rather than a `where`-filtered\n // query — the same \"don't build for scale you don't have\" tradeoff\n // `cadmea-plugin-redirects`/`cadmea-plugin-crm` make elsewhere. Revisit\n // with an indexed lookup if a high-volume store's orders/payments\n // tables make this a measured problem, not a theoretical one.\n const rows = (await api.find(context)) as Array<Record<string, unknown>>;\n return rows.find((row) => row[field] === value);\n}\n\nasync function dispatchEvent<TContext>(\n event: NormalizedWebhookEvent,\n options: WebhookHandlerOptions<TContext>,\n): Promise<void> {\n switch (event.kind) {\n case \"payment.updated\": {\n const payment = await findOneByField(\n options.payments,\n options.context,\n \"providerPaymentRef\",\n event.providerPaymentRef,\n );\n if (payment) {\n await options.payments.update(options.context, payment.id as number, {\n status: event.status,\n });\n }\n const order = await findOneByField(\n options.orders,\n options.context,\n \"providerPaymentRef\",\n event.providerPaymentRef,\n );\n if (order) {\n const status =\n event.status === \"succeeded\"\n ? \"paid\"\n : event.status === \"refunded\"\n ? \"refunded\"\n : \"failed\";\n await options.orders.update(options.context, order.id as number, {\n status,\n });\n }\n return;\n }\n case \"order.updated\": {\n const order = await findOneByField(\n options.orders,\n options.context,\n \"providerOrderRef\",\n event.providerOrderRef,\n );\n if (order) {\n await options.orders.update(options.context, order.id as number, {\n status: event.status,\n });\n }\n return;\n }\n case \"subscription.updated\": {\n if (!options.subscriptions) return;\n const subscription = await findOneByField(\n options.subscriptions,\n options.context,\n \"providerSubscriptionRef\",\n event.providerSubscriptionRef,\n );\n if (subscription) {\n await options.subscriptions.update(\n options.context,\n subscription.id as number,\n { status: event.status },\n );\n }\n return;\n }\n case \"unhandled\":\n return;\n }\n}\n\n/**\n * Returns a Hono handler implementing inbound webhook handling against a\n * `PaymentProvider`: verify signature (raw body, before any parsing) →\n * dedup via the `webhook_events` collection's unique `eventId` constraint\n * → dispatch by normalized event kind. Each step isolated so a handler bug\n * never prevents the 200 response a provider needs to stop retrying — the\n * dedup write IS the source of truth for \"already processed,\" checked\n * before dispatch, not after.\n *\n * Mount alongside `mountCmsRoutes`, same as `createCheckoutHandler` — not\n * part of the generic CMS REST surface.\n */\nexport function createWebhookHandler<TContext>(\n options: WebhookHandlerOptions<TContext>,\n) {\n return async (c: Context): Promise<Response> => {\n // Read raw body as text *before* any JSON parsing — signature is\n // computed over raw bytes, matching @thebes/cadmus/cms's own\n // outbound webhooks.ts HMAC idiom.\n const rawBody = await c.req.text();\n\n const verified = await options.provider.verifyWebhookSignature({\n rawBody,\n headers: c.req.raw.headers,\n secret: options.secret,\n notificationUrl: options.notificationUrl,\n });\n if (!verified) {\n return c.json({ error: \"Invalid signature\" }, 401);\n }\n\n const { eventId, event } = options.provider.parseWebhookEvent(rawBody);\n\n try {\n await options.webhookEvents.create(options.context, {\n provider: options.provider.name,\n eventId,\n eventType: event.kind,\n });\n } catch (error) {\n if (isUniqueConstraintError(error)) {\n // Already processed — the unique constraint is the actual guard,\n // not a preceding find() (avoids a TOCTOU window on concurrent\n // delivery of the same event).\n return c.json({ ok: true, duplicate: true }, 200);\n }\n throw error;\n }\n\n try {\n await dispatchEvent(event, options);\n } catch (error) {\n // A dispatch-handler bug must not cause the provider to retry the\n // whole event (it's already recorded as processed above) — log and\n // move on, same \"each handler isolated\" precedent the reference\n // Square plugin's own webhook.ts follows.\n console.error(\"[cadmea-plugin-ecommerce] webhook dispatch failed\", error);\n }\n\n return c.json({ ok: true }, 200);\n };\n}\n"],"mappings":"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"}