forked from hesabix/arc
301 lines
13 KiB
Markdown
301 lines
13 KiB
Markdown
# سناریو: زمانبندی خودکار ثبت نرخ تسعیر کسبوکار + آفست (درصد / مبلغ)
|
||
|
||
**تاریخ:** ۲۰۲۶-۰۷-۱۸
|
||
**وضعیت:** پیادهسازیشده (MVP)
|
||
**وابستگیها:** فاز ۰ (اسنپشات مرکزی) + فاز ۱ (نوار ابزار / apply دستی)
|
||
**جایگاه در نقشه راه:** جایگزین عملی «فاز ۱ب» قدیمی؛ دیگر هر کسبوکار به API خارجی وصل نمیشود.
|
||
|
||
---
|
||
|
||
## ۱. مسئله کاربر
|
||
|
||
کاربر میخواهد در **تنظیمات کسبوکار** بتواند:
|
||
|
||
1. ثبت نرخ در «تسعیر ارز» (`business_currency_rates`) را **خودکار و زمانبندیشده** کند.
|
||
2. برای هر ارز فرعی، نرخ نهایی را نسبت به نرخ مرجع **بالاتر یا پایینتر** تنظیم کند:
|
||
- با **درصد** (مثلاً +۲٪ یا −۱٫۵٪)، یا
|
||
- با **مبلغ ثابت** (مثلاً +۵۰٬۰۰۰ ریال روی هر واحد دلار).
|
||
|
||
هدف: بدون دخالت روزانه، نرخهای کسبوکار بهروز بماند و حاشیهٔ تجاری/احتیاطی هم اعمال شود.
|
||
|
||
---
|
||
|
||
## ۲. اصل معماری (خیلی مهم)
|
||
|
||
| لایه | نقش |
|
||
|------|-----|
|
||
| **ادمین کل** (فاز ۰) | واکشی متمرکز از BRS/مثقال → `fx_global_rates` |
|
||
| **کسبوکار** (این سناریو) | فقط از اسنپشات مرکزی میخواند و با آفست خودش در `business_currency_rates` مینویسد |
|
||
|
||
```text
|
||
[Provider API] --۱۵دقیقه--> [fx_global_rates]
|
||
│
|
||
▼ (زمانبندی کسبوکار)
|
||
[آفست per ارز]
|
||
│
|
||
▼
|
||
[business_currency_rates]
|
||
(نرخ تسعیر اسناد)
|
||
```
|
||
|
||
**نباید** برای هر کسبوکار درخواست جدا به brsapi زده شود (مسدود شدن کلید).
|
||
|
||
**فقط وقتی `is_multi_currency = true`** این تنظیمات دیده و اجرا میشود.
|
||
|
||
---
|
||
|
||
## ۳. تجربه کاربری (تنظیمات)
|
||
|
||
### ۳.۱ محل ورود
|
||
مسیر پیشنهادی (کنار سیاست تسعیر فاکتور):
|
||
|
||
`تنظیمات کسبوکار → ارزها / تسعیر → «بهروزرسانی خودکار نرخ»`
|
||
|
||
یا کارت جدا در دسته مالی:
|
||
- عنوان: «زمانبندی نرخ تسعیر»
|
||
- فقط اگر چندارزی فعال باشد؛ وگرنه مخفی یا غیرفعال با توضیح.
|
||
|
||
### ۳.۲ بلوک اصلی تنظیمات
|
||
|
||
| فیلد | توضیح |
|
||
|------|--------|
|
||
| فعالسازی | سوئیچ On/Off |
|
||
| منبع نرخ | فعلاً فقط «اسنپشات مرکزی سیستم» (ثابت) |
|
||
| حالت زمانبندی | یکی از گزینههای زیر |
|
||
| ارزهای مشمول | همه فرعیها / انتخابی |
|
||
| جلوگیری از تکرار | اگر نرخ محاسبهشده با آخرین نرخ ثبتشده تفاوت معناداری نداشت، ننویسد (تلورانس) |
|
||
|
||
### ۳.۳ حالتهای زمانبندی (پیشنهاد)
|
||
|
||
**الف) بازهای ساده (MVP)**
|
||
- هر N ساعت (۱ / ۲ / ۳ / ۶ / ۱۲ / ۲۴)
|
||
- همتراز با سادگی فعلی worker سیستم
|
||
|
||
**ب) ساعات ثابت روزانه**
|
||
- لیست ساعات مثل `09:00, 13:00, 18:00` به timezone کسبوکار (`display_timezone`)
|
||
- مناسب کسبوکارهایی که میخواهند قبل از شروع کار / ظهر / عصر نرخ قفل شود
|
||
|
||
**ج) Cron پیشرفته (فاز بعد از MVP — اختیاری)**
|
||
- عبارت cron کامل برای کاربران حرفهای
|
||
|
||
**توصیه MVP:** الف + ب. ج را عقب بیندازیم.
|
||
|
||
### ۳.۴ آفست per ارز
|
||
|
||
جدول برای هر ارز فرعی فعال:
|
||
|
||
| ستون | مثال |
|
||
|------|------|
|
||
| ارز | USD |
|
||
| نوع آفست | درصد / مبلغ ثابت / بدون آفست |
|
||
| جهت | بالاتر (+) / پایینتر (−) |
|
||
| مقدار | `2` برای ۲٪ یا `50000` برای ۵۰ هزار واحد پایه |
|
||
| فعال | ✓ |
|
||
|
||
**فرمول:**
|
||
|
||
```text
|
||
base_rate = نرخ اسنپشات (تبدیلشده به ارز پایه کسبوکار)
|
||
final_rate =
|
||
if none: base_rate
|
||
if percent: base_rate * (1 + sign * percent/100)
|
||
if amount: base_rate + sign * amount
|
||
```
|
||
|
||
`sign = +1` بالاتر، `−1` پایینتر.
|
||
|
||
نرخ نهایی باید `> 0` باشد؛ در غیر این صورت آن ارز در آن اجرا skip + خطا در لاگ.
|
||
|
||
**نمونه:**
|
||
اسنپشات: ۱ USD = 1,932,000 ریال
|
||
آفست: +۲٪ → ثبت: 1,970,640
|
||
آفست: −۵۰٬۰۰۰ ریال → ثبت: 1,882,000
|
||
|
||
### ۳.۵ پیشنمایش زنده در UI
|
||
|
||
قبل از ذخیره / کنار جدول:
|
||
|
||
- «نرخ مرجع الان: …»
|
||
- «نرخ پس از آفست: …»
|
||
- «آخرین اجرای موفق: …»
|
||
- «اجرای بعدی تقریبی: …»
|
||
- دکمه «اجرای آزمایشی اکنون» (فقط با مجوز add)
|
||
|
||
---
|
||
|
||
## ۴. مدل داده پیشنهادی
|
||
|
||
### ۴.۱ تنظیمات سطح کسبوکار
|
||
|
||
جدول `business_fx_auto_sync_settings` (یا JSON روی `businesses`):
|
||
|
||
| فیلد | نوع | توضیح |
|
||
|------|-----|--------|
|
||
| business_id | FK | |
|
||
| enabled | bool | |
|
||
| schedule_mode | enum | `interval` \| `daily_times` |
|
||
| interval_hours | int? | برای mode=interval |
|
||
| daily_times | json? | `["09:00","18:00"]` |
|
||
| timezone | str? | پیشفرض از `display_timezone` کسبوکار |
|
||
| skip_if_unchanged | bool | پیشفرض true |
|
||
| min_change_percent | numeric? | مثلاً ۰٫۰۱٪ — کمتر از این ننویس |
|
||
| last_run_at | datetime? | |
|
||
| last_run_status | str? | ok/error/partial |
|
||
| last_run_message | text? | |
|
||
| next_run_at | datetime? | برای UI و worker |
|
||
|
||
### ۴.۲ آفست per ارز
|
||
|
||
جدول `business_fx_auto_sync_currency_rules`:
|
||
|
||
| فیلد | نوع |
|
||
|------|-----|
|
||
| business_id | FK |
|
||
| currency_id | FK |
|
||
| enabled | bool |
|
||
| offset_type | `none` \| `percent` \| `amount` |
|
||
| offset_direction | `up` \| `down` |
|
||
| offset_value | Numeric |
|
||
| unique(business_id, currency_id) | |
|
||
|
||
اگر برای ارزی rule نباشد → `none` (ثبت عین اسنپشات).
|
||
|
||
### ۴.۳ خروجی هر اجرا
|
||
|
||
همان `business_currency_rates` موجود با:
|
||
|
||
```text
|
||
note = "خودکار | اسنپشات مرکزی | آفست +2% | ref=1932000 @ 2026-07-18T..."
|
||
created_by_user_id = کاربر سیستم / مالک کسبوکار (تصمیم باز D1)
|
||
```
|
||
|
||
---
|
||
|
||
## ۵. Worker / اجرا
|
||
|
||
### ۵.۱ حلقه پسزمینه
|
||
|
||
شبیه `fx_global_rates_fetch_loop`:
|
||
|
||
- هر ۱–۵ دقیقه کسبوکارهایی که `enabled` و `next_run_at <= now` را پیدا کند.
|
||
- برای هر کدام:
|
||
1. اسنپشات مرتبط با ارزهای فرعی را بخواند.
|
||
2. اگر اسنپشات خیلی قدیمی (مثلاً > ۲۴س) و سیاست `block_if_stale` → skip با status=error.
|
||
3. آفست را اعمال کند.
|
||
4. در صورت `skip_if_unchanged` با آخرین نرخ مقایسه کند.
|
||
5. ردیف(های) جدید در `business_currency_rates` بنویسد.
|
||
6. `last_run_*` و `next_run_at` را بهروز کند.
|
||
|
||
### ۵.۲ محدودیتها
|
||
|
||
- فقط کسبوکارهای MC
|
||
- حداکثر N کسبوکار در هر تیک (مثلاً ۵۰) تا فشار DB کنترل شود
|
||
- اجرا در `asyncio.to_thread` (مثل فاز ۰)
|
||
|
||
### ۵.۳ همپوشانی با فاز ۰
|
||
|
||
ترتیب زمانی ایدهآل:
|
||
|
||
```text
|
||
دقیقه ۰: واکشی مرکزی BRS
|
||
دقیقه ۱+: اعمال خودکار کسبوکارها روی همان اسنپشات
|
||
```
|
||
|
||
لازم نیست کسبوکار دقیقاً همزمان با fetch مرکزی باشد؛ کافی است اسنپشات تازه باشد.
|
||
|
||
---
|
||
|
||
## ۶. API پیشنهادی
|
||
|
||
| متد | مسیر | توضیح |
|
||
|-----|------|--------|
|
||
| GET | `/businesses/{id}/fx-auto-sync` | خواندن تنظیمات + rules + وضعیت آخرین اجرا |
|
||
| PUT | `/businesses/{id}/fx-auto-sync` | ذخیره تنظیمات و آفستها |
|
||
| POST | `/businesses/{id}/fx-auto-sync/run-now` | اجرای دستی فوری (تست) |
|
||
| GET | `/businesses/{id}/fx-auto-sync/preview` | پیشنمایش نرخ مرجع + نرخ نهایی بدون ذخیره |
|
||
|
||
مجوز: `currency_revaluation` (view/edit) یا `settings.business` — پیشنهاد:
|
||
- مشاهده: `currency_revaluation.view`
|
||
- ویرایش/اجرا: `currency_revaluation.add` + `settings.business`
|
||
|
||
---
|
||
|
||
## ۷. سناریوهای پذیرش
|
||
|
||
### S1 — تکارزی
|
||
تنظیمات دیده نمیشود؛ API → `MULTI_CURRENCY_REQUIRED`.
|
||
|
||
### S2 — فعالسازی بازهای بدون آفست
|
||
هر ۶ ساعت برای USD/EUR ردیف جدید با نرخ عین اسنپشات ثبت میشود؛ note شامل «خودکار».
|
||
|
||
### S3 — آفست درصدی
|
||
USD +۲٪؛ مقدار ثبتشده دقیقاً `ref * 1.02` (با گرد کردن طبق `decimal_places` ارز پایه).
|
||
|
||
### S4 — آفست مبلغی پایینتر
|
||
EUR −۱۰٬۰۰۰ ریال؛ نرخ منفی نشود.
|
||
|
||
### S5 — skip بدون تغییر
|
||
اگر نرخ نهایی با آخرین نرخ کمتر از تلورانس فرق داشت، ردیف جدید ساخته نشود؛ `last_run_status=ok` با پیام «بدون تغییر».
|
||
|
||
### S6 — اسنپشات خالی/کهنه
|
||
اجرا fail جزئی یا کامل با پیام واضح در تنظیمات؛ فاکتور از نرخ قبلی استفاده میکند.
|
||
|
||
### S7 — اجرای آزمایشی
|
||
دکمه Run now همان منطق را فوری اجرا میکند و در تاریخچه نرخ دیده میشود.
|
||
|
||
### S8 — خاموش کردن
|
||
با Off شدن، `next_run_at` خالی و worker دیگر این کسبوکار را برنمیدارد.
|
||
|
||
---
|
||
|
||
## ۸. تصمیمهای باز محصول
|
||
|
||
| # | موضوع | گزینهها | پیشنهاد |
|
||
|---|--------|----------|---------|
|
||
| D1 | `created_by_user_id` برای ردیف خودکار | کاربر سیستم / آخرین ویرایشکننده تنظیمات / مالک | آخرین کاربری که تنظیمات را ذخیره کرده |
|
||
| D2 | اگر اسنپشات برای یک ارز نبود | skip همان ارز / fail کل اجرا | skip همان ارز + status=partial |
|
||
| D3 | چند بار در روز با ساعات ثابت و timezone | اجباری به `display_timezone` | بله |
|
||
| D4 | آیا آفست روی فاکتور هم جداگانه باشد؟ | خیر — فقط روی نرخ تسعیر کسبوکار | فقط تسعیر |
|
||
| D5 | سقف آفست | مثلاً درصد ≤ ۵۰٪ | برای جلوگیری از اشتباه فاحش |
|
||
| D6 | نام فاز در نقشه راه | «فاز ۱٫۵ / Auto Sync» بهجای ۱ب قدیمی | بله |
|
||
|
||
---
|
||
|
||
## ۹. خارج از محدوده این سناریو
|
||
|
||
- اتصال مستقیم کسبوکار به API مثقال/BRS (کلید per business)
|
||
- تغییر خودکار قیمت کالا (فاز ۶ جداست؛ میتواند بعداً از همین نرخ تسعیر تغذیه شود)
|
||
- آفست متفاوت برای خرید در برابر فروش روی کالا
|
||
- تسعیر مجدد اسناد قدیمی با نرخ جدید
|
||
|
||
---
|
||
|
||
## ۱۰. تخمین فازبندی پیادهسازی
|
||
|
||
| مرحله | محتوا | وضعیت |
|
||
|-------|--------|--------|
|
||
| **A — MVP** | سوئیچ + interval ساعات + آفست percent/amount per ارز + worker + run-now + preview | ✅ |
|
||
| **B** | ساعات ثابت روزانه + timezone + skip_if_unchanged + هشدار stale | ✅ (همراه MVP) |
|
||
| **C** | لاگ اجراهای اخیر در UI + اعلان اختیاری «اجرای ناموفق» | ⏳ بعداً |
|
||
|
||
### API (خلاصه)
|
||
- `GET/PUT /api/v1/businesses/{id}/fx-auto-sync`
|
||
- `GET|POST /api/v1/businesses/{id}/fx-auto-sync/preview` (POST = پیشنویس بدون ذخیره)
|
||
- `POST /api/v1/businesses/{id}/fx-auto-sync/run-now`
|
||
|
||
### UI
|
||
- مسیر: `settings/fx-auto-sync`
|
||
- ورود از هاب تنظیمات مالی (فقط `is_multi_currency`)
|
||
|
||
---
|
||
|
||
## ۱۱. جمعبندی برای تأیید
|
||
|
||
این قابلیت **تنظیمات سطح کسبوکار** است، نه ادمین کل:
|
||
|
||
- منبع نرخ = اسنپشات مرکزی (فاز ۰)
|
||
- زمانبندی = بازه یا ساعات روز
|
||
- حاشیه = درصد یا مبلغ، بالاتر/پایینتر، per ارز
|
||
- خروجی = ردیفهای تاریخچه تسعیر که نوار ابزار و فاکتور همین الان از آنها استفاده میکنند
|
||
|
||
با تأیید تصمیمهای D1–D5 میتوان پیادهسازی مرحله A را شروع کرد.
|