@open-mercato/checkout 0.7.1-develop.7150.1.c1941e0c22 → 0.7.1-develop.7152.1.a69e92f9c9
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-CHKT-EMAIL-001.spec.js +61 -0
- package/dist/modules/checkout/__integration__/TC-CHKT-EMAIL-001.spec.js.map +7 -0
- package/dist/modules/checkout/__integration__/helpers/fixtures.js.map +2 -2
- package/dist/modules/checkout/workers/send-email.worker.js +6 -0
- package/dist/modules/checkout/workers/send-email.worker.js.map +2 -2
- package/package.json +5 -5
- package/src/modules/checkout/__integration__/TC-CHKT-EMAIL-001.spec.ts +67 -0
- package/src/modules/checkout/__integration__/helpers/fixtures.ts +9 -0
- package/src/modules/checkout/workers/send-email.worker.ts +6 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:checkout] found
|
|
1
|
+
[build:checkout] found 163 entry points
|
|
2
2
|
[build:checkout] built successfully
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { getAuthToken } from "@open-mercato/core/helpers/integration/api";
|
|
3
|
+
import { getTokenScope, readJsonSafe } from "@open-mercato/core/helpers/integration/generalFixtures";
|
|
4
|
+
import { drainIntegrationQueue } from "@open-mercato/core/helpers/integration/queue";
|
|
5
|
+
import {
|
|
6
|
+
clearCapturedSystemEmails,
|
|
7
|
+
isChannelSeedingAvailable,
|
|
8
|
+
seedSystemEmailChannel,
|
|
9
|
+
waitForCapturedSystemEmail
|
|
10
|
+
} from "@open-mercato/core/helpers/integration/communicationChannelsFixtures";
|
|
11
|
+
import {
|
|
12
|
+
createCustomerData,
|
|
13
|
+
createFixedTemplateInput,
|
|
14
|
+
createLinkFixture,
|
|
15
|
+
deleteCheckoutEntityIfExists,
|
|
16
|
+
submitPayLink
|
|
17
|
+
} from "./helpers/fixtures.js";
|
|
18
|
+
test.describe("TC-CHKT-EMAIL-001: Checkout transactional email uses system channel", () => {
|
|
19
|
+
test("public checkout submit dispatches payment-start email through Communications Hub", async ({ request }) => {
|
|
20
|
+
test.slow();
|
|
21
|
+
const token = await getAuthToken(request, "admin");
|
|
22
|
+
const scope = getTokenScope(token);
|
|
23
|
+
const seedingAvailable = await isChannelSeedingAvailable(request, token);
|
|
24
|
+
test.skip(!seedingAvailable, "OM_ENABLE_TEST_CHANNEL_SEEDING is not enabled.");
|
|
25
|
+
await seedSystemEmailChannel(request, token);
|
|
26
|
+
await clearCapturedSystemEmails(request, token);
|
|
27
|
+
const stamp = Date.now();
|
|
28
|
+
const customerEmail = `qa-checkout-email-${stamp}@example.test`;
|
|
29
|
+
let linkId = null;
|
|
30
|
+
try {
|
|
31
|
+
const link = await createLinkFixture(request, token, createFixedTemplateInput({
|
|
32
|
+
title: `QA checkout email ${stamp}`,
|
|
33
|
+
status: "active",
|
|
34
|
+
startEmailSubject: `Checkout started ${stamp}`,
|
|
35
|
+
sendStartEmail: true
|
|
36
|
+
}));
|
|
37
|
+
linkId = link.id;
|
|
38
|
+
const submit = await submitPayLink(request, link.slug, {
|
|
39
|
+
customerData: createCustomerData({ email: customerEmail }),
|
|
40
|
+
acceptedLegalConsents: {}
|
|
41
|
+
});
|
|
42
|
+
expect(submit.status()).toBe(201);
|
|
43
|
+
const body = await readJsonSafe(submit);
|
|
44
|
+
expect(typeof body?.transactionId).toBe("string");
|
|
45
|
+
await drainIntegrationQueue("events");
|
|
46
|
+
await drainIntegrationQueue("checkout-email");
|
|
47
|
+
const captured = await waitForCapturedSystemEmail(
|
|
48
|
+
request,
|
|
49
|
+
token,
|
|
50
|
+
(email) => email.metadata?.to === customerEmail && email.metadata?.subject === `Checkout started ${stamp}`,
|
|
51
|
+
{ description: "checkout payment start email" }
|
|
52
|
+
);
|
|
53
|
+
expect(captured.scope.tenantId).toBe(scope.tenantId);
|
|
54
|
+
expect(captured.scope.organizationId).toBe(scope.organizationId);
|
|
55
|
+
expect(captured.content.bodyFormat).toBe("html");
|
|
56
|
+
} finally {
|
|
57
|
+
await deleteCheckoutEntityIfExists(request, token, "links", linkId);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
//# sourceMappingURL=TC-CHKT-EMAIL-001.spec.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/checkout/__integration__/TC-CHKT-EMAIL-001.spec.ts"],
|
|
4
|
+
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken } from '@open-mercato/core/helpers/integration/api'\nimport { getTokenScope, readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'\nimport { drainIntegrationQueue } from '@open-mercato/core/helpers/integration/queue'\nimport {\n clearCapturedSystemEmails,\n isChannelSeedingAvailable,\n seedSystemEmailChannel,\n waitForCapturedSystemEmail,\n} from '@open-mercato/core/helpers/integration/communicationChannelsFixtures'\nimport {\n createCustomerData,\n createFixedTemplateInput,\n createLinkFixture,\n deleteCheckoutEntityIfExists,\n submitPayLink,\n} from './helpers/fixtures'\n\ntest.describe('TC-CHKT-EMAIL-001: Checkout transactional email uses system channel', () => {\n test('public checkout submit dispatches payment-start email through Communications Hub', async ({ request }) => {\n test.slow()\n const token = await getAuthToken(request, 'admin')\n const scope = getTokenScope(token)\n const seedingAvailable = await isChannelSeedingAvailable(request, token)\n test.skip(!seedingAvailable, 'OM_ENABLE_TEST_CHANNEL_SEEDING is not enabled.')\n\n await seedSystemEmailChannel(request, token)\n await clearCapturedSystemEmails(request, token)\n\n const stamp = Date.now()\n const customerEmail = `qa-checkout-email-${stamp}@example.test`\n let linkId: string | null = null\n\n try {\n const link = await createLinkFixture(request, token, createFixedTemplateInput({\n title: `QA checkout email ${stamp}`,\n status: 'active',\n startEmailSubject: `Checkout started ${stamp}`,\n sendStartEmail: true,\n }))\n linkId = link.id\n\n const submit = await submitPayLink(request, link.slug, {\n customerData: createCustomerData({ email: customerEmail }),\n acceptedLegalConsents: {},\n })\n expect(submit.status()).toBe(201)\n const body = await readJsonSafe<{ transactionId?: string }>(submit)\n expect(typeof body?.transactionId).toBe('string')\n\n await drainIntegrationQueue('events')\n await drainIntegrationQueue('checkout-email')\n\n const captured = await waitForCapturedSystemEmail(\n request,\n token,\n (email) => email.metadata?.to === customerEmail && email.metadata?.subject === `Checkout started ${stamp}`,\n { description: 'checkout payment start email' },\n )\n expect(captured.scope.tenantId).toBe(scope.tenantId)\n expect(captured.scope.organizationId).toBe(scope.organizationId)\n expect(captured.content.bodyFormat).toBe('html')\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'links', linkId)\n }\n })\n})\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,eAAe,oBAAoB;AAC5C,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,KAAK,SAAS,uEAAuE,MAAM;AACzF,OAAK,oFAAoF,OAAO,EAAE,QAAQ,MAAM;AAC9G,SAAK,KAAK;AACV,UAAM,QAAQ,MAAM,aAAa,SAAS,OAAO;AACjD,UAAM,QAAQ,cAAc,KAAK;AACjC,UAAM,mBAAmB,MAAM,0BAA0B,SAAS,KAAK;AACvE,SAAK,KAAK,CAAC,kBAAkB,gDAAgD;AAE7E,UAAM,uBAAuB,SAAS,KAAK;AAC3C,UAAM,0BAA0B,SAAS,KAAK;AAE9C,UAAM,QAAQ,KAAK,IAAI;AACvB,UAAM,gBAAgB,qBAAqB,KAAK;AAChD,QAAI,SAAwB;AAE5B,QAAI;AACF,YAAM,OAAO,MAAM,kBAAkB,SAAS,OAAO,yBAAyB;AAAA,QAC5E,OAAO,qBAAqB,KAAK;AAAA,QACjC,QAAQ;AAAA,QACR,mBAAmB,oBAAoB,KAAK;AAAA,QAC5C,gBAAgB;AAAA,MAClB,CAAC,CAAC;AACF,eAAS,KAAK;AAEd,YAAM,SAAS,MAAM,cAAc,SAAS,KAAK,MAAM;AAAA,QACrD,cAAc,mBAAmB,EAAE,OAAO,cAAc,CAAC;AAAA,QACzD,uBAAuB,CAAC;AAAA,MAC1B,CAAC;AACD,aAAO,OAAO,OAAO,CAAC,EAAE,KAAK,GAAG;AAChC,YAAM,OAAO,MAAM,aAAyC,MAAM;AAClE,aAAO,OAAO,MAAM,aAAa,EAAE,KAAK,QAAQ;AAEhD,YAAM,sBAAsB,QAAQ;AACpC,YAAM,sBAAsB,gBAAgB;AAE5C,YAAM,WAAW,MAAM;AAAA,QACrB;AAAA,QACA;AAAA,QACA,CAAC,UAAU,MAAM,UAAU,OAAO,iBAAiB,MAAM,UAAU,YAAY,oBAAoB,KAAK;AAAA,QACxG,EAAE,aAAa,+BAA+B;AAAA,MAChD;AACA,aAAO,SAAS,MAAM,QAAQ,EAAE,KAAK,MAAM,QAAQ;AACnD,aAAO,SAAS,MAAM,cAAc,EAAE,KAAK,MAAM,cAAc;AAC/D,aAAO,SAAS,QAAQ,UAAU,EAAE,KAAK,MAAM;AAAA,IACjD,UAAE;AACA,YAAM,6BAA6B,SAAS,OAAO,SAAS,MAAM;AAAA,IACpE;AAAA,EACF,CAAC;AACH,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/checkout/__integration__/helpers/fixtures.ts"],
|
|
4
|
-
"sourcesContent": ["import { createHmac } from 'node:crypto'\nimport { expect, type APIRequestContext, type APIResponse, type Page } from '@playwright/test'\nimport { apiRequest } from '@open-mercato/core/modules/core/__integration__/helpers/api'\nimport {\n readJsonSafe,\n} from '@open-mercato/core/modules/core/__integration__/helpers/generalFixtures'\nimport type { PublicSubmitInput } from '../../data/validators'\n\nconst MOCK_GATEWAY_DEV_WEBHOOK_SECRET = 'open-mercato-mock-dev-webhook-secret'\nconst MOCK_GATEWAY_SIGNATURE_HEADER = 'x-mock-signature'\n\ntype JsonRecord = Record<string, unknown>\n\nlet forwardedIpSequence = Math.floor(Math.random() * 10_000)\n\nexport type CheckoutTemplateInput = {\n name: string\n title?: string | null\n subtitle?: string | null\n description?: string | null\n pricingMode: 'fixed' | 'custom_amount' | 'price_list'\n fixedPriceAmount?: number | null\n fixedPriceCurrencyCode?: string | null\n fixedPriceIncludesTax?: boolean\n fixedPriceOriginalAmount?: number | null\n customAmountMin?: number | null\n customAmountMax?: number | null\n customAmountCurrencyCode?: string | null\n priceListItems?: Array<{\n id: string\n description: string\n amount: number\n currencyCode: string\n }> | null\n gatewayProviderKey: string\n gatewaySettings?: Record<string, unknown>\n customFieldsetCode?: string | null\n collectCustomerDetails?: boolean\n legalDocuments?: {\n terms?: { title: string; markdown: string; required?: boolean }\n privacyPolicy?: { title: string; markdown: string; required?: boolean }\n }\n displayCustomFieldsOnPage?: boolean\n password?: string | null\n maxCompletions?: number | null\n status?: 'draft' | 'active' | 'inactive'\n customFields?: Record<string, unknown>\n}\n\nexport type CheckoutLinkInput = CheckoutTemplateInput & {\n templateId?: string | null\n slug?: string | null\n}\n\nexport type CheckoutLinkRecord = {\n id: string\n slug: string\n name?: string\n title?: string | null\n subtitle?: string | null\n status?: string\n templateId?: string | null\n completionCount?: number\n activeReservationCount?: number\n fixedPriceAmount?: number | null\n displayCustomFieldsOnPage?: boolean\n publicCustomFields?: Array<{ key?: string; label?: string; value?: unknown }>\n customerFieldsSchema?: Array<{ key: string; required?: boolean }>\n requiresPassword?: boolean\n available?: boolean\n remainingUses?: number | null\n gatewayProviderKey?: string | null\n customFields?: Record<string, unknown>\n}\n\nexport type CheckoutTransactionRecord = {\n id: string\n linkId?: string\n status: string\n paymentStatus?: string | null\n gatewayTransactionId?: string | null\n email?: string | null\n firstName?: string | null\n lastName?: string | null\n phone?: string | null\n acceptedLegalConsents?: Record<string, unknown> | null\n customerData?: Record<string, unknown> | null\n}\n\nexport type ExampleCapturedEvent = {\n event: string\n payload: Record<string, unknown>\n capturedAt: string\n}\n\nfunction uniqueLabel(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`\n}\n\nfunction createUniqueForwardedIp(): string {\n forwardedIpSequence += 1\n const thirdOctet = Math.floor(forwardedIpSequence / 250) % 250 + 1\n const fourthOctet = forwardedIpSequence % 250 + 1\n return `198.51.${thirdOctet}.${fourthOctet}`\n}\n\nexport function createCheckoutClientHeaders(overrides: Record<string, string> = {}): Record<string, string> {\n if (overrides['x-forwarded-for'] || overrides['x-real-ip']) {\n return overrides\n }\n return {\n 'x-forwarded-for': createUniqueForwardedIp(),\n ...overrides,\n }\n}\n\nexport function createFixedTemplateInput(overrides: Partial<CheckoutLinkInput> = {}): CheckoutLinkInput {\n const label = uniqueLabel('checkout-fixed')\n return {\n name: `QA ${label}`,\n title: `QA ${label}`,\n subtitle: 'QA checkout fixture',\n description: 'QA checkout fixture description',\n pricingMode: 'fixed',\n fixedPriceAmount: 49.99,\n fixedPriceCurrencyCode: 'USD',\n fixedPriceIncludesTax: true,\n fixedPriceOriginalAmount: 69.99,\n gatewayProviderKey: 'mock',\n status: 'draft',\n collectCustomerDetails: true,\n displayCustomFieldsOnPage: false,\n ...overrides,\n }\n}\n\nexport function createCustomAmountTemplateInput(overrides: Partial<CheckoutLinkInput> = {}): CheckoutLinkInput {\n const label = uniqueLabel('checkout-custom')\n return {\n name: `QA ${label}`,\n title: `QA ${label}`,\n subtitle: 'QA custom amount fixture',\n description: 'QA custom amount description',\n pricingMode: 'custom_amount',\n customAmountMin: 10,\n customAmountMax: 100,\n customAmountCurrencyCode: 'USD',\n gatewayProviderKey: 'mock',\n status: 'active',\n collectCustomerDetails: true,\n displayCustomFieldsOnPage: false,\n ...overrides,\n }\n}\n\nexport function createPriceListTemplateInput(overrides: Partial<CheckoutLinkInput> = {}): CheckoutLinkInput {\n const label = uniqueLabel('checkout-price-list')\n return {\n name: `QA ${label}`,\n title: `QA ${label}`,\n subtitle: 'QA price list fixture',\n description: 'QA price list description',\n pricingMode: 'price_list',\n priceListItems: [\n { id: 'basic', description: 'Basic', amount: 19.99, currencyCode: 'USD' },\n { id: 'plus', description: 'Plus', amount: 49.99, currencyCode: 'USD' },\n ],\n gatewayProviderKey: 'mock',\n status: 'active',\n collectCustomerDetails: true,\n displayCustomFieldsOnPage: false,\n ...overrides,\n }\n}\n\nexport function createCustomerData(overrides: Record<string, unknown> = {}): Record<string, unknown> {\n const label = uniqueLabel('customer')\n return {\n firstName: 'Quinn',\n lastName: 'Checkout',\n email: `${label}@example.test`,\n phone: '+15550001111',\n ...overrides,\n }\n}\n\nexport async function createTemplateFixture(\n request: APIRequestContext,\n token: string,\n input: CheckoutTemplateInput,\n): Promise<string> {\n const response = await apiRequest(request, 'POST', '/api/checkout/templates', {\n token,\n data: input,\n })\n const body = await readJsonSafe<{ id?: string }>(response)\n expect(\n response.ok(),\n `Failed to create template: ${response.status()} ${JSON.stringify(body)}`,\n ).toBeTruthy()\n expect(typeof body?.id === 'string' && body.id.length > 0, 'Template id is required').toBeTruthy()\n return body!.id!\n}\n\nexport async function createLinkFixture(\n request: APIRequestContext,\n token: string,\n input: CheckoutLinkInput,\n): Promise<{ id: string; slug: string }> {\n const response = await apiRequest(request, 'POST', '/api/checkout/links', {\n token,\n data: input,\n })\n const body = await readJsonSafe<{ id?: string; slug?: string }>(response)\n expect(\n response.ok(),\n `Failed to create link: ${response.status()} ${JSON.stringify(body)}`,\n ).toBeTruthy()\n expect(typeof body?.id === 'string' && body.id.length > 0, 'Link id is required').toBeTruthy()\n expect(typeof body?.slug === 'string' && body.slug.length > 0, 'Link slug is required').toBeTruthy()\n return { id: body!.id!, slug: body!.slug! }\n}\n\nexport async function readTemplate(\n request: APIRequestContext,\n token: string,\n templateId: string,\n): Promise<JsonRecord> {\n const response = await apiRequest(request, 'GET', `/api/checkout/templates/${encodeURIComponent(templateId)}`, { token })\n expect(response.ok(), `Failed to read template ${templateId}: ${response.status()}`).toBeTruthy()\n return (await readJsonSafe<JsonRecord>(response)) ?? {}\n}\n\nexport async function readLink(\n request: APIRequestContext,\n token: string,\n linkId: string,\n): Promise<CheckoutLinkRecord> {\n const response = await apiRequest(request, 'GET', `/api/checkout/links/${encodeURIComponent(linkId)}`, { token })\n expect(response.ok(), `Failed to read link ${linkId}: ${response.status()}`).toBeTruthy()\n return ((await readJsonSafe<CheckoutLinkRecord>(response)) ?? {}) as CheckoutLinkRecord\n}\n\nexport async function updateTemplate(\n request: APIRequestContext,\n token: string,\n templateId: string,\n data: Record<string, unknown>,\n): Promise<APIResponse> {\n return apiRequest(request, 'PUT', `/api/checkout/templates/${encodeURIComponent(templateId)}`, {\n token,\n data,\n })\n}\n\nexport async function updateLink(\n request: APIRequestContext,\n token: string,\n linkId: string,\n data: Record<string, unknown>,\n): Promise<APIResponse> {\n return apiRequest(request, 'PUT', `/api/checkout/links/${encodeURIComponent(linkId)}`, {\n token,\n data,\n })\n}\n\nexport async function deleteTemplate(\n request: APIRequestContext,\n token: string,\n templateId: string,\n): Promise<APIResponse> {\n return apiRequest(request, 'DELETE', `/api/checkout/templates/${encodeURIComponent(templateId)}`, { token })\n}\n\nexport async function deleteLink(\n request: APIRequestContext,\n token: string,\n linkId: string,\n): Promise<APIResponse> {\n return apiRequest(request, 'DELETE', `/api/checkout/links/${encodeURIComponent(linkId)}`, { token })\n}\n\nexport async function listTemplates(\n request: APIRequestContext,\n token: string,\n query = '',\n): Promise<{ items: JsonRecord[]; total: number }> {\n const response = await apiRequest(request, 'GET', `/api/checkout/templates${query ? `?${query}` : ''}`, { token })\n expect(response.ok(), `Failed to list templates: ${response.status()}`).toBeTruthy()\n return ((await readJsonSafe<{ items?: JsonRecord[]; total?: number }>(response)) ?? {\n items: [],\n total: 0,\n }) as { items: JsonRecord[]; total: number }\n}\n\nexport async function listLinks(\n request: APIRequestContext,\n token: string,\n query = '',\n): Promise<{ items: JsonRecord[]; total: number }> {\n const response = await apiRequest(request, 'GET', `/api/checkout/links${query ? `?${query}` : ''}`, { token })\n expect(response.ok(), `Failed to list links: ${response.status()}`).toBeTruthy()\n return ((await readJsonSafe<{ items?: JsonRecord[]; total?: number }>(response)) ?? {\n items: [],\n total: 0,\n }) as { items: JsonRecord[]; total: number }\n}\n\nexport async function readPublicPayLink(\n request: APIRequestContext,\n slug: string,\n options?: { headers?: Record<string, string>; preview?: boolean },\n): Promise<APIResponse> {\n const query = options?.preview ? '?preview=true' : ''\n return request.fetch(`${process.env.BASE_URL || 'http://localhost:3000'}/api/checkout/pay/${encodeURIComponent(slug)}${query}`, {\n method: 'GET',\n headers: createCheckoutClientHeaders(options?.headers),\n })\n}\n\nexport async function verifyPayLinkPassword(\n request: APIRequestContext,\n slug: string,\n password: string,\n): Promise<APIResponse> {\n return request.fetch(`${process.env.BASE_URL || 'http://localhost:3000'}/api/checkout/pay/${encodeURIComponent(slug)}/verify-password`, {\n method: 'POST',\n headers: createCheckoutClientHeaders({ 'Content-Type': 'application/json' }),\n data: { password },\n })\n}\n\nexport async function submitPayLink(\n request: APIRequestContext,\n slug: string,\n input: PublicSubmitInput,\n options?: { idempotencyKey?: string; headers?: Record<string, string> },\n): Promise<APIResponse> {\n return request.fetch(`${process.env.BASE_URL || 'http://localhost:3000'}/api/checkout/pay/${encodeURIComponent(slug)}/submit`, {\n method: 'POST',\n headers: createCheckoutClientHeaders({\n 'Content-Type': 'application/json',\n 'Idempotency-Key': options?.idempotencyKey ?? uniqueLabel('idempotency'),\n ...(options?.headers ?? {}),\n }),\n data: input,\n })\n}\n\nexport async function readCheckoutTransaction(\n request: APIRequestContext,\n token: string,\n transactionId: string,\n): Promise<CheckoutTransactionRecord> {\n const response = await apiRequest(request, 'GET', `/api/checkout/transactions/${encodeURIComponent(transactionId)}`, { token })\n expect(response.ok(), `Failed to read transaction ${transactionId}: ${response.status()}`).toBeTruthy()\n const body = (await readJsonSafe<{ transaction?: CheckoutTransactionRecord }>(response)) ?? {}\n return (body.transaction ?? {}) as CheckoutTransactionRecord\n}\n\nexport async function listCheckoutTransactions(\n request: APIRequestContext,\n token: string,\n query = '',\n): Promise<{ items: CheckoutTransactionRecord[]; total: number; canViewPii?: boolean }> {\n const response = await apiRequest(request, 'GET', `/api/checkout/transactions${query ? `?${query}` : ''}`, { token })\n expect(response.ok(), `Failed to list checkout transactions: ${response.status()}`).toBeTruthy()\n return ((await readJsonSafe<{ items?: CheckoutTransactionRecord[]; total?: number; canViewPii?: boolean }>(response)) ?? {\n items: [],\n total: 0,\n }) as { items: CheckoutTransactionRecord[]; total: number; canViewPii?: boolean }\n}\n\nexport async function readCheckoutStatus(\n request: APIRequestContext,\n slug: string,\n transactionId: string,\n options?: { headers?: Record<string, string> },\n): Promise<APIResponse> {\n return request.fetch(\n `${process.env.BASE_URL || 'http://localhost:3000'}/api/checkout/pay/${encodeURIComponent(slug)}/status/${encodeURIComponent(transactionId)}`,\n {\n method: 'GET',\n headers: createCheckoutClientHeaders(options?.headers),\n },\n )\n}\n\nexport async function waitForCheckoutStatus(\n request: APIRequestContext,\n token: string,\n transactionId: string,\n expectedStatus: string,\n options?: { attempts?: number; intervalMs?: number },\n): Promise<CheckoutTransactionRecord> {\n const attempts = options?.attempts ?? 20\n const intervalMs = options?.intervalMs ?? 150\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n const transaction = await readCheckoutTransaction(request, token, transactionId)\n if (transaction.status === expectedStatus) return transaction\n await new Promise((resolve) => setTimeout(resolve, intervalMs))\n }\n return readCheckoutTransaction(request, token, transactionId)\n}\n\nexport async function findGatewayTransactionIdForCheckout(\n request: APIRequestContext,\n token: string,\n checkoutTransactionId: string,\n): Promise<string> {\n const response = await apiRequest(request, 'GET', '/api/payment_gateways/transactions?page=1&pageSize=100', { token })\n expect(response.ok(), `Failed to list gateway transactions: ${response.status()}`).toBeTruthy()\n const body = (await readJsonSafe<{ items?: Array<{ id: string; paymentId: string }> }>(response)) ?? {}\n const match = body.items?.find((item) => item.paymentId === checkoutTransactionId)\n expect(match, `Gateway transaction for checkout ${checkoutTransactionId} was not found`).toBeTruthy()\n return match!.id\n}\n\nexport async function readGatewayTransaction(\n request: APIRequestContext,\n token: string,\n gatewayTransactionId: string,\n): Promise<{\n id: string\n providerSessionId: string | null\n unifiedStatus: string\n}> {\n const response = await apiRequest(request, 'GET', `/api/payment_gateways/transactions/${encodeURIComponent(gatewayTransactionId)}`, { token })\n expect(response.ok(), `Failed to read gateway transaction ${gatewayTransactionId}: ${response.status()}`).toBeTruthy()\n const body = (await readJsonSafe<{ transaction?: { id: string; providerSessionId?: string | null; unifiedStatus?: string } }>(response)) ?? {}\n const transaction = body.transaction\n expect(transaction?.id, 'Gateway transaction payload is required').toBeTruthy()\n return {\n id: transaction!.id,\n providerSessionId: transaction?.providerSessionId ?? null,\n unifiedStatus: transaction?.unifiedStatus ?? 'unknown',\n }\n}\n\nexport async function sendMockGatewayWebhook(\n request: APIRequestContext,\n token: string,\n providerSessionId: string,\n status: 'captured' | 'cancelled' | 'failed',\n amount: number,\n options?: { providerKey?: string },\n): Promise<APIResponse> {\n const provider = encodeURIComponent(options?.providerKey ?? 'mock')\n const payload = {\n type: `payment.${status}`,\n id: uniqueLabel(`mock-${status}`),\n data: {\n id: providerSessionId,\n status,\n amount,\n },\n }\n const rawBody = JSON.stringify(payload)\n const signature = createHmac('sha256', MOCK_GATEWAY_DEV_WEBHOOK_SECRET)\n .update(rawBody, 'utf-8')\n .digest('hex')\n const BASE_URL = process.env.BASE_URL?.trim() || ''\n return request.fetch(`${BASE_URL}/api/payment_gateways/webhook/${provider}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n [MOCK_GATEWAY_SIGNATURE_HEADER]: signature,\n Authorization: `Bearer ${token}`,\n },\n data: rawBody,\n })\n}\n\nexport async function deleteCheckoutEntityIfExists(\n request: APIRequestContext,\n token: string | null,\n kind: 'templates' | 'links',\n id: string | null,\n): Promise<void> {\n if (!token || !id) return\n try {\n await apiRequest(request, 'DELETE', `/api/checkout/${kind}/${encodeURIComponent(id)}`, { token })\n } catch {\n return\n }\n}\n\nexport async function loginToBackendAndOpen(page: Page, path: string): Promise<void> {\n await page.goto('/login')\n await page.getByLabel('Email').fill('admin@acme.com')\n await page.getByLabel('Password', { exact: true }).fill('secret')\n await page.getByRole('button', { name: /sign in/i }).click()\n await page.waitForURL(/\\/backend(?:\\/.*)?$/)\n await page.goto(path)\n}\n\nexport async function clearCapturedExampleEvents(\n request: APIRequestContext,\n token: string,\n): Promise<void> {\n const response = await apiRequest(request, 'DELETE', '/api/example/qa-events', { token })\n expect(response.ok(), `Failed to clear captured example events: ${response.status()}`).toBeTruthy()\n}\n\nexport async function listCapturedExampleEvents(\n request: APIRequestContext,\n token: string,\n options?: { event?: string; prefix?: string },\n): Promise<ExampleCapturedEvent[]> {\n const query = new URLSearchParams()\n if (options?.event) query.set('event', options.event)\n if (options?.prefix) query.set('prefix', options.prefix)\n const response = await apiRequest(request, 'GET', `/api/example/qa-events${query.size > 0 ? `?${query.toString()}` : ''}`, {\n token,\n })\n expect(response.ok(), `Failed to list captured example events: ${response.status()}`).toBeTruthy()\n const body = await readJsonSafe<{ items?: ExampleCapturedEvent[] }>(response)\n return Array.isArray(body?.items) ? body.items : []\n}\n\nexport async function waitForCapturedExampleEvents(\n request: APIRequestContext,\n token: string,\n expectedEventNames: string[],\n): Promise<ExampleCapturedEvent[]> {\n for (let attempt = 0; attempt < 30; attempt += 1) {\n const events = await listCapturedExampleEvents(request, token)\n const seenNames = new Set(events.map((event) => event.event))\n if (expectedEventNames.every((eventName) => seenNames.has(eventName))) {\n return events\n }\n await new Promise((resolve) => setTimeout(resolve, 150))\n }\n return listCapturedExampleEvents(request, token)\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,kBAAkB;AAC3B,SAAS,cAAmE;AAC5E,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,OACK;AAGP,MAAM,kCAAkC;AACxC,MAAM,gCAAgC;AAItC,IAAI,sBAAsB,KAAK,MAAM,KAAK,OAAO,IAAI,GAAM;
|
|
4
|
+
"sourcesContent": ["import { createHmac } from 'node:crypto'\nimport { expect, type APIRequestContext, type APIResponse, type Page } from '@playwright/test'\nimport { apiRequest } from '@open-mercato/core/modules/core/__integration__/helpers/api'\nimport {\n readJsonSafe,\n} from '@open-mercato/core/modules/core/__integration__/helpers/generalFixtures'\nimport type { PublicSubmitInput } from '../../data/validators'\n\nconst MOCK_GATEWAY_DEV_WEBHOOK_SECRET = 'open-mercato-mock-dev-webhook-secret'\nconst MOCK_GATEWAY_SIGNATURE_HEADER = 'x-mock-signature'\n\ntype JsonRecord = Record<string, unknown>\n\nlet forwardedIpSequence = Math.floor(Math.random() * 10_000)\n\nexport type CheckoutTemplateInput = {\n name: string\n title?: string | null\n subtitle?: string | null\n description?: string | null\n pricingMode: 'fixed' | 'custom_amount' | 'price_list'\n fixedPriceAmount?: number | null\n fixedPriceCurrencyCode?: string | null\n fixedPriceIncludesTax?: boolean\n fixedPriceOriginalAmount?: number | null\n customAmountMin?: number | null\n customAmountMax?: number | null\n customAmountCurrencyCode?: string | null\n priceListItems?: Array<{\n id: string\n description: string\n amount: number\n currencyCode: string\n }> | null\n gatewayProviderKey: string\n gatewaySettings?: Record<string, unknown>\n customFieldsetCode?: string | null\n collectCustomerDetails?: boolean\n legalDocuments?: {\n terms?: { title: string; markdown: string; required?: boolean }\n privacyPolicy?: { title: string; markdown: string; required?: boolean }\n }\n sendStartEmail?: boolean\n startEmailSubject?: string | null\n startEmailBody?: string | null\n sendSuccessEmail?: boolean\n successEmailSubject?: string | null\n successEmailBody?: string | null\n sendErrorEmail?: boolean\n errorEmailSubject?: string | null\n errorEmailBody?: string | null\n displayCustomFieldsOnPage?: boolean\n password?: string | null\n maxCompletions?: number | null\n status?: 'draft' | 'active' | 'inactive'\n customFields?: Record<string, unknown>\n}\n\nexport type CheckoutLinkInput = CheckoutTemplateInput & {\n templateId?: string | null\n slug?: string | null\n}\n\nexport type CheckoutLinkRecord = {\n id: string\n slug: string\n name?: string\n title?: string | null\n subtitle?: string | null\n status?: string\n templateId?: string | null\n completionCount?: number\n activeReservationCount?: number\n fixedPriceAmount?: number | null\n displayCustomFieldsOnPage?: boolean\n publicCustomFields?: Array<{ key?: string; label?: string; value?: unknown }>\n customerFieldsSchema?: Array<{ key: string; required?: boolean }>\n requiresPassword?: boolean\n available?: boolean\n remainingUses?: number | null\n gatewayProviderKey?: string | null\n customFields?: Record<string, unknown>\n}\n\nexport type CheckoutTransactionRecord = {\n id: string\n linkId?: string\n status: string\n paymentStatus?: string | null\n gatewayTransactionId?: string | null\n email?: string | null\n firstName?: string | null\n lastName?: string | null\n phone?: string | null\n acceptedLegalConsents?: Record<string, unknown> | null\n customerData?: Record<string, unknown> | null\n}\n\nexport type ExampleCapturedEvent = {\n event: string\n payload: Record<string, unknown>\n capturedAt: string\n}\n\nfunction uniqueLabel(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`\n}\n\nfunction createUniqueForwardedIp(): string {\n forwardedIpSequence += 1\n const thirdOctet = Math.floor(forwardedIpSequence / 250) % 250 + 1\n const fourthOctet = forwardedIpSequence % 250 + 1\n return `198.51.${thirdOctet}.${fourthOctet}`\n}\n\nexport function createCheckoutClientHeaders(overrides: Record<string, string> = {}): Record<string, string> {\n if (overrides['x-forwarded-for'] || overrides['x-real-ip']) {\n return overrides\n }\n return {\n 'x-forwarded-for': createUniqueForwardedIp(),\n ...overrides,\n }\n}\n\nexport function createFixedTemplateInput(overrides: Partial<CheckoutLinkInput> = {}): CheckoutLinkInput {\n const label = uniqueLabel('checkout-fixed')\n return {\n name: `QA ${label}`,\n title: `QA ${label}`,\n subtitle: 'QA checkout fixture',\n description: 'QA checkout fixture description',\n pricingMode: 'fixed',\n fixedPriceAmount: 49.99,\n fixedPriceCurrencyCode: 'USD',\n fixedPriceIncludesTax: true,\n fixedPriceOriginalAmount: 69.99,\n gatewayProviderKey: 'mock',\n status: 'draft',\n collectCustomerDetails: true,\n displayCustomFieldsOnPage: false,\n ...overrides,\n }\n}\n\nexport function createCustomAmountTemplateInput(overrides: Partial<CheckoutLinkInput> = {}): CheckoutLinkInput {\n const label = uniqueLabel('checkout-custom')\n return {\n name: `QA ${label}`,\n title: `QA ${label}`,\n subtitle: 'QA custom amount fixture',\n description: 'QA custom amount description',\n pricingMode: 'custom_amount',\n customAmountMin: 10,\n customAmountMax: 100,\n customAmountCurrencyCode: 'USD',\n gatewayProviderKey: 'mock',\n status: 'active',\n collectCustomerDetails: true,\n displayCustomFieldsOnPage: false,\n ...overrides,\n }\n}\n\nexport function createPriceListTemplateInput(overrides: Partial<CheckoutLinkInput> = {}): CheckoutLinkInput {\n const label = uniqueLabel('checkout-price-list')\n return {\n name: `QA ${label}`,\n title: `QA ${label}`,\n subtitle: 'QA price list fixture',\n description: 'QA price list description',\n pricingMode: 'price_list',\n priceListItems: [\n { id: 'basic', description: 'Basic', amount: 19.99, currencyCode: 'USD' },\n { id: 'plus', description: 'Plus', amount: 49.99, currencyCode: 'USD' },\n ],\n gatewayProviderKey: 'mock',\n status: 'active',\n collectCustomerDetails: true,\n displayCustomFieldsOnPage: false,\n ...overrides,\n }\n}\n\nexport function createCustomerData(overrides: Record<string, unknown> = {}): Record<string, unknown> {\n const label = uniqueLabel('customer')\n return {\n firstName: 'Quinn',\n lastName: 'Checkout',\n email: `${label}@example.test`,\n phone: '+15550001111',\n ...overrides,\n }\n}\n\nexport async function createTemplateFixture(\n request: APIRequestContext,\n token: string,\n input: CheckoutTemplateInput,\n): Promise<string> {\n const response = await apiRequest(request, 'POST', '/api/checkout/templates', {\n token,\n data: input,\n })\n const body = await readJsonSafe<{ id?: string }>(response)\n expect(\n response.ok(),\n `Failed to create template: ${response.status()} ${JSON.stringify(body)}`,\n ).toBeTruthy()\n expect(typeof body?.id === 'string' && body.id.length > 0, 'Template id is required').toBeTruthy()\n return body!.id!\n}\n\nexport async function createLinkFixture(\n request: APIRequestContext,\n token: string,\n input: CheckoutLinkInput,\n): Promise<{ id: string; slug: string }> {\n const response = await apiRequest(request, 'POST', '/api/checkout/links', {\n token,\n data: input,\n })\n const body = await readJsonSafe<{ id?: string; slug?: string }>(response)\n expect(\n response.ok(),\n `Failed to create link: ${response.status()} ${JSON.stringify(body)}`,\n ).toBeTruthy()\n expect(typeof body?.id === 'string' && body.id.length > 0, 'Link id is required').toBeTruthy()\n expect(typeof body?.slug === 'string' && body.slug.length > 0, 'Link slug is required').toBeTruthy()\n return { id: body!.id!, slug: body!.slug! }\n}\n\nexport async function readTemplate(\n request: APIRequestContext,\n token: string,\n templateId: string,\n): Promise<JsonRecord> {\n const response = await apiRequest(request, 'GET', `/api/checkout/templates/${encodeURIComponent(templateId)}`, { token })\n expect(response.ok(), `Failed to read template ${templateId}: ${response.status()}`).toBeTruthy()\n return (await readJsonSafe<JsonRecord>(response)) ?? {}\n}\n\nexport async function readLink(\n request: APIRequestContext,\n token: string,\n linkId: string,\n): Promise<CheckoutLinkRecord> {\n const response = await apiRequest(request, 'GET', `/api/checkout/links/${encodeURIComponent(linkId)}`, { token })\n expect(response.ok(), `Failed to read link ${linkId}: ${response.status()}`).toBeTruthy()\n return ((await readJsonSafe<CheckoutLinkRecord>(response)) ?? {}) as CheckoutLinkRecord\n}\n\nexport async function updateTemplate(\n request: APIRequestContext,\n token: string,\n templateId: string,\n data: Record<string, unknown>,\n): Promise<APIResponse> {\n return apiRequest(request, 'PUT', `/api/checkout/templates/${encodeURIComponent(templateId)}`, {\n token,\n data,\n })\n}\n\nexport async function updateLink(\n request: APIRequestContext,\n token: string,\n linkId: string,\n data: Record<string, unknown>,\n): Promise<APIResponse> {\n return apiRequest(request, 'PUT', `/api/checkout/links/${encodeURIComponent(linkId)}`, {\n token,\n data,\n })\n}\n\nexport async function deleteTemplate(\n request: APIRequestContext,\n token: string,\n templateId: string,\n): Promise<APIResponse> {\n return apiRequest(request, 'DELETE', `/api/checkout/templates/${encodeURIComponent(templateId)}`, { token })\n}\n\nexport async function deleteLink(\n request: APIRequestContext,\n token: string,\n linkId: string,\n): Promise<APIResponse> {\n return apiRequest(request, 'DELETE', `/api/checkout/links/${encodeURIComponent(linkId)}`, { token })\n}\n\nexport async function listTemplates(\n request: APIRequestContext,\n token: string,\n query = '',\n): Promise<{ items: JsonRecord[]; total: number }> {\n const response = await apiRequest(request, 'GET', `/api/checkout/templates${query ? `?${query}` : ''}`, { token })\n expect(response.ok(), `Failed to list templates: ${response.status()}`).toBeTruthy()\n return ((await readJsonSafe<{ items?: JsonRecord[]; total?: number }>(response)) ?? {\n items: [],\n total: 0,\n }) as { items: JsonRecord[]; total: number }\n}\n\nexport async function listLinks(\n request: APIRequestContext,\n token: string,\n query = '',\n): Promise<{ items: JsonRecord[]; total: number }> {\n const response = await apiRequest(request, 'GET', `/api/checkout/links${query ? `?${query}` : ''}`, { token })\n expect(response.ok(), `Failed to list links: ${response.status()}`).toBeTruthy()\n return ((await readJsonSafe<{ items?: JsonRecord[]; total?: number }>(response)) ?? {\n items: [],\n total: 0,\n }) as { items: JsonRecord[]; total: number }\n}\n\nexport async function readPublicPayLink(\n request: APIRequestContext,\n slug: string,\n options?: { headers?: Record<string, string>; preview?: boolean },\n): Promise<APIResponse> {\n const query = options?.preview ? '?preview=true' : ''\n return request.fetch(`${process.env.BASE_URL || 'http://localhost:3000'}/api/checkout/pay/${encodeURIComponent(slug)}${query}`, {\n method: 'GET',\n headers: createCheckoutClientHeaders(options?.headers),\n })\n}\n\nexport async function verifyPayLinkPassword(\n request: APIRequestContext,\n slug: string,\n password: string,\n): Promise<APIResponse> {\n return request.fetch(`${process.env.BASE_URL || 'http://localhost:3000'}/api/checkout/pay/${encodeURIComponent(slug)}/verify-password`, {\n method: 'POST',\n headers: createCheckoutClientHeaders({ 'Content-Type': 'application/json' }),\n data: { password },\n })\n}\n\nexport async function submitPayLink(\n request: APIRequestContext,\n slug: string,\n input: PublicSubmitInput,\n options?: { idempotencyKey?: string; headers?: Record<string, string> },\n): Promise<APIResponse> {\n return request.fetch(`${process.env.BASE_URL || 'http://localhost:3000'}/api/checkout/pay/${encodeURIComponent(slug)}/submit`, {\n method: 'POST',\n headers: createCheckoutClientHeaders({\n 'Content-Type': 'application/json',\n 'Idempotency-Key': options?.idempotencyKey ?? uniqueLabel('idempotency'),\n ...(options?.headers ?? {}),\n }),\n data: input,\n })\n}\n\nexport async function readCheckoutTransaction(\n request: APIRequestContext,\n token: string,\n transactionId: string,\n): Promise<CheckoutTransactionRecord> {\n const response = await apiRequest(request, 'GET', `/api/checkout/transactions/${encodeURIComponent(transactionId)}`, { token })\n expect(response.ok(), `Failed to read transaction ${transactionId}: ${response.status()}`).toBeTruthy()\n const body = (await readJsonSafe<{ transaction?: CheckoutTransactionRecord }>(response)) ?? {}\n return (body.transaction ?? {}) as CheckoutTransactionRecord\n}\n\nexport async function listCheckoutTransactions(\n request: APIRequestContext,\n token: string,\n query = '',\n): Promise<{ items: CheckoutTransactionRecord[]; total: number; canViewPii?: boolean }> {\n const response = await apiRequest(request, 'GET', `/api/checkout/transactions${query ? `?${query}` : ''}`, { token })\n expect(response.ok(), `Failed to list checkout transactions: ${response.status()}`).toBeTruthy()\n return ((await readJsonSafe<{ items?: CheckoutTransactionRecord[]; total?: number; canViewPii?: boolean }>(response)) ?? {\n items: [],\n total: 0,\n }) as { items: CheckoutTransactionRecord[]; total: number; canViewPii?: boolean }\n}\n\nexport async function readCheckoutStatus(\n request: APIRequestContext,\n slug: string,\n transactionId: string,\n options?: { headers?: Record<string, string> },\n): Promise<APIResponse> {\n return request.fetch(\n `${process.env.BASE_URL || 'http://localhost:3000'}/api/checkout/pay/${encodeURIComponent(slug)}/status/${encodeURIComponent(transactionId)}`,\n {\n method: 'GET',\n headers: createCheckoutClientHeaders(options?.headers),\n },\n )\n}\n\nexport async function waitForCheckoutStatus(\n request: APIRequestContext,\n token: string,\n transactionId: string,\n expectedStatus: string,\n options?: { attempts?: number; intervalMs?: number },\n): Promise<CheckoutTransactionRecord> {\n const attempts = options?.attempts ?? 20\n const intervalMs = options?.intervalMs ?? 150\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n const transaction = await readCheckoutTransaction(request, token, transactionId)\n if (transaction.status === expectedStatus) return transaction\n await new Promise((resolve) => setTimeout(resolve, intervalMs))\n }\n return readCheckoutTransaction(request, token, transactionId)\n}\n\nexport async function findGatewayTransactionIdForCheckout(\n request: APIRequestContext,\n token: string,\n checkoutTransactionId: string,\n): Promise<string> {\n const response = await apiRequest(request, 'GET', '/api/payment_gateways/transactions?page=1&pageSize=100', { token })\n expect(response.ok(), `Failed to list gateway transactions: ${response.status()}`).toBeTruthy()\n const body = (await readJsonSafe<{ items?: Array<{ id: string; paymentId: string }> }>(response)) ?? {}\n const match = body.items?.find((item) => item.paymentId === checkoutTransactionId)\n expect(match, `Gateway transaction for checkout ${checkoutTransactionId} was not found`).toBeTruthy()\n return match!.id\n}\n\nexport async function readGatewayTransaction(\n request: APIRequestContext,\n token: string,\n gatewayTransactionId: string,\n): Promise<{\n id: string\n providerSessionId: string | null\n unifiedStatus: string\n}> {\n const response = await apiRequest(request, 'GET', `/api/payment_gateways/transactions/${encodeURIComponent(gatewayTransactionId)}`, { token })\n expect(response.ok(), `Failed to read gateway transaction ${gatewayTransactionId}: ${response.status()}`).toBeTruthy()\n const body = (await readJsonSafe<{ transaction?: { id: string; providerSessionId?: string | null; unifiedStatus?: string } }>(response)) ?? {}\n const transaction = body.transaction\n expect(transaction?.id, 'Gateway transaction payload is required').toBeTruthy()\n return {\n id: transaction!.id,\n providerSessionId: transaction?.providerSessionId ?? null,\n unifiedStatus: transaction?.unifiedStatus ?? 'unknown',\n }\n}\n\nexport async function sendMockGatewayWebhook(\n request: APIRequestContext,\n token: string,\n providerSessionId: string,\n status: 'captured' | 'cancelled' | 'failed',\n amount: number,\n options?: { providerKey?: string },\n): Promise<APIResponse> {\n const provider = encodeURIComponent(options?.providerKey ?? 'mock')\n const payload = {\n type: `payment.${status}`,\n id: uniqueLabel(`mock-${status}`),\n data: {\n id: providerSessionId,\n status,\n amount,\n },\n }\n const rawBody = JSON.stringify(payload)\n const signature = createHmac('sha256', MOCK_GATEWAY_DEV_WEBHOOK_SECRET)\n .update(rawBody, 'utf-8')\n .digest('hex')\n const BASE_URL = process.env.BASE_URL?.trim() || ''\n return request.fetch(`${BASE_URL}/api/payment_gateways/webhook/${provider}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n [MOCK_GATEWAY_SIGNATURE_HEADER]: signature,\n Authorization: `Bearer ${token}`,\n },\n data: rawBody,\n })\n}\n\nexport async function deleteCheckoutEntityIfExists(\n request: APIRequestContext,\n token: string | null,\n kind: 'templates' | 'links',\n id: string | null,\n): Promise<void> {\n if (!token || !id) return\n try {\n await apiRequest(request, 'DELETE', `/api/checkout/${kind}/${encodeURIComponent(id)}`, { token })\n } catch {\n return\n }\n}\n\nexport async function loginToBackendAndOpen(page: Page, path: string): Promise<void> {\n await page.goto('/login')\n await page.getByLabel('Email').fill('admin@acme.com')\n await page.getByLabel('Password', { exact: true }).fill('secret')\n await page.getByRole('button', { name: /sign in/i }).click()\n await page.waitForURL(/\\/backend(?:\\/.*)?$/)\n await page.goto(path)\n}\n\nexport async function clearCapturedExampleEvents(\n request: APIRequestContext,\n token: string,\n): Promise<void> {\n const response = await apiRequest(request, 'DELETE', '/api/example/qa-events', { token })\n expect(response.ok(), `Failed to clear captured example events: ${response.status()}`).toBeTruthy()\n}\n\nexport async function listCapturedExampleEvents(\n request: APIRequestContext,\n token: string,\n options?: { event?: string; prefix?: string },\n): Promise<ExampleCapturedEvent[]> {\n const query = new URLSearchParams()\n if (options?.event) query.set('event', options.event)\n if (options?.prefix) query.set('prefix', options.prefix)\n const response = await apiRequest(request, 'GET', `/api/example/qa-events${query.size > 0 ? `?${query.toString()}` : ''}`, {\n token,\n })\n expect(response.ok(), `Failed to list captured example events: ${response.status()}`).toBeTruthy()\n const body = await readJsonSafe<{ items?: ExampleCapturedEvent[] }>(response)\n return Array.isArray(body?.items) ? body.items : []\n}\n\nexport async function waitForCapturedExampleEvents(\n request: APIRequestContext,\n token: string,\n expectedEventNames: string[],\n): Promise<ExampleCapturedEvent[]> {\n for (let attempt = 0; attempt < 30; attempt += 1) {\n const events = await listCapturedExampleEvents(request, token)\n const seenNames = new Set(events.map((event) => event.event))\n if (expectedEventNames.every((eventName) => seenNames.has(eventName))) {\n return events\n }\n await new Promise((resolve) => setTimeout(resolve, 150))\n }\n return listCapturedExampleEvents(request, token)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -129,6 +129,8 @@ async function handle(job, ctx) {
|
|
|
129
129
|
subject,
|
|
130
130
|
from,
|
|
131
131
|
replyTo,
|
|
132
|
+
tenantId: payload.tenantId,
|
|
133
|
+
organizationId: payload.organizationId,
|
|
132
134
|
react: PaymentStartEmail({
|
|
133
135
|
firstName,
|
|
134
136
|
amount,
|
|
@@ -156,6 +158,8 @@ async function handle(job, ctx) {
|
|
|
156
158
|
subject,
|
|
157
159
|
from,
|
|
158
160
|
replyTo,
|
|
161
|
+
tenantId: payload.tenantId,
|
|
162
|
+
organizationId: payload.organizationId,
|
|
159
163
|
react: PaymentSuccessEmail({
|
|
160
164
|
firstName,
|
|
161
165
|
amount,
|
|
@@ -185,6 +189,8 @@ async function handle(job, ctx) {
|
|
|
185
189
|
subject,
|
|
186
190
|
from,
|
|
187
191
|
replyTo,
|
|
192
|
+
tenantId: payload.tenantId,
|
|
193
|
+
organizationId: payload.organizationId,
|
|
188
194
|
react: PaymentErrorEmail({
|
|
189
195
|
firstName,
|
|
190
196
|
linkTitle,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/checkout/workers/send-email.worker.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { DEFAULT_NOTIFICATION_DELIVERY_CONFIG, resolveNotificationDeliveryConfig } from '@open-mercato/core/modules/notifications/lib/deliveryConfig'\nimport type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CheckoutTransaction, CheckoutLink } from '../data/entities'\nimport PaymentStartEmail from '../emails/PaymentStartEmail'\nimport PaymentSuccessEmail from '../emails/PaymentSuccessEmail'\nimport PaymentErrorEmail from '../emails/PaymentErrorEmail'\n\nexport const CHECKOUT_EMAIL_QUEUE = 'checkout-email'\n\nexport type CheckoutEmailJob =\n | { type: 'start'; transactionId: string; tenantId: string; organizationId: string }\n | { type: 'success'; transactionId: string; tenantId: string; organizationId: string }\n | { type: 'error'; transactionId: string; tenantId: string; organizationId: string; errorMessage?: string | null }\n\nexport const metadata: WorkerMeta = {\n queue: CHECKOUT_EMAIL_QUEUE,\n id: 'checkout:send-email',\n concurrency: 5,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\nfunction interpolateVariables(template: string, variables: Record<string, string>): string {\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (match, key: string) => variables[key] ?? match)\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n\nfunction renderInlineMarkdown(value: string): string {\n let rendered = escapeHtml(value)\n rendered = rendered.replace(/`([^`]+)`/g, '<code>$1</code>')\n rendered = rendered.replace(/\\[([^\\]]+)\\]\\((https?:\\/\\/[^\\s)]+)\\)/g, '<a href=\"$2\">$1</a>')\n rendered = rendered.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n rendered = rendered.replace(/__([^_]+)__/g, '<strong>$1</strong>')\n rendered = rendered.replace(/(^|[\\s(])\\*([^*]+)\\*(?=$|[\\s).,!?:;])/g, '$1<em>$2</em>')\n rendered = rendered.replace(/(^|[\\s(])_([^_]+)_(?=$|[\\s).,!?:;])/g, '$1<em>$2</em>')\n return rendered\n}\n\nfunction renderParagraph(lines: string[]): string {\n return `<p>${renderInlineMarkdown(lines.join(' '))}</p>`\n}\n\nfunction renderMarkdownBody(markdown: string): string {\n const normalized = markdown.replace(/\\r\\n/g, '\\n').trim()\n if (!normalized) return ''\n\n const blocks: string[] = []\n const lines = normalized.split('\\n')\n let paragraph: string[] = []\n let listItems: string[] = []\n\n const flushParagraph = () => {\n if (paragraph.length === 0) return\n blocks.push(renderParagraph(paragraph))\n paragraph = []\n }\n\n const flushList = () => {\n if (listItems.length === 0) return\n blocks.push(`<ul>${listItems.map((item) => `<li>${renderInlineMarkdown(item)}</li>`).join('')}</ul>`)\n listItems = []\n }\n\n for (const rawLine of lines) {\n const line = rawLine.trim()\n if (!line) {\n flushParagraph()\n flushList()\n continue\n }\n\n const headingMatch = /^(#{1,6})\\s+(.+)$/.exec(line)\n if (headingMatch) {\n flushParagraph()\n flushList()\n const level = headingMatch[1].length\n blocks.push(`<h${level}>${renderInlineMarkdown(headingMatch[2])}</h${level}>`)\n continue\n }\n\n const listMatch = /^[-*]\\s+(.+)$/.exec(line)\n if (listMatch) {\n flushParagraph()\n listItems.push(listMatch[1])\n continue\n }\n\n flushList()\n paragraph.push(line)\n }\n\n flushParagraph()\n flushList()\n\n return blocks.join('')\n}\n\nexport default async function handle(job: QueuedJob<CheckoutEmailJob>, ctx: HandlerContext): Promise<void> {\n const { payload } = job\n const em = (ctx.resolve('em') as EntityManager).fork()\n const { t } = await resolveTranslations()\n\n const transaction = await findOneWithDecryption<CheckoutTransaction>(em, CheckoutTransaction, {\n id: payload.transactionId,\n organizationId: payload.organizationId,\n tenantId: payload.tenantId,\n })\n if (!transaction) return\n\n const email = transaction.email\n if (!email) return\n\n const link = await findOneWithDecryption(em, CheckoutLink, {\n id: transaction.linkId,\n organizationId: payload.organizationId,\n tenantId: payload.tenantId,\n deletedAt: null,\n }, undefined, { organizationId: payload.organizationId, tenantId: payload.tenantId })\n\n const firstName = transaction.firstName ?? t('checkout.systemEmails.common.customerFallback')\n const linkTitle = link?.title ?? link?.name ?? t('checkout.systemEmails.common.linkTitleFallback')\n const amount = String(transaction.amount ?? '0.00')\n const currencyCode = transaction.currencyCode ?? ''\n const errorMessage = payload.type === 'error' ? (payload.errorMessage ?? null) : null\n\n const variables: Record<string, string> = {\n firstName,\n amount,\n currencyCode,\n linkTitle,\n transactionId: transaction.id,\n errorMessage: errorMessage ?? '',\n }\n const deliveryConfig = await resolveNotificationDeliveryConfig(ctx, {\n defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG,\n })\n const from = deliveryConfig.strategies.email.from\n const replyTo = deliveryConfig.strategies.email.replyTo\n\n async function resolveEmailContent(\n subjectField: string | null | undefined,\n bodyField: string | null | undefined,\n defaultSubject: string,\n ): Promise<{ subject: string; bodyHtml: string | null }> {\n const subject = subjectField\n ? interpolateVariables(subjectField, variables)\n : defaultSubject\n const bodyHtml = bodyField\n ? renderMarkdownBody(interpolateVariables(bodyField, variables))\n : null\n return { subject, bodyHtml }\n }\n\n if (payload.type === 'start') {\n if (link?.sendStartEmail === false) return\n const { subject, bodyHtml } = await resolveEmailContent(\n link?.startEmailSubject,\n link?.startEmailBody,\n t('checkout.systemEmails.start.subject', 'Payment initiated - {linkTitle}', { linkTitle }),\n )\n await sendEmail({\n to: email,\n subject,\n from,\n replyTo,\n react: PaymentStartEmail({\n firstName,\n amount,\n currencyCode,\n linkTitle,\n bodyHtml,\n copy: {\n title: t('checkout.systemEmails.start.title'),\n preview: t('checkout.systemEmails.start.preview', { amount, currencyCode }),\n greeting: t('checkout.systemEmails.start.greeting', { firstName, linkTitle }),\n message: t('checkout.systemEmails.start.message'),\n hint: t('checkout.systemEmails.start.hint'),\n },\n }),\n })\n } else if (payload.type === 'success') {\n if (link?.sendSuccessEmail === false) return\n const { subject, bodyHtml } = await resolveEmailContent(\n link?.successEmailSubject,\n link?.successEmailBody,\n t('checkout.systemEmails.success.subject', 'Payment successful - {linkTitle}', { linkTitle }),\n )\n await sendEmail({\n to: email,\n subject,\n from,\n replyTo,\n react: PaymentSuccessEmail({\n firstName,\n amount,\n currencyCode,\n linkTitle,\n transactionId: transaction.id,\n bodyHtml,\n copy: {\n title: t('checkout.systemEmails.success.title'),\n preview: t('checkout.systemEmails.success.preview', { amount, currencyCode }),\n greeting: t('checkout.systemEmails.success.greeting', { firstName, linkTitle }),\n receipt: t('checkout.systemEmails.success.receipt'),\n hint: t('checkout.systemEmails.success.hint'),\n transactionLabel: t('checkout.systemEmails.success.transactionLabel'),\n },\n }),\n })\n } else if (payload.type === 'error') {\n if (link?.sendErrorEmail === false) return\n const { subject, bodyHtml } = await resolveEmailContent(\n link?.errorEmailSubject,\n link?.errorEmailBody,\n t('checkout.systemEmails.error.subject', 'Payment failed - {linkTitle}', { linkTitle }),\n )\n await sendEmail({\n to: email,\n subject,\n from,\n replyTo,\n react: PaymentErrorEmail({\n firstName,\n linkTitle,\n errorMessage,\n bodyHtml,\n copy: {\n title: t('checkout.systemEmails.error.title'),\n preview: t('checkout.systemEmails.error.preview', { linkTitle }),\n greeting: t('checkout.systemEmails.error.greeting', { firstName, linkTitle }),\n retry: t('checkout.systemEmails.error.retry'),\n hint: t('checkout.systemEmails.error.hint'),\n },\n }),\n })\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { DEFAULT_NOTIFICATION_DELIVERY_CONFIG, resolveNotificationDeliveryConfig } from '@open-mercato/core/modules/notifications/lib/deliveryConfig'\nimport type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CheckoutTransaction, CheckoutLink } from '../data/entities'\nimport PaymentStartEmail from '../emails/PaymentStartEmail'\nimport PaymentSuccessEmail from '../emails/PaymentSuccessEmail'\nimport PaymentErrorEmail from '../emails/PaymentErrorEmail'\n\nexport const CHECKOUT_EMAIL_QUEUE = 'checkout-email'\n\nexport type CheckoutEmailJob =\n | { type: 'start'; transactionId: string; tenantId: string; organizationId: string }\n | { type: 'success'; transactionId: string; tenantId: string; organizationId: string }\n | { type: 'error'; transactionId: string; tenantId: string; organizationId: string; errorMessage?: string | null }\n\nexport const metadata: WorkerMeta = {\n queue: CHECKOUT_EMAIL_QUEUE,\n id: 'checkout:send-email',\n concurrency: 5,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\nfunction interpolateVariables(template: string, variables: Record<string, string>): string {\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (match, key: string) => variables[key] ?? match)\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n\nfunction renderInlineMarkdown(value: string): string {\n let rendered = escapeHtml(value)\n rendered = rendered.replace(/`([^`]+)`/g, '<code>$1</code>')\n rendered = rendered.replace(/\\[([^\\]]+)\\]\\((https?:\\/\\/[^\\s)]+)\\)/g, '<a href=\"$2\">$1</a>')\n rendered = rendered.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n rendered = rendered.replace(/__([^_]+)__/g, '<strong>$1</strong>')\n rendered = rendered.replace(/(^|[\\s(])\\*([^*]+)\\*(?=$|[\\s).,!?:;])/g, '$1<em>$2</em>')\n rendered = rendered.replace(/(^|[\\s(])_([^_]+)_(?=$|[\\s).,!?:;])/g, '$1<em>$2</em>')\n return rendered\n}\n\nfunction renderParagraph(lines: string[]): string {\n return `<p>${renderInlineMarkdown(lines.join(' '))}</p>`\n}\n\nfunction renderMarkdownBody(markdown: string): string {\n const normalized = markdown.replace(/\\r\\n/g, '\\n').trim()\n if (!normalized) return ''\n\n const blocks: string[] = []\n const lines = normalized.split('\\n')\n let paragraph: string[] = []\n let listItems: string[] = []\n\n const flushParagraph = () => {\n if (paragraph.length === 0) return\n blocks.push(renderParagraph(paragraph))\n paragraph = []\n }\n\n const flushList = () => {\n if (listItems.length === 0) return\n blocks.push(`<ul>${listItems.map((item) => `<li>${renderInlineMarkdown(item)}</li>`).join('')}</ul>`)\n listItems = []\n }\n\n for (const rawLine of lines) {\n const line = rawLine.trim()\n if (!line) {\n flushParagraph()\n flushList()\n continue\n }\n\n const headingMatch = /^(#{1,6})\\s+(.+)$/.exec(line)\n if (headingMatch) {\n flushParagraph()\n flushList()\n const level = headingMatch[1].length\n blocks.push(`<h${level}>${renderInlineMarkdown(headingMatch[2])}</h${level}>`)\n continue\n }\n\n const listMatch = /^[-*]\\s+(.+)$/.exec(line)\n if (listMatch) {\n flushParagraph()\n listItems.push(listMatch[1])\n continue\n }\n\n flushList()\n paragraph.push(line)\n }\n\n flushParagraph()\n flushList()\n\n return blocks.join('')\n}\n\nexport default async function handle(job: QueuedJob<CheckoutEmailJob>, ctx: HandlerContext): Promise<void> {\n const { payload } = job\n const em = (ctx.resolve('em') as EntityManager).fork()\n const { t } = await resolveTranslations()\n\n const transaction = await findOneWithDecryption<CheckoutTransaction>(em, CheckoutTransaction, {\n id: payload.transactionId,\n organizationId: payload.organizationId,\n tenantId: payload.tenantId,\n })\n if (!transaction) return\n\n const email = transaction.email\n if (!email) return\n\n const link = await findOneWithDecryption(em, CheckoutLink, {\n id: transaction.linkId,\n organizationId: payload.organizationId,\n tenantId: payload.tenantId,\n deletedAt: null,\n }, undefined, { organizationId: payload.organizationId, tenantId: payload.tenantId })\n\n const firstName = transaction.firstName ?? t('checkout.systemEmails.common.customerFallback')\n const linkTitle = link?.title ?? link?.name ?? t('checkout.systemEmails.common.linkTitleFallback')\n const amount = String(transaction.amount ?? '0.00')\n const currencyCode = transaction.currencyCode ?? ''\n const errorMessage = payload.type === 'error' ? (payload.errorMessage ?? null) : null\n\n const variables: Record<string, string> = {\n firstName,\n amount,\n currencyCode,\n linkTitle,\n transactionId: transaction.id,\n errorMessage: errorMessage ?? '',\n }\n const deliveryConfig = await resolveNotificationDeliveryConfig(ctx, {\n defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG,\n })\n const from = deliveryConfig.strategies.email.from\n const replyTo = deliveryConfig.strategies.email.replyTo\n\n async function resolveEmailContent(\n subjectField: string | null | undefined,\n bodyField: string | null | undefined,\n defaultSubject: string,\n ): Promise<{ subject: string; bodyHtml: string | null }> {\n const subject = subjectField\n ? interpolateVariables(subjectField, variables)\n : defaultSubject\n const bodyHtml = bodyField\n ? renderMarkdownBody(interpolateVariables(bodyField, variables))\n : null\n return { subject, bodyHtml }\n }\n\n if (payload.type === 'start') {\n if (link?.sendStartEmail === false) return\n const { subject, bodyHtml } = await resolveEmailContent(\n link?.startEmailSubject,\n link?.startEmailBody,\n t('checkout.systemEmails.start.subject', 'Payment initiated - {linkTitle}', { linkTitle }),\n )\n await sendEmail({\n to: email,\n subject,\n from,\n replyTo,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n react: PaymentStartEmail({\n firstName,\n amount,\n currencyCode,\n linkTitle,\n bodyHtml,\n copy: {\n title: t('checkout.systemEmails.start.title'),\n preview: t('checkout.systemEmails.start.preview', { amount, currencyCode }),\n greeting: t('checkout.systemEmails.start.greeting', { firstName, linkTitle }),\n message: t('checkout.systemEmails.start.message'),\n hint: t('checkout.systemEmails.start.hint'),\n },\n }),\n })\n } else if (payload.type === 'success') {\n if (link?.sendSuccessEmail === false) return\n const { subject, bodyHtml } = await resolveEmailContent(\n link?.successEmailSubject,\n link?.successEmailBody,\n t('checkout.systemEmails.success.subject', 'Payment successful - {linkTitle}', { linkTitle }),\n )\n await sendEmail({\n to: email,\n subject,\n from,\n replyTo,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n react: PaymentSuccessEmail({\n firstName,\n amount,\n currencyCode,\n linkTitle,\n transactionId: transaction.id,\n bodyHtml,\n copy: {\n title: t('checkout.systemEmails.success.title'),\n preview: t('checkout.systemEmails.success.preview', { amount, currencyCode }),\n greeting: t('checkout.systemEmails.success.greeting', { firstName, linkTitle }),\n receipt: t('checkout.systemEmails.success.receipt'),\n hint: t('checkout.systemEmails.success.hint'),\n transactionLabel: t('checkout.systemEmails.success.transactionLabel'),\n },\n }),\n })\n } else if (payload.type === 'error') {\n if (link?.sendErrorEmail === false) return\n const { subject, bodyHtml } = await resolveEmailContent(\n link?.errorEmailSubject,\n link?.errorEmailBody,\n t('checkout.systemEmails.error.subject', 'Payment failed - {linkTitle}', { linkTitle }),\n )\n await sendEmail({\n to: email,\n subject,\n from,\n replyTo,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n react: PaymentErrorEmail({\n firstName,\n linkTitle,\n errorMessage,\n bodyHtml,\n copy: {\n title: t('checkout.systemEmails.error.title'),\n preview: t('checkout.systemEmails.error.preview', { linkTitle }),\n greeting: t('checkout.systemEmails.error.greeting', { firstName, linkTitle }),\n retry: t('checkout.systemEmails.error.retry'),\n hint: t('checkout.systemEmails.error.hint'),\n },\n }),\n })\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/checkout",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7152.1.a69e92f9c9",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -62,18 +62,18 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@open-mercato/core": "0.7.1-develop.
|
|
66
|
-
"@open-mercato/ui": "0.7.1-develop.
|
|
65
|
+
"@open-mercato/core": "0.7.1-develop.7152.1.a69e92f9c9",
|
|
66
|
+
"@open-mercato/ui": "0.7.1-develop.7152.1.a69e92f9c9",
|
|
67
67
|
"bcryptjs": "^3.0.3"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"@mikro-orm/postgresql": "^7.0.14",
|
|
71
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
71
|
+
"@open-mercato/shared": "0.7.1-develop.7152.1.a69e92f9c9",
|
|
72
72
|
"react": "^19.0.0",
|
|
73
73
|
"react-dom": "^19.0.0"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
76
|
+
"@open-mercato/shared": "0.7.1-develop.7152.1.a69e92f9c9",
|
|
77
77
|
"@types/jest": "^30.0.0",
|
|
78
78
|
"@types/react": "^19.2.17",
|
|
79
79
|
"@types/react-dom": "^19.2.3",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
|
+
import { getAuthToken } from '@open-mercato/core/helpers/integration/api'
|
|
3
|
+
import { getTokenScope, readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'
|
|
4
|
+
import { drainIntegrationQueue } from '@open-mercato/core/helpers/integration/queue'
|
|
5
|
+
import {
|
|
6
|
+
clearCapturedSystemEmails,
|
|
7
|
+
isChannelSeedingAvailable,
|
|
8
|
+
seedSystemEmailChannel,
|
|
9
|
+
waitForCapturedSystemEmail,
|
|
10
|
+
} from '@open-mercato/core/helpers/integration/communicationChannelsFixtures'
|
|
11
|
+
import {
|
|
12
|
+
createCustomerData,
|
|
13
|
+
createFixedTemplateInput,
|
|
14
|
+
createLinkFixture,
|
|
15
|
+
deleteCheckoutEntityIfExists,
|
|
16
|
+
submitPayLink,
|
|
17
|
+
} from './helpers/fixtures'
|
|
18
|
+
|
|
19
|
+
test.describe('TC-CHKT-EMAIL-001: Checkout transactional email uses system channel', () => {
|
|
20
|
+
test('public checkout submit dispatches payment-start email through Communications Hub', async ({ request }) => {
|
|
21
|
+
test.slow()
|
|
22
|
+
const token = await getAuthToken(request, 'admin')
|
|
23
|
+
const scope = getTokenScope(token)
|
|
24
|
+
const seedingAvailable = await isChannelSeedingAvailable(request, token)
|
|
25
|
+
test.skip(!seedingAvailable, 'OM_ENABLE_TEST_CHANNEL_SEEDING is not enabled.')
|
|
26
|
+
|
|
27
|
+
await seedSystemEmailChannel(request, token)
|
|
28
|
+
await clearCapturedSystemEmails(request, token)
|
|
29
|
+
|
|
30
|
+
const stamp = Date.now()
|
|
31
|
+
const customerEmail = `qa-checkout-email-${stamp}@example.test`
|
|
32
|
+
let linkId: string | null = null
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const link = await createLinkFixture(request, token, createFixedTemplateInput({
|
|
36
|
+
title: `QA checkout email ${stamp}`,
|
|
37
|
+
status: 'active',
|
|
38
|
+
startEmailSubject: `Checkout started ${stamp}`,
|
|
39
|
+
sendStartEmail: true,
|
|
40
|
+
}))
|
|
41
|
+
linkId = link.id
|
|
42
|
+
|
|
43
|
+
const submit = await submitPayLink(request, link.slug, {
|
|
44
|
+
customerData: createCustomerData({ email: customerEmail }),
|
|
45
|
+
acceptedLegalConsents: {},
|
|
46
|
+
})
|
|
47
|
+
expect(submit.status()).toBe(201)
|
|
48
|
+
const body = await readJsonSafe<{ transactionId?: string }>(submit)
|
|
49
|
+
expect(typeof body?.transactionId).toBe('string')
|
|
50
|
+
|
|
51
|
+
await drainIntegrationQueue('events')
|
|
52
|
+
await drainIntegrationQueue('checkout-email')
|
|
53
|
+
|
|
54
|
+
const captured = await waitForCapturedSystemEmail(
|
|
55
|
+
request,
|
|
56
|
+
token,
|
|
57
|
+
(email) => email.metadata?.to === customerEmail && email.metadata?.subject === `Checkout started ${stamp}`,
|
|
58
|
+
{ description: 'checkout payment start email' },
|
|
59
|
+
)
|
|
60
|
+
expect(captured.scope.tenantId).toBe(scope.tenantId)
|
|
61
|
+
expect(captured.scope.organizationId).toBe(scope.organizationId)
|
|
62
|
+
expect(captured.content.bodyFormat).toBe('html')
|
|
63
|
+
} finally {
|
|
64
|
+
await deleteCheckoutEntityIfExists(request, token, 'links', linkId)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -40,6 +40,15 @@ export type CheckoutTemplateInput = {
|
|
|
40
40
|
terms?: { title: string; markdown: string; required?: boolean }
|
|
41
41
|
privacyPolicy?: { title: string; markdown: string; required?: boolean }
|
|
42
42
|
}
|
|
43
|
+
sendStartEmail?: boolean
|
|
44
|
+
startEmailSubject?: string | null
|
|
45
|
+
startEmailBody?: string | null
|
|
46
|
+
sendSuccessEmail?: boolean
|
|
47
|
+
successEmailSubject?: string | null
|
|
48
|
+
successEmailBody?: string | null
|
|
49
|
+
sendErrorEmail?: boolean
|
|
50
|
+
errorEmailSubject?: string | null
|
|
51
|
+
errorEmailBody?: string | null
|
|
43
52
|
displayCustomFieldsOnPage?: boolean
|
|
44
53
|
password?: string | null
|
|
45
54
|
maxCompletions?: number | null
|
|
@@ -177,6 +177,8 @@ export default async function handle(job: QueuedJob<CheckoutEmailJob>, ctx: Hand
|
|
|
177
177
|
subject,
|
|
178
178
|
from,
|
|
179
179
|
replyTo,
|
|
180
|
+
tenantId: payload.tenantId,
|
|
181
|
+
organizationId: payload.organizationId,
|
|
180
182
|
react: PaymentStartEmail({
|
|
181
183
|
firstName,
|
|
182
184
|
amount,
|
|
@@ -204,6 +206,8 @@ export default async function handle(job: QueuedJob<CheckoutEmailJob>, ctx: Hand
|
|
|
204
206
|
subject,
|
|
205
207
|
from,
|
|
206
208
|
replyTo,
|
|
209
|
+
tenantId: payload.tenantId,
|
|
210
|
+
organizationId: payload.organizationId,
|
|
207
211
|
react: PaymentSuccessEmail({
|
|
208
212
|
firstName,
|
|
209
213
|
amount,
|
|
@@ -233,6 +237,8 @@ export default async function handle(job: QueuedJob<CheckoutEmailJob>, ctx: Hand
|
|
|
233
237
|
subject,
|
|
234
238
|
from,
|
|
235
239
|
replyTo,
|
|
240
|
+
tenantId: payload.tenantId,
|
|
241
|
+
organizationId: payload.organizationId,
|
|
236
242
|
react: PaymentErrorEmail({
|
|
237
243
|
firstName,
|
|
238
244
|
linkTitle,
|