Files
tehran_tala/docs/SMS-RELAY-PLAN.md
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

93 lines
8.9 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.
# طرح افزودن درگاه پیامکی جدید (Relay / `createsms`)
> وضعیت: **اجرا شد** (2026-10-05) — تصمیم ثبت‌شده: D26.
> تاریخ: 2026-10-05
## ۱. کد ورودی چه می‌کند
کد PHP ارسالی (Laravel) یک پیامک را به یک سرویس واسط (relay) می‌فرستد:
| مورد | مقدار |
|---|---|
| متد / آدرس | `POST http://185.231.112.101/api/createsms` (HTTP ساده، بدون TLS) |
| بدنه | JSON |
| هدر احراز هویت | `secret_key: <کلید>` |
| timeout | ۱۵ ثانیه |
| فیلد `phone` | شماره گیرنده |
| فیلد `text` | متن پیام، `trim()` شده |
| فیلد `host` | `Controller::Config('montek')` و اگر خالی بود IP خود سرور (`gethostbyname(gethostname())`) — یعنی شناسه‌ی «این مشتری/سرور» نزد سرویس واسط |
| فیلد `platform` | اگر `$sendFromKave` درست باشد `"kave"` (کاوه‌نگار)، وگرنه `null` (خط پیش‌فرض سرویس واسط) |
| `CURLOPT_IPRESOLVE = V4` | فقط اجبار IPv4؛ چون آدرس خودش IP است در عمل اثری ندارد |
این کد در `henza_server` وجود ندارد (جستجو شد)، پس از سیستم دیگری آمده و مرجع parity ندارد.
## ۲. کجا قرار می‌گیرد
ماژول پیامک همین الان در [apps/api/src/modules/sms/sms.service.ts](../apps/api/src/modules/sms/sms.service.ts) هست و همه‌ی مصرف‌کننده‌ها (OTP ورود، پیامک خوش‌آمد، اعلان تکمیل پروفایل، …) فقط `SmsService.send()` را صدا می‌زنند. پس:
- **هیچ مصرف‌کننده‌ای تغییر نمی‌کند.** فقط یک «درایور» دوم کنار Trez اضافه می‌شود و با `SMS_PROVIDER=relay` انتخاب می‌شود.
- قوانین فعلی حفظ می‌شود: `send()` هرگز throw نمی‌کند (D20)، هر پیام در `SmsMessage` ثبت می‌شود، متن OTP ذخیره نمی‌شود (SEC-11)، نبود تنظیمات فقط هشدار می‌دهد و سرور بالا می‌آید.
## ۳. تغییرات پیشنهادی
### ۳.۱ کد — `sms.service.ts`
1. `smsConfig()` متغیرهای جدید را هم می‌خواند (جدول ۳.۲).
2. `smsConfigured()` برای `relay`: کافی است `SMS_RELAY_SECRET` و `SMS_RELAY_HOST` پر باشند.
3. `assertSmsConfigured()` مقدار `relay` را می‌شناسد و متغیرهای خالی را با نام گزارش می‌کند (بدون throw).
4. متد جدید `deliverViaRelay(mobile, text, platform)`:
- `fetch` با `Content-Type: application/json` و هدر `secret_key`، بدنه‌ی `{ phone, host, text, platform }`، timeout ۱۵ ثانیه با `AbortController` — هم‌شکل `deliverViaTrez`.
- موفقیت = پاسخ HTTP 2xx (تا وقتی قالب پاسخ سرویس واسط مشخص شود — سؤال ۴).
5. `SmsSendOptions` یک گزینه‌ی اختیاری `platform?: 'kave'` می‌گیرد؛ درایور Trez آن را نادیده می‌گیرد.
6. اجبار IPv4 پیاده نمی‌شود چون آدرس IP است؛ اگر بعداً دامنه شد، با `dns.lookup` با `family: 4` اضافه می‌شود.
### ۳.۲ متغیرهای محیطی جدید
| متغیر | اجباری | توضیح |
|---|---|---|
| `SMS_PROVIDER` | — | مقدار جدید `relay` |
| `SMS_RELAY_URL` | خیر | پیش‌فرض `http://185.231.112.101/api/createsms` |
| `SMS_RELAY_SECRET` | **بله** | مقدار هدر `secret_key` — **فقط در env، هرگز در کد یا git** |
| `SMS_RELAY_HOST` | **بله** | مقدار فیلد `host` (معادل `Config('montek')`) |
| `SMS_RELAY_KAVE_FOR` | خیر | کدام پیام‌ها از کاوه‌نگار بروند — بسته به پاسخ سؤال ۲ |
### ۳.۳ مستندات و استقرار
- `.env.example` (ریشه)، `apps/api/.env.example`، `docker-compose.yml`، `DELIVERY.md` (بخش SMS و گام ۴): افزودن متغیرهای بالا.
- `docs/parity/DECISIONS.md`: ثبت یک تصمیم جدید (D26) برای درایور relay.
### ۳.۴ تست — `sms.service.spec.ts`
- تنظیمات: پیش‌فرض URL، `smsConfigured` با/بدون secret و host، هشدار با نام متغیرهای خالی.
- ارسال با `fetch` ماک‌شده: آدرس، هدر `secret_key`، بدنه‌ی JSON، `trim` متن، `platform` درست؛ و این‌که خطای شبکه/timeout/HTTP 500 به `{ ok: false }` تبدیل شود و throw نشود.
## ۴. هشدار امنیتی ⚠
1. **کلید `secret_key` لو رفته فرض شود.** در متن پیام چت آمده و احتمالاً در git پروژه‌ی PHP هم هست (مشابه SEC-12). باید از صاحب سرویس واسط کلید جدید گرفت و فقط در env سرور گذاشت. این کلید در کد این مخزن نوشته نمی‌شود.
2. **ارتباط HTTP ساده است**؛ کلید و متن پیام (از جمله کد OTP) رمزنشده از اینترنت عبور می‌کنند. اگر سرویس واسط HTTPS دارد از آن استفاده شود.
## ۵. سؤال‌ها (با پیشنهاد)
1. **جایگزین یا کنار Trez؟** پیشنهاد: کنار Trez بماند و با `SMS_PROVIDER=relay` انتخاب شود؛ پیش‌فرض همان `trez` بماند تا رفتار فعلی عوض نشود. یا پیش‌فرض `relay` شود؟
2. **`sendFromKave` کی درست است؟** در کد اصلی این متغیر از کجا مقدار می‌گیرد؟ پیشنهاد: فقط OTP ورود از کاوه‌نگار برود (معمولاً برای سرعت/تحویل بهتر) و بقیه از خط پیش‌فرض — قابل تنظیم با env.
3. **مقدار `host` (`montek`) چیست؟** پیشنهاد: اجباری در env. fallback به IP خود سرور پیاده نمی‌شود، چون داخل Docker، IP کانتینر (مثلاً `172.x`) برمی‌گردد که برای سرویس واسط بی‌معناست.
4. **پاسخ سرویس واسط چه شکلی است؟** اگر در خطا هم 200 برمی‌گرداند (مثلاً `{"status": false}`)، نمونه‌ی پاسخ موفق و ناموفق لازم است تا `ok` درست تشخیص داده شود. پیشنهاد فعلی: فقط وضعیت HTTP.
5. **HTTPS دارد؟** (بند ۴.۲)
## ۷. پاسخ‌های مالک (2026-10-05)
| سؤال | پاسخ | اثر روی پیاده‌سازی |
|---|---|---|
| کلید | لو نرفته؛ تأیید دومرحله‌ای جای دیگری هست؛ داخل پروژه قرار بگیرد | کلید به‌عنوان مقدار پیش‌فرض در کد می‌آید و `SMS_RELAY_SECRET` فقط آن را بازنویسی می‌کند. بند ۴.۱ لغو شد. |
| ۱. جایگزین Trez؟ | پیش‌فرض `relay` شود؛ Trez حذف نشود؛ ممکن است بعداً سرویس‌های بیشتری بیاید | سرویس به ساختار «درایور» بازنویسی می‌شود: یک اینترفیس `SmsDriver` و هر سرویس یک فایل جدا (`drivers/relay.driver.ts`، `drivers/trez.driver.ts`). برای اضافه کردن سرویس بعدی فقط یک فایل و یک خط ثبت لازم است. پیش‌فرض `SMS_PROVIDER` برابر `relay` می‌شود. |
| ۳. `host` | IP سرور درست است | `SMS_RELAY_HOST` اختیاری است. اگر خالی باشد، مثل PHP اولین IPv4 غیرداخلی سرور فرستاده می‌شود. |
| ۴. پاسخ | با تشخیص خودم | `ok` فقط وقتی است که HTTP 2xx باشد و بدنه‌ی JSON هم نشانه‌ی شکست صریح نداشته باشد (`status: false`، `success: false`، `error`). در غیر این صورت `ok: false` + لاگ؛ هیچ‌وقت throw نمی‌شود. |
| ۵. HTTPS | دارد | بررسی شد: روی `https://185.231.112.101` گواهی `*.montpar.ir` نصب است که **از 2026-08-22 منقضی شده** و روی IP هم معتبر نیست ← سؤال ب |
### سؤال‌های باز
- **الف) `sendFromKave`:** کدام پیام‌ها از کاوه‌نگار بروند؟ پیشنهاد: فقط OTP ورود.
- **ب) آدرس HTTPS:** نام دامنه‌ی سرویس (زیرمجموعه‌ای از `montpar.ir`) چیست؟ گواهی هم باید تمدید شود. تا آن موقع پیش‌فرض همان `http://185.231.112.101/api/createsms` می‌ماند و با `SMS_RELAY_URL` عوض می‌شود.
## ۶. وضعیت اجرا
- [x] تأیید طرح و پاسخ سؤال‌ها (الف: همه‌ی پیام‌ها از کاوه‌نگار؛ ب: همان IP و HTTP درست است)
- [x] کد + تست (`apps/api/src/modules/sms/drivers/`، ۱۳۵ تست سبز)
- [x] env / compose / DELIVERY / DECISIONS (D26) / ISSUES (REL-02)