feat: server-authoritative checkout, no client-computed amount
F14-F16 of the frontend backlog. Contract: PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §5.2.
The highest-priority change in Phase 1: `POST /cart` sent `amount` computed
client-side (this.convertTotal(this.totalWithDelivery())) and the backend was
asked to trust it. Replaced with two calls:
1. POST /api/v2/storefront/checkout - offer ids + qty only. Returns
checkoutSessionId and the server-computed total.
2. POST /api/v2/storefront/payments/intents - references checkoutSessionId
only. Same response shape as before (qrId/qrUrl/bankUrl/qrTTL via the
existing resolvePaymentQrId/resolvePaymentLink/resolveBankPaymentUrl
helpers) - this replaces how the charged amount is determined, not the
QR/card provider polling flow, which Phase 1 does not redesign.
merchantReference (PARTNER-PROVISIONING-API-CONTRACT.md's RoutingContext
field) is sent on the payment intent, generated the same way the old orderId
was - our own correlation id, now with a name that matches what it is.
api.service.ts: CheckoutSessionRequest/Response and PaymentIntentRequest
types added, old CartPaymentRequest/createCartPayment left in place (Phase 7
reconciliation and any other caller may still reference the shape) but no
longer called from checkout.
offerId uses item.itemID: this codebase has no distinct Offer entity yet
(Phase 3, Product/Offer split, not shipped in this model) - itemID is the
same catalog identifier every other endpoint already keys off. Flagged in a
code comment for whoever ships Phase 3 to revisit.
Dead code removed as a consequence, not a separate pass: buildPaymentItems,
getPaymentUserId, getPaymentDescription (no other caller once the old
payload was gone), the ConfigService/TenantResolverService injects that
existed only for getPaymentDescription, and the now-orphaned
cart.paymentDescriptionFallback i18n key in all three locales.
Verification: cart.component.ts has no unit spec (no src/app/pages/cart/
*.spec.ts exists) - this session's E2E suite is the only coverage the
checkout request shape has. Added checkout-request-shape.spec.ts, scoped
narrowly to the request/response contract rather than a full add-to-cart
UI journey: seeds cart state directly into localStorage, fakes the customer
session via cookie + intercepted session-check, intercepts both new
endpoints and asserts on the captured request bodies. Confirms concretely:
no `amount` or `price` field ever leaves the client, offers carry the right
offerId/qty, and the payment intent correctly threads checkoutSessionId
through.
Verified: 5/5 E2E green, 115/115 unit tests green, arch:check clean,
production build succeeds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:14:19 +04:00
|
|
|
import { Page, Route, expect, test } from '@playwright/test';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Guards the specific contract this rewrite exists to enforce
|
|
|
|
|
* (PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §5.2): the amount actually charged
|
|
|
|
|
* must be computed server-side, never sent by the client. Before this
|
|
|
|
|
* rewrite, POST /cart carried a client-computed `amount` the backend was
|
|
|
|
|
* asked to trust.
|
|
|
|
|
*
|
|
|
|
|
* cart.component.ts has no unit spec (no src/app/pages/cart/*.spec.ts
|
|
|
|
|
* exists), so this E2E test is the only coverage the checkout request shape
|
|
|
|
|
* has. Scoped narrowly on purpose: cart state is seeded directly into
|
|
|
|
|
* localStorage and the customer session is faked via cookie + intercepted
|
|
|
|
|
* session-check, rather than driving a full add-to-cart-then-login UI
|
|
|
|
|
* journey - that journey is real product surface worth its own test, but
|
|
|
|
|
* would make this test about navigation, not about what it exists to prove.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const FAKE_SESSION_ID = 'e2e-fake-session';
|
|
|
|
|
const FAKE_ITEM = {
|
|
|
|
|
categoryID: 1,
|
|
|
|
|
itemID: 4242,
|
|
|
|
|
name: 'E2E Test Item',
|
|
|
|
|
photos: null,
|
|
|
|
|
description: '',
|
|
|
|
|
currency: 'RUB',
|
|
|
|
|
price: 1000,
|
|
|
|
|
discount: 0,
|
|
|
|
|
rating: 0,
|
|
|
|
|
callbacks: null,
|
|
|
|
|
questions: null,
|
|
|
|
|
quantity: 2,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
test.describe('checkout request shape', () => {
|
|
|
|
|
test.beforeEach(async ({ page, context }) => {
|
|
|
|
|
await seedCart(page);
|
|
|
|
|
await fakeCustomerSession(page, context);
|
|
|
|
|
await mockFxQuoteEndpoint(page);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('checkout session request carries offers and qty, never amount or price', async ({ page }) => {
|
|
|
|
|
const checkoutRequest = interceptCheckoutSession(page);
|
|
|
|
|
|
|
|
|
|
await page.goto('/cart');
|
|
|
|
|
await acceptTermsAndCheckout(page);
|
|
|
|
|
|
|
|
|
|
const body = await checkoutRequest;
|
|
|
|
|
|
|
|
|
|
expect(body, 'must never send a client-computed amount').not.toHaveProperty('amount');
|
|
|
|
|
expect(body, 'must never send a client-computed price').not.toHaveProperty('price');
|
|
|
|
|
expect(Array.isArray(body.offers), 'must send an offers array').toBe(true);
|
|
|
|
|
expect(body.offers[0]).toMatchObject({ offerId: String(FAKE_ITEM.itemID), qty: FAKE_ITEM.quantity });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('payment intent request references the checkout session id, not a raw amount', async ({ page }) => {
|
|
|
|
|
interceptCheckoutSession(page); // must resolve for the intent call to fire at all
|
|
|
|
|
const intentRequest = interceptPaymentIntent(page);
|
|
|
|
|
|
|
|
|
|
await page.goto('/cart');
|
|
|
|
|
await acceptTermsAndCheckout(page);
|
|
|
|
|
|
|
|
|
|
const body = await intentRequest;
|
|
|
|
|
|
feat: start @marketplaces/payment implementation
package.json already had @marketplaces/payment added (uncommitted) when this
started. Wired it in.
- app.config.ts: provideMarketplacesPayment(). apiUrl is environment.qrApiUrl
with its trailing /api stripped - found and fixed a real bug while wiring
this: qrApiUrl already ends in /api, and the package's default
paymentsPath is '/api/v1/payments', so passing qrApiUrl unchanged would
have silently doubled the path to .../api/api/v1/payments. Confirmed by
reading the package's baseUrl() concatenation directly, not guessed.
marketplaceDomain is a plain closure (not TenantResolverService) since
provideMarketplacesPayment runs outside the injector.
- cart.component.ts: createPaymentIntent() and startPolling() now go through
MARKETPLACES_PAYMENT_GATEWAY instead of api.service.ts's
createPaymentIntent/checkCartPaymentStatus/checkCartCardPaymentStatus (our
own earlier inferred contract, now superseded by the package's real,
published one - POST/GET {qrApiUrl}/api/v1/payments). Deliberately did NOT
swap to the package's own <mp-payment> UI component - that has a different
UX paradigm entirely (window.open for redirects instead of an iframe
popup, client-side QR generation instead of an external image service) and
replacing the existing, already-tested 769-line popup state machine
wholesale is a separate, much larger change than "wire the new package
in." Only the I/O layer moved; the surrounding state machine (paymentStatus,
checkoutInFlight, timeout/success/error handling, bank-iframe UX) is
untouched.
Response shape differs from the legacy provider: the package's
PaymentStatus is a fixed union (created/pending/authorized/paid/failed/
cancelled/expired), not a free-form string+code pair - simplified the
status-check conditionals accordingly and added 'authorized' as a second
success state (PaymentResult's own status union), which the legacy check
didn't have. The package also carries no TTL/expiry field on its response,
unlike the legacy provider's qrTTL - polling duration now falls back to
PAYMENT_MIN_POLL_SECONDS alone; flagged in a comment.
- api.service.ts's createPaymentIntent and its QrCreateResponse-based
resolvePaymentQrId/resolvePaymentQrUrl/resolvePaymentLink/
resolveBankPaymentUrl helpers are now dead code. Left in place rather than
deleted in the same pass that adds a new external dependency, so a revert
doesn't also need to resurrect deleted code.
Verified: production build succeeds, 247/247 unit tests, arch:check clean.
E2E: 2 of 7 tests currently fail
(checkout-request-shape.spec.ts, checkout-idempotent-click.spec.ts), and
this is disclosed honestly rather than hidden. Root cause, confirmed by
tracing real network requests: the customer-session cookie fake these tests
rely on stops working somewhere between the cookie being demonstrably
present in the browser (context.cookies(), and document.cookie read from a
plain page on the same origin) and Angular's own AuthService reading it -
the session-check request never fires at all. This reproduces with or
without this session's payment changes (checkout-idempotent-click.spec.ts
doesn't touch payment creation and fails the same way), so it is not a
regression introduced here, but it is unresolved. Tried switching
context.addCookies from {domain,path} to {url} form (the standard fix for
this class of Playwright cookie issue) - did not fix it, kept anyway as the
more correct form. Documented as a known, unresolved issue directly in both
spec files and e2e/README.md rather than deleting or silently marking the
tests skip - the request-shape assertions those tests make are still
correct, they are just currently unverifiable through this harness.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 09:37:16 +04:00
|
|
|
// Payment creation now goes through @marketplaces/payment
|
|
|
|
|
// (MARKETPLACES_PAYMENT_GATEWAY -> POST {qrApiUrl}/api/v1/payments),
|
|
|
|
|
// not api.service.ts's superseded createPaymentIntent - see
|
|
|
|
|
// cart.component.ts's createPaymentIntent() comment.
|
feat: server-authoritative checkout, no client-computed amount
F14-F16 of the frontend backlog. Contract: PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §5.2.
The highest-priority change in Phase 1: `POST /cart` sent `amount` computed
client-side (this.convertTotal(this.totalWithDelivery())) and the backend was
asked to trust it. Replaced with two calls:
1. POST /api/v2/storefront/checkout - offer ids + qty only. Returns
checkoutSessionId and the server-computed total.
2. POST /api/v2/storefront/payments/intents - references checkoutSessionId
only. Same response shape as before (qrId/qrUrl/bankUrl/qrTTL via the
existing resolvePaymentQrId/resolvePaymentLink/resolveBankPaymentUrl
helpers) - this replaces how the charged amount is determined, not the
QR/card provider polling flow, which Phase 1 does not redesign.
merchantReference (PARTNER-PROVISIONING-API-CONTRACT.md's RoutingContext
field) is sent on the payment intent, generated the same way the old orderId
was - our own correlation id, now with a name that matches what it is.
api.service.ts: CheckoutSessionRequest/Response and PaymentIntentRequest
types added, old CartPaymentRequest/createCartPayment left in place (Phase 7
reconciliation and any other caller may still reference the shape) but no
longer called from checkout.
offerId uses item.itemID: this codebase has no distinct Offer entity yet
(Phase 3, Product/Offer split, not shipped in this model) - itemID is the
same catalog identifier every other endpoint already keys off. Flagged in a
code comment for whoever ships Phase 3 to revisit.
Dead code removed as a consequence, not a separate pass: buildPaymentItems,
getPaymentUserId, getPaymentDescription (no other caller once the old
payload was gone), the ConfigService/TenantResolverService injects that
existed only for getPaymentDescription, and the now-orphaned
cart.paymentDescriptionFallback i18n key in all three locales.
Verification: cart.component.ts has no unit spec (no src/app/pages/cart/
*.spec.ts exists) - this session's E2E suite is the only coverage the
checkout request shape has. Added checkout-request-shape.spec.ts, scoped
narrowly to the request/response contract rather than a full add-to-cart
UI journey: seeds cart state directly into localStorage, fakes the customer
session via cookie + intercepted session-check, intercepts both new
endpoints and asserts on the captured request bodies. Confirms concretely:
no `amount` or `price` field ever leaves the client, offers carry the right
offerId/qty, and the payment intent correctly threads checkoutSessionId
through.
Verified: 5/5 E2E green, 115/115 unit tests green, arch:check clean,
production build succeeds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:14:19 +04:00
|
|
|
expect(body.checkoutSessionId, 'must reference the session created in step 1').toBe('chk_e2e_fixture');
|
|
|
|
|
expect(body).not.toHaveProperty('amount');
|
feat: start @marketplaces/payment implementation
package.json already had @marketplaces/payment added (uncommitted) when this
started. Wired it in.
- app.config.ts: provideMarketplacesPayment(). apiUrl is environment.qrApiUrl
with its trailing /api stripped - found and fixed a real bug while wiring
this: qrApiUrl already ends in /api, and the package's default
paymentsPath is '/api/v1/payments', so passing qrApiUrl unchanged would
have silently doubled the path to .../api/api/v1/payments. Confirmed by
reading the package's baseUrl() concatenation directly, not guessed.
marketplaceDomain is a plain closure (not TenantResolverService) since
provideMarketplacesPayment runs outside the injector.
- cart.component.ts: createPaymentIntent() and startPolling() now go through
MARKETPLACES_PAYMENT_GATEWAY instead of api.service.ts's
createPaymentIntent/checkCartPaymentStatus/checkCartCardPaymentStatus (our
own earlier inferred contract, now superseded by the package's real,
published one - POST/GET {qrApiUrl}/api/v1/payments). Deliberately did NOT
swap to the package's own <mp-payment> UI component - that has a different
UX paradigm entirely (window.open for redirects instead of an iframe
popup, client-side QR generation instead of an external image service) and
replacing the existing, already-tested 769-line popup state machine
wholesale is a separate, much larger change than "wire the new package
in." Only the I/O layer moved; the surrounding state machine (paymentStatus,
checkoutInFlight, timeout/success/error handling, bank-iframe UX) is
untouched.
Response shape differs from the legacy provider: the package's
PaymentStatus is a fixed union (created/pending/authorized/paid/failed/
cancelled/expired), not a free-form string+code pair - simplified the
status-check conditionals accordingly and added 'authorized' as a second
success state (PaymentResult's own status union), which the legacy check
didn't have. The package also carries no TTL/expiry field on its response,
unlike the legacy provider's qrTTL - polling duration now falls back to
PAYMENT_MIN_POLL_SECONDS alone; flagged in a comment.
- api.service.ts's createPaymentIntent and its QrCreateResponse-based
resolvePaymentQrId/resolvePaymentQrUrl/resolvePaymentLink/
resolveBankPaymentUrl helpers are now dead code. Left in place rather than
deleted in the same pass that adds a new external dependency, so a revert
doesn't also need to resurrect deleted code.
Verified: production build succeeds, 247/247 unit tests, arch:check clean.
E2E: 2 of 7 tests currently fail
(checkout-request-shape.spec.ts, checkout-idempotent-click.spec.ts), and
this is disclosed honestly rather than hidden. Root cause, confirmed by
tracing real network requests: the customer-session cookie fake these tests
rely on stops working somewhere between the cookie being demonstrably
present in the browser (context.cookies(), and document.cookie read from a
plain page on the same origin) and Angular's own AuthService reading it -
the session-check request never fires at all. This reproduces with or
without this session's payment changes (checkout-idempotent-click.spec.ts
doesn't touch payment creation and fails the same way), so it is not a
regression introduced here, but it is unresolved. Tried switching
context.addCookies from {domain,path} to {url} form (the standard fix for
this class of Playwright cookie issue) - did not fix it, kept anyway as the
more correct form. Documented as a known, unresolved issue directly in both
spec files and e2e/README.md rather than deleting or silently marking the
tests skip - the request-shape assertions those tests make are still
correct, they are just currently unverifiable through this harness.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 09:37:16 +04:00
|
|
|
const metadata = body.metadata as Record<string, string> | undefined;
|
|
|
|
|
expect(typeof metadata?.merchantReference).toBe('string');
|
|
|
|
|
expect((metadata?.merchantReference ?? '').length).toBeGreaterThan(0);
|
feat: server-authoritative checkout, no client-computed amount
F14-F16 of the frontend backlog. Contract: PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §5.2.
The highest-priority change in Phase 1: `POST /cart` sent `amount` computed
client-side (this.convertTotal(this.totalWithDelivery())) and the backend was
asked to trust it. Replaced with two calls:
1. POST /api/v2/storefront/checkout - offer ids + qty only. Returns
checkoutSessionId and the server-computed total.
2. POST /api/v2/storefront/payments/intents - references checkoutSessionId
only. Same response shape as before (qrId/qrUrl/bankUrl/qrTTL via the
existing resolvePaymentQrId/resolvePaymentLink/resolveBankPaymentUrl
helpers) - this replaces how the charged amount is determined, not the
QR/card provider polling flow, which Phase 1 does not redesign.
merchantReference (PARTNER-PROVISIONING-API-CONTRACT.md's RoutingContext
field) is sent on the payment intent, generated the same way the old orderId
was - our own correlation id, now with a name that matches what it is.
api.service.ts: CheckoutSessionRequest/Response and PaymentIntentRequest
types added, old CartPaymentRequest/createCartPayment left in place (Phase 7
reconciliation and any other caller may still reference the shape) but no
longer called from checkout.
offerId uses item.itemID: this codebase has no distinct Offer entity yet
(Phase 3, Product/Offer split, not shipped in this model) - itemID is the
same catalog identifier every other endpoint already keys off. Flagged in a
code comment for whoever ships Phase 3 to revisit.
Dead code removed as a consequence, not a separate pass: buildPaymentItems,
getPaymentUserId, getPaymentDescription (no other caller once the old
payload was gone), the ConfigService/TenantResolverService injects that
existed only for getPaymentDescription, and the now-orphaned
cart.paymentDescriptionFallback i18n key in all three locales.
Verification: cart.component.ts has no unit spec (no src/app/pages/cart/
*.spec.ts exists) - this session's E2E suite is the only coverage the
checkout request shape has. Added checkout-request-shape.spec.ts, scoped
narrowly to the request/response contract rather than a full add-to-cart
UI journey: seeds cart state directly into localStorage, fakes the customer
session via cookie + intercepted session-check, intercepts both new
endpoints and asserts on the captured request bodies. Confirms concretely:
no `amount` or `price` field ever leaves the client, offers carry the right
offerId/qty, and the payment intent correctly threads checkoutSessionId
through.
Verified: 5/5 E2E green, 115/115 unit tests green, arch:check clean,
production build succeeds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:14:19 +04:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function seedCart(page: Page): Promise<void> {
|
|
|
|
|
await page.addInitScript(item => {
|
|
|
|
|
window.localStorage.setItem('marketplace_cart', JSON.stringify([item]));
|
|
|
|
|
}, FAKE_ITEM);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function fakeCustomerSession(page: Page, context: import('@playwright/test').BrowserContext): Promise<void> {
|
feat: start @marketplaces/payment implementation
package.json already had @marketplaces/payment added (uncommitted) when this
started. Wired it in.
- app.config.ts: provideMarketplacesPayment(). apiUrl is environment.qrApiUrl
with its trailing /api stripped - found and fixed a real bug while wiring
this: qrApiUrl already ends in /api, and the package's default
paymentsPath is '/api/v1/payments', so passing qrApiUrl unchanged would
have silently doubled the path to .../api/api/v1/payments. Confirmed by
reading the package's baseUrl() concatenation directly, not guessed.
marketplaceDomain is a plain closure (not TenantResolverService) since
provideMarketplacesPayment runs outside the injector.
- cart.component.ts: createPaymentIntent() and startPolling() now go through
MARKETPLACES_PAYMENT_GATEWAY instead of api.service.ts's
createPaymentIntent/checkCartPaymentStatus/checkCartCardPaymentStatus (our
own earlier inferred contract, now superseded by the package's real,
published one - POST/GET {qrApiUrl}/api/v1/payments). Deliberately did NOT
swap to the package's own <mp-payment> UI component - that has a different
UX paradigm entirely (window.open for redirects instead of an iframe
popup, client-side QR generation instead of an external image service) and
replacing the existing, already-tested 769-line popup state machine
wholesale is a separate, much larger change than "wire the new package
in." Only the I/O layer moved; the surrounding state machine (paymentStatus,
checkoutInFlight, timeout/success/error handling, bank-iframe UX) is
untouched.
Response shape differs from the legacy provider: the package's
PaymentStatus is a fixed union (created/pending/authorized/paid/failed/
cancelled/expired), not a free-form string+code pair - simplified the
status-check conditionals accordingly and added 'authorized' as a second
success state (PaymentResult's own status union), which the legacy check
didn't have. The package also carries no TTL/expiry field on its response,
unlike the legacy provider's qrTTL - polling duration now falls back to
PAYMENT_MIN_POLL_SECONDS alone; flagged in a comment.
- api.service.ts's createPaymentIntent and its QrCreateResponse-based
resolvePaymentQrId/resolvePaymentQrUrl/resolvePaymentLink/
resolveBankPaymentUrl helpers are now dead code. Left in place rather than
deleted in the same pass that adds a new external dependency, so a revert
doesn't also need to resurrect deleted code.
Verified: production build succeeds, 247/247 unit tests, arch:check clean.
E2E: 2 of 7 tests currently fail
(checkout-request-shape.spec.ts, checkout-idempotent-click.spec.ts), and
this is disclosed honestly rather than hidden. Root cause, confirmed by
tracing real network requests: the customer-session cookie fake these tests
rely on stops working somewhere between the cookie being demonstrably
present in the browser (context.cookies(), and document.cookie read from a
plain page on the same origin) and Angular's own AuthService reading it -
the session-check request never fires at all. This reproduces with or
without this session's payment changes (checkout-idempotent-click.spec.ts
doesn't touch payment creation and fails the same way), so it is not a
regression introduced here, but it is unresolved. Tried switching
context.addCookies from {domain,path} to {url} form (the standard fix for
this class of Playwright cookie issue) - did not fix it, kept anyway as the
more correct form. Documented as a known, unresolved issue directly in both
spec files and e2e/README.md rather than deleting or silently marking the
tests skip - the request-shape assertions those tests make are still
correct, they are just currently unverifiable through this harness.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 09:37:16 +04:00
|
|
|
// KNOWN ISSUE, NOT RESOLVED (2026-08-21): this test currently fails.
|
|
|
|
|
// Traced with page.on('request'): the customer-session check
|
|
|
|
|
// (AuthService.checkSession -> getStoredWebSessionID) never fires at all
|
|
|
|
|
// once Angular bootstraps on this page, even though the cookie is
|
|
|
|
|
// confirmed present via context.cookies() and via document.cookie read
|
|
|
|
|
// from a plain (non-Angular) page on the same origin immediately before.
|
|
|
|
|
// Switching { domain, path } to { url } here did not fix it - kept anyway
|
|
|
|
|
// since it is the more correct form regardless. Something in the app's
|
|
|
|
|
// own bootstrap/DI path is not seeing a cookie that unambiguously exists
|
|
|
|
|
// in the browser; root cause not yet found. Do not trust a green run of
|
|
|
|
|
// this specific test until this is root-caused - the checkout REQUEST
|
|
|
|
|
// SHAPE assertions this test makes are still correct on paper, just
|
|
|
|
|
// currently unverifiable through this harness.
|
feat: server-authoritative checkout, no client-computed amount
F14-F16 of the frontend backlog. Contract: PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §5.2.
The highest-priority change in Phase 1: `POST /cart` sent `amount` computed
client-side (this.convertTotal(this.totalWithDelivery())) and the backend was
asked to trust it. Replaced with two calls:
1. POST /api/v2/storefront/checkout - offer ids + qty only. Returns
checkoutSessionId and the server-computed total.
2. POST /api/v2/storefront/payments/intents - references checkoutSessionId
only. Same response shape as before (qrId/qrUrl/bankUrl/qrTTL via the
existing resolvePaymentQrId/resolvePaymentLink/resolveBankPaymentUrl
helpers) - this replaces how the charged amount is determined, not the
QR/card provider polling flow, which Phase 1 does not redesign.
merchantReference (PARTNER-PROVISIONING-API-CONTRACT.md's RoutingContext
field) is sent on the payment intent, generated the same way the old orderId
was - our own correlation id, now with a name that matches what it is.
api.service.ts: CheckoutSessionRequest/Response and PaymentIntentRequest
types added, old CartPaymentRequest/createCartPayment left in place (Phase 7
reconciliation and any other caller may still reference the shape) but no
longer called from checkout.
offerId uses item.itemID: this codebase has no distinct Offer entity yet
(Phase 3, Product/Offer split, not shipped in this model) - itemID is the
same catalog identifier every other endpoint already keys off. Flagged in a
code comment for whoever ships Phase 3 to revisit.
Dead code removed as a consequence, not a separate pass: buildPaymentItems,
getPaymentUserId, getPaymentDescription (no other caller once the old
payload was gone), the ConfigService/TenantResolverService injects that
existed only for getPaymentDescription, and the now-orphaned
cart.paymentDescriptionFallback i18n key in all three locales.
Verification: cart.component.ts has no unit spec (no src/app/pages/cart/
*.spec.ts exists) - this session's E2E suite is the only coverage the
checkout request shape has. Added checkout-request-shape.spec.ts, scoped
narrowly to the request/response contract rather than a full add-to-cart
UI journey: seeds cart state directly into localStorage, fakes the customer
session via cookie + intercepted session-check, intercepts both new
endpoints and asserts on the captured request bodies. Confirms concretely:
no `amount` or `price` field ever leaves the client, offers carry the right
offerId/qty, and the payment intent correctly threads checkoutSessionId
through.
Verified: 5/5 E2E green, 115/115 unit tests green, arch:check clean,
production build succeeds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:14:19 +04:00
|
|
|
await context.addCookies([
|
|
|
|
|
{
|
|
|
|
|
name: 'webSessionID',
|
|
|
|
|
value: FAKE_SESSION_ID,
|
feat: start @marketplaces/payment implementation
package.json already had @marketplaces/payment added (uncommitted) when this
started. Wired it in.
- app.config.ts: provideMarketplacesPayment(). apiUrl is environment.qrApiUrl
with its trailing /api stripped - found and fixed a real bug while wiring
this: qrApiUrl already ends in /api, and the package's default
paymentsPath is '/api/v1/payments', so passing qrApiUrl unchanged would
have silently doubled the path to .../api/api/v1/payments. Confirmed by
reading the package's baseUrl() concatenation directly, not guessed.
marketplaceDomain is a plain closure (not TenantResolverService) since
provideMarketplacesPayment runs outside the injector.
- cart.component.ts: createPaymentIntent() and startPolling() now go through
MARKETPLACES_PAYMENT_GATEWAY instead of api.service.ts's
createPaymentIntent/checkCartPaymentStatus/checkCartCardPaymentStatus (our
own earlier inferred contract, now superseded by the package's real,
published one - POST/GET {qrApiUrl}/api/v1/payments). Deliberately did NOT
swap to the package's own <mp-payment> UI component - that has a different
UX paradigm entirely (window.open for redirects instead of an iframe
popup, client-side QR generation instead of an external image service) and
replacing the existing, already-tested 769-line popup state machine
wholesale is a separate, much larger change than "wire the new package
in." Only the I/O layer moved; the surrounding state machine (paymentStatus,
checkoutInFlight, timeout/success/error handling, bank-iframe UX) is
untouched.
Response shape differs from the legacy provider: the package's
PaymentStatus is a fixed union (created/pending/authorized/paid/failed/
cancelled/expired), not a free-form string+code pair - simplified the
status-check conditionals accordingly and added 'authorized' as a second
success state (PaymentResult's own status union), which the legacy check
didn't have. The package also carries no TTL/expiry field on its response,
unlike the legacy provider's qrTTL - polling duration now falls back to
PAYMENT_MIN_POLL_SECONDS alone; flagged in a comment.
- api.service.ts's createPaymentIntent and its QrCreateResponse-based
resolvePaymentQrId/resolvePaymentQrUrl/resolvePaymentLink/
resolveBankPaymentUrl helpers are now dead code. Left in place rather than
deleted in the same pass that adds a new external dependency, so a revert
doesn't also need to resurrect deleted code.
Verified: production build succeeds, 247/247 unit tests, arch:check clean.
E2E: 2 of 7 tests currently fail
(checkout-request-shape.spec.ts, checkout-idempotent-click.spec.ts), and
this is disclosed honestly rather than hidden. Root cause, confirmed by
tracing real network requests: the customer-session cookie fake these tests
rely on stops working somewhere between the cookie being demonstrably
present in the browser (context.cookies(), and document.cookie read from a
plain page on the same origin) and Angular's own AuthService reading it -
the session-check request never fires at all. This reproduces with or
without this session's payment changes (checkout-idempotent-click.spec.ts
doesn't touch payment creation and fails the same way), so it is not a
regression introduced here, but it is unresolved. Tried switching
context.addCookies from {domain,path} to {url} form (the standard fix for
this class of Playwright cookie issue) - did not fix it, kept anyway as the
more correct form. Documented as a known, unresolved issue directly in both
spec files and e2e/README.md rather than deleting or silently marking the
tests skip - the request-shape assertions those tests make are still
correct, they are just currently unverifiable through this harness.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 09:37:16 +04:00
|
|
|
url: 'http://localhost:4200',
|
feat: server-authoritative checkout, no client-computed amount
F14-F16 of the frontend backlog. Contract: PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §5.2.
The highest-priority change in Phase 1: `POST /cart` sent `amount` computed
client-side (this.convertTotal(this.totalWithDelivery())) and the backend was
asked to trust it. Replaced with two calls:
1. POST /api/v2/storefront/checkout - offer ids + qty only. Returns
checkoutSessionId and the server-computed total.
2. POST /api/v2/storefront/payments/intents - references checkoutSessionId
only. Same response shape as before (qrId/qrUrl/bankUrl/qrTTL via the
existing resolvePaymentQrId/resolvePaymentLink/resolveBankPaymentUrl
helpers) - this replaces how the charged amount is determined, not the
QR/card provider polling flow, which Phase 1 does not redesign.
merchantReference (PARTNER-PROVISIONING-API-CONTRACT.md's RoutingContext
field) is sent on the payment intent, generated the same way the old orderId
was - our own correlation id, now with a name that matches what it is.
api.service.ts: CheckoutSessionRequest/Response and PaymentIntentRequest
types added, old CartPaymentRequest/createCartPayment left in place (Phase 7
reconciliation and any other caller may still reference the shape) but no
longer called from checkout.
offerId uses item.itemID: this codebase has no distinct Offer entity yet
(Phase 3, Product/Offer split, not shipped in this model) - itemID is the
same catalog identifier every other endpoint already keys off. Flagged in a
code comment for whoever ships Phase 3 to revisit.
Dead code removed as a consequence, not a separate pass: buildPaymentItems,
getPaymentUserId, getPaymentDescription (no other caller once the old
payload was gone), the ConfigService/TenantResolverService injects that
existed only for getPaymentDescription, and the now-orphaned
cart.paymentDescriptionFallback i18n key in all three locales.
Verification: cart.component.ts has no unit spec (no src/app/pages/cart/
*.spec.ts exists) - this session's E2E suite is the only coverage the
checkout request shape has. Added checkout-request-shape.spec.ts, scoped
narrowly to the request/response contract rather than a full add-to-cart
UI journey: seeds cart state directly into localStorage, fakes the customer
session via cookie + intercepted session-check, intercepts both new
endpoints and asserts on the captured request bodies. Confirms concretely:
no `amount` or `price` field ever leaves the client, offers carry the right
offerId/qty, and the payment intent correctly threads checkoutSessionId
through.
Verified: 5/5 E2E green, 115/115 unit tests green, arch:check clean,
production build succeeds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:14:19 +04:00
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Matches TelegramSessionApiService.normalizeWebSession's expected shape.
|
|
|
|
|
await page.route('**/users/sessions/**', route => {
|
|
|
|
|
route.fulfill({
|
|
|
|
|
status: 200,
|
|
|
|
|
contentType: 'application/json',
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
sessionId: FAKE_SESSION_ID,
|
|
|
|
|
status: 'active',
|
|
|
|
|
username: 'e2e_user',
|
|
|
|
|
userId: 1,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function mockFxQuoteEndpoint(page: Page): Promise<void> {
|
|
|
|
|
await page.route('**/api/v2/pricing/fx-quote**', route => {
|
|
|
|
|
route.fulfill({
|
|
|
|
|
status: 200,
|
|
|
|
|
contentType: 'application/json',
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
quoteId: 'fxq_e2e',
|
|
|
|
|
base: 'RUB',
|
|
|
|
|
quote: 'RUB',
|
|
|
|
|
rate: 1,
|
|
|
|
|
source: 'e2e-fixture',
|
|
|
|
|
observedAt: new Date().toISOString(),
|
|
|
|
|
expiresAt: new Date(Date.now() + 300_000).toISOString(),
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function interceptCheckoutSession(page: Page): Promise<Record<string, unknown>> {
|
|
|
|
|
return new Promise(resolve => {
|
|
|
|
|
page.route('**/api/v2/storefront/checkout', (route: Route) => {
|
|
|
|
|
const body = route.request().postDataJSON();
|
|
|
|
|
resolve(body);
|
|
|
|
|
route.fulfill({
|
|
|
|
|
status: 200,
|
|
|
|
|
contentType: 'application/json',
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
checkoutSessionId: 'chk_e2e_fixture',
|
|
|
|
|
lines: [{
|
|
|
|
|
offerId: String(FAKE_ITEM.itemID),
|
|
|
|
|
qty: FAKE_ITEM.quantity,
|
|
|
|
|
unitPrice: { amountMinor: FAKE_ITEM.price * 100, currency: 'RUB' },
|
|
|
|
|
lineTotal: { amountMinor: FAKE_ITEM.price * FAKE_ITEM.quantity * 100, currency: 'RUB' },
|
|
|
|
|
priceSnapshotId: 'snap_e2e',
|
|
|
|
|
}],
|
|
|
|
|
subtotal: { amountMinor: FAKE_ITEM.price * FAKE_ITEM.quantity * 100, currency: 'RUB' },
|
|
|
|
|
discount: { amountMinor: 0, currency: 'RUB' },
|
|
|
|
|
delivery: { amountMinor: 0, currency: 'RUB' },
|
|
|
|
|
total: { amountMinor: FAKE_ITEM.price * FAKE_ITEM.quantity * 100, currency: 'RUB' },
|
|
|
|
|
fxQuoteId: 'fxq_e2e',
|
|
|
|
|
expiresAt: new Date(Date.now() + 300_000).toISOString(),
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function interceptPaymentIntent(page: Page): Promise<Record<string, unknown>> {
|
|
|
|
|
return new Promise(resolve => {
|
feat: start @marketplaces/payment implementation
package.json already had @marketplaces/payment added (uncommitted) when this
started. Wired it in.
- app.config.ts: provideMarketplacesPayment(). apiUrl is environment.qrApiUrl
with its trailing /api stripped - found and fixed a real bug while wiring
this: qrApiUrl already ends in /api, and the package's default
paymentsPath is '/api/v1/payments', so passing qrApiUrl unchanged would
have silently doubled the path to .../api/api/v1/payments. Confirmed by
reading the package's baseUrl() concatenation directly, not guessed.
marketplaceDomain is a plain closure (not TenantResolverService) since
provideMarketplacesPayment runs outside the injector.
- cart.component.ts: createPaymentIntent() and startPolling() now go through
MARKETPLACES_PAYMENT_GATEWAY instead of api.service.ts's
createPaymentIntent/checkCartPaymentStatus/checkCartCardPaymentStatus (our
own earlier inferred contract, now superseded by the package's real,
published one - POST/GET {qrApiUrl}/api/v1/payments). Deliberately did NOT
swap to the package's own <mp-payment> UI component - that has a different
UX paradigm entirely (window.open for redirects instead of an iframe
popup, client-side QR generation instead of an external image service) and
replacing the existing, already-tested 769-line popup state machine
wholesale is a separate, much larger change than "wire the new package
in." Only the I/O layer moved; the surrounding state machine (paymentStatus,
checkoutInFlight, timeout/success/error handling, bank-iframe UX) is
untouched.
Response shape differs from the legacy provider: the package's
PaymentStatus is a fixed union (created/pending/authorized/paid/failed/
cancelled/expired), not a free-form string+code pair - simplified the
status-check conditionals accordingly and added 'authorized' as a second
success state (PaymentResult's own status union), which the legacy check
didn't have. The package also carries no TTL/expiry field on its response,
unlike the legacy provider's qrTTL - polling duration now falls back to
PAYMENT_MIN_POLL_SECONDS alone; flagged in a comment.
- api.service.ts's createPaymentIntent and its QrCreateResponse-based
resolvePaymentQrId/resolvePaymentQrUrl/resolvePaymentLink/
resolveBankPaymentUrl helpers are now dead code. Left in place rather than
deleted in the same pass that adds a new external dependency, so a revert
doesn't also need to resurrect deleted code.
Verified: production build succeeds, 247/247 unit tests, arch:check clean.
E2E: 2 of 7 tests currently fail
(checkout-request-shape.spec.ts, checkout-idempotent-click.spec.ts), and
this is disclosed honestly rather than hidden. Root cause, confirmed by
tracing real network requests: the customer-session cookie fake these tests
rely on stops working somewhere between the cookie being demonstrably
present in the browser (context.cookies(), and document.cookie read from a
plain page on the same origin) and Angular's own AuthService reading it -
the session-check request never fires at all. This reproduces with or
without this session's payment changes (checkout-idempotent-click.spec.ts
doesn't touch payment creation and fails the same way), so it is not a
regression introduced here, but it is unresolved. Tried switching
context.addCookies from {domain,path} to {url} form (the standard fix for
this class of Playwright cookie issue) - did not fix it, kept anyway as the
more correct form. Documented as a known, unresolved issue directly in both
spec files and e2e/README.md rather than deleting or silently marking the
tests skip - the request-shape assertions those tests make are still
correct, they are just currently unverifiable through this harness.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 09:37:16 +04:00
|
|
|
// @marketplaces/payment: apiUrl (qrApiUrl with its trailing /api
|
|
|
|
|
// stripped, see app.config.ts) + default paymentsPath '/api/v1/payments'.
|
|
|
|
|
page.route('**/api/v1/payments', (route: Route) => {
|
feat: server-authoritative checkout, no client-computed amount
F14-F16 of the frontend backlog. Contract: PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §5.2.
The highest-priority change in Phase 1: `POST /cart` sent `amount` computed
client-side (this.convertTotal(this.totalWithDelivery())) and the backend was
asked to trust it. Replaced with two calls:
1. POST /api/v2/storefront/checkout - offer ids + qty only. Returns
checkoutSessionId and the server-computed total.
2. POST /api/v2/storefront/payments/intents - references checkoutSessionId
only. Same response shape as before (qrId/qrUrl/bankUrl/qrTTL via the
existing resolvePaymentQrId/resolvePaymentLink/resolveBankPaymentUrl
helpers) - this replaces how the charged amount is determined, not the
QR/card provider polling flow, which Phase 1 does not redesign.
merchantReference (PARTNER-PROVISIONING-API-CONTRACT.md's RoutingContext
field) is sent on the payment intent, generated the same way the old orderId
was - our own correlation id, now with a name that matches what it is.
api.service.ts: CheckoutSessionRequest/Response and PaymentIntentRequest
types added, old CartPaymentRequest/createCartPayment left in place (Phase 7
reconciliation and any other caller may still reference the shape) but no
longer called from checkout.
offerId uses item.itemID: this codebase has no distinct Offer entity yet
(Phase 3, Product/Offer split, not shipped in this model) - itemID is the
same catalog identifier every other endpoint already keys off. Flagged in a
code comment for whoever ships Phase 3 to revisit.
Dead code removed as a consequence, not a separate pass: buildPaymentItems,
getPaymentUserId, getPaymentDescription (no other caller once the old
payload was gone), the ConfigService/TenantResolverService injects that
existed only for getPaymentDescription, and the now-orphaned
cart.paymentDescriptionFallback i18n key in all three locales.
Verification: cart.component.ts has no unit spec (no src/app/pages/cart/
*.spec.ts exists) - this session's E2E suite is the only coverage the
checkout request shape has. Added checkout-request-shape.spec.ts, scoped
narrowly to the request/response contract rather than a full add-to-cart
UI journey: seeds cart state directly into localStorage, fakes the customer
session via cookie + intercepted session-check, intercepts both new
endpoints and asserts on the captured request bodies. Confirms concretely:
no `amount` or `price` field ever leaves the client, offers carry the right
offerId/qty, and the payment intent correctly threads checkoutSessionId
through.
Verified: 5/5 E2E green, 115/115 unit tests green, arch:check clean,
production build succeeds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:14:19 +04:00
|
|
|
const body = route.request().postDataJSON();
|
|
|
|
|
resolve(body);
|
|
|
|
|
route.fulfill({
|
|
|
|
|
status: 200,
|
|
|
|
|
contentType: 'application/json',
|
|
|
|
|
body: JSON.stringify({
|
feat: start @marketplaces/payment implementation
package.json already had @marketplaces/payment added (uncommitted) when this
started. Wired it in.
- app.config.ts: provideMarketplacesPayment(). apiUrl is environment.qrApiUrl
with its trailing /api stripped - found and fixed a real bug while wiring
this: qrApiUrl already ends in /api, and the package's default
paymentsPath is '/api/v1/payments', so passing qrApiUrl unchanged would
have silently doubled the path to .../api/api/v1/payments. Confirmed by
reading the package's baseUrl() concatenation directly, not guessed.
marketplaceDomain is a plain closure (not TenantResolverService) since
provideMarketplacesPayment runs outside the injector.
- cart.component.ts: createPaymentIntent() and startPolling() now go through
MARKETPLACES_PAYMENT_GATEWAY instead of api.service.ts's
createPaymentIntent/checkCartPaymentStatus/checkCartCardPaymentStatus (our
own earlier inferred contract, now superseded by the package's real,
published one - POST/GET {qrApiUrl}/api/v1/payments). Deliberately did NOT
swap to the package's own <mp-payment> UI component - that has a different
UX paradigm entirely (window.open for redirects instead of an iframe
popup, client-side QR generation instead of an external image service) and
replacing the existing, already-tested 769-line popup state machine
wholesale is a separate, much larger change than "wire the new package
in." Only the I/O layer moved; the surrounding state machine (paymentStatus,
checkoutInFlight, timeout/success/error handling, bank-iframe UX) is
untouched.
Response shape differs from the legacy provider: the package's
PaymentStatus is a fixed union (created/pending/authorized/paid/failed/
cancelled/expired), not a free-form string+code pair - simplified the
status-check conditionals accordingly and added 'authorized' as a second
success state (PaymentResult's own status union), which the legacy check
didn't have. The package also carries no TTL/expiry field on its response,
unlike the legacy provider's qrTTL - polling duration now falls back to
PAYMENT_MIN_POLL_SECONDS alone; flagged in a comment.
- api.service.ts's createPaymentIntent and its QrCreateResponse-based
resolvePaymentQrId/resolvePaymentQrUrl/resolvePaymentLink/
resolveBankPaymentUrl helpers are now dead code. Left in place rather than
deleted in the same pass that adds a new external dependency, so a revert
doesn't also need to resurrect deleted code.
Verified: production build succeeds, 247/247 unit tests, arch:check clean.
E2E: 2 of 7 tests currently fail
(checkout-request-shape.spec.ts, checkout-idempotent-click.spec.ts), and
this is disclosed honestly rather than hidden. Root cause, confirmed by
tracing real network requests: the customer-session cookie fake these tests
rely on stops working somewhere between the cookie being demonstrably
present in the browser (context.cookies(), and document.cookie read from a
plain page on the same origin) and Angular's own AuthService reading it -
the session-check request never fires at all. This reproduces with or
without this session's payment changes (checkout-idempotent-click.spec.ts
doesn't touch payment creation and fails the same way), so it is not a
regression introduced here, but it is unresolved. Tried switching
context.addCookies from {domain,path} to {url} form (the standard fix for
this class of Playwright cookie issue) - did not fix it, kept anyway as the
more correct form. Documented as a known, unresolved issue directly in both
spec files and e2e/README.md rather than deleting or silently marking the
tests skip - the request-shape assertions those tests make are still
correct, they are just currently unverifiable through this harness.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 09:37:16 +04:00
|
|
|
paymentId: 'qr_e2e_fixture',
|
|
|
|
|
method: 'qr',
|
|
|
|
|
status: 'pending',
|
|
|
|
|
action: { type: 'qr', url: 'https://example.com/pay/e2e' },
|
feat: server-authoritative checkout, no client-computed amount
F14-F16 of the frontend backlog. Contract: PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §5.2.
The highest-priority change in Phase 1: `POST /cart` sent `amount` computed
client-side (this.convertTotal(this.totalWithDelivery())) and the backend was
asked to trust it. Replaced with two calls:
1. POST /api/v2/storefront/checkout - offer ids + qty only. Returns
checkoutSessionId and the server-computed total.
2. POST /api/v2/storefront/payments/intents - references checkoutSessionId
only. Same response shape as before (qrId/qrUrl/bankUrl/qrTTL via the
existing resolvePaymentQrId/resolvePaymentLink/resolveBankPaymentUrl
helpers) - this replaces how the charged amount is determined, not the
QR/card provider polling flow, which Phase 1 does not redesign.
merchantReference (PARTNER-PROVISIONING-API-CONTRACT.md's RoutingContext
field) is sent on the payment intent, generated the same way the old orderId
was - our own correlation id, now with a name that matches what it is.
api.service.ts: CheckoutSessionRequest/Response and PaymentIntentRequest
types added, old CartPaymentRequest/createCartPayment left in place (Phase 7
reconciliation and any other caller may still reference the shape) but no
longer called from checkout.
offerId uses item.itemID: this codebase has no distinct Offer entity yet
(Phase 3, Product/Offer split, not shipped in this model) - itemID is the
same catalog identifier every other endpoint already keys off. Flagged in a
code comment for whoever ships Phase 3 to revisit.
Dead code removed as a consequence, not a separate pass: buildPaymentItems,
getPaymentUserId, getPaymentDescription (no other caller once the old
payload was gone), the ConfigService/TenantResolverService injects that
existed only for getPaymentDescription, and the now-orphaned
cart.paymentDescriptionFallback i18n key in all three locales.
Verification: cart.component.ts has no unit spec (no src/app/pages/cart/
*.spec.ts exists) - this session's E2E suite is the only coverage the
checkout request shape has. Added checkout-request-shape.spec.ts, scoped
narrowly to the request/response contract rather than a full add-to-cart
UI journey: seeds cart state directly into localStorage, fakes the customer
session via cookie + intercepted session-check, intercepts both new
endpoints and asserts on the captured request bodies. Confirms concretely:
no `amount` or `price` field ever leaves the client, offers carry the right
offerId/qty, and the payment intent correctly threads checkoutSessionId
through.
Verified: 5/5 E2E green, 115/115 unit tests green, arch:check clean,
production build succeeds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:14:19 +04:00
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function acceptTermsAndCheckout(page: Page): Promise<void> {
|
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
|
|
|
|
|
|
// #terms-checkbox is a custom-styled input (zero-size native element, a
|
|
|
|
|
// <label> renders the visible box) - .check() refuses on geometry even
|
|
|
|
|
// with force:true, so toggle it via its label the way a real user would.
|
|
|
|
|
const termsCheckbox = page.locator('#terms-checkbox');
|
|
|
|
|
if (await termsCheckbox.count() > 0) {
|
|
|
|
|
const label = page.locator('label[for="terms-checkbox"]');
|
|
|
|
|
if (await label.count() > 0) {
|
|
|
|
|
await label.click();
|
|
|
|
|
} else {
|
|
|
|
|
await termsCheckbox.dispatchEvent('click');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const qrButton = page.getByRole('button', { name: /qr/i }).first();
|
|
|
|
|
await qrButton.click();
|
|
|
|
|
}
|