@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,4 +1,3 @@
1
- import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
2
1
  import { CHECKOUT_ENTITY_IDS } from "./lib/constants.js";
3
2
  function asSearchText(value) {
4
3
  return typeof value === "string" ? value : "";
@@ -32,52 +31,15 @@ const searchConfig = {
32
31
  searchable: ["name", "title"],
33
32
  excluded: ["passwordHash", "gatewaySettings", "customerFieldsSchema"]
34
33
  },
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
- },
60
34
  buildSource: async (ctx) => ({
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 }
35
+ text: [`${asSearchText(ctx.record.name)}: ${asSearchText(ctx.record.title)}`],
36
+ presenter: { title: asSearchText(ctx.record.name), subtitle: "Link Template" },
37
+ checksumSource: { record: ctx.record, customFields: ctx.customFields }
76
38
  }),
77
39
  formatResult: async (ctx) => ({
78
- title: asSearchText(ctx.record.email),
79
- subtitle: `${asSearchText(ctx.record.firstName)} ${asSearchText(ctx.record.lastName)}`.trim(),
80
- icon: "lucide:receipt"
40
+ title: asSearchText(ctx.record.name),
41
+ subtitle: "Link Template",
42
+ icon: "lucide:file-text"
81
43
  })
82
44
  }
83
45
  ]
@@ -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 { 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": "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",
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 text: [`${asSearchText(ctx.record.name)}: ${asSearchText(ctx.record.title)}`],\n presenter: { title: asSearchText(ctx.record.name), subtitle: 'Link Template' },\n checksumSource: { record: ctx.record, customFields: ctx.customFields },\n }),\n formatResult: async (ctx) => ({\n title: asSearchText(ctx.record.name),\n subtitle: 'Link Template',\n icon: 'lucide:file-text',\n }),\n },\n ],\n}\n\nexport const config = searchConfig\nexport default searchConfig\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,SAAS;AAAA,QAC3B,MAAM,CAAC,GAAG,aAAa,IAAI,OAAO,IAAI,CAAC,KAAK,aAAa,IAAI,OAAO,KAAK,CAAC,EAAE;AAAA,QAC5E,WAAW,EAAE,OAAO,aAAa,IAAI,OAAO,IAAI,GAAG,UAAU,gBAAgB;AAAA,QAC7E,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;AAAA,QACV,MAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,SAAS;AACtB,IAAO,iBAAQ;",
6
6
  "names": []
7
7
  }
@@ -1,40 +1,12 @@
1
- import { createHash } from "node:crypto";
2
1
  import { ensureCheckoutFieldsetsAndDefinitions } from "./seed/customFields.js";
3
2
  import { seedCheckoutExamples } from "./seed/examples.js";
4
- import { CHECKOUT_EXPIRY_QUEUE } from "./workers/transaction-expiry.worker.js";
5
3
  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
- }
13
4
  const setup = {
14
5
  async seedDefaults(ctx) {
15
6
  await ensureCheckoutFieldsetsAndDefinitions(ctx.em, {
16
7
  tenantId: ctx.tenantId,
17
8
  organizationId: ctx.organizationId
18
9
  });
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
- });
38
10
  },
39
11
  defaultRoleFeatures: {
40
12
  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 { 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;",
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;AAE1C,MAAM,QAA2B;AAAA,EACtC,MAAM,aAAa,KAAK;AACtB,UAAM,sCAAsC,IAAI,IAAI;AAAA,MAClD,UAAU,IAAI;AAAA,MACd,gBAAgB,IAAI;AAAA,IACtB,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
  }
@@ -14,21 +14,14 @@ 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;
21
17
  const staleTransactions = await findWithDecryption(
22
18
  em,
23
19
  CheckoutTransaction,
24
20
  {
25
21
  status: "processing",
26
- createdAt: { $lt: cutoff },
27
- tenantId,
28
- organizationId
22
+ createdAt: { $lt: cutoff }
29
23
  },
30
- { limit: batchSize, orderBy: { createdAt: "ASC" } },
31
- { tenantId, organizationId }
24
+ { limit: batchSize, orderBy: { createdAt: "ASC" } }
32
25
  );
33
26
  for (const transaction of staleTransactions) {
34
27
  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 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;",
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;AAMjC,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,QAAM,oBAAoB,MAAM;AAAA,IAC9B;AAAA,IACA;AAAA,IACA;AAAA,MACE,QAAQ;AAAA,MACR,WAAW,EAAE,KAAK,OAAO;AAAA,IAC3B;AAAA,IACA,EAAE,OAAO,WAAW,SAAS,EAAE,WAAW,MAAM,EAAE;AAAA,EACpD;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.7-develop.6862.1.c11a64ce0a",
3
+ "version": "0.6.7",
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.7-develop.6862.1.c11a64ce0a",
66
- "@open-mercato/ui": "0.6.7-develop.6862.1.c11a64ce0a",
65
+ "@open-mercato/core": "0.6.7",
66
+ "@open-mercato/ui": "0.6.7",
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.7-develop.6862.1.c11a64ce0a",
71
+ "@open-mercato/shared": "0.6.7",
72
72
  "react": "^19.0.0",
73
73
  "react-dom": "^19.0.0"
74
74
  },
75
75
  "devDependencies": {
76
- "@open-mercato/shared": "0.6.7-develop.6862.1.c11a64ce0a",
76
+ "@open-mercato/shared": "0.6.7",
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.8",
84
- "react-dom": "19.2.8",
85
- "ts-jest": "^29.4.12",
83
+ "react": "19.2.7",
84
+ "react-dom": "19.2.7",
85
+ "ts-jest": "^29.4.11",
86
86
  "typescript": "7.0.2"
87
87
  },
88
88
  "publishConfig": {
@@ -92,6 +92,5 @@
92
92
  "type": "git",
93
93
  "url": "https://github.com/open-mercato/open-mercato",
94
94
  "directory": "packages/checkout"
95
- },
96
- "stableVersion": "0.6.6"
95
+ }
97
96
  }
@@ -1,5 +1,4 @@
1
1
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
2
- import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
3
2
  import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
4
3
  import { checkRateLimit, getClientIp } from '@open-mercato/shared/lib/ratelimit/helpers'
5
4
  import { POST } from '../route'
@@ -299,80 +298,4 @@ describe('POST /api/checkout/pay/[slug]/submit', () => {
299
298
  const payload = await response.json()
300
299
  expect(payload.error).toBe('Invalid request host')
301
300
  })
302
-
303
- it('returns 201 when updateStatus rejects with concurrent_status_update (webhook won the race, payment succeeded)', async () => {
304
- // The webhook already advanced the transaction to 'completed' before the
305
- // submit route's updateStatus call — that 409 must be treated as benign
306
- // and the route must still return 201 with the real terminal transaction.
307
- const terminalTransaction = createTransaction({ status: 'completed', gatewayTransactionId: GATEWAY_TRANSACTION_ID })
308
- ;(findOneWithDecryption as jest.Mock)
309
- .mockResolvedValueOnce(createLink()) // link lookup
310
- .mockResolvedValueOnce(null) // no existing transaction (idempotency pre-check)
311
- .mockResolvedValueOnce(createTransaction()) // transaction after create
312
- .mockResolvedValueOnce(terminalTransaction) // refreshed transaction after conflict
313
-
314
- const conflictError = new CrudHttpError(409, {
315
- code: 'concurrent_status_update',
316
- currentStatus: 'completed',
317
- expectedStatus: 'processing',
318
- requestedStatus: 'processing',
319
- error: '[internal] concurrent update',
320
- })
321
- mockCommandExecute
322
- .mockResolvedValueOnce({ result: { id: TRANSACTION_ID } }) // create
323
- .mockRejectedValueOnce(conflictError) // updateStatus -> conflict
324
-
325
- const response = await POST(
326
- new Request('https://merchant.example/api/checkout/pay/donate/submit', {
327
- method: 'POST',
328
- headers: {
329
- 'content-type': 'application/json',
330
- 'Idempotency-Key': 'race-conflict-key-123',
331
- origin: 'https://merchant.example',
332
- },
333
- body: JSON.stringify({ customerData: {}, acceptedLegalConsents: {}, amount: 1 }),
334
- }),
335
- { params: { slug: 'donate' } },
336
- )
337
-
338
- expect(response.status).toBe(201)
339
- const payload = await response.json()
340
- expect(payload.transactionId).toBe(TRANSACTION_ID)
341
- })
342
-
343
- it('returns 201 when updateStatus rejects with invalid_status_transition (terminal state already set)', async () => {
344
- const terminalTransaction = createTransaction({ status: 'completed', gatewayTransactionId: GATEWAY_TRANSACTION_ID })
345
- ;(findOneWithDecryption as jest.Mock)
346
- .mockResolvedValueOnce(createLink())
347
- .mockResolvedValueOnce(null)
348
- .mockResolvedValueOnce(createTransaction())
349
- .mockResolvedValueOnce(terminalTransaction)
350
-
351
- const invalidTransitionError = new CrudHttpError(409, {
352
- code: 'invalid_status_transition',
353
- currentStatus: 'completed',
354
- requestedStatus: 'processing',
355
- error: '[internal] invalid transition',
356
- })
357
- mockCommandExecute
358
- .mockResolvedValueOnce({ result: { id: TRANSACTION_ID } })
359
- .mockRejectedValueOnce(invalidTransitionError)
360
-
361
- const response = await POST(
362
- new Request('https://merchant.example/api/checkout/pay/donate/submit', {
363
- method: 'POST',
364
- headers: {
365
- 'content-type': 'application/json',
366
- 'Idempotency-Key': 'invalid-transition-key1',
367
- origin: 'https://merchant.example',
368
- },
369
- body: JSON.stringify({ customerData: {}, acceptedLegalConsents: {}, amount: 1 }),
370
- }),
371
- { params: { slug: 'donate' } },
372
- )
373
-
374
- expect(response.status).toBe(201)
375
- const payload = await response.json()
376
- expect(payload.transactionId).toBe(TRANSACTION_ID)
377
- })
378
301
  })
@@ -3,7 +3,7 @@ import type { EntityManager } from '@mikro-orm/postgresql'
3
3
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
4
  import type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'
5
5
  import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
6
- import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
6
+ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
7
7
  import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
8
8
  import type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'
9
9
  import { checkRateLimit } from '@open-mercato/shared/lib/ratelimit/helpers'
@@ -472,10 +472,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ slug: s
472
472
  || link.gatewaySettings?.presentationMode === 'auto'
473
473
  ? link.gatewaySettings.presentationMode
474
474
  : undefined
475
-
476
- let sessionResult: Awaited<ReturnType<typeof paymentGatewayService.createPaymentSession>>
477
475
  try {
478
- sessionResult = await paymentGatewayService.createPaymentSession({
476
+ const sessionResult = await paymentGatewayService.createPaymentSession({
479
477
  providerKey: link.gatewayProviderKey,
480
478
  paymentId: transactionId,
481
479
  idempotencyKey,
@@ -499,61 +497,36 @@ export async function POST(req: Request, { params }: { params: Promise<{ slug: s
499
497
  organizationId: link.organizationId,
500
498
  tenantId: link.tenantId,
501
499
  })
502
- } catch (error) {
503
- // The gateway itself failed — mark the transaction failed and surface 502.
504
500
  await commandBus.execute('checkout.transaction.updateStatus', {
505
501
  input: {
506
502
  id: transaction.id,
507
- status: 'failed',
508
- paymentStatus: transaction.paymentStatus ?? 'failed',
503
+ status: mapGatewayStatusToCheckoutStatus(sessionResult.transaction.unifiedStatus),
504
+ paymentStatus: sessionResult.transaction.unifiedStatus,
505
+ gatewayTransactionId: sessionResult.transaction.id,
509
506
  organizationId: link.organizationId,
510
507
  tenantId: link.tenantId,
511
508
  },
512
509
  ctx,
513
- }).catch(() => undefined)
514
- logger.error('Failed to create payment session', {
515
- linkId: link.id,
516
- transactionId: transaction.id,
517
- providerKey: link.gatewayProviderKey,
518
- err: error,
519
510
  })
520
- throw new CrudHttpError(502, { error: 'checkout.payPage.errors.sessionStart' })
521
- }
522
-
523
- // Update the transaction status to reflect the gateway response.
524
- // If the webhook already raced ahead and completed this transaction,
525
- // updateStatus will throw a 409 conflict — that is correct behavior and
526
- // means the payment succeeded, so we treat it as benign and fall through
527
- // to return the real, already-terminal transaction below.
528
- let statusConflict = false
529
- try {
511
+ } catch (error) {
530
512
  await commandBus.execute('checkout.transaction.updateStatus', {
531
513
  input: {
532
514
  id: transaction.id,
533
- status: mapGatewayStatusToCheckoutStatus(sessionResult.transaction.unifiedStatus),
534
- paymentStatus: sessionResult.transaction.unifiedStatus,
535
- gatewayTransactionId: sessionResult.transaction.id,
515
+ status: 'failed',
516
+ paymentStatus: transaction.paymentStatus ?? 'failed',
536
517
  organizationId: link.organizationId,
537
518
  tenantId: link.tenantId,
538
519
  },
539
520
  ctx,
540
- })
541
- } catch (error) {
542
- const isConflict = isCrudHttpError(error)
543
- && error.status === 409
544
- && (
545
- (error.body as Record<string, unknown> | undefined)?.code === 'concurrent_status_update'
546
- || (error.body as Record<string, unknown> | undefined)?.code === 'invalid_status_transition'
547
- )
548
- if (!isConflict) throw error
549
- logger.info('Transaction status already advanced by concurrent writer — webhook won the race', {
521
+ }).catch(() => undefined)
522
+ logger.error('Failed to create payment session', {
550
523
  linkId: link.id,
551
524
  transactionId: transaction.id,
552
- conflictCode: (error.body as Record<string, unknown> | undefined)?.code,
525
+ providerKey: link.gatewayProviderKey,
526
+ err: error,
553
527
  })
554
- statusConflict = true
528
+ throw new CrudHttpError(502, { error: 'checkout.payPage.errors.sessionStart' })
555
529
  }
556
-
557
530
  const refreshedTransaction = await findOneWithDecryption(em, CheckoutTransaction, {
558
531
  id: transaction.id,
559
532
  organizationId: link.organizationId,
@@ -562,29 +535,26 @@ export async function POST(req: Request, { params }: { params: Promise<{ slug: s
562
535
  if (!refreshedTransaction) {
563
536
  throw new CrudHttpError(404, { error: 'Transaction not found' })
564
537
  }
565
- if (!statusConflict) {
566
- await emitCheckoutEvent('checkout.transaction.sessionStarted', {
567
- transactionId: refreshedTransaction.id,
568
- linkId: refreshedTransaction.linkId,
569
- templateId: link.templateId ?? null,
570
- slug: link.slug,
571
- status: refreshedTransaction.status,
572
- paymentStatus: refreshedTransaction.paymentStatus ?? null,
573
- amount: Number(refreshedTransaction.amount),
574
- currency: refreshedTransaction.currencyCode,
575
- gatewayProvider: link.gatewayProviderKey,
576
- gatewayTransactionId: refreshedTransaction.gatewayTransactionId ?? null,
577
- occurredAt: new Date().toISOString(),
578
- tenantId: link.tenantId,
579
- organizationId: link.organizationId,
580
- }).catch(() => undefined)
581
- }
538
+ await emitCheckoutEvent('checkout.transaction.sessionStarted', {
539
+ transactionId: refreshedTransaction.id,
540
+ linkId: refreshedTransaction.linkId,
541
+ templateId: link.templateId ?? null,
542
+ slug: link.slug,
543
+ status: refreshedTransaction.status,
544
+ paymentStatus: refreshedTransaction.paymentStatus ?? null,
545
+ amount: Number(refreshedTransaction.amount),
546
+ currency: refreshedTransaction.currencyCode,
547
+ gatewayProvider: link.gatewayProviderKey,
548
+ gatewayTransactionId: refreshedTransaction.gatewayTransactionId ?? null,
549
+ occurredAt: new Date().toISOString(),
550
+ tenantId: link.tenantId,
551
+ organizationId: link.organizationId,
552
+ }).catch(() => undefined)
582
553
  return NextResponse.json(
583
554
  await buildSubmitResponse(req, em, link, refreshedTransaction, link.gatewayProviderKey),
584
555
  { status: 201 },
585
556
  )
586
557
  }
587
-
588
558
  return NextResponse.json(await buildSubmitResponse(req, em, link, transaction, link.gatewayProviderKey), { status: 201 })
589
559
  } catch (error) {
590
560
  return handleCheckoutRouteError(error)
@@ -1,15 +1,11 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { findAndCountWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
3
3
  import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
4
- import { findEntityIdsBySearchTokens, type SearchTokenDatabase } from '@open-mercato/shared/lib/search/tokenLookup'
5
4
  import { CheckoutLink, CheckoutTransaction } from '../../data/entities'
6
- import { CHECKOUT_ENTITY_IDS } from '../../lib/constants'
7
5
  import { handleCheckoutRouteError, requireAdminContext, userHasCheckoutFeature } from '../helpers'
8
6
  import { checkoutTag } from '../openapi'
9
7
  import { serializeTransaction } from '../../lib/utils'
10
8
 
11
- const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
12
-
13
9
  export const metadata = {
14
10
  path: '/checkout/transactions',
15
11
  GET: { requireAuth: true, requireFeatures: ['checkout.view'] },
@@ -31,32 +27,12 @@ export async function GET(req: Request) {
31
27
  }
32
28
  if (linkId) where.linkId = linkId
33
29
  if (status) where.status = status
34
- if (search) {
35
- const pattern = `%${escapeLikePattern(search)}%`
36
- // email/first_name/last_name are covered by the checkout encryption map,
37
- // so an ILIKE alone compares the pattern against ciphertext and matches
38
- // nothing once encryption is on. Union it with the token index, which
39
- // stores hashes of the plaintext. Issue #2990.
40
- const searchOr: Record<string, unknown>[] = [
41
- { email: { $ilike: pattern } },
42
- { firstName: { $ilike: pattern } },
43
- { lastName: { $ilike: pattern } },
44
- ]
45
- // `id` is a uuid column: ILIKE against it has no Postgres operator and
46
- // raises 42883, so only an exact id lookup is meaningful here.
47
- if (UUID_PATTERN.test(search)) searchOr.push({ id: search })
48
- const tokenMatch = await findEntityIdsBySearchTokens({
49
- db: em.getKysely<SearchTokenDatabase>(),
50
- entityType: CHECKOUT_ENTITY_IDS.transaction,
51
- query: search,
52
- fields: ['email', 'first_name', 'last_name'],
53
- scope: { tenantId: auth.tenantId, organizationId: auth.orgId },
54
- })
55
- if (tokenMatch.matched && tokenMatch.ids.length) {
56
- searchOr.push({ id: { $in: tokenMatch.ids } })
57
- }
58
- where.$or = searchOr
59
- }
30
+ if (search) where.$or = [
31
+ { email: { $ilike: `%${escapeLikePattern(search)}%` } },
32
+ { firstName: { $ilike: `%${escapeLikePattern(search)}%` } },
33
+ { lastName: { $ilike: `%${escapeLikePattern(search)}%` } },
34
+ { id: { $ilike: `%${escapeLikePattern(search)}%` } },
35
+ ]
60
36
  const [items, total] = await findAndCountWithDecryption(
61
37
  em,
62
38
  CheckoutTransaction,
@@ -1,6 +1,5 @@
1
1
  "use client"
2
2
  import * as React from 'react'
3
- import { extensionPoints } from '@open-mercato/checkout/modules/checkout/extension-points'
4
3
  import Link from 'next/link'
5
4
  import { Plus } from 'lucide-react'
6
5
  import type { ColumnDef } from '@tanstack/react-table'
@@ -198,7 +197,7 @@ export default function CheckoutPayLinksPage() {
198
197
  onFiltersClear={() => { setFilters({}); setPage(1) }}
199
198
  pagination={{ page, pageSize: 25, total, totalPages, onPageChange: setPage }}
200
199
  isLoading={loading}
201
- perspective={{ tableId: extensionPoints.hosts.linksTable.tableId }}
200
+ perspective={{ tableId: 'checkout-links' }}
202
201
  actions={(
203
202
  <Button asChild>
204
203
  <Link href="/backend/checkout/pay-links/create">
@@ -1,6 +1,5 @@
1
1
  "use client"
2
2
  import * as React from 'react'
3
- import { extensionPoints } from '@open-mercato/checkout/modules/checkout/extension-points'
4
3
  import Link from 'next/link'
5
4
  import { Plus } from 'lucide-react'
6
5
  import type { ColumnDef } from '@tanstack/react-table'
@@ -122,7 +121,7 @@ export default function CheckoutTemplatesPage() {
122
121
  onFiltersApply={(next) => { setFilters(next); setPage(1) }}
123
122
  onFiltersClear={() => { setFilters({}); setPage(1) }}
124
123
  pagination={{ page, pageSize: 25, total, totalPages, onPageChange: setPage }}
125
- perspective={{ tableId: extensionPoints.hosts.templatesTable.tableId }}
124
+ perspective={{ tableId: 'checkout-templates' }}
126
125
  actions={(
127
126
  <Button asChild>
128
127
  <Link href="/backend/checkout/templates/create">
@@ -1,6 +1,5 @@
1
1
  "use client"
2
2
  import * as React from 'react'
3
- import { extensionPoints } from '@open-mercato/checkout/modules/checkout/extension-points'
4
3
  import { useSearchParams } from 'next/navigation'
5
4
  import type { ColumnDef } from '@tanstack/react-table'
6
5
  import { useT } from '@open-mercato/shared/lib/i18n/context'
@@ -238,7 +237,7 @@ export default function CheckoutTransactionsPage() {
238
237
  onFiltersApply={(next) => { setFilters(next); setPage(1) }}
239
238
  onFiltersClear={() => { setFilters({}); setPage(1) }}
240
239
  pagination={{ page, pageSize: 25, total, totalPages, onPageChange: setPage }}
241
- perspective={{ tableId: extensionPoints.hosts.transactionsTable.tableId }}
240
+ perspective={{ tableId: 'checkout-transactions' }}
242
241
  rowClickActionIds={['view']}
243
242
  rowActions={(row) => <RowActions items={[{ id: 'view', label: t('checkout.admin.transactions.actions.viewDetail'), href: `/backend/checkout/transactions/${encodeURIComponent(row.id)}` }]} />}
244
243
  />
@@ -520,9 +520,5 @@ registerCommand(updateLinkCommand)
520
520
  registerCommand(deleteLinkCommand)
521
521
 
522
522
  export function serializeLinkRecord(record: CheckoutLink) {
523
- // completionCount is link-only state (templates have no completions), so it is
524
- // added here rather than in the shared template/link serializer. It is the
525
- // observable proof that a terminal transaction was applied exactly once, which
526
- // TC-CHKT-043 asserts and could not previously read from any API.
527
- return { ...serializeTemplateOrLink(record), completionCount: record.completionCount ?? 0 }
523
+ return serializeTemplateOrLink(record)
528
524
  }