Database design
System-wide data model of the HOCO ecosystem: conventions, domain map, core diagram, numbering, migrations, seeds, indexing, volumes, retention and backups.
Status — baseline v0, to be frozen in SRV-02
This is the data-model baseline that SRV-02 implements as the Prisma 7 schema and its first migration. It already applies the product decisions of 17 Oct 2026 (AUTHORING §1b: three customer-type prices, attributes and facets, support regions, after-sales and returns in V1, availability-only stock). The API reference mirrors these pages field for field; any change after the freeze goes through an expand/contract migration and an update of both.
PostgreSQL 17 is the single database of the ecosystem (cahier §2). It holds 106 models: 96 created by the V1 migration and 10 documented as the V2 baseline (not created before P4). Every model below uses exactly the canonical names of AUTHORING §3.
Conventions
Schema layout and tooling
- Prisma 7, multi-file schema: one file per domain page of this section, plus
schema.prisma(generatorprisma-clientwith an explicitoutput, datasourcepostgresql) andenums.prisma(every enum, mirroringcontent/spec/enums.json).prisma.config.tspointsschematoprisma/schemaandmigrations.pathtoprisma/migrations; the connection URL comes from the environment and the client uses the@prisma/adapter-pgdriver adapter. - Names: models PascalCase singular (
OrderItem); tables snake_case plural through@@map("order_items"); every camelCase field maps to its snake_case column through@map(subtotalHt→subtotal_ht) — this rule is mechanical, so column names are not repeated in the tables; Postgres enum types are snake_case singular through@@map(OrderStatus→order_status); enum values areUPPER_SNAKE_CASEexactly as in enums.json, identical in the database, the API and the apps. - Prisma relation fields are named after the foreign key without
Id(resellerId→reseller,assignedDelegateId→assignedDelegate); every model page lists them with theironDelete.
Keys and identifiers
- Primary key:
id String @id @default(uuid(7)) @db.Uuid. UUIDv7 is time-ordered, soORDER BY id DESCis a valid cursor for newest-first lists and B-tree inserts stay local. - Client-generated ids (offline creation, AUTHORING §3): Reseller (field prospects), Visit (ad-hoc), VisitPhoto, VisitReport, Interaction (notes), ResellerPhoto and File accept a UUIDv7 sent by the app; the server checks the version, upserts idempotently and answers
409if the id exists for another creator. - Exceptions:
Wilaya.code(official code),DelegateProfile.id(= the delegate'sUser.id),Setting.key,NumberSequence(key,year) and the link tables with a composite primary key — RolePermission, UserRole, DelegateZone, ResellerSegment, Favorite, AnnouncementRead, SupportRegionWilaya. - Business numbers come from NumberSequence (see numbering).
Timestamps
createdAt DateTime @default(now())andupdatedAt DateTime @updatedAton every table, with three deliberate exceptions: append-only and link rows have noupdatedAtbecause they are never updated — RolePermission, UserRole, ZoneArea, DelegateZone, ResellerStatusChange, ResellerAssignment, ResellerSegment, ProductVisibilityRule, Favorite, PriceChange, StockMovement, OrderStatusChange, OrderChange, InvoiceLine, CreditLimitChange, PromotionScope, PromotionAudience, PackItem (V2), LoyaltyTransaction (V2), TourStatusChange, VisitPhoto, LocationPing, InvitationEvent, TicketMessage, SupportRegionWilaya, AuditLog; Setting has nocreatedAt(scaffold shape); AnnouncementRead usesreadAtas its creation time.- Every
DateTimeis@db.Timestamptz(3)(UTC instant); date-only values are@db.Date(tourDate,dueDate,periodStart). Business days are computed inAfrica/Algiers. - Offline actions keep two times:
occurredAt(device) and the server receipt time. On append-only rows the receipt time iscreatedAt— exposed by the API asreceivedAt; on rows that change state (Visit) the key receipt time has its own column (checkInReceivedAt).
Value types
| Kind | Type | Rule |
|---|---|---|
| Money | Int | Integer centimes of DZD, HT unless the column name ends in Ttc or is a VAT amount (vatTotal, vatAmount). Int holds 21 474 836,47 DA per amount, enough for any order, invoice or payment; sums are computed as bigint. Exception: Objective.targetValue is BigInt. |
| Quantities | Int | Units; signed only in ledgers (StockMovement.quantity, V2 LoyaltyTransaction.points). |
| Percentages | Int | Basis points: 1900 = 19 %. Columns end in Bp or are named vatRate. |
| Coordinates | Decimal @db.Decimal(9,6) | WGS 84 degrees in lat/lng pairs (checkInLat, checkInLng…), always both or neither (CHECK); accuracy and distances in metres as Int (…AccuracyMeters, distanceMeters). The API serialises them as JSON numbers. |
| Phones | String @db.VarChar(16) | E.164 after normalisation (+213555123456). |
| E-mails | String @db.VarChar(254) | Lower-cased on write. |
| Codes and numbers | @db.VarChar(n) / @db.Char(n) | Length is part of the contract (REV-000123, invitation codes, hashes). Other text is text, its maximum length enforced by Zod. |
| Enums | Prisma enum | Named exactly as in enums.json; new enums proposed by this design are listed in proposed enums. |
| Arrays | String[], String[] @db.Uuid, Enum[] | Only for tags, target lists and attachment ids; arrays of ids have no foreign key and are validated on write. |
| JSON | Json | Only where documented on the model page, each with a named Zod schema in packages/contracts (see JSON columns). |
Deletion, history and concurrency
- No hard deletes of business records. Records with a lifecycle change status (
DISABLED,ARCHIVED,CANCELLED…); reference and configuration rows getarchivedAtorisActive = false; nothing referenced is ever removed (SEC-06). - Rows that may be deleted (technical or link rows, with the change audited where it matters): Session, Verification, Device, CartItem, Favorite, ProductImage, ProductVisibilityRule, RolePermission, UserRole, DelegateZone, ZoneArea, ResellerSegment, SupportRegionWilaya, PaymentAllocation (replaced as a set), PromotionScope and PromotionAudience of an unpublished promotion, and a
DRAFTassisted order discarded by its delegate. onDeletepolicy:Cascadeonly for composition (lines of an order or invoice, items of a cart, areas of a zone…),SetNullfor optional pointers to replaceable rows (avatar or image files, templates, devices),Restricteverywhere else. Since parents are never deleted,Cascadefires only on the allowed deletions above.- History tables: ResellerStatusChange, ResellerAssignment, OrderStatusChange, OrderChange, TourStatusChange, StockMovement, PriceChange, CreditLimitChange, InvitationEvent, AuditLog (and V2 LoyaltyTransaction) — see the lifecycles page.
- Optimistic locking with
version Int @default(1)on Order, Tour, Visit, CreditAccount (and Reseller, edited offline by delegates): every update is… SET version = version + 1 WHERE id = :id AND version = :expected; zero rows →409 VERSION_CONFLICT(or a syncCONFLICTitem). Counters that change under concurrency (stock levels, promotion caps, number sequences) use atomicUPDATE … SET x = x + :ninstead.
JSON columns
| Column | Content | Zod schema |
|---|---|---|
Product.specs | technical characteristics, display order | ProductSpecs |
AttributeDefinition.allowedValues | values of an ENUM attribute | AttributeAllowedValues |
Order.addressSnapshot · appliedPromotions · controlResults | frozen delivery address · promotions applied · last control run | AddressSnapshot · AppliedPromotions · OrderControlResults |
OrderStatusChange.metadata · OrderChange.before/after | transition details · modification diff | OrderTransitionMeta · OrderChangeDiff |
Tour.summary | frozen closing report | TourSummary |
VisitType.reportFields · ChecklistTemplate.items | report field configuration · checklist items | VisitReportFieldConfig · ChecklistItems |
Notification.data | deep link and context | NotificationData |
InvitationEvent.metadata | raw attribution data | InvitationAttribution |
Setting.value | one schema per key | SettingsRegistry |
AuditLog.diff · IdempotencyRecord.responseBody · OutboxEvent.payload | before/after · stored response · versioned event | AuditDiff · (response schema) · one per event key |
ExportJob.filters · ImportJob.report · DashboardLayout.cards | export filters · row report · role cards | per report · ImportReport · DashboardCards |
Derived values that are not stored
| Value | Computed from | Why not stored |
|---|---|---|
| Available stock | physicalQty − reservedQty | two columns written in the same statement — a third could only drift; expression index covers alerts |
| Reseller-facing availability | available > 0 → IN_STOCK, else OUT_OF_STOCK | quantities never leave the back-office |
| Caller's price | variant column of the reseller's customerType, then promotions | depends on the caller; guests get none |
| Invoice balance | totalTtc + stampDuty − amountPaid − amountCredited | cheap, single writer of the inputs |
| Credit used and available | open CREDIT invoice balances + un-invoiced CREDIT orders | spans billing and orders; computed under a row lock |
| Overdue task · late visit · expired invitation · not-started tour | status + dates | rule 6 of the lifecycles page |
| Objective achievement · KPIs | counts over visits, orders, invitations | late syncs must always be reflected |
| Facet counts · category counts | visible products per filter | per caller type; cached 5 min |
Stored caches are named as such on their model page and have a single writer in the same transaction as their inputs: Reseller.lastOrderAt/lastVisitAt/supportRegionId, Invoice.amountPaid/amountCredited, CreditNote.amountApplied, Product.popularityScore, Promotion.usedQuantity, Visit.durationSeconds, Ticket.lastMessageAt, the Invitation step columns, V2 LoyaltyAccount.pointsBalance.
Domain map
Core diagram
The core entities and how a point of sale, its orders and the field work connect. Each domain page has its own detailed diagram.
Business numbers
Human-readable numbers are allocated by NumberSequence inside the transaction that creates the record (INSERT … ON CONFLICT … DO UPDATE SET value = value + 1 RETURNING value): concurrent allocations serialise on one row and a rollback leaves no gap. Yearly sequences use the Africa/Algiers year; padding grows beyond 6 digits instead of wrapping.
| Record | Column | Format | Sequence key · year | Allocated when |
|---|---|---|---|---|
| Reseller | Reseller.number | REV-000123 | RESELLER · 0 | first received by the server |
| Delegate | DelegateProfile.code | DLG-0007 (proposal) | DELEGATE · 0 | profile creation |
| Order | Order.number | CMD-2026-000158 | ORDER · year | the order leaves DRAFT |
| Delivery note | Delivery.number | BL-2026-000123 | DELIVERY_NOTE · year | delivery note generated (READY) |
| Invoice | Invoice.number | FAC-2026-000123 | INVOICE · year | invoice issued — gapless |
| Payment | Payment.number | PAY-2026-000077 | PAYMENT · year | payment recorded or declared |
| Credit note | CreditNote.number | AVO-2026-000004 (proposal) | CREDIT_NOTE · year | credit note issued — gapless |
| Tour | Tour.number | TRN-2026-000045 | TOUR · year | tour planned |
| Visit | Visit.number | VIS-2026-001234 | VISIT · year | first received by the server |
| Ticket | Ticket.number | TKT-2026-000045 | TICKET · year | ticket opened |
| Return | ReturnRequest.number | RET-2026-000012 | RETURN · year | return requested |
Invitation codes are not sequential: 8 random characters without ambiguous letters, unique (Invitation.code).
Proposed enums
The design needs categorical columns that enums.json does not define yet. They are Prisma enums with these values; French labels are to be added to enums.json by Anis before SRV-02.
| Enum | Values | Used by | Meaning |
|---|---|---|---|
DevicePlatform | ANDROID, IOS, WEB | Device.platform, AppVersionPolicy.platform | Platform of a client. The X-App-Platform header values android / ios map to ANDROID / IOS; WEB is used by the reseller web and back-office version policy. |
DelegateStatus | ACTIVE, ON_LEAVE, INACTIVE | DelegateProfile.status | Field availability of a delegate, distinct from the login status (User.status): ON_LEAVE blocks tour planning and is the trigger for temporary reassignment; INACTIVE = left the field team (account usually DISABLED). |
LocationSource | FIELD_SIGNUP, CHECK_IN_VALIDATED, BACK_OFFICE, IMPORT | Reseller.locationSource | Who set a store's position (SRV-42); same values as the API LocationSource. |
StatusChangeTrigger | MANUAL, AUTOMATIC, IMPORT | ResellerStatusChange.trigger | What caused a reseller status change: a person, an event/job (with eventKey), or an import (names as on the lifecycles page). |
PriceListType | AGREEMENT, LEVEL | PriceList.type (V2) | V2 (PRC-03): AGREEMENT = per-reseller agreement referenced by the V2 column Reseller.priceListId; LEVEL = loyalty-level prices. |
AttributeType | TEXT, NUMBER, BOOLEAN, ENUM | AttributeDefinition.type | Value type of a filterable attribute (CAT-13): free text, number with an optional unit, yes/no, or one of allowedValues. |
AttributeFilterWidget | CHECKBOXES, RANGE, TOGGLE, SWATCHES, NONE | AttributeDefinition.filterWidget | How the filter is drawn on web and mobile (CAT-12): value checkboxes with counts, a min–max range (numbers), a yes/no toggle, colour swatches. |
VisibilityRuleEffect | ALLOW, DENY | ProductVisibilityRule.effect | ALLOW rules turn a product into a whitelist for the matching resellers; DENY rules hide it from them. |
PriceField | PRICE_SUPER_GROS, PRICE_DEMI_GROS, PRICE_DETAIL, RECOMMENDED_PRICE, PURCHASE_PRICE, VAT_RATE | PriceChange.field | Which price a PriceChange row records. V2 adds AGREEMENT_PRICE and AGREEMENT_DISCOUNT for PriceListItem changes. |
InventorySessionStatus | OPEN, IN_REVIEW, VALIDATED, CANCELLED | InventorySession.status | OPEN while lines are counted; VALIDATED once the differences were posted as movements; CANCELLED discards the counts. |
CreditOverLimitPolicy | BLOCK, REQUIRE_APPROVAL | CreditAccount.overLimitPolicy | What happens when an order would exceed the available credit (cahier §4.7): BLOCK refuses checkout on CREDIT (422 CREDIT_LIMIT_EXCEEDED); REQUIRE_APPROVAL lets it through as PENDING with the reason CREDIT_OVER_LIMIT for a finance decision. |
BannerPlacement | HOME_CAROUSEL, HOME_DAILY_OFFER, CATALOG_TOP, PROMOTIONS_TOP | Banner.placement | Where a banner is shown: the reseller home carousel (RA-09, RW-02) or the top of the promotions screen (RA-24). |
RedemptionStatus | PENDING, FULFILLED, CANCELLED | RewardRedemption.status | V2 — a redemption is PENDING until the reward is handed over (gift, early access) or the voucher issued; CANCELLED refunds the points. |
TicketPriority | LOW, NORMAL, HIGH, URGENT | Ticket.priority | Back-office triage priority (admin.tickets.update — assign, change status or priority). |
NotificationDeliveryStatus | QUEUED, SENT, DELIVERED, FAILED, SKIPPED | NotificationDelivery.status | Outcome of one channel delivery: SKIPPED = opted out, no device token or channel disabled; DELIVERED only when the provider reports it (SMS receipts). |
FileVisibility | PUBLIC, PRIVATE | File.visibility | PUBLIC files (product images, brand logos, banners) are served by the CDN; PRIVATE ones only through 5-minute signed URLs after an access check (SEC-04). Fixed by the purpose. |
FileStatus | PENDING, READY, REJECTED, DELETED | File.status | PENDING after files.createUpload; READY after files.completeUpload checks size, type and checksum; REJECTED when they fail; DELETED when the object was purged (the row stays). |
ImportType | PRODUCTS, RESELLERS, USERS, STOCK | ImportJob.type | The four initial/recurring imports of cahier §13.2 and ADM-06. |
Migration strategy
- Prisma Migrate only.
prisma migrate devlocally,prisma migrate deployas the release step of theapiservice on Dokploy before the new image takes traffic; theworkerwaits for it. - Never edit an applied migration. A mistake is fixed by a new migration. CI runs
prisma migrate diffbetween the migrations folder and the schema and fails on drift. - Expand / contract for every breaking change: add the new column or table (nullable or with a default) → deploy code that writes both → backfill in a job → switch reads → enforce
NOT NULL/ drop the old column in a later release. Renames are expand/contract too, never in place. - Raw SQL inside migrations for what the Prisma schema cannot express — created with
prisma migrate dev --create-onlyand edited before applying: extensionspg_trgmandunaccentwith the immutable wrapper below, everyCHECKconstraint, every partial or expression index, BRIN indexes and theINSERT/SELECT-only grant onaudit_logs. Each model page shows its raw SQL; the same statements are listed in a comment at the top of the corresponding.prismafile so the schema stays self-explanatory. - Enum changes: adding a value is a migration (
ALTER TYPE … ADD VALUE, outside a transaction block); a value is never removed while referenced. - V2 tables (PriceList, PriceListItem, Pack, PackItem, loyalty, PaymentReminder) and the V2 columns mentioned on the model pages arrive with the P4 migrations, not in V1.
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS unaccent;
-- immutable wrapper so unaccent() can be used in index expressions
CREATE OR REPLACE FUNCTION f_unaccent(text) RETURNS text
LANGUAGE sql IMMUTABLE PARALLEL SAFE STRICT
AS $$ SELECT public.unaccent('public.unaccent'::regdictionary, $1) $$;Seed plan
Seeds are idempotent upserts on natural keys, split in two scripts: reference (every environment, re-runnable after each release) and demo (local and staging only, refused when the environment is production).
| Data | Volume | Source and rule |
|---|---|---|
| Wilayas | 69 | Official list in force: 58 wilayas plus the 11 created by the 2026 territorial reorganisation (law of April 2026 amending law 84-09, Journal officiel n° 25 of 2026; names and seats by executive decree 26-206 of 25 May 2026, Journal officiel n° 40), codes 01–69, French and Arabic names. Verify spellings and codes against the JO text at seed time. |
| Communes | 1,541 | Official list with code, wilaya, FR/AR names, postal code and centroid (centroidLat/centroidLng) — needed by the support-region resolution. Communes moved to the new wilayas take their new codes; ids stay stable. |
| Brands | 3 | HOCO, ACEFAST, BOROFONE with logos. |
| Categories | 8 top level + sub-categories | Chargers, cables, earphones, power banks, car holders, audio, watches, accessories (cahier §4.3) and HOCO's sub-categories (e.g. wall/car/wireless chargers; USB-C/Lightning/Micro-USB cables) — to confirm with HOCO. |
| Attribute definitions | ≈ 15 | Power (W), connector, cable length (m), colour, battery capacity (mAh), Bluetooth version, current (A)… per category (CAT-13). |
| Roles, permissions, grants | 8 roles, ≈ 90 permissions | From the permissions matrix; re-seeding never removes a grant added in BO-62. |
| Reference lists | 8 lists | BUSINESS_TYPE, PERSON_ROLE, VISIT_NOT_DONE_REASON, VISIT_ISSUE_TAG, TOUR_SUSPEND_REASON, ORDER_REFUSAL_REASON, ORDER_CANCEL_REASON, PAYMENT_REJECT_REASON, with the system items (ASSISTED_EXPIRED, ASSISTED_REJECTED, OTHER). |
| Settings | ≈ 35 keys | Defaults below. |
| Support regions | ≈ 4 + default | Proposal Centre, Est, Ouest, Sud covering the 69 wilayas, WhatsApp numbers from HOCO — to confirm (ADM-08). |
| Visit types and checklist | 3 types, 1 template | PROSPECTION, SUIVI, RECOUVREMENT (proposal) and the standard merchandising checklist. |
| Notification templates and rules | per event × channel × fr | From the notifications matrix. |
| Platform rows | — | Default warehouse, app version policies (reseller and delegate apps × Android/iOS, web), one dashboard layout per role. |
| Demo (dev/staging only) | — | ≈ 60 products with variants, three prices and attribute values; 40 resellers in 3 wilayas (all statuses and customer types); 2 delegates with zones; today's tour; orders in every status; an after-sales ticket, a return and a credit note (OPS-01). |
Setting keys seeded with their defaults (the values are tunable in BO-64; several are open decisions):
| Key | Default |
|---|---|
orders.minAmount · orders.cancelAllowedStatuses · orders.assistedConfirmationHours · returns.windowDays | 0 (off) · SUBMITTED, PENDING · 72 · 7 |
stock.mode · stock.reserveOn · stock.deductOn · stock.defaultAlertThreshold | GLOBAL · VALIDATED · SHIPPED · 10 |
catalog.pricesVisibleBeforeValidation | true — prices of the declared type right after sign-up (AUTHORING §1b-6, decision to confirm) |
billing.invoiceOn · billing.sellerIdentity · billing.deliveryVatRateBp · billing.stampDuty | SHIPPED · HOCO legal identity (legal name, address, NIF, NIS, RC, AI, RIB, optional logoFileId: a PNG or JPEG drawn on the documents) · 1900 · to confirm |
credit.defaultLimit · credit.defaultTermsDays · credit.overLimitPolicy · payments.enabledMethods | 0 · 30 · REQUIRE_APPROVAL · all five methods |
delivery.fees | default fee, free threshold and per-wilaya overrides (DLV-06) |
field.gpsToleranceMeters · field.gpsMaxAccuracyMeters · field.locationIntervalSeconds · field.locationDistanceMeters | 150 · 100 · 300 · 200 |
field.locationRetentionDays · field.visitUnlockHours · field.reopenMaxDays · field.closingRequiresSync · field.reportRequiredToEndVisit | 90 · 24 · 7 · true · true |
crm.inactiveAfterDays · invitations.validityDays · support.autoCloseDays · exports.retentionDays | 60 · 30 · 7 · 7 |
There is deliberately no automatic order confirmation setting (ORD-14) and no stock display setting (STK-07).
Indexing strategy
Every foreign key used in a filter has an index; composite indexes follow the main queries. The model pages list the exact column lists; the important patterns:
| Query | Index |
|---|---|
| Catalog search by name or reference (under 3 s, CAT-07) | GIN trigram on f_unaccent(products.name), f_unaccent(products.reference), product_variants.sku; exact barcode and sku uniques |
| Catalog filters and sorts | products (status, category_id), (status, brand_id), (status, popularity_score DESC), (status, published_at DESC); one index per price column for the price range of the caller's type |
| Attribute facets and filters (CAT-12) | variant_attribute_values (attribute_id, value_text), (attribute_id, value_number), (attribute_id, value_bool) + unique (variant_id, attribute_id) |
| Delegate scope (ACL-03) | resellers (assigned_delegate_id, status), (zone_id, status), delegate_zones (zone_id), zone_areas uniques on wilaya_code and commune_id |
| Delegate sync deltas | (assigned_delegate_id, updated_at) on resellers, (delegate_id, updated_at) on tours and visits, reseller_assignments (from_delegate_id, created_at) |
| Orders by status and date | orders (status, submitted_at DESC), (reseller_id, created_at DESC), partial orders_to_confirm (SUBMITTED, PENDING), trigram on number |
| KPI aggregates and reports | orders (validated_at), (delivered_at), (attributed_delegate_id, submitted_at), (wilaya_code, submitted_at), (zone_id, submitted_at); order_items (product_id); visits (delegate_id, planned_at); materialised views refreshed by the worker only if a report exceeds its p95 budget |
| Inbox and badges | partial notifications (user_id) WHERE read_at IS NULL, (user_id, created_at DESC) |
| Outbox and jobs | partial outbox_events (available_at) WHERE dispatched_at IS NULL; idempotency_records (expires_at) |
| Large append-only tables | BRIN on audit_logs.at and location_pings.occurred_at for range scans and purges |
Data volumes (planning assumptions)
Assumption — orders of magnitude for sizing, to be replaced by HOCO's figures at the cadrage workshop (PRJ-03):
| Entity | Pilot | After generalisation (per year unless stated) |
|---|---|---|
| Resellers and prospects | 300 | 5 000 – 10 000 in total |
| Delegates · staff users | 5 · 10 | 30 – 50 · 30 |
| Products · variants | 800 · 2 000 | 1 500 · 4 000 in total |
| Orders · order lines | 1 000 · 10 000 | 70 000 · 700 000 |
| Visits | 3 000 | 100 000 |
| Stock movements | 30 000 | 2 000 000 |
| Location pings | 60 000 | under 1 M live rows (90-day retention) |
| Notifications · deliveries | 20 000 | 500 000 · 1 000 000 |
| Audit log | 50 000 | 2 000 000 (6 M over 3 years) |
All of it fits one PostgreSQL instance with room to spare (tens of GB including indexes over three years); no table needs partitioning in V1. Files (photos, documents, PDFs) are in object storage, not in the database.
Retention
Proposals — the durations of personal data must be validated with HOCO (cahier §10, SEC-08, Law 18-07); they are settings where a job enforces them.
| Data | Retention | Mechanism |
|---|---|---|
| Location pings | 90 days | nightly batched delete (field.locationRetentionDays) |
| Audit log | 3 years | monthly purge by at |
| Invoices, credit notes, payments, orders and their lines | 10 years (accounting documents, Code de commerce) | never purged |
| Idempotency records | 30 days | daily purge on expiresAt |
| Dispatched outbox events | 30 days | daily purge |
| Notifications · delivery log | 12 months · 6 months | daily purge |
| Sessions · verifications | until expiry | hourly purge |
| Pending uploads · export files | 24 h · 7 days | hourly / daily purge (File rows stay) |
Prospects never converted (NOT_INTERESTED, no activity) | 3 years, then anonymised | yearly job (proposal) |
Backups
- Nightly logical dump (
pg_dump -Fc) of production to object storage outside the VPS, encrypted, kept 7 daily, 4 weekly and 12 monthly copies (OPS-02, SEC-05). - Point-in-time recovery with continuous WAL archiving (pgBackRest or WAL-G) is recommended once orders flow in production: RPO of minutes instead of 24 h — decision to confirm with HOCO.
- Object storage buckets are versioned and replicated to a second location; database rows and files are restored together.
- A restore test into a fresh database is part of hardening (SRV-28) and is repeated quarterly; procedure in deployment and operations.
All tables
| Table | Model | Release |
|---|---|---|
accounts | Account | V1 |
announcement_reads | AnnouncementRead | V1 |
announcements | Announcement | V1 |
app_version_policies | AppVersionPolicy | V1 |
attribute_definitions | AttributeDefinition | V1 |
audit_logs | AuditLog | V1 |
banners | Banner | V1 |
brands | Brand | V1 |
carriers | Carrier | V1 |
cart_items | CartItem | V1 |
carts | Cart | V1 |
categories | Category | V1 |
checklist_templates | ChecklistTemplate | V1 |
communes | Commune | V1 |
credit_accounts | CreditAccount | V1 |
credit_limit_changes | CreditLimitChange | V1 |
credit_notes | CreditNote | V1 |
crm_tasks | CrmTask | V1 |
dashboard_layouts | DashboardLayout | V1 |
delegate_profiles | DelegateProfile | V1 |
delegate_zones | DelegateZone | V1 |
deliveries | Delivery | V1 |
devices | Device | V1 |
export_jobs | ExportJob | V1 |
favorites | Favorite | V1 |
files | File | V1 |
idempotency_records | IdempotencyRecord | V1 |
import_jobs | ImportJob | V1 |
interactions | Interaction | V1 |
inventory_lines | InventoryLine | V1 |
inventory_sessions | InventorySession | V1 |
invitation_events | InvitationEvent | V1 |
invitations | Invitation | V1 |
invoice_lines | InvoiceLine | V1 |
invoices | Invoice | V1 |
location_pings | LocationPing | V1 |
loyalty_accounts | LoyaltyAccount | V2 |
loyalty_transactions | LoyaltyTransaction | V2 |
notification_deliveries | NotificationDelivery | V1 |
notification_preferences | NotificationPreference | V1 |
notification_rules | NotificationRule | V1 |
notification_templates | NotificationTemplate | V1 |
notifications | Notification | V1 |
number_sequences | NumberSequence | V1 |
objectives | Objective | V1 |
order_changes | OrderChange | V1 |
order_items | OrderItem | V1 |
order_status_changes | OrderStatusChange | V1 |
orders | Order | V1 |
outbox_events | OutboxEvent | V1 |
pack_items | PackItem | V2 |
packs | Pack | V2 |
payment_allocations | PaymentAllocation | V1 |
payment_reminders | PaymentReminder | V2 |
payments | Payment | V1 |
permissions | Permission | V1 |
price_changes | PriceChange | V1 |
price_list_items | PriceListItem | V2 |
price_lists | PriceList | V2 |
product_images | ProductImage | V1 |
product_variants | ProductVariant | V1 |
product_visibility_rules | ProductVisibilityRule | V1 |
products | Product | V1 |
promotion_audiences | PromotionAudience | V1 |
promotion_scopes | PromotionScope | V1 |
promotions | Promotion | V1 |
reference_items | ReferenceItem | V1 |
reseller_addresses | ResellerAddress | V1 |
reseller_assignments | ResellerAssignment | V1 |
reseller_documents | ResellerDocument | V1 |
reseller_photos | ResellerPhoto | V1 |
reseller_segments | ResellerSegment | V1 |
reseller_status_changes | ResellerStatusChange | V1 |
resellers | Reseller | V1 |
return_items | ReturnItem | V1 |
return_requests | ReturnRequest | V1 |
reward_redemptions | RewardRedemption | V2 |
rewards | Reward | V2 |
role_permissions | RolePermission | V1 |
roles | Role | V1 |
segments | Segment | V1 |
sessions | Session | V1 |
settings | Setting | V1 |
stock_levels | StockLevel | V1 |
stock_movements | StockMovement | V1 |
support_region_wilayas | SupportRegionWilaya | V1 |
support_regions | SupportRegion | V1 |
ticket_items | TicketItem | V1 |
ticket_messages | TicketMessage | V1 |
tickets | Ticket | V1 |
tour_status_changes | TourStatusChange | V1 |
tours | Tour | V1 |
user_roles | UserRole | V1 |
users | User | V1 |
variant_attribute_values | VariantAttributeValue | V1 |
verifications | Verification | V1 |
visit_checklist_items | VisitChecklistItem | V1 |
visit_photos | VisitPhoto | V1 |
visit_reports | VisitReport | V1 |
visit_types | VisitType | V1 |
visits | Visit | V1 |
vouchers | Voucher | V2 |
warehouses | Warehouse | V1 |
wilayas | Wilaya | V1 |
zone_areas | ZoneArea | V1 |
zones | Zone | V1 |
Assumptions to review
- Better Auth e-mail stays
NOT NULL: phone-only users get<id>@users.hoco.invalid, hidden by the API (verify in SRV-05). Reseller.customerTypeis nullable for prospects and required for an approved account (CHECK); prices shown right after sign-up (catalog.pricesVisibleBeforeValidation = true).Reseller.supportRegionIdis a stored derivation of the wilaya, maintained by thecrmandsupportmodules.- Delegate profile key = user id, so every
delegateIdis aUser.id. - Prices are stored HT; TTC is computed (VAT per line, half-up). If HOCO sets round TTC prices, the editor converts and the TTC may differ by one centime.
- Stamp duty (
Invoice.stampDuty) on cash-settled invoices — rates to confirm with HOCO's accountant. - Zero-total replacement orders issue no invoice.
- Credit-note number format
AVO-YYYY-NNNNNNand delegate codeDLG-0007are proposals. - 17 proposed enums (table) need French labels in enums.json.
- Names aligned with the lifecycles page:
assistedExpiresAt,pendingReasons,placedByUserId,trigger,inferred,unlockedUntil; that page'sTour.completedAtisclosedAthere (orders are never auto-confirmed and returns are V1, per the 17 Oct decisions).
Service catalogue
The 24 modules of the HOCO API server — responsibility, owned models, operations, events, jobs, settings, consumers, tasks and risks — and the catalogue of domain events and their SSE projections.
Identity and access
Users, Better Auth sessions and credentials, roles and permissions, devices and notification preferences — tables, keys and example rows.