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>
This commit is contained in:
@@ -28,6 +28,9 @@ BASE_URL=https://staging.example.com npm run e2e
|
||||
|---|---|
|
||||
| `currency-switch.spec.ts` | `160 RUB` must not silently become `160 USD` on a currency switch — Track Q Q4, and the regression guard `docs/backend/PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md` §5 exists to close. Written **before** the checkout money-truth rewrite (F10–F16 in the frontend backlog), specifically so that rewrite has a net under it. |
|
||||
| `smoke.spec.ts` | App boots, storefront renders, no console errors on first paint. |
|
||||
| `admin-dev-bypass.spec.ts` | `?devBypassAdmin=true` actually reaches the admin shell without a Telegram login (Track Q F59). |
|
||||
| `checkout-request-shape.spec.ts` | ⚠️ **Currently failing, known issue, not resolved (2026-08-21).** The checkout request-shape assertions are correct on paper; the customer-session fake this test relies on doesn't work right now for a reason not yet found — see the `fakeCustomerSession` comment in the file. Do not trust a green *or* red run of this specific test as a verdict on checkout correctness until it's root-caused. |
|
||||
| `checkout-idempotent-click.spec.ts` | ⚠️ Same known issue as above (Track Q F62) - fails the same way, for the same unresolved reason. |
|
||||
|
||||
## Adding a test
|
||||
|
||||
|
||||
@@ -20,7 +20,10 @@ test('double-clicking checkout sends exactly one checkout-session request', asyn
|
||||
window.localStorage.setItem('marketplace_cart', JSON.stringify([item]));
|
||||
}, FAKE_ITEM);
|
||||
|
||||
await context.addCookies([{ name: 'webSessionID', value: 'e2e-fake-session', domain: 'localhost', path: '/' }]);
|
||||
// KNOWN ISSUE, NOT RESOLVED (2026-08-21) - see checkout-request-shape.spec.ts's
|
||||
// fakeCustomerSession comment. This test currently fails the same way:
|
||||
// the session check never fires despite the cookie being present.
|
||||
await context.addCookies([{ name: 'webSessionID', value: 'e2e-fake-session', url: 'http://localhost:4200' }]);
|
||||
await page.route('**/users/sessions/**', route =>
|
||||
route.fulfill({
|
||||
status: 200, contentType: 'application/json',
|
||||
|
||||
@@ -62,10 +62,15 @@ test.describe('checkout request shape', () => {
|
||||
|
||||
const body = await intentRequest;
|
||||
|
||||
// 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.
|
||||
expect(body.checkoutSessionId, 'must reference the session created in step 1').toBe('chk_e2e_fixture');
|
||||
expect(body).not.toHaveProperty('amount');
|
||||
expect(typeof body.merchantReference).toBe('string');
|
||||
expect(body.merchantReference.length).toBeGreaterThan(0);
|
||||
const metadata = body.metadata as Record<string, string> | undefined;
|
||||
expect(typeof metadata?.merchantReference).toBe('string');
|
||||
expect((metadata?.merchantReference ?? '').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,12 +81,24 @@ async function seedCart(page: Page): Promise<void> {
|
||||
}
|
||||
|
||||
async function fakeCustomerSession(page: Page, context: import('@playwright/test').BrowserContext): Promise<void> {
|
||||
// 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.
|
||||
await context.addCookies([
|
||||
{
|
||||
name: 'webSessionID',
|
||||
value: FAKE_SESSION_ID,
|
||||
domain: 'localhost',
|
||||
path: '/',
|
||||
url: 'http://localhost:4200',
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -149,16 +166,19 @@ function interceptCheckoutSession(page: Page): Promise<Record<string, unknown>>
|
||||
|
||||
function interceptPaymentIntent(page: Page): Promise<Record<string, unknown>> {
|
||||
return new Promise(resolve => {
|
||||
page.route('**/api/v2/storefront/payments/intents', (route: Route) => {
|
||||
// @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) => {
|
||||
const body = route.request().postDataJSON();
|
||||
resolve(body);
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
qrId: 'qr_e2e_fixture',
|
||||
nspkurl: 'https://example.com/pay/e2e',
|
||||
qrTTL: 5,
|
||||
paymentId: 'qr_e2e_fixture',
|
||||
method: 'qr',
|
||||
status: 'pending',
|
||||
action: { type: 'qr', url: 'https://example.com/pay/e2e' },
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user