@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/checkout/api/pay/[slug]/submit/route.js +28 -43
- package/dist/modules/checkout/api/pay/[slug]/submit/route.js.map +2 -2
- package/dist/modules/checkout/api/transactions/route.js +6 -23
- package/dist/modules/checkout/api/transactions/route.js.map +2 -2
- package/dist/modules/checkout/backend/checkout/pay-links/page.js +1 -2
- package/dist/modules/checkout/backend/checkout/pay-links/page.js.map +2 -2
- package/dist/modules/checkout/backend/checkout/templates/page.js +1 -2
- package/dist/modules/checkout/backend/checkout/templates/page.js.map +2 -2
- package/dist/modules/checkout/backend/checkout/transactions/page.js +1 -2
- package/dist/modules/checkout/backend/checkout/transactions/page.js.map +2 -2
- package/dist/modules/checkout/commands/links.js +1 -1
- package/dist/modules/checkout/commands/links.js.map +2 -2
- package/dist/modules/checkout/commands/transactions.js +13 -45
- package/dist/modules/checkout/commands/transactions.js.map +2 -2
- package/dist/modules/checkout/components/PayPage.js +38 -39
- package/dist/modules/checkout/components/PayPage.js.map +2 -2
- package/dist/modules/checkout/search.js +6 -44
- package/dist/modules/checkout/search.js.map +2 -2
- package/dist/modules/checkout/setup.js +0 -28
- package/dist/modules/checkout/setup.js.map +2 -2
- package/dist/modules/checkout/workers/transaction-expiry.worker.js +2 -9
- package/dist/modules/checkout/workers/transaction-expiry.worker.js.map +2 -2
- package/package.json +9 -10
- package/src/modules/checkout/api/pay/[slug]/submit/__tests__/route.test.ts +0 -77
- package/src/modules/checkout/api/pay/[slug]/submit/route.ts +28 -58
- package/src/modules/checkout/api/transactions/route.ts +6 -30
- package/src/modules/checkout/backend/checkout/pay-links/page.tsx +1 -2
- package/src/modules/checkout/backend/checkout/templates/page.tsx +1 -2
- package/src/modules/checkout/backend/checkout/transactions/page.tsx +1 -2
- package/src/modules/checkout/commands/links.ts +1 -5
- package/src/modules/checkout/commands/transactions.ts +13 -68
- package/src/modules/checkout/components/PayPage.tsx +37 -38
- package/src/modules/checkout/i18n/de.json +0 -1
- package/src/modules/checkout/i18n/en.json +0 -1
- package/src/modules/checkout/i18n/es.json +0 -1
- package/src/modules/checkout/i18n/pl.json +0 -1
- package/src/modules/checkout/search.ts +6 -44
- package/src/modules/checkout/setup.ts +0 -51
- package/src/modules/checkout/workers/transaction-expiry.worker.ts +0 -11
- package/dist/modules/checkout/__integration__/TC-CHKT-042.spec.js +0 -39
- package/dist/modules/checkout/__integration__/TC-CHKT-042.spec.js.map +0 -7
- package/dist/modules/checkout/__integration__/TC-CHKT-043-concurrent-status-update.spec.js +0 -62
- package/dist/modules/checkout/__integration__/TC-CHKT-043-concurrent-status-update.spec.js.map +0 -7
- package/dist/modules/checkout/extension-points.js +0 -68
- package/dist/modules/checkout/extension-points.js.map +0 -7
- package/dist/modules/checkout/lib/transaction-status-machine.js +0 -31
- package/dist/modules/checkout/lib/transaction-status-machine.js.map +0 -7
- package/dist/modules/checkout/subscribers/search-reindex-transaction-created.js +0 -32
- package/dist/modules/checkout/subscribers/search-reindex-transaction-created.js.map +0 -7
- package/src/modules/checkout/__integration__/TC-CHKT-042.spec.ts +0 -41
- package/src/modules/checkout/__integration__/TC-CHKT-043-concurrent-status-update.spec.ts +0 -112
- package/src/modules/checkout/__tests__/search.test.ts +0 -21
- package/src/modules/checkout/__tests__/setup-schedules.test.ts +0 -99
- package/src/modules/checkout/api/transactions/__tests__/route.test.ts +0 -113
- package/src/modules/checkout/commands/__tests__/update-transaction-status.test.ts +0 -363
- package/src/modules/checkout/extension-points.ts +0 -67
- package/src/modules/checkout/i18n/ko.json +0 -463
- package/src/modules/checkout/lib/__tests__/transaction-status-machine.test.ts +0 -152
- package/src/modules/checkout/lib/transaction-status-machine.ts +0 -71
- package/src/modules/checkout/subscribers/__tests__/search-reindex-transaction-created.test.ts +0 -29
- package/src/modules/checkout/subscribers/search-reindex-transaction-created.ts +0 -39
- 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
|
-
|
|
63
|
-
|
|
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.
|
|
79
|
-
subtitle:
|
|
80
|
-
icon: "lucide:
|
|
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 {
|
|
5
|
-
"mappings": "AACA,SAAS,2BAA2B;
|
|
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 {
|
|
5
|
-
"mappings": "AACA,SAAS,
|
|
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
|
|
5
|
-
"mappings": "AAIA,SAAS,0BAA0B;AACnC,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,UAAU,EAAE,MAAM,EAAE,WAAW,qBAAqB,CAAC;AAE1E,MAAM,wBAAwB;AAErC,MAAM,oBAAoB,IAAI,KAAK,KAAK;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CheckoutTransaction } from '../data/entities'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('checkout').child({ component: 'transaction-expiry' })\n\nexport const CHECKOUT_EXPIRY_QUEUE = 'checkout-transaction-expiry'\n\nconst EXPIRY_TIMEOUT_MS = 2 * 60 * 60 * 1000 // 2 hours\n\nexport type CheckoutExpiryJob = {\n batchSize?: number\n}\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
|
|
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
|
|
66
|
-
"@open-mercato/ui": "0.6.7
|
|
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
|
|
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
|
|
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.
|
|
84
|
-
"react-dom": "19.2.
|
|
85
|
-
"ts-jest": "^29.4.
|
|
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
|
|
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:
|
|
508
|
-
paymentStatus: transaction.
|
|
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
|
-
|
|
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:
|
|
534
|
-
paymentStatus:
|
|
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
|
-
|
|
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
|
-
|
|
525
|
+
providerKey: link.gatewayProviderKey,
|
|
526
|
+
err: error,
|
|
553
527
|
})
|
|
554
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
}
|