@thebes/cadmea-plugin-ecommerce 1.0.0 → 1.1.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.cjs +108 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +136 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +136 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +107 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -292,6 +292,8 @@ function buildOrdersCollection(slug, customersSlug) {
|
|
|
292
292
|
"failed"
|
|
293
293
|
]
|
|
294
294
|
},
|
|
295
|
+
fulfillmentProvider: { type: "text" },
|
|
296
|
+
fulfillmentProviderRef: { type: "text" },
|
|
295
297
|
trackingNumber: { type: "text" },
|
|
296
298
|
trackingCarrier: { type: "text" },
|
|
297
299
|
trackingUrl: { type: "text" },
|
|
@@ -473,6 +475,104 @@ function ecommercePlugin(options = {}) {
|
|
|
473
475
|
};
|
|
474
476
|
}
|
|
475
477
|
//#endregion
|
|
478
|
+
//#region src/fulfillment.ts
|
|
479
|
+
/**
|
|
480
|
+
* The order-paid hook implementation: submits an already-paid order's line
|
|
481
|
+
* items to a `FulfillmentProvider` and persists the resulting
|
|
482
|
+
* `fulfillmentProvider`/`fulfillmentProviderRef`/`fulfillmentStatus` back
|
|
483
|
+
* onto the order row. Wire it as `WebhookHandlerOptions.onOrderPaid` on the
|
|
484
|
+
* *payment* provider's webhook handler:
|
|
485
|
+
*
|
|
486
|
+
* ```ts
|
|
487
|
+
* createWebhookHandler({
|
|
488
|
+
* provider: stripeProvider,
|
|
489
|
+
* orders, payments, webhookEvents, secret, context,
|
|
490
|
+
* onOrderPaid: (order) =>
|
|
491
|
+
* createFulfillmentOrder(order, { provider: printfulProvider, orders, context }),
|
|
492
|
+
* });
|
|
493
|
+
* ```
|
|
494
|
+
*
|
|
495
|
+
* Digital-goods-only stores simply never wire this — `fulfillmentProvider`
|
|
496
|
+
* is plugin-optional, not a hard dependency of `ecommercePlugin`.
|
|
497
|
+
*/
|
|
498
|
+
async function createFulfillmentOrder(order, options) {
|
|
499
|
+
const lineItems = order.lineItems ?? [];
|
|
500
|
+
const shippingAddress = order.shippingAddress ?? {};
|
|
501
|
+
const result = await options.provider.createFulfillmentOrder({
|
|
502
|
+
orderId: order.id,
|
|
503
|
+
lineItems: lineItems.filter((item) => Boolean(item.catalogRef)).map((item) => ({
|
|
504
|
+
catalogRef: item.catalogRef,
|
|
505
|
+
quantity: item.quantity
|
|
506
|
+
})),
|
|
507
|
+
shippingAddress,
|
|
508
|
+
customerEmail: order.guestEmail
|
|
509
|
+
});
|
|
510
|
+
await options.orders.update(options.context, order.id, {
|
|
511
|
+
fulfillmentProvider: options.provider.name,
|
|
512
|
+
fulfillmentProviderRef: result.providerFulfillmentRef,
|
|
513
|
+
fulfillmentStatus: result.status
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
async function findOrderByFulfillmentRef(orders, context, providerFulfillmentRef) {
|
|
517
|
+
return (await orders.find(context)).find((row) => row.fulfillmentProviderRef === providerFulfillmentRef);
|
|
518
|
+
}
|
|
519
|
+
function isUniqueConstraintError$1(error) {
|
|
520
|
+
return error instanceof Error && error.message.includes("Unique constraint violated");
|
|
521
|
+
}
|
|
522
|
+
async function dispatchFulfillmentEvent(event, options) {
|
|
523
|
+
if (event.kind === "unhandled") return;
|
|
524
|
+
const order = await findOrderByFulfillmentRef(options.orders, options.context, event.providerFulfillmentRef);
|
|
525
|
+
if (!order) return;
|
|
526
|
+
await options.orders.update(options.context, order.id, {
|
|
527
|
+
fulfillmentStatus: event.status,
|
|
528
|
+
trackingNumber: event.trackingNumber,
|
|
529
|
+
trackingCarrier: event.trackingCarrier,
|
|
530
|
+
trackingUrl: event.trackingUrl
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Returns a Hono handler implementing inbound fulfillment-webhook handling
|
|
535
|
+
* (shipment created/delivered/failed) against a `FulfillmentProvider` —
|
|
536
|
+
* verify signature → dedup via `webhook_events` (shared with the payment
|
|
537
|
+
* webhook handler; `eventId` is provider-specific so cross-provider
|
|
538
|
+
* collisions aren't a concern) → look up the order by
|
|
539
|
+
* `fulfillmentProviderRef` → update shipment status/tracking. Mirrors
|
|
540
|
+
* `createWebhookHandler`'s structure exactly; kept as a separate function
|
|
541
|
+
* rather than a generic merge of the two because the payment and
|
|
542
|
+
* fulfillment event vocabularies, dedup keys, and target collections don't
|
|
543
|
+
* overlap enough to share logic without branching on provider kind.
|
|
544
|
+
*/
|
|
545
|
+
function createFulfillmentWebhookHandler(options) {
|
|
546
|
+
return async (c) => {
|
|
547
|
+
const rawBody = await c.req.text();
|
|
548
|
+
if (!await options.provider.verifyWebhookSignature({
|
|
549
|
+
rawBody,
|
|
550
|
+
headers: c.req.raw.headers,
|
|
551
|
+
secret: options.secret
|
|
552
|
+
})) return c.json({ error: "Invalid signature" }, 401);
|
|
553
|
+
const { eventId, event } = options.provider.parseWebhookEvent(rawBody);
|
|
554
|
+
try {
|
|
555
|
+
await options.webhookEvents.create(options.context, {
|
|
556
|
+
provider: options.provider.name,
|
|
557
|
+
eventId,
|
|
558
|
+
eventType: event.kind
|
|
559
|
+
});
|
|
560
|
+
} catch (error) {
|
|
561
|
+
if (isUniqueConstraintError$1(error)) return c.json({
|
|
562
|
+
ok: true,
|
|
563
|
+
duplicate: true
|
|
564
|
+
}, 200);
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
await dispatchFulfillmentEvent(event, options);
|
|
569
|
+
} catch (error) {
|
|
570
|
+
console.error("[cadmea-plugin-ecommerce] fulfillment webhook dispatch failed", error);
|
|
571
|
+
}
|
|
572
|
+
return c.json({ ok: true }, 200);
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
//#endregion
|
|
476
576
|
//#region src/webhook.ts
|
|
477
577
|
function isUniqueConstraintError(error) {
|
|
478
578
|
return error instanceof Error && error.message.includes("Unique constraint violated");
|
|
@@ -488,13 +588,17 @@ async function dispatchEvent(event, options) {
|
|
|
488
588
|
const order = await findOneByField(options.orders, options.context, "providerPaymentRef", event.providerPaymentRef);
|
|
489
589
|
if (order) {
|
|
490
590
|
const status = event.status === "succeeded" ? "paid" : event.status === "refunded" ? "refunded" : "failed";
|
|
491
|
-
await options.orders.update(options.context, order.id, { status });
|
|
591
|
+
const updated = await options.orders.update(options.context, order.id, { status });
|
|
592
|
+
if (status === "paid" && options.onOrderPaid) await options.onOrderPaid(updated);
|
|
492
593
|
}
|
|
493
594
|
return;
|
|
494
595
|
}
|
|
495
596
|
case "order.updated": {
|
|
496
597
|
const order = await findOneByField(options.orders, options.context, "providerOrderRef", event.providerOrderRef);
|
|
497
|
-
if (order)
|
|
598
|
+
if (order) {
|
|
599
|
+
const updated = await options.orders.update(options.context, order.id, { status: event.status });
|
|
600
|
+
if (event.status === "paid" && options.onOrderPaid) await options.onOrderPaid(updated);
|
|
601
|
+
}
|
|
498
602
|
return;
|
|
499
603
|
}
|
|
500
604
|
case "subscription.updated": {
|
|
@@ -552,6 +656,8 @@ function createWebhookHandler(options) {
|
|
|
552
656
|
//#endregion
|
|
553
657
|
exports.CadmeaPaymentError = CadmeaPaymentError;
|
|
554
658
|
exports.createCheckoutHandler = createCheckoutHandler;
|
|
659
|
+
exports.createFulfillmentOrder = createFulfillmentOrder;
|
|
660
|
+
exports.createFulfillmentWebhookHandler = createFulfillmentWebhookHandler;
|
|
555
661
|
exports.createWebhookHandler = createWebhookHandler;
|
|
556
662
|
exports.ecommercePlugin = ecommercePlugin;
|
|
557
663
|
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","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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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["isUniqueConstraintError"],"sources":["../src/errors.ts","../src/checkout.ts","../src/collections.ts","../src/fulfillment.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 // Which FulfillmentProvider is shipping this order — independent of\n // `provider` (who charged the card); see types.ts's doc comment on\n // FulfillmentProvider for why these are separate axes.\n fulfillmentProvider: { type: \"text\" },\n // The fulfillment provider's own order identifier — the correlation\n // key createFulfillmentWebhookHandler dispatches inbound shipment\n // webhooks against, mirroring providerOrderRef's role for payments.\n fulfillmentProviderRef: { type: \"text\" },\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 {\n FulfillmentProvider,\n NormalizedFulfillmentWebhookEvent,\n} 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 CreateFulfillmentOrderOptions<TContext> {\n provider: FulfillmentProvider;\n orders: AnyLocalApi<TContext>;\n context: TContext;\n}\n\n/**\n * The order-paid hook implementation: submits an already-paid order's line\n * items to a `FulfillmentProvider` and persists the resulting\n * `fulfillmentProvider`/`fulfillmentProviderRef`/`fulfillmentStatus` back\n * onto the order row. Wire it as `WebhookHandlerOptions.onOrderPaid` on the\n * *payment* provider's webhook handler:\n *\n * ```ts\n * createWebhookHandler({\n * provider: stripeProvider,\n * orders, payments, webhookEvents, secret, context,\n * onOrderPaid: (order) =>\n * createFulfillmentOrder(order, { provider: printfulProvider, orders, context }),\n * });\n * ```\n *\n * Digital-goods-only stores simply never wire this — `fulfillmentProvider`\n * is plugin-optional, not a hard dependency of `ecommercePlugin`.\n */\nexport async function createFulfillmentOrder<TContext>(\n order: Record<string, unknown>,\n options: CreateFulfillmentOrderOptions<TContext>,\n): Promise<void> {\n const lineItems = (order.lineItems ?? []) as Array<{\n catalogRef?: string;\n quantity: number;\n }>;\n const shippingAddress =\n (order.shippingAddress as Record<string, string | undefined>) ?? {};\n\n const result = await options.provider.createFulfillmentOrder({\n orderId: order.id as number,\n lineItems: lineItems\n .filter((item): item is { catalogRef: string; quantity: number } =>\n Boolean(item.catalogRef),\n )\n .map((item) => ({\n catalogRef: item.catalogRef,\n quantity: item.quantity,\n })),\n shippingAddress,\n customerEmail: order.guestEmail as string | undefined,\n });\n\n await options.orders.update(options.context, order.id as number, {\n fulfillmentProvider: options.provider.name,\n fulfillmentProviderRef: result.providerFulfillmentRef,\n fulfillmentStatus: result.status,\n });\n}\n\nexport interface FulfillmentWebhookHandlerOptions<TContext> {\n provider: FulfillmentProvider;\n orders: AnyLocalApi<TContext>;\n webhookEvents: AnyLocalApi<TContext>;\n secret: string;\n /** See `WebhookHandlerOptions.context`'s identical note in webhook.ts. */\n context: TContext;\n}\n\nasync function findOrderByFulfillmentRef<TContext>(\n orders: AnyLocalApi<TContext>,\n context: TContext,\n providerFulfillmentRef: string,\n): Promise<Record<string, unknown> | undefined> {\n // Same in-memory-filter tradeoff as webhook.ts's findOneByField — revisit\n // with an indexed lookup only once volume makes it a measured problem.\n const rows = (await orders.find(context)) as Array<Record<string, unknown>>;\n return rows.find(\n (row) => row.fulfillmentProviderRef === providerFulfillmentRef,\n );\n}\n\nfunction isUniqueConstraintError(error: unknown): boolean {\n return (\n error instanceof Error &&\n error.message.includes(\"Unique constraint violated\")\n );\n}\n\nasync function dispatchFulfillmentEvent<TContext>(\n event: NormalizedFulfillmentWebhookEvent,\n options: FulfillmentWebhookHandlerOptions<TContext>,\n): Promise<void> {\n if (event.kind === \"unhandled\") return;\n\n const order = await findOrderByFulfillmentRef(\n options.orders,\n options.context,\n event.providerFulfillmentRef,\n );\n if (!order) return;\n\n await options.orders.update(options.context, order.id as number, {\n fulfillmentStatus: event.status,\n trackingNumber: event.trackingNumber,\n trackingCarrier: event.trackingCarrier,\n trackingUrl: event.trackingUrl,\n });\n}\n\n/**\n * Returns a Hono handler implementing inbound fulfillment-webhook handling\n * (shipment created/delivered/failed) against a `FulfillmentProvider` —\n * verify signature → dedup via `webhook_events` (shared with the payment\n * webhook handler; `eventId` is provider-specific so cross-provider\n * collisions aren't a concern) → look up the order by\n * `fulfillmentProviderRef` → update shipment status/tracking. Mirrors\n * `createWebhookHandler`'s structure exactly; kept as a separate function\n * rather than a generic merge of the two because the payment and\n * fulfillment event vocabularies, dedup keys, and target collections don't\n * overlap enough to share logic without branching on provider kind.\n */\nexport function createFulfillmentWebhookHandler<TContext>(\n options: FulfillmentWebhookHandlerOptions<TContext>,\n) {\n return async (c: Context): Promise<Response> => {\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 });\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 return c.json({ ok: true, duplicate: true }, 200);\n }\n throw error;\n }\n\n try {\n await dispatchFulfillmentEvent(event, options);\n } catch (error) {\n console.error(\n \"[cadmea-plugin-ecommerce] fulfillment webhook dispatch failed\",\n error,\n );\n }\n\n return c.json({ ok: true }, 200);\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 * Fires once, right after an order's status is written as \"paid\" by\n * this handler — the order-paid hook point a `FulfillmentProvider`\n * integration (e.g. `@thebes/cadmea-plugin-printful`) wires into to\n * submit the order for shipment. Receives the just-updated order row.\n * Errors thrown here are caught the same way dispatch-handler errors\n * are below — logged, never surfaced as a failure to the payment\n * provider, since the charge already succeeded and the provider must\n * not retry the whole webhook over a fulfillment-side problem.\n */\n onOrderPaid?: (order: Record<string, unknown>) => Promise<void>;\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 const updated = await options.orders.update(\n options.context,\n order.id as number,\n { status },\n );\n if (status === \"paid\" && options.onOrderPaid) {\n await options.onOrderPaid(updated);\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 const updated = await options.orders.update(\n options.context,\n order.id as number,\n { status: event.status },\n );\n if (event.status === \"paid\" && options.onOrderPaid) {\n await options.onOrderPaid(updated);\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":";;;;;;;;;;;;;;;;;;;;;;AAsBA,IAAa,qBAAb,cAAwC,MAAM;CAG1B;CAFlB,YACE,SACA,OACA;EACA,MAAM,OAAO;EAFG,KAAA,QAAA;EAGhB,KAAK,OAAO;CACd;AACF;;;ACaA,SAAS,cAAc,WAAmC;CACxD,OAAO,UAAU,QACd,KAAK,SAAS,MAAM,KAAK,gBAAgB,SAAS,KAAK,UACxD,CACF;AACF;AAEA,SAAS,sBAA8B;CACrC,OAAO,OAAO,OAAO,WAAW,CAAC,CAAC,QAAQ,MAAM,EAAE,CAAC,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,YAAY;AAC/E;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,sBACd,SACA;CACA,OAAO,OAAO,MAAkC;EAC9C,IAAI,QAAQ,WAAW;GACrB,MAAM,KAAK,EAAE,IAAI,OAAO,kBAAkB,KAAK;GAC/C,MAAM,MAAM,GAAG,QAAQ,UAAU,aAAa,WAAW,GAAG;GAO5D,IAAI,EAAC,OAAA,GAAA,0BAAA,eAAA,CALH,QAAQ,UAAU,IAClB,KACA,QAAQ,UAAU,OAClB,QAAQ,UAAU,aACpB,EAAA,CACY,SACV,OAAO,EAAE,KAAK,EAAE,OAAO,sBAAsB,GAAG,GAAG;EAEvD;EAEA,MAAM,OAAO,MAAM,EAAE,IAAI,KAA0B;EAKnD,IAAI,CAAC,MAAM,QAAQ,KAAK,SAAS,KAAK,KAAK,UAAU,WAAW,GAC9D,OAAO,EAAE,KACP,EAAE,OAAO,uDAAuD,GAChE,GACF;EAEF,IAAI,CAAC,KAAK,gBACR,OAAO,EAAE,KACP,EAAE,OAAO,kDAAkD,GAC3D,GACF;EAGF,IAAI;GAGF,MAAM,OAAO,KAAK,UAAU,KAAK,SAAS,KAAK,UAAU;GACzD,MAAM,cAAc,MAAM,QAAQ,SAAS,mBAAmB,IAAI;GAClE,MAAM,aAAa,IAAI,IACrB,YAAY,KAAK,UAAU,CAAC,MAAM,YAAY,KAAK,CAAC,CACtD;GACA,KAAK,MAAM,QAAQ,KAAK,WAAW;IACjC,MAAM,QAAQ,WAAW,IAAI,KAAK,UAAU;IAC5C,IAAI,CAAC,OACH,MAAM,IAAI,mBACR,yBAAyB,KAAK,WAAW,EAC3C;IAEF,IAAI,MAAM,gBAAgB,WAAW,KAAK,gBAAgB,QACxD,MAAM,IAAI,mBACR,uBAAuB,KAAK,WAAW,sBACzC;IAEF,IACE,MAAM,sBAAsB,KAAA,KAC5B,MAAM,oBAAoB,KAAK,UAE/B,MAAM,IAAI,mBACR,+BAA+B,KAAK,WAAW,EACjD;GAEJ;EACF,SAAS,OAAO;GACd,IAAI,iBAAiB,oBACnB,OAAO,EAAE,KAAK,EAAE,OAAO,MAAM,QAAQ,GAAG,GAAG;GAE7C,MAAM;EACR;EAEA,IAAI,KAAK,eAMP,MAAM,QAAQ,SAAS,qBACrB,KAAK,eACL,KAAK,cACP;EAGF,MAAM,SAAS,MAAM,QAAQ,SAAS,SAAS;GAC7C,WAAW,KAAK;GAChB,oBAAoB,KAAK;GACzB,eAAe,KAAK;GACpB,gBAAgB,KAAK;GACrB,UAAU,KAAK;EACjB,CAAC;EAED,MAAM,UAAU,MAAM,QAAQ,eAAe,CAAC;EAC9C,MAAM,YAAY;GAChB,aAAa,oBAAoB;GACjC,QAAQ,OAAO,WAAW,cAAc,SAAS;GACjD,YAAY,OAAO,OAAO;GAC1B,eAAe,cAAc,KAAK,SAAS;GAC3C,UAAU,OAAO,OAAO;GACxB,UAAU,QAAQ,SAAS;GAC3B,kBAAkB,OAAO;GACzB,oBAAoB,OAAO;GAC3B,YAAY,KAAK;GACjB,WAAW,KAAK,UAAU,KAAK,UAAU;IACvC,aAAa,KAAK;IAClB,UAAU,KAAK;IACf,gBAAgB,KAAK,gBAAgB;IACrC,iBAAiB,KAAK,gBAAgB,SAAS,KAAK;IACpD,YAAY,KAAK;GACnB,EAAE;GACF,iBAAiB,KAAK;EACxB;EAEA,IAAI;GACF,MAAM,QAAQ,MAAM,QAAQ,OAAO,OAAO,SAAS,SAAS;GAC5D,MAAM,QAAQ,SAAS,OAAO,SAAS;IACrC,UAAU,QAAQ,SAAS;IAC3B,oBAAoB,OAAO;IAC3B,kBAAkB,OAAO;IACzB,OAAO,MAAM;IACb,QAAQ,OAAO;IACf,aAAa,OAAO,OAAO;IAC3B,UAAU,OAAO,OAAO;IACxB,aAAa,OAAO;GACtB,CAAC;GACD,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG;EAC9B,SAAS,OAAO;GAGd,OAAO,EAAE,KACP;IACE,SACE;IACF,oBAAoB,OAAO;IAC3B,kBAAkB,OAAO;IACzB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC9D,GACA,GACF;EACF;CACF;AACF;;;ACtLA,MAAM,WAAW;CACf,UAAU;CACV,QAAQ;CACR,WAAW;CACX,UAAU;CACV,eAAe;CACf,eAAe;CACf,OAAO;AACT;AAEA,SAAS,wBAAwB,MAAgC;CAC/D,OAAO;EACL;EACA,QAAQ;GACN,IAAI;IAAE,MAAM;IAAU,eAAe;GAAK;GAC1C,MAAM;IAAE,MAAM;IAAQ,UAAU;GAAK;GACrC,aAAa,EAAE,MAAM,OAAO;GAC5B,QAAQ;IACN,MAAM;IACN,SAAS;KAAC;KAAS;KAAU;IAAU;IACvC,UAAU;IACV,cAAc;GAChB;GAEA,UAAU;IACR,MAAM;IACN,UAAU;IACV,QAAQ;KACN,KAAK;MAAE,MAAM;MAAQ,UAAU;KAAK;KACpC,YAAY;MAAE,MAAM;MAAQ,UAAU;KAAK;KAC3C,YAAY;MAAE,MAAM;MAAU,UAAU;KAAK;KAC7C,UAAU;MAAE,MAAM;MAAQ,cAAc;KAAM;KAC9C,gBAAgB,EAAE,MAAM,SAAS;IACnC;GACF;GACA,WAAW;IAAE,MAAM;IAAQ,MAAM;IAAa,cAAc;GAAM;EACpE;CACF;AACF;AAEA,SAAS,sBACP,MACA,eACkB;CAClB,OAAO;EACL;EACA,QAAQ;GACN,IAAI;IAAE,MAAM;IAAU,eAAe;GAAK;GAC1C,aAAa;IAAE,MAAM;IAAQ,UAAU;IAAM,QAAQ;GAAK;GAC1D,QAAQ;IACN,MAAM;IACN,SAAS;KACP;KACA;KACA;KACA;KACA;IACF;IACA,UAAU;IACV,cAAc;GAChB;GACA,YAAY;IAAE,MAAM;IAAU,UAAU;GAAK;GAC7C,eAAe;IAAE,MAAM;IAAU,UAAU;GAAK;GAChD,UAAU,EAAE,MAAM,SAAS;GAC3B,UAAU;IAAE,MAAM;IAAQ,cAAc;GAAM;GAI9C,UAAU;IACR,MAAM;IACN,SAAS,CAAC,UAAU,QAAQ;IAC5B,UAAU;GACZ;GACA,kBAAkB,EAAE,MAAM,OAAO;GACjC,oBAAoB,EAAE,MAAM,OAAO;GACnC,UAAU;IAAE,MAAM;IAAgB,YAAY;GAAc;GAI5D,YAAY,EAAE,MAAM,OAAO;GAC3B,WAAW;IACT,MAAM;IACN,UAAU;IACV,QAAQ;KACN,aAAa;MAAE,MAAM;MAAQ,UAAU;KAAK;KAC5C,UAAU;MAAE,MAAM;MAAU,UAAU;KAAK;KAC3C,gBAAgB;MAAE,MAAM;MAAU,UAAU;KAAK;KACjD,iBAAiB;MAAE,MAAM;MAAU,UAAU;KAAK;KAClD,YAAY,EAAE,MAAM,OAAO;IAC7B;GACF;GAIA,iBAAiB;IACf,MAAM;IACN,QAAQ;KACN,WAAW,EAAE,MAAM,OAAO;KAC1B,UAAU,EAAE,MAAM,OAAO;KACzB,UAAU,EAAE,MAAM,OAAO;KACzB,UAAU,EAAE,MAAM,OAAO;KACzB,MAAM,EAAE,MAAM,OAAO;KACrB,OAAO,EAAE,MAAM,OAAO;KACtB,KAAK,EAAE,MAAM,OAAO;KACpB,SAAS;MAAE,MAAM;MAAQ,cAAc;KAAK;KAC5C,OAAO,EAAE,MAAM,OAAO;IACxB;GACF;GACA,mBAAmB;IACjB,MAAM;IACN,SAAS;KAAC;KAAW;KAAW;KAAa;IAAQ;GACvD;GAIA,qBAAqB,EAAE,MAAM,OAAO;GAIpC,wBAAwB,EAAE,MAAM,OAAO;GACvC,gBAAgB,EAAE,MAAM,OAAO;GAC/B,iBAAiB,EAAE,MAAM,OAAO;GAChC,aAAa,EAAE,MAAM,OAAO;GAC5B,WAAW;IAAE,MAAM;IAAQ,MAAM;IAAa,cAAc;GAAM;EACpE;CACF;AACF;AAEA,SAAS,yBACP,MACA,WACkB;CAClB,OAAO;EACL;EACA,QAAQ;GACN,IAAI;IAAE,MAAM;IAAU,eAAe;GAAK;GAC1C,OAAO;IAAE,MAAM;IAAQ,UAAU;IAAM,QAAQ;GAAK;GACpD,UAAU;IAAE,MAAM;IAAU,SAAS,CAAC,UAAU,QAAQ;GAAE;GAC1D,qBAAqB,EAAE,MAAM,OAAO;GACpC,YAAY;IAAE,MAAM;IAAgB,YAAY;GAAU;GAG1D,mBAAmB,EAAE,MAAM,OAAO;GAClC,eAAe;IAAE,MAAM;IAAU,cAAc;GAAE;GACjD,WAAW;IAAE,MAAM;IAAQ,MAAM;IAAa,cAAc;GAAM;EACpE;CACF;AACF;AAEA,SAAS,wBACP,MACA,YACkB;CAClB,OAAO;EACL;EACA,QAAQ;GACN,IAAI;IAAE,MAAM;IAAU,eAAe;GAAK;GAC1C,UAAU;IACR,MAAM;IACN,SAAS,CAAC,UAAU,QAAQ;IAC5B,UAAU;GACZ;GACA,oBAAoB;IAAE,MAAM;IAAQ,UAAU;GAAK;GACnD,kBAAkB,EAAE,MAAM,OAAO;GACjC,OAAO;IAAE,MAAM;IAAgB,YAAY;GAAW;GAKtD,QAAQ,EAAE,MAAM,OAAO;GACvB,aAAa;IAAE,MAAM;IAAU,UAAU;GAAK;GAC9C,UAAU;IAAE,MAAM;IAAQ,cAAc;GAAM;GAI9C,aAAa,EAAE,MAAM,OAAO;GAC5B,WAAW;IAAE,MAAM;IAAQ,MAAM;IAAa,cAAc;GAAM;EACpE;CACF;AACF;AAEA,SAAS,6BAA6B,MAAgC;CACpE,OAAO;EACL;EACA,QAAQ;GACN,IAAI;IAAE,MAAM;IAAU,eAAe;GAAK;GAC1C,UAAU;IACR,MAAM;IACN,SAAS,CAAC,UAAU,QAAQ;IAC5B,UAAU;GACZ;GAKA,SAAS;IAAE,MAAM;IAAQ,UAAU;IAAM,QAAQ;GAAK;GACtD,WAAW,EAAE,MAAM,OAAO;GAC1B,YAAY;IAAE,MAAM;IAAQ,MAAM;IAAa,cAAc;GAAM;EACrE;CACF;AACF;AAEA,SAAS,6BACP,MACA,eACkB;CAClB,OAAO;EACL;EACA,QAAQ;GACN,IAAI;IAAE,MAAM;IAAU,eAAe;GAAK;GAC1C,UAAU;IACR,MAAM;IACN,SAAS,CAAC,UAAU,QAAQ;IAC5B,UAAU;GACZ;GACA,yBAAyB;IAAE,MAAM;IAAQ,UAAU;GAAK;GACxD,UAAU;IAAE,MAAM;IAAgB,YAAY;GAAc;GAC5D,QAAQ,EAAE,MAAM,OAAO;GACvB,oBAAoB;IAAE,MAAM;IAAQ,MAAM;GAAY;GACtD,WAAW;IAAE,MAAM;IAAQ,MAAM;IAAa,cAAc;GAAM;EACpE;CACF;AACF;;;;;;;AAQA,SAAgB,gBACd,UAAkC,CAAC,GACrB;CACd,MAAM,QAAQ;EACZ,UAAU,QAAQ,gBAAgB,SAAS;EAC3C,QAAQ,QAAQ,cAAc,SAAS;EACvC,WAAW,QAAQ,iBAAiB,SAAS;EAC7C,UAAU,QAAQ,gBAAgB,SAAS;EAC3C,eAAe,QAAQ,qBAAqB,SAAS;EACrD,eAAe,QAAQ,qBAAqB,SAAS;EACrD,OAAO,QAAQ,aAAa,SAAS;CACvC;CAEA,QAAQ,WAAW;EACjB,MAAM,cAAc,CAAC,GAAG,OAAO,WAAW;EAC1C,MAAM,gBAAgB,eAAiC;GACrD,IAAI,CAAC,YAAY,MAAM,MAAM,EAAE,SAAS,WAAW,IAAI,GACrD,YAAY,KAAK,UAAU;EAE/B;EAEA,aAAa,wBAAwB,MAAM,QAAQ,CAAC;EACpD,aAAa,sBAAsB,MAAM,QAAQ,MAAM,SAAS,CAAC;EACjE,aAAa,yBAAyB,MAAM,WAAW,MAAM,KAAK,CAAC;EACnE,aAAa,wBAAwB,MAAM,UAAU,MAAM,MAAM,CAAC;EAClE,aAAa,6BAA6B,MAAM,aAAa,CAAC;EAC9D,IAAI,QAAQ,sBACV,aACE,6BAA6B,MAAM,eAAe,MAAM,SAAS,CACnE;EAGF,OAAO;GAAE,GAAG;GAAQ;EAAY;CAClC;AACF;;;;;;;;;;;;;;;;;;;;;;AChQA,eAAsB,uBACpB,OACA,SACe;CACf,MAAM,YAAa,MAAM,aAAa,CAAC;CAIvC,MAAM,kBACH,MAAM,mBAA0D,CAAC;CAEpE,MAAM,SAAS,MAAM,QAAQ,SAAS,uBAAuB;EAC3D,SAAS,MAAM;EACf,WAAW,UACR,QAAQ,SACP,QAAQ,KAAK,UAAU,CACzB,CAAC,CACA,KAAK,UAAU;GACd,YAAY,KAAK;GACjB,UAAU,KAAK;EACjB,EAAE;EACJ;EACA,eAAe,MAAM;CACvB,CAAC;CAED,MAAM,QAAQ,OAAO,OAAO,QAAQ,SAAS,MAAM,IAAc;EAC/D,qBAAqB,QAAQ,SAAS;EACtC,wBAAwB,OAAO;EAC/B,mBAAmB,OAAO;CAC5B,CAAC;AACH;AAWA,eAAe,0BACb,QACA,SACA,wBAC8C;CAI9C,QAAO,MADa,OAAO,KAAK,OAAO,EAAA,CAC3B,MACT,QAAQ,IAAI,2BAA2B,sBAC1C;AACF;AAEA,SAASA,0BAAwB,OAAyB;CACxD,OACE,iBAAiB,SACjB,MAAM,QAAQ,SAAS,4BAA4B;AAEvD;AAEA,eAAe,yBACb,OACA,SACe;CACf,IAAI,MAAM,SAAS,aAAa;CAEhC,MAAM,QAAQ,MAAM,0BAClB,QAAQ,QACR,QAAQ,SACR,MAAM,sBACR;CACA,IAAI,CAAC,OAAO;CAEZ,MAAM,QAAQ,OAAO,OAAO,QAAQ,SAAS,MAAM,IAAc;EAC/D,mBAAmB,MAAM;EACzB,gBAAgB,MAAM;EACtB,iBAAiB,MAAM;EACvB,aAAa,MAAM;CACrB,CAAC;AACH;;;;;;;;;;;;;AAcA,SAAgB,gCACd,SACA;CACA,OAAO,OAAO,MAAkC;EAC9C,MAAM,UAAU,MAAM,EAAE,IAAI,KAAK;EAOjC,IAAI,CAAC,MALkB,QAAQ,SAAS,uBAAuB;GAC7D;GACA,SAAS,EAAE,IAAI,IAAI;GACnB,QAAQ,QAAQ;EAClB,CAAC,GAEC,OAAO,EAAE,KAAK,EAAE,OAAO,oBAAoB,GAAG,GAAG;EAGnD,MAAM,EAAE,SAAS,UAAU,QAAQ,SAAS,kBAAkB,OAAO;EAErE,IAAI;GACF,MAAM,QAAQ,cAAc,OAAO,QAAQ,SAAS;IAClD,UAAU,QAAQ,SAAS;IAC3B;IACA,WAAW,MAAM;GACnB,CAAC;EACH,SAAS,OAAO;GACd,IAAIA,0BAAwB,KAAK,GAC/B,OAAO,EAAE,KAAK;IAAE,IAAI;IAAM,WAAW;GAAK,GAAG,GAAG;GAElD,MAAM;EACR;EAEA,IAAI;GACF,MAAM,yBAAyB,OAAO,OAAO;EAC/C,SAAS,OAAO;GACd,QAAQ,MACN,iEACA,KACF;EACF;EAEA,OAAO,EAAE,KAAK,EAAE,IAAI,KAAK,GAAG,GAAG;CACjC;AACF;;;AC7HA,SAAS,wBAAwB,OAAyB;CACxD,OACE,iBAAiB,SACjB,MAAM,QAAQ,SAAS,4BAA4B;AAEvD;AAEA,eAAe,eACb,KACA,SACA,OACA,OAC8C;CAO9C,QAAO,MADa,IAAI,KAAK,OAAO,EAAA,CACxB,MAAM,QAAQ,IAAI,WAAW,KAAK;AAChD;AAEA,eAAe,cACb,OACA,SACe;CACf,QAAQ,MAAM,MAAd;EACE,KAAK,mBAAmB;GACtB,MAAM,UAAU,MAAM,eACpB,QAAQ,UACR,QAAQ,SACR,sBACA,MAAM,kBACR;GACA,IAAI,SACF,MAAM,QAAQ,SAAS,OAAO,QAAQ,SAAS,QAAQ,IAAc,EACnE,QAAQ,MAAM,OAChB,CAAC;GAEH,MAAM,QAAQ,MAAM,eAClB,QAAQ,QACR,QAAQ,SACR,sBACA,MAAM,kBACR;GACA,IAAI,OAAO;IACT,MAAM,SACJ,MAAM,WAAW,cACb,SACA,MAAM,WAAW,aACf,aACA;IACR,MAAM,UAAU,MAAM,QAAQ,OAAO,OACnC,QAAQ,SACR,MAAM,IACN,EAAE,OAAO,CACX;IACA,IAAI,WAAW,UAAU,QAAQ,aAC/B,MAAM,QAAQ,YAAY,OAAO;GAErC;GACA;EACF;EACA,KAAK,iBAAiB;GACpB,MAAM,QAAQ,MAAM,eAClB,QAAQ,QACR,QAAQ,SACR,oBACA,MAAM,gBACR;GACA,IAAI,OAAO;IACT,MAAM,UAAU,MAAM,QAAQ,OAAO,OACnC,QAAQ,SACR,MAAM,IACN,EAAE,QAAQ,MAAM,OAAO,CACzB;IACA,IAAI,MAAM,WAAW,UAAU,QAAQ,aACrC,MAAM,QAAQ,YAAY,OAAO;GAErC;GACA;EACF;EACA,KAAK,wBAAwB;GAC3B,IAAI,CAAC,QAAQ,eAAe;GAC5B,MAAM,eAAe,MAAM,eACzB,QAAQ,eACR,QAAQ,SACR,2BACA,MAAM,uBACR;GACA,IAAI,cACF,MAAM,QAAQ,cAAc,OAC1B,QAAQ,SACR,aAAa,IACb,EAAE,QAAQ,MAAM,OAAO,CACzB;GAEF;EACF;EACA,KAAK,aACH;CACJ;AACF;;;;;;;;;;;;;AAcA,SAAgB,qBACd,SACA;CACA,OAAO,OAAO,MAAkC;EAI9C,MAAM,UAAU,MAAM,EAAE,IAAI,KAAK;EAQjC,IAAI,CAAC,MANkB,QAAQ,SAAS,uBAAuB;GAC7D;GACA,SAAS,EAAE,IAAI,IAAI;GACnB,QAAQ,QAAQ;GAChB,iBAAiB,QAAQ;EAC3B,CAAC,GAEC,OAAO,EAAE,KAAK,EAAE,OAAO,oBAAoB,GAAG,GAAG;EAGnD,MAAM,EAAE,SAAS,UAAU,QAAQ,SAAS,kBAAkB,OAAO;EAErE,IAAI;GACF,MAAM,QAAQ,cAAc,OAAO,QAAQ,SAAS;IAClD,UAAU,QAAQ,SAAS;IAC3B;IACA,WAAW,MAAM;GACnB,CAAC;EACH,SAAS,OAAO;GACd,IAAI,wBAAwB,KAAK,GAI/B,OAAO,EAAE,KAAK;IAAE,IAAI;IAAM,WAAW;GAAK,GAAG,GAAG;GAElD,MAAM;EACR;EAEA,IAAI;GACF,MAAM,cAAc,OAAO,OAAO;EACpC,SAAS,OAAO;GAKd,QAAQ,MAAM,qDAAqD,KAAK;EAC1E;EAEA,OAAO,EAAE,KAAK,EAAE,IAAI,KAAK,GAAG,GAAG;CACjC;AACF"}
|
package/dist/index.d.cts
CHANGED
|
@@ -128,9 +128,81 @@ interface PaymentProvider {
|
|
|
128
128
|
cancel(providerSubscriptionRef: string): Promise<void>;
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* `FulfillmentProvider` is a second instance of the "plugin-defined
|
|
133
|
+
* provider interface" pattern documented in EXTENDING.md alongside
|
|
134
|
+
* `PaymentProvider` — orthogonal to it, not a variant of it. A
|
|
135
|
+
* `PaymentProvider` charges a card; a `FulfillmentProvider` ships physical
|
|
136
|
+
* goods (print-on-demand, a 3PL, etc.) once an order is paid. An order's
|
|
137
|
+
* `provider` field (who charged the card) and `fulfillmentProvider` field
|
|
138
|
+
* (who ships it) are independent — a single Stripe-charged order might be
|
|
139
|
+
* fulfilled by Printful, a different POD vendor, or not at all (digital
|
|
140
|
+
* goods).
|
|
141
|
+
*/
|
|
142
|
+
interface FulfillmentLineItem {
|
|
143
|
+
/** Fulfillment provider's own catalog identifier (e.g. Printful sync variant id) — opaque to this plugin, distinct from `CartLineItem.catalogRef`. */
|
|
144
|
+
catalogRef: string;
|
|
145
|
+
quantity: number;
|
|
146
|
+
}
|
|
147
|
+
interface FulfillmentOrderRequest {
|
|
148
|
+
/** This plugin's own `orders.id` — round-tripped so `createFulfillmentOrder` can correlate its result back to the order row that requested it. */
|
|
149
|
+
orderId: number;
|
|
150
|
+
lineItems: FulfillmentLineItem[];
|
|
151
|
+
shippingAddress: {
|
|
152
|
+
firstName?: string;
|
|
153
|
+
lastName?: string;
|
|
154
|
+
address1?: string;
|
|
155
|
+
address2?: string;
|
|
156
|
+
city?: string;
|
|
157
|
+
state?: string;
|
|
158
|
+
zip?: string;
|
|
159
|
+
country?: string;
|
|
160
|
+
phone?: string;
|
|
161
|
+
};
|
|
162
|
+
customerEmail?: string;
|
|
163
|
+
}
|
|
164
|
+
interface FulfillmentOrderResult {
|
|
165
|
+
/** Provider's own identifier for the fulfillment order — stored on `orders.fulfillmentProviderRef`, the correlation key inbound fulfillment webhooks dispatch against. */
|
|
166
|
+
providerFulfillmentRef: string;
|
|
167
|
+
status: "pending" | "shipped" | "delivered" | "failed";
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The shape every fulfillment provider's raw webhook payload is translated
|
|
171
|
+
* into — mirrors `NormalizedWebhookEvent`'s role for `PaymentProvider`, kept
|
|
172
|
+
* as a separate type rather than a union member of it since the two event
|
|
173
|
+
* vocabularies (payment status vs. shipment status) don't overlap.
|
|
174
|
+
*/
|
|
175
|
+
type NormalizedFulfillmentWebhookEvent = {
|
|
176
|
+
kind: "fulfillment.updated";
|
|
177
|
+
providerFulfillmentRef: string;
|
|
178
|
+
status: "shipped" | "delivered" | "failed";
|
|
179
|
+
trackingNumber?: string;
|
|
180
|
+
trackingCarrier?: string;
|
|
181
|
+
trackingUrl?: string;
|
|
182
|
+
} | {
|
|
183
|
+
kind: "unhandled";
|
|
184
|
+
rawType: string;
|
|
185
|
+
};
|
|
186
|
+
interface FulfillmentProvider {
|
|
187
|
+
/** e.g. "printful" — a plain string, not a closed union like `PaymentProvider.name`: fulfillment backends are a more open set (POD vendors, 3PLs) than the two payment rails this plugin ships providers for. */
|
|
188
|
+
readonly name: string;
|
|
189
|
+
/** Submits a paid order's line items for fulfillment. Called once per order, after `PaymentProvider` reports the order as paid. */
|
|
190
|
+
createFulfillmentOrder(request: FulfillmentOrderRequest): Promise<FulfillmentOrderResult>;
|
|
191
|
+
/** Verifies an inbound webhook's signature — same contract as `PaymentProvider.verifyWebhookSignature`. */
|
|
192
|
+
verifyWebhookSignature(args: {
|
|
193
|
+
rawBody: string;
|
|
194
|
+
headers: Headers;
|
|
195
|
+
secret: string;
|
|
196
|
+
}): Promise<boolean>;
|
|
197
|
+
/** Parses an already-verified raw webhook body into a normalized event, plus the provider's own event id for dedup. */
|
|
198
|
+
parseWebhookEvent(rawBody: string): {
|
|
199
|
+
eventId: string;
|
|
200
|
+
event: NormalizedFulfillmentWebhookEvent;
|
|
201
|
+
};
|
|
202
|
+
}
|
|
131
203
|
//#endregion
|
|
132
204
|
//#region src/checkout.d.ts
|
|
133
|
-
type AnyLocalApi$
|
|
205
|
+
type AnyLocalApi$2<TContext> = LocalApi<any, TContext>;
|
|
134
206
|
interface CheckoutRequestBody {
|
|
135
207
|
lineItems: CartLineItem[];
|
|
136
208
|
paymentSourceToken: string;
|
|
@@ -141,8 +213,8 @@ interface CheckoutRequestBody {
|
|
|
141
213
|
}
|
|
142
214
|
interface CheckoutHandlerOptions<TContext> {
|
|
143
215
|
provider: PaymentProvider;
|
|
144
|
-
orders: AnyLocalApi$
|
|
145
|
-
payments: AnyLocalApi$
|
|
216
|
+
orders: AnyLocalApi$2<TContext>;
|
|
217
|
+
payments: AnyLocalApi$2<TContext>;
|
|
146
218
|
/**
|
|
147
219
|
* Resolves the per-request access context passed to `orders`/`payments`
|
|
148
220
|
* — called once per request, the same shape and timing as
|
|
@@ -229,6 +301,55 @@ declare class CadmeaPaymentError extends Error {
|
|
|
229
301
|
constructor(message: string, cause?: unknown | undefined);
|
|
230
302
|
}
|
|
231
303
|
//#endregion
|
|
304
|
+
//#region src/fulfillment.d.ts
|
|
305
|
+
type AnyLocalApi$1<TContext> = LocalApi<any, TContext>;
|
|
306
|
+
interface CreateFulfillmentOrderOptions<TContext> {
|
|
307
|
+
provider: FulfillmentProvider;
|
|
308
|
+
orders: AnyLocalApi$1<TContext>;
|
|
309
|
+
context: TContext;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* The order-paid hook implementation: submits an already-paid order's line
|
|
313
|
+
* items to a `FulfillmentProvider` and persists the resulting
|
|
314
|
+
* `fulfillmentProvider`/`fulfillmentProviderRef`/`fulfillmentStatus` back
|
|
315
|
+
* onto the order row. Wire it as `WebhookHandlerOptions.onOrderPaid` on the
|
|
316
|
+
* *payment* provider's webhook handler:
|
|
317
|
+
*
|
|
318
|
+
* ```ts
|
|
319
|
+
* createWebhookHandler({
|
|
320
|
+
* provider: stripeProvider,
|
|
321
|
+
* orders, payments, webhookEvents, secret, context,
|
|
322
|
+
* onOrderPaid: (order) =>
|
|
323
|
+
* createFulfillmentOrder(order, { provider: printfulProvider, orders, context }),
|
|
324
|
+
* });
|
|
325
|
+
* ```
|
|
326
|
+
*
|
|
327
|
+
* Digital-goods-only stores simply never wire this — `fulfillmentProvider`
|
|
328
|
+
* is plugin-optional, not a hard dependency of `ecommercePlugin`.
|
|
329
|
+
*/
|
|
330
|
+
declare function createFulfillmentOrder<TContext>(order: Record<string, unknown>, options: CreateFulfillmentOrderOptions<TContext>): Promise<void>;
|
|
331
|
+
interface FulfillmentWebhookHandlerOptions<TContext> {
|
|
332
|
+
provider: FulfillmentProvider;
|
|
333
|
+
orders: AnyLocalApi$1<TContext>;
|
|
334
|
+
webhookEvents: AnyLocalApi$1<TContext>;
|
|
335
|
+
secret: string;
|
|
336
|
+
/** See `WebhookHandlerOptions.context`'s identical note in webhook.ts. */
|
|
337
|
+
context: TContext;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Returns a Hono handler implementing inbound fulfillment-webhook handling
|
|
341
|
+
* (shipment created/delivered/failed) against a `FulfillmentProvider` —
|
|
342
|
+
* verify signature → dedup via `webhook_events` (shared with the payment
|
|
343
|
+
* webhook handler; `eventId` is provider-specific so cross-provider
|
|
344
|
+
* collisions aren't a concern) → look up the order by
|
|
345
|
+
* `fulfillmentProviderRef` → update shipment status/tracking. Mirrors
|
|
346
|
+
* `createWebhookHandler`'s structure exactly; kept as a separate function
|
|
347
|
+
* rather than a generic merge of the two because the payment and
|
|
348
|
+
* fulfillment event vocabularies, dedup keys, and target collections don't
|
|
349
|
+
* overlap enough to share logic without branching on provider kind.
|
|
350
|
+
*/
|
|
351
|
+
declare function createFulfillmentWebhookHandler<TContext>(options: FulfillmentWebhookHandlerOptions<TContext>): (c: Context) => Promise<Response>;
|
|
352
|
+
//#endregion
|
|
232
353
|
//#region src/webhook.d.ts
|
|
233
354
|
type AnyLocalApi<TContext> = LocalApi<any, TContext>;
|
|
234
355
|
interface WebhookHandlerOptions<TContext> {
|
|
@@ -249,6 +370,17 @@ interface WebhookHandlerOptions<TContext> {
|
|
|
249
370
|
* own `access` config accepts for system-level writes.
|
|
250
371
|
*/
|
|
251
372
|
context: TContext;
|
|
373
|
+
/**
|
|
374
|
+
* Fires once, right after an order's status is written as "paid" by
|
|
375
|
+
* this handler — the order-paid hook point a `FulfillmentProvider`
|
|
376
|
+
* integration (e.g. `@thebes/cadmea-plugin-printful`) wires into to
|
|
377
|
+
* submit the order for shipment. Receives the just-updated order row.
|
|
378
|
+
* Errors thrown here are caught the same way dispatch-handler errors
|
|
379
|
+
* are below — logged, never surfaced as a failure to the payment
|
|
380
|
+
* provider, since the charge already succeeded and the provider must
|
|
381
|
+
* not retry the whole webhook over a fulfillment-side problem.
|
|
382
|
+
*/
|
|
383
|
+
onOrderPaid?: (order: Record<string, unknown>) => Promise<void>;
|
|
252
384
|
}
|
|
253
385
|
/**
|
|
254
386
|
* Returns a Hono handler implementing inbound webhook handling against a
|
|
@@ -264,5 +396,5 @@ interface WebhookHandlerOptions<TContext> {
|
|
|
264
396
|
*/
|
|
265
397
|
declare function createWebhookHandler<TContext>(options: WebhookHandlerOptions<TContext>): (c: Context) => Promise<Response>;
|
|
266
398
|
//#endregion
|
|
267
|
-
export { CadmeaPaymentError, CartLineItem, CatalogPriceCheck, CheckoutHandlerOptions, CheckoutRequest, CheckoutRequestBody, CheckoutResult, EcommercePluginOptions, Money, NormalizedWebhookEvent, PaymentProvider, WebhookHandlerOptions, createCheckoutHandler, createWebhookHandler, ecommercePlugin };
|
|
399
|
+
export { CadmeaPaymentError, CartLineItem, CatalogPriceCheck, CheckoutHandlerOptions, CheckoutRequest, CheckoutRequestBody, CheckoutResult, CreateFulfillmentOrderOptions, EcommercePluginOptions, FulfillmentLineItem, FulfillmentOrderRequest, FulfillmentOrderResult, FulfillmentProvider, FulfillmentWebhookHandlerOptions, Money, NormalizedFulfillmentWebhookEvent, NormalizedWebhookEvent, PaymentProvider, WebhookHandlerOptions, createCheckoutHandler, createFulfillmentOrder, createFulfillmentWebhookHandler, createWebhookHandler, ecommercePlugin };
|
|
268
400
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/checkout.ts","../src/collections.ts","../src/errors.ts","../src/webhook.ts"],"mappings":";;;;;;;UAsBiB,KAAA;EACf,MAAA;EACA,QAAQ;AAAA;AAAA,UAGO,YAAA;EAHP;EAKR,UAAA;EACA,QAAA;;;;;;;;EAQA,eAAA,EAAiB,KAAK;AAAA;AAAA,UAGP,eAAA;EACf,SAAA,EAAW,YAAA;EAOM;EALjB,kBAAA;EACA,aAAA;EADA;EAGA,cAAA;EAAA;EAEA,QAAA,GAAW,MAAM;AAAA;AAAA,UAGF,cAAA;EAHE;EAKjB,gBAAA;EAF6B;EAI7B,kBAAA;EACA,MAAA;EACA,MAAA,EAAQ,KAAA;EAFR;EAIA,GAAA,EAAK,MAAM;AAAA;;UAII,iBAAA;EACf,UAAA;EACA,eAAA,EAAiB,KAAK;EANX;EAQX,iBAAA;AAAA;;;;;;KAQU,sBAAA;EAEN,IAAA;EACA,kBAAA;EACA,MAAA;AAAA;EAGA,IAAA;EACA,gBAAA;EACA,MAAA;AAAA;EAGA,IAAA;EACA,uBAAA;EACA,MAAA;AAAA;EAEA,IAAA;EAAmB,OAAA;AAAA;AAAA,UAER,eAAA;EAAA,SACN,IAAA;EAHqB;EAM9B,kBAAA,CAAmB,IAAA,aAAiB,OAAA,CAAQ,iBAAA;EAJ7B;EAOf,oBAAA,CAAqB,KAAA,UAAe,cAAA,WAAyB,OAAA;;EAG7D,QAAA,CAAS,OAAA,EAAS,eAAA,GAAkB,OAAA,CAAQ,cAAA;EANR;;;;;EAapC,sBAAA,CAAuB,IAAA;IACrB,OAAA;IACA,OAAA,EAAS,OAAA;IACT,MAAA,UAsBE;IApBF,eAAA;EAAA,IACE,OAAA;EAuCuC;;;;;EAhC3C,iBAAA,CAAkB,OAAA;IAChB,OAAA;IACA,KAAA,EAAO,sBAAA;EAAA;EAzBY;;;;;EAiCrB,WAAA;IACE,gBAAA,IAAoB,OAAA,CAClB,KAAA;MACE,UAAA;MACA,IAAA;MACA,SAAA,EAAW,KAAA;MACX,GAAA;IAAA;EAAA;EAxBJ;;;;;EAkCF,aAAA;IACE,MAAA,CAAO,IAAA;MACL,WAAA;MACA,OAAA;MACA,cAAA;IAAA,IACE,OAAA;MAAU,uBAAA;MAAiC,MAAA;IAAA;IAC/C,MAAA,CAAO,uBAAA,WAAkC,OAAA;EAAA;AAAA;;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/checkout.ts","../src/collections.ts","../src/errors.ts","../src/fulfillment.ts","../src/webhook.ts"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -128,9 +128,81 @@ interface PaymentProvider {
|
|
|
128
128
|
cancel(providerSubscriptionRef: string): Promise<void>;
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* `FulfillmentProvider` is a second instance of the "plugin-defined
|
|
133
|
+
* provider interface" pattern documented in EXTENDING.md alongside
|
|
134
|
+
* `PaymentProvider` — orthogonal to it, not a variant of it. A
|
|
135
|
+
* `PaymentProvider` charges a card; a `FulfillmentProvider` ships physical
|
|
136
|
+
* goods (print-on-demand, a 3PL, etc.) once an order is paid. An order's
|
|
137
|
+
* `provider` field (who charged the card) and `fulfillmentProvider` field
|
|
138
|
+
* (who ships it) are independent — a single Stripe-charged order might be
|
|
139
|
+
* fulfilled by Printful, a different POD vendor, or not at all (digital
|
|
140
|
+
* goods).
|
|
141
|
+
*/
|
|
142
|
+
interface FulfillmentLineItem {
|
|
143
|
+
/** Fulfillment provider's own catalog identifier (e.g. Printful sync variant id) — opaque to this plugin, distinct from `CartLineItem.catalogRef`. */
|
|
144
|
+
catalogRef: string;
|
|
145
|
+
quantity: number;
|
|
146
|
+
}
|
|
147
|
+
interface FulfillmentOrderRequest {
|
|
148
|
+
/** This plugin's own `orders.id` — round-tripped so `createFulfillmentOrder` can correlate its result back to the order row that requested it. */
|
|
149
|
+
orderId: number;
|
|
150
|
+
lineItems: FulfillmentLineItem[];
|
|
151
|
+
shippingAddress: {
|
|
152
|
+
firstName?: string;
|
|
153
|
+
lastName?: string;
|
|
154
|
+
address1?: string;
|
|
155
|
+
address2?: string;
|
|
156
|
+
city?: string;
|
|
157
|
+
state?: string;
|
|
158
|
+
zip?: string;
|
|
159
|
+
country?: string;
|
|
160
|
+
phone?: string;
|
|
161
|
+
};
|
|
162
|
+
customerEmail?: string;
|
|
163
|
+
}
|
|
164
|
+
interface FulfillmentOrderResult {
|
|
165
|
+
/** Provider's own identifier for the fulfillment order — stored on `orders.fulfillmentProviderRef`, the correlation key inbound fulfillment webhooks dispatch against. */
|
|
166
|
+
providerFulfillmentRef: string;
|
|
167
|
+
status: "pending" | "shipped" | "delivered" | "failed";
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The shape every fulfillment provider's raw webhook payload is translated
|
|
171
|
+
* into — mirrors `NormalizedWebhookEvent`'s role for `PaymentProvider`, kept
|
|
172
|
+
* as a separate type rather than a union member of it since the two event
|
|
173
|
+
* vocabularies (payment status vs. shipment status) don't overlap.
|
|
174
|
+
*/
|
|
175
|
+
type NormalizedFulfillmentWebhookEvent = {
|
|
176
|
+
kind: "fulfillment.updated";
|
|
177
|
+
providerFulfillmentRef: string;
|
|
178
|
+
status: "shipped" | "delivered" | "failed";
|
|
179
|
+
trackingNumber?: string;
|
|
180
|
+
trackingCarrier?: string;
|
|
181
|
+
trackingUrl?: string;
|
|
182
|
+
} | {
|
|
183
|
+
kind: "unhandled";
|
|
184
|
+
rawType: string;
|
|
185
|
+
};
|
|
186
|
+
interface FulfillmentProvider {
|
|
187
|
+
/** e.g. "printful" — a plain string, not a closed union like `PaymentProvider.name`: fulfillment backends are a more open set (POD vendors, 3PLs) than the two payment rails this plugin ships providers for. */
|
|
188
|
+
readonly name: string;
|
|
189
|
+
/** Submits a paid order's line items for fulfillment. Called once per order, after `PaymentProvider` reports the order as paid. */
|
|
190
|
+
createFulfillmentOrder(request: FulfillmentOrderRequest): Promise<FulfillmentOrderResult>;
|
|
191
|
+
/** Verifies an inbound webhook's signature — same contract as `PaymentProvider.verifyWebhookSignature`. */
|
|
192
|
+
verifyWebhookSignature(args: {
|
|
193
|
+
rawBody: string;
|
|
194
|
+
headers: Headers;
|
|
195
|
+
secret: string;
|
|
196
|
+
}): Promise<boolean>;
|
|
197
|
+
/** Parses an already-verified raw webhook body into a normalized event, plus the provider's own event id for dedup. */
|
|
198
|
+
parseWebhookEvent(rawBody: string): {
|
|
199
|
+
eventId: string;
|
|
200
|
+
event: NormalizedFulfillmentWebhookEvent;
|
|
201
|
+
};
|
|
202
|
+
}
|
|
131
203
|
//#endregion
|
|
132
204
|
//#region src/checkout.d.ts
|
|
133
|
-
type AnyLocalApi$
|
|
205
|
+
type AnyLocalApi$2<TContext> = LocalApi<any, TContext>;
|
|
134
206
|
interface CheckoutRequestBody {
|
|
135
207
|
lineItems: CartLineItem[];
|
|
136
208
|
paymentSourceToken: string;
|
|
@@ -141,8 +213,8 @@ interface CheckoutRequestBody {
|
|
|
141
213
|
}
|
|
142
214
|
interface CheckoutHandlerOptions<TContext> {
|
|
143
215
|
provider: PaymentProvider;
|
|
144
|
-
orders: AnyLocalApi$
|
|
145
|
-
payments: AnyLocalApi$
|
|
216
|
+
orders: AnyLocalApi$2<TContext>;
|
|
217
|
+
payments: AnyLocalApi$2<TContext>;
|
|
146
218
|
/**
|
|
147
219
|
* Resolves the per-request access context passed to `orders`/`payments`
|
|
148
220
|
* — called once per request, the same shape and timing as
|
|
@@ -229,6 +301,55 @@ declare class CadmeaPaymentError extends Error {
|
|
|
229
301
|
constructor(message: string, cause?: unknown | undefined);
|
|
230
302
|
}
|
|
231
303
|
//#endregion
|
|
304
|
+
//#region src/fulfillment.d.ts
|
|
305
|
+
type AnyLocalApi$1<TContext> = LocalApi<any, TContext>;
|
|
306
|
+
interface CreateFulfillmentOrderOptions<TContext> {
|
|
307
|
+
provider: FulfillmentProvider;
|
|
308
|
+
orders: AnyLocalApi$1<TContext>;
|
|
309
|
+
context: TContext;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* The order-paid hook implementation: submits an already-paid order's line
|
|
313
|
+
* items to a `FulfillmentProvider` and persists the resulting
|
|
314
|
+
* `fulfillmentProvider`/`fulfillmentProviderRef`/`fulfillmentStatus` back
|
|
315
|
+
* onto the order row. Wire it as `WebhookHandlerOptions.onOrderPaid` on the
|
|
316
|
+
* *payment* provider's webhook handler:
|
|
317
|
+
*
|
|
318
|
+
* ```ts
|
|
319
|
+
* createWebhookHandler({
|
|
320
|
+
* provider: stripeProvider,
|
|
321
|
+
* orders, payments, webhookEvents, secret, context,
|
|
322
|
+
* onOrderPaid: (order) =>
|
|
323
|
+
* createFulfillmentOrder(order, { provider: printfulProvider, orders, context }),
|
|
324
|
+
* });
|
|
325
|
+
* ```
|
|
326
|
+
*
|
|
327
|
+
* Digital-goods-only stores simply never wire this — `fulfillmentProvider`
|
|
328
|
+
* is plugin-optional, not a hard dependency of `ecommercePlugin`.
|
|
329
|
+
*/
|
|
330
|
+
declare function createFulfillmentOrder<TContext>(order: Record<string, unknown>, options: CreateFulfillmentOrderOptions<TContext>): Promise<void>;
|
|
331
|
+
interface FulfillmentWebhookHandlerOptions<TContext> {
|
|
332
|
+
provider: FulfillmentProvider;
|
|
333
|
+
orders: AnyLocalApi$1<TContext>;
|
|
334
|
+
webhookEvents: AnyLocalApi$1<TContext>;
|
|
335
|
+
secret: string;
|
|
336
|
+
/** See `WebhookHandlerOptions.context`'s identical note in webhook.ts. */
|
|
337
|
+
context: TContext;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Returns a Hono handler implementing inbound fulfillment-webhook handling
|
|
341
|
+
* (shipment created/delivered/failed) against a `FulfillmentProvider` —
|
|
342
|
+
* verify signature → dedup via `webhook_events` (shared with the payment
|
|
343
|
+
* webhook handler; `eventId` is provider-specific so cross-provider
|
|
344
|
+
* collisions aren't a concern) → look up the order by
|
|
345
|
+
* `fulfillmentProviderRef` → update shipment status/tracking. Mirrors
|
|
346
|
+
* `createWebhookHandler`'s structure exactly; kept as a separate function
|
|
347
|
+
* rather than a generic merge of the two because the payment and
|
|
348
|
+
* fulfillment event vocabularies, dedup keys, and target collections don't
|
|
349
|
+
* overlap enough to share logic without branching on provider kind.
|
|
350
|
+
*/
|
|
351
|
+
declare function createFulfillmentWebhookHandler<TContext>(options: FulfillmentWebhookHandlerOptions<TContext>): (c: Context) => Promise<Response>;
|
|
352
|
+
//#endregion
|
|
232
353
|
//#region src/webhook.d.ts
|
|
233
354
|
type AnyLocalApi<TContext> = LocalApi<any, TContext>;
|
|
234
355
|
interface WebhookHandlerOptions<TContext> {
|
|
@@ -249,6 +370,17 @@ interface WebhookHandlerOptions<TContext> {
|
|
|
249
370
|
* own `access` config accepts for system-level writes.
|
|
250
371
|
*/
|
|
251
372
|
context: TContext;
|
|
373
|
+
/**
|
|
374
|
+
* Fires once, right after an order's status is written as "paid" by
|
|
375
|
+
* this handler — the order-paid hook point a `FulfillmentProvider`
|
|
376
|
+
* integration (e.g. `@thebes/cadmea-plugin-printful`) wires into to
|
|
377
|
+
* submit the order for shipment. Receives the just-updated order row.
|
|
378
|
+
* Errors thrown here are caught the same way dispatch-handler errors
|
|
379
|
+
* are below — logged, never surfaced as a failure to the payment
|
|
380
|
+
* provider, since the charge already succeeded and the provider must
|
|
381
|
+
* not retry the whole webhook over a fulfillment-side problem.
|
|
382
|
+
*/
|
|
383
|
+
onOrderPaid?: (order: Record<string, unknown>) => Promise<void>;
|
|
252
384
|
}
|
|
253
385
|
/**
|
|
254
386
|
* Returns a Hono handler implementing inbound webhook handling against a
|
|
@@ -264,5 +396,5 @@ interface WebhookHandlerOptions<TContext> {
|
|
|
264
396
|
*/
|
|
265
397
|
declare function createWebhookHandler<TContext>(options: WebhookHandlerOptions<TContext>): (c: Context) => Promise<Response>;
|
|
266
398
|
//#endregion
|
|
267
|
-
export { CadmeaPaymentError, CartLineItem, CatalogPriceCheck, CheckoutHandlerOptions, CheckoutRequest, CheckoutRequestBody, CheckoutResult, EcommercePluginOptions, Money, NormalizedWebhookEvent, PaymentProvider, WebhookHandlerOptions, createCheckoutHandler, createWebhookHandler, ecommercePlugin };
|
|
399
|
+
export { CadmeaPaymentError, CartLineItem, CatalogPriceCheck, CheckoutHandlerOptions, CheckoutRequest, CheckoutRequestBody, CheckoutResult, CreateFulfillmentOrderOptions, EcommercePluginOptions, FulfillmentLineItem, FulfillmentOrderRequest, FulfillmentOrderResult, FulfillmentProvider, FulfillmentWebhookHandlerOptions, Money, NormalizedFulfillmentWebhookEvent, NormalizedWebhookEvent, PaymentProvider, WebhookHandlerOptions, createCheckoutHandler, createFulfillmentOrder, createFulfillmentWebhookHandler, createWebhookHandler, ecommercePlugin };
|
|
268
400
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/checkout.ts","../src/collections.ts","../src/errors.ts","../src/webhook.ts"],"mappings":";;;;;;;UAsBiB,KAAA;EACf,MAAA;EACA,QAAQ;AAAA;AAAA,UAGO,YAAA;EAHP;EAKR,UAAA;EACA,QAAA;;;;;;;;EAQA,eAAA,EAAiB,KAAK;AAAA;AAAA,UAGP,eAAA;EACf,SAAA,EAAW,YAAA;EAOM;EALjB,kBAAA;EACA,aAAA;EADA;EAGA,cAAA;EAAA;EAEA,QAAA,GAAW,MAAM;AAAA;AAAA,UAGF,cAAA;EAHE;EAKjB,gBAAA;EAF6B;EAI7B,kBAAA;EACA,MAAA;EACA,MAAA,EAAQ,KAAA;EAFR;EAIA,GAAA,EAAK,MAAM;AAAA;;UAII,iBAAA;EACf,UAAA;EACA,eAAA,EAAiB,KAAK;EANX;EAQX,iBAAA;AAAA;;;;;;KAQU,sBAAA;EAEN,IAAA;EACA,kBAAA;EACA,MAAA;AAAA;EAGA,IAAA;EACA,gBAAA;EACA,MAAA;AAAA;EAGA,IAAA;EACA,uBAAA;EACA,MAAA;AAAA;EAEA,IAAA;EAAmB,OAAA;AAAA;AAAA,UAER,eAAA;EAAA,SACN,IAAA;EAHqB;EAM9B,kBAAA,CAAmB,IAAA,aAAiB,OAAA,CAAQ,iBAAA;EAJ7B;EAOf,oBAAA,CAAqB,KAAA,UAAe,cAAA,WAAyB,OAAA;;EAG7D,QAAA,CAAS,OAAA,EAAS,eAAA,GAAkB,OAAA,CAAQ,cAAA;EANR;;;;;EAapC,sBAAA,CAAuB,IAAA;IACrB,OAAA;IACA,OAAA,EAAS,OAAA;IACT,MAAA,UAsBE;IApBF,eAAA;EAAA,IACE,OAAA;EAuCuC;;;;;EAhC3C,iBAAA,CAAkB,OAAA;IAChB,OAAA;IACA,KAAA,EAAO,sBAAA;EAAA;EAzBY;;;;;EAiCrB,WAAA;IACE,gBAAA,IAAoB,OAAA,CAClB,KAAA;MACE,UAAA;MACA,IAAA;MACA,SAAA,EAAW,KAAA;MACX,GAAA;IAAA;EAAA;EAxBJ;;;;;EAkCF,aAAA;IACE,MAAA,CAAO,IAAA;MACL,WAAA;MACA,OAAA;MACA,cAAA;IAAA,IACE,OAAA;MAAU,uBAAA;MAAiC,MAAA;IAAA;IAC/C,MAAA,CAAO,uBAAA,WAAkC,OAAA;EAAA;AAAA;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/checkout.ts","../src/collections.ts","../src/errors.ts","../src/fulfillment.ts","../src/webhook.ts"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -291,6 +291,8 @@ function buildOrdersCollection(slug, customersSlug) {
|
|
|
291
291
|
"failed"
|
|
292
292
|
]
|
|
293
293
|
},
|
|
294
|
+
fulfillmentProvider: { type: "text" },
|
|
295
|
+
fulfillmentProviderRef: { type: "text" },
|
|
294
296
|
trackingNumber: { type: "text" },
|
|
295
297
|
trackingCarrier: { type: "text" },
|
|
296
298
|
trackingUrl: { type: "text" },
|
|
@@ -472,6 +474,104 @@ function ecommercePlugin(options = {}) {
|
|
|
472
474
|
};
|
|
473
475
|
}
|
|
474
476
|
//#endregion
|
|
477
|
+
//#region src/fulfillment.ts
|
|
478
|
+
/**
|
|
479
|
+
* The order-paid hook implementation: submits an already-paid order's line
|
|
480
|
+
* items to a `FulfillmentProvider` and persists the resulting
|
|
481
|
+
* `fulfillmentProvider`/`fulfillmentProviderRef`/`fulfillmentStatus` back
|
|
482
|
+
* onto the order row. Wire it as `WebhookHandlerOptions.onOrderPaid` on the
|
|
483
|
+
* *payment* provider's webhook handler:
|
|
484
|
+
*
|
|
485
|
+
* ```ts
|
|
486
|
+
* createWebhookHandler({
|
|
487
|
+
* provider: stripeProvider,
|
|
488
|
+
* orders, payments, webhookEvents, secret, context,
|
|
489
|
+
* onOrderPaid: (order) =>
|
|
490
|
+
* createFulfillmentOrder(order, { provider: printfulProvider, orders, context }),
|
|
491
|
+
* });
|
|
492
|
+
* ```
|
|
493
|
+
*
|
|
494
|
+
* Digital-goods-only stores simply never wire this — `fulfillmentProvider`
|
|
495
|
+
* is plugin-optional, not a hard dependency of `ecommercePlugin`.
|
|
496
|
+
*/
|
|
497
|
+
async function createFulfillmentOrder(order, options) {
|
|
498
|
+
const lineItems = order.lineItems ?? [];
|
|
499
|
+
const shippingAddress = order.shippingAddress ?? {};
|
|
500
|
+
const result = await options.provider.createFulfillmentOrder({
|
|
501
|
+
orderId: order.id,
|
|
502
|
+
lineItems: lineItems.filter((item) => Boolean(item.catalogRef)).map((item) => ({
|
|
503
|
+
catalogRef: item.catalogRef,
|
|
504
|
+
quantity: item.quantity
|
|
505
|
+
})),
|
|
506
|
+
shippingAddress,
|
|
507
|
+
customerEmail: order.guestEmail
|
|
508
|
+
});
|
|
509
|
+
await options.orders.update(options.context, order.id, {
|
|
510
|
+
fulfillmentProvider: options.provider.name,
|
|
511
|
+
fulfillmentProviderRef: result.providerFulfillmentRef,
|
|
512
|
+
fulfillmentStatus: result.status
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
async function findOrderByFulfillmentRef(orders, context, providerFulfillmentRef) {
|
|
516
|
+
return (await orders.find(context)).find((row) => row.fulfillmentProviderRef === providerFulfillmentRef);
|
|
517
|
+
}
|
|
518
|
+
function isUniqueConstraintError$1(error) {
|
|
519
|
+
return error instanceof Error && error.message.includes("Unique constraint violated");
|
|
520
|
+
}
|
|
521
|
+
async function dispatchFulfillmentEvent(event, options) {
|
|
522
|
+
if (event.kind === "unhandled") return;
|
|
523
|
+
const order = await findOrderByFulfillmentRef(options.orders, options.context, event.providerFulfillmentRef);
|
|
524
|
+
if (!order) return;
|
|
525
|
+
await options.orders.update(options.context, order.id, {
|
|
526
|
+
fulfillmentStatus: event.status,
|
|
527
|
+
trackingNumber: event.trackingNumber,
|
|
528
|
+
trackingCarrier: event.trackingCarrier,
|
|
529
|
+
trackingUrl: event.trackingUrl
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Returns a Hono handler implementing inbound fulfillment-webhook handling
|
|
534
|
+
* (shipment created/delivered/failed) against a `FulfillmentProvider` —
|
|
535
|
+
* verify signature → dedup via `webhook_events` (shared with the payment
|
|
536
|
+
* webhook handler; `eventId` is provider-specific so cross-provider
|
|
537
|
+
* collisions aren't a concern) → look up the order by
|
|
538
|
+
* `fulfillmentProviderRef` → update shipment status/tracking. Mirrors
|
|
539
|
+
* `createWebhookHandler`'s structure exactly; kept as a separate function
|
|
540
|
+
* rather than a generic merge of the two because the payment and
|
|
541
|
+
* fulfillment event vocabularies, dedup keys, and target collections don't
|
|
542
|
+
* overlap enough to share logic without branching on provider kind.
|
|
543
|
+
*/
|
|
544
|
+
function createFulfillmentWebhookHandler(options) {
|
|
545
|
+
return async (c) => {
|
|
546
|
+
const rawBody = await c.req.text();
|
|
547
|
+
if (!await options.provider.verifyWebhookSignature({
|
|
548
|
+
rawBody,
|
|
549
|
+
headers: c.req.raw.headers,
|
|
550
|
+
secret: options.secret
|
|
551
|
+
})) return c.json({ error: "Invalid signature" }, 401);
|
|
552
|
+
const { eventId, event } = options.provider.parseWebhookEvent(rawBody);
|
|
553
|
+
try {
|
|
554
|
+
await options.webhookEvents.create(options.context, {
|
|
555
|
+
provider: options.provider.name,
|
|
556
|
+
eventId,
|
|
557
|
+
eventType: event.kind
|
|
558
|
+
});
|
|
559
|
+
} catch (error) {
|
|
560
|
+
if (isUniqueConstraintError$1(error)) return c.json({
|
|
561
|
+
ok: true,
|
|
562
|
+
duplicate: true
|
|
563
|
+
}, 200);
|
|
564
|
+
throw error;
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
await dispatchFulfillmentEvent(event, options);
|
|
568
|
+
} catch (error) {
|
|
569
|
+
console.error("[cadmea-plugin-ecommerce] fulfillment webhook dispatch failed", error);
|
|
570
|
+
}
|
|
571
|
+
return c.json({ ok: true }, 200);
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
//#endregion
|
|
475
575
|
//#region src/webhook.ts
|
|
476
576
|
function isUniqueConstraintError(error) {
|
|
477
577
|
return error instanceof Error && error.message.includes("Unique constraint violated");
|
|
@@ -487,13 +587,17 @@ async function dispatchEvent(event, options) {
|
|
|
487
587
|
const order = await findOneByField(options.orders, options.context, "providerPaymentRef", event.providerPaymentRef);
|
|
488
588
|
if (order) {
|
|
489
589
|
const status = event.status === "succeeded" ? "paid" : event.status === "refunded" ? "refunded" : "failed";
|
|
490
|
-
await options.orders.update(options.context, order.id, { status });
|
|
590
|
+
const updated = await options.orders.update(options.context, order.id, { status });
|
|
591
|
+
if (status === "paid" && options.onOrderPaid) await options.onOrderPaid(updated);
|
|
491
592
|
}
|
|
492
593
|
return;
|
|
493
594
|
}
|
|
494
595
|
case "order.updated": {
|
|
495
596
|
const order = await findOneByField(options.orders, options.context, "providerOrderRef", event.providerOrderRef);
|
|
496
|
-
if (order)
|
|
597
|
+
if (order) {
|
|
598
|
+
const updated = await options.orders.update(options.context, order.id, { status: event.status });
|
|
599
|
+
if (event.status === "paid" && options.onOrderPaid) await options.onOrderPaid(updated);
|
|
600
|
+
}
|
|
497
601
|
return;
|
|
498
602
|
}
|
|
499
603
|
case "subscription.updated": {
|
|
@@ -549,6 +653,6 @@ function createWebhookHandler(options) {
|
|
|
549
653
|
};
|
|
550
654
|
}
|
|
551
655
|
//#endregion
|
|
552
|
-
export { CadmeaPaymentError, createCheckoutHandler, createWebhookHandler, ecommercePlugin };
|
|
656
|
+
export { CadmeaPaymentError, createCheckoutHandler, createFulfillmentOrder, createFulfillmentWebhookHandler, createWebhookHandler, ecommercePlugin };
|
|
553
657
|
|
|
554
658
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["isUniqueConstraintError"],"sources":["../src/errors.ts","../src/checkout.ts","../src/collections.ts","../src/fulfillment.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 // Which FulfillmentProvider is shipping this order — independent of\n // `provider` (who charged the card); see types.ts's doc comment on\n // FulfillmentProvider for why these are separate axes.\n fulfillmentProvider: { type: \"text\" },\n // The fulfillment provider's own order identifier — the correlation\n // key createFulfillmentWebhookHandler dispatches inbound shipment\n // webhooks against, mirroring providerOrderRef's role for payments.\n fulfillmentProviderRef: { type: \"text\" },\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 {\n FulfillmentProvider,\n NormalizedFulfillmentWebhookEvent,\n} 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 CreateFulfillmentOrderOptions<TContext> {\n provider: FulfillmentProvider;\n orders: AnyLocalApi<TContext>;\n context: TContext;\n}\n\n/**\n * The order-paid hook implementation: submits an already-paid order's line\n * items to a `FulfillmentProvider` and persists the resulting\n * `fulfillmentProvider`/`fulfillmentProviderRef`/`fulfillmentStatus` back\n * onto the order row. Wire it as `WebhookHandlerOptions.onOrderPaid` on the\n * *payment* provider's webhook handler:\n *\n * ```ts\n * createWebhookHandler({\n * provider: stripeProvider,\n * orders, payments, webhookEvents, secret, context,\n * onOrderPaid: (order) =>\n * createFulfillmentOrder(order, { provider: printfulProvider, orders, context }),\n * });\n * ```\n *\n * Digital-goods-only stores simply never wire this — `fulfillmentProvider`\n * is plugin-optional, not a hard dependency of `ecommercePlugin`.\n */\nexport async function createFulfillmentOrder<TContext>(\n order: Record<string, unknown>,\n options: CreateFulfillmentOrderOptions<TContext>,\n): Promise<void> {\n const lineItems = (order.lineItems ?? []) as Array<{\n catalogRef?: string;\n quantity: number;\n }>;\n const shippingAddress =\n (order.shippingAddress as Record<string, string | undefined>) ?? {};\n\n const result = await options.provider.createFulfillmentOrder({\n orderId: order.id as number,\n lineItems: lineItems\n .filter((item): item is { catalogRef: string; quantity: number } =>\n Boolean(item.catalogRef),\n )\n .map((item) => ({\n catalogRef: item.catalogRef,\n quantity: item.quantity,\n })),\n shippingAddress,\n customerEmail: order.guestEmail as string | undefined,\n });\n\n await options.orders.update(options.context, order.id as number, {\n fulfillmentProvider: options.provider.name,\n fulfillmentProviderRef: result.providerFulfillmentRef,\n fulfillmentStatus: result.status,\n });\n}\n\nexport interface FulfillmentWebhookHandlerOptions<TContext> {\n provider: FulfillmentProvider;\n orders: AnyLocalApi<TContext>;\n webhookEvents: AnyLocalApi<TContext>;\n secret: string;\n /** See `WebhookHandlerOptions.context`'s identical note in webhook.ts. */\n context: TContext;\n}\n\nasync function findOrderByFulfillmentRef<TContext>(\n orders: AnyLocalApi<TContext>,\n context: TContext,\n providerFulfillmentRef: string,\n): Promise<Record<string, unknown> | undefined> {\n // Same in-memory-filter tradeoff as webhook.ts's findOneByField — revisit\n // with an indexed lookup only once volume makes it a measured problem.\n const rows = (await orders.find(context)) as Array<Record<string, unknown>>;\n return rows.find(\n (row) => row.fulfillmentProviderRef === providerFulfillmentRef,\n );\n}\n\nfunction isUniqueConstraintError(error: unknown): boolean {\n return (\n error instanceof Error &&\n error.message.includes(\"Unique constraint violated\")\n );\n}\n\nasync function dispatchFulfillmentEvent<TContext>(\n event: NormalizedFulfillmentWebhookEvent,\n options: FulfillmentWebhookHandlerOptions<TContext>,\n): Promise<void> {\n if (event.kind === \"unhandled\") return;\n\n const order = await findOrderByFulfillmentRef(\n options.orders,\n options.context,\n event.providerFulfillmentRef,\n );\n if (!order) return;\n\n await options.orders.update(options.context, order.id as number, {\n fulfillmentStatus: event.status,\n trackingNumber: event.trackingNumber,\n trackingCarrier: event.trackingCarrier,\n trackingUrl: event.trackingUrl,\n });\n}\n\n/**\n * Returns a Hono handler implementing inbound fulfillment-webhook handling\n * (shipment created/delivered/failed) against a `FulfillmentProvider` —\n * verify signature → dedup via `webhook_events` (shared with the payment\n * webhook handler; `eventId` is provider-specific so cross-provider\n * collisions aren't a concern) → look up the order by\n * `fulfillmentProviderRef` → update shipment status/tracking. Mirrors\n * `createWebhookHandler`'s structure exactly; kept as a separate function\n * rather than a generic merge of the two because the payment and\n * fulfillment event vocabularies, dedup keys, and target collections don't\n * overlap enough to share logic without branching on provider kind.\n */\nexport function createFulfillmentWebhookHandler<TContext>(\n options: FulfillmentWebhookHandlerOptions<TContext>,\n) {\n return async (c: Context): Promise<Response> => {\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 });\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 return c.json({ ok: true, duplicate: true }, 200);\n }\n throw error;\n }\n\n try {\n await dispatchFulfillmentEvent(event, options);\n } catch (error) {\n console.error(\n \"[cadmea-plugin-ecommerce] fulfillment webhook dispatch failed\",\n error,\n );\n }\n\n return c.json({ ok: true }, 200);\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 * Fires once, right after an order's status is written as \"paid\" by\n * this handler — the order-paid hook point a `FulfillmentProvider`\n * integration (e.g. `@thebes/cadmea-plugin-printful`) wires into to\n * submit the order for shipment. Receives the just-updated order row.\n * Errors thrown here are caught the same way dispatch-handler errors\n * are below — logged, never surfaced as a failure to the payment\n * provider, since the charge already succeeded and the provider must\n * not retry the whole webhook over a fulfillment-side problem.\n */\n onOrderPaid?: (order: Record<string, unknown>) => Promise<void>;\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 const updated = await options.orders.update(\n options.context,\n order.id as number,\n { status },\n );\n if (status === \"paid\" && options.onOrderPaid) {\n await options.onOrderPaid(updated);\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 const updated = await options.orders.update(\n options.context,\n order.id as number,\n { status: event.status },\n );\n if (event.status === \"paid\" && options.onOrderPaid) {\n await options.onOrderPaid(updated);\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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thebes/cadmea-plugin-ecommerce",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Provider-agnostic ecommerce core for Cadmea — products/orders/customers/payments collections, the PaymentProvider interface, and checkout/webhook Hono handlers",
|
|
5
5
|
"author": "BowenLabs <hello@bowenlabs.io>",
|
|
6
6
|
"license": "MIT",
|