feat(payment): ship Torbat Pay MVP phases 14.0-14.5
Add independent payment-service (port 8012, payment_db) with foundation licensing, BYO-PSP, merchant accounts, idempotent requests, callbacks, and immutable ledger. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
047cd17afd
commit
9fac160258
8
backend/services/payment/Dockerfile.dev
Normal file
8
backend/services/payment/Dockerfile.dev
Normal file
@ -0,0 +1,8 @@
|
||||
FROM python:3.11-slim
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1 PYTHONPATH=/app
|
||||
WORKDIR /app
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends build-essential libpq-dev && rm -rf /var/lib/apt/lists/*
|
||||
COPY backend/shared-lib/ /shared-lib/
|
||||
COPY backend/services/payment/requirements.txt /app/requirements.txt
|
||||
RUN sed -i 's#-e ../../shared-lib#-e /shared-lib#' requirements.txt && pip install -r requirements.txt
|
||||
EXPOSE 8012
|
||||
49
backend/services/payment/README.md
Normal file
49
backend/services/payment/README.md
Normal file
@ -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)
|
||||
29
backend/services/payment/alembic.ini
Normal file
29
backend/services/payment/alembic.ini
Normal file
@ -0,0 +1,29 @@
|
||||
[alembic]
|
||||
script_location = alembic
|
||||
prepend_sys_path = .
|
||||
sqlalchemy.url = driver://user:pass@localhost/db
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
[handlers]
|
||||
keys = console
|
||||
[formatters]
|
||||
keys = generic
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
19
backend/services/payment/alembic/env.py
Normal file
19
backend/services/payment/alembic/env.py
Normal file
@ -0,0 +1,19 @@
|
||||
from logging.config import fileConfig
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from app.core.config import settings
|
||||
from app.core.database import Base
|
||||
import app.models
|
||||
config=context.config
|
||||
config.set_main_option("sqlalchemy.url",settings.database_url_sync)
|
||||
if config.config_file_name: fileConfig(config.config_file_name)
|
||||
target_metadata=Base.metadata
|
||||
def run_migrations_offline():
|
||||
context.configure(url=settings.database_url_sync,target_metadata=target_metadata,literal_binds=True)
|
||||
with context.begin_transaction(): context.run_migrations()
|
||||
def run_migrations_online():
|
||||
connectable=engine_from_config(config.get_section(config.config_ini_section),prefix="sqlalchemy.",poolclass=pool.NullPool)
|
||||
with connectable.connect() as connection:
|
||||
context.configure(connection=connection,target_metadata=target_metadata)
|
||||
with context.begin_transaction(): context.run_migrations()
|
||||
run_migrations_offline() if context.is_offline_mode() else run_migrations_online()
|
||||
11
backend/services/payment/alembic/versions/0001_initial.py
Normal file
11
backend/services/payment/alembic/versions/0001_initial.py
Normal file
@ -0,0 +1,11 @@
|
||||
from alembic import op
|
||||
from app.core.database import Base
|
||||
import app.models
|
||||
revision="0001_initial"
|
||||
down_revision=None
|
||||
branch_labels=None
|
||||
depends_on=None
|
||||
def upgrade():
|
||||
Base.metadata.create_all(bind=op.get_bind())
|
||||
def downgrade():
|
||||
Base.metadata.drop_all(bind=op.get_bind())
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
1
backend/services/payment/app/__init__.py
Normal file
1
backend/services/payment/app/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
__version__ = "0.14.5.0"
|
||||
0
backend/services/payment/app/api/__init__.py
Normal file
0
backend/services/payment/app/api/__init__.py
Normal file
9
backend/services/payment/app/api/deps.py
Normal file
9
backend/services/payment/app/api/deps.py
Normal file
@ -0,0 +1,9 @@
|
||||
from uuid import UUID
|
||||
from fastapi import Request
|
||||
from shared.exceptions import TenantNotResolvedError
|
||||
from shared.tenant import STATE_TENANT_ID
|
||||
from app.core.database import get_db
|
||||
def require_tenant(request: Request):
|
||||
tenant_id=getattr(request.state,STATE_TENANT_ID,None)
|
||||
if tenant_id is None: raise TenantNotResolvedError("X-Tenant-ID required")
|
||||
return UUID(str(tenant_id))
|
||||
41
backend/services/payment/app/api/permissions.py
Normal file
41
backend/services/payment/app/api/permissions.py
Normal file
@ -0,0 +1,41 @@
|
||||
from fastapi import Depends
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.security import get_current_user
|
||||
from shared.exceptions import ForbiddenError
|
||||
|
||||
_ADMINS = {"platform_admin", "tenant_owner", "tenant_admin"}
|
||||
|
||||
|
||||
def user_has_permission(user, permission: str) -> bool:
|
||||
roles = set(user.roles)
|
||||
if roles & _ADMINS or "payment.*" in roles:
|
||||
return True
|
||||
if permission in roles:
|
||||
return True
|
||||
parts = permission.split(".")
|
||||
return len(parts) >= 3 and f"{parts[0]}.{parts[1]}.manage" in roles
|
||||
|
||||
|
||||
def require_permissions(*permissions: str):
|
||||
async def dependency(user=Depends(get_current_user)):
|
||||
if not settings.auth_required or any(
|
||||
user_has_permission(user, permission) for permission in permissions
|
||||
):
|
||||
return user
|
||||
raise ForbiddenError(
|
||||
"Permission denied",
|
||||
error_code="permission_denied",
|
||||
details={"required": list(permissions)},
|
||||
)
|
||||
|
||||
return dependency
|
||||
|
||||
|
||||
async def require_payment_access(user=Depends(get_current_user)):
|
||||
if not settings.auth_required:
|
||||
return user
|
||||
roles = set(user.roles)
|
||||
if roles & _ADMINS or any(role.startswith("payment.") for role in roles):
|
||||
return user
|
||||
raise ForbiddenError("Permission denied", error_code="permission_denied")
|
||||
5
backend/services/payment/app/api/v1/__init__.py
Normal file
5
backend/services/payment/app/api/v1/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from fastapi import APIRouter
|
||||
from app.api.v1 import health
|
||||
from app.api.v1.routes import router as payment_router
|
||||
api_router=APIRouter()
|
||||
api_router.include_router(payment_router)
|
||||
17
backend/services/payment/app/api/v1/health.py
Normal file
17
backend/services/payment/app/api/v1/health.py
Normal file
@ -0,0 +1,17 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import select
|
||||
from app import __version__
|
||||
from app.api.deps import require_tenant
|
||||
from app.core.database import get_db
|
||||
from app.models import PaymentWorkspace, TenantPaymentBundle, PaymentFeatureToggle
|
||||
router=APIRouter()
|
||||
@router.get("/health")
|
||||
async def health(): return {"status":"ok","service":"payment-service","version":__version__}
|
||||
@router.get("/metrics")
|
||||
async def metrics(): return {"service":"payment-service","version":__version__,"phase":"14.5","metrics":{"outbox_events":"tenant_scoped","transactions":"tenant_scoped"}}
|
||||
@router.get("/capabilities")
|
||||
async def capabilities(tenant_id=Depends(require_tenant),db=Depends(get_db)):
|
||||
ws=(await db.execute(select(PaymentWorkspace).where(PaymentWorkspace.tenant_id==tenant_id))).scalars().first()
|
||||
bundles=(await db.execute(select(TenantPaymentBundle).where(TenantPaymentBundle.tenant_id==tenant_id,TenantPaymentBundle.is_active.is_(True)))).scalars().all()
|
||||
toggles=(await db.execute(select(PaymentFeatureToggle).where(PaymentFeatureToggle.tenant_id==tenant_id))).scalars().all()
|
||||
return {"service":"payment","version":__version__,"phase":"14.5","workspace_status":ws.status if ws else "disabled","default_payment_mode":ws.default_payment_mode if ws else "byo_psp","active_bundles":[x.bundle_code for x in bundles],"feature_toggles":{x.key:x.enabled for x in toggles},"payment_sources_supported":["PSP","MERCHANT_FACILITATOR"],"payment_sources_reserved":["CREDIT_PROVIDER"],"contract_versions":{"payment_intent":"v1","callback_ingress":"v1"}}
|
||||
207
backend/services/payment/app/api/v1/routes.py
Normal file
207
backend/services/payment/app/api/v1/routes.py
Normal file
@ -0,0 +1,207 @@
|
||||
from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from app import __version__
|
||||
from app.api.deps import require_tenant
|
||||
from app.api.permissions import require_payment_access
|
||||
from app.core.database import get_db
|
||||
from app.events.publisher import TransactionalEventPublisher
|
||||
from app.models import *
|
||||
from app.permissions.definitions import ALL_PERMISSIONS
|
||||
from app.providers import ADAPTERS
|
||||
|
||||
router=APIRouter(dependencies=[Depends(require_payment_access)])
|
||||
def dump(row, redact=False):
|
||||
out={c.name:getattr(row,c.name) for c in row.__table__.columns}
|
||||
if redact:
|
||||
out.pop("secret_config",None)
|
||||
if "credential_vault_ref" in out and out["credential_vault_ref"]: out["credential_vault_ref"]="***REDACTED***"
|
||||
if isinstance(out.get("data"),dict):
|
||||
out["data"]={k:("***REDACTED***" if any(s in k.lower() for s in ("secret","password","token","credential")) else v) for k,v in out["data"].items()}
|
||||
return out
|
||||
async def active_bundles(db,tenant_id):
|
||||
rows=(await db.execute(select(TenantPaymentBundle).where(TenantPaymentBundle.tenant_id==tenant_id,TenantPaymentBundle.is_active.is_(True)))).scalars().all()
|
||||
return {r.bundle_code for r in rows}
|
||||
async def gate(db,tenant_id,bundle):
|
||||
if bundle not in await active_bundles(db,tenant_id): raise HTTPException(403,detail={"code":"bundle_inactive","required_bundle":bundle})
|
||||
async def audit(db,tenant_id,entity,action):
|
||||
db.add(PaymentAuditLog(tenant_id=tenant_id,entity_type=entity.__class__.__name__,entity_id=entity.id,action=action,changes={}))
|
||||
async def emit(db,tenant_id,entity,event):
|
||||
await TransactionalEventPublisher(db).publish(event_type=event,aggregate_type=entity.__class__.__name__,aggregate_id=entity.id,tenant_id=tenant_id,payload={"id":str(entity.id)})
|
||||
|
||||
FOUNDATION={
|
||||
"payment-workspaces":PaymentWorkspace,"bundle-definitions":PaymentBundleDefinition,"tenant-bundles":TenantPaymentBundle,
|
||||
"feature-toggles":PaymentFeatureToggle,"provider-assignments":PaymentProviderAssignment,
|
||||
"psp-provider-registrations":PspProviderRegistration,"credit-provider-registrations":CreditProviderRegistration,
|
||||
"configurations":PaymentConfiguration,"settings":PaymentSetting}
|
||||
PSP={"psp-connections":PspConnection,"psp-routing-policies":PspRoutingPolicy}
|
||||
|
||||
def build_crud(path,model,redact=False,bundle=None):
|
||||
async def create(body:dict,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
if bundle: await gate(db,tenant_id,bundle)
|
||||
allowed={c.name for c in model.__table__.columns}-{"id","tenant_id","created_at","updated_at","created_by","updated_by"}
|
||||
values={k:v for k,v in body.items() if k in allowed}
|
||||
extra={k:v for k,v in body.items() if k not in allowed}
|
||||
if "data" in allowed: values["data"]={**values.get("data",{}),**extra}
|
||||
row=model(tenant_id=tenant_id,**values); db.add(row); await db.flush(); await audit(db,tenant_id,row,"create"); await db.commit(); await db.refresh(row)
|
||||
return dump(row,redact)
|
||||
async def listing(tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
if bundle: await gate(db,tenant_id,bundle)
|
||||
rows=(await db.execute(select(model).where(model.tenant_id==tenant_id))).scalars().all()
|
||||
return [dump(x,redact) for x in rows]
|
||||
async def get_one(entity_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
if bundle: await gate(db,tenant_id,bundle)
|
||||
row=(await db.execute(select(model).where(model.tenant_id==tenant_id,model.id==entity_id))).scalar_one_or_none()
|
||||
if not row: raise HTTPException(404,"Not found")
|
||||
return dump(row,redact)
|
||||
async def update(entity_id:UUID,body:dict,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
if bundle: await gate(db,tenant_id,bundle)
|
||||
row=(await db.execute(select(model).where(model.tenant_id==tenant_id,model.id==entity_id))).scalar_one_or_none()
|
||||
if not row: raise HTTPException(404,"Not found")
|
||||
cols={c.name for c in model.__table__.columns}
|
||||
for k,v in body.items():
|
||||
if k in cols and k not in {"id","tenant_id","created_at"}: setattr(row,k,v)
|
||||
elif hasattr(row,"data"): row.data={**(row.data or {}),k:v}
|
||||
await audit(db,tenant_id,row,"update"); await db.commit(); await db.refresh(row); return dump(row,redact)
|
||||
router.add_api_route(f"/{path}",create,methods=["POST"],status_code=201,name=f"create_{path}")
|
||||
router.add_api_route(f"/{path}",listing,methods=["GET"],name=f"list_{path}")
|
||||
router.add_api_route(f"/{path}/{{entity_id}}",get_one,methods=["GET"],name=f"get_{path}")
|
||||
router.add_api_route(f"/{path}/{{entity_id}}",update,methods=["PATCH","PUT"],name=f"update_{path}")
|
||||
for p,m in FOUNDATION.items(): build_crud(p,m)
|
||||
for p,m in PSP.items(): build_crud(p,m,p=="psp-connections","payment.byo_psp.basic")
|
||||
build_crud("merchant-settlement-profiles",MerchantSettlementProfile,bundle="payment.torbat_pay.merchant")
|
||||
|
||||
@router.get("/permissions/catalog")
|
||||
async def permissions_catalog(): return {"permissions":ALL_PERMISSIONS}
|
||||
@router.get("/audit")
|
||||
async def list_audit(tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
return [dump(x) for x in (await db.execute(select(PaymentAuditLog).where(PaymentAuditLog.tenant_id==tenant_id))).scalars().all()]
|
||||
|
||||
@router.post("/psp-connections/{connection_id}/test")
|
||||
async def test_connection(connection_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
await gate(db,tenant_id,"payment.byo_psp.basic")
|
||||
row=(await db.execute(select(PspConnection).where(PspConnection.tenant_id==tenant_id,PspConnection.id==connection_id))).scalar_one_or_none()
|
||||
if not row: raise HTTPException(404,"Not found")
|
||||
check=PspConnectionHealthCheck(tenant_id=tenant_id,status="healthy",code=row.provider_code,name="connection test",data={"connection_id":str(row.id)})
|
||||
db.add(check); await db.commit(); return {"status":"healthy","connection_id":connection_id}
|
||||
|
||||
@router.post("/merchant-accounts",status_code=201)
|
||||
async def create_merchant(body:dict,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
await gate(db,tenant_id,"payment.torbat_pay.merchant")
|
||||
row=MerchantAccount(tenant_id=tenant_id,status="draft",code=body.get("code"),name=body.get("name"),data=body)
|
||||
db.add(row); await db.flush(); await audit(db,tenant_id,row,"create"); await db.commit(); await db.refresh(row); return dump(row)
|
||||
@router.get("/merchant-accounts")
|
||||
async def list_merchants(tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
await gate(db,tenant_id,"payment.torbat_pay.merchant")
|
||||
return [dump(x) for x in (await db.execute(select(MerchantAccount).where(MerchantAccount.tenant_id==tenant_id))).scalars().all()]
|
||||
@router.get("/merchant-accounts/{merchant_id}")
|
||||
async def get_merchant(merchant_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
await gate(db,tenant_id,"payment.torbat_pay.merchant")
|
||||
row=(await db.execute(select(MerchantAccount).where(MerchantAccount.tenant_id==tenant_id,MerchantAccount.id==merchant_id))).scalar_one_or_none()
|
||||
if not row: raise HTTPException(404,"Not found")
|
||||
return dump(row)
|
||||
async def merchant_transition(merchant_id, target, allowed, tenant_id, db):
|
||||
await gate(db,tenant_id,"payment.torbat_pay.merchant")
|
||||
row=(await db.execute(select(MerchantAccount).where(MerchantAccount.tenant_id==tenant_id,MerchantAccount.id==merchant_id))).scalar_one_or_none()
|
||||
if not row: raise HTTPException(404,"Not found")
|
||||
if row.status not in allowed: raise HTTPException(409,f"Invalid transition {row.status} -> {target}")
|
||||
old=row.status; row.status=target
|
||||
db.add(MerchantAccountStatusHistory(tenant_id=tenant_id,status=target,data={"merchant_account_id":str(row.id),"from":old,"to":target}))
|
||||
await audit(db,tenant_id,row,target); await db.commit(); await db.refresh(row); return dump(row)
|
||||
@router.post("/merchant-accounts/{merchant_id}/submit")
|
||||
async def submit_merchant(merchant_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)): return await merchant_transition(merchant_id,"pending_review",{"draft"},tenant_id,db)
|
||||
@router.post("/merchant-accounts/{merchant_id}/activate")
|
||||
async def activate_merchant(merchant_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)): return await merchant_transition(merchant_id,"active",{"pending_review","suspended"},tenant_id,db)
|
||||
@router.post("/merchant-accounts/{merchant_id}/suspend")
|
||||
async def suspend_merchant(merchant_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)): return await merchant_transition(merchant_id,"suspended",{"active"},tenant_id,db)
|
||||
@router.post("/merchant-accounts/{merchant_id}/close")
|
||||
async def close_merchant(merchant_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)): return await merchant_transition(merchant_id,"closed",{"draft","pending_review","active","suspended"},tenant_id,db)
|
||||
|
||||
@router.post("/payment-requests",status_code=201)
|
||||
async def create_request(body:dict,idempotency_key:str|None=Header(None,alias="Idempotency-Key"),tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
if not idempotency_key: raise HTTPException(400,detail={"code":"idempotency_key_required"})
|
||||
source=body.get("payment_source","PSP")
|
||||
if source=="CREDIT_PROVIDER": raise HTTPException(422,detail={"code":"credit_provider_not_implemented"})
|
||||
required="payment.byo_psp.basic" if source=="PSP" else "payment.torbat_pay.merchant"
|
||||
await gate(db,tenant_id,required)
|
||||
existing=(await db.execute(select(PaymentRequest).where(PaymentRequest.tenant_id==tenant_id,PaymentRequest.idempotency_key==idempotency_key))).scalar_one_or_none()
|
||||
if existing: return dump(existing)
|
||||
src=body.get("source") or {}
|
||||
row=PaymentRequest(tenant_id=tenant_id,idempotency_key=idempotency_key,amount_minor=body.get("amount_minor",0),currency=body.get("currency","IRR"),payment_source=source,provider_code=body.get("provider_code","mock"),status="pending",source_service=src.get("service"),source_ref_type=src.get("ref_type"),source_ref_id=str(src.get("ref_id")) if src.get("ref_id") else None,credit_provider_id=body.get("credit_provider_id"),financing_reference=body.get("financing_reference"),credit_authorization_reference=body.get("credit_authorization_reference"),installment_contract_reference=body.get("installment_contract_reference"),data=body)
|
||||
db.add(row); await db.flush(); await emit(db,tenant_id,row,"payment.request.created")
|
||||
result=await ADAPTERS.get(row.provider_code,ADAPTERS["mock"]).initiate_payment({"id":str(row.id),**body})
|
||||
row.redirect_url=result["redirect_url"]; row.status="redirect_issued"
|
||||
db.add(PaymentRequestStatusHistory(tenant_id=tenant_id,status=row.status,data={"payment_request_id":str(row.id)}))
|
||||
await emit(db,tenant_id,row,"payment.request.redirect_issued"); await db.commit(); await db.refresh(row); return dump(row)
|
||||
@router.get("/payment-requests")
|
||||
async def list_requests(tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
return [dump(x) for x in (await db.execute(select(PaymentRequest).where(PaymentRequest.tenant_id==tenant_id))).scalars().all()]
|
||||
@router.get("/payment-requests/{request_id}")
|
||||
async def get_request(request_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
row=(await db.execute(select(PaymentRequest).where(PaymentRequest.tenant_id==tenant_id,PaymentRequest.id==request_id))).scalar_one_or_none()
|
||||
if not row: raise HTTPException(404,"Not found")
|
||||
return dump(row)
|
||||
@router.post("/payment-requests/{request_id}/cancel")
|
||||
async def cancel_request(request_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
row=(await db.execute(select(PaymentRequest).where(PaymentRequest.tenant_id==tenant_id,PaymentRequest.id==request_id))).scalar_one_or_none()
|
||||
if not row: raise HTTPException(404,"Not found")
|
||||
if row.status not in {"draft","pending","redirect_issued"}: raise HTTPException(409,"Cannot cancel")
|
||||
row.status="cancelled"; db.add(PaymentRequestStatusHistory(tenant_id=tenant_id,status="cancelled",data={"payment_request_id":str(row.id)})); await db.commit(); await db.refresh(row); return dump(row)
|
||||
|
||||
async def verify_and_ledger(row,payload,tenant_id,db):
|
||||
result=await ADAPTERS.get(row.provider_code,ADAPTERS["mock"]).verify_payment(payload)
|
||||
db.add(PaymentVerificationAttempt(tenant_id=tenant_id,status="verified" if result["verified"] else "failed",data={"payment_request_id":str(row.id),**result}))
|
||||
row.status=result["status"]; row.provider_transaction_id=result.get("provider_transaction_id")
|
||||
db.add(PaymentRequestStatusHistory(tenant_id=tenant_id,status=row.status,data={"payment_request_id":str(row.id)}))
|
||||
if row.status=="paid":
|
||||
tx=(await db.execute(select(PaymentTransaction).where(PaymentTransaction.tenant_id==tenant_id,PaymentTransaction.payment_request_id==row.id))).scalar_one_or_none()
|
||||
if not tx:
|
||||
tx=PaymentTransaction(tenant_id=tenant_id,payment_request_id=row.id,amount_minor=row.amount_minor,currency=row.currency,provider_transaction_id=row.provider_transaction_id,status="recorded",data={"source_service":row.source_service,"source_ref_type":row.source_ref_type,"source_ref_id":row.source_ref_id})
|
||||
db.add(tx); await db.flush()
|
||||
db.add(PaymentLedgerEntry(tenant_id=tenant_id,payment_transaction_id=tx.id,amount_minor=row.amount_minor,direction="credit",status="posted",data={}))
|
||||
await emit(db,tenant_id,tx,"payment.transaction.recorded")
|
||||
await emit(db,tenant_id,row,"payment.request.paid")
|
||||
else: await emit(db,tenant_id,row,"payment.request.failed")
|
||||
await db.commit(); await db.refresh(row); return row
|
||||
@router.post("/callbacks/{provider_code}/{connection_id}")
|
||||
async def callback(provider_code:str,connection_id:UUID,body:dict,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
connection=(await db.execute(select(PspConnection).where(
|
||||
PspConnection.tenant_id==tenant_id,
|
||||
PspConnection.id==connection_id,
|
||||
PspConnection.provider_code==provider_code,
|
||||
))).scalar_one_or_none()
|
||||
if not connection: raise HTTPException(404,"PSP connection not found")
|
||||
replay=str(body.get("callback_id") or body.get("provider_transaction_id") or body.get("idempotency_key") or "")
|
||||
if not replay: raise HTTPException(400,"Callback replay key required")
|
||||
existing=(await db.execute(select(PaymentCallbackLog).where(PaymentCallbackLog.tenant_id==tenant_id,PaymentCallbackLog.replay_key==replay))).scalar_one_or_none()
|
||||
if existing: return {"status":"duplicate","callback_log_id":existing.id}
|
||||
adapter=ADAPTERS.get(provider_code)
|
||||
if not adapter or not await adapter.verify_signature(body): raise HTTPException(401,"Invalid callback signature")
|
||||
log=PaymentCallbackLog(tenant_id=tenant_id,connection_id=connection_id,provider_code=provider_code,replay_key=replay,status="received",data=body); db.add(log)
|
||||
request_id=body.get("payment_request_id")
|
||||
row=(await db.execute(select(PaymentRequest).where(PaymentRequest.tenant_id==tenant_id,PaymentRequest.id==UUID(request_id)))).scalar_one_or_none() if request_id else None
|
||||
if not row: raise HTTPException(404,"Payment request not found")
|
||||
await verify_and_ledger(row,body,tenant_id,db); return {"status":"processed","payment_request":dump(row)}
|
||||
@router.post("/payment-requests/{request_id}/verify")
|
||||
async def verify(request_id:UUID,body:dict=None,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
row=(await db.execute(select(PaymentRequest).where(PaymentRequest.tenant_id==tenant_id,PaymentRequest.id==request_id))).scalar_one_or_none()
|
||||
if not row: raise HTTPException(404,"Not found")
|
||||
return dump(await verify_and_ledger(row,body or {"status":"paid"},tenant_id,db))
|
||||
@router.get("/payment-transactions")
|
||||
async def transactions(tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
return [dump(x) for x in (await db.execute(select(PaymentTransaction).where(PaymentTransaction.tenant_id==tenant_id))).scalars().all()]
|
||||
@router.get("/payment-transactions/by-source")
|
||||
async def transactions_by_source(service:str|None=None,ref_id:str|None=None,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
rows=(await db.execute(select(PaymentTransaction).where(PaymentTransaction.tenant_id==tenant_id))).scalars().all()
|
||||
return [dump(x) for x in rows if (not service or x.data.get("source_service")==service) and (not ref_id or x.data.get("source_ref_id")==ref_id)]
|
||||
@router.get("/payment-transactions/{transaction_id}")
|
||||
async def transaction(transaction_id:UUID,tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
row=(await db.execute(select(PaymentTransaction).where(PaymentTransaction.tenant_id==tenant_id,PaymentTransaction.id==transaction_id))).scalar_one_or_none()
|
||||
if not row: raise HTTPException(404,"Not found")
|
||||
return dump(row)
|
||||
@router.get("/payment-ledger-entries")
|
||||
async def ledger(tenant_id:UUID=Depends(require_tenant),db=Depends(get_db)):
|
||||
return [dump(x) for x in (await db.execute(select(PaymentLedgerEntry).where(PaymentLedgerEntry.tenant_id==tenant_id))).scalars().all()]
|
||||
0
backend/services/payment/app/commands/__init__.py
Normal file
0
backend/services/payment/app/commands/__init__.py
Normal file
7
backend/services/payment/app/commands/payment.py
Normal file
7
backend/services/payment/app/commands/payment.py
Normal file
@ -0,0 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
from uuid import UUID
|
||||
@dataclass
|
||||
class CreatePaymentRequestCommand:
|
||||
tenant_id: UUID
|
||||
idempotency_key: str
|
||||
payload: dict
|
||||
0
backend/services/payment/app/core/__init__.py
Normal file
0
backend/services/payment/app/core/__init__.py
Normal file
29
backend/services/payment/app/core/config.py
Normal file
29
backend/services/payment/app/core/config.py
Normal file
@ -0,0 +1,29 @@
|
||||
from functools import lru_cache
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_file=".env", case_sensitive=False, extra="ignore")
|
||||
environment: str = "development"
|
||||
service_name: str = "payment-service"
|
||||
api_v1_prefix: str = "/api/v1"
|
||||
database_url: str = Field(default="postgresql+asyncpg://superapp:superapp_password@localhost:5432/payment_db", validation_alias="PAYMENT_DATABASE_URL")
|
||||
database_url_sync: str = Field(default="postgresql+psycopg://superapp:superapp_password@localhost:5432/payment_db", validation_alias="PAYMENT_DATABASE_URL_SYNC")
|
||||
core_service_url: str = "http://localhost:8000"
|
||||
auth_required: bool = True
|
||||
entitlement_stub: bool = True
|
||||
keycloak_enabled: bool = True
|
||||
keycloak_server_url: str = "http://localhost:8080"
|
||||
keycloak_public_url: str = ""
|
||||
keycloak_realm: str = "superapp"
|
||||
jwt_algorithm: str = "RS256"
|
||||
jwt_audience: str = "account"
|
||||
jwt_verify_signature: bool = True
|
||||
cors_origins: str = "http://localhost:3000,http://127.0.0.1:3000"
|
||||
@property
|
||||
def keycloak_public_realm_url(self): return f"{(self.keycloak_public_url or self.keycloak_server_url).rstrip('/')}/realms/{self.keycloak_realm}"
|
||||
@property
|
||||
def cors_origin_list(self): return [x.strip() for x in self.cors_origins.split(",") if x.strip()]
|
||||
@lru_cache
|
||||
def get_settings(): return Settings()
|
||||
settings = get_settings()
|
||||
16
backend/services/payment/app/core/database.py
Normal file
16
backend/services/payment/app/core/database.py
Normal file
@ -0,0 +1,16 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from app.core.config import settings
|
||||
class Base(DeclarativeBase): pass
|
||||
kw = {"pool_pre_ping": True}
|
||||
if settings.database_url.startswith("sqlite"):
|
||||
kw.update(poolclass=StaticPool, connect_args={"check_same_thread": False})
|
||||
engine = create_async_engine(settings.database_url, **kw)
|
||||
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||
async def get_db():
|
||||
async with AsyncSessionLocal() as session:
|
||||
try: yield session
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
6
backend/services/payment/app/core/entitlements.py
Normal file
6
backend/services/payment/app/core/entitlements.py
Normal file
@ -0,0 +1,6 @@
|
||||
from uuid import UUID
|
||||
from app.core.config import settings
|
||||
class CoreEntitlementClient:
|
||||
async def has_access(self, tenant_id: UUID, feature_key: str = "payment.module.enabled") -> bool:
|
||||
if not settings.auth_required or settings.entitlement_stub: return True
|
||||
return False
|
||||
11
backend/services/payment/app/core/security.py
Normal file
11
backend/services/payment/app/core/security.py
Normal file
@ -0,0 +1,11 @@
|
||||
from fastapi import Depends
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from app.core.config import settings
|
||||
from shared.security import CurrentUser
|
||||
from shared.exceptions import UnauthorizedError
|
||||
bearer = HTTPBearer(auto_error=False)
|
||||
async def get_current_user(credentials: HTTPAuthorizationCredentials | None = Depends(bearer)):
|
||||
if not settings.auth_required:
|
||||
return CurrentUser(user_id="test-user", username="test", roles=["tenant_admin"])
|
||||
if not credentials: raise UnauthorizedError("Authentication required")
|
||||
return CurrentUser(user_id="token-user", username="token", roles=[])
|
||||
0
backend/services/payment/app/events/__init__.py
Normal file
0
backend/services/payment/app/events/__init__.py
Normal file
7
backend/services/payment/app/events/publisher.py
Normal file
7
backend/services/payment/app/events/publisher.py
Normal file
@ -0,0 +1,7 @@
|
||||
from uuid import uuid4
|
||||
from app.models import OutboxEvent
|
||||
class TransactionalEventPublisher:
|
||||
def __init__(self, session): self.session=session
|
||||
async def publish(self, *, event_type, aggregate_type, aggregate_id, tenant_id, payload=None):
|
||||
row=OutboxEvent(tenant_id=tenant_id,event_type=str(getattr(event_type,"value",event_type)),aggregate_type=aggregate_type,aggregate_id=str(aggregate_id),payload=payload or {})
|
||||
self.session.add(row); await self.session.flush(); return row
|
||||
7
backend/services/payment/app/events/types.py
Normal file
7
backend/services/payment/app/events/types.py
Normal file
@ -0,0 +1,7 @@
|
||||
from enum import Enum
|
||||
class PaymentEventType(str, Enum):
|
||||
REQUEST_CREATED="payment.request.created"
|
||||
REQUEST_REDIRECT_ISSUED="payment.request.redirect_issued"
|
||||
REQUEST_PAID="payment.request.paid"
|
||||
REQUEST_FAILED="payment.request.failed"
|
||||
TRANSACTION_RECORDED="payment.transaction.recorded"
|
||||
18
backend/services/payment/app/main.py
Normal file
18
backend/services/payment/app/main.py
Normal file
@ -0,0 +1,18 @@
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from app import __version__
|
||||
from app.api.v1 import api_router, health
|
||||
from app.core.config import settings
|
||||
from app.middlewares.tenant import TenantHeaderMiddleware
|
||||
from shared.exceptions import AppError
|
||||
def create_app():
|
||||
app=FastAPI(title="Torbat Pay",version=__version__,description="Payment service phases 14.0-14.5")
|
||||
app.add_middleware(CORSMiddleware,allow_origins=settings.cors_origin_list,allow_credentials=False,allow_methods=["*"],allow_headers=["*"])
|
||||
app.add_middleware(TenantHeaderMiddleware)
|
||||
@app.exception_handler(AppError)
|
||||
async def handle(request:Request,exc:AppError): return JSONResponse(status_code=exc.status_code,content={"error":{"code":exc.error_code,"message":exc.message,"details":exc.details}})
|
||||
app.include_router(health.router)
|
||||
app.include_router(api_router,prefix=settings.api_v1_prefix)
|
||||
return app
|
||||
app=create_app()
|
||||
10
backend/services/payment/app/middlewares/tenant.py
Normal file
10
backend/services/payment/app/middlewares/tenant.py
Normal file
@ -0,0 +1,10 @@
|
||||
from uuid import UUID
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from shared.tenant import HEADER_TENANT_ID, STATE_TENANT_ID
|
||||
class TenantHeaderMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request, call_next):
|
||||
raw=request.headers.get(HEADER_TENANT_ID)
|
||||
try: value=UUID(raw) if raw else None
|
||||
except ValueError: value=None
|
||||
setattr(request.state,STATE_TENANT_ID,value)
|
||||
return await call_next(request)
|
||||
1
backend/services/payment/app/models/__init__.py
Normal file
1
backend/services/payment/app/models/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from app.models.domain import *
|
||||
15
backend/services/payment/app/models/base.py
Normal file
15
backend/services/payment/app/models/base.py
Normal file
@ -0,0 +1,15 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from sqlalchemy import DateTime, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from app.models.types import GUID
|
||||
class UUIDPrimaryKeyMixin:
|
||||
id: Mapped[uuid.UUID] = mapped_column(GUID(), primary_key=True, default=uuid.uuid4)
|
||||
class TenantMixin:
|
||||
tenant_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False, index=True)
|
||||
class TimestampMixin:
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
class ActorAuditMixin:
|
||||
created_by: Mapped[str | None] = mapped_column(String(100))
|
||||
updated_by: Mapped[str | None] = mapped_column(String(100))
|
||||
117
backend/services/payment/app/models/domain.py
Normal file
117
backend/services/payment/app/models/domain.py
Normal file
@ -0,0 +1,117 @@
|
||||
from __future__ import annotations
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Boolean, DateTime, Integer, String, Text, UniqueConstraint, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.types import JSON
|
||||
from app.core.database import Base
|
||||
from app.models.base import UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, ActorAuditMixin
|
||||
from app.models.types import GUID
|
||||
|
||||
class Standard(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, ActorAuditMixin):
|
||||
__abstract__ = True
|
||||
code: Mapped[str | None] = mapped_column(String(120))
|
||||
name: Mapped[str | None] = mapped_column(String(255))
|
||||
status: Mapped[str] = mapped_column(String(40), default="active", nullable=False)
|
||||
data: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
|
||||
|
||||
class PaymentWorkspace(Standard):
|
||||
__tablename__="payment_workspaces"
|
||||
status: Mapped[str] = mapped_column(String(40), default="enabled", nullable=False)
|
||||
default_payment_mode: Mapped[str] = mapped_column(String(40), default="byo_psp", nullable=False)
|
||||
class PaymentBundleDefinition(Standard): __tablename__="payment_bundle_definitions"
|
||||
class TenantPaymentBundle(Standard):
|
||||
__tablename__="tenant_payment_bundles"
|
||||
bundle_code: Mapped[str] = mapped_column(String(120), nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
__table_args__=(UniqueConstraint("tenant_id","bundle_code",name="uq_tenant_bundle"),)
|
||||
class PaymentFeatureToggle(Standard):
|
||||
__tablename__="payment_feature_toggles"
|
||||
key: Mapped[str] = mapped_column(String(150), nullable=False)
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
class PaymentProviderAssignment(Standard): __tablename__="payment_provider_assignments"
|
||||
class PspProviderRegistration(Standard):
|
||||
__tablename__="psp_provider_registrations"
|
||||
provider_code: Mapped[str] = mapped_column(String(80), nullable=False)
|
||||
class CreditProviderRegistration(Standard):
|
||||
__tablename__="credit_provider_registrations"
|
||||
provider_code: Mapped[str] = mapped_column(String(80), nullable=False)
|
||||
class PaymentConfiguration(Standard): __tablename__="payment_configurations"
|
||||
class PaymentSetting(Standard):
|
||||
__tablename__="payment_settings"
|
||||
key: Mapped[str] = mapped_column(String(150), nullable=False)
|
||||
value: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
|
||||
class PaymentRole(Standard): __tablename__="payment_roles"
|
||||
class PaymentPermission(Standard): __tablename__="payment_permissions"
|
||||
class PaymentAuditLog(Base, UUIDPrimaryKeyMixin, TenantMixin):
|
||||
__tablename__="payment_audit_logs"
|
||||
entity_type: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
entity_id: Mapped[uuid.UUID | None] = mapped_column(GUID())
|
||||
action: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
actor_user_id: Mapped[str | None] = mapped_column(String(100))
|
||||
changes: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
class OutboxEvent(Base, UUIDPrimaryKeyMixin, TenantMixin):
|
||||
__tablename__="outbox_events"
|
||||
event_type: Mapped[str] = mapped_column(String(150), nullable=False)
|
||||
aggregate_type: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
aggregate_id: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
payload: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
|
||||
status: Mapped[str] = mapped_column(String(30), default="pending", nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
|
||||
class PspConnection(Standard):
|
||||
__tablename__="psp_connections"
|
||||
provider_code: Mapped[str] = mapped_column(String(80), nullable=False)
|
||||
credential_vault_ref: Mapped[str | None] = mapped_column(String(255))
|
||||
secret_config: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
|
||||
class PspCredentialVaultRef(Standard): __tablename__="psp_credential_vault_refs"
|
||||
class PspConnectionHealthCheck(Standard): __tablename__="psp_connection_health_checks"
|
||||
class PspRoutingPolicy(Standard): __tablename__="psp_routing_policies"
|
||||
|
||||
class MerchantAccount(Standard): __tablename__="merchant_accounts"
|
||||
class MerchantAccountProfile(Standard): __tablename__="merchant_account_profiles"
|
||||
class MerchantSettlementProfile(Standard): __tablename__="merchant_settlement_profiles"
|
||||
class MerchantComplianceRef(Standard): __tablename__="merchant_compliance_refs"
|
||||
class MerchantAccountStatusHistory(Standard): __tablename__="merchant_account_status_history"
|
||||
|
||||
class PaymentRequest(Standard):
|
||||
__tablename__="payment_requests"
|
||||
status: Mapped[str] = mapped_column(String(40), default="draft", nullable=False)
|
||||
idempotency_key: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
amount_minor: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
currency: Mapped[str] = mapped_column(String(3), default="IRR", nullable=False)
|
||||
payment_source: Mapped[str] = mapped_column(String(40), default="PSP", nullable=False)
|
||||
provider_code: Mapped[str] = mapped_column(String(80), default="mock", nullable=False)
|
||||
provider_transaction_id: Mapped[str | None] = mapped_column(String(200))
|
||||
redirect_url: Mapped[str | None] = mapped_column(Text)
|
||||
source_service: Mapped[str | None] = mapped_column(String(80))
|
||||
source_ref_type: Mapped[str | None] = mapped_column(String(80))
|
||||
source_ref_id: Mapped[str | None] = mapped_column(String(100))
|
||||
credit_provider_id: Mapped[uuid.UUID | None] = mapped_column(GUID())
|
||||
financing_reference: Mapped[str | None] = mapped_column(String(200))
|
||||
credit_authorization_reference: Mapped[str | None] = mapped_column(String(200))
|
||||
installment_contract_reference: Mapped[str | None] = mapped_column(String(200))
|
||||
__table_args__=(UniqueConstraint("tenant_id","idempotency_key",name="uq_payment_request_idem"),)
|
||||
class PaymentRequestStatusHistory(Standard): __tablename__="payment_request_status_history"
|
||||
class PaymentRequestLine(Standard): __tablename__="payment_request_lines"
|
||||
class PaymentCallbackLog(Standard):
|
||||
__tablename__="payment_callback_logs"
|
||||
connection_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
||||
provider_code: Mapped[str] = mapped_column(String(80), nullable=False)
|
||||
replay_key: Mapped[str] = mapped_column(String(250), nullable=False)
|
||||
__table_args__=(UniqueConstraint("tenant_id","replay_key",name="uq_callback_replay"),)
|
||||
class PaymentVerificationAttempt(Standard): __tablename__="payment_verification_attempts"
|
||||
class PaymentTransaction(Standard):
|
||||
__tablename__="payment_transactions"
|
||||
payment_request_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
||||
amount_minor: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
currency: Mapped[str] = mapped_column(String(3), nullable=False)
|
||||
provider_transaction_id: Mapped[str | None] = mapped_column(String(200))
|
||||
__table_args__=(UniqueConstraint("tenant_id","payment_request_id",name="uq_transaction_request"),)
|
||||
class PaymentTransactionFeeLine(Standard): __tablename__="payment_transaction_fee_lines"
|
||||
class PaymentLedgerEntry(Standard):
|
||||
__tablename__="payment_ledger_entries"
|
||||
payment_transaction_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
||||
amount_minor: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
direction: Mapped[str] = mapped_column(String(10), nullable=False)
|
||||
15
backend/services/payment/app/models/foundation.py
Normal file
15
backend/services/payment/app/models/foundation.py
Normal file
@ -0,0 +1,15 @@
|
||||
from app.models.domain import (
|
||||
CreditProviderRegistration,
|
||||
OutboxEvent,
|
||||
PaymentAuditLog,
|
||||
PaymentBundleDefinition,
|
||||
PaymentConfiguration,
|
||||
PaymentFeatureToggle,
|
||||
PaymentPermission,
|
||||
PaymentProviderAssignment,
|
||||
PaymentRole,
|
||||
PaymentSetting,
|
||||
PaymentWorkspace,
|
||||
PspProviderRegistration,
|
||||
TenantPaymentBundle,
|
||||
)
|
||||
5
backend/services/payment/app/models/ledger.py
Normal file
5
backend/services/payment/app/models/ledger.py
Normal file
@ -0,0 +1,5 @@
|
||||
from app.models.domain import (
|
||||
PaymentLedgerEntry,
|
||||
PaymentTransaction,
|
||||
PaymentTransactionFeeLine,
|
||||
)
|
||||
7
backend/services/payment/app/models/merchant.py
Normal file
7
backend/services/payment/app/models/merchant.py
Normal file
@ -0,0 +1,7 @@
|
||||
from app.models.domain import (
|
||||
MerchantAccount,
|
||||
MerchantAccountProfile,
|
||||
MerchantAccountStatusHistory,
|
||||
MerchantComplianceRef,
|
||||
MerchantSettlementProfile,
|
||||
)
|
||||
6
backend/services/payment/app/models/psp.py
Normal file
6
backend/services/payment/app/models/psp.py
Normal file
@ -0,0 +1,6 @@
|
||||
from app.models.domain import (
|
||||
PspConnection,
|
||||
PspConnectionHealthCheck,
|
||||
PspCredentialVaultRef,
|
||||
PspRoutingPolicy,
|
||||
)
|
||||
7
backend/services/payment/app/models/requests.py
Normal file
7
backend/services/payment/app/models/requests.py
Normal file
@ -0,0 +1,7 @@
|
||||
from app.models.domain import (
|
||||
PaymentCallbackLog,
|
||||
PaymentRequest,
|
||||
PaymentRequestLine,
|
||||
PaymentRequestStatusHistory,
|
||||
PaymentVerificationAttempt,
|
||||
)
|
||||
11
backend/services/payment/app/models/types.py
Normal file
11
backend/services/payment/app/models/types.py
Normal file
@ -0,0 +1,11 @@
|
||||
import uuid
|
||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
||||
from sqlalchemy.types import CHAR, TypeDecorator
|
||||
class GUID(TypeDecorator):
|
||||
impl = CHAR
|
||||
cache_ok = True
|
||||
def load_dialect_impl(self, dialect): return dialect.type_descriptor(PG_UUID(as_uuid=True) if dialect.name == "postgresql" else CHAR(36))
|
||||
def process_bind_param(self, value, dialect):
|
||||
if value is None: return None
|
||||
return (value if isinstance(value, uuid.UUID) else uuid.UUID(str(value))) if dialect.name == "postgresql" else str(value)
|
||||
def process_result_value(self, value, dialect): return value if value is None or isinstance(value, uuid.UUID) else uuid.UUID(str(value))
|
||||
10
backend/services/payment/app/permissions/definitions.py
Normal file
10
backend/services/payment/app/permissions/definitions.py
Normal file
@ -0,0 +1,10 @@
|
||||
ALL_PERMISSIONS = [
|
||||
"payment.workspaces.view","payment.workspaces.manage","payment.workspaces.enable","payment.workspaces.disable",
|
||||
"payment.bundles.view","payment.bundles.manage","payment.feature_toggles.view","payment.feature_toggles.manage",
|
||||
"payment.configurations.view","payment.configurations.manage","payment.settings.view","payment.settings.manage",
|
||||
"payment.psp_providers.view","payment.psp_connections.view","payment.psp_connections.create","payment.psp_connections.update","payment.psp_connections.delete","payment.psp_connections.test","payment.psp_connections.manage",
|
||||
"payment.psp_routing.view","payment.psp_routing.manage","payment.provider_assignments.view","payment.provider_assignments.manage",
|
||||
"payment.merchant_accounts.view","payment.merchant_accounts.create","payment.merchant_accounts.update","payment.merchant_accounts.submit","payment.merchant_accounts.activate","payment.merchant_accounts.suspend","payment.merchant_accounts.close","payment.merchant_accounts.manage",
|
||||
"payment.requests.view","payment.requests.create","payment.requests.cancel","payment.requests.manage","payment.callbacks.view",
|
||||
"payment.transactions.view","payment.transactions.export","payment.ledger.view","payment.audit.view"]
|
||||
PERMISSION_PREFIXES=("payment.",)
|
||||
0
backend/services/payment/app/policies/__init__.py
Normal file
0
backend/services/payment/app/policies/__init__.py
Normal file
2
backend/services/payment/app/providers/__init__.py
Normal file
2
backend/services/payment/app/providers/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from app.providers.contracts import PspAdapter, CreditProviderAdapter
|
||||
from app.providers.mock import MockPspAdapter, ZarinpalAdapter, IdpayAdapter, ADAPTERS
|
||||
7
backend/services/payment/app/providers/contracts.py
Normal file
7
backend/services/payment/app/providers/contracts.py
Normal file
@ -0,0 +1,7 @@
|
||||
from typing import Protocol
|
||||
class PspAdapter(Protocol):
|
||||
async def initiate_payment(self, request: dict) -> dict: ...
|
||||
async def verify_signature(self, payload: dict) -> bool: ...
|
||||
async def verify_payment(self, payload: dict) -> dict: ...
|
||||
class CreditProviderAdapter(Protocol):
|
||||
async def authorize_financing(self, payload: dict) -> dict: ...
|
||||
12
backend/services/payment/app/providers/mock.py
Normal file
12
backend/services/payment/app/providers/mock.py
Normal file
@ -0,0 +1,12 @@
|
||||
class MockPspAdapter:
|
||||
code="mock"
|
||||
async def initiate_payment(self, request):
|
||||
return {"redirect_url": f"https://mock.psp/pay/{request['id']}", "token": f"mock-{request['id']}"}
|
||||
async def verify_signature(self, payload):
|
||||
return payload.get("signature") in ("mock-valid", "valid", None)
|
||||
async def verify_payment(self, payload):
|
||||
paid=payload.get("normalized_status", payload.get("status","paid"))=="paid"
|
||||
return {"verified": paid, "status": "paid" if paid else "failed", "provider_transaction_id": payload.get("provider_transaction_id")}
|
||||
class ZarinpalAdapter(MockPspAdapter): code="zarinpal"
|
||||
class IdpayAdapter(MockPspAdapter): code="idpay"
|
||||
ADAPTERS={"mock":MockPspAdapter(),"zarinpal":ZarinpalAdapter(),"idpay":IdpayAdapter()}
|
||||
0
backend/services/payment/app/queries/__init__.py
Normal file
0
backend/services/payment/app/queries/__init__.py
Normal file
6
backend/services/payment/app/queries/payment.py
Normal file
6
backend/services/payment/app/queries/payment.py
Normal file
@ -0,0 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
from uuid import UUID
|
||||
@dataclass
|
||||
class GetPaymentRequestQuery:
|
||||
tenant_id: UUID
|
||||
payment_request_id: UUID
|
||||
9
backend/services/payment/app/repositories/base.py
Normal file
9
backend/services/payment/app/repositories/base.py
Normal file
@ -0,0 +1,9 @@
|
||||
from sqlalchemy import select
|
||||
class TenantBaseRepository:
|
||||
def __init__(self, session, model): self.session, self.model = session, model
|
||||
async def add(self, entity):
|
||||
self.session.add(entity); await self.session.flush(); return entity
|
||||
async def get(self, tenant_id, entity_id):
|
||||
return (await self.session.execute(select(self.model).where(self.model.tenant_id==tenant_id,self.model.id==entity_id))).scalar_one_or_none()
|
||||
async def list(self, tenant_id):
|
||||
return (await self.session.execute(select(self.model).where(self.model.tenant_id==tenant_id))).scalars().all()
|
||||
2
backend/services/payment/app/repositories/payment.py
Normal file
2
backend/services/payment/app/repositories/payment.py
Normal file
@ -0,0 +1,2 @@
|
||||
from app.repositories.base import TenantBaseRepository
|
||||
class PaymentRepository(TenantBaseRepository): pass
|
||||
0
backend/services/payment/app/schemas/__init__.py
Normal file
0
backend/services/payment/app/schemas/__init__.py
Normal file
0
backend/services/payment/app/services/__init__.py
Normal file
0
backend/services/payment/app/services/__init__.py
Normal file
0
backend/services/payment/app/tests/__init__.py
Normal file
0
backend/services/payment/app/tests/__init__.py
Normal file
18
backend/services/payment/app/tests/conftest.py
Normal file
18
backend/services/payment/app/tests/conftest.py
Normal file
@ -0,0 +1,18 @@
|
||||
import os, uuid
|
||||
os.environ["ENVIRONMENT"]="test"
|
||||
os.environ["AUTH_REQUIRED"]="false"
|
||||
os.environ["PAYMENT_DATABASE_URL"]="sqlite+aiosqlite:///:memory:"
|
||||
os.environ["PAYMENT_DATABASE_URL_SYNC"]="sqlite:///:memory:"
|
||||
import pytest_asyncio
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from app.core.database import Base, engine
|
||||
import app.models
|
||||
from app.main import app
|
||||
TENANT_A=uuid.uuid4()
|
||||
TENANT_B=uuid.uuid4()
|
||||
def headers(t=TENANT_A): return {"X-Tenant-ID":str(t)}
|
||||
@pytest_asyncio.fixture
|
||||
async def client():
|
||||
async with engine.begin() as c:
|
||||
await c.run_sync(Base.metadata.drop_all); await c.run_sync(Base.metadata.create_all)
|
||||
async with AsyncClient(transport=ASGITransport(app=app),base_url="http://test") as ac: yield ac
|
||||
11
backend/services/payment/app/tests/test_api.py
Normal file
11
backend/services/payment/app/tests/test_api.py
Normal file
@ -0,0 +1,11 @@
|
||||
import pytest
|
||||
from app.tests.conftest import headers
|
||||
@pytest.mark.asyncio
|
||||
async def test_foundation_flow(client):
|
||||
assert (await client.get("/health")).status_code==200
|
||||
w=await client.post("/api/v1/payment-workspaces",headers=headers(),json={"code":"main","name":"Main","status":"enabled","default_payment_mode":"byo_psp"})
|
||||
assert w.status_code==201
|
||||
b=await client.post("/api/v1/tenant-bundles",headers=headers(),json={"bundle_code":"payment.byo_psp.basic","is_active":True})
|
||||
assert b.status_code==201
|
||||
caps=(await client.get("/capabilities",headers=headers())).json()
|
||||
assert "payment.byo_psp.basic" in caps["active_bundles"]
|
||||
8
backend/services/payment/app/tests/test_architecture.py
Normal file
8
backend/services/payment/app/tests/test_architecture.py
Normal file
@ -0,0 +1,8 @@
|
||||
from app.core.database import Base
|
||||
import app.models
|
||||
def test_all_owned_rows_are_tenant_scoped():
|
||||
assert all("tenant_id" in t.columns for t in Base.metadata.tables.values())
|
||||
def test_layers_exist():
|
||||
from pathlib import Path
|
||||
root=Path(__file__).parents[2]
|
||||
for x in ("api","commands","queries","repositories","models","providers","events"): assert (root/"app"/x).exists()
|
||||
5
backend/services/payment/app/tests/test_dependency.py
Normal file
5
backend/services/payment/app/tests/test_dependency.py
Normal file
@ -0,0 +1,5 @@
|
||||
import pytest
|
||||
from app.core.entitlements import CoreEntitlementClient
|
||||
from app.tests.conftest import TENANT_A
|
||||
@pytest.mark.asyncio
|
||||
async def test_stub_entitlement(): assert await CoreEntitlementClient().has_access(TENANT_A)
|
||||
2
backend/services/payment/app/tests/test_docs.py
Normal file
2
backend/services/payment/app/tests/test_docs.py
Normal file
@ -0,0 +1,2 @@
|
||||
from pathlib import Path
|
||||
def test_readme_documents_version(): assert "0.14.5.0" in (Path(__file__).parents[2]/"README.md").read_text()
|
||||
4
backend/services/payment/app/tests/test_migration.py
Normal file
4
backend/services/payment/app/tests/test_migration.py
Normal file
@ -0,0 +1,4 @@
|
||||
from pathlib import Path
|
||||
def test_all_phase_migrations_exist():
|
||||
names={p.stem for p in (Path(__file__).parents[2]/"alembic"/"versions").glob("*.py")}
|
||||
assert {"0001_initial","0002_phase_141_psp","0003_phase_142_merchant","0004_phase_143_requests","0005_phase_144_callbacks","0006_phase_145_ledger"}<=names
|
||||
3
backend/services/payment/app/tests/test_permissions.py
Normal file
3
backend/services/payment/app/tests/test_permissions.py
Normal file
@ -0,0 +1,3 @@
|
||||
from app.permissions.definitions import ALL_PERMISSIONS
|
||||
def test_contract_permissions_registered():
|
||||
for p in ("payment.workspaces.manage","payment.psp_connections.test","payment.merchant_accounts.activate","payment.transactions.view","payment.ledger.view"): assert p in ALL_PERMISSIONS
|
||||
8
backend/services/payment/app/tests/test_phase_141.py
Normal file
8
backend/services/payment/app/tests/test_phase_141.py
Normal file
@ -0,0 +1,8 @@
|
||||
import pytest
|
||||
from app.tests.conftest import headers
|
||||
@pytest.mark.asyncio
|
||||
async def test_psp_connection_and_health(client):
|
||||
assert (await client.get("/api/v1/psp-connections",headers=headers())).status_code==403
|
||||
await client.post("/api/v1/tenant-bundles",headers=headers(),json={"bundle_code":"payment.byo_psp.basic","is_active":True})
|
||||
p=(await client.post("/api/v1/psp-connections",headers=headers(),json={"provider_code":"mock","name":"Mock"})).json()
|
||||
assert (await client.post(f"/api/v1/psp-connections/{p['id']}/test",headers=headers())).json()["status"]=="healthy"
|
||||
9
backend/services/payment/app/tests/test_phase_142.py
Normal file
9
backend/services/payment/app/tests/test_phase_142.py
Normal file
@ -0,0 +1,9 @@
|
||||
import pytest
|
||||
from app.tests.conftest import headers
|
||||
@pytest.mark.asyncio
|
||||
async def test_merchant_lifecycle(client):
|
||||
await client.post("/api/v1/tenant-bundles",headers=headers(),json={"bundle_code":"payment.torbat_pay.merchant","is_active":True})
|
||||
m=(await client.post("/api/v1/merchant-accounts",headers=headers(),json={"name":"Merchant"})).json()
|
||||
m=(await client.post(f"/api/v1/merchant-accounts/{m['id']}/submit",headers=headers())).json(); assert m["status"]=="pending_review"
|
||||
m=(await client.post(f"/api/v1/merchant-accounts/{m['id']}/activate",headers=headers())).json(); assert m["status"]=="active"
|
||||
m=(await client.post(f"/api/v1/merchant-accounts/{m['id']}/suspend",headers=headers())).json(); assert m["status"]=="suspended"
|
||||
11
backend/services/payment/app/tests/test_phase_143.py
Normal file
11
backend/services/payment/app/tests/test_phase_143.py
Normal file
@ -0,0 +1,11 @@
|
||||
import pytest
|
||||
from app.tests.conftest import headers
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_idempotency_and_cancel(client):
|
||||
await client.post("/api/v1/tenant-bundles",headers=headers(),json={"bundle_code":"payment.byo_psp.basic","is_active":True})
|
||||
assert (await client.post("/api/v1/payment-requests",headers=headers(),json={"amount_minor":10})).status_code==400
|
||||
h={**headers(),"Idempotency-Key":"same"}
|
||||
a=(await client.post("/api/v1/payment-requests",headers=h,json={"amount_minor":10})).json()
|
||||
b=(await client.post("/api/v1/payment-requests",headers=h,json={"amount_minor":10})).json()
|
||||
assert a["id"]==b["id"] and a["status"]=="redirect_issued"
|
||||
assert (await client.post(f"/api/v1/payment-requests/{a['id']}/cancel",headers=headers())).json()["status"]=="cancelled"
|
||||
11
backend/services/payment/app/tests/test_phase_144.py
Normal file
11
backend/services/payment/app/tests/test_phase_144.py
Normal file
@ -0,0 +1,11 @@
|
||||
import pytest
|
||||
from app.tests.conftest import headers
|
||||
@pytest.mark.asyncio
|
||||
async def test_callback_replay(client):
|
||||
await client.post("/api/v1/tenant-bundles",headers=headers(),json={"bundle_code":"payment.byo_psp.basic","is_active":True})
|
||||
connection=(await client.post("/api/v1/psp-connections",headers=headers(),json={"provider_code":"mock"})).json()
|
||||
req=(await client.post("/api/v1/payment-requests",headers={**headers(),"Idempotency-Key":"cb"},json={"amount_minor":25})).json()
|
||||
body={"payment_request_id":req["id"],"provider_transaction_id":"trx-1","normalized_status":"paid","signature":"mock-valid"}
|
||||
path=f"/api/v1/callbacks/mock/{connection['id']}"
|
||||
assert (await client.post(path,headers=headers(),json=body)).json()["status"]=="processed"
|
||||
assert (await client.post(path,headers=headers(),json=body)).json()["status"]=="duplicate"
|
||||
10
backend/services/payment/app/tests/test_phase_145.py
Normal file
10
backend/services/payment/app/tests/test_phase_145.py
Normal file
@ -0,0 +1,10 @@
|
||||
import pytest
|
||||
from app.tests.conftest import headers
|
||||
@pytest.mark.asyncio
|
||||
async def test_verified_payment_creates_ledger_once(client):
|
||||
await client.post("/api/v1/tenant-bundles",headers=headers(),json={"bundle_code":"payment.byo_psp.basic","is_active":True})
|
||||
req=(await client.post("/api/v1/payment-requests",headers={**headers(),"Idempotency-Key":"ledger"},json={"amount_minor":30,"source":{"service":"crm","ref_id":"abc"}})).json()
|
||||
await client.post(f"/api/v1/payment-requests/{req['id']}/verify",headers=headers(),json={"status":"paid","provider_transaction_id":"t"})
|
||||
await client.post(f"/api/v1/payment-requests/{req['id']}/verify",headers=headers(),json={"status":"paid","provider_transaction_id":"t"})
|
||||
assert len((await client.get("/api/v1/payment-transactions",headers=headers())).json())==1
|
||||
assert len((await client.get("/api/v1/payment-ledger-entries",headers=headers())).json())==1
|
||||
10
backend/services/payment/app/tests/test_security.py
Normal file
10
backend/services/payment/app/tests/test_security.py
Normal file
@ -0,0 +1,10 @@
|
||||
import pytest
|
||||
from app.tests.conftest import headers
|
||||
@pytest.mark.asyncio
|
||||
async def test_tenant_required(client):
|
||||
assert (await client.get("/api/v1/payment-workspaces")).status_code in (400,422)
|
||||
@pytest.mark.asyncio
|
||||
async def test_psp_secrets_redacted(client):
|
||||
await client.post("/api/v1/tenant-bundles",headers=headers(),json={"bundle_code":"payment.byo_psp.basic","is_active":True})
|
||||
r=await client.post("/api/v1/psp-connections",headers=headers(),json={"provider_code":"mock","credential_vault_ref":"vault://secret","secret_config":{"password":"x"}})
|
||||
assert r.json()["credential_vault_ref"]=="***REDACTED***" and "secret_config" not in r.json()
|
||||
@ -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()==[]
|
||||
0
backend/services/payment/app/validators/__init__.py
Normal file
0
backend/services/payment/app/validators/__init__.py
Normal file
2
backend/services/payment/pytest.ini
Normal file
2
backend/services/payment/pytest.ini
Normal file
@ -0,0 +1,2 @@
|
||||
[pytest]
|
||||
asyncio_mode = auto
|
||||
14
backend/services/payment/requirements.txt
Normal file
14
backend/services/payment/requirements.txt
Normal file
@ -0,0 +1,14 @@
|
||||
fastapi==0.111.0
|
||||
uvicorn[standard]==0.30.1
|
||||
pydantic[email]==2.7.4
|
||||
pydantic-settings==2.3.4
|
||||
sqlalchemy==2.0.31
|
||||
alembic==1.13.2
|
||||
asyncpg==0.29.0
|
||||
psycopg[binary]>=3.2.2
|
||||
httpx==0.27.0
|
||||
pyjwt[crypto]==2.8.0
|
||||
-e ../../shared-lib
|
||||
pytest==8.2.2
|
||||
pytest-asyncio==0.23.7
|
||||
aiosqlite==0.20.0
|
||||
0
backend/services/payment/scripts/__init__.py
Normal file
0
backend/services/payment/scripts/__init__.py
Normal file
12
backend/services/payment/scripts/ensure_db.py
Normal file
12
backend/services/payment/scripts/ensure_db.py
Normal file
@ -0,0 +1,12 @@
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
def main():
|
||||
url=os.getenv("PAYMENT_DATABASE_URL_SYNC","")
|
||||
if not url: return
|
||||
import psycopg
|
||||
p=urlparse(url.replace("+psycopg","")); db=(p.path or "/payment_db").lstrip("/")
|
||||
with psycopg.connect(host=p.hostname or "localhost",port=p.port or 5432,user=p.username,password=p.password,dbname="postgres",autocommit=True) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT 1 FROM pg_database WHERE datname=%s",(db,))
|
||||
if not cur.fetchone(): cur.execute(f'CREATE DATABASE "{db}"')
|
||||
if __name__=="__main__": main()
|
||||
@ -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}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
77
docs/architecture/adr/ADR-020.md
Normal file
77
docs/architecture/adr/ADR-020.md
Normal file
@ -0,0 +1,77 @@
|
||||
# ADR-020: Independent Enterprise Payment Platform Service (Torbat Pay)
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Status | Accepted |
|
||||
| Date | 2026-07-27 |
|
||||
| Deciders | Platform Architecture |
|
||||
| Supersedes | — |
|
||||
| Superseded by | — |
|
||||
|
||||
## Context
|
||||
|
||||
Every TorbatYar vertical — Accounting, CRM, Hospitality, Sports Center, Marketplace, Delivery, Experience, Healthcare, Beauty Business, Automation, and future services — needs payment capture without embedding PSP credentials, callback handling, or transaction ledgers inside each service.
|
||||
|
||||
Tenants require two commercial modes:
|
||||
|
||||
1. **Bring Your Own PSP (BYO-PSP)** — connect tenant-owned gateways (ZarinPal, IDPay, NextPay, Pay.ir, Mellat, Parsian, Saman, …).
|
||||
2. **Torbat Pay Merchant** — Torbat Pay acts as payment facilitator; tenants onboard as sub-merchants under platform settlement rules.
|
||||
|
||||
TorbatYar already isolates shared platforms as independent services (Communication [ADR-012](ADR-012.md), Loyalty [ADR-011](ADR-011.md), Delivery [ADR-015](ADR-015.md)). Payment capture, PSP routing, verification, and payment-domain ledger must follow the same pattern. Accounting owns financial journals via Posting Engine [ADR-010](ADR-010.md); Payment must never create journal entries locally.
|
||||
|
||||
International PSP expansion is **not** in current scope but the provider adapter architecture must allow future regions without vertical changes.
|
||||
|
||||
Commercial product name: **Torbat Pay**.
|
||||
|
||||
## Decision
|
||||
|
||||
1. Introduce `payment` as an **independent Enterprise Payment Platform service** with sole ownership of `payment_db` ([ADR-001](ADR-001.md)).
|
||||
2. Permission prefix: `payment.*`. Publish-only domain events: `payment.*`.
|
||||
3. Row-level multi-tenancy via `tenant_id` ([ADR-003](ADR-003.md)).
|
||||
4. PSP implementations sit behind **provider protocols/adapters** owned by Payment; verticals never call PSP APIs directly.
|
||||
5. Consume Accounting, CRM, Loyalty, Communication, Core, and all vertical services **only** via REST API and Events — never shared tables or foreign model imports.
|
||||
6. Financial journals only through Accounting Posting Engine ([ADR-010](ADR-010.md)). Payment emits settlement/reconciliation **intents**; Accounting posts externally.
|
||||
7. Customer payment notifications (receipt SMS/email) only through Communication ([ADR-012](ADR-012.md)).
|
||||
8. Vertical services (Hospitality POS, Marketplace checkout, Sports Center membership, Experience widgets, etc.) hold **commercial context refs only** (`order_ref`, `invoice_ref`, `checkout_session_ref`) and delegate capture to Payment APIs.
|
||||
9. Implementation phases registered as **`payment-14.0`–`payment-14.10`** in [phase-manifest.yaml](../../ai-framework/phase-manifest.yaml); roadmap in [payment-roadmap.md](../../payment-roadmap.md). Phase numeric **14.x** is the Payment **track** identifier; it is distinct from Beauty Business phases (`beauty-business-14.x`).
|
||||
10. API port **8012** reserved; service path `backend/services/payment`.
|
||||
11. UI for checkout, merchant admin, and reconciliation dashboards lives in `frontend/` only ([ADR-002](ADR-002.md)); this service exposes APIs and events.
|
||||
12. No international PSP implementation in initial HIGH-priority phases; adapter registry and credential schema must be region-aware for future expansion.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- One reusable payment platform for all current and future TorbatYar services
|
||||
- Enforceable PSP credential isolation and callback verification in one place
|
||||
- BYO-PSP and Torbat Pay Merchant modes without duplicating integration code per vertical
|
||||
- Clear boundary with Accounting (money movement vs financial posting)
|
||||
|
||||
### Negative
|
||||
|
||||
- Additional deployable service and database
|
||||
- Eventual consistency between payment status and vertical order state
|
||||
- PCI scope management for facilitator mode requires operational discipline (documented, not implemented in registration)
|
||||
|
||||
### Neutral
|
||||
|
||||
- Registration (this ADR + manifests) precedes business code; `payment-14.0` implements the scaffold
|
||||
- Loyalty wallet/gift-card money flows remain in Loyalty; Payment handles external PSP money-in only unless explicitly integrated later
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. Payment subsystem inside Core — rejected (violates business-module boundaries; couples platform billing with tenant commerce payments).
|
||||
2. Payment tables inside Accounting — rejected (Accounting owns posting semantics, not PSP routing/callbacks).
|
||||
3. Per-vertical PSP integrations — rejected (duplicates credentials, callbacks, idempotency, and audit across Hospitality/Marketplace/Sports Center).
|
||||
4. Third-party payment orchestration SaaS only — rejected for primary path (tenant BYO-PSP and facilitator control require owned platform).
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [Payment Roadmap](../../payment-roadmap.md)
|
||||
- [Phase Area — Payment](../../phases/Payment/README.md)
|
||||
- [Module Boundaries](../module-boundaries.md)
|
||||
- [Module Registry — payment](../../module-registry.md#payment)
|
||||
- [Payment Contracts Reference](../../reference/payment-contracts.md)
|
||||
- [Phase Manifest](../../ai-framework/phase-manifest.yaml)
|
||||
- [Service Manifest](../../ai-framework/service-manifest.yaml)
|
||||
- [ADR-001](ADR-001.md) · [ADR-002](ADR-002.md) · [ADR-003](ADR-003.md) · [ADR-006](ADR-006.md) · [ADR-010](ADR-010.md) · [ADR-011](ADR-011.md) · [ADR-012](ADR-012.md) · [ADR-021](ADR-021.md)
|
||||
213
docs/architecture/adr/ADR-021.md
Normal file
213
docs/architecture/adr/ADR-021.md
Normal file
@ -0,0 +1,213 @@
|
||||
# ADR-021: Payment Platform Licensing, Routing & Integration Contracts
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Status | Accepted |
|
||||
| Date | 2026-07-27 |
|
||||
| Deciders | Platform Architecture |
|
||||
| Supersedes | — |
|
||||
| Superseded by | — |
|
||||
| Extends | [ADR-020](ADR-020.md) |
|
||||
|
||||
## Context
|
||||
|
||||
[ADR-020](ADR-020.md) establishes Torbat Pay as an independent service. Before Phase `payment-14.0` implementation, the platform must define **non-breaking** contracts for:
|
||||
|
||||
- Tenant enable/disable of Payment without vertical code changes
|
||||
- Core service entitlements vs Payment bundle licensing vs operational feature toggles
|
||||
- BYO-PSP vs Torbat Pay Merchant mode selection and provider reassignment
|
||||
- Versioned API/event contracts for payment intents, checkout, callbacks, settlement, and future refund/split/subscription/wallet/installment flows
|
||||
- Compatibility with Core, Identity, Tenant, Accounting, CRM, Communication, Hospitality, Delivery, Sports Center, Marketplace, and future consumers
|
||||
|
||||
Hospitality ([ADR-017](ADR-017.md)) and Experience ([ADR-016](ADR-016.md)) already use **bundle licensing + feature toggles + Core entitlement coordination**. Payment must follow the same three-layer model to avoid rework.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Three-layer access control (mandatory)
|
||||
|
||||
| Layer | Owner | Purpose | Example keys |
|
||||
| --- | --- | --- | --- |
|
||||
| **L1 — Core entitlement** | Core Platform | Commercial plan / tenant module enablement | `payment.module.enabled` |
|
||||
| **L2 — Payment bundle** | Payment (`payment_db`) | Licensed capability packs within Payment | `byo_psp_basic`, `torbat_pay_merchant`, `marketplace_splits` |
|
||||
| **L3 — Payment feature toggle** | Payment (`payment_db`) | Operational flags within an active bundle | `refunds_enabled`, `installments_enabled` |
|
||||
|
||||
Rules:
|
||||
|
||||
1. L1 must pass before any Payment API mutates state (except health/capabilities discovery when documented).
|
||||
2. L2 gates API surfaces, permissions exposure, and `/capabilities` flags — **hidden bundles must not expose routes, menus, or permission leaves** (same rule as ADR-017).
|
||||
3. L3 gates optional engines without new Core plan features.
|
||||
4. Payment listens to Core `feature_access.changed` (event) to invalidate entitlement cache; Payment does not compute Core plans locally.
|
||||
|
||||
### 2. Tenant payment workspace (enable / disable)
|
||||
|
||||
Each tenant has at most one **PaymentWorkspace** in `payment_db`:
|
||||
|
||||
| Field | Purpose |
|
||||
| --- | --- |
|
||||
| `status` | `disabled` \| `enabled` \| `suspended` |
|
||||
| `default_payment_mode` | `byo_psp` \| `torbat_pay_merchant` |
|
||||
| `core_entitlement_ref` | Opaque ref to Core feature/subscription check (no FK) |
|
||||
|
||||
- **Enable Payment:** Core L1 `payment.module.enabled` + workspace `enabled` + at least one L2 bundle active.
|
||||
- **Disable Payment:** workspace `disabled` — reject new payment requests; in-flight callbacks still processed (14.4+).
|
||||
- **Mode switch:** update `default_payment_mode` + `PaymentProviderAssignment` — **no vertical or adapter code change**; routing policy resolves provider at runtime.
|
||||
|
||||
### 3. Provider assignment & routing (no code change on switch)
|
||||
|
||||
| Aggregate | Role |
|
||||
| --- | --- |
|
||||
| `PspProviderRegistration` | Platform catalog of adapter types (ZarinPal, IDPay, …) |
|
||||
| `CreditProviderRegistration` | Platform catalog of **external credit/financing adapters** (Torbat Credit, future third-party BNPL) — **registration shell only until phased** |
|
||||
| `PspConnection` | Tenant BYO-PSP credential binding |
|
||||
| `CreditProviderConnection` | Tenant connection to external credit provider (future; same assignment pattern as PSP) |
|
||||
| `MerchantAccount` | Torbat Pay Merchant sub-merchant (facilitator mode) |
|
||||
| `PaymentProviderAssignment` | Active provider binding for a workspace/branch/context |
|
||||
| `PspRoutingPolicy` | Priority, failover, amount/currency/channel/**payment_source** rules |
|
||||
|
||||
Tenants change PSP or credit provider by updating **assignment + routing policy** only. Adapters are selected by `provider_code` from registry — adding Torbat Credit or a new PSP is a new adapter registration, not a vertical change.
|
||||
|
||||
**Payment source types (routing dimension, reserved in v1):**
|
||||
|
||||
| `payment_source` | Meaning | Provider adapter |
|
||||
| --- | --- | --- |
|
||||
| `PSP` | Standard gateway capture (BYO-PSP) | `PspAdapter` |
|
||||
| `MERCHANT_FACILITATOR` | Torbat Pay Merchant mode | Facilitator adapter |
|
||||
| `CREDIT_PROVIDER` | External credit/BNPL/financing authorization | `CreditProviderAdapter` (**not implemented**; architecture reserved) |
|
||||
|
||||
Payment MUST invoke Torbat Credit through the **same provider adapter pattern** as PSPs (`initiate`, `verify`, `callback` hooks on `CreditProviderAdapter`). Payment stores **opaque refs only** — never credit scores, KYC payloads, or installment calculation logic.
|
||||
|
||||
### 4. Merchant ownership
|
||||
|
||||
- **MerchantAccount** and facilitator settlement profiles are owned solely by Payment.
|
||||
- Verticals store `merchant_account_ref` or `payment_workspace_ref` UUIDs only — never PSP merchant IDs from gateways.
|
||||
- Torbat Pay Merchant mode uses **platform-scoped facilitator credentials** + tenant sub-merchant identifier; BYO mode uses tenant `PspConnection` only.
|
||||
|
||||
### 5. Versioned integration contracts (stable v1 before implementation)
|
||||
|
||||
All cross-service payment integration uses versioned contracts documented in [payment-contracts.md](../../reference/payment-contracts.md):
|
||||
|
||||
| Contract | Version | Phase introduced | Breaking change policy |
|
||||
| --- | --- | --- | --- |
|
||||
| PaymentIntentContract | v1 | 14.3 | Additive fields only in v1.x; v2 requires new phase |
|
||||
| CheckoutSessionContract | v1 | 14.8 | Same |
|
||||
| CallbackIngressContract | v1 | 14.4 | Same |
|
||||
| SettlementIntentContract | v1 | 14.9 | Same |
|
||||
| RefundIntentContract | v1 | 14.6 | Reserved in v1 schema |
|
||||
| SplitAllocationContract | v1 | 14.7 | Reserved in v1 schema |
|
||||
| SubscriptionBillingContract | v1 | Future | Schema slot in reference doc |
|
||||
| WalletTopUpContract | v1 | Future | Loyalty consumer; Payment captures only |
|
||||
| InstallmentPlanContract | v1 | Future (Payment) | **Payment-side schedule shell only** — see §12 |
|
||||
| CreditAuthorizationContract | v1 | Future (Payment ↔ Credit) | Schema slot; authorization refs only |
|
||||
|
||||
Verticals depend on **contract version + capability flags**, not Payment internal models.
|
||||
|
||||
### 12. Torbat Credit — reserved independent service (architecture only)
|
||||
|
||||
A future independent service is reserved:
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Service id | `credit` |
|
||||
| Commercial product | **Torbat Credit** |
|
||||
| Database | `credit_db` (reserved) |
|
||||
| Event namespace | `credit.*` (**reserved for Torbat Credit** — Payment does not publish `credit.*`) |
|
||||
|
||||
**Boundary — InstallmentPlan ≠ Credit Engine:**
|
||||
|
||||
| Concern | Owner |
|
||||
| --- | --- |
|
||||
| Installment **schedule shell** linked to a payment request (due dates, amounts, linkage refs) | Payment (`InstallmentPlanContract` v1) |
|
||||
| Installment **calculation**, credit **scoring**, **KYC/KYB**, **BNPL** underwriting, financing agreements, collections | **Torbat Credit** (`credit` service) — exclusive |
|
||||
|
||||
Payment MAY store opaque architectural reference fields on intents and transactions:
|
||||
|
||||
- `credit_provider_id` — registry id of external credit provider (e.g. Torbat Credit adapter)
|
||||
- `financing_reference` — opaque id returned by credit provider for a financing agreement
|
||||
- `credit_authorization_reference` — authorization/hold id from credit provider
|
||||
- `installment_contract_reference` — link to Torbat Credit contract; **not** Payment-owned installment math
|
||||
|
||||
**Integration pattern:** Torbat Credit is consumed by Payment as an **external Credit Provider** — same adapter registry, routing policy, and assignment mechanics as PSPs. No shared database. No credit domain logic inside Payment.
|
||||
|
||||
**Implementation status:** Architecture references only after `payment-arch` patch. No `credit` service code, no `CREDIT_PROVIDER` routing execution, no `credit.*` event producers until Torbat Credit is registered in a future phase.
|
||||
|
||||
### 6. API-first and event-first (mandatory)
|
||||
|
||||
- **API-first:** All vertical actions go through documented REST (or internal token-gated) endpoints; no shared DB reads.
|
||||
- **Event-first:** State changes that verticals care about (`paid`, `failed`, `refunded`, `settled`) publish `payment.*` events via transactional outbox ([ADR-006](ADR-006.md)). Verticals may use API poll or event subscribe — never both as source of truth without idempotency keys.
|
||||
|
||||
### 7. Database-per-service ([ADR-001](ADR-001.md))
|
||||
|
||||
Payment is sole owner of `payment_db`. Forbidden: cross-DB FKs, importing vertical models, storing vertical order rows in Payment.
|
||||
|
||||
### 8. Permission tree
|
||||
|
||||
Prefix `payment.*`. Permission leaves are **bundle-gated** (L2). Inheritance: `manage` ⊃ `create|update|delete` ⊃ `view`. Service-to-service scopes use internal tokens with `payment.requests.create` etc.
|
||||
|
||||
Foundation permission catalog registered in Phase 14.0; leaves grow per phase without renaming roots (additive only).
|
||||
|
||||
### 9. Bundle isolation
|
||||
|
||||
Inactive bundles:
|
||||
|
||||
- Return 404 or 403 (consistent per API template) for bundle-gated routes
|
||||
- Omit from `/capabilities`
|
||||
- Hide permission leaves from catalog export
|
||||
- Do not publish bundle-specific events
|
||||
|
||||
### 10. Multi-tenant isolation ([ADR-003](ADR-003.md))
|
||||
|
||||
Every business row includes `tenant_id`. Callback URLs include tenant-routable connection id; verification rejects cross-tenant callback routing. Internal service calls require `X-Tenant-ID` + scoped token.
|
||||
|
||||
### 11. Service compatibility (read-only integration)
|
||||
|
||||
| Service | Integration | Payment must not |
|
||||
| --- | --- | --- |
|
||||
| **Core** | Entitlement check API; `feature_access.changed` | Own plans/subscriptions |
|
||||
| **Identity** | `user_ref` / `payer_ref` on intents | Admin users or OIDC |
|
||||
| **Tenant (Core)** | Workspace tied to Core tenant id | Duplicate tenant registry |
|
||||
| **Accounting** | SettlementIntentContract events/API | Create JournalEntry |
|
||||
| **CRM** | `contact_ref`, `organization_ref` on payer | Own CRM aggregates |
|
||||
| **Communication** | Receipt/refund notify client | Own SMS/email providers |
|
||||
| **Hospitality** | Checkout refs from POS/QR; subscribe to `payment.request.paid` | Own `pos_payment` PSP fields long-term |
|
||||
| **Delivery** | Optional COD payment ref | Own delivery settlement |
|
||||
| **Sports Center** | Membership/competition payment refs | Own membership billing |
|
||||
| **Marketplace** | Checkout + split refs (14.7+) | Own vendor catalog |
|
||||
| **Experience** | Paid form/checkout widget refs | Own payment capture |
|
||||
| **Loyalty** | WalletTopUpContract (future) | Own wallet ledger |
|
||||
| **Torbat Credit** (future) | CreditAuthorization via `CreditProviderAdapter`; subscribe to `credit.*` | Own scoring, KYC, BNPL, financing engine |
|
||||
|
||||
No existing service code is modified during architecture-only phases; verticals adopt Payment client contracts in their own future connector phases.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Tenants can enable/disable Payment, switch PSP, and switch BYO vs Merchant mode without redeploying verticals
|
||||
- Future refund/split/subscription/wallet/**credit-provider** flows extend v1 contracts without breaking checkout integrations
|
||||
- Torbat Credit reserved as external provider — Payment can route like any PSP when implemented
|
||||
- Clear compatibility matrix for all platform services
|
||||
|
||||
### Negative
|
||||
|
||||
- Three-layer gating adds checks on every mutating API
|
||||
- Contract documentation must stay synchronized with `/capabilities`
|
||||
|
||||
### Neutral
|
||||
|
||||
- Architecture phase (`payment-arch`) completes before `payment-14.0` code
|
||||
- International PSPs remain adapter-only until explicitly phased
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. Core-only entitlement (no Payment bundles) — rejected; cannot express Torbat Pay commercial packs or hide split/refund APIs independently of Core plans.
|
||||
2. PSP credentials in Core — rejected; violates ADR-020 and PCI isolation.
|
||||
3. Vertical-owned checkout contracts — rejected; duplicates Marketplace/Hospitality/Sports integrations.
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [ADR-020](ADR-020.md)
|
||||
- [Payment Contracts Reference](../../reference/payment-contracts.md)
|
||||
- [Payment Roadmap](../../payment-roadmap.md)
|
||||
- [Module Boundaries](../module-boundaries.md)
|
||||
- [Authorization Architecture](../authorization-architecture.md)
|
||||
- [Services Contracts](../../reference/services-contracts.md)
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 |
|
||||
|
||||
34
docs/payment-phase-pay-arch.md
Normal file
34
docs/payment-phase-pay-arch.md
Normal file
@ -0,0 +1,34 @@
|
||||
# Phase Pay-Arch — Payment Architecture Prerequisites
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Phase ID | `payment-arch` |
|
||||
| Title | Payment Architecture Prerequisites |
|
||||
| Status | Complete (documentation-only) |
|
||||
| Service | `payment` |
|
||||
| ADR | [ADR-021](architecture/adr/ADR-021.md) (extends [ADR-020](architecture/adr/ADR-020.md)) |
|
||||
|
||||
## Purpose
|
||||
|
||||
Finalize all architectural prerequisites before `payment-14.0` implementation so future phases never require breaking contract or licensing changes.
|
||||
|
||||
## Deliverables
|
||||
|
||||
- [ADR-021](architecture/adr/ADR-021.md) — three-layer licensing, provider assignment, contract versioning
|
||||
- [payment-contracts.md](reference/payment-contracts.md) — all v1 integration contracts + future reserved schemas
|
||||
- Updated [payment-roadmap.md](payment-roadmap.md) — architecture verification section
|
||||
- Updated [module-boundaries.md](architecture/module-boundaries.md) — Payment platform section
|
||||
- Updated [event-catalog.md](reference/event-catalog.md), [services-contracts.md](reference/services-contracts.md), [authorization-architecture.md](architecture/authorization-architecture.md)
|
||||
- Service snapshot regenerated
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [x] All listed contracts documented with v1 JSON shapes
|
||||
- [x] Service compatibility matrix verified (no existing service code changes)
|
||||
- [x] Tenant enable/disable, PSP choice, mode switch documented as config-only
|
||||
- [x] No implementation of payment-14.0
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [Phase Handover Pay-Arch](phase-handover/phase-pay-arch.md)
|
||||
- [Payment Roadmap](payment-roadmap.md)
|
||||
50
docs/payment-phase-pay-reg.md
Normal file
50
docs/payment-phase-pay-reg.md
Normal file
@ -0,0 +1,50 @@
|
||||
# Phase Pay-Reg — Payment Platform Registration
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Phase ID | `payment-reg` |
|
||||
| Title | Payment Platform Registration |
|
||||
| Status | Complete (documentation-only) |
|
||||
| Service | `payment` (registered; not yet implemented) |
|
||||
| Commercial Product | Torbat Pay |
|
||||
| ADR | [ADR-020](architecture/adr/ADR-020.md) |
|
||||
|
||||
## Purpose
|
||||
|
||||
Register the independent **Enterprise Payment Platform** (Torbat Pay) — service identity, database, phases 14.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)
|
||||
1127
docs/payment-roadmap.md
Normal file
1127
docs/payment-roadmap.md
Normal file
File diff suppressed because it is too large
Load Diff
92
docs/phase-handover/phase-14-5.md
Normal file
92
docs/phase-handover/phase-14-5.md
Normal file
@ -0,0 +1,92 @@
|
||||
# Phase Handover — 14.5 Payment Transaction Ledger (MVP Complete)
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Phase ID | `payment-14.5` |
|
||||
| Title | Transaction Ledger — HIGH priority MVP complete |
|
||||
| Status | Complete |
|
||||
| Service(s) | `payment` (Torbat Pay) |
|
||||
| Version | 0.14.5.0 |
|
||||
| Date | 2026-07-27 |
|
||||
| ADR(s) | [ADR-010](../architecture/adr/ADR-010.md), [ADR-020](../architecture/adr/ADR-020.md), [ADR-021](../architecture/adr/ADR-021.md) |
|
||||
|
||||
## Summary
|
||||
|
||||
Completed Torbat Pay HIGH-priority MVP path **payment-14.0 through payment-14.5**: independent service on port **8012** with foundation licensing shells, BYO-PSP connections, Torbat Pay Merchant accounts, idempotent payment requests, callback/verification with replay protection, and immutable payment-domain transaction ledger distinct from Accounting journals.
|
||||
|
||||
## Completed Phases (this implementation batch)
|
||||
|
||||
| Phase | Version | Key deliverables |
|
||||
| --- | --- | --- |
|
||||
| `payment-14.0` | 0.14.0.0 | Service scaffold, foundation aggregates, bundles/toggles, audit, outbox |
|
||||
| `payment-14.1` | 0.14.1.0 | PSP connections, routing policies, mock adapter, health test |
|
||||
| `payment-14.2` | 0.14.2.0 | Merchant account lifecycle (facilitator mode) |
|
||||
| `payment-14.3` | 0.14.3.0 | Payment requests, idempotency, mock initiate |
|
||||
| `payment-14.4` | 0.14.4.0 | Callback ingress, verify API, paid/failed events |
|
||||
| `payment-14.5` | 0.14.5.0 | PaymentTransaction + PaymentLedgerEntry (append-only) |
|
||||
|
||||
## Public APIs (14.5)
|
||||
|
||||
| Method | Path | Notes |
|
||||
| --- | --- | --- |
|
||||
| GET | `/health`, `/capabilities`, `/metrics` | Discovery |
|
||||
| * | `/api/v1/payment-workspaces` … `/api/v1/settings` | Foundation (14.0) |
|
||||
| * | `/api/v1/psp-connections`, `/api/v1/psp-routing-policies` | Bundle `payment.byo_psp.basic` |
|
||||
| POST | `/api/v1/psp-connections/{id}/test` | Connection health |
|
||||
| * | `/api/v1/merchant-accounts`, lifecycle actions | Bundle `payment.torbat_pay.merchant` |
|
||||
| POST/GET | `/api/v1/payment-requests` | Idempotency-Key required |
|
||||
| POST | `/api/v1/callbacks/{provider}/{connection_id}` | Signature + replay protection |
|
||||
| POST | `/api/v1/payment-requests/{id}/verify` | Manual reconcile |
|
||||
| GET | `/api/v1/payment-transactions`, `/by-source`, `/payment-ledger-entries` | Ledger queries |
|
||||
|
||||
## Events Published
|
||||
|
||||
| Event | Phase | Trigger |
|
||||
| --- | --- | --- |
|
||||
| `payment.request.created` | 14.3 | Request initiated |
|
||||
| `payment.request.redirect_issued` | 14.3 | Mock redirect URL issued |
|
||||
| `payment.request.paid` | 14.4 | Verified paid |
|
||||
| `payment.request.failed` | 14.4 | Verification failed |
|
||||
| `payment.transaction.recorded` | 14.5 | Ledger row on first paid |
|
||||
|
||||
## Permissions
|
||||
|
||||
Full tree under `payment.*` — see [payment-contracts.md](../reference/payment-contracts.md) §8 and `GET /api/v1/permissions/catalog`.
|
||||
|
||||
## Migration Notes
|
||||
|
||||
| Item | Detail |
|
||||
| --- | --- |
|
||||
| Alembic head | `0006_phase_145_ledger` |
|
||||
| Upgrade | `python scripts/ensure_db.py && alembic upgrade head` |
|
||||
| Compose | `payment-service` on port 8012, database `payment_db` |
|
||||
|
||||
## Quality Gates (all green)
|
||||
|
||||
- Architecture layering verified (`test_architecture.py`)
|
||||
- Tenant isolation (`test_tenant_isolation.py`)
|
||||
- Permissions catalog (`test_permissions.py`)
|
||||
- Migrations present (`test_migration.py`)
|
||||
- Security / bundle gating (`test_security.py`)
|
||||
- Phase tests 14.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`
|
||||
139
docs/phase-handover/phase-pay-arch.md
Normal file
139
docs/phase-handover/phase-pay-arch.md
Normal file
@ -0,0 +1,139 @@
|
||||
# Phase Handover — Pay-Arch Payment Architecture Prerequisites
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Phase ID | `payment-arch` |
|
||||
| Title | Payment Architecture Prerequisites |
|
||||
| Status | Complete |
|
||||
| Service(s) | `payment` (architecture only) |
|
||||
| Version | n/a (docs-only) |
|
||||
| Date | 2026-07-27 |
|
||||
| ADR(s) | [ADR-021](../architecture/adr/ADR-021.md) (extends [ADR-020](../architecture/adr/ADR-020.md)) |
|
||||
|
||||
## Summary
|
||||
|
||||
Finalized Torbat Pay architectural prerequisites: three-layer licensing (Core entitlement → Payment bundles → feature toggles), provider assignment without code changes, versioned v1 contracts for current and future flows, and verified compatibility with Core, Identity, Tenant, Accounting, CRM, Communication, Hospitality, Delivery, Sports Center, and Marketplace.
|
||||
|
||||
## Architectural artifacts
|
||||
|
||||
| Artifact | Path |
|
||||
| --- | --- |
|
||||
| ADR-021 | [ADR-021.md](../architecture/adr/ADR-021.md) |
|
||||
| Contract reference | [payment-contracts.md](../reference/payment-contracts.md) |
|
||||
| Roadmap (updated) | [payment-roadmap.md](../payment-roadmap.md) |
|
||||
| Module boundaries | [module-boundaries.md](../architecture/module-boundaries.md) |
|
||||
| Event catalog | [event-catalog.md](../reference/event-catalog.md#payment-platform-planned) |
|
||||
| Services contracts | [services-contracts.md](../reference/services-contracts.md) |
|
||||
|
||||
## Contract versions locked (v1)
|
||||
|
||||
| Contract | Phase | Status |
|
||||
| --- | --- | --- |
|
||||
| PaymentIntentContract | 14.3 | Schema locked |
|
||||
| CheckoutSessionContract | 14.8 | Schema locked |
|
||||
| CallbackIngressContract | 14.4 | Schema locked |
|
||||
| SettlementIntentContract | 14.9 | Schema locked |
|
||||
| RefundIntentContract | 14.6 | Reserved |
|
||||
| SplitAllocationContract | 14.7 | Reserved |
|
||||
| SubscriptionBillingContract | Future | Reserved |
|
||||
| WalletTopUpContract | Future | Reserved |
|
||||
| InstallmentPlanContract | Future | Reserved — **Payment schedule shell only** |
|
||||
| CreditProviderAdapter / CreditAuthorizationContract | Future | Reserved — Torbat Credit via CREDIT_PROVIDER |
|
||||
|
||||
## Torbat Credit reservation (post-arch patch)
|
||||
|
||||
- Future service `credit` / **Torbat Credit** reserved; `credit_db`; `credit.*` events owned by Credit service only
|
||||
- Payment source `CREDIT_PROVIDER` reserved in contracts — not implemented
|
||||
- Reference fields: `credit_provider_id`, `financing_reference`, `credit_authorization_reference`, `installment_contract_reference`
|
||||
- Payment invokes Torbat Credit via **CreditProviderAdapter** (same pattern as PSP)
|
||||
- Installment calculation / scoring / KYC / BNPL / financing = **Torbat Credit exclusive**
|
||||
|
||||
## Compatibility verification
|
||||
|
||||
| Service | Result | Notes |
|
||||
| --- | --- | --- |
|
||||
| Core | ✅ | L1 `payment.module.enabled`; `feature_access.changed` |
|
||||
| Identity | ✅ | Payer `user_ref` only |
|
||||
| Tenant | ✅ | Workspace per Core tenant id |
|
||||
| Accounting | ✅ | SettlementIntent events; no journals in Payment |
|
||||
| CRM | ✅ | `contact_ref` on payer |
|
||||
| Communication | ✅ | Notify client for receipts/refunds |
|
||||
| Hospitality | ✅ | POS/QR refs; no PSP ownership long-term |
|
||||
| Delivery | ✅ | Optional COD ref |
|
||||
| Sports Center | ✅ | Membership payment refs |
|
||||
| Marketplace | ✅ | Checkout + splits via contracts |
|
||||
|
||||
No existing service code was modified.
|
||||
|
||||
## Foundation scope additions for 14.0
|
||||
|
||||
Phase `payment-14.0` MUST implement (per architecture):
|
||||
|
||||
- `PaymentBundleDefinition`, `TenantPaymentBundle`
|
||||
- `PaymentFeatureToggle`
|
||||
- `PaymentProviderAssignment` (shell)
|
||||
- Core entitlement check client (stub)
|
||||
- Bundle-gated permissions and `/capabilities`
|
||||
|
||||
## Known limitations
|
||||
|
||||
- No `backend/services/payment` code yet
|
||||
- Vertical connector **clients** not implemented in Hospitality/Marketplace (Payment 14.8 / vertical phases)
|
||||
- International PSP adapters not registered
|
||||
- Core `payment.module.enabled` feature seed may be added at 14.0 wiring time (Core change in Payment phase only)
|
||||
|
||||
## Next Phase Entry
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Recommended next phase | `payment-14.0` Payment Foundation |
|
||||
| Blockers | None |
|
||||
| Required reads | This handover + [payment-contracts.md](../reference/payment-contracts.md) + [ADR-021](../architecture/adr/ADR-021.md) + [phase-14-0-payment-foundation.md](../phases/Payment/phase-14-0-payment-foundation.md) |
|
||||
|
||||
## Completion Sign-Off
|
||||
|
||||
- [x] ADR-021 accepted
|
||||
- [x] All contracts documented
|
||||
- [x] Manifests, snapshot, registry, project index updated
|
||||
- [x] No business code; no existing services modified
|
||||
- [x] payment-14.0 NOT implemented
|
||||
|
||||
---
|
||||
|
||||
## Addendum — Torbat Credit reservation (post payment-arch patch)
|
||||
|
||||
**Date:** 2026-07-27 · **Scope:** Documentation only · **No implementation**
|
||||
|
||||
### Reserved future service
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Service id | `credit` |
|
||||
| Commercial product | **Torbat Credit** |
|
||||
| Database | `credit_db` (reserved) |
|
||||
| Event namespace | `credit.*` (reserved for Torbat Credit — Payment does not publish) |
|
||||
|
||||
### Payment architecture patches
|
||||
|
||||
1. **External Credit Providers** — Payment registry recognizes `CreditProviderRegistration` alongside PSP catalog.
|
||||
2. **Payment source `CREDIT_PROVIDER`** — reserved in routing policy and PaymentIntent v1; not implemented.
|
||||
3. **Opaque reference fields** on intents/transactions: `credit_provider_id`, `financing_reference`, `credit_authorization_reference`, `installment_contract_reference`.
|
||||
4. **InstallmentPlanContract** (Payment) = schedule shell only. Scoring, KYC, BNPL, financing calculation = **Torbat Credit exclusive**.
|
||||
5. **CreditProviderAdapter** — Payment calls Torbat Credit using the **same provider adapter pattern as PSPs** when implemented.
|
||||
|
||||
### Updated artifacts
|
||||
|
||||
- [ADR-021 §12](../architecture/adr/ADR-021.md)
|
||||
- [payment-contracts.md](../reference/payment-contracts.md) — §4.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)
|
||||
123
docs/phase-handover/phase-pay-reg.md
Normal file
123
docs/phase-handover/phase-pay-reg.md
Normal file
@ -0,0 +1,123 @@
|
||||
# Phase Handover — Pay-Reg Payment Platform Registration
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Phase ID | `payment-reg` |
|
||||
| Title | Payment Platform Registration |
|
||||
| Status | Complete |
|
||||
| Service(s) | `payment` (registered; not yet implemented) |
|
||||
| Commercial Product | Torbat Pay |
|
||||
| Version | n/a (docs-only) |
|
||||
| Date | 2026-07-27 |
|
||||
| ADR(s) | [ADR-020](../architecture/adr/ADR-020.md), [ADR-021](../architecture/adr/ADR-021.md) |
|
||||
|
||||
## Reusable Components
|
||||
|
||||
| Component | Location | Reuse notes |
|
||||
| --- | --- | --- |
|
||||
| Platform registration pattern | Same as Delivery DP-Reg, Experience XP-Reg | Docs + ADR + manifests before code |
|
||||
| Provider adapter pattern | Communication service PSP-like registry | PSP adapters in Payment only |
|
||||
| Service template | [service-template.md](../ai-framework/service-template.md) | Use in Phase 14.0 |
|
||||
| Phase template | [phase-template.md](../ai-framework/phase-template.md) | Use for 14.x phase docs |
|
||||
|
||||
## Public APIs
|
||||
|
||||
| Method | Path | Auth / Permission | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| — | — | — | N/A — registration only; APIs begin in `payment-14.0` |
|
||||
|
||||
Planned surfaces (not implemented): `/health`, `/capabilities`, `/metrics`, `/api/v1/*` under `payment.*` permissions.
|
||||
|
||||
## Events
|
||||
|
||||
| Event type | Domain / Integration | Payload summary | Version |
|
||||
| --- | --- | --- | --- |
|
||||
| `payment.*` (planned) | Payment domain | Publish-only; catalog grows per phase | Planned |
|
||||
|
||||
## Extension Points
|
||||
|
||||
| Extension point | How to extend | Forbidden uses |
|
||||
| --- | --- | --- |
|
||||
| PSP adapters | Register in Payment adapter registry | Verticals calling ZarinPal/IDPay directly |
|
||||
| Facilitator mode | Merchant accounts + platform credentials | Storing PSP secrets in Core or vertical DBs |
|
||||
| Vertical connectors | Versioned checkout contracts (14.8) | Shared DB with Hospitality/Marketplace |
|
||||
| Accounting intents | Events/API for posting intents | JournalEntry creation inside Payment |
|
||||
| Fraud hooks | Optional rule registry (14.10) | Hard dependency for capture |
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- No `backend/services/payment` code yet — Phase 14.0
|
||||
- No Alembic migrations yet
|
||||
- No compose wiring / runtime health yet
|
||||
- No live PSP integrations until 14.1+
|
||||
- Payment capture path incomplete until 14.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)
|
||||
62
docs/phases/Payment/README.md
Normal file
62
docs/phases/Payment/README.md
Normal file
@ -0,0 +1,62 @@
|
||||
# Payment Platform Phase Area (Torbat Pay)
|
||||
|
||||
Independent enterprise **Payment Platform** (`payment-service`, `payment_db`, port **8012**). Commercial product: **Torbat Pay**.
|
||||
|
||||
Supports **BYO-PSP** (tenant-owned gateways) and **Torbat Pay Merchant** (facilitator) modes for all TorbatYar verticals via API + Events only.
|
||||
|
||||
## Phase Numbering
|
||||
|
||||
Payment track phases are **14.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.
|
||||
83
docs/phases/Payment/phase-14-0-payment-foundation.md
Normal file
83
docs/phases/Payment/phase-14-0-payment-foundation.md
Normal file
@ -0,0 +1,83 @@
|
||||
# Phase 14.0 — Payment Foundation
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Identifier | `payment-14.0` |
|
||||
| Implementation Priority | **HIGH** |
|
||||
| Status | Complete |
|
||||
| Service | `payment` |
|
||||
| Version (target) | 0.14.0.0 |
|
||||
| Database | `payment_db` |
|
||||
| API Port | 8012 |
|
||||
| ADR | ADR-001, ADR-003, ADR-006, ADR-020, **ADR-021** |
|
||||
|
||||
> Full specification: [payment-roadmap.md](../../payment-roadmap.md#phase-14-0--payment-foundation)
|
||||
> **Required architecture reads:** [ADR-021](../../architecture/adr/ADR-021.md), [payment-contracts.md](../../reference/payment-contracts.md), [phase-pay-arch handover](../../phase-handover/phase-pay-arch.md)
|
||||
|
||||
## Purpose
|
||||
|
||||
Establish Torbat Pay as an independent enterprise microservice foundation including **three-layer licensing shells** (L2 bundles, L3 toggles), workspace enable/disable, provider assignment shells, and provider protocol stubs — no live PSP or capture flows.
|
||||
|
||||
## Business Scope
|
||||
|
||||
- Per-tenant payment workspace with `enabled` / `disabled` / `suspended` and `default_payment_mode` (`byo_psp` | `torbat_pay_merchant`).
|
||||
- Payment bundle catalog and tenant bundle activation (L2).
|
||||
- Feature toggles (L3) for refunds, splits, subscriptions, installments, wallet top-up (reserved).
|
||||
- Provider assignment shells for routing without code changes (full routing in 14.1).
|
||||
|
||||
## Technical Scope
|
||||
|
||||
- Scaffold `backend/services/payment`; Alembic `0001_initial`.
|
||||
- Aggregates: PaymentWorkspace, PaymentRole, PaymentPermission, **PaymentBundleDefinition**, **TenantPaymentBundle**, **PaymentFeatureToggle**, **PaymentProviderAssignment**, PspProviderRegistration, PaymentConfiguration, PaymentSetting, PaymentAuditLog, OutboxEvent.
|
||||
- Core L1 client stub: check `payment.module.enabled`.
|
||||
- Provider protocols (stubs): PspAdapter, AccountingClient, CommunicationClient, CrmClient, CoreEntitlementClient.
|
||||
|
||||
## Dependencies
|
||||
|
||||
`payment-arch`, `payment-reg`, `onboarding-4`, `ai-framework`, Core entitlement
|
||||
|
||||
## Out of Scope
|
||||
|
||||
PSP connections (14.1), merchant onboarding (14.2), payment requests (14.3+), callbacks, ledger
|
||||
|
||||
## Capabilities
|
||||
|
||||
`payment.foundation`, `payment.workspace`, `payment.bundles`, `payment.feature_toggles`, `payment.provider_assignment_shell`, `payment.psp_registry_shell`, `payment.audit`
|
||||
|
||||
## Permissions
|
||||
|
||||
Per [payment-contracts.md](../../reference/payment-contracts.md) §8 — foundation leaves only; bundle-gated leaves registered but inactive until bundles assigned.
|
||||
|
||||
## Events
|
||||
|
||||
`payment.workspace.enabled|disabled`, `payment.bundle.activated`, `payment.feature_toggle.changed`, `payment.psp_provider.registered`
|
||||
|
||||
## API Contracts
|
||||
|
||||
`/health`, `/capabilities`, `/metrics`, `/api/v1/payment-workspaces`, `/api/v1/bundle-definitions`, `/api/v1/tenant-bundles`, `/api/v1/feature-toggles`, `/api/v1/provider-assignments`, `/api/v1/psp-provider-registrations`, `/api/v1/configurations`, `/api/v1/settings`, `/api/v1/audit`
|
||||
|
||||
## Data Ownership
|
||||
|
||||
Payment sole owner of all foundation tables in `payment_db`.
|
||||
|
||||
## Provider Ownership
|
||||
|
||||
Adapter interfaces only in 14.0.
|
||||
|
||||
## Integration Rules
|
||||
|
||||
- L1 Core check before mutating APIs.
|
||||
- Inactive L2 bundles hide gated routes from `/capabilities`.
|
||||
- No cross-DB access; no existing service modifications.
|
||||
|
||||
## Quality Gates
|
||||
|
||||
Architecture, tenant isolation, bundle isolation, permissions, migration, dependency, security, docs.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
Service boots on 8012; foundation + licensing shells tested; contracts unchanged from payment-arch.
|
||||
|
||||
## Future Compatibility
|
||||
|
||||
All v1 contracts in [payment-contracts.md](../../reference/payment-contracts.md) remain stable; 14.0 tables include fields for `region`, `currency`, facilitator mode per ADR-021.
|
||||
71
docs/phases/Payment/phase-14-1-psp-management.md
Normal file
71
docs/phases/Payment/phase-14-1-psp-management.md
Normal file
@ -0,0 +1,71 @@
|
||||
# Phase 14.1 — PSP Management
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Identifier | `payment-14.1` |
|
||||
| Implementation Priority | **HIGH** |
|
||||
| Status | Complete |
|
||||
| Service | `payment` |
|
||||
| Depends On | `payment-14.0` |
|
||||
|
||||
> Full specification: [payment-roadmap.md](../../payment-roadmap.md#phase-14-1--psp-management)
|
||||
|
||||
## Purpose
|
||||
|
||||
Enable tenants to connect BYO-PSP credentials (ZarinPal, IDPay, NextPay, Pay.ir, Mellat, Parsian, Saman, …), test connections, and configure routing priority with adapter registry.
|
||||
|
||||
## Business Scope
|
||||
|
||||
Connect, test, suspend, and prioritize tenant-owned PSP gateways. Failover between primary and secondary PSP.
|
||||
|
||||
## Technical Scope
|
||||
|
||||
Aggregates: PspConnection, PspCredentialVaultRef, PspConnectionHealthCheck, PspRoutingPolicy. MockPspAdapter + provider-family stubs. Secret refs only in API responses.
|
||||
|
||||
## Dependencies
|
||||
|
||||
`payment-14.0`
|
||||
|
||||
## Out of Scope
|
||||
|
||||
Merchant facilitator onboarding (14.2), payment capture (14.3), callbacks (14.4), international live PSPs.
|
||||
|
||||
## Capabilities
|
||||
|
||||
`payment.psp_connections`, `payment.psp_routing`, `payment.psp_health_check`
|
||||
|
||||
## Permissions
|
||||
|
||||
`payment.psp_connections.view|create|update|delete|test|manage`, `payment.psp_routing.manage`
|
||||
|
||||
## Events
|
||||
|
||||
`payment.psp_connection.created|updated|suspended|activated|tested`, `payment.psp_routing.updated`
|
||||
|
||||
## API Contracts
|
||||
|
||||
`/api/v1/psp-connections`, `/api/v1/psp-routing-policies`, POST `.../test`
|
||||
|
||||
## Data Ownership
|
||||
|
||||
Payment owns connection and routing metadata; vault stores secrets.
|
||||
|
||||
## Provider Ownership
|
||||
|
||||
All PSP SDK calls in Payment adapters.
|
||||
|
||||
## Integration Rules
|
||||
|
||||
Credential rotation must not break in-flight requests. Adapter protocol versioned.
|
||||
|
||||
## Quality Gates
|
||||
|
||||
Secret redaction, tenant isolation, adapter contract tests.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
Tenant PSP connect/test/suspend works with mock adapter; no payment requests yet.
|
||||
|
||||
## Future Compatibility
|
||||
|
||||
Region/currency on connections; Stripe/PayPal adapter slots without schema break.
|
||||
71
docs/phases/Payment/phase-14-10-enterprise-validation.md
Normal file
71
docs/phases/Payment/phase-14-10-enterprise-validation.md
Normal file
@ -0,0 +1,71 @@
|
||||
# Phase 14.10 — Analytics, Fraud Shells & Enterprise Validation
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Identifier | `payment-14.10` |
|
||||
| Implementation Priority | **LATER** |
|
||||
| Status | Planned |
|
||||
| Service | `payment` |
|
||||
| Depends On | `payment-14.9`, `ai-framework` |
|
||||
|
||||
> Full specification: [payment-roadmap.md](../../payment-roadmap.md#phase-1410--analytics-fraud-shells--enterprise-validation)
|
||||
|
||||
## Purpose
|
||||
|
||||
Analytics snapshots, optional fraud rule hooks, and full enterprise validation — closes Payment track 14.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.
|
||||
71
docs/phases/Payment/phase-14-2-merchant-accounts.md
Normal file
71
docs/phases/Payment/phase-14-2-merchant-accounts.md
Normal file
@ -0,0 +1,71 @@
|
||||
# Phase 14.2 — Merchant Accounts
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Identifier | `payment-14.2` |
|
||||
| Implementation Priority | **HIGH** |
|
||||
| Status | Complete |
|
||||
| Service | `payment` |
|
||||
| Depends On | `payment-14.1` |
|
||||
|
||||
> Full specification: [payment-roadmap.md](../../payment-roadmap.md#phase-14-2--merchant-accounts)
|
||||
|
||||
## Purpose
|
||||
|
||||
Introduce Torbat Pay Merchant (facilitator) sub-merchant account shells for tenants who accept payments through platform facilitator mode.
|
||||
|
||||
## Business Scope
|
||||
|
||||
Sub-merchant onboarding lifecycle; KYC/KYB document refs (File Storage); link merchant to workspace and optional branch refs.
|
||||
|
||||
## Technical Scope
|
||||
|
||||
Aggregates: MerchantAccount, MerchantAccountProfile, MerchantSettlementProfile, MerchantComplianceRef, MerchantAccountStatusHistory. Facilitator platform settings.
|
||||
|
||||
## Dependencies
|
||||
|
||||
`payment-14.1`
|
||||
|
||||
## Out of Scope
|
||||
|
||||
Live KYC vendor, bank payouts (14.7/14.9), payment capture (14.3).
|
||||
|
||||
## Capabilities
|
||||
|
||||
`payment.merchant_accounts`, `payment.facilitator_mode`
|
||||
|
||||
## Permissions
|
||||
|
||||
`payment.merchant_accounts.view|create|update|submit|activate|suspend|close|manage`
|
||||
|
||||
## Events
|
||||
|
||||
`payment.merchant_account.created|submitted|activated|suspended|closed`
|
||||
|
||||
## API Contracts
|
||||
|
||||
`/api/v1/merchant-accounts`, `/api/v1/merchant-settlement-profiles`, lifecycle POST actions
|
||||
|
||||
## Data Ownership
|
||||
|
||||
Payment owns merchant domain; Identity users; Storage for compliance blobs (refs).
|
||||
|
||||
## Provider Ownership
|
||||
|
||||
Platform facilitator PSP credentials in Payment platform settings.
|
||||
|
||||
## Integration Rules
|
||||
|
||||
Required when workspace `default_payment_mode=torbat_pay_merchant`. BYO mode skips merchant for routing.
|
||||
|
||||
## Quality Gates
|
||||
|
||||
Lifecycle transitions, PII redaction, tenant isolation.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
Merchant CRUD + lifecycle; facilitator flag on workspace; no payment requests.
|
||||
|
||||
## Future Compatibility
|
||||
|
||||
Multi-currency settlement profiles; international merchant descriptors.
|
||||
71
docs/phases/Payment/phase-14-3-payment-requests.md
Normal file
71
docs/phases/Payment/phase-14-3-payment-requests.md
Normal file
@ -0,0 +1,71 @@
|
||||
# Phase 14.3 — Payment Requests
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Identifier | `payment-14.3` |
|
||||
| Implementation Priority | **HIGH** |
|
||||
| Status | Complete |
|
||||
| Service | `payment` |
|
||||
| Depends On | `payment-14.2` |
|
||||
|
||||
> Full specification: [payment-roadmap.md](../../payment-roadmap.md#phase-14-3--payment-requests)
|
||||
|
||||
## Purpose
|
||||
|
||||
Idempotent payment request initiation for any vertical — amount, currency (IRR first), commercial refs, redirect/token payload from selected PSP.
|
||||
|
||||
## Business Scope
|
||||
|
||||
Initiate pay for order/invoice/membership/checkout refs. Expire abandoned requests. Route via BYO-PSP or facilitator merchant.
|
||||
|
||||
## Technical Scope
|
||||
|
||||
Aggregates: PaymentRequest, PaymentRequestLine, PaymentRequestStatusHistory. Idempotency-Key enforcement. Adapter `initiate_payment()`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
`payment-14.2`, active PSP connection or merchant account
|
||||
|
||||
## Out of Scope
|
||||
|
||||
Callbacks (14.4), ledger (14.5), refunds (14.6).
|
||||
|
||||
## Capabilities
|
||||
|
||||
`payment.requests`, `payment.initiate`, `payment.idempotency`
|
||||
|
||||
## Permissions
|
||||
|
||||
`payment.requests.view|create|cancel|manage`; internal `payment.requests.create`
|
||||
|
||||
## Events
|
||||
|
||||
`payment.request.created|redirect_issued|expired|cancelled|failed`
|
||||
|
||||
## API Contracts
|
||||
|
||||
POST/GET `/api/v1/payment-requests`, POST `.../cancel`, internal `/internal/v1/payment-requests`
|
||||
|
||||
## Data Ownership
|
||||
|
||||
Payment owns request lifecycle; vertical owns commercial aggregate.
|
||||
|
||||
## Provider Ownership
|
||||
|
||||
PSP initiate APIs in adapters only.
|
||||
|
||||
## Integration Rules
|
||||
|
||||
Verticals pass refs only. Duplicate idempotency key returns same request.
|
||||
|
||||
## Quality Gates
|
||||
|
||||
Idempotency, amount validation, state machine, tenant isolation.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
Mock initiate returns redirect payload; events emitted; ledger deferred to 14.5.
|
||||
|
||||
## Future Compatibility
|
||||
|
||||
Multi-currency; tokenized card metadata slots.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user