diff --git a/.env.example b/.env.example
index f98e1c3..5ae98f3 100644
--- a/.env.example
+++ b/.env.example
@@ -38,6 +38,20 @@ JWT_CUSTOMER_SECRET=
# Customer token lifetime (default 30d).
JWT_CUSTOMER_EXPIRES_IN=
+# ---------------------------------------------------------------------------
+# Accounting software integration (Tahesab / Zarrin, D27). Everything else —
+# which software, address, token, mappings — is set in the admin panel
+# («پیکر بندی ← اتصال حسابداری»), and the integration is off until then.
+# ---------------------------------------------------------------------------
+# Encrypts the accounting token at rest. Set a long random value. Blank falls
+# back to a key derived from JWT_SECRET (with a boot warning); changing that
+# key later means re-entering the token in the admin panel.
+ACCOUNTING_SECRET_KEY=
+# "off" keeps this API instance out of the background sync (only needed when
+# several instances run and you want exactly one doing it; a lease already
+# stops two from running the same job).
+ACCOUNTING_WORKER=
+
# ---------------------------------------------------------------------------
# Central IAM — delegated admin login (docs/iam-sso.md)
# ---------------------------------------------------------------------------
@@ -88,12 +102,21 @@ AUTH_SESSION_TRACKING=
OTP_MASTER_CODE=
# ---------------------------------------------------------------------------
-# SMS gateway (Trez). Leave blank to disable sending: the server still boots,
-# messages are logged instead of sent, and customers cannot receive a login
-# code (D20). Never commit real values.
+# SMS gateway. The default "relay" driver works with everything blank (D26).
+# A failed SMS never stops the server or an operation (D20).
# ---------------------------------------------------------------------------
-# "trez" (default) or "none" to disable explicitly.
+# "relay" (default), "trez", or "none" to disable explicitly.
SMS_PROVIDER=
+# relay — the owner's createsms gateway. All four are optional.
+# URL and secret have built-in defaults.
+SMS_RELAY_URL=
+SMS_RELAY_SECRET=
+# The "host" field. Blank sends this machine's first external IPv4 — inside
+# Docker that is the container's address.
+SMS_RELAY_HOST=
+# "kave" (default, Kavenegar) or "default" to let the gateway pick the line.
+SMS_RELAY_PLATFORM=
+# trez — only used with SMS_PROVIDER=trez. Never commit real values.
# Override only for staging; defaults to the provider's live endpoint.
SMS_TREZ_URL=
SMS_TREZ_USERNAME=
diff --git a/DELIVERY.md b/DELIVERY.md
index d063d65..397b40d 100644
--- a/DELIVERY.md
+++ b/DELIVERY.md
@@ -39,10 +39,9 @@ Everything else is optional and may be left blank. Two you may want now:
# real release (see "Before the real release" below).
OTP_MASTER_CODE=1379
-# Leave blank and SMS is logged, not sent. The server still starts.
-SMS_TREZ_USERNAME=
-SMS_TREZ_PASSWORD=
-SMS_TREZ_SENDER_LINE=
+# SMS goes through the "relay" gateway with nothing to set (D26).
+# SMS_PROVIDER=none turns sending off; the server still starts either way.
+SMS_PROVIDER=
```
### Step 2 — build
@@ -115,7 +114,9 @@ before it is a production release. The first three are blocking.
| **1** | **Set `JWT_CUSTOMER_SECRET` to its own random value**, different from `JWT_SECRET`. | Not a cosmetic default. Unset, the API signs customer and sales-expert **app** tokens with `JWT_SECRET` — *the same key admin tokens use* — and the two token realms stop being cryptographically separate. The only remaining barrier is the `typ` check in `jwt.strategy.ts`, and a sales expert's app token carries their **AdminUser id** in `sub`. Two independent defences collapse into one. A loud boot banner fires while it is missing. **REL-03 (Critical).** |
| **2** | **Remove `OTP_MASTER_CODE` from the environment.** | While set, **any active customer signs in with that one fixed code**. Unset or empty disables it completely; the API prints a banner at boot for as long as it is armed. **REL-01 (Critical).** |
| **3** | **Set real `JWT_SECRET`.** | Without it the API falls back to a well-known development string, so anyone can forge an admin token. |
-| **4** | **Configure the SMS credentials** (`SMS_TREZ_USERNAME`, `SMS_TREZ_PASSWORD`, `SMS_TREZ_SENDER_LINE`). | Until then **no customer can receive a real login code** — messages are written to the log instead of sent. The server still boots (D20). **REL-02.** |
+| **4** | **Confirm SMS is going out.** The default `relay` gateway needs no credentials (D26); check `docker compose logs api` for `SMS provider: relay` and send yourself a login code. | If the gateway refuses the `host` value (inside Docker it is the container's IP), set `SMS_RELAY_HOST`. A failed send is only a log line — the server still boots (D20). **REL-02.** |
+
+| **5** | **Accounting integration (D27), only if it will be switched on:** set `ACCOUNTING_SECRET_KEY`; count products with no image (they disappear from the app — query in ISSUES ACC-05) and photograph them first; for Zarrin, run the trial on temporary documents first (ACC-01). | The integration itself is off until an admin turns it on in «اتصال حسابداری»; nothing changes before that except the no-image rule, which applies to every product. |
After changing any of these: `docker compose up -d --build` and confirm the
`OTP_MASTER_CODE` banner is **gone** from `docker compose logs api`.
@@ -189,14 +190,21 @@ container.
| `ADMIN_PANEL_PORT`, `USER_APP_PORT`, `USER_APP_V2_PORT` | 5180–5182 | Dev servers only. |
| `RUN_MIGRATIONS` | `true` | Set `false` to skip the boot-time migration run. |
| `RUN_SEED` | `true` | Set `false` to skip seeding. |
+| `ACCOUNTING_SECRET_KEY` | *(derived from `JWT_SECRET`)* | Encrypts the accounting-software token the admin enters (D27). Set its own long random value; changing it later means re-entering the token. |
+| `ACCOUNTING_WORKER` | `on` | `off` keeps this instance out of the accounting background sync. |
| `PROFILE_ADMIN_MOBILES` | *(blank)* | Comma-separated admin mobiles told when a customer completes their profile. The reference hardcoded four numbers; blank means nobody is notified and the upload still succeeds. |
-### SMS — all optional; blank means "log, do not send"
+### SMS — all optional
-`SMS_PROVIDER` (`trez` default, or `none`), `SMS_TREZ_URL`,
-`SMS_TREZ_USERNAME`, `SMS_TREZ_PASSWORD`, `SMS_TREZ_SENDER_LINE`.
+`SMS_PROVIDER`: `relay` (default), `trez`, or `none` (D26).
-With these unset the server **still starts normally** and logs a warning; a
+- **relay**: `SMS_RELAY_URL`, `SMS_RELAY_SECRET` (both have built-in
+ defaults), `SMS_RELAY_HOST` (blank = the machine's first external IPv4),
+ `SMS_RELAY_PLATFORM` (`kave` default, or `default` to let the gateway pick).
+- **trez**: `SMS_TREZ_URL`, `SMS_TREZ_USERNAME`, `SMS_TREZ_PASSWORD`,
+ `SMS_TREZ_SENDER_LINE`.
+
+The server **always starts**; a misconfigured provider is a warning, and a
failed or skipped SMS never blocks an operation, including customer creation.
### ⚠ `OTP_MASTER_CODE` — delivery only
@@ -405,4 +413,6 @@ Recorded in `docs/parity/DECISIONS.md`:
the lowest-risk option, change nothing protected.
- **D20** — SMS can never block a boot or an operation; `OTP_MASTER_CODE` for
the delivery.
+- **D26** — SMS goes through the owner's `relay` gateway by default; Trez is
+ kept as a selectable driver.
- **D11 is cancelled** — superseded by D13.
diff --git a/apps/admin-panel/src/components/accounting/AccountingConnection.vue b/apps/admin-panel/src/components/accounting/AccountingConnection.vue
new file mode 100644
index 0000000..6b7cc03
--- /dev/null
+++ b/apps/admin-panel/src/components/accounting/AccountingConnection.vue
@@ -0,0 +1,743 @@
+
+
+
+
+
+
+
+
+ {{ status.title }}
+ تغییرات ذخیره نشده
+
+
{{ status.text }}
+
+
+
{{ row.label }}
+
{{ row.value }}
+
+
+
+
+
+
+
+
۱نرمافزار حسابداری
+
+
+
+
+
+
+
+
+
+
۲اطلاعات اتصال به {{ LABEL[p] }}
+
+
+
+
آدرسی که وبسرویس {{ LABEL[p] }} روی آن در دسترس است، با پورت.
+
+
+
+
+
نام دقیق دیتابیس همین شرکت. یک سرور ممکن است چند دیتابیس داشته باشد؛ بعد از تست، نام را در نتیجه چک کنید.
+
+
+
+ منفی یعنی بدهکار
+ مثبت یعنی بدهکار
+
+
در دورهی آزمایشی با یک حساب نمونه بررسی شود.
+
+
+
+
+
+ توکن ذخیره شده است و نمایش داده نمیشود.
+ توکن ذخیرهشده قابلخواندن نیست؛ دوباره وارد کنید.
+
+ تغییر توکن
+
+ حذف
+
+
+