feat(payment): ship Torbat Pay MVP phases 14.0-14.5

Add independent payment-service (port 8012, payment_db) with foundation licensing, BYO-PSP, merchant accounts, idempotent requests, callbacks, and immutable ledger.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Mortezakoohjani 2026-07-27 17:57:04 +03:30
parent 047cd17afd
commit 9fac160258
113 changed files with 5507 additions and 27 deletions

View File

@ -0,0 +1,8 @@
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1 PYTHONPATH=/app
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends build-essential libpq-dev && rm -rf /var/lib/apt/lists/*
COPY backend/shared-lib/ /shared-lib/
COPY backend/services/payment/requirements.txt /app/requirements.txt
RUN sed -i 's#-e ../../shared-lib#-e /shared-lib#' requirements.txt && pip install -r requirements.txt
EXPOSE 8012

View File

@ -0,0 +1,49 @@
# Torbat Pay — Enterprise Payment Platform
Independent, tenant-isolated payment microservice (**Torbat Pay**) implementing phases **14.014.5** (MVP).
| Field | Value |
| --- | --- |
| Service | `payment-service` |
| Version | **0.14.5.0** |
| Port | **8012** |
| Database | `payment_db` |
| API prefix | `/api/v1` |
| OpenAPI | `/docs` (FastAPI auto-generated) |
## Capabilities (14.5)
- Foundation: workspaces, L2 bundles, L3 toggles, provider assignments, audit, outbox
- BYO-PSP: connections, routing, mock adapter + health test
- Torbat Pay Merchant: merchant account lifecycle
- Payment requests: idempotency, mock redirect, credit ref fields (nullable)
- Callbacks: signature verify, replay protection, verify API
- Ledger: immutable transactions + append-only ledger entries
## Reserved (not implemented)
- `CREDIT_PROVIDER` payment source → Torbat Credit (422 until bundle + service exist)
- Refunds, splits, connectors, reconciliation (phases 14.6+)
## Local development
```bash
cd backend/services/payment
pip install -e ../../shared-lib
pip install -r requirements.txt
pytest app/tests -q
```
## Migrations
Alembic head: `0006_phase_145_ledger`
```bash
python scripts/ensure_db.py && alembic upgrade head
```
## Documentation
- [payment-roadmap.md](../../docs/payment-roadmap.md)
- [payment-contracts.md](../../docs/reference/payment-contracts.md)
- [phase-14-5 handover](../../docs/phase-handover/phase-14-5.md)

View File

@ -0,0 +1,29 @@
[alembic]
script_location = alembic
prepend_sys_path = .
sqlalchemy.url = driver://user:pass@localhost/db
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s

View File

@ -0,0 +1,19 @@
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
from app.core.config import settings
from app.core.database import Base
import app.models
config=context.config
config.set_main_option("sqlalchemy.url",settings.database_url_sync)
if config.config_file_name: fileConfig(config.config_file_name)
target_metadata=Base.metadata
def run_migrations_offline():
context.configure(url=settings.database_url_sync,target_metadata=target_metadata,literal_binds=True)
with context.begin_transaction(): context.run_migrations()
def run_migrations_online():
connectable=engine_from_config(config.get_section(config.config_ini_section),prefix="sqlalchemy.",poolclass=pool.NullPool)
with connectable.connect() as connection:
context.configure(connection=connection,target_metadata=target_metadata)
with context.begin_transaction(): context.run_migrations()
run_migrations_offline() if context.is_offline_mode() else run_migrations_online()

View File

@ -0,0 +1,11 @@
from alembic import op
from app.core.database import Base
import app.models
revision="0001_initial"
down_revision=None
branch_labels=None
depends_on=None
def upgrade():
Base.metadata.create_all(bind=op.get_bind())
def downgrade():
Base.metadata.drop_all(bind=op.get_bind())

View File

@ -0,0 +1,29 @@
"""Phase 14.1 — PSP Management schema (additive)."""
from alembic import op
from app.core.database import Base
import app.models # noqa: F401
revision = "0002_phase_141_psp"
down_revision = "0001_initial"
branch_labels = None
depends_on = None
_PSP_TABLES = (
"psp_connections",
"psp_credential_vault_refs",
"psp_connection_health_checks",
"psp_routing_policies",
)
def upgrade():
bind = op.get_bind()
Base.metadata.create_all(bind=bind)
def downgrade():
bind = op.get_bind()
for name in reversed(_PSP_TABLES):
table = Base.metadata.tables.get(name)
if table is not None:
table.drop(bind=bind, checkfirst=True)

View File

@ -0,0 +1,30 @@
"""Phase 14.2 — Merchant Accounts schema (additive)."""
from alembic import op
from app.core.database import Base
import app.models # noqa: F401
revision = "0003_phase_142_merchant"
down_revision = "0002_phase_141_psp"
branch_labels = None
depends_on = None
_MERCHANT_TABLES = (
"merchant_accounts",
"merchant_account_profiles",
"merchant_settlement_profiles",
"merchant_compliance_refs",
"merchant_account_status_history",
)
def upgrade():
bind = op.get_bind()
Base.metadata.create_all(bind=bind)
def downgrade():
bind = op.get_bind()
for name in reversed(_MERCHANT_TABLES):
table = Base.metadata.tables.get(name)
if table is not None:
table.drop(bind=bind, checkfirst=True)

View File

@ -0,0 +1,28 @@
"""Phase 14.3 — Payment Requests schema (additive)."""
from alembic import op
from app.core.database import Base
import app.models # noqa: F401
revision = "0004_phase_143_requests"
down_revision = "0003_phase_142_merchant"
branch_labels = None
depends_on = None
_REQUEST_TABLES = (
"payment_requests",
"payment_request_status_history",
"payment_request_lines",
)
def upgrade():
bind = op.get_bind()
Base.metadata.create_all(bind=bind)
def downgrade():
bind = op.get_bind()
for name in reversed(_REQUEST_TABLES):
table = Base.metadata.tables.get(name)
if table is not None:
table.drop(bind=bind, checkfirst=True)

View File

@ -0,0 +1,27 @@
"""Phase 14.4 — Callback & Verification schema (additive)."""
from alembic import op
from app.core.database import Base
import app.models # noqa: F401
revision = "0005_phase_144_callbacks"
down_revision = "0004_phase_143_requests"
branch_labels = None
depends_on = None
_CALLBACK_TABLES = (
"payment_callback_logs",
"payment_verification_attempts",
)
def upgrade():
bind = op.get_bind()
Base.metadata.create_all(bind=bind)
def downgrade():
bind = op.get_bind()
for name in reversed(_CALLBACK_TABLES):
table = Base.metadata.tables.get(name)
if table is not None:
table.drop(bind=bind, checkfirst=True)

View File

@ -0,0 +1,28 @@
"""Phase 14.5 — Transaction Ledger schema (additive)."""
from alembic import op
from app.core.database import Base
import app.models # noqa: F401
revision = "0006_phase_145_ledger"
down_revision = "0005_phase_144_callbacks"
branch_labels = None
depends_on = None
_LEDGER_TABLES = (
"payment_transactions",
"payment_transaction_fee_lines",
"payment_ledger_entries",
)
def upgrade():
bind = op.get_bind()
Base.metadata.create_all(bind=bind)
def downgrade():
bind = op.get_bind()
for name in reversed(_LEDGER_TABLES):
table = Base.metadata.tables.get(name)
if table is not None:
table.drop(bind=bind, checkfirst=True)

View File

@ -0,0 +1 @@
__version__ = "0.14.5.0"

View File

@ -0,0 +1,9 @@
from uuid import UUID
from fastapi import Request
from shared.exceptions import TenantNotResolvedError
from shared.tenant import STATE_TENANT_ID
from app.core.database import get_db
def require_tenant(request: Request):
tenant_id=getattr(request.state,STATE_TENANT_ID,None)
if tenant_id is None: raise TenantNotResolvedError("X-Tenant-ID required")
return UUID(str(tenant_id))

View File

@ -0,0 +1,41 @@
from fastapi import Depends
from app.core.config import settings
from app.core.security import get_current_user
from shared.exceptions import ForbiddenError
_ADMINS = {"platform_admin", "tenant_owner", "tenant_admin"}
def user_has_permission(user, permission: str) -> bool:
roles = set(user.roles)
if roles & _ADMINS or "payment.*" in roles:
return True
if permission in roles:
return True
parts = permission.split(".")
return len(parts) >= 3 and f"{parts[0]}.{parts[1]}.manage" in roles
def require_permissions(*permissions: str):
async def dependency(user=Depends(get_current_user)):
if not settings.auth_required or any(
user_has_permission(user, permission) for permission in permissions
):
return user
raise ForbiddenError(
"Permission denied",
error_code="permission_denied",
details={"required": list(permissions)},
)
return dependency
async def require_payment_access(user=Depends(get_current_user)):
if not settings.auth_required:
return user
roles = set(user.roles)
if roles & _ADMINS or any(role.startswith("payment.") for role in roles):
return user
raise ForbiddenError("Permission denied", error_code="permission_denied")

View File

@ -0,0 +1,5 @@
from fastapi import APIRouter
from app.api.v1 import health
from app.api.v1.routes import router as payment_router
api_router=APIRouter()
api_router.include_router(payment_router)

View File

@ -0,0 +1,17 @@
from fastapi import APIRouter, Depends
from sqlalchemy import select
from app import __version__
from app.api.deps import require_tenant
from app.core.database import get_db
from app.models import PaymentWorkspace, TenantPaymentBundle, PaymentFeatureToggle
router=APIRouter()
@router.get("/health")
async def health(): return {"status":"ok","service":"payment-service","version":__version__}
@router.get("/metrics")
async def metrics(): return {"service":"payment-service","version":__version__,"phase":"14.5","metrics":{"outbox_events":"tenant_scoped","transactions":"tenant_scoped"}}
@router.get("/capabilities")
async def capabilities(tenant_id=Depends(require_tenant),db=Depends(get_db)):
ws=(await db.execute(select(PaymentWorkspace).where(PaymentWorkspace.tenant_id==tenant_id))).scalars().first()
bundles=(await db.execute(select(TenantPaymentBundle).where(TenantPaymentBundle.tenant_id==tenant_id,TenantPaymentBundle.is_active.is_(True)))).scalars().all()
toggles=(await db.execute(select(PaymentFeatureToggle).where(PaymentFeatureToggle.tenant_id==tenant_id))).scalars().all()
return {"service":"payment","version":__version__,"phase":"14.5","workspace_status":ws.status if ws else "disabled","default_payment_mode":ws.default_payment_mode if ws else "byo_psp","active_bundles":[x.bundle_code for x in bundles],"feature_toggles":{x.key:x.enabled for x in toggles},"payment_sources_supported":["PSP","MERCHANT_FACILITATOR"],"payment_sources_reserved":["CREDIT_PROVIDER"],"contract_versions":{"payment_intent":"v1","callback_ingress":"v1"}}

View File

@ -0,0 +1,207 @@
from __future__ import annotations
from datetime import datetime
from uuid import UUID
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from app import __version__
from app.api.deps import require_tenant
from app.api.permissions import require_payment_access
from app.core.database import get_db
from app.events.publisher import TransactionalEventPublisher
from app.models import *
from app.permissions.definitions import ALL_PERMISSIONS
from app.providers import ADAPTERS
router=APIRouter(dependencies=[Depends(require_payment_access)])
def dump(row, redact=False):
out={c.name:getattr(row,c.name) for c in row.__table__.columns}
if redact:
out.pop("secret_config",None)
if "credential_vault_ref" in out and out["credential_vault_ref"]: out["credential_vault_ref"]="***REDACTED***"
if isinstance(out.get("data"),dict):
out["data"]={k:("***REDACTED***" if any(s in k.lower() for s in ("secret","password","token","credential")) else v) for k,v in out["data"].items()}
return out
async def active_bundles(db,tenant_id):
rows=(await db.execute(select(TenantPaymentBundle).where(TenantPaymentBundle.tenant_id==tenant_id,TenantPaymentBundle.is_active.is_(True)))).scalars().all()
return {r.bundle_code for r in rows}
async def gate(db,tenant_id,bundle):
if bundle not in await active_bundles(db,tenant_id): raise HTTPException(403,detail={"code":"bundle_inactive","required_bundle":bundle})
async def audit(db,tenant_id,entity,action):
db.add(PaymentAuditLog(tenant_id=tenant_id,entity_type=entity.__class__.__name__,entity_id=entity.id,action=action,changes={}))
async def emit(db,tenant_id,entity,event):
await TransactionalEventPublisher(db).publish(event_type=event,aggregate_type=entity.__class__.__name__,aggregate_id=entity.id,tenant_id=tenant_id,payload={"id":str(entity.id)})
FOUNDATION={
"payment-workspaces":PaymentWorkspace,"bundle-definitions":PaymentBundleDefinition,"tenant-bundles":TenantPaymentBundle,
"feature-toggles":PaymentFeatureToggle,"provider-assignments":PaymentProviderAssignment,
"psp-provider-registrations":PspProviderRegistration,"credit-provider-registrations":CreditProviderRegistration,
"configurations":PaymentConfiguration,"settings":PaymentSetting}
PSP={"psp-connections":PspConnection,"psp-routing-policies":PspRoutingPolicy}
def build_crud(path,model,redact=False,bundle=None):
async def create(body:dict,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
if bundle: await gate(db,tenant_id,bundle)
allowed={c.name for c in model.__table__.columns}-{"id","tenant_id","created_at","updated_at","created_by","updated_by"}
values={k:v for k,v in body.items() if k in allowed}
extra={k:v for k,v in body.items() if k not in allowed}
if "data" in allowed: values["data"]={**values.get("data",{}),**extra}
row=model(tenant_id=tenant_id,**values); db.add(row); await db.flush(); await audit(db,tenant_id,row,"create"); await db.commit(); await db.refresh(row)
return dump(row,redact)
async def listing(tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
if bundle: await gate(db,tenant_id,bundle)
rows=(await db.execute(select(model).where(model.tenant_id==tenant_id))).scalars().all()
return [dump(x,redact) for x in rows]
async def get_one(entity_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
if bundle: await gate(db,tenant_id,bundle)
row=(await db.execute(select(model).where(model.tenant_id==tenant_id,model.id==entity_id))).scalar_one_or_none()
if not row: raise HTTPException(404,"Not found")
return dump(row,redact)
async def update(entity_id:UUID,body:dict,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
if bundle: await gate(db,tenant_id,bundle)
row=(await db.execute(select(model).where(model.tenant_id==tenant_id,model.id==entity_id))).scalar_one_or_none()
if not row: raise HTTPException(404,"Not found")
cols={c.name for c in model.__table__.columns}
for k,v in body.items():
if k in cols and k not in {"id","tenant_id","created_at"}: setattr(row,k,v)
elif hasattr(row,"data"): row.data={**(row.data or {}),k:v}
await audit(db,tenant_id,row,"update"); await db.commit(); await db.refresh(row); return dump(row,redact)
router.add_api_route(f"/{path}",create,methods=["POST"],status_code=201,name=f"create_{path}")
router.add_api_route(f"/{path}",listing,methods=["GET"],name=f"list_{path}")
router.add_api_route(f"/{path}/{{entity_id}}",get_one,methods=["GET"],name=f"get_{path}")
router.add_api_route(f"/{path}/{{entity_id}}",update,methods=["PATCH","PUT"],name=f"update_{path}")
for p,m in FOUNDATION.items(): build_crud(p,m)
for p,m in PSP.items(): build_crud(p,m,p=="psp-connections","payment.byo_psp.basic")
build_crud("merchant-settlement-profiles",MerchantSettlementProfile,bundle="payment.torbat_pay.merchant")
@router.get("/permissions/catalog")
async def permissions_catalog(): return {"permissions":ALL_PERMISSIONS}
@router.get("/audit")
async def list_audit(tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
return [dump(x) for x in (await db.execute(select(PaymentAuditLog).where(PaymentAuditLog.tenant_id==tenant_id))).scalars().all()]
@router.post("/psp-connections/{connection_id}/test")
async def test_connection(connection_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
await gate(db,tenant_id,"payment.byo_psp.basic")
row=(await db.execute(select(PspConnection).where(PspConnection.tenant_id==tenant_id,PspConnection.id==connection_id))).scalar_one_or_none()
if not row: raise HTTPException(404,"Not found")
check=PspConnectionHealthCheck(tenant_id=tenant_id,status="healthy",code=row.provider_code,name="connection test",data={"connection_id":str(row.id)})
db.add(check); await db.commit(); return {"status":"healthy","connection_id":connection_id}
@router.post("/merchant-accounts",status_code=201)
async def create_merchant(body:dict,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
await gate(db,tenant_id,"payment.torbat_pay.merchant")
row=MerchantAccount(tenant_id=tenant_id,status="draft",code=body.get("code"),name=body.get("name"),data=body)
db.add(row); await db.flush(); await audit(db,tenant_id,row,"create"); await db.commit(); await db.refresh(row); return dump(row)
@router.get("/merchant-accounts")
async def list_merchants(tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
await gate(db,tenant_id,"payment.torbat_pay.merchant")
return [dump(x) for x in (await db.execute(select(MerchantAccount).where(MerchantAccount.tenant_id==tenant_id))).scalars().all()]
@router.get("/merchant-accounts/{merchant_id}")
async def get_merchant(merchant_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
await gate(db,tenant_id,"payment.torbat_pay.merchant")
row=(await db.execute(select(MerchantAccount).where(MerchantAccount.tenant_id==tenant_id,MerchantAccount.id==merchant_id))).scalar_one_or_none()
if not row: raise HTTPException(404,"Not found")
return dump(row)
async def merchant_transition(merchant_id, target, allowed, tenant_id, db):
await gate(db,tenant_id,"payment.torbat_pay.merchant")
row=(await db.execute(select(MerchantAccount).where(MerchantAccount.tenant_id==tenant_id,MerchantAccount.id==merchant_id))).scalar_one_or_none()
if not row: raise HTTPException(404,"Not found")
if row.status not in allowed: raise HTTPException(409,f"Invalid transition {row.status} -> {target}")
old=row.status; row.status=target
db.add(MerchantAccountStatusHistory(tenant_id=tenant_id,status=target,data={"merchant_account_id":str(row.id),"from":old,"to":target}))
await audit(db,tenant_id,row,target); await db.commit(); await db.refresh(row); return dump(row)
@router.post("/merchant-accounts/{merchant_id}/submit")
async def submit_merchant(merchant_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)): return await merchant_transition(merchant_id,"pending_review",{"draft"},tenant_id,db)
@router.post("/merchant-accounts/{merchant_id}/activate")
async def activate_merchant(merchant_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)): return await merchant_transition(merchant_id,"active",{"pending_review","suspended"},tenant_id,db)
@router.post("/merchant-accounts/{merchant_id}/suspend")
async def suspend_merchant(merchant_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)): return await merchant_transition(merchant_id,"suspended",{"active"},tenant_id,db)
@router.post("/merchant-accounts/{merchant_id}/close")
async def close_merchant(merchant_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)): return await merchant_transition(merchant_id,"closed",{"draft","pending_review","active","suspended"},tenant_id,db)
@router.post("/payment-requests",status_code=201)
async def create_request(body:dict,idempotency_key:str|None=Header(None,alias="Idempotency-Key"),tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
if not idempotency_key: raise HTTPException(400,detail={"code":"idempotency_key_required"})
source=body.get("payment_source","PSP")
if source=="CREDIT_PROVIDER": raise HTTPException(422,detail={"code":"credit_provider_not_implemented"})
required="payment.byo_psp.basic" if source=="PSP" else "payment.torbat_pay.merchant"
await gate(db,tenant_id,required)
existing=(await db.execute(select(PaymentRequest).where(PaymentRequest.tenant_id==tenant_id,PaymentRequest.idempotency_key==idempotency_key))).scalar_one_or_none()
if existing: return dump(existing)
src=body.get("source") or {}
row=PaymentRequest(tenant_id=tenant_id,idempotency_key=idempotency_key,amount_minor=body.get("amount_minor",0),currency=body.get("currency","IRR"),payment_source=source,provider_code=body.get("provider_code","mock"),status="pending",source_service=src.get("service"),source_ref_type=src.get("ref_type"),source_ref_id=str(src.get("ref_id")) if src.get("ref_id") else None,credit_provider_id=body.get("credit_provider_id"),financing_reference=body.get("financing_reference"),credit_authorization_reference=body.get("credit_authorization_reference"),installment_contract_reference=body.get("installment_contract_reference"),data=body)
db.add(row); await db.flush(); await emit(db,tenant_id,row,"payment.request.created")
result=await ADAPTERS.get(row.provider_code,ADAPTERS["mock"]).initiate_payment({"id":str(row.id),**body})
row.redirect_url=result["redirect_url"]; row.status="redirect_issued"
db.add(PaymentRequestStatusHistory(tenant_id=tenant_id,status=row.status,data={"payment_request_id":str(row.id)}))
await emit(db,tenant_id,row,"payment.request.redirect_issued"); await db.commit(); await db.refresh(row); return dump(row)
@router.get("/payment-requests")
async def list_requests(tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
return [dump(x) for x in (await db.execute(select(PaymentRequest).where(PaymentRequest.tenant_id==tenant_id))).scalars().all()]
@router.get("/payment-requests/{request_id}")
async def get_request(request_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
row=(await db.execute(select(PaymentRequest).where(PaymentRequest.tenant_id==tenant_id,PaymentRequest.id==request_id))).scalar_one_or_none()
if not row: raise HTTPException(404,"Not found")
return dump(row)
@router.post("/payment-requests/{request_id}/cancel")
async def cancel_request(request_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
row=(await db.execute(select(PaymentRequest).where(PaymentRequest.tenant_id==tenant_id,PaymentRequest.id==request_id))).scalar_one_or_none()
if not row: raise HTTPException(404,"Not found")
if row.status not in {"draft","pending","redirect_issued"}: raise HTTPException(409,"Cannot cancel")
row.status="cancelled"; db.add(PaymentRequestStatusHistory(tenant_id=tenant_id,status="cancelled",data={"payment_request_id":str(row.id)})); await db.commit(); await db.refresh(row); return dump(row)
async def verify_and_ledger(row,payload,tenant_id,db):
result=await ADAPTERS.get(row.provider_code,ADAPTERS["mock"]).verify_payment(payload)
db.add(PaymentVerificationAttempt(tenant_id=tenant_id,status="verified" if result["verified"] else "failed",data={"payment_request_id":str(row.id),**result}))
row.status=result["status"]; row.provider_transaction_id=result.get("provider_transaction_id")
db.add(PaymentRequestStatusHistory(tenant_id=tenant_id,status=row.status,data={"payment_request_id":str(row.id)}))
if row.status=="paid":
tx=(await db.execute(select(PaymentTransaction).where(PaymentTransaction.tenant_id==tenant_id,PaymentTransaction.payment_request_id==row.id))).scalar_one_or_none()
if not tx:
tx=PaymentTransaction(tenant_id=tenant_id,payment_request_id=row.id,amount_minor=row.amount_minor,currency=row.currency,provider_transaction_id=row.provider_transaction_id,status="recorded",data={"source_service":row.source_service,"source_ref_type":row.source_ref_type,"source_ref_id":row.source_ref_id})
db.add(tx); await db.flush()
db.add(PaymentLedgerEntry(tenant_id=tenant_id,payment_transaction_id=tx.id,amount_minor=row.amount_minor,direction="credit",status="posted",data={}))
await emit(db,tenant_id,tx,"payment.transaction.recorded")
await emit(db,tenant_id,row,"payment.request.paid")
else: await emit(db,tenant_id,row,"payment.request.failed")
await db.commit(); await db.refresh(row); return row
@router.post("/callbacks/{provider_code}/{connection_id}")
async def callback(provider_code:str,connection_id:UUID,body:dict,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
connection=(await db.execute(select(PspConnection).where(
PspConnection.tenant_id==tenant_id,
PspConnection.id==connection_id,
PspConnection.provider_code==provider_code,
))).scalar_one_or_none()
if not connection: raise HTTPException(404,"PSP connection not found")
replay=str(body.get("callback_id") or body.get("provider_transaction_id") or body.get("idempotency_key") or "")
if not replay: raise HTTPException(400,"Callback replay key required")
existing=(await db.execute(select(PaymentCallbackLog).where(PaymentCallbackLog.tenant_id==tenant_id,PaymentCallbackLog.replay_key==replay))).scalar_one_or_none()
if existing: return {"status":"duplicate","callback_log_id":existing.id}
adapter=ADAPTERS.get(provider_code)
if not adapter or not await adapter.verify_signature(body): raise HTTPException(401,"Invalid callback signature")
log=PaymentCallbackLog(tenant_id=tenant_id,connection_id=connection_id,provider_code=provider_code,replay_key=replay,status="received",data=body); db.add(log)
request_id=body.get("payment_request_id")
row=(await db.execute(select(PaymentRequest).where(PaymentRequest.tenant_id==tenant_id,PaymentRequest.id==UUID(request_id)))).scalar_one_or_none() if request_id else None
if not row: raise HTTPException(404,"Payment request not found")
await verify_and_ledger(row,body,tenant_id,db); return {"status":"processed","payment_request":dump(row)}
@router.post("/payment-requests/{request_id}/verify")
async def verify(request_id:UUID,body:dict=None,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
row=(await db.execute(select(PaymentRequest).where(PaymentRequest.tenant_id==tenant_id,PaymentRequest.id==request_id))).scalar_one_or_none()
if not row: raise HTTPException(404,"Not found")
return dump(await verify_and_ledger(row,body or {"status":"paid"},tenant_id,db))
@router.get("/payment-transactions")
async def transactions(tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
return [dump(x) for x in (await db.execute(select(PaymentTransaction).where(PaymentTransaction.tenant_id==tenant_id))).scalars().all()]
@router.get("/payment-transactions/by-source")
async def transactions_by_source(service:str|None=None,ref_id:str|None=None,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
rows=(await db.execute(select(PaymentTransaction).where(PaymentTransaction.tenant_id==tenant_id))).scalars().all()
return [dump(x) for x in rows if (not service or x.data.get("source_service")==service) and (not ref_id or x.data.get("source_ref_id")==ref_id)]
@router.get("/payment-transactions/{transaction_id}")
async def transaction(transaction_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
row=(await db.execute(select(PaymentTransaction).where(PaymentTransaction.tenant_id==tenant_id,PaymentTransaction.id==transaction_id))).scalar_one_or_none()
if not row: raise HTTPException(404,"Not found")
return dump(row)
@router.get("/payment-ledger-entries")
async def ledger(tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
return [dump(x) for x in (await db.execute(select(PaymentLedgerEntry).where(PaymentLedgerEntry.tenant_id==tenant_id))).scalars().all()]

View File

@ -0,0 +1,7 @@
from dataclasses import dataclass
from uuid import UUID
@dataclass
class CreatePaymentRequestCommand:
tenant_id: UUID
idempotency_key: str
payload: dict

View File

@ -0,0 +1,29 @@
from functools import lru_cache
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", case_sensitive=False, extra="ignore")
environment: str = "development"
service_name: str = "payment-service"
api_v1_prefix: str = "/api/v1"
database_url: str = Field(default="postgresql+asyncpg://superapp:superapp_password@localhost:5432/payment_db", validation_alias="PAYMENT_DATABASE_URL")
database_url_sync: str = Field(default="postgresql+psycopg://superapp:superapp_password@localhost:5432/payment_db", validation_alias="PAYMENT_DATABASE_URL_SYNC")
core_service_url: str = "http://localhost:8000"
auth_required: bool = True
entitlement_stub: bool = True
keycloak_enabled: bool = True
keycloak_server_url: str = "http://localhost:8080"
keycloak_public_url: str = ""
keycloak_realm: str = "superapp"
jwt_algorithm: str = "RS256"
jwt_audience: str = "account"
jwt_verify_signature: bool = True
cors_origins: str = "http://localhost:3000,http://127.0.0.1:3000"
@property
def keycloak_public_realm_url(self): return f"{(self.keycloak_public_url or self.keycloak_server_url).rstrip('/')}/realms/{self.keycloak_realm}"
@property
def cors_origin_list(self): return [x.strip() for x in self.cors_origins.split(",") if x.strip()]
@lru_cache
def get_settings(): return Settings()
settings = get_settings()

View File

@ -0,0 +1,16 @@
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.pool import StaticPool
from app.core.config import settings
class Base(DeclarativeBase): pass
kw = {"pool_pre_ping": True}
if settings.database_url.startswith("sqlite"):
kw.update(poolclass=StaticPool, connect_args={"check_same_thread": False})
engine = create_async_engine(settings.database_url, **kw)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
async def get_db():
async with AsyncSessionLocal() as session:
try: yield session
except Exception:
await session.rollback()
raise

View File

@ -0,0 +1,6 @@
from uuid import UUID
from app.core.config import settings
class CoreEntitlementClient:
async def has_access(self, tenant_id: UUID, feature_key: str = "payment.module.enabled") -> bool:
if not settings.auth_required or settings.entitlement_stub: return True
return False

View File

@ -0,0 +1,11 @@
from fastapi import Depends
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from app.core.config import settings
from shared.security import CurrentUser
from shared.exceptions import UnauthorizedError
bearer = HTTPBearer(auto_error=False)
async def get_current_user(credentials: HTTPAuthorizationCredentials | None = Depends(bearer)):
if not settings.auth_required:
return CurrentUser(user_id="test-user", username="test", roles=["tenant_admin"])
if not credentials: raise UnauthorizedError("Authentication required")
return CurrentUser(user_id="token-user", username="token", roles=[])

View File

@ -0,0 +1,7 @@
from uuid import uuid4
from app.models import OutboxEvent
class TransactionalEventPublisher:
def __init__(self, session): self.session=session
async def publish(self, *, event_type, aggregate_type, aggregate_id, tenant_id, payload=None):
row=OutboxEvent(tenant_id=tenant_id,event_type=str(getattr(event_type,"value",event_type)),aggregate_type=aggregate_type,aggregate_id=str(aggregate_id),payload=payload or {})
self.session.add(row); await self.session.flush(); return row

View File

@ -0,0 +1,7 @@
from enum import Enum
class PaymentEventType(str, Enum):
REQUEST_CREATED="payment.request.created"
REQUEST_REDIRECT_ISSUED="payment.request.redirect_issued"
REQUEST_PAID="payment.request.paid"
REQUEST_FAILED="payment.request.failed"
TRANSACTION_RECORDED="payment.transaction.recorded"

View File

@ -0,0 +1,18 @@
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from app import __version__
from app.api.v1 import api_router, health
from app.core.config import settings
from app.middlewares.tenant import TenantHeaderMiddleware
from shared.exceptions import AppError
def create_app():
app=FastAPI(title="Torbat Pay",version=__version__,description="Payment service phases 14.0-14.5")
app.add_middleware(CORSMiddleware,allow_origins=settings.cors_origin_list,allow_credentials=False,allow_methods=["*"],allow_headers=["*"])
app.add_middleware(TenantHeaderMiddleware)
@app.exception_handler(AppError)
async def handle(request:Request,exc:AppError): return JSONResponse(status_code=exc.status_code,content={"error":{"code":exc.error_code,"message":exc.message,"details":exc.details}})
app.include_router(health.router)
app.include_router(api_router,prefix=settings.api_v1_prefix)
return app
app=create_app()

View File

@ -0,0 +1,10 @@
from uuid import UUID
from starlette.middleware.base import BaseHTTPMiddleware
from shared.tenant import HEADER_TENANT_ID, STATE_TENANT_ID
class TenantHeaderMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
raw=request.headers.get(HEADER_TENANT_ID)
try: value=UUID(raw) if raw else None
except ValueError: value=None
setattr(request.state,STATE_TENANT_ID,value)
return await call_next(request)

View File

@ -0,0 +1 @@
from app.models.domain import *

View File

@ -0,0 +1,15 @@
import uuid
from datetime import datetime
from sqlalchemy import DateTime, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.models.types import GUID
class UUIDPrimaryKeyMixin:
id: Mapped[uuid.UUID] = mapped_column(GUID(), primary_key=True, default=uuid.uuid4)
class TenantMixin:
tenant_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False, index=True)
class TimestampMixin:
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
class ActorAuditMixin:
created_by: Mapped[str | None] = mapped_column(String(100))
updated_by: Mapped[str | None] = mapped_column(String(100))

View File

@ -0,0 +1,117 @@
from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import Boolean, DateTime, Integer, String, Text, UniqueConstraint, func
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.types import JSON
from app.core.database import Base
from app.models.base import UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, ActorAuditMixin
from app.models.types import GUID
class Standard(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, ActorAuditMixin):
__abstract__ = True
code: Mapped[str | None] = mapped_column(String(120))
name: Mapped[str | None] = mapped_column(String(255))
status: Mapped[str] = mapped_column(String(40), default="active", nullable=False)
data: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
class PaymentWorkspace(Standard):
__tablename__="payment_workspaces"
status: Mapped[str] = mapped_column(String(40), default="enabled", nullable=False)
default_payment_mode: Mapped[str] = mapped_column(String(40), default="byo_psp", nullable=False)
class PaymentBundleDefinition(Standard): __tablename__="payment_bundle_definitions"
class TenantPaymentBundle(Standard):
__tablename__="tenant_payment_bundles"
bundle_code: Mapped[str] = mapped_column(String(120), nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
__table_args__=(UniqueConstraint("tenant_id","bundle_code",name="uq_tenant_bundle"),)
class PaymentFeatureToggle(Standard):
__tablename__="payment_feature_toggles"
key: Mapped[str] = mapped_column(String(150), nullable=False)
enabled: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
class PaymentProviderAssignment(Standard): __tablename__="payment_provider_assignments"
class PspProviderRegistration(Standard):
__tablename__="psp_provider_registrations"
provider_code: Mapped[str] = mapped_column(String(80), nullable=False)
class CreditProviderRegistration(Standard):
__tablename__="credit_provider_registrations"
provider_code: Mapped[str] = mapped_column(String(80), nullable=False)
class PaymentConfiguration(Standard): __tablename__="payment_configurations"
class PaymentSetting(Standard):
__tablename__="payment_settings"
key: Mapped[str] = mapped_column(String(150), nullable=False)
value: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
class PaymentRole(Standard): __tablename__="payment_roles"
class PaymentPermission(Standard): __tablename__="payment_permissions"
class PaymentAuditLog(Base, UUIDPrimaryKeyMixin, TenantMixin):
__tablename__="payment_audit_logs"
entity_type: Mapped[str] = mapped_column(String(100), nullable=False)
entity_id: Mapped[uuid.UUID | None] = mapped_column(GUID())
action: Mapped[str] = mapped_column(String(50), nullable=False)
actor_user_id: Mapped[str | None] = mapped_column(String(100))
changes: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
class OutboxEvent(Base, UUIDPrimaryKeyMixin, TenantMixin):
__tablename__="outbox_events"
event_type: Mapped[str] = mapped_column(String(150), nullable=False)
aggregate_type: Mapped[str] = mapped_column(String(100), nullable=False)
aggregate_id: Mapped[str] = mapped_column(String(100), nullable=False)
payload: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
status: Mapped[str] = mapped_column(String(30), default="pending", nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
class PspConnection(Standard):
__tablename__="psp_connections"
provider_code: Mapped[str] = mapped_column(String(80), nullable=False)
credential_vault_ref: Mapped[str | None] = mapped_column(String(255))
secret_config: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
class PspCredentialVaultRef(Standard): __tablename__="psp_credential_vault_refs"
class PspConnectionHealthCheck(Standard): __tablename__="psp_connection_health_checks"
class PspRoutingPolicy(Standard): __tablename__="psp_routing_policies"
class MerchantAccount(Standard): __tablename__="merchant_accounts"
class MerchantAccountProfile(Standard): __tablename__="merchant_account_profiles"
class MerchantSettlementProfile(Standard): __tablename__="merchant_settlement_profiles"
class MerchantComplianceRef(Standard): __tablename__="merchant_compliance_refs"
class MerchantAccountStatusHistory(Standard): __tablename__="merchant_account_status_history"
class PaymentRequest(Standard):
__tablename__="payment_requests"
status: Mapped[str] = mapped_column(String(40), default="draft", nullable=False)
idempotency_key: Mapped[str] = mapped_column(String(200), nullable=False)
amount_minor: Mapped[int] = mapped_column(Integer, nullable=False)
currency: Mapped[str] = mapped_column(String(3), default="IRR", nullable=False)
payment_source: Mapped[str] = mapped_column(String(40), default="PSP", nullable=False)
provider_code: Mapped[str] = mapped_column(String(80), default="mock", nullable=False)
provider_transaction_id: Mapped[str | None] = mapped_column(String(200))
redirect_url: Mapped[str | None] = mapped_column(Text)
source_service: Mapped[str | None] = mapped_column(String(80))
source_ref_type: Mapped[str | None] = mapped_column(String(80))
source_ref_id: Mapped[str | None] = mapped_column(String(100))
credit_provider_id: Mapped[uuid.UUID | None] = mapped_column(GUID())
financing_reference: Mapped[str | None] = mapped_column(String(200))
credit_authorization_reference: Mapped[str | None] = mapped_column(String(200))
installment_contract_reference: Mapped[str | None] = mapped_column(String(200))
__table_args__=(UniqueConstraint("tenant_id","idempotency_key",name="uq_payment_request_idem"),)
class PaymentRequestStatusHistory(Standard): __tablename__="payment_request_status_history"
class PaymentRequestLine(Standard): __tablename__="payment_request_lines"
class PaymentCallbackLog(Standard):
__tablename__="payment_callback_logs"
connection_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
provider_code: Mapped[str] = mapped_column(String(80), nullable=False)
replay_key: Mapped[str] = mapped_column(String(250), nullable=False)
__table_args__=(UniqueConstraint("tenant_id","replay_key",name="uq_callback_replay"),)
class PaymentVerificationAttempt(Standard): __tablename__="payment_verification_attempts"
class PaymentTransaction(Standard):
__tablename__="payment_transactions"
payment_request_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
amount_minor: Mapped[int] = mapped_column(Integer, nullable=False)
currency: Mapped[str] = mapped_column(String(3), nullable=False)
provider_transaction_id: Mapped[str | None] = mapped_column(String(200))
__table_args__=(UniqueConstraint("tenant_id","payment_request_id",name="uq_transaction_request"),)
class PaymentTransactionFeeLine(Standard): __tablename__="payment_transaction_fee_lines"
class PaymentLedgerEntry(Standard):
__tablename__="payment_ledger_entries"
payment_transaction_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
amount_minor: Mapped[int] = mapped_column(Integer, nullable=False)
direction: Mapped[str] = mapped_column(String(10), nullable=False)

View File

@ -0,0 +1,15 @@
from app.models.domain import (
CreditProviderRegistration,
OutboxEvent,
PaymentAuditLog,
PaymentBundleDefinition,
PaymentConfiguration,
PaymentFeatureToggle,
PaymentPermission,
PaymentProviderAssignment,
PaymentRole,
PaymentSetting,
PaymentWorkspace,
PspProviderRegistration,
TenantPaymentBundle,
)

View File

@ -0,0 +1,5 @@
from app.models.domain import (
PaymentLedgerEntry,
PaymentTransaction,
PaymentTransactionFeeLine,
)

View File

@ -0,0 +1,7 @@
from app.models.domain import (
MerchantAccount,
MerchantAccountProfile,
MerchantAccountStatusHistory,
MerchantComplianceRef,
MerchantSettlementProfile,
)

View File

@ -0,0 +1,6 @@
from app.models.domain import (
PspConnection,
PspConnectionHealthCheck,
PspCredentialVaultRef,
PspRoutingPolicy,
)

View File

@ -0,0 +1,7 @@
from app.models.domain import (
PaymentCallbackLog,
PaymentRequest,
PaymentRequestLine,
PaymentRequestStatusHistory,
PaymentVerificationAttempt,
)

View File

@ -0,0 +1,11 @@
import uuid
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.types import CHAR, TypeDecorator
class GUID(TypeDecorator):
impl = CHAR
cache_ok = True
def load_dialect_impl(self, dialect): return dialect.type_descriptor(PG_UUID(as_uuid=True) if dialect.name == "postgresql" else CHAR(36))
def process_bind_param(self, value, dialect):
if value is None: return None
return (value if isinstance(value, uuid.UUID) else uuid.UUID(str(value))) if dialect.name == "postgresql" else str(value)
def process_result_value(self, value, dialect): return value if value is None or isinstance(value, uuid.UUID) else uuid.UUID(str(value))

View File

@ -0,0 +1,10 @@
ALL_PERMISSIONS = [
"payment.workspaces.view","payment.workspaces.manage","payment.workspaces.enable","payment.workspaces.disable",
"payment.bundles.view","payment.bundles.manage","payment.feature_toggles.view","payment.feature_toggles.manage",
"payment.configurations.view","payment.configurations.manage","payment.settings.view","payment.settings.manage",
"payment.psp_providers.view","payment.psp_connections.view","payment.psp_connections.create","payment.psp_connections.update","payment.psp_connections.delete","payment.psp_connections.test","payment.psp_connections.manage",
"payment.psp_routing.view","payment.psp_routing.manage","payment.provider_assignments.view","payment.provider_assignments.manage",
"payment.merchant_accounts.view","payment.merchant_accounts.create","payment.merchant_accounts.update","payment.merchant_accounts.submit","payment.merchant_accounts.activate","payment.merchant_accounts.suspend","payment.merchant_accounts.close","payment.merchant_accounts.manage",
"payment.requests.view","payment.requests.create","payment.requests.cancel","payment.requests.manage","payment.callbacks.view",
"payment.transactions.view","payment.transactions.export","payment.ledger.view","payment.audit.view"]
PERMISSION_PREFIXES=("payment.",)

View File

@ -0,0 +1,2 @@
from app.providers.contracts import PspAdapter, CreditProviderAdapter
from app.providers.mock import MockPspAdapter, ZarinpalAdapter, IdpayAdapter, ADAPTERS

View File

@ -0,0 +1,7 @@
from typing import Protocol
class PspAdapter(Protocol):
async def initiate_payment(self, request: dict) -> dict: ...
async def verify_signature(self, payload: dict) -> bool: ...
async def verify_payment(self, payload: dict) -> dict: ...
class CreditProviderAdapter(Protocol):
async def authorize_financing(self, payload: dict) -> dict: ...

View File

@ -0,0 +1,12 @@
class MockPspAdapter:
code="mock"
async def initiate_payment(self, request):
return {"redirect_url": f"https://mock.psp/pay/{request['id']}", "token": f"mock-{request['id']}"}
async def verify_signature(self, payload):
return payload.get("signature") in ("mock-valid", "valid", None)
async def verify_payment(self, payload):
paid=payload.get("normalized_status", payload.get("status","paid"))=="paid"
return {"verified": paid, "status": "paid" if paid else "failed", "provider_transaction_id": payload.get("provider_transaction_id")}
class ZarinpalAdapter(MockPspAdapter): code="zarinpal"
class IdpayAdapter(MockPspAdapter): code="idpay"
ADAPTERS={"mock":MockPspAdapter(),"zarinpal":ZarinpalAdapter(),"idpay":IdpayAdapter()}

View File

@ -0,0 +1,6 @@
from dataclasses import dataclass
from uuid import UUID
@dataclass
class GetPaymentRequestQuery:
tenant_id: UUID
payment_request_id: UUID

View File

@ -0,0 +1,9 @@
from sqlalchemy import select
class TenantBaseRepository:
def __init__(self, session, model): self.session, self.model = session, model
async def add(self, entity):
self.session.add(entity); await self.session.flush(); return entity
async def get(self, tenant_id, entity_id):
return (await self.session.execute(select(self.model).where(self.model.tenant_id==tenant_id,self.model.id==entity_id))).scalar_one_or_none()
async def list(self, tenant_id):
return (await self.session.execute(select(self.model).where(self.model.tenant_id==tenant_id))).scalars().all()

View File

@ -0,0 +1,2 @@
from app.repositories.base import TenantBaseRepository
class PaymentRepository(TenantBaseRepository): pass

View File

@ -0,0 +1,18 @@
import os, uuid
os.environ["ENVIRONMENT"]="test"
os.environ["AUTH_REQUIRED"]="false"
os.environ["PAYMENT_DATABASE_URL"]="sqlite+aiosqlite:///:memory:"
os.environ["PAYMENT_DATABASE_URL_SYNC"]="sqlite:///:memory:"
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from app.core.database import Base, engine
import app.models
from app.main import app
TENANT_A=uuid.uuid4()
TENANT_B=uuid.uuid4()
def headers(t=TENANT_A): return {"X-Tenant-ID":str(t)}
@pytest_asyncio.fixture
async def client():
async with engine.begin() as c:
await c.run_sync(Base.metadata.drop_all); await c.run_sync(Base.metadata.create_all)
async with AsyncClient(transport=ASGITransport(app=app),base_url="http://test") as ac: yield ac

View File

@ -0,0 +1,11 @@
import pytest
from app.tests.conftest import headers
@pytest.mark.asyncio
async def test_foundation_flow(client):
assert (await client.get("/health")).status_code==200
w=await client.post("/api/v1/payment-workspaces",headers=headers(),json={"code":"main","name":"Main","status":"enabled","default_payment_mode":"byo_psp"})
assert w.status_code==201
b=await client.post("/api/v1/tenant-bundles",headers=headers(),json={"bundle_code":"payment.byo_psp.basic","is_active":True})
assert b.status_code==201
caps=(await client.get("/capabilities",headers=headers())).json()
assert "payment.byo_psp.basic" in caps["active_bundles"]

View File

@ -0,0 +1,8 @@
from app.core.database import Base
import app.models
def test_all_owned_rows_are_tenant_scoped():
assert all("tenant_id" in t.columns for t in Base.metadata.tables.values())
def test_layers_exist():
from pathlib import Path
root=Path(__file__).parents[2]
for x in ("api","commands","queries","repositories","models","providers","events"): assert (root/"app"/x).exists()

View File

@ -0,0 +1,5 @@
import pytest
from app.core.entitlements import CoreEntitlementClient
from app.tests.conftest import TENANT_A
@pytest.mark.asyncio
async def test_stub_entitlement(): assert await CoreEntitlementClient().has_access(TENANT_A)

View File

@ -0,0 +1,2 @@
from pathlib import Path
def test_readme_documents_version(): assert "0.14.5.0" in (Path(__file__).parents[2]/"README.md").read_text()

View File

@ -0,0 +1,4 @@
from pathlib import Path
def test_all_phase_migrations_exist():
names={p.stem for p in (Path(__file__).parents[2]/"alembic"/"versions").glob("*.py")}
assert {"0001_initial","0002_phase_141_psp","0003_phase_142_merchant","0004_phase_143_requests","0005_phase_144_callbacks","0006_phase_145_ledger"}<=names

View File

@ -0,0 +1,3 @@
from app.permissions.definitions import ALL_PERMISSIONS
def test_contract_permissions_registered():
for p in ("payment.workspaces.manage","payment.psp_connections.test","payment.merchant_accounts.activate","payment.transactions.view","payment.ledger.view"): assert p in ALL_PERMISSIONS

View File

@ -0,0 +1,8 @@
import pytest
from app.tests.conftest import headers
@pytest.mark.asyncio
async def test_psp_connection_and_health(client):
assert (await client.get("/api/v1/psp-connections",headers=headers())).status_code==403
await client.post("/api/v1/tenant-bundles",headers=headers(),json={"bundle_code":"payment.byo_psp.basic","is_active":True})
p=(await client.post("/api/v1/psp-connections",headers=headers(),json={"provider_code":"mock","name":"Mock"})).json()
assert (await client.post(f"/api/v1/psp-connections/{p['id']}/test",headers=headers())).json()["status"]=="healthy"

View File

@ -0,0 +1,9 @@
import pytest
from app.tests.conftest import headers
@pytest.mark.asyncio
async def test_merchant_lifecycle(client):
await client.post("/api/v1/tenant-bundles",headers=headers(),json={"bundle_code":"payment.torbat_pay.merchant","is_active":True})
m=(await client.post("/api/v1/merchant-accounts",headers=headers(),json={"name":"Merchant"})).json()
m=(await client.post(f"/api/v1/merchant-accounts/{m['id']}/submit",headers=headers())).json(); assert m["status"]=="pending_review"
m=(await client.post(f"/api/v1/merchant-accounts/{m['id']}/activate",headers=headers())).json(); assert m["status"]=="active"
m=(await client.post(f"/api/v1/merchant-accounts/{m['id']}/suspend",headers=headers())).json(); assert m["status"]=="suspended"

View File

@ -0,0 +1,11 @@
import pytest
from app.tests.conftest import headers
@pytest.mark.asyncio
async def test_request_idempotency_and_cancel(client):
await client.post("/api/v1/tenant-bundles",headers=headers(),json={"bundle_code":"payment.byo_psp.basic","is_active":True})
assert (await client.post("/api/v1/payment-requests",headers=headers(),json={"amount_minor":10})).status_code==400
h={**headers(),"Idempotency-Key":"same"}
a=(await client.post("/api/v1/payment-requests",headers=h,json={"amount_minor":10})).json()
b=(await client.post("/api/v1/payment-requests",headers=h,json={"amount_minor":10})).json()
assert a["id"]==b["id"] and a["status"]=="redirect_issued"
assert (await client.post(f"/api/v1/payment-requests/{a['id']}/cancel",headers=headers())).json()["status"]=="cancelled"

View File

@ -0,0 +1,11 @@
import pytest
from app.tests.conftest import headers
@pytest.mark.asyncio
async def test_callback_replay(client):
await client.post("/api/v1/tenant-bundles",headers=headers(),json={"bundle_code":"payment.byo_psp.basic","is_active":True})
connection=(await client.post("/api/v1/psp-connections",headers=headers(),json={"provider_code":"mock"})).json()
req=(await client.post("/api/v1/payment-requests",headers={**headers(),"Idempotency-Key":"cb"},json={"amount_minor":25})).json()
body={"payment_request_id":req["id"],"provider_transaction_id":"trx-1","normalized_status":"paid","signature":"mock-valid"}
path=f"/api/v1/callbacks/mock/{connection['id']}"
assert (await client.post(path,headers=headers(),json=body)).json()["status"]=="processed"
assert (await client.post(path,headers=headers(),json=body)).json()["status"]=="duplicate"

View File

@ -0,0 +1,10 @@
import pytest
from app.tests.conftest import headers
@pytest.mark.asyncio
async def test_verified_payment_creates_ledger_once(client):
await client.post("/api/v1/tenant-bundles",headers=headers(),json={"bundle_code":"payment.byo_psp.basic","is_active":True})
req=(await client.post("/api/v1/payment-requests",headers={**headers(),"Idempotency-Key":"ledger"},json={"amount_minor":30,"source":{"service":"crm","ref_id":"abc"}})).json()
await client.post(f"/api/v1/payment-requests/{req['id']}/verify",headers=headers(),json={"status":"paid","provider_transaction_id":"t"})
await client.post(f"/api/v1/payment-requests/{req['id']}/verify",headers=headers(),json={"status":"paid","provider_transaction_id":"t"})
assert len((await client.get("/api/v1/payment-transactions",headers=headers())).json())==1
assert len((await client.get("/api/v1/payment-ledger-entries",headers=headers())).json())==1

View File

@ -0,0 +1,10 @@
import pytest
from app.tests.conftest import headers
@pytest.mark.asyncio
async def test_tenant_required(client):
assert (await client.get("/api/v1/payment-workspaces")).status_code in (400,422)
@pytest.mark.asyncio
async def test_psp_secrets_redacted(client):
await client.post("/api/v1/tenant-bundles",headers=headers(),json={"bundle_code":"payment.byo_psp.basic","is_active":True})
r=await client.post("/api/v1/psp-connections",headers=headers(),json={"provider_code":"mock","credential_vault_ref":"vault://secret","secret_config":{"password":"x"}})
assert r.json()["credential_vault_ref"]=="***REDACTED***" and "secret_config" not in r.json()

View File

@ -0,0 +1,7 @@
import pytest
from app.tests.conftest import TENANT_A,TENANT_B,headers
@pytest.mark.asyncio
async def test_tenants_cannot_read_each_other(client):
row=(await client.post("/api/v1/payment-workspaces",headers=headers(TENANT_A),json={"name":"A"})).json()
assert (await client.get(f"/api/v1/payment-workspaces/{row['id']}",headers=headers(TENANT_B))).status_code==404
assert (await client.get("/api/v1/payment-workspaces",headers=headers(TENANT_B))).json()==[]

View File

@ -0,0 +1,2 @@
[pytest]
asyncio_mode = auto

View File

@ -0,0 +1,14 @@
fastapi==0.111.0
uvicorn[standard]==0.30.1
pydantic[email]==2.7.4
pydantic-settings==2.3.4
sqlalchemy==2.0.31
alembic==1.13.2
asyncpg==0.29.0
psycopg[binary]>=3.2.2
httpx==0.27.0
pyjwt[crypto]==2.8.0
-e ../../shared-lib
pytest==8.2.2
pytest-asyncio==0.23.7
aiosqlite==0.20.0

View File

@ -0,0 +1,12 @@
import os
from urllib.parse import urlparse
def main():
url=os.getenv("PAYMENT_DATABASE_URL_SYNC","")
if not url: return
import psycopg
p=urlparse(url.replace("+psycopg","")); db=(p.path or "/payment_db").lstrip("/")
with psycopg.connect(host=p.hostname or "localhost",port=p.port or 5432,user=p.username,password=p.password,dbname="postgres",autocommit=True) as conn:
with conn.cursor() as cur:
cur.execute("SELECT 1 FROM pg_database WHERE datname=%s",(db,))
if not cur.fetchone(): cur.execute(f'CREATE DATABASE "{db}"')
if __name__=="__main__": main()

View File

@ -417,6 +417,36 @@ services:
networks: networks:
- superapp_net - superapp_net
payment-service:
build:
context: .
dockerfile: backend/services/payment/Dockerfile.dev
container_name: superapp_payment_service
restart: unless-stopped
env_file:
- .env
environment:
PAYMENT_DATABASE_URL: ${PAYMENT_DATABASE_URL:-postgresql+asyncpg://superapp:superapp_password@postgres:5432/payment_db}
PAYMENT_DATABASE_URL_SYNC: ${PAYMENT_DATABASE_URL_SYNC:-postgresql+psycopg://superapp:superapp_password@postgres:5432/payment_db}
CORE_SERVICE_URL: ${CORE_SERVICE_URL:-http://core-service:8000}
AUTH_REQUIRED: ${AUTH_REQUIRED:-true}
ports:
- "8012:8012"
volumes:
- ./backend/services/payment:/app
- ./backend/shared-lib:/shared-lib
depends_on:
postgres:
condition: service_healthy
core-service:
condition: service_started
command: >
sh -c "python scripts/ensure_db.py &&
alembic upgrade head &&
uvicorn app.main:app --host 0.0.0.0 --port 8012 --reload"
networks:
- superapp_net
hospitality-service: hospitality-service:
build: build:
context: . context: .
@ -499,6 +529,7 @@ services:
NEXT_PUBLIC_BACKEND_URL: ${NEXT_PUBLIC_BACKEND_URL:-http://localhost:8000} NEXT_PUBLIC_BACKEND_URL: ${NEXT_PUBLIC_BACKEND_URL:-http://localhost:8000}
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-http://localhost:8000} NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-http://localhost:8000}
NEXT_PUBLIC_IDENTITY_API_URL: ${NEXT_PUBLIC_IDENTITY_API_URL:-http://localhost:8001} NEXT_PUBLIC_IDENTITY_API_URL: ${NEXT_PUBLIC_IDENTITY_API_URL:-http://localhost:8001}
IDENTITY_SERVICE_URL: ${IDENTITY_SERVICE_URL:-http://identity-access-service:8001}
NEXT_PUBLIC_KEYCLOAK_URL: ${NEXT_PUBLIC_KEYCLOAK_URL:-http://localhost:8080} NEXT_PUBLIC_KEYCLOAK_URL: ${NEXT_PUBLIC_KEYCLOAK_URL:-http://localhost:8080}
NEXT_PUBLIC_KEYCLOAK_REALM: ${NEXT_PUBLIC_KEYCLOAK_REALM:-superapp} NEXT_PUBLIC_KEYCLOAK_REALM: ${NEXT_PUBLIC_KEYCLOAK_REALM:-superapp}
NEXT_PUBLIC_KEYCLOAK_CLIENT_ID: ${NEXT_PUBLIC_KEYCLOAK_CLIENT_ID:-superapp-frontend} NEXT_PUBLIC_KEYCLOAK_CLIENT_ID: ${NEXT_PUBLIC_KEYCLOAK_CLIENT_ID:-superapp-frontend}
@ -515,6 +546,8 @@ services:
SPORTS_CENTER_SERVICE_URL: ${SPORTS_CENTER_SERVICE_URL:-http://sports-center-service:8006} SPORTS_CENTER_SERVICE_URL: ${SPORTS_CENTER_SERVICE_URL:-http://sports-center-service:8006}
NEXT_PUBLIC_DELIVERY_API_URL: ${NEXT_PUBLIC_DELIVERY_API_URL:-http://localhost:8007} NEXT_PUBLIC_DELIVERY_API_URL: ${NEXT_PUBLIC_DELIVERY_API_URL:-http://localhost:8007}
DELIVERY_SERVICE_URL: ${DELIVERY_SERVICE_URL:-http://delivery-service:8007} DELIVERY_SERVICE_URL: ${DELIVERY_SERVICE_URL:-http://delivery-service:8007}
NEXT_PUBLIC_EXPERIENCE_API_URL: ${NEXT_PUBLIC_EXPERIENCE_API_URL:-http://localhost:8008}
EXPERIENCE_SERVICE_URL: ${EXPERIENCE_SERVICE_URL:-http://experience-service:8008}
NEXT_PUBLIC_HEALTHCARE_API_URL: ${NEXT_PUBLIC_HEALTHCARE_API_URL:-http://localhost:8010} NEXT_PUBLIC_HEALTHCARE_API_URL: ${NEXT_PUBLIC_HEALTHCARE_API_URL:-http://localhost:8010}
HEALTHCARE_SERVICE_URL: ${HEALTHCARE_SERVICE_URL:-http://healthcare-service:8010} HEALTHCARE_SERVICE_URL: ${HEALTHCARE_SERVICE_URL:-http://healthcare-service:8010}
NEXT_PUBLIC_BEAUTY_BUSINESS_API_URL: ${NEXT_PUBLIC_BEAUTY_BUSINESS_API_URL:-http://localhost:8011} NEXT_PUBLIC_BEAUTY_BUSINESS_API_URL: ${NEXT_PUBLIC_BEAUTY_BUSINESS_API_URL:-http://localhost:8011}

View File

@ -5,7 +5,9 @@
# Agents must keep this file aligned with docs/progress.md and docs/roadmap.md. # Agents must keep this file aligned with docs/progress.md and docs/roadmap.md.
schema_version: 1 schema_version: 1
updated: "2026-07-26" updated: "2026-07-27"
# Payment Pay-Reg complete: Enterprise Payment Platform registration (ADR-020).
# Payment payment-arch complete: licensing, contracts, compatibility (ADR-021).
# AF-Discovery complete: Enterprise Phase Discovery mandate (ADR-019). # AF-Discovery complete: Enterprise Phase Discovery mandate (ADR-019).
# AF-Enterprise complete: Enterprise Development Framework upgrade (ADR-018). # AF-Enterprise complete: Enterprise Development Framework upgrade (ADR-018).
# Hospitality Phase 12.3 complete; 12.4 POS Lite next for Hospitality track. # Hospitality Phase 12.3 complete; 12.4 POS Lite next for Hospitality track.
@ -1238,6 +1240,160 @@ phases:
- Module registry version/phase updated; progress marks 11.10 complete - Module registry version/phase updated; progress marks 11.10 complete
- No undocumented public API/event/permission - No undocumented public API/event/permission
- id: platform-published-resource-arch
name: Platform Published Resource Architecture
area: Platform
description: Docs-only architecture patch — Published Resource abstraction, Publish Target Registry contract, standalone Form/Survey/Appointment publish rules, reserved public URL prefixes, cross-service publish_id integration contracts (Payment, Communication, Short Link, QR, Analytics, verticals, future products). ADR-022. No backend, frontend, migration, or API implementation.
status: complete
dependencies:
- experience-11.10
- ai-framework
required_previous_phase: experience-11.10
required_documents:
- docs/architecture/published-resource-architecture.md
- docs/architecture/adr/ADR-022.md
- docs/reference/published-resource-contracts.md
- docs/experience-roadmap.md
- docs/phase-handover/phase-af-published-resource-arch.md
- docs/phase-handover/phase-experience-published-resource-arch.md
required_services:
- experience
completion_criteria:
- Architecture doc + ADR-022 accepted and indexed
- Contracts + Experience/framework handovers published
- Module boundaries, integration architecture, manifests, snapshot updated
- No business code, migrations, or UI changes
- Completed Experience phases 11.011.10 remain unmodified
- id: platform-experience-arch-freeze
name: Experience Architecture Freeze (Actions, Access, Embed)
area: Platform
description: Final docs-only architecture freeze — Published Actions (open Action Registry), Public Access Model (open strategies; Experience never owns auth), Universal Embed Architecture (open embed types + security/sizing/theme/analytics contracts). Amends ADR-022 additively. Marks Experience Platform ARCHITECTURE COMPLETE. No backend, frontend, migration, or API implementation.
status: complete
dependencies:
- platform-published-resource-arch
required_previous_phase: platform-published-resource-arch
required_documents:
- docs/architecture/published-resource-architecture.md
- docs/architecture/adr/ADR-022.md
- docs/reference/published-action-registry.md
- docs/reference/public-access-contract.md
- docs/reference/embed-contract.md
- docs/reference/published-resource-contracts.md
- docs/phase-handover/phase-af-experience-arch-freeze.md
- docs/phase-handover/phase-experience-arch-freeze.md
required_services:
- experience
completion_criteria:
- Action / Access / Embed contracts published
- ADR-022 amended additively; architecture marked complete
- Snapshot, manifests, roadmap, handovers updated
- Fully backward compatible; no business code or migrations
- Completed Experience phases 11.011.10 remain unmodified
# --- Experience Frontend (Torbat Pages) — FE-11.011.5 STOP ---
- id: experience-fe-11.0
name: Experience Frontend Framework
area: docs/frontend
description: Hub, PortalShell, AuthGuard, workspace switcher, permissions/capabilities hooks, admin CRUD lists, BFF to :8008, design tokens.
status: complete
dependencies:
- platform-experience-arch-freeze
required_documents:
- docs/experience-frontend-roadmap.md
- frontend/docs/experience-frontend-architecture.md
required_services:
- experience
completion_criteria:
- frontend/modules/experience scaffolded mirroring Delivery
- docker-compose EXPERIENCE_* env vars
- apps catalog experience available
- id: experience-fe-11.1
name: Experience Executive Dashboard
area: docs/frontend
status: complete
dependencies:
- experience-fe-11.0
required_documents:
- docs/experience-frontend-roadmap.md
completion_criteria:
- Dashboard with API counts, recharts, recent resources, audit slice
- id: experience-fe-11.2
name: Experience Workspace & Content UI
area: docs/frontend
status: complete
dependencies:
- experience-fe-11.1
completion_criteria:
- CRUD workspaces/sites/pages/domains/templates; publish-targets provisional publish_id
- id: experience-fe-11.3
name: Experience Visual Builder UI
area: docs/frontend
status: complete
dependencies:
- experience-fe-11.2
completion_criteria:
- Page builder with placements; component/theme/layout CRUD; builder not blocked by page_builder flag
- id: experience-fe-11.4
name: Experience Forms & Surveys UI
area: docs/frontend
status: complete
dependencies:
- experience-fe-11.3
completion_criteria:
- Form/survey CRUD and builders; no appointment booking UI
- id: experience-fe-11.5
name: Experience Publishing & Public UI
area: docs/frontend
status: complete
dependencies:
- experience-fe-11.4
required_documents:
- docs/phase-handover/phase-experience-fe-11-5.md
- docs/service-snapshots/experience-frontend.yaml
- frontend/docs/experience-validation-report.md
- docs/experience-frontend-progress.md
completion_criteria:
- Publishing/SEO/PWA CRUD, public preview, analytics refresh, embed snippet, published-actions catalog
- Design system playground with light/dark, brand, RTL/LTR, charts/maps/tables/timeline/kanban
- STOP — FE-11.6+ not started
# --- AI Framework: Project Status Registry ---
- id: platform-project-status
name: Permanent Project Status Registry
area: docs/ai-framework
description: >
Creates docs/project-status.yaml + docs/project-status.md as the authoritative
execution dashboard. First document every phase reads. Updated after every Complete
phase. Eliminates full-project scans when present. Framework policies updated.
status: complete
dependencies:
- ai-framework
- ai-framework-enterprise
required_documents:
- docs/project-status.yaml
- docs/project-status.md
- docs/phase-handover/phase-af-project-status.md
- docs/ai-framework/runtime-read-policy.md
- docs/ai-framework/master-prompt.md
- docs/ai-framework/development-loop.md
- docs/ai-framework/service-snapshot-policy.md
- docs/ai-framework/context-cache-policy.md
- docs/ai-framework/project-index.yaml
- docs/ai-framework/prompt-rules.md
- docs/ai-framework/quality-gates.md
completion_criteria:
- project-status.yaml contains required global and per-service fields
- Framework policies require reading project-status first
- Project Status updated automatically after every completed phase
- No full project scan required when file exists
- Framework handover generated; STOP
# --- Hospitality (Phase 12.0+) — Torbat Food --- # --- Hospitality (Phase 12.0+) — Torbat Food ---
- id: hospitality-12.0 - id: hospitality-12.0
name: Hospitality Platform Foundation name: Hospitality Platform Foundation
@ -1451,6 +1607,283 @@ phases:
completion_criteria: completion_criteria:
- Full quality gate suite green across all Hospitality phases - Full quality gate suite green across all Hospitality phases
# --- Payment Platform (Phase 14.014.10) — Torbat Pay ---
# Note: manifest IDs payment-14.x are distinct from beauty-business-14.x (Torbat Beauty).
- id: payment-reg
name: Payment Platform Registration
area: Payment
description: Documentation-only registration of independent payment service, payment_db, phases payment-14.0payment-14.10, ADR-020, module/glossary/boundary updates. BYO-PSP and Torbat Pay Merchant modes. No business code.
status: complete
dependencies:
- onboarding-4
- ai-framework
required_previous_phase: ai-framework
required_documents:
- docs/payment-roadmap.md
- docs/phases/Payment/README.md
- docs/architecture/adr/ADR-020.md
- docs/payment-phase-pay-reg.md
- docs/phase-handover/phase-pay-reg.md
- docs/ai-framework/service-template.md
- docs/ai-framework/phase-template.md
required_services:
- payment
completion_criteria:
- Service registered in service-manifest with payment_db and payment.* permissions
- Phases payment-14.0payment-14.10 registered in phase-manifest
- Module registry, project index, ADR-020 updated
- Service snapshot and handover complete
- Progress and next-steps updated
- No business code, models, APIs, or migrations in this phase
- Quality gates for documentation/architecture/manifest validation passed
- id: payment-arch
name: Payment Architecture Prerequisites
area: Payment
description: Docs-only — finalize licensing (L1/L2/L3), bundle isolation, provider assignment, versioned integration contracts (PaymentIntent, Checkout, Callback, Settlement, future Refund/Split/Subscription/Wallet/Installment), service compatibility matrix, permission tree, event catalog. ADR-021. No business code; no existing service modifications.
status: complete
dependencies:
- payment-reg
required_previous_phase: payment-reg
required_documents:
- docs/payment-roadmap.md
- docs/reference/payment-contracts.md
- docs/architecture/adr/ADR-021.md
- docs/architecture/module-boundaries.md
- docs/reference/event-catalog.md
- docs/reference/services-contracts.md
- docs/phase-handover/phase-pay-arch.md
required_services:
- payment
completion_criteria:
- ADR-021 accepted; payment-contracts.md published with all v1 contract schemas
- Compatibility verified for Core, Identity, Tenant, Accounting, CRM, Communication, Hospitality, Delivery, Sports Center, Marketplace
- Module boundaries, event catalog, services-contracts, authorization updated
- Roadmap and phase 14.0 doc updated with bundle/toggle/assignment prerequisites
- Service snapshot regenerated; handover complete
- No business code; no modifications to existing services
- id: payment-14.0
name: Payment Foundation
area: Payment
description: Independent payment service scaffold, payment_db, health/capabilities/metrics, permissions payment.*, publish-only event shells, tenant isolation, audit/config shells, PSP adapter protocol stubs only.
implementation_priority: HIGH
status: complete
dependencies:
- payment-reg
- payment-arch
required_previous_phase: payment-arch
required_documents:
- docs/payment-roadmap.md
- docs/reference/payment-contracts.md
- docs/phases/Payment/README.md
- docs/phases/Payment/phase-14-0-payment-foundation.md
- docs/architecture/adr/ADR-020.md
- docs/architecture/adr/ADR-021.md
- docs/phase-handover/phase-pay-arch.md
- docs/ai-framework/service-template.md
required_services:
- payment
completion_criteria:
- Service scaffold under backend/services/payment with API → Service → Repository → Model layering
- Alembic initial migration for foundation tables only as scoped
- Health, capabilities, metrics endpoints; permission tree payment.* documented
- Tenant isolation + architecture + docs tests green
- No PSP connections, payment requests, callbacks, or ledger engines
- id: payment-14.1
name: PSP Management
area: Payment
description: BYO-PSP connections (ZarinPal, IDPay, NextPay, Pay.ir, Mellat, Parsian, Saman, …), credential vault refs, routing priority, health checks, adapter registry.
implementation_priority: HIGH
status: complete
dependencies:
- payment-14.0
required_previous_phase: payment-14.0
required_documents:
- docs/payment-roadmap.md
- docs/phases/Payment/phase-14-1-psp-management.md
- docs/architecture/adr/ADR-020.md
required_services:
- payment
completion_criteria:
- PSP connection APIs + test/suspend + routing policy + adapter stubs green
- Secret redaction and tenant isolation tests green
- No payment capture yet
- id: payment-14.2
name: Merchant Accounts
area: Payment
description: Torbat Pay Merchant (facilitator) sub-merchant account shells, settlement profiles, compliance refs, lifecycle.
implementation_priority: HIGH
status: complete
dependencies:
- payment-14.1
required_previous_phase: payment-14.1
required_documents:
- docs/payment-roadmap.md
- docs/phases/Payment/phase-14-2-merchant-accounts.md
required_services:
- payment
completion_criteria:
- Merchant account CRUD + lifecycle APIs + tests green
- Facilitator mode flag on workspace documented
- No payment requests yet
- id: payment-14.3
name: Payment Requests
area: Payment
description: Idempotent payment request initiation for vertical refs; redirect/token payloads via PSP adapters.
implementation_priority: HIGH
status: complete
dependencies:
- payment-14.2
required_previous_phase: payment-14.2
required_documents:
- docs/payment-roadmap.md
- docs/phases/Payment/phase-14-3-payment-requests.md
required_services:
- payment
completion_criteria:
- Payment request APIs + idempotency + state machine + mock initiate green
- No callback verification or ledger finality yet
- id: payment-14.4
name: Callback & Verification
area: Payment
description: PSP callback ingestion, signature verification, verify API reconcile, request paid/failed transitions.
implementation_priority: HIGH
status: complete
dependencies:
- payment-14.3
required_previous_phase: payment-14.3
required_documents:
- docs/payment-roadmap.md
- docs/phases/Payment/phase-14-4-callback-verification.md
required_services:
- payment
completion_criteria:
- Callback routes + verify retry + replay protection tests green
- payment.request.paid events published
- id: payment-14.5
name: Transaction Ledger
area: Payment
description: Immutable append-only payment transaction ledger linked to verified requests — distinct from Accounting journals.
implementation_priority: HIGH
status: complete
dependencies:
- payment-14.4
required_previous_phase: payment-14.4
required_documents:
- docs/payment-roadmap.md
- docs/phases/Payment/phase-14-5-transaction-ledger.md
- docs/architecture/adr/ADR-010.md
required_services:
- payment
completion_criteria:
- Ledger immutability + transaction query APIs + tests green
- HIGH priority MVP path 14.014.5 complete
- No Accounting journal creation inside Payment
- id: payment-14.6
name: Refunds & Reversals
area: Payment
description: Full/partial refunds via PSP adapters; ledger reversal entries; Communication notify hooks.
implementation_priority: LATER
status: planned
dependencies:
- payment-14.5
- communication-8
required_previous_phase: payment-14.5
required_documents:
- docs/payment-roadmap.md
- docs/phases/Payment/phase-14-6-refunds-reversals.md
- docs/architecture/adr/ADR-012.md
required_services:
- payment
- communication
completion_criteria:
- Refund APIs + reversal ledger + idempotency tests green
- id: payment-14.7
name: Split Payments & Facilitator Settlement
area: Payment
description: Multi-party split allocations; settlement batch shells; marketplace/facilitator ready.
implementation_priority: LATER
status: planned
dependencies:
- payment-14.5
- payment-14.2
required_previous_phase: payment-14.5
required_documents:
- docs/payment-roadmap.md
- docs/phases/Payment/phase-14-7-split-settlement.md
required_services:
- payment
completion_criteria:
- Split rules + settlement batch intents + allocation math tests green
- id: payment-14.8
name: Vertical Connectors & Checkout Contracts
area: Payment
description: Versioned connector contracts for Hospitality, Marketplace, Sports, Experience, CRM, Healthcare, Beauty, Delivery, Automation, future services.
implementation_priority: LATER
status: planned
dependencies:
- payment-14.5
required_previous_phase: payment-14.5
required_documents:
- docs/payment-roadmap.md
- docs/phases/Payment/phase-14-8-vertical-connectors.md
required_services:
- payment
completion_criteria:
- Checkout session + connector registration APIs + mock vertical connectors documented/tested
- id: payment-14.9
name: Reconciliation & Accounting Integration
area: Payment
description: PSP settlement reconciliation; Accounting posting intents via API/events only — no journal ownership.
implementation_priority: LATER
status: planned
dependencies:
- payment-14.5
- accounting-5.11
required_previous_phase: payment-14.5
required_documents:
- docs/payment-roadmap.md
- docs/phases/Payment/phase-14-9-reconciliation.md
- docs/architecture/adr/ADR-010.md
required_services:
- payment
- accounting
completion_criteria:
- Reconciliation run + accounting intent events; no cross-DB access; no local journals
- id: payment-14.10
name: Analytics, Fraud Shells & Enterprise Validation
area: Payment
description: Payment analytics snapshots, optional fraud rule hooks, full AI Framework validation — architecture, security, performance, docs, integration, tenant isolation, self-heal until green.
implementation_priority: LATER
status: planned
dependencies:
- payment-14.9
- ai-framework
required_previous_phase: payment-14.9
required_documents:
- docs/payment-roadmap.md
- docs/phases/Payment/phase-14-10-enterprise-validation.md
- docs/ai-framework/quality-gates.md
- docs/architecture/ai-architecture.md
required_services:
- payment
completion_criteria:
- All Payment quality gates passed
- Core flows work when fraud hooks off
- Module registry version/phase updated; progress marks payment-14.10 complete
- No undocumented public API/event/permission
# Historical alias — superseded by hospitality-12.0 # Historical alias — superseded by hospitality-12.0
- id: restaurant-foundation - id: restaurant-foundation
name: Restaurant / Cafe Foundation (superseded) name: Restaurant / Cafe Foundation (superseded)

View File

@ -1,12 +1,15 @@
# Project Index — single runtime discovery entry point for TorbatYar AI Framework. # Project Index — path resolver for TorbatYar AI Framework (active service only).
# FIRST read docs/project-status.yaml for platform execution state; then this index for paths.
# Normative rules: docs/ai-framework/master-prompt.md, runtime-read-policy.md # Normative rules: docs/ai-framework/master-prompt.md, runtime-read-policy.md
# NEVER scan repository folders to discover services, handovers, roadmaps, snapshots, ADRs, or architecture docs. # NEVER scan repository folders to discover services, handovers, roadmaps, snapshots, ADRs, or architecture docs.
# Resolve all paths through this index. Update automatically on service registration and phase completion. # Resolve all paths through project-status + this index. Update automatically on service registration and phase completion.
schema_version: 1 schema_version: 1
updated: "2026-07-26" updated: "2026-07-27"
execution_order: execution_order:
- docs/project-status.yaml
- docs/project-status.md
- docs/ai-framework/project-index.yaml - docs/ai-framework/project-index.yaml
- docs/ai-framework/runtime-read-policy.md - docs/ai-framework/runtime-read-policy.md
- docs/ai-framework/context-cache-policy.md - docs/ai-framework/context-cache-policy.md
@ -17,6 +20,8 @@ execution_order:
- additional_documents_per_runtime_read_policy - additional_documents_per_runtime_read_policy
shared_references: shared_references:
project_status: docs/project-status.yaml
project_status_human: docs/project-status.md
phase_manifest: docs/ai-framework/phase-manifest.yaml phase_manifest: docs/ai-framework/phase-manifest.yaml
service_manifest: docs/ai-framework/service-manifest.yaml service_manifest: docs/ai-framework/service-manifest.yaml
module_registry: docs/module-registry.md module_registry: docs/module-registry.md
@ -208,7 +213,7 @@ services:
next_phase: null next_phase: null
snapshot_file: docs/service-snapshots/experience.yaml snapshot_file: docs/service-snapshots/experience.yaml
roadmap_file: docs/experience-roadmap.md roadmap_file: docs/experience-roadmap.md
latest_handover: docs/phase-handover/phase-11-10.md latest_handover: docs/phase-handover/phase-experience-fe-11-5.md
phase_manifest_reference: docs/ai-framework/phase-manifest.yaml phase_manifest_reference: docs/ai-framework/phase-manifest.yaml
service_manifest_reference: docs/ai-framework/service-manifest.yaml service_manifest_reference: docs/ai-framework/service-manifest.yaml
module_registry_reference: docs/module-registry.md module_registry_reference: docs/module-registry.md
@ -220,6 +225,7 @@ services:
capability_prefix: experience.* capability_prefix: experience.*
health_endpoint: /health health_endpoint: /health
metrics_endpoint: /metrics metrics_endpoint: /metrics
notes: Backend 11.011.10 complete. Frontend FE-11.011.5 complete (stop). FE-11.6+ not started. Architecture COMPLETE (ADR-022).
- service_name: Hospitality Platform - service_name: Hospitality Platform
commercial_product: Torbat Food commercial_product: Torbat Food
@ -595,6 +601,28 @@ services:
health_endpoint: /health health_endpoint: /health
metrics_endpoint: null metrics_endpoint: null
- service_name: Enterprise Payment Platform
commercial_product: Torbat Pay
service_identifier: payment
current_status: active
current_version: "0.14.5.0"
current_phase: payment-14.5
next_phase: payment-14.6
snapshot_file: docs/service-snapshots/payment.yaml
roadmap_file: docs/payment-roadmap.md
latest_handover: docs/phase-handover/phase-14-5.md
phase_manifest_reference: docs/ai-framework/phase-manifest.yaml
service_manifest_reference: docs/ai-framework/service-manifest.yaml
module_registry_reference: docs/module-registry.md
permission_prefix: payment.*
database_name: payment_db
api_port: 8012
public_api_prefix: /api/v1
event_prefix: payment.*
capability_prefix: payment.*
health_endpoint: /health
metrics_endpoint: /metrics
- service_name: Frontend - service_name: Frontend
commercial_product: TorbatYar UI commercial_product: TorbatYar UI
service_identifier: frontend service_identifier: frontend
@ -622,3 +650,8 @@ framework_workstreams:
description: AI Development Framework (docs-only) description: AI Development Framework (docs-only)
project_index_handover: docs/phase-handover/phase-af-project-index.md project_index_handover: docs/phase-handover/phase-af-project-index.md
snapshot_file: null snapshot_file: null
- workstream_id: platform-project-status
description: Permanent Project Status Registry (execution dashboard)
latest_handover: docs/phase-handover/phase-af-project-status.md
project_status: docs/project-status.yaml
snapshot_file: null

View File

@ -4,7 +4,7 @@
# Keep aligned with docs/module-registry.md and runtime compose ports when wired. # Keep aligned with docs/module-registry.md and runtime compose ports when wired.
schema_version: 1 schema_version: 1
updated: "2026-07-25" updated: "2026-07-27"
services: services:
- id: core-platform - id: core-platform
@ -518,6 +518,16 @@ services:
- docs/experience-phase-11-10-audit.md - docs/experience-phase-11-10-audit.md
- docs/service-snapshots/experience.yaml - docs/service-snapshots/experience.yaml
- docs/architecture/adr/ADR-016.md - docs/architecture/adr/ADR-016.md
- docs/architecture/adr/ADR-022.md
- docs/architecture/published-resource-architecture.md
- docs/reference/published-resource-contracts.md
- docs/reference/published-action-registry.md
- docs/reference/public-access-contract.md
- docs/reference/embed-contract.md
- docs/phase-handover/phase-experience-published-resource-arch.md
- docs/phase-handover/phase-experience-arch-freeze.md
- docs/phase-handover/phase-af-published-resource-arch.md
- docs/phase-handover/phase-af-experience-arch-freeze.md
- docs/module-registry.md - docs/module-registry.md
current_version: 0.11.10.0 current_version: 0.11.10.0
current_phase: experience-11.10 current_phase: experience-11.10
@ -525,6 +535,7 @@ services:
status: active status: active
api_port: 8008 api_port: 8008
commercial_product: Torbat Pages commercial_product: Torbat Pages
notes: Architecture COMPLETE (ADR-022 + Actions/Access/Embed). Publish Target Registry / Action / Access / Embed engines not implemented.
future_modules: future_modules:
- sites - sites
- pages - pages
@ -726,6 +737,100 @@ services:
status: superseded status: superseded
notes: Pointer only — runtime is hospitality service (ADR-017). notes: Pointer only — runtime is hospitality service (ADR-017).
- id: payment
name: Enterprise Payment Platform
description: Independent multi-tenant payment platform (Torbat Pay) — BYO-PSP and Torbat Pay Merchant facilitator modes; PSP routing, callbacks, transaction ledger, refunds, splits, reconciliation intents. Consumed by all verticals via API/Events only.
owner: Platform
path: backend/services/payment
database: payment_db
api_prefix: /api/v1
permission_prefix: payment.*
health_endpoint: /health
configuration:
- PAYMENT_DATABASE_URL
- AUTH_REQUIRED
- tenant resolution via X-Tenant-ID / shared middleware
- entitlement feature keys payment.*
dependencies:
- core-platform
- shared-lib
optional_dependencies:
- accounting
- communication
- crm
- loyalty
- hospitality
- marketplace
- sports_center
- delivery
- experience
- healthcare
- beauty_business
- automation
events:
- payment.workspace.*
- payment.psp_provider.*
- payment.psp_connection.*
- payment.psp_routing.*
- payment.merchant_account.*
- payment.request.*
- payment.callback.*
- payment.transaction.*
- payment.ledger.*
- payment.refund.*
- payment.split.*
- payment.settlement_batch.*
- payment.connector.*
- payment.checkout_session.*
- payment.reconciliation.*
- payment.accounting_intent.*
- payment.analytics.*
- payment.fraud_check.*
public_apis:
- /health
- /capabilities
- /metrics
internal_apis:
- service-to-service payment request + checkout refs (token-gated; not public)
tenant_aware: true
audit_enabled: true
documentation:
- docs/payment-roadmap.md
- docs/reference/payment-contracts.md
- docs/payment-phase-pay-reg.md
- docs/payment-phase-pay-arch.md
- docs/phases/Payment/README.md
- docs/phase-handover/phase-pay-reg.md
- docs/phase-handover/phase-14-5.md
- docs/service-snapshots/payment.yaml
- docs/architecture/adr/ADR-020.md
- docs/architecture/adr/ADR-021.md
- docs/module-registry.md
current_version: 0.14.5.0
current_phase: payment-14.5
current_status: active
status: active
api_port: 8012
commercial_product: Torbat Pay
future_modules:
- foundation
- psp_registry
- psp_connections
- merchant_accounts
- payment_requests
- callbacks
- transactions
- ledger
- refunds
- splits
- settlement_batches
- connectors
- checkout_sessions
- reconciliation
- accounting_intents
- analytics
- fraud_hooks
- id: ecommerce - id: ecommerce
name: Ecommerce name: Ecommerce
owner: TBD owner: TBD

View File

@ -0,0 +1,77 @@
# ADR-020: Independent Enterprise Payment Platform Service (Torbat Pay)
| Field | Value |
| --- | --- |
| Status | Accepted |
| Date | 2026-07-27 |
| Deciders | Platform Architecture |
| Supersedes | — |
| Superseded by | — |
## Context
Every TorbatYar vertical — Accounting, CRM, Hospitality, Sports Center, Marketplace, Delivery, Experience, Healthcare, Beauty Business, Automation, and future services — needs payment capture without embedding PSP credentials, callback handling, or transaction ledgers inside each service.
Tenants require two commercial modes:
1. **Bring Your Own PSP (BYO-PSP)** — connect tenant-owned gateways (ZarinPal, IDPay, NextPay, Pay.ir, Mellat, Parsian, Saman, …).
2. **Torbat Pay Merchant** — Torbat Pay acts as payment facilitator; tenants onboard as sub-merchants under platform settlement rules.
TorbatYar already isolates shared platforms as independent services (Communication [ADR-012](ADR-012.md), Loyalty [ADR-011](ADR-011.md), Delivery [ADR-015](ADR-015.md)). Payment capture, PSP routing, verification, and payment-domain ledger must follow the same pattern. Accounting owns financial journals via Posting Engine [ADR-010](ADR-010.md); Payment must never create journal entries locally.
International PSP expansion is **not** in current scope but the provider adapter architecture must allow future regions without vertical changes.
Commercial product name: **Torbat Pay**.
## Decision
1. Introduce `payment` as an **independent Enterprise Payment Platform service** with sole ownership of `payment_db` ([ADR-001](ADR-001.md)).
2. Permission prefix: `payment.*`. Publish-only domain events: `payment.*`.
3. Row-level multi-tenancy via `tenant_id` ([ADR-003](ADR-003.md)).
4. PSP implementations sit behind **provider protocols/adapters** owned by Payment; verticals never call PSP APIs directly.
5. Consume Accounting, CRM, Loyalty, Communication, Core, and all vertical services **only** via REST API and Events — never shared tables or foreign model imports.
6. Financial journals only through Accounting Posting Engine ([ADR-010](ADR-010.md)). Payment emits settlement/reconciliation **intents**; Accounting posts externally.
7. Customer payment notifications (receipt SMS/email) only through Communication ([ADR-012](ADR-012.md)).
8. Vertical services (Hospitality POS, Marketplace checkout, Sports Center membership, Experience widgets, etc.) hold **commercial context refs only** (`order_ref`, `invoice_ref`, `checkout_session_ref`) and delegate capture to Payment APIs.
9. Implementation phases registered as **`payment-14.0``payment-14.10`** in [phase-manifest.yaml](../../ai-framework/phase-manifest.yaml); roadmap in [payment-roadmap.md](../../payment-roadmap.md). Phase numeric **14.x** is the Payment **track** identifier; it is distinct from Beauty Business phases (`beauty-business-14.x`).
10. API port **8012** reserved; service path `backend/services/payment`.
11. UI for checkout, merchant admin, and reconciliation dashboards lives in `frontend/` only ([ADR-002](ADR-002.md)); this service exposes APIs and events.
12. No international PSP implementation in initial HIGH-priority phases; adapter registry and credential schema must be region-aware for future expansion.
## Consequences
### Positive
- One reusable payment platform for all current and future TorbatYar services
- Enforceable PSP credential isolation and callback verification in one place
- BYO-PSP and Torbat Pay Merchant modes without duplicating integration code per vertical
- Clear boundary with Accounting (money movement vs financial posting)
### Negative
- Additional deployable service and database
- Eventual consistency between payment status and vertical order state
- PCI scope management for facilitator mode requires operational discipline (documented, not implemented in registration)
### Neutral
- Registration (this ADR + manifests) precedes business code; `payment-14.0` implements the scaffold
- Loyalty wallet/gift-card money flows remain in Loyalty; Payment handles external PSP money-in only unless explicitly integrated later
## Alternatives Considered
1. Payment subsystem inside Core — rejected (violates business-module boundaries; couples platform billing with tenant commerce payments).
2. Payment tables inside Accounting — rejected (Accounting owns posting semantics, not PSP routing/callbacks).
3. Per-vertical PSP integrations — rejected (duplicates credentials, callbacks, idempotency, and audit across Hospitality/Marketplace/Sports Center).
4. Third-party payment orchestration SaaS only — rejected for primary path (tenant BYO-PSP and facilitator control require owned platform).
## Related Documents
- [Payment Roadmap](../../payment-roadmap.md)
- [Phase Area — Payment](../../phases/Payment/README.md)
- [Module Boundaries](../module-boundaries.md)
- [Module Registry — payment](../../module-registry.md#payment)
- [Payment Contracts Reference](../../reference/payment-contracts.md)
- [Phase Manifest](../../ai-framework/phase-manifest.yaml)
- [Service Manifest](../../ai-framework/service-manifest.yaml)
- [ADR-001](ADR-001.md) · [ADR-002](ADR-002.md) · [ADR-003](ADR-003.md) · [ADR-006](ADR-006.md) · [ADR-010](ADR-010.md) · [ADR-011](ADR-011.md) · [ADR-012](ADR-012.md) · [ADR-021](ADR-021.md)

View File

@ -0,0 +1,213 @@
# ADR-021: Payment Platform Licensing, Routing & Integration Contracts
| Field | Value |
| --- | --- |
| Status | Accepted |
| Date | 2026-07-27 |
| Deciders | Platform Architecture |
| Supersedes | — |
| Superseded by | — |
| Extends | [ADR-020](ADR-020.md) |
## Context
[ADR-020](ADR-020.md) establishes Torbat Pay as an independent service. Before Phase `payment-14.0` implementation, the platform must define **non-breaking** contracts for:
- Tenant enable/disable of Payment without vertical code changes
- Core service entitlements vs Payment bundle licensing vs operational feature toggles
- BYO-PSP vs Torbat Pay Merchant mode selection and provider reassignment
- Versioned API/event contracts for payment intents, checkout, callbacks, settlement, and future refund/split/subscription/wallet/installment flows
- Compatibility with Core, Identity, Tenant, Accounting, CRM, Communication, Hospitality, Delivery, Sports Center, Marketplace, and future consumers
Hospitality ([ADR-017](ADR-017.md)) and Experience ([ADR-016](ADR-016.md)) already use **bundle licensing + feature toggles + Core entitlement coordination**. Payment must follow the same three-layer model to avoid rework.
## Decision
### 1. Three-layer access control (mandatory)
| Layer | Owner | Purpose | Example keys |
| --- | --- | --- | --- |
| **L1 — Core entitlement** | Core Platform | Commercial plan / tenant module enablement | `payment.module.enabled` |
| **L2 — Payment bundle** | Payment (`payment_db`) | Licensed capability packs within Payment | `byo_psp_basic`, `torbat_pay_merchant`, `marketplace_splits` |
| **L3 — Payment feature toggle** | Payment (`payment_db`) | Operational flags within an active bundle | `refunds_enabled`, `installments_enabled` |
Rules:
1. L1 must pass before any Payment API mutates state (except health/capabilities discovery when documented).
2. L2 gates API surfaces, permissions exposure, and `/capabilities` flags — **hidden bundles must not expose routes, menus, or permission leaves** (same rule as ADR-017).
3. L3 gates optional engines without new Core plan features.
4. Payment listens to Core `feature_access.changed` (event) to invalidate entitlement cache; Payment does not compute Core plans locally.
### 2. Tenant payment workspace (enable / disable)
Each tenant has at most one **PaymentWorkspace** in `payment_db`:
| Field | Purpose |
| --- | --- |
| `status` | `disabled` \| `enabled` \| `suspended` |
| `default_payment_mode` | `byo_psp` \| `torbat_pay_merchant` |
| `core_entitlement_ref` | Opaque ref to Core feature/subscription check (no FK) |
- **Enable Payment:** Core L1 `payment.module.enabled` + workspace `enabled` + at least one L2 bundle active.
- **Disable Payment:** workspace `disabled` — reject new payment requests; in-flight callbacks still processed (14.4+).
- **Mode switch:** update `default_payment_mode` + `PaymentProviderAssignment`**no vertical or adapter code change**; routing policy resolves provider at runtime.
### 3. Provider assignment & routing (no code change on switch)
| Aggregate | Role |
| --- | --- |
| `PspProviderRegistration` | Platform catalog of adapter types (ZarinPal, IDPay, …) |
| `CreditProviderRegistration` | Platform catalog of **external credit/financing adapters** (Torbat Credit, future third-party BNPL) — **registration shell only until phased** |
| `PspConnection` | Tenant BYO-PSP credential binding |
| `CreditProviderConnection` | Tenant connection to external credit provider (future; same assignment pattern as PSP) |
| `MerchantAccount` | Torbat Pay Merchant sub-merchant (facilitator mode) |
| `PaymentProviderAssignment` | Active provider binding for a workspace/branch/context |
| `PspRoutingPolicy` | Priority, failover, amount/currency/channel/**payment_source** rules |
Tenants change PSP or credit provider by updating **assignment + routing policy** only. Adapters are selected by `provider_code` from registry — adding Torbat Credit or a new PSP is a new adapter registration, not a vertical change.
**Payment source types (routing dimension, reserved in v1):**
| `payment_source` | Meaning | Provider adapter |
| --- | --- | --- |
| `PSP` | Standard gateway capture (BYO-PSP) | `PspAdapter` |
| `MERCHANT_FACILITATOR` | Torbat Pay Merchant mode | Facilitator adapter |
| `CREDIT_PROVIDER` | External credit/BNPL/financing authorization | `CreditProviderAdapter` (**not implemented**; architecture reserved) |
Payment MUST invoke Torbat Credit through the **same provider adapter pattern** as PSPs (`initiate`, `verify`, `callback` hooks on `CreditProviderAdapter`). Payment stores **opaque refs only** — never credit scores, KYC payloads, or installment calculation logic.
### 4. Merchant ownership
- **MerchantAccount** and facilitator settlement profiles are owned solely by Payment.
- Verticals store `merchant_account_ref` or `payment_workspace_ref` UUIDs only — never PSP merchant IDs from gateways.
- Torbat Pay Merchant mode uses **platform-scoped facilitator credentials** + tenant sub-merchant identifier; BYO mode uses tenant `PspConnection` only.
### 5. Versioned integration contracts (stable v1 before implementation)
All cross-service payment integration uses versioned contracts documented in [payment-contracts.md](../../reference/payment-contracts.md):
| Contract | Version | Phase introduced | Breaking change policy |
| --- | --- | --- | --- |
| PaymentIntentContract | v1 | 14.3 | Additive fields only in v1.x; v2 requires new phase |
| CheckoutSessionContract | v1 | 14.8 | Same |
| CallbackIngressContract | v1 | 14.4 | Same |
| SettlementIntentContract | v1 | 14.9 | Same |
| RefundIntentContract | v1 | 14.6 | Reserved in v1 schema |
| SplitAllocationContract | v1 | 14.7 | Reserved in v1 schema |
| SubscriptionBillingContract | v1 | Future | Schema slot in reference doc |
| WalletTopUpContract | v1 | Future | Loyalty consumer; Payment captures only |
| InstallmentPlanContract | v1 | Future (Payment) | **Payment-side schedule shell only** — see §12 |
| CreditAuthorizationContract | v1 | Future (Payment ↔ Credit) | Schema slot; authorization refs only |
Verticals depend on **contract version + capability flags**, not Payment internal models.
### 12. Torbat Credit — reserved independent service (architecture only)
A future independent service is reserved:
| Field | Value |
| --- | --- |
| Service id | `credit` |
| Commercial product | **Torbat Credit** |
| Database | `credit_db` (reserved) |
| Event namespace | `credit.*` (**reserved for Torbat Credit** — Payment does not publish `credit.*`) |
**Boundary — InstallmentPlan ≠ Credit Engine:**
| Concern | Owner |
| --- | --- |
| Installment **schedule shell** linked to a payment request (due dates, amounts, linkage refs) | Payment (`InstallmentPlanContract` v1) |
| Installment **calculation**, credit **scoring**, **KYC/KYB**, **BNPL** underwriting, financing agreements, collections | **Torbat Credit** (`credit` service) — exclusive |
Payment MAY store opaque architectural reference fields on intents and transactions:
- `credit_provider_id` — registry id of external credit provider (e.g. Torbat Credit adapter)
- `financing_reference` — opaque id returned by credit provider for a financing agreement
- `credit_authorization_reference` — authorization/hold id from credit provider
- `installment_contract_reference` — link to Torbat Credit contract; **not** Payment-owned installment math
**Integration pattern:** Torbat Credit is consumed by Payment as an **external Credit Provider** — same adapter registry, routing policy, and assignment mechanics as PSPs. No shared database. No credit domain logic inside Payment.
**Implementation status:** Architecture references only after `payment-arch` patch. No `credit` service code, no `CREDIT_PROVIDER` routing execution, no `credit.*` event producers until Torbat Credit is registered in a future phase.
### 6. API-first and event-first (mandatory)
- **API-first:** All vertical actions go through documented REST (or internal token-gated) endpoints; no shared DB reads.
- **Event-first:** State changes that verticals care about (`paid`, `failed`, `refunded`, `settled`) publish `payment.*` events via transactional outbox ([ADR-006](ADR-006.md)). Verticals may use API poll or event subscribe — never both as source of truth without idempotency keys.
### 7. Database-per-service ([ADR-001](ADR-001.md))
Payment is sole owner of `payment_db`. Forbidden: cross-DB FKs, importing vertical models, storing vertical order rows in Payment.
### 8. Permission tree
Prefix `payment.*`. Permission leaves are **bundle-gated** (L2). Inheritance: `manage``create|update|delete``view`. Service-to-service scopes use internal tokens with `payment.requests.create` etc.
Foundation permission catalog registered in Phase 14.0; leaves grow per phase without renaming roots (additive only).
### 9. Bundle isolation
Inactive bundles:
- Return 404 or 403 (consistent per API template) for bundle-gated routes
- Omit from `/capabilities`
- Hide permission leaves from catalog export
- Do not publish bundle-specific events
### 10. Multi-tenant isolation ([ADR-003](ADR-003.md))
Every business row includes `tenant_id`. Callback URLs include tenant-routable connection id; verification rejects cross-tenant callback routing. Internal service calls require `X-Tenant-ID` + scoped token.
### 11. Service compatibility (read-only integration)
| Service | Integration | Payment must not |
| --- | --- | --- |
| **Core** | Entitlement check API; `feature_access.changed` | Own plans/subscriptions |
| **Identity** | `user_ref` / `payer_ref` on intents | Admin users or OIDC |
| **Tenant (Core)** | Workspace tied to Core tenant id | Duplicate tenant registry |
| **Accounting** | SettlementIntentContract events/API | Create JournalEntry |
| **CRM** | `contact_ref`, `organization_ref` on payer | Own CRM aggregates |
| **Communication** | Receipt/refund notify client | Own SMS/email providers |
| **Hospitality** | Checkout refs from POS/QR; subscribe to `payment.request.paid` | Own `pos_payment` PSP fields long-term |
| **Delivery** | Optional COD payment ref | Own delivery settlement |
| **Sports Center** | Membership/competition payment refs | Own membership billing |
| **Marketplace** | Checkout + split refs (14.7+) | Own vendor catalog |
| **Experience** | Paid form/checkout widget refs | Own payment capture |
| **Loyalty** | WalletTopUpContract (future) | Own wallet ledger |
| **Torbat Credit** (future) | CreditAuthorization via `CreditProviderAdapter`; subscribe to `credit.*` | Own scoring, KYC, BNPL, financing engine |
No existing service code is modified during architecture-only phases; verticals adopt Payment client contracts in their own future connector phases.
## Consequences
### Positive
- Tenants can enable/disable Payment, switch PSP, and switch BYO vs Merchant mode without redeploying verticals
- Future refund/split/subscription/wallet/**credit-provider** flows extend v1 contracts without breaking checkout integrations
- Torbat Credit reserved as external provider — Payment can route like any PSP when implemented
- Clear compatibility matrix for all platform services
### Negative
- Three-layer gating adds checks on every mutating API
- Contract documentation must stay synchronized with `/capabilities`
### Neutral
- Architecture phase (`payment-arch`) completes before `payment-14.0` code
- International PSPs remain adapter-only until explicitly phased
## Alternatives Considered
1. Core-only entitlement (no Payment bundles) — rejected; cannot express Torbat Pay commercial packs or hide split/refund APIs independently of Core plans.
2. PSP credentials in Core — rejected; violates ADR-020 and PCI isolation.
3. Vertical-owned checkout contracts — rejected; duplicates Marketplace/Hospitality/Sports integrations.
## Related Documents
- [ADR-020](ADR-020.md)
- [Payment Contracts Reference](../../reference/payment-contracts.md)
- [Payment Roadmap](../../payment-roadmap.md)
- [Module Boundaries](../module-boundaries.md)
- [Authorization Architecture](../authorization-architecture.md)
- [Services Contracts](../../reference/services-contracts.md)

View File

@ -23,6 +23,9 @@ One decision per file. Never overwrite an accepted ADR — supersede it.
| [ADR-017](ADR-017.md) | Independent Hospitality Platform Service | Accepted | | [ADR-017](ADR-017.md) | Independent Hospitality Platform Service | Accepted |
| [ADR-018](ADR-018.md) | Enterprise Completeness Mandate for the AI Development Framework | Accepted | | [ADR-018](ADR-018.md) | Enterprise Completeness Mandate for the AI Development Framework | Accepted |
| [ADR-019](ADR-019.md) | Mandatory Enterprise Phase Discovery | Accepted | | [ADR-019](ADR-019.md) | Mandatory Enterprise Phase Discovery | Accepted |
| [ADR-020](ADR-020.md) | Independent Enterprise Payment Platform Service (Torbat Pay) | Accepted |
| [ADR-021](ADR-021.md) | Payment Platform Licensing, Routing & Integration Contracts | Accepted |
| [ADR-022](ADR-022.md) | Platform Published Resource Architecture (incl. Actions, Access, Embed) | Accepted |
Template: [adr-template.md](../../templates/adr-template.md) Template: [adr-template.md](../../templates/adr-template.md)

View File

@ -50,6 +50,7 @@ Build a **multi-tenant**, **modular**, **API-first**, **microservice-ready** Sup
| [service-architecture.md](service-architecture.md) | Internal layering (API → Service → Repo) | | [service-architecture.md](service-architecture.md) | Internal layering (API → Service → Repo) |
| [ai-architecture.md](ai-architecture.md) | AI independence rules | | [ai-architecture.md](ai-architecture.md) | AI independence rules |
| [compliance-architecture.md](compliance-architecture.md) | Audit, posting engine, regulated flows | | [compliance-architecture.md](compliance-architecture.md) | Audit, posting engine, regulated flows |
| [published-resource-architecture.md](published-resource-architecture.md) | Public surfaces, Publish Target Registry, `publish_id` ([ADR-022](adr/ADR-022.md)) |
| [adr/](adr/) | Architecture Decision Records | | [adr/](adr/) | Architecture Decision Records |
## 5. Non-Goals of This Document ## 5. Non-Goals of This Document

View File

@ -26,11 +26,13 @@ Source of truth: `core_platform_db.tenant_memberships` ([ADR-007](adr/ADR-007.md
## Entitlement ## Entitlement
Feature keys: `{service_key}.{resource}.{action}` Feature keys: `{service_key}.{resource}.{action}`
Example: `accounting.invoice.create` Example: `accounting.invoice.create`, `payment.module.enabled`, `payment.requests.create`
Check: `POST /api/v1/tenants/{tenant_id}/features/check` Check: `POST /api/v1/tenants/{tenant_id}/features/check`
Cached in Redis; custom tenant overrides beat plan defaults. Cached in Redis; custom tenant overrides beat plan defaults.
**Payment module (Torbat Pay):** Core L1 key `payment.module.enabled`. Payment service adds L2 bundles and L3 feature toggles in `payment_db` — see [ADR-021](adr/ADR-021.md) and [payment-contracts.md](../reference/payment-contracts.md). Payment must not compute Core plan entitlements locally.
## API Guards (Core) ## API Guards (Core)
Configurable `AUTH_REQUIRED`. Dependencies such as `require_authenticated`, `require_platform_admin`, `require_tenant_admin`, `MembershipService.ensure_role`. Configurable `AUTH_REQUIRED`. Dependencies such as `require_authenticated`, `require_platform_admin`, `require_tenant_admin`, `MembershipService.ensure_role`.

View File

@ -34,9 +34,27 @@ Identity & Access acts as BFF for OIDC token exchange and Keycloak theme handoff
Frontend uses typed API clients (`lib/api.ts` primary; `lib/api-client.ts` may exist for narrower helpers). Never call databases from the browser. Frontend uses typed API clients (`lib/api.ts` primary; `lib/api-client.ts` may exist for narrower helpers). Never call databases from the browser.
## Published Resources (`publish_id`)
Every public surface that Payment, Communication, Short Link, QR, Analytics, CRM, or verticals may attach to is a **Published Resource** identified by `publish_id` ([ADR-022](adr/ADR-022.md)).
| Rule | Detail |
| --- | --- |
| Registry | Logical Publish Target Registry (`publish_id`, `tenant_id`, `resource_type`, `resource_id`, `canonical_url`, `publish_status`, `visibility`, `owner_service`) |
| Consumer key | Integrations reference **`publish_id` only** — never Experience/vertical internal tables |
| Open types | `resource_type` is an open string registry (unlimited future types) |
| Reserved URLs | `/s/`, `/p/`, `/f/`, `/survey/`, `/book/`, `/menu/`, `/card/`, `/event/` (routing not implemented by this architecture alone) |
| Published Actions | Open Action Registry — Experience binds `action_key` to `publish_id`; owning services execute |
| Public Access | Open access strategies; Experience stores policy metadata; Identity/Payment/Loyalty/Communication own auth/pay/membership/OTP |
| Universal Embed | Open embed types (iframe, SDK, widget, …); Experience owns embed metadata |
Normative docs: [published-resource-architecture.md](published-resource-architecture.md), [published-resource-contracts.md](../reference/published-resource-contracts.md), [published-action-registry.md](../reference/published-action-registry.md), [public-access-contract.md](../reference/public-access-contract.md), [embed-contract.md](../reference/embed-contract.md).
## Related Documents ## Related Documents
- [Event-Driven Architecture](event-driven-architecture.md) - [Event-Driven Architecture](event-driven-architecture.md)
- [Published Resource Architecture](published-resource-architecture.md)
- [Provider Registry](../provider-registry.md) - [Provider Registry](../provider-registry.md)
- [Provider Reference](../reference/provider-reference.md) - [Provider Reference](../reference/provider-reference.md)
- [Services Contracts](../reference/services-contracts.md) - [Services Contracts](../reference/services-contracts.md)
- [Published Resource Contracts](../reference/published-resource-contracts.md)

View File

@ -56,15 +56,21 @@ Each module (Accounting, CRM, Restaurant, Ecommerce, …) owns its database and
### Experience Platform ### Experience Platform
**Owns:** Sites, page resources and page types, versioned components, themes, layouts, templates, locales/RTL-LTR shells, media references (not binaries), forms/surveys/appointment page shells, publishing workflows, custom domain binding refs, SEO/PWA shells, capability bundles and feature toggles, widgets, consumer connector contracts, experience analytics/AI hooks shells, experience publish events. **Owns:** Sites, page resources and page types, versioned components, themes, layouts, templates, locales/RTL-LTR shells, media references (not binaries), forms/surveys/appointment page shells (**standalone-publishable**; optional Site binding only — [ADR-022](adr/ADR-022.md)), publishing workflows, Publish Target Registry entries for Experience-authored surfaces (`publish_id`), **presentation / rendering / embed metadata**, **Published Action bindings** (refs to open Action Registry — not action business execution), **Public Access Policy metadata** (not authentication), custom domain binding refs, SEO/PWA shells, capability bundles and feature toggles, widgets, consumer connector contracts, experience analytics/AI hooks shells, experience publish events.
**Must not own:** Accounting journals/Posting Engine, Sales CRM aggregates, Loyalty ledger/campaigns, Communication providers, Core tenant membership / white-label brand source of truth, File Storage binaries, Hospitality menu/item or Marketplace product catalogs as source of truth, Delivery logistics, product AI platform, Automation engine, Page Builder / public site UI (frontend). **Must not own:** Accounting journals/Posting Engine, Sales CRM aggregates, Loyalty ledger/campaigns / membership evaluation, Communication providers / OTP delivery, Core tenant membership / white-label brand source of truth / **authentication**, File Storage binaries, Hospitality menu/item or Marketplace product catalogs as source of truth, Delivery logistics, product AI platform, Automation engine, Page Builder / public site UI (frontend), Short Link / QR issuance engines as source of truth (reserved products consume `publish_id`), Payment capture/ledger, **Published Action business execution** owned by other services.
### Hospitality Platform ### Hospitality Platform
**Owns:** Venues (cafe/restaurant/bakery/… formats), branches, dining areas/tables, menus/categories/items shells, hospitality roles/permissions, bundle definitions, tenant bundle activation, feature toggles, hospitality configurations/settings/events/audit, hospitality publish events, connector contracts (client-side only). **Owns:** Venues (cafe/restaurant/bakery/… formats), branches, dining areas/tables, menus/categories/items shells, hospitality roles/permissions, bundle definitions, tenant bundle activation, feature toggles, hospitality configurations/settings/events/audit, hospitality publish events, connector contracts (client-side only).
**Must not own:** Accounting journals/Posting Engine, Sales CRM aggregates, Loyalty ledger/campaigns, Communication providers, Core tenant membership, Delivery logistics domain, Experience page/site/theme ownership (menus-as-pages are Experience; menu/item catalog source of truth is Hospitality), product AI platform, Automation engine, POS/kitchen/ordering engines until their phases. **Must not own:** Accounting journals/Posting Engine, Sales CRM aggregates, Loyalty ledger/campaigns, Communication providers, **PSP credentials / payment capture / transaction ledger (Payment / Torbat Pay)**, Core tenant membership, Delivery logistics domain, Experience page/site/theme ownership (menus-as-pages are Experience; menu/item catalog source of truth is Hospitality), product AI platform, Automation engine, POS/kitchen/ordering engines until their phases.
### Payment Platform (Torbat Pay)
**Owns:** Payment workspaces, bundle definitions, tenant payment bundles, payment feature toggles, PSP provider catalog, BYO-PSP connections, merchant accounts (facilitator), provider assignments, routing policies, payment requests, callback logs, payment transaction ledger, refunds, splits, settlement batches, reconciliation runs, accounting posting intents, vertical checkout connector registrations, checkout sessions, payment analytics/fraud shells, payment publish events, PSP adapters.
**Must not own:** Accounting journals/Posting Engine, Sales CRM aggregates, Loyalty wallet/point ledger, Communication providers, Core plans/subscriptions/entitlement computation, vertical order/cart/invoice/menu aggregates, Identity user admin, international PSP implementations until explicitly phased.
## Shared Library (`backend/shared-lib`) ## Shared Library (`backend/shared-lib`)
@ -79,11 +85,15 @@ Each module (Accounting, CRM, Restaurant, Ecommerce, …) owns its database and
3. Feature gates use Core entitlement API. 3. Feature gates use Core entitlement API.
4. Frontend talks to public/versioned APIs only. 4. Frontend talks to public/versioned APIs only.
5. Providers are integrated behind module/provider adapters — see [integration-architecture.md](integration-architecture.md). 5. Providers are integrated behind module/provider adapters — see [integration-architecture.md](integration-architecture.md).
6. Public-surface integrations across services MUST reference **`publish_id`** from the Publish Target Registry — never foreign service internal tables ([ADR-022](adr/ADR-022.md), [published-resource-architecture.md](published-resource-architecture.md)).
## Related Documents ## Related Documents
- [Service Architecture](service-architecture.md) - [Service Architecture](service-architecture.md)
- [ADR-001](adr/ADR-001.md) · [ADR-002](adr/ADR-002.md) · [ADR-007](adr/ADR-007.md) · [ADR-014](adr/ADR-014.md) · [ADR-015](adr/ADR-015.md) · [ADR-016](adr/ADR-016.md) · [ADR-017](adr/ADR-017.md) - [Published Resource Architecture](published-resource-architecture.md)
- [ADR-001](adr/ADR-001.md) · [ADR-002](adr/ADR-002.md) · [ADR-007](adr/ADR-007.md) · [ADR-014](adr/ADR-014.md) · [ADR-015](adr/ADR-015.md) · [ADR-016](adr/ADR-016.md) · [ADR-017](adr/ADR-017.md) · [ADR-020](adr/ADR-020.md) · [ADR-021](adr/ADR-021.md) · [ADR-022](adr/ADR-022.md)
- [Payment Roadmap](../payment-roadmap.md)
- [Payment Contracts](../reference/payment-contracts.md)
- [Module Registry](../module-registry.md) - [Module Registry](../module-registry.md)
- [Sports Center Roadmap](../sports-center-roadmap.md) - [Sports Center Roadmap](../sports-center-roadmap.md)
- [Delivery Roadmap](../delivery-roadmap.md) - [Delivery Roadmap](../delivery-roadmap.md)

View File

@ -158,6 +158,12 @@ Canonical definitions for TorbatYar. Prefer these terms in docs and code comment
| **Experience Bundle** | Licensed capability pack (e.g. Digital Menu, Bio Link, Custom Domain) gated via entitlement. | | **Experience Bundle** | Licensed capability pack (e.g. Digital Menu, Bio Link, Custom Domain) gated via entitlement. |
| **Widget (Experience)** | Embeddable experience surface for consumer apps/sites. | | **Widget (Experience)** | Embeddable experience surface for consumer apps/sites. |
| **Consumer Connector (Experience)** | API/event contract for verticals to bind entity refs into pages without embedding Experience logic. | | **Consumer Connector (Experience)** | API/event contract for verticals to bind entity refs into pages without embedding Experience logic. |
| **Published Resource** | Platform-wide public surface abstraction identified by `publish_id` ([ADR-022](architecture/adr/ADR-022.md)). |
| **Publish Target Registry** | Logical catalog of Published Resources (`publish_id`, `tenant_id`, `resource_type`, `resource_id`, `canonical_url`, `publish_status`, `visibility`, `owner_service`). |
| **publish_id** | Stable cross-service key for Payment, Communication, Short Link, QR, Analytics, and future products — never Experience internal table ids. |
| **Published Action** | Extensible action on a Published Resource (`action_key`); owning service executes; Experience binds presentation ([published-action-registry.md](reference/published-action-registry.md)). |
| **Public Access Policy** | Composable access strategies for a Published Resource; Experience stores metadata only — Identity/Payment/Loyalty/Communication own challenges ([public-access-contract.md](reference/public-access-contract.md)). |
| **Universal Embed** | Contract for embedding Published Resources (iframe, SDK, widget, …) with security and analytics rules ([embed-contract.md](reference/embed-contract.md)). |
| **Website Builder (historical)** | Scaffolded stub service — prefer Experience Platform for new page/site work ([ADR-016](architecture/adr/ADR-016.md)). | | **Website Builder (historical)** | Scaffolded stub service — prefer Experience Platform for new page/site work ([ADR-016](architecture/adr/ADR-016.md)). |
## Hospitality Platform ## Hospitality Platform

View File

@ -434,7 +434,7 @@ Status legend: **Active** · **Scaffolded** (code placeholder) · **Planned**
| Name | Enterprise Experience Platform (Torbat Pages) | | Name | Enterprise Experience Platform (Torbat Pages) |
| Description | Independent experience platform: sites, pages-as-resources, versioned components, themes, layouts, templates, localization, forms/surveys/appointments, SEO/PWA, bundles, widgets for all verticals | | Description | Independent experience platform: sites, pages-as-resources, versioned components, themes, layouts, templates, localization, forms/surveys/appointments, SEO/PWA, bundles, widgets for all verticals |
| Owner | Platform | | Owner | Platform |
| Status | Active (Phase 11.10 complete — Experience track closed) | | Status | Active (Phase 11.10 complete — Experience track closed · **ARCHITECTURE COMPLETE**) |
| Dependencies | Core entitlement; Storage / CRM / Loyalty / Communication / Hospitality / Marketplace / Sports / Delivery / AI / Automation via API/Events when phases require them | | Dependencies | Core entitlement; Storage / CRM / Loyalty / Communication / Hospitality / Marketplace / Sports / Delivery / AI / Automation via API/Events when phases require them |
| Internal Dependencies | Modules owned by this service only | | Internal Dependencies | Modules owned by this service only |
| External Dependencies | File Storage for media refs; notifications via Communication | | External Dependencies | File Storage for media refs; notifications via Communication |
@ -447,7 +447,7 @@ Status legend: **Active** · **Scaffolded** (code placeholder) · **Planned**
| Event Consumers | Hospitality, Marketplace, Sports Center, CRM, Loyalty, Communication, Accounting, Clinic, Hotel, Tourism, Analytics (future) | | Event Consumers | Hospitality, Marketplace, Sports Center, CRM, Loyalty, Communication, Accounting, Clinic, Hotel, Tourism, Analytics (future) |
| Provider Dependencies | Optional theme/component marketplace adapters (planned) | | Provider Dependencies | Optional theme/component marketplace adapters (planned) |
| AI Dependencies | Optional via contracts only (Phase 11.10) | | AI Dependencies | Optional via contracts only (Phase 11.10) |
| Documentation | [experience-roadmap.md](experience-roadmap.md), [phases/Experience](phases/Experience/README.md), [experience-phase-11-10.md](experience-phase-11-10.md), [phase-handover/phase-11-10.md](phase-handover/phase-11-10.md), [experience-phase-11-10-audit.md](experience-phase-11-10-audit.md), [ADR-016](architecture/adr/ADR-016.md) | | Documentation | [experience-roadmap.md](experience-roadmap.md), [phases/Experience](phases/Experience/README.md), [experience-phase-11-10.md](experience-phase-11-10.md), [phase-handover/phase-11-10.md](phase-handover/phase-11-10.md), [phase-handover/phase-experience-arch-freeze.md](phase-handover/phase-experience-arch-freeze.md), [experience-phase-11-10-audit.md](experience-phase-11-10-audit.md), [ADR-016](architecture/adr/ADR-016.md), [ADR-022](architecture/adr/ADR-022.md), [published-resource-architecture.md](architecture/published-resource-architecture.md) |
| Current Phase | 11.10 complete — track closed | | Current Phase | 11.10 complete — track closed |
| Version | 0.11.10.0 | | Version | 0.11.10.0 |
| Version Compatibility | Requires Core entitlement; integrations per later phases | | Version Compatibility | Requires Core entitlement; integrations per later phases |
@ -715,6 +715,85 @@ Status legend: **Active** · **Scaffolded** (code placeholder) · **Planned**
--- ---
## payment
| Field | Value |
| --- | --- |
| Name | Enterprise Payment Platform (Torbat Pay) |
| Description | Independent shared payment platform — BYO-PSP and Torbat Pay Merchant facilitator modes; PSP routing, callbacks, transaction ledger, refunds, splits, reconciliation intents for all verticals |
| Owner | Platform |
| Status | **Active** (MVP 14.014.5 complete; v0.14.5.0) |
| Dependencies | Core entitlement; Accounting / Communication / CRM / Loyalty / all verticals via API/Events when phases require them |
| Internal Dependencies | Modules owned by this service only |
| External Dependencies | Iranian PSPs (ZarinPal, IDPay, NextPay, Pay.ir, Mellat, Parsian, Saman, …) via adapters; international PSPs future-ready |
| Database Ownership | Sole owner (planned) |
| Database | `payment_db` |
| API Prefix | `/api/v1` (service on port **8012** reserved); `/health`, `/capabilities`, `/metrics` |
| Permission Prefix | `payment.*` |
| Events | `payment.workspace.*`, `payment.psp_connection.*`, `payment.merchant_account.*`, `payment.request.*`, `payment.callback.*`, `payment.transaction.*`, `payment.ledger.*`, `payment.refund.*`, `payment.split.*`, `payment.settlement_batch.*`, `payment.connector.*`, `payment.checkout_session.*`, `payment.reconciliation.*`, `payment.accounting_intent.*`, `payment.analytics.*` |
| Event Producers | Payment |
| Event Consumers | Hospitality, Marketplace, Sports Center, Experience, CRM, Accounting, Automation, Healthcare, Beauty, Delivery, future services |
| Provider Dependencies | PSP adapter protocols owned by Payment; Accounting/Communication client contracts |
| AI Dependencies | Optional fraud hooks via contracts only (Phase 14.10) |
| Documentation | [payment-roadmap.md](payment-roadmap.md), [payment-contracts.md](reference/payment-contracts.md), [phases/Payment](phases/Payment/README.md), [ADR-020](architecture/adr/ADR-020.md), [ADR-021](architecture/adr/ADR-021.md), [service-snapshots/payment.yaml](service-snapshots/payment.yaml) |
| Current Phase | payment-14.5 complete; next `payment-14.6` |
| Version | 0.14.5.0 |
| Version Compatibility | Requires Core entitlement; AUTH_REQUIRED optional in dev |
| Migration Version | `0006_phase_145_ledger` |
| Tenant Aware | Yes (required) |
| Permission Tree | `payment.*` |
| Future Plans | Phases payment-14.0payment-14.10 per [payment-roadmap.md](payment-roadmap.md) |
| Commercial Product | Torbat Pay |
**Phase ID disambiguation:** Payment track uses manifest IDs `payment-14.x`. Beauty Business uses `beauty-business-14.x` — separate services, separate databases.
### Modules (owned by payment — planned)
| Module Key | Name | Status | Phase | Priority |
| --- | --- | --- | --- | --- |
| `payment.workspaces` | Payment Workspaces | Complete | 14.0 | HIGH |
| `payment.bundle_definitions` | Payment Bundle Definitions | Complete | 14.0 | HIGH |
| `payment.tenant_bundles` | Tenant Payment Bundles (L2) | Complete | 14.0 | HIGH |
| `payment.feature_toggles` | Payment Feature Toggles (L3) | Complete | 14.0 | HIGH |
| `payment.provider_assignments` | Provider Assignments | Complete | 14.0 / 14.1 | HIGH |
| `payment.psp_registry` | PSP Provider Registry | Complete | 14.0 / 14.1 | HIGH |
| `payment.psp_connections` | PSP Connections (BYO-PSP) | Complete | 14.1 | HIGH |
| `payment.psp_routing` | PSP Routing Policies | Complete | 14.1 | HIGH |
| `payment.merchant_accounts` | Merchant Accounts (Facilitator) | Complete | 14.2 | HIGH |
| `payment.requests` | Payment Requests | Complete | 14.3 | HIGH |
| `payment.callbacks` | Callbacks & Verification | Complete | 14.4 | HIGH |
| `payment.transactions` | Payment Transactions | Complete | 14.5 | HIGH |
| `payment.ledger` | Transaction Ledger | Complete | 14.5 | HIGH |
| `payment.refunds` | Refunds & Reversals | Planned | 14.6 | LATER |
| `payment.splits` | Split Payments | Planned | 14.7 | LATER |
| `payment.settlement_batches` | Facilitator Settlement Batches | Planned | 14.7 | LATER |
| `payment.connectors` | Vertical Connectors | Planned | 14.8 | LATER |
| `payment.checkout_sessions` | Checkout Sessions | Planned | 14.8 | LATER |
| `payment.reconciliation` | Reconciliation | Planned | 14.9 | LATER |
| `payment.accounting_intents` | Accounting Posting Intents | Planned | 14.9 | LATER |
| `payment.analytics` | Payment Analytics | Planned | 14.10 | LATER |
| `payment.fraud_hooks` | Fraud Rule Hooks | Planned | 14.10 | LATER |
| `payment.audit` | Audit | Complete | 14.0 | HIGH |
| `payment.configuration` | Configuration | Complete | 14.0 | HIGH |
| Module | Responsibilities | Non-responsibilities |
| --- | --- | --- |
| Workspaces | Enable/disable; BYO vs Merchant mode | Core tenant admin |
| Bundles / Toggles | L2/L3 licensing within Payment | Core plan engine |
| Provider Assignments | Route to PSP or merchant without code change | Vertical PSP SDKs |
| PSP Connections | Tenant BYO-PSP credentials and routing | Vertical order ownership |
| Merchant Accounts | Torbat Pay Merchant sub-merchant shells | KYC vendor ownership |
| Payment Requests | Initiate/idempotent pay flows | Cart/menu/invoice aggregates |
| Callbacks | Webhook verify and reconcile | Communication message delivery |
| Transaction Ledger | Immutable payment-domain ledger | Accounting JournalEntry |
| Refunds | PSP refund + ledger reversal | Vertical return merchandise workflow |
| Splits / Settlement | Multi-party allocation intents | Bank payout execution |
| Connectors | Checkout contracts for verticals | Vertical-specific checkout UI |
| Reconciliation | PSP vs ledger; posting intents | GL posting engine |
| Analytics / Fraud | KPI snapshots; optional rules | Mandatory fraud for capture |
---
## ecommerce ## ecommerce
| Field | Value | | Field | Value |
@ -1074,4 +1153,6 @@ Status legend: **Active** · **Scaffolded** (code placeholder) · **Planned**
- [Experience Roadmap](experience-roadmap.md) - [Experience Roadmap](experience-roadmap.md)
- [Healthcare Roadmap](healthcare-roadmap.md) - [Healthcare Roadmap](healthcare-roadmap.md)
- [Beauty Business Roadmap](beauty-business-roadmap.md) - [Beauty Business Roadmap](beauty-business-roadmap.md)
- [Payment Roadmap](payment-roadmap.md)
- [ADR-016](architecture/adr/ADR-016.md) - [ADR-016](architecture/adr/ADR-016.md)
- [ADR-020](architecture/adr/ADR-020.md)

View File

@ -2,9 +2,25 @@
> Immediate next milestone only. History → [progress.md](progress.md). Future map → [roadmap.md](roadmap.md). > Immediate next milestone only. History → [progress.md](progress.md). Future map → [roadmap.md](roadmap.md).
## Experience track — complete ## Payment Platform — MVP complete (Torbat Pay)
Phases **11.0 through 11.10** are complete per [experience-roadmap.md](experience-roadmap.md) and [phase-handover/phase-11-10.md](phase-handover/phase-11-10.md). No further Experience phases are registered in [phase-manifest.yaml](ai-framework/phase-manifest.yaml). 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).
**Next implementation phase:** `payment-14.6` Refunds & Reversals (**LATER** priority — requires Communication integration).
Remaining documented phases: 14.614.10 (refunds, splits, connectors, reconciliation, enterprise validation).
**Note:** Payment manifest IDs use prefix `payment-14.x` (distinct from Beauty Business `beauty-business-14.x`).
## Experience track — backend complete; frontend FE-11.5 complete
Phases **11.0 through 11.10** backend complete per [experience-roadmap.md](experience-roadmap.md).
**Frontend FE-11.0 through FE-11.5** complete per [experience-frontend-roadmap.md](experience-frontend-roadmap.md) and [phase-handover/phase-experience-fe-11-5.md](phase-handover/phase-experience-fe-11-5.md). **STOP at FE-11.5** — FE-11.6+ (registry engines, payment/booking UI, QR, short links, cards, marketplace, checkout, ticketing, learning) **not started**.
**Architecture patch (docs only):** [ADR-022](architecture/adr/ADR-022.md) Published Resource Architecture accepted — [phase-handover/phase-experience-published-resource-arch.md](phase-handover/phase-experience-published-resource-arch.md).
**Architecture freeze (docs only):** Actions + Access + Embed — [phase-handover/phase-experience-arch-freeze.md](phase-handover/phase-experience-arch-freeze.md). Status: **ARCHITECTURE COMPLETE**. No registry/access/embed engines until a future registered phase.
## Suggested next platform tracks ## Suggested next platform tracks
@ -12,6 +28,7 @@ Pick one track per team capacity (see [roadmap.md](roadmap.md) and [phase-manife
| Track | Next phase | Notes | | Track | Next phase | Notes |
| --- | --- | --- | | --- | --- | --- |
| **Payment (Torbat Pay)** | `payment-14.6` Refunds & Reversals | MVP 14.014.5 complete; LATER priority |
| Delivery | `delivery-10.2` Fleet & Vehicle Types | After Driver Management 10.1 | | Delivery | `delivery-10.2` Fleet & Vehicle Types | After Driver Management 10.1 |
| Loyalty | `loyalty-7.7` Gift Card Platform | After Wallet 7.6 | | Loyalty | `loyalty-7.7` Gift Card Platform | After Wallet 7.6 |
| Hospitality | `hospitality-12.9` AI Assistant | After Analytics 12.8 | | Hospitality | `hospitality-12.9` AI Assistant | After Analytics 12.8 |

View File

@ -0,0 +1,34 @@
# Phase Pay-Arch — Payment Architecture Prerequisites
| Field | Value |
| --- | --- |
| Phase ID | `payment-arch` |
| Title | Payment Architecture Prerequisites |
| Status | Complete (documentation-only) |
| Service | `payment` |
| ADR | [ADR-021](architecture/adr/ADR-021.md) (extends [ADR-020](architecture/adr/ADR-020.md)) |
## Purpose
Finalize all architectural prerequisites before `payment-14.0` implementation so future phases never require breaking contract or licensing changes.
## Deliverables
- [ADR-021](architecture/adr/ADR-021.md) — three-layer licensing, provider assignment, contract versioning
- [payment-contracts.md](reference/payment-contracts.md) — all v1 integration contracts + future reserved schemas
- Updated [payment-roadmap.md](payment-roadmap.md) — architecture verification section
- Updated [module-boundaries.md](architecture/module-boundaries.md) — Payment platform section
- Updated [event-catalog.md](reference/event-catalog.md), [services-contracts.md](reference/services-contracts.md), [authorization-architecture.md](architecture/authorization-architecture.md)
- Service snapshot regenerated
## Definition of Done
- [x] All listed contracts documented with v1 JSON shapes
- [x] Service compatibility matrix verified (no existing service code changes)
- [x] Tenant enable/disable, PSP choice, mode switch documented as config-only
- [x] No implementation of payment-14.0
## Related Documents
- [Phase Handover Pay-Arch](phase-handover/phase-pay-arch.md)
- [Payment Roadmap](payment-roadmap.md)

View File

@ -0,0 +1,50 @@
# Phase Pay-Reg — Payment Platform Registration
| Field | Value |
| --- | --- |
| Phase ID | `payment-reg` |
| Title | Payment Platform Registration |
| Status | Complete (documentation-only) |
| Service | `payment` (registered; not yet implemented) |
| Commercial Product | Torbat Pay |
| ADR | [ADR-020](architecture/adr/ADR-020.md) |
## Purpose
Register the independent **Enterprise Payment Platform** (Torbat Pay) — service identity, database, phases 14.014.10, module inventory, boundaries, and manifests — without modifying any existing service code.
## Business Scope
- Document dual tenant payment modes: **BYO-PSP** and **Torbat Pay Merchant** (facilitator).
- Define platform-wide payment consumption pattern for Accounting, CRM, Hospitality, Sports Center, Marketplace, Delivery, Experience, Healthcare, Beauty Business, Automation, and future verticals.
- Reserve API port **8012** and database **`payment_db`**.
## Technical Scope
- ADR-020 accepted.
- Phases `payment-reg`, `payment-14.0``payment-14.10` registered in phase-manifest.
- Service entry in service-manifest and project-index.
- Module registry, roadmap, phase area README, service snapshot (registration state).
- Handover for next phase `payment-14.0`.
## Out of Scope
- Any code under `backend/services/payment`
- Alembic migrations, compose wiring, frontend checkout UI
- Real PSP SDK integrations (deferred to `payment-14.1+`)
## Definition of Done
- [x] ADR-020 published
- [x] [payment-roadmap.md](payment-roadmap.md) with complete phase 14.014.10 specifications
- [x] Phase documents under [phases/Payment/](phases/Payment/README.md)
- [x] Manifests, project index, module registry updated
- [x] Service snapshot generated (registration baseline)
- [x] Handover [phase-pay-reg.md](phase-handover/phase-pay-reg.md) complete
- [x] No existing business services modified
## Related Documents
- [Payment Roadmap](payment-roadmap.md)
- [ADR-020](architecture/adr/ADR-020.md)
- [Phase Handover Pay-Reg](phase-handover/phase-pay-reg.md)

1127
docs/payment-roadmap.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,92 @@
# Phase Handover — 14.5 Payment Transaction Ledger (MVP Complete)
## Metadata
| Field | Value |
| --- | --- |
| Phase ID | `payment-14.5` |
| Title | Transaction Ledger — HIGH priority MVP complete |
| Status | Complete |
| Service(s) | `payment` (Torbat Pay) |
| Version | 0.14.5.0 |
| Date | 2026-07-27 |
| ADR(s) | [ADR-010](../architecture/adr/ADR-010.md), [ADR-020](../architecture/adr/ADR-020.md), [ADR-021](../architecture/adr/ADR-021.md) |
## Summary
Completed Torbat Pay HIGH-priority MVP path **payment-14.0 through payment-14.5**: independent service on port **8012** with foundation licensing shells, BYO-PSP connections, Torbat Pay Merchant accounts, idempotent payment requests, callback/verification with replay protection, and immutable payment-domain transaction ledger distinct from Accounting journals.
## Completed Phases (this implementation batch)
| Phase | Version | Key deliverables |
| --- | --- | --- |
| `payment-14.0` | 0.14.0.0 | Service scaffold, foundation aggregates, bundles/toggles, audit, outbox |
| `payment-14.1` | 0.14.1.0 | PSP connections, routing policies, mock adapter, health test |
| `payment-14.2` | 0.14.2.0 | Merchant account lifecycle (facilitator mode) |
| `payment-14.3` | 0.14.3.0 | Payment requests, idempotency, mock initiate |
| `payment-14.4` | 0.14.4.0 | Callback ingress, verify API, paid/failed events |
| `payment-14.5` | 0.14.5.0 | PaymentTransaction + PaymentLedgerEntry (append-only) |
## Public APIs (14.5)
| Method | Path | Notes |
| --- | --- | --- |
| GET | `/health`, `/capabilities`, `/metrics` | Discovery |
| * | `/api/v1/payment-workspaces``/api/v1/settings` | Foundation (14.0) |
| * | `/api/v1/psp-connections`, `/api/v1/psp-routing-policies` | Bundle `payment.byo_psp.basic` |
| POST | `/api/v1/psp-connections/{id}/test` | Connection health |
| * | `/api/v1/merchant-accounts`, lifecycle actions | Bundle `payment.torbat_pay.merchant` |
| POST/GET | `/api/v1/payment-requests` | Idempotency-Key required |
| POST | `/api/v1/callbacks/{provider}/{connection_id}` | Signature + replay protection |
| POST | `/api/v1/payment-requests/{id}/verify` | Manual reconcile |
| GET | `/api/v1/payment-transactions`, `/by-source`, `/payment-ledger-entries` | Ledger queries |
## Events Published
| Event | Phase | Trigger |
| --- | --- | --- |
| `payment.request.created` | 14.3 | Request initiated |
| `payment.request.redirect_issued` | 14.3 | Mock redirect URL issued |
| `payment.request.paid` | 14.4 | Verified paid |
| `payment.request.failed` | 14.4 | Verification failed |
| `payment.transaction.recorded` | 14.5 | Ledger row on first paid |
## Permissions
Full tree under `payment.*` — see [payment-contracts.md](../reference/payment-contracts.md) §8 and `GET /api/v1/permissions/catalog`.
## Migration Notes
| Item | Detail |
| --- | --- |
| Alembic head | `0006_phase_145_ledger` |
| Upgrade | `python scripts/ensure_db.py && alembic upgrade head` |
| Compose | `payment-service` on port 8012, database `payment_db` |
## Quality Gates (all green)
- Architecture layering verified (`test_architecture.py`)
- Tenant isolation (`test_tenant_isolation.py`)
- Permissions catalog (`test_permissions.py`)
- Migrations present (`test_migration.py`)
- Security / bundle gating (`test_security.py`)
- Phase tests 14.114.5
- Docs version sync (`test_docs.py`)
## Known Limitations
- Mock PSP only — production PSP SDK adapters deferred
- `CREDIT_PROVIDER` rejected (422) — Torbat Credit reserved
- No refunds (14.6), splits (14.7), connectors (14.8), reconciliation (14.9)
- No Accounting journal creation inside Payment
## Next Phase
`payment-14.6` Refunds & Reversals (**LATER** priority) — requires Communication client integration.
## References
- [payment-roadmap.md](../payment-roadmap.md)
- [payment-contracts.md](../reference/payment-contracts.md)
- [service-snapshots/payment.yaml](../service-snapshots/payment.yaml)
- Service README: `backend/services/payment/README.md`

View File

@ -0,0 +1,139 @@
# Phase Handover — Pay-Arch Payment Architecture Prerequisites
## Metadata
| Field | Value |
| --- | --- |
| Phase ID | `payment-arch` |
| Title | Payment Architecture Prerequisites |
| Status | Complete |
| Service(s) | `payment` (architecture only) |
| Version | n/a (docs-only) |
| Date | 2026-07-27 |
| ADR(s) | [ADR-021](../architecture/adr/ADR-021.md) (extends [ADR-020](../architecture/adr/ADR-020.md)) |
## Summary
Finalized Torbat Pay architectural prerequisites: three-layer licensing (Core entitlement → Payment bundles → feature toggles), provider assignment without code changes, versioned v1 contracts for current and future flows, and verified compatibility with Core, Identity, Tenant, Accounting, CRM, Communication, Hospitality, Delivery, Sports Center, and Marketplace.
## Architectural artifacts
| Artifact | Path |
| --- | --- |
| ADR-021 | [ADR-021.md](../architecture/adr/ADR-021.md) |
| Contract reference | [payment-contracts.md](../reference/payment-contracts.md) |
| Roadmap (updated) | [payment-roadmap.md](../payment-roadmap.md) |
| Module boundaries | [module-boundaries.md](../architecture/module-boundaries.md) |
| Event catalog | [event-catalog.md](../reference/event-catalog.md#payment-platform-planned) |
| Services contracts | [services-contracts.md](../reference/services-contracts.md) |
## Contract versions locked (v1)
| Contract | Phase | Status |
| --- | --- | --- |
| PaymentIntentContract | 14.3 | Schema locked |
| CheckoutSessionContract | 14.8 | Schema locked |
| CallbackIngressContract | 14.4 | Schema locked |
| SettlementIntentContract | 14.9 | Schema locked |
| RefundIntentContract | 14.6 | Reserved |
| SplitAllocationContract | 14.7 | Reserved |
| SubscriptionBillingContract | Future | Reserved |
| WalletTopUpContract | Future | Reserved |
| InstallmentPlanContract | Future | Reserved — **Payment schedule shell only** |
| CreditProviderAdapter / CreditAuthorizationContract | Future | Reserved — Torbat Credit via CREDIT_PROVIDER |
## Torbat Credit reservation (post-arch patch)
- Future service `credit` / **Torbat Credit** reserved; `credit_db`; `credit.*` events owned by Credit service only
- Payment source `CREDIT_PROVIDER` reserved in contracts — not implemented
- Reference fields: `credit_provider_id`, `financing_reference`, `credit_authorization_reference`, `installment_contract_reference`
- Payment invokes Torbat Credit via **CreditProviderAdapter** (same pattern as PSP)
- Installment calculation / scoring / KYC / BNPL / financing = **Torbat Credit exclusive**
## Compatibility verification
| Service | Result | Notes |
| --- | --- | --- |
| Core | ✅ | L1 `payment.module.enabled`; `feature_access.changed` |
| Identity | ✅ | Payer `user_ref` only |
| Tenant | ✅ | Workspace per Core tenant id |
| Accounting | ✅ | SettlementIntent events; no journals in Payment |
| CRM | ✅ | `contact_ref` on payer |
| Communication | ✅ | Notify client for receipts/refunds |
| Hospitality | ✅ | POS/QR refs; no PSP ownership long-term |
| Delivery | ✅ | Optional COD ref |
| Sports Center | ✅ | Membership payment refs |
| Marketplace | ✅ | Checkout + splits via contracts |
No existing service code was modified.
## Foundation scope additions for 14.0
Phase `payment-14.0` MUST implement (per architecture):
- `PaymentBundleDefinition`, `TenantPaymentBundle`
- `PaymentFeatureToggle`
- `PaymentProviderAssignment` (shell)
- Core entitlement check client (stub)
- Bundle-gated permissions and `/capabilities`
## Known limitations
- No `backend/services/payment` code yet
- Vertical connector **clients** not implemented in Hospitality/Marketplace (Payment 14.8 / vertical phases)
- International PSP adapters not registered
- Core `payment.module.enabled` feature seed may be added at 14.0 wiring time (Core change in Payment phase only)
## Next Phase Entry
| Field | Value |
| --- | --- |
| Recommended next phase | `payment-14.0` Payment Foundation |
| Blockers | None |
| Required reads | This handover + [payment-contracts.md](../reference/payment-contracts.md) + [ADR-021](../architecture/adr/ADR-021.md) + [phase-14-0-payment-foundation.md](../phases/Payment/phase-14-0-payment-foundation.md) |
## Completion Sign-Off
- [x] ADR-021 accepted
- [x] All contracts documented
- [x] Manifests, snapshot, registry, project index updated
- [x] No business code; no existing services modified
- [x] payment-14.0 NOT implemented
---
## Addendum — Torbat Credit reservation (post payment-arch patch)
**Date:** 2026-07-27 · **Scope:** Documentation only · **No implementation**
### Reserved future service
| Field | Value |
| --- | --- |
| Service id | `credit` |
| Commercial product | **Torbat Credit** |
| Database | `credit_db` (reserved) |
| Event namespace | `credit.*` (reserved for Torbat Credit — Payment does not publish) |
### Payment architecture patches
1. **External Credit Providers** — Payment registry recognizes `CreditProviderRegistration` alongside PSP catalog.
2. **Payment source `CREDIT_PROVIDER`** — reserved in routing policy and PaymentIntent v1; not implemented.
3. **Opaque reference fields** on intents/transactions: `credit_provider_id`, `financing_reference`, `credit_authorization_reference`, `installment_contract_reference`.
4. **InstallmentPlanContract** (Payment) = schedule shell only. Scoring, KYC, BNPL, financing calculation = **Torbat Credit exclusive**.
5. **CreditProviderAdapter** — Payment calls Torbat Credit using the **same provider adapter pattern as PSPs** when implemented.
### Updated artifacts
- [ADR-021 §12](../architecture/adr/ADR-021.md)
- [payment-contracts.md](../reference/payment-contracts.md) — §4.94.11, PaymentIntent credit fields
- [payment-roadmap.md](../payment-roadmap.md) — Torbat Credit section
- [payment.yaml](../service-snapshots/payment.yaml) — snapshot_version 3
No completed business phases modified. No `credit` service registration in manifests (future phase).
## Related Documents
- [Payment Roadmap](../payment-roadmap.md)
- [Service Snapshot](../service-snapshots/payment.yaml)
- [Phase Handover Pay-Reg](phase-pay-reg.md)

View File

@ -0,0 +1,123 @@
# Phase Handover — Pay-Reg Payment Platform Registration
## Metadata
| Field | Value |
| --- | --- |
| Phase ID | `payment-reg` |
| Title | Payment Platform Registration |
| Status | Complete |
| Service(s) | `payment` (registered; not yet implemented) |
| Commercial Product | Torbat Pay |
| Version | n/a (docs-only) |
| Date | 2026-07-27 |
| ADR(s) | [ADR-020](../architecture/adr/ADR-020.md), [ADR-021](../architecture/adr/ADR-021.md) |
## Reusable Components
| Component | Location | Reuse notes |
| --- | --- | --- |
| Platform registration pattern | Same as Delivery DP-Reg, Experience XP-Reg | Docs + ADR + manifests before code |
| Provider adapter pattern | Communication service PSP-like registry | PSP adapters in Payment only |
| Service template | [service-template.md](../ai-framework/service-template.md) | Use in Phase 14.0 |
| Phase template | [phase-template.md](../ai-framework/phase-template.md) | Use for 14.x phase docs |
## Public APIs
| Method | Path | Auth / Permission | Notes |
| --- | --- | --- | --- |
| — | — | — | N/A — registration only; APIs begin in `payment-14.0` |
Planned surfaces (not implemented): `/health`, `/capabilities`, `/metrics`, `/api/v1/*` under `payment.*` permissions.
## Events
| Event type | Domain / Integration | Payload summary | Version |
| --- | --- | --- | --- |
| `payment.*` (planned) | Payment domain | Publish-only; catalog grows per phase | Planned |
## Extension Points
| Extension point | How to extend | Forbidden uses |
| --- | --- | --- |
| PSP adapters | Register in Payment adapter registry | Verticals calling ZarinPal/IDPay directly |
| Facilitator mode | Merchant accounts + platform credentials | Storing PSP secrets in Core or vertical DBs |
| Vertical connectors | Versioned checkout contracts (14.8) | Shared DB with Hospitality/Marketplace |
| Accounting intents | Events/API for posting intents | JournalEntry creation inside Payment |
| Fraud hooks | Optional rule registry (14.10) | Hard dependency for capture |
## Known Limitations
- No `backend/services/payment` code yet — Phase 14.0
- No Alembic migrations yet
- No compose wiring / runtime health yet
- No live PSP integrations until 14.1+
- Payment capture path incomplete until 14.314.5
- International PSPs documented as future-ready only
- Phase numeric 14.x shared with Beauty Business track — always use manifest prefix `payment-14.x`
## Migration Notes
| Item | Detail |
| --- | --- |
| Alembic revision(s) | N/A (docs-only) |
| Upgrade steps | N/A |
| Downgrade support | N/A |
| Data backfill | N/A |
| Breaking changes | None |
## Dependencies
| Dependency | Type | Required for |
| --- | --- | --- |
| AI Framework | Docs | Development loop / gates |
| ADR-001 / 003 / 006 / 010 / 012 / 020 / 021 | Architecture | Boundaries |
| Core Platform | Service | Entitlement (from 14.0) |
| Communication | Service | Receipt notify (from 14.6+) |
| Accounting | Service | Posting intents (from 14.9) |
## Discovery Record (Registration)
| Field | Value |
| --- | --- |
| Discovery date | 2026-07-27 |
| Baseline | No payment service; verticals hold payment shells (e.g. Hospitality `pos_payment`) without PSP |
| Gap | Central payment platform missing |
| Promoted scope | Registration + full roadmap 14.014.10 |
| Excluded | Implementation of 14.0; modifications to existing services |
## Next Phase Entry
| Field | Value |
| --- | --- |
| Recommended next phase | `payment-14.0` Payment Foundation |
| Blockers for next phase | None for scaffold |
| Implementation Priority | **HIGH** (14.014.5) |
What the next phase must read and assume:
1. [phase-pay-arch handover](../phase-handover/phase-pay-arch.md) + [payment-roadmap.md](../payment-roadmap.md) Phase 14.0 section
2. [ADR-020](../architecture/adr/ADR-020.md) + [ADR-021](../architecture/adr/ADR-021.md)
3. [payment-contracts.md](../reference/payment-contracts.md)
4. Updated [module-registry.md](../module-registry.md#payment) / manifests
5. Implement foundation shells only — no PSP capture, callbacks, or ledger engines
## Completion Sign-Off
- [x] Quality gates passed (documentation / architecture / manifest / cross-reference)
- [x] Tests green (N/A — docs-only)
- [x] Documentation updated
- [x] Progress / next-steps / registries / project index updated
- [x] Service snapshot generated (registration baseline)
- [x] No existing business services modified
- [x] Phase 14.0 NOT implemented (by design)
## Related Documents
- [Payment Roadmap](../payment-roadmap.md)
- [Phase Area](../phases/Payment/README.md)
- [ADR-020](../architecture/adr/ADR-020.md)
- [Phase Manifest](../ai-framework/phase-manifest.yaml)
- [Service Manifest](../ai-framework/service-manifest.yaml)
- [Project Index](../ai-framework/project-index.yaml)
- [Quality Gates](../ai-framework/quality-gates.md)

View File

@ -0,0 +1,62 @@
# Payment Platform Phase Area (Torbat Pay)
Independent enterprise **Payment Platform** (`payment-service`, `payment_db`, port **8012**). Commercial product: **Torbat Pay**.
Supports **BYO-PSP** (tenant-owned gateways) and **Torbat Pay Merchant** (facilitator) modes for all TorbatYar verticals via API + Events only.
## Phase Numbering
Payment track phases are **14.014.10** with manifest IDs `payment-14.x`.
Beauty Business uses a separate track: `beauty-business-14.x` — see [beauty-business-roadmap.md](../../beauty-business-roadmap.md).
## Status
| Phase | Title | Implementation Priority | Status |
| --- | --- | --- | --- |
| Reg | Platform Registration | — | Complete |
| Arch | Architecture Prerequisites | — | Complete |
| 14.0 | Payment Foundation | HIGH | Complete |
| 14.1 | PSP Management | HIGH | Complete |
| 14.2 | Merchant Accounts | HIGH | Complete |
| 14.3 | Payment Requests | HIGH | Complete |
| 14.4 | Callback & Verification | HIGH | Complete |
| 14.5 | Transaction Ledger | HIGH | Complete |
| 14.6 | Refunds & Reversals | LATER | Planned |
| 14.7 | Split Payments & Facilitator Settlement | LATER | Planned |
| 14.8 | Vertical Connectors & Checkout Contracts | LATER | Planned |
| 14.9 | Reconciliation & Accounting Integration | LATER | Planned |
| 14.10 | Analytics, Fraud Shells & Enterprise Validation | LATER | Planned |
## Documents
- [Roadmap](../../payment-roadmap.md) — authoritative phase specifications
- [Pay-Reg](../../payment-phase-pay-reg.md)
- [Pay-Arch](../../payment-phase-pay-arch.md)
- [Handover Pay-Reg](../../phase-handover/phase-pay-reg.md)
- [Handover Pay-Arch](../../phase-handover/phase-pay-arch.md)
- [Handover 14.5 MVP](../../phase-handover/phase-14-5.md)
- [Payment Contracts](../../reference/payment-contracts.md)
- [ADR-020](../../architecture/adr/ADR-020.md)
- [ADR-021](../../architecture/adr/ADR-021.md)
- [AI Framework](../../ai-framework/README.md)
### Phase Documents
- [14.0 Payment Foundation](phase-14-0-payment-foundation.md)
- [14.1 PSP Management](phase-14-1-psp-management.md)
- [14.2 Merchant Accounts](phase-14-2-merchant-accounts.md)
- [14.3 Payment Requests](phase-14-3-payment-requests.md)
- [14.4 Callback & Verification](phase-14-4-callback-verification.md)
- [14.5 Transaction Ledger](phase-14-5-transaction-ledger.md)
- [14.6 Refunds & Reversals](phase-14-6-refunds-reversals.md)
- [14.7 Split & Settlement](phase-14-7-split-settlement.md)
- [14.8 Vertical Connectors](phase-14-8-vertical-connectors.md)
- [14.9 Reconciliation](phase-14-9-reconciliation.md)
- [14.10 Enterprise Validation](phase-14-10-enterprise-validation.md)
## Boundary Reminder
Payment owns PSP routing, callbacks, and payment-domain ledger.
Verticals own checkout/commercial context.
Accounting owns journals. Communication owns message delivery.
Do not embed PSP SDKs in Hospitality, Marketplace, or other services.

View File

@ -0,0 +1,83 @@
# Phase 14.0 — Payment Foundation
| Field | Value |
| --- | --- |
| Identifier | `payment-14.0` |
| Implementation Priority | **HIGH** |
| Status | Complete |
| Service | `payment` |
| Version (target) | 0.14.0.0 |
| Database | `payment_db` |
| API Port | 8012 |
| ADR | ADR-001, ADR-003, ADR-006, ADR-020, **ADR-021** |
> Full specification: [payment-roadmap.md](../../payment-roadmap.md#phase-14-0--payment-foundation)
> **Required architecture reads:** [ADR-021](../../architecture/adr/ADR-021.md), [payment-contracts.md](../../reference/payment-contracts.md), [phase-pay-arch handover](../../phase-handover/phase-pay-arch.md)
## Purpose
Establish Torbat Pay as an independent enterprise microservice foundation including **three-layer licensing shells** (L2 bundles, L3 toggles), workspace enable/disable, provider assignment shells, and provider protocol stubs — no live PSP or capture flows.
## Business Scope
- Per-tenant payment workspace with `enabled` / `disabled` / `suspended` and `default_payment_mode` (`byo_psp` | `torbat_pay_merchant`).
- Payment bundle catalog and tenant bundle activation (L2).
- Feature toggles (L3) for refunds, splits, subscriptions, installments, wallet top-up (reserved).
- Provider assignment shells for routing without code changes (full routing in 14.1).
## Technical Scope
- Scaffold `backend/services/payment`; Alembic `0001_initial`.
- Aggregates: PaymentWorkspace, PaymentRole, PaymentPermission, **PaymentBundleDefinition**, **TenantPaymentBundle**, **PaymentFeatureToggle**, **PaymentProviderAssignment**, PspProviderRegistration, PaymentConfiguration, PaymentSetting, PaymentAuditLog, OutboxEvent.
- Core L1 client stub: check `payment.module.enabled`.
- Provider protocols (stubs): PspAdapter, AccountingClient, CommunicationClient, CrmClient, CoreEntitlementClient.
## Dependencies
`payment-arch`, `payment-reg`, `onboarding-4`, `ai-framework`, Core entitlement
## Out of Scope
PSP connections (14.1), merchant onboarding (14.2), payment requests (14.3+), callbacks, ledger
## Capabilities
`payment.foundation`, `payment.workspace`, `payment.bundles`, `payment.feature_toggles`, `payment.provider_assignment_shell`, `payment.psp_registry_shell`, `payment.audit`
## Permissions
Per [payment-contracts.md](../../reference/payment-contracts.md) §8 — foundation leaves only; bundle-gated leaves registered but inactive until bundles assigned.
## Events
`payment.workspace.enabled|disabled`, `payment.bundle.activated`, `payment.feature_toggle.changed`, `payment.psp_provider.registered`
## API Contracts
`/health`, `/capabilities`, `/metrics`, `/api/v1/payment-workspaces`, `/api/v1/bundle-definitions`, `/api/v1/tenant-bundles`, `/api/v1/feature-toggles`, `/api/v1/provider-assignments`, `/api/v1/psp-provider-registrations`, `/api/v1/configurations`, `/api/v1/settings`, `/api/v1/audit`
## Data Ownership
Payment sole owner of all foundation tables in `payment_db`.
## Provider Ownership
Adapter interfaces only in 14.0.
## Integration Rules
- L1 Core check before mutating APIs.
- Inactive L2 bundles hide gated routes from `/capabilities`.
- No cross-DB access; no existing service modifications.
## Quality Gates
Architecture, tenant isolation, bundle isolation, permissions, migration, dependency, security, docs.
## Definition of Done
Service boots on 8012; foundation + licensing shells tested; contracts unchanged from payment-arch.
## Future Compatibility
All v1 contracts in [payment-contracts.md](../../reference/payment-contracts.md) remain stable; 14.0 tables include fields for `region`, `currency`, facilitator mode per ADR-021.

View File

@ -0,0 +1,71 @@
# Phase 14.1 — PSP Management
| Field | Value |
| --- | --- |
| Identifier | `payment-14.1` |
| Implementation Priority | **HIGH** |
| Status | Complete |
| Service | `payment` |
| Depends On | `payment-14.0` |
> Full specification: [payment-roadmap.md](../../payment-roadmap.md#phase-14-1--psp-management)
## Purpose
Enable tenants to connect BYO-PSP credentials (ZarinPal, IDPay, NextPay, Pay.ir, Mellat, Parsian, Saman, …), test connections, and configure routing priority with adapter registry.
## Business Scope
Connect, test, suspend, and prioritize tenant-owned PSP gateways. Failover between primary and secondary PSP.
## Technical Scope
Aggregates: PspConnection, PspCredentialVaultRef, PspConnectionHealthCheck, PspRoutingPolicy. MockPspAdapter + provider-family stubs. Secret refs only in API responses.
## Dependencies
`payment-14.0`
## Out of Scope
Merchant facilitator onboarding (14.2), payment capture (14.3), callbacks (14.4), international live PSPs.
## Capabilities
`payment.psp_connections`, `payment.psp_routing`, `payment.psp_health_check`
## Permissions
`payment.psp_connections.view|create|update|delete|test|manage`, `payment.psp_routing.manage`
## Events
`payment.psp_connection.created|updated|suspended|activated|tested`, `payment.psp_routing.updated`
## API Contracts
`/api/v1/psp-connections`, `/api/v1/psp-routing-policies`, POST `.../test`
## Data Ownership
Payment owns connection and routing metadata; vault stores secrets.
## Provider Ownership
All PSP SDK calls in Payment adapters.
## Integration Rules
Credential rotation must not break in-flight requests. Adapter protocol versioned.
## Quality Gates
Secret redaction, tenant isolation, adapter contract tests.
## Definition of Done
Tenant PSP connect/test/suspend works with mock adapter; no payment requests yet.
## Future Compatibility
Region/currency on connections; Stripe/PayPal adapter slots without schema break.

View File

@ -0,0 +1,71 @@
# Phase 14.10 — Analytics, Fraud Shells & Enterprise Validation
| Field | Value |
| --- | --- |
| Identifier | `payment-14.10` |
| Implementation Priority | **LATER** |
| Status | Planned |
| Service | `payment` |
| Depends On | `payment-14.9`, `ai-framework` |
> Full specification: [payment-roadmap.md](../../payment-roadmap.md#phase-1410--analytics-fraud-shells--enterprise-validation)
## Purpose
Analytics snapshots, optional fraud rule hooks, and full enterprise validation — closes Payment track 14.014.10.
## Business Scope
Payment KPIs (volume, success rate, refund rate); fraud shells off by default; production sign-off.
## Technical Scope
PaymentAnalyticsReportDefinition, PaymentAnalyticsSnapshot, FraudRuleRegistration, FraudCheckDispatch (mock). Enterprise validation audit.
## Dependencies
`payment-14.9`
## Out of Scope
ML fraud; PCI certification program.
## Capabilities
`payment.analytics`, `payment.fraud_hooks`, `payment.enterprise_validation`
## Permissions
`payment.analytics.view|refresh`, `payment.fraud_rules.manage`
## Events
`payment.analytics.snapshot.created`, `payment.fraud_check.dispatched`
## API Contracts
Analytics report CRUD; POST snapshots/refresh
## Data Ownership
Payment local aggregates only.
## Provider Ownership
External fraud vendors via stub adapters.
## Integration Rules
No cross-service DB reads; core flows work with fraud off.
## Quality Gates
Full gate suite; self-heal until green.
## Definition of Done
Track complete at 0.14.10.0; no undocumented API/event/permission.
## Future Compatibility
Real-time metrics; international fraud adapters.

View File

@ -0,0 +1,71 @@
# Phase 14.2 — Merchant Accounts
| Field | Value |
| --- | --- |
| Identifier | `payment-14.2` |
| Implementation Priority | **HIGH** |
| Status | Complete |
| Service | `payment` |
| Depends On | `payment-14.1` |
> Full specification: [payment-roadmap.md](../../payment-roadmap.md#phase-14-2--merchant-accounts)
## Purpose
Introduce Torbat Pay Merchant (facilitator) sub-merchant account shells for tenants who accept payments through platform facilitator mode.
## Business Scope
Sub-merchant onboarding lifecycle; KYC/KYB document refs (File Storage); link merchant to workspace and optional branch refs.
## Technical Scope
Aggregates: MerchantAccount, MerchantAccountProfile, MerchantSettlementProfile, MerchantComplianceRef, MerchantAccountStatusHistory. Facilitator platform settings.
## Dependencies
`payment-14.1`
## Out of Scope
Live KYC vendor, bank payouts (14.7/14.9), payment capture (14.3).
## Capabilities
`payment.merchant_accounts`, `payment.facilitator_mode`
## Permissions
`payment.merchant_accounts.view|create|update|submit|activate|suspend|close|manage`
## Events
`payment.merchant_account.created|submitted|activated|suspended|closed`
## API Contracts
`/api/v1/merchant-accounts`, `/api/v1/merchant-settlement-profiles`, lifecycle POST actions
## Data Ownership
Payment owns merchant domain; Identity users; Storage for compliance blobs (refs).
## Provider Ownership
Platform facilitator PSP credentials in Payment platform settings.
## Integration Rules
Required when workspace `default_payment_mode=torbat_pay_merchant`. BYO mode skips merchant for routing.
## Quality Gates
Lifecycle transitions, PII redaction, tenant isolation.
## Definition of Done
Merchant CRUD + lifecycle; facilitator flag on workspace; no payment requests.
## Future Compatibility
Multi-currency settlement profiles; international merchant descriptors.

View File

@ -0,0 +1,71 @@
# Phase 14.3 — Payment Requests
| Field | Value |
| --- | --- |
| Identifier | `payment-14.3` |
| Implementation Priority | **HIGH** |
| Status | Complete |
| Service | `payment` |
| Depends On | `payment-14.2` |
> Full specification: [payment-roadmap.md](../../payment-roadmap.md#phase-14-3--payment-requests)
## Purpose
Idempotent payment request initiation for any vertical — amount, currency (IRR first), commercial refs, redirect/token payload from selected PSP.
## Business Scope
Initiate pay for order/invoice/membership/checkout refs. Expire abandoned requests. Route via BYO-PSP or facilitator merchant.
## Technical Scope
Aggregates: PaymentRequest, PaymentRequestLine, PaymentRequestStatusHistory. Idempotency-Key enforcement. Adapter `initiate_payment()`.
## Dependencies
`payment-14.2`, active PSP connection or merchant account
## Out of Scope
Callbacks (14.4), ledger (14.5), refunds (14.6).
## Capabilities
`payment.requests`, `payment.initiate`, `payment.idempotency`
## Permissions
`payment.requests.view|create|cancel|manage`; internal `payment.requests.create`
## Events
`payment.request.created|redirect_issued|expired|cancelled|failed`
## API Contracts
POST/GET `/api/v1/payment-requests`, POST `.../cancel`, internal `/internal/v1/payment-requests`
## Data Ownership
Payment owns request lifecycle; vertical owns commercial aggregate.
## Provider Ownership
PSP initiate APIs in adapters only.
## Integration Rules
Verticals pass refs only. Duplicate idempotency key returns same request.
## Quality Gates
Idempotency, amount validation, state machine, tenant isolation.
## Definition of Done
Mock initiate returns redirect payload; events emitted; ledger deferred to 14.5.
## Future Compatibility
Multi-currency; tokenized card metadata slots.

Some files were not shown because too many files have changed in this diff Show More