Files
marketplaces/docs/backend/PHASE-10-CONTENT-MODULES-CONTRACT.md
sdarbinyan f9e09b1757 docs(backend): harvest platform mechanisms into the contracts (Wave 2, FH-E.1-E.4)
Writes the 14 harvested mechanisms from FORK-ANALYSIS-2026-08-21.md into
the backend contracts. Each section is dated 2026-08-21 and tagged FH-*
so any wording traces back to why it is worded that way.

The through-line: several contracts stated correctness as behaviour
("the webhook must be idempotent"). Behaviour written as an if-statement
gets deleted by a refactor and the failure mode is a double charge. These
sections restate it as schema and mechanism.

PHASE-3  3.1 conditional-write reservation, 409 on zero rows, cart-wide
             rollback, 15 min TTL
         3.2 InventoryMovement append-only journal with resultingAvailable
         6   bulk import idempotent by SKU, rollback while unsold
         6a  digital code pools, revealed only when paid
PHASE-7  5   unique constraints for payment idempotency and webhook
             replay, insert-first handling, signature over raw body,
             24h poll as reconciliation not primary
TRACK-S  2.1 session model - 32 bytes stored as SHA-256 only, HttpOnly,
             one cookie per contour, Argon2id params, mandatory TOTP
         2.2 origin allowlist ahead of routing on every cookie mutation
         4.2 AES-256-GCM envelope for stored secrets, HMAC fingerprints
         8a  order manager as a separate contour, scoped by membership
             rows rather than by configuration
PHASE-9  5.1 revision immutability, version = max+1, pointer flipped
             in-transaction, operational state does not travel
         5.2 clone carry / no-carry list, inventory to zero
         5.3 signed read-only preview, non-GET 404s while previewing
         6   host normalization, verifiedAt required, cache invalidation
PHASE-10 3a  server re-runs the editor's validation, clamp-and-fallback
PHASE-2  3.1 order publicToken, snapshot completeness, never updated

FH-2.12 rejected on the merits: our marketplace lifecycle state machine
is richer than theirs, adopting it would be a downgrade. Recorded in the
TODO so it is not raised again.

Also adds BACKEND-HANDOFF.md sections 0 and 0a - nine falsifiable
invariants as a release gate, each cross-referenced to the contract that
specifies it, plus PR and release discipline. And ADR-0006 recording what
we take, what we reject, what we keep because ours is better, and the
organizational question it deliberately does not settle.

No implementation changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 11:12:05 +04:00

139 lines
5.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 10 Backend Contract — Tenant Content Modules (Gorbushka-class tenants)
Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Phase 10 (Sprints 10.110.2). Covers plan §11.
**Status: ready to build, lowest priority.** Only after Commerce Core (Phases 17) is real — the plan is explicit that this tenant type does not define the platform architecture; it is one configuration of the shared runtime, not a separate build.
---
## 1. Entities
```ts
interface Shop {
id: string;
marketplaceId: string;
shopCategoryId: string;
name: string;
floorId?: string;
status: 'draft' | 'published';
}
interface ShopCategory {
id: string;
marketplaceId: string;
title: string;
}
interface Service {
id: string;
marketplaceId: string;
title: string;
description: string;
status: 'draft' | 'published';
}
interface Floor {
id: string;
marketplaceId: string;
order: number;
label: string;
}
interface SchemePin {
id: string;
marketplaceId: string;
floorId: string;
shopId?: string;
x: number;
y: number;
}
interface RentListing {
id: string;
marketplaceId: string;
title: string;
areaSqm: number;
floorId?: string;
status: 'available' | 'leased';
}
interface Lead {
id: string;
marketplaceId: string;
rentListingId?: string;
contactName: string;
contactPhone: string;
message?: string;
createdAt: string;
}
interface NewsPromo {
id: string;
marketplaceId: string;
title: string;
body: string;
publishedAt?: string;
}
interface MallSettings {
marketplaceId: string;
openingHours: Record<string, string>;
contactInfo: Record<string, string>;
}
```
Every entity above carries `marketplaceId`, an audit trail, and the same draft/preview/publish flow as [Phase 9's revision model](PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md) §5 — not a separate content pipeline.
## 2. Endpoints
```
GET/POST/PATCH/DELETE /api/admin/v2/content/shops
GET/POST/PATCH/DELETE /api/admin/v2/content/shop-categories
GET/POST/PATCH/DELETE /api/admin/v2/content/services
GET/POST/PATCH/DELETE /api/admin/v2/content/floors
GET/POST/PATCH/DELETE /api/admin/v2/content/scheme-pins
GET/POST/PATCH/DELETE /api/admin/v2/content/rent-listings
POST /api/admin/v2/content/rent-listings/{id}/leads
GET/POST/PATCH/DELETE /api/admin/v2/content/news
PATCH /api/admin/v2/content/mall-settings
```
## 3. Tenant feature configuration (Gorbushka's v1 default, per plan §11.1)
```json
{
"cms": true, "shops": true, "services": true, "mallScheme": true,
"rentListings": true, "news": true, "seoMedia": true,
"catalog": false, "sellerPortal": false,
"cart": false, "checkout": false, "payments": false, "orders": false
}
```
Commerce modules are **platform-ready but off** — the point of Phase 10 is proving this tenant can flip `catalog`/`cart`/`checkout`/etc. to `true` later via [Phase 9's `MarketplaceFeatureSet`](PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md) with zero backend or storefront code changes, since the commerce core is already generic by the time Phase 10 starts.
## 3a. The server re-validates everything the editor validates
Added 2026-08-21 (FH-2.10). Our storefront editor has the stronger validation engine of the two implementations reviewed — blockers, warnings and informational notices, evaluated before publish. It all runs in the browser, which means it constrains the editor UI and nothing else. Anyone with a session and `curl` bypasses the entire thing.
The backend re-runs the same rules on write. It is the only copy that binds.
Ergonomics matter here, because a validator that hard-fails on cosmetic input makes the editor unusable. Follow clamp-and-fallback:
- Numbers outside their allowed range are **clamped** to the range, not rejected.
- A colour that is not a valid hex value falls back to the documented default.
- A URL is accepted only if it is a same-origin path (`/…`, not `//…`, no backslashes) or `https://`. Anything else is stored as empty, not stored as given.
- Free text is trimmed and truncated at its documented maximum.
- Structural violations — an unknown block type, a malformed id, more blocks than the page allows, more referenced entity ids than the list allows — are a `400`. These cannot be silently coerced into something meaningful.
Hard limits belong in this contract rather than in the client: maximum blocks per page, maximum referenced ids per block, maximum length per text field. Publish it as one schema and let both sides read it, so the editor and the server cannot drift.
Referential checks run at publish, not on every keystroke: a block pointing at a deleted category or an unpublished offer is a publish **blocker** unless the block declares a fallback. This is the same rule as [Phase 3 §5](PHASE-3-CATALOG-OFFER-FULFILLMENT-CONTRACT.md) publish-time executability, applied to content instead of offers.
**Acceptance:** a hand-crafted API call cannot store a configuration the editor would have refused.
## 4. What the frontend will start doing once this ships
- Mall scheme / floor / pin editor UI.
- Rent listing + lead capture forms.
- Confirm the existing Gorbushka frontend/archive is used as UX reference only — production data and auth route through the shared platform per ADR-0001.