@open-mercato/checkout 0.6.7 → 0.6.8-develop.6874.1.982d6097d8
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/checkout/__integration__/TC-CHKT-042.spec.js +39 -0
- package/dist/modules/checkout/__integration__/TC-CHKT-042.spec.js.map +7 -0
- package/dist/modules/checkout/__integration__/TC-CHKT-043-concurrent-status-update.spec.js +62 -0
- package/dist/modules/checkout/__integration__/TC-CHKT-043-concurrent-status-update.spec.js.map +7 -0
- package/dist/modules/checkout/api/pay/[slug]/submit/route.js +43 -28
- package/dist/modules/checkout/api/pay/[slug]/submit/route.js.map +2 -2
- package/dist/modules/checkout/api/transactions/route.js +23 -6
- package/dist/modules/checkout/api/transactions/route.js.map +2 -2
- package/dist/modules/checkout/backend/checkout/pay-links/page.js +2 -1
- package/dist/modules/checkout/backend/checkout/pay-links/page.js.map +2 -2
- package/dist/modules/checkout/backend/checkout/templates/page.js +2 -1
- package/dist/modules/checkout/backend/checkout/templates/page.js.map +2 -2
- package/dist/modules/checkout/backend/checkout/transactions/page.js +2 -1
- package/dist/modules/checkout/backend/checkout/transactions/page.js.map +2 -2
- package/dist/modules/checkout/commands/links.js +1 -1
- package/dist/modules/checkout/commands/links.js.map +2 -2
- package/dist/modules/checkout/commands/transactions.js +45 -13
- package/dist/modules/checkout/commands/transactions.js.map +2 -2
- package/dist/modules/checkout/components/CustomerFieldsEditor.js +2 -2
- package/dist/modules/checkout/components/CustomerFieldsEditor.js.map +2 -2
- package/dist/modules/checkout/components/GatewaySettingsFields.js +2 -2
- package/dist/modules/checkout/components/GatewaySettingsFields.js.map +2 -2
- package/dist/modules/checkout/components/LinkTemplateForm.js +8 -8
- package/dist/modules/checkout/components/LinkTemplateForm.js.map +2 -2
- package/dist/modules/checkout/components/LogoUploadField.js +1 -1
- package/dist/modules/checkout/components/LogoUploadField.js.map +2 -2
- package/dist/modules/checkout/components/PayPage.js +39 -38
- package/dist/modules/checkout/components/PayPage.js.map +2 -2
- package/dist/modules/checkout/extension-points.js +68 -0
- package/dist/modules/checkout/extension-points.js.map +7 -0
- package/dist/modules/checkout/lib/transaction-status-machine.js +31 -0
- package/dist/modules/checkout/lib/transaction-status-machine.js.map +7 -0
- package/dist/modules/checkout/search.js +44 -6
- package/dist/modules/checkout/search.js.map +2 -2
- package/dist/modules/checkout/setup.js +28 -0
- package/dist/modules/checkout/setup.js.map +2 -2
- package/dist/modules/checkout/subscribers/search-reindex-transaction-created.js +32 -0
- package/dist/modules/checkout/subscribers/search-reindex-transaction-created.js.map +7 -0
- package/dist/modules/checkout/workers/transaction-expiry.worker.js +9 -2
- package/dist/modules/checkout/workers/transaction-expiry.worker.js.map +2 -2
- package/package.json +10 -9
- package/src/modules/checkout/__integration__/TC-CHKT-042.spec.ts +41 -0
- package/src/modules/checkout/__integration__/TC-CHKT-043-concurrent-status-update.spec.ts +112 -0
- package/src/modules/checkout/__tests__/search.test.ts +21 -0
- package/src/modules/checkout/__tests__/setup-schedules.test.ts +99 -0
- package/src/modules/checkout/api/pay/[slug]/submit/__tests__/route.test.ts +77 -0
- package/src/modules/checkout/api/pay/[slug]/submit/route.ts +58 -28
- package/src/modules/checkout/api/transactions/__tests__/route.test.ts +113 -0
- package/src/modules/checkout/api/transactions/route.ts +30 -6
- package/src/modules/checkout/backend/checkout/pay-links/page.tsx +2 -1
- package/src/modules/checkout/backend/checkout/templates/page.tsx +2 -1
- package/src/modules/checkout/backend/checkout/transactions/page.tsx +2 -1
- package/src/modules/checkout/commands/__tests__/update-transaction-status.test.ts +363 -0
- package/src/modules/checkout/commands/links.ts +5 -1
- package/src/modules/checkout/commands/transactions.ts +68 -13
- package/src/modules/checkout/components/CustomerFieldsEditor.tsx +2 -2
- package/src/modules/checkout/components/GatewaySettingsFields.tsx +2 -2
- package/src/modules/checkout/components/LinkTemplateForm.tsx +8 -8
- package/src/modules/checkout/components/LogoUploadField.tsx +1 -1
- package/src/modules/checkout/components/PayPage.tsx +38 -37
- package/src/modules/checkout/extension-points.ts +67 -0
- package/src/modules/checkout/i18n/de.json +1 -0
- package/src/modules/checkout/i18n/en.json +1 -0
- package/src/modules/checkout/i18n/es.json +1 -0
- package/src/modules/checkout/i18n/ko.json +463 -0
- package/src/modules/checkout/i18n/pl.json +1 -0
- package/src/modules/checkout/lib/__tests__/transaction-status-machine.test.ts +152 -0
- package/src/modules/checkout/lib/transaction-status-machine.ts +71 -0
- package/src/modules/checkout/search.ts +44 -6
- package/src/modules/checkout/setup.ts +51 -0
- package/src/modules/checkout/subscribers/__tests__/search-reindex-transaction-created.test.ts +29 -0
- package/src/modules/checkout/subscribers/search-reindex-transaction-created.ts +39 -0
- package/src/modules/checkout/workers/__tests__/transaction-expiry.worker.test.ts +124 -0
- package/src/modules/checkout/workers/transaction-expiry.worker.ts +11 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
componentExtensionHost,
|
|
3
|
+
dataTableExtensionHost,
|
|
4
|
+
defineModuleExtensionPoints,
|
|
5
|
+
injectionExtensionHost
|
|
6
|
+
} from "@open-mercato/shared/modules/widgets/extension-points";
|
|
7
|
+
const payPageSpot = (spotId) => injectionExtensionHost({
|
|
8
|
+
family: "detail",
|
|
9
|
+
spotId,
|
|
10
|
+
supported: ["render-widget"],
|
|
11
|
+
contextContract: "checkout.pay-page.v1",
|
|
12
|
+
source: "components/PayPage.tsx"
|
|
13
|
+
});
|
|
14
|
+
const payPageComponent = (componentId) => componentExtensionHost({
|
|
15
|
+
componentId,
|
|
16
|
+
propsContract: "checkout.pay-page.v1",
|
|
17
|
+
source: "components/PayPage.tsx"
|
|
18
|
+
});
|
|
19
|
+
const extensionPoints = defineModuleExtensionPoints({
|
|
20
|
+
moduleId: "checkout",
|
|
21
|
+
hosts: {
|
|
22
|
+
linksTable: dataTableExtensionHost({ tableId: "checkout-links", source: "backend/checkout/pay-links/page.tsx" }),
|
|
23
|
+
templatesTable: dataTableExtensionHost({ tableId: "checkout-templates", source: "backend/checkout/templates/page.tsx" }),
|
|
24
|
+
transactionsTable: dataTableExtensionHost({ tableId: "checkout-transactions", source: "backend/checkout/transactions/page.tsx" }),
|
|
25
|
+
gatewayWidgetBefore: payPageSpot("checkout.pay-page:gateway-widget:before"),
|
|
26
|
+
gatewayRendererBefore: payPageSpot("checkout.pay-page:gateway-widget:renderer:before"),
|
|
27
|
+
gatewayRendererAfter: payPageSpot("checkout.pay-page:gateway-widget:renderer:after"),
|
|
28
|
+
gatewayActionsBefore: payPageSpot("checkout.pay-page:gateway-widget:actions:before"),
|
|
29
|
+
gatewayActionsAfter: payPageSpot("checkout.pay-page:gateway-widget:actions:after"),
|
|
30
|
+
gatewayWidgetAfter: payPageSpot("checkout.pay-page:gateway-widget:after"),
|
|
31
|
+
headerBefore: payPageSpot("checkout.pay-page:header:before"),
|
|
32
|
+
headerAfter: payPageSpot("checkout.pay-page:header:after"),
|
|
33
|
+
descriptionAfter: payPageSpot("checkout.pay-page:description:after"),
|
|
34
|
+
customerFieldsBefore: payPageSpot("checkout.pay-page:customer-fields:before"),
|
|
35
|
+
customerFieldsAfter: payPageSpot("checkout.pay-page:customer-fields:after"),
|
|
36
|
+
pricingBefore: payPageSpot("checkout.pay-page:pricing:before"),
|
|
37
|
+
pricingAfter: payPageSpot("checkout.pay-page:pricing:after"),
|
|
38
|
+
summaryBefore: payPageSpot("checkout.pay-page:summary:before"),
|
|
39
|
+
summaryAfter: payPageSpot("checkout.pay-page:summary:after"),
|
|
40
|
+
legalConsentBefore: payPageSpot("checkout.pay-page:legal-consent:before"),
|
|
41
|
+
legalConsentAfter: payPageSpot("checkout.pay-page:legal-consent:after"),
|
|
42
|
+
submitBefore: payPageSpot("checkout.pay-page:submit:before"),
|
|
43
|
+
submitAfter: payPageSpot("checkout.pay-page:submit:after"),
|
|
44
|
+
helpBefore: payPageSpot("checkout.pay-page:help:before"),
|
|
45
|
+
helpAfter: payPageSpot("checkout.pay-page:help:after"),
|
|
46
|
+
paymentBefore: payPageSpot("checkout.pay-page:payment:before"),
|
|
47
|
+
paymentAfter: payPageSpot("checkout.pay-page:payment:after"),
|
|
48
|
+
footerBefore: payPageSpot("checkout.pay-page:footer:before"),
|
|
49
|
+
footerAfter: payPageSpot("checkout.pay-page:footer:after"),
|
|
50
|
+
payPage: payPageComponent("page:checkout.pay-page"),
|
|
51
|
+
header: payPageComponent("section:checkout.pay-page.header"),
|
|
52
|
+
description: payPageComponent("section:checkout.pay-page.description"),
|
|
53
|
+
summary: payPageComponent("section:checkout.pay-page.summary"),
|
|
54
|
+
pricing: payPageComponent("section:checkout.pay-page.pricing"),
|
|
55
|
+
payment: payPageComponent("section:checkout.pay-page.payment"),
|
|
56
|
+
customerForm: payPageComponent("section:checkout.pay-page.customer-form"),
|
|
57
|
+
legalConsent: payPageComponent("section:checkout.pay-page.legal-consent"),
|
|
58
|
+
gatewayForm: payPageComponent("section:checkout.pay-page.gateway-form"),
|
|
59
|
+
help: payPageComponent("section:checkout.pay-page.help"),
|
|
60
|
+
footer: payPageComponent("section:checkout.pay-page.footer")
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
var extension_points_default = extensionPoints;
|
|
64
|
+
export {
|
|
65
|
+
extension_points_default as default,
|
|
66
|
+
extensionPoints
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=extension-points.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/checkout/extension-points.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n componentExtensionHost,\n dataTableExtensionHost,\n defineModuleExtensionPoints,\n injectionExtensionHost,\n} from '@open-mercato/shared/modules/widgets/extension-points'\n\nconst payPageSpot = (spotId: string) => injectionExtensionHost({\n family: 'detail',\n spotId,\n supported: ['render-widget'],\n contextContract: 'checkout.pay-page.v1',\n source: 'components/PayPage.tsx',\n})\n\nconst payPageComponent = (componentId: string) => componentExtensionHost({\n componentId,\n propsContract: 'checkout.pay-page.v1',\n source: 'components/PayPage.tsx',\n})\n\nexport const extensionPoints = defineModuleExtensionPoints({\n moduleId: 'checkout',\n hosts: {\n linksTable: dataTableExtensionHost({ tableId: 'checkout-links', source: 'backend/checkout/pay-links/page.tsx' }),\n templatesTable: dataTableExtensionHost({ tableId: 'checkout-templates', source: 'backend/checkout/templates/page.tsx' }),\n transactionsTable: dataTableExtensionHost({ tableId: 'checkout-transactions', source: 'backend/checkout/transactions/page.tsx' }),\n gatewayWidgetBefore: payPageSpot('checkout.pay-page:gateway-widget:before'),\n gatewayRendererBefore: payPageSpot('checkout.pay-page:gateway-widget:renderer:before'),\n gatewayRendererAfter: payPageSpot('checkout.pay-page:gateway-widget:renderer:after'),\n gatewayActionsBefore: payPageSpot('checkout.pay-page:gateway-widget:actions:before'),\n gatewayActionsAfter: payPageSpot('checkout.pay-page:gateway-widget:actions:after'),\n gatewayWidgetAfter: payPageSpot('checkout.pay-page:gateway-widget:after'),\n headerBefore: payPageSpot('checkout.pay-page:header:before'),\n headerAfter: payPageSpot('checkout.pay-page:header:after'),\n descriptionAfter: payPageSpot('checkout.pay-page:description:after'),\n customerFieldsBefore: payPageSpot('checkout.pay-page:customer-fields:before'),\n customerFieldsAfter: payPageSpot('checkout.pay-page:customer-fields:after'),\n pricingBefore: payPageSpot('checkout.pay-page:pricing:before'),\n pricingAfter: payPageSpot('checkout.pay-page:pricing:after'),\n summaryBefore: payPageSpot('checkout.pay-page:summary:before'),\n summaryAfter: payPageSpot('checkout.pay-page:summary:after'),\n legalConsentBefore: payPageSpot('checkout.pay-page:legal-consent:before'),\n legalConsentAfter: payPageSpot('checkout.pay-page:legal-consent:after'),\n submitBefore: payPageSpot('checkout.pay-page:submit:before'),\n submitAfter: payPageSpot('checkout.pay-page:submit:after'),\n helpBefore: payPageSpot('checkout.pay-page:help:before'),\n helpAfter: payPageSpot('checkout.pay-page:help:after'),\n paymentBefore: payPageSpot('checkout.pay-page:payment:before'),\n paymentAfter: payPageSpot('checkout.pay-page:payment:after'),\n footerBefore: payPageSpot('checkout.pay-page:footer:before'),\n footerAfter: payPageSpot('checkout.pay-page:footer:after'),\n payPage: payPageComponent('page:checkout.pay-page'),\n header: payPageComponent('section:checkout.pay-page.header'),\n description: payPageComponent('section:checkout.pay-page.description'),\n summary: payPageComponent('section:checkout.pay-page.summary'),\n pricing: payPageComponent('section:checkout.pay-page.pricing'),\n payment: payPageComponent('section:checkout.pay-page.payment'),\n customerForm: payPageComponent('section:checkout.pay-page.customer-form'),\n legalConsent: payPageComponent('section:checkout.pay-page.legal-consent'),\n gatewayForm: payPageComponent('section:checkout.pay-page.gateway-form'),\n help: payPageComponent('section:checkout.pay-page.help'),\n footer: payPageComponent('section:checkout.pay-page.footer'),\n },\n})\n\nexport default extensionPoints\n"],
|
|
5
|
+
"mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,cAAc,CAAC,WAAmB,uBAAuB;AAAA,EAC7D,QAAQ;AAAA,EACR;AAAA,EACA,WAAW,CAAC,eAAe;AAAA,EAC3B,iBAAiB;AAAA,EACjB,QAAQ;AACV,CAAC;AAED,MAAM,mBAAmB,CAAC,gBAAwB,uBAAuB;AAAA,EACvE;AAAA,EACA,eAAe;AAAA,EACf,QAAQ;AACV,CAAC;AAEM,MAAM,kBAAkB,4BAA4B;AAAA,EACzD,UAAU;AAAA,EACV,OAAO;AAAA,IACL,YAAY,uBAAuB,EAAE,SAAS,kBAAkB,QAAQ,sCAAsC,CAAC;AAAA,IAC/G,gBAAgB,uBAAuB,EAAE,SAAS,sBAAsB,QAAQ,sCAAsC,CAAC;AAAA,IACvH,mBAAmB,uBAAuB,EAAE,SAAS,yBAAyB,QAAQ,yCAAyC,CAAC;AAAA,IAChI,qBAAqB,YAAY,yCAAyC;AAAA,IAC1E,uBAAuB,YAAY,kDAAkD;AAAA,IACrF,sBAAsB,YAAY,iDAAiD;AAAA,IACnF,sBAAsB,YAAY,iDAAiD;AAAA,IACnF,qBAAqB,YAAY,gDAAgD;AAAA,IACjF,oBAAoB,YAAY,wCAAwC;AAAA,IACxE,cAAc,YAAY,iCAAiC;AAAA,IAC3D,aAAa,YAAY,gCAAgC;AAAA,IACzD,kBAAkB,YAAY,qCAAqC;AAAA,IACnE,sBAAsB,YAAY,0CAA0C;AAAA,IAC5E,qBAAqB,YAAY,yCAAyC;AAAA,IAC1E,eAAe,YAAY,kCAAkC;AAAA,IAC7D,cAAc,YAAY,iCAAiC;AAAA,IAC3D,eAAe,YAAY,kCAAkC;AAAA,IAC7D,cAAc,YAAY,iCAAiC;AAAA,IAC3D,oBAAoB,YAAY,wCAAwC;AAAA,IACxE,mBAAmB,YAAY,uCAAuC;AAAA,IACtE,cAAc,YAAY,iCAAiC;AAAA,IAC3D,aAAa,YAAY,gCAAgC;AAAA,IACzD,YAAY,YAAY,+BAA+B;AAAA,IACvD,WAAW,YAAY,8BAA8B;AAAA,IACrD,eAAe,YAAY,kCAAkC;AAAA,IAC7D,cAAc,YAAY,iCAAiC;AAAA,IAC3D,cAAc,YAAY,iCAAiC;AAAA,IAC3D,aAAa,YAAY,gCAAgC;AAAA,IACzD,SAAS,iBAAiB,wBAAwB;AAAA,IAClD,QAAQ,iBAAiB,kCAAkC;AAAA,IAC3D,aAAa,iBAAiB,uCAAuC;AAAA,IACrE,SAAS,iBAAiB,mCAAmC;AAAA,IAC7D,SAAS,iBAAiB,mCAAmC;AAAA,IAC7D,SAAS,iBAAiB,mCAAmC;AAAA,IAC7D,cAAc,iBAAiB,yCAAyC;AAAA,IACxE,cAAc,iBAAiB,yCAAyC;AAAA,IACxE,aAAa,iBAAiB,wCAAwC;AAAA,IACtE,MAAM,iBAAiB,gCAAgC;AAAA,IACvD,QAAQ,iBAAiB,kCAAkC;AAAA,EAC7D;AACF,CAAC;AAED,IAAO,2BAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
2
|
+
const VALID_CHECKOUT_TRANSITIONS = {
|
|
3
|
+
pending: ["processing", "completed", "failed", "cancelled", "expired"],
|
|
4
|
+
processing: ["completed", "failed", "cancelled", "expired"],
|
|
5
|
+
// Terminal states — no transitions out
|
|
6
|
+
completed: [],
|
|
7
|
+
failed: [],
|
|
8
|
+
cancelled: [],
|
|
9
|
+
expired: []
|
|
10
|
+
};
|
|
11
|
+
function isValidCheckoutStatusTransition(from, to) {
|
|
12
|
+
if (from === to) return true;
|
|
13
|
+
const allowed = VALID_CHECKOUT_TRANSITIONS[from];
|
|
14
|
+
if (!allowed) return false;
|
|
15
|
+
return allowed.includes(to);
|
|
16
|
+
}
|
|
17
|
+
function assertValidCheckoutStatusTransition(from, to) {
|
|
18
|
+
if (isValidCheckoutStatusTransition(from, to)) return;
|
|
19
|
+
throw new CrudHttpError(409, {
|
|
20
|
+
error: `[internal] Cannot transition payment from "${from}" to "${to}"`,
|
|
21
|
+
code: "invalid_status_transition",
|
|
22
|
+
currentStatus: from,
|
|
23
|
+
requestedStatus: to
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
VALID_CHECKOUT_TRANSITIONS,
|
|
28
|
+
assertValidCheckoutStatusTransition,
|
|
29
|
+
isValidCheckoutStatusTransition
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=transaction-status-machine.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/checkout/lib/transaction-status-machine.ts"],
|
|
4
|
+
"sourcesContent": ["import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { CheckoutTransaction } from '../data/entities'\n\n/**\n * Allowed status transitions for a CheckoutTransaction.\n *\n * Terminal states (completed, failed, cancelled, expired) have an empty\n * allowed-set, meaning NO transition out of them is permitted. This prevents\n * the race condition where the submit route's late `updateStatus('processing')`\n * call overwrites a `'completed'` status that the gateway webhook set first.\n *\n * Allowed path:\n * pending \u2500\u2500\u25BA processing (transaction created, payment session initiated)\n * pending \u2500\u2500\u25BA completed (captured webhook arrives before processing state)\n * processing \u2500\u2500\u25BA completed (webhook: payment captured)\n * processing \u2500\u2500\u25BA failed (webhook or session error)\n * processing \u2500\u2500\u25BA cancelled (customer or merchant cancellation)\n * processing \u2500\u2500\u25BA expired (expiry worker)\n *\n * Rejected (regression guard):\n * completed \u2500\u2500\u25BA * REJECTED\n * failed \u2500\u2500\u25BA * REJECTED\n * cancelled \u2500\u2500\u25BA * REJECTED\n * expired \u2500\u2500\u25BA * REJECTED\n */\nexport const VALID_CHECKOUT_TRANSITIONS: Record<CheckoutTransaction['status'], CheckoutTransaction['status'][]> = {\n pending: ['processing', 'completed', 'failed', 'cancelled', 'expired'],\n processing: ['completed', 'failed', 'cancelled', 'expired'],\n // Terminal states \u2014 no transitions out\n completed: [],\n failed: [],\n cancelled: [],\n expired: [],\n}\n\n/**\n * Returns `true` when transitioning from `from` to `to` is allowed by the\n * state machine. Same-state transitions are allowed as safe no-ops to support\n * idempotency.\n */\nexport function isValidCheckoutStatusTransition(\n from: CheckoutTransaction['status'],\n to: CheckoutTransaction['status'],\n): boolean {\n if (from === to) return true\n const allowed = VALID_CHECKOUT_TRANSITIONS[from]\n if (!allowed) return false\n return allowed.includes(to)\n}\n\n/**\n * Throws a structured 409 Conflict when the requested status transition is\n * not allowed by the state machine. The error body is compatible with the\n * `conflict` error shape that the frontend handles.\n *\n * This must be called **inside** a database transaction so that the check and\n * the subsequent write are atomic. The atomic `nativeUpdate` WHERE-clause\n * provides the TOCTOU safety net; this function is the human-readable guard.\n */\nexport function assertValidCheckoutStatusTransition(\n from: CheckoutTransaction['status'],\n to: CheckoutTransaction['status'],\n): void {\n if (isValidCheckoutStatusTransition(from, to)) return\n throw new CrudHttpError(409, {\n error: `[internal] Cannot transition payment from \"${from}\" to \"${to}\"`,\n code: 'invalid_status_transition',\n currentStatus: from,\n requestedStatus: to,\n })\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,qBAAqB;AAyBvB,MAAM,6BAAqG;AAAA,EAChH,SAAS,CAAC,cAAc,aAAa,UAAU,aAAa,SAAS;AAAA,EACrE,YAAY,CAAC,aAAa,UAAU,aAAa,SAAS;AAAA;AAAA,EAE1D,WAAW,CAAC;AAAA,EACZ,QAAQ,CAAC;AAAA,EACT,WAAW,CAAC;AAAA,EACZ,SAAS,CAAC;AACZ;AAOO,SAAS,gCACd,MACA,IACS;AACT,MAAI,SAAS,GAAI,QAAO;AACxB,QAAM,UAAU,2BAA2B,IAAI;AAC/C,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,QAAQ,SAAS,EAAE;AAC5B;AAWO,SAAS,oCACd,MACA,IACM;AACN,MAAI,gCAAgC,MAAM,EAAE,EAAG;AAC/C,QAAM,IAAI,cAAc,KAAK;AAAA,IAC3B,OAAO,8CAA8C,IAAI,SAAS,EAAE;AAAA,IACpE,MAAM;AAAA,IACN,eAAe;AAAA,IACf,iBAAiB;AAAA,EACnB,CAAC;AACH;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
1
2
|
import { CHECKOUT_ENTITY_IDS } from "./lib/constants.js";
|
|
2
3
|
function asSearchText(value) {
|
|
3
4
|
return typeof value === "string" ? value : "";
|
|
@@ -31,15 +32,52 @@ const searchConfig = {
|
|
|
31
32
|
searchable: ["name", "title"],
|
|
32
33
|
excluded: ["passwordHash", "gatewaySettings", "customerFieldsSchema"]
|
|
33
34
|
},
|
|
35
|
+
buildSource: async (ctx) => {
|
|
36
|
+
const { t } = await resolveTranslations();
|
|
37
|
+
const linkTemplate = t("checkout.search.subtitle.linkTemplate", "Link Template");
|
|
38
|
+
return {
|
|
39
|
+
text: [`${asSearchText(ctx.record.name)}: ${asSearchText(ctx.record.title)}`],
|
|
40
|
+
presenter: { title: asSearchText(ctx.record.name), subtitle: linkTemplate },
|
|
41
|
+
checksumSource: { record: ctx.record, customFields: ctx.customFields }
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
formatResult: async (ctx) => {
|
|
45
|
+
const { t } = await resolveTranslations();
|
|
46
|
+
return {
|
|
47
|
+
title: asSearchText(ctx.record.name),
|
|
48
|
+
subtitle: t("checkout.search.subtitle.linkTemplate", "Link Template"),
|
|
49
|
+
icon: "lucide:file-text"
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
entityId: CHECKOUT_ENTITY_IDS.transaction,
|
|
55
|
+
enabled: true,
|
|
56
|
+
priority: 6,
|
|
57
|
+
fieldPolicy: {
|
|
58
|
+
searchable: ["email", "firstName", "lastName"]
|
|
59
|
+
},
|
|
34
60
|
buildSource: async (ctx) => ({
|
|
35
|
-
text: [
|
|
36
|
-
|
|
37
|
-
|
|
61
|
+
text: [
|
|
62
|
+
asSearchText(ctx.record.email),
|
|
63
|
+
asSearchText(ctx.record.firstName),
|
|
64
|
+
asSearchText(ctx.record.lastName)
|
|
65
|
+
],
|
|
66
|
+
fields: {
|
|
67
|
+
email: ctx.record.email,
|
|
68
|
+
first_name: ctx.record.firstName,
|
|
69
|
+
last_name: ctx.record.lastName
|
|
70
|
+
},
|
|
71
|
+
presenter: {
|
|
72
|
+
title: asSearchText(ctx.record.email),
|
|
73
|
+
subtitle: `${asSearchText(ctx.record.firstName)} ${asSearchText(ctx.record.lastName)}`.trim()
|
|
74
|
+
},
|
|
75
|
+
checksumSource: { record: ctx.record }
|
|
38
76
|
}),
|
|
39
77
|
formatResult: async (ctx) => ({
|
|
40
|
-
title: asSearchText(ctx.record.
|
|
41
|
-
subtitle:
|
|
42
|
-
icon: "lucide:
|
|
78
|
+
title: asSearchText(ctx.record.email),
|
|
79
|
+
subtitle: `${asSearchText(ctx.record.firstName)} ${asSearchText(ctx.record.lastName)}`.trim(),
|
|
80
|
+
icon: "lucide:receipt"
|
|
43
81
|
})
|
|
44
82
|
}
|
|
45
83
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/checkout/search.ts"],
|
|
4
|
-
"sourcesContent": ["import type { SearchModuleConfig } from '@open-mercato/shared/modules/search'\nimport { CHECKOUT_ENTITY_IDS } from './lib/constants'\n\nfunction asSearchText(value: unknown): string {\n return typeof value === 'string' ? value : ''\n}\n\nexport const searchConfig: SearchModuleConfig = {\n entities: [\n {\n entityId: CHECKOUT_ENTITY_IDS.link,\n enabled: true,\n priority: 10,\n fieldPolicy: {\n searchable: ['name', 'title', 'slug'],\n excluded: ['passwordHash', 'gatewaySettings', 'customerFieldsSchema'],\n },\n buildSource: async (ctx) => ({\n text: [`${asSearchText(ctx.record.name)}: ${asSearchText(ctx.record.title)} (${asSearchText(ctx.record.slug)})`],\n presenter: { title: asSearchText(ctx.record.name), subtitle: asSearchText(ctx.record.slug) },\n checksumSource: { record: ctx.record, customFields: ctx.customFields },\n }),\n formatResult: async (ctx) => ({\n title: asSearchText(ctx.record.name),\n subtitle: `/pay/${asSearchText(ctx.record.slug)}`,\n icon: 'lucide:link',\n }),\n },\n {\n entityId: CHECKOUT_ENTITY_IDS.template,\n enabled: true,\n priority: 8,\n fieldPolicy: {\n searchable: ['name', 'title'],\n excluded: ['passwordHash', 'gatewaySettings', 'customerFieldsSchema'],\n },\n buildSource: async (ctx) => ({\n
|
|
5
|
-
"mappings": "AACA,SAAS,2BAA2B;AAEpC,SAAS,aAAa,OAAwB;AAC5C,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;AAEO,MAAM,eAAmC;AAAA,EAC9C,UAAU;AAAA,IACR;AAAA,MACE,UAAU,oBAAoB;AAAA,MAC9B,SAAS;AAAA,MACT,UAAU;AAAA,MACV,aAAa;AAAA,QACX,YAAY,CAAC,QAAQ,SAAS,MAAM;AAAA,QACpC,UAAU,CAAC,gBAAgB,mBAAmB,sBAAsB;AAAA,MACtE;AAAA,MACA,aAAa,OAAO,SAAS;AAAA,QAC3B,MAAM,CAAC,GAAG,aAAa,IAAI,OAAO,IAAI,CAAC,KAAK,aAAa,IAAI,OAAO,KAAK,CAAC,KAAK,aAAa,IAAI,OAAO,IAAI,CAAC,GAAG;AAAA,QAC/G,WAAW,EAAE,OAAO,aAAa,IAAI,OAAO,IAAI,GAAG,UAAU,aAAa,IAAI,OAAO,IAAI,EAAE;AAAA,QAC3F,gBAAgB,EAAE,QAAQ,IAAI,QAAQ,cAAc,IAAI,aAAa;AAAA,MACvE;AAAA,MACA,cAAc,OAAO,SAAS;AAAA,QAC5B,OAAO,aAAa,IAAI,OAAO,IAAI;AAAA,QACnC,UAAU,QAAQ,aAAa,IAAI,OAAO,IAAI,CAAC;AAAA,QAC/C,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU,oBAAoB;AAAA,MAC9B,SAAS;AAAA,MACT,UAAU;AAAA,MACV,aAAa;AAAA,QACX,YAAY,CAAC,QAAQ,OAAO;AAAA,QAC5B,UAAU,CAAC,gBAAgB,mBAAmB,sBAAsB;AAAA,MACtE;AAAA,MACA,aAAa,OAAO,
|
|
4
|
+
"sourcesContent": ["import type { SearchModuleConfig } from '@open-mercato/shared/modules/search'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CHECKOUT_ENTITY_IDS } from './lib/constants'\n\nfunction asSearchText(value: unknown): string {\n return typeof value === 'string' ? value : ''\n}\n\nexport const searchConfig: SearchModuleConfig = {\n entities: [\n {\n entityId: CHECKOUT_ENTITY_IDS.link,\n enabled: true,\n priority: 10,\n fieldPolicy: {\n searchable: ['name', 'title', 'slug'],\n excluded: ['passwordHash', 'gatewaySettings', 'customerFieldsSchema'],\n },\n buildSource: async (ctx) => ({\n text: [`${asSearchText(ctx.record.name)}: ${asSearchText(ctx.record.title)} (${asSearchText(ctx.record.slug)})`],\n presenter: { title: asSearchText(ctx.record.name), subtitle: asSearchText(ctx.record.slug) },\n checksumSource: { record: ctx.record, customFields: ctx.customFields },\n }),\n formatResult: async (ctx) => ({\n title: asSearchText(ctx.record.name),\n subtitle: `/pay/${asSearchText(ctx.record.slug)}`,\n icon: 'lucide:link',\n }),\n },\n {\n entityId: CHECKOUT_ENTITY_IDS.template,\n enabled: true,\n priority: 8,\n fieldPolicy: {\n searchable: ['name', 'title'],\n excluded: ['passwordHash', 'gatewaySettings', 'customerFieldsSchema'],\n },\n buildSource: async (ctx) => {\n const { t } = await resolveTranslations()\n const linkTemplate = t('checkout.search.subtitle.linkTemplate', 'Link Template')\n return {\n text: [`${asSearchText(ctx.record.name)}: ${asSearchText(ctx.record.title)}`],\n presenter: { title: asSearchText(ctx.record.name), subtitle: linkTemplate },\n checksumSource: { record: ctx.record, customFields: ctx.customFields },\n }\n },\n formatResult: async (ctx) => {\n const { t } = await resolveTranslations()\n return {\n title: asSearchText(ctx.record.name),\n subtitle: t('checkout.search.subtitle.linkTemplate', 'Link Template'),\n icon: 'lucide:file-text',\n }\n },\n },\n {\n entityId: CHECKOUT_ENTITY_IDS.transaction,\n enabled: true,\n priority: 6,\n fieldPolicy: {\n searchable: ['email', 'firstName', 'lastName'],\n },\n buildSource: async (ctx) => ({\n text: [\n asSearchText(ctx.record.email),\n asSearchText(ctx.record.firstName),\n asSearchText(ctx.record.lastName),\n ],\n fields: {\n email: ctx.record.email,\n first_name: ctx.record.firstName,\n last_name: ctx.record.lastName,\n },\n presenter: {\n title: asSearchText(ctx.record.email),\n subtitle: `${asSearchText(ctx.record.firstName)} ${asSearchText(ctx.record.lastName)}`.trim(),\n },\n checksumSource: { record: ctx.record },\n }),\n formatResult: async (ctx) => ({\n title: asSearchText(ctx.record.email),\n subtitle: `${asSearchText(ctx.record.firstName)} ${asSearchText(ctx.record.lastName)}`.trim(),\n icon: 'lucide:receipt',\n }),\n },\n ],\n}\n\nexport const config = searchConfig\nexport default searchConfig\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AAEpC,SAAS,aAAa,OAAwB;AAC5C,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;AAEO,MAAM,eAAmC;AAAA,EAC9C,UAAU;AAAA,IACR;AAAA,MACE,UAAU,oBAAoB;AAAA,MAC9B,SAAS;AAAA,MACT,UAAU;AAAA,MACV,aAAa;AAAA,QACX,YAAY,CAAC,QAAQ,SAAS,MAAM;AAAA,QACpC,UAAU,CAAC,gBAAgB,mBAAmB,sBAAsB;AAAA,MACtE;AAAA,MACA,aAAa,OAAO,SAAS;AAAA,QAC3B,MAAM,CAAC,GAAG,aAAa,IAAI,OAAO,IAAI,CAAC,KAAK,aAAa,IAAI,OAAO,KAAK,CAAC,KAAK,aAAa,IAAI,OAAO,IAAI,CAAC,GAAG;AAAA,QAC/G,WAAW,EAAE,OAAO,aAAa,IAAI,OAAO,IAAI,GAAG,UAAU,aAAa,IAAI,OAAO,IAAI,EAAE;AAAA,QAC3F,gBAAgB,EAAE,QAAQ,IAAI,QAAQ,cAAc,IAAI,aAAa;AAAA,MACvE;AAAA,MACA,cAAc,OAAO,SAAS;AAAA,QAC5B,OAAO,aAAa,IAAI,OAAO,IAAI;AAAA,QACnC,UAAU,QAAQ,aAAa,IAAI,OAAO,IAAI,CAAC;AAAA,QAC/C,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU,oBAAoB;AAAA,MAC9B,SAAS;AAAA,MACT,UAAU;AAAA,MACV,aAAa;AAAA,QACX,YAAY,CAAC,QAAQ,OAAO;AAAA,QAC5B,UAAU,CAAC,gBAAgB,mBAAmB,sBAAsB;AAAA,MACtE;AAAA,MACA,aAAa,OAAO,QAAQ;AAC1B,cAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,cAAM,eAAe,EAAE,yCAAyC,eAAe;AAC/E,eAAO;AAAA,UACL,MAAM,CAAC,GAAG,aAAa,IAAI,OAAO,IAAI,CAAC,KAAK,aAAa,IAAI,OAAO,KAAK,CAAC,EAAE;AAAA,UAC5E,WAAW,EAAE,OAAO,aAAa,IAAI,OAAO,IAAI,GAAG,UAAU,aAAa;AAAA,UAC1E,gBAAgB,EAAE,QAAQ,IAAI,QAAQ,cAAc,IAAI,aAAa;AAAA,QACvE;AAAA,MACF;AAAA,MACA,cAAc,OAAO,QAAQ;AAC3B,cAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,eAAO;AAAA,UACL,OAAO,aAAa,IAAI,OAAO,IAAI;AAAA,UACnC,UAAU,EAAE,yCAAyC,eAAe;AAAA,UACpE,MAAM;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU,oBAAoB;AAAA,MAC9B,SAAS;AAAA,MACT,UAAU;AAAA,MACV,aAAa;AAAA,QACX,YAAY,CAAC,SAAS,aAAa,UAAU;AAAA,MAC/C;AAAA,MACA,aAAa,OAAO,SAAS;AAAA,QAC3B,MAAM;AAAA,UACJ,aAAa,IAAI,OAAO,KAAK;AAAA,UAC7B,aAAa,IAAI,OAAO,SAAS;AAAA,UACjC,aAAa,IAAI,OAAO,QAAQ;AAAA,QAClC;AAAA,QACA,QAAQ;AAAA,UACN,OAAO,IAAI,OAAO;AAAA,UAClB,YAAY,IAAI,OAAO;AAAA,UACvB,WAAW,IAAI,OAAO;AAAA,QACxB;AAAA,QACA,WAAW;AAAA,UACT,OAAO,aAAa,IAAI,OAAO,KAAK;AAAA,UACpC,UAAU,GAAG,aAAa,IAAI,OAAO,SAAS,CAAC,IAAI,aAAa,IAAI,OAAO,QAAQ,CAAC,GAAG,KAAK;AAAA,QAC9F;AAAA,QACA,gBAAgB,EAAE,QAAQ,IAAI,OAAO;AAAA,MACvC;AAAA,MACA,cAAc,OAAO,SAAS;AAAA,QAC5B,OAAO,aAAa,IAAI,OAAO,KAAK;AAAA,QACpC,UAAU,GAAG,aAAa,IAAI,OAAO,SAAS,CAAC,IAAI,aAAa,IAAI,OAAO,QAAQ,CAAC,GAAG,KAAK;AAAA,QAC5F,MAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,SAAS;AACtB,IAAO,iBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,12 +1,40 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { ensureCheckoutFieldsetsAndDefinitions } from "./seed/customFields.js";
|
|
2
3
|
import { seedCheckoutExamples } from "./seed/examples.js";
|
|
4
|
+
import { CHECKOUT_EXPIRY_QUEUE } from "./workers/transaction-expiry.worker.js";
|
|
3
5
|
import { DEFAULT_CHECKOUT_CUSTOMER_FIELDS } from "./lib/defaults.js";
|
|
6
|
+
function stableUuidFromString(input) {
|
|
7
|
+
const bytes = createHash("sha256").update(input).digest().subarray(0, 16);
|
|
8
|
+
bytes[6] = bytes[6] & 15 | 80;
|
|
9
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
10
|
+
const hex = Buffer.from(bytes).toString("hex");
|
|
11
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
12
|
+
}
|
|
4
13
|
const setup = {
|
|
5
14
|
async seedDefaults(ctx) {
|
|
6
15
|
await ensureCheckoutFieldsetsAndDefinitions(ctx.em, {
|
|
7
16
|
tenantId: ctx.tenantId,
|
|
8
17
|
organizationId: ctx.organizationId
|
|
9
18
|
});
|
|
19
|
+
const cradle = ctx.container;
|
|
20
|
+
if (typeof cradle.hasRegistration !== "function" || !cradle.hasRegistration("schedulerService")) return;
|
|
21
|
+
const schedulerService = ctx.container.resolve("schedulerService");
|
|
22
|
+
await schedulerService.register({
|
|
23
|
+
id: stableUuidFromString(`checkout:transaction-expiry:${ctx.tenantId}:${ctx.organizationId}`),
|
|
24
|
+
name: "Checkout transaction expiry",
|
|
25
|
+
description: "Marks stale checkout transactions as expired and runs usage limit updates.",
|
|
26
|
+
scopeType: "organization",
|
|
27
|
+
organizationId: ctx.organizationId,
|
|
28
|
+
tenantId: ctx.tenantId,
|
|
29
|
+
scheduleType: "interval",
|
|
30
|
+
scheduleValue: "10m",
|
|
31
|
+
timezone: "UTC",
|
|
32
|
+
targetType: "queue",
|
|
33
|
+
targetQueue: CHECKOUT_EXPIRY_QUEUE,
|
|
34
|
+
sourceType: "module",
|
|
35
|
+
sourceModule: "checkout",
|
|
36
|
+
isEnabled: true
|
|
37
|
+
});
|
|
10
38
|
},
|
|
11
39
|
defaultRoleFeatures: {
|
|
12
40
|
superadmin: ["checkout.*"],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/checkout/setup.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport { ensureCheckoutFieldsetsAndDefinitions } from './seed/customFields'\nimport { seedCheckoutExamples } from './seed/examples'\nexport { DEFAULT_CHECKOUT_CUSTOMER_FIELDS } from './lib/defaults'\n\nexport const setup: ModuleSetupConfig = {\n async seedDefaults(ctx) {\n await ensureCheckoutFieldsetsAndDefinitions(ctx.em, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n },\n\n defaultRoleFeatures: {\n superadmin: ['checkout.*'],\n admin: ['checkout.view', 'checkout.create', 'checkout.edit', 'checkout.delete', 'checkout.viewPii', 'checkout.export'],\n employee: ['checkout.view'],\n },\n\n async seedExamples(ctx) {\n await seedCheckoutExamples(ctx)\n },\n}\n\nexport default setup\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,6CAA6C;AACtD,SAAS,4BAA4B;AACrC,SAAS,wCAAwC;
|
|
4
|
+
"sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport { createHash } from 'node:crypto'\nimport { ensureCheckoutFieldsetsAndDefinitions } from './seed/customFields'\nimport { seedCheckoutExamples } from './seed/examples'\nimport { CHECKOUT_EXPIRY_QUEUE } from './workers/transaction-expiry.worker'\nexport { DEFAULT_CHECKOUT_CUSTOMER_FIELDS } from './lib/defaults'\n\nfunction stableUuidFromString(input: string): string {\n const bytes = createHash('sha256').update(input).digest().subarray(0, 16)\n bytes[6] = (bytes[6] & 0x0f) | 0x50\n bytes[8] = (bytes[8] & 0x3f) | 0x80\n const hex = Buffer.from(bytes).toString('hex')\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`\n}\n\ntype SchedulerServiceLike = {\n register: (registration: {\n id: string\n name: string\n description?: string\n scopeType: 'organization'\n organizationId: string\n tenantId: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue'\n targetQueue: string\n targetPayload?: Record<string, unknown>\n sourceType: 'module'\n sourceModule: string\n isEnabled?: boolean\n }) => Promise<void>\n}\n\nexport const setup: ModuleSetupConfig = {\n async seedDefaults(ctx) {\n await ensureCheckoutFieldsetsAndDefinitions(ctx.em, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n\n const cradle = ctx.container as { hasRegistration?: (name: string) => boolean }\n if (typeof cradle.hasRegistration !== 'function' || !cradle.hasRegistration('schedulerService')) return\n\n const schedulerService = ctx.container.resolve('schedulerService') as SchedulerServiceLike\n await schedulerService.register({\n id: stableUuidFromString(`checkout:transaction-expiry:${ctx.tenantId}:${ctx.organizationId}`),\n name: 'Checkout transaction expiry',\n description: 'Marks stale checkout transactions as expired and runs usage limit updates.',\n scopeType: 'organization',\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n scheduleType: 'interval',\n scheduleValue: '10m',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: CHECKOUT_EXPIRY_QUEUE,\n sourceType: 'module',\n sourceModule: 'checkout',\n isEnabled: true,\n })\n },\n\n defaultRoleFeatures: {\n superadmin: ['checkout.*'],\n admin: ['checkout.view', 'checkout.create', 'checkout.edit', 'checkout.delete', 'checkout.viewPii', 'checkout.export'],\n employee: ['checkout.view'],\n },\n\n async seedExamples(ctx) {\n await seedCheckoutExamples(ctx)\n },\n}\n\nexport default setup\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,kBAAkB;AAC3B,SAAS,6CAA6C;AACtD,SAAS,4BAA4B;AACrC,SAAS,6BAA6B;AACtC,SAAS,wCAAwC;AAEjD,SAAS,qBAAqB,OAAuB;AACnD,QAAM,QAAQ,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,GAAG,EAAE;AACxE,QAAM,CAAC,IAAK,MAAM,CAAC,IAAI,KAAQ;AAC/B,QAAM,CAAC,IAAK,MAAM,CAAC,IAAI,KAAQ;AAC/B,QAAM,MAAM,OAAO,KAAK,KAAK,EAAE,SAAS,KAAK;AAC7C,SAAO,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC;AAC9G;AAsBO,MAAM,QAA2B;AAAA,EACtC,MAAM,aAAa,KAAK;AACtB,UAAM,sCAAsC,IAAI,IAAI;AAAA,MAClD,UAAU,IAAI;AAAA,MACd,gBAAgB,IAAI;AAAA,IACtB,CAAC;AAED,UAAM,SAAS,IAAI;AACnB,QAAI,OAAO,OAAO,oBAAoB,cAAc,CAAC,OAAO,gBAAgB,kBAAkB,EAAG;AAEjG,UAAM,mBAAmB,IAAI,UAAU,QAAQ,kBAAkB;AACjE,UAAM,iBAAiB,SAAS;AAAA,MAC9B,IAAI,qBAAqB,+BAA+B,IAAI,QAAQ,IAAI,IAAI,cAAc,EAAE;AAAA,MAC5F,MAAM;AAAA,MACN,aAAa;AAAA,MACb,WAAW;AAAA,MACX,gBAAgB,IAAI;AAAA,MACpB,UAAU,IAAI;AAAA,MACd,cAAc;AAAA,MACd,eAAe;AAAA,MACf,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,WAAW;AAAA,IACb,CAAC;AAAA,EACH;AAAA,EAEA,qBAAqB;AAAA,IACnB,YAAY,CAAC,YAAY;AAAA,IACzB,OAAO,CAAC,iBAAiB,mBAAmB,iBAAiB,mBAAmB,oBAAoB,iBAAiB;AAAA,IACrH,UAAU,CAAC,eAAe;AAAA,EAC5B;AAAA,EAEA,MAAM,aAAa,KAAK;AACtB,UAAM,qBAAqB,GAAG;AAAA,EAChC;AACF;AAEA,IAAO,gBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const metadata = {
|
|
2
|
+
event: "checkout.transaction.created",
|
|
3
|
+
persistent: false,
|
|
4
|
+
id: "checkout:query-index-reindex-transaction-created"
|
|
5
|
+
};
|
|
6
|
+
async function handle(payload, ctx) {
|
|
7
|
+
const recordId = typeof payload.transactionId === "string" ? payload.transactionId : null;
|
|
8
|
+
const tenantId = typeof payload.tenantId === "string" ? payload.tenantId : null;
|
|
9
|
+
if (!recordId || !tenantId) return;
|
|
10
|
+
let eventBus = null;
|
|
11
|
+
try {
|
|
12
|
+
eventBus = ctx.resolve("eventBus");
|
|
13
|
+
} catch {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
await eventBus.emitEvent("query_index.upsert_one", {
|
|
17
|
+
entityType: "checkout:checkout_transaction",
|
|
18
|
+
recordId,
|
|
19
|
+
organizationId: payload.organizationId ?? null,
|
|
20
|
+
tenantId,
|
|
21
|
+
crudAction: "created",
|
|
22
|
+
coverageBaseDelta: 1
|
|
23
|
+
}, {
|
|
24
|
+
tenantId,
|
|
25
|
+
organizationId: payload.organizationId ?? null
|
|
26
|
+
}).catch(() => void 0);
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
handle as default,
|
|
30
|
+
metadata
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=search-reindex-transaction-created.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/checkout/subscribers/search-reindex-transaction-created.ts"],
|
|
4
|
+
"sourcesContent": ["export const metadata = {\n event: 'checkout.transaction.created',\n persistent: false,\n id: 'checkout:query-index-reindex-transaction-created',\n}\n\ntype Payload = {\n transactionId?: string\n tenantId?: string | null\n organizationId?: string | null\n}\n\ntype EventBus = { emitEvent: (name: string, body: unknown, options?: unknown) => Promise<void> }\ntype HandlerContext = { resolve: <T = unknown>(name: string) => T }\n\nexport default async function handle(payload: Payload, ctx: HandlerContext): Promise<void> {\n const recordId = typeof payload.transactionId === 'string' ? payload.transactionId : null\n const tenantId = typeof payload.tenantId === 'string' ? payload.tenantId : null\n if (!recordId || !tenantId) return\n\n let eventBus: EventBus | null = null\n try {\n eventBus = ctx.resolve<EventBus>('eventBus')\n } catch {\n return\n }\n\n await eventBus.emitEvent('query_index.upsert_one', {\n entityType: 'checkout:checkout_transaction',\n recordId,\n organizationId: payload.organizationId ?? null,\n tenantId,\n crudAction: 'created',\n coverageBaseDelta: 1,\n }, {\n tenantId,\n organizationId: payload.organizationId ?? null,\n }).catch(() => undefined)\n}\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAWA,eAAO,OAA8B,SAAkB,KAAoC;AACzF,QAAM,WAAW,OAAO,QAAQ,kBAAkB,WAAW,QAAQ,gBAAgB;AACrF,QAAM,WAAW,OAAO,QAAQ,aAAa,WAAW,QAAQ,WAAW;AAC3E,MAAI,CAAC,YAAY,CAAC,SAAU;AAE5B,MAAI,WAA4B;AAChC,MAAI;AACF,eAAW,IAAI,QAAkB,UAAU;AAAA,EAC7C,QAAQ;AACN;AAAA,EACF;AAEA,QAAM,SAAS,UAAU,0BAA0B;AAAA,IACjD,YAAY;AAAA,IACZ;AAAA,IACA,gBAAgB,QAAQ,kBAAkB;AAAA,IAC1C;AAAA,IACA,YAAY;AAAA,IACZ,mBAAmB;AAAA,EACrB,GAAG;AAAA,IACD;AAAA,IACA,gBAAgB,QAAQ,kBAAkB;AAAA,EAC5C,CAAC,EAAE,MAAM,MAAM,MAAS;AAC1B;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -14,14 +14,21 @@ async function handle(job, ctx) {
|
|
|
14
14
|
const commandBus = ctx.resolve("commandBus");
|
|
15
15
|
const batchSize = job.payload?.batchSize ?? 100;
|
|
16
16
|
const cutoff = new Date(Date.now() - EXPIRY_TIMEOUT_MS);
|
|
17
|
+
if (!job.payload?.tenantId || !job.payload?.organizationId) {
|
|
18
|
+
throw new Error("[internal] tenantId and organizationId are required in CheckoutExpiryJob");
|
|
19
|
+
}
|
|
20
|
+
const { tenantId, organizationId } = job.payload;
|
|
17
21
|
const staleTransactions = await findWithDecryption(
|
|
18
22
|
em,
|
|
19
23
|
CheckoutTransaction,
|
|
20
24
|
{
|
|
21
25
|
status: "processing",
|
|
22
|
-
createdAt: { $lt: cutoff }
|
|
26
|
+
createdAt: { $lt: cutoff },
|
|
27
|
+
tenantId,
|
|
28
|
+
organizationId
|
|
23
29
|
},
|
|
24
|
-
{ limit: batchSize, orderBy: { createdAt: "ASC" } }
|
|
30
|
+
{ limit: batchSize, orderBy: { createdAt: "ASC" } },
|
|
31
|
+
{ tenantId, organizationId }
|
|
25
32
|
);
|
|
26
33
|
for (const transaction of staleTransactions) {
|
|
27
34
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/checkout/workers/transaction-expiry.worker.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CheckoutTransaction } from '../data/entities'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('checkout').child({ component: 'transaction-expiry' })\n\nexport const CHECKOUT_EXPIRY_QUEUE = 'checkout-transaction-expiry'\n\nconst EXPIRY_TIMEOUT_MS = 2 * 60 * 60 * 1000 // 2 hours\n\nexport type CheckoutExpiryJob = {\n batchSize?: number\n}\n\nexport const metadata: WorkerMeta = {\n queue: CHECKOUT_EXPIRY_QUEUE,\n id: 'checkout:transaction-expiry',\n concurrency: 1,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport default async function handle(job: QueuedJob<CheckoutExpiryJob>, ctx: HandlerContext): Promise<void> {\n const em = (ctx.resolve('em') as EntityManager).fork()\n const commandBus = ctx.resolve('commandBus') as CommandBus\n const batchSize = job.payload?.batchSize ?? 100\n const cutoff = new Date(Date.now() - EXPIRY_TIMEOUT_MS)\n\n const staleTransactions = await findWithDecryption(\n em,\n CheckoutTransaction,\n {\n status: 'processing',\n createdAt: { $lt: cutoff },\n },\n { limit: batchSize, orderBy: { createdAt: 'ASC' } },\n )\n\n for (const transaction of staleTransactions) {\n try {\n const commandCtx: CommandRuntimeContext = {\n container: { resolve: ctx.resolve } as unknown as CommandRuntimeContext['container'],\n auth: null,\n organizationScope: null,\n selectedOrganizationId: transaction.organizationId,\n organizationIds: [transaction.organizationId],\n }\n await commandBus.execute('checkout.transaction.updateStatus', {\n input: {\n id: transaction.id,\n status: 'expired',\n paymentStatus: 'expired',\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n },\n ctx: commandCtx,\n })\n } catch (error) {\n logger.error('Failed to expire transaction', { transactionId: transaction.id, err: error })\n }\n }\n}\n"],
|
|
5
|
-
"mappings": "AAIA,SAAS,0BAA0B;AACnC,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,UAAU,EAAE,MAAM,EAAE,WAAW,qBAAqB,CAAC;AAE1E,MAAM,wBAAwB;AAErC,MAAM,oBAAoB,IAAI,KAAK,KAAK;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CheckoutTransaction } from '../data/entities'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('checkout').child({ component: 'transaction-expiry' })\n\nexport const CHECKOUT_EXPIRY_QUEUE = 'checkout-transaction-expiry'\n\nconst EXPIRY_TIMEOUT_MS = 2 * 60 * 60 * 1000 // 2 hours\n\nexport type CheckoutExpiryJob = {\n batchSize?: number\n tenantId: string\n organizationId: string\n}\n\nexport const metadata: WorkerMeta = {\n queue: CHECKOUT_EXPIRY_QUEUE,\n id: 'checkout:transaction-expiry',\n concurrency: 1,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport default async function handle(job: QueuedJob<CheckoutExpiryJob>, ctx: HandlerContext): Promise<void> {\n const em = (ctx.resolve('em') as EntityManager).fork()\n const commandBus = ctx.resolve('commandBus') as CommandBus\n const batchSize = job.payload?.batchSize ?? 100\n const cutoff = new Date(Date.now() - EXPIRY_TIMEOUT_MS)\n\n if (!job.payload?.tenantId || !job.payload?.organizationId) {\n throw new Error('[internal] tenantId and organizationId are required in CheckoutExpiryJob')\n }\n\n const { tenantId, organizationId } = job.payload\n\n const staleTransactions = await findWithDecryption(\n em,\n CheckoutTransaction,\n {\n status: 'processing',\n createdAt: { $lt: cutoff },\n tenantId,\n organizationId,\n },\n { limit: batchSize, orderBy: { createdAt: 'ASC' } },\n { tenantId, organizationId },\n )\n\n for (const transaction of staleTransactions) {\n try {\n const commandCtx: CommandRuntimeContext = {\n container: { resolve: ctx.resolve } as unknown as CommandRuntimeContext['container'],\n auth: null,\n organizationScope: null,\n selectedOrganizationId: transaction.organizationId,\n organizationIds: [transaction.organizationId],\n }\n await commandBus.execute('checkout.transaction.updateStatus', {\n input: {\n id: transaction.id,\n status: 'expired',\n paymentStatus: 'expired',\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n },\n ctx: commandCtx,\n })\n } catch (error) {\n logger.error('Failed to expire transaction', { transactionId: transaction.id, err: error })\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "AAIA,SAAS,0BAA0B;AACnC,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,UAAU,EAAE,MAAM,EAAE,WAAW,qBAAqB,CAAC;AAE1E,MAAM,wBAAwB;AAErC,MAAM,oBAAoB,IAAI,KAAK,KAAK;AAQjC,MAAM,WAAuB;AAAA,EAClC,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AACf;AAMA,eAAO,OAA8B,KAAmC,KAAoC;AAC1G,QAAM,KAAM,IAAI,QAAQ,IAAI,EAAoB,KAAK;AACrD,QAAM,aAAa,IAAI,QAAQ,YAAY;AAC3C,QAAM,YAAY,IAAI,SAAS,aAAa;AAC5C,QAAM,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,iBAAiB;AAEtD,MAAI,CAAC,IAAI,SAAS,YAAY,CAAC,IAAI,SAAS,gBAAgB;AAC1D,UAAM,IAAI,MAAM,0EAA0E;AAAA,EAC5F;AAEA,QAAM,EAAE,UAAU,eAAe,IAAI,IAAI;AAEzC,QAAM,oBAAoB,MAAM;AAAA,IAC9B;AAAA,IACA;AAAA,IACA;AAAA,MACE,QAAQ;AAAA,MACR,WAAW,EAAE,KAAK,OAAO;AAAA,MACzB;AAAA,MACA;AAAA,IACF;AAAA,IACA,EAAE,OAAO,WAAW,SAAS,EAAE,WAAW,MAAM,EAAE;AAAA,IAClD,EAAE,UAAU,eAAe;AAAA,EAC7B;AAEA,aAAW,eAAe,mBAAmB;AAC3C,QAAI;AACF,YAAM,aAAoC;AAAA,QACxC,WAAW,EAAE,SAAS,IAAI,QAAQ;AAAA,QAClC,MAAM;AAAA,QACN,mBAAmB;AAAA,QACnB,wBAAwB,YAAY;AAAA,QACpC,iBAAiB,CAAC,YAAY,cAAc;AAAA,MAC9C;AACA,YAAM,WAAW,QAAQ,qCAAqC;AAAA,QAC5D,OAAO;AAAA,UACL,IAAI,YAAY;AAAA,UAChB,QAAQ;AAAA,UACR,eAAe;AAAA,UACf,gBAAgB,YAAY;AAAA,UAC5B,UAAU,YAAY;AAAA,QACxB;AAAA,QACA,KAAK;AAAA,MACP,CAAC;AAAA,IACH,SAAS,OAAO;AACd,aAAO,MAAM,gCAAgC,EAAE,eAAe,YAAY,IAAI,KAAK,MAAM,CAAC;AAAA,IAC5F;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/checkout",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8-develop.6874.1.982d6097d8",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -62,27 +62,27 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@open-mercato/core": "0.6.
|
|
66
|
-
"@open-mercato/ui": "0.6.
|
|
65
|
+
"@open-mercato/core": "0.6.8-develop.6874.1.982d6097d8",
|
|
66
|
+
"@open-mercato/ui": "0.6.8-develop.6874.1.982d6097d8",
|
|
67
67
|
"bcryptjs": "^3.0.3"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"@mikro-orm/postgresql": "^7.0.14",
|
|
71
|
-
"@open-mercato/shared": "0.6.
|
|
71
|
+
"@open-mercato/shared": "0.6.8-develop.6874.1.982d6097d8",
|
|
72
72
|
"react": "^19.0.0",
|
|
73
73
|
"react-dom": "^19.0.0"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@open-mercato/shared": "0.6.
|
|
76
|
+
"@open-mercato/shared": "0.6.8-develop.6874.1.982d6097d8",
|
|
77
77
|
"@types/jest": "^30.0.0",
|
|
78
78
|
"@types/react": "^19.2.17",
|
|
79
79
|
"@types/react-dom": "^19.2.3",
|
|
80
80
|
"esbuild": "^0.28.1",
|
|
81
81
|
"glob": "^13.0.6",
|
|
82
82
|
"jest": "^30.4.2",
|
|
83
|
-
"react": "19.2.
|
|
84
|
-
"react-dom": "19.2.
|
|
85
|
-
"ts-jest": "^29.4.
|
|
83
|
+
"react": "19.2.8",
|
|
84
|
+
"react-dom": "19.2.8",
|
|
85
|
+
"ts-jest": "^29.4.12",
|
|
86
86
|
"typescript": "7.0.2"
|
|
87
87
|
},
|
|
88
88
|
"publishConfig": {
|
|
@@ -92,5 +92,6 @@
|
|
|
92
92
|
"type": "git",
|
|
93
93
|
"url": "https://github.com/open-mercato/open-mercato",
|
|
94
94
|
"directory": "packages/checkout"
|
|
95
|
-
}
|
|
95
|
+
},
|
|
96
|
+
"stableVersion": "0.6.7"
|
|
96
97
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
|
+
import { getAuthToken } from '@open-mercato/core/modules/core/__integration__/helpers/api'
|
|
3
|
+
import {
|
|
4
|
+
createCustomerData,
|
|
5
|
+
createFixedTemplateInput,
|
|
6
|
+
createLinkFixture,
|
|
7
|
+
deleteCheckoutEntityIfExists,
|
|
8
|
+
listCheckoutTransactions,
|
|
9
|
+
submitPayLink,
|
|
10
|
+
} from './helpers/fixtures'
|
|
11
|
+
|
|
12
|
+
test.describe('TC-CHKT-042: Transaction search over encrypted customer data', () => {
|
|
13
|
+
test('finds a newly created transaction through its token index', async ({ request }) => {
|
|
14
|
+
const email = `token-search-${Date.now()}@example.test`
|
|
15
|
+
let token: string | null = null
|
|
16
|
+
let linkId: string | null = null
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
token = await getAuthToken(request)
|
|
20
|
+
const link = await createLinkFixture(request, token, createFixedTemplateInput({ status: 'active' }))
|
|
21
|
+
linkId = link.id
|
|
22
|
+
const submitted = await submitPayLink(request, link.slug, {
|
|
23
|
+
customerData: createCustomerData({ email }),
|
|
24
|
+
acceptedLegalConsents: {},
|
|
25
|
+
amount: 49.99,
|
|
26
|
+
})
|
|
27
|
+
expect(submitted.ok()).toBeTruthy()
|
|
28
|
+
|
|
29
|
+
await expect.poll(async () => {
|
|
30
|
+
const result = await listCheckoutTransactions(
|
|
31
|
+
request,
|
|
32
|
+
token!,
|
|
33
|
+
`search=${encodeURIComponent(email)}&page=1&pageSize=10`,
|
|
34
|
+
)
|
|
35
|
+
return result.items.some((item) => item.email === email)
|
|
36
|
+
}, { timeout: 15_000 }).toBeTruthy()
|
|
37
|
+
} finally {
|
|
38
|
+
await deleteCheckoutEntityIfExists(request, token, 'links', linkId)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
|
+
import { getAuthToken, apiRequest } from '@open-mercato/core/modules/core/__integration__/helpers/api'
|
|
3
|
+
import { readJsonSafe } from '@open-mercato/core/modules/core/__integration__/helpers/generalFixtures'
|
|
4
|
+
import {
|
|
5
|
+
createFixedTemplateInput,
|
|
6
|
+
createLinkFixture,
|
|
7
|
+
deleteCheckoutEntityIfExists,
|
|
8
|
+
findGatewayTransactionIdForCheckout,
|
|
9
|
+
readCheckoutTransaction,
|
|
10
|
+
sendMockGatewayWebhook,
|
|
11
|
+
submitPayLink,
|
|
12
|
+
waitForCheckoutStatus,
|
|
13
|
+
} from './helpers/fixtures'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* TC-CHKT-043: Two-contender concurrency test for updateTransactionStatus.
|
|
17
|
+
*
|
|
18
|
+
* Verifies that the compare-and-swap (CAS) in updateTransactionStatusCommand
|
|
19
|
+
* serializes two concurrent writers correctly:
|
|
20
|
+
*
|
|
21
|
+
* - Exactly one of two concurrent updateStatus calls wins.
|
|
22
|
+
* - The loser receives a 409 (concurrent_status_update).
|
|
23
|
+
* - The persisted terminal status stays 'completed'.
|
|
24
|
+
* - CheckoutLink.completionCount is incremented exactly once.
|
|
25
|
+
*
|
|
26
|
+
* This test exercises the race described in #4700 using a real database,
|
|
27
|
+
* satisfying .ai/review-checklist.md §20 (concurrency-sensitive commands
|
|
28
|
+
* require an executable two-contender test).
|
|
29
|
+
*/
|
|
30
|
+
test.describe('TC-CHKT-043 (concurrency): Two-contender race — only one updateStatus call wins', () => {
|
|
31
|
+
test('concurrent completed + processing status updates: one wins, one gets 409, completionCount incremented once', async ({ request }) => {
|
|
32
|
+
let token: string | null = null
|
|
33
|
+
let linkId: string | null = null
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
token = await getAuthToken(request)
|
|
37
|
+
|
|
38
|
+
// Create an active link that accepts a single payment.
|
|
39
|
+
const link = await createLinkFixture(request, token, {
|
|
40
|
+
...createFixedTemplateInput({ status: 'active', collectCustomerDetails: false }),
|
|
41
|
+
})
|
|
42
|
+
linkId = link.id
|
|
43
|
+
|
|
44
|
+
// Submit a checkout to create a transaction and a gateway session.
|
|
45
|
+
const submitResponse = await submitPayLink(request, link.slug, {
|
|
46
|
+
customerData: {},
|
|
47
|
+
acceptedLegalConsents: {},
|
|
48
|
+
amount: 49.99,
|
|
49
|
+
})
|
|
50
|
+
expect(submitResponse.status(), `Submit failed: ${submitResponse.status()}`).toBe(201)
|
|
51
|
+
const submitBody = await submitResponse.json() as { transactionId: string }
|
|
52
|
+
const transactionId = submitBody.transactionId
|
|
53
|
+
expect(transactionId).toBeTruthy()
|
|
54
|
+
|
|
55
|
+
// Find the mock-gateway session ID so we can fire the captured webhook.
|
|
56
|
+
const gatewayTransactionId = await findGatewayTransactionIdForCheckout(request, token, transactionId)
|
|
57
|
+
const gatewayTransaction = await apiRequest(request, 'GET', `/api/payment_gateways/transactions/${encodeURIComponent(gatewayTransactionId)}`, { token })
|
|
58
|
+
const gtBody = await readJsonSafe<{ transaction?: { providerSessionId?: string | null } }>(gatewayTransaction)
|
|
59
|
+
const providerSessionId = gtBody?.transaction?.providerSessionId
|
|
60
|
+
expect(providerSessionId, 'providerSessionId is required for webhook simulation').toBeTruthy()
|
|
61
|
+
|
|
62
|
+
// --- Two-contender race ---
|
|
63
|
+
// Simultaneously fire:
|
|
64
|
+
// (A) The gateway "captured" webhook ? tries to set status = 'completed'.
|
|
65
|
+
// (B) A direct updateStatus API call ? tries to set status = 'processing'
|
|
66
|
+
// (simulates the submit route's late write losing the race).
|
|
67
|
+
//
|
|
68
|
+
// Both target the same transaction. Exactly one must succeed; the other
|
|
69
|
+
// must receive a 409 so no lost-update occurs.
|
|
70
|
+
const [webhookResult, directResult] = await Promise.allSettled([
|
|
71
|
+
sendMockGatewayWebhook(request, token!, providerSessionId!, 'captured', 49.99),
|
|
72
|
+
apiRequest(request, 'POST', `/api/checkout/transactions/${encodeURIComponent(transactionId)}/status`, {
|
|
73
|
+
token,
|
|
74
|
+
data: { status: 'processing', paymentStatus: 'processing' },
|
|
75
|
+
}),
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
// At least the webhook path should have been attempted. The direct call
|
|
79
|
+
// may receive 409 (CAS missed) or 422 (invalid transition) depending on
|
|
80
|
+
// which writer committed first — both are acceptable losing outcomes.
|
|
81
|
+
// The important invariant is the final persisted state.
|
|
82
|
+
expect(webhookResult.status === 'fulfilled' || directResult.status === 'fulfilled').toBe(true)
|
|
83
|
+
|
|
84
|
+
// Wait for the transaction to reach a terminal state (the webhook sets it
|
|
85
|
+
// to 'completed'; the submit route's 'processing' write must not win).
|
|
86
|
+
const finalTransaction = await waitForCheckoutStatus(request, token!, transactionId, 'completed', {
|
|
87
|
+
attempts: 30,
|
|
88
|
+
intervalMs: 200,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// --- Core assertions ---
|
|
92
|
+
|
|
93
|
+
// 1. The persisted status must be 'completed' — not 'processing'.
|
|
94
|
+
expect(finalTransaction.status).toBe('completed')
|
|
95
|
+
|
|
96
|
+
// 2. completionCount must be exactly 1 — the terminal state must have
|
|
97
|
+
// been applied exactly once, not zero or twice.
|
|
98
|
+
const updatedLinkResponse = await apiRequest(request, 'GET', `/api/checkout/links/${encodeURIComponent(link.id)}`, { token })
|
|
99
|
+
// The detail route spreads the serialized link at the top level; there is
|
|
100
|
+
// no `link` wrapper. Reading one made this assertion compare undefined to 1.
|
|
101
|
+
const updatedLinkBody = await readJsonSafe<{ completionCount?: number }>(updatedLinkResponse)
|
|
102
|
+
expect(updatedLinkBody?.completionCount).toBe(1)
|
|
103
|
+
|
|
104
|
+
// 3. The stored transaction must have a gatewayTransactionId from the webhook.
|
|
105
|
+
const storedTransaction = await readCheckoutTransaction(request, token!, transactionId)
|
|
106
|
+
expect(storedTransaction.status).toBe('completed')
|
|
107
|
+
expect(storedTransaction.gatewayTransactionId).toBeTruthy()
|
|
108
|
+
} finally {
|
|
109
|
+
await deleteCheckoutEntityIfExists(request, token, 'links', linkId)
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import searchConfig from '../search'
|
|
2
|
+
import { CHECKOUT_ENTITY_IDS } from '../lib/constants'
|
|
3
|
+
|
|
4
|
+
describe('checkout search configuration', () => {
|
|
5
|
+
it('indexes the encrypted transaction fields used by the list API', async () => {
|
|
6
|
+
const transaction = searchConfig.entities.find((entry) => entry.entityId === CHECKOUT_ENTITY_IDS.transaction)
|
|
7
|
+
expect(transaction?.fieldPolicy?.searchable).toEqual(['email', 'firstName', 'lastName'])
|
|
8
|
+
|
|
9
|
+
const source = await transaction?.buildSource?.({
|
|
10
|
+
record: { email: 'buyer@example.test', firstName: 'Ada', lastName: 'Lovelace' },
|
|
11
|
+
customFields: {},
|
|
12
|
+
tenantId: 'tenant-1',
|
|
13
|
+
organizationId: 'org-1',
|
|
14
|
+
})
|
|
15
|
+
expect(source?.fields).toMatchObject({
|
|
16
|
+
email: 'buyer@example.test',
|
|
17
|
+
first_name: 'Ada',
|
|
18
|
+
last_name: 'Lovelace',
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
})
|