# GuaranteeApp — دیتابیس (MySQL + SQLite)

> مرجع کانونی اسکیما. فایل `database.sql` پروژه باید دقیقاً از همین بخش ساخته شود.
> نام‌گذاری هر دو دیتابیس یکسان است (SQLite: PascalCase، MySQL: snake_case).

---

## ۱. نگاشت نام‌ها

| مفهوم | SQLite (اپ) | MySQL (سرور) |
|---|---|---|
| خانوادهٔ آپشن | `OptionFamilies` | `option_families` |
| آپشن | `DeviceOptions` | `device_options` |
| مدل محصول | `ProductModels` | `product_models` |
| جدول واسط مدل↔آپشن | `ProductModelOptions` | `product_model_options` |
| دستهٔ تولید | `ProductionBatches` | `production_batches` |
| محصول/سریال | `Products` | `products` |
| رویداد انبار | — | `warehouse_events` |
| فعال‌سازی گارانتی | — | `warranty_activations` |
| ادمین تلگرام | — | `telegram_admins` |
| کلید API | — | `api_keys` |
| پروفایل لیبل | `LabelProfiles` | `label_template_backups` (فقط پشتیبان) |
| ترجیح آخرین پروفایل | `ModelLabelPreferences` | — |
| لاگ ارسال لیبل | `PrintLogs` | — |

---

## ۲. MySQL — اسکیمای کامل (`database.sql`)

```sql
SET NAMES utf8mb4;

CREATE TABLE option_families (
  id            INT AUTO_INCREMENT PRIMARY KEY,
  local_uuid    CHAR(36)   NOT NULL UNIQUE,
  family_number TINYINT UNSIGNED NOT NULL UNIQUE,        -- رقم اول کد، ۱ تا ۹
  name          VARCHAR(100) NOT NULL,
  created_at    DATETIME   NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE device_options (
  id          INT AUTO_INCREMENT PRIMARY KEY,
  local_uuid  CHAR(36)   NOT NULL UNIQUE,
  family_id   INT        NOT NULL,
  value_index TINYINT UNSIGNED NOT NULL,                 -- رقم دوم کد، ۱ تا ۹
  code        CHAR(2)    NOT NULL,                       -- family_number || value_index
  name        VARCHAR(100) NOT NULL,
  is_locked   TINYINT(1) NOT NULL DEFAULT 0,
  created_at  DATETIME   NOT NULL,
  UNIQUE KEY uq_family_value (family_id, value_index),
  FOREIGN KEY (family_id) REFERENCES option_families(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE product_models (
  id                   INT AUTO_INCREMENT PRIMARY KEY,
  local_uuid           CHAR(36)    NOT NULL UNIQUE,
  name                 VARCHAR(150) NOT NULL,
  base_code            VARCHAR(20)  NOT NULL,
  part_number_code     VARCHAR(40)  NOT NULL UNIQUE,     -- base_code[-کدهای خانواده]
  is_locked            TINYINT(1)   NOT NULL DEFAULT 0,
  warranty_months      SMALLINT UNSIGNED NOT NULL,       -- ۱ تا ۱۲۰ (اعتبارسنجی در API)
  warranty_label_text  VARCHAR(60)  NOT NULL,            -- مثلاً "1 Year Warranty"
  created_at           DATETIME     NOT NULL,
  synced_at            DATETIME     NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE product_model_options (
  product_model_id INT NOT NULL,
  device_option_id INT NOT NULL,
  PRIMARY KEY (product_model_id, device_option_id),
  FOREIGN KEY (product_model_id) REFERENCES product_models(id),
  FOREIGN KEY (device_option_id) REFERENCES device_options(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE production_batches (
  id               INT AUTO_INCREMENT PRIMARY KEY,
  local_uuid       CHAR(36)    NOT NULL UNIQUE,
  product_model_id INT         NOT NULL,
  batch_code       VARCHAR(30) NOT NULL UNIQUE,          -- 260923-B1
  quantity         INT         NOT NULL,
  created_at       DATETIME    NOT NULL,
  FOREIGN KEY (product_model_id) REFERENCES product_models(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE products (
  id                  BIGINT AUTO_INCREMENT PRIMARY KEY,
  serial_number       VARCHAR(64) NOT NULL UNIQUE,       -- 40 + 1 + 6 + 1 + 4 = 52 ≤ 64
  product_model_id    INT         NOT NULL,
  part_number_code    VARCHAR(40) NOT NULL,              -- denormalized
  production_batch_id INT         NOT NULL,
  production_date     DATE        NOT NULL,
  label1_printed_at   DATETIME    NULL,
  label2_printed_at   DATETIME    NULL,
  created_at          DATETIME    NOT NULL,
  synced_at           DATETIME    NOT NULL,
  KEY idx_prod_date_model (production_date, product_model_id),
  FOREIGN KEY (product_model_id)    REFERENCES product_models(id),
  FOREIGN KEY (production_batch_id) REFERENCES production_batches(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE warehouse_events (
  id            BIGINT AUTO_INCREMENT PRIMARY KEY,
  serial_number VARCHAR(64) NOT NULL,
  product_id    BIGINT      NULL,                        -- NULL تا وقتی محصول واقعی سینک شود
  event_type    ENUM('packaging','warehouse_exit') NOT NULL,
  scanned_at    DATETIME    NOT NULL,
  is_orphan     TINYINT(1)  NOT NULL DEFAULT 0,
  matched_at    DATETIME    NULL,
  UNIQUE KEY dup_guard (serial_number, event_type),
  KEY idx_event_time (event_type, scanned_at),
  KEY idx_product (product_id),
  FOREIGN KEY (product_id) REFERENCES products(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE warranty_activations (
  id             BIGINT AUTO_INCREMENT PRIMARY KEY,
  serial_number  VARCHAR(64) NOT NULL UNIQUE,
  product_id     BIGINT      NULL,
  activated_at   DATETIME    NOT NULL,
  admin_notified TINYINT(1)  NOT NULL DEFAULT 0,
  FOREIGN KEY (product_id) REFERENCES products(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE telegram_admins (
  id      INT AUTO_INCREMENT PRIMARY KEY,
  chat_id VARCHAR(30)  NOT NULL UNIQUE,
  name    VARCHAR(100) NOT NULL,
  active  TINYINT(1)   NOT NULL DEFAULT 1
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE api_keys (
  id            INT AUTO_INCREMENT PRIMARY KEY,
  key_hash      CHAR(64)     NOT NULL UNIQUE,            -- SHA-256 هگز؛ نه متن خام
  workshop_name VARCHAR(100) NOT NULL,
  active        TINYINT(1)   NOT NULL DEFAULT 1,
  created_at    DATETIME     NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE label_template_backups (
  id             INT AUTO_INCREMENT PRIMARY KEY,
  label_type     TINYINT      NOT NULL,                  -- 1 یا 2
  profile_name   VARCHAR(100) NOT NULL,
  file_name      VARCHAR(150) NOT NULL,
  content_hash   CHAR(64)     NOT NULL,
  print_warranty TINYINT(1)   NOT NULL DEFAULT 0,
  btw_blob       MEDIUMBLOB   NOT NULL,
  updated_at     DATETIME     NOT NULL,
  UNIQUE KEY uq_type_profile (label_type, profile_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

### ثبت کلید API (یک‌بار، در phpMyAdmin)
```sql
INSERT INTO api_keys (key_hash, workshop_name, active, created_at)
VALUES (SHA2('<کلید تصادفی حداقل ۳۲ کاراکتر>', 256), 'کارگاه', 1, NOW());
```
خود کلید فقط در تنظیمات اپ دسکتاپ (`ApiKey`) نگهداری می‌شود.

### نکات
- همهٔ زمان‌ها را PHP با `date_default_timezone_set('Asia/Tehran')` محاسبه و درج می‌کند (از `NOW()` و `CURDATE()` دیتابیس استفاده نشود؛ در کوئری‌های گزارش، تاریخ امروز را PHP به‌صورت پارامتر بدهد).
- Import با `utf8mb4` انجام شود.

---

## ۳. SQLite محلی (اپ دسکتاپ — EF Core)

```
OptionFamilies      Id, FamilyNumber (UNIQUE 1..9), Name, LocalUuid, Synced
DeviceOptions       Id, FamilyId FK, ValueIndex (1..9), Name, IsLocked, LocalUuid, Synced
                    Code = [NotMapped] = FamilyNumber*10 + ValueIndex  (مثل "11")
                    UNIQUE(FamilyId, ValueIndex)
ProductModels       Id, Name, BaseCode, PartNumberCode (UNIQUE), IsLocked,
                    WarrantyMonths (int, الزامی)، WarrantyLabelText (string 60، الزامی),
                    LocalUuid, Synced
ProductModelOptions ProductModelId FK, DeviceOptionId FK  — PK مرکب
ProductionBatches   Id, ProductModelId FK, BatchCode (UNIQUE), Quantity, CreatedAt,
                    LocalUuid, Synced
Products            Id, SerialNumber (UNIQUE), ProductModelId FK, PartNumberCode,
                    ProductionBatchId FK, ProductionDate, CreatedAt,
                    Label1PrintedAt?, Label2PrintedAt?, Synced
PrintLogs           Id, ProductId FK, LabelType (1|2), ProfileName, SentAt

LabelProfiles       Id, LabelType (1|2), ProfileName, BtwFileName, PrintWarranty,
                    ContentHash, LastBackupHash?, CreatedAt, UpdatedAt, LastUsedAt?
                    UNIQUE(LabelType, ProfileName)
ModelLabelPreferences  ProductModelId FK, LabelType, LastProfileId FK  — PK(ProductModelId, LabelType)
```

- `PrinterName` عمداً وجود ندارد؛ پرینتر داخل `.btw` است.
- حذف مدل (بدون سریال) ← حذف `ModelLabelPreferences` همان مدل.
- هر ویرایش مدل/آپشن/خانواده، `Synced=false` می‌کند. تغییر زمان چاپ لیبل، `Products.Synced=false` می‌کند.

---

## ۴. قوانین داده

### ۴.۱ Part Number
`PartNumber = base_code` اگر مدل آپشن ندارد؛ وگرنه `base_code + "-" + (کد آپشن‌ها به ترتیب صعودی شمارهٔ خانواده، پشت‌سرهم)`. در هر خانواده حداکثر یک آپشن. مثال: لنز 4mm (`12`) + WiFi (`23`) ← `IP0756z-1223`.

### ۴.۲ سریال
`{PartNumber}-{YYMMDD}-{####}` — تاریخ میلادی. شمارندهٔ ۴رقمی به‌ازای **هر مدل در هر روز** از ۰۰۰۱ (سقف ۹۹۹۹). محاسبه: max+۱ روی سریال‌های محلی همان مدل و روز؛ برخورد با unique index ← تلاش با شمارندهٔ بعدی.

### ۴.۳ کد دسته
`{YYMMDD}-B{n}`، n = تعداد دسته‌های همان روز + ۱ (سراسری، نه به‌ازای مدل). یکتا.

### ۴.۴ قفل
- تولید اولین سریال یک مدل ← `IsLocked=true` برای مدل و `IsLocked=true` برای همهٔ آپشن‌های آن.
- مدل قفل‌شده: ترکیب آپشن‌ها و Part Number ثابت. **نام، مدت گارانتی و متن گارانتی همچنان قابل ویرایش‌اند** (فقط `Synced=false` می‌شوند).
- آپشن قفل‌شده حذف نمی‌شود و اندیس آن برای مقدار دیگر استفاده نمی‌شود.
- افزودن خانواده/آپشن جدید همیشه آزاد است (اندیس آزاد بعدی).

### ۴.۵ Orphan
`warehouse_events` با `product_id=NULL, is_orphan=1`. هر بار محصولی در `sync/products` درج شد، رویدادهای Orphan همان سریال وصل می‌شوند (کوئری در `02-Backend-API §۸.۲`).

### ۴.۶ گارانتی
`warranty_months` ۱ تا ۱۲۰. متن خودکار: ۱۲ ← `1 Year Warranty`، ۲۴ ← `2 Years Warranty`، ۶ ← `6 Months Warranty`، ۱۸ ← `18 Months Warranty`، (مضرب ۱۲ ← سال، وگرنه ماه؛ مفرد/جمع رعایت شود). قابل ویرایش دستی. فقط برای لیبل؛ وب و ربات استفاده نمی‌کنند.
