# Payments (Zarinpal via shetabit/payment)

## Objectives
- Reliable, idempotent payment flow using Zarinpal.
- Accurate subscription activation and reconciliation.

## Flow & State Machine
- States (payments.status): `pending` → `ok` | `failed`
- States (subscriptions.status): `inactive` → `active` | `expired` | `canceled`

### Sequence
1) Create
- Input: `{ plan }` (plans.key)
- Process: lookup plan (amount_irr, duration_days), init gateway, get `authority`, insert `payments(pending)` with `ref=authority`.
- Output: `{ payment_id, gateway:"zarinpal", redirect_url, authority }`

2) Verify (callback)
- Input: `?authority=...`
- Idempotent lookup: payments where `ref=authority`.
- If already `ok` → return success (idempotent path).
- Else mark `ok`, then activate subscription:
  - If subscription exists for user → update; else insert new (starts_at=now, ends_at=now+duration_days).
- Emit event `UpgradeSuccess` {plan, amount}.

3) Reconcile (job)
- Close stale `pending` (e.g., `created_at < now()-24h`) as `failed`.

## Idempotency
- Key: `authority` (stored in `payments.ref`).
- DB: add index on `payments.ref` (migration) for O(1) lookup.
- Verify handler must be safe to retry without double-activation.

## Errors & Handling
- Gateway init/verify timeout: log; show friendly retry message to user.
- Invalid authority: return `{status:"failed"}`; no subscription change.
- Partial failures (subscription write): rollback with transaction; log.

## Security
- Never trust plan amount from client; always from DB.
- Log minimal info; avoid sensitive details.
- Consider webhook signature (if provided) or IP whitelist if available.

## Sample Payloads
- Create (request): `{ "plan": "basic" }`
- Create (response): `{ "payment_id": 123, "gateway": "zarinpal", "redirect_url": "https://.../start/123", "authority": "AUTH-..." }`
- Verify (success): `{ "status": "ok", "payment_id": 123, "ref": "AUTH-...", "subscription": {"status":"active"} }`
- Verify (failed): `{ "status": "failed", "payment_id": 0, "ref": "AUTH-...", "subscription": null }`

## Testing
- Double-callback: second verify returns success without re-activating subscription.
- Missing/invalid authority: failed response; no subscription mutation.
- Reconcile job: marks old pendings failed.

