diff --git a/backend/services/payment/Dockerfile.dev b/backend/services/payment/Dockerfile.dev new file mode 100644 index 0000000..b16274f --- /dev/null +++ b/backend/services/payment/Dockerfile.dev @@ -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 diff --git a/backend/services/payment/README.md b/backend/services/payment/README.md new file mode 100644 index 0000000..548fa22 --- /dev/null +++ b/backend/services/payment/README.md @@ -0,0 +1,49 @@ +# Torbat Pay — Enterprise Payment Platform + +Independent, tenant-isolated payment microservice (**Torbat Pay**) implementing phases **14.0–14.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) diff --git a/backend/services/payment/alembic.ini b/backend/services/payment/alembic.ini new file mode 100644 index 0000000..44ba650 --- /dev/null +++ b/backend/services/payment/alembic.ini @@ -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 diff --git a/backend/services/payment/alembic/env.py b/backend/services/payment/alembic/env.py new file mode 100644 index 0000000..e2afd80 --- /dev/null +++ b/backend/services/payment/alembic/env.py @@ -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() diff --git a/backend/services/payment/alembic/versions/0001_initial.py b/backend/services/payment/alembic/versions/0001_initial.py new file mode 100644 index 0000000..9faf5a1 --- /dev/null +++ b/backend/services/payment/alembic/versions/0001_initial.py @@ -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()) diff --git a/backend/services/payment/alembic/versions/0002_phase_141_psp.py b/backend/services/payment/alembic/versions/0002_phase_141_psp.py new file mode 100644 index 0000000..2b0763c --- /dev/null +++ b/backend/services/payment/alembic/versions/0002_phase_141_psp.py @@ -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) diff --git a/backend/services/payment/alembic/versions/0003_phase_142_merchant.py b/backend/services/payment/alembic/versions/0003_phase_142_merchant.py new file mode 100644 index 0000000..8a9a78d --- /dev/null +++ b/backend/services/payment/alembic/versions/0003_phase_142_merchant.py @@ -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) diff --git a/backend/services/payment/alembic/versions/0004_phase_143_requests.py b/backend/services/payment/alembic/versions/0004_phase_143_requests.py new file mode 100644 index 0000000..f530bd1 --- /dev/null +++ b/backend/services/payment/alembic/versions/0004_phase_143_requests.py @@ -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) diff --git a/backend/services/payment/alembic/versions/0005_phase_144_callbacks.py b/backend/services/payment/alembic/versions/0005_phase_144_callbacks.py new file mode 100644 index 0000000..219a856 --- /dev/null +++ b/backend/services/payment/alembic/versions/0005_phase_144_callbacks.py @@ -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) diff --git a/backend/services/payment/alembic/versions/0006_phase_145_ledger.py b/backend/services/payment/alembic/versions/0006_phase_145_ledger.py new file mode 100644 index 0000000..1703e4f --- /dev/null +++ b/backend/services/payment/alembic/versions/0006_phase_145_ledger.py @@ -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) diff --git a/backend/services/payment/alembic/versions/__init__.py b/backend/services/payment/alembic/versions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/payment/app/__init__.py b/backend/services/payment/app/__init__.py new file mode 100644 index 0000000..a6b639c --- /dev/null +++ b/backend/services/payment/app/__init__.py @@ -0,0 +1 @@ +__version__ = "0.14.5.0" diff --git a/backend/services/payment/app/api/__init__.py b/backend/services/payment/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/payment/app/api/deps.py b/backend/services/payment/app/api/deps.py new file mode 100644 index 0000000..66c2eb4 --- /dev/null +++ b/backend/services/payment/app/api/deps.py @@ -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)) diff --git a/backend/services/payment/app/api/permissions.py b/backend/services/payment/app/api/permissions.py new file mode 100644 index 0000000..592de5b --- /dev/null +++ b/backend/services/payment/app/api/permissions.py @@ -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") diff --git a/backend/services/payment/app/api/v1/__init__.py b/backend/services/payment/app/api/v1/__init__.py new file mode 100644 index 0000000..24d51eb --- /dev/null +++ b/backend/services/payment/app/api/v1/__init__.py @@ -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) diff --git a/backend/services/payment/app/api/v1/health.py b/backend/services/payment/app/api/v1/health.py new file mode 100644 index 0000000..4d16b97 --- /dev/null +++ b/backend/services/payment/app/api/v1/health.py @@ -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"}} diff --git a/backend/services/payment/app/api/v1/routes.py b/backend/services/payment/app/api/v1/routes.py new file mode 100644 index 0000000..9d45c3a --- /dev/null +++ b/backend/services/payment/app/api/v1/routes.py @@ -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()] diff --git a/backend/services/payment/app/commands/__init__.py b/backend/services/payment/app/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/payment/app/commands/payment.py b/backend/services/payment/app/commands/payment.py new file mode 100644 index 0000000..389e274 --- /dev/null +++ b/backend/services/payment/app/commands/payment.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass +from uuid import UUID +@dataclass +class CreatePaymentRequestCommand: + tenant_id: UUID + idempotency_key: str + payload: dict diff --git a/backend/services/payment/app/core/__init__.py b/backend/services/payment/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/payment/app/core/config.py b/backend/services/payment/app/core/config.py new file mode 100644 index 0000000..6850779 --- /dev/null +++ b/backend/services/payment/app/core/config.py @@ -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() diff --git a/backend/services/payment/app/core/database.py b/backend/services/payment/app/core/database.py new file mode 100644 index 0000000..1df3c33 --- /dev/null +++ b/backend/services/payment/app/core/database.py @@ -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 diff --git a/backend/services/payment/app/core/entitlements.py b/backend/services/payment/app/core/entitlements.py new file mode 100644 index 0000000..9519522 --- /dev/null +++ b/backend/services/payment/app/core/entitlements.py @@ -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 diff --git a/backend/services/payment/app/core/security.py b/backend/services/payment/app/core/security.py new file mode 100644 index 0000000..630b26f --- /dev/null +++ b/backend/services/payment/app/core/security.py @@ -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=[]) diff --git a/backend/services/payment/app/events/__init__.py b/backend/services/payment/app/events/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/payment/app/events/publisher.py b/backend/services/payment/app/events/publisher.py new file mode 100644 index 0000000..f8ad138 --- /dev/null +++ b/backend/services/payment/app/events/publisher.py @@ -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 diff --git a/backend/services/payment/app/events/types.py b/backend/services/payment/app/events/types.py new file mode 100644 index 0000000..f7004a9 --- /dev/null +++ b/backend/services/payment/app/events/types.py @@ -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" diff --git a/backend/services/payment/app/main.py b/backend/services/payment/app/main.py new file mode 100644 index 0000000..1e84b8b --- /dev/null +++ b/backend/services/payment/app/main.py @@ -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() diff --git a/backend/services/payment/app/middlewares/__init__.py b/backend/services/payment/app/middlewares/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/payment/app/middlewares/tenant.py b/backend/services/payment/app/middlewares/tenant.py new file mode 100644 index 0000000..8b77448 --- /dev/null +++ b/backend/services/payment/app/middlewares/tenant.py @@ -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) diff --git a/backend/services/payment/app/models/__init__.py b/backend/services/payment/app/models/__init__.py new file mode 100644 index 0000000..72f108f --- /dev/null +++ b/backend/services/payment/app/models/__init__.py @@ -0,0 +1 @@ +from app.models.domain import * diff --git a/backend/services/payment/app/models/base.py b/backend/services/payment/app/models/base.py new file mode 100644 index 0000000..4110a1b --- /dev/null +++ b/backend/services/payment/app/models/base.py @@ -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)) diff --git a/backend/services/payment/app/models/domain.py b/backend/services/payment/app/models/domain.py new file mode 100644 index 0000000..0270cda --- /dev/null +++ b/backend/services/payment/app/models/domain.py @@ -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) diff --git a/backend/services/payment/app/models/foundation.py b/backend/services/payment/app/models/foundation.py new file mode 100644 index 0000000..2d56888 --- /dev/null +++ b/backend/services/payment/app/models/foundation.py @@ -0,0 +1,15 @@ +from app.models.domain import ( + CreditProviderRegistration, + OutboxEvent, + PaymentAuditLog, + PaymentBundleDefinition, + PaymentConfiguration, + PaymentFeatureToggle, + PaymentPermission, + PaymentProviderAssignment, + PaymentRole, + PaymentSetting, + PaymentWorkspace, + PspProviderRegistration, + TenantPaymentBundle, +) diff --git a/backend/services/payment/app/models/ledger.py b/backend/services/payment/app/models/ledger.py new file mode 100644 index 0000000..67c7f3b --- /dev/null +++ b/backend/services/payment/app/models/ledger.py @@ -0,0 +1,5 @@ +from app.models.domain import ( + PaymentLedgerEntry, + PaymentTransaction, + PaymentTransactionFeeLine, +) diff --git a/backend/services/payment/app/models/merchant.py b/backend/services/payment/app/models/merchant.py new file mode 100644 index 0000000..9423590 --- /dev/null +++ b/backend/services/payment/app/models/merchant.py @@ -0,0 +1,7 @@ +from app.models.domain import ( + MerchantAccount, + MerchantAccountProfile, + MerchantAccountStatusHistory, + MerchantComplianceRef, + MerchantSettlementProfile, +) diff --git a/backend/services/payment/app/models/psp.py b/backend/services/payment/app/models/psp.py new file mode 100644 index 0000000..bb5c585 --- /dev/null +++ b/backend/services/payment/app/models/psp.py @@ -0,0 +1,6 @@ +from app.models.domain import ( + PspConnection, + PspConnectionHealthCheck, + PspCredentialVaultRef, + PspRoutingPolicy, +) diff --git a/backend/services/payment/app/models/requests.py b/backend/services/payment/app/models/requests.py new file mode 100644 index 0000000..0f65ab0 --- /dev/null +++ b/backend/services/payment/app/models/requests.py @@ -0,0 +1,7 @@ +from app.models.domain import ( + PaymentCallbackLog, + PaymentRequest, + PaymentRequestLine, + PaymentRequestStatusHistory, + PaymentVerificationAttempt, +) diff --git a/backend/services/payment/app/models/types.py b/backend/services/payment/app/models/types.py new file mode 100644 index 0000000..9dd7912 --- /dev/null +++ b/backend/services/payment/app/models/types.py @@ -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)) diff --git a/backend/services/payment/app/permissions/__init__.py b/backend/services/payment/app/permissions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/payment/app/permissions/definitions.py b/backend/services/payment/app/permissions/definitions.py new file mode 100644 index 0000000..ec5ac8b --- /dev/null +++ b/backend/services/payment/app/permissions/definitions.py @@ -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.",) diff --git a/backend/services/payment/app/policies/__init__.py b/backend/services/payment/app/policies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/payment/app/providers/__init__.py b/backend/services/payment/app/providers/__init__.py new file mode 100644 index 0000000..6ce341f --- /dev/null +++ b/backend/services/payment/app/providers/__init__.py @@ -0,0 +1,2 @@ +from app.providers.contracts import PspAdapter, CreditProviderAdapter +from app.providers.mock import MockPspAdapter, ZarinpalAdapter, IdpayAdapter, ADAPTERS diff --git a/backend/services/payment/app/providers/contracts.py b/backend/services/payment/app/providers/contracts.py new file mode 100644 index 0000000..7f41e36 --- /dev/null +++ b/backend/services/payment/app/providers/contracts.py @@ -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: ... diff --git a/backend/services/payment/app/providers/mock.py b/backend/services/payment/app/providers/mock.py new file mode 100644 index 0000000..cfda2c5 --- /dev/null +++ b/backend/services/payment/app/providers/mock.py @@ -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()} diff --git a/backend/services/payment/app/queries/__init__.py b/backend/services/payment/app/queries/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/payment/app/queries/payment.py b/backend/services/payment/app/queries/payment.py new file mode 100644 index 0000000..90b50cb --- /dev/null +++ b/backend/services/payment/app/queries/payment.py @@ -0,0 +1,6 @@ +from dataclasses import dataclass +from uuid import UUID +@dataclass +class GetPaymentRequestQuery: + tenant_id: UUID + payment_request_id: UUID diff --git a/backend/services/payment/app/repositories/__init__.py b/backend/services/payment/app/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/payment/app/repositories/base.py b/backend/services/payment/app/repositories/base.py new file mode 100644 index 0000000..355964a --- /dev/null +++ b/backend/services/payment/app/repositories/base.py @@ -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() diff --git a/backend/services/payment/app/repositories/payment.py b/backend/services/payment/app/repositories/payment.py new file mode 100644 index 0000000..11d198d --- /dev/null +++ b/backend/services/payment/app/repositories/payment.py @@ -0,0 +1,2 @@ +from app.repositories.base import TenantBaseRepository +class PaymentRepository(TenantBaseRepository): pass diff --git a/backend/services/payment/app/schemas/__init__.py b/backend/services/payment/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/payment/app/services/__init__.py b/backend/services/payment/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/payment/app/specifications/__init__.py b/backend/services/payment/app/specifications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/payment/app/tests/__init__.py b/backend/services/payment/app/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/payment/app/tests/conftest.py b/backend/services/payment/app/tests/conftest.py new file mode 100644 index 0000000..92450e1 --- /dev/null +++ b/backend/services/payment/app/tests/conftest.py @@ -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 diff --git a/backend/services/payment/app/tests/test_api.py b/backend/services/payment/app/tests/test_api.py new file mode 100644 index 0000000..3d4debd --- /dev/null +++ b/backend/services/payment/app/tests/test_api.py @@ -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"] diff --git a/backend/services/payment/app/tests/test_architecture.py b/backend/services/payment/app/tests/test_architecture.py new file mode 100644 index 0000000..405b5cf --- /dev/null +++ b/backend/services/payment/app/tests/test_architecture.py @@ -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() diff --git a/backend/services/payment/app/tests/test_dependency.py b/backend/services/payment/app/tests/test_dependency.py new file mode 100644 index 0000000..804f016 --- /dev/null +++ b/backend/services/payment/app/tests/test_dependency.py @@ -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) diff --git a/backend/services/payment/app/tests/test_docs.py b/backend/services/payment/app/tests/test_docs.py new file mode 100644 index 0000000..c134a26 --- /dev/null +++ b/backend/services/payment/app/tests/test_docs.py @@ -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() diff --git a/backend/services/payment/app/tests/test_migration.py b/backend/services/payment/app/tests/test_migration.py new file mode 100644 index 0000000..299dc19 --- /dev/null +++ b/backend/services/payment/app/tests/test_migration.py @@ -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 diff --git a/backend/services/payment/app/tests/test_permissions.py b/backend/services/payment/app/tests/test_permissions.py new file mode 100644 index 0000000..86c2941 --- /dev/null +++ b/backend/services/payment/app/tests/test_permissions.py @@ -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 diff --git a/backend/services/payment/app/tests/test_phase_141.py b/backend/services/payment/app/tests/test_phase_141.py new file mode 100644 index 0000000..a12ebfb --- /dev/null +++ b/backend/services/payment/app/tests/test_phase_141.py @@ -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" diff --git a/backend/services/payment/app/tests/test_phase_142.py b/backend/services/payment/app/tests/test_phase_142.py new file mode 100644 index 0000000..b82cd45 --- /dev/null +++ b/backend/services/payment/app/tests/test_phase_142.py @@ -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" diff --git a/backend/services/payment/app/tests/test_phase_143.py b/backend/services/payment/app/tests/test_phase_143.py new file mode 100644 index 0000000..9f40379 --- /dev/null +++ b/backend/services/payment/app/tests/test_phase_143.py @@ -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" diff --git a/backend/services/payment/app/tests/test_phase_144.py b/backend/services/payment/app/tests/test_phase_144.py new file mode 100644 index 0000000..7e68a4a --- /dev/null +++ b/backend/services/payment/app/tests/test_phase_144.py @@ -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" diff --git a/backend/services/payment/app/tests/test_phase_145.py b/backend/services/payment/app/tests/test_phase_145.py new file mode 100644 index 0000000..6bfa44b --- /dev/null +++ b/backend/services/payment/app/tests/test_phase_145.py @@ -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 diff --git a/backend/services/payment/app/tests/test_security.py b/backend/services/payment/app/tests/test_security.py new file mode 100644 index 0000000..5c94264 --- /dev/null +++ b/backend/services/payment/app/tests/test_security.py @@ -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() diff --git a/backend/services/payment/app/tests/test_tenant_isolation.py b/backend/services/payment/app/tests/test_tenant_isolation.py new file mode 100644 index 0000000..7d62f2a --- /dev/null +++ b/backend/services/payment/app/tests/test_tenant_isolation.py @@ -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()==[] diff --git a/backend/services/payment/app/validators/__init__.py b/backend/services/payment/app/validators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/payment/pytest.ini b/backend/services/payment/pytest.ini new file mode 100644 index 0000000..efa3b55 --- /dev/null +++ b/backend/services/payment/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +asyncio_mode = auto diff --git a/backend/services/payment/requirements.txt b/backend/services/payment/requirements.txt new file mode 100644 index 0000000..bbdf395 --- /dev/null +++ b/backend/services/payment/requirements.txt @@ -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 diff --git a/backend/services/payment/scripts/__init__.py b/backend/services/payment/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/payment/scripts/ensure_db.py b/backend/services/payment/scripts/ensure_db.py new file mode 100644 index 0000000..acab35b --- /dev/null +++ b/backend/services/payment/scripts/ensure_db.py @@ -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() diff --git a/docker-compose.yml b/docker-compose.yml index 16da043..c0c9344 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -417,6 +417,36 @@ services: networks: - 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: build: context: . @@ -499,6 +529,7 @@ services: 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_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_REALM: ${NEXT_PUBLIC_KEYCLOAK_REALM:-superapp} 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} NEXT_PUBLIC_DELIVERY_API_URL: ${NEXT_PUBLIC_DELIVERY_API_URL:-http://localhost: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} 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} diff --git a/docs/ai-framework/phase-manifest.yaml b/docs/ai-framework/phase-manifest.yaml index 2f63279..3489d27 100644 --- a/docs/ai-framework/phase-manifest.yaml +++ b/docs/ai-framework/phase-manifest.yaml @@ -5,7 +5,9 @@ # Agents must keep this file aligned with docs/progress.md and docs/roadmap.md. 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-Enterprise complete: Enterprise Development Framework upgrade (ADR-018). # 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 - 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.0–11.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.0–11.10 remain unmodified + + # --- Experience Frontend (Torbat Pages) — FE-11.0–11.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 --- - id: hospitality-12.0 name: Hospitality Platform Foundation @@ -1451,6 +1607,283 @@ phases: completion_criteria: - Full quality gate suite green across all Hospitality phases + # --- Payment Platform (Phase 14.0–14.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.0–payment-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.0–payment-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.0–14.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 - id: restaurant-foundation name: Restaurant / Cafe Foundation (superseded) diff --git a/docs/ai-framework/project-index.yaml b/docs/ai-framework/project-index.yaml index 9bfaf35..a737f95 100644 --- a/docs/ai-framework/project-index.yaml +++ b/docs/ai-framework/project-index.yaml @@ -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 # 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 -updated: "2026-07-26" +updated: "2026-07-27" execution_order: + - docs/project-status.yaml + - docs/project-status.md - docs/ai-framework/project-index.yaml - docs/ai-framework/runtime-read-policy.md - docs/ai-framework/context-cache-policy.md @@ -17,6 +20,8 @@ execution_order: - additional_documents_per_runtime_read_policy shared_references: + project_status: docs/project-status.yaml + project_status_human: docs/project-status.md phase_manifest: docs/ai-framework/phase-manifest.yaml service_manifest: docs/ai-framework/service-manifest.yaml module_registry: docs/module-registry.md @@ -208,7 +213,7 @@ services: next_phase: null snapshot_file: docs/service-snapshots/experience.yaml 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 service_manifest_reference: docs/ai-framework/service-manifest.yaml module_registry_reference: docs/module-registry.md @@ -220,6 +225,7 @@ services: capability_prefix: experience.* health_endpoint: /health metrics_endpoint: /metrics + notes: Backend 11.0–11.10 complete. Frontend FE-11.0–11.5 complete (stop). FE-11.6+ not started. Architecture COMPLETE (ADR-022). - service_name: Hospitality Platform commercial_product: Torbat Food @@ -595,6 +601,28 @@ services: health_endpoint: /health 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 commercial_product: TorbatYar UI service_identifier: frontend @@ -622,3 +650,8 @@ framework_workstreams: description: AI Development Framework (docs-only) project_index_handover: docs/phase-handover/phase-af-project-index.md 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 diff --git a/docs/ai-framework/service-manifest.yaml b/docs/ai-framework/service-manifest.yaml index 5726cc5..26d665a 100644 --- a/docs/ai-framework/service-manifest.yaml +++ b/docs/ai-framework/service-manifest.yaml @@ -4,7 +4,7 @@ # Keep aligned with docs/module-registry.md and runtime compose ports when wired. schema_version: 1 -updated: "2026-07-25" +updated: "2026-07-27" services: - id: core-platform @@ -518,6 +518,16 @@ services: - docs/experience-phase-11-10-audit.md - docs/service-snapshots/experience.yaml - 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 current_version: 0.11.10.0 current_phase: experience-11.10 @@ -525,6 +535,7 @@ services: status: active api_port: 8008 commercial_product: Torbat Pages + notes: Architecture COMPLETE (ADR-022 + Actions/Access/Embed). Publish Target Registry / Action / Access / Embed engines not implemented. future_modules: - sites - pages @@ -726,6 +737,100 @@ services: status: superseded 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 name: Ecommerce owner: TBD diff --git a/docs/architecture/adr/ADR-020.md b/docs/architecture/adr/ADR-020.md new file mode 100644 index 0000000..a886b88 --- /dev/null +++ b/docs/architecture/adr/ADR-020.md @@ -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) diff --git a/docs/architecture/adr/ADR-021.md b/docs/architecture/adr/ADR-021.md new file mode 100644 index 0000000..9f78150 --- /dev/null +++ b/docs/architecture/adr/ADR-021.md @@ -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) diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md index 11571db..8645801 100644 --- a/docs/architecture/adr/README.md +++ b/docs/architecture/adr/README.md @@ -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-018](ADR-018.md) | Enterprise Completeness Mandate for the AI Development Framework | 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) diff --git a/docs/architecture/architecture.md b/docs/architecture/architecture.md index 12c1fee..7d2a7ac 100644 --- a/docs/architecture/architecture.md +++ b/docs/architecture/architecture.md @@ -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) | | [ai-architecture.md](ai-architecture.md) | AI independence rules | | [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 | ## 5. Non-Goals of This Document diff --git a/docs/architecture/authorization-architecture.md b/docs/architecture/authorization-architecture.md index 4d012b0..1fae0f3 100644 --- a/docs/architecture/authorization-architecture.md +++ b/docs/architecture/authorization-architecture.md @@ -26,11 +26,13 @@ Source of truth: `core_platform_db.tenant_memberships` ([ADR-007](adr/ADR-007.md ## Entitlement 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` 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) Configurable `AUTH_REQUIRED`. Dependencies such as `require_authenticated`, `require_platform_admin`, `require_tenant_admin`, `MembershipService.ensure_role`. diff --git a/docs/architecture/integration-architecture.md b/docs/architecture/integration-architecture.md index ef873e0..df9b797 100644 --- a/docs/architecture/integration-architecture.md +++ b/docs/architecture/integration-architecture.md @@ -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. +## 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 - [Event-Driven Architecture](event-driven-architecture.md) +- [Published Resource Architecture](published-resource-architecture.md) - [Provider Registry](../provider-registry.md) - [Provider Reference](../reference/provider-reference.md) - [Services Contracts](../reference/services-contracts.md) +- [Published Resource Contracts](../reference/published-resource-contracts.md) diff --git a/docs/architecture/module-boundaries.md b/docs/architecture/module-boundaries.md index 918b443..d04d34d 100644 --- a/docs/architecture/module-boundaries.md +++ b/docs/architecture/module-boundaries.md @@ -56,15 +56,21 @@ Each module (Accounting, CRM, Restaurant, Ecommerce, …) owns its database and ### 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 **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`) @@ -79,11 +85,15 @@ Each module (Accounting, CRM, Restaurant, Ecommerce, …) owns its database and 3. Feature gates use Core entitlement API. 4. Frontend talks to public/versioned APIs only. 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 - [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) - [Sports Center Roadmap](../sports-center-roadmap.md) - [Delivery Roadmap](../delivery-roadmap.md) diff --git a/docs/glossary.md b/docs/glossary.md index 000e5a3..82f16e6 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -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. | | **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. | +| **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)). | ## Hospitality Platform diff --git a/docs/module-registry.md b/docs/module-registry.md index e6fac6b..7188b0a 100644 --- a/docs/module-registry.md +++ b/docs/module-registry.md @@ -434,7 +434,7 @@ Status legend: **Active** · **Scaffolded** (code placeholder) · **Planned** | 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 | | 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 | | Internal Dependencies | Modules owned by this service only | | 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) | | Provider Dependencies | Optional theme/component marketplace adapters (planned) | | 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 | | Version | 0.11.10.0 | | 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.0–14.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.0–payment-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 | Field | Value | @@ -1074,4 +1153,6 @@ Status legend: **Active** · **Scaffolded** (code placeholder) · **Planned** - [Experience Roadmap](experience-roadmap.md) - [Healthcare Roadmap](healthcare-roadmap.md) - [Beauty Business Roadmap](beauty-business-roadmap.md) +- [Payment Roadmap](payment-roadmap.md) - [ADR-016](architecture/adr/ADR-016.md) +- [ADR-020](architecture/adr/ADR-020.md) diff --git a/docs/next-steps.md b/docs/next-steps.md index f548f2d..c1f81a9 100644 --- a/docs/next-steps.md +++ b/docs/next-steps.md @@ -2,9 +2,25 @@ > 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.6–14.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 @@ -12,6 +28,7 @@ Pick one track per team capacity (see [roadmap.md](roadmap.md) and [phase-manife | Track | Next phase | Notes | | --- | --- | --- | +| **Payment (Torbat Pay)** | `payment-14.6` Refunds & Reversals | MVP 14.0–14.5 complete; LATER priority | | Delivery | `delivery-10.2` Fleet & Vehicle Types | After Driver Management 10.1 | | Loyalty | `loyalty-7.7` Gift Card Platform | After Wallet 7.6 | | Hospitality | `hospitality-12.9` AI Assistant | After Analytics 12.8 | diff --git a/docs/payment-phase-pay-arch.md b/docs/payment-phase-pay-arch.md new file mode 100644 index 0000000..b05dbfc --- /dev/null +++ b/docs/payment-phase-pay-arch.md @@ -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) diff --git a/docs/payment-phase-pay-reg.md b/docs/payment-phase-pay-reg.md new file mode 100644 index 0000000..3ec14b2 --- /dev/null +++ b/docs/payment-phase-pay-reg.md @@ -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.0–14.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.0–14.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) diff --git a/docs/payment-roadmap.md b/docs/payment-roadmap.md new file mode 100644 index 0000000..1482614 --- /dev/null +++ b/docs/payment-roadmap.md @@ -0,0 +1,1127 @@ +# Payment Platform — Enterprise Roadmap (Torbat Pay) + +> Platform roadmap. Implementation proceeds phase-by-phase under the AI Framework. +> Framework: [ai-framework/](ai-framework/README.md) · ADR: [ADR-020](architecture/adr/ADR-020.md) · Manifests: [phase-manifest.yaml](ai-framework/phase-manifest.yaml), [service-manifest.yaml](ai-framework/service-manifest.yaml) + +| Field | Value | +| --- | --- | +| Service | `payment` | +| Commercial Product | Torbat Pay | +| Database | `payment_db` | +| Permission Prefix | `payment.*` | +| API Port (reserved) | **8012** | +| Phases | **14.0 – 14.10** (manifest IDs: `payment-14.0` … `payment-14.10`) | +| Status | Pay-Reg complete; **payment-arch** complete; **payment-14.0–14.5** MVP implemented (v0.14.5.0); next `payment-14.6` Refunds | +| Architecture | [ADR-020](architecture/adr/ADR-020.md), [ADR-021](architecture/adr/ADR-021.md), [payment-contracts.md](reference/payment-contracts.md) | + +## Phase Numbering Disambiguation + +| Track | Manifest ID pattern | Example | +| --- | --- | --- | +| **Payment Platform** | `payment-14.x` | `payment-14.0` Payment Foundation | +| Beauty Business | `beauty-business-14.x` | `beauty-business-14.0` Beauty Foundation | + +Both use numeric **14.x** in documentation tables; manifest IDs are always prefixed and never collide. + +--- + +## Vision + +Deliver an independent, multi-tenant **Enterprise Payment Platform** inside TorbatYar so every current and future service can accept, verify, refund, and reconcile payments without owning PSP credentials, callback endpoints, or payment transaction ledgers — and without duplicating business logic or sharing databases. + +Torbat Pay supports: + +- **Mode A — BYO-PSP:** Tenant connects and operates their own Payment Service Provider credentials. +- **Mode B — Torbat Pay Merchant:** Tenant uses Torbat Pay as payment facilitator; platform manages sub-merchant onboarding shells and settlement routing (implementation phased; architecture ready from registration). + +Initial PSP catalog targets **Iranian gateways** (ZarinPal, IDPay, NextPay, Pay.ir, Mellat, Parsian, Saman, …). **International PSPs are out of scope for implementation now** but adapter contracts must accept `region`, `currency`, and `provider_family` metadata for future expansion. + +--- + +## Business Goals + +1. Single payment API for all TorbatYar verticals and future services. +2. Tenant choice between own PSP and Torbat Pay Merchant facilitator mode. +3. Idempotent payment requests, verifiable callbacks, immutable transaction ledger. +4. Settlement and accounting handoff via Accounting Posting Engine only. +5. PCI-aware credential storage patterns (secrets outside logs; vault refs in DB). +6. Future-ready provider registry without rewriting vertical integrations. + +--- + +## Architecture Scope + +| Rule | Reference | +| --- | --- | +| Independent service + `payment_db` | [ADR-020](architecture/adr/ADR-020.md), [ADR-001](architecture/adr/ADR-001.md) | +| Layering API → Service → Repository → Model | [service-architecture.md](architecture/service-architecture.md) | +| Tenant-aware business tables | [ADR-003](architecture/adr/ADR-003.md) | +| Outbox-ready events `payment.*` | [ADR-006](architecture/adr/ADR-006.md) | +| No journal ownership | [ADR-010](architecture/adr/ADR-010.md) | +| Receipt/notify via Communication only | [ADR-012](architecture/adr/ADR-012.md) | +| Vertical checkout context stays in vertical | [boundary-rules.md](ai-framework/boundary-rules.md) | +| Implementation via AI Framework | [ai-framework/](ai-framework/README.md) | +| Licensing & contracts | [ADR-021](architecture/adr/ADR-021.md), [payment-contracts.md](reference/payment-contracts.md) | + +--- + +## Architectural prerequisites (complete before 14.0 code) + +Documented in **payment-arch** phase. Implementation phases MUST NOT redefine these contracts without a new ADR and contract major version. + +### Three-layer access control + +| Layer | Owner | Purpose | +| --- | --- | --- | +| L1 Core entitlement | Core | `payment.module.enabled` — tenant may use Torbat Pay | +| L2 Payment bundle | Payment | Commercial packs: BYO-PSP, Torbat Pay Merchant, splits, refunds, … | +| L3 Feature toggle | Payment | Operational flags: refunds, installments, subscriptions, wallet top-up | + +### Tenant independence guarantees + +| Action | Mechanism | Code change required | +| --- | --- | --- | +| Enable Payment | Core L1 + workspace `enabled` + active L2 bundle | No | +| Disable Payment | workspace `disabled` | No | +| Choose BYO-PSP | `default_payment_mode=byo_psp` + `PspConnection` + assignment | No | +| Choose Torbat Pay Merchant | `default_payment_mode=torbat_pay_merchant` + `MerchantAccount` | No | +| Change PSP / routing | Update `PaymentProviderAssignment` + `PspRoutingPolicy` | No | + +### Stable v1 contracts (see [payment-contracts.md](reference/payment-contracts.md)) + +PaymentIntent · CheckoutSession · CallbackIngress · SettlementIntent · RefundIntent (reserved) · SplitAllocation (reserved) · SubscriptionBilling (reserved) · WalletTopUp (reserved) · InstallmentPlan (Payment schedule shell) · CreditProvider (reserved) + +### Torbat Credit — reserved independent service + +| Field | Value | +| --- | --- | +| Service | `credit` (future) | +| Commercial product | **Torbat Credit** | +| Database | `credit_db` (reserved) | +| Event namespace | `credit.*` (reserved — **not** published by Payment) | + +Payment recognizes **external Credit Providers** via the same adapter registry as PSPs. Future payment source **`CREDIT_PROVIDER`** is reserved in routing policy and PaymentIntent v1 — **not implemented** in HIGH-priority phases. + +**Boundary:** `InstallmentPlanContract` inside Payment = schedule shell linked to captures. Installment calculation, scoring, KYC, BNPL, and financing are **exclusive to Torbat Credit**. + +Architectural reference fields (opaque): `credit_provider_id`, `financing_reference`, `credit_authorization_reference`, `installment_contract_reference`. + +See [ADR-021 §12](architecture/adr/ADR-021.md) and [payment-contracts.md](reference/payment-contracts.md). + +### Platform compatibility (verified — no breaking changes in existing services) + +| Service | Status | Integration pattern | +| --- | --- | --- | +| Core | ✅ | Entitlement API + `feature_access.changed` | +| Identity | ✅ | `user_ref` / payer refs only | +| Tenant (Core) | ✅ | `tenant_id` + workspace lifecycle | +| Accounting | ✅ | SettlementIntent events only ([ADR-010](architecture/adr/ADR-010.md)) | +| CRM | ✅ | `contact_ref` on payer | +| Communication | ✅ | Receipt/refund notify client | +| Hospitality | ✅ | POS/QR refs → PaymentIntent; subscribe `payment.request.paid` | +| Delivery | ✅ | Optional COD payment ref | +| Sports Center | ✅ | Membership payment refs | +| Marketplace | ✅ | Checkout + SplitAllocation (14.7+) | +| Torbat Credit (future) | ✅ | CreditProviderAdapter; Payment calls like PSP; `credit.*` owned by Credit service | + +### Architecture verification + +| Principle | Verified | +| --- | --- | +| API-first | ✅ REST + internal token APIs | +| Event-first | ✅ `payment.*` outbox | +| Database-per-service | ✅ `payment_db` sole owner | +| Feature toggles | ✅ L3 in Payment | +| Permission tree | ✅ Bundle-gated `payment.*` | +| Bundle isolation | ✅ Hidden routes/capabilities when inactive | +| Multi-tenant isolation | ✅ `tenant_id` + callback routing | + +--- + +## Module Map + +| Module | Responsibility | Phase | +| --- | --- | --- | +| Foundation / Workspace | Tenant payment workspace, enable/disable, mode flags | 14.0 | +| Bundle Licensing | PaymentBundleDefinition, TenantPaymentBundle (L2) | 14.0 | +| Feature Toggles | PaymentFeatureToggle (L3) | 14.0 | +| Provider Assignment | PaymentProviderAssignment shells | 14.0 / 14.1 | +| PSP Registry | Provider catalog, adapter protocol, capability flags | 14.0 / 14.1 | +| Credit Provider Registry | External credit adapter catalog (Torbat Credit reserved) | 14.0 shell / future | +| PSP Connections | Tenant BYO-PSP credentials, health, routing priority | 14.1 | +| Merchant Accounts | Sub-merchant / facilitator onboarding shells | 14.2 | +| Payment Requests | Initiate, idempotency, redirect/token payloads | 14.3 | +| Callbacks & Verification | PSP callbacks, signature verify, status reconcile | 14.4 | +| Transaction Ledger | Immutable payment transaction entries | 14.5 | +| Refunds & Reversals | Partial/full refund lifecycle | 14.6 | +| Split & Settlement | Multi-party splits; facilitator settlement intents | 14.7 | +| Vertical Connectors | Hospitality/Marketplace/Sports/Experience checkout contracts | 14.8 | +| Reconciliation | PSP vs ledger; Accounting posting intents | 14.9 | +| Analytics & Validation | Dashboards shells, fraud hooks, enterprise gates | 14.10 | + +Canonical inventory: [module-registry.md](module-registry.md#payment). + +--- + +## Integration Map + +``` +Verticals (Hospitality POS / Marketplace / Sports Center / Experience / CRM / …) + │ + │ API / Events (checkout refs only — no shared DB) + ▼ + Payment Platform (Torbat Pay) + │ + ├──Adapters──▶ PSPs (ZarinPal, IDPay, NextPay, Pay.ir, Mellat, …) + ├──API/Events──▶ Accounting (settlement / reconciliation intents → Posting Engine) + ├──API/Events──▶ Communication (payment receipt / failure notify) + ├──API─────────▶ Core (entitlement, tenant) + └──API/Events──▶ CRM / Loyalty (optional customer refs — no ledger duplication) +``` + +**Forbidden:** cross-DB queries; verticals calling PSP APIs directly; Payment creating JournalEntry; Payment owning Hospitality order or Marketplace cart aggregates. + +--- + +## Consumer Services (current and future) + +| Service | Payment usage | +| --- | --- | +| Accounting | Consumes settlement intents; posts journals | +| Hospitality | POS Pro payments, QR ordering checkout | +| Marketplace / Ecommerce | Checkout capture, multi-vendor splits (later) | +| Sports Center | Membership/competition fees | +| Delivery | COD collection refs (optional) | +| Experience | Paid forms, appointment deposits | +| CRM | Quote-to-pay links (refs) | +| Loyalty | Paid wallet top-up via Payment (integration contract) | +| Healthcare / Beauty | Appointment/service prepayment | +| Automation | Trigger on `payment.transaction.paid` | +| Future services | Same Payment API — no re-analysis required | + +--- + +## Phase Map + +| Phase | ID | Name | Implementation Priority | Status | +| --- | --- | --- | --- | --- | +| Reg | `payment-reg` | Platform Registration | — | Complete | +| Arch | `payment-arch` | Architecture Prerequisites | — | Complete | +| 14.0 | `payment-14.0` | Payment Foundation | **HIGH** | Complete | +| 14.1 | `payment-14.1` | PSP Management | **HIGH** | Complete | +| 14.2 | `payment-14.2` | Merchant Accounts | **HIGH** | Complete | +| 14.3 | `payment-14.3` | Payment Requests | **HIGH** | Complete | +| 14.4 | `payment-14.4` | Callback & Verification | **HIGH** | Complete | +| 14.5 | `payment-14.5` | Transaction Ledger | **HIGH** | Complete | +| 14.6 | `payment-14.6` | Refunds & Reversals | LATER | Planned | +| 14.7 | `payment-14.7` | Split Payments & Facilitator Settlement | LATER | Planned | +| 14.8 | `payment-14.8` | Vertical Connectors & Checkout Contracts | LATER | Planned | +| 14.9 | `payment-14.9` | Reconciliation & Accounting Integration | LATER | Planned | +| 14.10 | `payment-14.10` | Analytics, Fraud Shells & Enterprise Validation | LATER | Planned | + +Phase detail documents: [phases/Payment/](phases/Payment/README.md). + +--- + +# Phase Specifications + +--- + +## Phase 14.0 — Payment Foundation + +| Field | Value | +| --- | --- | +| Phase ID | `payment-14.0` | +| Implementation Priority | **HIGH** | + +### Purpose + +Establish Torbat Pay as an independent enterprise microservice with foundation aggregates, health/capabilities/metrics, permission tree, publish-only event shells, tenant isolation, audit, and **provider protocol stubs only** — no live PSP calls. + +### Business Scope + +- Register payment workspace per tenant. +- Expose capability discovery for upcoming PSP, merchant, and checkout features. +- Document BYO-PSP vs Torbat Pay Merchant mode flags at configuration level (no facilitator onboarding yet). + +### Technical Scope + +- Service scaffold `backend/services/payment` (API → Service → Repository → Model). +- Foundation aggregates: PaymentWorkspace, PaymentRole, PaymentPermission, PaymentBundleDefinition, TenantPaymentBundle, PaymentFeatureToggle, PaymentProviderAssignment (shell), PspProviderRegistration (catalog shell), PaymentConfiguration, PaymentSetting, PaymentAuditLog, OutboxEvent. +- L1 gate: Core `payment.module.enabled` check client (stub in 14.0). +- Endpoints: `/health`, `/capabilities`, `/metrics`, foundation CRUD under `/api/v1/*`. +- Alembic `0001_initial`; compose port **8012**. +- Provider Protocol interfaces: `PspAdapter`, `AccountingClient`, `CommunicationClient`, `CrmClient` (stubs/mock). + +### Dependencies + +- `payment-reg`, `onboarding-4`, `ai-framework` +- Core entitlement (tenant context) + +### Out of Scope + +- Live PSP credentials (14.1) +- Merchant onboarding (14.2) +- Payment initiation/capture (14.3–14.5) +- Refunds, splits, connectors, reconciliation, analytics + +### Capabilities (flags) + +`payment.foundation`, `payment.workspace`, `payment.bundles`, `payment.feature_toggles`, `payment.provider_assignment_shell`, `payment.psp_registry_shell`, `payment.audit` + +### Permissions + +`payment.*` root; `payment.workspaces.{view,manage,enable,disable}`, `payment.bundles.*`, `payment.feature_toggles.*`, `payment.provider_assignments.*`, `payment.configurations.*`, `payment.settings.*`, `payment.psp_providers.view`, `payment.audit.view`, planned leaves per [payment-contracts.md](reference/payment-contracts.md). + +### Events + +`payment.workspace.created`, `payment.configuration.updated`, `payment.setting.upserted`, `payment.psp_provider.registered` (catalog shell) + +### API Contracts + +| Resource | Method / Path | +| --- | --- | +| Workspaces | CRUD `/api/v1/payment-workspaces` (+ enable/disable actions) | +| Bundle definitions | CRUD `/api/v1/bundle-definitions` | +| Tenant bundles | CRUD `/api/v1/tenant-bundles` | +| Feature toggles | CRUD `/api/v1/feature-toggles` | +| Provider assignments | CRUD `/api/v1/provider-assignments` (shell) | +| PSP provider catalog | CRUD `/api/v1/psp-provider-registrations` | +| Configurations | CRUD `/api/v1/configurations` | +| Settings | CRUD `/api/v1/settings` | +| Audit | GET `/api/v1/audit` | +| Discovery | GET `/health`, `/capabilities`, `/metrics` | + +### Data Ownership + +Payment sole owner: workspace, roles, permission catalog, provider registration metadata, configuration, settings, audit, outbox. + +### Provider Ownership + +Adapter **interfaces** owned by Payment; no vendor SDK in 14.0. + +### Integration Rules + +- No cross-DB access. +- Verticals must not import payment models. +- Accounting/Communication accessed via client protocols only. + +### Quality Gates + +Architecture, tenant isolation, permissions, migration, dependency, security, documentation validation per [quality-gates.md](ai-framework/quality-gates.md). + +### Definition of Done + +- Service boots on 8012; tests green; manifests/snapshot/handover updated; no PSP payment flow. + +### Future Compatibility + +- `PspProviderRegistration` includes `region`, `currency_codes[]`, `provider_family`, `supports_facilitator_mode` for international expansion. +- Workspace stores `default_payment_mode`: `byo_psp` | `torbat_pay_merchant`. + +--- + +## Phase 14.1 — PSP Management + +| Field | Value | +| --- | --- | +| Phase ID | `payment-14.1` | +| Implementation Priority | **HIGH** | + +### Purpose + +Enable tenants to connect **BYO-PSP** credentials, register adapter implementations, and route payments by priority with health checks — Iranian PSPs first (mock + at least one real adapter stub). + +### Business Scope + +- Tenant admin connects ZarinPal / IDPay / NextPay / Pay.ir / Mellat / Parsian / Saman credentials. +- Test connection and mark PSP connection active/suspended. +- Failover priority list per tenant (primary/secondary PSP). + +### Technical Scope + +- Aggregates: PspConnection, PspCredentialVaultRef, PspConnectionHealthCheck, PspRoutingPolicy. +- Adapter registry with `MockPspAdapter` + stub adapters per provider family. +- Encrypt/store credential refs (no plaintext secrets in API responses). +- APIs: connect, test, rotate credential ref, suspend, set routing priority. + +### Dependencies + +- `payment-14.0` + +### Out of Scope + +- Torbat Pay Merchant sub-accounts (14.2) +- Payment capture (14.3) +- Callback handling (14.4) +- International PSP live integrations + +### 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`, `payment.psp_connection.tested`, `payment.psp_routing.updated` + +### API Contracts + +| Resource | Path | +| --- | --- | +| PSP connections | `/api/v1/psp-connections` | +| Routing policies | `/api/v1/psp-routing-policies` | +| Health checks | POST `/api/v1/psp-connections/{id}/test` | + +### Data Ownership + +Payment owns connection metadata, routing policies, health check history; vault refs point to secret store — not Core DB. + +### Provider Ownership + +PSP vendor SDKs live in Payment adapter layer only; verticals forbidden. + +### Integration Rules + +- Credential rotation must not orphan in-flight payment requests (document migration policy). +- Adapter protocol versioned (`adapter_version`). + +### Quality Gates + ++ secret redaction tests, tenant isolation, adapter contract tests. + +### Definition of Done + +- Tenant can register/test/suspend PSP connection; routing policy persisted; events published; no payment capture yet. + +### Future Compatibility + +- `region` + `currency` on connections; adapter slot for Stripe/PayPal without schema break. + +--- + +## Phase 14.2 — Merchant Accounts + +| Field | Value | +| --- | --- | +| Phase ID | `payment-14.2` | +| Implementation Priority | **HIGH** | + +### Purpose + +Introduce **Torbat Pay Merchant** (facilitator) sub-merchant account shells so tenants can accept payments through platform facilitator mode alongside BYO-PSP. + +### Business Scope + +- Tenant onboarding as Torbat Pay sub-merchant (KYC/KYB **refs only** — no document storage binaries). +- Merchant account lifecycle: draft → pending_review → active → suspended → closed. +- Link merchant account to workspace; optional branch/vertical refs. + +### Technical Scope + +- Aggregates: MerchantAccount, MerchantAccountProfile, MerchantSettlementProfile, MerchantComplianceRef, MerchantAccountStatusHistory. +- Facilitator configuration at platform level (PaymentSetting keys). +- APIs for create/submit/activate/suspend merchant account. + +### Dependencies + +- `payment-14.1` + +### Out of Scope + +- Live KYC vendor integration +- Actual fund settlement to bank accounts (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 + +| Resource | Path | +| --- | --- | +| Merchant accounts | `/api/v1/merchant-accounts` | +| Settlement profiles | `/api/v1/merchant-settlement-profiles` | +| Status actions | POST `.../submit`, `.../activate`, `.../suspend` | + +### Data Ownership + +Payment owns merchant account domain; Identity owns users; File Storage owns compliance document blobs (refs only). + +### Provider Ownership + +Facilitator PSP credentials are platform-scoped Payment settings — not tenant BYO connections. + +### Integration Rules + +- Merchant account required when `default_payment_mode=torbat_pay_merchant`. +- BYO-PSP mode bypasses merchant account for capture routing. + +### Quality Gates + +Lifecycle transition tests, forbidden plaintext PII in logs, tenant isolation. + +### Definition of Done + +- Merchant account CRUD + lifecycle; facilitator flag on workspace; no payment requests yet. + +### Future Compatibility + +- Multi-currency settlement profile; international merchant descriptors. + +--- + +## Phase 14.3 — Payment Requests + +| Field | Value | +| --- | --- | +| Phase ID | `payment-14.3` | +| Implementation Priority | **HIGH** | + +### Purpose + +Allow any vertical to create **idempotent payment requests** with amount, currency (IRR first), payer refs, and commercial context refs — returning redirect URL / token / QR payload from selected PSP. + +### Business Scope + +- Initiate payment for order/invoice/membership/checkout session refs. +- Support BYO-PSP routing (14.1) or facilitator merchant (14.2). +- Expire abandoned requests; single active capture per idempotency key. + +### Technical Scope + +- Aggregates: PaymentRequest, PaymentRequestLine (optional), PaymentRequestStatusHistory. +- Idempotency-Key header enforcement. +- State machine: draft → pending → redirect_issued → processing → (terminal: paid|failed|expired|cancelled). +- Adapter call: `initiate_payment()` only; no local ledger mutation beyond request state. + +### Dependencies + +- `payment-14.2`, active PSP connection or merchant account + +### Out of Scope + +- Callback verification (14.4) +- Immutable transaction ledger (14.5) +- Refunds (14.6) + +### Capabilities + +`payment.requests`, `payment.initiate`, `payment.idempotency` + +### Permissions + +`payment.requests.view|create|cancel|manage`; service-to-service `payment.requests.create` (internal) + +### Events + +`payment.request.created|redirect_issued|expired|cancelled|failed` + +### API Contracts + +| Resource | Path | +| --- | --- | +| Payment requests | POST/GET `/api/v1/payment-requests` | +| Cancel | POST `/api/v1/payment-requests/{id}/cancel` | +| Internal initiate | POST `/internal/v1/payment-requests` (token-gated) | + +Payload includes: `amount_minor`, `currency`, `idempotency_key`, `source_service`, `source_ref_type`, `source_ref_id`, `payer_contact_ref`, `metadata`, `return_url`, `callback_url`. + +### Data Ownership + +Payment owns request lifecycle; vertical owns commercial aggregate (order/ticket/invoice). + +### Provider Ownership + +PSP initiate APIs invoked only inside Payment adapters. + +### Integration Rules + +- Vertical passes refs only — never PSP credentials. +- Duplicate idempotency key returns same request (200/409 policy documented). + +### Quality Gates + +Idempotency tests, amount validation, tenant isolation, state machine guards. + +### Definition of Done + +- End-to-end initiate with mock adapter; redirect payload returned; events emitted; ledger not final until 14.4/14.5. + +### Future Compatibility + +- Multi-currency amounts; tokenized card flows (metadata slots); wallet debit refs. + +--- + +## Phase 14.4 — Callback & Verification + +| Field | Value | +| --- | --- | +| Phase ID | `payment-14.4` | +| Implementation Priority | **HIGH** | + +### Purpose + +Receive PSP callbacks/webhooks, verify signatures, reconcile status with PSP verify API, and transition payment requests to verified paid/failed states. + +### Business Scope + +- Trusted callback ingestion per PSP adapter. +- Manual reconcile/admin retry for stuck requests. +- Publish domain events for verticals to mark orders paid. + +### Technical Scope + +- Aggregates: PaymentCallbackLog (append-only), PaymentVerificationAttempt. +- Public callback routes: `/api/v1/callbacks/{provider_code}/{connection_id}` (design per adapter). +- Signature verification, replay protection (nonce/timestamp window), idempotent callback processing. +- Adapter `verify_payment()` + `parse_callback()`. + +### Dependencies + +- `payment-14.3` + +### Out of Scope + +- Immutable financial ledger entries (14.5) — may dual-write transition hooks but ledger is 14.5 authority +- Refunds +- Accounting posting + +### Capabilities + +`payment.callbacks`, `payment.verification`, `payment.webhooks` + +### Permissions + +`payment.callbacks.view` (admin), `payment.requests.reconcile|manage`; callbacks unauthenticated with signature gate + +### Events + +`payment.callback.received|verified|rejected`, `payment.request.paid|failed` + +### API Contracts + +| Resource | Path | +| --- | --- | +| PSP callbacks | POST `/api/v1/callbacks/{provider_code}/{connection_id}` | +| Verify retry | POST `/api/v1/payment-requests/{id}/verify` | +| Callback logs | GET `/api/v1/payment-callback-logs` | + +### Data Ownership + +Payment owns callback logs and verification attempts; PSP owns authoritative transaction ids (stored as refs). + +### Provider Ownership + +Webhook secrets validated in Payment adapter layer. + +### Integration Rules + +- Callback handlers must be fast — async verify via worker allowed. +- Never trust client-side return URL alone; always verify with PSP. + +### Quality Gates + +Signature forgery tests, replay tests, tenant routing isolation. + +### Definition of Done + +- Mock callback → verified paid flow; events to outbox; vertical can subscribe. + +### Future Compatibility + +- Multiple callback versions per PSP; international webhook formats. + +--- + +## Phase 14.5 — Transaction Ledger + +| Field | Value | +| --- | --- | +| Phase ID | `payment-14.5` | +| Implementation Priority | **HIGH** | + +### Purpose + +Maintain an **immutable append-only payment transaction ledger** as the system of record for captured payments — distinct from Accounting journals. + +### Business Scope + +- Record paid transactions with PSP refs, fees, net amounts, merchant account linkage. +- Query ledger for tenant admin and service-to-service status checks. +- Support facilitator fee lines (metadata) without Accounting posting. + +### Technical Scope + +- Aggregates: PaymentTransaction (immutable after insert), PaymentTransactionFeeLine, PaymentLedgerEntry (append-only). +- No UPDATE on monetary fields post-insert; corrections via reversal entries (14.6). +- Link transaction 1:1 (or 1:n partial) to PaymentRequest. + +### Dependencies + +- `payment-14.4` + +### Out of Scope + +- Refund entries (14.6) +- Accounting journal creation +- Split allocation (14.7) + +### Capabilities + +`payment.ledger`, `payment.transactions` + +### Permissions + +`payment.transactions.view|export|manage` (manage = admin tools only, not mutate amounts) + +### Events + +`payment.transaction.recorded`, `payment.ledger.entry_appended` + +### API Contracts + +| Resource | Path | +| --- | --- | +| Transactions | GET `/api/v1/payment-transactions`, GET `/{id}` | +| By source ref | GET `/api/v1/payment-transactions/by-source` | +| Ledger entries | GET `/api/v1/payment-ledger-entries` | + +### Data Ownership + +Payment owns payment-domain ledger; Accounting owns GL journals. + +### Provider Ownership + +PSP transaction ids stored as external refs only. + +### Integration Rules + +- Verticals query Payment for payment status — not vice versa polling vertical DBs. +- Ledger write occurs only after verified paid (14.4). + +### Quality Gates + +Immutability tests, append-only constraints, tenant isolation, correlation id traceability. + +### Definition of Done + +- Paid flow produces ledger entries; queries work; HIGH priority MVP path complete (14.0–14.5). + +### Future Compatibility + +- Multi-currency ledger partitions; facilitator settlement batch ids. + +--- + +## Phase 14.6 — Refunds & Reversals + +| Field | Value | +| --- | --- | +| Phase ID | `payment-14.6` | +| Implementation Priority | **LATER** | + +### Purpose + +Support full and partial refunds with PSP adapter calls, idempotency, and ledger reversal entries. + +### Business Scope + +- Merchant-initiated refunds linked to original transaction. +- Track refund status: requested → processing → succeeded|failed. +- Notify payer via Communication on refund completion. + +### Technical Scope + +- Aggregates: RefundRequest, RefundStatusHistory, PaymentLedgerEntry (type=reversal). +- Adapter `refund_payment()`. +- Partial refund sum ≤ captured amount invariant. + +### Dependencies + +- `payment-14.5`, Communication client + +### Out of Scope + +- Chargeback dispute workflow (future) +- Accounting credit note posting (14.9 intents only) + +### Capabilities + +`payment.refunds` + +### Permissions + +`payment.refunds.view|create|manage` + +### Events + +`payment.refund.requested|succeeded|failed` + +### API Contracts + +POST `/api/v1/refund-requests`; GET `/api/v1/refund-requests/{id}` + +### Data Ownership + +Payment owns refund domain; vertical owns return/RMA context ref. + +### Provider Ownership + +PSP refund APIs via adapters only. + +### Integration Rules + +- Refund requires original transaction in paid state. +- Idempotency on refund requests. + +### Quality Gates + +Partial refund math, PSP failure handling, ledger reversal integrity. + +### Definition of Done + +- Mock refund flow with ledger reversal and event publish. + +### Future Compatibility + +- Multi-currency refund; facilitator pass-through fee reversal lines. + +--- + +## Phase 14.7 — Split Payments & Facilitator Settlement + +| Field | Value | +| --- | --- | +| Phase ID | `payment-14.7` | +| Implementation Priority | **LATER** | + +### Purpose + +Allocate captured funds across multiple parties (marketplace sellers, branches) and emit **facilitator settlement intents** for Torbat Pay Merchant mode. + +### Business Scope + +- Split rules: fixed, percentage, remainder-to-platform. +- Settlement batch shells for facilitator payouts. +- Marketplace multi-vendor ready. + +### Technical Scope + +- Aggregates: PaymentSplitRule, PaymentSplitAllocation, SettlementBatch, SettlementBatchLine. +- APIs to define splits on payment request; compute allocations on capture. +- Events for Accounting consumption (intent only). + +### Dependencies + +- `payment-14.5`, `payment-14.2` (facilitator mode) + +### Out of Scope + +- Bank payout execution +- Accounting journal creation + +### Capabilities + +`payment.splits`, `payment.settlement_batches` + +### Permissions + +`payment.splits.manage`, `payment.settlement_batches.view|manage` + +### Events + +`payment.split.allocated`, `payment.settlement_batch.created|closed` + +### API Contracts + +CRUD `/api/v1/payment-split-rules`; POST `/api/v1/settlement-batches`; POST `.../close` + +### Data Ownership + +Payment owns split/settlement batch domain; Accounting owns cash movement journals. + +### Provider Ownership + +Facilitator payout rails via future adapter; not in 14.7 MVP. + +### Integration Rules + +- Sum of allocations = captured amount (minor units). +- Marketplace passes seller refs — not seller bank details in vertical DB. + +### Quality Gates + +Allocation math tests, batch close invariants, tenant isolation. + +### Definition of Done + +- Split on capture with settlement intent events; no bank transfer. + +### Future Compatibility + +- Cross-border split currency conversion metadata. + +--- + +## Phase 14.8 — Vertical Connectors & Checkout Contracts + +| Field | Value | +| --- | --- | +| Phase ID | `payment-14.8` | +| Implementation Priority | **LATER** | + +### Purpose + +Publish versioned **consumer connector contracts** so Hospitality, Marketplace, Sports Center, Experience, CRM, Healthcare, Beauty, Delivery, and future services integrate without custom per-vertical code in Payment. + +### Business Scope + +- Standard checkout session contract: create → pay → confirm. +- Embedded widget / redirect checkout API shapes documented. +- Connector registration per vertical with capability flags. + +### Technical Scope + +- Aggregates: PaymentConnectorRegistration, PaymentConnectorDispatch, CheckoutSession (refs to vertical source). +- Mock connectors for Hospitality POS and Marketplace order refs. +- Internal APIs documented in OpenAPI companion. + +### Dependencies + +- `payment-14.5` + +### Out of Scope + +- Vertical-side implementation (each vertical phase owns connector client) +- Frontend checkout UI (frontend module) + +### Capabilities + +`payment.connectors`, `payment.checkout_sessions` + +### Permissions + +`payment.connectors.manage`, `payment.checkout_sessions.create|view` + +### Events + +`payment.connector.registered`, `payment.checkout_session.created|completed|expired` + +### API Contracts + +CRUD `/api/v1/payment-connector-registrations`; POST `/api/v1/checkout-sessions`; POST `.../complete` + +### Data Ownership + +Payment owns checkout session shell; vertical owns order/ticket/invoice aggregate. + +### Provider Ownership + +N/A — connector layer is Payment-owned contract surface. + +### Integration Rules + +- One checkout session maps to one payment request chain. +- Vertical must subscribe to `payment.transaction.recorded` or poll transaction API. + +### Quality Gates + +Connector contract tests, forbidden cross-DB imports, mock vertical dispatch tests. + +### Definition of Done + +- Two mock vertical connectors documented and tested; contract doc published. + +### Future Compatibility + +- SDK stubs for mobile/web; webhook subscription registry. + +--- + +## Phase 14.9 — Reconciliation & Accounting Integration + +| Field | Value | +| --- | --- | +| Phase ID | `payment-14.9` | +| Implementation Priority | **LATER** | + +### Purpose + +Reconcile PSP settlement reports with payment ledger and emit **Accounting posting intents** (revenue, fees, facilitator payable) via API/events only. + +### Business Scope + +- Import PSP settlement file metadata (refs to File Storage). +- Mark transactions reconciled / exception. +- Daily reconciliation report shells. + +### Technical Scope + +- Aggregates: ReconciliationRun, ReconciliationException, AccountingPostingIntent. +- AccountingClient mock + event `payment.accounting_intent.created`. +- No JournalEntry in Payment service. + +### Dependencies + +- `payment-14.5`, Accounting service (contract), optional `payment-14.7` + +### Out of Scope + +- Tax/e-invoice (Accounting/future compliance) +- Automatic bank feed matching + +### Capabilities + +`payment.reconciliation`, `payment.accounting_integration` + +### Permissions + +`payment.reconciliation.view|run|manage`, `payment.accounting_intents.view` + +### Events + +`payment.reconciliation.started|completed`, `payment.reconciliation.exception.created`, `payment.accounting_intent.created` + +### API Contracts + +POST `/api/v1/reconciliation-runs`; GET exceptions; GET `/api/v1/accounting-posting-intents` + +### Data Ownership + +Payment owns reconciliation state; Accounting owns posted vouchers. + +### Provider Ownership + +PSP settlement files fetched via adapter or manual upload ref. + +### Integration Rules + +- Idempotent posting intents (Accounting dedupes by intent id). +- Never double-post on reconciliation retry. + +### Quality Gates + +Intent idempotency, no local journal tables, reconciliation exception workflow tests. + +### Definition of Done + +- Reconciliation run marks ledger rows; accounting intent event emitted; Accounting not modified in this phase. + +### Future Compatibility + +- Multi-PSP consolidated reconciliation; FX adjustment intents. + +--- + +## Phase 14.10 — Analytics, Fraud Shells & Enterprise Validation + +| Field | Value | +| --- | --- | +| Phase ID | `payment-14.10` | +| Implementation Priority | **LATER** | + +### Purpose + +Close the Payment track with analytics snapshots, optional fraud/rule hooks, and full AI Framework enterprise validation. + +### Business Scope + +- Tenant payment KPIs: volume, success rate, average ticket, refund rate. +- Fraud rule shells (velocity, amount threshold) — optional, off by default. +- Production readiness sign-off. + +### Technical Scope + +- Aggregates: PaymentAnalyticsReportDefinition, PaymentAnalyticsSnapshot, FraudRuleRegistration, FraudCheckDispatch (mock). +- Enterprise validation audit doc. +- Self-heal test suite until green. + +### Dependencies + +- `payment-14.9`, `ai-framework` + +### Out of Scope + +- ML fraud models +- PCI certification (operational program outside code phase) + +### 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 + +CRUD `/api/v1/analytics-report-definitions`; POST `/api/v1/analytics-snapshots/refresh` + +### Data Ownership + +Payment owns analytics snapshots (local aggregates only). + +### Provider Ownership + +External fraud vendors via adapter registry (stub). + +### Integration Rules + +- Analytics queries Payment DB only — no cross-service DB reads. +- Core flows work when fraud hooks disabled. + +### Quality Gates + +Full quality gate suite from [quality-gates.md](ai-framework/quality-gates.md); architecture/security/performance/docs/integration/tenant isolation. + +### Definition of Done + +- Track 14.0–14.10 complete; snapshot at `0.14.10.0`; no undocumented public API/event/permission. + +### Future Compatibility + +- Real-time metrics export; international fraud provider adapters. + +--- + +## Platform Expose (every implementation phase) + +Health API · Capability API · Metrics · Events · REST APIs · Permission APIs + +## Out of Scope (platform-wide) + +- Owning Accounting ledgers or Posting Engine +- Owning vertical order/cart/invoice aggregates +- Owning Communication providers +- International PSP live integrations (initial HIGH phases) +- PCI SAQ execution (operational) +- Payment UI in backend service + +## Related Documents + +- [Phase Area README](phases/Payment/README.md) +- [ADR-020](architecture/adr/ADR-020.md) · [ADR-021](architecture/adr/ADR-021.md) +- [Payment Contracts](reference/payment-contracts.md) +- [Module Registry](module-registry.md#payment) +- [Service Snapshot](service-snapshots/payment.yaml) +- [Phase Handover Pay-Reg](phase-handover/phase-pay-reg.md) +- [Progress](progress.md) +- [Next Steps](next-steps.md) diff --git a/docs/phase-handover/phase-14-5.md b/docs/phase-handover/phase-14-5.md new file mode 100644 index 0000000..0394793 --- /dev/null +++ b/docs/phase-handover/phase-14-5.md @@ -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.1–14.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` diff --git a/docs/phase-handover/phase-pay-arch.md b/docs/phase-handover/phase-pay-arch.md new file mode 100644 index 0000000..5af3f77 --- /dev/null +++ b/docs/phase-handover/phase-pay-arch.md @@ -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.9–4.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) diff --git a/docs/phase-handover/phase-pay-reg.md b/docs/phase-handover/phase-pay-reg.md new file mode 100644 index 0000000..da91e0f --- /dev/null +++ b/docs/phase-handover/phase-pay-reg.md @@ -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.3–14.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.0–14.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.0–14.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) diff --git a/docs/phases/Payment/README.md b/docs/phases/Payment/README.md new file mode 100644 index 0000000..5f17f6b --- /dev/null +++ b/docs/phases/Payment/README.md @@ -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.0–14.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. diff --git a/docs/phases/Payment/phase-14-0-payment-foundation.md b/docs/phases/Payment/phase-14-0-payment-foundation.md new file mode 100644 index 0000000..5018efe --- /dev/null +++ b/docs/phases/Payment/phase-14-0-payment-foundation.md @@ -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. diff --git a/docs/phases/Payment/phase-14-1-psp-management.md b/docs/phases/Payment/phase-14-1-psp-management.md new file mode 100644 index 0000000..e16be0f --- /dev/null +++ b/docs/phases/Payment/phase-14-1-psp-management.md @@ -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. diff --git a/docs/phases/Payment/phase-14-10-enterprise-validation.md b/docs/phases/Payment/phase-14-10-enterprise-validation.md new file mode 100644 index 0000000..b55b572 --- /dev/null +++ b/docs/phases/Payment/phase-14-10-enterprise-validation.md @@ -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.0–14.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. diff --git a/docs/phases/Payment/phase-14-2-merchant-accounts.md b/docs/phases/Payment/phase-14-2-merchant-accounts.md new file mode 100644 index 0000000..9309af7 --- /dev/null +++ b/docs/phases/Payment/phase-14-2-merchant-accounts.md @@ -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. diff --git a/docs/phases/Payment/phase-14-3-payment-requests.md b/docs/phases/Payment/phase-14-3-payment-requests.md new file mode 100644 index 0000000..44aa5dd --- /dev/null +++ b/docs/phases/Payment/phase-14-3-payment-requests.md @@ -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. diff --git a/docs/phases/Payment/phase-14-4-callback-verification.md b/docs/phases/Payment/phase-14-4-callback-verification.md new file mode 100644 index 0000000..030eca7 --- /dev/null +++ b/docs/phases/Payment/phase-14-4-callback-verification.md @@ -0,0 +1,71 @@ +# Phase 14.4 — Callback & Verification + +| Field | Value | +| --- | --- | +| Identifier | `payment-14.4` | +| Implementation Priority | **HIGH** | +| Status | Complete | +| Service | `payment` | +| Depends On | `payment-14.3` | + +> Full specification: [payment-roadmap.md](../../payment-roadmap.md#phase-14-4--callback--verification) + +## Purpose + +Ingest PSP callbacks/webhooks, verify signatures, reconcile with PSP verify API, transition requests to paid/failed. + +## Business Scope + +Trusted callback processing; admin reconcile retry; publish events for verticals to mark orders paid. + +## Technical Scope + +Aggregates: PaymentCallbackLog, PaymentVerificationAttempt. Public callback routes per adapter. `verify_payment()`, `parse_callback()`. + +## Dependencies + +`payment-14.3` + +## Out of Scope + +Immutable ledger authority (14.5), refunds, Accounting posting. + +## Capabilities + +`payment.callbacks`, `payment.verification`, `payment.webhooks` + +## Permissions + +`payment.callbacks.view`, `payment.requests.reconcile|manage`; signature-gated public callbacks + +## Events + +`payment.callback.received|verified|rejected`, `payment.request.paid|failed` + +## API Contracts + +POST `/api/v1/callbacks/{provider_code}/{connection_id}`, POST `.../verify`, GET callback logs + +## Data Ownership + +Payment owns callback logs; PSP transaction ids as external refs. + +## Provider Ownership + +Webhook secrets in adapter layer. + +## Integration Rules + +Never trust return URL alone; always PSP verify. Fast callback ack; async verify allowed. + +## Quality Gates + +Signature forgery, replay protection, tenant routing isolation. + +## Definition of Done + +Mock callback → verified paid; outbox events; verticals can subscribe. + +## Future Compatibility + +Multi-version callbacks per PSP; international webhook formats. diff --git a/docs/phases/Payment/phase-14-5-transaction-ledger.md b/docs/phases/Payment/phase-14-5-transaction-ledger.md new file mode 100644 index 0000000..0ccef14 --- /dev/null +++ b/docs/phases/Payment/phase-14-5-transaction-ledger.md @@ -0,0 +1,71 @@ +# Phase 14.5 — Transaction Ledger + +| Field | Value | +| --- | --- | +| Identifier | `payment-14.5` | +| Implementation Priority | **HIGH** | +| Status | Complete | +| Service | `payment` | +| Depends On | `payment-14.4` | + +> Full specification: [payment-roadmap.md](../../payment-roadmap.md#phase-14-5--transaction-ledger) + +## Purpose + +Immutable append-only payment transaction ledger as system of record for captured payments — distinct from Accounting journals. + +## Business Scope + +Record paid transactions with PSP refs, fees, net amounts; query for admin and service-to-service status; completes HIGH priority MVP (14.0–14.5). + +## Technical Scope + +Aggregates: PaymentTransaction, PaymentTransactionFeeLine, PaymentLedgerEntry. No monetary UPDATE post-insert. + +## Dependencies + +`payment-14.4` + +## Out of Scope + +Refunds (14.6), splits (14.7), Accounting journals. + +## Capabilities + +`payment.ledger`, `payment.transactions` + +## Permissions + +`payment.transactions.view|export|manage` + +## Events + +`payment.transaction.recorded`, `payment.ledger.entry_appended` + +## API Contracts + +GET `/api/v1/payment-transactions`, GET by source ref, GET ledger entries + +## Data Ownership + +Payment owns payment ledger; Accounting owns GL. + +## Provider Ownership + +PSP transaction ids as refs. + +## Integration Rules + +Ledger write only after verified paid. Verticals query Payment for status. + +## Quality Gates + +Immutability, append-only, tenant isolation, traceability. + +## Definition of Done + +Paid flow produces ledger; queries work; HIGH priority path complete. + +## Future Compatibility + +Multi-currency partitions; facilitator settlement batch ids. diff --git a/docs/phases/Payment/phase-14-6-refunds-reversals.md b/docs/phases/Payment/phase-14-6-refunds-reversals.md new file mode 100644 index 0000000..6f78598 --- /dev/null +++ b/docs/phases/Payment/phase-14-6-refunds-reversals.md @@ -0,0 +1,71 @@ +# Phase 14.6 — Refunds & Reversals + +| Field | Value | +| --- | --- | +| Identifier | `payment-14.6` | +| Implementation Priority | **LATER** | +| Status | Planned | +| Service | `payment` | +| Depends On | `payment-14.5` | + +> Full specification: [payment-roadmap.md](../../payment-roadmap.md#phase-14-6--refunds--reversals) + +## Purpose + +Full and partial refunds via PSP adapters with idempotency and ledger reversal entries. + +## Business Scope + +Merchant-initiated refunds; status tracking; payer notify via Communication. + +## Technical Scope + +RefundRequest, RefundStatusHistory, ledger reversal entries. Adapter `refund_payment()`. + +## Dependencies + +`payment-14.5`, Communication client + +## Out of Scope + +Chargeback disputes; Accounting credit notes (14.9 intents). + +## Capabilities + +`payment.refunds` + +## Permissions + +`payment.refunds.view|create|manage` + +## Events + +`payment.refund.requested|succeeded|failed` + +## API Contracts + +POST/GET `/api/v1/refund-requests` + +## Data Ownership + +Payment owns refunds; vertical owns RMA/return ref. + +## Provider Ownership + +PSP refund APIs via adapters. + +## Integration Rules + +Refund requires paid transaction; idempotent refund keys. + +## Quality Gates + +Partial refund math, failure handling, ledger integrity. + +## Definition of Done + +Mock refund with reversal and events. + +## Future Compatibility + +Multi-currency refund; facilitator fee reversal lines. diff --git a/docs/phases/Payment/phase-14-7-split-settlement.md b/docs/phases/Payment/phase-14-7-split-settlement.md new file mode 100644 index 0000000..a0784af --- /dev/null +++ b/docs/phases/Payment/phase-14-7-split-settlement.md @@ -0,0 +1,71 @@ +# Phase 14.7 — Split Payments & Facilitator Settlement + +| Field | Value | +| --- | --- | +| Identifier | `payment-14.7` | +| Implementation Priority | **LATER** | +| Status | Planned | +| Service | `payment` | +| Depends On | `payment-14.5`, `payment-14.2` | + +> Full specification: [payment-roadmap.md](../../payment-roadmap.md#phase-14-7--split-payments--facilitator-settlement) + +## Purpose + +Allocate captured funds across parties (marketplace sellers, branches) and emit facilitator settlement intents. + +## Business Scope + +Split rules (fixed/percent/remainder); settlement batch shells; marketplace multi-vendor. + +## Technical Scope + +PaymentSplitRule, PaymentSplitAllocation, SettlementBatch, SettlementBatchLine. + +## Dependencies + +`payment-14.5`, facilitator merchant (14.2) + +## Out of Scope + +Bank payout execution; Accounting journal creation. + +## Capabilities + +`payment.splits`, `payment.settlement_batches` + +## Permissions + +`payment.splits.manage`, `payment.settlement_batches.view|manage` + +## Events + +`payment.split.allocated`, `payment.settlement_batch.created|closed` + +## API Contracts + +CRUD split rules; POST settlement batches; POST close + +## Data Ownership + +Payment owns splits/batches; Accounting owns cash journals. + +## Provider Ownership + +Future payout rail adapters. + +## Integration Rules + +Allocations sum to captured amount; marketplace passes seller refs only. + +## Quality Gates + +Allocation math, batch invariants, tenant isolation. + +## Definition of Done + +Split on capture with settlement intent events. + +## Future Compatibility + +Cross-border split FX metadata. diff --git a/docs/phases/Payment/phase-14-8-vertical-connectors.md b/docs/phases/Payment/phase-14-8-vertical-connectors.md new file mode 100644 index 0000000..750cd98 --- /dev/null +++ b/docs/phases/Payment/phase-14-8-vertical-connectors.md @@ -0,0 +1,71 @@ +# Phase 14.8 — Vertical Connectors & Checkout Contracts + +| Field | Value | +| --- | --- | +| Identifier | `payment-14.8` | +| Implementation Priority | **LATER** | +| Status | Planned | +| Service | `payment` | +| Depends On | `payment-14.5` | + +> Full specification: [payment-roadmap.md](../../payment-roadmap.md#phase-14-8--vertical-connectors--checkout-contracts) + +## Purpose + +Versioned consumer connector contracts for Hospitality, Marketplace, Sports Center, Experience, CRM, Healthcare, Beauty, Delivery, Automation, and future services. + +## Business Scope + +Standard checkout session: create → pay → confirm. Document embed/redirect shapes. + +## Technical Scope + +PaymentConnectorRegistration, PaymentConnectorDispatch, CheckoutSession. Mock Hospitality + Marketplace connectors. + +## Dependencies + +`payment-14.5` + +## Out of Scope + +Vertical client implementation; frontend checkout UI. + +## Capabilities + +`payment.connectors`, `payment.checkout_sessions` + +## Permissions + +`payment.connectors.manage`, `payment.checkout_sessions.create|view` + +## Events + +`payment.connector.registered`, `payment.checkout_session.created|completed|expired` + +## API Contracts + +Connector CRUD; POST checkout-sessions; POST complete + +## Data Ownership + +Payment owns checkout session shell; vertical owns order/ticket. + +## Provider Ownership + +Connector contract surface owned by Payment. + +## Integration Rules + +One session → one payment request chain; subscribe to `payment.transaction.recorded`. + +## Quality Gates + +Contract tests, mock vertical dispatch, no cross-DB imports. + +## Definition of Done + +Two mock connectors documented and tested. + +## Future Compatibility + +Mobile/web SDK stubs; webhook subscription registry. diff --git a/docs/phases/Payment/phase-14-9-reconciliation.md b/docs/phases/Payment/phase-14-9-reconciliation.md new file mode 100644 index 0000000..8a17de3 --- /dev/null +++ b/docs/phases/Payment/phase-14-9-reconciliation.md @@ -0,0 +1,71 @@ +# Phase 14.9 — Reconciliation & Accounting Integration + +| Field | Value | +| --- | --- | +| Identifier | `payment-14.9` | +| Implementation Priority | **LATER** | +| Status | Planned | +| Service | `payment` | +| Depends On | `payment-14.5`, Accounting contract | + +> Full specification: [payment-roadmap.md](../../payment-roadmap.md#phase-14-9--reconciliation--accounting-integration) + +## Purpose + +Reconcile PSP settlements with payment ledger; emit Accounting posting intents via API/events only. + +## Business Scope + +Settlement file import refs; exception handling; daily reconciliation shells. + +## Technical Scope + +ReconciliationRun, ReconciliationException, AccountingPostingIntent. AccountingClient mock. + +## Dependencies + +`payment-14.5`, optional `payment-14.7`, Accounting + +## Out of Scope + +Tax/e-invoice; automatic bank feeds. + +## Capabilities + +`payment.reconciliation`, `payment.accounting_integration` + +## Permissions + +`payment.reconciliation.view|run|manage`, `payment.accounting_intents.view` + +## Events + +`payment.reconciliation.started|completed`, `payment.reconciliation.exception.created`, `payment.accounting_intent.created` + +## API Contracts + +POST reconciliation-runs; GET exceptions; GET accounting-posting-intents + +## Data Ownership + +Payment owns reconciliation; Accounting owns vouchers. + +## Provider Ownership + +PSP settlement files via adapter or upload ref. + +## Integration Rules + +Idempotent posting intents; no JournalEntry in Payment. + +## Quality Gates + +Intent idempotency, no local journal tables. + +## Definition of Done + +Reconciliation run + accounting intent event; Accounting unchanged. + +## Future Compatibility + +Multi-PSP consolidated reconciliation; FX adjustment intents. diff --git a/docs/progress.md b/docs/progress.md index 6df81bd..a9a3b6d 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -621,6 +621,44 @@ Remaining (honest): Phase 5.12 AI blocked; budget/DMS/integration still typed-op --- +## Platform — Published Resource Architecture (docs only) ✅ + +- [x] [ADR-022](architecture/adr/ADR-022.md) Accepted — Platform Published Resource Architecture +- [x] [published-resource-architecture.md](architecture/published-resource-architecture.md) + [published-resource-contracts.md](reference/published-resource-contracts.md) +- [x] Standalone Form / Survey / Appointment publish rules; reserved public URL prefixes +- [x] Cross-service `publish_id` contracts (Payment, Communication, Short Link, QR, Analytics, verticals, future products) +- [x] Handovers: [phase-af-published-resource-arch.md](phase-handover/phase-af-published-resource-arch.md), [phase-experience-published-resource-arch.md](phase-handover/phase-experience-published-resource-arch.md) +- [x] Manifests / snapshot / project-index / boundaries updated — **no backend or frontend implementation** + +--- + +## Platform — Experience Architecture Freeze (docs only) ✅ + +- [x] Published Actions — open Action Registry ([published-action-registry.md](reference/published-action-registry.md)) +- [x] Public Access Model — open strategies; Experience never owns auth ([public-access-contract.md](reference/public-access-contract.md)) +- [x] Universal Embed Architecture ([embed-contract.md](reference/embed-contract.md)) +- [x] ADR-022 amended additively; architecture marked **ARCHITECTURE COMPLETE** +- [x] Handovers: [phase-af-experience-arch-freeze.md](phase-handover/phase-af-experience-arch-freeze.md), [phase-experience-arch-freeze.md](phase-handover/phase-experience-arch-freeze.md) +- [x] Fully backward compatible — **no backend/frontend/migrations** + +--- + +## Experience Frontend FE-11.0 – FE-11.5 ✅ (STOP) + +- [x] Module `frontend/modules/experience/` — PortalShell, ListCrudPage, BFF, design system (mirrors Delivery) +- [x] FE-11.0: hub, workspace switcher, permissions/capabilities, admin lists, locale-profiles, settings, audit, health +- [x] FE-11.1: executive dashboard (recharts, API counts, recent resources, audit) +- [x] FE-11.2: workspaces, sites, pages, domains, templates, publish-targets (provisional publish_id) +- [x] FE-11.3: page builder (placements, undo/redo, breakpoints, media); components/themes/layouts CRUD +- [x] FE-11.4: forms/surveys CRUD + builders — **no booking UI** +- [x] FE-11.5: publishing/SEO/PWA, public preview, analytics refresh, embed, published-actions +- [x] Apps catalog: تربت پیجز → `/experience/hub` +- [x] Seed: `backend/services/experience/scripts/seed_experience.py` +- [x] Docs: [experience-frontend-roadmap.md](experience-frontend-roadmap.md), [phase-experience-fe-11-5.md](phase-handover/phase-experience-fe-11-5.md) +- [x] **FE-11.6+ not started** (registry, payment, booking, QR, short links, cards, marketplace, checkout, ticketing, learning) + +--- + ## Phase 12.0 — Hospitality Platform Foundation ✅ - [x] Independent `backend/services/hospitality` (`hospitality_db`, port **8009**, version **0.12.0.0**, commercial product **Torbat Food**) @@ -965,6 +1003,78 @@ Remaining (honest): dispatch/fleet/tracking/maps live/realtime/settlement screen --- +## Phase Pay-Reg — Payment Platform Registration ✅ + +> Documentation-only registration. No business code, models, APIs, migrations, or repositories. No existing services modified. + +- [x] Phase roadmap payment-14.0–payment-14.10 documented in [payment-roadmap.md](payment-roadmap.md) +- [x] Planned modules registered in [module-registry.md](module-registry.md#payment) +- [x] Phase area README: [phases/Payment/README.md](phases/Payment/README.md) +- [x] Phase documents: 14.0 Foundation through 14.10 Enterprise Validation +- [x] ADR-020 Accepted — Independent Enterprise Payment Platform (Torbat Pay) +- [x] Service registered in [service-manifest.yaml](ai-framework/service-manifest.yaml) and [project-index.yaml](ai-framework/project-index.yaml) +- [x] Port **8012** and `payment_db` reserved in registry +- [x] Service snapshot: [service-snapshots/payment.yaml](service-snapshots/payment.yaml) +- [x] Handover: [phase-handover/phase-pay-reg.md](phase-handover/phase-pay-reg.md) +- [x] Phase 14.0 NOT implemented (by design) + +--- + +## Phase Pay-Arch — Payment Architecture Prerequisites ✅ + +> Documentation-only. Finalizes contracts and licensing before implementation. No business code; no existing services modified. + +- [x] [ADR-021](architecture/adr/ADR-021.md) Accepted — licensing, routing, integration contracts +- [x] [payment-contracts.md](reference/payment-contracts.md) — PaymentIntent, Checkout, Callback, Settlement + future Refund/Split/Subscription/Wallet/Installment v1 schemas +- [x] Three-layer model: Core L1 → Payment bundles L2 → feature toggles L3 +- [x] Provider assignment + routing policy — change PSP without code changes +- [x] Compatibility verified: Core, Identity, Tenant, Accounting, CRM, Communication, Hospitality, Delivery, Sports Center, Marketplace +- [x] Updated: module-boundaries, event-catalog, services-contracts, authorization-architecture, payment-roadmap, phase-14-0 +- [x] Service snapshot v2; handover [phase-pay-arch.md](phase-handover/phase-pay-arch.md) +- [x] Implementation started in payment-14.0 (see MVP section below) + +--- + +## Phase Payment 14.0–14.5 — Torbat Pay MVP ✅ + +> Independent `payment-service` on port **8012**, database `payment_db`, version **0.14.5.0**. No other platform services modified. + +### 14.0 Payment Foundation +- [x] Service scaffold `backend/services/payment` — API → commands/queries → repositories → models +- [x] Foundation aggregates: workspaces, bundles, toggles, provider assignments, PSP/credit registry shells, audit, outbox +- [x] Health, capabilities, metrics; permission tree `payment.*` +- [x] Alembic `0001_initial`; docker-compose `payment-service` +- [x] Tests: architecture, API, permissions, migration, dependency, security, docs + +### 14.1 PSP Management +- [x] PspConnection, routing policies, health checks, MockPspAdapter +- [x] Bundle gate `payment.byo_psp.basic`; secret redaction +- [x] Alembic `0002_phase_141_psp`; `test_phase_141.py` + +### 14.2 Merchant Accounts +- [x] MerchantAccount lifecycle (draft → pending_review → active → suspended → closed) +- [x] Bundle gate `payment.torbat_pay.merchant` +- [x] Alembic `0003_phase_142_merchant`; `test_phase_142.py` + +### 14.3 Payment Requests +- [x] Idempotent payment requests; PaymentIntentContract v1 fields including credit refs (nullable) +- [x] Mock initiate → redirect_issued; rejects CREDIT_PROVIDER (422) +- [x] Alembic `0004_phase_143_requests`; `test_phase_143.py` + +### 14.4 Callback & Verification +- [x] Callback ingress with signature verify + replay protection +- [x] Verify API; events `payment.request.paid` / `payment.request.failed` +- [x] Alembic `0005_phase_144_callbacks`; `test_phase_144.py` + +### 14.5 Transaction Ledger +- [x] Immutable PaymentTransaction + append-only PaymentLedgerEntry on verified paid +- [x] Query APIs: transactions, by-source, ledger entries +- [x] Alembic `0006_phase_145_ledger`; `test_phase_145.py` +- [x] Handover: [phase-handover/phase-14-5.md](phase-handover/phase-14-5.md) +- [x] Snapshot v4: [service-snapshots/payment.yaml](service-snapshots/payment.yaml) + +--- + ## Related Documents - [Next Steps](next-steps.md) @@ -1039,6 +1149,11 @@ Remaining (honest): dispatch/fleet/tracking/maps live/realtime/settlement screen - [Beauty Business Phase Area](phases/BeautyBusiness/README.md) - [Beauty Business Snapshot](service-snapshots/beauty-business.yaml) - [Phase Handover 14.7](phase-handover/phase-14-7.md) +- [Payment Roadmap](payment-roadmap.md) +- [Payment Snapshot](service-snapshots/payment.yaml) +- [Phase Handover Pay-Reg](phase-handover/phase-pay-reg.md) +- [Phase Handover Pay-Arch](phase-handover/phase-pay-arch.md) +- [Payment Phase Area](phases/Payment/README.md) ## Loyalty Frontend (2026-07-26) - Added `frontend/modules/loyalty` engage portal + thin `app/loyalty` routes. diff --git a/docs/provider-registry.md b/docs/provider-registry.md index 1cfacea..dd3ab73 100644 --- a/docs/provider-registry.md +++ b/docs/provider-registry.md @@ -94,21 +94,22 @@ Inventory of external providers. Template: [provider-template.md](templates/prov --- -## Payment Gateway (planned) +## Payment Platform — Torbat Pay (planned) | Field | Value | | --- | --- | -| Provider Name | TBD (Iranian PSP) | -| Country | IR | -| Capabilities | Checkout, refunds, webhooks | -| Version | TBD | -| Dependencies | Subscription billing, ecommerce/restaurant payments | -| Supported Modules | Core subscriptions, ecommerce, restaurant | -| Configuration | TBD | -| API | TBD | -| Status | Planned | -| Documentation | [roadmap.md](roadmap.md) | -| Tests | TBD | +| Provider Name | Torbat Pay (`payment` service) | +| Country | IR (initial); architecture supports future regions | +| Capabilities | BYO-PSP, Torbat Pay Merchant, PaymentIntent, Checkout, Callback, Settlement; future Refund/Split/Subscription/Wallet/Installment | +| Version | Contract v1 per [payment-contracts.md](reference/payment-contracts.md) | +| Dependencies | Core entitlement `payment.module.enabled`; Accounting posting intents; Communication notify | +| Supported Modules | All verticals via API/events — Hospitality, Marketplace, Sports Center, Experience, CRM, Delivery, … | +| Configuration | `PAYMENT_*`, PSP adapters in Payment only ([ADR-020](architecture/adr/ADR-020.md), [ADR-021](architecture/adr/ADR-021.md)) | +| API | Port **8012** `/api/v1/*` | +| PSP adapters (initial catalog) | ZarinPal, IDPay, NextPay, Pay.ir, Mellat, Parsian, Saman (+ mock) | +| Status | Architecture complete (`payment-arch`); implementation from `payment-14.0` | +| Documentation | [payment-roadmap.md](payment-roadmap.md), [payment-contracts.md](reference/payment-contracts.md) | +| Tests | From payment-14.0 (architecture, tenant, bundle isolation) | --- diff --git a/docs/reference/event-catalog.md b/docs/reference/event-catalog.md index f080a7f..52db9a7 100644 --- a/docs/reference/event-catalog.md +++ b/docs/reference/event-catalog.md @@ -178,3 +178,50 @@ When adding events: update this catalog, module registry producers/consumers, an | `delivery.driver.deleted` | Driver soft-deleted | | `delivery.driver.credential_added` | Driver credential registered | | `delivery.driver.document_attached` | Driver document metadata attached | + +## Payment Platform (`payment`) — planned + +> Normative contract payloads: [payment-contracts.md](payment-contracts.md). Architecture: [ADR-020](../architecture/adr/ADR-020.md), [ADR-021](../architecture/adr/ADR-021.md). + +| Event | When | +| --- | --- | +| `payment.workspace.enabled` | Payment workspace enabled for tenant | +| `payment.workspace.disabled` | Payment disabled for tenant | +| `payment.workspace.suspended` | Payment workspace suspended | +| `payment.bundle.activated` | Tenant payment bundle activated | +| `payment.bundle.deactivated` | Tenant payment bundle deactivated | +| `payment.feature_toggle.changed` | Operational feature toggle changed | +| `payment.psp_provider.registered` | PSP type registered in catalog | +| `payment.psp_connection.created` | BYO-PSP connection created | +| `payment.psp_connection.activated` | BYO-PSP connection activated | +| `payment.psp_connection.suspended` | BYO-PSP connection suspended | +| `payment.psp_routing.updated` | Routing policy updated | +| `payment.provider_assignment.changed` | Active provider assignment changed | +| `payment.merchant_account.created` | Facilitator merchant account created | +| `payment.merchant_account.activated` | Merchant account activated | +| `payment.request.created` | Payment intent created | +| `payment.request.redirect_issued` | Redirect/token issued to payer | +| `payment.request.paid` | Payment verified paid | +| `payment.request.failed` | Payment failed or expired | +| `payment.request.cancelled` | Payment request cancelled | +| `payment.callback.received` | PSP callback received | +| `payment.callback.verified` | Callback signature verified | +| `payment.transaction.recorded` | Ledger transaction recorded | +| `payment.ledger.entry_appended` | Append-only ledger entry | +| `payment.refund.requested` | Refund initiated (14.6+) | +| `payment.refund.succeeded` | Refund completed | +| `payment.refund.failed` | Refund failed | +| `payment.split.allocated` | Split allocation computed (14.7+) | +| `payment.settlement_batch.created` | Settlement batch opened | +| `payment.settlement_batch.closed` | Settlement batch closed | +| `payment.checkout_session.created` | Checkout session created (14.8+) | +| `payment.checkout_session.completed` | Checkout session completed | +| `payment.reconciliation.completed` | Reconciliation run completed (14.9+) | +| `payment.accounting_intent.created` | Accounting posting intent emitted | +| `payment.analytics.snapshot.created` | Analytics snapshot refreshed (14.10+) | + +## Related Documents + +- [Payment Contracts](payment-contracts.md) +- [Services Contracts](services-contracts.md) +- [Event-Driven Architecture](../architecture/event-driven-architecture.md) diff --git a/docs/reference/payment-contracts.md b/docs/reference/payment-contracts.md new file mode 100644 index 0000000..88261fc --- /dev/null +++ b/docs/reference/payment-contracts.md @@ -0,0 +1,553 @@ +# Payment Platform — Integration & Contract Reference + +> **Normative contract reference** for Torbat Pay (`payment` service). +> Architecture: [ADR-020](../architecture/adr/ADR-020.md), [ADR-021](../architecture/adr/ADR-021.md) +> Roadmap: [payment-roadmap.md](../payment-roadmap.md) + +All contracts are **versioned**. Implementations MUST accept unknown additive JSON fields. Breaking changes require a new major version and a registered phase. + +--- + +## 1. Architectural verification checklist + +| Requirement | How Torbat Pay satisfies it | +| --- | --- | +| **API-first** | All vertical integration via REST `/api/v1/*` and token-gated `/internal/v1/*`; no shared DB | +| **Event-first** | Domain transitions publish `payment.*` via transactional outbox ([ADR-006](../architecture/adr/ADR-006.md)) | +| **Database-per-service** | Sole owner of `payment_db` ([ADR-001](../architecture/adr/ADR-001.md)) | +| **Feature toggles** | L3 `PaymentFeatureToggle` in Payment; operational flags within bundles | +| **Permission tree** | `payment.*` bundle-gated leaves; see §8 | +| **Bundle isolation** | Inactive bundles → hidden APIs, capabilities, permissions ([ADR-021](../architecture/adr/ADR-021.md)) | +| **Multi-tenant isolation** | `tenant_id` on all rows; callback routing validated ([ADR-003](../architecture/adr/ADR-003.md)) | +| **Tenant enable/disable** | Core L1 + PaymentWorkspace.status | +| **Choose own PSP** | BYO `PspConnection` + `PaymentProviderAssignment` | +| **Choose Torbat Pay Merchant** | `default_payment_mode=torbat_pay_merchant` + `MerchantAccount` | +| **Change provider without code** | Update `PaymentProviderAssignment` / `PspRoutingPolicy` only | + +--- + +## 2. Three-layer licensing model + +### L1 — Core service entitlement + +| Feature key | Meaning | +| --- | --- | +| `payment.module.enabled` | Tenant may use Torbat Pay at all | + +Check: `POST /api/v1/tenants/{tenant_id}/features/check` (Core) — see [services-contracts.md](services-contracts.md). + +Payment caches result; invalidates on `feature_access.changed`. + +### L2 — Payment bundles (owned by Payment) + +| Bundle code | Capabilities unlocked | Typical mode | +| --- | --- | --- | +| `payment.byo_psp.basic` | PSP connections, routing, payment requests, callbacks, ledger | BYO-PSP | +| `payment.torbat_pay.merchant` | Merchant accounts, facilitator routing, payment requests, callbacks, ledger | Torbat Pay Merchant | +| `payment.marketplace.splits` | Split rules, settlement batches | Facilitator + marketplace | +| `payment.advanced.refunds` | Refund APIs (14.6+) | Either mode | +| `payment.advanced.reconciliation` | Reconciliation + accounting intents (14.9+) | Either mode | +| `payment.future.subscriptions` | SubscriptionBillingContract (reserved) | Either mode | +| `payment.future.installments` | InstallmentPlanContract (reserved) | Either mode — **Payment schedule shell only** | +| `payment.future.credit_provider` | CREDIT_PROVIDER routing via external credit adapters | Either mode — **not implemented** | +| `payment.future.wallet_topup` | WalletTopUpContract with Loyalty (reserved) | Either mode | + +Aggregates: `PaymentBundleDefinition`, `TenantPaymentBundle` (activation refs Core entitlement only). + +### L3 — Payment feature toggles (owned by Payment) + +| Toggle key | Default | Requires bundle | +| --- | --- | --- | +| `payment.capture.enabled` | true when workspace enabled | any active L2 | +| `payment.refunds.enabled` | false | `payment.advanced.refunds` | +| `payment.splits.enabled` | false | `payment.marketplace.splits` | +| `payment.installments.enabled` | false | `payment.future.installments` | +| `payment.subscriptions.enabled` | false | `payment.future.subscriptions` | +| `payment.wallet_topup.enabled` | false | `payment.future.wallet_topup` | +| `payment.credit_provider.enabled` | false | `payment.future.credit_provider` | +| `payment.fraud_hooks.enabled` | false | any active L2 | + +--- + +## 3. Provider assignment & routing + +### PaymentProviderAssignment + +Links a workspace (and optional scope) to the active payment source: + +```json +{ + "assignment_id": "uuid", + "tenant_id": "uuid", + "payment_workspace_id": "uuid", + "scope_type": "workspace|branch|vertical", + "scope_ref_id": "uuid|null", + "source_type": "psp_connection|merchant_account", + "source_id": "uuid", + "priority": 1, + "is_active": true +} +``` + +**Changing PSP or credit provider:** deactivate old assignment, activate new — routing policy selects at request time. No adapter code change in verticals. + +### Payment source types (routing — reserved) + +| Value | Description | Adapter | Status | +| --- | --- | --- | --- | +| `PSP` | BYO payment gateway | `PspAdapter` | Phase 14.1+ | +| `MERCHANT_FACILITATOR` | Torbat Pay Merchant | Facilitator adapter | Phase 14.2+ | +| `CREDIT_PROVIDER` | External credit/BNPL/financing | `CreditProviderAdapter` | **Reserved — not implemented** | + +When `payment_source=CREDIT_PROVIDER`, Payment routes to an external **Credit Provider** (primary reserved: **Torbat Credit**) using the same assignment and adapter registry pattern as PSPs. + +### Credit provider architectural references (opaque refs only) + +| Field | Type | Owner of truth | +| --- | --- | --- | +| `credit_provider_id` | uuid | Payment registry → external provider | +| `financing_reference` | string | Torbat Credit | +| `credit_authorization_reference` | string | Torbat Credit | +| `installment_contract_reference` | string | Torbat Credit | + +### PspRoutingPolicy + +```json +{ + "policy_id": "uuid", + "tenant_id": "uuid", + "rules": [ + { + "priority": 1, + "provider_assignment_id": "uuid", + "conditions": { + "currency": ["IRR"], + "payment_source": ["PSP", "MERCHANT_FACILITATOR", "CREDIT_PROVIDER"], + "min_amount_minor": 0, + "max_amount_minor": null, + "channel": ["web", "mobile", "pos"] + } + } + ], + "failover_enabled": true +} +``` + +--- + +## 4. Contract catalog + +### 4.1 PaymentIntentContract v1 (Phase 14.3+) + +**Purpose:** Vertical initiates payment; Payment returns redirect/token payload. + +**HTTP:** `POST /api/v1/payment-requests` +**Headers:** `Authorization`, `X-Tenant-ID`, `Idempotency-Key` (required) + +```json +{ + "contract_version": "payment_intent.v1", + "amount_minor": 150000, + "currency": "IRR", + "idempotency_key": "string", + "source": { + "service": "hospitality|marketplace|sports_center|experience|crm|delivery|accounting|other", + "ref_type": "pos_ticket|order|membership|invoice|checkout_session|quote|other", + "ref_id": "uuid" + }, + "payer": { + "contact_ref": "uuid|null", + "user_ref": "uuid|null", + "mobile": "string|null" + }, + "payment_mode": "byo_psp|torbat_pay_merchant|default", + "payment_source": "PSP|MERCHANT_FACILITATOR|CREDIT_PROVIDER", + "provider_assignment_id": "uuid|null", + "credit_provider_id": "uuid|null", + "financing_reference": "string|null", + "credit_authorization_reference": "string|null", + "installment_contract_reference": "string|null", + "return_url": "https://...", + "metadata": {}, + "line_items": [ + { "description": "string", "amount_minor": 150000, "quantity": 1 } + ] +} +``` + +**Response 201:** + +```json +{ + "payment_request_id": "uuid", + "status": "pending|redirect_issued", + "redirect_url": "https://...", + "token": "string|null", + "expires_at": "ISO-8601" +} +``` + +**Notes:** + +- `payment_source` defaults from routing policy when omitted. +- `CREDIT_PROVIDER` fields are **ignored** until bundle `payment.future.credit_provider` is active and Torbat Credit adapter is registered — validators reject if source is `CREDIT_PROVIDER` before implementation. +- Verticals pass refs only; Torbat Credit owns authorization lifecycle. + +**Events:** `payment.request.created`, `payment.request.redirect_issued`, `payment.request.paid`, `payment.request.failed` + +--- + +### 4.2 CheckoutSessionContract v1 (Phase 14.8+) + +**Purpose:** Higher-level checkout wrapper for embeddable/widget flows. + +**HTTP:** `POST /api/v1/checkout-sessions` + +```json +{ + "contract_version": "checkout_session.v1", + "source": { "service": "string", "ref_type": "string", "ref_id": "uuid" }, + "amount_minor": 0, + "currency": "IRR", + "success_url": "https://...", + "cancel_url": "https://...", + "metadata": {}, + "payment_intent_overrides": {} +} +``` + +Completing session creates/links a PaymentIntent internally. Verticals SHOULD prefer CheckoutSession for UX flows; low-level verticals MAY call PaymentIntent directly. + +--- + +### 4.3 CallbackIngressContract v1 (Phase 14.4+) + +**Purpose:** PSP → Payment webhook ingress (adapter-normalized). + +**HTTP:** `POST /api/v1/callbacks/{provider_code}/{connection_id}` + +Adapter normalizes to: + +```json +{ + "contract_version": "callback_ingress.v1", + "provider_code": "zarinpal|idpay|...", + "connection_id": "uuid", + "provider_transaction_id": "string", + "payment_request_id": "uuid|null", + "raw_status": "string", + "normalized_status": "paid|failed|pending", + "amount_minor": 0, + "currency": "IRR", + "signature": "string", + "received_at": "ISO-8601" +} +``` + +**Events:** `payment.callback.received`, `payment.callback.verified`, `payment.request.paid` + +--- + +### 4.4 SettlementIntentContract v1 (Phase 14.9+) + +**Purpose:** Payment → Accounting (no journal in Payment). + +**Event:** `payment.accounting_intent.created` + +```json +{ + "contract_version": "settlement_intent.v1", + "intent_id": "uuid", + "tenant_id": "uuid", + "intent_type": "capture|fee|facilitator_payout|reconciliation_adjustment", + "payment_transaction_id": "uuid", + "amount_minor": 0, + "currency": "IRR", + "accounting_refs": { + "customer_ref": "uuid|null", + "revenue_account_code_ref": "string|null" + }, + "idempotency_key": "string", + "occurred_at": "ISO-8601" +} +``` + +Accounting consumes and posts via Posting Engine ([ADR-010](../architecture/adr/ADR-010.md)). + +--- + +### 4.5 RefundIntentContract v1 (Phase 14.6+ — schema reserved) + +**HTTP:** `POST /api/v1/refund-requests` + +```json +{ + "contract_version": "refund_intent.v1", + "payment_transaction_id": "uuid", + "amount_minor": 0, + "reason": "string", + "source_ref": { "service": "string", "ref_type": "string", "ref_id": "uuid" }, + "idempotency_key": "string" +} +``` + +**Events:** `payment.refund.requested`, `payment.refund.succeeded`, `payment.refund.failed` + +--- + +### 4.6 SplitAllocationContract v1 (Phase 14.7+ — schema reserved) + +Embedded in capture or post-capture allocation: + +```json +{ + "contract_version": "split_allocation.v1", + "payment_transaction_id": "uuid", + "allocations": [ + { + "party_ref": { "type": "merchant_account|seller_ref|platform", "id": "uuid" }, + "amount_minor": 0, + "description": "string" + } + ], + "remainder_to": "platform|primary_merchant" +} +``` + +**Events:** `payment.split.allocated`, `payment.settlement_batch.created` + +--- + +### 4.7 SubscriptionBillingContract v1 (Future — schema reserved) + +```json +{ + "contract_version": "subscription_billing.v1", + "subscription_ref": { "service": "string", "ref_id": "uuid" }, + "billing_cycle": "monthly|annual|custom", + "amount_minor": 0, + "currency": "IRR", + "payment_method_assignment_id": "uuid", + "trial_days": 0 +} +``` + +Recurring charge execution deferred to future Payment phase; contract stable for Marketplace/CRM/Experience planning. + +--- + +### 4.8 WalletTopUpContract v1 (Future — schema reserved) + +Payment captures external money; Loyalty owns wallet ledger ([ADR-011](../architecture/adr/ADR-011.md)). + +```json +{ + "contract_version": "wallet_topup.v1", + "loyalty_wallet_account_ref": "uuid", + "amount_minor": 0, + "currency": "IRR", + "payment_intent_id": "uuid" +} +``` + +**Event (handoff):** `payment.transaction.recorded` with `metadata.intent_kind=wallet_topup` — Loyalty subscribes. + +--- + +### 4.9 InstallmentPlanContract v1 (Future — Payment schedule shell only) + +> **Not the Credit Engine.** Installment **calculation**, credit **scoring**, **KYC**, **BNPL** underwriting, and financing agreements are exclusive to the future **Torbat Credit** service (`credit`). This contract is a Payment-side **schedule shell** linked to a payment request or `installment_contract_reference` from Torbat Credit. + +```json +{ + "contract_version": "installment_plan.v1", + "source_ref": { "service": "string", "ref_type": "order|invoice", "ref_id": "uuid" }, + "total_amount_minor": 0, + "currency": "IRR", + "installments": [ + { "sequence": 1, "due_at": "ISO-8601", "amount_minor": 0, "payment_request_ref": "uuid|null" } + ], + "provider_assignment_id": "uuid", + "payment_source": "PSP|CREDIT_PROVIDER", + "credit_provider_id": "uuid|null", + "installment_contract_reference": "string|null" +} +``` + +Payment records due dates and links to capture requests; Torbat Credit owns financing terms and authorization. + +--- + +### 4.10 CreditProviderAdapter pattern (Future — architecture reserved) + +Torbat Credit (`credit` service, commercial product **Torbat Credit**) integrates as an **external Credit Provider** — invoked by Payment identically to a PSP adapter: + +| Adapter method | Purpose | +| --- | --- | +| `authorize_financing()` | Reserve credit / BNPL authorization | +| `capture_financing()` | Capture against authorization | +| `verify_authorization()` | Reconcile status | +| `parse_callback()` | Normalize credit provider webhooks | + +Registry entry example: + +```json +{ + "provider_code": "torbat_credit", + "provider_family": "credit_bnpl", + "service_ref": "credit", + "adapter_version": "1", + "supports_payment_source": "CREDIT_PROVIDER" +} +``` + +Payment calls Torbat Credit via **internal service API + adapter protocol** — never direct DB access. + +--- + +### 4.11 Reserved event namespace — `credit.*` (Torbat Credit only) + +The **`credit.*` event namespace is reserved** for the future independent **Torbat Credit** service. Payment **does not** publish or consume `credit.*` events in current or HIGH-priority phases. + +Examples (documentation only — no implementation): + +| event_type | Owner | +| --- | --- | +| `credit.application.submitted` | Torbat Credit | +| `credit.authorization.approved` | Torbat Credit | +| `credit.authorization.declined` | Torbat Credit | +| `credit.financing.created` | Torbat Credit | +| `credit.installment.due` | Torbat Credit | +| `credit.installment.paid` | Torbat Credit | + +Payment integrates via adapter callbacks and opaque refs; cross-service coordination uses `payment.*` events on the Payment side. + +--- + +## 5. Service compatibility matrix + +| Service | Consumes (API) | Consumes (Events) | Provides to Payment | Compatible | Notes | +| --- | --- | --- | --- | --- | --- | +| **Core** | — | `feature_access.changed` | Entitlement check API | ✅ | L1 gate; service registry | +| **Identity** | — | — | `user_ref` validation (optional) | ✅ | Payer refs only | +| **Tenant (Core)** | — | `tenant.suspended` | Tenant id context | ✅ | Suspend → workspace suspend | +| **Accounting** | Posting intent ingest (future) | `payment.accounting_intent.created` | COA/account refs | ✅ | No journal in Payment | +| **CRM** | — | optional | `contact_ref` | ✅ | Payer linkage | +| **Communication** | Send notify (client) | — | Templates | ✅ | Receipt/refund SMS | +| **Hospitality** | PaymentIntent/Checkout | `payment.request.paid` | `pos_ticket` refs | ✅ | Replace POS PSP embed long-term | +| **Delivery** | PaymentIntent (optional) | `payment.request.paid` | COD job refs | ✅ | Optional COD | +| **Sports Center** | PaymentIntent | `payment.request.paid` | membership refs | ✅ | No local PSP | +| **Marketplace** | Checkout + Split | `payment.*` | seller/order refs | ✅ | Splits need bundle L2 | +| **Experience** | CheckoutSession | `payment.request.paid` | form/checkout refs | ✅ | Paid forms/widgets | +| **Loyalty** | WalletTopUp (future) | `payment.transaction.recorded` | wallet refs | ✅ | Wallet ledger stays Loyalty | +| **Torbat Credit** (future) | CreditProviderAdapter from Payment | `credit.*` (Credit service only) | authorization/financing refs | ✅ | Scoring/KYC/BNPL exclusive to Credit | +| **Healthcare** | PaymentIntent (future) | `payment.request.paid` | appointment refs | ✅ | Same pattern as Beauty | +| **Beauty Business** | PaymentIntent (future) | `payment.request.paid` | appointment refs | ✅ | Same pattern | +| **Automation** | — | `payment.transaction.recorded` | — | ✅ | Trigger workflows | + +**No breaking changes required** in listed services before `payment-14.0`; vertical connector phases adopt contracts incrementally (Payment 14.8). + +--- + +## 6. Event catalog (Payment — planned) + +| event_type | When | Primary consumers | +| --- | --- | --- | +| `payment.workspace.enabled` | Workspace activated | Core audit | +| `payment.workspace.disabled` | Payment disabled for tenant | Verticals cache | +| `payment.bundle.activated` | L2 bundle on | Frontend capabilities | +| `payment.feature_toggle.changed` | L3 toggle | Internal | +| `payment.psp_connection.activated` | BYO PSP live | — | +| `payment.merchant_account.activated` | Facilitator ready | — | +| `payment.provider_assignment.changed` | PSP switch | — | +| `payment.request.created` | Intent created | Vertical | +| `payment.request.paid` | Verified paid | Hospitality, Marketplace, … | +| `payment.request.failed` | Failed/expired | Vertical | +| `payment.transaction.recorded` | Ledger write | Accounting prep, Loyalty | +| `payment.refund.succeeded` | Refund done | Vertical, Communication | +| `payment.split.allocated` | Split computed | Marketplace | +| `payment.accounting_intent.created` | Settlement intent | Accounting | +| `payment.reconciliation.completed` | Recon done | Admin | + +Full envelope: [event-catalog.md](event-catalog.md#payment-platform-planned). + +--- + +## 7. Internal service API (token-gated) + +| Scope | Path | Purpose | +| --- | --- | --- | +| `payment.requests.create` | POST `/internal/v1/payment-requests` | Vertical server-side initiate | +| `payment.transactions.read` | GET `/internal/v1/payment-transactions/{id}` | Status poll | +| `payment.capabilities.read` | GET `/internal/v1/capabilities` | Service discovery | + +Requires Internal Service Token ([services-contracts.md](services-contracts.md)) + `X-Tenant-ID`. + +--- + +## 8. Permission tree (foundation + growth) + +``` +payment.* +├── payment.workspaces.{view,manage,enable,disable} +├── payment.bundles.{view,manage} +├── payment.feature_toggles.{view,manage} +├── payment.configurations.{view,manage} +├── payment.settings.{view,manage} +├── payment.psp_providers.view +├── payment.psp_connections.{view,create,update,delete,test,manage} [bundle: byo_psp.basic] +├── payment.psp_routing.{view,manage} [bundle: byo_psp.basic] +├── payment.provider_assignments.{view,manage} +├── payment.merchant_accounts.{view,create,update,submit,activate,suspend,close,manage} [bundle: torbat_pay.merchant] +├── payment.requests.{view,create,cancel,manage} +├── payment.callbacks.view +├── payment.transactions.{view,export} +├── payment.ledger.view +├── payment.refunds.{view,create,manage} [bundle: advanced.refunds] +├── payment.splits.{view,manage} [bundle: marketplace.splits] +├── payment.settlement_batches.{view,manage} +├── payment.connectors.manage +├── payment.checkout_sessions.{view,create} +├── payment.reconciliation.{view,run,manage} [bundle: advanced.reconciliation] +├── payment.accounting_intents.view +├── payment.analytics.{view,refresh} +├── payment.fraud_rules.manage +└── payment.audit.view +``` + +--- + +## 9. Capability discovery (`GET /capabilities`) + +Response includes (when implemented): + +```json +{ + "service": "payment", + "phase": "14.x", + "workspace_status": "enabled|disabled", + "default_payment_mode": "byo_psp|torbat_pay_merchant", + "active_bundles": ["payment.byo_psp.basic"], + "feature_toggles": { "payment.refunds.enabled": false }, + "contract_versions": { + "payment_intent": "v1", + "checkout_session": "v1", + "settlement_intent": "v1", + "installment_plan": "v1", + "credit_authorization": "v1" + }, + "payment_sources_supported": ["PSP", "MERCHANT_FACILITATOR"], + "payment_sources_reserved": ["CREDIT_PROVIDER"], + "credit_providers_registered": [], +} +``` + +--- + +## 10. Related documents + +- [Payment Roadmap](../payment-roadmap.md) +- [ADR-020](../architecture/adr/ADR-020.md) · [ADR-021](../architecture/adr/ADR-021.md) +- [Module Registry — payment](../module-registry.md#payment) +- [Provider Reference](provider-reference.md) (PSP entries added at 14.1) +- [Event Catalog](event-catalog.md) diff --git a/docs/reference/services-contracts.md b/docs/reference/services-contracts.md index 1b46778..504fd41 100644 --- a/docs/reference/services-contracts.md +++ b/docs/reference/services-contracts.md @@ -273,3 +273,33 @@ Base URL: `http://identity-access-service:8001` (Docker) یا `http://localhost: سرویس‌های داخلی آینده باید خود را در `service_registry` ثبت کنند (`service_key`, `base_url`, `health_check_url`, `status`) تا discovery و health-check ممکن باشد. + +## ۱۰. Payment Platform (Torbat Pay) — planned + +> قراردادهای کامل: [payment-contracts.md](payment-contracts.md) · ADR: [ADR-020](../architecture/adr/ADR-020.md), [ADR-021](../architecture/adr/ADR-021.md) + +### Entitlement (Core L1) +| feature_key | Meaning | +| --- | --- | +| `payment.module.enabled` | Tenant may use Torbat Pay | + +### Integration rules +1. Verticals **never** store PSP credentials — only `payment_request_ref`, `payment_transaction_ref`, or `checkout_session_ref`. +2. Paid state: subscribe to `payment.request.paid` or poll `GET /api/v1/payment-transactions/{id}` with idempotency. +3. Settlement: consume `payment.accounting_intent.created` in Accounting — Payment does not post journals. +4. Internal calls: Internal Service Token + `X-Tenant-ID` + scope e.g. `payment.requests.create`. + +### Contract versions (stable before implementation) +`payment_intent.v1`, `checkout_session.v1`, `callback_ingress.v1`, `settlement_intent.v1`, plus reserved: `refund_intent.v1`, `split_allocation.v1`, `subscription_billing.v1`, `wallet_topup.v1`, `installment_plan.v1`. + +## ۱۱. Published Resource (`publish_id`) — architecture contracts + +> قراردادهای کامل: [published-resource-contracts.md](published-resource-contracts.md) · ADR: [ADR-022](../architecture/adr/ADR-022.md) · Architecture: [published-resource-architecture.md](../architecture/published-resource-architecture.md) + +### قوانین +1. هر سطح عمومی قابل‌اتصال برای سرویس‌های دیگر باید `publish_id` داشته باشد. +2. Payment، Communication، Short Link، QR، Analytics، CRM و verticalها فقط با `publish_id` وصل می‌شوند — نه جداول داخلی Experience. +3. Form / Survey / Appointment باید standalone publish شوند (وابستگی اجباری به Site/Page ممنوع). +4. Published Actions، Public Access، و Universal Embed قراردادهای additive هستند — رجیستری‌ها بازند؛ جزئیات در [published-action-registry.md](published-action-registry.md)، [public-access-contract.md](public-access-contract.md)، [embed-contract.md](embed-contract.md). +5. Experience مالک authentication / payment / membership نیست. +6. پیاده‌سازی Registry/API تا ثبت فاز آینده ممنوع است (این بخش فقط قرارداد است). diff --git a/docs/service-snapshots/payment.yaml b/docs/service-snapshots/payment.yaml new file mode 100644 index 0000000..71b10e0 --- /dev/null +++ b/docs/service-snapshots/payment.yaml @@ -0,0 +1,255 @@ +# Service Snapshot — Payment Platform (Torbat Pay MVP 14.0–14.5) + +# Normative spec: docs/ai-framework/service-snapshot-policy.md + +schema_version: 1 +snapshot_version: 4 + +service_name: payment +commercial_product: Torbat Pay +current_version: "0.14.5.0" +current_phase: payment-14.5 +last_completed_phase: payment-14.5 +next_phase: payment-14.6 + +completed_phases: + - payment-reg + - payment-arch + - payment-14.0 + - payment-14.1 + - payment-14.2 + - payment-14.3 + - payment-14.4 + - payment-14.5 + +remaining_phases: + - payment-14.6 + - payment-14.7 + - payment-14.8 + - payment-14.9 + - payment-14.10 + +registered_modules: + - payment.foundation + - payment.workspaces + - payment.bundle_definitions + - payment.tenant_bundles + - payment.feature_toggles + - payment.provider_assignments + - payment.psp_registry + - payment.credit_provider_registry + - payment.psp_connections + - payment.psp_routing + - payment.merchant_accounts + - payment.requests + - payment.callbacks + - payment.transactions + - payment.ledger + - payment.audit + - payment.configuration + - payment.refunds + - payment.splits + - payment.settlement_batches + - payment.connectors + - payment.checkout_sessions + - payment.reconciliation + - payment.accounting_intents + - payment.analytics + - payment.fraud_hooks + +enabled_capabilities: + - payment.foundation + - payment.workspaces + - payment.bundles + - payment.feature_toggles + - payment.provider_assignments + - payment.psp_registry + - payment.psp_connections + - payment.psp_routing + - payment.merchant_accounts + - payment.requests + - payment.callbacks + - payment.transactions + - payment.ledger + - payment.audit + - payment.configuration + +enabled_bundles: + - payment.byo_psp.basic + - payment.torbat_pay.merchant + +public_apis: + - GET /health + - GET /capabilities + - GET /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/credit-provider-registrations + - /api/v1/configurations + - /api/v1/settings + - /api/v1/audit + - /api/v1/permissions/catalog + - /api/v1/psp-connections + - /api/v1/psp-routing-policies + - /api/v1/merchant-accounts + - /api/v1/merchant-settlement-profiles + - /api/v1/payment-requests + - /api/v1/callbacks/{provider_code}/{connection_id} + - /api/v1/payment-transactions + - /api/v1/payment-ledger-entries + +published_events: + - payment.request.created + - payment.request.redirect_issued + - payment.request.paid + - payment.request.failed + - payment.transaction.recorded + +permission_prefix: payment.* + +active_adrs: + - ADR-001 + - ADR-003 + - ADR-006 + - ADR-010 + - ADR-012 + - ADR-020 + - ADR-021 + +payment_sources: + implemented: + - PSP + - MERCHANT_FACILITATOR + reserved: + - CREDIT_PROVIDER + +credit_reference_fields: + - credit_provider_id + - financing_reference + - credit_authorization_reference + - installment_contract_reference + +reserved_future_services: + - service_id: credit + commercial_product: Torbat Credit + database: credit_db + event_namespace: credit.* + integration_pattern: CreditProviderAdapter from Payment (same as PSP provider pattern) + ownership: scoring, KYC, BNPL, financing, installment calculation — exclusive to Torbat Credit + payment_ownership: schedule shell via InstallmentPlanContract only; opaque refs on intents + +reserved_event_namespaces: + - credit.* + owner: credit + note: Payment does not publish or consume credit.* in current implementation + +integration_contracts: + - contract: payment_intent.v1 + phase: payment-14.3 + status: implemented + doc: docs/reference/payment-contracts.md + includes_credit_refs: true + - contract: callback_ingress.v1 + phase: payment-14.4 + status: implemented + doc: docs/reference/payment-contracts.md + - contract: checkout_session.v1 + phase: payment-14.8 + doc: docs/reference/payment-contracts.md + status: reserved + - contract: settlement_intent.v1 + phase: payment-14.9 + doc: docs/reference/payment-contracts.md + status: reserved + - contract: refund_intent.v1 + phase: payment-14.6 + status: reserved + - contract: split_allocation.v1 + phase: payment-14.7 + status: reserved + - contract: subscription_billing.v1 + phase: future + status: reserved + - contract: wallet_topup.v1 + phase: future + status: reserved + - contract: installment_plan.v1 + phase: future + status: reserved + note: Payment schedule shell only — not Credit Engine + - contract: credit_authorization.v1 + phase: future + status: reserved + consumer: payment via CreditProviderAdapter + - service: credit + pattern: external Credit Provider; adapter registry like PSP; not implemented + status: architecture reserved + - service: core-platform + pattern: L1 entitlement payment.module.enabled + feature_access.changed + - service: accounting + pattern: settlement_intent.v1 events — posting intents only + - service: communication + pattern: receipt/refund notify client + - service: identity + pattern: payer user_ref validation optional + - service: crm + pattern: contact_ref on payer + - service: hospitality + pattern: PaymentIntent consumer; pos_ticket refs + - service: marketplace + pattern: CheckoutSession + split_allocation.v1 + - service: sports_center + pattern: PaymentIntent; membership refs + - service: delivery + pattern: optional COD payment ref + - service: experience + pattern: CheckoutSession; paid forms + - service: loyalty + pattern: wallet_topup.v1 future consumer + +licensing_model: + l1_core_entitlement: payment.module.enabled + l2_payment_bundles: + - payment.byo_psp.basic + - payment.torbat_pay.merchant + - payment.marketplace.splits + - payment.advanced.refunds + - payment.advanced.reconciliation + - payment.future.subscriptions + - payment.future.installments + - payment.future.wallet_topup + - payment.future.credit_provider + l3_feature_toggles: + - payment.capture.enabled + - payment.refunds.enabled + - payment.splits.enabled + - payment.installments.enabled + - payment.subscriptions.enabled + - payment.wallet_topup.enabled + - payment.credit_provider.enabled + - payment.fraud_hooks.enabled + +runtime: + service_path: backend/services/payment + database: payment_db + api_port: 8012 + migration_head: "0006_phase_145_ledger" + compose_service: payment-service + +known_limitations: + - Mock PSP adapter only — live ZarinPal/IDPay integrations deferred + - CREDIT_PROVIDER payment source reserved but rejected at API (422) + - Torbat Credit service not registered or implemented + - credit.* event namespace reserved only — no producers + - Refunds, splits, connectors, reconciliation, analytics not implemented (14.6+) + - Vertical connector clients not implemented in consumer services + - Accounting journal posting remains in Accounting service only + +open_todos: [] + +last_handover_reference: docs/phase-handover/phase-14-5.md +last_updated: "2026-07-27" diff --git a/infrastructure/postgres/init-dbs.sql b/infrastructure/postgres/init-dbs.sql index 5faa2dd..9929f16 100644 --- a/infrastructure/postgres/init-dbs.sql +++ b/infrastructure/postgres/init-dbs.sql @@ -9,3 +9,4 @@ CREATE DATABASE experience_db; CREATE DATABASE healthcare_db; CREATE DATABASE delivery_db; CREATE DATABASE beauty_business_db; +CREATE DATABASE payment_db;