diff --git a/docs/GO-LIVE-PLAN.md b/docs/GO-LIVE-PLAN.md new file mode 100644 index 0000000..cd8a663 --- /dev/null +++ b/docs/GO-LIVE-PLAN.md @@ -0,0 +1,357 @@ +# پلن رفتن به تولید — ارتقای پشته، جداسازی انتشار، جابه‌جایی از henza + +سند برنامه‌ریزی، نوشته‌شده در ۲۰۲۶-۱۰-۰۴. **هیچ قدمی هنوز اجرا نشده.** + +> این سند طوری نوشته شده که **روی سیستم دیگری، بدون گفتگوی قبلی، قابل ادامه باشد.** از §۰ شروع کنید. +> +> نسخه‌ها و تاریخ‌های این سند مال روز نوشتن‌اند. قبل از اجرای هر قدم، با +> `node scripts/check-majors.mjs` دوباره بسنجید. + +--- + +## خلاصه + +- **هدف:** سیستم زنده‌ی henza که الان زیر بار است، روی پشته‌ی جدید برود — **با همان ظاهر برای کاربر**، سریع‌تر و امن‌تر. +- **تصمیم مخزن:** پروژه‌ی جدید ساخته نمی‌شود. همین مونوریپو «نسخه‌ی بعدی تولید» است و امنیت تولید با **جداسازی انتشار** تأمین می‌شود (§۱). +- **سرعت از بک‌اند می‌آید، نه از بازنویسی ظاهر.** اپ کاربر قدیم همین حالا Vue 3 است (§۲). +- **بزرگ‌ترین ریسک داده است، نه کد.** ابزار انتقال داده از دیتابیس henza به Postgres هنوز وجود ندارد (§۷). + +### ترتیب اجرا + +| # | قدم | بخش | وابسته به | +|---|---|---|---| +| 1 | راه‌اندازی روی هر سیستم + commit و push اسناد | §۰ | — | +| 2 | اندازه‌گیری مبنا روی سیستم زنده | §۳ | — | +| 3 | ارتقای پشته | §۵ | — | +| 4 | جداسازی انتشار | §۶ | — | +| 5 | پورت اپ کلاسیک | `docs/PWA-PORT-PLAN.md` | تصمیم‌های مالک در آن سند | +| 6 | ابزار مهاجرت داده و تطبیق | §۷ | — (موازی با ۵ قابل انجام است) | +| 7 | اجرای موازی روی staging | §۸ | ۵ و ۶ | +| 8 | بستن مسدودکننده‌ها | §۹ | — | +| 9 | روز جابه‌جایی | §۱۰ | همه | + +--- + +## §۰ — ادامه روی سیستم دیگر + +### چه چیزی با git منتقل می‌شود و چه چیزی نه + +| چیز | در git مونوریپو؟ | روی سیستم جدید | +|---|---|---| +| کد مونوریپو و `docs/` | بله، **بعد از commit و push** | clone از `http://get.feep.ir/s.esmaeili/gold-bank.git` | +| همین سند، `docs/PWA-PORT-PLAN.md` و `scripts/check-majors.mjs` | **امروز نه** — هنوز commit نشده‌اند | قبل از ترک این سیستم، commit و push | +| `henza_pwa`، `henza_server`، `henza_admin` | **نه** — هر کدام مخزن git جدای خودش است و مونوریپو آن‌ها را track نمی‌کند | جدا clone و روی commit پین‌شده checkout (جدول بعد) | +| `.env` | نه — ignore شده، و باید همین‌طور بماند | از `.env.example` بسازید؛ secret ها را از جای امن بیاورید | +| `CLAUDE.md` | نه — در ریشه‌ی workspace قبلی (`E:\gold bank`) بوده و اسناد parity به §2، §3، §4 و §6 آن ارجاع می‌دهند | اگر نسخه‌اش را دارید به مخزن اضافه کنید؛ بازسازی قواعدش در `docs/PWA-PORT-PLAN.md` آمده | +| skill `reference-parity-audit` | نه — روی ماشین قبلی | اگر دارید منتقل کنید؛ وگرنه D12 و همین اسناد روش را می‌گویند | +| حافظه‌ی Claude | نه — محلی است | همین اسناد جایش را می‌گیرند | + +### مخزن‌های مرجع — commit های پین‌شده + +| مخزن | remote | commit | نکته | +|---|---|---|---| +| `henza_pwa` | `http://get.feep.ir/henzagold/henza_pwa.git` | `b171fff` (2026-09-02) | مرجع پورت کلاسیک | +| `henza_server` | `http://get.feep.ir/henzagold/henza_server.git` | `37c7d7a` (2026-09-17) | روی سیستم فعلی سه فایل حذف‌شده‌ی commit‌نشده دارد (`resources/js/app.js`، `resources/js/bootstrap.js`، `webpack.mix.js`). این‌ها اسکلت Laravel Mix‌اند و به API ربطی ندارند؛ clone تازه آن‌ها را دارد و در parity فرقی ایجاد نمی‌کند | +| `henza_admin` | `http://get.feep.ir/henzagold/henza_admin.git` | `1b67448` (2026-08-11) | — | + +```bash +git clone http://get.feep.ir/s.esmaeili/gold-bank.git +cd gold-bank +git checkout release/delivery # یا هر شاخه‌ای که این اسناد رویش commit شده‌اند +git clone http://get.feep.ir/henzagold/henza_pwa.git && git -C henza_pwa checkout b171fff +git clone http://get.feep.ir/henzagold/henza_server.git && git -C henza_server checkout 37c7d7a +git clone http://get.feep.ir/henzagold/henza_admin.git && git -C henza_admin checkout 1b67448 +cp .env.example .env # بعد secret ها را پر کنید +pnpm install --frozen-lockfile +``` + +### دو تله + +1. **پوشه‌های `henza_*` در `.gitignore` نیستند.** یک `git add .` یا `git add -A` آن‌ها را به‌صورت «embedded repository» اضافه می‌کند، و clone روی سیستم دیگر به‌جایشان پوشه‌ی خالی می‌گیرد. یا این سه نام به `.gitignore` اضافه شوند، یا همیشه فایل‌ها با نام add شوند. +2. **remote ها `http://` هستند، نه `https://`.** نام کاربری و رمز git بدون رمزنگاری منتقل می‌شوند. + +### ابزار روی سیستم جدید + +- **Node 24.** سیستم فعلی 24.21 دارد؛ `engines` در `package.json` می‌گوید `>=20`؛ ایمیج Docker API روی 20 است (§۵.۱). +- **pnpm 9** (`packageManager: pnpm@9.0.0`). +- **build روی خود ماشین انجام می‌شود، نه داخل Docker** (DEPLOY-03). Dockerfile ها فقط خروجی ساخته‌شده (`release/` و `dist/`) را کپی می‌کنند، پس Node و سیستم‌عامل ماشین build روی خروجی اثر دارند. قبل از `docker compose build` همیشه `pnpm build:release` اجرا شود. +- **ترتیب خواندن:** همین سند ← `docs/parity/PROGRESS.md` ← `docs/parity/DECISIONS.md` ← `docs/PWA-PORT-PLAN.md`. + +--- + +## §۱ — تصمیم مخزن: پروژه‌ی جدید نه + +**وضعیت:** پیشنهاد (۲۰۲۶-۱۰-۰۴)؛ مالک خواست نوشته شود. + +1. **اپ‌ها و API با هم تکامل پیدا می‌کنند.** اپ کلاسیک برای ۱۸ endpoint تازه به `apps/api` وابسته است، و ادمین و اپ‌ها یک دیتابیس و یک schema دارند. جدا کردن اپ و API در دو مخزن یعنی هر تغییر دو PR، و ناهمخوانی نسخه‌ی کلاینت و سرور. +2. **امنیت تولید از جداسازی انتشار می‌آید، نه جداسازی مخزن.** هر اپ همین حالا Dockerfile و سرویس جدا دارد. فقط `build:release` همه را با هم می‌سازد، پس خرابی یک اپ آزمایشی انتشار تولید را هم می‌خواباند. راه‌حل در §۶. +3. **تاریخچه و قواعد اینجاست.** تاریخچه‌ی git و کل `docs/parity` (تصمیم‌ها، PROTECTED، ISSUES) در همین مخزن است؛ مخزن تازه یا گمشان می‌کند یا دو نسخه می‌سازد. + +**چه وقت پروژه‌ی جدید درست است:** اگر `user-app` و `user-app-v2` قرار است محصول، برند یا تیم دیگری شوند. اگر قرار است هرگز منتشر نشوند، بایگانی‌شان کنید (یک tag، و خروج از انتشار) — باز هم بدون مخزن جدید. حذفشان طبق D18 و `PROTECTED.md` تأیید صریح مالک می‌خواهد. + +--- + +## §۲ — واقعیت درباره‌ی «سرعت» + +| بخش | در تولید امروز | وضعیت | جایگزین در مونوریپو | +|---|---|---|---| +| سرور | Laravel 8 + PHP `^7.3` (`henza_server/composer.json`) | پشتیبانی امنیتی Laravel 8 در ژانویه‌ی ۲۰۲۳ و PHP 7.x در ۲۰۲۲ تمام شده | `apps/api` — NestJS + Prisma + Postgres | +| ادمین | Vue 2.6 + Vuex 3 + Vuesax + Tailwind 1 + vue-cli 3 (`henza_admin/package.json`) | Vue 2 پایان ۲۰۲۳ تمام شد | `apps/admin-panel` — Vue 3 | +| اپ کاربر | Vue 3 + Vite + Tailwind 3 — lockfile آن Vue 3.5.40 دارد و هر ۲۳ route آن lazy است | **پشته به‌روز است** | `apps/user-app-classic` (`docs/PWA-PORT-PLAN.md`) | + +پس بردن اپ کاربر روی «Vue 3، Tailwind، Pinia» به‌خودی‌خود سریعش نمی‌کند. اهرم‌های واقعی سرعت در آن: + +1. **کل CSS ant-design** با `import 'ant-design-vue/dist/antd.less'` (`henza_pwa/src/main.ts:7`) → فقط استایل کامپوننت‌های استفاده‌شده. +2. **کل CSS vant، دو بار:** `vant/lib/index.css` هم در `main.ts:10` و هم در `main.ts:11`. +3. **درخواست‌های تکراری:** `/price` از دو مسیر جدا (`store/store.ts:114` و `Dashboard.vue:634`)؛ `/front` دو بار (`home/index.vue:184` و `Mixins/getProducts.ts:46`). +4. **بدون صفحه‌بندی:** `/get-orders` و `/get-my-findoc` کل داده را برمی‌گردانند. +5. **گزارش حساب** از یک سیستم حسابداری بیرونی proxy می‌شد (D24)؛ در هدف از دفتر داخلی خوانده می‌شود. + +همه‌ی این‌ها زیر کاپوت‌اند و ظاهر را عوض نمی‌کنند، پس با قرارداد «دقیقاً مثل مرجع» در `docs/PWA-PORT-PLAN.md` سازگارند. + +--- + +## §۳ — اندازه‌گیری مبنا (قدم ۲) + +بدون عدد «قبل»، معلوم نیست کندی کجاست و بعداً هم نمی‌شود نشان داد سریع‌تر شده. **روی تولید آزمون بار (load test) اجرا نشود.** + +| لایه | ابزار | چه چیزی ثبت شود | +|---|---|---| +| اپ — مهمان | `npx lighthouse https://app.henzagold.com/home --only-categories=performance --output=json --output-path=baseline-home.json` (پیش‌فرض Lighthouse شبیه‌سازی موبایل است)؛ همین برای `/search` و یک `/product/` | LCP، TBT، حجم JS و CSS منتقل‌شده، تعداد درخواست‌ها | +| اپ — واردشده | Chrome DevTools ← Performance و Network، روی یک گوشی متوسط واقعی | زمان تا نمایش میز کار؛ تعداد و زمان درخواست‌های شروع | +| سرور | لاگ nginx با `$request_time` یا لاگ Laravel، در یک روز کاری | p50 و p95 برای `/front`، `/get-products`، `/me-data`، `/get-orders`، `/get-my-findoc`، `/price`، `/fin_tahesab/*`، `/set-order` | +| دیتابیس | slow query log (مثلاً بالای 0.5 ثانیه)، یک روز | ده کوئری کندتر | + +| سنجه | مبنا (henza) | بعد از جابه‌جایی | +|---|---|---| +| LCP خانه (موبایل) | | | +| حجم JS + CSS خانه | | | +| تعداد درخواست تا میز کار | | | +| p95 `/front` | | | +| p95 `/get-products` | | | +| p95 `/set-order` | | | + +--- + +## §۴ — استاندارد نسخه‌ها + +«دو نسخه عقب‌تر» به‌عنوان قاعده‌ی ثابت جواب نمی‌دهد: دو نسخه قبل از vue-router فعلی، router مخصوص Vue 2 است که مرده؛ و دو نسخه قبل از NestJS فعلی، همان نسخه‌ای است که الان دارید و از ژانویه patch نگرفته. + +**قاعده:** جدیدترین major که حدود ۶ ماه از انتشارش گذشته، هنوز patch می‌گیرد و وابستگی‌ها پشتیبانی‌اش می‌کنند. **دو استثنا:** + +- **ابزارهای زمان build** (TypeScript، vue-tsc، Vite) روی سرور اجرا نمی‌شوند؛ patch نگرفتنشان ریسک امنیتی زمان اجرا ندارد. +- **کتابخانه‌هایی که ظاهر را می‌سازند** مستقل از سنشان ثابت می‌مانند (جدول دوم). + +داده‌ی npm در ۲۰۲۶-۱۰-۰۴ — با `node scripts/check-majors.mjs` تازه کنید: + +| بسته | جدیدترین | پیشنهاد | الان در مونوریپو | خط فعلی آخرین بار کی patch شد | نوع | +|---|---|---|---|---|---| +| Node | 26 (هنوز LTS نیست) | **24 LTS** | Docker API: **20** | Node 20 از ۲۰۲۶-۰۴-۳۰ پشتیبانی ندارد | زمان اجرا | +| NestJS | 12.1.2 (یک‌ماهه) | **11.x** — فعال | `^10.4.0` | 10.4.22 در ۲۰۲۶-۰۱-۱۰ | زمان اجرا | +| Prisma | 8 (RC) / 7.10.0 | **6.19.x** حالا؛ 7 بعد از تولید | `^5.19.0` | 5.22.0 در ۲۰۲۴-۱۱-۰۵ | زمان اجرا | +| ایمیج nginx | `latest` | **tag ثابت** | `latest` | — | زمان اجرا | +| Vue | 3.5.43 | **3.5.x** | `^3.5.0` | فعال | کلاینت | +| vue-router | 5.3.1 | **4.6.4** (آخرین 4.x) | `^4.4.0` | 4.6.4 در ۲۰۲۵-۱۲-۱۱ | کلاینت | +| Pinia | 4.0.3 (سه‌ماهه) | **2.3.1** (آخرین 2.x) | `^2.2.0` | 2.3.1 در ۲۰۲۵-۰۱-۲۰ | کلاینت | +| Vite | 8.3.2 | **7.3.x** | `^5.4.0` | 5.4.21 در ۲۰۲۵-۱۰-۲۰ | build | +| @vitejs/plugin-vue | 6.0.9 | **6.0.x** | `^5.1.0` | 5.2.4 در ۲۰۲۵-۰۵-۰۹ | build | +| vite-plugin-pwa | 2.0.0 (یک‌روزه) | **1.3.x** | `^0.20.5` | 0.21.2 در ۲۰۲۵-۰۳-۲۱ | build | +| TypeScript | 7.0.2 (سه‌ماهه، کامپایلر تازه) | **5.9.3** | `^5.5.0` | 5.9.3 در ۲۰۲۵-۰۹-۳۰ | build | +| vue-tsc | 3.3.12 | **3.x** | `^2.1.0` | 2.2.12 در ۲۰۲۵-۰۷-۰۲ | build | + +**چرا Pinia 3 نه:** خط 3 هم ۳۳۳ روز است release نداشته، و برای پروژه‌ی Vue 3 چیزی بیشتر از 2 نمی‌دهد. تنها خط فعال 4 است که هنوز سه ماه بیشتر عمر ندارد — بعداً. +**چرا Prisma 6 با اینکه ۱۸۶ روز release نداشته:** پله‌ی کوچک‌تر و کم‌ریسک‌تر از 5 است (§۵.۷)؛ مقصد بلندمدت 7 است که بعد از تولید می‌آید. + +سازگاری این ترکیب بررسی شد: `@vitejs/plugin-vue@6` با Vite 5 تا 8، `vite-plugin-pwa@1.3` با Vite 3 تا 8، `vue-tsc@3` با TypeScript ≥ 5.0، Vite 7 با Node ≥ 20.19، و NestJS 11 با Node ≥ 20 جور است. + +### کتابخانه‌هایی که به‌خاطر ظاهر ثابت می‌مانند + +| بسته | نسخه | چرا | +|---|---|---| +| `ant-design-vue` | 3.2.20 برای اپ‌های کاربر؛ 4.2.6 برای ادمین | v4 استایل را به CSS-in-JS برده. **هیچ‌کدام از دو خط از نوامبر ۲۰۲۴ release نداشته‌اند** — ریسک بلندمدتی که به‌خاطر ظاهر پذیرفته می‌شود | +| `tailwindcss` | 3.0.18 | Tailwind 4 بازنویسی است و پیش‌فرض‌هایش ظاهر را عوض می‌کند | +| `vant` | 4.10.x (اپ کلاسیک دقیقاً 4.10.0) | — | +| `swiper` | 7.4.1 | — | +| `@vueuse/core` | 9.13.0 | مرجع با همین نسخه است؛ ارتقا رفتار `useTransition` و `useInfiniteScroll` را عوض می‌کند و تست جدا می‌خواهد | + +**پین:** نسخه‌ها بدون `^` و `~`، lockfile commit‌شده، و `pnpm install --frozen-lockfile` روی ماشین build. + +--- + +## §۵ — قدم‌های ارتقا (قدم ۳) + +**قواعد همه‌ی زیرقدم‌ها:** + +- هر زیرقدم یک commit یا PR جدا روی شاخه‌ی خودش (`chore/upgrade-*`)، با تست کامل، **بدون کار فیچر قاطی**. +- `admin-panel`، `user-app` و `user-app-v2` در `PROTECTED.md` هستند: ارتقای ابزارشان **تأیید صریح مالک** و اسکرین‌شات قبل و بعد از صفحه‌های اصلی می‌خواهد. هیچ تغییر ظاهری یا رفتاری مجاز نیست. +- برای دیتابیس، روند D14 و D15 برقرار است: هرگز `migrate reset` و هرگز دست زدن به `_prisma_migrations`. + +ترتیب از کم‌ریسک به پرریسک است، به‌جز Node که اول می‌آید چون Vite 7 به Node ≥ 20.19 نیاز دارد و Node 20 تمام شده. + +### ۵.۱ — Node 24 + +- **تغییر:** `apps/api/Dockerfile` از `node:20-bookworm` به `node:24-bookworm`؛ `engines.node` به `">=24 <25"`؛ فایل `.nvmrc` با مقدار `24`. +- **ممکن است بشکند:** ریسک پایین — `apps/api/package.json` هیچ وابستگی native ندارد (بررسی شد). +- **سنجش:** `pnpm build:release`؛ بالا آمدن API در Docker؛ `apps/api/scripts/auth-smoke.sh` (۴۶ assertion)؛ `pnpm test` در api. + +### ۵.۲ — پین ایمیج nginx + +- **تغییر:** نسخه‌ی واقعی `latest` فعلی را پیدا کنید (`docker run --rm docker.arvancloud.ir/nginx:latest nginx -v`) و در سه Dockerfile همان tag را بنویسید. +- **رفتار:** بی‌تغییر؛ فقط از این به بعد هر build همان nginx را می‌گیرد. + +### ۵.۳ — پین دقیق و patch های داخل همان خط + +- **تغییر:** حذف `^` و `~` از وابستگی‌های همه‌ی `package.json` ها؛ بالا بردن به آخرین patch در همان major: Vue 3.5.43، vue-router 4.6.4، Pinia 2.3.1، TypeScript 5.9.3؛ `pnpm install --frozen-lockfile` روی ماشین build و در CI. +- **سنجش:** روی یک clone تازه، `pnpm install --frozen-lockfile` بدون تغییر lockfile تمام شود؛ typecheck و build همه‌ی اپ‌ها. + +### ۵.۴ — vue-tsc 3 + +- **ممکن است بشکند:** vue-tsc 3 خطاهای تایپی تازه در template ها پیدا می‌کند. همه درست شوند، نه خاموش. +- **سنجش:** typecheck تمیز در `admin-panel`، `user-app` و `user-app-v2`. + +### ۵.۵ — Vite 7، plugin-vue 6، vite-plugin-pwa 1.3 + +- **مهم‌ترین تله:** طبق راهنمای مهاجرت Vite 7، پیش‌فرض `build.target` از `'modules'` به `'baseline-widely-available'` عوض شده — یعنی مرورگرهای جدیدتر (حدود Chrome 107 و Safari 16). **هیچ‌کدام از `vite.config.ts` ها `build.target` ندارند** (بررسی شد)، پس گوشی‌های قدیمی‌تر کاربران بی‌صدا از کار می‌افتند. ← `build.target` صریحاً روی مقدار قبلی، `['es2020', 'edge88', 'firefox78', 'chrome87', 'safari14']`، گذاشته شود، مگر مالک بر اساس آمار گوشی‌های کاربران تصمیم دیگری بگیرد. قبل از اجرا با راهنمای رسمی تطبیق داده شود. +- `vite-plugin-pwa@1.3` به `workbox-build` و `workbox-window` ^7.4.1 نیاز دارد. +- **سنجش:** build هر سه اپ؛ سناریوی دود (ورود، کاتالوگ، سبد)؛ نصب PWA و جریان به‌روزرسانی؛ اسکرین‌شات قبل و بعد. + +### ۵.۶ — NestJS 11 (همه‌ی بسته‌های `@nestjs/*` با هم) + +- **ممکن است بشکند:** NestJS 11 روی Express 5 است. route با wildcard یا `forRoutes('*')` در کد نیست (بررسی شد) — رایج‌ترین شکست صدق نمی‌کند. ولی parser پیش‌فرض query در Express 5 «simple» شده؛ هر endpoint که query تودرتو می‌خواند باید چک شود. با راهنمای مهاجرت رسمی تطبیق داده شود. +- **سنجش:** jest کامل؛ `auth-smoke.sh`؛ تست‌های توکن متقابل (T1). + +### ۵.۷ — Prisma 6.19 + +- **ممکن است بشکند:** مهم‌ترین شکست‌های Prisma 6 اینجا صدق نمی‌کنند (بررسی شد): در migration ها رابطه‌ی many-to-many ضمنی نیست، و فیلد `Bytes` و `NotFoundError` استفاده نشده‌اند. +- **سنجش:** بعد از ارتقا، `prisma migrate diff --from-schema-datasource --to-schema-datamodel --script` باید «empty migration» بدهد؛ `prisma migrate status`؛ jest؛ دود. + +### ۵.۸ — بعداً، نه قبل از تولید + +Prisma 7، Pinia 4، vue-router 5، Vite 8، TypeScript 6 و 7، NestJS 12 — هر کدام وقتی پخته شد، جدا و با تست. **Tailwind 4 و ant-design-vue 4 هرگز برای اپ‌های کاربر**، چون ظاهر عوض می‌شود. + +--- + +## §۶ — جداسازی انتشار (قدم ۴) + +- **مجموعه‌ی تولید:** `apps/api`، `apps/admin-panel`، و `apps/user-app-classic` وقتی آماده شد. **آزمایشی:** `apps/user-app`، `apps/user-app-v2`. +- **اسکریپت تازه‌ی `build:prod`** فقط برای مجموعه‌ی تولید. `build:release` فعلی دست نمی‌خورد، چون `DELIVERY.md` به آن تکیه دارد. +- **`docker-compose.yml`:** سرویس‌های آزمایشی زیر `profiles: ["experimental"]` بروند، تا `docker compose up` فقط تولید را بالا بیاورد و آزمایشی‌ها با `--profile experimental` بیایند. `DELIVERY.md` هم‌زمان به‌روز شود. +- **شاخه‌ها:** `main` همیشه آماده‌ی تولید؛ کار روی شاخه‌های `chore/upgrade-*` و `feature/classic-*`؛ شاخه‌ی انتشار از `main` بریده شود. امروز همه‌چیز روی `release/delivery` است. +- **DEPLOY-03 — build داخل Docker با multi-stage:** حالا که کار ممکن است بین ماشین‌ها جابه‌جا شود، اولویتش بالاتر است. خروجی نباید به Node و سیستم‌عامل ماشین build وابسته باشد. + +--- + +## §۷ — مهاجرت داده و تطبیق (قدم ۶) — بزرگ‌ترین شکاف + +**واقعیت:** ابزار انتقال داده از دیتابیس henza به Postgres نه در مخزن هست و نه در اسناد (جست‌وجوی ۲۰۲۶-۱۰-۰۴). موتور دیتابیس henza هم از مخزن معلوم نیست — `config/database.php` پیش‌فرض `'localhost'` دارد و `.env.example` تنظیم DB ندارد. احتمالاً MySQL است؛ از سرور تولید تأیید شود. + +**ویژگی‌های اجباری ابزار:** + +- تکرارپذیر و idempotent؛ همیشه روی **کپی** داده‌ی تولید. +- برای هر موجودیت یک جدول نگاشت `legacyId ← id جدید` نگه دارد. لازم است برای: + - تطبیق؛ + - ترتیب ۹ دسته‌ی خانه که با id عددی هاردکد شده (Q5 در `docs/PWA-PORT-PLAN.md`)؛ + - لینک‌های قدیمی محصول (`/product/123`) که کاربران فرستاده‌اند — بدون نگاشت، به خانه redirect می‌شوند؛ + - سبدهای باز `appCart` روی گوشی‌ها. +- یک گزارش تطبیق بسازد (جدول دوم). + +| henza | مقصد | نکته | +|---|---|---| +| مشتری (`customers`) | `Customer` | نام ← `firstName`/`lastName` (D0)؛ `type` ← `CustomerKind`؛ `percent` ← `makingChargeOffsetPercent` (D9)؛ `gram_level` ← `goldCreditLimitGrams`؛ `remain` ← `goldDebtGrams`؛ `rial_remain` ← `fiatDebtRial` **با بررسی واحد** (نقص #19 در `docs/PWA-PORT-PLAN.md`) | +| کارشناس فروش (`customers.type = 1`) | `AdminUser` با نقش `SALES_EXPERT` + `SalesExpertProfile` | D17 و D21 | +| مشتری تک (`SingleCustomer`/`login`) | `Customer` با `kind = RETAIL` | D13 — داده حفظ شود، حتی اگر خارج از کار است | +| کاربران ادمین (`users`) و `permission` | `AdminUser` + `UserType` + `Permission` | — | +| محصول، تنوع، تصویر، دسته، برند، کالکشن | مدل‌های هم‌نام | **فایل‌های تصویر هم کپی شوند** (`UPLOAD_DIR`) | +| سفارش‌ها — عمده و تک، دو جدول جدا در henza | `Order` + `OrderItem` + `OrderStatusEvent` | `sts` عددی ← `OrderStatus` | +| سفارش ساخت | `PreOrder` | — | +| اسناد مالی (`cash`) | `LedgerEntry` | فقط تأییدشده‌ها در موجودی حساب می‌شوند (D24) | +| کیف پول و cashback | مدل‌های موجود | D13 — حفظ، بدون کار | +| علاقه‌مندی، تخفیف و استثنا، اعلان، شهر، تنظیمات | مدل‌های موجود یا تازه | علاقه‌مندی جدول تازه می‌خواهد (`docs/PWA-PORT-PLAN.md`) | + +| بررسی تطبیق | آستانه | +|---|---| +| تعداد مشتری، کارشناس و کاربر ادمین | دقیقاً برابر | +| بدهی طلای هر مشتری (`remain` ↔ `goldDebtGrams`) | **صفر اختلاف** | +| بدهی ریالی هر مشتری | **صفر اختلاف**، بعد از تبدیل واحد | +| سقف اعتبار و آفست اجرت هر مشتری | صفر اختلاف | +| تعداد سفارش به تفکیک وضعیت؛ جمع وزن سفارش‌های باز | برابر | +| تعداد و جمع اسناد تأییدشده و تأییدنشده | برابر | +| تعداد محصول، تنوع و تصویر؛ فایل تصویر گم‌شده | برابر؛ صفر | + +**شماره‌ی تکراری بین مشتری و کارشناس** قبل از مهاجرت فهرست شود. یکتایی شماره بین `Customer` و `AdminUser` امروز اجرا نمی‌شود — هر چهار بررسی تداخل فقط `adminUser` را کوئری می‌کنند — و در `ISSUES.md` هم ثبت نشده. قاعده‌ی مالک در D21: «کارشناس برنده است». + +--- + +## §۸ — اجرای موازی روی staging (قدم ۷) + +- یک دامنه‌ی staging؛ هر شب داده‌ی تولید با ابزار §۷ منتقل و تطبیق شود. +- **پیامک در staging خاموش، یا فقط به شماره‌ی تست.** داده‌ی واقعی یعنی شماره‌ی واقعی مشتری‌ها. +- کاربران داخلی جریان‌های واقعی را روی staging امتحان کنند. +- **معیار آمادگی:** مثلاً سه شب پیاپی تطبیق تمیز، و هیچ باگ مسدودکننده‌ی باز. + +--- + +## §۹ — مسدودکننده‌های قبل از تولید (قدم ۸) + +| مورد | منبع | +|---|---| +| مقدار جدا برای `JWT_CUSTOMER_SECRET` (REL-03) | `DELIVERY.md` §A1 | +| حذف `OTP_MASTER_CODE` (REL-01) | §A2 | +| `JWT_SECRET` واقعی | §A3 | +| اعتبارنامه‌ی SMS (REL-02) | §A4 | +| چرخاندن اعتبارنامه‌های نشت‌کرده: پنل SMS (SEC-12)، توکن‌های PayPing و Tahesab و pepper کیف پول (SEC-06). **هم‌زمان با سیستم قدیم**، چون همان حساب‌های زنده‌اند؛ چرخاندن یک‌طرفه پیامک سیستم قدیم را قطع می‌کند | §B | +| MON-08 — سرور اجرت و تخفیف را از کلاینت نپذیرد. وضعیت فعلی چک شود | `docs/parity/ISSUES.md` | +| MON-05 — اجرای `canPlaceOrders` و `verificationStep` | `docs/parity/ISSUES.md` | +| یکتایی شماره بین `Customer` و `AdminUser` | §۷ | + +--- + +## §۱۰ — روز جابه‌جایی (قدم ۹) + +### شرط‌های go/no-go + +- همه‌ی موارد §۹ بسته‌اند. +- معیار آمادگی §۸ برقرار است. +- برگشت (rollback) یک بار تمرین شده. +- به کاربران اطلاع داده شده: زمان قطعی؛ یک بار ورود دوباره با OTP، چون توکن‌های قدیمی باطل می‌شوند؛ خالی شدن سبدهای باز، مگر با نگاشت §۷ منتقل شوند. + +### گام‌ها + +1. سیستم قدیم در حالت نگهداری؛ نوشتن قفل. +2. مهاجرت نهایی و گزارش تطبیق ← **تصمیم go/no-go.** +3. سوئیچ مسیرها روی **همان دامنه‌ها**: + - service worker جدید در همان مسیر `/sw.js`، تا جای قبلی را بگیرد. + - نسخه‌ی اپ کلاسیک بالاتر از `1.0.7` باشد، تا دیالوگ مرجع «نسخه جدید هنزا منتشر شد!» یک بار برای همه بیاید و اپ تازه شود (`henza_pwa/src/App.vue:56-74`). +4. اولین باز شدن بعد از سوئیچ ممکن است پوسته‌ی کش‌شده‌ی قدیمی را نشان دهد که به API قدیم (`henzagold.com/oapi/api/`) درخواست می‌فرستد. آن مسیر تا مدتی پاسخ تمیز (مثلاً 503) بدهد، نه timeout. +5. آزمون دود: ورود با OTP؛ موجودی میز کار برابر گزارش تطبیق؛ ثبت سفارش روی یک مشتری تست؛ ورود ادمین و دیدن همان سفارش؛ رسیدن پیامک. +6. باز کردن برای همه. + +### برگشت + +- سیستم قدیم چند روز (مثلاً ۱۴ روز) فقط‌خواندنی بماند؛ پشتیبان کامل از هر دو دیتابیس. +- **مهلت تصمیم برگشت از قبل تعیین شود** (مثلاً ۲ ساعت بعد از باز کردن). بعد از آن، سفارش‌ها و اسناد تازه فقط در سیستم جدیدند و برگشت یعنی انتقال دستی آن‌ها به دیتابیس قدیم. +- **برگشت سمت گوشی‌ها فوری نیست:** service worker جدید روی گوشی‌ها کش شده. برای برگرداندن، نسخه‌ی قدیم باید با شماره‌ی نسخه‌ی بالاتر دوباره منتشر شود. + +--- + +## §۱۱ — بعد از جابه‌جایی + +- مقایسه با مبنای §۳ و ثبت نتیجه در جدول همان بخش. +- پایش خطا و زمان پاسخ در هفته‌ی اول. +- بعد: ارتقاهای §۵.۸؛ وصل کردن `user-app` و `user-app-v2` به قابلیت‌های تازه‌ی بک‌اند (تنش ۱ در `docs/PWA-PORT-PLAN.md`). + +--- + +# وضعیت اجرا + +| # | قدم | وضعیت | +|---|---|---| +| 1 | راه‌اندازی روی هر سیستم + commit و push اسناد | ⬜ | +| 2 | اندازه‌گیری مبنا | ⬜ | +| 3 | ارتقای پشته (۵.۱ تا ۵.۷) | ⬜ | +| 4 | جداسازی انتشار | ⬜ | +| 5 | پورت اپ کلاسیک | ⬜ منتظر تصمیم‌های مالک در `docs/PWA-PORT-PLAN.md` | +| 6 | ابزار مهاجرت داده و تطبیق | ⬜ | +| 7 | اجرای موازی روی staging | ⬜ | +| 8 | بستن مسدودکننده‌ها | ⬜ | +| 9 | روز جابه‌جایی | ⬜ | diff --git a/docs/PWA-PORT-PLAN.md b/docs/PWA-PORT-PLAN.md new file mode 100644 index 0000000..bb2ce47 --- /dev/null +++ b/docs/PWA-PORT-PLAN.md @@ -0,0 +1,538 @@ +# پلن پورت اپ کاربر کلاسیک — henza_pwa → apps/user-app-classic + +سند برنامه‌ریزی. **قبل از تأیید شما هیچ کدی نوشته نمی‌شود.** + +> **مرجع پین‌شده:** `henza_pwa` در کامیت `b171fff` (2026-09-02، «fee in bonakdar removed»). +> هر ارجاع `path:line` در این سند به همین نسخه است و نسبت به `henza_pwa/` نوشته شده. +> منبع اصلی شواهد: `docs/parity/_inv-ref-pwa.md` (سیاهه‌ی کامل مرجع) به‌علاوه‌ی +> راستی‌آزمایی مستقیم روی کد. اگر مرجع عوض شد، این سند باید دوباره راستی‌آزمایی شود. + +--- + +## خواسته + +1. یک اپ کاربر **سوم** در مونوریپو، کنار `user-app` و `user-app-v2`. +2. طراحی آن = **طراحی قبلی همین سیستم**، یعنی `henza_pwa`. +3. همه‌ی صفحه‌ها، کارکردها، عملیات، انیمیشن‌ها و ناوبری **دقیقاً مثل مرجع**. +4. با **تکنولوژی جدید و کد بهتر**. +5. **هیچ چیزی از بین نرود.** + +--- + +## قرارداد «دقیقاً مثل مرجع» + +| لایه | قاعده | +|---|---| +| **آنچه کاربر می‌بیند و انجام می‌دهد** | **عیناً مرجع.** چیدمان و ابعاد، رنگ‌ها، فونت، آیکون‌ها، متن‌های فارسی (بایت‌به‌بایت — حتی فاصله‌ی دوتایی بعد از «لطفا» و غلط املایی «کارکتر»)، ارقام، ترتیب و برچسب تب‌ها، پیام‌های اعتبارسنجی، رنگ و مدت toast ها، پارامترهای انیمیشن، مسیرها و نام route ها، رفتار guard ها، محاسبه‌ها و گرد کردن‌ها. | +| **زیر کاپوت** | **بازنویسی‌شده و بهتر.** TypeScript با `strict`، Pinia، composable ها، یک لایه‌ی HTTP تایپ‌شده، تنظیمات از env، حذف کد مرده و تکراری، تست. | +| **کوییرک بصری بی‌خطر** | **عیناً تکرار می‌شود** — مثلاً notify ورود که حتی برای موفقیت قرمز است، یا تشدید سرگردان در «ّکاربر فعال شود؟». | +| **استثنا** | **فقط خرابی‌های کارکردی مرجع، طبق D4.** هر کدام شماره دارد، در §تنش ۴ و §چیزی که پورت نمی‌شود فهرست شده، و هیچ‌کدام بی‌صدا نیست. | + +«هیچ چیزی از بین نرود» دو معنا دارد و هر دو تعهد این پلن است: + +1. **هیچ صفحه، کارکرد، انیمیشن یا متنی از مرجع جا نمی‌ماند** — با سرشماری خودکار اثبات می‌شود (§معیار تمام شد). +2. **هیچ چیز موجود در مونوریپو آسیب نمی‌بیند** — `user-app`، `user-app-v2` و `admin-panel` دست نمی‌خورند؛ تغییرات `apps/api` فقط افزایشی است. + +--- + +## قواعد حاکم (ارث‌برده از پروژه) + +1. `henza_pwa/`، `henza_server/` و `henza_admin/` **فقط خواندنی‌اند.** برای اجرای مرجع یک کپی بیرون از آن‌ها ساخته می‌شود؛ حتی `node_modules` هم داخلشان نصب نمی‌شود. +2. **D4** — رفتار تکرار می‌شود، خرابی نه. هر عدم‌تکرار عمدی در ماتریس: «عمداً تکرار نشد — ISSUE #n». +3. **D2** — چیزی که مرجع ندارد ساخته نمی‌شود. یعنی اپ کلاسیک صفحه‌ی اعلان‌ها، تنظیمات ظاهر و تب پنجم ناوبری را **ندارد**، چون مرجع ندارد. +4. **PROTECTED.md** — `user-app` و `user-app-v2` دست نمی‌خورند. حتی بازساختاردهی (مثلاً بیرون کشیدن کد مشترک) تأیید جدا می‌خواهد. +5. **D13** — سطوح فروشگاه تک‌فروشی (کیف پول، cashback، قیمت محصول طبق D16) دست نمی‌خورند. +6. **D12** — ماتریس فیلد به فیلد، و تست رفت‌وبرگشت برای هر موجودیتی که endpoint نوشتنی تازه می‌گیرد. +7. **D18** — تأیید دائمی برای شکاف‌های بی‌ابهام؛ توقف فقط برای موارد فهرست D18. +8. **T1 / D21** — مشتری همیشه از توکن، هرگز از بدنه یا URL؛ ادعای `typ`؛ تست توکن متقابل. +9. **D25** — خواندن env فقط با `envOr` / `envOrNull`. +10. **D15** — تست‌هایی که ورودی فارسی دارند، ورودی را از فایل UTF-8 می‌خوانند. + +> ⚠️ **`CLAUDE.md` در این مخزن نیست.** اسناد parity مدام به §2، §3، §4 و §6 آن ارجاع می‌دهند، +> ولی فایل در ریشه‌ی workspace قبلی بوده. قواعدش از روی همین ارجاع‌ها بازسازی شده: +> §2 منطق اپ‌های کاربر باید یکسان باشد و فقط طراحی فرق کند؛ §3 باگ مرجع تکرار و ثبت شود، +> مگر حفره‌ی امنیتی در جریان پول — آنجا توقف و سؤال؛ §4 PROTECTED فقط افزایشی؛ +> §6 هر حذف داده در CHANGELOG ثبت شود. +> **پیشنهاد: نسخه‌ی اصلی را به مخزن برگردانید** تا متن اصلی جای این بازسازی را بگیرد. + +--- + +## دامنه‌ی واقعی کار (اعداد راستی‌آزمایی‌شده) + +| سنجه | عدد | نحوه‌ی راستی‌آزمایی | +|---|---|---| +| route | **22** (۲۱ واقعی + catch-all) | `_inv-ref-pwa.md` §3 | +| فایل `.vue` غیر vendored | **77** فایل، **12,445** خط | `find src -name '*.vue'` بدون دو پوشه‌ی DatePicker | +| فایل‌های `views/` | 34 — سه‌تا مرده: `login.vue` (کل فایل کامنت)، `user/src/components/report.vue` (صفحه‌ی دیباگ)، `NotificationList.vue` (سوار نشده) | §6.1 | +| کامپوننت اپ | 40 | §9 | +| store | ۹ singleton دست‌ساز + ۱ Pinia | §7 | +| endpoint یکتا | **44** در ۵۸ محل فراخوانی → **۳۹ ردیف کارکردی زنده** بعد از کنار گذاشتن مرده‌ها | §2.4 + §نقشه‌ی endpoint در همین سند | +| پوشش API هدف از آن ۳۹ ردیف | ✅ **12** · 🟡 **9** · ❌ **18** | §نقشه‌ی endpoint | +| `@keyframes` | 8 | grep | +| محل `` | ۷ زنده + ۲ کامنت‌شده (از جمله **انتقال بین صفحه‌ها**) | grep | +| GSAP / `useTransition` | ۱ / ۱ — هر دو در ناوبری پایین | `navigation.vue:83,110` | +| overlay | ۲۰ `Drawer`، ۳ `Modal`، ۲ `van-action-sheet` | grep | +| asset | ۲۴ svg، ۹ png، ۱ jpg، ۱ ico، فونت Yekan Bakh FaNum (۸ وزن × ۳ فرمت)، `icons.json` | `find src/assets public` | +| نقص شناخته‌شده‌ی مرجع | 20 | §13 | + +--- + +## «تکنولوژی جدید» — چه چیزی عوض می‌شود و چه چیزی نه + +### کتابخانه‌های UI روی نسخه‌ی lockfile مرجع قفل می‌شوند + +**یافته:** `yarn.lock` مرجع خودش Vue **3.5.40** را resolve کرده. پس «تکنولوژی جدید» در +کتابخانه‌های UI معنایی ندارد — و نباید داشته باشد، چون ظاهر پیکسلی دقیقاً به همین نسخه‌ها +وابسته است. مثلاً ant-design-vue 4 استایل را به CSS-in-JS برده و ظاهر عوض می‌شود. + +| بسته | نسخه‌ی دقیق (بدون `^`) | +|---|---| +| `ant-design-vue` | 3.2.20 | +| `vant` | 4.10.0 — v1/v2 روی 4.10.2 هستند؛ اپ کلاسیک نسخه‌ی مرجع را می‌گیرد | +| `swiper` | 7.4.1 | +| `gsap` | 3.15.0 | +| `tailwindcss` | 3.0.18 | +| `@vueuse/core` | 9.13.0 | +| `moment-jalaali` | 0.10.4 | +| `@lottiefiles/dotlottie-vue` | 0.4.18 | +| `vue3-lazyload` | 0.2.5-beta | +| `vue` / `vue-router` | 3.5.x / 4.x — همان خانواده‌ی lockfile | + +**ارقام فارسی از فونت می‌آیند، نه از تبدیل.** مرجع هیچ تبدیل رقمی ندارد (§8.1)؛ ارقام +به‌خاطر فونت FaNum فارسی دیده می‌شوند. اپ کلاسیک هم تبدیل رقم اضافه نمی‌کند. + +### آنچه واقعاً نو می‌شود + +| مرجع | اپ کلاسیک | چرا | +|---|---|---| +| Vite 3 + plugin-vue 2، TS 4.4 بدون strict، vue-tsc 0.29 | Vite 5، TS 5 با `strict`، vue-tsc 2 — همان زنجیره‌ی v1/v2 | ابزار نگهداری‌شده؛ خطا در زمان build | +| axios 0.25 با سه instance؛ interceptor خطا promise را برنمی‌گرداند؛ response interceptor ندارد | یک client تایپ‌شده؛ توکن در هر درخواست؛ رفتار 401 مرجع (`myStore.init()`) به‌صورت سراسری | نقص #1 و #2 | +| ۹ singleton کلاسی دست‌ساز | Pinia — **با همان شکل state و همان getter های readonly** | یکدستی با v1/v2 | +| Mixin ها (`getProducts`، `product`، `favorite`، ...) | composable های تایپ‌شده با تست واحد | قابل تست | +| URL و کلید هاردکد در ۵ فایل | متغیرهای `VITE_*` | پیکربندی محیط‌ها | +| دو درخت vendored از DatePicker (fork از ant-design-vue) | `generatePicker` عمومی ant-design-vue 3 با config جلالی — **به شرط اثبات ظاهر یکسان در Phase 0** | حذف هزاران خط کد شخص ثالث | +| ۵ کتابخانه‌ی تاریخ | `moment-jalaali` برای فرمت‌ها (خروجی یکسان) + dayjs/jalaliday فقط برای picker | کمتر، با تست خروجی | +| ۴ کامپوننت تقریباً یکسان `ScrollFetch*` | یک کامپوننت با prop | تکرار | +| `addTransaction.vue` و `addFindoc.vue` تکراری | یک فرم، دو پوسته (صفحه و drawer) | تکرار | +| کد مرده (§چیزی که پورت نمی‌شود) | پورت نمی‌شود | — | +| PWA در `mode: "development"` | production | نقص #17 | +| بدون تست | تست واحد (adapter، formatter، validator، موتور تخفیف) + e2e و مقایسه‌ی تصویری با Playwright | اثبات «دقیقاً» | + +--- + +## تنش‌هایی که قبل از شروع باید تصمیمشان را بدانید + +### تنش ۱ — ۱۸ endpoint مرجع در API هدف نیست + +از ۳۹ ردیف زنده، **۱۸ تا در هدف معادل ندارند و ۹ تا ناقص‌اند** (§نقشه‌ی endpoint). بدون آن‌ها +«دقیقاً مثل مرجع» ممکن نیست. وضعیت امروز v1/v2 این را نشان می‌دهد: + +- ثبت‌نام **هیچ چیزی ذخیره نمی‌کند** — کامنت خود `RegisterView.vue:17` این را می‌گوید. +- علاقه‌مندی‌ها **فقط محلی‌اند** (`favoritesStore.ts:1-3`). +- ثبت سند، سفارش ساخت، مدیریت مشتری توسط کارشناس، ویرایش سفارش مشتری، آنالیتیکس و فاکتور اصلاً وجود ندارند. + +ساختن این‌ها در `apps/api` افزایشی است (route و جدول/ستون جدید). **ولی با `CLAUDE.md` §2 +تعارض دارد:** منطق اپ‌های کاربر باید یکسان باشد. به محض اینکه اپ کلاسیک ثبت‌نام و +علاقه‌مندی واقعی داشته باشد، v1 و v2 عقب می‌افتند. + +**پیشنهاد:** بک‌اند **یک بار**، افزایشی و تست‌شده ساخته شود. وصل کردن v1/v2 به آن یک کار +جدا و ثبت‌شده باشد — نه بخشی از این پروژه. **تصمیم شما لازم است** که §2 شامل اپ کلاسیک +هم می‌شود یا نه. + +### تنش ۲ — «انتخاب مشتری»: کار کردن به‌جای مشتری + +**در مرجع:** کارشناس فروش (type 1) روی یک مشتری می‌زند → `toggleActiveUser` توکن خود آن +مشتری را به‌عنوان `activeChildToken` ذخیره می‌کند، و این توکن **روی همه‌ی درخواست‌ها +اولویت دارد** (`store/tokenManage.ts:8-10`، `store/myDataStore.ts:159-169`). از آن لحظه کل اپ +به‌جای مشتری کار می‌کند: میز کار موجودی مشتری را نشان می‌دهد، سفارش سبد روی حساب مشتری +با `from_parent: true` ثبت می‌شود تا پیامک به کارشناس برسد (`views/cart.vue:162-164`)، +و دکمه به «خروج از حساب مشتری» تبدیل می‌شود. + +**در هدف:** D21 **سازوکار** را رد کرد — ساختن توکن مشتری برای کارشناس یک ابزار ربودن نشست +است. `CustomerChoiceView` در v1 با لمس کارت فقط یک drawer جزئیات باز می‌کند +(`openDetail`). پس **خود این قابلیت** امروز در هدف وجود ندارد. + +**پیشنهاد:** همان قابلیت، با سازوکار امن: + +- توکن خود کارشناس + هدر `X-Acting-As: `. +- سرور در **هر درخواست** چک می‌کند `referredByAgentId` آن مشتری = همین کارشناس. +- موضوع همه‌ی route های مشتری = آن مشتری. +- سفارش با پرچم `from_parent` ثبت می‌شود. +- هر تعویض در لاگ ثبت می‌شود. +- **هیچ توکنی ساخته نمی‌شود** و سمت کلاینت فقط شناسه‌ی مشتری انتخاب‌شده نگه داشته می‌شود. + +**تأیید شما لازم است**، چون امنیت و جریان پول است (`CLAUDE.md` §3). این کار ریسک REL-03 را هم +بالاتر می‌برد — §پیش‌نیازها. + +### تنش ۳ — نقش کارگاه (type 5) در اپ + +**در مرجع:** سرپرست کارگاه یک ردیف مشتری با type 5 است. کاشی «سفارش ساخت» برایش مستقیم به +صفحه‌ی وضعیت می‌رود و دکمه‌های «تایید سفارش» و «ارسال سفارش» را می‌بیند +(`makeOrderStatus.vue:68-75`). + +**در هدف دو نامزد هست:** + +- `AdminUser` با نقش `WORKSHOP_SUPERVISOR` — `Workshop.supervisorId` به اینجا اشاره می‌کند و `PreOrder.workshopId` به `Workshop`. +- `Customer` با `kind = WORKSHOP_SUPERVISOR` — این مقدار در `CustomerKind` هست و برخلاف `SALES_AGENT` منسوخ اعلام نشده. + +D21 فقط به `SALES_EXPERT` اجازه‌ی ورود به اپ‌ها را داده. + +**پیشنهاد:** `AdminUser`، چون مسیر داده‌ای تمیز همین است. الگوی D21 به `WORKSHOP_SUPERVISOR` +گسترش پیدا کند، با دامنه‌ی محدود به `PreOrder` های کارگاه خودش. **تصمیم شما لازم است**، +از جمله اینکه مشتری با `kind = WORKSHOP_SUPERVISOR` در این صورت چه نقشی دارد. + +### تنش ۴ — خرابی‌های مرجع که به منطق کسب‌وکار می‌خورند + +D18 برای این‌ها توقف می‌خواهد (مورد ۴). شماره‌ها از `_inv-ref-pwa.md` §13 است. + +| # | خرابی در مرجع | اثر برای کاربر | پیشنهاد من | +|---|---|---|---| +| 4 | دکمه‌ی ثبت در صفحه‌ی `addTransaction` بیرون از `
` است و `html-type="submit"` ندارد (`addTransaction.vue:84-97`) | ثبت سند از **صفحه** کار نمی‌کند؛ فقط از drawer | درست شود — یک فرم مشترک، هر دو پوسته کار کنند | +| 5 | کاشی «سفارش مشتریان» تابع را صدا نمی‌زند (`Dashboard.vue:119`) | کارشناس از میز کار به آن صفحه نمی‌رسد | درست شود — D3 خود صفحه را خواسته | +| 6 | کادرهای ته‌حساب طلایی و ریالی اندیس‌ها و formatter ها را جابه‌جا خوانده‌اند (`Dashboard.vue:182-192`) | موجودی طلا و ریال در کادر اشتباه و با قالب اشتباه | هر کادر مقدار درست را با قالب متناسب با برچسبش نشان دهد | +| 8 | دکمه‌ی به‌روزرسانی قیمت، فهرستی را تازه می‌کند که نمایش داده نمی‌شود (`store.ts:107` در برابر `Dashboard.vue:632`) | به‌روزرسانی بی‌اثر | **قبلاً تصمیم گرفته شده (D4):** فهرست دیده‌شده تازه شود | +| 9 | «ارسال سفارش» همان `{sts: 2}` «تایید سفارش» را می‌فرستد (`makeOrderStatus.vue:212,234`) | ارسال در سرور ثبت نمی‌شود | تایید → «تایید کارگاه»، ارسال → «ساخته شده آماده ارسال». مقدار دقیق `PreOrderStatus` را شما تعیین کنید | +| 12 | radio «حواله به» **اندیس** آرایه را می‌فرستد نه متن مقصد (`addTransaction.vue:50`). سرور مرجع کل بدنه را بی‌تغییر ذخیره می‌کرد (`ApiMobileController::setFindoc`) | مقصد حواله به‌صورت عدد ذخیره می‌شد | سرور اندیس را به متن مقصد نگاشت کند؛ UI بی‌تغییر | +| 14 | در `editUser` سه آپلودر مدرک شناسه را دور می‌ریزند و دو تا یک `fileList` مشترک دارند (`editUser.vue:65,69`) | آپلود «موفق» دیده می‌شود ولی چیزی ذخیره نمی‌شود | **عیناً تکرار شود و ثبت شود** — وصل کردنش داده‌ی احراز را از صفحه‌ای عوض می‌کند که مرجع هرگز اجازه نداده بود | +| 15 | شرط infinite scroll گزارش `tableData.value.count` را می‌خواند که هرگز مقدار نمی‌گیرد (`report/index.vue:234`) | فقط صفحه‌ی اول گزارش دیده می‌شود | درست شود — endpoint های D24 صفحه‌بندی دارند | +| 19 | پسوند ` تومان ` روی فیلدی به نام `rial_remain` (`filters/index.ts:13`)؛ ستون هدف `fiatDebtRial` به ریال است | اگر واحدها نخوانند، بدهی ده برابر کم یا زیاد خوانده می‌شود | اول در `henza_server` واحد واقعی `rial_remain` معلوم شود؛ بعد adapter **همان عدد و برچسبی** را نشان دهد که مرجع برای همان بدهی نشان می‌داد | +| — | `singleProduct` دکمه‌ی back را می‌رباید: `history.pushState` + `window.onpopstate = () => history.go(1)`، سراسری و هرگز برداشته نمی‌شود (`singleProduct.vue:376-379`) | بعد از دیدن اولین محصول، back مرورگر یا گوشی **در کل اپ** از کار می‌افتد | محدود به خود صفحه‌ی محصول — نیت ظاهری کد همین است، نشتش به کل اپ خرابی است | + +### تنش ۵ — سرور هدف از مرجع سخت‌گیرتر است + +سرور مرجع تقریباً هر چیزی را قبول می‌کرد. سرور هدف بیشتر رد می‌کند: قاعده‌ی موجودی و +پیش‌سفارش (PROTECTED §C.1)، MON-05 وقتی اجرا شود، DTO های اعتبارسنجی، قیدهای یکتایی. +ولی UI مرجع در بیشتر خطاها **هیچ چیزی نشان نمی‌دهد** — فقط `console.log(e)`، مثلاً در +`Register.vue:340-342`، `cart.vue:189-191` و ثبت سند. پس درخواستی که مرجع بی‌صدا قبول می‌کرد، +در اپ کلاسیک بی‌صدا شکست می‌خورد و کاربر گیج می‌شود. + +| مورد | پیشنهاد من | +|---|---| +| **سقف موجودی:** سبد مرجع سقف ندارد؛ قاعده‌ی موجودی هدف (`capQty`/`maxQty` در v1) وجود دارد | سقف **بی‌صدا** در دکمه‌ی `+` اعمال شود — بدون هیچ عنصر یا متن تازه | +| **خطاهای بی‌صدا** | هر جا مرجع بی‌صدا بوده بی‌صدا بماند، **مگر** جایی که سرور هدف ردی می‌کند که مرجع هرگز نداشت. آنجا نزدیک‌ترین پیام **موجود** مرجع نشان داده شود (مثلاً «خطا در ثبت سفارش» از `makeOrder.vue:229`). هیچ متن تازه‌ای ساخته نمی‌شود | + +### تصمیم‌های کوچک‌تر + +| # | سؤال | پیشنهاد من | +|---|---|---| +| Q1 | نام، مسیر و پورت اپ | `apps/user-app-classic`، پورت **5183** (`USER_APP_CLASSIC_PORT`)، سرویس docker با همین نام. «classic» و نه «v3»، چون v3 «جدیدترین» را القا می‌کند در حالی که این قدیمی‌ترین طراحی است | +| Q2 | کد مشترک بین سه اپ | فعلاً داخل خود اپ در `src/core/` (api، adapter، قواعد دامنه)، با مرز تمیز تا بعداً قابل استخراج باشد. پکیج مشترک یعنی بازساختاردهی v1/v2 → PROTECTED → تصمیم جدا | +| Q3 | فیلتر رنگ در جست‌وجو — هدف اصلاً داده‌ی رنگ ندارد (`Product` و `ProductVariant` فقط `brand` دارند) | مدل افزایشی `Color` و ارتباطش با variant؛ ویرایشش در پنل ادمین کار جداگانه است. تا آن موقع فهرست خالی — همان چیزی که مرجع با داده‌ی خالی نشان می‌داد | +| Q4 | تابلوی قیمت میز کار: مرجع ۱۴ ردیف دارد (۸ طلا و سکه + ۶ ارز و انس، `store.ts:118-119`)؛ هدف فقط نرخ طلا به ازای عیار | ردیف‌هایی که هدف داده‌ی واقعی برایشان دارد نمایش داده شوند؛ بقیه حذف (قاعده‌ی صداقت، مثل REP-01) تا منبع قیمت تأیید شود. منبع خارجی = تصمیم شما | +| Q5 | ترتیب دسته‌های خانه: آرایه‌ی هاردکد `[37, 45, 31, 32, 42, 26, 36, 35, 34]` با شناسه‌ی عددی legacy (`home/index.vue:189-204`)؛ شناسه‌ی `Category` در هدف cuid است و نه legacyId دارد نه ستون ترتیب | ستون افزایشی ترتیب روی `Category` و پر کردنش از نگاشت مهاجرت. پر کردن = تغییر داده‌ی موجود → D18 توقف #5 → تأیید لازم | +| Q6 | آنالیتیکس مهمان: مرجع برای مهمان هم لاگ می‌فرستد؛ `CustomerActivityLog.customerId` اجباری است و ستون پایان ندارد | فقط کاربر واردشده لاگ شود؛ ستون افزایشی `endedAt` برای معادل `log/update` | +| Q7 | اعلان‌ها، FCM و پرکردن خودکار OTP — هر سه در مرجع سوار نیستند یا غیرفعال‌اند | ساخته نمی‌شوند — دقیقاً مثل مرجع | +| Q8 | اجرای مرجع برای مقایسه‌ی تصویری | کپی در `.parity/ref-pwa/` (gitignored) و رهگیری شبکه با Playwright؛ خود مرجع دست نمی‌خورد | +| Q9 | وضعیت D19 (حالت تحویل) | این پلن فرض می‌کند D19 تمام شده و D18 برقرار است | + +--- + +## پیش‌نیازهای باز از کارهای قبلی + +| مورد | چرا این اپ به آن نیاز دارد | Batch | +|---|---|---| +| **D9 قدم ۵** — جابه‌جایی `filterPercentPay` به اندازه‌ی آفست مشتری | اسلایدر اجرت در فیلتر جست‌وجو | B | +| **شکاف یکتایی شماره بین `Customer` و `AdminUser`** — هر چهار بررسی تداخل فقط `adminUser` را چک می‌کنند؛ در `ISSUES.md` ثبت نشده | ثبت‌نام و «افزودن کاربر» هر دو مشتری می‌سازند | A، F | +| **MON-08** — سرور اجرت و تخفیف را خودش بسازد | ثبت سفارش از سبد | E | +| **MON-05** — اجرای `canPlaceOrders` و `verificationStep` | مرجع فقط در کلاینت چک می‌کند (`sts_verify_step_two === 2`) | E | +| **REL-03** — مقدار جدای `JWT_CUSTOMER_SECRET` | کار به‌جای مشتری (تنش ۲) ریسک آن را بیشتر می‌کند | قبل از F | + +--- + +## نقشه‌ی endpoint ها — مرجع ↔ هدف + +✅ موجود (فقط adapter شکل) · 🟡 ناقص · ❌ وجود ندارد + +| # | مرجع | هدف امروز | وضعیت | Batch | +|---|---|---|---|---| +| 1 | `POST /web/auth` | `POST /customer/auth/request-otp` | ✅ | A | +| 2 | `POST /user-verfiy` | `POST /customer/auth/verify-otp` | ✅ — رد ورود برای `sts_verify_step_one == 3` بررسی شود | A | +| 3 | `POST /major/register` | — | ❌ | A | +| 4 | `POST /major/complete` | `POST /customer/me/complete-profile` | ✅ (D22) | A | +| 5 | `POST file/create` | `POST /customer/me/documents` | ✅ با `purpose` | A، D، G | +| 6 | `POST /me-data` | `GET /customer/me` | 🟡 هیچ‌کدام از `filterBrand`، `filterColor`، `filterGram`، `filterPercentPay`، `favorits`، `to`، `reagent`، `grams_sell`، `mcode` را ندارد | Phase 2 | +| 7 | `POST /front` | `GET /catalog/products` | 🟡 دسته‌ها، `more` و `ciny` آماده نیستند | B | +| 8 | `POST /get-products` (Query) | `GET /catalog/products` | 🟡 فیلتر و صفحه‌بندی سمت سرور ندارد (STUB-03) | B | +| 9 | `POST /get-products` (`products_id`) | `GET /catalog/products` | 🟡 | B، E | +| 10 | `GET /ciny/{cat}` | داخل catalog (`collections`) | 🟡 | B | +| 11 | `GET /product/{id}` | — فقط فهرست | 🟡 | B | +| 12 | `GET /config` | `GET /home/slider` | 🟡 `about` ندارد | B | +| 13 | `GET /promotion` | — | ❌ | B | +| 14 | `GET /ignore` | — | ❌ | B | +| 15 | `GET /price` | `GET /pricing/latest` | 🟡 فقط طلا به ازای عیار (Q4) | C | +| 16 | `GET /tahesab/me` | `GET /customer/me/report/balances` | ✅ (D24) | C | +| 17 | `POST /add-to-fav` | — | ❌ (v1/v2 محلی) | D | +| 18 | `POST /get-favorites` | — | ❌ | D | +| 19 | `POST /get-orders` | `GET /customer/me/orders` | ✅ فیلتر `sts` در کلاینت | D | +| 20 | `POST /update-profile` | — | ❌ | D | +| 21 | `POST /get-my-findoc` | `GET /customer/me/ledger-entries` | 🟡 شکل متفاوت | D | +| 22 | `POST /set-findoc` | — | ❌ | D | +| 23 | `GET /fin_tahesab/table` | `GET /customer/me/report` | ✅ (D24) | D | +| 24 | `GET /fin_tahesab/my_detail/{offset}` | `GET /customer/me/report/detail` | ✅ | D | +| 25 | `GET /fin_tahesab/detail/{code}` | `GET /customer/me/report/:id` | ✅ | D | +| 26 | `report/order?order_id` — فاکتور با `window.open` | — | ❌ | D | +| 27 | `report/doc?doc_id` — فایل سند | — | ❌ | D | +| 28 | `POST /set-order` | `POST /orders` | ✅ (MON-08) | E | +| 29 | `POST /my-child` | `POST /expert/my-customers` | ✅ | F | +| 30 | `POST /customer/toggle-active` | — | ❌ | F | +| 31 | `POST /customer/create` | — | ❌ | F | +| 32 | `POST /customer/update` | — | ❌ | F | +| 33 | `GET admin/major/list_type` | — | ❌ از `CustomerKind` ساخته می‌شود | F | +| 34 | `POST /order/child` | `POST /expert/child-orders` | ✅ | F | +| 35 | `PUT /order/child/{id}` | — | ❌ | F | +| 36 | `POST /create-custom-order` | — مدل `PreOrder` فقط در ادمین | ❌ | G | +| 37 | `GET /get-custom-orders` | — | ❌ | G | +| 38 | `POST /update-custom-order/{id}` | — | ❌ | G | +| 39 | `POST /log/create/{key}` + `GET /log/update/{id}` | — | ❌ | G | + +**ساخته نمی‌شوند (در مرجع مرده‌اند):** `POST /hesab_detail/table` (هرگز صدا زده نمی‌شود)، +تکرار `GET /ignore` در `getCustomerListType`، تکرار مرده‌ی `toggle-active` در Dashboard، +`POST /cities/list` (داده گرفته می‌شود ولی UI آبشاری کامنت است و فیلد شهر متن آزاد است — +`Register.vue:58-64`)، و سه endpoint اعلان (کامپوننتشان سوار نیست). + +--- + +## مشخصات حس و حرکت — انیمیشن‌ها و ناوبری + +این بخش همان چیزی است که یک پورت سرسری از دست می‌دهد. هر ردیف در Phase 0 با ضبط frame +اثبات می‌شود. + +### انیمیشن‌ها + +| # | انیمیشن | پارامتر دقیق | محرک | شاهد | +|---|---|---|---|---| +| 1 | فرورفتگی (notch) ناوبری پایین | `gsap.to('.from-path', {duration: .3, attr: {d: draw()}})` | تغییر تب | `components/ui/navigation.vue:50-86` | +| 2 | جابه‌جایی آیکون فعال | `useTransition` با `TransitionPresets.easeInOutCubic`، ‏150ms | تغییر تب | `navigation.vue:110-117` | +| 3 | **shutter هدر** | `@keyframes shutter` در 1s با `cubic-bezier(0.33, 1, 0.68, 1)`؛ `max-height` از 0 تا 120px و `opacity` از 0 تا 1 | **هر تغییر route**: `afterEach` پرچم را false و بعد از 100ms دوباره true می‌کند | `components/appHeader.vue:1-45`، `router/index.ts:273-286` | +| 4 | فاصله‌ی محتوا از هدر | `margin-top = headerHeight` وقتی `transitionEnded` | همراه ۳ | `components/content.vue:2` | +| 5 | **انتقال بین صفحه‌ها** | **ندارد.** `` در `App.vue:6,17` کامنت است. CSS `slideDown` (`App.vue:151-166`) و `setPgaTransition` زنده‌اند ولی به هیچ چیز وصل نیستند | — | **اضافه نمی‌شود** | +| 6 | باز شدن بخش فیلتر | `@keyframes drop` با `mode="out-in" appear` | باز و بسته کردن | `components/dropDown.vue:11,40` | +| 7 | تعویض تصویر گالری | `slideUp` با `mode="out-in"` | تغییر تصویر یا تنوع | `views/singleProduct.vue:28`، `components/slider/slider.vue:22` | +| 8 | اسپینر | `@keyframes rotate` و `rotateOpacity` (سه حلقه) | بارگذاری | `components/spinner.vue:104,115` | +| 9 | ریل سفارش ساخت | `@keyframes gradientMove` و `colorChange` روی رابط مرحله‌ی جاری | نمایش وضعیت | `makeOrderStatus.vue:283,295` | +| 10 | دیالوگ‌ها و notify | قواعد `animation:` | باز شدن | `DialogVant.vue`، `DialogVantOneButton.vue`، `NotifyVant.vue` | +| 11 | فهرست اسناد | `TransitionGroup name="list"` + کوییرک `testBind` (#13) | بارگذاری فهرست | `transactions.vue:17,177-205` | +| 12 | notify قدیمی | `@keyframes notify-in` | — | `customNotification.vue:47`، `Notification.vue:81` — **اول بررسی شود سوار هستند یا نه** | +| 13 | اسلایدر خانه | Swiper با `loop` و `autoplay` | خودکار | `slider/homeSlider.vue:7,10` | +| 14 | اسلایدر محصول | Swiper با ماژول `Zoom`، بدون loop | لمس | `slider/slider.vue:43-45` | +| 15 | overlay ها | انیمیشن خود کتابخانه برای ۲۰ Drawer، ۳ Modal و ۲ action-sheet | — | با قفل نسخه‌ها تضمین می‌شود | +| 16 | دیالوگ نسخه‌ی جدید | Lottie | تغییر `version` | `App.vue:22-38` | + +### زمان‌بندی‌ها + +| چه چیزی | مقدار | شاهد | +|---|---|---| +| دیالوگ تشویق به ورود در خانه | 5 ثانیه، **در هر بازدید** — گارد «فقط یک بار» کامنت شده | `home/index.vue:210-216` | +| شمارش معکوس OTP | 120 ثانیه، با صفر پیشرو زیر 10 | `Login.vue:122-131,187-189` | +| toast ها | 4000ms | `Dashboard.vue:269-284` و بقیه | +| debounce جست‌وجو | 700ms | `components/search/SearchInput.vue` | +| بازگرداندن پرچم انتقال هدر | 100ms | `router/index.ts:273-286` | + +### ناوبری + +- **۲۲ route با همان path، name و meta**، از جمله حروف و نام‌های عجیب: `/ChildOrder`، `/customerChoice`، `/addTransaction`، `/makeOrder_status`، `/complete_profile`، `userPage::hasSubRoute`. خانه `/home` است و `/` به آن redirect می‌شود. +- **`beforeEach`** همان شش قدم `_inv-ref-pwa.md` §5.1، با تصمیم‌های D3: + - مهمان روی پنج route محافظت‌شده → toast «ابتدا وارد شوید» + خانه. + - کاربر واردشده روی `/login` یا `/` → خانه. + - تله‌ی cold-boot (`isLoaded`) — D3 گفته تکرار شود. + - `completeProfile` بسته است وقتی `sts_verify_step_two ∈ {1, 2}`. +- route هایی که مرجع در guard ندارد (`addTransaction`، `makeOrderList`، `makeOrderStatus`، `addUser`، `editUser`، `ChildOrder`، `customerChoice`، و `report`/`reportDetail` که خودشان redirect می‌کنند) در اپ کلاسیک هم در guard نیستند؛ سرور هدف به‌هرحال 401 می‌دهد. +- **ناوبری پایین:** چهار تب به ترتیب index — کاربر (۱)، سبد (۲)، جست‌وجو (۳)، خانه (۴)؛ پیش‌فرض فعال ۴. فقط روی همین چهار route دیده می‌شود، و وقتی پنل فیلتر باز است پنهان می‌شود. +- **`keep-alive` + `suspense` با کلید `route.name`** — هر صفحه‌ی دیده‌شده mount می‌ماند (مثلاً نتایج جست‌وجو بعد از برگشت حفظ می‌شوند). این بخشی از حس ناوبری مرجع است و حفظ می‌شود. +- `scrollBehavior`: بازگرداندن `savedPosition`، وگرنه بالای صفحه. +- layout `FullPage` (بدون ناوبری پایین) برای همان ۸ route مرجع؛ بقیه `Master`. +- ربودن دکمه‌ی back در صفحه‌ی محصول — تنش ۴. + +--- + +## ساختار فازها + +### Phase 0 — تصمیم‌ها و زیرساخت اثبات (بدون کد اپ) + +1. پاسخ شما به تنش‌های ۱ تا ۵ و Q1 تا Q9. +2. **اجرای مرجع:** کپی `henza_pwa@b171fff` در `.parity/ref-pwa/` (gitignored)، `yarn install --frozen-lockfile`، اجرا روی پورت 3030. اگر toolchain قدیمی روی Node فعلی بالا نیاید → ریسک R1. +3. **یک مجموعه‌داده با دو خروجی:** JSON به شکل API مرجع (برای رهگیری درخواست‌های `https://henzagold.com/oapi/**` در Playwright) و seed دیتابیس برای API هدف. یک منبع، دو projection — تا هر دو اپ دقیقاً یک داده را نشان دهند. +4. **پرسوناها:** مهمان؛ مشتری تأییدنشده؛ مشتری تأییدشده (`sts_verify_step_two = 2`)؛ بنکدار (type 4)؛ کارشناس فروش (type 1)، و کارشناس در حال کار به‌جای مشتری؛ کارگاه (type 5). +5. **مقایسه‌ی تصویری:** Playwright روی یک ماشین و یک مرورگر برای هر دو اپ؛ viewport های 390×844 و 768×1024 (کانتینر مرجع `max-w-screen-md` است)؛ نواحی پویا مثل تاریخ و تایمر ماسک می‌شوند. آستانه‌ی قبول همین‌جا توافق می‌شود. +6. **بررسی انیمیشن:** ضبط frame در زمان‌های ثابت پس از محرک (مثلاً 0، 150، 300 و 1000ms) برای هر ردیف جدول انیمیشن. +7. **spike تقویم جلالی:** `generatePicker` عمومی + config جلالی کنار fork مرجع. اگر مقایسه‌ی تصویری صفر نشد، fork به‌صورت vendored نگه داشته می‌شود. +8. ساخت `docs/parity/03-PWA-CLASSIC-MATRIX.md` — فیلد به فیلد طبق D12. + +🛑 **Gate — توقف و تأیید شما.** + +### Phase 1 — پایه‌ی `apps/user-app-classic` + +- اسکلت Vite 5 + Vue 3.5 + TS strict + Pinia + vue-router، با نسخه‌های قفل‌شده‌ی بالا. +- **asset ها بایت‌به‌بایت با checksum:** فونت‌ها، `icons.json` و `Icon.vue`، تصاویر `assets/img` و empty، favicon، آیکون‌های manifest، فایل Lottie، `whatsapp.png`. +- `themeConfig/colors.json`، متغیرهای less، `tailwind.config.js` و `postcss-rtl` — همان مقادیر. +- `index.html` همان: عنوان «طلای هنزا»، `theme-color` ‏`#1C2D56`، قفل zoom، دکمه‌ی شناور واتس‌اپ (شماره از env، با مقدار پیش‌فرض مرجع). `bale.png` در `public/` هست ولی در `index.html` استفاده نمی‌شود — بررسی می‌شود. +- PWA: `injectManifest`، `sw.ts` فقط precache، manifest همان، **mode production**. +- زیرساخت: پورت 5183، `Dockerfile` و nginx به الگوی v1، سرویس compose، اضافه شدن به `build:release`، به‌روزرسانی `DELIVERY.md`. + +### Phase 2 — لایه‌ی core + +- **client HTTP:** base از env، توکن در هر درخواست؛ 401 سراسری = همان کار `myStore.init()` مرجع (پاک کردن `appToken`، `appCart` و کلید acting-as، رفتن به خانه). +- **adapter ها — قلب کار:** DTO هدف → شکل‌هایی که view های مرجع انتظار دارند (`MyData`، `Product`، `Variant`، `cartItem`، سفارش با `sts` عددی، ردیف سند، ردیف گزارش). view ها با همان نام فیلدهای مرجع bind می‌شوند. **قاعده‌ی صداقت:** چیزی که هدف ندارد `null` می‌شود و ردیفش پنهان — هرگز مقدار ساختگی. +- **نگاشت enum ها:** + - `CustomerKind` → `type` مرجع (۲، ۳، ۴، ۵؛ و ۱ برای کارشناس). + - `OrderStatus` هدف (۹ وضعیت) → `sts` مرجع (۸ تب). + - `PreOrderStatus` → رشته‌های فارسی `sts_str_fa`. + - `LedgerSourceType` → نوع سند ۱ تا ۴ و ۹. +- **بک‌اند پایه:** معادل کامل `POST /me-data` (ردیف ۶ نقشه) — همه‌ی صفحه‌ها به آن وابسته‌اند. +- **store های Pinia با همان state مرجع:** base، me (از همین حالا با جای `_activeUser` برای تنش ۲)، cart (`useStorage('appCart')` با همان کلید و شکل)، nav، query، token، transition، windowData، report. +- **composable ها:** `getProducts` با فرمول دقیق صفحه‌بندی `floor(count/limit) - 1`؛ موتور تخفیف (۴ نوع، نوع ۳ خاموش، بیشترین `discount_percent`)؛ `getWage`؛ `getHasDiscountWage`؛ علاقه‌مندی خوش‌بینانه. +- **formatter ها با همان خروجی:** ` تومان ` با فاصله‌ها، truncate (نه round) در `useTowDecimal`، حذف علامت در `useFormat`. **validator ها با همان پیام‌ها**، ولی بدون پرچم `g` (نقص #18). +- تست واحد برای همه‌ی این‌ها، با نمونه‌های مرجع. + +### Phase 3 — پوسته، ناوبری، انیمیشن + +- `App.vue` با `keep-alive`/`suspense`، دو layout، چک نسخه + دیالوگ Lottie، به‌روزرسانی بی‌صدای service worker. +- router با ۲۲ route و guard ها؛ hook آنالیتیکس (وصلش در Batch G). +- ناوبری پایین با GSAP، `appHeader` با shutter، `content` با margin. +- کامپوننت‌های پایه: `DialogVant`، `DialogVantOneButton`، `NotifyVant`، `spinner`، `empty`، `WorkActionSheet`، `container`، `ScrollFetch`، `SearchInput`، `otpInput`، `SingleImageUploader`، `InputDatePicker`. +- هر ردیف جدول انیمیشن با harness فاز ۰ اثبات می‌شود. + +### Batch A — ورود، ثبت‌نام، تکمیل پروفایل · حجم بک‌اند: کم + +- **صفحه‌ها:** `LoginRegister/index.vue`، `Login.vue`، `Register.vue`، `completeProfile.vue`، `input/otpInput.vue`، `input/SingleOtpInput.vue`. +- **بک‌اند:** + - معادل `POST /major/register` — مشتری تازه با `verificationStep = UNVERIFIED`، با بررسی یکتایی شماره در **هر دو** جدول. + - رد ورود برای `sts_verify_step_one == 3` با پیام «مجوز ورود ندارید» — نگاشتش به وضعیت هدف بررسی می‌شود. + - تکمیل پروفایل و آپلود مدارک: موجود (D22). +- **کوییرک‌هایی که حفظ می‌شوند:** OTP چهاررقمی (APP-01)؛ رنگ notify همیشه قرمز `#DC2626`، حتی برای موفقیت؛ برچسب دکمه «دریافت رمز عبور» / «دریافت مجدد رمز عبور»؛ دیالوگ موفقیت ثبت‌نام که با کلیک بیرون بسته نمی‌شود؛ فیلد شهر متن آزاد؛ تلفن فروشگاه بدون الگو. + +### Batch B — کاتالوگ · حجم بک‌اند: زیاد + +- **صفحه‌ها:** `home/index.vue`، `search/searchPage.vue`، `search/collection/index.vue` و `CollectionCard.vue`، `newest.vue`، `singleProduct.vue` (صفحه و حالت drawer)، `components/product/*`، `components/search/*`، `components/slider/*`، `viewAll.vue`. +- **بک‌اند:** + - معادل `POST /front`: دسته‌ها با `total`، `icon` و `image`؛ محصولات جدید؛ `more`؛ `ciny`. + - معادل `POST /get-products`: همه‌ی کلیدهای `Query` (`colors`، `brands`، `limit_percent`، `limit_gram`، `has_discount`، `text`، `cats`، `ciny_id`، `page`، `limit`) با پاسخ `{rows, count, limit}`، و حالت `products_id` برای rehydrate سبد. سمت سرور، چون صفحه‌بندی مرجع سمت سرور است. + - معادل `GET /product/{id}` و `GET /ciny/{cat}`. + - معادل `GET /config` (`about` + `main_slider`). + - معادل `GET /promotion` و `GET /ignore` برای کاربر واردشده. +- **کوییرک‌ها:** ترتیب ۹ دسته (Q5)؛ دیالوگ ورود در هر بازدید بعد از ۵ ثانیه؛ ارتفاع اسلایدر `min(width, 768) / 2.2125`؛ تنوع پیش‌فرض = کمترین `percentage_pay` با fallback «بدون تنوع»؛ تخفیف نوع ۳ خاموش؛ «افزودن به سبد» فقط با `sts_verify_step_two === 2`؛ نمونه‌ی تصادفی ۶ کالکشن؛ برچسب «N فیلتر» / «اعمال فیلتر»؛ بازه فقط وقتی «فعال» حساب می‌شود که هر دو سرش عوض شده باشد؛ بنکدار اسلایدر اجرت را نمی‌بیند. +- **وابسته به:** D9 قدم ۵، Q3، Q5، تصمیم دکمه‌ی back. + +### Batch C — میز کار (`/user`) · حجم بک‌اند: کم تا متوسط + +- **صفحه‌ها:** `views/user/index.vue`، `Dashboard.vue`، `about.vue`. +- **بک‌اند:** تابلوی قیمت (Q4)؛ `grams_sell` برای کارشناس. +- **کوییرک‌ها:** «بدهکار گرمی» / «بستانکار گرمی» با قدر مطلق؛ «فروش گرمی» فقط برای کارشناس؛ کاشی‌ها برای مهمان → دیالوگ ورود؛ دیالوگ «ّکاربر فعال شود؟» با همان تشدید سرگردان؛ دیالوگ خروج؛ متن‌های toast قیمت؛ سرتیتر تاریخ و ساعت قیمت که همیشه خالی است (#7 — کوییرک بصری). +- **تصمیم‌ها:** #5، #6، #8، #19؛ تنش ۲ برای دکمه‌ی «انتخاب مشتری» / «خروج از حساب مشتری». + +### Batch D — سفارش‌ها، علاقه‌مندی‌ها، پروفایل، اسناد، گزارش · حجم بک‌اند: زیاد + +- **صفحه‌ها:** `Orders.vue`، `product/order.vue`، `product/orderItem.vue`، `favorits.vue`، `profile.vue`، `editUser.vue`، `transactions.vue`، `addTransaction.vue`، `addFindoc.vue`، `report/index.vue`، `report/detail.vue`، `Accounting.vue`، `AccountingDetail.vue`. +- **بک‌اند:** + - علاقه‌مندی: جدول افزایشی، toggle و list. + - ویرایش پروفایل: **فقط فیلدهای فرم** (whitelist). مرجع کل بدنه را روی ردیف مشتری می‌نوشت (`ApiMobileController::updateProfile` — همان خانواده‌ی SEC-14). ستون‌های افزایشی `birthDate` و `marriageDate` (PERS-03). + - اسناد: فهرست به شکل مرجع از `LedgerEntry`. ثبت سند به‌صورت ورودی **تأییدنشده**، با نرمال‌سازی آب‌شده `value × ayar / 750` (همان `REPAYMENT_MELTED_GOLD`). پیامک به مدیران از env، به‌جای دو شماره‌ی هاردکد مرجع (الگوی SEC-15). مرجع اینجا هم کل بدنه را ذخیره می‌کرد. + - فاکتور سفارش و فایل سند (معادل `report/order` و `report/doc`). + - گزارش: موجود (D24). +- **کوییرک‌ها:** ۸ تب وضعیت با همان رنگ‌ها (۰ و ۲ بدون رنگ)؛ ریل چهارمرحله‌ای با `current = 1` پیش‌فرض؛ جست‌وجوی دقیق شماره در کلاینت؛ مودال تمام‌صفحه؛ باز شدن فاکتور در پنجره‌ای به نام `_bank`؛ **دو دکمه‌ی شناور هم‌پوشان** در اسناد، با همان ترتیب لایه‌ها؛ رنگ و آیکون ردیف برای `type == 9`؛ چهار تب فرم سند و متن ثابت تب «پولی»؛ دکمه‌ی «افزودن کاربر» که هنگام کار به‌جای مشتری پنهان است (#11). +- **تصمیم‌ها:** #4، #12، #14، #15. + +### Batch E — سبد و ثبت سفارش · حجم بک‌اند: متوسط + +- **صفحه‌ها:** `cart.vue`، `product/cartItem.vue`. +- **بک‌اند:** MON-08؛ MON-05؛ پرچم `from_parent` (تنش ۲). +- **کوییرک‌ها:** کلید `appCart` و شکلش؛ حذف بی‌صدای ردیف‌هایی که محصولشان دیگر نیست؛ وقتی تعداد ۱ است، منفی به سطل زباله تبدیل می‌شود؛ drawer یادداشت 280px با textarea پنج‌خطی؛ جمع **فقط وزن** با `.toFixed(2)`؛ دیالوگ تأیید؛ صفحه‌ی موفقیت با شماره‌ی سفارش؛ بنکدار اجرت را نمی‌بیند. +- **تصمیم‌ها:** تنش ۵. + +### Batch F — کارشناس فروش · حجم بک‌اند: زیاد + +- **صفحه‌ها:** `customerChoice.vue`، `addUser.vue`، `ChildOrders.vue`، و بخش‌های کارشناس در Dashboard و profile. +- **بک‌اند:** + - کار به‌جای مشتری (تنش ۲). + - ساخت، ویرایش و فعال/غیرفعال کردن مشتری توسط کارشناس، با دامنه‌ی `referredByAgentId` و یکتایی شماره در هر دو جدول. + - فهرست نوع مشتری (معادل `admin/major/list_type`) از `CustomerKind`. + - ویرایش سفارش مشتری و ثبت نهایی (معادل `PUT /order/child/{id}`). +- **کوییرک‌ها:** پیش‌پر کردن از `sessionStorage.editCustomer`؛ برگشت به `customerChoice` یا `profile` بر اساس `query.from`؛ ۲۲ منطقه‌ی تهران؛ بنکدار → `sts = -1`؛ toast شماره‌ی تکراری با قرمز `#b10f0f`؛ جست‌وجوی نام در کلاینت؛ ویرایش وزن خام (بدون اجرت)؛ دو متن تأیید. + +### Batch G — سفارش ساخت + آنالیتیکس · حجم بک‌اند: متوسط + +- **صفحه‌ها:** `makeOrder.vue`، `makeOrderStatus.vue`، کاشی سفارش ساخت در Dashboard، `router/customAnalytics.ts`. +- **بک‌اند:** + - ثبت و فهرست سفارش ساخت مشتری روی مدل موجود `PreOrder` (`colorName`، `sizeSpec`، `minaColor`، `stoneColor`، `description`، تصویر) + ستون افزایشی تعداد — فیلد `sum` مرجع در `PreOrder` نیست. + - تأیید و ارسال توسط کارگاه (تنش ۳، #9). + - آنالیتیکس روی `CustomerActivityLog` + ستون افزایشی `endedAt` (Q6). +- **کوییرک‌ها:** نگاشت ۵ مرحله از رشته‌ی فارسی `sts_str_fa` — سرور باید **همان رشته‌ها** را بدهد؛ `confirmedOrders` فقط در حافظه است و با reload از دست می‌رود؛ کاشی برای type 5 مستقیم و برای بقیه مودال دوگزینه‌ای؛ اسپینری که به‌خاطر سایه‌انداختن متغیر `loading` هرگز دیده نمی‌شود. + +### Phase نهایی — ممیزی و تحویل + +- همه‌ی ردیف‌های ماتریس: ✅، یا ➖ با شماره‌ی ISSUE، یا ⏸️. +- گزارش مقایسه‌ی تصویری همه‌ی صفحه‌ها × پرسوناها × viewport ها. +- سرشماری‌ها (§معیار تمام شد). +- typecheck، lint، build، `docker compose`، `build:release`. +- به‌روزرسانی `DELIVERY.md` و `PROGRESS.md`. + +--- + +## روش کار هر Batch + +1. خواندن **خط‌به‌خط** فایل‌های مرجع آن batch — نه فقط inventory. +2. ردیف‌های ماتریس، فیلد به فیلد. +3. شکاف‌های بک‌اند: endpoint افزایشی + تست (رفت‌وبرگشت، توکن متقابل، IDOR). +4. پورت view ها: template و style تا جای ممکن عین مرجع؛ script بازنویسی‌شده. +5. مقایسه‌ی تصویری و انیمیشن در برابر مرجع. +6. typecheck، lint، build. +7. گزارش کوتاه: چه پورت شد، چه عمداً نشد (با شماره)، چه باگ تازه‌ای در مرجع پیدا شد. + +تحت D18 بین batch ها توقف نمی‌شود؛ فقط برای موارد توقف D18 و gate فاز ۰. + +--- + +## معیار «تمام شد» — اثبات خودکار اینکه چیزی از دست نرفت + +| سرشماری | چطور | قبول | +|---|---|---| +| route | استخراج path، name و meta از هر دو router | ۲۲ از ۲۲، یکسان | +| رشته‌ها | استخراج هر literal فارسی از فایل‌های زنده‌ی مرجع (template و script) | همه در اپ کلاسیک موجود؛ هر غایب یا در §چیزی که پورت نمی‌شود هست یا خطاست | +| asset | checksum | همه یکسان | +| endpoint | هر ردیف نقشه | ✅ یا تصمیم ثبت‌شده | +| تصویر | مقایسه‌ی هر صفحه × پرسونا × viewport | زیر آستانه‌ی توافق‌شده در فاز ۰ | +| انیمیشن | frame های زمان ثابت | منطبق | +| ماتریس | — | بدون ردیف باز | +| کیفیت | typecheck strict، lint، build، compose | سبز | + +--- + +## چیزی که پورت نمی‌شود (و چرا) + +هر مورد «کد مرده» قبل از کنار گذاشتن با جست‌وجوی importer تأیید می‌شود. + +| مورد | چرا | شاهد | +|---|---|---| +| `src/views/login.vue` | کل فایل کامنت است | `login.vue:1,201` | +| `user/src/components/report.vue` | صفحه‌ی دیباگ، سوار نشده | `_inv-ref-pwa.md` §6.1 | +| `NotificationList.vue` و سه endpoint اعلان | در مرجع سوار نیست | `home/index.vue:5` | +| FCM (`Mixins/fireBase.js`) و `firebase-messaging-sw.js` | در مرجع غیرفعال؛ کلید API هاردکد | `App.vue:105` | +| پرکردن خودکار OTP (Web OTP API) | غیرفعال | `App.vue:100` | +| `POST /cities/list` | UI آبشاری کامنت است | `Register.vue:58-64` | +| `cax`، `noneAuthUrl`، `canNavigate`، `getHomeRouteForLoggedInUser`، `getCustomerListType`، `loadingStore`، `filters sample.js`، `Date.vue`، `UploadPic.vue`، `updateProfile` بی‌مصرف در `makeOrder.vue`، `logOut` دوم در `user/index.vue` | کد مرده | `_inv-ref-pwa.md` §2.2، §5.1، §8.6، §9 | +| دو fetch تکراری در `singleProduct` | یک fetch؛ نتیجه‌ی دیده‌شده یکسان | #20 | +| `wage_weight` و `weight` ثابت در payload سفارش | سرور نادیده می‌گیرد | DEAD-06 | +| حلقه‌ی `setInterval(reload)` | یک reload | #10 | +| کلید analytics به شکل `undefined` | کلید از نام route | #16 | +| PWA در dev mode | production | #17 | + +--- + +## ریسک‌ها + +| # | ریسک | اثر | کاهش | +|---|---|---|---| +| R1 | مرجع روی Node فعلی بالا نیاید (Vite 3، plugin-vue 2، وابستگی `webpack 5.0.0`) | بدون مقایسه‌ی تصویری، «دقیقاً» فقط با خواندن کد ادعا می‌شود | Node قدیمی‌تر فقط در همان کپی؛ اگر نشد، اعلام صریح سطح اطمینان پایین‌تر | +| R2 | تفاوت داده بین دو API | adapter وسوسه می‌شود چیزی جعل کند | قاعده‌ی صداقت؛ تست adapter با fixture | +| R3 | تقویم جلالی | ظاهر متفاوت | spike فاز ۰؛ fallback به fork | +| R4 | حجم بک‌اند | بیشتر کار در `apps/api` است، نه در UI | batch بندی بر اساس وابستگی | +| R5 | رندر فونت بین ماشین‌ها | تفاوت کاذب در مقایسه‌ی تصویری | هر دو اپ روی یک ماشین و یک مرورگر | +| R6 | `keep-alive` | state کهنه بعد از برگشت — که رفتار مرجع است | حفظ، با تست سناریوی برگشت | +| R7 | v1/v2 عقب می‌افتند (`CLAUDE.md` §2) | ناسازگاری منطقی بین اپ‌ها | تنش ۱ | +| R8 | شناسه‌های عددی دسته‌ها | ترتیب اشتباه دسته‌ها در خانه | Q5 | + +--- + +# وضعیت اجرا + +هنوز شروع نشده. **قدم بعدی:** پاسخ شما به تنش‌های ۱ تا ۵ و Q1 تا Q9؛ بعد Phase 0. diff --git a/docs/parity/PROGRESS.md b/docs/parity/PROGRESS.md index 4404fd9..5ee8f23 100644 --- a/docs/parity/PROGRESS.md +++ b/docs/parity/PROGRESS.md @@ -2,6 +2,16 @@ > Read this file FIRST at the start of every session and after every `/compact`. > Rules live in `CLAUDE.md` (workspace root). Method lives in the `reference-parity-audit` skill. +> Neither is in this repository — see `docs/GO-LIVE-PLAN.md` §0. + +## New workstreams (2026-10-04) — plans only, nothing executed + +| Plan | What | Waiting on | +|---|---|---| +| `docs/GO-LIVE-PLAN.md` | Stack upgrade, release isolation, data migration and cutover from the live henza system. **§0 explains how to continue on another machine.** | Owner go-ahead per step | +| `docs/PWA-PORT-PLAN.md` | Third user app `apps/user-app-classic`: a pixel-exact port of `henza_pwa@b171fff` onto the new API. | Owner rulings on its tensions 1–5 and Q1–Q9 | + +The "Current state" table below is the parity project's and was last updated 2026-09-22. ## Current state diff --git a/henza_admin b/henza_admin new file mode 160000 index 0000000..1b67448 --- /dev/null +++ b/henza_admin @@ -0,0 +1 @@ +Subproject commit 1b67448143b699be90d779cad9a02b78451043a7 diff --git a/henza_pwa b/henza_pwa new file mode 160000 index 0000000..b171fff --- /dev/null +++ b/henza_pwa @@ -0,0 +1 @@ +Subproject commit b171ffffb58b67b3f192ccd22dd8cde56d040eae diff --git a/henza_server b/henza_server new file mode 160000 index 0000000..37c7d7a --- /dev/null +++ b/henza_server @@ -0,0 +1 @@ +Subproject commit 37c7d7a92f722da45cc2e4e1d9a0f0ee9c756393 diff --git a/scripts/check-majors.mjs b/scripts/check-majors.mjs new file mode 100644 index 0000000..f334a55 --- /dev/null +++ b/scripts/check-majors.mjs @@ -0,0 +1,69 @@ +#!/usr/bin/env node +// For each npm package, prints its four newest major lines: first release, +// latest version and when that latest version was published. A line with no +// release for a long time has probably stopped receiving patches. +// +// Backs the version table in docs/GO-LIVE-PLAN.md §4. Re-run it before acting +// on that table — the numbers there are only true for the day they were taken. +// +// node scripts/check-majors.mjs # the packages the plan tracks +// node scripts/check-majors.mjs vite pinia # any packages +// +// Goes through the npm CLI rather than fetch() so the machine's registry or +// mirror settings (.npmrc) apply. +import { execSync } from 'node:child_process'; + +const TRACKED = [ + 'vue', 'vite', '@vitejs/plugin-vue', 'vite-plugin-pwa', 'vue-router', 'pinia', + 'typescript', 'vue-tsc', 'tailwindcss', 'ant-design-vue', 'vant', '@vueuse/core', + '@nestjs/core', 'prisma', +]; +// Package names only — they are interpolated into a shell command. +const PACKAGE_NAME = /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i; +const STALE_AFTER_DAYS = 180; + +const pkgs = process.argv.length > 2 ? process.argv.slice(2) : TRACKED; +const now = Date.now(); + +for (const pkg of pkgs) { + if (!PACKAGE_NAME.test(pkg)) { + console.log(`\n${pkg}: not a package name, skipped`); + continue; + } + + let published; + try { + published = JSON.parse( + execSync(`npm view ${pkg} time --json`, { + encoding: 'utf8', + timeout: 60_000, + stdio: ['ignore', 'pipe', 'ignore'], + }), + ); + } catch { + console.log(`\n${pkg}: npm view failed`); + continue; + } + + const lines = new Map(); + for (const [version, date] of Object.entries(published)) { + // Stable releases only; this also drops the `created` / `modified` keys. + if (!/^\d+\.\d+\.\d+$/.test(version)) continue; + const major = Number(version.split('.')[0]); + const line = lines.get(major) ?? { firstVersion: version, first: date, lastVersion: version, last: date }; + if (date < line.first) Object.assign(line, { firstVersion: version, first: date }); + if (date > line.last) Object.assign(line, { lastVersion: version, last: date }); + lines.set(major, line); + } + + console.log(`\n${pkg}`); + const newestFirst = [...lines].sort(([a], [b]) => b - a).slice(0, 4); + for (const [major, line] of newestFirst) { + const idleDays = Math.floor((now - Date.parse(line.last)) / 86_400_000); + const stale = idleDays > STALE_AFTER_DAYS ? ` <- no release for ${idleDays} days` : ''; + console.log( + ` v${String(major).padEnd(3)} first ${line.firstVersion.padEnd(9)} ${line.first.slice(0, 10)}` + + ` latest ${line.lastVersion.padEnd(9)} ${line.last.slice(0, 10)}${stale}`, + ); + } +}