Files
tehran_taj/docs/ACCOUNTING-INTEGRATION-PLAN.md
T
s.esmaeiliandClaude Opus 4.6 19d490e790 feat: accounting integration (D27), SMS relay driver (D26), rebuild user apps
- Add Tahesab/Zarrin accounting sync module with admin panel UI
- Add SMS relay driver as default provider, refactor SMS into driver pattern
- Update customer services, orders, products, and catalog queries
- Rebuild user-app and user-app-v2 dist assets
- Add Prisma migration for accounting tables
- Update docker-compose and env examples
- Update parity docs (DECISIONS, ISSUES, PROGRESS)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-06 16:48:36 +03:30

220 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# طرح اتصال به نرم‌افزار حسابداری (ته‌حساب / زرین)
> وضعیت: **تأیید شد (2026-10-05) — در حال اجرا.** پیشرفت در بخش ۱۰ («وضعیت اجرا»). تصمیم: D27 در `docs/parity/DECISIONS.md`.
> تاریخ: 2026-10-05
## ۰. زمینه
الان اپ هیچ اتصال خارجی به حسابداری ندارد: مانده، گزارش و بدهی سفارش همه از دفتر داخلی (`LedgerEntry`) است (D24). سیستم قدیم (henza) به ته‌حساب وصل بود ولی با توکن هاردکد، TLS خاموش و بدون چک مالکیت (SEC-06/16/17). حالا باید اپ به **یکی از دو** نرم‌افزار حسابداری وصل شود و در پنل ادمین با یک سوییچ انتخاب شود کدام. خروجی: همگام‌سازی مشتری، مانده، کارساخته/محصول، و ثبت سند سفارش — امن، idempotent و قابل پیگیری از ادمین.
## ۱. خلاصه‌ی دو API (از داکیومنت‌ها)
| | ته‌حساب | زرین |
|---|---|---|
| مستند | OpenAPI 3.1 کامل، ۶۶ متد (`tahesab.ir/APiDoc/openapi.json`) | ۸۲ API (`zarrin.ir/wp-content/themes/Hirad/data/api.json`) |
| نصب | Self-hosted روی سیستم/سرور مشتری؛ پورت https 8081 / http 9550 | Self-hosted، `http://host:8000/ZarrinService/accounting` |
| فراخوانی | `POST /<Method>` با بدنه‌ی `{"<Method>": [آرگومان‌ها به ترتیب اندیس]}` | `GET ?kind=<api>&token=…&param=…` |
| احراز هویت | `Authorization: Bearer` + هدر `DBname` | `token` در query string |
| ضد تکرار | `Factor_Code` یکتای ≥۲۰ کاراکتر + `DoNewSanadInquiry-By-Factor-Code` + `DoDeleteSanad` | فقط `codeRahgiri` در `sanadPooli`؛ `foroushSakhte` کلید ضد تکرار **ندارد** |
| سرور تست | دارد (`api-test.tahesab.ir`، توکن عمومی در خود داکیومنت) | ندارد (مالک: فعلاً دسترسی نداریم) |
نگاشت قابلیت‌ها به متدها:
| قابلیت | ته‌حساب | زرین |
|---|---|---|
| لیست/جستجوی مشتری | `DoListMoshtari-ByCode`، `-ByPhone`، `DoListGorooh` | `listhesabha` / `Get100Account`، `getinfoByPhone`، `listGorooheHesabha` |
| ساخت/ویرایش مشتری | `DoNewMoshtari` (کد `-1` = خودکار)، `DoEditMoshtari` | `addAccount` (`fkNoe`=گروه)، `edithesab` |
| مانده | `GetMandeHesabByCode` (لیست کدها، دسته‌ای) → `MandeyeVazni`، `MandeyeMali` | `listMandeHesabha` (همه‌ی حساب‌ها یک‌جا) / `mandeHesabGeramiPooliTedadi` |
| ریز اسناد (گزارش) | `DoListAsnad` (با مانده‌ی ردیف‌به‌ردیف `TahesabVazni/Mali`) | `parvande` (صفحه‌بندی، ردیف «مانده از قبل») |
| کارساخته: لیست/اطلاعات | `DoListNameKarsakhte` (فقط کارهای دارای سند) | `listKarSakhte` (کند: ~۶ ثانیه)، `infosakhte`، `goroohSakhte` |
| کارساخته: موجودی | `GetMojoodiKarSakhte` (با نام کار) | `mojoodiKala` |
| کارساخته: گردش | `DoListAsnad` با فیلتر نوع سند (باید روی سرور تست تأیید شود) | `infogardeshsakhte` |
| کارساخته: ساخت | **متد ندارد** — نام کار با اولین سند ساخته می‌شود | `addFelezsakhteModel` (ویرایش ندارد) |
| سند سفارش (کارساخته، بنکداری) | `DoNewSanadVKHKar` (خروج کارساخته، چندردیفی با `MultiRadif`) | `foroushSakhte` با `bonak=t` (ردیف اول با `idHesab`، بقیه با `idFactor`) |
| موقت/حذف سند | `Sabte_Kol_Or_Movaghat=0`، `DoDeleteSanad` | `movaghat=1`، `hazfRadifSanad` (فقط ردیف‌های API) |
## ۲. تصمیم‌های مالک (این جلسه)
1. سه محور + کارساخته: سینک مشتری، سینک مانده، ثبت سند سفارش، و همگام‌سازی کارساخته↔محصول. **بقیه‌ی چیزها از اپ خودمان** (اسناد دریافت/آبشده/چک در اپ می‌مانند و ارسال نمی‌شوند).
2. مانده‌ی حسابداری **به مشتری نمایش داده می‌شود**؛ **سقف اعتبار از دفتر داخلی اپ** می‌ماند.
3. **گزارش (ریز اسناد) هم از حسابداری** خوانده شود تا با مانده یکی باشد.
4. مشتری جدید اپ **بعد از احراز هویت (VERIFIED)** در حسابداری ساخته می‌شود؛ مشتری‌های موجود با **موبایل** لینک می‌شوند. مشتری‌ای که فقط در حسابداری است **در اپ ساخته شود** — اولویت با موبایل است، نه کد ملی.
5. سند سفارش در **لحظه‌ی ثبت بدهی (تحویل مسئول فروش)** و به‌صورت **موقت** ثبت می‌شود.
6. فعلاً **فقط کارساخته**؛ معاملات طلا/سکه بعداً (لایه طوری ساخته می‌شود که با یک متد اضافه شود).
7. کارساخته‌ی حسابداری = **محصول** اپ. چه در ادمین ثبت شود چه در حسابداری، باید همگام شود.
8. فیلدهای مشترک (عنوان، کد، موجودی…) **مال حسابداری** است؛ عکس، توضیحات، واریانت‌ها، دسته‌بندی و **درصد اجرت مال اپ** (درصد حسابداری فقط پیش‌فرض واریانت جدید).
9. **بدون عکس در اپ نمایش داده نشود** — خودکار، برای همه‌ی محصولات.
10. موجودی حسابداری در ادمین نمایش داده می‌شود و اگر صفر شد محصول در اپ **ناموجود** می‌شود (مگر پیش‌سفارش).
11. محیط تست زرین نداریم → درایور زرین با داده‌های نمونه‌ی داکیومنت تست می‌شود و روشن‌کردنش روی نصب واقعی فقط بعد از یک دوره‌ی آزمایشی با سند موقت.
## ۳. معماری
### ۳.۱ ماژول و درایورها
ماژول جدید `apps/api/src/modules/accounting-sync/` با همان الگوی درایور پیامک (`apps/api/src/modules/sms/drivers/` — یک فایل برای هر درایور + یک رجیستری):
- `providers/accounting-provider.ts` — اینترفیس مشترک: `health()`, `listCustomerGroups()`, `findCustomersByPhone()`, `listCustomers(cursor)`, `createCustomer()`, `updateCustomer()`, `getBalances(codes[])`, `getStatement(code, from, to, page)`, `listKarSakhte()`, `getKarSakhteStock(ref)`, `getKarSakhteMovement(from, to)`, `createKarSakhte()` (اختیاری — `capabilities` اعلام می‌کند)، `postOrderDocument(order)`, `inquireDocument(ref)`, `deleteDocument(ref)`.
- `providers/tahesab.provider.ts` ، `providers/zarrin.provider.ts` ، `providers/index.ts` (رجیستری).
- `accounting-http.ts` — کلاینت HTTP با timeout (`AbortController`، مثل `deliverViaTrez`)، **TLS همیشه روشن** با امکان دادن گواهی CA اختصاصی برای نصب‌های self-signed (نه `withoutVerifying` سیستم قدیم — SEC-17). پاک‌سازی پاسخ خراب ته‌حساب (BOM/کاراکتر کنترلی) مثل `TahesabController::test` قدیم.
- `jalali.ts` — تبدیل تاریخ با `Intl` (`-u-ca-persian`)، بدون وابستگی جدید؛ ته‌حساب سال/ماه/روز شمسی جدا می‌خواهد.
- `accounting-config.service.ts`, `accounting-sync.service.ts` (منطق همگام‌سازی)، `accounting-outbox.worker.ts`، `accounting-admin.controller.ts`.
### ۳.۲ تنظیمات (ادمین، نه env)
یک ردیف `Setting` با کلید `accounting.config`: `provider: 'none' | 'tahesab' | 'zarrin'`، اتصال هر درایور (آدرس، `DBname`، توکن، CA)، نگاشت‌ها، کلید روشن/خاموش هر قابلیت و بازه‌های زمان‌بندی.
- **راز (توکن)** write-only: هیچ API آن را برنمی‌گرداند (فقط `••••` + «تنظیم شده»)، با کلید env `ACCOUNTING_SECRET_KEY` (خوانده‌شده با `envOr` طبق D25) رمز می‌شود.
- **حفره‌ی موجود را می‌بندیم:** `GET/PUT/DELETE const` در [admin-misc.service.ts](apps/api/src/modules/admin-misc/admin-misc.service.ts) همه‌ی ردیف‌های `Setting` را به هر کسی با `config.statics` می‌دهد؛ کلیدهای `accounting.*` از این مسیرها حذف/رد می‌شوند.
- تعویض درایور: صف کارهای درایور قبلی متوقف (نه حذف) می‌شود؛ لینک‌ها per-provider هستند، پس برگشت به درایور قبلی بی‌خطر است. ادمین هنگام تعویض هشدار می‌بیند.
### ۳.۳ تغییرات اسکیما (`apps/api/prisma/schema.prisma` + migration)
- `AccountingLink` — `provider`, `entityType (CUSTOMER|PRODUCT)`, `entityId`, `externalCode`, `externalName`, `snapshot Json`, `syncedAt`؛ یکتا روی `(provider, entityType, entityId)` و `(provider, entityType, externalCode)`.
- `AccountingOutbox` — `provider`, `kind (CUSTOMER_PUSH|ORDER_DOCUMENT|ORDER_DOCUMENT_DELETE|PRODUCT_PUSH)`, `entityId`, `status (PENDING|SENT|FAILED|NEEDS_REVIEW|PAUSED)`, `attempts`, `nextAttemptAt`, `lastError`, `externalRef`, `progress Json` (برای سند چندردیفی زرین)، `request/response` خلاصه **بدون توکن**.
- `Customer`: `accountingGoldBalance`, `accountingRialBalance` (Decimal)، `accountingBalanceAt`.
- `Product`: `accountingStockWeight`, `accountingStockCount` (Decimal?)، `accountingStockAt`.
### ۳.۴ صف خروجی (Outbox) و زمان‌بند
- رویدادها **داخل همان تراکنش** کار اصلی در `AccountingOutbox` نوشته می‌شوند (مثلاً سند سفارش در همان تراکنشی که `postOrderDebt` اجرا می‌شود) → هیچ رویدادی گم نمی‌شود و قطعی حسابداری سفارش را متوقف نمی‌کند.
- Worker درون‌پردازه‌ای با `setInterval` + `pg_try_advisory_lock` (چند نمونه‌ی API هم‌زمان اجرا نکنند) — بدون وابستگی جدید. retry با backoff نمایی؛ بعد از N بار → `FAILED` و نمایش در ادمین.
- **ابهام timeout:** ته‌حساب → قبل از تلاش دوباره `DoNewSanadInquiry-By-Factor-Code`؛ زرین (بدون کلید ضد تکرار) → `NEEDS_REVIEW` به‌جای ارسال کور دوباره.
- کارهای pull دوره‌ای (مانده، کارساخته، مشتری) همین worker با بازه‌های قابل تنظیم.
## ۴. قابلیت‌ها
### F1 — مشتری
- **Pull (حسابداری → اپ):** حساب‌های گروه‌های انتخاب‌شده در ادمین (مثلاً «مشتري» GID 13 در ته‌حساب، «مشتری (خریدار)» در زرین) خوانده می‌شوند؛ موبایل نرمال می‌شود (`faToEn` + تبدیل `912…`→`0912…`).
- موبایل در اپ هست → لینک.
- نیست → **مشتری اپ ساخته می‌شود**: `nationalCode` = کد ملی حسابداری اگر معتبر و آزاد بود، وگرنه **خود موبایل** (همان fallback موجود در `customer-auth.service.ts:258` و `admin-customers.service.ts`)؛ `passwordHash: ''` (ورود با OTP)؛ `UNVERIFIED`، سقف اعتبار ۰ (بقیه مال اپ است).
- حساب بدون موبایل، یا چند حساب با یک موبایل → لیست «مغایرت» در ادمین، بدون اقدام خودکار.
- **Push (اپ → حسابداری):** وقتی ادمین مشتری را VERIFIED می‌کند و لینک ندارد → اول جستجو با موبایل (لینک اگر پیدا شد)، وگرنه ساخت (`DoNewMoshtari` / `addAccount`) در گروه انتخاب‌شده. ویرایش نام/تلفن/آدرس مشتری لینک‌شده → `DoEditMoshtari` / `edithesab`. همه از طریق Outbox.
- ثبت‌نام با موبایلی که قبلاً از حسابداری وارد شده → همان ردیف (موبایل یکتاست)، بدون حساب تکراری.
### F2 — مانده
- دسته‌ای و دوره‌ای (پیش‌فرض ۱۰ دقیقه): ته‌حساب `GetMandeHesabByCode` با تکه‌های ۱۰۰تایی کدها؛ زرین `listMandeHesabha` یک‌جا. ذخیره در `Customer.accountingGold/RialBalance`.
- تازه‌سازی تکی وقتی مشتری داشبورد را باز می‌کند و مانده قدیمی‌تر از X ثانیه است (timeout ۳ ثانیه، در خطا همان مقدار ذخیره‌شده).
- **قرارداد علامت:** ته‌حساب منفی = بدهکار (کد قدیم `* -1` می‌کرد)؛ به قرارداد اپ (مثبت = بدهکار) تبدیل می‌شود. روی سرور تست تأیید می‌شود.
- نمایش: `balances()` در [customer-report.service.ts](apps/api/src/modules/customer-report/customer-report.service.ts) وقتی اتصال روشن است مانده‌ی حسابداری + زمان آخرین سینک را برمی‌گرداند (همان شکل `{arz, mandeh}` → اپ‌ها بدون تغییر). پنل ادمین هر دو مانده را کنار هم نشان می‌دهد.
- **سقف اعتبار دست نمی‌خورد:** چک در [orders.service.ts:88-92](apps/api/src/modules/orders/orders.service.ts#L88-L92) همچنان با `goldDebtGrams` داخلی.
### F3 — سند سفارش (کارساخته)
- ماشه: شاخه‌ی `DELIVERED_TO_AGENT && !order.deliveredAt` در `updateStatus` ([orders.service.ts:154-157](apps/api/src/modules/orders/orders.service.ts#L154-L157)) — همان تراکنش `postOrderDebt` — یک ردیف `ORDER_DOCUMENT` در Outbox. پیش‌سفارش تبدیل‌شده هم چون Order می‌شود از همین مسیر می‌رود.
- همه‌ی اسناد **موقت** ثبت می‌شوند تا حسابدار نهایی کند.
- **ته‌حساب:** هر قلم یک ردیف `DoNewSanadVKHKar` (خروج کارساخته، `IsVoroodOrKhorooj=1`)، `MultiRadif=1`، `Radif_Number` ۱..n، `Factor_Code` قطعی = `GB-ORD-<order.id>` (≥۲۰ کاراکتر → ضد تکرار)، `Vazn`=وزن راش×تعداد، `Ayar`=750 (قابل تنظیم)، `Count`، `Name_Kar`=نام کار لینک‌شده، درصد اجرت مؤثر (اجرت − تخفیف)، `PoolSang`=هزینه‌ی ریالی ثابت.
- **زرین:** `foroushSakhte` با `bonak=t`, `pd=-1`, `movaghat=1`؛ قلم اول با `idHesab` → `idFactor`، اقلام بعد با `idFactor`؛ `idSakhte`=کارساخته‌ی لینک‌شده، `vazn`، `darsad`=اجرت مؤثر، `vaznSang/mablaghSang`. پیشرفت هر قلم در `progress` ذخیره می‌شود تا شکست وسط کار از همان‌جا ادامه یابد.
- **پذیرش:** بعد از ثبت، تغییر مانده‌ی وزنی حسابداری باید دقیقاً برابر `order.totalGoldWeightGrams` باشد (این‌که اجرت در کدام فیلد برود — `Darsad_Talayee` یا وزن محاسبه‌شده — با همین معیار روی سرور تست تعیین می‌شود).
- لغو سفارش بعد از ارسال سند → `ORDER_DOCUMENT_DELETE` (`DoDeleteSanad` / `hazfRadifSanad kol=1`) فقط اگر سند هنوز موقت است؛ وگرنه `NEEDS_REVIEW`.
- محصولِ بدون لینک موقع سند: چون طبق F4 هر محصول لینک دارد، این حالت استثناست → `NEEDS_REVIEW` با پیام روشن.
### F4 — کارساخته ↔ محصول
- **Pull (حسابداری → اپ)** دوره‌ای (پیش‌فرض ۳۰ دقیقه چون `listKarSakhte` کند است): کارساخته‌ی جدید → `Product` جدید (عنوان = نام، `productCode` = کد حسابداری، دسته از نگاشت «گروه ساخته → دسته‌بندی» در ادمین) بدون عکس و بدون واریانت → در اپ دیده نمی‌شود تا ادمین عکس و واریانت (وزن/تعداد) بگذارد. درصد فروش حسابداری فقط پیش‌فرض واریانت جدید.
- محصول لینک‌شده: **عنوان و کد** از حسابداری به‌روز می‌شوند و در فرم ادمین قفل‌اند؛ عکس، توضیحات، واریانت، دسته و اجرت آزاد.
- **Push (ادمین → حسابداری):**
- زرین: محصول جدید → `addFelezsakhteModel` (`noeKar` و `gorooheKar` از نگاشت ادمین) → لینک `idKar`. چون زرین ویرایش ندارد، عنوان بعد از لینک فقط از حسابداری عوض می‌شود.
- ته‌حساب: متد ساخت ندارد → `Name_Kar` = عنوان محصول؛ کار با اولین سند در ته‌حساب پدید می‌آید و از آن به بعد در `DoListNameKarsakhte` دیده و لینک می‌شود. (محدودیت API، نه انتخاب ما.)
- **موجودی:** `mojoodiKala` / `GetMojoodiKarSakhte` → `Product.accountingStock*`؛ ستون در لیست محصولات ادمین. اگر ≤ ۰ → محصول در اپ ناموجود (واریانت‌های پیش‌سفارش مستثنا) — هم در کاتالوگ هم در `createOrder` اعمال می‌شود. تعداد واریانت‌ها همچنان در ادمین.
- **گردش کارساخته:** صفحه‌ی گزارش در ادمین (بازه‌ی تاریخ) — زرین `infogardeshsakhte`؛ ته‌حساب از `DoListAsnad` اگر روی سرور تست فیلتر نوع سند جواب داد، وگرنه در این صفحه «پشتیبانی نمی‌شود».
### F5 — قانون عکس
- [catalog.service.ts](apps/api/src/modules/catalog/catalog.service.ts) (تنها مسیر لیست محصول برای مشتری) فیلتر `images: { some: {} }` می‌گیرد؛ `createOrder` واریانتِ محصول بی‌عکس را رد می‌کند.
- قبل از انتشار، تعداد محصولات فعلی بی‌عکس که پنهان می‌شوند گزارش می‌شود.
### F6 — گزارش از حسابداری
- وقتی اتصال روشن است، `summary`/`detail` در `customer-report.service.ts` از درایور خوانده می‌شود و به همان شکل `henza_pwa/src/type/accounting.ts` نگاشت می‌شود (اپ‌ها بدون تغییر):
- ته‌حساب `DoListAsnad` (`Count_Last=-1` برای مانده‌ی ردیف‌به‌ردیف) → `tahesab_T`=`TahesabVazni`، `tahesab_P`=`TahesabMali`، `factor_Code`، `sh_factor`، `tarikh`…
- زرین `parvande` → وزن/پول دریافت و پرداخت؛ مانده‌ی جاری از ردیف «مانده از قبل» جمع زده می‌شود.
- **SEC-16 حفظ می‌شود:** جزئیات سند فقط اگر کد حساب سند = کد لینک‌شده‌ی همین مشتری (از توکن).
- ستون‌هایی که REP-01 خالی گذاشته بود (نام کارساخته، جنس فلز…) حالا منبع دارند.
- اتصال خاموش یا خطا → همان گزارش دفتر داخلی فعلی (با برچسب منبع).
## ۵. پنل ادمین
- صفحه‌ی جدید `/config/manage/accounting` «اتصال حسابداری»، مجوز جدید `config.accounting` (در `apps/api/src/modules/auth/acl.constants.ts`، `apps/admin-panel/src/views/user/permissionConst.ts`، `layouts/navMenuItems.ts`، `router/admin.ts`)، با الگوی Ant Design صفحه‌ی `views/manage/wallet.vue`:
1. **اتصال:** سوییچ «بدون اتصال / ته‌حساب / زرین»، فیلدهای همان درایور، دکمه‌ی «تست اتصال» (`CheckHealth` / `version`).
2. **نگاشت:** گروه‌های مشتری برای ورود، گروه مشتری جدید، گروه ساخته ↔ دسته‌بندی، نوع کارساخته‌ی جدید (زرین)، عیار پیش‌فرض.
3. **قابلیت‌ها و زمان‌بندی:** روشن/خاموش هر قابلیت و بازه‌ها + دکمه‌های «سینک الان».
4. **لاگ:** جدول Outbox با فیلتر وضعیت، تلاش دوباره، «حل‌شده»، مشاهده‌ی درخواست/پاسخ (بدون توکن).
5. **مغایرت‌ها:** موبایل تکراری/خالی، کارساخته‌ی بی‌دسته، سند `NEEDS_REVIEW`.
- صفحه‌ی مشتری: کد حسابداری + هر دو مانده + «لینک دستی با کد».
- لیست/فرم محصول: کد حسابداری، موجودی حسابداری، قفل عنوان/کد، نشان «بدون عکس — در اپ پنهان».
## ۶. فازها
| فاز | محتوا |
|---|---|
| P0 | مستندات (این طرح در `docs/`، D27، به‌روزرسانی REP-01/SEC-17)، migration، تنظیمات + راز + بستن حفره‌ی `const`، اینترفیس درایور، کلاینت HTTP، Outbox/worker، تب «اتصال» + تست اتصال |
| P1 | F1 مشتری |
| P2 | F2 مانده + F6 گزارش |
| P3 | F4 کارساخته/محصول + F5 قانون عکس |
| P4 | F3 سند سفارش |
هر دو درایور در هر فاز ساخته می‌شوند؛ ته‌حساب روی سرور تست عمومی تأیید می‌شود، زرین با fixtureهای داکیومنت و برای نصب واقعی با دوره‌ی آزمایشی سند موقت (تصمیم ۱۱).
## ۷. فایل‌های اصلی
- جدید: `apps/api/src/modules/accounting-sync/**`، `apps/admin-panel/src/views/manage/accounting.vue`، migration جدید Prisma.
- تغییر: `apps/api/prisma/schema.prisma`، `apps/api/src/app.module.ts`، `modules/orders/orders.service.ts` (Outbox + قفل ناموجود)، `modules/admin-customers/admin-customers.service.ts` (هوک VERIFIED/ویرایش، لینک دستی)، `modules/customer-report/customer-report.service.ts`، `modules/catalog/catalog.service.ts`، `modules/admin-products/admin-products.service.ts` (push + قفل فیلد)، `modules/admin-misc/admin-misc.service.ts` (حفاظت `accounting.*`)، `modules/auth/acl.constants.ts`، فایل‌های router/nav/permission پنل ادمین، صفحه‌های مشتری و محصول ادمین.
- مستندات/استقرار: `docs/parity/DECISIONS.md`، `ISSUES.md`، `DELIVERY.md`، `.env.example`، `docker-compose.yml` (`ACCOUNTING_SECRET_KEY`).
## ۸. آزمون
- **واحد (Jest، کنار `sms.service.spec.ts`):** هر درایور با `fetch` ماک‌شده و پاسخ‌های نمونه‌ی خود داکیومنت‌ها: ترتیب آرگومان‌های ته‌حساب، هدرها، تاریخ شمسی، علامت مانده، `Factor_Code`، ادامه‌ی سند چندردیفی زرین، رفتار timeout/500/JSON خراب.
- **Outbox:** نوشتن در همان تراکنش، retry/backoff، inquiry پیش از تکرار (ته‌حساب)، `NEEDS_REVIEW` (زرین)، توقف هنگام تعویض درایور، قفل advisory.
- **یکپارچه با سرور تست ته‌حساب** (اسکریپت در `apps/api/scripts/`، کنار `auth-smoke.sh`): ساخت مشتری → خواندن مانده → سند موقت `VKHKar` → inquiry → بررسی تغییر مانده = وزن سفارش → حذف سند.
- **e2e محلی:** سفارش آزمایشی تا «تحویل مسئول فروش» → ردیف Outbox → `SENT`؛ سقف اعتبار همچنان با دفتر داخلی؛ محصول بی‌عکس در `catalog/products` نیست؛ `GET const` کلید `accounting.config` را برنمی‌گرداند و توکن در هیچ پاسخ/لاگی نیست.
- `pnpm test`، `pnpm build`، lint، و یک دور دستی در پنل ادمین (اتصال، تست، سینک الان، لاگ).
## ۹. مواردی که روی سرور تست بسته می‌شوند (سؤال از شما نیست)
- علامت و واحد مانده‌ی وزنی (گرم ۷۵۰؟) در هر دو.
- فیلد درست اجرت در `VKHKar` (`Darsad_Talayee` یا وزن محاسبه‌شده) با معیار پذیرش F3.
- قالب تاریخ هر API زرین (بعضی میلادی، بعضی شمسی‌اند).
- شناسه‌های `fkNoe`/`noeKar` زرین روی نصب واقعی.
## ۱۰. وضعیت اجرا (2026-10-05)
| فاز | وضعیت |
|---|---|
| P0 | ✅ اسکیما + migration، تنظیمات با توکن رمزشده و write-only، بستن مسیرهای `const`، اینترفیس درایور، کلاینت HTTP، Outbox + worker با lease، صفحه‌ی «اتصال حسابداری» |
| P1 | ✅ مشتری: ورود/لینک با موبایل، ساخت در حسابداری پس از VERIFIED، ویرایش، لیست مغایرت |
| P2 | ✅ مانده (دوره‌ای + تازه‌سازی ۳ ثانیه‌ای در `/customer/me`) و گزارش از حسابداری با بازگشت به دفتر داخلی |
| P3 | ✅ کارساخته ↔ محصول، قفل عنوان/کد، ستون موجودی، قفل ناموجود، قانون عکس |
| P4 | ✅ سند موقت در «تحویل مسئول فروش»، حذف با لغو (فقط سند موقت) |
**آزمون:** ۴۳ تست واحد درایورها/ابزارها (کل API: ۱۸۰ تست سبز)، و `apps/api/scripts/test-accounting-tahesab.ts` روی **سرور تست واقعی ته‌حساب** و یک دیتابیس دورریختنی: ۲۹/۲۹ — از جمله لینک دوباره‌ی همان حساب با موبایل در اجرای دوم، و سندی که دقیقاً به اندازه‌ی وزن سفارش جابه‌جا می‌کند. سند آزمایشی در پایان حذف می‌شود.
### نتیجه‌ی موارد بخش ۹ (اندازه‌گیری روی سرور تست ته‌حساب)
- **علامت مانده:** «خروج» مانده‌ی وزنی را کم می‌کند → منفی = بدهکار. در درایور برعکس می‌شود.
- **اجرت:** اثر سند روی مانده = وزن × عیار ÷ ۷۵۰؛ `Darsad_Talayee` و `Ojrat` فقط ذخیره می‌شوند و اثری ندارند. پس اجرت به‌صورت **افزایش عیار** فرستاده می‌شود (۷۵۰ با ۱۰٪ → ۸۲۵) و وزن فیزیکی دست نمی‌خورد.
- **سنگ:** `PoolSang` روی مانده‌ی ریالی اثر ندارد؛ هزینه‌ی ثابت ریالی در `MablaghKol` می‌رود.
- **سند موقت** تا نهایی‌شدن روی مانده اثر ندارد (ACC-02).
- **ضد تکرار:** `Factor_Code` کوچک‌حرف ذخیره می‌شود؛ ارسال دوباره‌ی همان ردیف خطای duplicate می‌دهد → تلاش دوباره امن است. استعلام سند چندردیفی `Duplicate` برمی‌گرداند (= پیدا شد).
- **زرین:** هنوز روی نصب واقعی آزموده نشده (ACC-01).
### یافته‌های خارج از دامنه (در ISSUES)
- **ACC-03 (High):** ثبت دوباره‌ی بدهی سفارش اگر دو بار به «تحویل مسئول فروش» برود — ناحیه‌ی محافظت‌شده، اصلاح نشد.
- **ACC-04:** `runMigrations` روی دیتابیس کاملاً خالی به‌خاطر ترتیب نام `0002_admin_schema` می‌شکند.
## ۱۱. همگام‌سازی لحظه‌ای (حکم مالک، ۱۴۰۵/۰۷/۱۳ — جایگزین «سینک دوره‌ای»)
مالک: «این باید ریل‌تایم باشد، نه با تأخیر یا سینک دستی». هیچ‌کدام از دو API وب‌هوک ندارند، پس تأخیر با سه لایه به چند ثانیه می‌رسد:
| مسیر | قبل | حالا |
|---|---|---|
| اپ ← حسابداری (سند سفارش، مشتری تأییدشده/ویرایش‌شده) | منتظر tick ۳۰ ثانیه‌ای | بعد از commit همان تراکنش (≈۰٫۷ ثانیه) صف ارسال را بیدار می‌کند؛ حلقه‌ی سریع هر ۲ ثانیه هم صف را می‌خواند |
| حسابداری ← اپ (مانده، موجودی کارساخته) | ۱۰ و ۳۰ دقیقه | **فید تغییرات** هر ۵ ثانیه (ثابت در کد؛ تنظیم ادمینش به خواست مالک حذف شد)؛ فقط حساب‌ها/کارساخته‌های تغییرکرده دوباره خوانده می‌شوند |
| مانده‌ای که مشتری می‌بیند | تازه‌سازی اگر قدیمی‌تر از ۶۰ ثانیه | اگر قدیمی‌تر از ۵ ثانیه، همان لحظه از حسابداری (مهلت ۳ ثانیه؛ در خطا آخرین مقدار) |
| موجودی هنگام **ثبت سفارش** | مقدار ذخیره‌شده | مستقیم از حسابداری (مهلت ۲٫۵ ثانیه)؛ **اگر پاسخ ندهد، سفارش با آخرین موجودی معلوم ثبت می‌شود** و تغییر بعدی اصلاحش می‌کند (حکم مالک) |
**فید تغییرات** (`getChanges` در هر درایور؛ مکان‌نما در حافظه، اولین poll فقط مبدأ را می‌گیرد):
- **ته‌حساب:** `DoListUpdatedAsnad(200)` (≈۴۰۰ms). سند «جدید» = `ID` بزرگ‌تر از مکان‌نما یا `ZamanEdit` دیرتر؛ از هر سند `MCode` (حساب) و برای اسناد کارساخته `No_Kar` (نام کار). روی سرور تست واقعی ثابت شد: سند ثبت‌شد و در poll بعدی (۰٫۷ ثانیه) حساب و کارساخته‌اش گزارش شد.
- **زرین:** `parLastTenRadif` بدون حساب (تازه‌ترین ردیف‌های روزنامه، `idRadif` صعودی) — **هنوز روی نصب واقعی دیده نشده (ACC-01)**؛ نام کارساخته‌ی تغییرکرده را نمی‌دهد، پس موجودی در زرین فقط با بازه‌ی کارساخته و خواندن لحظه‌ی سفارش تازه می‌شود.
- **چیزی که فید نمی‌بیند** (حذف سند/حساب، مشتری جدید بی‌سند، ویرایش مشخصات مشتری): حلقه‌ی ۳۰ ثانیه‌ای قبلی با بازه‌های دقیقه‌ای به‌عنوان **شبکه‌ی اطمینان** می‌ماند. اگر پنجره‌ی فید سرریز شود (همه‌ی ردیف‌ها جدید) یا حسابی/کاری لینک‌نشده دیده شود، همان لحظه کار کامل مربوط (مانده/کارساخته/مشتری، مشتری حداکثر دقیقه‌ای یک بار) اجرا می‌شود.
- حلقه‌ی سریع جدا از حلقه‌ی کند است؛ import طولانی مشتریان آن را نگه نمی‌دارد.
محدودیت: ته‌حساب سند **موقت** را روی مانده اعمال نمی‌کند (ACC-02)؛ لحظه‌ای بودن یعنی «تغییر ثبت‌شده در حسابداری چند ثانیه بعد در اپ است»، نه اینکه سند موقت مانده را عوض کند.
## ۱۲. دکمه‌های سینک، مانده‌ی پروفایل، قانون زمان ثبت سند (حکم مالک، ۱۴۰۵/۰۷/۱۳)
حکم‌ها: دکمه‌ها و مانده‌ی پروفایل **مستقل از کلیدهای قابلیت** کار می‌کنند (فقط اتصال برقرار باشد)؛ قانون سند در همان صفحه‌ی «اتصال حسابداری».
| جا | رفتار |
|---|---|
| «لیست مشتریان عمده» | دکمه‌ی «سینک با …» (فقط وقتی اتصال برقرار است): `POST admin/customers-omde/accounting-sync` (مجوز همان لیست) — ورود/لینک مشتریان (اگر گروه انتخاب شده باشد) + مانده‌ی همه؛ منتظر می‌ماند و نتیجه‌ی هر بخش را نشان می‌دهد |
| «لیست محصولات» | دکمه‌ی مشابه: `POST admin/products/accounting-sync` (مجوز `product.list`) — کارساخته‌ها و موجودی |
| پروفایل اپ (هر دو اپ) | هر بار باز شدن صفحه یک درخواست جدا `GET customer/me/accounting-balance` مانده‌ی **همان** مشتری را همان لحظه از حسابداری می‌خواند و در بخش «مانده‌ی حساب» (طلا و ریال، بدهکار/بستانکار) نشان می‌دهد؛ اگر حسابداری پاسخ ندهد «آخرین مانده‌ی ثبت‌شده» |
| ثبت سند سفارش | قانون `documents.triggerStatus` در تب «قابلیت‌ها و زمان‌بندی»، کنار کلید ثبت سند: سند **موقت (پیش‌فاکتور)** وقتی سفارش به این وضعیت **یا بعد از آن** برسد ثبت می‌شود (ثبت سفارش، تأیید مدیر، تأمین، آماده‌سازی، تحویل مسئول فروش [پیش‌فرض = رفتار قبلی]، تحویل مشتری). ارسال بلافاصله پس از commit |
جزئیات قانون سند: `SUSPENDED` هیچ‌وقت ماشه نیست؛ برگشت وضعیت به زیر قانون (برداشتن وضعیت در ادمین) یا لغو یا **حذف سفارش از ادمین** سند موقت را پس می‌گیرد (حذف فقط اگر هنوز موقت است). در ته‌حساب رسیدن دوباره به قانون سند تازه می‌فرستد (ترتیب صف: اول حذف)؛ در زرین هرگز دو بار (کلید ضد تکرار ندارد). کار حذف سند دیگر به ردیف سفارش نیاز ندارد (کد حساب از درخواست خود سند خوانده می‌شود). ثبت بدهی دفتر داخلی (`postOrderDebt`) دست نخورد.