178 lines
15 KiB
Markdown
178 lines
15 KiB
Markdown
# Pre-launch — the hard gate before real users touch this
|
|
|
|
> Last verified: 2026-08-02 against commit `cd8144e`. Populated by phase 5 of the
|
|
> [documentation clean-up chain](../_plan/README.md).
|
|
|
|
Everything below must be true before a real user with real money uses the platform. This is a gate, not a
|
|
backlog — items here are not ranked by convenience, they're ranked by what "real user, real money" requires.
|
|
Four things put you here: **committed credentials**, **mocked money/identity rails**, **Development running in
|
|
production**, and **legal/tax code that isn't finished**. A fifth — **every Phase 4 blocker** — is included
|
|
because "blocker" means *the product is wrong or unusable*, and that bar applies with or without real money.
|
|
|
|
---
|
|
|
|
## 1. Rotate the committed credentials
|
|
|
|
The repo contains live credentials **by deliberate pre-launch decision**
|
|
([root CLAUDE.md §6](../../CLAUDE.md), [BL-003](../status/backlog.md#blockers-18)): DB `sa`, both
|
|
JWE/field-encryption key halves, Kavenegar, Neshan, Finnotech, and the Telegram bot token. All of them must be
|
|
rotated and the secret half moved out of git before onboarding real users.
|
|
|
|
| Credential | Rotation is | What it takes |
|
|
| --- | --- | --- |
|
|
| DB `sa` password | a straight rotation | Change on the SQL Server, update `appsettings.Production.json`. No data migration. |
|
|
| Kavenegar / Neshan / Finnotech API keys | a straight rotation | Issue new keys with each vendor, update config. No data migration. |
|
|
| Telegram bot token | a straight rotation | Regenerate via BotFather, update `docker-compose.yml` (`otp-relay.environment`) and the appsettings copy — the two must match ([DEPLOY.md](../../DEPLOY.md)). |
|
|
| `IdentitySettings:SecretKey` / `Encryptkey` (JWE) | a straight rotation | Safe to rotate any time — the only side effect is signing everyone out. |
|
|
| **`Seams:FieldEncryption:Key` / `:HashKey`** | **not** a rotation — see below | **Never** change without the migration in the next section. |
|
|
|
|
### The one item that is a project, not a config edit
|
|
|
|
`Seams:FieldEncryption:Key` decrypts every encrypted column (phone numbers, addresses, IBANs, clinical
|
|
notes); `:HashKey` derives `users.PhoneHash`, which every login looks up. Changing either value in place makes
|
|
all existing encrypted data unreadable and locks every account out — this is stated as a permanent invariant
|
|
in [DEPLOY.md](../../DEPLOY.md) and [decisions.md](../status/decisions.md), and it is correct **as long as the
|
|
key never needs to change**. It will need to change eventually (security incident, key-management policy,
|
|
routine hygiene), and at that point it is its own migration project:
|
|
|
|
1. Decrypt every affected column with the old key, in place, inside a maintenance window or behind a
|
|
dual-key read path.
|
|
2. Re-encrypt with the new key.
|
|
3. Recompute `PhoneHash` for every user with the new `HashKey`, since it's a one-way derivation — there is no
|
|
way to "re-key" a hash without the plaintext.
|
|
4. Decide the cutover strategy: a maintenance-window rewrite (simplest, requires downtime sized to the data
|
|
volume) versus a dual-read migration (no downtime, more code to write and then delete).
|
|
|
|
**Rough size: large.** Nothing about this is hard, but nothing about it is a config edit either — track it as
|
|
its own piece of work with its own testing pass, not a line item inside a generic "rotate credentials" task.
|
|
|
|
---
|
|
|
|
## 2. Still-mocked production seams
|
|
|
|
Exactly one `Seams:*:Provider` is set anywhere in the repo — `Seams:Sms:Provider = telegram`, and the Telegram
|
|
relay is [documented as the pre-launch demo rail](../../CLAUDE.md), not a production SMS gateway. Every other
|
|
rail runs on its mock, in both development and the deployed stack
|
|
([docs/flows/index.md](../flows/index.md#mock-vs-real-map--server-seams)):
|
|
|
|
| Rail | Mock today | Real adapter | What flipping takes |
|
|
| --- | --- | --- | --- |
|
|
| SMS / OTP | `LoggingSmsSender` (real path is `telegram`, a demo relay) | `KavenegarSmsSender` — already coded | Set `Seams:Sms:Provider = kavenegar` + real API key/sender. Attempting `smsir`/`ghasedak` throws at startup by design — those adapters don't exist ([BL-235](../status/backlog.md#deferred-43)). |
|
|
| Card PSP | `MockPaymentProvider` (redirects to a non-existent host — [BL-005](../status/backlog.md#blockers-18)) | `ZarinPalPaymentProvider` — already coded | Real merchant credentials from a licensed PSP — which requires e-namad (§5) — plus a reachable webhook endpoint. **The selector is a plain string check, not an enum** — any non-`mock` value silently selects ZarinPal, so a typo in this setting picks a real gateway by accident. |
|
|
| Settlement split (تسهیم) | `MockSettlementSplitProvider` | `ProviderSettlementSplitProvider` — already coded | Real settlement-provider credentials. |
|
|
| BNPL | `MockBnplProvider` (no gateway row even seeded — [BL-007](../status/backlog.md#blockers-18)) | `SnappPayBnplProvider` / `DigipayBnplProvider` — already coded | Real provider credentials, a seeded `Bnpl` gateway row, and a webhook receiver reachable from the provider (nothing fires it in dev today — [BL-131](../status/backlog.md#minor-115)). |
|
|
| Object storage | `LocalDiskObjectStorage` (`GetUrl` returns a `file://` URI no browser can fetch — [BL-024](../status/backlog.md#major-86)) | `S3ObjectStorage` — already coded | A real bucket + credentials. Flipping this also happens to fix BL-024, since S3's `GetUrl` returns an actual HTTP(S) URL. |
|
|
| Geocoder | `MockGeocoder` (server) / keyless grid stand-in (client, `NEXT_PUBLIC_NESHAN_KEY` unset — [BL-025](../status/backlog.md#major-86)) | `NeshanGeocoder` — already coded | A real Neshan API key on **both** sides — the client key is separate from any server-side one. |
|
|
| **Bank transfer (payouts)** | `MockBankTransferProvider` | **none — moves no money** | Nothing to flip. This is the one rail with no real adapter written at all; even after every other rail above goes real, **payouts still cannot move real money** until a real bank-transfer integration is built from scratch. Size this as new work, not a config change. |
|
|
| e-invoicing (مودیان) | `MockMoadianClient` | `MoadianClient` — already coded | Token-exchange refresh and the Moadian signing certificate are not wired ([BL-236](../status/backlog.md#deferred-43)) — both are deploy-time actions once real credentials exist, not code changes. |
|
|
| Shahkar · e-KYC · IBAN ownership | mocks | Finnotech adapters — already coded | Real Finnotech credentials. |
|
|
| Credential (MoH/INO) · eNamad · review moderation | mock, always | **none — deliberate** | These stay manual/mocked by product decision, not a launch gap — see [deferred.md](deferred.md). |
|
|
| Search | *(no mock)* | `SqlNurseSearch` | Already real. Not a launch item. |
|
|
|
|
**Net: of 10 rails with a real adapter, 9 are a credentials-and-config flip; 1 (bank transfer) doesn't have a
|
|
real adapter yet.** Two rails throw at startup rather than silently falling back to the mock if misconfigured
|
|
(SMS providers other than Kavenegar, and any `Search:Backend` other than `sql`) — that fail-fast behavior is
|
|
intentional and should stay.
|
|
|
|
---
|
|
|
|
## 3. Running as Development in production
|
|
|
|
[DEPLOY.md](../../DEPLOY.md) documents this as a **deliberate trade** for a demo deployment among trusted
|
|
people. Every consequence below stops being acceptable the moment strangers can reach the site:
|
|
|
|
| Consequence | Why it matters | Fix |
|
|
| --- | --- | --- |
|
|
| The developer exception page is public | Any unhandled 500 on `api.balinyaar.ir` returns a stack trace and configuration detail to the caller | Switch to `Production` environment |
|
|
| `GET /api/v1/dev/last_otp/{phone}` is live and anonymous ([BL-004](../status/backlog.md#blockers-18)) | Anyone who knows a registered phone number can read its login code and sign in as that user — **the single biggest exposure today** | Same — the endpoint is Development-gated in code; it disappears once the environment flips |
|
|
| Swagger served at `/swagger` | Full API surface exposed | Same |
|
|
| Seeders re-run on every boot; migrations auto-apply on boot | Safe today (idempotent), but not how a production release process should work | Run migrations as a one-shot command instead (`docker compose run --rm api dotnet Baya.Web.Api.dll migrate`) |
|
|
| gRPC reflection enabled; the demo `bookings/convert` payment-capture simulator is wired | Extra attack surface + a fake-payment code path reachable in a real deployment | Same — disappears with the environment flip |
|
|
|
|
**What must change**, per [DEPLOY.md "Going to Production"](../../DEPLOY.md#going-to-production-later):
|
|
|
|
1. Set `ASPNETCORE_ENVIRONMENT: Production` in `docker-compose.yml`.
|
|
2. Create `appsettings.Production.json` (it does not exist yet) with real `IdentitySettings:SecretKey` /
|
|
`Encryptkey` — `StartupSecretsGuard` rejects any value containing `not-for-production` outside Development,
|
|
so today's dev keys refuse to boot in Production by design. Keep `Seams:FieldEncryption` byte-identical to
|
|
the Development file (§1).
|
|
3. Run migrations as a one-shot instead of on boot.
|
|
4. Swap the OTP rail to `kavenegar` (§2) — the Telegram relay broadcasting every code to a fixed recipient
|
|
list stops being acceptable once someone outside that list can request a code.
|
|
|
|
---
|
|
|
|
## 4. Every Phase 4 blocker
|
|
|
|
"Blocker" means *the product is wrong or unusable* — that bar holds regardless of whether money is involved.
|
|
All 18 are pre-launch gate items; full detail and code traces are in
|
|
[backlog.md](../status/backlog.md#blockers-18).
|
|
|
|
| ID | One line | Rough effort |
|
|
| --- | --- | --- |
|
|
| BL-001 | Admin RBAC grants only the literal role `admin`; every seeded admin 403s everywhere | M — see [next-up.md](next-up.md) unit 3 |
|
|
| BL-002 | No seeded account holds the literal `admin` role, so even a fixed BL-001 is untestable out of the box | S — bundled with BL-001 |
|
|
| BL-003 | Committed live credentials, unrotated | See §1 |
|
|
| BL-004 | Dev-only OTP-read endpoint live on the production domain | See §3 |
|
|
| BL-005 | Card payment is a dead end everywhere — mock redirects to a non-existent host, no local webhook | M — see [next-up.md](next-up.md) unit 2 |
|
|
| BL-006 | Booking deadlines ship without a timezone; a 30-min window can render as ~4h and expire silently | S — see [next-up.md](next-up.md) unit 2 |
|
|
| BL-007 | No `bnpl` gateway row ever seeded; every BNPL call 400s | S — see [next-up.md](next-up.md) unit 4 |
|
|
| BL-008 | BNPL wizard's mock cross-imports the bookings-mock store; dead end for every real booking id | M — see [next-up.md](next-up.md) unit 4 |
|
|
| BL-009 | Refunds mock reads a retired store and would show a 10000%-scale refund on flip | M — see [next-up.md](next-up.md) unit 4 |
|
|
| BL-010 | Verification is 100% client-mocked; a server-verified nurse renders unverified everywhere | L — see [next-up.md](next-up.md) unit 1 |
|
|
| BL-011 | Patient/care records are 100% mocked; real DTO shapes would break on a naive flip | M |
|
|
| BL-012 | Nurse payouts: mock hides 4 working endpoints; no UI for the irreversible "process batch" step; "paid" status computed wrong | M — see [next-up.md](next-up.md) unit 5 |
|
|
| BL-013 | Partner center 100% mocked, zero real tenancy; 5 core routes don't exist server-side | L |
|
|
| BL-014 | Search results are index rows, not de-duplicated nurses; trust dossier mocked; unverified nurse profile page asserts "verified" | M |
|
|
| BL-015 | Editing an address silently nulls recipient name/phone/postal code on every save | S |
|
|
| BL-016 | A booking swept to `missed` never reaches a payable state | M |
|
|
| BL-017 | Reviews can never leave moderation on the live stack (sits behind BL-001; `AutoApproveClean` unset) | S once BL-001 lands |
|
|
| BL-018 | Zero option groups outside Development — every builder collapses to two steps, deployed | M |
|
|
|
|
---
|
|
|
|
## 5. Legal / tax items that are code-side
|
|
|
|
From [product/business/13-tax-invoicing-and-legal.md](../../product/business/13-tax-invoicing-and-legal.md),
|
|
confirmed the platform's weakest business area in [implemented.md](../status/implemented.md):
|
|
|
|
- **Terms & Privacy still ship placeholder legal copy behind a draft banner** — flagged for human/legal review
|
|
since ui-phase-3 and still unreviewed ([BL-097](../status/backlog.md#major-86)). Swapping in
|
|
counsel-reviewed copy (and removing the banner) is a pre-launch item, not a nice-to-have.
|
|
- **مودیان (e-invoice) integration is mocked past the point the business doc calls MVP.** The `invoices` model,
|
|
VAT split, and reference fields exist correctly (§ commission/VAT model below), but the Moadian
|
|
token-exchange refresh and signing certificate are unwired ([BL-236](../status/backlog.md#deferred-43)), and
|
|
seeded partner-center invoices carry null Moadian reference/PDF fields
|
|
([BL-200](../status/backlog.md#major-86)). These are deploy-time actions once real Moadian credentials
|
|
exist — but they don't exist yet, and مودیان readiness is explicitly named as MVP scope in the business doc.
|
|
- **The commission/VAT split itself is already correct and does not need code work**: platform commission is
|
|
15% of gross, VAT is 10% of the *commission only* — this became the single source of truth in
|
|
refinement-phase-3 ([decisions.md](../status/decisions.md)) and matches the business doc's nurse-as-
|
|
taxable-seller / platform-as-commission-seller model. The one open defect is a **display** bug, not a model
|
|
bug: the invoice screen computes a client-side row that's wrong by exactly the VAT amount instead of
|
|
rendering the server's real total ([BL-060](../status/backlog.md#major-86) — folded into
|
|
[next-up.md](next-up.md) unit 2).
|
|
- **`partner_centers` as merchant-of-record is the business doc's launch-critical legal vehicle** ("the fast,
|
|
legal go-to-market is to partner with already-licensed centers") — and it is 100% mocked with zero real
|
|
tenancy today ([BL-013](../status/backlog.md#blockers-18)). Until this is real, there is no functioning
|
|
legal invoice-issuer path for a booking routed through a partner center.
|
|
- **e-namad is a business/licensing prerequisite, not a code task, but it gates one**: per
|
|
[legal-landscape.md](../../product/research/legal-landscape.md), a monetized Iranian site needs e-namad to
|
|
obtain an online payment gateway at all — meaning the real ZarinPal flip in §2 cannot complete until the
|
|
launch entity holds e-namad, independent of anything in this codebase.
|
|
- **VAT-exempt-or-0% is a live legal question, not yet a code gap**: the business doc notes medical services'
|
|
own VAT treatment is unconfirmed in Iran and asks for the rate to stay config-driven so it can land either
|
|
way. Confirm with an Iranian tax advisor before launch, then confirm the rate is read from a
|
|
`platform_configs` row (the repo's config-is-rows convention) rather than a hardcoded constant — this
|
|
wasn't independently re-verified this phase and is worth a direct check before relying on it.
|
|
|
|
---
|
|
|
|
## Not in this file
|
|
|
|
Everything here is a **gate**, not a roadmap. What comes after the gate — the highest-leverage next units of
|
|
work, what's deliberately deferred and why, and what technical debt is accruing — is
|
|
[next-up.md](next-up.md), [deferred.md](deferred.md), and [tech-debt.md](tech-debt.md) respectively. Several
|
|
pre-launch items and next-up items overlap on purpose (e.g. BL-006, BL-060): the same fix both closes a
|
|
backlog item worth doing regardless of launch timing, and clears a launch gate.
|