Files
marketplaces/docs/backend/PHASE-10-CONTENT-MODULES-CONTRACT.md

139 lines
5.2 KiB
Markdown
Raw Normal View History

# 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.
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
## 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.