Collapses the entire docs/backend/ set - Phase 1-10, Track A/S, the
partner API, the two handoffs, the frontend surface inventory, and the
harvest requirements - into a single source of truth,
docs/backend/BACKEND-INTEGRATION.md.
Every contract's entities, endpoints, and invariants are preserved,
reorganised by domain rather than by sprint. The nine release
invariants, the FH-* harvest mechanisms, the RBAC/audit/secrets
cross-cutting rules, the tenant-routing infra contract, the 15
acceptance tests, build order, dev setup, and open decisions are all in
the one file, with a change log (§14) at the bottom.
The file opens with the maintenance rule: any new backend need, contract
change, or shipped item updates this file in the same change - the
affected section and the change log. No new backend .md files.
Inbound links from BACKEND-API-REFERENCE, the ADRs, the fork docs,
DEPLOYMENT, PACKAGES-USAGE, the delivery plan, and e2e/README are
repointed at the single doc (section anchors collapse to the file; the
prose section refs remain as context). Also recorded the rule in the
repo CLAUDE.md.
17 backend docs removed, 1 added. No implementation changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Reconcile tenant API domains step ran on every push to main. On the
production host that is actively harmful: api.gorbushka.market already has
a hand-written vhost, and configure-api-domain.sh writes its own file per
domain - so the step would hand nginx a second server block for a
server_name that already has one and re-run certbot against a live API,
once per deploy. Shipping frontend files needs none of it. Gate it behind
a workflow_dispatch input, off by default, for standing up a NEW base
domain.
This also shrinks the secrets a normal deploy requires to four
(DEPLOY_HOST, DEPLOY_USER, DEPLOY_SSH_KEY, DEPLOY_KNOWN_HOSTS);
STOREFRONT_DOMAINS, CERTBOT_EMAIL and BACKEND_UPSTREAM are now read only
on the opt-in path.
Document the production host as it actually is: provisioned by hand before
server-setup.sh existed, per-domain vhosts rooted at
/var/www/dexarmarket/browser, which is now a symlink to
/srv/marketplaces/current/frontend. Before 2026-08-22 it pointed straight
at a pinned release with no `current` in between, so releases 14d46ce and
98c39f6 uploaded successfully and were never served.
HARVEST-BACKEND-REQUIREMENTS.md - single index the backend builds the
fork harvest from, so the FH-* mechanisms are not scattered across nine
phase contracts:
- the nine release invariants (the acceptance gate)
- every FH-* requirement with its exact mechanism and phase-contract
reference, grouped by area (correctness core, sessions/access,
tenancy/publish/content, identity, operations)
- 15 acceptance tests mapped to the invariant each guards
- build order, and what the frontend already delivered so the backend
builds to a known target rather than guessing
Linked from the backend README index.
No implementation changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
checkout-request-shape.spec.ts and checkout-idempotent-click.spec.ts were
flagged known-failing pending investigation; dda0a3d found and fixed the
actual cause (circular DI in apiHeadersInterceptor). Update the comments
and README so they no longer point at an unresolved mystery.
apiHeadersInterceptor injected @marketplaces/auth's AuthService to attach
a WebSessionID header. AuthService's own constructor makes a synchronous
GET /users/sessions/:id call to verify a persisted session, which runs
through this exact interceptor - Angular throws NG0200 (circular
dependency) mid-construction, silently swallowed by the package's
catchError(() => of(null)), read as "session invalid," and the cookie
gets cleared on every single page load. This is what was gating the
architecture-governance e2e job on Gitea (3 checkout tests failing on a
disabled QR button). Session-check requests are the identity mechanism
itself and never needed that header - skip AuthService injection for
them instead.
Also fixes mock-data.interceptor's session-check mock, which required
3 polls before reporting an id active with no way to represent a
returning session with an already-valid cookie - not the actual trigger
for this bug (useMockData is false in the dev config CI uses), but a
real gap in the mock's fidelity worth closing while in this file.
Audited all 21 localStorage users against the "localStorage as source of
truth" objection. It was already false almost everywhere:
- Every admin facade (products, categories, orders, moderation,
dashboard) uses localStorage only for view preferences - viewMode,
density, visibleColumns, expandedIds, sort. Entity CRUD goes through
the API gateways.
- currency-rates.service already removed its localStorage-typed rates.
- language, location region, search history, the anonymous session id,
admin preferences - all legitimate cache/preference.
- The editor already shows an "unsaved local draft restored" banner
(draftRestored -> save bar), which is the recovery-cache indicator this
item called for.
One real gap remains and it is backend-blocked: project-editor
publish() applies config to the in-memory runtime and saves the draft to
localStorage, then declares itself published - no server round-trip,
because the PHASE-9 §5 revision API does not exist yet. Marked precisely
in publish() with the required behaviour (await the server, only then
mark published) and cross-referenced to the contract.
Nothing to rip out: the codebase was already at the target state
everywhere the backend exists to support it.
262 tests pass. Boundaries green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FH-4.7 - AccountIdentitiesComponent under
features/website/account/identities/. Lists linked identities from
GET /me/identities, offers attach buttons only for OAuth providers not
already linked (reusing SocialLoginButtonComponent), detaches through
unlink(). Refuses to detach the last remaining identity - it is the only
way back in - with the control disabled and an explanatory title, matching
the backend's last-identity 409. Loading / error / ready states; a load
failure surfaces an error rather than rendering an empty account, and a
slot carries the identity-conflict message from PHASE-8 §2.3. 6 unit tests.
Not wired into a route: the storefront has no customer account area yet
and no live OAuth application to authorize against (FH-0.1). This is the
surface both depend on, buildable and tested now.
FH-4.6 (client + contract) - the gateway now separates the two provider
sets. SocialProvider (vk | yandex) is what has an OAuth authorize
redirect; ExternalIdentityProvider (adds telegram | max) is what can be
listed and unlinked. unlink() widened to the latter so Telegram detaches
through the same path as VK, with no second code path. The dev local
gateway seeds a Telegram identity so the linking screen is exercisable
before any real provider exists.
PHASE-8 §2.6 specifies the backend migration: a Telegram login writes an
ExternalIdentity row under the same uniqueness and identity-conflict rule
as VK, appears in /me/identities, is removable subject to the
last-identity 409, and keeps customer (marketplace_session) and admin
(bo_session) sessions as distinct cookies - closing the shared
customer/admin Telegram session the audit flagged. The identity row and
the messaging BotConversationBinding stay separate records.
FH-4.8 - PHASE-8 §3 now states email/phone OTP's position explicitly:
recovery when a linked messenger is unreachable and an addable second
factor, never the primary login, and one more identity on the same
customer rather than a parallel account.
262 tests pass. Build green, boundaries and cycles green, bundle scan clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Built with --stats-json and read the esbuild metafile instead of guessing.
@angular/compiler was 495,831 bytes of a 1.5 MB main chunk - the JIT
compiler, in an AOT production build, 33% of everything an anonymous
visitor downloads.
src/main.ts imported it deliberately, with a comment explaining that
@marketplaces/auth shipped plain tsc output carrying no Ivy metadata, so
Angular JIT-compiled its classes at runtime and bootstrap threw without
it.
That comment was stale. The package is 0.2.0, built with ng-packagr,
module: dist/fesm2022/marketplaces-auth.mjs - proper Angular Package
Format, partial-compiled (ɵɵngDeclareInjectable), linked at consumer
build time. Nothing needs JIT.
Verified against the production bundle served statically, not just a
green build: the failure mode this guarded was a runtime throw, so a
successful compile proves nothing. Angular 22.0.8 bootstrapped, the
router resolved /ru, and the app rendered its own "server unavailable"
screen - meaning DI, HttpClient and the full interceptor chain ran. That
chain injects AuthService from @marketplaces/auth, the exact class the
old comment named. Zero JIT or compiler errors; the only console output
was the expected 404s from having no backend behind a static server.
Initial bundle 1.55 MB -> 1.04 MB raw, 323.58 kB -> 215.45 kB transfer.
Budget ratchet lowered 1.6MB -> 1.1MB, which is now also what stops the
import being re-added.
Next lever, deliberately not taken here: i18n/ru.ts is 290 kB, eager,
while en/hy are already lazy. TranslateService has the loader plumbing
and languageGuard already awaits a preload, so it is mechanically small -
but it adds a round-trip before first paint for the majority language,
which is a product tradeoff, not a cleanup. Roughly a 750 kB bundle once
someone decides.
256 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
21 DI tokens selected their implementation like this:
factory: () => (environment.useMockData ? inject(XLocal) : inject(XApi))
That reads as a toggle and is not one. Naming both classes in the factory
keeps both reachable, so every mock shipped regardless of the flag - and
`useMockData` is false in both environment files, so none of them were
ever the selected implementation in the first place. Verified: a fixture
string from partner-hierarchy-local.gateway.ts was present in a
production bundle.
Token factories now inject the API gateway unconditionally. Mock
overrides move to src/app/mock-gateway.providers.ts, swapped for a
production copy that imports nothing, via the same fileReplacements
mechanism mock-data.interceptor.production.ts already uses. Dev behaviour
is unchanged - flip useMockData in environment.ts exactly as before.
useExisting rather than useClass: the local gateways are already
providedIn: 'root' singletons, and an app-level provider for the token
wins over its tree-shakable default.
scan-bundle.sh gains two patterns so this cannot come back: any
*LocalGateway class name, and known fixture literals. Verified in both
directions - clean against the real dist, exit 1 against a planted
OfferLocalGateway.
Result: zero LocalGateway classes and zero fixtures in the production
bundle, down from 21 classes and 75 kB of source. Initial bundle is
unchanged at 1.55 MB because these all sat in lazy chunks; the win is
that production can no longer serve seeded fixtures as real data, not
bytes off the critical path.
Not addressed here: MediaRepository is still bound to MockMediaRepository
unconditionally in app.config.ts. That one cannot be deleted - no real
implementation exists yet - so it is a missing API gateway, not dead
weight. Tracked separately.
256 tests pass. Build green, boundaries and cycles green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
server-setup.sh configured ufw and stopped there, which leaves SSH open
to unlimited password guessing and the kernel on defaults that are wrong
for an internet-facing host.
Adds three drop-in files, so a re-run replaces its own config and never
edits a distro file in place:
/etc/ssh/sshd_config.d/10-marketplaces-hardening.conf
password and keyboard-interactive auth off, root key-only,
no agent/X11 forwarding, MaxAuthTries 3, 30s login grace
/etc/fail2ban/jail.d/marketplaces.local
sshd, nginx-http-auth, nginx-bad-request; 5 in 10m, 1h ban
/etc/sysctl.d/99-marketplaces-hardening.conf
no redirects or source routing, rp_filter, SYN cookies,
forwarding off, restricted kernel pointers and dmesg
Both accounts on the host are key-only by construction - the deploy user
is created with no password at all - so disabling password auth cannot
lock anyone out. It only closes guessing against a credential nobody
intended to exist.
The sshd block runs `sshd -t` first and removes its own drop-in if the
test fails. A bad sshd config that takes effect on a remote box is how
people lock themselves out permanently.
DEPLOYMENT.md §3.2 documents all three plus the post-provision checks.
Not copied from the reference implementation: its hardcoded server IP.
Kept as-is because ours is already better: add-domain.sh pre-checks the
DNS A record and runs nginx -t before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The VK-only scaffolding had a shape problem worth fixing before anything
was built on it: completeCallback(code, codeVerifier) took the PKCE
verifier from the client, which forces the browser to generate and hold
it. We are a confidential client - a browser-held verifier buys nothing
and adds a place to steal it from.
Replaces the four vk-id-* files with a provider-agnostic surface:
getAuthorizeUrl(provider, returnTo?)
listIdentities()
unlink(provider)
completeCallback is gone entirely. The backend mints and stores state and
code_verifier single-use for 10 minutes, handles the provider's callback
itself, issues the session cookie and redirects. VK and Yandex differ
only in a path segment, because everything that actually differs between
them - PKCE handling, VK's device_id, Yandex's Basic-auth exchange -
lives backend-side.
vk-id-login becomes social-login-button with a provider input; adding
Yandex to the UI is an input value, not new code. Adds yandex_id to
ExternalIdentityProvider, plus optional email/phone/displayName since VK
frequently returns no email.
social-identity-gateway.spec.ts (5 tests) asserts the requests carry no
code_verifier and no client_secret, so reintroducing a browser-held
verifier fails the build rather than passing review.
PHASE-8 §2 rewritten to match: the four endpoints, backend-owned state
and verifier, UNIQUE (provider, providerUserId) with conflict routed to
controlled resolution rather than a silent rebind, per-tenant OAuth app
config under the Track S §4.2 envelope, and both providers' full endpoint
sets. Two things recorded there because they are expensive to discover
later: VK's callback returns device_id alongside code and the token
exchange fails without it, and both providers validate redirect_uri
against an exact registered list - which a multi-tenant platform cannot
satisfy without a central identity host (FH-0.1, still undecided).
256 tests pass. Build green, boundaries and cycles green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Writes the 14 harvested mechanisms from FORK-ANALYSIS-2026-08-21.md into
the backend contracts. Each section is dated 2026-08-21 and tagged FH-*
so any wording traces back to why it is worded that way.
The through-line: several contracts stated correctness as behaviour
("the webhook must be idempotent"). Behaviour written as an if-statement
gets deleted by a refactor and the failure mode is a double charge. These
sections restate it as schema and mechanism.
PHASE-3 3.1 conditional-write reservation, 409 on zero rows, cart-wide
rollback, 15 min TTL
3.2 InventoryMovement append-only journal with resultingAvailable
6 bulk import idempotent by SKU, rollback while unsold
6a digital code pools, revealed only when paid
PHASE-7 5 unique constraints for payment idempotency and webhook
replay, insert-first handling, signature over raw body,
24h poll as reconciliation not primary
TRACK-S 2.1 session model - 32 bytes stored as SHA-256 only, HttpOnly,
one cookie per contour, Argon2id params, mandatory TOTP
2.2 origin allowlist ahead of routing on every cookie mutation
4.2 AES-256-GCM envelope for stored secrets, HMAC fingerprints
8a order manager as a separate contour, scoped by membership
rows rather than by configuration
PHASE-9 5.1 revision immutability, version = max+1, pointer flipped
in-transaction, operational state does not travel
5.2 clone carry / no-carry list, inventory to zero
5.3 signed read-only preview, non-GET 404s while previewing
6 host normalization, verifiedAt required, cache invalidation
PHASE-10 3a server re-runs the editor's validation, clamp-and-fallback
PHASE-2 3.1 order publicToken, snapshot completeness, never updated
FH-2.12 rejected on the merits: our marketplace lifecycle state machine
is richer than theirs, adopting it would be a downgrade. Recorded in the
TODO so it is not raised again.
Also adds BACKEND-HANDOFF.md sections 0 and 0a - nine falsifiable
invariants as a release gate, each cross-referenced to the contract that
specifies it, plus PR and release discipline. And ADR-0006 recording what
we take, what we reject, what we keep because ours is better, and the
organizational question it deliberately does not settle.
No implementation changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bundle budget was warning-only: initial warning 700 kB, error 1.8 MB.
Measured today the initial bundle is 1.55 MB raw / 324.58 kB transfer -
up from the 1.15 MB measured on 11 August, so it had been growing with
nothing to stop it.
Lowers maximumError to 1.6 MB. That is a ratchet, not a target: just
above today's size so the bundle cannot grow, with the 700 kB warning
left in place as the goal. Lower it each time the number comes down.
Adds scripts/ci/scan-bundle.sh (npm run scan:bundle), run in CI after
the build. Seven patterns: both provider auth headers, the partner ID
shape, client_secret, private key blocks, AWS keys, Telegram bot
tokens. The legacy payment code that put credentials in the browser is
already deleted; this is what stops it coming back. Verified in both
directions - clean against the real dist, exit 1 against a planted
credential.
Measurement also corrected two assumptions recorded in the harvest
TODO: admin and editor code is already lazy-loaded, so the initial
bundle is main alone rather than a deployable-split problem; and mock
gateway fixtures do reach production chunks, which is now filed as
FH-E.6 with the cause identified.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
detectLocation() fetched http://ip-api.com over plaintext from an HTTPS
storefront. Browsers block mixed active content, so the request never
completed and auto-detect only ever took its error branch - region
detection has been dead in production, not merely insecure. The attempt
also handed every visitor's IP to a third party from the page itself.
Geo now resolves through the tenant API at {baseUrl}/geo/resolve, the
same base /regions already uses. The server reads the client IP; the
browser sends nothing and receives no third-party payload.
The endpoint is specified in BACKEND-API-REFERENCE.md and is not built
yet. Until it ships the client falls back to the manual region picker -
identical to the behaviour production already had.
Adds location.service.spec.ts: geo goes to the tenant API, no request
leaves that origin or uses http://, failure degrades to the manual
picker, and detection is not retried once attempted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verified zero remaining callers for each before deleting (grepped
src/app for every method name individually), not assumed from the
earlier commit's dead-code note.
Deleted from api.service.ts:
- createPayment() - legacy direct QR creation (POST {qrBaseUrl}/qr)
- createCartPayment() - legacy /cart payment creation, client-sent amount
- createPaymentIntent() - superseded by @marketplaces/payment's gateway
- checkCartPaymentStatus(), checkCartCardPaymentStatus(), checkPaymentStatus()
- legacy QR/card status polls, superseded by the same gateway
- resolvePaymentQrId/resolvePaymentQrUrl/resolvePaymentLink/
resolveBankPaymentUrl - QrCreateResponse field-normalization helpers,
no longer had a caller once the methods above were gone
- Types: QrCreateRequest, QrCreateResponse, CartPaymentRequest,
PaymentIntentRequest, QrDynamicStatusResponse
- Fields: qrBaseUrl, cartPaymentPartnerId - no longer read by anything
- Imports: HttpHeaders, environment - no longer used in this file
Did NOT touch createOrder() or createCheckoutSession() - both still have
live callers in cart.component.ts, confirmed before deciding what to keep.
cart.component.ts: corrected the createPaymentIntent() comment, which
referenced the deleted method/helper names, to name what actually got
deleted instead of what was merely "dead as of that commit."
docs/backend/FRONTEND-API-SURFACE-COMPLETE.md: moved the 4 now-deleted
QR/card endpoints out of the "still live" legacy table into a dated removal
note - the doc's own premise is "every endpoint this codebase currently
calls," so it was wrong to leave them listed as called once they weren't.
Legacy-undocumented count corrected 15->11, total 97->93.
Verified: production build succeeds (no dangling references), 247/247 unit
tests, arch:check clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Deep analysis of marketplaces-main.zip (hub.numus.cc/numus/marketplaces).
Findings:
- Not a fork of us. Separate platform monorepo (NestJS + Postgres +
2 Angular apps + infra) sharing an older dexarmarket ancestor.
- Our repo is vendored inside it as reference/parallel-frontend/,
SHA-256 pinned, dated 11 Aug 2026, classified "reference, not production".
- Zero VK/Yandex/OAuth code anywhere in their source. Their only
customer login is Telegram, proxied to an external service.
- They lead on backend truth and ops; we lead on frontend depth,
tests, e2e, and framework currency.
Three of their audit findings are still live in our code and are
defects, not just posture: plaintext ip-api.com call from an HTTPS
origin (mixed content, region detect silently dead), unvalidated
bypassSecurityTrustResourceUrl on a bank URL rendered in an iframe,
and provider credentials plus a partner ID literal in the bundle.
Adds:
- docs/FORK-ANALYSIS-2026-08-21.md - full comparison, their audit of
us assessed line by line, and a VK ID + Yandex ID design.
- docs/superpowers/specs/2026-08-21-fork-harvest-design.md - working
brief, five lanes, four waves, scope and rejection rules.
- docs/FORK-HARVEST-TODO.md - 42 items with effort, dependencies and
acceptance criteria. Improvements only; nothing regresses our
Angular version, test count, or architecture governance.
No implementation changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Direct question caught a real gap: "everything is there? payment auth?"
The census only grepped src/app/ - auth moved into the external
@marketplaces/auth package this session, and its HTTP calls were never
captured. Seven real endpoints were silently absent from a doc that called
itself "complete":
- 3 Telegram QR/session endpoints (POST/GET/DELETE .../users/sessions) -
live today, customer and admin login share them, which is exactly why
every admin endpoint must independently verify authorization server-side
- 4 ed25519 admin challenge/response endpoints - specified in
BACKEND-HANDOFF.md §3 and the package's own auth-api.model.ts, but not
built server-side. Client shows backend-unavailable until they exist.
Added §0.3 stating plainly what actually connects auth to payment: there is
no separate payment login. Checkout, order pricing (§20), and partner
credentials (§16) each ride on whichever of the two sessions above is
active, or on the partner API's own separate signed-request auth (§6 of
that contract - unrelated to Telegram/ed25519, already built, not a gap).
The real payment gap is §1 (QR/card creation and polling, undocumented
anywhere), not auth.
Counts corrected: 51->54 specified, 90->97 total. Added item 0 to the
action list, ahead of everything else: the ed25519 endpoints are the single
most serious open issue named anywhere in docs/backend/, and every other
item on the list assumes a working admin session to authorize against.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Updates FRONTEND-API-SURFACE-COMPLETE.md to cover everything built after it
was first generated - this is the final version for this pass, not a
snapshot mid-way through.
Added, none of which existed in the doc before:
- §18: the 4 marketplace revision endpoints (create draft/validate/
publish/rollback) - built this session, missing from the census entirely.
Flags the same draft/validated/preview/published ambiguity already
documented in the model itself, so backend sees it without having to
read source.
- §19: RoutingContext as an optional field addition on GET
/api/admin/v2/orders/{id} - not a new endpoint, a response-shape ask.
- §20: order pricing-breakdown fields (unitPriceMinor, lineTotalMinor,
priceSnapshotId, discountMinor, fxQuoteId, deliveryMinor) feeding the new
total-formula panel. States plainly that the panel shows nothing rather
than a wrong number while these are absent.
- §21: 8 optional dashboard-metrics fields (GMV, conversion, moderation
queue, etc.) - explicitly labelled a genuine ask, not a confirmed
contract, since no spec exists for this endpoint at all (§15).
- §22: states directly that the frontend's new double-click guard does not
replace backend idempotency enforcement and was never meant to - closes
one UI race, does nothing for a retried request or a duplicate webhook.
Counts updated (47->51 specified, 86->90 total; +3 response-shape asks that
aren't new endpoints). Closing section states the one item genuinely
blocked pending a live backend (F60, the full acceptance-path E2E) so
nobody mistakes "frontend backlog complete" for "nothing left to build."
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Delivery plan Q9: "set a justified coverage floor and CI gate. Deliberately
unset today." Both parts of that were still true - karma.conf.js had no
check thresholds, and architecture-governance.yml built and checked
boundaries but never invoked `ng test` at all.
Floor set 5 points below the measured level right after this session's
facade-test pass (43.3%/29.0%/34.1%/43.7% statements/branches/functions/
lines): 40/25/30/40. A deliberate floor, not an aspiration - meant to be
ratcheted up as coverage grows, and to fail a PR that drops below it
rather than silently accept a lower number.
Verified the gate actually fails, not just logs a warning: set
statements to an impossible 99% locally, confirmed `npm run test:coverage`
exits 1 (my first attempt at this check was wrong - piping through `tail`
meant the $? I read back was tail's exit code, not npm's; fixed by
capturing it directly). Restored the real floor and confirmed a clean
exit 0 before committing.
CI changes: added the Setup Chrome + coverage-gated test step
karma.conf.js's CHROME_BIN needs (its fallback is a Windows path, useless
on ubuntu-latest), plus an E2E step. Structurally verified locally (no
tabs, step count, manual read-through) - a real GitHub Actions run on
Anthropic's infrastructure could not be executed from this session, so
this is not confirmed end-to-end the way the coverage gate itself was.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
E2E found a real, pre-existing bug, not a test artifact: isCheckoutDisabled
only checked terms/auth/delivery-selection, never whether a checkout was
already in flight. A double-click (or any rapid repeat click) fired two
handler calls before showPaymentPopup's change detection had a chance to
cover the button, producing two separate POST /api/v2/storefront/checkout
requests for one click.
Fixed with checkoutInFlight, set synchronously at the top of checkout()
before anything async happens, checked in isCheckoutDisabled. Released in
both closePaymentPopup() (every retry/close path routes through it) and
setPaymentError() directly, since the popup can stay open to show an error
rather than closing - relying on only one of those would leave a failed
attempt unable to retry.
Track Q coverage (F59, F62):
- admin-dev-bypass.spec.ts - proves ?devBypassAdmin=true (already shipped
in app.ts, gated by @marketplaces/auth's isDevMode() check at runtime)
actually gets an E2E run into the admin shell without a Telegram login.
This was the missing piece behind Q2's note that past "verified live"
admin claims were code-inspection only.
- checkout-idempotent-click.spec.ts - the frontend-testable half of Q5
("repeat webhook and double-click create exactly one order"). The
webhook-idempotency half is a backend contract
(PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §6.3) this suite can't exercise
without a live backend.
One own test bug fixed en route, not shipped: the idempotency test's first
draft waited on label[for="terms-checkbox"], which does not exist in the
markup (the checkbox and its text share a plain clickable wrapper, no
label/for). checkout-request-shape.spec.ts already had the correct fallback
(dispatchEvent('click') on the input directly) for exactly this reason -
this test just hadn't copied it.
Verified: 237/237 unit tests, arch:check clean, 7/7 E2E, production build
succeeds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes out F63 - every domain named in the delivery plan's Q7 gap now has
facade-level coverage.
admin-users.facade.spec.ts (14 tests): sessions/audit dialog targeting (a
revokeSession must reload the CURRENTLY open user's sessions, not whichever
user happened to be open first), invite() trims and rejects whitespace-only
email, roleName() falls back to the raw id rather than rendering blank for
an unmapped role.
cart.service.spec.ts (23 tests) - this had zero coverage despite feeding
totalWithDelivery and allRequiredDeliveriesSelected directly into
cart.component.ts's checkout gate and the offers/qty payload sent to
POST /api/v2/storefront/checkout, the exact contract this session's F14-F16
rewired. A regression here would silently let checkout proceed with a
missing delivery selection, or compute the wrong total. Covers the delivery-
requirement matrix explicitly (digital items never require selection,
deliverySelectionRequired: false overrides having options present, a
selection satisfies the requirement once made) and the quantity-to-zero ->
line-removal behavior in updateQuantity.
Verified: 237/237 unit tests (37 new since the last commit), arch:check
clean, 5/5 E2E, production build succeeds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>