# 02 — Architecture ## Local-First Principle **SQLite** is the single source of truth for the user. **MariaDB** is a sync server only — it never blocks local operations. ``` User │ ▼ SQLite (qfs.db) ← all CRUD operations go here │ │ background sync (every 5 min) ▼ MariaDB (RFQ_LOG) ← pull/push only ``` **Rules:** - All CRUD operations go through SQLite only - If MariaDB is unavailable → local work continues without restrictions - Changes are queued in `pending_changes` and pushed on next sync --- ## Synchronization ### Data Flow Diagram ``` [ SERVER / MariaDB ] ┌───────────────────────────┐ │ qt_projects │ │ qt_configurations │ │ qt_pricelists │ │ qt_pricelist_items │ │ qt_pricelist_sync_status │ └─────────────┬─────────────┘ │ pull (projects/configs/pricelists) │ ┌────────────────────┴────────────────────┐ │ │ [ CLIENT A / SQLite ] [ CLIENT B / SQLite ] local_projects local_projects local_configurations local_configurations local_pricelists local_pricelists local_pricelist_items local_pricelist_items pending_changes pending_changes │ │ └────── push (projects/configs only) ─────┘ │ [ SERVER / MariaDB ] ``` ### Sync Direction by Entity | Entity | Direction | |--------|-----------| | Configurations | Client ↔ Server ↔ Other Clients | | Projects | Client ↔ Server ↔ Other Clients | | Pricelists | Server → Clients only (no push) | | Components | Server → Clients only | Local pricelists not present on the server and not referenced by active configurations are deleted automatically on sync. ### Soft Deletes (Archive Pattern) Configurations and projects are **never hard-deleted**. Deletion is archive via `is_active = false`. - `DELETE /api/configs/:uuid` → sets `is_active = false` (archived); can be restored via `reactivate` - `DELETE /api/projects/:uuid` → archives a project **variant** only (`variant` field must be non-empty); main projects cannot be deleted via this endpoint ## Sync Readiness Guard Before every push/pull, a preflight check runs: 1. Is the server (MariaDB) reachable? 2. Can centralized local DB migrations be applied? 3. Does the application version satisfy `min_app_version` of pending migrations? **If the check fails:** - Local CRUD continues without restriction - Sync API returns `423 Locked` with `reason_code` and `reason_text` - UI shows a red indicator with the block reason --- ## Pricing ### Principle **Prices come only from `local_pricelist_items`.** Components (`local_components`) are metadata-only — they contain no pricing information. ### Lookup Pattern ```go // Look up a price for a line item price, found := s.lookupPriceByPricelistID(pricelistID, lotName) if found && price > 0 { // use price } // Inside lookupPriceByPricelistID: localPL, err := s.localDB.GetLocalPricelistByServerID(pricelistID) price, err := s.localDB.GetLocalPriceForLot(localPL.ID, lotName) ``` ### Multi-Level Pricelists A configuration can reference up to three pricelists simultaneously: | Field | Purpose | |-------|---------| | `pricelist_id` | Primary (estimate) | | `warehouse_pricelist_id` | Warehouse pricing | | `competitor_pricelist_id` | Competitor pricing | Pricelist sources: `estimate` | `warehouse` | `competitor` --- ## Configuration Versioning ### Principle Append-only: every save creates an immutable snapshot in `local_configuration_versions`. ``` local_configurations └── current_version_id ──► local_configuration_versions (v3) ← active local_configuration_versions (v2) local_configuration_versions (v1) ``` - `version_no = max + 1` on every save - Old versions are never modified or deleted in normal flow - Rollback does **not** rewind history — it creates a **new** version from the snapshot ### Rollback ```bash POST /api/configs/:uuid/rollback { "target_version": 3, "note": "optional comment" } ``` Result: - A new version `vN` is created with `data` from the target version - `change_note = "rollback to v{target_version}"` (+ note if provided) - `current_version_id` is switched to the new version - Configuration moves to `sync_status = pending` ### Sync Status Flow ``` local → pending → synced ``` --- ## Sync Payload for Versioning Events in `pending_changes` for configurations contain: | Field | Description | |-------|-------------| | `configuration_uuid` | Identifier | | `operation` | `create` / `update` / `rollback` | | `current_version_id` | Active version ID | | `current_version_no` | Version number | | `snapshot` | Current configuration state | | `idempotency_key` | For idempotent push | | `conflict_policy` | `last_write_wins` | --- ## Background Processes | Process | Interval | What it does | |---------|----------|--------------| | Sync worker | 5 min | push pending + pull all | | Backup scheduler | configurable (`backup.time`) | creates ZIP archives |