Files
marketplaces/BACKEND-API-REFERENCE.md
sdarbinyan e6d64abd56
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
docs: replace 62 scattered/stale markdown files with two living references
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>
2026-08-13 03:17:09 +04:00

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

  1. 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.
  2. No API versioning. No /v1/ segment, no Accept-Version header, anywhere. The only version field in the whole contract is BootstrapConfig.schemaVersion, and it's checked for presence only, not semantically enforced.
  3. No WebSocket / SSE. Every "live" feeling feature (QR login polling, payment status) is plain setInterval/RxJS polling against a normal request/response endpoint.
  4. Tenant resolution is 100% by hostname, not by header or path. TenantResolverService reads the first DNS label (skipping www) and uses it to pick a base URL. No X-Tenant header, no /tenant/{id}/... prefix, ever. Auth requests carry no tenant identifier either — origin is the only signal.
  5. Two independent API bases exist, plus a third for auth:
    • Marketplace/tenant API — ApiConfigService.getBaseUrl() — default https://api.dexarmarket.ru:445 (or per-tenant subdomain), /api on localhost.
    • Payment/QR API — environment.qrApiUrl = https://qr.vitanova.network/api.
    • Session auth API — environment.authApiUrl (currently same host as the marketplace API).
  6. Two independent mock mechanisms coexist — don't conflate them. (a) mock-data.interceptor.ts globally short-circuits a hardcoded URL list (/ping, /users/sessions*, /category, /items/*, /searchitems, /cart, /qr*, /websession/*) when environment.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 per RuntimeProviderStrategyService. PRODUCT_DATA_PROVIDER and CATEGORY_REPOSITORY always 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_GATEWAY always resolves to the local/mock class the other direction — no real implementation is bound yet even though the token exists.
  7. GET retries: ApiService/ApiCategoryRepository wrap reads in a shared retry({ count: 2, delay: exponential from 500ms }) — expect up to 3 attempts per read before a caller sees a failure.
  8. 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.
  9. Backend engineers should not "clean up" the tolerant adapters. ApiService.normalizeItem()/normalizeCategory() and TelegramSessionApiService.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.
  10. Nullable fields: the frontend treats null, undefined, and an omitted key as the same "absent" signal everywhere except a handful of fields explicitly typed T | null (e.g. AuthSession.userId) where null specifically means "known to be absent." Omit or send null interchangeably elsewhere.
  11. Do not invent endpoints, fields, or business rules beyond what a real frontend call already implies. Every open question below is flagged Requires backend decision with 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) and skip (offset, default 0). searchItems returns { 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 derives totalPages = 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 | ratinglatest/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[]translations
  • description as a key/value array ↔ a plain string
  • commentscallbacks (reviews)
  • color 0xRRGGBB → normalized #RRGGBB
  • remaining count → 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 /categoryCategoryDto[] ({ categoryID, names: [{lang,name}], subcategories: [...] }) → CategoryMapper flattens the tree, dedupes by id, normalizes am→hy → domain Category.
  • Legacy stack: the same /category response also feeds ApiService.normalizeCategory() → a different Category type (src/app/models/category.model.ts). Two unrelated Category types 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)

  • CategoriesloadCategories(filters), loadCategory(id), createCategory, updateCategory, deleteCategory, restoreCategory, isSlugTaken(slug, excludingId).
  • Dashboard metricsloadMetrics(): AdminDashboardMetrics (no params — a seller/scope filter would need a new parameter, no object to extend).
  • OrdersloadOrders(filters), loadOrder(id), updateStatus(id, status), requestRefund(id), addNote(id, note, internal), archiveOrder, restoreOrder, deleteOrder.
  • ProductsloadProducts(filters), loadProduct(id), loadCategories(), createProduct, updateProduct, deleteProduct, duplicateProduct, archiveProduct, restoreProduct.
  • UsersloadUsers, loadRoles, loadInvitations, loadSessions(userId), loadAudit(userId), setUserRole, setUserStatus, inviteUser(email, roleId, scope), revokeInvitation, revokeSession.
  • TransactionsloadTransactions(filters), retryFailed(id), setFraudFlag(id, flagged).
  • MonitoringloadEvents(filters), loadQueues(), loadWebhooks().
  • ModerationloadReviews(filters), loadReview(id), setReviewStatus, setReviewVisible, setReviewPinned, setReviewFeatured, addModeratorNote, deleteReview, loadReports(), setReportStatus(id, status).
  • Medialist(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} voidsoft 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.staticPages in-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 to localStorage only. "Publish" today only promotes the local draft signal. A builder API is declared only as an empty apiEndpoints.builder: {} placeholder in bootstrap.
  • SearchSearchFacade is a client-side orchestration over the product/category providers (history, trending, autocomplete, cache all local). The only real backend traffic underneath it is GET /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 /ping health 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)

  1. Auth + session — blocks everything admin-gated.
  2. Bootstrap content (branding/theme/nav/seo) — transport (GET /bootstrap) already works; the content is still default stubs. Tenant resolution depends on it.
  3. Categories — already LIVE both storefront and admin; products reference categories.
  4. Products / catalog — storefront reads are LIVE; admin Products CRUD is the first no-seam admin domain to build.
  5. Media — products/categories editors reference media assets.
  6. Cart / Orders / Transactions — checkout is LIVE; admin Orders CRUD, then Transactions (derives from Orders).
  7. Reviews / Moderation — customer writes are LIVE; admin Moderation gates them.
  8. Users / roles / invitations — independent of commerce, needs auth.
  9. Dashboard metrics, then Monitoring — operational visibility layers.
  10. 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).
  11. Builder draft/publish + CMS — net-new write paths, can proceed in parallel once bootstrap content (step 2) is real.
  12. 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

  • AdminRole defined twice with unrelated shapes (§2b) — auth string-union vs. Users-page display interface.
  • Category defined twice (§6.2) — legacy vs. clean-stack, both fed by the same /category response.
  • Duplicate search models under features/search/models/ and core/search/models/.
  • submitQuestion endpoint path has a literal typo (questiion, not question) — this matches the real backend spec, do not "fix" it.
  • The Ed25519 error-code bug (§5) — TOKEN_EXPIRED/INVALID_SIGNATURE screens 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_GATEWAY and USER_EXPERIENCE_REPOSITORY token 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.