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

34 KiB
Raw Blame History

طرح اتصال به نرم‌افزار حسابداری (ته‌حساب / زرین)

وضعیت: تأیید شد (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 همه‌ی ردیف‌های 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 وقتی اتصال روشن است مانده‌ی حسابداری + زمان آخرین سینک را برمی‌گرداند (همان شکل {arz, mandeh} → اپ‌ها بدون تغییر). پنل ادمین هر دو مانده را کنار هم نشان می‌دهد.
  • سقف اعتبار دست نمی‌خورد: چک در orders.service.ts:88-92 همچنان با goldDebtGrams داخلی.

F3 — سند سفارش (کارساخته)

  • ماشه: شاخه‌ی DELIVERED_TO_AGENT && !order.deliveredAt در updateStatus (orders.service.ts:154-157) — همان تراکنش 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 (تنها مسیر لیست محصول برای مشتری) فیلتر 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) دست نخورد.