{
  "version": "1.6.6",
  "generatedAt": "2026-06-17",
  "source": {
    "repo": "NEXX-Ecosystem/nexx-platform-backend",
    "branch": "rel-d33-d36-slice2-relationship-lifecycle",
    "commit": "23bfaf0",
    "openapi": "docs/contracts/openapi-internal-v1.json",
    "publicOpenapi": "https://nexx-proxy-progress.srv.acebox.eu/contracts/openapi-internal-v1.json",
    "publicHandoff": "https://nexx-proxy-progress.srv.acebox.eu/contracts/frontend-handoff.json",
    "liveSchema": "https://nexx-api.srv.acebox.eu/api/schema/"
  },
  "changesSince": {
    "v1.6.6": [
      "REL D3.10-D3.11 Track B — order.created mock export + IntegrationJob + retry. Backend-owned, mock/non-final: NO live 1C/Engine, NO official documents/PDF/VAT/stock (Stage 4). On successful order creation PLATFORM emits ONE order.created integration job per supplier order, best-effort — a mock-export failure NEVER rolls back the created order (order stays created; the job goes to failed/retry). The mock returns a deterministic externalOrderId (MOCK-ORD-<orderId>). Retry up to 3 attempts with backoff -> failed_permanent, run by the `process_integration_jobs` management command (Stage 3 has no live scheduler). Idempotency key order.created:<orderId> — retries never duplicate the job or re-send.",
      "v1 order responses (GET /orders/ list, GET /orders/{id}/, POST /orders/, and the confirm/ship/deliver/accept/cancel actions) gain a READ-ONLY `export` block: { status: pending|in_progress|done|failed|failed_permanent, externalOrderId: string|null, lastError: string|null, nextRetryAt: datetime|null, updatedAt: datetime|null }. The UI may show: order created + transfer to accounting system pending/done/failed (status=failed WITH a non-null nextRetryAt means a retry is scheduled).",
      "NULLABLE CASE (explicit): orders WITHOUT an order.created job — legacy/seed orders, or any order created outside the current checkout flow — return `export: null`. Treat null as 'no export job'. Legacy /api/orders/ responses are unchanged (no `export` block at all).",
      "Stage 3 exports only order.created; the lifecycle events (order.confirmed/shipped/delivered/accepted/cancelled) are reserved for Stage 4 (live Engine + official documents). seed_frontend_demo attaches a `done` and a `failed_permanent` export job to demo orders so both UI states are visible without a live Engine."
    ],
    "v1.6.5": [
      "REL D3.7-D3.9 Track A — APP-facing cart/checkout/order expansion. Additive over v1.6.4; legacy /api/checkout|orders/ shapes unchanged. Order creation is now GATED per supplier group: requires an active relationship AND a published default price list (Q1) AND, if SupplierProfile.minOrderAmount is set, the group NET subtotal (before VAT) >= minOrderAmount (Q2) AND a resolvable delivery address (Q3).",
      "POST /api/internal/v1/checkout/validate/ enriched (additive keys only). Each supplierGroups[] now carries: supplierCompanyId (STABLE key for selective submit), totals{net,vat,gross}, lineCount, itemCount (summed quantity, 2dp), currency (supplier default or EUR), minOrderAmount (nullable), canSubmit, and typed blockers[]. New top-level keys: canSubmit (= all groups ready AND delivery resolved), blockers[] (checkout-level), and delivery {resolved, source, address}. UI text for min_order_not_met: compare group totals.net (net subtotal, before VAT) vs minOrderAmount in currency.",
      "Typed checkout blocker codes (in supplierGroups[].blockers[] unless noted): relationship_not_active; price_list_not_published (Q1); min_order_not_met (Q2, vs group NET subtotal); delivery_address_missing (TOP-LEVEL blockers[], Q3).",
      "Delivery (Q3): ONE checkout-level snapshot applied to every order in the submit set. Resolution precedence at submit: explicit deliveryAddress -> BuyerProfile.delivery_address -> default Warehouse.address. Legal address is NEVER a delivery fallback. checkout/validate delivery.source is one of buyer_profile | default_warehouse | null; at submit an explicitly typed address is source 'explicit' (the order keeps the resolved deliveryAddress snapshot). A delivery address is MANDATORY; with none resolvable, submit returns 409 CHECKOUT_BLOCKED carrying the delivery_address_missing blocker.",
      "POST /api/internal/v1/orders/ — selective checkout (Q4). New optional body field supplierCompanyIds: int[]. Omitted = submit the WHOLE cart all-or-nothing (backward compatible). Provided = submit ONLY those supplier groups, ATOMIC over the selected set: if any selected group is blocked/stale, NO orders are created and 409 CHECKOUT_BLOCKED is returned with details.checkout in the SAME shape as checkout/validate. Unselected/blocked groups stay in the cart. The 201 response now also returns remainingCart (cart payload after consumption). Unknown or empty supplierCompanyIds -> 400 ORDER_SELECTION_INVALID.",
      "Order action role gates (Q7): mutating /api/internal/v1 order actions (order submit, cancel, confirm, ship, deliver, accept) now require COMPANY_ADMIN or MANAGER on the active company; USER -> 403 ORDER_ACTION_FORBIDDEN_FOR_ROLE. Reads unchanged; service tokens stay read-only. Legacy /api/orders/ actions are intentionally NOT role-gated (frozen compat bridge).",
      "Order lines continue to snapshot price/VAT/totals at creation; later price-list edits do not alter historical orders (Q5). Engine order.created export + IntegrationJob/retry (D3.10-D3.11, Q6) is NOT in this slice — deferred pending the PLATFORM<->Engine payload decision."
    ],
    "v1.6.4": [
      "REL D3.3-D3.6 Slice 2 part B = D3.5 email-link confirm — additive over v1.6.3; legacy /api/relationships/ unchanged. Slice 2 is now COMPLETE. A confirm email is sent AUTOMATICALLY when a buyer relationship request is created (and on re-request after terminated). Recipients: active COMPANY_ADMIN/MANAGER members of the supplier company + SupplierProfile.orderEmail (if set); best-effort — a mail failure never fails request creation. Link: ${NEXX_PUBLIC_APP_URL}/relationship-confirm/{token}. Token is one-time, TTL 7 days.",
      "POST /api/internal/v1/relationships/confirm/validate/ — PUBLIC (no auth), body { token } -> 200 { tokenState: valid|invalid|expired|used, requiresAuth: true, expiresAt, relationshipStatus }. Does NOT consume the token and does NOT reveal the buyer legal block (pre-auth safe state only).",
      "POST /api/internal/v1/relationships/confirm/ — Bearer + X-NEXX-Company-ID (active company MUST be the relationship's supplier). Body { token } -> consumes the token, moves requested -> active (same business action as in-app activate; `confirmed` enum stays reserved), returns RelationshipResponse + `alreadyConfirmed` (bool). Typed errors: 404 RELATIONSHIP_CONFIRM_TOKEN_INVALID; 403 RELATIONSHIP_CONFIRM_FORBIDDEN (active company is not the supplier); 400 RELATIONSHIP_CONFIRM_TOKEN_EXPIRED / RELATIONSHIP_CONFIRM_TOKEN_USED / RELATIONSHIP_NOT_CONFIRMABLE (status no longer requested).",
      "Idempotent (double-click/refresh safe): re-confirming an already-used token whose relationship is already active -> 200 with alreadyConfirmed=true. Confirm-after-auth: a not-logged-in / wrong-company click does not activate (validate tells the UI requiresAuth; confirm enforces supplier-auth). Migration 0029 (RelationshipConfirmationToken). Email delivery via the existing send_mail pipeline (interim seohost sender on prod)."
    ],
    "v1.6.3": [
      "REL D3.3-D3.6 Slice 2 part A (request details + enrichment + detail endpoint) — additive over v1.6.2; legacy /api/relationships/ unchanged. POST /api/internal/v1/relationships/ create gains optional `message` (<=500), `procurementAreas[]` (BKG-009 keys; unknown -> 400 VALIDATION_ERROR), `expectedVolume` (free-text). Only `supplierCompanyId` is still required.",
      "RelationshipResponse enrichment (list + detail + transitions): `requestMessage`, `expectedVolume`, `requestedProcurementAreas[]`, `requestedBy` {id, name} (null if unknown), `decisionReason`, `createdAt` (original row creation). The buyer/supplier company summary now also carries a legal block for supplier-side request review: `registrationNumber`, `vatNumber`, `legalAddress`, `country`, `verificationMethod`. All additive — existing fields unchanged.",
      "Re-request date semantics: `requestedAt` now reflects the CURRENT/latest (re-)request time (was row-creation time in v1.6.2; falls back to `createdAt` for pre-migration rows). `createdAt` carries the original creation. A re-request after `terminated` refreshes requestMessage/procurementAreas/expectedVolume/requestedBy + `requestedAt` and clears `decisionReason`.",
      "New `GET /api/internal/v1/relationships/{id}/` detail endpoint (party-scoped; non-party or unknown id -> 404 RELATIONSHIP_NOT_FOUND).",
      "reject/suspend/terminate accept an OPTIONAL `{ \"reason\": \"...\" }` body (<=500) recorded as `decisionReason`; no-body still valid (resume/activate stay no-body). Migration 0028 (additive nullable/blank fields, no backfill).",
      "NOT in this bump: D3.5 email-link confirm (the remaining Slice 2 item) — pending the frontend product-flow answers (who triggers/receives the email, what the link confirms, confirm-after-auth, token validate/confirm, expired-token). Ships as a follow-up (v1.6.4)."
    ],
    "v1.6.2": [
      "REL D3.3-D3.6 relationship lifecycle (Slice 1) — additive over v1.6.1; legacy /api/relationships/ unchanged. GET /api/internal/v1/relationships/ gains `status` (one of requested/active/suspended/terminated; unknown/`confirmed`/multi-value -> 400 VALIDATION_ERROR) and `q` (server-side search by COUNTERPARTY company name: supplier name on buyer-side rows, buyer name on supplier-side rows). Combinable with limit/offset; envelope unchanged.",
      "RelationshipResponse gains `requestedAt` (creation time) and `updatedAt` (ISO 8601). Purely additive; all existing fields (id/buyer/supplier/status/orderEligible/viewerRole) unchanged. v1 only.",
      "New supplier-owned transitions (no-body POST; non-empty JSON body -> 400): POST /relationships/{id}/reject/ (requested->terminated), /suspend/ (active->suspended), /resume/ (suspended->active). A buyer active-company calling these -> 403 SUPPLIER_COMPANY_REQUIRED. Invalid source state -> 400 RELATIONSHIP_NOT_REJECTABLE / RELATIONSHIP_NOT_SUSPENDABLE / RELATIONSHIP_NOT_RESUMABLE.",
      "New POST /relationships/{id}/terminate/ — allowed from EITHER party (buyer or supplier) from requested/active/suspended -> terminated; already terminated -> 400 RELATIONSHIP_ALREADY_TERMINATED. Unknown id / non-party active company -> 404 RELATIONSHIP_NOT_FOUND.",
      "Re-request semantics on POST /relationships/: after `terminated`, a new request to the same supplier revives the SAME relationship row back to `requested` (200, id unchanged) — no new row, the unique (buyer, supplier) pair is preserved. requested/active/suspended stay idempotent (200, existing row returned).",
      "orderEligible stays true only while `active` (suspend/terminate roll it back). Price visibility rule unchanged: prices still require an active relationship AND a published default price list.",
      "Demo seed (seed_frontend_demo) now gives the demo buyer a relationship in every state for QA: none (norel supplier) / requested / active / suspended / terminated.",
      "Out of scope (held for later): email-link confirm D3.5 (= Slice 2; email delivery is already wired on the backend side), in-app notifications, Engine/1C. No model/migration change."
    ],
    "v1.6.1": [
      "BKG-019 country allowlist Baltics (now LIVE): R1 `country` allowlist widened {LV} -> {LV, LT, EE}. POST /api/internal/v1/companies/onboarding/manual/ accepts a required `country` in {LV, LT, EE} and persists it; all app-facing company surfaces return the saved value (onboarding response, /auth/token/me memberships company object, /companies/active/, supplier card). Existing/backfilled rows stay country='LV'.",
      "Unknown/unsupported country -> 400 VALIDATION_ERROR with `details.field = \"country\"` (the onboarding validation error now surfaces the offending field in details). Additive to the error envelope.",
      "Out of scope (unchanged): VAT engine/rules by country, invoices/official docs, localization, reporting/1C, verification workflow expansion. Live Lursoft/registry lookup stays LV-only — LT/EE use manual onboarding; a lookup that returns company.country='LV' is unchanged. No model/migration change (Company.country CharField already fits LT/EE)."
    ],
    "v1.6.0": [
      "BKG-001 pagination/search envelope (now LIVE): the four list endpoints — GET /catalog/items/, /catalog/suppliers/, /orders/, /relationships/ — gain additive `total`/`limit`/`offset` alongside the existing domain key (items/suppliers/orders/relationships). limit default 50, max 200; offset default 0; total = count after filters, before slice. Out-of-range or non-integer limit/offset → 400 VALIDATION_ERROR. Existing v1.5.2 fields are unchanged (purely additive). New search/filter params: /catalog/suppliers/ gains `q` (supplier name); /orders/ gains `status`. Ordering is deterministic: orders newest-first (`-id`), others by id.",
      "BKG-009 procurementArea (now LIVE): new GET /api/internal/v1/catalog/procurement-areas/ dictionary (8-key provisional seed — key, labels{ru,en}, sortOrder, selectable; data-driven reference data, NOT an OpenAPI enum, values validated server-side). Category response gains `procurementArea` (string|null): root = assigned key, child = effective inherited value (derived, read-only). Root category create/PATCH accept optional `procurementArea` (a valid selectable key, or null to clear); setting it on a child category, or an unknown/non-selectable key, → 400 VALIDATION_ERROR. Supplier list rows and the supplier card gain a derived `procurementAreas: string[]` (aggregated from root categories with ≥1 active item in the subtree, deduped, ordered by dictionary sortOrder). New filter `area=<key>` on /catalog/items/ and /catalog/suppliers/ (combinable with q/limit/offset; unknown key → 400). Legacy categories stay null (no backfill).",
      "procurementArea is PLATFORM-only catalog-discovery metadata (RU «Направление»): NOT involved in pricing, relationship lifecycle, cart/checkout eligibility, never copied into order/orderLine snapshots, never in official docs/PDF/VAT/accounting, and never pushed to Engine/1C. The word 'domain' is intentionally not used.",
      "BKG-001 + BKG-009 ship together as the v1.6.0 package, implemented after Stage 2 sign-off (2026-06-12). Additive / backward-compatible — v1.5.2 clients keep working. Supersedes the planned/non-live notes pagination-search-planned-v1.6.0.md and procurement-area-planned-v1.6.0.md (now implemented and live)."
    ],
    "v1.5.2": [
      "Live Lursoft ENABLED ON PRODUCTION (2026-06-11, approved by Sasha): credentials configured on platform-api.nexx.beyondhorizon.dev, live-verified — real LV reg number 40003170000 resolves to SIA \"LURSOFT IT\" (200). DEF-009 lifted: live lookup now works on staging AND production; demo fixture patterns stay mock/quota-safe. Lursoft caveat lines removed from knownLimitations/stillPending. Doc-text only — request/response contract unchanged."
    ],
    "v1.5.1": [
      "Doc-text refresh only — NO schema/behavior change (LURSOFT-003, per frontend request 2026-06-10). Lursoft lookup description, handoff notes, knownLimitations and stillPending updated to reflect the env-gated live adapter (LURSOFT-002): live on staging (nexx-api.srv.acebox.eu); production (platform-api.nexx.beyondhorizon.dev) currently serves mock fixtures — live credentials not yet enabled there (separate go decision). Request/response structure, error codes and verificationMethod unchanged.",
      "openapi regen also picks up GET /api/internal/v1/service/companies/ (SVC-AUTH-001, partner service-token lane via X-API-Key — for NEXX Engine pulls, NOT for the APP UI; APP-facing endpoints unchanged)."
    ],
    "v1.5.0": [
      "D2.8 (PRICE-002): real R1 default price list — new GET/PUT /api/internal/v1/price-lists/default/. One default list per supplier company. Header (name, currency=EUR, effectiveDate, isDefault, isActive) is backed by SupplierProfile default fields; lines are backed by the supplier's CatalogItems (sku/name/unit/category, netPrice, vatRate, backend-computed grossPrice). PUT (COMPANY_ADMIN/MANAGER, supplier/both) updates header + line prices atomically. This SUPERSEDES the v1.2 alt-б note that 'PriceList CRUD is not implemented'.",
      "OpenAPI info.version now tracks the handoff version (was cosmetically 0.1.0) — addresses the D2.12 audit note. New schemas: V1PriceListDetail, V1PriceListLine, V1PriceListLineUpdate, V1PriceListUpdateRequest.",
      "D2.13: PUT /price-lists/default/ forwards a `/price-lists` command to mock BACKEND via engine_contract (mock-v0, non-fatal). SupplierProfile gains additive field default_price_list_name (migration 0025)."
    ],
    "v1.4.0": [
      "D2.5: new GET/POST /api/internal/v1/companies/{id}/warehouses/ — company-structure warehouse. Every company gets a default \"Основной склад\" at onboarding; COMPANY_ADMIN can add more. Path {id} must equal the active company (X-NEXX-Company-ID header). Frontend can now build the company-structure / warehouse UI.",
      "D2.13 (backend-internal, no FE contract change): PLATFORM structural writes (company, warehouse, buyer/supplier profile, category, item, relationship) now forward to mock BACKEND via the engine_contract seam (mock-v0, no field-level payloads, non-fatal).",
      "Lursoft (D2.1/D2.11): live adapter implemented (LURSOFT-002), env-gated and OFF by default — runtime behavior is still mock until live credentials are enabled on the server, so no FE contract change. Manual onboarding fallback unchanged."
    ],
    "v1.3.1": [
      "CAT-005 (SCREEN-005 fix): GET /api/internal/v1/catalog/items/ gains optional `mine=true` owner-listing mode — a supplier or both-role company lists its OWN catalog items INCLUDING inactive, prices always visible, no buyer context required. Fixes supplier-only admins getting 403 BUYER_COMPANY_REQUIRED on Company → My catalog.",
      "?mine=true from a buyer-only company → 403 SUPPLIER_COMPANY_REQUIRED. Default (no mine) buyer-browse behavior is unchanged (CAT-004 price visibility intact).",
      "V1CatalogItem list schema gains `isActive` (boolean, always present; true in buyer-browse, real value in owner-listing).",
      "Owner-listing read is open to any active member of the supplier company (matches existing v1 read pattern, e.g. supplier-profile GET); item create/update/deactivate/reactivate remain COMPANY_ADMIN/MANAGER. Additive, non-breaking."
    ],
    "v1.2.0": [
      "Stage 2 contract bundle: closes D2.1 (Lursoft lookup mock), D2.2 (manual onboarding country + verification_method), D2.3 (BuyerProfile fields + CRUD), D2.4+D2.5+D2.9 (SupplierProfile fields + CRUD + supplier-card profile block), D2.6 (per-supplier Category CRUD with 2-level hierarchy + delete-blocked), D2.7 (CatalogItem CRUD + barcode + immutable SKU + deactivate/reactivate), D2.8 (default price list as SupplierProfile fields, alt-б), D2.10 (server-side buyer-preview hides prices without active relationship), D2.11 (Lursoft graceful fallback via mock-only patterns), D2.14 (seed_frontend_demo extended with Stage 2 fixtures).",
      "12 new v1 endpoints across companies/lursoft-lookup, companies/buyer-profile, companies/supplier-profile, catalog/categories CRUD, catalog/items CRUD + activate toggle.",
      "Company model gains country (R1 allowlist = {LV}) + verification_method (manual|lursoft).",
      "BuyerProfile and SupplierProfile gain typed fields (contact, phone, order email, address, working hours, min order, delivery terms, external IDs); SupplierProfile also has default_currency / default_price_list_effective_date / default_price_list_active (alt-б default-price-list concept).",
      "Category gains supplier_company FK (nullable) + parent self-FK (2-level cap).",
      "CatalogItem gains barcode + external_item_id; v1 mutation endpoints added.",
      "Buyer-side GET /catalog/items/ now hides netPrice/vatRate (returns null + pricesVisible=false) unless buyer has active relationship AND supplier has default_price_list_active=True.",
      "OpenAPI bundle regenerated; 67 paths total covering Stage 1 + Stage 2 contract surface."
    ]
  },
  "summary": {
    "scope": "v1.6.0 package: BKG-001 pagination/search envelope + BKG-009 procurementArea catalog-discovery metadata, implemented after Stage 2 sign-off (2026-06-12). Additive / backward-compatible over v1.5.2 — existing fields and domain keys unchanged.",
    "newEndpoints": 1,
    "modifiedEndpoints": 7,
    "breakingChanges": 0
  },
  "endpoints": {
    "added": [
      {
        "method": "GET/PUT",
        "path": "/api/internal/v1/price-lists/default/",
        "auth": "bearer + X-NEXX-Company-ID (active supplier or both). GET: any active member; PUT: COMPANY_ADMIN or MANAGER.",
        "request": {
          "name": "string",
          "currency": "string (R1 allowlist=EUR; defaults EUR)",
          "effectiveDate": "date|null",
          "isActive": "boolean",
          "lines": [
            {
              "catalogItemId": "int",
              "netPrice": "decimal>=0",
              "vatRate": "decimal>=0"
            }
          ]
        },
        "responses": {
          "200": {
            "id": "int (= supplierCompanyId; facade has no PriceList table)",
            "supplierCompanyId": "int",
            "name": "string",
            "currency": "EUR",
            "effectiveDate": "date|null",
            "isDefault": "true",
            "isActive": "boolean",
            "rowCount": "int (all catalog items)",
            "pricedItemCount": "int (active items)",
            "lines": [
              {
                "catalogItemId": "int",
                "sku": "string",
                "name": "string",
                "unit": "string",
                "categoryName": "string|null",
                "itemActive": "boolean",
                "netPrice": "string",
                "vatRate": "string",
                "grossPrice": "string (net*(1+vat/100), scale 2, ROUND_HALF_UP)"
              }
            ]
          },
          "400": "VALIDATION_ERROR | CATALOG_ITEM_NOT_FOUND (a line references a catalog item not in the active supplier)",
          "401": "INVALID_ACCESS_TOKEN",
          "403": "SUPPLIER_COMPANY_REQUIRED | SUPPLIER_PROFILE_FORBIDDEN_FOR_ROLE (USER role on PUT)"
        },
        "notes": "D2.8 R1 default price list (Option B facade). Header backed by SupplierProfile default fields; lines backed by CatalogItem net/vat (gross computed). One default list per supplier; PUT updates header + item prices atomically and forwards /price-lists to mock BACKEND (D2.13). Buyer price visibility is unchanged — driven by relationship + isActive via the existing buyer catalog/supplier endpoints (CAT-004). See priceListRules block for the 9 acceptance rules."
      },
      {
        "method": "GET/POST",
        "path": "/api/internal/v1/companies/{id}/warehouses/",
        "auth": "bearer + X-NEXX-Company-ID (path {id} must equal the active company)",
        "request": {
          "name?": "string (default \"Основной склад\")",
          "code?": "string",
          "address?": "string"
        },
        "responses": {
          "200": {
            "warehouses": [
              {
                "id": "int",
                "name": "string",
                "code": "string",
                "address": "string",
                "isDefault": "boolean"
              }
            ]
          },
          "201": {
            "warehouse": {
              "id": "int",
              "name": "string",
              "code": "string",
              "address": "string",
              "isDefault": "boolean"
            }
          },
          "400": "VALIDATION_ERROR",
          "401": "INVALID_ACCESS_TOKEN",
          "403": "COMPANY_ADMIN_REQUIRED (POST by non-admin) / ACTIVE_COMPANY_FORBIDDEN (path id != active company)"
        },
        "notes": "D2.5 company structure. One company = one default warehouse (\"Основной склад\") auto-created at onboarding; COMPANY_ADMIN can add more. Structure only — PLATFORM holds no stock/accounting. Create forwards to mock BACKEND (D2.13)."
      },
      {
        "method": "POST",
        "path": "/api/internal/v1/companies/lursoft-lookup/",
        "auth": "bearer (email_verified NOT required)",
        "request": {
          "registrationNumber": "string"
        },
        "responses": {
          "200": {
            "found": "boolean",
            "company": {
              "registrationNumber": "string",
              "name": "string",
              "legalAddress": "string",
              "country": "string (LV|LT|EE)",
              "vatNumber": "string",
              "verificationMethod": "lursoft"
            }
          },
          "400": "VALIDATION_ERROR",
          "401": "INVALID_ACCESS_TOKEN",
          "404": "LURSOFT_NOT_FOUND",
          "503": "LURSOFT_UNAVAILABLE"
        },
        "notes": "Env-gated live (LURSOFT-002): real LV reg numbers use live Lursoft — enabled on staging and production (DEF-009 lifted 2026-06-11); 24h cache + audit on the live path. Demo fixtures stay mock/quota-safe. Fixture patterns: LV-FOUND-1 / LV40001111 -> Mock Trade SIA; LV-FOUND-2 / LV40002222 -> Mock Supplier Food SIA; LV-DOWN-* -> 503; LV-NOTFOUND-* and anything else -> 404."
      },
      {
        "method": "GET",
        "path": "/api/internal/v1/companies/buyer-profile/",
        "auth": "bearer + X-NEXX-Company-ID (active buyer or both)",
        "responses": {
          "200": {
            "companyId": "int",
            "contactPerson": "string",
            "phone": "string",
            "orderEmail": "string",
            "deliveryAddress": "string",
            "workingHours": "string",
            "externalBuyerId": "string"
          },
          "400": "ACTIVE_COMPANY_REQUIRED",
          "401": "INVALID_ACCESS_TOKEN",
          "403": "ACTIVE_COMPANY_FORBIDDEN | BUYER_COMPANY_REQUIRED"
        }
      },
      {
        "method": "PATCH",
        "path": "/api/internal/v1/companies/buyer-profile/",
        "auth": "bearer + X-NEXX-Company-ID (active buyer or both; COMPANY_ADMIN or MANAGER)",
        "request": {
          "contactPerson?": "string",
          "phone?": "string",
          "orderEmail?": "email",
          "deliveryAddress?": "string",
          "workingHours?": "string",
          "externalBuyerId?": "string"
        },
        "responses": {
          "200": "Same shape as GET",
          "400": "VALIDATION_ERROR (unknown key / empty body / invalid email)",
          "403": "BUYER_PROFILE_FORBIDDEN_FOR_ROLE (USER role) | BUYER_COMPANY_REQUIRED"
        }
      },
      {
        "method": "GET",
        "path": "/api/internal/v1/companies/supplier-profile/",
        "auth": "bearer + X-NEXX-Company-ID (active supplier or both)",
        "responses": {
          "200": {
            "companyId": "int",
            "contactPerson": "string",
            "phone": "string",
            "orderEmail": "string",
            "warehouseAddress": "string",
            "workingHours": "string",
            "minOrderAmount": "string|null",
            "deliveryTerms": "string",
            "externalSupplierId": "string",
            "defaultCurrency": "string",
            "defaultPriceListEffectiveDate": "date|null",
            "defaultPriceListActive": "boolean"
          },
          "403": "SUPPLIER_COMPANY_REQUIRED | ACTIVE_COMPANY_FORBIDDEN"
        }
      },
      {
        "method": "PATCH",
        "path": "/api/internal/v1/companies/supplier-profile/",
        "auth": "bearer + X-NEXX-Company-ID (active supplier or both; COMPANY_ADMIN or MANAGER)",
        "request": {
          "contactPerson?": "string",
          "phone?": "string",
          "orderEmail?": "email",
          "warehouseAddress?": "string",
          "workingHours?": "string",
          "minOrderAmount?": "decimal",
          "deliveryTerms?": "string",
          "externalSupplierId?": "string",
          "defaultCurrency?": "string (R1 allowlist=EUR)",
          "defaultPriceListEffectiveDate?": "date|null",
          "defaultPriceListActive?": "boolean"
        },
        "responses": {
          "200": "Same shape as GET",
          "400": "VALIDATION_ERROR (unknown key, empty body, invalid email, unsupported currency, invalid date)",
          "403": "SUPPLIER_PROFILE_FORBIDDEN_FOR_ROLE (USER role) | SUPPLIER_COMPANY_REQUIRED"
        }
      },
      {
        "method": "GET",
        "path": "/api/internal/v1/catalog/categories/",
        "auth": "bearer + X-NEXX-Company-ID (active supplier or both)",
        "responses": {
          "200": {
            "categories": [
              {
                "id": "int",
                "name": "string",
                "slug": "string",
                "parentId": "int|null",
                "itemCount": "int"
              }
            ]
          },
          "403": "SUPPLIER_COMPANY_REQUIRED"
        }
      },
      {
        "method": "POST",
        "path": "/api/internal/v1/catalog/categories/",
        "auth": "bearer + X-NEXX-Company-ID (active supplier or both; COMPANY_ADMIN or MANAGER)",
        "request": {
          "name": "string",
          "parentId?": "int|null"
        },
        "responses": {
          "201": "Single category shape",
          "400": "VALIDATION_ERROR | CATEGORY_PARENT_DEPTH_EXCEEDED | CATEGORY_PARENT_NOT_IN_SUPPLIER",
          "403": "SUPPLIER_PROFILE_FORBIDDEN_FOR_ROLE (USER role)"
        }
      },
      {
        "method": "PATCH",
        "path": "/api/internal/v1/catalog/categories/{id}/",
        "auth": "bearer + X-NEXX-Company-ID (active supplier or both; COMPANY_ADMIN or MANAGER)",
        "request": {
          "name?": "string",
          "parentId?": "int|null"
        },
        "responses": {
          "200": "Updated category shape",
          "400": "VALIDATION_ERROR | CATEGORY_PARENT_DEPTH_EXCEEDED | CATEGORY_PARENT_NOT_IN_SUPPLIER",
          "404": "CATEGORY_NOT_FOUND (also cross-supplier id)"
        }
      },
      {
        "method": "DELETE",
        "path": "/api/internal/v1/catalog/categories/{id}/",
        "auth": "bearer + X-NEXX-Company-ID (active supplier or both; COMPANY_ADMIN or MANAGER)",
        "responses": {
          "204": "Empty leaf deleted",
          "404": "CATEGORY_NOT_FOUND",
          "409": "CATEGORY_HAS_CHILDREN | CATEGORY_HAS_ITEMS"
        }
      },
      {
        "method": "POST",
        "path": "/api/internal/v1/catalog/items/",
        "auth": "bearer + X-NEXX-Company-ID (active supplier or both; COMPANY_ADMIN or MANAGER)",
        "request": {
          "sku": "string",
          "name": "string",
          "description?": "string",
          "unit?": "string",
          "netPrice": "decimal>0",
          "vatRate": "decimal",
          "categoryId": "int",
          "barcode?": "string",
          "externalItemId?": "string"
        },
        "responses": {
          "201": {
            "id": "int",
            "sku": "string",
            "name": "string",
            "description": "string",
            "unit": "string",
            "netPrice": "string (decimal)",
            "vatRate": "string",
            "barcode": "string",
            "externalItemId": "string",
            "isActive": "boolean",
            "supplier": "object",
            "category": "object"
          },
          "400": "VALIDATION_ERROR | CATALOG_ITEM_CATEGORY_NOT_IN_SUPPLIER",
          "409": "CATALOG_ITEM_SKU_TAKEN"
        }
      },
      {
        "method": "PATCH",
        "path": "/api/internal/v1/catalog/items/{id}/",
        "auth": "bearer + X-NEXX-Company-ID (active supplier or both; COMPANY_ADMIN or MANAGER)",
        "request": "Any subset of non-SKU mutation fields; SKU is immutable",
        "responses": {
          "200": "Updated item shape",
          "400": "CATALOG_ITEM_SKU_IMMUTABLE | CATALOG_ITEM_CATEGORY_NOT_IN_SUPPLIER | VALIDATION_ERROR",
          "404": "CATALOG_ITEM_NOT_FOUND (also cross-supplier)"
        }
      },
      {
        "method": "POST",
        "path": "/api/internal/v1/catalog/items/{id}/deactivate/ and /reactivate/",
        "auth": "bearer + X-NEXX-Company-ID (active supplier or both; COMPANY_ADMIN or MANAGER)",
        "responses": {
          "200": "Item shape with isActive toggled (idempotent — no-op if already in target state)",
          "404": "CATALOG_ITEM_NOT_FOUND"
        }
      }
    ],
    "modified": [
      {
        "method": "POST",
        "path": "/api/internal/v1/companies/onboarding/manual/",
        "change": "Request now REQUIRES `country` (R1 allowlist = {LV, LT, EE} since v1.6.1/BKG-019; unknown -> 400 VALIDATION_ERROR details.field=country); accepts optional `verificationMethod` (default 'manual', choices manual|lursoft). Response company object now includes country + verificationMethod.",
        "breaking": true
      },
      {
        "method": "GET",
        "path": "/api/internal/v1/companies/active/ and /auth/token/me/ memberships[].company",
        "change": "Company payload now includes country + verificationMethod fields."
      },
      {
        "method": "GET",
        "path": "/api/internal/v1/catalog/suppliers/{id}/",
        "change": "Supplier card response now includes a profile block exposing contactPerson, phone, orderEmail, warehouseAddress, workingHours, minOrderAmount, deliveryTerms, defaultCurrency, defaultPriceListEffectiveDate, defaultPriceListActive."
      },
      {
        "method": "GET",
        "path": "/api/internal/v1/catalog/items/",
        "change": "Item shape now includes pricesVisible (boolean). When pricesVisible=false (buyer without active relationship to supplier OR supplier has no published default price list), netPrice and vatRate are returned as null instead of numeric strings. Names/SKUs/descriptions remain visible for discovery."
      }
    ]
  },
  "errorCodes": [
    {
      "code": "VALIDATION_ERROR",
      "http": 400,
      "context": "All strict v1 serializers (unknown keys, empty body, type errors)"
    },
    {
      "code": "INVALID_ACCESS_TOKEN",
      "http": 401,
      "context": "Missing or invalid Bearer JWT"
    },
    {
      "code": "ACTIVE_COMPANY_REQUIRED",
      "http": 400,
      "context": "X-NEXX-Company-ID header missing"
    },
    {
      "code": "ACTIVE_COMPANY_FORBIDDEN",
      "http": 403,
      "context": "X-NEXX-Company-ID set but user has no active membership in that company"
    },
    {
      "code": "BUYER_COMPANY_REQUIRED",
      "http": 403,
      "context": "Active company is supplier-only; endpoint requires buyer or both"
    },
    {
      "code": "SUPPLIER_COMPANY_REQUIRED",
      "http": 403,
      "context": "Active company is buyer-only; endpoint requires supplier or both"
    },
    {
      "code": "BUYER_PROFILE_FORBIDDEN_FOR_ROLE",
      "http": 403,
      "context": "USER role attempts PATCH /companies/buyer-profile/"
    },
    {
      "code": "SUPPLIER_PROFILE_FORBIDDEN_FOR_ROLE",
      "http": 403,
      "context": "USER role attempts PATCH /companies/supplier-profile/ or category/item mutation"
    },
    {
      "code": "LURSOFT_NOT_FOUND",
      "http": 404,
      "context": "POST /companies/lursoft-lookup/ — registrationNumber not in mock fixture set"
    },
    {
      "code": "LURSOFT_UNAVAILABLE",
      "http": 503,
      "context": "POST /companies/lursoft-lookup/ — mock LV-DOWN-* pattern (Lursoft outage simulation)"
    },
    {
      "code": "CATEGORY_NOT_FOUND",
      "http": 404,
      "context": "PATCH/DELETE /catalog/categories/{id}/ — unknown or cross-supplier id"
    },
    {
      "code": "CATEGORY_HAS_ITEMS",
      "http": 409,
      "context": "DELETE category with CatalogItem rows referencing it"
    },
    {
      "code": "CATEGORY_HAS_CHILDREN",
      "http": 409,
      "context": "DELETE category that has child categories"
    },
    {
      "code": "CATEGORY_PARENT_DEPTH_EXCEEDED",
      "http": 400,
      "context": "Category hierarchy >2 levels"
    },
    {
      "code": "CATEGORY_PARENT_NOT_IN_SUPPLIER",
      "http": 400,
      "context": "parentId belongs to different supplier"
    },
    {
      "code": "CATALOG_ITEM_NOT_FOUND",
      "http": 404,
      "context": "PATCH/deactivate/reactivate item — unknown or cross-supplier id"
    },
    {
      "code": "CATALOG_ITEM_SKU_TAKEN",
      "http": 409,
      "context": "POST catalog/items with SKU already used by this supplier"
    },
    {
      "code": "CATALOG_ITEM_SKU_IMMUTABLE",
      "http": 400,
      "context": "PATCH /catalog/items/{id}/ tried to change SKU"
    },
    {
      "code": "CATALOG_ITEM_CATEGORY_NOT_IN_SUPPLIER",
      "http": 400,
      "context": "categoryId belongs to different supplier"
    },
    {
      "code": "STAFF_REQUIRED",
      "http": 403,
      "context": "GET /api/internal/v1/admin/logs/ — authenticated user is not Django staff/superuser (SEC-001)"
    },
    {
      "code": "ORDER_SELECTION_INVALID",
      "http": 400,
      "context": "POST /orders/ supplierCompanyIds is empty or contains a supplier not present in the current cart"
    },
    {
      "code": "ORDER_ACTION_FORBIDDEN_FOR_ROLE",
      "http": 403,
      "context": "USER role attempted a mutating order action (submit/cancel/confirm/ship/deliver/accept); requires COMPANY_ADMIN or MANAGER"
    }
  ],
  "models": {
    "added": [
      "Company.country (CharField max_length=2; default 'LV'; R1 allowlist {LV, LT, EE} since v1.6.1/BKG-019)",
      "Company.verification_method (manual | lursoft; default manual)",
      "BuyerProfile.{contact_person, phone, order_email, delivery_address, working_hours, external_buyer_id}",
      "SupplierProfile.{contact_person, phone, order_email, warehouse_address, working_hours, min_order_amount, delivery_terms, external_supplier_id, default_currency, default_price_list_name, default_price_list_effective_date, default_price_list_active}",
      "Category.{supplier_company FK (CASCADE, nullable), parent self-FK (PROTECT, nullable)}",
      "CatalogItem.{barcode, external_item_id}"
    ],
    "dataMigration": "Migrations 0017-0025 are all additive with safe defaults (0023 LursoftLookupAudit, 0024 Warehouse, 0025 SupplierProfile.default_price_list_name=blank). No data migration of prices for the D2.8 facade. Existing prod rows backfill: country='LV', verification_method='manual'; all new profile / catalog fields blank-default; existing 3 demo seed categories keep supplier_company=NULL until SEED-002 / seed_frontend_demo run."
  },
  "knownLimitations": [
    "Photos on CatalogItem are NOT implemented in R1 — the frontend item form should not draw a photo upload widget as live; mark as Preview if rendered.",
    "Currency allowlist is {EUR} in R1; expansion is a 1-line constant change.",
    "Country allowlist is {LV} in R1; expansion is a 1-line constant change.",
    "D2.8 R1 default price list IS implemented as GET/PUT /api/internal/v1/price-lists/default/ (Option B facade): header backed by SupplierProfile default fields, lines backed by the supplier's CatalogItems (net/vat, backend-computed gross). There is NO separate PriceList/PriceListLine table — a 'row' is a catalog item with a price, and editing a line edits the underlying CatalogItem. One default list per supplier. Canonical PriceList tables + personal/per-buyer price lists remain Phase 2 (Option A).",
    "Audit log of profile/category/item edits is request-level only (RequestLog middleware); no business-event log table in R1."
  ],
  "demoFixtures": {
    "command": "docker exec docker-nexx-app-1 python manage.py seed_frontend_demo (staging) or ../.venv/bin/python manage.py seed_frontend_demo (local)",
    "lursoftPatterns": {
      "LV-FOUND-1 / LV40001111": "200 -> Mock Trade SIA",
      "LV-FOUND-2 / LV40002222": "200 -> Mock Supplier Food SIA",
      "LV-DOWN-*": "503 LURSOFT_UNAVAILABLE",
      "LV-NOTFOUND-* (and any other input)": "404 LURSOFT_NOT_FOUND"
    },
    "publishedPriceList": [
      "DEMO-SUP-FOOD-001 (10 dairy items)",
      "DEMO-BOTH-001 (1 bakery item)"
    ],
    "unpublishedPriceList": [
      "DEMO-SUP-FRESH-001 (CAT-004 preview fixture; 1 produce item)"
    ],
    "blankProfiles": [
      "DEMO-BUYER-001 BuyerProfile defaults — profile-needing demo path"
    ],
    "relationships": "Existing matrix exercises both branches: buyer_demo->supplier_food=ACTIVE (sees prices), buyer_demo->supplier_fresh=REQUESTED (no prices), buyer_demo->both_trade=ACTIVE (sees prices), both_trade->supplier_fresh=ACTIVE (no prices because no published list)"
  },
  "priceListRules": {
    "uniqueness": "Exactly one default price list per supplier company (1:1 with SupplierProfile).",
    "activeSemantics": "isActive=false hides buyer prices (drives SupplierProfile.default_price_list_active; buyer visibility via CAT-004).",
    "effectiveDate": "R1 metadata only — NOT a hide-until date.",
    "missingLine": "Every catalog item is a line; an active priced item is buyer-visible, an inactive item is hidden from buyers.",
    "inactiveItem": "Inactive catalog items remain rows server-side (itemActive=false) but are hidden from buyers.",
    "relationshipGating": "Buyer sees prices only when relationship is ACTIVE and the default price list isActive (unchanged CAT-004).",
    "orderSnapshot": "Order lines snapshot net/vat at order creation (ORD-001); later price-list edits do NOT mutate historical orders.",
    "permissions": "GET: any active member of the supplier/both company. PUT: COMPANY_ADMIN/MANAGER. Buyer-only company -> 403 SUPPLIER_COMPANY_REQUIRED; USER role on PUT -> 403 SUPPLIER_PROFILE_FORBIDDEN_FOR_ROLE.",
    "delete": "R1 has no hard delete — 'remove/unpublish' = PUT with isActive=false.",
    "migrationStrategy": "Option B facade: lines map to existing CatalogItem net/vat, header maps to SupplierProfile default fields. No data migration. Canonical PriceList tables (Option A) deferred to Phase 2.",
    "mockBackend": "PUT /price-lists/default/ forwards command family `/price-lists` to mock BACKEND via engine_contract (mock-v0, no field-level payload, non-fatal — never blocks the user write). Proven by core/tests/test_api_v1_price_lists_endpoint_api.py and test_mock_backend_stage2_commands.py."
  },
  "ttlAndCooldownDefaults": {
    "verifyTokenSeconds": 86400,
    "passwordResetTokenSeconds": 3600,
    "invitationTokenSeconds": 604800,
    "resendCooldownSeconds": 60
  },
  "deepLinkPaths": {
    "emailVerify": "{NEXX_PUBLIC_APP_URL}/auth/verify-email?token=<raw>",
    "passwordReset": "{NEXX_PUBLIC_APP_URL}/auth/password-reset/set?token=<raw>",
    "invite": "{NEXX_PUBLIC_APP_URL}/auth/invite?token=<raw>"
  },
  "stillPending": [
    "DONE (SEC-001): /api/internal/v1/admin/logs/ is now staff/superuser-only (403 STAFF_REQUIRED otherwise). PRE-PILOT residual: remove the public dashboard log-viewer login — status/index.html ships a demo.platform.operator staff login in page source for staging convenience; drop it (and ideally the viewer) before pilot.",
    "Photos on CatalogItem (deferred R1 — see knownLimitations).",
    "Canonical PriceList model (Option A) + per-buyer price lists — Phase 2 (R1 ships the Option B default-price-list facade at /price-lists/default/).",
    "Live Engine/1C field contract (DEF-008; mock-v0 only via ENG-CONTRACT-001).",
    "Audit log table for business events (request-level only in R1)."
  ],
  "references": {
    "githubIssue": "https://github.com/grabbly/nexx-platform-backend/issues/5",
    "telegramThread": "Stage 2 coordination via Codex front-bridge msg #140 (2026-05-23 19:23) + msg #141 (2026-05-23 20:13)",
    "frontendCoordPacket": "shao3d/NEXX-frontend memory-bank/tasks/2026-05-23-FE-STAGE2-COORD-001-stage2-design-frontend-backend-sync/",
    "kostyaDocs": "NEXX_User_Stories_MVP.md (30042026 customer SSOT) Stage 2 sections US-ONB-*, US-PROF-*, US-CAT-*, US-PRC-*",
    "scopeDecisions": "Stage 2 backend task folders 2026-05-25-*/task.md record per-task scope decisions confirmed by user (defaults + alt-б on D2.8 PriceList)"
  }
}
