@open-mercato/checkout 0.6.7-develop.6862.1.c11a64ce0a → 0.6.7

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.
Files changed (63) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/checkout/api/pay/[slug]/submit/route.js +28 -43
  3. package/dist/modules/checkout/api/pay/[slug]/submit/route.js.map +2 -2
  4. package/dist/modules/checkout/api/transactions/route.js +6 -23
  5. package/dist/modules/checkout/api/transactions/route.js.map +2 -2
  6. package/dist/modules/checkout/backend/checkout/pay-links/page.js +1 -2
  7. package/dist/modules/checkout/backend/checkout/pay-links/page.js.map +2 -2
  8. package/dist/modules/checkout/backend/checkout/templates/page.js +1 -2
  9. package/dist/modules/checkout/backend/checkout/templates/page.js.map +2 -2
  10. package/dist/modules/checkout/backend/checkout/transactions/page.js +1 -2
  11. package/dist/modules/checkout/backend/checkout/transactions/page.js.map +2 -2
  12. package/dist/modules/checkout/commands/links.js +1 -1
  13. package/dist/modules/checkout/commands/links.js.map +2 -2
  14. package/dist/modules/checkout/commands/transactions.js +13 -45
  15. package/dist/modules/checkout/commands/transactions.js.map +2 -2
  16. package/dist/modules/checkout/components/PayPage.js +38 -39
  17. package/dist/modules/checkout/components/PayPage.js.map +2 -2
  18. package/dist/modules/checkout/search.js +6 -44
  19. package/dist/modules/checkout/search.js.map +2 -2
  20. package/dist/modules/checkout/setup.js +0 -28
  21. package/dist/modules/checkout/setup.js.map +2 -2
  22. package/dist/modules/checkout/workers/transaction-expiry.worker.js +2 -9
  23. package/dist/modules/checkout/workers/transaction-expiry.worker.js.map +2 -2
  24. package/package.json +9 -10
  25. package/src/modules/checkout/api/pay/[slug]/submit/__tests__/route.test.ts +0 -77
  26. package/src/modules/checkout/api/pay/[slug]/submit/route.ts +28 -58
  27. package/src/modules/checkout/api/transactions/route.ts +6 -30
  28. package/src/modules/checkout/backend/checkout/pay-links/page.tsx +1 -2
  29. package/src/modules/checkout/backend/checkout/templates/page.tsx +1 -2
  30. package/src/modules/checkout/backend/checkout/transactions/page.tsx +1 -2
  31. package/src/modules/checkout/commands/links.ts +1 -5
  32. package/src/modules/checkout/commands/transactions.ts +13 -68
  33. package/src/modules/checkout/components/PayPage.tsx +37 -38
  34. package/src/modules/checkout/i18n/de.json +0 -1
  35. package/src/modules/checkout/i18n/en.json +0 -1
  36. package/src/modules/checkout/i18n/es.json +0 -1
  37. package/src/modules/checkout/i18n/pl.json +0 -1
  38. package/src/modules/checkout/search.ts +6 -44
  39. package/src/modules/checkout/setup.ts +0 -51
  40. package/src/modules/checkout/workers/transaction-expiry.worker.ts +0 -11
  41. package/dist/modules/checkout/__integration__/TC-CHKT-042.spec.js +0 -39
  42. package/dist/modules/checkout/__integration__/TC-CHKT-042.spec.js.map +0 -7
  43. package/dist/modules/checkout/__integration__/TC-CHKT-043-concurrent-status-update.spec.js +0 -62
  44. package/dist/modules/checkout/__integration__/TC-CHKT-043-concurrent-status-update.spec.js.map +0 -7
  45. package/dist/modules/checkout/extension-points.js +0 -68
  46. package/dist/modules/checkout/extension-points.js.map +0 -7
  47. package/dist/modules/checkout/lib/transaction-status-machine.js +0 -31
  48. package/dist/modules/checkout/lib/transaction-status-machine.js.map +0 -7
  49. package/dist/modules/checkout/subscribers/search-reindex-transaction-created.js +0 -32
  50. package/dist/modules/checkout/subscribers/search-reindex-transaction-created.js.map +0 -7
  51. package/src/modules/checkout/__integration__/TC-CHKT-042.spec.ts +0 -41
  52. package/src/modules/checkout/__integration__/TC-CHKT-043-concurrent-status-update.spec.ts +0 -112
  53. package/src/modules/checkout/__tests__/search.test.ts +0 -21
  54. package/src/modules/checkout/__tests__/setup-schedules.test.ts +0 -99
  55. package/src/modules/checkout/api/transactions/__tests__/route.test.ts +0 -113
  56. package/src/modules/checkout/commands/__tests__/update-transaction-status.test.ts +0 -363
  57. package/src/modules/checkout/extension-points.ts +0 -67
  58. package/src/modules/checkout/i18n/ko.json +0 -463
  59. package/src/modules/checkout/lib/__tests__/transaction-status-machine.test.ts +0 -152
  60. package/src/modules/checkout/lib/transaction-status-machine.ts +0 -71
  61. package/src/modules/checkout/subscribers/__tests__/search-reindex-transaction-created.test.ts +0 -29
  62. package/src/modules/checkout/subscribers/search-reindex-transaction-created.ts +0 -39
  63. package/src/modules/checkout/workers/__tests__/transaction-expiry.worker.test.ts +0 -124
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../src/modules/checkout/__integration__/TC-CHKT-043-concurrent-status-update.spec.ts"],
4
- "sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken, apiRequest } from '@open-mercato/core/modules/core/__integration__/helpers/api'\nimport { readJsonSafe } from '@open-mercato/core/modules/core/__integration__/helpers/generalFixtures'\nimport {\n createFixedTemplateInput,\n createLinkFixture,\n deleteCheckoutEntityIfExists,\n findGatewayTransactionIdForCheckout,\n readCheckoutTransaction,\n sendMockGatewayWebhook,\n submitPayLink,\n waitForCheckoutStatus,\n} from './helpers/fixtures'\n\n/**\n * TC-CHKT-043: Two-contender concurrency test for updateTransactionStatus.\n *\n * Verifies that the compare-and-swap (CAS) in updateTransactionStatusCommand\n * serializes two concurrent writers correctly:\n *\n * - Exactly one of two concurrent updateStatus calls wins.\n * - The loser receives a 409 (concurrent_status_update).\n * - The persisted terminal status stays 'completed'.\n * - CheckoutLink.completionCount is incremented exactly once.\n *\n * This test exercises the race described in #4700 using a real database,\n * satisfying .ai/review-checklist.md \u00A720 (concurrency-sensitive commands\n * require an executable two-contender test).\n */\ntest.describe('TC-CHKT-043 (concurrency): Two-contender race \u2014 only one updateStatus call wins', () => {\n test('concurrent completed + processing status updates: one wins, one gets 409, completionCount incremented once', async ({ request }) => {\n let token: string | null = null\n let linkId: string | null = null\n\n try {\n token = await getAuthToken(request)\n\n // Create an active link that accepts a single payment.\n const link = await createLinkFixture(request, token, {\n ...createFixedTemplateInput({ status: 'active', collectCustomerDetails: false }),\n })\n linkId = link.id\n\n // Submit a checkout to create a transaction and a gateway session.\n const submitResponse = await submitPayLink(request, link.slug, {\n customerData: {},\n acceptedLegalConsents: {},\n amount: 49.99,\n })\n expect(submitResponse.status(), `Submit failed: ${submitResponse.status()}`).toBe(201)\n const submitBody = await submitResponse.json() as { transactionId: string }\n const transactionId = submitBody.transactionId\n expect(transactionId).toBeTruthy()\n\n // Find the mock-gateway session ID so we can fire the captured webhook.\n const gatewayTransactionId = await findGatewayTransactionIdForCheckout(request, token, transactionId)\n const gatewayTransaction = await apiRequest(request, 'GET', `/api/payment_gateways/transactions/${encodeURIComponent(gatewayTransactionId)}`, { token })\n const gtBody = await readJsonSafe<{ transaction?: { providerSessionId?: string | null } }>(gatewayTransaction)\n const providerSessionId = gtBody?.transaction?.providerSessionId\n expect(providerSessionId, 'providerSessionId is required for webhook simulation').toBeTruthy()\n\n // --- Two-contender race ---\n // Simultaneously fire:\n // (A) The gateway \"captured\" webhook ? tries to set status = 'completed'.\n // (B) A direct updateStatus API call ? tries to set status = 'processing'\n // (simulates the submit route's late write losing the race).\n //\n // Both target the same transaction. Exactly one must succeed; the other\n // must receive a 409 so no lost-update occurs.\n const [webhookResult, directResult] = await Promise.allSettled([\n sendMockGatewayWebhook(request, token!, providerSessionId!, 'captured', 49.99),\n apiRequest(request, 'POST', `/api/checkout/transactions/${encodeURIComponent(transactionId)}/status`, {\n token,\n data: { status: 'processing', paymentStatus: 'processing' },\n }),\n ])\n\n // At least the webhook path should have been attempted. The direct call\n // may receive 409 (CAS missed) or 422 (invalid transition) depending on\n // which writer committed first \u2014 both are acceptable losing outcomes.\n // The important invariant is the final persisted state.\n expect(webhookResult.status === 'fulfilled' || directResult.status === 'fulfilled').toBe(true)\n\n // Wait for the transaction to reach a terminal state (the webhook sets it\n // to 'completed'; the submit route's 'processing' write must not win).\n const finalTransaction = await waitForCheckoutStatus(request, token!, transactionId, 'completed', {\n attempts: 30,\n intervalMs: 200,\n })\n\n // --- Core assertions ---\n\n // 1. The persisted status must be 'completed' \u2014 not 'processing'.\n expect(finalTransaction.status).toBe('completed')\n\n // 2. completionCount must be exactly 1 \u2014 the terminal state must have\n // been applied exactly once, not zero or twice.\n const updatedLinkResponse = await apiRequest(request, 'GET', `/api/checkout/links/${encodeURIComponent(link.id)}`, { token })\n // The detail route spreads the serialized link at the top level; there is\n // no `link` wrapper. Reading one made this assertion compare undefined to 1.\n const updatedLinkBody = await readJsonSafe<{ completionCount?: number }>(updatedLinkResponse)\n expect(updatedLinkBody?.completionCount).toBe(1)\n\n // 3. The stored transaction must have a gatewayTransactionId from the webhook.\n const storedTransaction = await readCheckoutTransaction(request, token!, transactionId)\n expect(storedTransaction.status).toBe('completed')\n expect(storedTransaction.gatewayTransactionId).toBeTruthy()\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'links', linkId)\n }\n })\n})\n"],
5
- "mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,cAAc,kBAAkB;AACzC,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAiBP,KAAK,SAAS,wFAAmF,MAAM;AACrG,OAAK,8GAA8G,OAAO,EAAE,QAAQ,MAAM;AACxI,QAAI,QAAuB;AAC3B,QAAI,SAAwB;AAE5B,QAAI;AACF,cAAQ,MAAM,aAAa,OAAO;AAGlC,YAAM,OAAO,MAAM,kBAAkB,SAAS,OAAO;AAAA,QACnD,GAAG,yBAAyB,EAAE,QAAQ,UAAU,wBAAwB,MAAM,CAAC;AAAA,MACjF,CAAC;AACD,eAAS,KAAK;AAGd,YAAM,iBAAiB,MAAM,cAAc,SAAS,KAAK,MAAM;AAAA,QAC7D,cAAc,CAAC;AAAA,QACf,uBAAuB,CAAC;AAAA,QACxB,QAAQ;AAAA,MACV,CAAC;AACD,aAAO,eAAe,OAAO,GAAG,kBAAkB,eAAe,OAAO,CAAC,EAAE,EAAE,KAAK,GAAG;AACrF,YAAM,aAAa,MAAM,eAAe,KAAK;AAC7C,YAAM,gBAAgB,WAAW;AACjC,aAAO,aAAa,EAAE,WAAW;AAGjC,YAAM,uBAAuB,MAAM,oCAAoC,SAAS,OAAO,aAAa;AACpG,YAAM,qBAAqB,MAAM,WAAW,SAAS,OAAO,sCAAsC,mBAAmB,oBAAoB,CAAC,IAAI,EAAE,MAAM,CAAC;AACvJ,YAAM,SAAS,MAAM,aAAsE,kBAAkB;AAC7G,YAAM,oBAAoB,QAAQ,aAAa;AAC/C,aAAO,mBAAmB,sDAAsD,EAAE,WAAW;AAU7F,YAAM,CAAC,eAAe,YAAY,IAAI,MAAM,QAAQ,WAAW;AAAA,QAC7D,uBAAuB,SAAS,OAAQ,mBAAoB,YAAY,KAAK;AAAA,QAC7E,WAAW,SAAS,QAAQ,8BAA8B,mBAAmB,aAAa,CAAC,WAAW;AAAA,UACpG;AAAA,UACA,MAAM,EAAE,QAAQ,cAAc,eAAe,aAAa;AAAA,QAC5D,CAAC;AAAA,MACH,CAAC;AAMD,aAAO,cAAc,WAAW,eAAe,aAAa,WAAW,WAAW,EAAE,KAAK,IAAI;AAI7F,YAAM,mBAAmB,MAAM,sBAAsB,SAAS,OAAQ,eAAe,aAAa;AAAA,QAChG,UAAU;AAAA,QACV,YAAY;AAAA,MACd,CAAC;AAKD,aAAO,iBAAiB,MAAM,EAAE,KAAK,WAAW;AAIhD,YAAM,sBAAsB,MAAM,WAAW,SAAS,OAAO,uBAAuB,mBAAmB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC;AAG5H,YAAM,kBAAkB,MAAM,aAA2C,mBAAmB;AAC5F,aAAO,iBAAiB,eAAe,EAAE,KAAK,CAAC;AAG/C,YAAM,oBAAoB,MAAM,wBAAwB,SAAS,OAAQ,aAAa;AACtF,aAAO,kBAAkB,MAAM,EAAE,KAAK,WAAW;AACjD,aAAO,kBAAkB,oBAAoB,EAAE,WAAW;AAAA,IAC5D,UAAE;AACA,YAAM,6BAA6B,SAAS,OAAO,SAAS,MAAM;AAAA,IACpE;AAAA,EACF,CAAC;AACH,CAAC;",
6
- "names": []
7
- }
@@ -1,68 +0,0 @@
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
@@ -1,7 +0,0 @@
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
- }
@@ -1,31 +0,0 @@
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
@@ -1,7 +0,0 @@
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,32 +0,0 @@
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
@@ -1,7 +0,0 @@
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
- }
@@ -1,41 +0,0 @@
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
- })
@@ -1,112 +0,0 @@
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
- })
@@ -1,21 +0,0 @@
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
- })
@@ -1,99 +0,0 @@
1
- import { describe, expect, it, jest } from '@jest/globals'
2
- import type { EntityManager } from '@mikro-orm/postgresql'
3
- import type { AwilixContainer } from 'awilix'
4
- import type { InitSetupContext } from '@open-mercato/shared/modules/setup'
5
- import { CHECKOUT_EXPIRY_QUEUE } from '../workers/transaction-expiry.worker'
6
-
7
- const ensureCheckoutFieldsetsAndDefinitions = jest.fn(async (..._args: unknown[]) => undefined)
8
-
9
- jest.mock('../seed/customFields', () => ({
10
- ensureCheckoutFieldsetsAndDefinitions: (...args: unknown[]) => ensureCheckoutFieldsetsAndDefinitions(...args),
11
- }))
12
-
13
- type ScheduleRegistration = {
14
- id: string
15
- organizationId: string
16
- tenantId: string
17
- scopeType: string
18
- targetQueue: string
19
- }
20
-
21
- function createSetupContext(options: {
22
- tenantId?: string
23
- organizationId?: string
24
- hasScheduler?: boolean
25
- register?: (registration: ScheduleRegistration) => Promise<void>
26
- }) {
27
- const register = jest.fn(options.register ?? (async () => undefined))
28
- const resolve = jest.fn((name: string) => {
29
- if (name === 'schedulerService') return { register }
30
- throw new Error(`[internal] Missing dependency: ${name}`)
31
- })
32
- const container = {
33
- hasRegistration: jest.fn(() => options.hasScheduler ?? true),
34
- resolve,
35
- }
36
- const context: InitSetupContext = {
37
- tenantId: options.tenantId ?? 'tenant-1',
38
- organizationId: options.organizationId ?? 'organization-1',
39
- em: {} as EntityManager,
40
- container: container as unknown as AwilixContainer,
41
- }
42
- return { context, register, resolve }
43
- }
44
-
45
- async function seedDefaults(context: InitSetupContext): Promise<void> {
46
- const { setup } = await import('../setup')
47
- if (!setup.seedDefaults) throw new Error('[internal] Checkout seedDefaults hook is missing')
48
- await setup.seedDefaults(context)
49
- }
50
-
51
- describe('checkout setup schedules', () => {
52
- it('registers one organization-scoped transaction-expiry schedule', async () => {
53
- const { context, register } = createSetupContext({})
54
-
55
- await seedDefaults(context)
56
-
57
- expect(register).toHaveBeenCalledWith(expect.objectContaining({
58
- organizationId: 'organization-1',
59
- tenantId: 'tenant-1',
60
- scopeType: 'organization',
61
- targetQueue: CHECKOUT_EXPIRY_QUEUE,
62
- }))
63
- })
64
-
65
- it('uses stable IDs that remain distinct across organizations in one tenant', async () => {
66
- const first = createSetupContext({ organizationId: 'organization-1' })
67
- const second = createSetupContext({ organizationId: 'organization-2' })
68
- const repeated = createSetupContext({ organizationId: 'organization-1' })
69
-
70
- await seedDefaults(first.context)
71
- await seedDefaults(second.context)
72
- await seedDefaults(repeated.context)
73
-
74
- const firstId = first.register.mock.calls[0][0].id
75
- const secondId = second.register.mock.calls[0][0].id
76
- const repeatedId = repeated.register.mock.calls[0][0].id
77
- expect(firstId).not.toBe(secondId)
78
- expect(repeatedId).toBe(firstId)
79
- })
80
-
81
- it('skips schedule registration when the optional scheduler module is absent', async () => {
82
- const { context, register, resolve } = createSetupContext({ hasScheduler: false })
83
-
84
- await seedDefaults(context)
85
-
86
- expect(register).not.toHaveBeenCalled()
87
- expect(resolve).not.toHaveBeenCalled()
88
- })
89
-
90
- it('propagates scheduler registration failures', async () => {
91
- const { context } = createSetupContext({
92
- register: async () => {
93
- throw new Error('[internal] scheduler unavailable')
94
- },
95
- })
96
-
97
- await expect(seedDefaults(context)).rejects.toThrow('[internal] scheduler unavailable')
98
- })
99
- })
@@ -1,113 +0,0 @@
1
- /** @jest-environment node */
2
-
3
- import { GET } from '../route'
4
- import { CheckoutTransaction } from '../../../data/entities'
5
-
6
- const mockFindAndCountWithDecryption = jest.fn()
7
- const mockFindWithDecryption = jest.fn()
8
-
9
- jest.mock('@open-mercato/shared/lib/encryption/find', () => ({
10
- findAndCountWithDecryption: (...args: unknown[]) => mockFindAndCountWithDecryption(...args),
11
- findWithDecryption: (...args: unknown[]) => mockFindWithDecryption(...args),
12
- }))
13
-
14
- const mockFindEntityIdsBySearchTokens = jest.fn(async () => ({ matched: false, reason: 'no-tokens' as const }))
15
-
16
- jest.mock('@open-mercato/shared/lib/search/tokenLookup', () => ({
17
- findEntityIdsBySearchTokens: (...args: unknown[]) => mockFindEntityIdsBySearchTokens(...(args as [])),
18
- }))
19
-
20
- const mockEm = { getKysely: jest.fn(() => ({})) }
21
- const auth = { tenantId: 'tenant-1', orgId: 'org-1', sub: 'user-1' }
22
-
23
- jest.mock('../../helpers', () => ({
24
- requireAdminContext: jest.fn(async () => ({ auth, container: {}, em: mockEm, commandBus: {} })),
25
- userHasCheckoutFeature: jest.fn(async () => true),
26
- handleCheckoutRouteError: (error: unknown) => {
27
- throw error
28
- },
29
- }))
30
-
31
- function makeRequest(params?: Record<string, string>) {
32
- const url = new URL('http://localhost/api/checkout/transactions')
33
- for (const [key, value] of Object.entries(params ?? {})) url.searchParams.set(key, value)
34
- return new Request(url.toString(), { method: 'GET' })
35
- }
36
-
37
- function capturedWhere(): Record<string, unknown> {
38
- return mockFindAndCountWithDecryption.mock.calls[0][2] as Record<string, unknown>
39
- }
40
-
41
- describe('GET /api/checkout/transactions', () => {
42
- beforeEach(() => {
43
- jest.clearAllMocks()
44
- mockFindAndCountWithDecryption.mockResolvedValue([[], 0])
45
- mockFindWithDecryption.mockResolvedValue([])
46
- mockFindEntityIdsBySearchTokens.mockResolvedValue({ matched: false, reason: 'no-tokens' } as never)
47
- })
48
-
49
- it('applies no search predicate when no term is supplied', async () => {
50
- await GET(makeRequest())
51
-
52
- expect(mockFindAndCountWithDecryption).toHaveBeenCalledWith(
53
- mockEm,
54
- CheckoutTransaction,
55
- { organizationId: 'org-1', tenantId: 'tenant-1' },
56
- expect.any(Object),
57
- expect.any(Object),
58
- )
59
- expect(mockFindEntityIdsBySearchTokens).not.toHaveBeenCalled()
60
- })
61
-
62
- it('keeps the escaped ILIKE predicates for the PII columns', async () => {
63
- await GET(makeRequest({ search: '100%_off' }))
64
-
65
- expect(capturedWhere().$or).toEqual([
66
- { email: { $ilike: '%100\\%\\_off%' } },
67
- { firstName: { $ilike: '%100\\%\\_off%' } },
68
- { lastName: { $ilike: '%100\\%\\_off%' } },
69
- ])
70
- })
71
-
72
- it('unions the encrypted-column token matches into the search predicate', async () => {
73
- mockFindEntityIdsBySearchTokens.mockResolvedValueOnce({
74
- matched: true,
75
- ids: ['tx-1', 'tx-2'],
76
- } as never)
77
-
78
- await GET(makeRequest({ search: 'ada@example.com' }))
79
-
80
- expect(mockFindEntityIdsBySearchTokens).toHaveBeenCalledWith(expect.objectContaining({
81
- entityType: 'checkout:checkout_transaction',
82
- query: 'ada@example.com',
83
- fields: ['email', 'first_name', 'last_name'],
84
- scope: { tenantId: 'tenant-1', organizationId: 'org-1' },
85
- }))
86
- expect(capturedWhere().$or).toContainEqual({ id: { $in: ['tx-1', 'tx-2'] } })
87
- })
88
-
89
- it('never emits an ILIKE against the uuid id column', async () => {
90
- await GET(makeRequest({ search: 'abc' }))
91
-
92
- const or = capturedWhere().$or as Array<Record<string, unknown>>
93
- expect(or.some((clause) => 'id' in clause)).toBe(false)
94
- })
95
-
96
- it('matches a uuid search term against the id column exactly', async () => {
97
- const id = '3f2504e0-4f89-41d3-9a0c-0305e82c3301'
98
- await GET(makeRequest({ search: id }))
99
-
100
- expect(capturedWhere().$or).toContainEqual({ id })
101
- })
102
-
103
- it('keeps link and status filters alongside the search predicate', async () => {
104
- await GET(makeRequest({ search: 'ada', status: 'completed', linkId: 'link-1' }))
105
-
106
- expect(capturedWhere()).toMatchObject({
107
- organizationId: 'org-1',
108
- tenantId: 'tenant-1',
109
- status: 'completed',
110
- linkId: 'link-1',
111
- })
112
- })
113
- })