diff --git a/docker-compose.yml b/docker-compose.yml
index c0c9344..388b718 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -554,6 +554,8 @@ services:
BEAUTY_BUSINESS_SERVICE_URL: ${BEAUTY_BUSINESS_SERVICE_URL:-http://beauty-business-service:8011}
NEXT_PUBLIC_HOSPITALITY_API_URL: ${NEXT_PUBLIC_HOSPITALITY_API_URL:-http://localhost:8009}
HOSPITALITY_SERVICE_URL: ${HOSPITALITY_SERVICE_URL:-http://hospitality-service:8009}
+ NEXT_PUBLIC_PAYMENT_API_URL: ${NEXT_PUBLIC_PAYMENT_API_URL:-http://localhost:8012}
+ PAYMENT_SERVICE_URL: ${PAYMENT_SERVICE_URL:-http://payment-service:8012}
# polling برای hot-reload پایدار روی volume mount ویندوز
WATCHPACK_POLLING: "true"
CHOKIDAR_USEPOLLING: "true"
diff --git a/docs/next-steps.md b/docs/next-steps.md
index c1f81a9..7e97928 100644
--- a/docs/next-steps.md
+++ b/docs/next-steps.md
@@ -4,9 +4,9 @@
## Payment Platform — MVP complete (Torbat Pay)
-Phases **payment-14.0 through payment-14.5** are implemented (v**0.14.5.0**) per [phase-handover/phase-14-5.md](phase-handover/phase-14-5.md) and [service-snapshots/payment.yaml](service-snapshots/payment.yaml).
+Phases **payment-14.0 through payment-14.5** backend (v**0.14.5.0**) per [phase-handover/phase-14-5.md](phase-handover/phase-14-5.md). **Payment frontend (payment-frontend)** complete per [phase-handover/phase-payment-frontend.md](phase-handover/phase-payment-frontend.md) and [service-snapshots/payment-frontend.yaml](service-snapshots/payment-frontend.yaml).
-**Next implementation phase:** `payment-14.6` Refunds & Reversals (**LATER** priority — requires Communication integration).
+**Next implementation phase:** `payment-14.6` Refunds & Reversals (**LATER** priority — requires Communication integration). No 14.6+ payment UI until backend phase lands.
Remaining documented phases: 14.6–14.10 (refunds, splits, connectors, reconciliation, enterprise validation).
@@ -28,7 +28,7 @@ Pick one track per team capacity (see [roadmap.md](roadmap.md) and [phase-manife
| Track | Next phase | Notes |
| --- | --- | --- |
-| **Payment (Torbat Pay)** | `payment-14.6` Refunds & Reversals | MVP 14.0–14.5 complete; LATER priority |
+| **Payment (Torbat Pay)** | `payment-14.6` Refunds & Reversals | Backend MVP 14.0–14.5 + frontend complete; LATER priority |
| Delivery | `delivery-10.2` Fleet & Vehicle Types | After Driver Management 10.1 |
| Loyalty | `loyalty-7.7` Gift Card Platform | After Wallet 7.6 |
| Hospitality | `hospitality-12.9` AI Assistant | After Analytics 12.8 |
diff --git a/docs/phase-handover/phase-payment-frontend.md b/docs/phase-handover/phase-payment-frontend.md
new file mode 100644
index 0000000..0674c9b
--- /dev/null
+++ b/docs/phase-handover/phase-payment-frontend.md
@@ -0,0 +1,46 @@
+# Phase Handover — Payment Frontend (payment-frontend)
+
+**Completed:** 2026-07-27
+**Service:** Torbat Pay — Enterprise Payment Platform
+**Backend version at handover:** 0.14.5.0 (phases 14.0–14.5)
+
+## Summary
+
+Full MVP admin portal for Torbat Pay delivered under `frontend/modules/payment/` and `frontend/app/payment/**`. Persian labels, real API wiring via BFF to port 8012, recharts on dashboard/KPIs.
+
+## Deliverables
+
+- 26 feature modules under `modules/payment/features/`
+- Thin Next.js app routes with loading/error boundaries
+- BFF `app/api/payment/[...path]/route.ts` (Idempotency-Key forwarded)
+- `scripts/validate-payment-routes.mjs` + `npm run validate:payment`
+- Apps catalog entry: Torbat Pay → `/payment/hub`
+- Docker env: `NEXT_PUBLIC_PAYMENT_API_URL`, `PAYMENT_SERVICE_URL`
+- Snapshots/docs: `payment-frontend.yaml`, architecture + validation reports
+
+## Checkout session mapping
+
+`PaymentRequest` detail page (`/payment/requests/[id]`) is the checkout session view — cancel, verify, redirect URL display.
+
+## Stop boundary
+
+**Do not implement 14.6+ UI** (refunds, splits, subscriptions) until backend phases complete.
+
+## Next backend phase
+
+`payment-14.6` Refunds & Reversals — frontend remains list/read for MVP until explicitly scoped.
+
+## Verification
+
+```bash
+cd frontend
+npm run validate:payment
+npm run typecheck
+```
+
+## References
+
+- [payment-frontend-architecture.md](../../frontend/docs/payment-frontend-architecture.md)
+- [payment-frontend-validation-report.md](../../frontend/docs/payment-frontend-validation-report.md)
+- [service-snapshots/payment-frontend.yaml](../service-snapshots/payment-frontend.yaml)
+- [service-snapshots/payment.yaml](../service-snapshots/payment.yaml)
diff --git a/docs/progress.md b/docs/progress.md
index a9a3b6d..92e0e04 100644
--- a/docs/progress.md
+++ b/docs/progress.md
@@ -1075,6 +1075,21 @@ Remaining (honest): dispatch/fleet/tracking/maps live/realtime/settlement screen
---
+## Phase Payment Frontend — Torbat Pay Admin Portal ✅
+
+> `frontend/modules/payment/` + `frontend/app/payment/**` — MVP UI for phases 14.0–14.5 only.
+
+- [x] 26 feature screens (hub, dashboard, KPIs, transactions, ledger, PSP, merchants, requests, audit, bundles, toggles, capabilities, settings, metrics, monitoring, API explorer, OpenAPI)
+- [x] BFF `/api/payment` → port 8012; Idempotency-Key forwarded
+- [x] Checkout session = `PaymentRequest` detail (`/payment/requests/[id]`)
+- [x] Apps catalog: Torbat Pay → `/payment/hub`
+- [x] Validation: `npm run validate:payment`
+- [x] Handover: [phase-handover/phase-payment-frontend.md](phase-handover/phase-payment-frontend.md)
+- [x] Snapshot: [service-snapshots/payment-frontend.yaml](service-snapshots/payment-frontend.yaml)
+- [x] **14.6+ UI not started** (refunds, splits — by design)
+
+---
+
## Related Documents
- [Next Steps](next-steps.md)
diff --git a/docs/project-status.md b/docs/project-status.md
new file mode 100644
index 0000000..5acb908
--- /dev/null
+++ b/docs/project-status.md
@@ -0,0 +1,122 @@
+# Project Status — TorbatYar Single Execution Dashboard (v2)
+
+**Authoritative machine file:** [`project-status.yaml`](project-status.yaml)
+
+This is the **first document** every AI implementation run must read. When it exists, agents must **not** full-scan the repository or inspect unrelated services.
+
+| Field | Value |
+| --- | --- |
+| Schema | **2** |
+| Project version | `0.14.5-platform` |
+| Last updated | 2026-07-27 |
+| Last completed phase | `platform-project-status-v2` |
+
+**Deprecated:** `next_recommended_phase` → replaced by `execution_priority` + `critical_path` + `deployment_readiness`.
+
+---
+
+## Resume rules (every execution)
+
+1. Read `docs/project-status.yaml` **FIRST**.
+2. Never inspect unrelated services.
+3. Never scan the repository for completed work.
+4. Determine remaining work **exclusively** from this file.
+5. Load only documents required by [Runtime Read Policy](ai-framework/runtime-read-policy.md) for the active service.
+6. Execute higher `execution_priority` before lower.
+7. Follow `critical_path` order for commercial-release sequencing.
+
+---
+
+## Critical path (first commercial release)
+
+Ordered remaining milestones:
+
+1. **Payment Backend MVP** — `payment-14.6` … `payment-14.10`
+2. **Payment Frontend**
+3. **Experience Frontend** — Publish Registry `experience-fe-11.6+`
+4. **Restaurant Production** — `hospitality-12.9` … `12.10`
+5. **Driver Production** — `delivery-10.2` … `10.10`
+6. **Medical Booking**
+7. **Booking**
+8. **Short Links**
+9. **QR**
+10. **Digital Card**
+
+---
+
+## Execution priority
+
+| Bucket | Phases |
+| --- | --- |
+| **critical** | `payment-14.6`–`14.10`, `payment-frontend`, `hospitality-12.9`–`12.10` |
+| **high** | `delivery-10.2`–`10.10`, `experience-fe-11.6`, `communication-frontend-complete` |
+| **medium** | `loyalty-7.7`–`7.10`, `sports-center-9.8`–`9.10`, `accounting-5.12` |
+| **low** | Identity / CRM / Healthcare / Beauty frontend polish |
+| **future** | Ecommerce, Marketplace, Automation, Academy, Clinic, Hotel, Tourism, AI, SMS Panel, Short Links, QR, Digital Card, Booking, Notification, File Storage, Live Chat, Messenger |
+
+---
+
+## Global statistics
+
+| Metric | Value |
+| --- | --- |
+| Number of services | 27 |
+| Completed services | 6 |
+| In progress | 7 |
+| Planned / scaffolded | 14 |
+| Backend completion | ~48% |
+| Frontend completion | ~22% |
+| Overall completion | ~35% |
+| Critical path remaining | 10 |
+
+---
+
+## Deployment readiness
+
+Official indicator per service:
+
+```yaml
+deployment_readiness:
+ backend: NOT_STARTED | IN_PROGRESS | COMPLETE
+ frontend: NOT_STARTED | IN_PROGRESS | COMPLETE
+ database: NOT_STARTED | IN_PROGRESS | COMPLETE
+ api: NOT_STARTED | IN_PROGRESS | COMPLETE
+ tests: NOT_STARTED | IN_PROGRESS | COMPLETE
+ documentation: NOT_STARTED | IN_PROGRESS | COMPLETE
+ production_ready: NOT_STARTED | IN_PROGRESS | COMPLETE
+```
+
+---
+
+## Service completion (canonical)
+
+| Service | Completed | Remaining | BE% | FE% | Overall% | Prod ready |
+| --- | --- | --- | --- | --- | --- | --- |
+| Core | onboarding-4 | — | 100 | 100 | 100 | COMPLETE |
+| Identity | identity-2 | FE polish | 100 | 70 | 85 | COMPLETE |
+| Accounting | 5.11 | 5.12 | 92 | 40 | 66 | IN_PROGRESS |
+| CRM | 6.3 | FE polish | 100 | 55 | 78 | COMPLETE |
+| Loyalty | 7.6 | 7.7–7.10 | 60 | 0 | 30 | IN_PROGRESS |
+| Communication | 8 | FE complete | 100 | 65 | 82 | COMPLETE |
+| Sports Center | 9.7 | 9.8–9.10 | 70 | 0 | 35 | IN_PROGRESS |
+| Delivery | 10.1 | 10.2–10.10 | 18 | 25 | 22 | IN_PROGRESS |
+| Experience | FE-11.5 | FE-11.6+ | 100 | 83 | 92 | IN_PROGRESS |
+| Hospitality | 12.8 | 12.9–12.10 | 80 | 0 | 40 | IN_PROGRESS |
+| Healthcare | 13-7 | FE polish | 100 | 55 | 78 | COMPLETE |
+| Beauty | 14-7 | FE polish | 100 | 55 | 78 | COMPLETE |
+| Payment | 14.5 | 14.6–14.10 + FE | 50 | 0 | 25 | IN_PROGRESS |
+| Future products | — | foundations | 0–5 | 0 | 0–2 | NOT_STARTED |
+
+## Dependency graph
+
+Use `depends_on` / `required_by` on each service row — do not derive by scanning code.
+
+## Automatic updates
+
+After every Complete phase, update: completed/remaining phases, deployment readiness, percentages, critical path, execution priorities, service completion, timestamps. No manual editing when following the Documentation Update stage.
+
+## Framework refs
+
+[Runtime Read Policy](ai-framework/runtime-read-policy.md) · [Context Cache](ai-framework/context-cache-policy.md) · [Service Snapshot](ai-framework/service-snapshot-policy.md) · [Development Loop](ai-framework/development-loop.md) · [Master Prompt](ai-framework/master-prompt.md)
+
+Handover: [phase-af-project-status-v2.md](phase-handover/phase-af-project-status-v2.md)
diff --git a/docs/project-status.yaml b/docs/project-status.yaml
new file mode 100644
index 0000000..93e4e5c
--- /dev/null
+++ b/docs/project-status.yaml
@@ -0,0 +1,1200 @@
+# Project Status Registry v2 — single execution dashboard for all future AI runs.
+# FIRST document every implementation phase reads.
+# Normative: docs/ai-framework/runtime-read-policy.md, master-prompt.md, development-loop.md
+# Update AUTOMATICALLY after every completed phase. Do NOT full-project-scan when this file exists.
+# Human-readable companion: docs/project-status.md
+#
+# DEPRECATED: next_recommended_phase — replaced by execution_priority + critical_path + deployment_readiness.
+
+schema_version: 2
+project_version: "0.14.5-platform"
+last_updated: "2026-07-27"
+last_completed_phase: payment-frontend
+
+# --- Resume Rules (mandatory for every AI execution) ---
+resume_rules:
+ - Read docs/project-status.yaml FIRST.
+ - Never inspect unrelated services.
+ - Never scan the repository for completed work.
+ - Determine remaining work exclusively from this file.
+ - Only load additional documents required by Runtime Read Policy for the active service.
+ - Execute higher execution_priority work before lower priority.
+ - Follow critical_path order for commercial-release milestones.
+
+# --- Automatic Update Rules (after every Complete phase) ---
+automatic_update_rules:
+ - completed_phase / last_completed_phase
+ - remaining_phases per service
+ - deployment_readiness per service
+ - backend_percent / frontend_percent / overall_percent per service
+ - global_statistics
+ - critical_path (remove completed milestones; reorder if needed)
+ - execution_priority (move completed phases out of all buckets)
+ - service completion summary fields
+ - depends_on / required_by if registration changes
+ - last_updated timestamp
+ - project-status.md companion
+ - No manual editing required when agent follows Documentation Update stage
+
+authority:
+ role: single_execution_dashboard
+ read_first: true
+ replaces_full_project_scan: true
+ replaces_next_recommended_phase: true
+ companions:
+ - docs/project-status.md
+ - docs/ai-framework/project-index.yaml
+ - docs/ai-framework/phase-manifest.yaml
+ - docs/ai-framework/service-manifest.yaml
+
+# --- Critical Path (ordered milestones to first commercial release) ---
+# Replaces next_recommended_phase as the commercial-release sequencer.
+critical_path:
+ - id: payment-backend-mvp
+ label: Payment Backend MVP (14.6–14.10)
+ phases: [payment-14.6, payment-14.7, payment-14.8, payment-14.9, payment-14.10]
+ status: remaining
+ - id: payment-frontend
+ label: Payment Frontend
+ phases: [payment-frontend]
+ status: complete
+ - id: experience-frontend-registry
+ label: Experience Frontend (Publish Registry FE-11.6+)
+ phases: [experience-fe-11.6]
+ status: remaining
+ - id: hospitality-production
+ label: Restaurant Production (Hospitality 12.9–12.10)
+ phases: [hospitality-12.9, hospitality-12.10]
+ status: remaining
+ - id: delivery-production
+ label: Driver Production (Delivery 10.2–10.10)
+ phases: [delivery-10.2, delivery-10.3, delivery-10.4, delivery-10.5, delivery-10.6, delivery-10.7, delivery-10.8, delivery-10.9, delivery-10.10]
+ status: remaining
+ - id: medical-booking
+ label: Medical Booking
+ phases: [clinic-foundation, healthcare-booking]
+ status: remaining
+ - id: booking-platform
+ label: Booking
+ phases: [booking-foundation]
+ status: remaining
+ - id: short-links
+ label: Short Links
+ phases: [link-shortener-foundation]
+ status: remaining
+ - id: qr
+ label: QR
+ phases: [qr-foundation]
+ status: remaining
+ - id: digital-card
+ label: Digital Card
+ phases: [digital-card-foundation]
+ status: remaining
+
+# --- Execution Priority (every remaining phase in exactly one bucket) ---
+# AI MUST execute critical → high → medium → low → future.
+execution_priority:
+ critical:
+ - payment-14.6
+ - payment-14.7
+ - payment-14.8
+ - payment-14.9
+ - payment-14.10
+ - hospitality-12.9
+ - hospitality-12.10
+ high:
+ - delivery-10.2
+ - delivery-10.3
+ - delivery-10.4
+ - delivery-10.5
+ - delivery-10.6
+ - delivery-10.7
+ - delivery-10.8
+ - delivery-10.9
+ - delivery-10.10
+ - experience-fe-11.6
+ - communication-frontend-complete
+ medium:
+ - loyalty-7.7
+ - loyalty-7.8
+ - loyalty-7.9
+ - loyalty-7.10
+ - sports-center-9.8
+ - sports-center-9.9
+ - sports-center-9.10
+ - accounting-5.12
+ low:
+ - identity-frontend-polish
+ - crm-frontend-polish
+ - healthcare-frontend-polish
+ - beauty-frontend-polish
+ future:
+ - ecommerce-foundation
+ - marketplace-foundation
+ - automation-foundation
+ - academy-foundation
+ - clinic-foundation
+ - hotel-foundation
+ - tourism-foundation
+ - ai-assistant-foundation
+ - sms-panel-future
+ - link-shortener-foundation
+ - qr-foundation
+ - digital-card-foundation
+ - booking-foundation
+ - healthcare-booking
+ - notification-foundation
+ - file-storage-foundation
+ - live-chat-foundation
+ - smart-messenger-foundation
+ - future-services
+
+services:
+ - name: Core Platform
+ service_identifier: core-platform
+ commercial_product: TorbatYar Platform
+ backend_status: complete
+ frontend_status: n/a
+ latest_backend_phase: onboarding-4
+ latest_frontend_phase: null
+ roadmap_last_phase: onboarding-4
+ architecture_complete: true
+ backend_complete: true
+ frontend_complete: true
+ production_ready: true
+ snapshot: docs/service-snapshots/core-platform.yaml
+ latest_handover: null
+ remaining_phases: []
+ blockers: []
+ depends_on: []
+ required_by:
+ - identity-access
+ - accounting
+ - crm
+ - loyalty
+ - communication
+ - sports_center
+ - delivery
+ - experience
+ - hospitality
+ - healthcare
+ - beauty_business
+ - payment
+ - ecommerce
+ - marketplace
+ - automation
+ - academy
+ - clinic
+ - hotel
+ - tourism
+ - ai_assistant
+ - link_shortener
+ - file_storage
+ completed_phase: onboarding-4
+ remaining_phase: null
+ backend_percent: 100
+ frontend_percent: 100
+ overall_percent: 100
+ deployment_readiness:
+ backend: COMPLETE
+ frontend: COMPLETE
+ database: COMPLETE
+ api: COMPLETE
+ tests: COMPLETE
+ documentation: COMPLETE
+ production_ready: COMPLETE
+
+ - name: Identity & Access
+ service_identifier: identity-access
+ commercial_product: Torbat Identity
+ backend_status: complete
+ frontend_status: partial
+ latest_backend_phase: identity-2
+ latest_frontend_phase: onboarding-4
+ roadmap_last_phase: identity-2
+ architecture_complete: true
+ backend_complete: true
+ frontend_complete: false
+ production_ready: true
+ snapshot: docs/service-snapshots/identity-access.yaml
+ latest_handover: null
+ remaining_phases:
+ - identity-frontend-polish
+ blockers: []
+ depends_on:
+ - core-platform
+ required_by:
+ - accounting
+ - crm
+ - loyalty
+ - communication
+ - sports_center
+ - delivery
+ - experience
+ - hospitality
+ - healthcare
+ - beauty_business
+ - payment
+ completed_phase: identity-2
+ remaining_phase: identity-frontend-polish
+ backend_percent: 100
+ frontend_percent: 70
+ overall_percent: 85
+ deployment_readiness:
+ backend: COMPLETE
+ frontend: IN_PROGRESS
+ database: COMPLETE
+ api: COMPLETE
+ tests: COMPLETE
+ documentation: COMPLETE
+ production_ready: COMPLETE
+
+ - name: Accounting
+ service_identifier: accounting
+ commercial_product: Torbat Accounting
+ backend_status: in_progress
+ frontend_status: partial
+ latest_backend_phase: accounting-5.11
+ latest_frontend_phase: null
+ roadmap_last_phase: accounting-5.12
+ architecture_complete: true
+ backend_complete: false
+ frontend_complete: false
+ production_ready: false
+ snapshot: docs/service-snapshots/accounting.yaml
+ latest_handover: null
+ remaining_phases:
+ - accounting-5.12
+ blockers: []
+ depends_on:
+ - core-platform
+ - identity-access
+ required_by: []
+ completed_phase: accounting-5.11
+ remaining_phase: accounting-5.12
+ backend_percent: 92
+ frontend_percent: 40
+ overall_percent: 66
+ deployment_readiness:
+ backend: IN_PROGRESS
+ frontend: IN_PROGRESS
+ database: COMPLETE
+ api: COMPLETE
+ tests: COMPLETE
+ documentation: COMPLETE
+ production_ready: IN_PROGRESS
+
+ - name: CRM
+ service_identifier: crm
+ commercial_product: Torbat CRM
+ backend_status: complete
+ frontend_status: partial
+ latest_backend_phase: crm-6.3
+ latest_frontend_phase: null
+ roadmap_last_phase: crm-6.3
+ architecture_complete: true
+ backend_complete: true
+ frontend_complete: false
+ production_ready: true
+ snapshot: docs/service-snapshots/crm.yaml
+ latest_handover: null
+ remaining_phases:
+ - crm-frontend-polish
+ blockers: []
+ depends_on:
+ - core-platform
+ - identity-access
+ required_by: []
+ completed_phase: crm-6.3
+ remaining_phase: crm-frontend-polish
+ backend_percent: 100
+ frontend_percent: 55
+ overall_percent: 78
+ deployment_readiness:
+ backend: COMPLETE
+ frontend: IN_PROGRESS
+ database: COMPLETE
+ api: COMPLETE
+ tests: COMPLETE
+ documentation: COMPLETE
+ production_ready: COMPLETE
+
+ - name: Enterprise Loyalty Platform
+ service_identifier: loyalty
+ commercial_product: Torbat Loyalty
+ backend_status: in_progress
+ frontend_status: not_started
+ latest_backend_phase: loyalty-7.6
+ latest_frontend_phase: null
+ roadmap_last_phase: loyalty-7.10
+ architecture_complete: true
+ backend_complete: false
+ frontend_complete: false
+ production_ready: false
+ snapshot: docs/service-snapshots/loyalty.yaml
+ latest_handover: docs/phase-handover/phase-7-6.md
+ remaining_phases:
+ - loyalty-7.7
+ - loyalty-7.8
+ - loyalty-7.9
+ - loyalty-7.10
+ blockers: []
+ depends_on:
+ - core-platform
+ - identity-access
+ required_by: []
+ completed_phase: loyalty-7.6
+ remaining_phase: loyalty-7.7
+ backend_percent: 60
+ frontend_percent: 0
+ overall_percent: 30
+ deployment_readiness:
+ backend: IN_PROGRESS
+ frontend: NOT_STARTED
+ database: COMPLETE
+ api: COMPLETE
+ tests: COMPLETE
+ documentation: COMPLETE
+ production_ready: IN_PROGRESS
+
+ - name: Enterprise Communication Platform
+ service_identifier: communication
+ commercial_product: Torbat Communication
+ backend_status: complete
+ frontend_status: in_progress
+ latest_backend_phase: communication-8
+ latest_frontend_phase: null
+ roadmap_last_phase: communication-8
+ architecture_complete: true
+ backend_complete: true
+ frontend_complete: false
+ production_ready: true
+ snapshot: docs/service-snapshots/communication.yaml
+ latest_handover: docs/phase-handover/phase-8.md
+ remaining_phases:
+ - communication-frontend-complete
+ blockers:
+ - sms-panel-future deferred as separate product
+ depends_on:
+ - core-platform
+ - identity-access
+ required_by:
+ - sms_panel
+ - notification
+ - live_chat
+ - smart_messenger
+ completed_phase: communication-8
+ remaining_phase: communication-frontend-complete
+ backend_percent: 100
+ frontend_percent: 65
+ overall_percent: 82
+ deployment_readiness:
+ backend: COMPLETE
+ frontend: IN_PROGRESS
+ database: COMPLETE
+ api: COMPLETE
+ tests: COMPLETE
+ documentation: COMPLETE
+ production_ready: COMPLETE
+
+ - name: Sports Center Platform
+ service_identifier: sports_center
+ commercial_product: Sports Center
+ backend_status: in_progress
+ frontend_status: not_started
+ latest_backend_phase: sports-center-9.7
+ latest_frontend_phase: null
+ roadmap_last_phase: sports-center-9.10
+ architecture_complete: true
+ backend_complete: false
+ frontend_complete: false
+ production_ready: false
+ snapshot: docs/service-snapshots/sports-center.yaml
+ latest_handover: docs/phase-handover/phase-9-7.md
+ remaining_phases:
+ - sports-center-9.8
+ - sports-center-9.9
+ - sports-center-9.10
+ blockers: []
+ depends_on:
+ - core-platform
+ - identity-access
+ required_by: []
+ completed_phase: sports-center-9.7
+ remaining_phase: sports-center-9.8
+ backend_percent: 70
+ frontend_percent: 0
+ overall_percent: 35
+ deployment_readiness:
+ backend: IN_PROGRESS
+ frontend: NOT_STARTED
+ database: COMPLETE
+ api: COMPLETE
+ tests: COMPLETE
+ documentation: COMPLETE
+ production_ready: IN_PROGRESS
+
+ - name: Delivery & Fleet Platform
+ service_identifier: delivery
+ commercial_product: Torbat Driver
+ backend_status: in_progress
+ frontend_status: in_progress
+ latest_backend_phase: delivery-10.1
+ latest_frontend_phase: null
+ roadmap_last_phase: delivery-10.10
+ architecture_complete: true
+ backend_complete: false
+ frontend_complete: false
+ production_ready: false
+ snapshot: docs/service-snapshots/delivery.yaml
+ latest_handover: docs/phase-handover/phase-10-1.md
+ remaining_phases:
+ - delivery-10.2
+ - delivery-10.3
+ - delivery-10.4
+ - delivery-10.5
+ - delivery-10.6
+ - delivery-10.7
+ - delivery-10.8
+ - delivery-10.9
+ - delivery-10.10
+ blockers: []
+ depends_on:
+ - core-platform
+ - identity-access
+ required_by: []
+ completed_phase: delivery-10.1
+ remaining_phase: delivery-10.2
+ backend_percent: 18
+ frontend_percent: 25
+ overall_percent: 22
+ deployment_readiness:
+ backend: IN_PROGRESS
+ frontend: IN_PROGRESS
+ database: COMPLETE
+ api: COMPLETE
+ tests: IN_PROGRESS
+ documentation: COMPLETE
+ production_ready: IN_PROGRESS
+
+ - name: Enterprise Experience Platform
+ service_identifier: experience
+ commercial_product: Torbat Pages
+ backend_status: complete
+ frontend_status: complete
+ latest_backend_phase: experience-11.10
+ latest_frontend_phase: experience-fe-11.5
+ roadmap_last_phase: experience-fe-11.5
+ architecture_complete: true
+ backend_complete: true
+ frontend_complete: true
+ production_ready: false
+ snapshot: docs/service-snapshots/experience.yaml
+ frontend_snapshot: docs/service-snapshots/experience-frontend.yaml
+ latest_handover: docs/phase-handover/phase-experience-fe-11-5.md
+ remaining_phases:
+ - experience-fe-11.6
+ blockers:
+ - FE-11.6+ Publish Target Registry / official publish_id not started (hard stop after FE-11.5)
+ depends_on:
+ - core-platform
+ - identity-access
+ required_by:
+ - link_shortener
+ completed_phase: experience-fe-11.5
+ remaining_phase: experience-fe-11.6
+ backend_percent: 100
+ frontend_percent: 83
+ overall_percent: 92
+ deployment_readiness:
+ backend: COMPLETE
+ frontend: COMPLETE
+ database: COMPLETE
+ api: COMPLETE
+ tests: COMPLETE
+ documentation: COMPLETE
+ production_ready: IN_PROGRESS
+ notes: Backend 11.0–11.10 complete. Frontend FE-11.0–11.5 complete (stop). Architecture COMPLETE (ADR-022). production_ready IN_PROGRESS until publish registry.
+
+ - name: Hospitality Platform
+ service_identifier: hospitality
+ commercial_product: Torbat Food
+ backend_status: in_progress
+ frontend_status: not_started
+ latest_backend_phase: hospitality-12.8
+ latest_frontend_phase: null
+ roadmap_last_phase: hospitality-12.10
+ architecture_complete: true
+ backend_complete: false
+ frontend_complete: false
+ production_ready: false
+ snapshot: docs/service-snapshots/hospitality.yaml
+ latest_handover: docs/phase-handover/phase-12-8.md
+ remaining_phases:
+ - hospitality-12.9
+ - hospitality-12.10
+ blockers: []
+ depends_on:
+ - core-platform
+ - identity-access
+ required_by:
+ - hotel
+ completed_phase: hospitality-12.8
+ remaining_phase: hospitality-12.9
+ backend_percent: 80
+ frontend_percent: 0
+ overall_percent: 40
+ deployment_readiness:
+ backend: IN_PROGRESS
+ frontend: NOT_STARTED
+ database: COMPLETE
+ api: COMPLETE
+ tests: COMPLETE
+ documentation: COMPLETE
+ production_ready: IN_PROGRESS
+
+ - name: Healthcare
+ service_identifier: healthcare
+ commercial_product: Torbat Healthcare
+ backend_status: complete
+ frontend_status: partial
+ latest_backend_phase: healthcare-13-7
+ latest_frontend_phase: null
+ roadmap_last_phase: healthcare-13-7
+ architecture_complete: true
+ backend_complete: true
+ frontend_complete: false
+ production_ready: true
+ snapshot: docs/service-snapshots/healthcare.yaml
+ latest_handover: docs/phase-handover/phase-13-7.md
+ remaining_phases:
+ - healthcare-frontend-polish
+ - healthcare-booking
+ blockers: []
+ depends_on:
+ - core-platform
+ - identity-access
+ required_by:
+ - clinic
+ completed_phase: healthcare-13-7
+ remaining_phase: healthcare-frontend-polish
+ backend_percent: 100
+ frontend_percent: 55
+ overall_percent: 78
+ deployment_readiness:
+ backend: COMPLETE
+ frontend: IN_PROGRESS
+ database: COMPLETE
+ api: COMPLETE
+ tests: COMPLETE
+ documentation: COMPLETE
+ production_ready: COMPLETE
+
+ - name: Beauty Business
+ service_identifier: beauty_business
+ commercial_product: Torbat Beauty
+ backend_status: complete
+ frontend_status: partial
+ latest_backend_phase: beauty-business-14-7
+ latest_frontend_phase: null
+ roadmap_last_phase: beauty-business-14-7
+ architecture_complete: true
+ backend_complete: true
+ frontend_complete: false
+ production_ready: true
+ snapshot: docs/service-snapshots/beauty-business.yaml
+ latest_handover: docs/phase-handover/phase-14-7.md
+ remaining_phases:
+ - beauty-frontend-polish
+ blockers: []
+ depends_on:
+ - core-platform
+ - identity-access
+ required_by: []
+ completed_phase: beauty-business-14-7
+ remaining_phase: beauty-frontend-polish
+ backend_percent: 100
+ frontend_percent: 55
+ overall_percent: 78
+ deployment_readiness:
+ backend: COMPLETE
+ frontend: IN_PROGRESS
+ database: COMPLETE
+ api: COMPLETE
+ tests: COMPLETE
+ documentation: COMPLETE
+ production_ready: COMPLETE
+
+ - name: Enterprise Payment Platform
+ service_identifier: payment
+ commercial_product: Torbat Pay
+ backend_status: in_progress
+ frontend_status: complete
+ latest_backend_phase: payment-14.5
+ latest_frontend_phase: payment-frontend
+ roadmap_last_phase: payment-14.10
+ architecture_complete: true
+ backend_complete: false
+ frontend_complete: true
+ production_ready: false
+ snapshot: docs/service-snapshots/payment.yaml
+ latest_handover: docs/phase-handover/phase-payment-frontend.md
+ remaining_phases:
+ - payment-14.6
+ - payment-14.7
+ - payment-14.8
+ - payment-14.9
+ - payment-14.10
+ blockers: []
+ depends_on:
+ - core-platform
+ - identity-access
+ required_by:
+ - ecommerce
+ - marketplace
+ completed_phase: payment-frontend
+ remaining_phase: payment-14.6
+ backend_percent: 50
+ frontend_percent: 100
+ overall_percent: 50
+ deployment_readiness:
+ backend: IN_PROGRESS
+ frontend: COMPLETE
+ database: COMPLETE
+ api: COMPLETE
+ tests: COMPLETE
+ documentation: COMPLETE
+ production_ready: IN_PROGRESS
+
+ - name: Ecommerce
+ service_identifier: ecommerce
+ commercial_product: Torbat Commerce
+ backend_status: planned
+ frontend_status: not_started
+ latest_backend_phase: ecommerce-foundation
+ latest_frontend_phase: null
+ roadmap_last_phase: ecommerce-foundation
+ architecture_complete: false
+ backend_complete: false
+ frontend_complete: false
+ production_ready: false
+ snapshot: docs/service-snapshots/ecommerce.yaml
+ latest_handover: null
+ remaining_phases:
+ - ecommerce-foundation
+ blockers:
+ - foundation not started
+ depends_on:
+ - core-platform
+ - payment
+ required_by: []
+ completed_phase: null
+ remaining_phase: ecommerce-foundation
+ backend_percent: 0
+ frontend_percent: 0
+ overall_percent: 0
+ deployment_readiness:
+ backend: NOT_STARTED
+ frontend: NOT_STARTED
+ database: NOT_STARTED
+ api: NOT_STARTED
+ tests: NOT_STARTED
+ documentation: NOT_STARTED
+ production_ready: NOT_STARTED
+
+ - name: Marketplace
+ service_identifier: marketplace
+ commercial_product: Torbat Marketplace
+ backend_status: planned
+ frontend_status: not_started
+ latest_backend_phase: marketplace-foundation
+ latest_frontend_phase: null
+ roadmap_last_phase: marketplace-foundation
+ architecture_complete: false
+ backend_complete: false
+ frontend_complete: false
+ production_ready: false
+ snapshot: docs/service-snapshots/marketplace.yaml
+ latest_handover: null
+ remaining_phases:
+ - marketplace-foundation
+ blockers:
+ - foundation not started
+ depends_on:
+ - core-platform
+ - payment
+ required_by: []
+ completed_phase: null
+ remaining_phase: marketplace-foundation
+ backend_percent: 0
+ frontend_percent: 0
+ overall_percent: 0
+ deployment_readiness:
+ backend: NOT_STARTED
+ frontend: NOT_STARTED
+ database: NOT_STARTED
+ api: NOT_STARTED
+ tests: NOT_STARTED
+ documentation: NOT_STARTED
+ production_ready: NOT_STARTED
+
+ - name: Automation
+ service_identifier: automation
+ commercial_product: Torbat Automation
+ backend_status: planned
+ frontend_status: not_started
+ latest_backend_phase: automation-foundation
+ latest_frontend_phase: null
+ roadmap_last_phase: automation-foundation
+ architecture_complete: false
+ backend_complete: false
+ frontend_complete: false
+ production_ready: false
+ snapshot: docs/service-snapshots/automation.yaml
+ latest_handover: null
+ remaining_phases:
+ - automation-foundation
+ blockers:
+ - foundation not started
+ depends_on:
+ - core-platform
+ required_by: []
+ completed_phase: null
+ remaining_phase: automation-foundation
+ backend_percent: 0
+ frontend_percent: 0
+ overall_percent: 0
+ deployment_readiness:
+ backend: NOT_STARTED
+ frontend: NOT_STARTED
+ database: NOT_STARTED
+ api: NOT_STARTED
+ tests: NOT_STARTED
+ documentation: NOT_STARTED
+ production_ready: NOT_STARTED
+
+ - name: Academy
+ service_identifier: academy
+ commercial_product: Torbat Academy
+ backend_status: planned
+ frontend_status: not_started
+ latest_backend_phase: academy-foundation
+ latest_frontend_phase: null
+ roadmap_last_phase: academy-foundation
+ architecture_complete: false
+ backend_complete: false
+ frontend_complete: false
+ production_ready: false
+ snapshot: docs/service-snapshots/academy.yaml
+ latest_handover: null
+ remaining_phases:
+ - academy-foundation
+ blockers:
+ - foundation not started
+ depends_on:
+ - core-platform
+ required_by: []
+ completed_phase: null
+ remaining_phase: academy-foundation
+ backend_percent: 0
+ frontend_percent: 0
+ overall_percent: 0
+ deployment_readiness:
+ backend: NOT_STARTED
+ frontend: NOT_STARTED
+ database: NOT_STARTED
+ api: NOT_STARTED
+ tests: NOT_STARTED
+ documentation: NOT_STARTED
+ production_ready: NOT_STARTED
+
+ - name: Clinic
+ service_identifier: clinic
+ commercial_product: Torbat Clinic
+ backend_status: planned
+ frontend_status: not_started
+ latest_backend_phase: clinic-foundation
+ latest_frontend_phase: null
+ roadmap_last_phase: clinic-foundation
+ architecture_complete: false
+ backend_complete: false
+ frontend_complete: false
+ production_ready: false
+ snapshot: docs/service-snapshots/clinic.yaml
+ latest_handover: null
+ remaining_phases:
+ - clinic-foundation
+ blockers:
+ - foundation not started
+ depends_on:
+ - core-platform
+ - healthcare
+ required_by: []
+ completed_phase: null
+ remaining_phase: clinic-foundation
+ backend_percent: 0
+ frontend_percent: 0
+ overall_percent: 0
+ deployment_readiness:
+ backend: NOT_STARTED
+ frontend: NOT_STARTED
+ database: NOT_STARTED
+ api: NOT_STARTED
+ tests: NOT_STARTED
+ documentation: NOT_STARTED
+ production_ready: NOT_STARTED
+
+ - name: Hotel
+ service_identifier: hotel
+ commercial_product: Torbat Hotel
+ backend_status: planned
+ frontend_status: not_started
+ latest_backend_phase: hotel-foundation
+ latest_frontend_phase: null
+ roadmap_last_phase: hotel-foundation
+ architecture_complete: false
+ backend_complete: false
+ frontend_complete: false
+ production_ready: false
+ snapshot: docs/service-snapshots/hotel.yaml
+ latest_handover: null
+ remaining_phases:
+ - hotel-foundation
+ blockers:
+ - foundation not started
+ depends_on:
+ - core-platform
+ - hospitality
+ required_by: []
+ completed_phase: null
+ remaining_phase: hotel-foundation
+ backend_percent: 0
+ frontend_percent: 0
+ overall_percent: 0
+ deployment_readiness:
+ backend: NOT_STARTED
+ frontend: NOT_STARTED
+ database: NOT_STARTED
+ api: NOT_STARTED
+ tests: NOT_STARTED
+ documentation: NOT_STARTED
+ production_ready: NOT_STARTED
+
+ - name: Tourism
+ service_identifier: tourism
+ commercial_product: Torbat Tourism
+ backend_status: planned
+ frontend_status: not_started
+ latest_backend_phase: tourism-foundation
+ latest_frontend_phase: null
+ roadmap_last_phase: tourism-foundation
+ architecture_complete: false
+ backend_complete: false
+ frontend_complete: false
+ production_ready: false
+ snapshot: docs/service-snapshots/tourism.yaml
+ latest_handover: null
+ remaining_phases:
+ - tourism-foundation
+ blockers:
+ - foundation not started
+ depends_on:
+ - core-platform
+ required_by: []
+ completed_phase: null
+ remaining_phase: tourism-foundation
+ backend_percent: 0
+ frontend_percent: 0
+ overall_percent: 0
+ deployment_readiness:
+ backend: NOT_STARTED
+ frontend: NOT_STARTED
+ database: NOT_STARTED
+ api: NOT_STARTED
+ tests: NOT_STARTED
+ documentation: NOT_STARTED
+ production_ready: NOT_STARTED
+
+ - name: AI Assistant
+ service_identifier: ai_assistant
+ commercial_product: Torbat AI
+ backend_status: scaffolded
+ frontend_status: not_started
+ latest_backend_phase: ai-assistant-foundation
+ latest_frontend_phase: null
+ roadmap_last_phase: ai-assistant-foundation
+ architecture_complete: false
+ backend_complete: false
+ frontend_complete: false
+ production_ready: false
+ snapshot: docs/service-snapshots/ai-assistant.yaml
+ latest_handover: null
+ remaining_phases:
+ - ai-assistant-foundation
+ blockers:
+ - foundation scaffold only
+ depends_on:
+ - core-platform
+ required_by: []
+ completed_phase: null
+ remaining_phase: ai-assistant-foundation
+ backend_percent: 5
+ frontend_percent: 0
+ overall_percent: 2
+ deployment_readiness:
+ backend: NOT_STARTED
+ frontend: NOT_STARTED
+ database: NOT_STARTED
+ api: NOT_STARTED
+ tests: NOT_STARTED
+ documentation: NOT_STARTED
+ production_ready: NOT_STARTED
+
+ - name: SMS Panel
+ service_identifier: sms_panel
+ commercial_product: Torbat SMS
+ backend_status: deferred
+ frontend_status: not_started
+ latest_backend_phase: sms-panel-future
+ latest_frontend_phase: null
+ roadmap_last_phase: sms-panel-future
+ architecture_complete: false
+ backend_complete: false
+ frontend_complete: false
+ production_ready: false
+ snapshot: docs/service-snapshots/sms-panel.yaml
+ latest_handover: null
+ remaining_phases:
+ - sms-panel-future
+ blockers:
+ - deferred — SMS MVP lives under communication
+ depends_on:
+ - communication
+ required_by: []
+ completed_phase: null
+ remaining_phase: sms-panel-future
+ backend_percent: 0
+ frontend_percent: 0
+ overall_percent: 0
+ deployment_readiness:
+ backend: NOT_STARTED
+ frontend: NOT_STARTED
+ database: NOT_STARTED
+ api: NOT_STARTED
+ tests: NOT_STARTED
+ documentation: NOT_STARTED
+ production_ready: NOT_STARTED
+
+ - name: Link Shortener
+ service_identifier: link_shortener
+ commercial_product: Torbat Links
+ backend_status: scaffolded
+ frontend_status: not_started
+ latest_backend_phase: future-services
+ latest_frontend_phase: null
+ roadmap_last_phase: future-services
+ architecture_complete: false
+ backend_complete: false
+ frontend_complete: false
+ production_ready: false
+ snapshot: docs/service-snapshots/link-shortener.yaml
+ latest_handover: null
+ remaining_phases:
+ - link-shortener-foundation
+ blockers:
+ - future product
+ depends_on:
+ - core-platform
+ - experience
+ required_by: []
+ completed_phase: null
+ remaining_phase: link-shortener-foundation
+ backend_percent: 5
+ frontend_percent: 0
+ overall_percent: 2
+ deployment_readiness:
+ backend: NOT_STARTED
+ frontend: NOT_STARTED
+ database: NOT_STARTED
+ api: NOT_STARTED
+ tests: NOT_STARTED
+ documentation: NOT_STARTED
+ production_ready: NOT_STARTED
+
+ - name: Notification
+ service_identifier: notification
+ commercial_product: Torbat Notification
+ backend_status: scaffolded
+ frontend_status: not_started
+ latest_backend_phase: future-services
+ latest_frontend_phase: null
+ roadmap_last_phase: future-services
+ architecture_complete: false
+ backend_complete: false
+ frontend_complete: false
+ production_ready: false
+ snapshot: docs/service-snapshots/notification.yaml
+ latest_handover: null
+ remaining_phases:
+ - notification-foundation
+ blockers:
+ - future product
+ depends_on:
+ - communication
+ required_by: []
+ completed_phase: null
+ remaining_phase: notification-foundation
+ backend_percent: 5
+ frontend_percent: 0
+ overall_percent: 2
+ deployment_readiness:
+ backend: NOT_STARTED
+ frontend: NOT_STARTED
+ database: NOT_STARTED
+ api: NOT_STARTED
+ tests: NOT_STARTED
+ documentation: NOT_STARTED
+ production_ready: NOT_STARTED
+
+ - name: File Storage
+ service_identifier: file_storage
+ commercial_product: Torbat Storage
+ backend_status: scaffolded
+ frontend_status: n/a
+ latest_backend_phase: future-services
+ latest_frontend_phase: null
+ roadmap_last_phase: future-services
+ architecture_complete: false
+ backend_complete: false
+ frontend_complete: true
+ production_ready: false
+ snapshot: docs/service-snapshots/file-storage.yaml
+ latest_handover: null
+ remaining_phases:
+ - file-storage-foundation
+ blockers:
+ - future platform service
+ depends_on:
+ - core-platform
+ required_by: []
+ completed_phase: null
+ remaining_phase: file-storage-foundation
+ backend_percent: 5
+ frontend_percent: 100
+ overall_percent: 5
+ deployment_readiness:
+ backend: NOT_STARTED
+ frontend: COMPLETE
+ database: NOT_STARTED
+ api: NOT_STARTED
+ tests: NOT_STARTED
+ documentation: NOT_STARTED
+ production_ready: NOT_STARTED
+
+ - name: Live Chat
+ service_identifier: live_chat
+ commercial_product: Torbat Live Chat
+ backend_status: scaffolded
+ frontend_status: not_started
+ latest_backend_phase: future-services
+ latest_frontend_phase: null
+ roadmap_last_phase: future-services
+ architecture_complete: false
+ backend_complete: false
+ frontend_complete: false
+ production_ready: false
+ snapshot: docs/service-snapshots/live-chat.yaml
+ latest_handover: null
+ remaining_phases:
+ - live-chat-foundation
+ blockers:
+ - future product
+ depends_on:
+ - communication
+ required_by: []
+ completed_phase: null
+ remaining_phase: live-chat-foundation
+ backend_percent: 5
+ frontend_percent: 0
+ overall_percent: 2
+ deployment_readiness:
+ backend: NOT_STARTED
+ frontend: NOT_STARTED
+ database: NOT_STARTED
+ api: NOT_STARTED
+ tests: NOT_STARTED
+ documentation: NOT_STARTED
+ production_ready: NOT_STARTED
+
+ - name: Smart Messenger
+ service_identifier: smart_messenger
+ commercial_product: Torbat Messenger
+ backend_status: scaffolded
+ frontend_status: not_started
+ latest_backend_phase: future-services
+ latest_frontend_phase: null
+ roadmap_last_phase: future-services
+ architecture_complete: false
+ backend_complete: false
+ frontend_complete: false
+ production_ready: false
+ snapshot: docs/service-snapshots/smart-messenger.yaml
+ latest_handover: null
+ remaining_phases:
+ - smart-messenger-foundation
+ blockers:
+ - future product
+ depends_on:
+ - communication
+ required_by: []
+ completed_phase: null
+ remaining_phase: smart-messenger-foundation
+ backend_percent: 5
+ frontend_percent: 0
+ overall_percent: 2
+ deployment_readiness:
+ backend: NOT_STARTED
+ frontend: NOT_STARTED
+ database: NOT_STARTED
+ api: NOT_STARTED
+ tests: NOT_STARTED
+ documentation: NOT_STARTED
+ production_ready: NOT_STARTED
+
+global_statistics:
+ number_of_services: 27
+ completed_services: 6
+ services_in_progress: 7
+ services_planned_or_scaffolded: 14
+ backend_completion_percent: 48
+ frontend_completion_percent: 24
+ overall_completion_percent: 36
+ critical_path_remaining: 9
+ notes: >
+ Percentages from per-service backend_percent/frontend_percent/overall_percent.
+ completed_services = deployment_readiness.production_ready COMPLETE AND no critical remaining phases.
+ Recalculate after every phase Complete via automatic_update_rules.
+
+deprecated:
+ next_recommended_phase:
+ status: deprecated
+ replaced_by:
+ - execution_priority
+ - critical_path
+ - deployment_readiness
+ former_value: payment-14.6
+ recommended_execution_queue:
+ status: deprecated
+ replaced_by: execution_priority
+ dependencies_field:
+ status: deprecated
+ replaced_by:
+ - depends_on
+ - required_by
+
+framework:
+ last_framework_phase: platform-project-status-v2
+ latest_handover: docs/phase-handover/phase-af-project-status-v2.md
+ project_index: docs/ai-framework/project-index.yaml
+ phase_manifest: docs/ai-framework/phase-manifest.yaml
+ service_manifest: docs/ai-framework/service-manifest.yaml
diff --git a/docs/service-snapshots/payment-frontend.yaml b/docs/service-snapshots/payment-frontend.yaml
new file mode 100644
index 0000000..014742c
--- /dev/null
+++ b/docs/service-snapshots/payment-frontend.yaml
@@ -0,0 +1,78 @@
+# docs/service-snapshots/payment-frontend.yaml
+
+schema_version: 1
+snapshot_version: 1
+
+service_name: payment
+layer: frontend
+commercial_product: Torbat Pay
+backend_version: "0.14.5.0"
+current_phase: payment-frontend
+last_completed_phase: payment-frontend
+next_phase: payment-14.6
+stop_boundary: MVP 14.0–14.5 (no refunds/splits UI)
+
+completed_phases:
+ - payment-frontend
+
+module_path: frontend/modules/payment
+bff_path: frontend/app/api/payment/[...path]/route.ts
+app_routes_prefix: /payment
+route_count: 26
+
+env_vars:
+ - NEXT_PUBLIC_PAYMENT_API_URL
+ - PAYMENT_SERVICE_URL
+
+apps_catalog:
+ id: payment
+ href: /payment/hub
+ status: available
+
+features:
+ - hub
+ - dashboard
+ - overview
+ - kpis
+ - transactions
+ - transactionDetail
+ - ledger
+ - psp
+ - gatewayHealth
+ - providers
+ - merchants
+ - merchantDetail
+ - merchantApprovals
+ - requests
+ - requestDetail
+ - callbacks
+ - verification
+ - audit
+ - bundles
+ - featureToggles
+ - capabilities
+ - settings
+ - metrics
+ - monitoring
+ - apiExplorer
+ - openapi
+
+api_client: frontend/modules/payment/services/payment-api.ts
+real_backend_only: true
+mock_data: false
+
+documentation:
+ - docs/payment-roadmap.md
+ - frontend/docs/payment-frontend-architecture.md
+ - frontend/docs/payment-frontend-validation-report.md
+ - docs/phase-handover/phase-payment-frontend.md
+ - docs/service-snapshots/payment.yaml
+
+open_todos:
+ - payment-14.6+ Refunds & Reversals UI (explicitly out of scope)
+ - payment-14.7+ Splits UI (explicitly out of scope)
+ - Dedicated GET /callback-logs endpoint when backend exposes list API
+ - Fine-grained permission gating per screen from identity membership
+
+last_handover_reference: docs/phase-handover/phase-payment-frontend.md
+last_updated: "2026-07-27"
diff --git a/frontend/app/api/payment/[...path]/route.ts b/frontend/app/api/payment/[...path]/route.ts
new file mode 100644
index 0000000..d8b5fdd
--- /dev/null
+++ b/frontend/app/api/payment/[...path]/route.ts
@@ -0,0 +1,59 @@
+import { NextRequest, NextResponse } from "next/server";
+
+const UPSTREAM =
+ process.env.PAYMENT_SERVICE_URL ||
+ process.env.NEXT_PUBLIC_PAYMENT_API_URL ||
+ "http://localhost:8012";
+
+async function proxy(req: NextRequest, pathSegments: string[]) {
+ const p = pathSegments.join("/");
+ const url = new URL(req.url);
+ const target = `${UPSTREAM.replace(/\/$/, "")}/${p}${url.search}`;
+ const headers = new Headers();
+ for (const h of [
+ "authorization",
+ "content-type",
+ "x-tenant-id",
+ "accept",
+ "idempotency-key",
+ ]) {
+ const v = req.headers.get(h);
+ if (v) headers.set(h, v);
+ }
+ let body: ArrayBuffer | undefined;
+ if (req.method !== "GET" && req.method !== "HEAD") body = await req.arrayBuffer();
+ try {
+ const res = await fetch(target, { method: req.method, headers, body, cache: "no-store" });
+ const out = new Headers();
+ const ct = res.headers.get("content-type");
+ if (ct) out.set("content-type", ct);
+ return new NextResponse(await res.arrayBuffer(), { status: res.status, headers: out });
+ } catch (err) {
+ const detail = err instanceof Error ? err.message : "upstream_unreachable";
+ return NextResponse.json(
+ { error: { code: "payment_proxy_error", message: `پروکسی تربتپی — ${detail}` } },
+ { status: 502 }
+ );
+ }
+}
+
+export async function GET(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
+ const { path: p } = await ctx.params;
+ return proxy(req, p);
+}
+export async function POST(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
+ const { path: p } = await ctx.params;
+ return proxy(req, p);
+}
+export async function PATCH(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
+ const { path: p } = await ctx.params;
+ return proxy(req, p);
+}
+export async function PUT(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
+ const { path: p } = await ctx.params;
+ return proxy(req, p);
+}
+export async function DELETE(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
+ const { path: p } = await ctx.params;
+ return proxy(req, p);
+}
diff --git a/frontend/app/payment/api-explorer/error.tsx b/frontend/app/payment/api-explorer/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/api-explorer/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/api-explorer/loading.tsx b/frontend/app/payment/api-explorer/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/api-explorer/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/api-explorer/page.tsx b/frontend/app/payment/api-explorer/page.tsx
new file mode 100644
index 0000000..6618025
--- /dev/null
+++ b/frontend/app/payment/api-explorer/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentApiExplorer as default } from "@/modules/payment/features/apiExplorer";
diff --git a/frontend/app/payment/audit/error.tsx b/frontend/app/payment/audit/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/audit/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/audit/loading.tsx b/frontend/app/payment/audit/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/audit/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/audit/page.tsx b/frontend/app/payment/audit/page.tsx
new file mode 100644
index 0000000..128bab6
--- /dev/null
+++ b/frontend/app/payment/audit/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentAudit as default } from "@/modules/payment/features/audit";
diff --git a/frontend/app/payment/bundles/error.tsx b/frontend/app/payment/bundles/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/bundles/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/bundles/loading.tsx b/frontend/app/payment/bundles/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/bundles/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/bundles/page.tsx b/frontend/app/payment/bundles/page.tsx
new file mode 100644
index 0000000..5ae72f7
--- /dev/null
+++ b/frontend/app/payment/bundles/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentBundles as default } from "@/modules/payment/features/bundles";
diff --git a/frontend/app/payment/callbacks/error.tsx b/frontend/app/payment/callbacks/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/callbacks/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/callbacks/loading.tsx b/frontend/app/payment/callbacks/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/callbacks/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/callbacks/page.tsx b/frontend/app/payment/callbacks/page.tsx
new file mode 100644
index 0000000..ae25cd4
--- /dev/null
+++ b/frontend/app/payment/callbacks/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentCallbacks as default } from "@/modules/payment/features/callbacks";
diff --git a/frontend/app/payment/capabilities/error.tsx b/frontend/app/payment/capabilities/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/capabilities/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/capabilities/loading.tsx b/frontend/app/payment/capabilities/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/capabilities/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/capabilities/page.tsx b/frontend/app/payment/capabilities/page.tsx
new file mode 100644
index 0000000..1dd5336
--- /dev/null
+++ b/frontend/app/payment/capabilities/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentCapabilities as default } from "@/modules/payment/features/capabilities";
diff --git a/frontend/app/payment/dashboard/error.tsx b/frontend/app/payment/dashboard/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/dashboard/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/dashboard/loading.tsx b/frontend/app/payment/dashboard/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/dashboard/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/dashboard/page.tsx b/frontend/app/payment/dashboard/page.tsx
new file mode 100644
index 0000000..f75497c
--- /dev/null
+++ b/frontend/app/payment/dashboard/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentDashboard as default } from "@/modules/payment/features/dashboard";
diff --git a/frontend/app/payment/error.tsx b/frontend/app/payment/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/feature-toggles/error.tsx b/frontend/app/payment/feature-toggles/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/feature-toggles/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/feature-toggles/loading.tsx b/frontend/app/payment/feature-toggles/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/feature-toggles/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/feature-toggles/page.tsx b/frontend/app/payment/feature-toggles/page.tsx
new file mode 100644
index 0000000..a4151ad
--- /dev/null
+++ b/frontend/app/payment/feature-toggles/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentFeatureToggles as default } from "@/modules/payment/features/featureToggles";
diff --git a/frontend/app/payment/hub/error.tsx b/frontend/app/payment/hub/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/hub/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/hub/loading.tsx b/frontend/app/payment/hub/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/hub/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/hub/page.tsx b/frontend/app/payment/hub/page.tsx
new file mode 100644
index 0000000..4f753f8
--- /dev/null
+++ b/frontend/app/payment/hub/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentHub as default } from "@/modules/payment/features/hub";
diff --git a/frontend/app/payment/kpis/error.tsx b/frontend/app/payment/kpis/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/kpis/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/kpis/loading.tsx b/frontend/app/payment/kpis/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/kpis/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/kpis/page.tsx b/frontend/app/payment/kpis/page.tsx
new file mode 100644
index 0000000..822ffa2
--- /dev/null
+++ b/frontend/app/payment/kpis/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentKpis as default } from "@/modules/payment/features/kpis";
diff --git a/frontend/app/payment/layout.tsx b/frontend/app/payment/layout.tsx
new file mode 100644
index 0000000..b0808ae
--- /dev/null
+++ b/frontend/app/payment/layout.tsx
@@ -0,0 +1,5 @@
+"use client";
+
+import { PaymentRootLayout } from "@/modules/payment/components/createPortalLayout";
+
+export default PaymentRootLayout;
diff --git a/frontend/app/payment/ledger/error.tsx b/frontend/app/payment/ledger/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/ledger/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/ledger/loading.tsx b/frontend/app/payment/ledger/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/ledger/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/ledger/page.tsx b/frontend/app/payment/ledger/page.tsx
new file mode 100644
index 0000000..1220e84
--- /dev/null
+++ b/frontend/app/payment/ledger/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentLedger as default } from "@/modules/payment/features/ledger";
diff --git a/frontend/app/payment/loading.tsx b/frontend/app/payment/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/merchants/[id]/error.tsx b/frontend/app/payment/merchants/[id]/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/merchants/[id]/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/merchants/[id]/loading.tsx b/frontend/app/payment/merchants/[id]/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/merchants/[id]/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/merchants/[id]/page.tsx b/frontend/app/payment/merchants/[id]/page.tsx
new file mode 100644
index 0000000..c7bc998
--- /dev/null
+++ b/frontend/app/payment/merchants/[id]/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentMerchantDetail as default } from "@/modules/payment/features/merchantDetail";
diff --git a/frontend/app/payment/merchants/approvals/error.tsx b/frontend/app/payment/merchants/approvals/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/merchants/approvals/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/merchants/approvals/loading.tsx b/frontend/app/payment/merchants/approvals/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/merchants/approvals/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/merchants/approvals/page.tsx b/frontend/app/payment/merchants/approvals/page.tsx
new file mode 100644
index 0000000..2d12a98
--- /dev/null
+++ b/frontend/app/payment/merchants/approvals/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentMerchantApprovals as default } from "@/modules/payment/features/merchantApprovals";
diff --git a/frontend/app/payment/merchants/error.tsx b/frontend/app/payment/merchants/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/merchants/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/merchants/loading.tsx b/frontend/app/payment/merchants/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/merchants/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/merchants/page.tsx b/frontend/app/payment/merchants/page.tsx
new file mode 100644
index 0000000..621cd35
--- /dev/null
+++ b/frontend/app/payment/merchants/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentMerchants as default } from "@/modules/payment/features/merchants";
diff --git a/frontend/app/payment/metrics/error.tsx b/frontend/app/payment/metrics/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/metrics/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/metrics/loading.tsx b/frontend/app/payment/metrics/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/metrics/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/metrics/page.tsx b/frontend/app/payment/metrics/page.tsx
new file mode 100644
index 0000000..7fada1b
--- /dev/null
+++ b/frontend/app/payment/metrics/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentMetrics as default } from "@/modules/payment/features/metrics";
diff --git a/frontend/app/payment/monitoring/error.tsx b/frontend/app/payment/monitoring/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/monitoring/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/monitoring/loading.tsx b/frontend/app/payment/monitoring/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/monitoring/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/monitoring/page.tsx b/frontend/app/payment/monitoring/page.tsx
new file mode 100644
index 0000000..8266c8c
--- /dev/null
+++ b/frontend/app/payment/monitoring/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentMonitoring as default } from "@/modules/payment/features/monitoring";
diff --git a/frontend/app/payment/openapi/error.tsx b/frontend/app/payment/openapi/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/openapi/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/openapi/loading.tsx b/frontend/app/payment/openapi/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/openapi/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/openapi/page.tsx b/frontend/app/payment/openapi/page.tsx
new file mode 100644
index 0000000..97190b2
--- /dev/null
+++ b/frontend/app/payment/openapi/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentOpenApi as default } from "@/modules/payment/features/openapi";
diff --git a/frontend/app/payment/overview/error.tsx b/frontend/app/payment/overview/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/overview/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/overview/loading.tsx b/frontend/app/payment/overview/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/overview/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/overview/page.tsx b/frontend/app/payment/overview/page.tsx
new file mode 100644
index 0000000..cb32466
--- /dev/null
+++ b/frontend/app/payment/overview/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentOverview as default } from "@/modules/payment/features/overview";
diff --git a/frontend/app/payment/page.tsx b/frontend/app/payment/page.tsx
new file mode 100644
index 0000000..016753a
--- /dev/null
+++ b/frontend/app/payment/page.tsx
@@ -0,0 +1,5 @@
+import { redirect } from "next/navigation";
+
+export default function PaymentIndexPage() {
+ redirect("/payment/hub");
+}
diff --git a/frontend/app/payment/providers/error.tsx b/frontend/app/payment/providers/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/providers/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/providers/loading.tsx b/frontend/app/payment/providers/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/providers/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/providers/page.tsx b/frontend/app/payment/providers/page.tsx
new file mode 100644
index 0000000..beba1f9
--- /dev/null
+++ b/frontend/app/payment/providers/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentProviders as default } from "@/modules/payment/features/providers";
diff --git a/frontend/app/payment/psp/error.tsx b/frontend/app/payment/psp/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/psp/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/psp/health/error.tsx b/frontend/app/payment/psp/health/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/psp/health/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/psp/health/loading.tsx b/frontend/app/payment/psp/health/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/psp/health/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/psp/health/page.tsx b/frontend/app/payment/psp/health/page.tsx
new file mode 100644
index 0000000..47e9268
--- /dev/null
+++ b/frontend/app/payment/psp/health/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentGatewayHealth as default } from "@/modules/payment/features/gatewayHealth";
diff --git a/frontend/app/payment/psp/loading.tsx b/frontend/app/payment/psp/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/psp/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/psp/page.tsx b/frontend/app/payment/psp/page.tsx
new file mode 100644
index 0000000..e5d51e1
--- /dev/null
+++ b/frontend/app/payment/psp/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentPsp as default } from "@/modules/payment/features/psp";
diff --git a/frontend/app/payment/requests/[id]/error.tsx b/frontend/app/payment/requests/[id]/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/requests/[id]/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/requests/[id]/loading.tsx b/frontend/app/payment/requests/[id]/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/requests/[id]/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/requests/[id]/page.tsx b/frontend/app/payment/requests/[id]/page.tsx
new file mode 100644
index 0000000..c0491cd
--- /dev/null
+++ b/frontend/app/payment/requests/[id]/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentRequestDetail as default } from "@/modules/payment/features/requestDetail";
diff --git a/frontend/app/payment/requests/error.tsx b/frontend/app/payment/requests/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/requests/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/requests/loading.tsx b/frontend/app/payment/requests/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/requests/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/requests/page.tsx b/frontend/app/payment/requests/page.tsx
new file mode 100644
index 0000000..fcdfac5
--- /dev/null
+++ b/frontend/app/payment/requests/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentRequests as default } from "@/modules/payment/features/requests";
diff --git a/frontend/app/payment/settings/error.tsx b/frontend/app/payment/settings/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/settings/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/settings/loading.tsx b/frontend/app/payment/settings/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/settings/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/settings/page.tsx b/frontend/app/payment/settings/page.tsx
new file mode 100644
index 0000000..bb514e6
--- /dev/null
+++ b/frontend/app/payment/settings/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentSettings as default } from "@/modules/payment/features/settings";
diff --git a/frontend/app/payment/transactions/[id]/error.tsx b/frontend/app/payment/transactions/[id]/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/transactions/[id]/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/transactions/[id]/loading.tsx b/frontend/app/payment/transactions/[id]/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/transactions/[id]/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/transactions/[id]/page.tsx b/frontend/app/payment/transactions/[id]/page.tsx
new file mode 100644
index 0000000..7804177
--- /dev/null
+++ b/frontend/app/payment/transactions/[id]/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentTransactionDetail as default } from "@/modules/payment/features/transactionDetail";
diff --git a/frontend/app/payment/transactions/error.tsx b/frontend/app/payment/transactions/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/transactions/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/transactions/loading.tsx b/frontend/app/payment/transactions/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/transactions/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/transactions/page.tsx b/frontend/app/payment/transactions/page.tsx
new file mode 100644
index 0000000..e5be92c
--- /dev/null
+++ b/frontend/app/payment/transactions/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentTransactions as default } from "@/modules/payment/features/transactions";
diff --git a/frontend/app/payment/verification/error.tsx b/frontend/app/payment/verification/error.tsx
new file mode 100644
index 0000000..8f69ee5
--- /dev/null
+++ b/frontend/app/payment/verification/error.tsx
@@ -0,0 +1,5 @@
+"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
diff --git a/frontend/app/payment/verification/loading.tsx b/frontend/app/payment/verification/loading.tsx
new file mode 100644
index 0000000..52a490b
--- /dev/null
+++ b/frontend/app/payment/verification/loading.tsx
@@ -0,0 +1,4 @@
+import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
diff --git a/frontend/app/payment/verification/page.tsx b/frontend/app/payment/verification/page.tsx
new file mode 100644
index 0000000..64d483b
--- /dev/null
+++ b/frontend/app/payment/verification/page.tsx
@@ -0,0 +1,2 @@
+"use client";
+export { PaymentVerification as default } from "@/modules/payment/features/verification";
diff --git a/frontend/docs/payment-frontend-architecture.md b/frontend/docs/payment-frontend-architecture.md
new file mode 100644
index 0000000..01f8221
--- /dev/null
+++ b/frontend/docs/payment-frontend-architecture.md
@@ -0,0 +1,44 @@
+# Payment Frontend Architecture
+
+Torbat Pay (`frontend/modules/payment/`) mirrors the Experience/Delivery module layout.
+
+## Layers
+
+```
+Browser → /api/payment BFF → payment-service:8012
+```
+
+- **BFF:** `frontend/app/api/payment/[...path]/route.ts` — forwards `Authorization`, `X-Tenant-ID`, `Idempotency-Key`
+- **API client:** `services/payment-api.ts` — real endpoints only (MVP 14.0–14.5)
+- **Checkout session:** `PaymentRequest` detail at `/payment/requests/[id]`
+
+## Module structure
+
+| Path | Role |
+| --- | --- |
+| `components/` | PaymentPortalShell, PaymentListPage, PaymentCharts, createPortalLayout |
+| `constants/` | portals.ts (nav + MVP routes), permissions.ts |
+| `features/` | Screen implementations (26 files) |
+| `hooks/` | usePaymentCapabilities, usePaymentPermissions |
+| `pages/shared.tsx` | Loaders, errors, CSV export |
+| `types/` | API response types |
+
+## Auth & tenant
+
+- `AuthGuard` + `useMe` tenant gate (same as other portals)
+- `X-Tenant-ID` on all authenticated API calls
+- Bundle-gated nav via `filterNavByBundles(active_bundles)`
+
+## Charts
+
+Dashboard and KPIs use `PaymentCharts` (recharts): bar, donut, stat cards.
+
+## Validation
+
+```bash
+npm run validate:payment
+```
+
+## Out of scope (14.6+)
+
+Refunds, splits, subscriptions, wallet top-up UI — backend phases not yet implemented.
diff --git a/frontend/docs/payment-frontend-validation-report.md b/frontend/docs/payment-frontend-validation-report.md
new file mode 100644
index 0000000..f8c2ecf
--- /dev/null
+++ b/frontend/docs/payment-frontend-validation-report.md
@@ -0,0 +1,55 @@
+# Payment Frontend Validation Report
+
+**Date:** 2026-07-27
+**Phase:** payment-frontend
+**Backend boundary:** MVP 14.0–14.5 (v0.14.5.0)
+
+## Route validation
+
+```bash
+npm run validate:payment-routes
+# Payment routes OK (23 MVP routes, 26 features)
+```
+
+## Module inventory
+
+| Category | Count |
+| --- | --- |
+| Feature screens | 26 |
+| MVP list routes | 23 |
+| Detail routes | 3 (`requests/[id]`, `transactions/[id]`, `merchants/[id]`) |
+| Core components (pre-existing) | 5 |
+| Hooks | 2 |
+| BFF proxy | 1 |
+
+## API wiring
+
+All list/detail screens call `paymentApi` methods against real `/api/v1/*` paths. No mock fixtures.
+
+| Screen | API |
+| --- | --- |
+| Dashboard / KPIs | `requests.list`, `transactions.list`, `audit.list`, `metrics` |
+| Transactions | `transactions.list/get` |
+| Ledger | `ledger.list` |
+| PSP | `pspConnections.list/test` |
+| Merchants | `merchants.*` lifecycle |
+| Requests | `requests.list/get/cancel/verify` |
+| Callbacks | `audit.list` + `requests` filtered client-side |
+| Verification | `requests.verify` queue |
+| Bundles / toggles / settings | CRUD list endpoints |
+| OpenAPI | `GET /openapi.json` |
+
+## Known gaps
+
+1. **Callback log list** — backend exposes POST callback ingress only; UI derives from audit + `redirect_issued` requests.
+2. **14.6+ UI** — refunds, splits, reconciliation not implemented (by design).
+3. **Create flows** — list pages are read-focused; POST create forms for workspace/PSP/merchant can be added incrementally.
+4. **Permission UI gates** — `usePaymentPermissions` available; per-screen enforcement is soft (403 from API).
+
+## Apps catalog
+
+Torbat Pay registered as `available` → `/payment/hub`.
+
+## Docker env
+
+`NEXT_PUBLIC_PAYMENT_API_URL` + `PAYMENT_SERVICE_URL` added to frontend service in docker-compose.
diff --git a/frontend/lib/apps-catalog.ts b/frontend/lib/apps-catalog.ts
index 0a1fda2..5568ea8 100644
--- a/frontend/lib/apps-catalog.ts
+++ b/frontend/lib/apps-catalog.ts
@@ -103,12 +103,22 @@ export const PLATFORM_APPS: PlatformApp[] = [
status: "coming_soon",
},
{
- id: "website_builder",
- name: "سایتساز",
- description: "ساخت صفحات و لندینگ",
+ id: "experience",
+ name: "تربت پیجز",
+ description: "سایت، صفحه، قالب، فرم و انتشار",
icon: "🌐",
gradient: "from-cyan-500 to-sky-600",
- status: "coming_soon",
+ status: "available",
+ href: "/experience/hub",
+ },
+ {
+ id: "payment",
+ name: "تربتپی",
+ description: "پلتفرم پرداخت سازمانی — BYO-PSP و پذیرنده",
+ icon: "💳",
+ gradient: "from-teal-500 to-emerald-600",
+ status: "available",
+ href: "/payment/hub",
},
{
id: "restaurant",
diff --git a/frontend/modules/payment/components/PaymentCharts.tsx b/frontend/modules/payment/components/PaymentCharts.tsx
new file mode 100644
index 0000000..b61e867
--- /dev/null
+++ b/frontend/modules/payment/components/PaymentCharts.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import {
+ Bar,
+ BarChart,
+ CartesianGrid,
+ Cell,
+ Pie,
+ PieChart,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from "recharts";
+
+const COLORS = ["#0d9488", "#0284c7", "#d97706", "#e11d48", "#6366f1", "#64748b"];
+
+export function PaymentBarChart({
+ data,
+ dataKey = "value",
+ nameKey = "name",
+}: {
+ data: { name: string; value: number }[];
+ dataKey?: string;
+ nameKey?: string;
+}) {
+ if (!data.length) {
+ return
دادهای برای نمودار نیست.
;
+ }
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function PaymentDonutChart({ data }: { data: { name: string; value: number }[] }) {
+ if (!data.length) {
+ return دادهای برای نمودار نیست.
;
+ }
+ return (
+
+
+
+
+ {data.map((_, i) => (
+ |
+ ))}
+
+
+
+
+
+ );
+}
+
+export function PaymentStatCards({
+ items,
+}: {
+ items: { label: string; value: string | number; hint?: string }[];
+}) {
+ return (
+
+ {items.map((item) => (
+
+
{item.label}
+
{item.value}
+ {item.hint ?
{item.hint}
: null}
+
+ ))}
+
+ );
+}
diff --git a/frontend/modules/payment/components/PaymentListPage.tsx b/frontend/modules/payment/components/PaymentListPage.tsx
new file mode 100644
index 0000000..34dfb72
--- /dev/null
+++ b/frontend/modules/payment/components/PaymentListPage.tsx
@@ -0,0 +1,248 @@
+"use client";
+
+import { useMemo, useState } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { Download, RefreshCw, Search } from "lucide-react";
+import { Button, Input, Select } from "@/components/ds";
+import { useTenantId } from "@/hooks/useTenantId";
+import { PaymentStatusChip } from "@/modules/payment/components/PaymentStatusChip";
+import {
+ PaymentEmptyState,
+ PaymentPageError,
+ PaymentPageLoader,
+ exportToCsv,
+} from "@/modules/payment/pages/shared";
+import type { PaymentRecord } from "@/modules/payment/types";
+
+type Column = {
+ key: string;
+ header: string;
+ type?: "status" | "datetime" | "text";
+ render?: (row: PaymentRecord) => React.ReactNode;
+};
+
+type Props = {
+ title: string;
+ description?: string;
+ resourceKey: string;
+ columns: Column[];
+ fetcher: (tenantId: string) => Promise;
+ filterKey?: string;
+ detailHref?: (row: PaymentRecord) => string | null;
+ rowActions?: React.ReactNode | ((row: PaymentRecord, refresh: () => void) => React.ReactNode);
+ headerActions?: React.ReactNode;
+};
+
+const PAGE_SIZE = 10;
+
+export function PaymentListPage({
+ title,
+ description,
+ resourceKey,
+ columns,
+ fetcher,
+ filterKey = "status",
+ detailHref,
+ rowActions,
+ headerActions,
+}: Props) {
+ const { tenantId } = useTenantId();
+ const [search, setSearch] = useState("");
+ const [filter, setFilter] = useState("");
+ const [sortKey, setSortKey] = useState(columns[0]?.key || "created_at");
+ const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
+ const [page, setPage] = useState(1);
+
+ const q = useQuery({
+ queryKey: ["payment", tenantId, resourceKey],
+ queryFn: () => fetcher(tenantId!),
+ enabled: !!tenantId,
+ });
+
+ const rows = useMemo(() => {
+ let data = [...(q.data ?? [])];
+ if (filter) data = data.filter((r) => String(r[filterKey] ?? "") === filter);
+ if (search.trim()) {
+ const s = search.trim().toLowerCase();
+ data = data.filter((r) => JSON.stringify(r).toLowerCase().includes(s));
+ }
+ data.sort((a, b) => {
+ const av = String(a[sortKey] ?? "");
+ const bv = String(b[sortKey] ?? "");
+ return sortDir === "asc" ? av.localeCompare(bv) : bv.localeCompare(av);
+ });
+ return data;
+ }, [q.data, filter, filterKey, search, sortKey, sortDir]);
+
+ const filterOptions = useMemo(() => {
+ const set = new Set((q.data ?? []).map((r) => String(r[filterKey] ?? "")).filter(Boolean));
+ return Array.from(set);
+ }, [q.data, filterKey]);
+
+ const totalPages = Math.max(1, Math.ceil(rows.length / PAGE_SIZE));
+ const pageRows = rows.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
+
+ if (!tenantId || q.isLoading) return ;
+ if (q.isError) return q.refetch()} />;
+
+ return (
+
+
+
+
{title}
+ {description ?
{description}
: null}
+
+
+ {headerActions}
+
+
+
+
+
+
+
+
+ {
+ setSearch(e.target.value);
+ setPage(1);
+ }}
+ aria-label="جستجو"
+ />
+
+
+
+
+
+ {rows.length === 0 ? (
+
+ ) : (
+
+
+
+
+ {columns.map((c) => (
+ |
+ {c.header}
+ |
+ ))}
+ {(detailHref || rowActions) && (
+ عملیات |
+ )}
+
+
+
+ {pageRows.map((row, idx) => (
+
+ {columns.map((c) => (
+ |
+ {c.render
+ ? c.render(row)
+ : c.type === "status"
+ ?
+ : c.type === "datetime" && row[c.key]
+ ? new Date(String(row[c.key])).toLocaleString("fa-IR")
+ : String(row[c.key] ?? "—")}
+ |
+ ))}
+ {(detailHref || rowActions) && (
+
+
+ {detailHref?.(row) ? (
+
+ جزئیات
+
+ ) : null}
+ {typeof rowActions === "function"
+ ? rowActions(row, () => q.refetch())
+ : rowActions}
+
+ |
+ )}
+
+ ))}
+
+
+
+ )}
+
+
+
+ {rows.length} مورد — صفحه {page} از {totalPages}
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/modules/payment/components/PaymentPortalShell.tsx b/frontend/modules/payment/components/PaymentPortalShell.tsx
new file mode 100644
index 0000000..79a60e3
--- /dev/null
+++ b/frontend/modules/payment/components/PaymentPortalShell.tsx
@@ -0,0 +1,184 @@
+"use client";
+
+import { useMemo, useState } from "react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { ChevronDown, Moon, Sun, Menu, X } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { useColorMode } from "@/components/providers/ColorModeProvider";
+import { Button } from "@/components/ds";
+import {
+ PAYMENT_PORTAL,
+ navForPayment,
+ filterNavByBundles,
+} from "@/modules/payment/constants/portals";
+import { usePaymentCapabilities } from "@/modules/payment/hooks/usePaymentCapabilities";
+
+function isActive(pathname: string, href: string) {
+ const base = href.split("?")[0];
+ if (base === "/payment") return pathname === "/payment";
+ return pathname === base || pathname.startsWith(`${base}/`);
+}
+
+export function PaymentPortalShell({
+ children,
+ title,
+}: {
+ children: React.ReactNode;
+ title?: string;
+}) {
+ const pathname = usePathname();
+ const { mode, toggle } = useColorMode();
+ const caps = usePaymentCapabilities();
+ const [openGroups, setOpenGroups] = useState>({});
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ const nav = useMemo(
+ () => filterNavByBundles(navForPayment(), caps.data?.active_bundles),
+ [caps.data?.active_bundles]
+ );
+
+ const autoOpen = useMemo(() => {
+ const map: Record = {};
+ for (const g of nav) {
+ if (g.href && isActive(pathname, g.href)) map[g.id] = true;
+ if (g.items?.some((i) => isActive(pathname, i.href))) map[g.id] = true;
+ }
+ return map;
+ }, [pathname, nav]);
+
+ const expanded = { ...autoOpen, ...openGroups };
+
+ const sidebar = (
+ <>
+
+
تربتپی
+
{title ?? PAYMENT_PORTAL.label}
+
+
+
+
+ هاب تربتپی
+
+
+
+ >
+ );
+
+ return (
+
+
+ {mobileOpen ? (
+
+
+ ) : null}
+
+
+
+ {PAYMENT_PORTAL.label}
+
+ {children}
+
+
+ );
+}
diff --git a/frontend/modules/payment/components/PaymentStatusChip.tsx b/frontend/modules/payment/components/PaymentStatusChip.tsx
new file mode 100644
index 0000000..62efa9b
--- /dev/null
+++ b/frontend/modules/payment/components/PaymentStatusChip.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+
+const COLORS: Record = {
+ paid: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300",
+ recorded: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300",
+ active: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300",
+ enabled: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300",
+ healthy: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300",
+ pending: "bg-amber-500/15 text-amber-700 dark:text-amber-300",
+ pending_review: "bg-amber-500/15 text-amber-700 dark:text-amber-300",
+ redirect_issued: "bg-sky-500/15 text-sky-700 dark:text-sky-300",
+ processing: "bg-sky-500/15 text-sky-700 dark:text-sky-300",
+ draft: "bg-slate-500/15 text-slate-700 dark:text-slate-300",
+ failed: "bg-rose-500/15 text-rose-700 dark:text-rose-300",
+ cancelled: "bg-slate-500/15 text-slate-600 dark:text-slate-400",
+ expired: "bg-slate-500/15 text-slate-600 dark:text-slate-400",
+ suspended: "bg-orange-500/15 text-orange-700 dark:text-orange-300",
+ closed: "bg-slate-500/15 text-slate-600 dark:text-slate-400",
+ disabled: "bg-slate-500/15 text-slate-600 dark:text-slate-400",
+};
+
+export function PaymentStatusChip({ status }: { status?: string | null }) {
+ const key = (status || "unknown").toLowerCase();
+ return (
+
+ {status || "—"}
+
+ );
+}
diff --git a/frontend/modules/payment/components/createPortalLayout.tsx b/frontend/modules/payment/components/createPortalLayout.tsx
new file mode 100644
index 0000000..8a32d35
--- /dev/null
+++ b/frontend/modules/payment/components/createPortalLayout.tsx
@@ -0,0 +1,41 @@
+"use client";
+
+import { AuthGuard } from "@/components/AuthGuard";
+import { useMe } from "@/hooks/useMe";
+import { useRouter, usePathname } from "next/navigation";
+import { useEffect } from "react";
+import { LoadingState, ErrorState } from "@/components/ds";
+import { PaymentPortalShell } from "@/modules/payment/components/PaymentPortalShell";
+
+function PortalGate({ children }: { children: React.ReactNode }) {
+ const { me, loading, error, reload } = useMe();
+ const router = useRouter();
+
+ useEffect(() => {
+ if (loading) return;
+ if (me?.onboarding_required) router.replace("/onboarding");
+ }, [me, loading, router]);
+
+ if (loading) return ;
+ if (error) return ;
+ if (!me?.current_tenant_id) {
+ return ;
+ }
+
+ return {children};
+}
+
+export function PaymentRootLayout({ children }: { children: React.ReactNode }) {
+ const pathname = usePathname();
+ const isHub = pathname === "/payment/hub";
+
+ if (isHub) {
+ return {children};
+ }
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/frontend/modules/payment/constants/permissions.ts b/frontend/modules/payment/constants/permissions.ts
new file mode 100644
index 0000000..682175a
--- /dev/null
+++ b/frontend/modules/payment/constants/permissions.ts
@@ -0,0 +1,27 @@
+export const PAYMENT_VIEW = "payment.view";
+export const PAYMENT_MANAGE = "payment.manage";
+export const PAYMENT_AUDIT_VIEW = "payment.audit.view";
+export const PAYMENT_SETTINGS_MANAGE = "payment.settings.manage";
+export const PAYMENT_PSP_VIEW = "payment.psp_connections.view";
+export const PAYMENT_PSP_MANAGE = "payment.psp_connections.manage";
+export const PAYMENT_MERCHANT_VIEW = "payment.merchant_accounts.view";
+export const PAYMENT_MERCHANT_MANAGE = "payment.merchant_accounts.manage";
+export const PAYMENT_REQUEST_VIEW = "payment.requests.view";
+export const PAYMENT_REQUEST_CREATE = "payment.requests.create";
+export const PAYMENT_TX_VIEW = "payment.transactions.view";
+export const PAYMENT_LEDGER_VIEW = "payment.ledger.view";
+
+export const ALL_PAYMENT_PERMISSIONS = [
+ PAYMENT_VIEW,
+ PAYMENT_MANAGE,
+ PAYMENT_AUDIT_VIEW,
+ PAYMENT_SETTINGS_MANAGE,
+ PAYMENT_PSP_VIEW,
+ PAYMENT_PSP_MANAGE,
+ PAYMENT_MERCHANT_VIEW,
+ PAYMENT_MERCHANT_MANAGE,
+ PAYMENT_REQUEST_VIEW,
+ PAYMENT_REQUEST_CREATE,
+ PAYMENT_TX_VIEW,
+ PAYMENT_LEDGER_VIEW,
+] as const;
diff --git a/frontend/modules/payment/constants/portals.ts b/frontend/modules/payment/constants/portals.ts
new file mode 100644
index 0000000..80377de
--- /dev/null
+++ b/frontend/modules/payment/constants/portals.ts
@@ -0,0 +1,158 @@
+import type { LucideIcon } from "lucide-react";
+import {
+ LayoutDashboard,
+ CreditCard,
+ Building2,
+ Settings,
+ Shield,
+ Activity,
+ BarChart3,
+ Wallet,
+ Plug,
+ FileText,
+ BookOpen,
+ KeyRound,
+ Gauge,
+ ScrollText,
+ Network,
+ Eye,
+} from "lucide-react";
+
+export type PortalNavItem = { href: string; label: string; bundle?: string | null };
+export type PortalNavGroup = {
+ id: string;
+ label: string;
+ icon: LucideIcon;
+ href?: string;
+ bundle?: string | null;
+ items?: PortalNavItem[];
+};
+
+export const PAYMENT_PORTAL = {
+ id: "payment" as const,
+ label: "تربتپی",
+ description: "پلتفرم پرداخت سازمانی — BYO-PSP و پذیرنده",
+ basePath: "/payment",
+ icon: CreditCard,
+};
+
+export const PAYMENT_NAV: PortalNavGroup[] = [
+ {
+ id: "dashboards",
+ label: "داشبوردها",
+ icon: LayoutDashboard,
+ items: [
+ { href: "/payment/dashboard", label: "داشبورد اجرایی" },
+ { href: "/payment/overview", label: "نمای کلی" },
+ { href: "/payment/kpis", label: "شاخصهای کلیدی" },
+ ],
+ },
+ {
+ id: "operations",
+ label: "عملیات پرداخت",
+ icon: Wallet,
+ items: [
+ { href: "/payment/requests", label: "درخواستهای پرداخت" },
+ { href: "/payment/transactions", label: "تراکنشها" },
+ { href: "/payment/ledger", label: "دفتر کل" },
+ { href: "/payment/callbacks", label: "کالبکها" },
+ { href: "/payment/verification", label: "تأیید پرداخت" },
+ ],
+ },
+ {
+ id: "psp",
+ label: "درگاهها (PSP)",
+ icon: Plug,
+ bundle: "payment.byo_psp.basic",
+ items: [
+ { href: "/payment/psp", label: "مدیریت درگاه", bundle: "payment.byo_psp.basic" },
+ { href: "/payment/psp/health", label: "سلامت درگاه", bundle: "payment.byo_psp.basic" },
+ { href: "/payment/providers", label: "تخصیص ارائهدهنده" },
+ ],
+ },
+ {
+ id: "merchants",
+ label: "پذیرندگان",
+ icon: Building2,
+ bundle: "payment.torbat_pay.merchant",
+ items: [
+ { href: "/payment/merchants", label: "حسابها", bundle: "payment.torbat_pay.merchant" },
+ { href: "/payment/merchants/approvals", label: "تأییدها", bundle: "payment.torbat_pay.merchant" },
+ ],
+ },
+ {
+ id: "licensing",
+ label: "مجوز و باندل",
+ icon: KeyRound,
+ items: [
+ { href: "/payment/bundles", label: "باندلها" },
+ { href: "/payment/feature-toggles", label: "کلیدهای ویژگی" },
+ { href: "/payment/capabilities", label: "قابلیتها" },
+ ],
+ },
+ {
+ id: "ops",
+ label: "عملیات پلتفرم",
+ icon: Activity,
+ items: [
+ { href: "/payment/monitoring", label: "مانیتورینگ" },
+ { href: "/payment/metrics", label: "متریکها" },
+ { href: "/payment/audit", label: "حسابرسی" },
+ { href: "/payment/settings", label: "تنظیمات" },
+ ],
+ },
+ {
+ id: "dev",
+ label: "توسعهدهنده",
+ icon: Network,
+ items: [
+ { href: "/payment/api-explorer", label: "کاوشگر API" },
+ { href: "/payment/openapi", label: "OpenAPI" },
+ ],
+ },
+];
+
+export function navForPayment() {
+ return PAYMENT_NAV;
+}
+
+export function filterNavByBundles(
+ nav: PortalNavGroup[],
+ activeBundles: string[] | undefined
+) {
+ const bundles = new Set(activeBundles ?? []);
+ return nav
+ .map((g) => {
+ if (g.bundle && !bundles.has(g.bundle)) return null;
+ const items = g.items?.filter((i) => !i.bundle || bundles.has(i.bundle));
+ if (g.items && (!items || items.length === 0)) return null;
+ return { ...g, items };
+ })
+ .filter(Boolean) as PortalNavGroup[];
+}
+
+export const MVP_ROUTE_HREFS = [
+ "/payment/hub",
+ "/payment/dashboard",
+ "/payment/overview",
+ "/payment/kpis",
+ "/payment/transactions",
+ "/payment/ledger",
+ "/payment/psp",
+ "/payment/psp/health",
+ "/payment/providers",
+ "/payment/merchants",
+ "/payment/merchants/approvals",
+ "/payment/requests",
+ "/payment/callbacks",
+ "/payment/verification",
+ "/payment/audit",
+ "/payment/bundles",
+ "/payment/feature-toggles",
+ "/payment/capabilities",
+ "/payment/settings",
+ "/payment/metrics",
+ "/payment/monitoring",
+ "/payment/api-explorer",
+ "/payment/openapi",
+] as const;
diff --git a/frontend/modules/payment/features/apiExplorer.tsx b/frontend/modules/payment/features/apiExplorer.tsx
new file mode 100644
index 0000000..8a80fe3
--- /dev/null
+++ b/frontend/modules/payment/features/apiExplorer.tsx
@@ -0,0 +1,44 @@
+"use client";
+
+import { PAYMENT_API_CATALOG } from "@/modules/payment/services/payment-api";
+import { PageHeader, Card, CardContent, Badge } from "@/components/ds";
+
+export const PaymentApiExplorer = function PaymentApiExplorerPage() {
+ return (
+
+
+
+
+
+
+ | متد |
+ مسیر |
+ فاز |
+
+
+
+ {PAYMENT_API_CATALOG.map((row) => (
+
+ | {row.method} |
+ {row.path} |
+
+ {row.phase}
+ |
+
+ ))}
+
+
+
+
+
+ BFF: /api/payment/* — upstream port 8012. هدر Idempotency-Key برای POST درخواست پرداخت الزامی است.
+
+
+
+ );
+};
+
+export default PaymentApiExplorer;
diff --git a/frontend/modules/payment/features/audit.tsx b/frontend/modules/payment/features/audit.tsx
new file mode 100644
index 0000000..9e766f3
--- /dev/null
+++ b/frontend/modules/payment/features/audit.tsx
@@ -0,0 +1,24 @@
+"use client";
+
+import { PaymentListPage } from "@/modules/payment/components/PaymentListPage";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+
+export const PaymentAudit = function PaymentAuditPage() {
+ return (
+ paymentApi.audit.list(tenantId)}
+ filterKey="entity_type"
+ columns={[
+ { key: "action", header: "عمل" },
+ { key: "entity_type", header: "نوع موجودیت" },
+ { key: "entity_id", header: "شناسه" },
+ { key: "created_at", header: "تاریخ", type: "datetime" },
+ ]}
+ />
+ );
+};
+
+export default PaymentAudit;
diff --git a/frontend/modules/payment/features/bundles.tsx b/frontend/modules/payment/features/bundles.tsx
new file mode 100644
index 0000000..8d8fa18
--- /dev/null
+++ b/frontend/modules/payment/features/bundles.tsx
@@ -0,0 +1,24 @@
+"use client";
+
+import { PaymentListPage } from "@/modules/payment/components/PaymentListPage";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+
+export const PaymentBundles = function PaymentBundlesPage() {
+ return (
+ paymentApi.tenantBundles.list(tenantId)}
+ filterKey="bundle_code"
+ columns={[
+ { key: "bundle_code", header: "کد باندل" },
+ { key: "is_active", header: "فعال", render: (r) => (r.is_active ? "بله" : "خیر") },
+ { key: "activated_at", header: "فعالسازی", type: "datetime" },
+ { key: "created_at", header: "تاریخ", type: "datetime" },
+ ]}
+ />
+ );
+};
+
+export default PaymentBundles;
diff --git a/frontend/modules/payment/features/callbacks.tsx b/frontend/modules/payment/features/callbacks.tsx
new file mode 100644
index 0000000..56c700d
--- /dev/null
+++ b/frontend/modules/payment/features/callbacks.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import Link from "next/link";
+import { useTenantId } from "@/hooks/useTenantId";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+import { PaymentPageLoader, PaymentPageError, PaymentEmptyState } from "@/modules/payment/pages/shared";
+import { PageHeader, Card, CardContent } from "@/components/ds";
+import { PaymentStatusChip } from "@/modules/payment/components/PaymentStatusChip";
+
+export const PaymentCallbacks = function PaymentCallbacksPage() {
+ const { tenantId } = useTenantId();
+
+ const q = useQuery({
+ queryKey: ["payment", tenantId, "callbacks"],
+ queryFn: async () => {
+ const [audit, requests] = await Promise.all([
+ paymentApi.audit.list(tenantId!),
+ paymentApi.requests.list(tenantId!),
+ ]);
+ const callbackAudit = audit.filter(
+ (a) =>
+ String(a.entity_type ?? "").includes("Callback") ||
+ String(a.action ?? "").toLowerCase().includes("callback")
+ );
+ const awaiting = requests.filter((r) => r.status === "redirect_issued");
+ return { callbackAudit, awaiting };
+ },
+ enabled: !!tenantId,
+ });
+
+ if (!tenantId || q.isLoading) return ;
+ if (q.error) return q.refetch()} />;
+
+ const { callbackAudit, awaiting } = q.data!;
+
+ return (
+
+
+
+
+ در انتظار callback ({awaiting.length})
+ {awaiting.length === 0 ? (
+
+ ) : (
+
+ {awaiting.map((r) => (
+ -
+
+ {String(r.amount_minor)} {String(r.currency)}
+
+
+
+ ))}
+
+ )}
+
+
+
+
+ لاگ حسابرسی callback
+ {callbackAudit.length === 0 ? (
+ رویداد callback در audit ثبت نشده.
+ ) : (
+
+ {callbackAudit.map((a) => (
+ -
+ {String(a.action)} — {String(a.entity_type)} —{" "}
+ {a.created_at ? new Date(String(a.created_at)).toLocaleString("fa-IR") : "—"}
+
+ ))}
+
+ )}
+
+
+
+ );
+};
+
+export default PaymentCallbacks;
diff --git a/frontend/modules/payment/features/capabilities.tsx b/frontend/modules/payment/features/capabilities.tsx
new file mode 100644
index 0000000..7d10f09
--- /dev/null
+++ b/frontend/modules/payment/features/capabilities.tsx
@@ -0,0 +1,55 @@
+"use client";
+
+import { usePaymentCapabilities } from "@/modules/payment/hooks/usePaymentCapabilities";
+import { PaymentPageLoader, PaymentPageError } from "@/modules/payment/pages/shared";
+import { PageHeader, Card, CardContent, Badge } from "@/components/ds";
+
+export const PaymentCapabilities = function PaymentCapabilitiesPage() {
+ const q = usePaymentCapabilities();
+ if (q.isLoading) return ;
+ if (q.error) return q.refetch()} />;
+
+ const toggles = q.data?.feature_toggles ?? {};
+
+ return (
+
+
+
+
+ workspace: {q.data?.workspace_status}
+ حالت پیشفرض: {q.data?.default_payment_mode}
+
+
+
+ {Object.entries(toggles).map(([k, v]) => (
+
+
+ {k}
+ {v ? "فعال" : "غیرفعال"}
+
+
+ ))}
+ {Object.keys(toggles).length === 0 ? (
+
کلید ویژگی از capabilities دریافت نشد.
+ ) : null}
+
+ {q.data?.active_bundles ? (
+
+
+ باندلهای فعال
+
+ {q.data.active_bundles.map((b) => (
+ - {b}
+ ))}
+
+
+
+ ) : null}
+
+ );
+};
+
+export default PaymentCapabilities;
diff --git a/frontend/modules/payment/features/dashboard.tsx b/frontend/modules/payment/features/dashboard.tsx
new file mode 100644
index 0000000..e4d1812
--- /dev/null
+++ b/frontend/modules/payment/features/dashboard.tsx
@@ -0,0 +1,137 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import Link from "next/link";
+import { useTenantId } from "@/hooks/useTenantId";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+import {
+ usePaymentCapabilities,
+ usePaymentMetrics,
+} from "@/modules/payment/hooks/usePaymentCapabilities";
+import { PaymentPageLoader, PaymentPageError } from "@/modules/payment/pages/shared";
+import { PageHeader, Card, CardContent, Button, Badge } from "@/components/ds";
+import {
+ PaymentBarChart,
+ PaymentStatCards,
+} from "@/modules/payment/components/PaymentCharts";
+
+export const PaymentDashboard = function PaymentExecutiveDashboard() {
+ const { tenantId } = useTenantId();
+ const caps = usePaymentCapabilities();
+ const metrics = usePaymentMetrics();
+
+ const countsQ = useQuery({
+ queryKey: ["payment", tenantId, "dashboard-counts"],
+ queryFn: async () => {
+ const [requests, transactions, merchants, connections, audit] = await Promise.all([
+ paymentApi.requests.list(tenantId!),
+ paymentApi.transactions.list(tenantId!),
+ paymentApi.merchants.list(tenantId!).catch(() => []),
+ paymentApi.pspConnections.list(tenantId!).catch(() => []),
+ paymentApi.audit.list(tenantId!),
+ ]);
+ return {
+ requests: requests.length,
+ transactions: transactions.length,
+ merchants: merchants.length,
+ connections: connections.length,
+ paid: requests.filter((r) => r.status === "paid").length,
+ pending: requests.filter((r) =>
+ ["pending", "redirect_issued"].includes(String(r.status))
+ ).length,
+ recentRequests: requests.slice(0, 5),
+ recentAudit: audit.slice(0, 8),
+ };
+ },
+ enabled: !!tenantId,
+ });
+
+ if (!tenantId || countsQ.isLoading || caps.isLoading) return ;
+ if (countsQ.error) return countsQ.refetch()} />;
+
+ const c = countsQ.data!;
+ const chartData = [
+ { name: "درخواست", value: c.requests },
+ { name: "تراکنش", value: c.transactions },
+ { name: "پذیرنده", value: c.merchants },
+ { name: "درگاه", value: c.connections },
+ ];
+
+ return (
+
+
Torbat Pay v{caps.data?.version}}
+ />
+
+
+
+
+ توزیع منابع
+
+
+
+
+
+ اقدامات سریع
+
+
+
+
+
+
+
+
+
+
+
+
+
+ آخرین درخواستها
+
+ {c.recentRequests.map((r) => (
+ -
+
+ {String(r.amount_minor)} {String(r.currency)} — {String(r.status)}
+
+
+ ))}
+ {c.recentRequests.length === 0 ? - درخواستی نیست
: null}
+
+
+
+
+
+ فعالیت اخیر (حسابرسی)
+
+ {c.recentAudit.map((a) => (
+ -
+ {String(a.action)} — {String(a.entity_type)} —{" "}
+ {a.created_at ? new Date(String(a.created_at)).toLocaleString("fa-IR") : "—"}
+
+ ))}
+ {c.recentAudit.length === 0 ? - فعالیتی ثبت نشده
: null}
+
+
+
+
+ {metrics.data ? (
+
+
+ فاز {metrics.data.phase} — سرویس {metrics.data.service}
+
+
+ ) : null}
+
+ );
+};
+
+export default PaymentDashboard;
diff --git a/frontend/modules/payment/features/featureToggles.tsx b/frontend/modules/payment/features/featureToggles.tsx
new file mode 100644
index 0000000..e72a742
--- /dev/null
+++ b/frontend/modules/payment/features/featureToggles.tsx
@@ -0,0 +1,23 @@
+"use client";
+
+import { PaymentListPage } from "@/modules/payment/components/PaymentListPage";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+
+export const PaymentFeatureToggles = function PaymentFeatureTogglesPage() {
+ return (
+ paymentApi.featureToggles.list(tenantId)}
+ filterKey="key"
+ columns={[
+ { key: "key", header: "کلید" },
+ { key: "enabled", header: "فعال", render: (r) => (r.enabled ? "بله" : "خیر") },
+ { key: "created_at", header: "تاریخ", type: "datetime" },
+ ]}
+ />
+ );
+};
+
+export default PaymentFeatureToggles;
diff --git a/frontend/modules/payment/features/gatewayHealth.tsx b/frontend/modules/payment/features/gatewayHealth.tsx
new file mode 100644
index 0000000..87abac7
--- /dev/null
+++ b/frontend/modules/payment/features/gatewayHealth.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { useTenantId } from "@/hooks/useTenantId";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+import { usePaymentHealth } from "@/modules/payment/hooks/usePaymentCapabilities";
+import { PaymentPageLoader, PaymentPageError } from "@/modules/payment/pages/shared";
+import { PageHeader, Card, CardContent, Badge } from "@/components/ds";
+import { PaymentStatusChip } from "@/modules/payment/components/PaymentStatusChip";
+
+export const PaymentGatewayHealth = function PaymentGatewayHealthPage() {
+ const { tenantId } = useTenantId();
+ const health = usePaymentHealth();
+
+ const connectionsQ = useQuery({
+ queryKey: ["payment", tenantId, "psp-health"],
+ queryFn: () => paymentApi.pspConnections.list(tenantId!),
+ enabled: !!tenantId,
+ });
+
+ if (!tenantId || connectionsQ.isLoading || health.isLoading) return ;
+ if (connectionsQ.error) return connectionsQ.refetch()} />;
+
+ return (
+
+
+ {health.data?.status ?? "—"}
+
+ }
+ />
+
+
+ سرویس: {health.data?.service}
+ نسخه: {health.data?.version}
+ وضعیت: {health.data?.status}
+
+
+
+ {(connectionsQ.data ?? []).map((c) => (
+
+
+
+
{String(c.name ?? c.provider_code)}
+
{String(c.provider_code)}
+
+
+
+
+ ))}
+ {(connectionsQ.data ?? []).length === 0 ? (
+
اتصال PSP ثبت نشده — از بخش مدیریت درگاه اضافه کنید.
+ ) : null}
+
+
+ );
+};
+
+export default PaymentGatewayHealth;
diff --git a/frontend/modules/payment/features/hub.tsx b/frontend/modules/payment/features/hub.tsx
new file mode 100644
index 0000000..8ceb723
--- /dev/null
+++ b/frontend/modules/payment/features/hub.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import Link from "next/link";
+import { useMe } from "@/hooks/useMe";
+import { PAYMENT_PORTAL } from "@/modules/payment/constants/portals";
+import { Card, CardContent, Button, LoadingState, ErrorState } from "@/components/ds";
+
+export const PaymentHub = function PaymentHubPage() {
+ const { me, loading, error, reload } = useMe();
+ if (loading) return ;
+ if (error) return ;
+ if (!me?.current_tenant_id) {
+ return ;
+ }
+
+ return (
+
+
💳
+
+
{PAYMENT_PORTAL.label}
+
{PAYMENT_PORTAL.description}
+
+
+
+ {PAYMENT_PORTAL.label}
+ {PAYMENT_PORTAL.description}
+
+
+
+
+
+
+ );
+};
+
+export default PaymentHub;
diff --git a/frontend/modules/payment/features/kpis.tsx b/frontend/modules/payment/features/kpis.tsx
new file mode 100644
index 0000000..a5b1b78
--- /dev/null
+++ b/frontend/modules/payment/features/kpis.tsx
@@ -0,0 +1,94 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { useTenantId } from "@/hooks/useTenantId";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+import { usePaymentMetrics } from "@/modules/payment/hooks/usePaymentCapabilities";
+import { PaymentPageLoader, PaymentPageError } from "@/modules/payment/pages/shared";
+import { PageHeader, Card, CardContent } from "@/components/ds";
+import {
+ PaymentBarChart,
+ PaymentDonutChart,
+ PaymentStatCards,
+} from "@/modules/payment/components/PaymentCharts";
+
+export const PaymentKpis = function PaymentKpisPage() {
+ const { tenantId } = useTenantId();
+ const metrics = usePaymentMetrics();
+
+ const kpiQ = useQuery({
+ queryKey: ["payment", tenantId, "kpis"],
+ queryFn: async () => {
+ const [requests, transactions] = await Promise.all([
+ paymentApi.requests.list(tenantId!),
+ paymentApi.transactions.list(tenantId!),
+ ]);
+ const byStatus: Record = {};
+ for (const r of requests) {
+ const s = String(r.status ?? "unknown");
+ byStatus[s] = (byStatus[s] ?? 0) + 1;
+ }
+ const totalAmount = requests.reduce((sum, r) => sum + Number(r.amount_minor ?? 0), 0);
+ const paidCount = requests.filter((r) => r.status === "paid").length;
+ const successRate = requests.length ? Math.round((paidCount / requests.length) * 100) : 0;
+ return {
+ totalRequests: requests.length,
+ totalTransactions: transactions.length,
+ totalAmount,
+ successRate,
+ statusChart: Object.entries(byStatus).map(([name, value]) => ({ name, value })),
+ sourceChart: Object.entries(
+ requests.reduce>((acc, r) => {
+ const k = String(r.payment_source ?? "unknown");
+ acc[k] = (acc[k] ?? 0) + 1;
+ return acc;
+ }, {})
+ ).map(([name, value]) => ({ name, value })),
+ };
+ },
+ enabled: !!tenantId,
+ });
+
+ if (!tenantId || kpiQ.isLoading || metrics.isLoading) return ;
+ if (kpiQ.error) return kpiQ.refetch()} />;
+
+ const k = kpiQ.data!;
+
+ return (
+
+
+
+
+
+
+ توزیع وضعیت
+
+
+
+
+
+ منبع پرداخت
+
+
+
+
+ {metrics.data?.metrics ? (
+
+
+ متریکهای سرویس
+ {JSON.stringify(metrics.data.metrics, null, 2)}
+
+
+ ) : null}
+
+ );
+};
+
+export default PaymentKpis;
diff --git a/frontend/modules/payment/features/ledger.tsx b/frontend/modules/payment/features/ledger.tsx
new file mode 100644
index 0000000..f145509
--- /dev/null
+++ b/frontend/modules/payment/features/ledger.tsx
@@ -0,0 +1,26 @@
+"use client";
+
+import { PaymentListPage } from "@/modules/payment/components/PaymentListPage";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+
+export const PaymentLedger = function PaymentLedgerPage() {
+ return (
+ paymentApi.ledger.list(tenantId)}
+ filterKey="direction"
+ columns={[
+ { key: "id", header: "شناسه" },
+ { key: "amount_minor", header: "مبلغ (ریال)" },
+ { key: "direction", header: "جهت" },
+ { key: "status", header: "وضعیت", type: "status" },
+ { key: "payment_transaction_id", header: "تراکنش" },
+ { key: "created_at", header: "تاریخ", type: "datetime" },
+ ]}
+ />
+ );
+};
+
+export default PaymentLedger;
diff --git a/frontend/modules/payment/features/merchantApprovals.tsx b/frontend/modules/payment/features/merchantApprovals.tsx
new file mode 100644
index 0000000..f3cfa15
--- /dev/null
+++ b/frontend/modules/payment/features/merchantApprovals.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import Link from "next/link";
+import { useTenantId } from "@/hooks/useTenantId";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+import { PaymentPageLoader, PaymentPageError, PaymentEmptyState } from "@/modules/payment/pages/shared";
+import { PageHeader, Card, CardContent, Button } from "@/components/ds";
+import { PaymentStatusChip } from "@/modules/payment/components/PaymentStatusChip";
+
+export const PaymentMerchantApprovals = function PaymentMerchantApprovalsPage() {
+ const { tenantId } = useTenantId();
+
+ const q = useQuery({
+ queryKey: ["payment", tenantId, "merchant-approvals"],
+ queryFn: async () => {
+ const all = await paymentApi.merchants.list(tenantId!);
+ return all.filter((m) => m.status === "pending_review");
+ },
+ enabled: !!tenantId,
+ });
+
+ if (!tenantId || q.isLoading) return ;
+ if (q.error) return q.refetch()} />;
+
+ const pending = q.data ?? [];
+
+ return (
+
+
+ {pending.length === 0 ? (
+
+ ) : (
+
+ {pending.map((m) => (
+
+
+
+
{String(m.name)}
+
{String(m.code)}
+
+
+
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default PaymentMerchantApprovals;
diff --git a/frontend/modules/payment/features/merchantDetail.tsx b/frontend/modules/payment/features/merchantDetail.tsx
new file mode 100644
index 0000000..36d5814
--- /dev/null
+++ b/frontend/modules/payment/features/merchantDetail.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import { useParams } from "next/navigation";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { toast } from "sonner";
+import { useTenantId } from "@/hooks/useTenantId";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+import { PaymentPageLoader, PaymentPageError } from "@/modules/payment/pages/shared";
+import { PageHeader, Card, CardContent, Button } from "@/components/ds";
+import { PaymentStatusChip } from "@/modules/payment/components/PaymentStatusChip";
+
+export const PaymentMerchantDetail = function PaymentMerchantDetailPage() {
+ const params = useParams();
+ const id = String(params?.id ?? "");
+ const { tenantId } = useTenantId();
+ const qc = useQueryClient();
+
+ const q = useQuery({
+ queryKey: ["payment", tenantId, "merchant", id],
+ queryFn: () => paymentApi.merchants.get(tenantId!, id),
+ enabled: !!tenantId && !!id,
+ });
+
+ const actionM = useMutation({
+ mutationFn: (action: "submit" | "activate" | "suspend" | "close") => {
+ const api = paymentApi.merchants[action];
+ return api(tenantId!, id);
+ },
+ onSuccess: () => {
+ toast.success("وضعیت بهروز شد");
+ qc.invalidateQueries({ queryKey: ["payment", tenantId, "merchant", id] });
+ },
+ onError: (e: Error) => toast.error(e.message),
+ });
+
+ if (!tenantId || q.isLoading) return ;
+ if (q.error) return q.refetch()} />;
+
+ const m = q.data!;
+ const status = String(m.status);
+
+ return (
+
+
+
+
+
+
+ {status === "draft" ? (
+
+ ) : null}
+ {status === "pending_review" ? (
+
+ ) : null}
+ {status === "active" ? (
+
+ ) : null}
+ {!["closed"].includes(status) ? (
+
+ ) : null}
+
+
+
+
+
+ {JSON.stringify(m, null, 2)}
+
+
+
+ );
+};
+
+export default PaymentMerchantDetail;
diff --git a/frontend/modules/payment/features/merchants.tsx b/frontend/modules/payment/features/merchants.tsx
new file mode 100644
index 0000000..d1af9f7
--- /dev/null
+++ b/frontend/modules/payment/features/merchants.tsx
@@ -0,0 +1,24 @@
+"use client";
+
+import { PaymentListPage } from "@/modules/payment/components/PaymentListPage";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+
+export const PaymentMerchants = function PaymentMerchantsPage() {
+ return (
+ paymentApi.merchants.list(tenantId)}
+ columns={[
+ { key: "code", header: "کد" },
+ { key: "name", header: "نام" },
+ { key: "status", header: "وضعیت", type: "status" },
+ { key: "created_at", header: "تاریخ", type: "datetime" },
+ ]}
+ detailHref={(row) => (row.id ? `/payment/merchants/${row.id}` : null)}
+ />
+ );
+};
+
+export default PaymentMerchants;
diff --git a/frontend/modules/payment/features/metrics.tsx b/frontend/modules/payment/features/metrics.tsx
new file mode 100644
index 0000000..dc10a89
--- /dev/null
+++ b/frontend/modules/payment/features/metrics.tsx
@@ -0,0 +1,24 @@
+"use client";
+
+import { usePaymentMetrics } from "@/modules/payment/hooks/usePaymentCapabilities";
+import { PaymentPageLoader, PaymentPageError } from "@/modules/payment/pages/shared";
+import { PageHeader, Card, CardContent } from "@/components/ds";
+
+export const PaymentMetrics = function PaymentMetricsPage() {
+ const q = usePaymentMetrics();
+ if (q.isLoading) return ;
+ if (q.error) return q.refetch()} />;
+
+ return (
+
+
+
+
+ {JSON.stringify(q.data?.metrics ?? {}, null, 2)}
+
+
+
+ );
+};
+
+export default PaymentMetrics;
diff --git a/frontend/modules/payment/features/monitoring.tsx b/frontend/modules/payment/features/monitoring.tsx
new file mode 100644
index 0000000..4f27750
--- /dev/null
+++ b/frontend/modules/payment/features/monitoring.tsx
@@ -0,0 +1,77 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { useTenantId } from "@/hooks/useTenantId";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+import { usePaymentHealth, usePaymentMetrics } from "@/modules/payment/hooks/usePaymentCapabilities";
+import { PaymentPageLoader, PaymentPageError } from "@/modules/payment/pages/shared";
+import { PageHeader, Card, CardContent, Badge } from "@/components/ds";
+import { PaymentStatCards } from "@/modules/payment/components/PaymentCharts";
+
+export const PaymentMonitoring = function PaymentMonitoringPage() {
+ const { tenantId } = useTenantId();
+ const health = usePaymentHealth();
+ const metrics = usePaymentMetrics();
+
+ const opsQ = useQuery({
+ queryKey: ["payment", tenantId, "monitoring"],
+ queryFn: async () => {
+ const [requests, transactions] = await Promise.all([
+ paymentApi.requests.list(tenantId!),
+ paymentApi.transactions.list(tenantId!),
+ ]);
+ const failed = requests.filter((r) => r.status === "failed").length;
+ const pending = requests.filter((r) =>
+ ["pending", "redirect_issued"].includes(String(r.status))
+ ).length;
+ return { failed, pending, requests: requests.length, transactions: transactions.length };
+ },
+ enabled: !!tenantId,
+ refetchInterval: 30_000,
+ });
+
+ if (!tenantId || health.isLoading || metrics.isLoading || opsQ.isLoading) {
+ return ;
+ }
+ if (opsQ.error) return opsQ.refetch()} />;
+
+ const ops = opsQ.data!;
+
+ return (
+
+
+ {health.data?.status ?? "—"}
+
+ }
+ />
+
+
+
+
+ Health
+ {JSON.stringify(health.data, null, 2)}
+
+
+
+
+ Metrics
+ {JSON.stringify(metrics.data?.metrics ?? {}, null, 2)}
+
+
+
+
+ );
+};
+
+export default PaymentMonitoring;
diff --git a/frontend/modules/payment/features/openapi.tsx b/frontend/modules/payment/features/openapi.tsx
new file mode 100644
index 0000000..05340eb
--- /dev/null
+++ b/frontend/modules/payment/features/openapi.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+import { PaymentPageLoader, PaymentPageError } from "@/modules/payment/pages/shared";
+import { PageHeader, Card, CardContent } from "@/components/ds";
+
+export const PaymentOpenApi = function PaymentOpenApiPage() {
+ const q = useQuery({
+ queryKey: ["payment", "openapi"],
+ queryFn: () => paymentApi.openapi(),
+ });
+
+ if (q.isLoading) return ;
+ if (q.error) return q.refetch()} />;
+
+ return (
+
+
+
+
+ {JSON.stringify(q.data, null, 2)}
+
+
+
+ );
+};
+
+export default PaymentOpenApi;
diff --git a/frontend/modules/payment/features/overview.tsx b/frontend/modules/payment/features/overview.tsx
new file mode 100644
index 0000000..d36a355
--- /dev/null
+++ b/frontend/modules/payment/features/overview.tsx
@@ -0,0 +1,80 @@
+"use client";
+
+import { usePaymentCapabilities, usePaymentHealth } from "@/modules/payment/hooks/usePaymentCapabilities";
+import { PaymentPageLoader, PaymentPageError } from "@/modules/payment/pages/shared";
+import { PageHeader, Card, CardContent, Badge } from "@/components/ds";
+import { PaymentStatCards } from "@/modules/payment/components/PaymentCharts";
+
+export const PaymentOverview = function PaymentOverviewPage() {
+ const caps = usePaymentCapabilities();
+ const health = usePaymentHealth();
+
+ if (caps.isLoading || health.isLoading) return ;
+ if (caps.error) return caps.refetch()} />;
+
+ const data = caps.data;
+
+ return (
+
+
+ {health.data?.status ?? "—"}
+
+ }
+ />
+
+
+
+
+ باندلهای فعال
+
+ {(data?.active_bundles ?? []).map((b) => (
+ - {b}
+ ))}
+ {(data?.active_bundles ?? []).length === 0 ? (
+ - باندلی فعال نیست
+ ) : null}
+
+
+
+
+
+ منابع پرداخت
+ پشتیبانیشده
+
+ {(data?.payment_sources_supported ?? []).map((s) => (
+ {s}
+ ))}
+
+ رزرو شده
+
+ {(data?.payment_sources_reserved ?? []).map((s) => (
+ {s}
+ ))}
+
+
+
+
+ {data?.contract_versions ? (
+
+
+ نسخه قراردادها
+ {JSON.stringify(data.contract_versions, null, 2)}
+
+
+ ) : null}
+
+ );
+};
+
+export default PaymentOverview;
diff --git a/frontend/modules/payment/features/providers.tsx b/frontend/modules/payment/features/providers.tsx
new file mode 100644
index 0000000..9f3ea8b
--- /dev/null
+++ b/frontend/modules/payment/features/providers.tsx
@@ -0,0 +1,25 @@
+"use client";
+
+import { PaymentListPage } from "@/modules/payment/components/PaymentListPage";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+
+export const PaymentProviders = function PaymentProvidersPage() {
+ return (
+ paymentApi.providerAssignments.list(tenantId)}
+ filterKey="source_type"
+ columns={[
+ { key: "source_type", header: "نوع منبع" },
+ { key: "source_id", header: "شناسه منبع" },
+ { key: "scope_type", header: "محدوده" },
+ { key: "priority", header: "اولویت" },
+ { key: "is_active", header: "فعال", render: (r) => (r.is_active ? "بله" : "خیر") },
+ ]}
+ />
+ );
+};
+
+export default PaymentProviders;
diff --git a/frontend/modules/payment/features/psp.tsx b/frontend/modules/payment/features/psp.tsx
new file mode 100644
index 0000000..e71b9d5
--- /dev/null
+++ b/frontend/modules/payment/features/psp.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { toast } from "sonner";
+import { Button } from "@/components/ds";
+import { PaymentListPage } from "@/modules/payment/components/PaymentListPage";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+import { useTenantId } from "@/hooks/useTenantId";
+import type { PaymentRecord } from "@/modules/payment/types";
+
+function TestConnectionButton({ row, refresh }: { row: PaymentRecord; refresh: () => void }) {
+ const { tenantId } = useTenantId();
+ const qc = useQueryClient();
+ const testM = useMutation({
+ mutationFn: () => paymentApi.pspConnections.test(tenantId!, String(row.id)),
+ onSuccess: () => {
+ toast.success("تست اتصال موفق");
+ refresh();
+ qc.invalidateQueries({ queryKey: ["payment", tenantId] });
+ },
+ onError: (e: Error) => toast.error(e.message),
+ });
+ return (
+
+ );
+}
+
+export const PaymentPsp = function PaymentPspPage() {
+ return (
+ paymentApi.pspConnections.list(tenantId)}
+ columns={[
+ { key: "provider_code", header: "ارائهدهنده" },
+ { key: "name", header: "نام" },
+ { key: "status", header: "وضعیت", type: "status" },
+ { key: "created_at", header: "تاریخ", type: "datetime" },
+ ]}
+ rowActions={(row, refresh) => }
+ />
+ );
+};
+
+export default PaymentPsp;
diff --git a/frontend/modules/payment/features/requestDetail.tsx b/frontend/modules/payment/features/requestDetail.tsx
new file mode 100644
index 0000000..b4bbb15
--- /dev/null
+++ b/frontend/modules/payment/features/requestDetail.tsx
@@ -0,0 +1,98 @@
+"use client";
+
+import { useParams } from "next/navigation";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { toast } from "sonner";
+import { useTenantId } from "@/hooks/useTenantId";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+import { PaymentPageLoader, PaymentPageError } from "@/modules/payment/pages/shared";
+import { PageHeader, Card, CardContent, Button } from "@/components/ds";
+import { PaymentStatusChip } from "@/modules/payment/components/PaymentStatusChip";
+
+export const PaymentRequestDetail = function PaymentRequestDetailPage() {
+ const params = useParams();
+ const id = String(params?.id ?? "");
+ const { tenantId } = useTenantId();
+ const qc = useQueryClient();
+
+ const q = useQuery({
+ queryKey: ["payment", tenantId, "request", id],
+ queryFn: () => paymentApi.requests.get(tenantId!, id),
+ enabled: !!tenantId && !!id,
+ });
+
+ const cancelM = useMutation({
+ mutationFn: () => paymentApi.requests.cancel(tenantId!, id),
+ onSuccess: () => {
+ toast.success("درخواست لغو شد");
+ qc.invalidateQueries({ queryKey: ["payment", tenantId, "request", id] });
+ },
+ onError: (e: Error) => toast.error(e.message),
+ });
+
+ const verifyM = useMutation({
+ mutationFn: () => paymentApi.requests.verify(tenantId!, id, { status: "paid" }),
+ onSuccess: () => {
+ toast.success("پرداخت تأیید شد");
+ qc.invalidateQueries({ queryKey: ["payment", tenantId, "request", id] });
+ },
+ onError: (e: Error) => toast.error(e.message),
+ });
+
+ if (!tenantId || q.isLoading) return ;
+ if (q.error) return q.refetch()} />;
+
+ const r = q.data!;
+ const status = String(r.status);
+ const canCancel = ["draft", "pending", "redirect_issued"].includes(status);
+ const canVerify = ["redirect_issued", "pending"].includes(status);
+
+ return (
+
+
+
+
+
+ مبلغ: {String(r.amount_minor)} {String(r.currency)}
+ منبع: {String(r.payment_source)}
+ PSP: {String(r.provider_code)}
+ {r.redirect_url ? (
+
+ redirect:{" "}
+
+ {String(r.redirect_url)}
+
+
+ ) : null}
+ idempotency: {String(r.idempotency_key ?? "—")}
+
+ {canCancel ? (
+
+ ) : null}
+ {canVerify ? (
+
+ ) : null}
+
+
+
+
+
+ داده کامل
+ {JSON.stringify(r, null, 2)}
+
+
+
+ );
+};
+
+export default PaymentRequestDetail;
diff --git a/frontend/modules/payment/features/requests.tsx b/frontend/modules/payment/features/requests.tsx
new file mode 100644
index 0000000..1a47098
--- /dev/null
+++ b/frontend/modules/payment/features/requests.tsx
@@ -0,0 +1,26 @@
+"use client";
+
+import { PaymentListPage } from "@/modules/payment/components/PaymentListPage";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+
+export const PaymentRequests = function PaymentRequestsPage() {
+ return (
+ paymentApi.requests.list(tenantId)}
+ columns={[
+ { key: "amount_minor", header: "مبلغ (ریال)" },
+ { key: "currency", header: "ارز" },
+ { key: "payment_source", header: "منبع" },
+ { key: "provider_code", header: "PSP" },
+ { key: "status", header: "وضعیت", type: "status" },
+ { key: "created_at", header: "تاریخ", type: "datetime" },
+ ]}
+ detailHref={(row) => (row.id ? `/payment/requests/${row.id}` : null)}
+ />
+ );
+};
+
+export default PaymentRequests;
diff --git a/frontend/modules/payment/features/settings.tsx b/frontend/modules/payment/features/settings.tsx
new file mode 100644
index 0000000..e9617b4
--- /dev/null
+++ b/frontend/modules/payment/features/settings.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import { PaymentListPage } from "@/modules/payment/components/PaymentListPage";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+
+export const PaymentSettings = function PaymentSettingsPage() {
+ return (
+ paymentApi.settings.list(tenantId)}
+ filterKey="key"
+ columns={[
+ { key: "key", header: "کلید" },
+ {
+ key: "value",
+ header: "مقدار",
+ render: (r) => (
+ {JSON.stringify(r.value ?? r.data ?? "—")}
+ ),
+ },
+ { key: "created_at", header: "تاریخ", type: "datetime" },
+ ]}
+ />
+ );
+};
+
+export default PaymentSettings;
diff --git a/frontend/modules/payment/features/transactionDetail.tsx b/frontend/modules/payment/features/transactionDetail.tsx
new file mode 100644
index 0000000..8d5d008
--- /dev/null
+++ b/frontend/modules/payment/features/transactionDetail.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import { useParams } from "next/navigation";
+import { useQuery } from "@tanstack/react-query";
+import Link from "next/link";
+import { useTenantId } from "@/hooks/useTenantId";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+import { PaymentPageLoader, PaymentPageError } from "@/modules/payment/pages/shared";
+import { PageHeader, Card, CardContent, Button } from "@/components/ds";
+import { PaymentStatusChip } from "@/modules/payment/components/PaymentStatusChip";
+
+export const PaymentTransactionDetail = function PaymentTransactionDetailPage() {
+ const params = useParams();
+ const id = String(params?.id ?? "");
+ const { tenantId } = useTenantId();
+
+ const q = useQuery({
+ queryKey: ["payment", tenantId, "transaction", id],
+ queryFn: () => paymentApi.transactions.get(tenantId!, id),
+ enabled: !!tenantId && !!id,
+ });
+
+ if (!tenantId || q.isLoading) return ;
+ if (q.error) return q.refetch()} />;
+
+ const tx = q.data!;
+
+ return (
+
+
+
+
+ ) : null
+ }
+ />
+
+
+
+ مبلغ: {String(tx.amount_minor)} {String(tx.currency)}
+ شناسه PSP: {String(tx.provider_transaction_id ?? "—")}
+ تاریخ:{" "}
+ {tx.created_at ? new Date(String(tx.created_at)).toLocaleString("fa-IR") : "—"}
+
+
+
+
+
+ داده کامل
+ {JSON.stringify(tx, null, 2)}
+
+
+
+ );
+};
+
+export default PaymentTransactionDetail;
diff --git a/frontend/modules/payment/features/transactions.tsx b/frontend/modules/payment/features/transactions.tsx
new file mode 100644
index 0000000..b222bf9
--- /dev/null
+++ b/frontend/modules/payment/features/transactions.tsx
@@ -0,0 +1,26 @@
+"use client";
+
+import { PaymentListPage } from "@/modules/payment/components/PaymentListPage";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+
+export const PaymentTransactions = function PaymentTransactionsPage() {
+ return (
+ paymentApi.transactions.list(tenantId)}
+ columns={[
+ { key: "id", header: "شناسه" },
+ { key: "amount_minor", header: "مبلغ (ریال)" },
+ { key: "currency", header: "ارز" },
+ { key: "status", header: "وضعیت", type: "status" },
+ { key: "provider_transaction_id", header: "شناسه PSP" },
+ { key: "created_at", header: "تاریخ", type: "datetime" },
+ ]}
+ detailHref={(row) => (row.id ? `/payment/transactions/${row.id}` : null)}
+ />
+ );
+};
+
+export default PaymentTransactions;
diff --git a/frontend/modules/payment/features/verification.tsx b/frontend/modules/payment/features/verification.tsx
new file mode 100644
index 0000000..ea3d519
--- /dev/null
+++ b/frontend/modules/payment/features/verification.tsx
@@ -0,0 +1,76 @@
+"use client";
+
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import Link from "next/link";
+import { toast } from "sonner";
+import { useTenantId } from "@/hooks/useTenantId";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+import { PaymentPageLoader, PaymentPageError, PaymentEmptyState } from "@/modules/payment/pages/shared";
+import { PageHeader, Card, CardContent, Button } from "@/components/ds";
+import { PaymentStatusChip } from "@/modules/payment/components/PaymentStatusChip";
+
+export const PaymentVerification = function PaymentVerificationPage() {
+ const { tenantId } = useTenantId();
+ const qc = useQueryClient();
+
+ const q = useQuery({
+ queryKey: ["payment", tenantId, "verification-queue"],
+ queryFn: async () => {
+ const requests = await paymentApi.requests.list(tenantId!);
+ return requests.filter((r) => ["redirect_issued", "pending"].includes(String(r.status)));
+ },
+ enabled: !!tenantId,
+ });
+
+ const verifyM = useMutation({
+ mutationFn: (id: string) => paymentApi.requests.verify(tenantId!, id, { status: "paid" }),
+ onSuccess: () => {
+ toast.success("پرداخت تأیید شد");
+ qc.invalidateQueries({ queryKey: ["payment", tenantId, "verification-queue"] });
+ },
+ onError: (e: Error) => toast.error(e.message),
+ });
+
+ if (!tenantId || q.isLoading) return ;
+ if (q.error) return q.refetch()} />;
+
+ const queue = q.data ?? [];
+
+ return (
+
+
+ {queue.length === 0 ? (
+
+ ) : (
+
+ {queue.map((r) => (
+
+
+
+
+ {String(r.amount_minor)} {String(r.currency)}
+
+
{String(r.provider_code)}
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default PaymentVerification;
diff --git a/frontend/modules/payment/hooks/usePaymentCapabilities.ts b/frontend/modules/payment/hooks/usePaymentCapabilities.ts
new file mode 100644
index 0000000..505677c
--- /dev/null
+++ b/frontend/modules/payment/hooks/usePaymentCapabilities.ts
@@ -0,0 +1,31 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { useTenantId } from "@/hooks/useTenantId";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+
+export function usePaymentCapabilities() {
+ const { tenantId } = useTenantId();
+ return useQuery({
+ queryKey: ["payment", tenantId, "capabilities"],
+ queryFn: () => paymentApi.capabilities(tenantId!),
+ enabled: !!tenantId,
+ staleTime: 60_000,
+ });
+}
+
+export function usePaymentHealth() {
+ return useQuery({
+ queryKey: ["payment", "health"],
+ queryFn: () => paymentApi.health(),
+ });
+}
+
+export function usePaymentMetrics() {
+ const { tenantId } = useTenantId();
+ return useQuery({
+ queryKey: ["payment", tenantId, "metrics"],
+ queryFn: () => paymentApi.metrics(tenantId ?? undefined),
+ staleTime: 30_000,
+ });
+}
diff --git a/frontend/modules/payment/hooks/usePaymentPermissions.ts b/frontend/modules/payment/hooks/usePaymentPermissions.ts
new file mode 100644
index 0000000..33019d6
--- /dev/null
+++ b/frontend/modules/payment/hooks/usePaymentPermissions.ts
@@ -0,0 +1,30 @@
+"use client";
+
+import { useMemo } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { useTenantId } from "@/hooks/useTenantId";
+import { paymentApi } from "@/modules/payment/services/payment-api";
+
+type MembershipLike = { tenant_id?: string; permissions?: string[] };
+
+export function usePaymentPermissions() {
+ const { tenantId, me } = useTenantId();
+ const catalog = useQuery({
+ queryKey: ["payment", tenantId, "permissions-catalog"],
+ queryFn: () => paymentApi.permissions.catalog(tenantId!),
+ enabled: !!tenantId,
+ staleTime: 60_000,
+ });
+ const membership = (me?.memberships as MembershipLike[] | undefined)?.find(
+ (m) => m.tenant_id === tenantId
+ );
+ const userPerms = useMemo(() => new Set(membership?.permissions ?? []), [membership]);
+
+ function can(permission: string) {
+ if (userPerms.size === 0) return true;
+ if (userPerms.has("payment.manage") || userPerms.has("*")) return true;
+ return userPerms.has(permission);
+ }
+
+ return { catalog, can, tenantId };
+}
diff --git a/frontend/modules/payment/index.ts b/frontend/modules/payment/index.ts
new file mode 100644
index 0000000..8c2d68a
--- /dev/null
+++ b/frontend/modules/payment/index.ts
@@ -0,0 +1,11 @@
+export { paymentApi, PaymentApiError, PAYMENT_API_CATALOG } from "./services/payment-api";
+export { PAYMENT_PORTAL, PAYMENT_NAV, navForPayment, filterNavByBundles, MVP_ROUTE_HREFS } from "./constants/portals";
+export { ALL_PAYMENT_PERMISSIONS } from "./constants/permissions";
+export { PaymentPortalShell } from "./components/PaymentPortalShell";
+export { PaymentListPage } from "./components/PaymentListPage";
+export { PaymentStatusChip } from "./components/PaymentStatusChip";
+export { PaymentBarChart, PaymentDonutChart, PaymentStatCards } from "./components/PaymentCharts";
+export { PaymentRootLayout } from "./components/createPortalLayout";
+export { usePaymentCapabilities, usePaymentHealth, usePaymentMetrics } from "./hooks/usePaymentCapabilities";
+export { usePaymentPermissions } from "./hooks/usePaymentPermissions";
+export type { PaymentRecord, CapabilitiesResponse, HealthResponse, MetricsResponse } from "./types";
diff --git a/frontend/modules/payment/pages/shared.tsx b/frontend/modules/payment/pages/shared.tsx
new file mode 100644
index 0000000..90071cf
--- /dev/null
+++ b/frontend/modules/payment/pages/shared.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import { LoadingState, ErrorState, EmptyState } from "@/components/ds";
+import { PaymentApiError } from "@/modules/payment/services/payment-api";
+import { PermissionDeniedState } from "@/src/shared/ui";
+
+export function PaymentPageLoader({ label = "در حال بارگذاری تربتپی…" }: { label?: string }) {
+ return ;
+}
+
+export function PaymentPageError({ error, onRetry }: { error: unknown; onRetry?: () => void }) {
+ if (error instanceof PaymentApiError && error.status === 403) {
+ return (
+
+ );
+ }
+ const offline = typeof navigator !== "undefined" && !navigator.onLine;
+ if (offline) {
+ return ;
+ }
+ const message = error instanceof Error ? error.message : "خطا در دریافت دادهها";
+ return ;
+}
+
+export function PaymentEmptyState({
+ title = "موردی یافت نشد",
+ description = "هنوز دادهای از سرویس پرداخت دریافت نشده است.",
+ action,
+}: {
+ title?: string;
+ description?: string;
+ action?: React.ReactNode;
+}) {
+ return ;
+}
+
+export function exportToCsv(title: string, rows: Record[], columns: string[]) {
+ if (rows.length === 0) return;
+ const header = columns.join(",");
+ const body = rows
+ .map((r) => columns.map((c) => JSON.stringify(r[c] ?? "")).join(","))
+ .join("\n");
+ const blob = new Blob(["\uFEFF" + header + "\n" + body], {
+ type: "text/csv;charset=utf-8",
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `${title}-export.csv`;
+ a.click();
+ URL.revokeObjectURL(url);
+}
diff --git a/frontend/modules/payment/services/payment-api.ts b/frontend/modules/payment/services/payment-api.ts
new file mode 100644
index 0000000..28ba1a8
--- /dev/null
+++ b/frontend/modules/payment/services/payment-api.ts
@@ -0,0 +1,210 @@
+/**
+ * Torbat Pay API client — real backend only (MVP 14.0–14.5).
+ * Browser → /api/payment BFF; SSR → direct upstream :8012
+ */
+import { getValidAccessToken, refreshSession } from "@/lib/auth";
+import type {
+ CapabilitiesResponse,
+ HealthResponse,
+ MetricsResponse,
+ PaymentRecord,
+ PermissionsCatalogResponse,
+} from "@/modules/payment/types";
+
+const PAYMENT_BASE =
+ typeof window !== "undefined"
+ ? "/api/payment"
+ : process.env.PAYMENT_SERVICE_URL ||
+ process.env.NEXT_PUBLIC_PAYMENT_API_URL ||
+ "http://localhost:8012";
+
+export class PaymentApiError extends Error {
+ constructor(
+ public status: number,
+ public code: string,
+ message: string,
+ public details?: unknown
+ ) {
+ super(message);
+ this.name = "PaymentApiError";
+ }
+}
+
+type RequestOptions = RequestInit & {
+ tenantId: string;
+ auth?: boolean;
+ idempotencyKey?: string;
+};
+
+async function request(path: string, options: RequestOptions): Promise {
+ const { tenantId, auth = true, idempotencyKey, headers: customHeaders, ...init } = options;
+ const headers = new Headers(customHeaders);
+ if (!headers.has("Content-Type") && init.body) headers.set("Content-Type", "application/json");
+ headers.set("X-Tenant-ID", tenantId);
+ if (idempotencyKey) headers.set("Idempotency-Key", idempotencyKey);
+ if (auth) {
+ const token = await getValidAccessToken();
+ if (token) headers.set("Authorization", `Bearer ${token}`);
+ }
+ const url = path.startsWith("http")
+ ? path
+ : `${PAYMENT_BASE.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
+
+ let res: Response;
+ try {
+ res = await fetch(url, { ...init, headers, cache: "no-store" });
+ } catch (err) {
+ const detail = err instanceof Error ? err.message : "network_error";
+ throw new Error(`اتصال به سرویس تربتپی برقرار نشد — ${detail}`);
+ }
+
+ if (res.status === 401 && auth) {
+ const refreshed = await refreshSession({ force: true });
+ if (refreshed) {
+ headers.set("Authorization", `Bearer ${refreshed}`);
+ res = await fetch(url, { ...init, headers, cache: "no-store" });
+ }
+ }
+
+ if (!res.ok) {
+ const body = await res.json().catch(() => ({}));
+ const detail = body?.error ?? body?.detail ?? {};
+ throw new PaymentApiError(
+ res.status,
+ detail?.code || (typeof detail === "string" ? detail : "unknown_error"),
+ detail?.message || (typeof detail === "string" ? detail : res.statusText),
+ detail
+ );
+ }
+ if (res.status === 204) return undefined as T;
+ return res.json() as Promise;
+}
+
+function crud(basePath: string) {
+ return {
+ list: (tenantId: string) => request(basePath, { tenantId }),
+ get: (tenantId: string, id: string) => request(`${basePath}/${id}`, { tenantId }),
+ create: (tenantId: string, body: Record) =>
+ request(basePath, { tenantId, method: "POST", body: JSON.stringify(body) }),
+ update: (tenantId: string, id: string, body: Record) =>
+ request(`${basePath}/${id}`, {
+ tenantId,
+ method: "PATCH",
+ body: JSON.stringify(body),
+ }),
+ };
+}
+
+export const paymentApi = {
+ health: (): Promise => fetch(`${PAYMENT_BASE}/health`).then((r) => r.json()),
+ capabilities: (tenantId: string) =>
+ request("/capabilities", { tenantId }),
+ metrics: (tenantId?: string) =>
+ tenantId
+ ? request("/metrics", { tenantId })
+ : fetch(`${PAYMENT_BASE}/metrics`).then((r) => r.json()),
+ openapi: async (): Promise> => {
+ const r = await fetch(`${PAYMENT_BASE}/openapi.json`);
+ if (!r.ok) throw new PaymentApiError(r.status, "openapi_error", "OpenAPI در دسترس نیست");
+ return r.json();
+ },
+ permissions: {
+ catalog: (tenantId: string) =>
+ request("/api/v1/permissions/catalog", { tenantId }),
+ },
+ audit: {
+ list: (tenantId: string) => request("/api/v1/audit", { tenantId }),
+ },
+ workspaces: crud("/api/v1/payment-workspaces"),
+ bundleDefinitions: crud("/api/v1/bundle-definitions"),
+ tenantBundles: crud("/api/v1/tenant-bundles"),
+ featureToggles: crud("/api/v1/feature-toggles"),
+ providerAssignments: crud("/api/v1/provider-assignments"),
+ pspRegistrations: crud("/api/v1/psp-provider-registrations"),
+ creditRegistrations: crud("/api/v1/credit-provider-registrations"),
+ configurations: crud("/api/v1/configurations"),
+ settings: crud("/api/v1/settings"),
+ pspConnections: {
+ ...crud("/api/v1/psp-connections"),
+ test: (tenantId: string, id: string) =>
+ request>(`/api/v1/psp-connections/${id}/test`, {
+ tenantId,
+ method: "POST",
+ body: "{}",
+ }),
+ },
+ pspRouting: crud("/api/v1/psp-routing-policies"),
+ merchants: {
+ list: (tenantId: string) => request("/api/v1/merchant-accounts", { tenantId }),
+ get: (tenantId: string, id: string) =>
+ request(`/api/v1/merchant-accounts/${id}`, { tenantId }),
+ create: (tenantId: string, body: Record) =>
+ request("/api/v1/merchant-accounts", {
+ tenantId,
+ method: "POST",
+ body: JSON.stringify(body),
+ }),
+ submit: (tenantId: string, id: string) =>
+ request(`/api/v1/merchant-accounts/${id}/submit`, { tenantId, method: "POST", body: "{}" }),
+ activate: (tenantId: string, id: string) =>
+ request(`/api/v1/merchant-accounts/${id}/activate`, { tenantId, method: "POST", body: "{}" }),
+ suspend: (tenantId: string, id: string) =>
+ request(`/api/v1/merchant-accounts/${id}/suspend`, { tenantId, method: "POST", body: "{}" }),
+ close: (tenantId: string, id: string) =>
+ request(`/api/v1/merchant-accounts/${id}/close`, { tenantId, method: "POST", body: "{}" }),
+ },
+ settlementProfiles: crud("/api/v1/merchant-settlement-profiles"),
+ requests: {
+ list: (tenantId: string) => request("/api/v1/payment-requests", { tenantId }),
+ get: (tenantId: string, id: string) =>
+ request(`/api/v1/payment-requests/${id}`, { tenantId }),
+ create: (tenantId: string, body: Record, idempotencyKey: string) =>
+ request("/api/v1/payment-requests", {
+ tenantId,
+ method: "POST",
+ body: JSON.stringify(body),
+ idempotencyKey,
+ }),
+ cancel: (tenantId: string, id: string) =>
+ request(`/api/v1/payment-requests/${id}/cancel`, { tenantId, method: "POST", body: "{}" }),
+ verify: (tenantId: string, id: string, body?: Record) =>
+ request(`/api/v1/payment-requests/${id}/verify`, {
+ tenantId,
+ method: "POST",
+ body: JSON.stringify(body ?? { status: "paid" }),
+ }),
+ },
+ transactions: {
+ list: (tenantId: string) => request("/api/v1/payment-transactions", { tenantId }),
+ get: (tenantId: string, id: string) =>
+ request(`/api/v1/payment-transactions/${id}`, { tenantId }),
+ bySource: (tenantId: string, service?: string, refId?: string) => {
+ const sp = new URLSearchParams();
+ if (service) sp.set("service", service);
+ if (refId) sp.set("ref_id", refId);
+ const q = sp.toString();
+ return request(`/api/v1/payment-transactions/by-source${q ? `?${q}` : ""}`, {
+ tenantId,
+ });
+ },
+ },
+ ledger: {
+ list: (tenantId: string) => request("/api/v1/payment-ledger-entries", { tenantId }),
+ },
+};
+
+export const PAYMENT_API_CATALOG = [
+ { method: "GET", path: "/health", phase: "14.0" },
+ { method: "GET", path: "/capabilities", phase: "14.0" },
+ { method: "GET", path: "/metrics", phase: "14.0" },
+ { method: "GET|POST", path: "/api/v1/payment-workspaces", phase: "14.0" },
+ { method: "GET|POST", path: "/api/v1/tenant-bundles", phase: "14.0" },
+ { method: "GET|POST", path: "/api/v1/feature-toggles", phase: "14.0" },
+ { method: "GET|POST", path: "/api/v1/psp-connections", phase: "14.1" },
+ { method: "POST", path: "/api/v1/psp-connections/{id}/test", phase: "14.1" },
+ { method: "GET|POST", path: "/api/v1/merchant-accounts", phase: "14.2" },
+ { method: "POST", path: "/api/v1/payment-requests", phase: "14.3" },
+ { method: "POST", path: "/api/v1/payment-requests/{id}/verify", phase: "14.4" },
+ { method: "GET", path: "/api/v1/payment-transactions", phase: "14.5" },
+ { method: "GET", path: "/api/v1/payment-ledger-entries", phase: "14.5" },
+] as const;
diff --git a/frontend/modules/payment/types/index.ts b/frontend/modules/payment/types/index.ts
new file mode 100644
index 0000000..5977c78
--- /dev/null
+++ b/frontend/modules/payment/types/index.ts
@@ -0,0 +1,22 @@
+export type PaymentRecord = Record & { id?: string; status?: string };
+
+export type HealthResponse = { status: string; service: string; version: string };
+export type CapabilitiesResponse = {
+ service: string;
+ version: string;
+ phase: string;
+ workspace_status?: string;
+ default_payment_mode?: string;
+ active_bundles?: string[];
+ feature_toggles?: Record;
+ payment_sources_supported?: string[];
+ payment_sources_reserved?: string[];
+ contract_versions?: Record;
+};
+export type MetricsResponse = {
+ service: string;
+ version: string;
+ phase: string;
+ metrics: Record;
+};
+export type PermissionsCatalogResponse = { permissions: string[] };
diff --git a/frontend/package.json b/frontend/package.json
index 06f816a..a5986ff 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -26,7 +26,12 @@
"validate:hospitality": "npm run validate:hospitality-routes && npm run validate:architecture",
"validate:delivery-routes": "node scripts/validate-delivery-routes.mjs",
"validate:delivery": "npm run validate:delivery-routes && npm run validate:architecture",
- "scaffold:communication": "node scripts/scaffold-communication-module.mjs"
+ "validate:experience-routes": "node scripts/validate-experience-routes.mjs",
+ "validate:experience": "npm run validate:experience-routes && node modules/experience/__tests__/nav-filter.mjs && node modules/experience/__tests__/a11y-smoke.mjs && node modules/experience/__tests__/perf-smoke.mjs && npm run validate:architecture",
+ "validate:payment-routes": "node scripts/validate-payment-routes.mjs",
+ "validate:payment": "npm run validate:payment-routes && npm run validate:architecture",
+ "scaffold:communication": "node scripts/scaffold-communication-module.mjs",
+ "scaffold:experience": "node scripts/scaffold-experience-module.mjs"
},
"dependencies": {
"@hookform/resolvers": "^5.4.0",
diff --git a/frontend/scripts/scaffold-payment-module.mjs b/frontend/scripts/scaffold-payment-module.mjs
new file mode 100644
index 0000000..897fec9
--- /dev/null
+++ b/frontend/scripts/scaffold-payment-module.mjs
@@ -0,0 +1,478 @@
+#!/usr/bin/env node
+/**
+ * Scaffold Torbat Pay frontend module (MVP payment-14.0–14.5).
+ * Run: node scripts/scaffold-payment-module.mjs
+ */
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const ROOT = path.resolve(__dirname, "..");
+const MOD = path.join(ROOT, "modules", "payment");
+const APP = path.join(ROOT, "app", "payment");
+
+function write(rel, content) {
+ const full = path.isAbsolute(rel) ? rel : path.join(ROOT, rel);
+ fs.mkdirSync(path.dirname(full), { recursive: true });
+ fs.writeFileSync(full, content.replace(/\r\n/g, "\n"), "utf8");
+ console.log("write", path.relative(ROOT, full));
+}
+
+const ROUTES = [
+ { route: "hub", feature: "hub", title: "هاب تربتپی" },
+ { route: "dashboard", feature: "dashboard", title: "داشبورد اجرایی" },
+ { route: "overview", feature: "overview", title: "نمای کلی پرداخت" },
+ { route: "kpis", feature: "kpis", title: "شاخصهای کلیدی" },
+ { route: "transactions", feature: "transactions", title: "تراکنشهای اخیر" },
+ { route: "transactions/[id]", feature: "transactionDetail", title: "جزئیات تراکنش" },
+ { route: "ledger", feature: "ledger", title: "دفتر کل پرداخت" },
+ { route: "psp", feature: "psp", title: "مدیریت درگاهها" },
+ { route: "psp/health", feature: "gatewayHealth", title: "سلامت درگاه" },
+ { route: "providers", feature: "providers", title: "تخصیص ارائهدهنده" },
+ { route: "merchants", feature: "merchants", title: "حسابهای پذیرنده" },
+ { route: "merchants/[id]", feature: "merchantDetail", title: "جزئیات پذیرنده" },
+ { route: "merchants/approvals", feature: "merchantApprovals", title: "تأیید پذیرنده" },
+ { route: "requests", feature: "requests", title: "درخواستهای پرداخت" },
+ { route: "requests/[id]", feature: "requestDetail", title: "جزئیات درخواست / چکاوت" },
+ { route: "callbacks", feature: "callbacks", title: "مانیتور کالبک" },
+ { route: "verification", feature: "verification", title: "مانیتور تأیید" },
+ { route: "audit", feature: "audit", title: "لاگ حسابرسی" },
+ { route: "bundles", feature: "bundles", title: "باندلها" },
+ { route: "feature-toggles", feature: "featureToggles", title: "کلیدهای ویژگی" },
+ { route: "capabilities", feature: "capabilities", title: "قابلیتها" },
+ { route: "settings", feature: "settings", title: "تنظیمات پرداخت" },
+ { route: "metrics", feature: "metrics", title: "متریکها" },
+ { route: "monitoring", feature: "monitoring", title: "مانیتورینگ" },
+ { route: "api-explorer", feature: "apiExplorer", title: "کاوشگر API" },
+ { route: "openapi", feature: "openapi", title: "مشاهده OpenAPI" },
+];
+
+// --- core module files ---
+write("modules/payment/types/index.ts", `export type PaymentRecord = Record & { id?: string; status?: string };
+
+export type HealthResponse = { status: string; service: string; version: string };
+export type CapabilitiesResponse = {
+ service: string;
+ version: string;
+ phase: string;
+ workspace_status?: string;
+ default_payment_mode?: string;
+ active_bundles?: string[];
+ feature_toggles?: Record;
+ payment_sources_supported?: string[];
+ payment_sources_reserved?: string[];
+ contract_versions?: Record;
+};
+export type MetricsResponse = {
+ service: string;
+ version: string;
+ phase: string;
+ metrics: Record;
+};
+export type PermissionsCatalogResponse = { permissions: string[] };
+`);
+
+write("modules/payment/constants/permissions.ts", `export const PAYMENT_VIEW = "payment.view";
+export const PAYMENT_MANAGE = "payment.manage";
+export const PAYMENT_AUDIT_VIEW = "payment.audit.view";
+export const PAYMENT_SETTINGS_MANAGE = "payment.settings.manage";
+export const PAYMENT_PSP_VIEW = "payment.psp_connections.view";
+export const PAYMENT_PSP_MANAGE = "payment.psp_connections.manage";
+export const PAYMENT_MERCHANT_VIEW = "payment.merchant_accounts.view";
+export const PAYMENT_MERCHANT_MANAGE = "payment.merchant_accounts.manage";
+export const PAYMENT_REQUEST_VIEW = "payment.requests.view";
+export const PAYMENT_REQUEST_CREATE = "payment.requests.create";
+export const PAYMENT_TX_VIEW = "payment.transactions.view";
+export const PAYMENT_LEDGER_VIEW = "payment.ledger.view";
+
+export const ALL_PAYMENT_PERMISSIONS = [
+ PAYMENT_VIEW,
+ PAYMENT_MANAGE,
+ PAYMENT_AUDIT_VIEW,
+ PAYMENT_SETTINGS_MANAGE,
+ PAYMENT_PSP_VIEW,
+ PAYMENT_PSP_MANAGE,
+ PAYMENT_MERCHANT_VIEW,
+ PAYMENT_MERCHANT_MANAGE,
+ PAYMENT_REQUEST_VIEW,
+ PAYMENT_REQUEST_CREATE,
+ PAYMENT_TX_VIEW,
+ PAYMENT_LEDGER_VIEW,
+] as const;
+`);
+
+write("modules/payment/constants/portals.ts", `import type { LucideIcon } from "lucide-react";
+import {
+ LayoutDashboard,
+ CreditCard,
+ Building2,
+ Settings,
+ Shield,
+ Activity,
+ BarChart3,
+ Wallet,
+ Plug,
+ FileText,
+ BookOpen,
+ KeyRound,
+ Gauge,
+ ScrollText,
+ Network,
+ Eye,
+} from "lucide-react";
+
+export type PortalNavItem = { href: string; label: string; bundle?: string | null };
+export type PortalNavGroup = {
+ id: string;
+ label: string;
+ icon: LucideIcon;
+ href?: string;
+ bundle?: string | null;
+ items?: PortalNavItem[];
+};
+
+export const PAYMENT_PORTAL = {
+ id: "payment" as const,
+ label: "تربتپی",
+ description: "پلتفرم پرداخت سازمانی — BYO-PSP و پذیرنده",
+ basePath: "/payment",
+ icon: CreditCard,
+};
+
+export const PAYMENT_NAV: PortalNavGroup[] = [
+ {
+ id: "dashboards",
+ label: "داشبوردها",
+ icon: LayoutDashboard,
+ items: [
+ { href: "/payment/dashboard", label: "داشبورد اجرایی" },
+ { href: "/payment/overview", label: "نمای کلی" },
+ { href: "/payment/kpis", label: "شاخصهای کلیدی" },
+ ],
+ },
+ {
+ id: "operations",
+ label: "عملیات پرداخت",
+ icon: Wallet,
+ items: [
+ { href: "/payment/requests", label: "درخواستهای پرداخت" },
+ { href: "/payment/transactions", label: "تراکنشها" },
+ { href: "/payment/ledger", label: "دفتر کل" },
+ { href: "/payment/callbacks", label: "کالبکها" },
+ { href: "/payment/verification", label: "تأیید پرداخت" },
+ ],
+ },
+ {
+ id: "psp",
+ label: "درگاهها (PSP)",
+ icon: Plug,
+ bundle: "payment.byo_psp.basic",
+ items: [
+ { href: "/payment/psp", label: "مدیریت درگاه", bundle: "payment.byo_psp.basic" },
+ { href: "/payment/psp/health", label: "سلامت درگاه", bundle: "payment.byo_psp.basic" },
+ { href: "/payment/providers", label: "تخصیص ارائهدهنده" },
+ ],
+ },
+ {
+ id: "merchants",
+ label: "پذیرندگان",
+ icon: Building2,
+ bundle: "payment.torbat_pay.merchant",
+ items: [
+ { href: "/payment/merchants", label: "حسابها", bundle: "payment.torbat_pay.merchant" },
+ { href: "/payment/merchants/approvals", label: "تأییدها", bundle: "payment.torbat_pay.merchant" },
+ ],
+ },
+ {
+ id: "licensing",
+ label: "مجوز و باندل",
+ icon: KeyRound,
+ items: [
+ { href: "/payment/bundles", label: "باندلها" },
+ { href: "/payment/feature-toggles", label: "کلیدهای ویژگی" },
+ { href: "/payment/capabilities", label: "قابلیتها" },
+ ],
+ },
+ {
+ id: "ops",
+ label: "عملیات پلتفرم",
+ icon: Activity,
+ items: [
+ { href: "/payment/monitoring", label: "مانیتورینگ" },
+ { href: "/payment/metrics", label: "متریکها" },
+ { href: "/payment/audit", label: "حسابرسی" },
+ { href: "/payment/settings", label: "تنظیمات" },
+ ],
+ },
+ {
+ id: "dev",
+ label: "توسعهدهنده",
+ icon: Network,
+ items: [
+ { href: "/payment/api-explorer", label: "کاوشگر API" },
+ { href: "/payment/openapi", label: "OpenAPI" },
+ ],
+ },
+];
+
+export function navForPayment() {
+ return PAYMENT_NAV;
+}
+
+export function filterNavByBundles(
+ nav: PortalNavGroup[],
+ activeBundles: string[] | undefined
+) {
+ const bundles = new Set(activeBundles ?? []);
+ return nav
+ .map((g) => {
+ if (g.bundle && !bundles.has(g.bundle)) return null;
+ const items = g.items?.filter((i) => !i.bundle || bundles.has(i.bundle));
+ if (g.items && (!items || items.length === 0)) return null;
+ return { ...g, items };
+ })
+ .filter(Boolean) as PortalNavGroup[];
+}
+
+export const MVP_ROUTE_HREFS = [
+ "/payment/hub",
+ "/payment/dashboard",
+ "/payment/overview",
+ "/payment/kpis",
+ "/payment/transactions",
+ "/payment/ledger",
+ "/payment/psp",
+ "/payment/psp/health",
+ "/payment/providers",
+ "/payment/merchants",
+ "/payment/merchants/approvals",
+ "/payment/requests",
+ "/payment/callbacks",
+ "/payment/verification",
+ "/payment/audit",
+ "/payment/bundles",
+ "/payment/feature-toggles",
+ "/payment/capabilities",
+ "/payment/settings",
+ "/payment/metrics",
+ "/payment/monitoring",
+ "/payment/api-explorer",
+ "/payment/openapi",
+] as const;
+`);
+
+write(
+ "modules/payment/services/payment-api.ts",
+ `/**
+ * Torbat Pay API client — real backend only (MVP 14.0–14.5).
+ * Browser → /api/payment BFF; SSR → direct upstream :8012
+ */
+import { getValidAccessToken, refreshSession } from "@/lib/auth";
+import type {
+ CapabilitiesResponse,
+ HealthResponse,
+ MetricsResponse,
+ PaymentRecord,
+ PermissionsCatalogResponse,
+} from "@/modules/payment/types";
+
+const PAYMENT_BASE =
+ typeof window !== "undefined"
+ ? "/api/payment"
+ : process.env.PAYMENT_SERVICE_URL ||
+ process.env.NEXT_PUBLIC_PAYMENT_API_URL ||
+ "http://localhost:8012";
+
+export class PaymentApiError extends Error {
+ constructor(
+ public status: number,
+ public code: string,
+ message: string,
+ public details?: unknown
+ ) {
+ super(message);
+ this.name = "PaymentApiError";
+ }
+}
+
+type RequestOptions = RequestInit & {
+ tenantId: string;
+ auth?: boolean;
+ idempotencyKey?: string;
+};
+
+async function request(path: string, options: RequestOptions): Promise {
+ const { tenantId, auth = true, idempotencyKey, headers: customHeaders, ...init } = options;
+ const headers = new Headers(customHeaders);
+ if (!headers.has("Content-Type") && init.body) headers.set("Content-Type", "application/json");
+ headers.set("X-Tenant-ID", tenantId);
+ if (idempotencyKey) headers.set("Idempotency-Key", idempotencyKey);
+ if (auth) {
+ const token = await getValidAccessToken();
+ if (token) headers.set("Authorization", \`Bearer \${token}\`);
+ }
+ const url = path.startsWith("http")
+ ? path
+ : \`\${PAYMENT_BASE.replace(/\\/$/, "")}\${path.startsWith("/") ? path : \`/\${path}\`}\`;
+
+ let res: Response;
+ try {
+ res = await fetch(url, { ...init, headers, cache: "no-store" });
+ } catch (err) {
+ const detail = err instanceof Error ? err.message : "network_error";
+ throw new Error(\`اتصال به سرویس تربتپی برقرار نشد — \${detail}\`);
+ }
+
+ if (res.status === 401 && auth) {
+ const refreshed = await refreshSession({ force: true });
+ if (refreshed) {
+ headers.set("Authorization", \`Bearer \${refreshed}\`);
+ res = await fetch(url, { ...init, headers, cache: "no-store" });
+ }
+ }
+
+ if (!res.ok) {
+ const body = await res.json().catch(() => ({}));
+ const detail = body?.error ?? body?.detail ?? {};
+ throw new PaymentApiError(
+ res.status,
+ detail?.code || (typeof detail === "string" ? detail : "unknown_error"),
+ detail?.message || (typeof detail === "string" ? detail : res.statusText),
+ detail
+ );
+ }
+ if (res.status === 204) return undefined as T;
+ return res.json() as Promise;
+}
+
+function crud(basePath: string) {
+ return {
+ list: (tenantId: string) => request(basePath, { tenantId }),
+ get: (tenantId: string, id: string) => request(\`\${basePath}/\${id}\`, { tenantId }),
+ create: (tenantId: string, body: Record) =>
+ request(basePath, { tenantId, method: "POST", body: JSON.stringify(body) }),
+ update: (tenantId: string, id: string, body: Record) =>
+ request(\`\${basePath}/\${id}\`, {
+ tenantId,
+ method: "PATCH",
+ body: JSON.stringify(body),
+ }),
+ };
+}
+
+export const paymentApi = {
+ health: (): Promise => fetch(\`\${PAYMENT_BASE}/health\`).then((r) => r.json()),
+ capabilities: (tenantId: string) =>
+ request("/capabilities", { tenantId }),
+ metrics: (tenantId?: string) =>
+ tenantId
+ ? request("/metrics", { tenantId })
+ : fetch(\`\${PAYMENT_BASE}/metrics\`).then((r) => r.json()),
+ openapi: async (): Promise> => {
+ const r = await fetch(\`\${PAYMENT_BASE}/openapi.json\`);
+ if (!r.ok) throw new PaymentApiError(r.status, "openapi_error", "OpenAPI در دسترس نیست");
+ return r.json();
+ },
+ permissions: {
+ catalog: (tenantId: string) =>
+ request("/api/v1/permissions/catalog", { tenantId }),
+ },
+ audit: {
+ list: (tenantId: string) => request("/api/v1/audit", { tenantId }),
+ },
+ workspaces: crud("/api/v1/payment-workspaces"),
+ bundleDefinitions: crud("/api/v1/bundle-definitions"),
+ tenantBundles: crud("/api/v1/tenant-bundles"),
+ featureToggles: crud("/api/v1/feature-toggles"),
+ providerAssignments: crud("/api/v1/provider-assignments"),
+ pspRegistrations: crud("/api/v1/psp-provider-registrations"),
+ creditRegistrations: crud("/api/v1/credit-provider-registrations"),
+ configurations: crud("/api/v1/configurations"),
+ settings: crud("/api/v1/settings"),
+ pspConnections: {
+ ...crud("/api/v1/psp-connections"),
+ test: (tenantId: string, id: string) =>
+ request>(\`/api/v1/psp-connections/\${id}/test\`, {
+ tenantId,
+ method: "POST",
+ body: "{}",
+ }),
+ },
+ pspRouting: crud("/api/v1/psp-routing-policies"),
+ merchants: {
+ list: (tenantId: string) => request("/api/v1/merchant-accounts", { tenantId }),
+ get: (tenantId: string, id: string) =>
+ request(\`/api/v1/merchant-accounts/\${id}\`, { tenantId }),
+ create: (tenantId: string, body: Record) =>
+ request("/api/v1/merchant-accounts", {
+ tenantId,
+ method: "POST",
+ body: JSON.stringify(body),
+ }),
+ submit: (tenantId: string, id: string) =>
+ request(\`/api/v1/merchant-accounts/\${id}/submit\`, { tenantId, method: "POST", body: "{}" }),
+ activate: (tenantId: string, id: string) =>
+ request(\`/api/v1/merchant-accounts/\${id}/activate\`, { tenantId, method: "POST", body: "{}" }),
+ suspend: (tenantId: string, id: string) =>
+ request(\`/api/v1/merchant-accounts/\${id}/suspend\`, { tenantId, method: "POST", body: "{}" }),
+ close: (tenantId: string, id: string) =>
+ request(\`/api/v1/merchant-accounts/\${id}/close\`, { tenantId, method: "POST", body: "{}" }),
+ },
+ settlementProfiles: crud("/api/v1/merchant-settlement-profiles"),
+ requests: {
+ list: (tenantId: string) => request("/api/v1/payment-requests", { tenantId }),
+ get: (tenantId: string, id: string) =>
+ request(\`/api/v1/payment-requests/\${id}\`, { tenantId }),
+ create: (tenantId: string, body: Record, idempotencyKey: string) =>
+ request("/api/v1/payment-requests", {
+ tenantId,
+ method: "POST",
+ body: JSON.stringify(body),
+ idempotencyKey,
+ }),
+ cancel: (tenantId: string, id: string) =>
+ request(\`/api/v1/payment-requests/\${id}/cancel\`, { tenantId, method: "POST", body: "{}" }),
+ verify: (tenantId: string, id: string, body?: Record) =>
+ request(\`/api/v1/payment-requests/\${id}/verify\`, {
+ tenantId,
+ method: "POST",
+ body: JSON.stringify(body ?? { status: "paid" }),
+ }),
+ },
+ transactions: {
+ list: (tenantId: string) => request("/api/v1/payment-transactions", { tenantId }),
+ get: (tenantId: string, id: string) =>
+ request(\`/api/v1/payment-transactions/\${id}\`, { tenantId }),
+ bySource: (tenantId: string, service?: string, refId?: string) => {
+ const sp = new URLSearchParams();
+ if (service) sp.set("service", service);
+ if (refId) sp.set("ref_id", refId);
+ const q = sp.toString();
+ return request(\`/api/v1/payment-transactions/by-source\${q ? \`?\${q}\` : ""}\`, {
+ tenantId,
+ });
+ },
+ },
+ ledger: {
+ list: (tenantId: string) => request("/api/v1/payment-ledger-entries", { tenantId }),
+ },
+};
+
+export const PAYMENT_API_CATALOG = [
+ { method: "GET", path: "/health", phase: "14.0" },
+ { method: "GET", path: "/capabilities", phase: "14.0" },
+ { method: "GET", path: "/metrics", phase: "14.0" },
+ { method: "GET|POST", path: "/api/v1/payment-workspaces", phase: "14.0" },
+ { method: "GET|POST", path: "/api/v1/tenant-bundles", phase: "14.0" },
+ { method: "GET|POST", path: "/api/v1/feature-toggles", phase: "14.0" },
+ { method: "GET|POST", path: "/api/v1/psp-connections", phase: "14.1" },
+ { method: "POST", path: "/api/v1/psp-connections/{id}/test", phase: "14.1" },
+ { method: "GET|POST", path: "/api/v1/merchant-accounts", phase: "14.2" },
+ { method: "POST", path: "/api/v1/payment-requests", phase: "14.3" },
+ { method: "POST", path: "/api/v1/payment-requests/{id}/verify", phase: "14.4" },
+ { method: "GET", path: "/api/v1/payment-transactions", phase: "14.5" },
+ { method: "GET", path: "/api/v1/payment-ledger-entries", phase: "14.5" },
+] as const;
+`
+);
diff --git a/frontend/scripts/scaffold-payment-routes.mjs b/frontend/scripts/scaffold-payment-routes.mjs
new file mode 100644
index 0000000..e3eb1ba
--- /dev/null
+++ b/frontend/scripts/scaffold-payment-routes.mjs
@@ -0,0 +1,120 @@
+#!/usr/bin/env node
+/** Scaffold payment app routes — thin wrappers to @/modules/payment/features */
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
+const APP = path.join(ROOT, "app", "payment");
+
+const ROUTES = [
+ { dir: "hub", export: "PaymentHub" },
+ { dir: "dashboard", export: "PaymentDashboard" },
+ { dir: "overview", export: "PaymentOverview" },
+ { dir: "kpis", export: "PaymentKpis" },
+ { dir: "transactions", export: "PaymentTransactions" },
+ { dir: "transactions/[id]", export: "PaymentTransactionDetail" },
+ { dir: "ledger", export: "PaymentLedger" },
+ { dir: "psp", export: "PaymentPsp" },
+ { dir: "psp/health", export: "PaymentGatewayHealth" },
+ { dir: "providers", export: "PaymentProviders" },
+ { dir: "merchants", export: "PaymentMerchants" },
+ { dir: "merchants/[id]", export: "PaymentMerchantDetail" },
+ { dir: "merchants/approvals", export: "PaymentMerchantApprovals" },
+ { dir: "requests", export: "PaymentRequests" },
+ { dir: "requests/[id]", export: "PaymentRequestDetail" },
+ { dir: "callbacks", export: "PaymentCallbacks" },
+ { dir: "verification", export: "PaymentVerification" },
+ { dir: "audit", export: "PaymentAudit" },
+ { dir: "bundles", export: "PaymentBundles" },
+ { dir: "feature-toggles", export: "PaymentFeatureToggles" },
+ { dir: "capabilities", export: "PaymentCapabilities" },
+ { dir: "settings", export: "PaymentSettings" },
+ { dir: "metrics", export: "PaymentMetrics" },
+ { dir: "monitoring", export: "PaymentMonitoring" },
+ { dir: "api-explorer", export: "PaymentApiExplorer" },
+ { dir: "openapi", export: "PaymentOpenApi" },
+];
+
+const LOADING = `import { LoadingState } from "@/components/ds";
+export default function Loading() {
+ return ;
+}
+`;
+
+const ERROR = `"use client";
+import { ErrorState } from "@/components/ds";
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return ;
+}
+`;
+
+function featureImport(exportName, dir) {
+ const featureMap = {
+ PaymentHub: "hub",
+ PaymentDashboard: "dashboard",
+ PaymentOverview: "overview",
+ PaymentKpis: "kpis",
+ PaymentTransactions: "transactions",
+ PaymentTransactionDetail: "transactionDetail",
+ PaymentLedger: "ledger",
+ PaymentPsp: "psp",
+ PaymentGatewayHealth: "gatewayHealth",
+ PaymentProviders: "providers",
+ PaymentMerchants: "merchants",
+ PaymentMerchantDetail: "merchantDetail",
+ PaymentMerchantApprovals: "merchantApprovals",
+ PaymentRequests: "requests",
+ PaymentRequestDetail: "requestDetail",
+ PaymentCallbacks: "callbacks",
+ PaymentVerification: "verification",
+ PaymentAudit: "audit",
+ PaymentBundles: "bundles",
+ PaymentFeatureToggles: "featureToggles",
+ PaymentCapabilities: "capabilities",
+ PaymentSettings: "settings",
+ PaymentMetrics: "metrics",
+ PaymentMonitoring: "monitoring",
+ PaymentApiExplorer: "apiExplorer",
+ PaymentOpenApi: "openapi",
+ };
+ const feature = featureMap[exportName];
+ return `"use client";
+export { ${exportName} as default } from "@/modules/payment/features/${feature}";
+`;
+}
+
+for (const { dir, export: exp } of ROUTES) {
+ const routeDir = path.join(APP, dir);
+ fs.mkdirSync(routeDir, { recursive: true });
+ fs.writeFileSync(path.join(routeDir, "page.tsx"), featureImport(exp, dir));
+ fs.writeFileSync(path.join(routeDir, "loading.tsx"), LOADING);
+ fs.writeFileSync(path.join(routeDir, "error.tsx"), ERROR);
+}
+
+// layout
+fs.writeFileSync(
+ path.join(APP, "layout.tsx"),
+ `"use client";
+
+import { PaymentRootLayout } from "@/modules/payment/components/createPortalLayout";
+
+export default PaymentRootLayout;
+`
+);
+
+// root page redirect
+fs.writeFileSync(
+ path.join(APP, "page.tsx"),
+ `import { redirect } from "next/navigation";
+
+export default function PaymentIndexPage() {
+ redirect("/payment/hub");
+}
+`
+);
+
+fs.writeFileSync(path.join(APP, "loading.tsx"), LOADING);
+fs.writeFileSync(path.join(APP, "error.tsx"), ERROR);
+
+console.log(`Scaffolded ${ROUTES.length} payment routes`);
diff --git a/frontend/scripts/validate-payment-routes.mjs b/frontend/scripts/validate-payment-routes.mjs
new file mode 100644
index 0000000..a95aae2
--- /dev/null
+++ b/frontend/scripts/validate-payment-routes.mjs
@@ -0,0 +1,119 @@
+#!/usr/bin/env node
+/** Validates payment MVP routes and module files exist */
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
+const APP = path.join(ROOT, "app", "payment");
+const MODULE = path.join(ROOT, "modules", "payment");
+const MAX_LINES = 25;
+let errors = [];
+
+const MVP_ROUTES = [
+ "hub",
+ "dashboard",
+ "overview",
+ "kpis",
+ "transactions",
+ "ledger",
+ "psp",
+ "psp/health",
+ "providers",
+ "merchants",
+ "merchants/approvals",
+ "requests",
+ "callbacks",
+ "verification",
+ "audit",
+ "bundles",
+ "feature-toggles",
+ "capabilities",
+ "settings",
+ "metrics",
+ "monitoring",
+ "api-explorer",
+ "openapi",
+];
+
+const FEATURE_FILES = [
+ "hub.tsx",
+ "dashboard.tsx",
+ "overview.tsx",
+ "kpis.tsx",
+ "transactions.tsx",
+ "transactionDetail.tsx",
+ "ledger.tsx",
+ "psp.tsx",
+ "gatewayHealth.tsx",
+ "providers.tsx",
+ "merchants.tsx",
+ "merchantDetail.tsx",
+ "merchantApprovals.tsx",
+ "requests.tsx",
+ "requestDetail.tsx",
+ "callbacks.tsx",
+ "verification.tsx",
+ "audit.tsx",
+ "bundles.tsx",
+ "featureToggles.tsx",
+ "capabilities.tsx",
+ "settings.tsx",
+ "metrics.tsx",
+ "monitoring.tsx",
+ "apiExplorer.tsx",
+ "openapi.tsx",
+];
+
+const REQUIRED_FILES = [
+ path.join(MODULE, "index.ts"),
+ path.join(MODULE, "services", "payment-api.ts"),
+ path.join(ROOT, "app", "api", "payment", "[...path]", "route.ts"),
+ path.join(APP, "layout.tsx"),
+ path.join(APP, "page.tsx"),
+];
+
+function walkPages(dir) {
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
+ const p = path.join(dir, ent.name);
+ if (ent.isDirectory()) walkPages(p);
+ else if (ent.name === "page.tsx") checkPage(p);
+ }
+}
+
+function checkPage(file) {
+ const rel = path.relative(APP, file);
+ const content = fs.readFileSync(file, "utf8");
+ const lines = content.split("\n").length;
+ if (rel !== "page.tsx" && lines > MAX_LINES) {
+ errors.push(`${rel}: ${lines} lines (max ${MAX_LINES})`);
+ }
+ if (rel !== "page.tsx" && !content.includes("@/modules/payment/")) {
+ errors.push(`${rel}: must import from @/modules/payment/`);
+ }
+}
+
+for (const route of MVP_ROUTES) {
+ for (const file of ["page.tsx", "loading.tsx", "error.tsx"]) {
+ const p = path.join(APP, route, file);
+ if (!fs.existsSync(p)) errors.push(`missing app/payment/${route}/${file}`);
+ }
+}
+
+for (const f of FEATURE_FILES) {
+ const p = path.join(MODULE, "features", f);
+ if (!fs.existsSync(p)) errors.push(`missing modules/payment/features/${f}`);
+}
+
+for (const f of REQUIRED_FILES) {
+ if (!fs.existsSync(f)) errors.push(`missing ${path.relative(ROOT, f)}`);
+}
+
+if (fs.existsSync(APP)) walkPages(APP);
+else errors.push("app/payment not found");
+
+if (errors.length) {
+ console.error("Payment route validation failed:\n" + errors.map((e) => " - " + e).join("\n"));
+ process.exit(1);
+}
+console.log(`Payment routes OK (${MVP_ROUTES.length} MVP routes, ${FEATURE_FILES.length} features)`);