arc/docs/MULTI_CURRENCY_IMPLEMENTATION_GUIDE.md

569 lines
29 KiB
Markdown
Raw Permalink 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.

# راهنمای فنی پیاده‌سازی چندارزی عملیاتی (Hesabix)
**وضعیت:** نسخه ۱ پیاده‌سازی‌شده (هسته عملیاتی) — برنامه نسخه ۲ در `MULTI_CURRENCY_EXECUTION_PLAN.md` بخش‌های ۱۶–۱۸
**مخاطب:** تیم توسعه Backend / Flutter
**هدف:** چندارزی عملیاتی با حفظ سادگی کامل برای کسب‌وکارهای تک‌ارزی.
> برای وضعیت فازها و برنامه v2 به سند اجرایی مراجعه کنید؛ این راهنما قراردادهای طراحی را نگه می‌دارد.
---
## ۱. خلاصه اجرایی
امروز حسابیکس **چندارزی در سطح سند** دارد (ارز اصلی/فرعی، تاریخچه نرخ دستی، تسعیر فاکتور، فیلتر ارز در بسیاری گزارش‌ها). درخواست کاربر عمدتاً به **چندارزی عملیاتی** مربوط است:
| قابلیت | وضعیت فعلی | هدف |
|--------|------------|-----|
| نرخ روز در نوار ابزار + API خارجی | ندارد | دارد |
| قیمت دوگانه کالا + sync از نرخ | ناقص | دارد |
| جمع دوگانه فاکتور (ارزی + پایه + نرخ) | ناقص | دارد |
| پرداخت فاکتور با ارز دیگر + نرخ per قسط | مسدود | دارد |
| انتقال بانکی بین‌ارزی با نرخ دستی | ندارد | دارد |
| مانده شخص به تفکیک ارز | ندارد | دارد |
| فیلتر/نمایش دوگانه در اسناد و گزارش‌ها | ناقص | یکدست |
**اصل طلایی طراحی:** کسب‌وکاری که فقط یک ارز دارد، باید UI و جریان کاری‌اش دقیقاً مثل قبل بماند و هیچ اثری از چندارزی نبیند.
---
## ۲. اصل Progressive Disclosure (مخفی‌سازی برای تک‌ارزی)
### ۲.۱ تعریف رسمی «حالت چندارزی فعال»
```text
is_multi_currency_enabled(business) :=
تعداد ارزهای فعال در business_currencies >= 2
OR
(وجود حداقل یک ارز فرعی غیر از default_currency_id)
```
- **تک‌ارزی (MC = OFF):** فقط `default_currency_id` فعال است؛ هیچ ارز فرعی در `business_currencies` نیست.
- **چندارزی (MC = ON):** حداقل یک ارز فرعی به کسب‌وکار اضافه شده.
> فعال‌سازی چندارزی = افزودن ارز فرعی در تنظیمات ارزهای کسب‌وکار (مسیر موجود). نیازی به سوئیچ جداگانه نیست مگر بعداً برای «خاموش کردن موقت UI» لازم شود.
### ۲.۲ منبع حقیقت در کلاینت
یک مقدار واحد در `AuthStore` / context کسب‌وکار:
| فیلد پیشنهادی | نوع | توضیح |
|---------------|-----|--------|
| `isMultiCurrency` | `bool` | از API پروفایل/کسب‌وکار محاسبه و ارسال شود |
| `secondaryCurrencyCount` | `int` | اختیاری برای دیباگ |
| `baseCurrency` | object | ارز اصلی |
| `activeCurrencies` | list | فقط وقتی MC=ON لازم است |
**Backend:** در پاسخ `GET /businesses/{id}` یا bootstrap session فیلد `is_multi_currency: bool` اضافه شود تا UI مجبور به حدس نباشد.
### ۲.۳ قوانین مخفی‌سازی UI (اجباری)
وقتی `isMultiCurrency == false`:
| محل | رفتار |
|-----|--------|
| نوار ابزار بالایی | بدون ویجت نرخ ارز |
| منوی تنظیمات | لینک «تسعیر ارز / نرخ‌ها / سیاست تسعیر» مخفی یا داخل «ارزها» فقط به‌صورت غیرفعال با توضیح «پس از افزودن ارز دوم فعال می‌شود» |
| فاکتور | بدون انتخابگر ارز (ارز = پیش‌فرض)، بدون فیلد نرخ تسعیر، بدون جمع دوگانه |
| پرداخت فاکتور | بدون انتخاب ارز پرداخت / نرخ تبدیل |
| انتقال بانکی | بدون انتخابگر ارز (یا فقط نمایش واحد ارز پیش‌فرض)، بدون نرخ تبدیل |
| فرم کالا | بدون بخش قیمت ارزی / sync نرخ |
| اشخاص | بدون تفکیک مانده per ارز |
| اسناد / گزارش‌ها | بدون فیلتر ارز و بدون گزینه «همه ارزها» |
| چاپ فاکتور | بدون بلوک نرخ / معادل ریالی |
وقتی `isMultiCurrency == true`: همه قابلیت‌های فازهای زیر به‌تدریج ظاهر می‌شوند.
### ۲.۴ قوانین Backend (دفاع در عمق)
حتی اگر کلاینت باگ داشته باشد:
- APIهای مخصوص MC (نرخ روز، پرداخت چندارزی، انتقال بین‌ارزی، قیمت sync) اگر کسب‌وکار تک‌ارزی باشد → `400 MULTI_CURRENCY_REQUIRED` یا silently no-op طبق قرارداد هر endpoint.
- اسناد همیشه `currency_id = default` برای تک‌ارزی اجباری بماند.
- پرداخت با ارز متفاوت از فاکتور فقط وقتی MC=ON و ارز پرداخت در `business_currencies` باشد مجاز است.
### ۲.۵ ویجت‌های مشترک (الگو)
```dart
/// الگوی پیشنهادی — پیاده‌سازی در فاز ۰
class MultiCurrencyGate extends StatelessWidget {
final bool isMultiCurrency;
final Widget child;
final Widget? singleCurrencyChild; // معمولاً SizedBox.shrink()
...
}
```
همه ورودی‌های جدید MC باید پشت این gate باشند تا پراکندگی شرط‌ها کم شود.
### ۲.۶ مهاجرت کسب‌وکارهای موجود
- کسب‌وکارهایی که از قبل ارز فرعی دارند → MC=ON؛ UI جدید را می‌بینند.
- بقیه → هیچ تغییری در ظاهر.
- افزودن اولین ارز فرعی → بلافاصله MC=ON؛ بهتر است یک onboarding کوتاه («نرخ مرجع را ثبت کنید») نمایش داده شود (فاز ۱).
---
## ۳. وضعیت موجود (Baseline) — چه چیزهایی از قبل هست
استفاده مجدد؛ از نو ساخته نشود مگر ناقص باشد.
| قطعه | مسیر تقریبی | نقش |
|------|-------------|-----|
| مدل ارزها | `adapters/db/models/currency.py` | کاتالوگ + `business_currencies` |
| نرخ تسعیر دستی | `business_currency_rate.py` + service/API | تاریخچه نرخ نسبت به ارز پایه |
| تسعیر فاکتور | `invoice_fx_revaluation.py` | snapshot در `extra_info.fx` |
| صفحه نرخ‌ها | `currency_revaluation_page.dart` | CRUD نرخ |
| سیاست تسعیر | `fx_revaluation_settings_page.dart` | as_of / when_no_rate |
| انتخاب ارز فاکتور | `invoice_info_form` + `InvoiceFxRateField` | فقط وقتی ارز ≠ پایه |
| مانده شخص به پایه | `person_service.calculate_person_balance` | تجمیع تسعیرشده |
| لیست‌قیمت چندارزی | `price_items.currency_id` | قیمت per ارز در لیست‌قیمت |
| انتقال هم‌ارز | `transfer_service` | یک ارز؛ حساب‌ها فیلتر می‌شوند |
| پرداخت هم‌ارز | `invoice_service._validate_invoice_payment_item_currency` | mismatch = خطا |
---
## ۴. معماری هدف (Target Architecture)
### ۴.۱ مفاهیم
```text
Base Currency = ارز اصلی کسب‌وکار (default_currency_id)
Document Currency = ارز سند (فاکتور/دریافت/انتقال/...)
Payment Currency = ارز حساب/وسیله پرداخت (ممکن است ≠ Document Currency)
Spot / Reference Rate = نرخ مرجع روز (۱ واحد ارز فرعی = rate × پایه)
Transaction Rate = نرخ قفل‌شده روی همان تراکنش/قسط (ممکن است ≠ نرخ روز)
```
### ۴.۲ اصل حسابداری
- مانده‌های حساب بانکی/صندوق/تنخواه **همیشه به ارز همان حساب** نگه داشته می‌شوند.
- بدهی شخص بابت فاکتور **به ارز فاکتور** است.
- وقتی پرداخت با ارز دیگر انجام شود، باید:
1. مبلغ به ارز فاکتور (کاهش بدهی)،
2. مبلغ به ارز پرداخت (حرکت حساب نقدی)،
3. نرخ و مابه‌التفاوت تسعیر (در صورت نیاز طبق سیاست)،
همگی ثبت و قابل گزارش باشند.
### ۴.۳ ذخیره نرخ روی تراکنش
هر جا نرخ مهم است، **snapshot** شود (مثل فاکتور فعلی با `extra_info.fx`):
```json
{
"fx": {
"base_currency_id": 1,
"from_currency_id": 2,
"to_currency_id": 1,
"rate": "1600000",
"rate_row_id": 123,
"mode": "manual|auto|selected|api",
"effective_at": "2026-07-18T10:00:00+00:00",
"source": "user|business_rate|provider:mesghal"
}
}
```
برای پرداخت‌های چندمرحله‌ای: هر آیتم پرداخت `fx` خودش را دارد.
### ۴.۴ قابلیت مشاهده برای تک‌ارزی
هیچ جدول/ستون جدیدی رفتار تک‌ارزی را تغییر ندهد. ستون‌های جدید nullable و فقط در مسیر MC پر شوند.
---
## ۵. فازهای پیاده‌سازی (ترتیب پیشنهادی)
هر فاز باید به‌تنهایی قابل انتشار باشد و پشت `isMultiCurrency` gate باشد.
---
### فاز ۰ — زیرساخت Gate و قرارداد API
**هدف:** شرط یکتا برای مخفی‌سازی؛ بدون قابلیت کاربرپسند جدید.
#### Backend
- [ ] افزودن `is_multi_currency` به پاسخ کسب‌وکار / session bootstrap
- [ ] هلپر مشترک: `assert_multi_currency(business)` و `business_is_multi_currency(db, id)`
- [ ] تست واحد: کسب‌وکار با ۱ ارز → false؛ با ۲ ارز → true
#### Frontend
- [ ] خواندن `isMultiCurrency` در `AuthStore`
- [ ] ویجت/اکستنشن `MultiCurrencyGate`
- [ ] مخفی‌سازی منوهای موجود تسعیر/نرخ وقتی MC=OFF (در صورت نمایش فعلی)
- [ ] مخفی‌سازی انتخابگر ارز در فرم‌هایی که فقط یک گزینه دارند (اختیاری ولی توصیه‌شده برای یکدست‌سازی)
#### معیار پذیرش
- کسب‌وکار تک‌ارزی هیچ منو/فیلد جدید MC نمی‌بیند.
- افزودن ارز دوم → بدون رفرش اجباری شدید، پس از reload session، UI MC ظاهر شود.
**وابستگی:** ندارد — اول انجام شود.
---
### فاز ۱ — نرخ مرجع روز (دستی + نمایش نوار ابزار)
**هدف:** آیتم نرخ روزانه در نوار ابزار بالایی؛ ورود دستی؛ بدون الزام API خارجی در همین فاز.
#### دامنه
- ویجت toolbar فقط وقتی `isMultiCurrency`
- نمایش آخرین نرخ هر ارز فرعی نسبت به پایه (مثلاً USD = 1,600,000)
- دکمه «به‌روزرسانی / ثبت نرخ» → دیالوگ یا لینک به صفحه نرخ‌ها
- ثبت در همان `business_currency_rates` (effective_at = now)
#### Backend
- [x] endpoint خلاصه نرخ‌های جاری:
`GET /businesses/{id}/currency-rates/latest`
- [x] `POST .../currency-rates/bulk` برای ثبت چند ارز یکجا از دیالوگ toolbar
#### Frontend
- [x] ویجت `DailyFxRatesToolbarChip` در shell کسب‌وکار
- [x] دیالوگ/شیت ثبت سریع نرخ + «از نرخ روز» (اسنپ‌شات مرکزی فاز ۰)
- [x] کش کوتاه‌مدت + refresh دستی
#### معیار پذیرش
- تک‌ارزی: toolbar بدون تغییر. ✅
- چندارزی: نرخ‌ها دیده می‌شوند؛ ثبت دستی در تاریخچه ظاهر می‌شود. ✅
---
### فاز ۱ب — آپدیت خودکار نرخ از اسنپ‌شات مرکزی + آفست (پیاده‌سازی‌شده)
**هدف:** ثبت زمان‌بندی‌شدهٔ نرخ تسعیر کسب‌وکار از اسنپ‌شات مرکزی، با حاشیهٔ درصد/مبلغ per ارز.
> جایگزین طرح قدیمی «کلید API per business». جزئیات: `docs/MULTI_CURRENCY_AUTO_SYNC_SCENARIO.md`
#### آنچه پیاده شد
- جداول `business_fx_auto_sync_settings` + `business_fx_auto_sync_currency_rules`
- API: `GET/PUT .../fx-auto-sync` ، `GET/POST .../preview` ، `POST .../run-now`
- Worker: `fx_auto_sync_loop` (هر دقیقه due ها)
- UI: تنظیمات کسب‌وکار → «به‌روزرسانی خودکار نرخ تسعیر» (فقط MC)
- حالت‌ها: interval (۱–۲۴س) و ساعات ثابت روزانه + timezone
- ایمنی: skip_if_unchanged، block_if_stale، سقف آفست درصد ۵۰٪
#### معیار پذیرش
- تک‌ارزی: منوی تنظیمات و UI مخفی/گیت‌شده. ✅
- چندارزی + فعال: نرخ‌ها طبق زمان‌بندی در `business_currency_rates` ثبت و در toolbar دیده می‌شوند. ✅
- بدون فعال‌سازی → همه چیز دستی مثل فاز ۱. ✅
---
### فاز ۲ — نمایش دوگانه جمع فاکتور + نرخ کنار جمع
**هدف:** در ثبت/نمایش/چاپ فاکتور ارزی: جمع ارزی + معادل پایه + نرخ استفاده‌شده.
#### دامنه
- فقط وقتی ارز فاکتور ≠ پایه و MC=ON
- استفاده از snapshot موجود `extra_info.fx` (تقویت نمایش؛ نه لزوماً مدل جدید)
- امکان override نرخ دستی **داخل فاکتور** (اگر هنوز فقط از لیست نرخ است: اجازه ورود نرخ آزاد + ذخیره به‌عنوان rate جدید یا inline در `fx.rate` با `mode=manual`)
#### UI
- انتهای فاکتور:
- جمع کل: `1,000 USD`
- معادل پایه: `1,600,000,000 IRR` (نرخ: `1,600,000`)
- چاپ و لینک عمومی همان منطق
#### Backend
- [ ] در پاسخ جزئیات فاکتور فیلدهای محاسبه‌شده:
`totals.foreign`, `totals.base`, `fx` (اگر نباشد از resolve بساز — بهتر snapshot اجباری بماند)
- [ ] اطمینان از پر شدن `fx` هنگام create/update (الان هست؛ تست پوشش)
#### معیار پذیرش
- تک‌ارزی / فاکتور پایه: هیچ بلوک معادلی نیست.
- فاکتور ارزی: جمع دوگانه + نرخ در UI و چاپ.
---
### فاز ۳ — پرداخت چندارزی فاکتور (هسته عملیاتی)
**هدف:** فاکتور دلاری؛ پرداخت ریالی (یا ارز دیگر)؛ چند قسط با نرخ جدا؛ نمایش در بخش پرداخت‌ها.
#### مثال کاربر
فاکتور `1000 USD`
- قسط ۱: `200 USD` معادل با نرخ `140,000` تومان → پرداخت از حساب ریالی
- قسط ۲: `800 USD` با نرخ `150,000`
نمایش: `100$ (۱۶۰,۰۰۰,۰۰۰ ریال با نرخ …)` — واحدها طبق ارز پایه کسب‌وکار.
> توجه: نرخ‌های مثال کاربر ممکن است تومان باشد؛ در سیستم ارز پایه اغلب IRR است. در UI برچسب واحد باید از ارز پایه بیاید و در docs تبدیل تومان↔ریال صریح باشد.
#### مدل داده پیشنهادی (آیتم پرداخت)
```json
{
"transaction_type": "bank",
"bank_id": 10,
"amount": 280000000,
"amount_currency_id": 1,
"settles_amount": 200,
"settles_currency_id": 2,
"fx": {
"rate": "1400000",
"mode": "manual",
"base_currency_id": 1,
"from_currency_id": 2,
"to_currency_id": 1
}
}
```
معنی:
- `settles_amount` به ارز فاکتور از بدهی کم می‌کند.
- `amount` از حساب پرداخت کم/زیاد می‌شود.
- `amount ≈ settles_amount × rate` (با تلورانس گرد کردن طبق `decimal_places`).
#### Backend (شکستن قفل فعلی)
- [ ] حذف/شل کردن فرض «حساب باید = ارز فاکتور» وقتی:
- MC=ON
- و فیلدهای settles/fx معتبرند
- [ ] مسیر هم‌ارز فعلی بدون تغییر بماند (سازگاری عقب‌رو)
- [ ] تولید خطوط سند حسابداری صحیح (بدهکار/بستانکار شخص به ارز فاکتور یا معادل؛ نقد به ارز حساب؛ تفاوت تسعیر در صورت سیاست)
- [ ] ذخیره fx روی هر payment line در `extra_info`
#### Frontend
- [ ] در `invoice_transactions_widget`: اگر MC و حساب≠ارز فاکتور → فیلد نرخ + مبلغ تسویه ارزی
- [ ] نمایش لیست پرداخت‌ها با فرمت دوگانه
- [ ] اگر MC=OFF → UI و validation عین امروز
#### معیار پذیرش
- سناریو مثال ۱۰۰۰ دلاری با دو نرخ مختلف قابل ثبت و گزارش است.
- کسب‌وکار تک‌ارزی هیچ فیلد جدیدی نمی‌بیند و رفتار پرداخت تغییر نکرده.
#### ریسک / نیاز تصمیم محصول (قبل از کد)
1. آیا تفاوت نرخ نسبت به نرخ فاکتور به‌عنوان سود/زیان تسعیر ثبت شود یا فقط اطلاعاتی باشد؟
2. مانده فاکتور همیشه به ارز فاکتور کم شود (`settles_amount`) — توصیه: بله.
3. واحد نمایش نرخ (ریال در برابر تومان) در تنظیمات کسب‌وکار.
---
### فاز ۴ — انتقال بانکی بین‌ارزی با نرخ دستی
**هدف:** از حساب دلاری به حساب ریالی (یا بالعکس) با نرخ دستی.
#### مدل
```json
{
"source": { "type": "bank", "id": 1 },
"destination": { "type": "bank", "id": 2 },
"source_amount": 1000,
"destination_amount": 1600000000,
"fx": { "rate": "1600000", "mode": "manual" }
}
```
- ارز از حساب مبدأ/مقصد خوانده می‌شود (نه یک `currency_id` سند واحد — یا سند با ارز پایه و جزئیات در extra).
- تصمیم طراحی سند: ترجیح = `extra_info` با دو مبلغ + fx؛ خطوط DocumentLine به ارز منطقی هر حساب (یا معادل پایه طبق الگوی حسابداری فعلی).
#### UI
- فقط وقتی ارز مبدأ ≠ ارز مقصد و MC=ON: فیلد نرخ + مبلغ مقصد (یا محاسبه خودکار).
- هم‌ارز: فرم فعلی بدون تغییر ظاهری.
#### معیار پذیرش
- انتقال هم‌ارز عین قبل.
- انتقال بین‌ارزی با نرخ دستی موجودی دو حساب را درست جابه‌جا می‌کند.
---
### فاز ۵ — مانده اشخاص به تفکیک ارز
**هدف:** خلاصه وضعیت مالی:
```text
بدهکار: 10,000,000 IRR
بستانکار: 200 USD
────────────────
جمع معادل پایه (نرخ مرجع روز): …
جمع ریالی جدا (اختیاری طبق ارز پایه)
نمایش ردیفی: 160,000,000 IRR (100 USD)
```
#### Backend
- [ ] `GET .../persons/{id}/balances-by-currency`
- [ ] تجمیع خطوط سند **بدون تبدیل** per `document.currency_id`
- [ ] فیلد اختیاری `base_equivalent` با نرخ latest یا نرخ روز
#### Frontend
- [ ] کارت خلاصه در جزئیات شخص — فقط MC=ON
- [ ] لیست اشخاص: ستون تراز فعلی برای تک‌ارزی بماند؛ برای MC می‌توان tooltip/زیرنویس per ارز گذاشت (فاز بعدی UI)
#### معیار پذیرش
- تک‌ارزی: همان یک عدد تراز.
- چندارزی: تفکیک واضح؛ حساب‌ها «به هم نمی‌ریزد».
---
### فاز ۶ — قیمت دوگانه کالا + آپدیت از نرخ مرجع
**هدف:** روی کالا قیمت خرید/فروش ریالی و ارزی؛ در صورت انتخاب کاربر، ریالی از نرخ روز آپدیت شود.
#### گزینه‌های طراحی (یکی انتخاب شود قبل از پیاده‌سازی)
**گزینه A — فیلدهای اختصاصی روی Product (ساده‌تر برای کاربر ایرانی)**
`purchase_price_base`, `sales_price_base`, `purchase_price_fx`, `sales_price_fx`, `fx_currency_id`, `auto_update_base_from_fx: bool`
**گزینه B — تکیه بر PriceList موجود**
قیمت ارزی و ریالی به‌صورت دو ردیف price_item؛ sync جابیتوانی بین دو ردیف.
**توصیه:** گزینه A برای UX فرم کالا + همگام‌سازی اختیاری با لیست‌قیمت پیش‌فرض.
#### رفتار sync
- وقتی `auto_update_base_from_fx` و نرخ latest موجود:
`base = fx_price × rate`
- دکمه دستی «بروزرسانی از نرخ روز» در فرم کالا / عملیات گروهی
- فقط MC=ON؛ تک‌ارزی فقط همان `base_*` فعلی
#### معیار پذیرش
- تک‌ارزی: فرم قیمت بدون بخش ارزی.
- چندارزی: دو قیمت + آپدیت اختیاری از نرخ toolbar/فاز۱.
---
### فاز ۷ — یکدست‌سازی اسناد و گزارش‌ها
**هدف:** در بالای صفحات اسناد/ریزگردش/گزارش‌ها:
- انتخاب ارز: دلار / ریال / … / **همه ارزها**
- فقط وقتی MC=ON
- در حالت یک ارز مشخص: فقط اسناد همان ارز
- در حالت همه: جمع نهایی با تسعیر به پایه **و** نمایش مقدار اصلی کنارش
مثال: `۱۶۰,۰۰۰,۰۰۰ ریال (100 $)`
#### کارها
- [ ] فیلتر ارز در `documents_page` و لیست‌های مشابه
- [ ] یکدست کردن گزارش‌های بدهکار/بستانکار با گزینه «همه»
- [ ] فرمتر مشترک `formatAmountWithOriginalCurrency(...)`
- [ ] داشبورد: الگوی `totals_by_currency` موجود گسترش داده شود
#### معیار پذیرش
- تک‌ارزی: بدون dropdown ارز.
- چندارزی: فیلتر کار می‌کند؛ در «همه» شفافیت ارز اصلی سند حفظ می‌شود.
---
## ۶. ماتریس وابستگی فازها
```text
فاز ۰ (Gate)
└─► فاز ۱ (نرخ روز دستی + toolbar)
├─► فاز ۱ب (API خودکار) [موازی نسبی]
├─► فاز ۲ (جمع دوگانه فاکتور)
├─► فاز ۳ (پرداخت چندارزی) [نیاز به نرخ + تصمیم حسابداری]
├─► فاز ۴ (انتقال بین‌ارزی)
├─► فاز ۵ (مانده شخص per ارز)
├─► فاز ۶ (قیمت کالا) [وابسته به نرخ روز]
└─► فاز ۷ (گزارش‌ها/اسناد) [می‌تواند موازی بعد از ۰/۱]
```
پیشنهاد ترتیب انتشار: **۰ → ۱ → ۲ → ۳ → ۵ → ۴ → ۶ → ۷** و ۱ب هر زمان پس از ۱.
---
## ۷. قراردادهای کراس‌کاتینگ
### ۷.۱ Localization
- همه رشته‌های جدید در `app_fa.arb` / `app_en.arb`
- هیچ متن سخت‌کد فارسی در ویجت‌های جدید
### ۷.۲ Permissions
- نرخ‌ها: همان `currency_revaluation` (view/add/edit/delete)
- پرداخت چندارزی: مجوزهای موجود فاکتور/دریافت‌پرداخت
- تنظیمات provider: `settings.business`
### ۷.۳ گرد کردن
- احترام به `currencies.decimal_places` و `round_monetary_amounts`
- تلورانس اختلاف `amount` vs `settles × rate` در حد ۱ واحد کوچک‌ترین اعشار
### ۷.۴ تست
هر فاز حداقل:
- تست API برای کسب‌وکار تک‌ارزی (باید رد/مخفی شود)
- تست API برای چندارزی (مسیر خوشحال)
- یک golden/widget test برای Gate در Flutter در صورت امکان
### ۷.۵ لاگ و پشتیبانی
- در payment/transfer چندارزی: log شامل rate و currency ids
- در UI خطاهای `PAYMENT_CURRENCY_MISMATCH` پیام فارسی راهنما وقتی کاربر نرخ وارد نکرده
---
## ۸. تصمیم‌های باز محصول (باید قبل از فاز مربوطه بسته شوند)
| # | موضوع | فاز | گزینه‌ها |
|---|--------|-----|----------|
| D1 | تفاوت نرخ پرداخت vs نرخ فاکتور به‌عنوان سود/زیان تسعیر حسابداری؟ | ۳ | ثبت حسابداری / فقط نمایش |
| D2 | واحد نرخ پیش‌فرض UI: ریال یا تومان؟ | ۱،۳ | IRR خالص / نمایش تومان با ×۱۰ |
| D3 | قیمت کالا: فیلد روی Product یا فقط PriceList؟ | ۶ | A / B |
| D4 | ارز سند انتقال بین‌ارزی؟ | ۴ | ارز پایه / ارز مبدأ / بدون ارز واحد + extra |
| D5 | Provider اول API: کدام سرویس؟ | ۱ب | مثقال / سایر |
| D6 | آیا حذف آخرین ارز فرعی (برگشت به تک‌ارزی) اسناد قدیمی ارزی را قفل کند؟ | ۰ | بله توصیه می‌شود اگر سند ارزی وجود دارد |
---
## ۹. چک‌لیست «تک‌ارزی چیزی نمی‌بیند»
قبل از merge هر PR چندارزی:
- [ ] `isMultiCurrency == false` در emulator/session تست شده
- [ ] toolbar بدون chip نرخ
- [ ] فاکتور بدون fx field و بدون dual total
- [ ] پرداخت بدون فیلد نرخ/ارز دوم
- [ ] انتقال بدون نرخ تبدیل
- [ ] شخص بدون کارت per-currency
- [ ] گزارش/اسناد بدون فیلتر ارز
- [ ] منوی تنظیمات بدون آیتم‌های فقط-MC (یا با توضیح غیرفعال)
- [ ] APIهای جدید برای business تک‌ارزی رفتار امن دارند
---
## ۱۰. نقشه فایل‌های کلیدی (مرجع سریع)
### Backend
- `hesabixAPI/adapters/db/models/currency.py`
- `hesabixAPI/adapters/db/models/business_currency_rate.py`
- `hesabixAPI/app/services/business_currency_rate_service.py`
- `hesabixAPI/app/services/invoice_fx_revaluation.py`
- `hesabixAPI/app/services/invoice_service.py` (پرداخت و validation ارز)
- `hesabixAPI/app/services/transfer_service.py`
- `hesabixAPI/app/services/person_service.py`
- `hesabixAPI/adapters/api/v1/business_currency_rates.py`
### Frontend
- `hesabixUI/hesabix_ui/lib/core/auth_store.dart` (افزودن flag)
- shell کسب‌وکار (toolbar)
- `widgets/invoice/invoice_fx_rate_field.dart`
- `widgets/invoice/invoice_transactions_widget.dart`
- `widgets/transfer/transfer_form_dialog.dart`
- `pages/business/currency_revaluation_page.dart`
- `pages/business/persons_page.dart` / جزئیات شخص
- `widgets/product/sections/product_pricing_inventory_section.dart`
---
## ۱۱. نحوه پیشبرد کار روی این سند
1. فاز ۰ را تمام و merge کنید.
2. برای هر فاز بعدی: یک issue/PR با عنوان `MC-Phase-N: ...` و لینک به بخش همان فاز در این فایل.
3. تصمیم‌های باز (بخش ۸) را قبل از شروع فاز در همین سند به‌روز کنید (وضعیت: تصمیم گرفته شد — تاریخ — انتخاب).
4. پس از اتمام هر فاز، چک‌باکس‌های همان بخش را تیک بزنید و در انتهای بخش یک خط «انجام‌شده در: commit/PR» اضافه کنید.
5. تا وقتی فاز ۰ و قانون Gate پایدار نشده، فازهای بعدی شروع نشوند.
---
## ۱۲. خارج از محدوده این برنامه (فعلاً)
- چند واحد پول همزمان به‌عنوان «ارز پایه چندتایی» (فقط یک base)
- ارزهای رمزنگاری / نرخ لحظه‌ای معاملات
- تسعیر اجباری پایان دوره فراتر از منطق موجود year-end
- تغییر رفتار مالیاتی سامانه مودیان برای فاکتور غیرریالی (قوانین جدا)
---
## ۱۳. وضعیت فاز ۰ (به‌روز ۲۰۲۶-۰۷-۱۸)
سناریوی اجرایی: [MULTI_CURRENCY_PHASE0_SCENARIO.md](./MULTI_CURRENCY_PHASE0_SCENARIO.md)
پیاده‌سازی‌شده:
- `is_multi_currency` روی پاسخ کسب‌وکار + Gate UI (منوی تسعیر و سیاست FX فقط برای MC)
- ادمین: ارائه‌دهندگان نرخ (`/admin/fx-providers`) با کلید رمزنگاری‌شده
- اسنپ‌شات مرکزی `fx_global_rates` + worker دوره‌ای (بازه پیش‌فرض ۱۵ دقیقه)
- BRS API (`brsapi.ir`) به‌عنوان provider اول
- ثبت سریع تسعیر کسب‌وکار از اسنپ‌شات (`apply-from-global`)
کلید API را در ریپو commit نکنید؛ از UI ادمین یا `HESABIX_FX_BRSAPI_API_KEY` تنظیم کنید.