@open-mercato/checkout 0.6.5-develop.5187.1.82e5532561 → 0.6.5-develop.5212.1.b47932beef
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-UNDO-001-no-undo-affordance.spec.js +40 -0
- package/dist/modules/checkout/__integration__/TC-UNDO-001-no-undo-affordance.spec.js.map +7 -0
- package/dist/modules/checkout/__integration__/TC-UNDO-001-pay-link.spec.js +205 -0
- package/dist/modules/checkout/__integration__/TC-UNDO-001-pay-link.spec.js.map +7 -0
- package/dist/modules/checkout/__integration__/TC-UNDO-001-template.spec.js +171 -0
- package/dist/modules/checkout/__integration__/TC-UNDO-001-template.spec.js.map +7 -0
- package/dist/modules/checkout/components/LinkTemplateForm.js +30 -2
- package/dist/modules/checkout/components/LinkTemplateForm.js.map +2 -2
- package/dist/modules/checkout/lib/recordNotFound.js +20 -0
- package/dist/modules/checkout/lib/recordNotFound.js.map +7 -0
- package/package.json +5 -5
- package/src/modules/checkout/__integration__/TC-UNDO-001-no-undo-affordance.spec.ts +51 -0
- package/src/modules/checkout/__integration__/TC-UNDO-001-pay-link.spec.ts +266 -0
- package/src/modules/checkout/__integration__/TC-UNDO-001-template.spec.ts +228 -0
- package/src/modules/checkout/components/LinkTemplateForm.tsx +39 -2
- package/src/modules/checkout/i18n/de.json +4 -0
- package/src/modules/checkout/i18n/en.json +4 -0
- package/src/modules/checkout/i18n/es.json +4 -0
- package/src/modules/checkout/i18n/pl.json +4 -0
- package/src/modules/checkout/lib/__tests__/recordNotFound.test.ts +30 -0
- package/src/modules/checkout/lib/recordNotFound.ts +17 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:checkout] found
|
|
1
|
+
[build:checkout] found 157 entry points
|
|
2
2
|
[build:checkout] built successfully
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { getAuthToken } from "@open-mercato/core/helpers/integration/api";
|
|
3
|
+
import { extractOperation, skipIfUndoTestsDisabled } from "@open-mercato/core/helpers/integration/undoHarness";
|
|
4
|
+
import {
|
|
5
|
+
createCustomerData,
|
|
6
|
+
createFixedTemplateInput,
|
|
7
|
+
createLinkFixture,
|
|
8
|
+
createTemplateFixture,
|
|
9
|
+
deleteCheckoutEntityIfExists,
|
|
10
|
+
submitPayLink
|
|
11
|
+
} from "./helpers/fixtures.js";
|
|
12
|
+
test.describe("TC-UNDO-001 checkout \xA74 \u2014 non-undoable commands expose no undo token", () => {
|
|
13
|
+
test.beforeAll(() => {
|
|
14
|
+
skipIfUndoTestsDisabled();
|
|
15
|
+
});
|
|
16
|
+
test("public checkout submit creates a transaction with no undo token", async ({ request }) => {
|
|
17
|
+
const token = await getAuthToken(request, "admin");
|
|
18
|
+
let templateId = null;
|
|
19
|
+
let linkId = null;
|
|
20
|
+
try {
|
|
21
|
+
templateId = await createTemplateFixture(request, token, createFixedTemplateInput({ status: "draft" }));
|
|
22
|
+
const link = await createLinkFixture(request, token, createFixedTemplateInput({ status: "active", templateId }));
|
|
23
|
+
linkId = link.id;
|
|
24
|
+
const submitRes = await submitPayLink(request, link.slug, {
|
|
25
|
+
customerData: createCustomerData(),
|
|
26
|
+
acceptedLegalConsents: {},
|
|
27
|
+
amount: 49.99
|
|
28
|
+
});
|
|
29
|
+
expect(submitRes.status(), `submit status ${submitRes.status()}`).toBe(201);
|
|
30
|
+
expect(
|
|
31
|
+
extractOperation(submitRes),
|
|
32
|
+
"a financial checkout submit must expose no undo token (\xA74)"
|
|
33
|
+
).toBeNull();
|
|
34
|
+
} finally {
|
|
35
|
+
await deleteCheckoutEntityIfExists(request, token, "links", linkId);
|
|
36
|
+
await deleteCheckoutEntityIfExists(request, token, "templates", templateId);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
//# sourceMappingURL=TC-UNDO-001-no-undo-affordance.spec.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/checkout/__integration__/TC-UNDO-001-no-undo-affordance.spec.ts"],
|
|
4
|
+
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken } from '@open-mercato/core/helpers/integration/api'\nimport { extractOperation, skipIfUndoTestsDisabled } from '@open-mercato/core/helpers/integration/undoHarness'\nimport {\n createCustomerData,\n createFixedTemplateInput,\n createLinkFixture,\n createTemplateFixture,\n deleteCheckoutEntityIfExists,\n submitPayLink,\n} from './helpers/fixtures'\n\n/**\n * TC-UNDO-001 (\u00A74 checkout) \u2014 non-undoable commands expose no undo affordance.\n *\n * Public checkout submissions create a CheckoutTransaction via the `checkout.transaction.*`\n * commands, which are intentionally NOT undoable (financial events). The mutating response\n * must therefore carry no `x-om-operation` undo envelope: `extractOperation(res) === null`.\n */\n\ntest.describe('TC-UNDO-001 checkout \u00A74 \u2014 non-undoable commands expose no undo token', () => {\n test.beforeAll(() => {\n skipIfUndoTestsDisabled()\n })\n\n test('public checkout submit creates a transaction with no undo token', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n let templateId: string | null = null\n let linkId: string | null = null\n try {\n templateId = await createTemplateFixture(request, token, createFixedTemplateInput({ status: 'draft' }))\n const link = await createLinkFixture(request, token, createFixedTemplateInput({ status: 'active', templateId }))\n linkId = link.id\n\n const submitRes = await submitPayLink(request, link.slug, {\n customerData: createCustomerData(),\n acceptedLegalConsents: {},\n amount: 49.99,\n })\n expect(submitRes.status(), `submit status ${submitRes.status()}`).toBe(201)\n\n expect(\n extractOperation(submitRes),\n 'a financial checkout submit must expose no undo token (\u00A74)',\n ).toBeNull()\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'links', linkId)\n await deleteCheckoutEntityIfExists(request, token, 'templates', templateId)\n }\n })\n})\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB,+BAA+B;AAC1D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAUP,KAAK,SAAS,gFAAwE,MAAM;AAC1F,OAAK,UAAU,MAAM;AACnB,4BAAwB;AAAA,EAC1B,CAAC;AAED,OAAK,mEAAmE,OAAO,EAAE,QAAQ,MAAM;AAC7F,UAAM,QAAQ,MAAM,aAAa,SAAS,OAAO;AACjD,QAAI,aAA4B;AAChC,QAAI,SAAwB;AAC5B,QAAI;AACF,mBAAa,MAAM,sBAAsB,SAAS,OAAO,yBAAyB,EAAE,QAAQ,QAAQ,CAAC,CAAC;AACtG,YAAM,OAAO,MAAM,kBAAkB,SAAS,OAAO,yBAAyB,EAAE,QAAQ,UAAU,WAAW,CAAC,CAAC;AAC/G,eAAS,KAAK;AAEd,YAAM,YAAY,MAAM,cAAc,SAAS,KAAK,MAAM;AAAA,QACxD,cAAc,mBAAmB;AAAA,QACjC,uBAAuB,CAAC;AAAA,QACxB,QAAQ;AAAA,MACV,CAAC;AACD,aAAO,UAAU,OAAO,GAAG,iBAAiB,UAAU,OAAO,CAAC,EAAE,EAAE,KAAK,GAAG;AAE1E;AAAA,QACE,iBAAiB,SAAS;AAAA,QAC1B;AAAA,MACF,EAAE,SAAS;AAAA,IACb,UAAE;AACA,YAAM,6BAA6B,SAAS,OAAO,SAAS,MAAM;AAClE,YAAM,6BAA6B,SAAS,OAAO,aAAa,UAAU;AAAA,IAC5E;AAAA,EACF,CAAC;AACH,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { apiRequest, getAuthToken } from "@open-mercato/core/helpers/integration/api";
|
|
3
|
+
import { readJsonSafe } from "@open-mercato/core/helpers/integration/generalFixtures";
|
|
4
|
+
import {
|
|
5
|
+
expectOperation,
|
|
6
|
+
undoOk,
|
|
7
|
+
redoOk,
|
|
8
|
+
expectTokenConsumed,
|
|
9
|
+
skipIfUndoTestsDisabled
|
|
10
|
+
} from "@open-mercato/core/helpers/integration/undoHarness";
|
|
11
|
+
import {
|
|
12
|
+
createFixedTemplateInput,
|
|
13
|
+
createLinkFixture,
|
|
14
|
+
deleteCheckoutEntityIfExists,
|
|
15
|
+
deleteLink,
|
|
16
|
+
updateLink
|
|
17
|
+
} from "./helpers/fixtures.js";
|
|
18
|
+
const LINKS = "/api/checkout/links";
|
|
19
|
+
async function settle() {
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
21
|
+
}
|
|
22
|
+
async function getLink(request, token, id) {
|
|
23
|
+
const res = await apiRequest(request, "GET", `${LINKS}/${encodeURIComponent(id)}`, { token });
|
|
24
|
+
return { status: res.status(), body: await readJsonSafe(res) };
|
|
25
|
+
}
|
|
26
|
+
test.describe("TC-UNDO-001 checkout.pay-link undo/redo", () => {
|
|
27
|
+
test.beforeAll(() => {
|
|
28
|
+
skipIfUndoTestsDisabled();
|
|
29
|
+
});
|
|
30
|
+
test("create \u2192 undo soft-deletes (I3) + token consumed (I5)", async ({ request }) => {
|
|
31
|
+
const token = await getAuthToken(request, "admin");
|
|
32
|
+
let linkId = null;
|
|
33
|
+
try {
|
|
34
|
+
const createRes = await apiRequest(request, "POST", LINKS, {
|
|
35
|
+
token,
|
|
36
|
+
data: createFixedTemplateInput({ status: "draft" })
|
|
37
|
+
});
|
|
38
|
+
expect(createRes.status(), `create status ${createRes.status()}`).toBe(201);
|
|
39
|
+
const createOp = expectOperation(createRes, "checkout.link.create");
|
|
40
|
+
linkId = createOp.resourceId;
|
|
41
|
+
expect(linkId, "create returns a resource id").toBeTruthy();
|
|
42
|
+
expect((await getLink(request, token, linkId)).status, "pay-link exists after create").toBe(200);
|
|
43
|
+
await undoOk(request, token, createOp.undoToken, "undo create pay-link");
|
|
44
|
+
expect(
|
|
45
|
+
(await getLink(request, token, linkId)).status,
|
|
46
|
+
"pay-link is gone after undoing create (I3 \u2014 soft-deleted, not readable)"
|
|
47
|
+
).not.toBe(200);
|
|
48
|
+
await expectTokenConsumed(request, token, createOp.undoToken, "checkout.link.create double-undo (I5)");
|
|
49
|
+
} finally {
|
|
50
|
+
await deleteCheckoutEntityIfExists(request, token, "links", linkId);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
test("update \u2192 undo restores scalars (I1) \u2192 redo re-applies (I6)", async ({ request }) => {
|
|
54
|
+
const token = await getAuthToken(request, "admin");
|
|
55
|
+
let linkId = null;
|
|
56
|
+
try {
|
|
57
|
+
const link = await createLinkFixture(request, token, createFixedTemplateInput({ status: "draft" }));
|
|
58
|
+
linkId = link.id;
|
|
59
|
+
const before = await getLink(request, token, linkId);
|
|
60
|
+
const beforeName = before.body?.name;
|
|
61
|
+
expect(beforeName, "pay-link name readable before update").toBeTruthy();
|
|
62
|
+
await settle();
|
|
63
|
+
const changedName = `Renamed by undo test ${Date.now()}`;
|
|
64
|
+
const updateRes = await updateLink(request, token, linkId, { name: changedName });
|
|
65
|
+
expect(updateRes.status(), `update status ${updateRes.status()}`).toBe(200);
|
|
66
|
+
const updateOp = expectOperation(updateRes, "checkout.link.update");
|
|
67
|
+
expect((await getLink(request, token, linkId)).body?.name, "update changed the name").toBe(changedName);
|
|
68
|
+
await settle();
|
|
69
|
+
await undoOk(request, token, updateOp.undoToken, "undo update pay-link");
|
|
70
|
+
const afterUndo = await getLink(request, token, linkId);
|
|
71
|
+
expect(afterUndo.body?.name, "update\u2192undo restores the prior name (I1)").toBe(beforeName);
|
|
72
|
+
expect(typeof afterUndo.body?.updatedAt, "pay-link surfaces updatedAt").toBe("string");
|
|
73
|
+
expect(afterUndo.body?.updatedAt, "undo bumps updatedAt (I1)").not.toBe(before.body?.updatedAt);
|
|
74
|
+
await redoOk(request, token, updateOp.logId, "redo update pay-link");
|
|
75
|
+
expect((await getLink(request, token, linkId)).body?.name, "redo re-applies the renamed value (I6)").toBe(changedName);
|
|
76
|
+
} finally {
|
|
77
|
+
await deleteCheckoutEntityIfExists(request, token, "links", linkId);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
test("delete \u2192 undo re-materializes (I2) \u2192 redo re-deletes (I6)", async ({ request }) => {
|
|
81
|
+
const token = await getAuthToken(request, "admin");
|
|
82
|
+
let linkId = null;
|
|
83
|
+
try {
|
|
84
|
+
const link = await createLinkFixture(request, token, createFixedTemplateInput({ status: "draft" }));
|
|
85
|
+
linkId = link.id;
|
|
86
|
+
const beforeName = (await getLink(request, token, linkId)).body?.name;
|
|
87
|
+
await settle();
|
|
88
|
+
const deleteRes = await deleteLink(request, token, linkId);
|
|
89
|
+
expect(deleteRes.ok(), `delete status ${deleteRes.status()}`).toBeTruthy();
|
|
90
|
+
const deleteOp = expectOperation(deleteRes, "checkout.link.delete");
|
|
91
|
+
expect((await getLink(request, token, linkId)).status, "gone after delete").not.toBe(200);
|
|
92
|
+
await undoOk(request, token, deleteOp.undoToken, "undo delete pay-link");
|
|
93
|
+
const afterUndo = await getLink(request, token, linkId);
|
|
94
|
+
expect(afterUndo.status, "delete\u2192undo re-materializes the row (I2)").toBe(200);
|
|
95
|
+
expect(afterUndo.body?.name, "re-materialized record keeps its scalars (I2)").toBe(beforeName);
|
|
96
|
+
await redoOk(request, token, deleteOp.logId, "redo delete pay-link");
|
|
97
|
+
expect((await getLink(request, token, linkId)).status, "gone again after redo delete (I6)").not.toBe(200);
|
|
98
|
+
} finally {
|
|
99
|
+
await deleteCheckoutEntityIfExists(request, token, "links", linkId);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
test("create \u2192 undo \u2192 redo restores the SAME record (I6)", async ({ request }) => {
|
|
103
|
+
const token = await getAuthToken(request, "admin");
|
|
104
|
+
let linkId = null;
|
|
105
|
+
try {
|
|
106
|
+
const createRes = await apiRequest(request, "POST", LINKS, {
|
|
107
|
+
token,
|
|
108
|
+
data: createFixedTemplateInput({ status: "draft" })
|
|
109
|
+
});
|
|
110
|
+
const createOp = expectOperation(createRes, "checkout.link.create");
|
|
111
|
+
linkId = createOp.resourceId;
|
|
112
|
+
await settle();
|
|
113
|
+
const undoLogId = await undoOk(request, token, createOp.undoToken, "undo create pay-link");
|
|
114
|
+
expect((await getLink(request, token, linkId)).status, "gone after undo create").not.toBe(200);
|
|
115
|
+
await redoOk(request, token, undoLogId, "redo create pay-link");
|
|
116
|
+
const afterRedo = await getLink(request, token, linkId);
|
|
117
|
+
expect(afterRedo.status, "same pay-link restored after redo (I6)").toBe(200);
|
|
118
|
+
expect(afterRedo.body?.id, "redo preserves the original id (I6)").toBe(linkId);
|
|
119
|
+
} finally {
|
|
120
|
+
await deleteCheckoutEntityIfExists(request, token, "links", linkId);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
test("custom fields revert on undo and re-apply on redo (I4)", async ({ request }) => {
|
|
124
|
+
const token = await getAuthToken(request, "admin");
|
|
125
|
+
let linkId = null;
|
|
126
|
+
try {
|
|
127
|
+
const beforeValue = `before ${Date.now()}`;
|
|
128
|
+
const link = await createLinkFixture(
|
|
129
|
+
request,
|
|
130
|
+
token,
|
|
131
|
+
createFixedTemplateInput({
|
|
132
|
+
status: "draft",
|
|
133
|
+
customFieldsetCode: "service_package",
|
|
134
|
+
customFields: { delivery_timeline: beforeValue }
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
linkId = link.id;
|
|
138
|
+
expect(
|
|
139
|
+
(await getLink(request, token, linkId)).body?.customFields?.delivery_timeline,
|
|
140
|
+
"custom field persisted on create"
|
|
141
|
+
).toBe(beforeValue);
|
|
142
|
+
await settle();
|
|
143
|
+
const afterValue = `after ${Date.now()}`;
|
|
144
|
+
const updateRes = await updateLink(request, token, linkId, { customFields: { delivery_timeline: afterValue } });
|
|
145
|
+
const updateOp = expectOperation(updateRes, "checkout.link.update");
|
|
146
|
+
expect(
|
|
147
|
+
(await getLink(request, token, linkId)).body?.customFields?.delivery_timeline,
|
|
148
|
+
"update changed the custom field"
|
|
149
|
+
).toBe(afterValue);
|
|
150
|
+
await settle();
|
|
151
|
+
await undoOk(request, token, updateOp.undoToken, "undo update custom field");
|
|
152
|
+
expect(
|
|
153
|
+
(await getLink(request, token, linkId)).body?.customFields?.delivery_timeline,
|
|
154
|
+
"custom field reverts on undo (I4)"
|
|
155
|
+
).toBe(beforeValue);
|
|
156
|
+
await redoOk(request, token, updateOp.logId, "redo update custom field");
|
|
157
|
+
expect(
|
|
158
|
+
(await getLink(request, token, linkId)).body?.customFields?.delivery_timeline,
|
|
159
|
+
"custom field re-applies on redo (I4)"
|
|
160
|
+
).toBe(afterValue);
|
|
161
|
+
} finally {
|
|
162
|
+
await deleteCheckoutEntityIfExists(request, token, "links", linkId);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
test("null gatewayProviderKey edit \u2192 undo restores the prior gateway (#2540, I1)", async ({ request }) => {
|
|
166
|
+
const token = await getAuthToken(request, "admin");
|
|
167
|
+
let linkId = null;
|
|
168
|
+
try {
|
|
169
|
+
const link = await createLinkFixture(request, token, createFixedTemplateInput({ status: "draft" }));
|
|
170
|
+
linkId = link.id;
|
|
171
|
+
const before = await getLink(request, token, linkId);
|
|
172
|
+
expect(before.body?.gatewayProviderKey, "pay-link starts with the mock gateway").toBe("mock");
|
|
173
|
+
await settle();
|
|
174
|
+
const updateRes = await updateLink(request, token, linkId, {
|
|
175
|
+
name: "Pay link (no gateway)",
|
|
176
|
+
pricingMode: "fixed",
|
|
177
|
+
fixedPriceAmount: 49.99,
|
|
178
|
+
fixedPriceCurrencyCode: "USD",
|
|
179
|
+
status: "draft",
|
|
180
|
+
gatewayProviderKey: null
|
|
181
|
+
});
|
|
182
|
+
expect(
|
|
183
|
+
updateRes.ok(),
|
|
184
|
+
`clearing the gateway on a draft pay-link should succeed: ${updateRes.status()}`
|
|
185
|
+
).toBeTruthy();
|
|
186
|
+
const updateOp = expectOperation(updateRes, "checkout.link.update (null gateway)");
|
|
187
|
+
const afterUpdate = await getLink(request, token, linkId);
|
|
188
|
+
expect(afterUpdate.body?.gatewayProviderKey ?? null, "gateway cleared to null after edit").toBeNull();
|
|
189
|
+
expect(afterUpdate.body?.name, "name changed by edit").toBe("Pay link (no gateway)");
|
|
190
|
+
await settle();
|
|
191
|
+
await undoOk(request, token, updateOp.undoToken, "undo null-gateway edit");
|
|
192
|
+
const afterUndo = await getLink(request, token, linkId);
|
|
193
|
+
expect(afterUndo.body?.gatewayProviderKey, "undo restores the prior gateway (#2540, I1)").toBe("mock");
|
|
194
|
+
expect(afterUndo.body?.name, "undo restores the prior name (I1)").toBe(before.body?.name);
|
|
195
|
+
await redoOk(request, token, updateOp.logId, "redo null-gateway edit");
|
|
196
|
+
expect(
|
|
197
|
+
(await getLink(request, token, linkId)).body?.gatewayProviderKey ?? null,
|
|
198
|
+
"redo re-clears the gateway to null (I6)"
|
|
199
|
+
).toBeNull();
|
|
200
|
+
} finally {
|
|
201
|
+
await deleteCheckoutEntityIfExists(request, token, "links", linkId);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
//# sourceMappingURL=TC-UNDO-001-pay-link.spec.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/checkout/__integration__/TC-UNDO-001-pay-link.spec.ts"],
|
|
4
|
+
"sourcesContent": ["import { expect, test, type APIRequestContext } from '@playwright/test'\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api'\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'\nimport {\n expectOperation,\n undoOk,\n redoOk,\n expectTokenConsumed,\n skipIfUndoTestsDisabled,\n} from '@open-mercato/core/helpers/integration/undoHarness'\nimport {\n createFixedTemplateInput,\n createLinkFixture,\n deleteCheckoutEntityIfExists,\n deleteLink,\n updateLink,\n} from './helpers/fixtures'\n\n/**\n * TC-UNDO-001 (\u00A73 checkout.pay-link) \u2014 Undo/Redo correctness for the checkout pay-link\n * command bus, driven through the real `/api/checkout/links` routes plus the\n * `/api/audit_logs/audit-logs/actions/undo|redo` endpoints.\n *\n * Invariants asserted: I1 (update\u2192undo restores), I2 (delete\u2192undo re-materializes),\n * I3 (create\u2192undo soft-deletes), I4 (custom fields restore), I5 (token consumed),\n * I6 (redo reproduces post-state). checkout.link.create ships an id-preserving redo\n * handler, so the create\u2192undo\u2192redo SAME-id leg is asserted as corrected behaviour.\n *\n * Also covers the #2540 fix: a draft pay-link may persist a null gatewayProviderKey,\n * and editing it to null then undoing must restore the prior gateway.\n */\n\nconst LINKS = '/api/checkout/links'\n\ntype LinkRecord = {\n id?: string\n name?: string\n slug?: string\n updatedAt?: string\n gatewayProviderKey?: string | null\n customFields?: Record<string, unknown>\n}\n\n// The undo endpoint resolves the *latest* undoable log for a resource ordered by\n// millisecond-precision created_at, so consecutive mutations issued within the same\n// millisecond can tie. A short settle keeps each round-trip deterministic.\nasync function settle(): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, 50))\n}\n\nasync function getLink(\n request: APIRequestContext,\n token: string,\n id: string,\n): Promise<{ status: number; body: LinkRecord | null }> {\n const res = await apiRequest(request, 'GET', `${LINKS}/${encodeURIComponent(id)}`, { token })\n return { status: res.status(), body: await readJsonSafe<LinkRecord>(res) }\n}\n\ntest.describe('TC-UNDO-001 checkout.pay-link undo/redo', () => {\n test.beforeAll(() => {\n skipIfUndoTestsDisabled()\n })\n\n test('create \u2192 undo soft-deletes (I3) + token consumed (I5)', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n let linkId: string | null = null\n try {\n const createRes = await apiRequest(request, 'POST', LINKS, {\n token,\n data: createFixedTemplateInput({ status: 'draft' }),\n })\n expect(createRes.status(), `create status ${createRes.status()}`).toBe(201)\n const createOp = expectOperation(createRes, 'checkout.link.create')\n linkId = createOp.resourceId\n expect(linkId, 'create returns a resource id').toBeTruthy()\n\n expect((await getLink(request, token, linkId as string)).status, 'pay-link exists after create').toBe(200)\n\n await undoOk(request, token, createOp.undoToken, 'undo create pay-link')\n expect(\n (await getLink(request, token, linkId as string)).status,\n 'pay-link is gone after undoing create (I3 \u2014 soft-deleted, not readable)',\n ).not.toBe(200)\n\n await expectTokenConsumed(request, token, createOp.undoToken, 'checkout.link.create double-undo (I5)')\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'links', linkId)\n }\n })\n\n test('update \u2192 undo restores scalars (I1) \u2192 redo re-applies (I6)', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n let linkId: string | null = null\n try {\n const link = await createLinkFixture(request, token, createFixedTemplateInput({ status: 'draft' }))\n linkId = link.id\n const before = await getLink(request, token, linkId)\n const beforeName = before.body?.name\n expect(beforeName, 'pay-link name readable before update').toBeTruthy()\n\n await settle()\n const changedName = `Renamed by undo test ${Date.now()}`\n const updateRes = await updateLink(request, token, linkId, { name: changedName })\n expect(updateRes.status(), `update status ${updateRes.status()}`).toBe(200)\n const updateOp = expectOperation(updateRes, 'checkout.link.update')\n expect((await getLink(request, token, linkId)).body?.name, 'update changed the name').toBe(changedName)\n\n await settle()\n await undoOk(request, token, updateOp.undoToken, 'undo update pay-link')\n const afterUndo = await getLink(request, token, linkId)\n expect(afterUndo.body?.name, 'update\u2192undo restores the prior name (I1)').toBe(beforeName)\n expect(typeof afterUndo.body?.updatedAt, 'pay-link surfaces updatedAt').toBe('string')\n expect(afterUndo.body?.updatedAt, 'undo bumps updatedAt (I1)').not.toBe(before.body?.updatedAt)\n\n await redoOk(request, token, updateOp.logId, 'redo update pay-link')\n expect((await getLink(request, token, linkId)).body?.name, 'redo re-applies the renamed value (I6)').toBe(changedName)\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'links', linkId)\n }\n })\n\n test('delete \u2192 undo re-materializes (I2) \u2192 redo re-deletes (I6)', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n let linkId: string | null = null\n try {\n const link = await createLinkFixture(request, token, createFixedTemplateInput({ status: 'draft' }))\n linkId = link.id\n const beforeName = (await getLink(request, token, linkId)).body?.name\n\n await settle()\n const deleteRes = await deleteLink(request, token, linkId)\n expect(deleteRes.ok(), `delete status ${deleteRes.status()}`).toBeTruthy()\n const deleteOp = expectOperation(deleteRes, 'checkout.link.delete')\n expect((await getLink(request, token, linkId)).status, 'gone after delete').not.toBe(200)\n\n await undoOk(request, token, deleteOp.undoToken, 'undo delete pay-link')\n const afterUndo = await getLink(request, token, linkId)\n expect(afterUndo.status, 'delete\u2192undo re-materializes the row (I2)').toBe(200)\n expect(afterUndo.body?.name, 're-materialized record keeps its scalars (I2)').toBe(beforeName)\n\n await redoOk(request, token, deleteOp.logId, 'redo delete pay-link')\n expect((await getLink(request, token, linkId)).status, 'gone again after redo delete (I6)').not.toBe(200)\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'links', linkId)\n }\n })\n\n test('create \u2192 undo \u2192 redo restores the SAME record (I6)', async ({ request }) => {\n // checkout.link.create registers an id-preserving redo handler, so redo must restore\n // the original soft-deleted row \u2014 not mint a new id (the #2468 customers.people defect).\n const token = await getAuthToken(request, 'admin')\n let linkId: string | null = null\n try {\n const createRes = await apiRequest(request, 'POST', LINKS, {\n token,\n data: createFixedTemplateInput({ status: 'draft' }),\n })\n const createOp = expectOperation(createRes, 'checkout.link.create')\n linkId = createOp.resourceId\n\n await settle()\n const undoLogId = await undoOk(request, token, createOp.undoToken, 'undo create pay-link')\n expect((await getLink(request, token, linkId as string)).status, 'gone after undo create').not.toBe(200)\n\n await redoOk(request, token, undoLogId, 'redo create pay-link')\n const afterRedo = await getLink(request, token, linkId as string)\n expect(afterRedo.status, 'same pay-link restored after redo (I6)').toBe(200)\n expect(afterRedo.body?.id, 'redo preserves the original id (I6)').toBe(linkId)\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'links', linkId)\n }\n })\n\n test('custom fields revert on undo and re-apply on redo (I4)', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n let linkId: string | null = null\n try {\n const beforeValue = `before ${Date.now()}`\n const link = await createLinkFixture(\n request,\n token,\n createFixedTemplateInput({\n status: 'draft',\n customFieldsetCode: 'service_package',\n customFields: { delivery_timeline: beforeValue },\n }),\n )\n linkId = link.id\n expect(\n (await getLink(request, token, linkId)).body?.customFields?.delivery_timeline,\n 'custom field persisted on create',\n ).toBe(beforeValue)\n\n await settle()\n const afterValue = `after ${Date.now()}`\n const updateRes = await updateLink(request, token, linkId, { customFields: { delivery_timeline: afterValue } })\n const updateOp = expectOperation(updateRes, 'checkout.link.update')\n expect(\n (await getLink(request, token, linkId)).body?.customFields?.delivery_timeline,\n 'update changed the custom field',\n ).toBe(afterValue)\n\n await settle()\n await undoOk(request, token, updateOp.undoToken, 'undo update custom field')\n expect(\n (await getLink(request, token, linkId)).body?.customFields?.delivery_timeline,\n 'custom field reverts on undo (I4)',\n ).toBe(beforeValue)\n\n await redoOk(request, token, updateOp.logId, 'redo update custom field')\n expect(\n (await getLink(request, token, linkId)).body?.customFields?.delivery_timeline,\n 'custom field re-applies on redo (I4)',\n ).toBe(afterValue)\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'links', linkId)\n }\n })\n\n test('null gatewayProviderKey edit \u2192 undo restores the prior gateway (#2540, I1)', async ({ request }) => {\n // #2540: a draft pay-link may persist a null gatewayProviderKey. Editing the gateway to\n // null must round-trip, and undoing the edit must restore the prior gateway value.\n const token = await getAuthToken(request, 'admin')\n let linkId: string | null = null\n try {\n const link = await createLinkFixture(request, token, createFixedTemplateInput({ status: 'draft' }))\n linkId = link.id\n const before = await getLink(request, token, linkId)\n expect(before.body?.gatewayProviderKey, 'pay-link starts with the mock gateway').toBe('mock')\n\n await settle()\n const updateRes = await updateLink(request, token, linkId, {\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 updateRes.ok(),\n `clearing the gateway on a draft pay-link should succeed: ${updateRes.status()}`,\n ).toBeTruthy()\n const updateOp = expectOperation(updateRes, 'checkout.link.update (null gateway)')\n\n const afterUpdate = await getLink(request, token, linkId)\n expect(afterUpdate.body?.gatewayProviderKey ?? null, 'gateway cleared to null after edit').toBeNull()\n expect(afterUpdate.body?.name, 'name changed by edit').toBe('Pay link (no gateway)')\n\n await settle()\n await undoOk(request, token, updateOp.undoToken, 'undo null-gateway edit')\n const afterUndo = await getLink(request, token, linkId)\n expect(afterUndo.body?.gatewayProviderKey, 'undo restores the prior gateway (#2540, I1)').toBe('mock')\n expect(afterUndo.body?.name, 'undo restores the prior name (I1)').toBe(before.body?.name)\n\n await redoOk(request, token, updateOp.logId, 'redo null-gateway edit')\n expect(\n (await getLink(request, token, linkId)).body?.gatewayProviderKey ?? null,\n 'redo re-clears the gateway to null (I6)',\n ).toBeNull()\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'links', linkId)\n }\n })\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { apiRequest, getAuthToken } from "@open-mercato/core/helpers/integration/api";
|
|
3
|
+
import { readJsonSafe } from "@open-mercato/core/helpers/integration/generalFixtures";
|
|
4
|
+
import {
|
|
5
|
+
expectOperation,
|
|
6
|
+
undoOk,
|
|
7
|
+
redoOk,
|
|
8
|
+
expectTokenConsumed,
|
|
9
|
+
skipIfUndoTestsDisabled
|
|
10
|
+
} from "@open-mercato/core/helpers/integration/undoHarness";
|
|
11
|
+
import {
|
|
12
|
+
createFixedTemplateInput,
|
|
13
|
+
createTemplateFixture,
|
|
14
|
+
deleteCheckoutEntityIfExists,
|
|
15
|
+
deleteTemplate,
|
|
16
|
+
updateTemplate
|
|
17
|
+
} from "./helpers/fixtures.js";
|
|
18
|
+
const TEMPLATES = "/api/checkout/templates";
|
|
19
|
+
async function settle() {
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
21
|
+
}
|
|
22
|
+
async function getTemplate(request, token, id) {
|
|
23
|
+
const res = await apiRequest(request, "GET", `${TEMPLATES}/${encodeURIComponent(id)}`, { token });
|
|
24
|
+
return { status: res.status(), body: await readJsonSafe(res) };
|
|
25
|
+
}
|
|
26
|
+
test.describe("TC-UNDO-001 checkout.template undo/redo", () => {
|
|
27
|
+
test.beforeAll(() => {
|
|
28
|
+
skipIfUndoTestsDisabled();
|
|
29
|
+
});
|
|
30
|
+
test("create \u2192 undo soft-deletes (I3) + token consumed (I5)", async ({ request }) => {
|
|
31
|
+
const token = await getAuthToken(request, "admin");
|
|
32
|
+
let templateId = null;
|
|
33
|
+
try {
|
|
34
|
+
const createRes = await apiRequest(request, "POST", TEMPLATES, {
|
|
35
|
+
token,
|
|
36
|
+
data: createFixedTemplateInput({ status: "draft" })
|
|
37
|
+
});
|
|
38
|
+
expect(createRes.status(), `create status ${createRes.status()}`).toBe(201);
|
|
39
|
+
const createOp = expectOperation(createRes, "checkout.template.create");
|
|
40
|
+
templateId = createOp.resourceId;
|
|
41
|
+
expect(templateId, "create returns a resource id").toBeTruthy();
|
|
42
|
+
expect((await getTemplate(request, token, templateId)).status, "template exists after create").toBe(200);
|
|
43
|
+
await undoOk(request, token, createOp.undoToken, "undo create template");
|
|
44
|
+
expect(
|
|
45
|
+
(await getTemplate(request, token, templateId)).status,
|
|
46
|
+
"template is gone after undoing create (I3 \u2014 soft-deleted, not readable)"
|
|
47
|
+
).not.toBe(200);
|
|
48
|
+
await expectTokenConsumed(request, token, createOp.undoToken, "checkout.template.create double-undo (I5)");
|
|
49
|
+
} finally {
|
|
50
|
+
await deleteCheckoutEntityIfExists(request, token, "templates", templateId);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
test("update \u2192 undo restores scalars (I1) \u2192 redo re-applies (I6)", async ({ request }) => {
|
|
54
|
+
const token = await getAuthToken(request, "admin");
|
|
55
|
+
let templateId = null;
|
|
56
|
+
try {
|
|
57
|
+
templateId = await createTemplateFixture(request, token, createFixedTemplateInput({ status: "draft" }));
|
|
58
|
+
const before = await getTemplate(request, token, templateId);
|
|
59
|
+
const beforeName = before.body?.name;
|
|
60
|
+
expect(beforeName, "template name readable before update").toBeTruthy();
|
|
61
|
+
await settle();
|
|
62
|
+
const changedName = `Renamed by undo test ${Date.now()}`;
|
|
63
|
+
const updateRes = await updateTemplate(request, token, templateId, { name: changedName });
|
|
64
|
+
expect(updateRes.status(), `update status ${updateRes.status()}`).toBe(200);
|
|
65
|
+
const updateOp = expectOperation(updateRes, "checkout.template.update");
|
|
66
|
+
expect((await getTemplate(request, token, templateId)).body?.name, "update changed the name").toBe(changedName);
|
|
67
|
+
await settle();
|
|
68
|
+
await undoOk(request, token, updateOp.undoToken, "undo update template");
|
|
69
|
+
const afterUndo = await getTemplate(request, token, templateId);
|
|
70
|
+
expect(afterUndo.body?.name, "update\u2192undo restores the prior name (I1)").toBe(beforeName);
|
|
71
|
+
expect(typeof afterUndo.body?.updatedAt, "template surfaces updatedAt").toBe("string");
|
|
72
|
+
expect(afterUndo.body?.updatedAt, "undo bumps updatedAt (I1)").not.toBe(before.body?.updatedAt);
|
|
73
|
+
await redoOk(request, token, updateOp.logId, "redo update template");
|
|
74
|
+
expect(
|
|
75
|
+
(await getTemplate(request, token, templateId)).body?.name,
|
|
76
|
+
"redo re-applies the renamed value (I6)"
|
|
77
|
+
).toBe(changedName);
|
|
78
|
+
} finally {
|
|
79
|
+
await deleteCheckoutEntityIfExists(request, token, "templates", templateId);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
test("delete \u2192 undo re-materializes (I2) \u2192 redo re-deletes (I6)", async ({ request }) => {
|
|
83
|
+
const token = await getAuthToken(request, "admin");
|
|
84
|
+
let templateId = null;
|
|
85
|
+
try {
|
|
86
|
+
templateId = await createTemplateFixture(request, token, createFixedTemplateInput({ status: "draft" }));
|
|
87
|
+
const beforeName = (await getTemplate(request, token, templateId)).body?.name;
|
|
88
|
+
await settle();
|
|
89
|
+
const deleteRes = await deleteTemplate(request, token, templateId);
|
|
90
|
+
expect(deleteRes.ok(), `delete status ${deleteRes.status()}`).toBeTruthy();
|
|
91
|
+
const deleteOp = expectOperation(deleteRes, "checkout.template.delete");
|
|
92
|
+
expect((await getTemplate(request, token, templateId)).status, "gone after delete").not.toBe(200);
|
|
93
|
+
await undoOk(request, token, deleteOp.undoToken, "undo delete template");
|
|
94
|
+
const afterUndo = await getTemplate(request, token, templateId);
|
|
95
|
+
expect(afterUndo.status, "delete\u2192undo re-materializes the row (I2)").toBe(200);
|
|
96
|
+
expect(afterUndo.body?.name, "re-materialized record keeps its scalars (I2)").toBe(beforeName);
|
|
97
|
+
await redoOk(request, token, deleteOp.logId, "redo delete template");
|
|
98
|
+
expect(
|
|
99
|
+
(await getTemplate(request, token, templateId)).status,
|
|
100
|
+
"gone again after redo delete (I6)"
|
|
101
|
+
).not.toBe(200);
|
|
102
|
+
} finally {
|
|
103
|
+
await deleteCheckoutEntityIfExists(request, token, "templates", templateId);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
test("create \u2192 undo \u2192 redo restores the SAME record (I6)", async ({ request }) => {
|
|
107
|
+
const token = await getAuthToken(request, "admin");
|
|
108
|
+
let templateId = null;
|
|
109
|
+
try {
|
|
110
|
+
const createRes = await apiRequest(request, "POST", TEMPLATES, {
|
|
111
|
+
token,
|
|
112
|
+
data: createFixedTemplateInput({ status: "draft" })
|
|
113
|
+
});
|
|
114
|
+
const createOp = expectOperation(createRes, "checkout.template.create");
|
|
115
|
+
templateId = createOp.resourceId;
|
|
116
|
+
await settle();
|
|
117
|
+
const undoLogId = await undoOk(request, token, createOp.undoToken, "undo create template");
|
|
118
|
+
expect((await getTemplate(request, token, templateId)).status, "gone after undo create").not.toBe(200);
|
|
119
|
+
await redoOk(request, token, undoLogId, "redo create template");
|
|
120
|
+
const afterRedo = await getTemplate(request, token, templateId);
|
|
121
|
+
expect(afterRedo.status, "same template restored after redo (I6)").toBe(200);
|
|
122
|
+
expect(afterRedo.body?.id, "redo preserves the original id (I6)").toBe(templateId);
|
|
123
|
+
} finally {
|
|
124
|
+
await deleteCheckoutEntityIfExists(request, token, "templates", templateId);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
test("custom fields revert on undo and re-apply on redo (I4)", async ({ request }) => {
|
|
128
|
+
const token = await getAuthToken(request, "admin");
|
|
129
|
+
let templateId = null;
|
|
130
|
+
try {
|
|
131
|
+
const beforeValue = `before ${Date.now()}`;
|
|
132
|
+
templateId = await createTemplateFixture(
|
|
133
|
+
request,
|
|
134
|
+
token,
|
|
135
|
+
createFixedTemplateInput({
|
|
136
|
+
status: "draft",
|
|
137
|
+
customFieldsetCode: "service_package",
|
|
138
|
+
customFields: { delivery_timeline: beforeValue }
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
expect(
|
|
142
|
+
(await getTemplate(request, token, templateId)).body?.customFields?.delivery_timeline,
|
|
143
|
+
"custom field persisted on create"
|
|
144
|
+
).toBe(beforeValue);
|
|
145
|
+
await settle();
|
|
146
|
+
const afterValue = `after ${Date.now()}`;
|
|
147
|
+
const updateRes = await updateTemplate(request, token, templateId, {
|
|
148
|
+
customFields: { delivery_timeline: afterValue }
|
|
149
|
+
});
|
|
150
|
+
const updateOp = expectOperation(updateRes, "checkout.template.update");
|
|
151
|
+
expect(
|
|
152
|
+
(await getTemplate(request, token, templateId)).body?.customFields?.delivery_timeline,
|
|
153
|
+
"update changed the custom field"
|
|
154
|
+
).toBe(afterValue);
|
|
155
|
+
await settle();
|
|
156
|
+
await undoOk(request, token, updateOp.undoToken, "undo update custom field");
|
|
157
|
+
expect(
|
|
158
|
+
(await getTemplate(request, token, templateId)).body?.customFields?.delivery_timeline,
|
|
159
|
+
"custom field reverts on undo (I4)"
|
|
160
|
+
).toBe(beforeValue);
|
|
161
|
+
await redoOk(request, token, updateOp.logId, "redo update custom field");
|
|
162
|
+
expect(
|
|
163
|
+
(await getTemplate(request, token, templateId)).body?.customFields?.delivery_timeline,
|
|
164
|
+
"custom field re-applies on redo (I4)"
|
|
165
|
+
).toBe(afterValue);
|
|
166
|
+
} finally {
|
|
167
|
+
await deleteCheckoutEntityIfExists(request, token, "templates", templateId);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
//# sourceMappingURL=TC-UNDO-001-template.spec.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/checkout/__integration__/TC-UNDO-001-template.spec.ts"],
|
|
4
|
+
"sourcesContent": ["import { expect, test, type APIRequestContext } from '@playwright/test'\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api'\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'\nimport {\n expectOperation,\n undoOk,\n redoOk,\n expectTokenConsumed,\n skipIfUndoTestsDisabled,\n} from '@open-mercato/core/helpers/integration/undoHarness'\nimport {\n createFixedTemplateInput,\n createTemplateFixture,\n deleteCheckoutEntityIfExists,\n deleteTemplate,\n updateTemplate,\n} from './helpers/fixtures'\n\n/**\n * TC-UNDO-001 (\u00A73 checkout.template) \u2014 Undo/Redo correctness for the checkout template\n * command bus, driven through the real `/api/checkout/templates` routes plus the\n * `/api/audit_logs/audit-logs/actions/undo|redo` endpoints.\n *\n * Invariants asserted (per the #2468 tracking issue):\n * I1 update\u2192undo restores scalars (and bumps updatedAt)\n * I2 delete\u2192undo re-materializes the soft-deleted row\n * I3 create\u2192undo soft-deletes (never hard-deletes)\n * I4 custom fields revert on undo and re-apply on redo\n * I5 a consumed undo token is rejected on a second undo\n * I6 redo reproduces the command's post-state\n *\n * Unlike the customers.people pilot (where create\u2192undo\u2192redo mints a new id, #2468),\n * checkout.template.create ships an id-preserving redo handler, so the SAME-id redo\n * leg is asserted as corrected behaviour rather than quarantined.\n */\n\nconst TEMPLATES = '/api/checkout/templates'\n\ntype TemplateRecord = {\n id?: string\n name?: string\n updatedAt?: string\n gatewayProviderKey?: string | null\n customFields?: Record<string, unknown>\n}\n\n// The undo endpoint resolves the *latest* undoable log for a resource ordered by\n// millisecond-precision created_at, so consecutive mutations issued within the same\n// millisecond can tie. A short settle keeps each round-trip deterministic.\nasync function settle(): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, 50))\n}\n\nasync function getTemplate(\n request: APIRequestContext,\n token: string,\n id: string,\n): Promise<{ status: number; body: TemplateRecord | null }> {\n const res = await apiRequest(request, 'GET', `${TEMPLATES}/${encodeURIComponent(id)}`, { token })\n return { status: res.status(), body: await readJsonSafe<TemplateRecord>(res) }\n}\n\ntest.describe('TC-UNDO-001 checkout.template undo/redo', () => {\n test.beforeAll(() => {\n skipIfUndoTestsDisabled()\n })\n\n test('create \u2192 undo soft-deletes (I3) + token consumed (I5)', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n let templateId: string | null = null\n try {\n const createRes = await apiRequest(request, 'POST', TEMPLATES, {\n token,\n data: createFixedTemplateInput({ status: 'draft' }),\n })\n expect(createRes.status(), `create status ${createRes.status()}`).toBe(201)\n const createOp = expectOperation(createRes, 'checkout.template.create')\n templateId = createOp.resourceId\n expect(templateId, 'create returns a resource id').toBeTruthy()\n\n expect((await getTemplate(request, token, templateId as string)).status, 'template exists after create').toBe(200)\n\n await undoOk(request, token, createOp.undoToken, 'undo create template')\n expect(\n (await getTemplate(request, token, templateId as string)).status,\n 'template is gone after undoing create (I3 \u2014 soft-deleted, not readable)',\n ).not.toBe(200)\n\n await expectTokenConsumed(request, token, createOp.undoToken, 'checkout.template.create double-undo (I5)')\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'templates', templateId)\n }\n })\n\n test('update \u2192 undo restores scalars (I1) \u2192 redo re-applies (I6)', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n let templateId: string | null = null\n try {\n templateId = await createTemplateFixture(request, token, createFixedTemplateInput({ status: 'draft' }))\n const before = await getTemplate(request, token, templateId)\n const beforeName = before.body?.name\n expect(beforeName, 'template name readable before update').toBeTruthy()\n\n await settle()\n const changedName = `Renamed by undo test ${Date.now()}`\n const updateRes = await updateTemplate(request, token, templateId, { name: changedName })\n expect(updateRes.status(), `update status ${updateRes.status()}`).toBe(200)\n const updateOp = expectOperation(updateRes, 'checkout.template.update')\n expect((await getTemplate(request, token, templateId)).body?.name, 'update changed the name').toBe(changedName)\n\n await settle()\n await undoOk(request, token, updateOp.undoToken, 'undo update template')\n const afterUndo = await getTemplate(request, token, templateId)\n expect(afterUndo.body?.name, 'update\u2192undo restores the prior name (I1)').toBe(beforeName)\n expect(typeof afterUndo.body?.updatedAt, 'template surfaces updatedAt').toBe('string')\n expect(afterUndo.body?.updatedAt, 'undo bumps updatedAt (I1)').not.toBe(before.body?.updatedAt)\n\n await redoOk(request, token, updateOp.logId, 'redo update template')\n expect(\n (await getTemplate(request, token, templateId)).body?.name,\n 'redo re-applies the renamed value (I6)',\n ).toBe(changedName)\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'templates', templateId)\n }\n })\n\n test('delete \u2192 undo re-materializes (I2) \u2192 redo re-deletes (I6)', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n let templateId: string | null = null\n try {\n templateId = await createTemplateFixture(request, token, createFixedTemplateInput({ status: 'draft' }))\n const beforeName = (await getTemplate(request, token, templateId)).body?.name\n\n await settle()\n const deleteRes = await deleteTemplate(request, token, templateId)\n expect(deleteRes.ok(), `delete status ${deleteRes.status()}`).toBeTruthy()\n const deleteOp = expectOperation(deleteRes, 'checkout.template.delete')\n expect((await getTemplate(request, token, templateId)).status, 'gone after delete').not.toBe(200)\n\n await undoOk(request, token, deleteOp.undoToken, 'undo delete template')\n const afterUndo = await getTemplate(request, token, templateId)\n expect(afterUndo.status, 'delete\u2192undo re-materializes the row (I2)').toBe(200)\n expect(afterUndo.body?.name, 're-materialized record keeps its scalars (I2)').toBe(beforeName)\n\n await redoOk(request, token, deleteOp.logId, 'redo delete template')\n expect(\n (await getTemplate(request, token, templateId)).status,\n 'gone again after redo delete (I6)',\n ).not.toBe(200)\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'templates', templateId)\n }\n })\n\n test('create \u2192 undo \u2192 redo restores the SAME record (I6)', async ({ request }) => {\n // checkout.template.create registers an id-preserving redo handler, so redo must restore\n // the original soft-deleted row \u2014 not mint a new id (the #2468 customers.people defect).\n const token = await getAuthToken(request, 'admin')\n let templateId: string | null = null\n try {\n const createRes = await apiRequest(request, 'POST', TEMPLATES, {\n token,\n data: createFixedTemplateInput({ status: 'draft' }),\n })\n const createOp = expectOperation(createRes, 'checkout.template.create')\n templateId = createOp.resourceId\n\n await settle()\n const undoLogId = await undoOk(request, token, createOp.undoToken, 'undo create template')\n expect((await getTemplate(request, token, templateId as string)).status, 'gone after undo create').not.toBe(200)\n\n await redoOk(request, token, undoLogId, 'redo create template')\n const afterRedo = await getTemplate(request, token, templateId as string)\n expect(afterRedo.status, 'same template restored after redo (I6)').toBe(200)\n expect(afterRedo.body?.id, 'redo preserves the original id (I6)').toBe(templateId)\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'templates', templateId)\n }\n })\n\n test('custom fields revert on undo and re-apply on redo (I4)', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n let templateId: string | null = null\n try {\n const beforeValue = `before ${Date.now()}`\n templateId = await createTemplateFixture(\n request,\n token,\n createFixedTemplateInput({\n status: 'draft',\n customFieldsetCode: 'service_package',\n customFields: { delivery_timeline: beforeValue },\n }),\n )\n expect(\n (await getTemplate(request, token, templateId)).body?.customFields?.delivery_timeline,\n 'custom field persisted on create',\n ).toBe(beforeValue)\n\n await settle()\n const afterValue = `after ${Date.now()}`\n const updateRes = await updateTemplate(request, token, templateId, {\n customFields: { delivery_timeline: afterValue },\n })\n const updateOp = expectOperation(updateRes, 'checkout.template.update')\n expect(\n (await getTemplate(request, token, templateId)).body?.customFields?.delivery_timeline,\n 'update changed the custom field',\n ).toBe(afterValue)\n\n await settle()\n await undoOk(request, token, updateOp.undoToken, 'undo update custom field')\n expect(\n (await getTemplate(request, token, templateId)).body?.customFields?.delivery_timeline,\n 'custom field reverts on undo (I4)',\n ).toBe(beforeValue)\n\n await redoOk(request, token, updateOp.logId, 'redo update custom field')\n expect(\n (await getTemplate(request, token, templateId)).body?.customFields?.delivery_timeline,\n 'custom field re-applies on redo (I4)',\n ).toBe(afterValue)\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'templates', templateId)\n }\n })\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -23,6 +23,7 @@ import { slugify } from "@open-mercato/shared/lib/slugify";
|
|
|
23
23
|
import { CrudForm } from "@open-mercato/ui/backend/CrudForm";
|
|
24
24
|
import { ComboboxInput } from "@open-mercato/ui/backend/inputs";
|
|
25
25
|
import { Page, PageBody } from "@open-mercato/ui/backend/Page";
|
|
26
|
+
import { RecordNotFoundState } from "@open-mercato/ui/backend/detail";
|
|
26
27
|
import { SwitchableMarkdownInput } from "@open-mercato/ui/backend/inputs";
|
|
27
28
|
import { apiCall, apiCallOrThrow, readApiResultOrThrow, withScopedApiRequestHeaders } from "@open-mercato/ui/backend/utils/apiCall";
|
|
28
29
|
import { buildOptimisticLockHeader } from "@open-mercato/ui/backend/utils/optimisticLock";
|
|
@@ -39,6 +40,7 @@ import { CHECKOUT_ENTITY_IDS } from "../lib/constants.js";
|
|
|
39
40
|
import { getLocalizedDefaultCheckoutCustomerFields } from "../lib/defaults.js";
|
|
40
41
|
import { getGatewayProviderConfigurationMessageKey } from "../lib/gatewayProviderAvailability.js";
|
|
41
42
|
import { readCustomerFieldsSectionError } from "../lib/customerFieldErrors.js";
|
|
43
|
+
import { isRecordNotFoundError } from "../lib/recordNotFound.js";
|
|
42
44
|
import { CheckoutCurrencySelect } from "./CheckoutCurrencySelect.js";
|
|
43
45
|
import { CustomerFieldsEditor } from "./CustomerFieldsEditor.js";
|
|
44
46
|
import { GatewaySettingsFields } from "./GatewaySettingsFields.js";
|
|
@@ -1155,6 +1157,7 @@ function LinkTemplateForm({ mode, recordId }) {
|
|
|
1155
1157
|
const [initialValues, setInitialValues] = React.useState(
|
|
1156
1158
|
recordId ? null : normalizeFormValues(createDefaultValues(t), t)
|
|
1157
1159
|
);
|
|
1160
|
+
const [notFound, setNotFound] = React.useState(false);
|
|
1158
1161
|
const replaceInitialValues = React.useCallback((nextValues) => {
|
|
1159
1162
|
setInitialValues(nextValues);
|
|
1160
1163
|
setFormInstanceKey((current) => current + 1);
|
|
@@ -1235,11 +1238,17 @@ function LinkTemplateForm({ mode, recordId }) {
|
|
|
1235
1238
|
React.useEffect(() => {
|
|
1236
1239
|
if (!recordId) return;
|
|
1237
1240
|
let active = true;
|
|
1241
|
+
setNotFound(false);
|
|
1238
1242
|
void readApiResultOrThrow(`/api/checkout/${mode === "link" ? "links" : "templates"}/${encodeURIComponent(recordId)}`).then((result) => {
|
|
1239
1243
|
if (!active) return;
|
|
1240
1244
|
replaceInitialValues(normalizeFormValues(result, t));
|
|
1241
|
-
}).catch(() => {
|
|
1242
|
-
if (active)
|
|
1245
|
+
}).catch((error) => {
|
|
1246
|
+
if (!active) return;
|
|
1247
|
+
if (isRecordNotFoundError(error)) {
|
|
1248
|
+
setNotFound(true);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
replaceInitialValues(normalizeFormValues({}, t));
|
|
1243
1252
|
});
|
|
1244
1253
|
return () => {
|
|
1245
1254
|
active = false;
|
|
@@ -1357,6 +1366,17 @@ function LinkTemplateForm({ mode, recordId }) {
|
|
|
1357
1366
|
/* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-foreground", children: t("checkout.linkTemplateForm.locked.overlayTitle") }),
|
|
1358
1367
|
/* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: t("checkout.linkTemplateForm.locked.overlayDescription") })
|
|
1359
1368
|
] }) : void 0;
|
|
1369
|
+
const listHref = mode === "link" ? "/backend/checkout/pay-links" : "/backend/checkout/templates";
|
|
1370
|
+
if (notFound) {
|
|
1371
|
+
return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx(
|
|
1372
|
+
RecordNotFoundState,
|
|
1373
|
+
{
|
|
1374
|
+
label: t(mode === "link" ? "checkout.linkTemplateForm.notFound.link.title" : "checkout.linkTemplateForm.notFound.template.title"),
|
|
1375
|
+
description: t(mode === "link" ? "checkout.linkTemplateForm.notFound.link.description" : "checkout.linkTemplateForm.notFound.template.description"),
|
|
1376
|
+
backHref: listHref
|
|
1377
|
+
}
|
|
1378
|
+
) }) });
|
|
1379
|
+
}
|
|
1360
1380
|
return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { children: initialValues ? /* @__PURE__ */ jsx(
|
|
1361
1381
|
CrudForm,
|
|
1362
1382
|
{
|
|
@@ -1434,6 +1454,10 @@ function LinkTemplateForm({ mode, recordId }) {
|
|
|
1434
1454
|
);
|
|
1435
1455
|
} catch (error) {
|
|
1436
1456
|
if (surfaceRecordConflict(error, t)) return;
|
|
1457
|
+
if (recordId && isRecordNotFoundError(error)) {
|
|
1458
|
+
setNotFound(true);
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1437
1461
|
throw error;
|
|
1438
1462
|
}
|
|
1439
1463
|
const targetId = recordId ?? (typeof response?.id === "string" ? response.id : null);
|
|
@@ -1478,6 +1502,10 @@ function LinkTemplateForm({ mode, recordId }) {
|
|
|
1478
1502
|
);
|
|
1479
1503
|
} catch (error) {
|
|
1480
1504
|
if (surfaceRecordConflict(error, t)) return;
|
|
1505
|
+
if (isRecordNotFoundError(error)) {
|
|
1506
|
+
setNotFound(true);
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1481
1509
|
throw error;
|
|
1482
1510
|
}
|
|
1483
1511
|
window.location.href = mode === "link" ? `/backend/checkout/pay-links?flash=${encodeURIComponent(t("checkout.common.flash.deleted"))}&type=success` : `/backend/checkout/templates?flash=${encodeURIComponent(t("checkout.common.flash.deleted"))}&type=success`;
|