@open-mercato/checkout 0.6.4-develop.4371.1.8f3030407e → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/checkout/__integration__/TC-CHK-CRUDFORM-001.spec.js +125 -0
- package/dist/modules/checkout/__integration__/TC-CHK-CRUDFORM-001.spec.js.map +7 -0
- package/dist/modules/checkout/__integration__/TC-CHK-CRUDFORM-002.spec.js +139 -0
- package/dist/modules/checkout/__integration__/TC-CHK-CRUDFORM-002.spec.js.map +7 -0
- package/dist/modules/checkout/__integration__/TC-CHKT-008.spec.js +2 -0
- package/dist/modules/checkout/__integration__/TC-CHKT-008.spec.js.map +2 -2
- package/dist/modules/checkout/__integration__/TC-CHKT-039-null-gateway-edit.spec.js +115 -0
- package/dist/modules/checkout/__integration__/TC-CHKT-039-null-gateway-edit.spec.js.map +7 -0
- package/dist/modules/checkout/__integration__/TC-CHKT-039-stale-delete-lock.spec.js +66 -0
- package/dist/modules/checkout/__integration__/TC-CHKT-039-stale-delete-lock.spec.js.map +7 -0
- package/dist/modules/checkout/__integration__/TC-CHKT-040-gateway-settings-select.spec.js +52 -0
- package/dist/modules/checkout/__integration__/TC-CHKT-040-gateway-settings-select.spec.js.map +7 -0
- package/dist/modules/checkout/__integration__/TC-CHKT-041-template-stale-delete-conflict.spec.js +44 -0
- package/dist/modules/checkout/__integration__/TC-CHKT-041-template-stale-delete-conflict.spec.js.map +7 -0
- package/dist/modules/checkout/backend/checkout/pay-links/page.js +9 -0
- package/dist/modules/checkout/backend/checkout/pay-links/page.js.map +2 -2
- package/dist/modules/checkout/backend/checkout/templates/page.js +9 -0
- package/dist/modules/checkout/backend/checkout/templates/page.js.map +2 -2
- package/dist/modules/checkout/commands/links.js +67 -0
- package/dist/modules/checkout/commands/links.js.map +2 -2
- package/dist/modules/checkout/commands/templates.js +69 -2
- package/dist/modules/checkout/commands/templates.js.map +2 -2
- package/dist/modules/checkout/components/GatewaySettingsFields.js +32 -15
- package/dist/modules/checkout/components/GatewaySettingsFields.js.map +2 -2
- package/dist/modules/checkout/components/LinkTemplateForm.js +27 -7
- package/dist/modules/checkout/components/LinkTemplateForm.js.map +2 -2
- package/dist/modules/checkout/data/validators.js +18 -2
- package/dist/modules/checkout/data/validators.js.map +2 -2
- package/dist/modules/checkout/lib/utils.js +26 -5
- package/dist/modules/checkout/lib/utils.js.map +2 -2
- package/jest.config.cjs +2 -0
- package/package.json +7 -8
- package/src/modules/checkout/__integration__/TC-CHK-CRUDFORM-001.spec.ts +158 -0
- package/src/modules/checkout/__integration__/TC-CHK-CRUDFORM-002.spec.ts +171 -0
- package/src/modules/checkout/__integration__/TC-CHKT-008.spec.ts +4 -0
- package/src/modules/checkout/__integration__/TC-CHKT-039-null-gateway-edit.spec.ts +131 -0
- package/src/modules/checkout/__integration__/TC-CHKT-039-stale-delete-lock.spec.ts +98 -0
- package/src/modules/checkout/__integration__/TC-CHKT-040-gateway-settings-select.spec.ts +60 -0
- package/src/modules/checkout/__integration__/TC-CHKT-041-template-stale-delete-conflict.spec.ts +58 -0
- package/src/modules/checkout/backend/checkout/pay-links/page.tsx +8 -0
- package/src/modules/checkout/backend/checkout/templates/page.tsx +8 -0
- package/src/modules/checkout/commands/__tests__/optimistic-lock.test.ts +261 -0
- package/src/modules/checkout/commands/__tests__/redo-coverage.test.ts +21 -0
- package/src/modules/checkout/commands/links.ts +67 -0
- package/src/modules/checkout/commands/templates.ts +74 -2
- package/src/modules/checkout/components/GatewaySettingsFields.tsx +40 -18
- package/src/modules/checkout/components/LinkTemplateForm.tsx +27 -7
- package/src/modules/checkout/data/__tests__/validators.test.ts +66 -1
- package/src/modules/checkout/data/validators.ts +18 -2
- package/src/modules/checkout/lib/__tests__/utils.test.ts +112 -0
- package/src/modules/checkout/lib/utils.ts +41 -5
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:checkout] found
|
|
1
|
+
[build:checkout] found 153 entry points
|
|
2
2
|
[build:checkout] built successfully
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { getAuthToken } from "@open-mercato/core/modules/core/__integration__/helpers/api";
|
|
3
|
+
import {
|
|
4
|
+
assertScalarFieldsPersisted,
|
|
5
|
+
skipIfCrudFormExtensionTestsDisabled
|
|
6
|
+
} from "@open-mercato/core/helpers/integration/crudFormPersistence";
|
|
7
|
+
import {
|
|
8
|
+
createPriceListTemplateInput,
|
|
9
|
+
createTemplateFixture,
|
|
10
|
+
deleteCheckoutEntityIfExists,
|
|
11
|
+
readTemplate,
|
|
12
|
+
updateTemplate
|
|
13
|
+
} from "./helpers/fixtures.js";
|
|
14
|
+
test.describe("TC-CHK-CRUDFORM-001: pay-link template CrudForm persists scalars, array + custom fields", () => {
|
|
15
|
+
test.beforeAll(() => {
|
|
16
|
+
skipIfCrudFormExtensionTestsDisabled();
|
|
17
|
+
});
|
|
18
|
+
test("round-trips scalars, priceListItems array, and custom fields on create and update", async ({ request }) => {
|
|
19
|
+
const token = await getAuthToken(request);
|
|
20
|
+
const stamp = Date.now();
|
|
21
|
+
let templateId = null;
|
|
22
|
+
try {
|
|
23
|
+
const createInput = createPriceListTemplateInput({
|
|
24
|
+
name: `QA CRUDFORM Template ${stamp}`,
|
|
25
|
+
title: `QA CRUDFORM Template Title ${stamp}`,
|
|
26
|
+
subtitle: "Original template subtitle",
|
|
27
|
+
description: "Original template description",
|
|
28
|
+
priceListItems: [
|
|
29
|
+
{ id: "tier-basic", description: "Basic tier", amount: 19.99, currencyCode: "USD" },
|
|
30
|
+
{ id: "tier-pro", description: "Pro tier", amount: 49.99, currencyCode: "USD" }
|
|
31
|
+
],
|
|
32
|
+
collectCustomerDetails: true,
|
|
33
|
+
displayCustomFieldsOnPage: true,
|
|
34
|
+
customFieldsetCode: "service_package",
|
|
35
|
+
maxCompletions: 25,
|
|
36
|
+
status: "draft",
|
|
37
|
+
customFields: {
|
|
38
|
+
service_deliverables: "Discovery workshop and implementation memo",
|
|
39
|
+
delivery_timeline: "Within 5 business days",
|
|
40
|
+
support_contact: "ops@example.test"
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
templateId = await createTemplateFixture(request, token, createInput);
|
|
44
|
+
const afterCreate = await readTemplate(request, token, templateId);
|
|
45
|
+
assertScalarFieldsPersisted(
|
|
46
|
+
afterCreate,
|
|
47
|
+
{
|
|
48
|
+
name: `QA CRUDFORM Template ${stamp}`,
|
|
49
|
+
title: `QA CRUDFORM Template Title ${stamp}`,
|
|
50
|
+
subtitle: "Original template subtitle",
|
|
51
|
+
description: "Original template description",
|
|
52
|
+
pricingMode: "price_list",
|
|
53
|
+
priceListItems: [
|
|
54
|
+
{ id: "tier-basic", description: "Basic tier", amount: 19.99, currencyCode: "USD" },
|
|
55
|
+
{ id: "tier-pro", description: "Pro tier", amount: 49.99, currencyCode: "USD" }
|
|
56
|
+
],
|
|
57
|
+
collectCustomerDetails: true,
|
|
58
|
+
displayCustomFieldsOnPage: true,
|
|
59
|
+
customFieldsetCode: "service_package",
|
|
60
|
+
maxCompletions: 25,
|
|
61
|
+
status: "draft"
|
|
62
|
+
},
|
|
63
|
+
"after-create"
|
|
64
|
+
);
|
|
65
|
+
expect(afterCreate.customFields, "after-create custom fields should persist").toMatchObject({
|
|
66
|
+
service_deliverables: "Discovery workshop and implementation memo",
|
|
67
|
+
delivery_timeline: "Within 5 business days",
|
|
68
|
+
support_contact: "ops@example.test"
|
|
69
|
+
});
|
|
70
|
+
const updatePayload = {
|
|
71
|
+
name: `QA CRUDFORM Template ${stamp} EDITED`,
|
|
72
|
+
title: `QA CRUDFORM Template Title ${stamp} EDITED`,
|
|
73
|
+
subtitle: "Updated template subtitle",
|
|
74
|
+
description: "Updated template description",
|
|
75
|
+
pricingMode: "price_list",
|
|
76
|
+
priceListItems: [
|
|
77
|
+
{ id: "tier-solo", description: "Solo tier", amount: 99, currencyCode: "USD" }
|
|
78
|
+
],
|
|
79
|
+
gatewayProviderKey: "mock",
|
|
80
|
+
collectCustomerDetails: false,
|
|
81
|
+
displayCustomFieldsOnPage: false,
|
|
82
|
+
customFieldsetCode: "service_package",
|
|
83
|
+
maxCompletions: 50,
|
|
84
|
+
status: "active",
|
|
85
|
+
customFields: {
|
|
86
|
+
delivery_timeline: "Within 2 business days",
|
|
87
|
+
session_format: "Remote video call"
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const updateResponse = await updateTemplate(request, token, templateId, updatePayload);
|
|
91
|
+
expect(
|
|
92
|
+
updateResponse.ok(),
|
|
93
|
+
`update template failed: ${updateResponse.status()}`
|
|
94
|
+
).toBeTruthy();
|
|
95
|
+
const afterUpdate = await readTemplate(request, token, templateId);
|
|
96
|
+
assertScalarFieldsPersisted(
|
|
97
|
+
afterUpdate,
|
|
98
|
+
{
|
|
99
|
+
name: `QA CRUDFORM Template ${stamp} EDITED`,
|
|
100
|
+
title: `QA CRUDFORM Template Title ${stamp} EDITED`,
|
|
101
|
+
subtitle: "Updated template subtitle",
|
|
102
|
+
description: "Updated template description",
|
|
103
|
+
pricingMode: "price_list",
|
|
104
|
+
priceListItems: [
|
|
105
|
+
{ id: "tier-solo", description: "Solo tier", amount: 99, currencyCode: "USD" }
|
|
106
|
+
],
|
|
107
|
+
collectCustomerDetails: false,
|
|
108
|
+
displayCustomFieldsOnPage: false,
|
|
109
|
+
maxCompletions: 50,
|
|
110
|
+
status: "active"
|
|
111
|
+
},
|
|
112
|
+
"after-update"
|
|
113
|
+
);
|
|
114
|
+
expect(afterUpdate.customFields, "after-update custom fields should persist + retain omitted keys").toMatchObject({
|
|
115
|
+
delivery_timeline: "Within 2 business days",
|
|
116
|
+
session_format: "Remote video call",
|
|
117
|
+
service_deliverables: "Discovery workshop and implementation memo",
|
|
118
|
+
support_contact: "ops@example.test"
|
|
119
|
+
});
|
|
120
|
+
} finally {
|
|
121
|
+
await deleteCheckoutEntityIfExists(request, token, "templates", templateId);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
//# sourceMappingURL=TC-CHK-CRUDFORM-001.spec.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/checkout/__integration__/TC-CHK-CRUDFORM-001.spec.ts"],
|
|
4
|
+
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken } from '@open-mercato/core/modules/core/__integration__/helpers/api'\nimport {\n assertScalarFieldsPersisted,\n skipIfCrudFormExtensionTestsDisabled,\n} from '@open-mercato/core/helpers/integration/crudFormPersistence'\nimport {\n createPriceListTemplateInput,\n createTemplateFixture,\n deleteCheckoutEntityIfExists,\n readTemplate,\n updateTemplate,\n type CheckoutLinkInput,\n} from './helpers/fixtures'\n\n/**\n * TC-CHK-CRUDFORM-001: pay-link template CrudForm persists scalars, a price-list array\n * + custom fields (#2466 / #2566).\n *\n * The checkout template surface is hand-written (command bus + bespoke serializer), so it\n * does NOT fit `runCrudFormRoundTrip` from the sweep harness:\n * - writes go through the collection POST (`/api/checkout/templates`) but updates/deletes go\n * through the RESTful detail route (`/api/checkout/templates/[id]`), not a `?id=` collection\n * route;\n * - the serializer returns camelCase fields (not the makeCrud snake_case shape);\n * - custom fields come back as a top-level `customFields` object (not an array / `customValues`).\n *\n * This spec therefore drives the canonical create \u2192 read-back \u2192 assert \u2192 update \u2192 read-back \u2192\n * assert \u2192 delete cycle inline using the checkout integration fixtures, while reusing the sweep\n * harness gate (`skipIfCrudFormExtensionTestsDisabled`) and scalar assertion helper. It proves\n * every field type the template CrudForm edits round-trips: string scalars, enums (pricingMode,\n * status), booleans, an integer, the `priceListItems` array, and default-seeded custom fields.\n *\n * Verified contract:\n * - Read-back uses the detail GET (the list route does not filter by `?ids=`/`?id=`).\n * - Custom fields submit as a `customFields` object and return under `record.customFields`.\n * - PUT is a partial update \u2014 omitted custom fields are retained.\n * - Self-contained: custom-field definitions are seeded by `seedDefaults`; the template is the\n * only fixture and is deleted in `finally`.\n *\n * Gated by `OM_INTEGRATION_CRUDFORM_EXTENSION_TESTS_DISABLED` (default off \u2192 runs).\n */\ntest.describe('TC-CHK-CRUDFORM-001: pay-link template CrudForm persists scalars, array + custom fields', () => {\n test.beforeAll(() => {\n skipIfCrudFormExtensionTestsDisabled()\n })\n\n test('round-trips scalars, priceListItems array, and custom fields on create and update', async ({ request }) => {\n const token = await getAuthToken(request)\n const stamp = Date.now()\n let templateId: string | null = null\n\n try {\n const createInput: CheckoutLinkInput = createPriceListTemplateInput({\n name: `QA CRUDFORM Template ${stamp}`,\n title: `QA CRUDFORM Template Title ${stamp}`,\n subtitle: 'Original template subtitle',\n description: 'Original template description',\n priceListItems: [\n { id: 'tier-basic', description: 'Basic tier', amount: 19.99, currencyCode: 'USD' },\n { id: 'tier-pro', description: 'Pro tier', amount: 49.99, currencyCode: 'USD' },\n ],\n collectCustomerDetails: true,\n displayCustomFieldsOnPage: true,\n customFieldsetCode: 'service_package',\n maxCompletions: 25,\n status: 'draft',\n customFields: {\n service_deliverables: 'Discovery workshop and implementation memo',\n delivery_timeline: 'Within 5 business days',\n support_contact: 'ops@example.test',\n },\n })\n templateId = await createTemplateFixture(request, token, createInput)\n\n const afterCreate = await readTemplate(request, token, templateId)\n assertScalarFieldsPersisted(\n afterCreate,\n {\n name: `QA CRUDFORM Template ${stamp}`,\n title: `QA CRUDFORM Template Title ${stamp}`,\n subtitle: 'Original template subtitle',\n description: 'Original template description',\n pricingMode: 'price_list',\n priceListItems: [\n { id: 'tier-basic', description: 'Basic tier', amount: 19.99, currencyCode: 'USD' },\n { id: 'tier-pro', description: 'Pro tier', amount: 49.99, currencyCode: 'USD' },\n ],\n collectCustomerDetails: true,\n displayCustomFieldsOnPage: true,\n customFieldsetCode: 'service_package',\n maxCompletions: 25,\n status: 'draft',\n },\n 'after-create',\n )\n expect(afterCreate.customFields, 'after-create custom fields should persist').toMatchObject({\n service_deliverables: 'Discovery workshop and implementation memo',\n delivery_timeline: 'Within 5 business days',\n support_contact: 'ops@example.test',\n })\n\n const updatePayload: Partial<CheckoutLinkInput> = {\n name: `QA CRUDFORM Template ${stamp} EDITED`,\n title: `QA CRUDFORM Template Title ${stamp} EDITED`,\n subtitle: 'Updated template subtitle',\n description: 'Updated template description',\n pricingMode: 'price_list',\n priceListItems: [\n { id: 'tier-solo', description: 'Solo tier', amount: 99, currencyCode: 'USD' },\n ],\n gatewayProviderKey: 'mock',\n collectCustomerDetails: false,\n displayCustomFieldsOnPage: false,\n customFieldsetCode: 'service_package',\n maxCompletions: 50,\n status: 'active',\n customFields: {\n delivery_timeline: 'Within 2 business days',\n session_format: 'Remote video call',\n },\n }\n const updateResponse = await updateTemplate(request, token, templateId, updatePayload)\n expect(\n updateResponse.ok(),\n `update template failed: ${updateResponse.status()}`,\n ).toBeTruthy()\n\n const afterUpdate = await readTemplate(request, token, templateId)\n assertScalarFieldsPersisted(\n afterUpdate,\n {\n name: `QA CRUDFORM Template ${stamp} EDITED`,\n title: `QA CRUDFORM Template Title ${stamp} EDITED`,\n subtitle: 'Updated template subtitle',\n description: 'Updated template description',\n pricingMode: 'price_list',\n priceListItems: [\n { id: 'tier-solo', description: 'Solo tier', amount: 99, currencyCode: 'USD' },\n ],\n collectCustomerDetails: false,\n displayCustomFieldsOnPage: false,\n maxCompletions: 50,\n status: 'active',\n },\n 'after-update',\n )\n expect(afterUpdate.customFields, 'after-update custom fields should persist + retain omitted keys').toMatchObject({\n delivery_timeline: 'Within 2 business days',\n session_format: 'Remote video call',\n service_deliverables: 'Discovery workshop and implementation memo',\n support_contact: 'ops@example.test',\n })\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'templates', templateId)\n }\n })\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { getAuthToken } from "@open-mercato/core/modules/core/__integration__/helpers/api";
|
|
3
|
+
import {
|
|
4
|
+
assertScalarFieldsPersisted,
|
|
5
|
+
skipIfCrudFormExtensionTestsDisabled
|
|
6
|
+
} from "@open-mercato/core/helpers/integration/crudFormPersistence";
|
|
7
|
+
import {
|
|
8
|
+
createFixedTemplateInput,
|
|
9
|
+
createLinkFixture,
|
|
10
|
+
createTemplateFixture,
|
|
11
|
+
deleteCheckoutEntityIfExists,
|
|
12
|
+
readLink,
|
|
13
|
+
updateLink
|
|
14
|
+
} from "./helpers/fixtures.js";
|
|
15
|
+
test.describe("TC-CHK-CRUDFORM-002: pay-link CrudForm persists scalars, templateId + slug + custom fields", () => {
|
|
16
|
+
test.beforeAll(() => {
|
|
17
|
+
skipIfCrudFormExtensionTestsDisabled();
|
|
18
|
+
});
|
|
19
|
+
test("round-trips scalars, templateId, slug, and custom fields on create and update", async ({ request }) => {
|
|
20
|
+
const token = await getAuthToken(request);
|
|
21
|
+
const stamp = Date.now();
|
|
22
|
+
let templateId = null;
|
|
23
|
+
let linkId = null;
|
|
24
|
+
try {
|
|
25
|
+
templateId = await createTemplateFixture(
|
|
26
|
+
request,
|
|
27
|
+
token,
|
|
28
|
+
createFixedTemplateInput({ name: `QA CRUDFORM Link Template ${stamp}` })
|
|
29
|
+
);
|
|
30
|
+
const createInput = createFixedTemplateInput({
|
|
31
|
+
name: `QA CRUDFORM Link ${stamp}`,
|
|
32
|
+
title: `QA CRUDFORM Link Title ${stamp}`,
|
|
33
|
+
subtitle: "Original link subtitle",
|
|
34
|
+
description: "Original link description",
|
|
35
|
+
fixedPriceAmount: 49.99,
|
|
36
|
+
fixedPriceCurrencyCode: "USD",
|
|
37
|
+
fixedPriceIncludesTax: true,
|
|
38
|
+
fixedPriceOriginalAmount: 69.99,
|
|
39
|
+
collectCustomerDetails: true,
|
|
40
|
+
displayCustomFieldsOnPage: true,
|
|
41
|
+
customFieldsetCode: "service_package",
|
|
42
|
+
maxCompletions: 10,
|
|
43
|
+
status: "draft",
|
|
44
|
+
templateId,
|
|
45
|
+
slug: `qa-crudform-link-${stamp}`,
|
|
46
|
+
customFields: {
|
|
47
|
+
service_deliverables: "One-on-one consultation call",
|
|
48
|
+
delivery_timeline: "Same week",
|
|
49
|
+
support_contact: "help@example.test"
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const created = await createLinkFixture(request, token, createInput);
|
|
53
|
+
linkId = created.id;
|
|
54
|
+
const afterCreate = await readLink(request, token, linkId);
|
|
55
|
+
assertScalarFieldsPersisted(
|
|
56
|
+
afterCreate,
|
|
57
|
+
{
|
|
58
|
+
name: `QA CRUDFORM Link ${stamp}`,
|
|
59
|
+
title: `QA CRUDFORM Link Title ${stamp}`,
|
|
60
|
+
subtitle: "Original link subtitle",
|
|
61
|
+
description: "Original link description",
|
|
62
|
+
pricingMode: "fixed",
|
|
63
|
+
fixedPriceAmount: 49.99,
|
|
64
|
+
fixedPriceCurrencyCode: "USD",
|
|
65
|
+
fixedPriceIncludesTax: true,
|
|
66
|
+
fixedPriceOriginalAmount: 69.99,
|
|
67
|
+
collectCustomerDetails: true,
|
|
68
|
+
displayCustomFieldsOnPage: true,
|
|
69
|
+
customFieldsetCode: "service_package",
|
|
70
|
+
maxCompletions: 10,
|
|
71
|
+
status: "draft",
|
|
72
|
+
templateId,
|
|
73
|
+
slug: created.slug
|
|
74
|
+
},
|
|
75
|
+
"after-create"
|
|
76
|
+
);
|
|
77
|
+
expect(afterCreate.customFields, "after-create custom fields should persist").toMatchObject({
|
|
78
|
+
service_deliverables: "One-on-one consultation call",
|
|
79
|
+
delivery_timeline: "Same week",
|
|
80
|
+
support_contact: "help@example.test"
|
|
81
|
+
});
|
|
82
|
+
const updatePayload = {
|
|
83
|
+
name: `QA CRUDFORM Link ${stamp} EDITED`,
|
|
84
|
+
title: `QA CRUDFORM Link Title ${stamp} EDITED`,
|
|
85
|
+
subtitle: "Updated link subtitle",
|
|
86
|
+
description: "Updated link description",
|
|
87
|
+
pricingMode: "fixed",
|
|
88
|
+
fixedPriceAmount: 89.5,
|
|
89
|
+
fixedPriceCurrencyCode: "USD",
|
|
90
|
+
fixedPriceIncludesTax: false,
|
|
91
|
+
fixedPriceOriginalAmount: 129.99,
|
|
92
|
+
gatewayProviderKey: "mock",
|
|
93
|
+
collectCustomerDetails: false,
|
|
94
|
+
displayCustomFieldsOnPage: false,
|
|
95
|
+
customFieldsetCode: "service_package",
|
|
96
|
+
maxCompletions: 20,
|
|
97
|
+
status: "active",
|
|
98
|
+
customFields: {
|
|
99
|
+
delivery_timeline: "Next business day",
|
|
100
|
+
session_format: "In person"
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const updateResponse = await updateLink(request, token, linkId, updatePayload);
|
|
104
|
+
expect(updateResponse.ok(), `update link failed: ${updateResponse.status()}`).toBeTruthy();
|
|
105
|
+
const afterUpdate = await readLink(request, token, linkId);
|
|
106
|
+
assertScalarFieldsPersisted(
|
|
107
|
+
afterUpdate,
|
|
108
|
+
{
|
|
109
|
+
name: `QA CRUDFORM Link ${stamp} EDITED`,
|
|
110
|
+
title: `QA CRUDFORM Link Title ${stamp} EDITED`,
|
|
111
|
+
subtitle: "Updated link subtitle",
|
|
112
|
+
description: "Updated link description",
|
|
113
|
+
pricingMode: "fixed",
|
|
114
|
+
fixedPriceAmount: 89.5,
|
|
115
|
+
fixedPriceCurrencyCode: "USD",
|
|
116
|
+
fixedPriceIncludesTax: false,
|
|
117
|
+
fixedPriceOriginalAmount: 129.99,
|
|
118
|
+
collectCustomerDetails: false,
|
|
119
|
+
displayCustomFieldsOnPage: false,
|
|
120
|
+
maxCompletions: 20,
|
|
121
|
+
status: "active",
|
|
122
|
+
templateId,
|
|
123
|
+
slug: created.slug
|
|
124
|
+
},
|
|
125
|
+
"after-update"
|
|
126
|
+
);
|
|
127
|
+
expect(afterUpdate.customFields, "after-update custom fields should persist + retain omitted keys").toMatchObject({
|
|
128
|
+
delivery_timeline: "Next business day",
|
|
129
|
+
session_format: "In person",
|
|
130
|
+
service_deliverables: "One-on-one consultation call",
|
|
131
|
+
support_contact: "help@example.test"
|
|
132
|
+
});
|
|
133
|
+
} finally {
|
|
134
|
+
await deleteCheckoutEntityIfExists(request, token, "links", linkId);
|
|
135
|
+
await deleteCheckoutEntityIfExists(request, token, "templates", templateId);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
//# sourceMappingURL=TC-CHK-CRUDFORM-002.spec.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/checkout/__integration__/TC-CHK-CRUDFORM-002.spec.ts"],
|
|
4
|
+
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken } from '@open-mercato/core/modules/core/__integration__/helpers/api'\nimport {\n assertScalarFieldsPersisted,\n skipIfCrudFormExtensionTestsDisabled,\n} from '@open-mercato/core/helpers/integration/crudFormPersistence'\nimport {\n createFixedTemplateInput,\n createLinkFixture,\n createTemplateFixture,\n deleteCheckoutEntityIfExists,\n readLink,\n updateLink,\n type CheckoutLinkInput,\n} from './helpers/fixtures'\n\n/**\n * TC-CHK-CRUDFORM-002: pay-link CrudForm persists scalars, the templateId FK, slug\n * + custom fields (#2466 / #2566).\n *\n * The checkout pay-link surface is hand-written (command bus + bespoke serializer) and so does\n * NOT fit `runCrudFormRoundTrip` \u2014 see the note in TC-CHK-CRUDFORM-001. This spec drives the\n * canonical create \u2192 read-back \u2192 assert \u2192 update \u2192 read-back \u2192 assert \u2192 delete cycle inline,\n * reusing the sweep harness gate + scalar assertion helper.\n *\n * It covers the link-specific fields on top of the shared content fields: the `templateId`\n * foreign key and the generated `slug`, plus fixed-price money scalars, enums, booleans, an\n * integer, and default-seeded custom fields. Every explicitly-submitted field overrides the\n * source template (`pickExplicitParsedOverrides`), so the round-trip asserts the link's own\n * values, not inherited ones.\n *\n * Verified contract:\n * - Read-back uses the detail GET (the list route does not filter by `?ids=`/`?id=`).\n * - Custom fields submit as a `customFields` object and return under `record.customFields`.\n * - PUT is a partial update \u2014 omitted custom fields are retained; the slug is preserved when the\n * name/title change (it recomputes from the existing slug).\n * - Self-contained: creates a throwaway template (no custom fields, so the link's own custom\n * fields are not affected by template copy) and deletes both fixtures in `finally`.\n *\n * Gated by `OM_INTEGRATION_CRUDFORM_EXTENSION_TESTS_DISABLED` (default off \u2192 runs).\n */\ntest.describe('TC-CHK-CRUDFORM-002: pay-link CrudForm persists scalars, templateId + slug + custom fields', () => {\n test.beforeAll(() => {\n skipIfCrudFormExtensionTestsDisabled()\n })\n\n test('round-trips scalars, templateId, slug, and custom fields on create and update', async ({ request }) => {\n const token = await getAuthToken(request)\n const stamp = Date.now()\n let templateId: string | null = null\n let linkId: string | null = null\n\n try {\n templateId = await createTemplateFixture(\n request,\n token,\n createFixedTemplateInput({ name: `QA CRUDFORM Link Template ${stamp}` }),\n )\n\n const createInput: CheckoutLinkInput = createFixedTemplateInput({\n name: `QA CRUDFORM Link ${stamp}`,\n title: `QA CRUDFORM Link Title ${stamp}`,\n subtitle: 'Original link subtitle',\n description: 'Original link description',\n fixedPriceAmount: 49.99,\n fixedPriceCurrencyCode: 'USD',\n fixedPriceIncludesTax: true,\n fixedPriceOriginalAmount: 69.99,\n collectCustomerDetails: true,\n displayCustomFieldsOnPage: true,\n customFieldsetCode: 'service_package',\n maxCompletions: 10,\n status: 'draft',\n templateId,\n slug: `qa-crudform-link-${stamp}`,\n customFields: {\n service_deliverables: 'One-on-one consultation call',\n delivery_timeline: 'Same week',\n support_contact: 'help@example.test',\n },\n })\n const created = await createLinkFixture(request, token, createInput)\n linkId = created.id\n\n const afterCreate = await readLink(request, token, linkId)\n assertScalarFieldsPersisted(\n afterCreate,\n {\n name: `QA CRUDFORM Link ${stamp}`,\n title: `QA CRUDFORM Link Title ${stamp}`,\n subtitle: 'Original link subtitle',\n description: 'Original link description',\n pricingMode: 'fixed',\n fixedPriceAmount: 49.99,\n fixedPriceCurrencyCode: 'USD',\n fixedPriceIncludesTax: true,\n fixedPriceOriginalAmount: 69.99,\n collectCustomerDetails: true,\n displayCustomFieldsOnPage: true,\n customFieldsetCode: 'service_package',\n maxCompletions: 10,\n status: 'draft',\n templateId,\n slug: created.slug,\n },\n 'after-create',\n )\n expect(afterCreate.customFields, 'after-create custom fields should persist').toMatchObject({\n service_deliverables: 'One-on-one consultation call',\n delivery_timeline: 'Same week',\n support_contact: 'help@example.test',\n })\n\n const updatePayload: Partial<CheckoutLinkInput> = {\n name: `QA CRUDFORM Link ${stamp} EDITED`,\n title: `QA CRUDFORM Link Title ${stamp} EDITED`,\n subtitle: 'Updated link subtitle',\n description: 'Updated link description',\n pricingMode: 'fixed',\n fixedPriceAmount: 89.5,\n fixedPriceCurrencyCode: 'USD',\n fixedPriceIncludesTax: false,\n fixedPriceOriginalAmount: 129.99,\n gatewayProviderKey: 'mock',\n collectCustomerDetails: false,\n displayCustomFieldsOnPage: false,\n customFieldsetCode: 'service_package',\n maxCompletions: 20,\n status: 'active',\n customFields: {\n delivery_timeline: 'Next business day',\n session_format: 'In person',\n },\n }\n const updateResponse = await updateLink(request, token, linkId, updatePayload)\n expect(updateResponse.ok(), `update link failed: ${updateResponse.status()}`).toBeTruthy()\n\n const afterUpdate = await readLink(request, token, linkId)\n assertScalarFieldsPersisted(\n afterUpdate,\n {\n name: `QA CRUDFORM Link ${stamp} EDITED`,\n title: `QA CRUDFORM Link Title ${stamp} EDITED`,\n subtitle: 'Updated link subtitle',\n description: 'Updated link description',\n pricingMode: 'fixed',\n fixedPriceAmount: 89.5,\n fixedPriceCurrencyCode: 'USD',\n fixedPriceIncludesTax: false,\n fixedPriceOriginalAmount: 129.99,\n collectCustomerDetails: false,\n displayCustomFieldsOnPage: false,\n maxCompletions: 20,\n status: 'active',\n templateId,\n slug: created.slug,\n },\n 'after-update',\n )\n expect(afterUpdate.customFields, 'after-update custom fields should persist + retain omitted keys').toMatchObject({\n delivery_timeline: 'Next business day',\n session_format: 'In person',\n service_deliverables: 'One-on-one consultation call',\n support_contact: 'help@example.test',\n })\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'links', linkId)\n await deleteCheckoutEntityIfExists(request, token, 'templates', templateId)\n }\n })\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
waitForCheckoutStatus
|
|
14
14
|
} from "./helpers/fixtures.js";
|
|
15
15
|
test.describe("TC-CHKT-008: Attempt update on locked link, verify 422", () => {
|
|
16
|
+
test.setTimeout(12e4);
|
|
16
17
|
test("rejects link edits after the first transaction locks the record", async ({ request }) => {
|
|
18
|
+
test.slow();
|
|
17
19
|
let token = null;
|
|
18
20
|
let linkId = null;
|
|
19
21
|
let transactionId = null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/checkout/__integration__/TC-CHKT-008.spec.ts"],
|
|
4
|
-
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken } from '@open-mercato/core/modules/core/__integration__/helpers/api'\nimport {\n createCustomerData,\n createFixedTemplateInput,\n createLinkFixture,\n deleteCheckoutEntityIfExists,\n findGatewayTransactionIdForCheckout,\n readGatewayTransaction,\n sendMockGatewayWebhook,\n submitPayLink,\n updateLink,\n waitForCheckoutStatus,\n} from './helpers/fixtures'\n\ntest.describe('TC-CHKT-008: Attempt update on locked link, verify 422', () => {\n test('rejects link edits after the first transaction locks the record', async ({ request }) => {\n let token: string | null = null\n let linkId: string | null = null\n let transactionId: string | null = null\n\n try {\n token = await getAuthToken(request)\n const link = await createLinkFixture(request, token, {\n ...createFixedTemplateInput({\n status: 'active',\n gatewayProviderKey: 'mock_processing',\n }),\n })\n linkId = link.id\n\n const submitResponse = await submitPayLink(request, link.slug, {\n customerData: createCustomerData(),\n acceptedLegalConsents: {},\n amount: 49.99,\n })\n expect(submitResponse.status()).toBe(201)\n const submitBody = await submitResponse.json()\n transactionId = typeof submitBody.transactionId === 'string' ? submitBody.transactionId : null\n\n const updateResponse = await updateLink(request, token, link.id, {\n title: 'Should fail after lock',\n })\n expect(updateResponse.status()).toBe(422)\n\n const body = await updateResponse.json()\n expect(body.error).toContain('cannot be edited')\n } finally {\n if (token && transactionId) {\n const gatewayTransactionId = await findGatewayTransactionIdForCheckout(request, token, transactionId)\n const gatewayTransaction = await readGatewayTransaction(request, token, gatewayTransactionId)\n if (gatewayTransaction.providerSessionId) {\n await sendMockGatewayWebhook(request, token, gatewayTransaction.providerSessionId, 'captured', 49.99, {\n providerKey: 'mock_processing',\n })\n await waitForCheckoutStatus(request, token, transactionId, 'completed')\n }\n }\n await deleteCheckoutEntityIfExists(request, token, 'links', linkId)\n }\n })\n})\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,KAAK,SAAS,0DAA0D,MAAM;AAC5E,OAAK,mEAAmE,OAAO,EAAE,QAAQ,MAAM;AAC7F,QAAI,QAAuB;AAC3B,QAAI,SAAwB;AAC5B,QAAI,gBAA+B;AAEnC,QAAI;AACF,cAAQ,MAAM,aAAa,OAAO;AAClC,YAAM,OAAO,MAAM,kBAAkB,SAAS,OAAO;AAAA,QACnD,GAAG,yBAAyB;AAAA,UAC1B,QAAQ;AAAA,UACR,oBAAoB;AAAA,QACtB,CAAC;AAAA,MACH,CAAC;AACD,eAAS,KAAK;AAEd,YAAM,iBAAiB,MAAM,cAAc,SAAS,KAAK,MAAM;AAAA,QAC7D,cAAc,mBAAmB;AAAA,QACjC,uBAAuB,CAAC;AAAA,QACxB,QAAQ;AAAA,MACV,CAAC;AACD,aAAO,eAAe,OAAO,CAAC,EAAE,KAAK,GAAG;AACxC,YAAM,aAAa,MAAM,eAAe,KAAK;AAC7C,sBAAgB,OAAO,WAAW,kBAAkB,WAAW,WAAW,gBAAgB;AAE1F,YAAM,iBAAiB,MAAM,WAAW,SAAS,OAAO,KAAK,IAAI;AAAA,QAC/D,OAAO;AAAA,MACT,CAAC;AACD,aAAO,eAAe,OAAO,CAAC,EAAE,KAAK,GAAG;AAExC,YAAM,OAAO,MAAM,eAAe,KAAK;AACvC,aAAO,KAAK,KAAK,EAAE,UAAU,kBAAkB;AAAA,IACjD,UAAE;AACA,UAAI,SAAS,eAAe;AAC1B,cAAM,uBAAuB,MAAM,oCAAoC,SAAS,OAAO,aAAa;AACpG,cAAM,qBAAqB,MAAM,uBAAuB,SAAS,OAAO,oBAAoB;AAC5F,YAAI,mBAAmB,mBAAmB;AACxC,gBAAM,uBAAuB,SAAS,OAAO,mBAAmB,mBAAmB,YAAY,OAAO;AAAA,YACpG,aAAa;AAAA,UACf,CAAC;AACD,gBAAM,sBAAsB,SAAS,OAAO,eAAe,WAAW;AAAA,QACxE;AAAA,MACF;AACA,YAAM,6BAA6B,SAAS,OAAO,SAAS,MAAM;AAAA,IACpE;AAAA,EACF,CAAC;AACH,CAAC;",
|
|
4
|
+
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken } from '@open-mercato/core/modules/core/__integration__/helpers/api'\nimport {\n createCustomerData,\n createFixedTemplateInput,\n createLinkFixture,\n deleteCheckoutEntityIfExists,\n findGatewayTransactionIdForCheckout,\n readGatewayTransaction,\n sendMockGatewayWebhook,\n submitPayLink,\n updateLink,\n waitForCheckoutStatus,\n} from './helpers/fixtures'\n\ntest.describe('TC-CHKT-008: Attempt update on locked link, verify 422', () => {\n test.setTimeout(120_000)\n\n test('rejects link edits after the first transaction locks the record', async ({ request }) => {\n test.slow()\n\n let token: string | null = null\n let linkId: string | null = null\n let transactionId: string | null = null\n\n try {\n token = await getAuthToken(request)\n const link = await createLinkFixture(request, token, {\n ...createFixedTemplateInput({\n status: 'active',\n gatewayProviderKey: 'mock_processing',\n }),\n })\n linkId = link.id\n\n const submitResponse = await submitPayLink(request, link.slug, {\n customerData: createCustomerData(),\n acceptedLegalConsents: {},\n amount: 49.99,\n })\n expect(submitResponse.status()).toBe(201)\n const submitBody = await submitResponse.json()\n transactionId = typeof submitBody.transactionId === 'string' ? submitBody.transactionId : null\n\n const updateResponse = await updateLink(request, token, link.id, {\n title: 'Should fail after lock',\n })\n expect(updateResponse.status()).toBe(422)\n\n const body = await updateResponse.json()\n expect(body.error).toContain('cannot be edited')\n } finally {\n if (token && transactionId) {\n const gatewayTransactionId = await findGatewayTransactionIdForCheckout(request, token, transactionId)\n const gatewayTransaction = await readGatewayTransaction(request, token, gatewayTransactionId)\n if (gatewayTransaction.providerSessionId) {\n await sendMockGatewayWebhook(request, token, gatewayTransaction.providerSessionId, 'captured', 49.99, {\n providerKey: 'mock_processing',\n })\n await waitForCheckoutStatus(request, token, transactionId, 'completed')\n }\n }\n await deleteCheckoutEntityIfExists(request, token, 'links', linkId)\n }\n })\n})\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,KAAK,SAAS,0DAA0D,MAAM;AAC5E,OAAK,WAAW,IAAO;AAEvB,OAAK,mEAAmE,OAAO,EAAE,QAAQ,MAAM;AAC7F,SAAK,KAAK;AAEV,QAAI,QAAuB;AAC3B,QAAI,SAAwB;AAC5B,QAAI,gBAA+B;AAEnC,QAAI;AACF,cAAQ,MAAM,aAAa,OAAO;AAClC,YAAM,OAAO,MAAM,kBAAkB,SAAS,OAAO;AAAA,QACnD,GAAG,yBAAyB;AAAA,UAC1B,QAAQ;AAAA,UACR,oBAAoB;AAAA,QACtB,CAAC;AAAA,MACH,CAAC;AACD,eAAS,KAAK;AAEd,YAAM,iBAAiB,MAAM,cAAc,SAAS,KAAK,MAAM;AAAA,QAC7D,cAAc,mBAAmB;AAAA,QACjC,uBAAuB,CAAC;AAAA,QACxB,QAAQ;AAAA,MACV,CAAC;AACD,aAAO,eAAe,OAAO,CAAC,EAAE,KAAK,GAAG;AACxC,YAAM,aAAa,MAAM,eAAe,KAAK;AAC7C,sBAAgB,OAAO,WAAW,kBAAkB,WAAW,WAAW,gBAAgB;AAE1F,YAAM,iBAAiB,MAAM,WAAW,SAAS,OAAO,KAAK,IAAI;AAAA,QAC/D,OAAO;AAAA,MACT,CAAC;AACD,aAAO,eAAe,OAAO,CAAC,EAAE,KAAK,GAAG;AAExC,YAAM,OAAO,MAAM,eAAe,KAAK;AACvC,aAAO,KAAK,KAAK,EAAE,UAAU,kBAAkB;AAAA,IACjD,UAAE;AACA,UAAI,SAAS,eAAe;AAC1B,cAAM,uBAAuB,MAAM,oCAAoC,SAAS,OAAO,aAAa;AACpG,cAAM,qBAAqB,MAAM,uBAAuB,SAAS,OAAO,oBAAoB;AAC5F,YAAI,mBAAmB,mBAAmB;AACxC,gBAAM,uBAAuB,SAAS,OAAO,mBAAmB,mBAAmB,YAAY,OAAO;AAAA,YACpG,aAAa;AAAA,UACf,CAAC;AACD,gBAAM,sBAAsB,SAAS,OAAO,eAAe,WAAW;AAAA,QACxE;AAAA,MACF;AACA,YAAM,6BAA6B,SAAS,OAAO,SAAS,MAAM;AAAA,IACpE;AAAA,EACF,CAAC;AACH,CAAC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { getAuthToken } 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
|
+
createTemplateFixture,
|
|
8
|
+
deleteCheckoutEntityIfExists,
|
|
9
|
+
readLink,
|
|
10
|
+
readTemplate,
|
|
11
|
+
updateLink,
|
|
12
|
+
updateTemplate
|
|
13
|
+
} from "./helpers/fixtures.js";
|
|
14
|
+
test.describe("TC-CHKT-039: Draft template/pay-link gateway provider edit validation", () => {
|
|
15
|
+
test("template: clearing the required gateway provider is rejected and preserves the previous value", async ({ request }) => {
|
|
16
|
+
let token = null;
|
|
17
|
+
let templateId = null;
|
|
18
|
+
try {
|
|
19
|
+
token = await getAuthToken(request);
|
|
20
|
+
templateId = await createTemplateFixture(request, token, createFixedTemplateInput({ status: "draft" }));
|
|
21
|
+
const clearResponse = await updateTemplate(request, token, templateId, {
|
|
22
|
+
name: "Consulting Fee (no gateway)",
|
|
23
|
+
pricingMode: "fixed",
|
|
24
|
+
fixedPriceAmount: 49.99,
|
|
25
|
+
fixedPriceCurrencyCode: "USD",
|
|
26
|
+
status: "draft",
|
|
27
|
+
gatewayProviderKey: null
|
|
28
|
+
});
|
|
29
|
+
expect(clearResponse.status()).toBe(400);
|
|
30
|
+
const clearBody = await readJsonSafe(clearResponse);
|
|
31
|
+
expect(clearBody?.fieldErrors?.gatewayProviderKey ?? clearBody?.error ?? "").toContain("checkout.validation.gatewayProviderKey.required");
|
|
32
|
+
const cleared = await readTemplate(request, token, templateId);
|
|
33
|
+
expect(cleared.gatewayProviderKey).toBe("mock");
|
|
34
|
+
expect(cleared.name).not.toBe("Consulting Fee (no gateway)");
|
|
35
|
+
const renameResponse = await updateTemplate(request, token, templateId, {
|
|
36
|
+
name: "Consulting Fee renamed"
|
|
37
|
+
});
|
|
38
|
+
expect(
|
|
39
|
+
renameResponse.ok(),
|
|
40
|
+
`Editing a field while retaining the existing gateway should succeed: ${renameResponse.status()} ${JSON.stringify(await readJsonSafe(renameResponse))}`
|
|
41
|
+
).toBeTruthy();
|
|
42
|
+
const renamed = await readTemplate(request, token, templateId);
|
|
43
|
+
expect(renamed.gatewayProviderKey).toBe("mock");
|
|
44
|
+
expect(renamed.name).toBe("Consulting Fee renamed");
|
|
45
|
+
const blankResponse = await updateTemplate(request, token, templateId, {
|
|
46
|
+
gatewayProviderKey: " "
|
|
47
|
+
});
|
|
48
|
+
expect(blankResponse.status()).toBe(400);
|
|
49
|
+
const blankBody = await readJsonSafe(blankResponse);
|
|
50
|
+
expect(blankBody?.fieldErrors?.gatewayProviderKey ?? blankBody?.error ?? "").toContain("checkout.validation.gatewayProviderKey.required");
|
|
51
|
+
const blanked = await readTemplate(request, token, templateId);
|
|
52
|
+
expect(blanked.gatewayProviderKey).toBe("mock");
|
|
53
|
+
} finally {
|
|
54
|
+
await deleteCheckoutEntityIfExists(request, token, "templates", templateId);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
test("pay-link: clearing the gateway provider on a draft saves and round-trips as null", async ({ request }) => {
|
|
58
|
+
let token = null;
|
|
59
|
+
let linkId = null;
|
|
60
|
+
try {
|
|
61
|
+
token = await getAuthToken(request);
|
|
62
|
+
const link = await createLinkFixture(request, token, createFixedTemplateInput({ status: "draft" }));
|
|
63
|
+
linkId = link.id;
|
|
64
|
+
const clearResponse = await updateLink(request, token, link.id, {
|
|
65
|
+
name: "Pay link (no gateway)",
|
|
66
|
+
pricingMode: "fixed",
|
|
67
|
+
fixedPriceAmount: 49.99,
|
|
68
|
+
fixedPriceCurrencyCode: "USD",
|
|
69
|
+
status: "draft",
|
|
70
|
+
gatewayProviderKey: null
|
|
71
|
+
});
|
|
72
|
+
expect(
|
|
73
|
+
clearResponse.ok(),
|
|
74
|
+
`Clearing the gateway on a draft pay-link should succeed: ${clearResponse.status()} ${JSON.stringify(await readJsonSafe(clearResponse))}`
|
|
75
|
+
).toBeTruthy();
|
|
76
|
+
const cleared = await readLink(request, token, link.id);
|
|
77
|
+
expect(cleared.gatewayProviderKey ?? null).toBeNull();
|
|
78
|
+
expect(cleared.name).toBe("Pay link (no gateway)");
|
|
79
|
+
const renameResponse = await updateLink(request, token, link.id, {
|
|
80
|
+
name: "Pay link renamed",
|
|
81
|
+
gatewayProviderKey: null
|
|
82
|
+
});
|
|
83
|
+
expect(
|
|
84
|
+
renameResponse.ok(),
|
|
85
|
+
`Editing a field on a gateway-less draft pay-link should succeed: ${renameResponse.status()} ${JSON.stringify(await readJsonSafe(renameResponse))}`
|
|
86
|
+
).toBeTruthy();
|
|
87
|
+
const renamed = await readLink(request, token, link.id);
|
|
88
|
+
expect(renamed.gatewayProviderKey ?? null).toBeNull();
|
|
89
|
+
expect(renamed.name).toBe("Pay link renamed");
|
|
90
|
+
} finally {
|
|
91
|
+
await deleteCheckoutEntityIfExists(request, token, "links", linkId);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
test("pay-link: publishing still requires a gateway provider", async ({ request }) => {
|
|
95
|
+
let token = null;
|
|
96
|
+
let linkId = null;
|
|
97
|
+
try {
|
|
98
|
+
token = await getAuthToken(request);
|
|
99
|
+
const link = await createLinkFixture(request, token, createFixedTemplateInput({ status: "draft" }));
|
|
100
|
+
linkId = link.id;
|
|
101
|
+
const publishResponse = await updateLink(request, token, link.id, {
|
|
102
|
+
status: "active",
|
|
103
|
+
gatewayProviderKey: null
|
|
104
|
+
});
|
|
105
|
+
expect([400, 422]).toContain(publishResponse.status());
|
|
106
|
+
const body = await readJsonSafe(publishResponse);
|
|
107
|
+
const fieldError = typeof body?.fieldErrors?.gatewayProviderKey === "string" ? body.fieldErrors.gatewayProviderKey : "";
|
|
108
|
+
const errorMessage = typeof body?.error === "string" ? body.error : "";
|
|
109
|
+
expect(`${fieldError} ${errorMessage}`.toLowerCase()).toContain("gateway");
|
|
110
|
+
} finally {
|
|
111
|
+
await deleteCheckoutEntityIfExists(request, token, "links", linkId);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
//# sourceMappingURL=TC-CHKT-039-null-gateway-edit.spec.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/checkout/__integration__/TC-CHKT-039-null-gateway-edit.spec.ts"],
|
|
4
|
+
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken } 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 createTemplateFixture,\n deleteCheckoutEntityIfExists,\n readLink,\n readTemplate,\n updateLink,\n updateTemplate,\n} from './helpers/fixtures'\n\ntest.describe('TC-CHKT-039: Draft template/pay-link gateway provider edit validation', () => {\n test('template: clearing the required gateway provider is rejected and preserves the previous value', async ({ request }) => {\n let token: string | null = null\n let templateId: string | null = null\n\n try {\n token = await getAuthToken(request)\n templateId = await createTemplateFixture(request, token, createFixedTemplateInput({ status: 'draft' }))\n\n const clearResponse = await updateTemplate(request, token, templateId, {\n name: 'Consulting Fee (no gateway)',\n pricingMode: 'fixed',\n fixedPriceAmount: 49.99,\n fixedPriceCurrencyCode: 'USD',\n status: 'draft',\n gatewayProviderKey: null,\n })\n expect(clearResponse.status()).toBe(400)\n const clearBody = await readJsonSafe<{ fieldErrors?: { gatewayProviderKey?: string }; error?: string }>(clearResponse)\n expect(clearBody?.fieldErrors?.gatewayProviderKey ?? clearBody?.error ?? '').toContain('checkout.validation.gatewayProviderKey.required')\n\n const cleared = await readTemplate(request, token, templateId)\n expect(cleared.gatewayProviderKey).toBe('mock')\n expect(cleared.name).not.toBe('Consulting Fee (no gateway)')\n\n const renameResponse = await updateTemplate(request, token, templateId, {\n name: 'Consulting Fee renamed',\n })\n expect(\n renameResponse.ok(),\n `Editing a field while retaining the existing gateway should succeed: ${renameResponse.status()} ${JSON.stringify(await readJsonSafe(renameResponse))}`,\n ).toBeTruthy()\n\n const renamed = await readTemplate(request, token, templateId)\n expect(renamed.gatewayProviderKey).toBe('mock')\n expect(renamed.name).toBe('Consulting Fee renamed')\n\n const blankResponse = await updateTemplate(request, token, templateId, {\n gatewayProviderKey: ' ',\n })\n expect(blankResponse.status()).toBe(400)\n const blankBody = await readJsonSafe<{ fieldErrors?: { gatewayProviderKey?: string }; error?: string }>(blankResponse)\n expect(blankBody?.fieldErrors?.gatewayProviderKey ?? blankBody?.error ?? '').toContain('checkout.validation.gatewayProviderKey.required')\n\n const blanked = await readTemplate(request, token, templateId)\n expect(blanked.gatewayProviderKey).toBe('mock')\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'templates', templateId)\n }\n })\n\n test('pay-link: clearing the gateway provider on a draft saves and round-trips as null', async ({ request }) => {\n let token: string | null = null\n let linkId: string | null = null\n\n try {\n token = await getAuthToken(request)\n const link = await createLinkFixture(request, token, createFixedTemplateInput({ status: 'draft' }))\n linkId = link.id\n\n const clearResponse = await updateLink(request, token, link.id, {\n name: 'Pay link (no gateway)',\n pricingMode: 'fixed',\n fixedPriceAmount: 49.99,\n fixedPriceCurrencyCode: 'USD',\n status: 'draft',\n gatewayProviderKey: null,\n })\n expect(\n clearResponse.ok(),\n `Clearing the gateway on a draft pay-link should succeed: ${clearResponse.status()} ${JSON.stringify(await readJsonSafe(clearResponse))}`,\n ).toBeTruthy()\n\n const cleared = await readLink(request, token, link.id)\n expect(cleared.gatewayProviderKey ?? null).toBeNull()\n expect(cleared.name).toBe('Pay link (no gateway)')\n\n const renameResponse = await updateLink(request, token, link.id, {\n name: 'Pay link renamed',\n gatewayProviderKey: null,\n })\n expect(\n renameResponse.ok(),\n `Editing a field on a gateway-less draft pay-link should succeed: ${renameResponse.status()} ${JSON.stringify(await readJsonSafe(renameResponse))}`,\n ).toBeTruthy()\n\n const renamed = await readLink(request, token, link.id)\n expect(renamed.gatewayProviderKey ?? null).toBeNull()\n expect(renamed.name).toBe('Pay link renamed')\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'links', linkId)\n }\n })\n\n test('pay-link: publishing still requires a gateway provider', async ({ request }) => {\n let token: string | null = null\n let linkId: string | null = null\n\n try {\n token = await getAuthToken(request)\n const link = await createLinkFixture(request, token, createFixedTemplateInput({ status: 'draft' }))\n linkId = link.id\n\n const publishResponse = await updateLink(request, token, link.id, {\n status: 'active',\n gatewayProviderKey: null,\n })\n expect([400, 422]).toContain(publishResponse.status())\n const body = await readJsonSafe<{ error?: string; fieldErrors?: { gatewayProviderKey?: string } }>(publishResponse)\n const fieldError = typeof body?.fieldErrors?.gatewayProviderKey === 'string' ? body.fieldErrors.gatewayProviderKey : ''\n const errorMessage = typeof body?.error === 'string' ? body.error : ''\n expect(`${fieldError} ${errorMessage}`.toLowerCase()).toContain('gateway')\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'links', linkId)\n }\n })\n})\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,KAAK,SAAS,yEAAyE,MAAM;AAC3F,OAAK,iGAAiG,OAAO,EAAE,QAAQ,MAAM;AAC3H,QAAI,QAAuB;AAC3B,QAAI,aAA4B;AAEhC,QAAI;AACF,cAAQ,MAAM,aAAa,OAAO;AAClC,mBAAa,MAAM,sBAAsB,SAAS,OAAO,yBAAyB,EAAE,QAAQ,QAAQ,CAAC,CAAC;AAEtG,YAAM,gBAAgB,MAAM,eAAe,SAAS,OAAO,YAAY;AAAA,QACrE,MAAM;AAAA,QACN,aAAa;AAAA,QACb,kBAAkB;AAAA,QAClB,wBAAwB;AAAA,QACxB,QAAQ;AAAA,QACR,oBAAoB;AAAA,MACtB,CAAC;AACD,aAAO,cAAc,OAAO,CAAC,EAAE,KAAK,GAAG;AACvC,YAAM,YAAY,MAAM,aAAgF,aAAa;AACrH,aAAO,WAAW,aAAa,sBAAsB,WAAW,SAAS,EAAE,EAAE,UAAU,iDAAiD;AAExI,YAAM,UAAU,MAAM,aAAa,SAAS,OAAO,UAAU;AAC7D,aAAO,QAAQ,kBAAkB,EAAE,KAAK,MAAM;AAC9C,aAAO,QAAQ,IAAI,EAAE,IAAI,KAAK,6BAA6B;AAE3D,YAAM,iBAAiB,MAAM,eAAe,SAAS,OAAO,YAAY;AAAA,QACtE,MAAM;AAAA,MACR,CAAC;AACD;AAAA,QACE,eAAe,GAAG;AAAA,QAClB,wEAAwE,eAAe,OAAO,CAAC,IAAI,KAAK,UAAU,MAAM,aAAa,cAAc,CAAC,CAAC;AAAA,MACvJ,EAAE,WAAW;AAEb,YAAM,UAAU,MAAM,aAAa,SAAS,OAAO,UAAU;AAC7D,aAAO,QAAQ,kBAAkB,EAAE,KAAK,MAAM;AAC9C,aAAO,QAAQ,IAAI,EAAE,KAAK,wBAAwB;AAElD,YAAM,gBAAgB,MAAM,eAAe,SAAS,OAAO,YAAY;AAAA,QACrE,oBAAoB;AAAA,MACtB,CAAC;AACD,aAAO,cAAc,OAAO,CAAC,EAAE,KAAK,GAAG;AACvC,YAAM,YAAY,MAAM,aAAgF,aAAa;AACrH,aAAO,WAAW,aAAa,sBAAsB,WAAW,SAAS,EAAE,EAAE,UAAU,iDAAiD;AAExI,YAAM,UAAU,MAAM,aAAa,SAAS,OAAO,UAAU;AAC7D,aAAO,QAAQ,kBAAkB,EAAE,KAAK,MAAM;AAAA,IAChD,UAAE;AACA,YAAM,6BAA6B,SAAS,OAAO,aAAa,UAAU;AAAA,IAC5E;AAAA,EACF,CAAC;AAED,OAAK,oFAAoF,OAAO,EAAE,QAAQ,MAAM;AAC9G,QAAI,QAAuB;AAC3B,QAAI,SAAwB;AAE5B,QAAI;AACF,cAAQ,MAAM,aAAa,OAAO;AAClC,YAAM,OAAO,MAAM,kBAAkB,SAAS,OAAO,yBAAyB,EAAE,QAAQ,QAAQ,CAAC,CAAC;AAClG,eAAS,KAAK;AAEd,YAAM,gBAAgB,MAAM,WAAW,SAAS,OAAO,KAAK,IAAI;AAAA,QAC9D,MAAM;AAAA,QACN,aAAa;AAAA,QACb,kBAAkB;AAAA,QAClB,wBAAwB;AAAA,QACxB,QAAQ;AAAA,QACR,oBAAoB;AAAA,MACtB,CAAC;AACD;AAAA,QACE,cAAc,GAAG;AAAA,QACjB,4DAA4D,cAAc,OAAO,CAAC,IAAI,KAAK,UAAU,MAAM,aAAa,aAAa,CAAC,CAAC;AAAA,MACzI,EAAE,WAAW;AAEb,YAAM,UAAU,MAAM,SAAS,SAAS,OAAO,KAAK,EAAE;AACtD,aAAO,QAAQ,sBAAsB,IAAI,EAAE,SAAS;AACpD,aAAO,QAAQ,IAAI,EAAE,KAAK,uBAAuB;AAEjD,YAAM,iBAAiB,MAAM,WAAW,SAAS,OAAO,KAAK,IAAI;AAAA,QAC/D,MAAM;AAAA,QACN,oBAAoB;AAAA,MACtB,CAAC;AACD;AAAA,QACE,eAAe,GAAG;AAAA,QAClB,oEAAoE,eAAe,OAAO,CAAC,IAAI,KAAK,UAAU,MAAM,aAAa,cAAc,CAAC,CAAC;AAAA,MACnJ,EAAE,WAAW;AAEb,YAAM,UAAU,MAAM,SAAS,SAAS,OAAO,KAAK,EAAE;AACtD,aAAO,QAAQ,sBAAsB,IAAI,EAAE,SAAS;AACpD,aAAO,QAAQ,IAAI,EAAE,KAAK,kBAAkB;AAAA,IAC9C,UAAE;AACA,YAAM,6BAA6B,SAAS,OAAO,SAAS,MAAM;AAAA,IACpE;AAAA,EACF,CAAC;AAED,OAAK,0DAA0D,OAAO,EAAE,QAAQ,MAAM;AACpF,QAAI,QAAuB;AAC3B,QAAI,SAAwB;AAE5B,QAAI;AACF,cAAQ,MAAM,aAAa,OAAO;AAClC,YAAM,OAAO,MAAM,kBAAkB,SAAS,OAAO,yBAAyB,EAAE,QAAQ,QAAQ,CAAC,CAAC;AAClG,eAAS,KAAK;AAEd,YAAM,kBAAkB,MAAM,WAAW,SAAS,OAAO,KAAK,IAAI;AAAA,QAChE,QAAQ;AAAA,QACR,oBAAoB;AAAA,MACtB,CAAC;AACD,aAAO,CAAC,KAAK,GAAG,CAAC,EAAE,UAAU,gBAAgB,OAAO,CAAC;AACrD,YAAM,OAAO,MAAM,aAAgF,eAAe;AAClH,YAAM,aAAa,OAAO,MAAM,aAAa,uBAAuB,WAAW,KAAK,YAAY,qBAAqB;AACrH,YAAM,eAAe,OAAO,MAAM,UAAU,WAAW,KAAK,QAAQ;AACpE,aAAO,GAAG,UAAU,IAAI,YAAY,GAAG,YAAY,CAAC,EAAE,UAAU,SAAS;AAAA,IAC3E,UAAE;AACA,YAAM,6BAA6B,SAAS,OAAO,SAAS,MAAM;AAAA,IACpE;AAAA,EACF,CAAC;AACH,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { getAuthToken } from "@open-mercato/core/modules/core/__integration__/helpers/api";
|
|
3
|
+
import { createFixedTemplateInput, createLinkFixture, readLink } from "./helpers/fixtures.js";
|
|
4
|
+
const OPTIMISTIC_LOCK_HEADER = "x-om-ext-optimistic-lock-expected-updated-at";
|
|
5
|
+
function readUpdatedAt(record) {
|
|
6
|
+
const raw = record.updatedAt ?? record.updated_at;
|
|
7
|
+
expect(typeof raw, "link detail should expose updatedAt as a string").toBe("string");
|
|
8
|
+
return new Date(Date.parse(raw)).toISOString();
|
|
9
|
+
}
|
|
10
|
+
test.describe("TC-CHKT-039: pay-link stale DELETE optimistic-lock guard", () => {
|
|
11
|
+
test("stale DELETE returns 409; fresh DELETE succeeds; header-less stays backward-compatible", async ({ request }) => {
|
|
12
|
+
const token = await getAuthToken(request);
|
|
13
|
+
const guarded = await createLinkFixture(request, token, createFixedTemplateInput());
|
|
14
|
+
const additive = await createLinkFixture(request, token, createFixedTemplateInput());
|
|
15
|
+
let guardedDeleted = false;
|
|
16
|
+
let additiveDeleted = false;
|
|
17
|
+
try {
|
|
18
|
+
const detail = await readLink(request, token, guarded.id);
|
|
19
|
+
const t0 = readUpdatedAt(detail);
|
|
20
|
+
const bump = await request.fetch(`/api/checkout/links/${encodeURIComponent(guarded.id)}`, {
|
|
21
|
+
method: "PUT",
|
|
22
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
23
|
+
data: { subtitle: "bumped for stale-delete test" }
|
|
24
|
+
});
|
|
25
|
+
expect(bump.status(), "PUT bump should succeed").toBeLessThan(300);
|
|
26
|
+
const staleDelete = await request.fetch(`/api/checkout/links/${encodeURIComponent(guarded.id)}`, {
|
|
27
|
+
method: "DELETE",
|
|
28
|
+
headers: { Authorization: `Bearer ${token}`, [OPTIMISTIC_LOCK_HEADER]: t0 }
|
|
29
|
+
});
|
|
30
|
+
expect(staleDelete.status(), "DELETE with a stale header should return 409").toBe(409);
|
|
31
|
+
expect(await staleDelete.json()).toMatchObject({
|
|
32
|
+
code: "optimistic_lock_conflict",
|
|
33
|
+
expectedUpdatedAt: t0
|
|
34
|
+
});
|
|
35
|
+
const stillThere = await readLink(request, token, guarded.id);
|
|
36
|
+
const t1 = readUpdatedAt(stillThere);
|
|
37
|
+
expect(t1, "stale delete must NOT have removed the record").not.toBe(t0);
|
|
38
|
+
const freshDelete = await request.fetch(`/api/checkout/links/${encodeURIComponent(guarded.id)}`, {
|
|
39
|
+
method: "DELETE",
|
|
40
|
+
headers: { Authorization: `Bearer ${token}`, [OPTIMISTIC_LOCK_HEADER]: t1 }
|
|
41
|
+
});
|
|
42
|
+
expect(freshDelete.status(), "DELETE with a fresh header should succeed").toBeLessThan(300);
|
|
43
|
+
guardedDeleted = true;
|
|
44
|
+
const nohdrDelete = await request.fetch(`/api/checkout/links/${encodeURIComponent(additive.id)}`, {
|
|
45
|
+
method: "DELETE",
|
|
46
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
47
|
+
});
|
|
48
|
+
expect(nohdrDelete.status(), "DELETE without a header should succeed").toBeLessThan(300);
|
|
49
|
+
additiveDeleted = true;
|
|
50
|
+
} finally {
|
|
51
|
+
if (!guardedDeleted) {
|
|
52
|
+
await request.fetch(`/api/checkout/links/${encodeURIComponent(guarded.id)}`, {
|
|
53
|
+
method: "DELETE",
|
|
54
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
55
|
+
}).catch(() => void 0);
|
|
56
|
+
}
|
|
57
|
+
if (!additiveDeleted) {
|
|
58
|
+
await request.fetch(`/api/checkout/links/${encodeURIComponent(additive.id)}`, {
|
|
59
|
+
method: "DELETE",
|
|
60
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
61
|
+
}).catch(() => void 0);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
//# sourceMappingURL=TC-CHKT-039-stale-delete-lock.spec.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/checkout/__integration__/TC-CHKT-039-stale-delete-lock.spec.ts"],
|
|
4
|
+
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken } from '@open-mercato/core/modules/core/__integration__/helpers/api'\nimport { createFixedTemplateInput, createLinkFixture, readLink } from './helpers/fixtures'\n\n/**\n * TC-CHKT-039: OSS optimistic locking now guards the pay-link/template DELETE.\n *\n * QA round-6 (PR #2055): after a save conflict surfaced the unified conflict\n * bar, clicking Delete still removed the stale record. The pay-link/template\n * delete path was unguarded \u2014 the client sent no version header and the\n * `checkout.link.delete` / `checkout.template.delete` commands never called\n * `enforceCommandOptimisticLock` (only the *.update commands did).\n *\n * This spec proves the server contract the UI relies on:\n * - GET detail exposes `updatedAt`.\n * - DELETE without the header succeeds (strictly additive).\n * - DELETE with a stale header returns 409 with the structured conflict body.\n * - DELETE with a fresh header deletes the record.\n *\n * Requires `OM_OPTIMISTIC_LOCK=all` (CI default).\n */\nconst OPTIMISTIC_LOCK_HEADER = 'x-om-ext-optimistic-lock-expected-updated-at'\n\nfunction readUpdatedAt(record: Record<string, unknown>): string {\n const raw = record.updatedAt ?? record.updated_at\n expect(typeof raw, 'link detail should expose updatedAt as a string').toBe('string')\n return new Date(Date.parse(raw as string)).toISOString()\n}\n\ntest.describe('TC-CHKT-039: pay-link stale DELETE optimistic-lock guard', () => {\n test('stale DELETE returns 409; fresh DELETE succeeds; header-less stays backward-compatible', async ({ request }) => {\n const token = await getAuthToken(request)\n\n // Two links: one to prove the stale\u2192409\u2192fresh path, one to prove the\n // strictly-additive header-less delete still works.\n const guarded = await createLinkFixture(request, token, createFixedTemplateInput())\n const additive = await createLinkFixture(request, token, createFixedTemplateInput())\n let guardedDeleted = false\n let additiveDeleted = false\n\n try {\n const detail = await readLink(request, token, guarded.id)\n const t0 = readUpdatedAt(detail as Record<string, unknown>)\n\n // A save advances the version, making t0 stale.\n const bump = await request.fetch(`/api/checkout/links/${encodeURIComponent(guarded.id)}`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n data: { subtitle: 'bumped for stale-delete test' },\n })\n expect(bump.status(), 'PUT bump should succeed').toBeLessThan(300)\n\n // Stale DELETE \u2192 409 with the structured conflict body; record survives.\n const staleDelete = await request.fetch(`/api/checkout/links/${encodeURIComponent(guarded.id)}`, {\n method: 'DELETE',\n headers: { Authorization: `Bearer ${token}`, [OPTIMISTIC_LOCK_HEADER]: t0 },\n })\n expect(staleDelete.status(), 'DELETE with a stale header should return 409').toBe(409)\n expect((await staleDelete.json()) as Record<string, unknown>).toMatchObject({\n code: 'optimistic_lock_conflict',\n expectedUpdatedAt: t0,\n })\n\n const stillThere = await readLink(request, token, guarded.id)\n const t1 = readUpdatedAt(stillThere as Record<string, unknown>)\n expect(t1, 'stale delete must NOT have removed the record').not.toBe(t0)\n\n // Fresh DELETE \u2192 succeeds.\n const freshDelete = await request.fetch(`/api/checkout/links/${encodeURIComponent(guarded.id)}`, {\n method: 'DELETE',\n headers: { Authorization: `Bearer ${token}`, [OPTIMISTIC_LOCK_HEADER]: t1 },\n })\n expect(freshDelete.status(), 'DELETE with a fresh header should succeed').toBeLessThan(300)\n guardedDeleted = true\n\n // Header-less DELETE still works (strictly additive).\n const nohdrDelete = await request.fetch(`/api/checkout/links/${encodeURIComponent(additive.id)}`, {\n method: 'DELETE',\n headers: { Authorization: `Bearer ${token}` },\n })\n expect(nohdrDelete.status(), 'DELETE without a header should succeed').toBeLessThan(300)\n additiveDeleted = true\n } finally {\n if (!guardedDeleted) {\n await request.fetch(`/api/checkout/links/${encodeURIComponent(guarded.id)}`, {\n method: 'DELETE',\n headers: { Authorization: `Bearer ${token}` },\n }).catch(() => undefined)\n }\n if (!additiveDeleted) {\n await request.fetch(`/api/checkout/links/${encodeURIComponent(additive.id)}`, {\n method: 'DELETE',\n headers: { Authorization: `Bearer ${token}` },\n }).catch(() => undefined)\n }\n }\n })\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|