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