diff --git a/docs/ISSUES-TRIAGE-PLAN.md b/docs/ISSUES-TRIAGE-PLAN.md new file mode 100644 index 0000000..9c711e5 --- /dev/null +++ b/docs/ISSUES-TRIAGE-PLAN.md @@ -0,0 +1,149 @@ +# برنامه‌ی رسیدگی به فهرست مشکلات ثبت‌شده + +**تاریخ:** ۱۷ مهر ۱۴۰۵ (۲۰۲۶-۱۰-۰۸) +**منبع:** [`docs/parity/ISSUES.md`](./parity/ISSUES.md) + +**روش کار:** هر مورد بازِ آن فهرست با کد امروز دوباره سنجیده شد. بعضی در این +فاصله درست شده‌اند ولی فهرست به‌روز نشده بود. + +> تا تأیید کارفرما هیچ کدی نوشته نمی‌شود. موارد «حساس» (دفتر بدهی، حذف داده) +> جداگانه هم تأیید می‌خواهند. + +--- + +## الف) درست شده‌اند، فقط باید در فهرست بسته شوند + +| شناسه | چه بود | وضعیت امروز | +|---|---|---| +| MON-01 / V2-01 | اپ نسخه‌ی ۲ سقف موجودی نداشت؛ می‌شد بیشتر از موجودی سفارش داد | سقف در هر دو اپ هست | +| V2-02 / APP-02 | اسلایدر تنظیم‌شده در پنل در اپ نسخه‌ی ۲ نمی‌آمد | اپ نسخه‌ی ۲ آن را می‌خواند | +| MON-03 | عدد مثقال (۴٫۳۳۱۸۰۲) اعشاری معمولی بود و نگرانیِ گرد شدن داشت | سنجیده شد: هنگام محاسبه دقیقاً به همان رقم تبدیل می‌شود و **هیچ خطایی نمی‌سازد** | +| PERS-03 | ستونی برای تاریخ تولد مشتری نبود | ستونش با انتقال داده اضافه شد؛ **فقط فیلد فرم مانده** (پایین، بند ج) | + +**پیشنهاد:** همین‌ها را در فهرست ببندم. کد عوض نمی‌شود. + +--- + +## ب) قبل از راه‌اندازی واقعی — حتماً + +### ۱. علاقه‌مندی‌ها در اپ ذخیره نمی‌شوند (تازه پیدا شد — مرتبط با انتقال) + +**مشکل:** +- اپ علاقه‌مندی‌ها را فقط در حافظه‌ی گوشی نگه می‌دارد، پس با بستن اپ پاک + می‌شوند. +- **۸۷۴ علاقه‌مندیِ منتقل‌شده از سیستم قدیم در دیتابیس هست، ولی اپ هیچ‌وقت آن‌ها + را نشان نمی‌دهد.** +- سیستم قدیم علاقه‌مندی را روی سرور ذخیره می‌کرد. + +**پیشنهاد:** سه مسیر ساده در سرور اضافه شود (فهرست، افزودن، حذف) و هر دو اپ به +آن وصل شوند. + +### ۲. تنظیمات امنیتی سرور (REL-01، REL-03، SEC-04، SEC-05) + +**این‌ها کد نیستند؛ تنظیم سرور هستند:** +- **کد ثابت ورود آزمایشی (REL-01):** حذف شود. تا وقتی هست، هر کسی با آن کد + وارد هر حسابی می‌شود. +- **کلید رمز توکن مشتری‌ها (REL-03):** از کلید مدیرها جدا شود. +- **کلید پیش‌فرض توکن (SEC-04):** اگر کلید تنظیم نشده باشد، سرور با یک کلید + پیش‌فرضِ معلوم بالا می‌آید. + +**پیشنهاد برای SEC-04:** در محیط اصلی، بدون کلید اصلاً بالا نیاید. این تغییر +کوچک است. + +**دو مورد دیگر:** +- **دسترسی از هر دامنه (CORS):** هر سایتی می‌تواند به سرور درخواست بفرستد. + باید فقط به دامنه‌های پنل و اپ محدود شود. +- **حساب آزمایشی (SEC-05):** دیتای اولیه یک حساب مدیر ارشد آزمایشی می‌سازد که + رمزش همان شماره است. باید فقط در محیط توسعه ساخته شود. + +### ۳. رمزهای لو رفته‌ی سیستم قدیم (SEC-06، SEC-12، SEC-17) — کارِ خود کارفرما + +رمز پنل پیامک، توکن درگاه پرداخت و توکن ته‌حساب در کد سیستم قدیم نوشته شده‌اند. +باید **عوض شوند**. این کار از دست من برنمی‌آید. + +### ۴. کالاهای بدون عکس (ACC-05) + +کالای بدون عکس در اپ نمایش داده نمی‌شود و قابل سفارش نیست. + +**پیشنهاد:** بعد از انتقال داده یک شمارش بگیرم که چند کالا عکس ندارند. + +--- + +## ج) درستی پول و سفارش — مهم + +### ۵. بدهی دوبار ثبت می‌شود (ACC-03 = MON-04) — 🔴 مهم‌ترین مورد پولی + +**مشکل:** اگر سفارشی دوبار به وضعیت «تحویل به کارشناس» برود (مثلاً با برگرداندن +وضعیت و جلو بردن دوباره)، **بدهی طلا و ریالِ مشتری دوبار ثبت می‌شود.** + +**پیشنهاد:** پیش از ثبت بدهی، بررسی شود که برای همین سفارش قبلاً بدهی ثبت شده +یا نه. + +**وضعیت:** دفتر بدهی ناحیه‌ی حفاظت‌شده است، پس **تأیید جداگانه‌ی شما لازم است.** + +### ۶. مشتری اجرت سفارش را خودش تعیین می‌کند (MON-08) + +**مشکل:** +- درصد اجرت، تخفیف و هزینه‌ی ریالی را **اپ** به سرور می‌فرستد و سرور همان را + قبول می‌کند. +- کسی که درخواست را دستکاری کند، می‌تواند با اجرت صفر سفارش بدهد. +- سیستم قدیم هم همین‌طور بود (حتی بدتر). + +**پیشنهاد:** سرور این اعداد را خودش از کالا و مشتری حساب کند و عدد اپ را +نادیده بگیرد. **این رفتار را عوض می‌کند، پس تأیید شما لازم است.** + +### ۷. «معلق کردن سفارش» و «احراز هویت» جلوی سفارش را نمی‌گیرند (MON-05) + +**مشکل:** مدیر می‌تواند ثبت سفارش یک مشتری را معلق کند، ولی سرور موقع سفارش +آن را بررسی نمی‌کند. + +**وضعیت سیستم قدیم:** آن هم در سرور بررسی نمی‌کرد. اپ قدیم را باید ببینم که +آن‌جا جلوی کار را می‌گرفت یا نه. + +**پیشنهاد:** مشتریِ «عدم سفارش» نتواند سفارش ثبت کند. تأیید شما لازم است. + +--- + +## د) متوسط — بعد از موارد بالا + +| شناسه | مشکل به زبان ساده | پیشنهاد | +|---|---|---| +| PERS-03 | فیلد تاریخ تولد در فرم مشتری نیست (۵۴۱ تاریخ تولد واقعی منتقل شده) | فیلد شمسی به فرم اضافه شود | +| SEC-07 | ذخیره‌ی دسترسی‌ها هر متنی را قبول می‌کند، حتی دسترسی بی‌معنی | فقط دسترسی‌های معتبر پذیرفته شود | +| VAL-02 | بیشتر فرم‌های پنل در سرور بررسی ورودی ندارند | به‌تدریج برای فرم‌های ثبت و ویرایش اضافه شود | +| MON-06 | تنظیمات کیف پول، اسلایدر و تخفیف خودکار در نصب تازه مقدار اولیه ندارند | مقدار پیش‌فرض ساخته شود | +| PERM-01 | دو دسترسی قدیمیِ داشبورد در دیتابیس هست که در صفحه‌ی دسترسی‌ها دیده نمی‌شود | حذفشان **حذف داده است و تأیید می‌خواهد** | +| PERS-02 | شهرها در قدیم استان ← شهر بودند؛ در جدید فهرست ساده‌اند | تصمیم با شما؛ فعلاً کار می‌کند | +| PERS-01 | در قدیم موبایل مشتری بعد از ثبت عوض نمی‌شد؛ در جدید می‌شود | قبلاً تصمیم گرفته شد که بماند؛ فقط یادآوری | +| REL-02 | اگر پیامک خراب باشد سرور فقط در گزارش داخلی می‌نویسد | یک صفحه‌ی «وضعیت سلامت» که خرابی پیامک را نشان دهد | +| STUB-05 | شماره‌ی مدیرهایی که پیامک سفارش می‌گیرند | از تنظیمات خوانده شود (بخشی‌اش انجام شده؛ بررسی می‌کنم) | +| V2-03 | اپ نسخه‌ی ۲ عکس هر مدل (واریانت) را نشان نمی‌دهد | — | +| V2-05 | اپ نسخه‌ی ۲ برچسب موجودی روی مدل‌ها ندارد | هر دو باید با اپ نسخه‌ی ۱ یکسان شوند (فقط ظاهر باید فرق کند) | +| VAL-03 | اپ وقتی خطا می‌گیرد پیام درستی به کاربر نشان نمی‌دهد | — | +| ACC-01 | اتصال زرین روی نصب واقعی امتحان نشده | وقتی نصب واقعی داشتید | +| ACC-04 | روی دیتابیس کاملاً خالی، ترتیب تغییرات دیتابیس به هم می‌خورد | نام یک فایل عوض شود، بدون اثر روی دیتابیس‌های موجود | + +--- + +## ه) کم‌اهمیت / تمیزکاری + +| شناسه | مشکل | پیشنهاد | +|---|---|---| +| DEAD-01..03 | کد و پوشه‌ی بلااستفاده در اپ‌ها و ریشه‌ی پروژه | حذف، با تأیید | +| PERF-01 | فهرست کارشناسان برای هر ردیف یک پرس‌وجو جدا می‌زند | الان با ۵۱ کارشناس مشکلی ندارد | +| REP-02 | گزارش مشتری برای هر بار نمایش کل تاریخچه را حساب می‌کند | الان مشکلی ندارد | +| DEPLOY-03 | قبل از ساخت نسخه‌ی سرور باید خروجی تازه گرفته شود؛ وگرنه نسخه‌ی کهنه ساخته می‌شود | در چک‌لیست انتشار هست؛ بعداً خودکار شود | + +--- + +## ترتیب پیشنهادی کار + +| ترتیب | کار | نیاز به تأیید جداگانه | +|---|---|---| +| ۱ | بستن موارد درست‌شده در فهرست (بند الف) | — | +| ۲ | **علاقه‌مندی‌ها روی سرور** (بند ۱) | — | +| ۳ | **بدهی دوباره** (بند ۵) | ✅ ناحیه‌ی حفاظت‌شده | +| ۴ | امنیت سرور (بند ۲) | — | +| ۵ | اجرت از سرور (بند ۶) و جلوگیری از سفارشِ معلق (بند ۷) | ✅ تغییر رفتار | +| ۶ | فیلد تاریخ تولد (PERS-03) و بررسی دسترسی‌ها (SEC-07) | — | +| ۷ | بقیه‌ی بند «د» و «ه» به ترتیب | حذف‌ها ✅ | diff --git a/docs/parity/ISSUES.md b/docs/parity/ISSUES.md index 3ac5a72..ab90727 100644 --- a/docs/parity/ISSUES.md +++ b/docs/parity/ISSUES.md @@ -43,9 +43,9 @@ Severity: `Critical` (money loss / security / data loss) · `High` · `Medium` | ID | Severity | Where | Description | Evidence | Suggested fix | |---|---|---|---|---|---| -| MON-01 | Critical | TGT `apps/user-app-v2/src/stores/cartStore.ts` | v2 has **no `capQty` / `maxQty` stock ceiling**, which `user-app` does have (`apps/user-app/src/stores/cartStore.ts:62,65`; `components/product/CartItem.vue:82-83`). A v2 user can order more units than exist. | Verified by grep across both apps — zero hits in v2. | Port the ceiling to v2 (this is also a **Phase 2 parity gap**, row to be added to `02-APPS-MATRIX.md`). | +| MON-01 | ~~Critical~~ **FIXED (verified 2026-10-08)** | TGT `apps/user-app-v2/src/stores/cartStore.ts` | v2 has **no `capQty` / `maxQty` stock ceiling**, which `user-app` does have (`apps/user-app/src/stores/cartStore.ts:62,65`; `components/product/CartItem.vue:82-83`). A v2 user can order more units than exist. | Verified by grep across both apps — zero hits in v2. | Port the ceiling to v2 (this is also a **Phase 2 parity gap**, row to be added to `02-APPS-MATRIX.md`). | | MON-02 | High | REF `henza_server` wallet package | The wallet `balance` column **mixes grams and toman in one field**, and money operations are not wrapped in DB transactions. | `_inv-ref-server.md` §7. | Separate the units; wrap mutations in transactions. | -| MON-03 | High | TGT `apps/api/src/modules/accounting/ledger.service.ts:189` | `GRAMS_PER_MESGHAL = 4.331802` is a JS **float** used inside an otherwise-`Prisma.Decimal` CASH repayment formula, so the divisor is pre-rounded before the Decimal division. | Read at `ledger.service.ts:182-190`; the source comment already flags the formula for stakeholder review. | Make the constant a `Decimal` (or a string literal passed to `Decimal`). | +| MON-03 | ~~High~~ **NO EFFECT (verified 2026-10-08)** — `Prisma.Decimal(4.331802)` converts through the number's shortest decimal string, i.e. exactly `4.331802`; nothing is pre-rounded | TGT `apps/api/src/modules/accounting/ledger.service.ts:189` | `GRAMS_PER_MESGHAL = 4.331802` is a JS **float** used inside an otherwise-`Prisma.Decimal` CASH repayment formula, so the divisor is pre-rounded before the Decimal division. | Read at `ledger.service.ts:182-190`; the source comment already flags the formula for stakeholder review. | Make the constant a `Decimal` (or a string literal passed to `Decimal`). | | MON-04 | High | TGT `apps/api/src/modules/orders/orders.service.ts` | Debt is recognised at `DELIVERED_TO_AGENT`, but the idempotency guard checks `deliveredAt`, which is only set by `DELIVERED_TO_CUSTOMER` — so the debt trigger is not reliably idempotent. There is also **no status-transition validation** anywhere. | api inventory §8. | Guard on the same field the trigger fires on; add an explicit transition table. | | MON-05 | Medium | TGT `apps/api` | `canPlaceOrders` and `verificationStep` are stored and toggled by the admin panel but **never enforced** on any order path. | api inventory §8. | Enforce at order creation, or remove the toggles. | | MON-06 | Medium | TGT `apps/api` | `Setting` rows are never seeded — `wallet.config`, `wallet.birthday`, `promotion.auto`, `mobile_main_slider` only materialise on first write, so a fresh install has undefined behaviour until an admin saves each one. | api inventory §7. | Seed the defaults. | @@ -66,8 +66,8 @@ Severity: `Critical` (money loss / security / data loss) · `High` · `Medium` | ID | Severity | Where | Description | Evidence | |---|---|---|---|---| -| V2-01 | Critical | v2 missing stock ceiling | see MON-01 | grep | -| V2-02 | Medium | v2 missing `getHomeSlider()` / `GET /home/slider` admin banner | v1 `api/client.ts:152-170` defines `AdminSlide` + `getHomeSlider`; v2 has neither | `diff apps/user-app/src/api/client.ts apps/user-app-v2/src/api/client.ts` | +| V2-01 | ~~Critical~~ **FIXED (verified 2026-10-08)** | v2 missing stock ceiling | see MON-01 | grep | +| V2-02 | ~~Medium~~ **FIXED (verified 2026-10-08 — v2 calls `getHomeSlider`)** | v2 missing `getHomeSlider()` / `GET /home/slider` admin banner | v1 `api/client.ts:152-170` defines `AdminSlide` + `getHomeSlider`; v2 has neither | `diff apps/user-app/src/api/client.ts apps/user-app-v2/src/api/client.ts` | | V2-03 | Medium | v2 missing per-variant images | v1 defines `ProductImageSummary` (`id/url/sortOrder/role`) and `imageId`/`image` on the variant; v2's `images` is just `Array<{url:string}>` | same diff | | V2-04 | Medium | v2 missing orderable-first variant defaulting | v1 `composables/useProduct.ts:26` picks the first orderable variant; v2 has no equivalent | grep | | V2-05 | Low | v2 missing variant-chip preview cap + per-chip stock badges | v1 `components/product/SingleProduct.vue:388-392` | grep | @@ -135,7 +135,7 @@ Severity: `Critical` (money loss / security / data loss) · `High` · `Medium` | **REL-02** | High | TGT `apps/api/src/modules/sms/sms.service.ts` | D20 removed the rule that a missing SMS credential aborts the boot in production. The server now starts with SMS disabled, so **customers cannot receive a login code** and the failure is only a log line. Deliberate, to keep the delivery unblocked. | D14 (superseded) vs D20 | After the delivery, decide whether to restore a boot check or add a health endpoint that reports SMS readiness. **Eased by D26 (2026-10-05):** the default `relay` driver needs no credentials, so SMS works on a fresh deployment; a gateway refusal is still only a log line. | | **PERS-01** | Medium | REF `AdminController.php:348` vs TGT `admin-customers.service.ts` `update()` | The reference **discards the submitted mobile on edit** (`$input['mobile'] = $customer->mobile;`), making it immutable once set; the target lets an operator change it. D19 ruled the lock NOT ported, so the target keeps the editable behaviour. Changing a mobile changes the customer's login identity. | `AdminController.php:348`; D19 answer B | If the lock is wanted later, apply it in `update()` and hide/disable the field on the edit form. | | **PERS-02** | Medium | REF `cities` table vs TGT `City` model | The reference's cities are a **two-level hierarchy** (`parent` + `level`, provinces at level 0) rendered as an `a-tree-select` with provinces **not selectable** (`citiesModel.js:38`). The target flattened this to a nullable `province` **string** and a flat `a-select` on all three Persons forms. Functionally the city is still captured; the province grouping and its tree UI are absent. | `2021_09_18_171700_city_table.php:18-19`; `henza_admin/src/mixins/citiesModel.js:20-48`; `schema.prisma` `model City` | Structural change; decide with the owner during the cities screen (Phase 1c). | -| **PERS-03** | Low | REF `bulkCustomer.vue:111`, `singleCustomer.vue:29` vs TGT | The reference collects a **Jalali birth date** on both the wholesale (`birth_date`) and retail (`birth_day`) customer forms via `DateInput`. The target's `Customer` model has no column for it and both forms omit the field. | `henza_admin/.../bulkCustomer.vue:111`; `schema.prisma` `model Customer` (no birth field) | Additive: add a nullable `birthDate` column and the picker, if the owner wants it. | +| **PERS-03** | Low — **column added 2026-10-07 (`Customer.birthDate`, 541 legacy dates imported); form field still missing** | REF `bulkCustomer.vue:111`, `singleCustomer.vue:29` vs TGT | The reference collects a **Jalali birth date** on both the wholesale (`birth_date`) and retail (`birth_day`) customer forms via `DateInput`. The target's `Customer` model has no column for it and both forms omit the field. | `henza_admin/.../bulkCustomer.vue:111`; `schema.prisma` `model Customer` (no birth field) | Additive: add a nullable `birthDate` column and the picker, if the owner wants it. | | **PERF-01** | Low | TGT `admin-customers.service.ts` `listReagents()` | The per-agent gold-sold total runs **one `order.aggregate` per row** inside `Promise.all` — an N+1 on every page of the sales-expert grid. Harmless at the current agent count. | `listReagents()` `rows.map(async ...)` | Replace with a single `groupBy` over the referring agent. | ## Apps — found during the delivery smoke test (2026-09-23) @@ -143,7 +143,7 @@ Severity: `Critical` (money loss / security / data loss) · `High` · `Medium` | ID | Severity | Where | Description | Evidence | Suggested fix | |---|---|---|---|---|---| | **APP-01** | Medium (constraint, handled) | TGT `apps/user-app/src/views/user/LoginView.vue:43`, `user-app-v2` same, `components/input/OtpInput.vue:43` | Both apps render the login OTP as **four single-character inputs** and submit only on `@on-complete`, i.e. at exactly 4 digits. The API accepts any length, so an `OTP_MASTER_CODE` longer than 4 **works over HTTP but cannot be typed into either app**. The real OTP is 4 digits, so the box itself is correct. | `OtpInput.vue:43` `otp.join('').length === numInputs`; `Math.floor(1000 + random*9000)` in `customer-auth.service.ts` | Handled without touching the shipped login UI: `main.ts` warns at boot when the code is not 4 digits, and `.env.example` states the rule. | -| **APP-02** | Medium | TGT `apps/user-app-v2/src/api/client.ts` (no `getHomeSlider`) vs `apps/user-app/src/api/client.ts:228` | `user-app` calls `GET /home/slider` and prefers admin-configured slides, falling back to category-derived ones (`HomeView.vue:258`). `user-app-v2` has no such call at all and always derives its hero from categories, so **slides configured in the admin panel never appear in v2**. | `grep getHomeSlider` finds it only in user-app | Add `getHomeSlider` to v2's client and let `HeroCarousel` prefer admin slides. Deliberately deferred: v2's home is its own design (PROTECTED) and rewiring its hero during a delivery risks the page being demonstrated. | +| **APP-02** | ~~Medium~~ **FIXED (verified 2026-10-08)** | TGT `apps/user-app-v2/src/api/client.ts` (no `getHomeSlider`) vs `apps/user-app/src/api/client.ts:228` | `user-app` calls `GET /home/slider` and prefers admin-configured slides, falling back to category-derived ones (`HomeView.vue:258`). `user-app-v2` has no such call at all and always derives its hero from categories, so **slides configured in the admin panel never appear in v2**. | `grep getHomeSlider` finds it only in user-app | Add `getHomeSlider` to v2's client and let `HeroCarousel` prefer admin slides. Deliberately deferred: v2's home is its own design (PROTECTED) and rewiring its hero during a delivery risks the page being demonstrated. | | **DEPLOY-01** | ~~High~~ **FIXED** | TGT `docker-compose.yml` | The `api` service passed only `DATABASE_URL` and `PORT`, so **every auth and SMS variable was silently ignored in Docker** — `JWT_SECRET` would fall back to the development default, and `OTP_MASTER_CODE` would never reach the container, so the delivery login would not have worked in a deployed environment. | `docker compose config` before/after | ✅ All auth, SMS and master-OTP variables are now passed through, each defaulting to empty (which the code treats as "unset"). | | **TEST-01** | Low (fixed) | TGT `apps/api/scripts/test-sales-expert-roundtrip.cjs:10` | The script had `localhost:3077` hardcoded — a port from an earlier dev session — so it could only ever connect by accident. It failed with ECONNREFUSED on this branch. | Run against the standard port | ✅ Now defaults to `localhost:3000` (`API_PORT`) and accepts an override argument. Re-ran: 33/33. | @@ -186,3 +186,9 @@ Severity: `Critical` (money loss / security / data loss) · `High` · `Medium` | **ACC-07** | Medium (fixed 2026-10-05) | TGT `accounting-sync` (config, worker, admin) | "Connected but nothing syncs": the switch was saved on «زرین» while only Tahesab had an address/token. The worker found no usable provider and did nothing, with no message; «سینک الان» answered "started". Separately, an empty «گروه‌های مشتری برای ورود» meant *all groups* — on the owner's server that includes صندوق, کارمندان, سرمایه‌گذار, آبشده, کلاهبرداران… which would have become app customers. | Owner's Tahesab: 228 accounts in 15 groups, 23 کارساخته; read-only provider calls (groups, customers, balances, items, stock) all fine. | Fixed: saving a provider without address/token is refused; the screen shows why nothing syncs (`inactiveReason`); «سینک الان» refuses with the reason (or "feature off"). Customer pull now refuses to run with no groups selected; creating a customer with no «گروه مشتری جدید» goes to NEEDS_REVIEW instead of an ungrouped account. | | **ACC-08** | Medium (done 2026-10-05) | TGT `accounting-sync` (worker, providers, sync, catalog) | Owner: sync must be real-time, not delayed or manual. It was: outbox tick 30 s, balances every 10 min, products every 30 min, stock read from the stored copy at order time. | Neither Tahesab nor Zarrin has webhooks. Tahesab `DoListUpdatedAsnad` lists new/edited documents (≈400 ms); proven on the public test server (document posted → account+item seen in the next poll, 0.7 s). Zarrin `parLastTenRadif` is unverified (ACC-01). | Done: fast loop (2 s) for outbox + change poll every 5 s (fixed; the admin setting was removed at the owner's request); `kickSoon()` right after a hook enqueues; targeted balance/stock refresh; order-time live stock with 2.5 s timeout, falling back to the last known stock (owner's ruling); balance freshness 5 s. Interval jobs stay as the safety net (deletions, edits, new customers). `getChanges` tests added (55 accounting tests). See plan §11. | | **ACC-09** | Medium (done 2026-10-05) | TGT `accounting-sync`, `orders`, `admin-orders`, admin lists, both apps' profile | Owner: sync also on demand — a button on the wholesale-customer list and the product list; the app profile reads its own balance live on every visit; an optional rule for the order status that posts the temporary document (پیش‌فاکتور). | Owner: buttons/profile work whenever connected, regardless of the feature switches; the rule lives on «اتصال حسابداری». Admin order delete and status rollback could leave a temporary document behind once it can be posted before the debt. | Done: `AccountingQuickController` (`admin/accounting-status`, `admin/customers-omde/accounting-sync`, `admin/products/accounting-sync`, `customer/me/accounting-balance`), `documents.triggerStatus` (default DELIVERED_TO_AGENT), `orderStatusReached` hook from createOrder/updateStatus/rollback, withdrawal on delete/rollback, delete job independent of the order row. 8 hook tests. Plan §12. | + +## Found during the legacy data migration (2026-10-08) + +| ID | Severity | Where | Description | Evidence | Status | +|---|---|---|---|---|---| +| **FAV-01** | High (data invisible) | TGT both apps `stores/favoritesStore.ts`; no API route | Favourites were **memory-only** in both apps (lost on reload), and the API had no favourites route — so the 874 favourites imported from legacy `major_favorites` into `CustomerFavorite` were never shown. Legacy kept them on the server (`POST add-to-fav`, `POST get-favorites`, `routes/api.php:84-85`). | Store header said "local-only … until that backend support exists". | ✅ Fixed 2026-10-08: `GET customer/me/favorites`, `POST customer/me/favorites/toggle` (customer from the token); both apps load on sign-in, clear on sign-out, toggle optimistically and roll back on failure. Verified on the imported data: a customer with 116 legacy favourites sees all 116. |