# GuaranteeApp — گزارش پیاده‌سازی وب/بک‌اند (PHP API + پنل + صفحهٔ گارانتی)

> این فایل را سازندهٔ **بخش وب و بک‌اند** نوشته است تا نفر بعدی (ربات تلگرام / استقرار / تست پذیرش) بداند دقیقاً چه چیزی ساخته شده، چه قراردادی با اپ دسکتاپ دارد و چه چیزهایی باز است.
> مراجع اصلی: `00-Overview`، `01-Database`، `02-Backend-API`، `05-WebPanel`، `07-Deployment`، `08-BuildPlan`، `09-Desktop-App-Implementation §۴`.

---

## ۱. وضعیت در یک نگاه

| مورد | وضعیت |
|---|---|
| فاز ۱ (دیتابیس: `database.sql` با ۱۱ جدول) | ✅ کامل — دقیقاً از `01 §۲` |
| فاز ۲ (PHP API: `lib/*`، scan / warranty / product / sync×۴ / restore×۵، Rate limit، لاگ، `test.php`) | ✅ کامل |
| فاز ۳ (وب: `panel.php`، `warranty.php`، assets) | ✅ کامل — تست دوربین فیزیکی روی گوشی باقی است (نیازمند HTTPS/هاست) |
| فاز ۴ (ربات تلگرام: پوشهٔ `telegram/`) | ❌ ساخته **نشده** — خارج از این بخش بود؛ اما همهٔ زیرساختش آماده است (بخش ۶) |
| `php -l` روی ۳۰ فایل PHP (PHP 8.5.11) | ✅ صفر خطا |
| تست منطقی (Serial / PartNumberValidator / Jalali / تاریخ‌ها) | ✅ ۲۹/۲۹ پاس |
| تست HTTP با وب‌سرور داخلی PHP | ✅ panel/warranty=200، test.php بدون key=403، بدون X-Api-Key=401، Rate limit واقعی=429 بعد از سقف، پیام JSON «تنظیمات ناقص» به‌جای صفحهٔ سفید |
| تست با MySQL واقعی | ⏳ روی هاست انجام شود با `test.php` (محلی MySQL نبود) — چک‌لیست `02 §۹` و `07 §۵.۱` |

---

## ۲. ساختار فایل‌ها (چیزی که روی هاست آپلود می‌شود)

```
config.php               ← تنظیمات (DB_*, TEST_KEY, TELEGRAM_BOT_TOKEN, RATE_LIMIT_PER_MIN, GW_INVENTORY_MODE) — قبل از آپلود پر شود
.htaccess                ← بستن config.php، *.sql، *.log، *.md + Options -Indexes + UTF-8
panel.php                ← پنل عملیات انبار (05 §۲) — HTML استاتیک + JS
warranty.php             ← صفحهٔ عمومی گارانتی (05 §۳)
test.php                 ← سلامت‌سنجی + تست زندهٔ ۹ مرحله‌ای + لاگ + پاک‌سازی (?key=TEST_KEY)
database.sql             ← اسکیمای ۱۱ جدول (01 §۲)
api/
  scan.php               ← POST عمومی (Rate limit) — پنل انبار
  product.php            ← GET ?serial= (X-Api-Key) — استعلام
  warranty/activate.php  ← POST عمومی (Rate limit) — فعال‌سازی گارانتی
  sync/{options,models,products,templates}.php   ← POST (X-Api-Key) — اپ دسکتاپ
  restore/{options,models,batches,products,templates}.php ← GET (X-Api-Key) — بازیابی
lib/                     ← (.htaccess: Deny all)
  bootstrap.php          ← لود همهٔ lib + timezone + gw_uuid_ok + gw_date + gw_datetime
  db.php                 ← Db::pdo() تک‌نمونه + Db::isDuplicateException()
  Response.php           ← قالب JSON + Response::body() (خواندن JSON درخواست)
  Auth.php               ← Auth::requireApiKey() / Auth::requireTestKey()
  RateLimiter.php         ← فایل‌محور در sys_get_temp_dir()؛ ۶۰ثانیه/به‌ازای IP
  Logger.php             ← gw_log() + Logger::tail()
  Serial.php             ← Serial::extract() (URL→پارامتر serial) + Serial::valid()
  Jalali.php             ← میلادی→جلالی + ارقام فارسی + format/faDateTime
  PartNumberValidator.php ← کد دوریقمی، یکتایی خانواده، سازگاری پارت‌نامبر
  TelegramNotifier.php    ← sendToAllAdmins + اعلان Orphan/گارانتی + sendTest
  ProductService.php      ← history($serial) — فیلدهای 02 §۴.۳
  InventoryService.php    ← stock() / daily($date) — GW_INVENTORY_MODE
  ScanService.php         ← منطق اسکن (04 §۴.۱) — مشترک API و test.php
  WarrantyService.php     ← منطق فعال‌سازی (02 §۴.۲) — مشترک API و test.php
assets/
  css/app.css             ← تم تیرهٔ RTL مشترک
  js/panel.js             ← دوربین html5-qrcode + پیام‌ها + شمارندهٔ نشست + localStorage
  js/warranty.js          ← فعال‌سازی خودکار هنگام لود + تاریخ شمسی سمت کلاینت
  js/test.js              ← دکمه‌های test.php (step1..9 / logs / cleanup)
logs/
  .htaccess               ← Deny all؛ app.log خودکار ساخته می‌شود
```

---

## ۳. قرارداد با اپ دسکتاپ (تأیید هماهنگی با `09 §۴`)

همهٔ فیلدها **دقیقاً** با نام‌های `09 §۴.۱/۴.۲` پیاده شده‌اند:

| Endpoint | وضعیت |
|---|---|
| `POST api/sync/options.php` | ✅ `familyLocalUuid, familyNumber, familyName, options[{localUuid,valueIndex,name,isLocked}]` → پاسخ `data.syncedOptionUuids` |
| `POST api/sync/models.php` | ✅ فیلدها طبق 09؛ پاسخ `data.modelId, data.partNumberCode` |
| `POST api/sync/products.php` | ✅ سقف ۲۰۰؛ پاسخ `insertedCount/updatedCount/matchedOrphanEvents/rejected[{serialNumber,code}]` |
| `POST api/sync/templates.php` | ✅ پاسخ `data.id, data.updated` (`updated:false` اگر هش برابر) |
| `GET api/restore/*.php` | ✅ فیلدهای 02 §۶؛ صفحه‌بندی `afterId&limit=500`، `nextAfterId=null` = پایان؛ `restore/options.php` = «تست اتصال» اپ |
| کدهای خطای اپ (`09 §۴.۳`) | ✅ همه تولید می‌شوند: AUTH_INVALID_KEY، MODEL_LOCKED_CONFLICT، SERIAL_ALREADY_EXISTS (ردیفی)، RATE_LIMITED، VALIDATION_ERROR، INTERNAL_ERROR + خطای منطقی با **HTTP 200** |

قواعد حساس رعایت‌شده:
- **ادغام Orphan** (02 §۸.۲): بعد از هر INSERT موفق در products، `warehouse_events` با همان سریال و `is_orphan=1` به محصول وصل و به `matchedOrphanEvents` اضافه می‌شود.
- **سریال موجود با همان دسته** = به‌روزرسانی `label1/2_printed_at` فقط با مقادیر غیر-null (`COALESCE`) → سینک مجدد/Retry تعارض ندارد.
- **قفل مدل**: فقط **ترکیب آپشن‌ها** (`MODEL_LOCKED_CONFLICT`)؛ تغییر نام/گارانتی/بستن مدل مجاز.
- **آپشن قفل‌شده**: `value_index` موجود با `local_uuid` دیگر و `is_locked=1` → `MODEL_LOCKED_CONFLICT`.
- زمان‌ها را همیشه **PHP با Asia/Tehran** می‌سازد؛ در کوئری‌ها `NOW()/CURDATE()` استفاده نشده (01 §۲ نکات).

---

## ۴. تصمیم‌ها و فرض‌های علاوه بر مستندات (برای آگاهی نفر بعد)

1. **`ScanService` و `WarrantyService` در `lib/` هستند نه داخل Endpointها** — تا `test.php` هم همان منطق را مستقیم (بدون HTTP و Rate limit) صدا بزند؛ `api/scan.php` فقط پوستهٔ Rate limit + اعتبارسنجی است. ربات تلگرام هم اگر لازم شد می‌تواند همین‌ها را مستقیم صدا بزند.
2. **اعتبارسنجی سریال آسان است** (`^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`) — چون `TEST-WEB-0001` و سریال‌های واقعی هر دو باید رد نشوند؛ regex سختگیرانهٔ `06 §۴` (`{PN}-YYMMDD-####`) فقط برای تشخیص «متنِ فقط سریال» در **ربات** است، نه وب.
3. **`sync/templates.php` هش را با محتوا می‌سنجد**: `hash('sha256', base64_decode(contentBase64))` باید با `contentHash` برابر وگرنه `VALIDATION_ERROR`. طبق `09 §۴.۲` («ناسازگار = رد»). اگر اپ دسکتاپ هش را روی چیز دیگری غیر از **بایت‌های اصلی فایل .btw** حساب کند، اینجا رد می‌شود — در اولین سینک واقعی قالب بررسی کنید.
4. **گزارش روزانه Orphan را حساب نمی‌کند**: `InventoryService::daily()` فقط رویدادهای `is_orphan=0` را می‌شمارد (سریال ناشناخته واقعاً «وارد انبار» نیست). `02 §۷` صریح نبود؛ تصمیم منطقی و مستند. موجودی (stock) هم به‌طور طبیعی فقط از رویدادهای متصل به محصول می‌آید.
5. **تکراریِ Orphan**: اسکن دوبارهٔ سریال ناشناخته → برخورد `dup_guard` → `SERIAL_ALREADY_REGISTERED`؛ **اعلان تلگرام فقط بار اول** رفته است (همان 05 §۲.۴).
6. **یکتایی‌های منطقی که MD صریح نگفته بود و با `VALIDATION_ERROR` پاس می‌شوند**: تکرار `family_number` برای خانوادهٔ دیگر، تکرار `part_number_code` برای مدل دیگر، تکرار `batch_code` با uuid دیگر، تغییر `batch_code` روی دستهٔ موجود، تعلق دسته به مدل دیگر، بیش از یک آپشن از یک خانواده در مدل.
7. **`gw_uuid_ok`** (uuid تا ۳۶ کاراکتر `[A-Za-z0-9-]`) در `lib/bootstrap.php` است و همهٔ syncها از آن استفاده می‌کنند (تابع عمومی، نه داخل یک Endpoint).
8. **`activatedAt` در پاسخ‌ها ISO با 'T'** است (`2026-09-25T18:45:00` — طبق 02 §۴.۲) ولی در دیتابیس `'Y-m-d H:i:s'` ذخیره می‌شود.
9. **Rate limit**: فایل JSON در `sys_get_temp_dir()` (بدون فشار به DB)، پنجرهٔ ۶۰ثانیه‌ای به‌ازای (bucket, IP)، سقف `RATE_LIMIT_PER_MIN` (پیش‌فرض ۳۰). تست شد → بعد از سقف، HTTP 429 با کد `RATE_LIMITED`.
10. **`panel.php` بدون PIN** (سؤال باز `08`؛ پیش‌فرض: لینک نیمه‌مخفی). ساختش آماده است: در آینده یک چک ساده در بالای `panel.php` اضافه شود.
11. **CDN اسکنر pin شده**: `html5-qrcode@2.3.8` از unpkg (05 §۲.۳). فرمت‌ها: QR + Code128 + Code39 + DataMatrix. اگر CDN مشکل داشت، فایل را در `assets/js/` لوکال کنید و تگ اسکریپت `panel.php` را عوض کنید — هیچ چیز دیگری لازم نیست.
12. **تبدیل جلالی دوبار پیاده شده**: PHP در `lib/Jalali.php` (اعلان‌های تلگرام + test.php) و JS در `warranty.js` (نمایش «قبلاً فعال شده» شمسی برای مشتری). الگوریتم یکی است؛ اگر عوض شد، **هر دو** را عوض کنید.
13. **test.php گام ۱** دادهٔ تست را **مستقیم در DB** می‌سازد (مدل/دسته/محصول با uuidهای `test-uuid-*` و سریال `TEST-WEB-0001`). چون سریال تست از پیشوند پارت‌نامبر تبعیت نمی‌کند (قاعدهٔ پیشوند مال sync است، نه scan)، ساخت مستقیم در DB عمداً انتخاب شد. گام‌های ۲ تا ۸ همان سرویس‌های واقعی را صدا می‌زنند. پاک‌سازی: سریال‌های `TEST-%` + uuidهای `test-uuid-%`.
14. **`Response::body()`** بدنهٔ خالی را `[]` برمی‌گرداند (خطا نمی‌دهد) → اسکن خالی → `VALIDATION_ERROR` سریال نامعتبر.
15. خروجی HTML در `test.php` همه با `htmlspecialchars` اسکیپ شده (02 §۳).

---

## ۵. تست‌های انجام‌شده (محلی — ویندوز + PHP 8.5.11)

- `php -l` روی **۳۰ فایل PHP**: صفر خطا.
- تست منطقی ۲۹ موردی (فایل موقت `_logic_test.php` که بعد از تست حذف شد): استخراج سریال از URL خام/وسط پارامترها، اعتبارسنجی کد دوریقمی، یکتایی خانواده، سازگاری پارت‌نامبر (`IP0756z` + `12`,`23` = `IP0756z-1223`)، جلالی (2026-09-25 → ۱۴۰۵/۰۷/۰۳ و 21-Mar-2025 → ۱۴۰۴/۰۱/۰۱)، ارقام فارسی، `gw_date/gw_datetime` سختگیر — **همه پاس**.
- تست HTTP با وب‌سرور داخلی PHP: `panel.php`/`warranty.php` = 200؛ `test.php` بدون key = **403**؛ `api/product.php`، `api/sync/*`، `api/restore/*` بدون X-Api-Key = **401 + JSON**؛ ۳۵ درخواست پیاپی scan → بعد از سقف **429**؛ scan با DB خالی → **HTTP 500 + JSON «تنظیمات ناقص…»** (نه صفحهٔ سفید)؛ eventType نامعتبر → HTTP 200 + `VALIDATION_ERROR`.
- **تست کامل با MySQL روی هاست انجام نشده** (محلی MySQL نبود) — با `test.php?key=…` بخش‌های ۱ تا ۴ طبق `07 §۵.۱` اجرا شود.

---

## ۶. برای سازندهٔ ربات تلگرام (فاز ۴) — زیرساخت آماده

پوشهٔ `telegram/` ساخته **نشده** (خارج از این بخش بود)، اما طبق `06 §۳` ربات از زیرساخت وب استفاده می‌کند و **همه آماده است**:

| نیاز ربات | کجاست |
|---|---|
| اتصال DB / قالب JSON / لاگ | `lib/db.php`، `lib/Response.php`، `lib/Logger.php` |
| secret تلگرام (`substr(hash('sha256', TOKEN), 0, 48)` — 06 §۲.۲) | در `telegram/lib/TgApi.php` خودتان بسازید؛ برای set و webhook یک تابع مشترک |
| محافظت `?key=TEST_KEY` برای `daily_report.php` / `setup_webhook.php` | `Auth::requireTestKey()` |
| ارسال به ادمین‌ها (اعلان‌های فوری Orphan/گارانتی همین الان از PHP کار می‌کنند) | `TelegramNotifier::sendToAllAdmins()` — فقط جدول `telegram_admins` را پر کنید |
| گزارش / موجودی / تاریخچه | `InventoryService::stock()/daily()`، `ProductService::history()` |
| جلالی + ارقام فارسی (قالب 06 §۴.۱/۵) | `lib/Jalali.php` (`format`، `faDateTime`، `faDigits`) |
| `/inventory` با `GW_INVENTORY_MODE` | از `config.php` خوانده می‌شود |
| تشخیص «متنِ فقط سریال» | regex `06 §۴` را در خود ربات بگذارید — دقت: `Serial::valid` وب عمداً آسان‌تر است (تصمیم ۲) |

**نکته:** در `daily_report.php` تاریخ را با `date('Y-m-d')` (bootstrap خودش `Asia/Tehran` را ست می‌کند) به `InventoryService::daily()` بدهید، نه `CURDATE()` دیتابیس.

---

## ۷. موارد باز (نقطهٔ شروع نفر بعدی)

| مورد | توضیح |
|---|---|
| **تست زنده روی هاست** | Import `database.sql` (utf8mb4) → ثبت کلید API (`07 §۲`) → پر کردن `config.php` → `test.php` بخش ۱ تا ۴ → اسکن واقعی روی گوشی (`panel.php` با https) |
| فاز ۴ ربات تلگرام | بخش ۶ همین فایل + فایل `06` |
| PIN برای `panel.php` | سؤال باز `08`؛ فعلاً بدون PIN (تصمیم ۱۰) |
| `ApiBaseUrl` / `WarrantyUrlTemplate` در `appsettings.json` اپ | بعد از مشخص شدن دامنهٔ نهایی (`08 §۳`) |
| دقت هش قالب | تصمیم ۳ همین فایل — در اولین سینک واقعی قالب چک شود |
| حذف فایل‌های حساس پس از راه‌اندازی | `test.php` و (بعد از ساخت) `setup_webhook.php` طبق `07 §۹` |

---

## ۸. هماهنگی با بخش‌های دیگر

- **اپ دسکتاپ (ساخته‌شده، `09`):** بخش ۳ همین فایل = تطابق فیلد‌به‌فیلد با `09 §۴`. «تست اتصال» اپ = `GET api/restore/options.php`. اولین سینک واقعی، نقطهٔ تأیید نهایی قرارداد است.
- **ربات تلگرام (بعدی):** بخش ۶ همین فایل. اعلان‌های لحظه‌ای Orphan/گارانتی هم‌اکنون از `TelegramNotifier` ارسال می‌شوند و مستقل از ربات کار می‌کنند.
- **استقرار:** مراحل `07` بدون تغییر قابل اجراست؛ ZIP شامل همهٔ فایل‌های بخش ۲ این گزارش + سه `.htaccess` (ریشه، `lib/`، `logs/`) است.

---
*نوشته‌شده پس از اتمام ساخت بخش وب/بک‌اند — `php -l` ۳۰/۳۰ ✅، تست منطقی ۲۹/۲۹ ✅، تست HTTP (401/403/429/پیام JSON) ✅. تست MySQL و دوربین واقعی روی هاست باقی است.*


