Removes all tracked repo documentation (root status docs, docs/,
docs/architecture/foundation/**, docs/archive/**, docs/context/BACKEND-AUDIT.md
+ adrs, src/assets/mock/README.md) and replaces it with:
- GAPS-AND-IMPROVEMENTS.md — role-based findings (user, PO, QA, backend,
accessibility, engineering) plus automated code-review passes over the
storefront and backoffice, each with file:line references. Findings only,
no fixes applied.
- BACKEND-API-REFERENCE.md — single consolidated backend contract: auth
(both mechanisms), bootstrap, pagination/sorting/filtering conventions,
error model, every live/mock-only endpoint with JSON examples, and the
admin-domain DI-token seam gaps.
Open items and unresolved decisions from the deleted docs (KNOWN-ISSUES,
PRODUCT_BACKLOG, SPRINT-PLAN-NEXT, Seller-Management audits, etc.) were
harvested into the two new files before deletion, not lost.
CLAUDE.md/AGENTS.md/GEMINI.md/.claude/ and docs/context/{INDEX,LOG,
MAINTENANCE,README}.md are untouched — confirmed gitignored, never part of
git history, outside this cleanup's scope.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
38 KiB
Backend API Reference
One document, everyone reads it: product, backend, frontend, QA. It answers three questions for every domain — what does the frontend already call, what shape does it send/expect, and is it real or mocked today. Generated from the actual Angular frontend source (this repo has no backend code — it is a pure client consuming an external API), cross-checked against the frontend's own tolerant adapters, not aspirational.
Maturity tags used throughout:
| Tag | Meaning |
|---|---|
| LIVE | Real HttpClient call exists in code today, hits a real endpoint. |
| MOCK-SWAPPABLE | Interface + DI token exist; a real implementation can be dropped in without touching UI. May or may not have a real impl yet. |
| MOCK-ONLY (no seam) | A mock/local implementation exists but the facade injects the concrete mock class directly — no DI token. A backend needs a token introduced first before it can be wired in. |
| LOCAL-ONLY | Never talks to a backend by design — localStorage / in-memory / derived from bootstrap. |
1. Core principles
- No response envelope. There is no
{ success, data, error }wrapper anywhere. Every call is typed to the bare payload —HttpClient.get<Item>(...),get<Category[]>(...),get<BootstrapConfig>(...). Success = the raw resource (object, array, or{ items, total }for lists). Do not wrap new endpoints in an envelope unless it's a deliberate, coordinated breaking change. - No API versioning. No
/v1/segment, noAccept-Versionheader, anywhere. The only version field in the whole contract isBootstrapConfig.schemaVersion, and it's checked for presence only, not semantically enforced. - No WebSocket / SSE. Every "live" feeling feature (QR login polling, payment status) is plain
setInterval/RxJS polling against a normal request/response endpoint. - Tenant resolution is 100% by hostname, not by header or path.
TenantResolverServicereads the first DNS label (skippingwww) and uses it to pick a base URL. NoX-Tenantheader, no/tenant/{id}/...prefix, ever. Auth requests carry no tenant identifier either — origin is the only signal. - Two independent API bases exist, plus a third for auth:
- Marketplace/tenant API —
ApiConfigService.getBaseUrl()— defaulthttps://api.dexarmarket.ru:445(or per-tenant subdomain),/apion localhost. - Payment/QR API —
environment.qrApiUrl=https://qr.vitanova.network/api. - Session auth API —
environment.authApiUrl(currently same host as the marketplace API).
- Marketplace/tenant API —
- Two independent mock mechanisms coexist — don't conflate them. (a)
mock-data.interceptor.tsglobally short-circuits a hardcoded URL list (/ping,/users/sessions*,/category,/items/*,/searchitems,/cart,/qr*,/websession/*) whenenvironment.useMockData=true— off in both shipped environments today. (b) Per-domain DI-token factories (CONFIG_PROVIDER,CATEGORY_REPOSITORY,PRODUCT_DATA_PROVIDER,BACKOFFICE_DATA_PROVIDER,ADMIN_CATEGORIES_GATEWAY) pick a mock vs. real class perRuntimeProviderStrategyService.PRODUCT_DATA_PROVIDERandCATEGORY_REPOSITORYalways resolve to the real API implementation regardless of mode — their mock branch is dead code (product-data-provider.token.ts:12-18,category-repository.token.ts:12-19).ADMIN_DASHBOARD_METRICS_GATEWAYalways resolves to the local/mock class the other direction — no real implementation is bound yet even though the token exists. - GET retries:
ApiService/ApiCategoryRepositorywrap reads in a sharedretry({ count: 2, delay: exponential from 500ms })— expect up to 3 attempts per read before a caller sees a failure. - Dead scaffolding, not missing files:
src/app/core/error-handling/,src/app/core/guards/,src/app/core/interceptors/each contain only a.gitkeep— reserved directory structure for a centralized error-handling layer that was never built. Every error today is handled ad hoc at the call site. - Backend engineers should not "clean up" the tolerant adapters.
ApiService.normalizeItem()/normalizeCategory()andTelegramSessionApiService.normalizeWebSession()accept multiple historical field-name casings/aliases on purpose (see §7 Products). A payload landing anywhere inside that tolerance envelope works; a stricter renamed shape breaks the client. - Nullable fields: the frontend treats
null,undefined, and an omitted key as the same "absent" signal everywhere except a handful of fields explicitly typedT | null(e.g.AuthSession.userId) wherenullspecifically means "known to be absent." Omit or sendnullinterchangeably elsewhere. - Do not invent endpoints, fields, or business rules beyond what a real frontend call already implies. Every open question below is flagged
Requires backend decisionwith a recommended default — apply the default and move on unless it's flagged as a business/security decision.
2. Authentication
Two independent, coexisting mechanisms. Neither is a stand-in for the other; they authenticate different populations today.
2a. Telegram QR / session login — customer AND admin (LIVE)
Single mechanism for both; only client-side storage differs (separate cookie/signals per surface). Source: src/app/services/telegram-session-api.service.ts.
| Endpoint | Method | Auth | Body / Headers | Response |
|---|---|---|---|---|
/users/sessions |
POST | none | body { webSessionID } (client-generated GUID) + header WebSessionID: <same guid> |
{ webSessionID, url } — url is a https://t.me/{bot}?start={id} deep link |
/users/sessions/{id} |
GET | none | — | Session object, field-tolerant, normalized to AuthSession |
/users/sessions/{id} |
DELETE | none | header WebSessionID: <id> |
ignored — client clears local state regardless of response |
POST https://api.dexarmarket.ru:445/users/sessions
WebSessionID: 3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11
Content-Type: application/json
{ "webSessionID": "3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11" }
{ "webSessionID": "3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11", "url": "https://t.me/myAMLKYCBOT?start=3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11" }
Poll response (field-tolerant — send real field names, the client accepts many aliases):
{
"webSessionID": "3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11",
"status": "active",
"user": { "id": 8823771, "username": "buyer_ivan", "firstName": "Ivan", "lastName": "P" },
"expiresAt": "2026-07-26T05:00:00Z"
}
Send a real expiresAt/expires — if absent, the client fabricates now + 3600s.
Client-side model (src/app/models/auth.model.ts):
interface AuthSession { sessionId: string; userId: number | null; username: string | null; displayName: string; active: boolean; expires: string; }
interface WebSessionStart { webSessionID: string; url: string; }
Expiry handling: expires drives a client timer that re-polls GET /users/sessions/{id} shortly before expiry; if the backend reports inactive, local state clears. There is no reactive 401 handling for this mechanism — expiry is only discovered on the next explicit poll.
2b. Ed25519 challenge/response admin auth (wired client-side, backend not implemented — calls 404 today)
Source: src/app/core/auth/services/auth-api.service.ts. Base {authApiUrl}/api/admin/auth.
| Endpoint | Method | Request | Response |
|---|---|---|---|
/challenge |
GET | — | AuthChallenge { nonce, issuedAt, expiresAt } |
/verify |
POST | VerifySignatureRequest { publicKey, signature, nonce } |
AuthTokenPair { token, refreshToken } |
/refresh |
POST | RefreshTokenRequest { refreshToken } |
AuthTokenPair |
/logout |
POST | { refreshToken } |
void |
JWT claims (JwtClaims, decode-only client-side — the frontend never verifies the signature, that's the backend's job on every request):
interface JwtClaims { sub: string; role: AdminRole; iat: number; exp: number; publicKey: string; }
type AdminRole = 'Owner' | 'Administrator' | 'Editor' | 'Support' | 'ReadOnly';
Storage: localStorage['ed25519AdminToken'] (access), localStorage['ed25519AdminRefreshToken'] (refresh, opaque, never decoded client-side).
Header: intended as standard Authorization: Bearer <token>, but the interceptor that would auto-attach it (authInterceptor) is not registered in app.config.ts today — no request currently attaches the bearer token automatically. adminAuthHeadersInterceptor sets it if a token happens to be in storage, but nothing populates one in the live flow yet.
Refresh: client proactively refreshes ~60s before exp via a scheduled timer, and (once authInterceptor is registered) would reactively refresh once on any 401 before giving up. Every /refresh response is expected to return a new refreshToken (rotation) — the backend should invalidate the one just used.
Role → permission table (ROLE_PERMISSIONS, coarse, enforced client-side only for UX — backend must independently authorize every mutation):
| Role | Permissions |
|---|---|
Owner |
backoffice.read, backoffice.write, builder.read, builder.write, users.manage, settings.manage |
Administrator |
backoffice.read, backoffice.write, builder.read, builder.write, users.manage |
Editor |
backoffice.read, backoffice.write, builder.read, builder.write |
Support |
backoffice.read |
ReadOnly |
backoffice.read, builder.read |
Known naming collision: AdminRole is defined twice — the string union above (core/auth/models/permission.model.ts, the real JWT/auth contract) and an unrelated interface in features/admin/users/models/admin-user.model.ts (display-only labels in the Users admin page, not connected to auth). Treat the string union as the authoritative role for auth purposes; the interface needs a rename (e.g. AdminUserRoleRecord) — this is flagged, not yet fixed.
Route guards: adminAuthGuard (live, checks only "is there an active Telegram session," no role check) gates /edit, /edit/:section, /backoffice. ed25519AuthGuard and permissionGuard(permission) exist and are fully built but attached to no route today — dormant until Mechanism B cuts over. Every guard is a client-side UX gate only; the backend must independently verify authorization on every admin mutation regardless of what a guard decided.
Open decision (business, not technical — ask a human): whether Mechanism A is retired outright in favor of Mechanism B at cutover, or both run in parallel gated by role/tenant config.
3. Bootstrap — the runtime config document
The single payload that drives the entire multi-tenant storefront/builder/backoffice. Fetched once at app startup, held in memory; nearly every feature reads from it instead of a dedicated endpoint.
| Method / Route | GET /bootstrap (relative, rewritten onto the tenant base) |
| Auth | None — must be publicly cacheable per tenant, fetched before any login |
| Query / body | none |
interface BootstrapConfig {
schemaVersion: string; generatedAt: string;
tenant: TenantConfig; branding: BrandingConfig; theme: ThemeConfig; company: CompanyConfig;
featureFlags: FeatureFlagsConfig; features?: MarketplaceFeaturesConfig;
apiEndpoints: ApiEndpointsConfig; localization: LocalizationConfig; seo: SeoConfig;
permissions: PermissionsConfig; header?: HeaderConfig; catalog?: CatalogConfig;
layout?: PlatformLayoutConfig; navigation: NavigationConfig; footer?: FooterConfig;
productPage?: ProductPageConfig; userExperience?: UserExperienceConfig;
pages: PageConfig[]; staticPages?: StaticPagesConfig; widgetRegistry?: WidgetRegistryConfig;
}
Required top-level keys (must always be emitted): schemaVersion, generatedAt, tenant, branding, theme, company, featureFlags, apiEndpoints, localization, seo, permissions, navigation, pages. Everything marked ? may be omitted — the client applies defaults.
apiEndpoints.{website,builder,backoffice} is where a tenant is meant to declare its per-surface endpoint paths at runtime (Record<string, { path, method, timeoutMs? }>) — these are empty {} in the mock today; no builder/backoffice CRUD path exists as a hardcoded literal anywhere in the client. Any concrete admin CRUD path in this document is a proposal, not a verified literal, until populated here.
Abridged real example (from src/assets/mock/bootstrap/bootstrap.json):
{
"schemaVersion": "1.0.0",
"generatedAt": "2026-07-03T00:00:00Z",
"tenant": {
"id": "tenant-default-001", "slug": "default", "code": "DEFAULT", "host": "default.local",
"name": "Marketplace", "websiteBaseUrl": "https://marketplace.local",
"builderBaseUrl": "https://builder.marketplace.local", "backofficeBaseUrl": "https://backoffice.marketplace.local",
"defaultLocale": "ru", "supportedLocales": ["ru", "en", "hy"],
"defaultCurrency": "RUB", "supportedCurrencies": ["RUB", "USD", "EUR", "AMD"],
"timezone": "Europe/Moscow", "documentationUrl": "https://docs.marketplace.local"
},
"branding": { "brandName": "Marketplace", "logoUrl": "/icons/icon-192x192.png", "faviconUrl": "/favicon.ico", "supportEmail": "support@marketplace.local" },
"theme": {
"themeId": "default-light", "mode": "light",
"palette": { "primary": "#497671", "secondary": "#a1b4b5", "success": "#10b981", "warning": "#f59e0b", "danger": "#ef4444", "textPrimary": "#1e3c38", "backgroundPrimary": "#ffffff", "border": "#d3dad9" },
"typography": { "primaryFontFamily": "DM Sans, sans-serif", "baseFontSize": 16 },
"spacing": { "unit": 4, "scale": [0, 4, 8, 12, 16, 24, 32, 48] }
},
"featureFlags": { "wishlist": true, "compare": true, "reviews": true, "blog": false, "chat": false, "coupons": true },
"apiEndpoints": { "bootstrap": { "path": "/bootstrap", "method": "GET", "timeoutMs": 10000 }, "website": {}, "builder": {}, "backoffice": {} },
"localization": { "defaultLocale": "ru", "supportedLocales": ["ru", "en", "hy"], "currencyByLocale": { "ru": "RUB", "en": "USD", "hy": "AMD" } },
"catalog": { "layout": "grid", "defaultSort": "relevance", "availableSorts": ["relevance", "latest", "price_asc", "price_desc", "rating", "popular", "discount"] },
"navigation": { "header": [{ "id": "nav-home", "labelKey": "nav.home", "route": "/", "order": 1 }], "footer": [{ "id": "footer-about", "labelKey": "nav.about", "route": "/about-us", "order": 1 }] },
"widgetRegistry": { "manifestUrl": "/assets/mock/bootstrap/widget-manifest.json" },
"pages": [{
"id": "page-home", "key": "home", "title": "Home", "route": { "path": "/", "exact": true }, "visible": true,
"sections": [{ "id": "section-hero", "type": "hero", "order": 1, "layout": { "strategy": "hero", "columns": 1 }, "widgets": [{ "id": "widget-hero-main", "type": "hero", "version": "1.0.0", "order": 1, "props": { "title": { "ru": "Добро пожаловать", "en": "Welcome" } } }] }]
}]
}
Which provider fires: mock (GET /assets/mock/bootstrap/bootstrap.json) when useMockData=true, or when useMockBootstrapOnLocal=true and host is localhost; otherwise real GET /bootstrap.
No write path exists. Publishing a marketplace (builder "Publish") only promotes an in-memory/localStorage draft signal today — nothing reaches a backend. See §8 Builder.
Requires backend decision: X-Language/Accept-Language pre-selection on this call (today the client always gets and holds the full multi-locale document); whether schemaVersion is ever semantically enforced (today presence-only); ETag/conditional-request caching (none exists); the entire draft→publish write path.
4. Pagination, sorting, filtering, search — conventions
Two pagination styles coexist — support both, they are not interchangeable:
- Offset/count (marketplace storefront reads) — query params
count(page size, default 50) andskip(offset, default 0).searchItemsreturns{ items, total }; other list reads (getCategoryItems,getRandomItems) return a bare array with no total. - Page/pageSize (admin lists, storefront engagement lists, media) — request
{ page, pageSize, ...filters }, response{ items, total, page, pageSize }. Client derivestotalPages = ceil(total / pageSize)itself.
No cursor/keyset pagination exists anywhere. No server-side page-size cap is enforced by the client (it just sends 50 as a default) — requires backend decision on max page size.
Sorting: enumerated in bootstrap catalog.availableSorts: relevance | latest | price_asc | price_desc | rating | popular | discount (7 values). The live sort query param on GET /searchitems only accepts a 5-value subset: relevance | price_asc | price_desc | popular | rating — latest/discount have no confirmed search-endpoint mapping. Requires backend decision to reconcile these two vocabularies, and to define wire encoding for admin-list sorting (no convention exists yet — admin CRUD is mock-only).
Filtering: storefront search accepts categoryIDs (comma-joined ints), minPrice, maxPrice, tag. Admin list filter objects (in-memory today, not confirmed wire contracts) all follow { search: string, <field>: 'all' | <enum>, page, pageSize } — 'all' is the "no filter on this facet" sentinel. Requires backend decision: whether 'all' is sent literally or the param omitted.
Search: GET /searchitems?search=<q>&count=&skip=[&categoryIDs&minPrice&maxPrice&tag&sort] → { items, total }. No dedicated autocomplete/suggestion/trending backend endpoint exists — those are derived client-side from already-loaded catalog data today.
5. Error model
The frontend does not currently parse any backend error envelope for any real endpoint — no interceptor inspects error responses; every caller reacts at the raw HttpErrorResponse.status/.message level. The one partial exception (Ed25519 admin auth) derives its error code from HTTP status only, ignoring any body field, which is itself a known bug (see below). Everything in this section is therefore a recommended envelope to adopt going forward, not something already wired end-to-end — apply it to new endpoints and treat the frontend gaps below as follow-up work, not something this doc can silently paper over.
The envelope
{
"error": {
"code": "VALIDATION_FAILED",
"message": "One or more fields are invalid.",
"status": 422,
"requestId": "b3f1c2a0-4e21-4d3a-9e77-1e8f6a2d9c11",
"details": [{ "field": "sku", "code": "REQUIRED", "message": "SKU is required." }]
}
}
| Field | Required | Notes |
|---|---|---|
error.code |
yes | Stable, UPPER_SNAKE_CASE, never localized — this is what code should branch on, never message. |
error.message |
yes | Human-readable English fallback only. |
error.status |
yes | Mirrors the HTTP status. |
error.requestId |
recommended | Correlation id for support/ops, echoed in logs. |
error.details |
only on 422 | { field, code, message }[] — matches the client's existing local-validation issue shape, so a future adapter can merge backend 422s into the same inline-error UI without inventing a second mechanism. |
Status-by-status
| Status | code |
Frontend reaction today |
|---|---|---|
| 401 | UNAUTHENTICATED |
Ed25519 flow → generic "Unauthorized, sign in" screen. Customer Telegram auth: no 401 branch anywhere — session validity is only ever discovered by polling. Admin CRUD facades: none have ever seen a real 401 (all mock). |
| 403 | FORBIDDEN |
Ed25519 flow → "Forbidden, back to dashboard." No tenant-vs-role distinction exists — both render identical copy. |
| 404 | NOT_FOUND |
No code path distinguishes 404 from any other failure — a deleted product and a 500 render the identical generic empty-state today. |
| 409 | CONFLICT |
Nothing reacts to 409 anywhere. Only related mechanism: AdminCategoriesGateway.isSlugTaken(), a proactive pre-check, not a 409 handler. |
| 422 | VALIDATION_FAILED + details[] |
No admin form parses a backend validation body today (all mock). Client's own ProjectEditorFacade.fieldError(fieldKey) inline-error pattern is the convention to align a future adapter to. |
| 429 | RATE_LIMITED (+retryAfterSeconds) |
Zero handling anywhere — no interceptor, facade, or component references 429 at all. |
| 500 | INTERNAL_ERROR |
Falls into whatever generic catch-all a given caller has (retry-button empty state, or — for LocationService.getRegions() — silently falls back to 6 hardcoded regions with no visible error at all). |
| 503 (infra down) | SERVICE_UNAVAILABLE |
Same "backend unavailable, retry" screen as 500, on the Ed25519 flow only. |
| 503 (maintenance) | MAINTENANCE_MODE (+maintenanceUntil) |
No maintenance-mode concept exists in the frontend at all today. Same HTTP status as infra-down 503 — error.code is the only way to distinguish them. |
| 403 (tenant disabled) | TENANT_DISABLED |
No handling exists. No code path today distinguishes "tenant exists but is disabled" from any other 403. |
| 401 (token expired) | TOKEN_EXPIRED |
Known bug, not just a gap: the client has a dedicated "Session expired" screen wired and ready, but toAuthErrorShape() only reaches it via a no-refresh-token-present client-side branch — a real backend 401 on /refresh always renders the generic "Unauthorized" screen instead, because the mapping function ignores any body code and derives purely from HTTP status. Fix requires the backend to send error.code: "TOKEN_EXPIRED" and a small frontend change to prefer it. |
| 401 (bad signature) | INVALID_SIGNATURE |
Same bug class as above — dedicated screen exists, unreachable from a real HTTP response for the identical reason. |
Every admin backoffice list page (Users/Orders/Monitoring/Moderation/Transactions/Products/Categories/Analytics/Customers/Dashboard) shares one generic pattern: a boolean error signal → "Something went wrong" + retry button. None of them branch on status or code today — every status above collapses into the same generic UI until facades are individually updated.
6. Marketplace / storefront API (LIVE)
Base: ApiConfigService.getBaseUrl(). Headers on every call (apiHeadersInterceptor): X-Region, X-Language (ru→RU, en→EN, hy→AM), Currency (default RUB), WebSessionID. Source: src/app/services/api.service.ts.
| Endpoint | Method | Params / Body | Response |
|---|---|---|---|
/ping |
GET | — | { message } |
/category |
GET | — | Category[] (normalized) |
/category/{id} |
GET | count, skip |
Item[] |
/items/{id} |
GET | — | Item |
/items/randomitems |
GET | count, category? |
Item[] (featured/random) |
/searchitems |
GET | search, count, skip, categoryIDs?, minPrice?, maxPrice?, tag?, sort? |
{ items: Item[], total: number } |
/websession/{sessionId} |
POST | item array | cart echo |
/items/{id}/callback |
POST | { rating, comment, sessionID, timestamp } |
{ message } — review |
/items/{id}/questiion |
POST | { question, sessionID, timestamp } |
{ message } — literal typo questiion, preserve it, matches the client |
/purchase-email |
POST | { email, phone?, telegramUserId, items[] } |
{ message } |
/regions |
GET | — | Region[] — client falls back silently to 6 hardcoded regions on any error |
6.1 Products — the tolerance contract
The wire DTO Item (src/app/models/item.model.ts) is reconciled by ApiService.normalizeItem() — the single largest inline adapter in the codebase. It tolerates two historical shapes at once:
id(string) ↔itemID(numeric)imgs[]↔photos[]names[]↔translationsdescriptionas a key/value array ↔ a plain stringcomments↔callbacks(reviews)- color
0xRRGGBB→ normalized#RRGGBB remainingcount → a stock band
A real backend can send either historical shape — do not invent a third, cleaner shape. normalizeCategory() does the same job for categories.
6.2 Categories — two parallel stacks exist
- Clean stack (real, LIVE):
GET /category→CategoryDto[]({ categoryID, names: [{lang,name}], subcategories: [...] }) →CategoryMapperflattens the tree, dedupes by id, normalizesam→hy→ domainCategory. - Legacy stack: the same
/categoryresponse also feedsApiService.normalizeCategory()→ a differentCategorytype (src/app/models/category.model.ts). Two unrelatedCategorytypes exist in the codebase with the same name — a known duplication, not a bug to silently fix on the backend side; just be aware both consume the same wire shape.
[{ "categoryID": 12, "names": [{ "lang": "ru", "name": "Электроника" }, { "lang": "en", "name": "Electronics" }], "subcategories": [{ "categoryID": 34, "names": [{ "lang": "en", "name": "Phones" }] }] }]
7. Cart / Orders / Payments (LIVE)
Cart contents are LOCAL-ONLY (localStorage marketplace_cart, + Telegram CloudStorage in-app) — there is no backend cart. Checkout produces real payment + order calls.
| Endpoint | Method | Base | Body | Response |
|---|---|---|---|---|
/cart |
POST | marketplace | CartPaymentRequest |
QrCreateResponse |
/orders |
POST | marketplace | CreateOrderRequest |
CreateOrderResponse — fire-and-forget after payment succeeds, doesn't touch the payment call chain |
/qr |
POST | qrApiUrl |
QrCreateRequest (headers authorization-key, userid-value) |
QrCreateResponse |
/qr/dynamic/{partnerId}/{qrId} |
GET | qrApiUrl |
— | QrDynamicStatusResponse |
/card/{partnerId}/{orderId} |
GET | qrApiUrl |
— | QrDynamicStatusResponse |
Const partnerId = web-97ec-9c57-4dde-9037-3a68f7f83750.
interface CartPaymentRequest {
amount: number; currency: 'RUB'; siteuserID: string; siteorderID: string; redirectUrl: string;
telegramUsername: string; paymentMethod: 'qr' | 'card'; qrDescription?: string; customerID?: string;
items: Array<{ itemID: number; price: number; name: string; quantity?: number }>;
}
interface CreateOrderRequest {
items: Array<{ productId: string; name: string; quantity: number; price: number }>;
customer: { name: string; email: string; phone: string };
payment?: { method: string; currency: string };
shipping?: { address: string; method: string; trackingNumber: string };
}
interface CreateOrderResponse { id: string; orderNumber: string; status: string; total: number; currency: string; }
QrCreateResponse is deliberately alias-tolerant — many casings accepted for id/url/partner fields (qrId/qrID, nspkurl/nspkId, partnerID/partnerId/PartnerID, etc). Pick one canonical casing on the backend; the client resolves whichever it gets.
POST https://api.dexarmarket.ru:445/cart
WebSessionID: 3f1c2a0e-…
{ "amount": 4990, "currency": "RUB", "siteuserID": "8823771", "siteorderID": "order-2026-0007", "redirectUrl": "https://marketplace.local/checkout/done", "telegramUsername": "buyer_ivan", "paymentMethod": "qr", "items": [{ "itemID": 101, "price": 4990, "name": "Wireless Keyboard", "quantity": 1 }] }
{ "qrId": "QR-77f0", "nspkurl": "https://qr.nspk.ru/AD10…", "status": "created", "qrExpirationDate": "2026-07-26T04:10:00Z" }
Payments are frozen — this call chain is explicitly out of scope for changes; document only, don't modify.
8. Admin (backoffice) domains
Structural finding, the single most important fact in this section: of 11 admin gateway domains, only Categories and Dashboard-metrics are bound through a DI token — a real backend can be dropped in for those two with zero facade changes. Every other admin domain's facade injects its mock *LocalGateway class directly, so a token has to be added before a real backend can be wired in at all, regardless of whether the endpoint itself is easy to build. Only one real admin HTTP implementation exists anywhere: AdminCategoriesApiGateway.
| Domain | Interface | Real impl? | DI token? | Facade | Seam status |
|---|---|---|---|---|---|
| Categories | AdminCategoriesGateway |
yes (admin-categories-api.gateway.ts) |
yes (ADMIN_CATEGORIES_GATEWAY) |
AdminCategoriesFacade |
MOCK-SWAPPABLE, done |
| Dashboard metrics | AdminDashboardMetricsGateway |
no | yes (ADMIN_DASHBOARD_METRICS_GATEWAY) |
AdminDashboardFacade |
MOCK-SWAPPABLE, token only |
| Orders | AdminOrdersGateway |
no | none | AdminOrdersFacade |
MOCK-ONLY, no seam |
| Products | AdminProductsGateway |
no | none | AdminProductsFacade |
MOCK-ONLY, no seam |
| Users | AdminUsersGateway |
no | none | AdminUsersFacade |
MOCK-ONLY, no seam |
| Transactions | AdminTransactionsGateway |
no | none | AdminTransactionsFacade |
MOCK-ONLY, no seam |
| Monitoring | AdminMonitoringGateway |
no | none | AdminMonitoringFacade |
MOCK-ONLY, no seam |
| Moderation | AdminModerationGateway |
no | none | AdminModerationFacade |
MOCK-ONLY, no seam |
| Customers | (none — derived) | no | n/a | AdminCustomersFacade |
derives from Orders' mock gateway |
| Analytics | (none — derived) | no | partial | AdminAnalyticsFacade |
composes 5 other gateways, no data source |
| Media | abstract class MediaRepository |
no | yes (class token) | MediaLibraryFacade |
MOCK-SWAPPABLE |
Gateway interface method contracts (what a real backend must satisfy)
- Categories —
loadCategories(filters),loadCategory(id),createCategory,updateCategory,deleteCategory,restoreCategory,isSlugTaken(slug, excludingId). - Dashboard metrics —
loadMetrics(): AdminDashboardMetrics(no params — a seller/scope filter would need a new parameter, no object to extend). - Orders —
loadOrders(filters),loadOrder(id),updateStatus(id, status),requestRefund(id),addNote(id, note, internal),archiveOrder,restoreOrder,deleteOrder. - Products —
loadProducts(filters),loadProduct(id),loadCategories(),createProduct,updateProduct,deleteProduct,duplicateProduct,archiveProduct,restoreProduct. - Users —
loadUsers,loadRoles,loadInvitations,loadSessions(userId),loadAudit(userId),setUserRole,setUserStatus,inviteUser(email, roleId, scope),revokeInvitation,revokeSession. - Transactions —
loadTransactions(filters),retryFailed(id),setFraudFlag(id, flagged). - Monitoring —
loadEvents(filters),loadQueues(),loadWebhooks(). - Moderation —
loadReviews(filters),loadReview(id),setReviewStatus,setReviewVisible,setReviewPinned,setReviewFeatured,addModeratorNote,deleteReview,loadReports(),setReportStatus(id, status). - Media —
list(params?),upload(file, options?),remove(id),update(id, patch),listFolders().
The one real admin endpoint — Categories, exact paths
Base: ${apiConfig.getBaseUrl()}/backoffice/categories. Mode-switched between this and the local mock via getCategoryProviderMode() — mock in local dev (no reachable backoffice API there), real API in production.
| Method | Path | Body | Response |
|---|---|---|---|
| GET | /backoffice/categories?search=&visibility=&includeDeleted= |
— | AdminCategory[] |
| GET | /backoffice/categories/{id} |
— | AdminCategory | null (404 → null) |
| POST | /backoffice/categories |
AdminCategory minus {id, itemsCount, deletedAt, createdAt, updatedAt} |
AdminCategory |
| PUT | /backoffice/categories/{id} |
full AdminCategory |
AdminCategory |
| DELETE | /backoffice/categories/{id} |
— | void — soft delete only, no hard delete exists |
| POST | /backoffice/categories/{id}/restore |
{} |
AdminCategory | null |
| GET | /backoffice/categories/slug-taken?slug=&excludingId= |
— | { taken: boolean } |
Use this exact path shape as the template for every other admin domain in §8.5's build order — it's the only one proven end-to-end.
Worked example — Admin Orders (no mapper exists yet, backend has freedom here)
Unlike Categories/Products (which have a wire DTO to match), admin domains other than Categories have no wire DTO and no mapper today — the mock gateways build view models directly in memory. This means the JSON shape below is a proposal the new AdminOrdersApiGateway would map into the existing AdminOrder view model, not a shape already fixed by an adapter:
{
"id": "ord_1042", "status": "processing", "paymentStatus": "paid",
"customer": { "id": "cus_88", "name": "…", "email": "…" },
"items": [{ "productId": "…", "title": "…", "qty": 2, "unitPrice": 1990 }],
"shipping": { "method": "…", "address": "…" },
"timeline": [{ "event": "created", "at": "2026-07-01T10:00:00Z" }]
}
The same "no mapper exists, write one inside the new *ApiGateway" note applies to Products, Users, Transactions, Monitoring, Moderation.
Widget manifest (LIVE — static/remote JSON, separate from admin CRUD)
GET <bootstrap.widgetRegistry.manifestUrl> (default /assets/mock/bootstrap/widget-manifest.json), falls back to { widgets: [] } on any error, never throws to the UI.
{ "widgets": [{ "type": "hero", "version": "1.0.0", "componentKey": "HeroWidgetComponent", "supportedLayouts": ["hero"], "supportedDataSources": ["manual"], "settingsSchema": { "type": "object", "properties": { "title": { "type": "string" } } }, "defaultSettings": { "title": "Welcome" }, "enabled": true }] }
Backoffice storefront cards (LIVE — distinct from admin CRUD above)
GET /api/backoffice/products, GET /api/backoffice/categories — feeds storefront product/category card widgets, not the admin panel.
9. Everything that is LOCAL-ONLY (no backend call exists at all)
Worth knowing explicitly, so nobody assumes a gateway swap will "just work" for these:
- Content management / static pages (CMS) — reads/writes
BootstrapConfig.staticPagesin-memory. No dedicated backend call. Publishing = writing bootstrap back, for which no client write call exists. - Project editor / builder — edits an in-memory
BootstrapConfig, persists drafts tolocalStorageonly. "Publish" today only promotes the local draft signal. A builder API is declared only as an emptyapiEndpoints.builder: {}placeholder in bootstrap. - Search —
SearchFacadeis a client-side orchestration over the product/category providers (history, trending, autocomplete, cache all local). The only real backend traffic underneath it isGET /searchitems. - User experience (wishlist/compare/recently-viewed/saved-searches) — fully denormalized objects in
localStorage, guest-first. A DI token exists for a future authenticated repository, but nothing is bound to it — comment in code notes it "can be switched to authenticated repository later." - Diagnostics — inspects runtime/bootstrap/widget state locally; the one live-ish probe is a
/pinghealth check. - Cart contents — see §7, real payment/order calls exist, cart state never round-trips to a backend.
10. Backend build order (dependency-driven, not document order)
- Auth + session — blocks everything admin-gated.
- Bootstrap content (branding/theme/nav/seo) — transport (
GET /bootstrap) already works; the content is still default stubs. Tenant resolution depends on it. - Categories — already LIVE both storefront and admin; products reference categories.
- Products / catalog — storefront reads are LIVE; admin Products CRUD is the first no-seam admin domain to build.
- Media — products/categories editors reference media assets.
- Cart / Orders / Transactions — checkout is LIVE; admin Orders CRUD, then Transactions (derives from Orders).
- Reviews / Moderation — customer writes are LIVE; admin Moderation gates them.
- Users / roles / invitations — independent of commerce, needs auth.
- Dashboard metrics, then Monitoring — operational visibility layers.
- Analytics — last. Needs orders/products/moderation real and a tracking pipeline that doesn't exist yet anywhere (not just a missing endpoint — no data source at all).
- Builder draft/publish + CMS — net-new write paths, can proceed in parallel once bootstrap content (step 2) is real.
- User-experience sync, search suggestions — enhancements over already-working local features.
Per-domain migration pattern for the six no-seam admin domains (Orders, Products, Users, Transactions, Monitoring, Moderation): add a DI token → switch the facade to inject the token instead of the concrete mock class → implement the *ApiGateway (contains the DTO→view-model mapper) → bind the token → retire or keep the mock behind the existing useMockData flag. This is the exact pattern already proven by Categories — replicate it, don't redesign it per domain.
11. Known discrepancies to reconcile before/while building
AdminRoledefined twice with unrelated shapes (§2b) — auth string-union vs. Users-page display interface.Categorydefined twice (§6.2) — legacy vs. clean-stack, both fed by the same/categoryresponse.- Duplicate search models under
features/search/models/andcore/search/models/. submitQuestionendpoint path has a literal typo (questiion, notquestion) — this matches the real backend spec, do not "fix" it.- The Ed25519 error-code bug (§5) —
TOKEN_EXPIRED/INVALID_SIGNATUREscreens are fully built and unreachable from real HTTP responses today because the client only reads HTTP status, never a body code. Needs a coordinated backend + frontend fix, not backend alone. ADMIN_DASHBOARD_METRICS_GATEWAYandUSER_EXPERIENCE_REPOSITORYtoken factories return the mock/local class in every mode — a real implementation must be written and explicitly bound; the seam existing does not mean a real backend is one line away.
For open product/business decisions this document deliberately does not resolve (rate limiting posture, refresh-token reuse detection, tenant-scoped auth, API versioning scheme, etc.), see GAPS-AND-IMPROVEMENTS.md.