feat: implement three-tier module classification and framework layer

Module Classification:
- Core (4): core, tenancy, cms, customers - always enabled
- Optional (7): payments, billing, inventory, orders, marketplace, analytics, messaging
- Internal (2): dev-tools, monitoring - admin-only

Key Changes:
- Rename platform-admin module to tenancy
- Promote CMS and Customers to core modules
- Create new payments module (gateway abstractions)
- Add billing→payments and orders→payments dependencies
- Mark dev-tools and monitoring as internal modules

New Infrastructure:
- app/modules/events.py: Module event bus (ENABLED, DISABLED, STARTUP, SHUTDOWN)
- app/modules/migrations.py: Module-specific migration discovery
- app/core/observability.py: Health checks, Prometheus metrics, Sentry integration

Enhanced ModuleDefinition:
- version, is_internal, permissions
- config_schema, default_config
- migrations_path
- Lifecycle hooks: on_enable, on_disable, on_startup, health_check

New Registry Functions:
- get_optional_module_codes(), get_internal_module_codes()
- is_core_module(), is_internal_module()
- get_modules_by_tier(), get_module_tier()

Migrations:
- zc*: Rename platform-admin to tenancy
- zd*: Ensure CMS and Customers enabled for all platforms

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-27 22:02:39 +01:00
parent 9a828999fe
commit 1a52611438
26 changed files with 3084 additions and 67 deletions

View File

@@ -0,0 +1,13 @@
# app/modules/payments/services/__init__.py
"""
Payments module services.
Provides:
- PaymentService: Core payment processing
- GatewayService: Gateway abstraction layer
"""
from app.modules.payments.services.payment_service import PaymentService
from app.modules.payments.services.gateway_service import GatewayService
__all__ = ["PaymentService", "GatewayService"]

View File

@@ -0,0 +1,351 @@
# app/modules/payments/services/gateway_service.py
"""
Gateway service for managing payment gateway configurations.
This service handles:
- Gateway configuration and credentials
- Gateway health checks
- Gateway-specific operations
Each gateway has its own implementation that conforms to the
gateway protocol.
Usage:
from app.modules.payments.services import GatewayService
gateway_service = GatewayService()
# Get available gateways
gateways = gateway_service.get_available_gateways()
# Check gateway status
status = await gateway_service.check_gateway_health("stripe")
"""
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from typing import Any, Protocol
logger = logging.getLogger(__name__)
class GatewayStatus(str, Enum):
"""Gateway operational status."""
ACTIVE = "active"
INACTIVE = "inactive"
ERROR = "error"
MAINTENANCE = "maintenance"
@dataclass
class GatewayInfo:
"""Information about a payment gateway."""
code: str
name: str
status: GatewayStatus
enabled: bool
supports_refunds: bool = True
supports_recurring: bool = False
supported_currencies: list[str] | None = None
config: dict[str, Any] | None = None
class GatewayProtocol(Protocol):
"""Protocol that all gateway implementations must follow."""
@property
def code(self) -> str:
"""Gateway code identifier."""
...
@property
def name(self) -> str:
"""Gateway display name."""
...
async def process_payment(
self,
amount: int,
currency: str,
payment_method: str,
**kwargs: Any,
) -> dict[str, Any]:
"""Process a payment."""
...
async def refund(
self,
transaction_id: str,
amount: int | None = None,
) -> dict[str, Any]:
"""Issue a refund."""
...
async def health_check(self) -> bool:
"""Check if gateway is operational."""
...
class BaseGateway(ABC):
"""Base class for gateway implementations."""
@property
@abstractmethod
def code(self) -> str:
"""Gateway code identifier."""
pass
@property
@abstractmethod
def name(self) -> str:
"""Gateway display name."""
pass
@abstractmethod
async def process_payment(
self,
amount: int,
currency: str,
payment_method: str,
**kwargs: Any,
) -> dict[str, Any]:
"""Process a payment."""
pass
@abstractmethod
async def refund(
self,
transaction_id: str,
amount: int | None = None,
) -> dict[str, Any]:
"""Issue a refund."""
pass
async def health_check(self) -> bool:
"""Check if gateway is operational."""
return True
class StripeGateway(BaseGateway):
"""Stripe payment gateway implementation."""
@property
def code(self) -> str:
return "stripe"
@property
def name(self) -> str:
return "Stripe"
async def process_payment(
self,
amount: int,
currency: str,
payment_method: str,
**kwargs: Any,
) -> dict[str, Any]:
"""Process a payment through Stripe."""
# TODO: Implement Stripe payment processing
logger.info(f"Processing Stripe payment: {amount} {currency}")
return {
"success": True,
"transaction_id": f"pi_mock_{amount}",
"gateway": self.code,
}
async def refund(
self,
transaction_id: str,
amount: int | None = None,
) -> dict[str, Any]:
"""Issue a refund through Stripe."""
# TODO: Implement Stripe refund
logger.info(f"Processing Stripe refund for {transaction_id}")
return {
"success": True,
"refund_id": f"re_mock_{transaction_id}",
}
class PayPalGateway(BaseGateway):
"""PayPal payment gateway implementation."""
@property
def code(self) -> str:
return "paypal"
@property
def name(self) -> str:
return "PayPal"
async def process_payment(
self,
amount: int,
currency: str,
payment_method: str,
**kwargs: Any,
) -> dict[str, Any]:
"""Process a payment through PayPal."""
# TODO: Implement PayPal payment processing
logger.info(f"Processing PayPal payment: {amount} {currency}")
return {
"success": True,
"transaction_id": f"paypal_mock_{amount}",
"gateway": self.code,
}
async def refund(
self,
transaction_id: str,
amount: int | None = None,
) -> dict[str, Any]:
"""Issue a refund through PayPal."""
# TODO: Implement PayPal refund
logger.info(f"Processing PayPal refund for {transaction_id}")
return {
"success": True,
"refund_id": f"paypal_refund_{transaction_id}",
}
class BankTransferGateway(BaseGateway):
"""Bank transfer gateway implementation."""
@property
def code(self) -> str:
return "bank_transfer"
@property
def name(self) -> str:
return "Bank Transfer"
async def process_payment(
self,
amount: int,
currency: str,
payment_method: str,
**kwargs: Any,
) -> dict[str, Any]:
"""Record a bank transfer payment (manual verification)."""
logger.info(f"Recording bank transfer: {amount} {currency}")
return {
"success": True,
"transaction_id": f"bt_mock_{amount}",
"gateway": self.code,
"status": "pending_verification",
}
async def refund(
self,
transaction_id: str,
amount: int | None = None,
) -> dict[str, Any]:
"""Record a bank transfer refund (manual process)."""
logger.info(f"Recording bank transfer refund for {transaction_id}")
return {
"success": True,
"refund_id": f"bt_refund_{transaction_id}",
"status": "pending_manual",
}
class GatewayService:
"""
Service for managing payment gateway configurations.
Provides a registry of available gateways and methods for
gateway operations.
"""
def __init__(self) -> None:
self._gateways: dict[str, BaseGateway] = {
"stripe": StripeGateway(),
"paypal": PayPalGateway(),
"bank_transfer": BankTransferGateway(),
}
self._enabled_gateways: set[str] = {"stripe", "bank_transfer"}
def get_gateway(self, code: str) -> BaseGateway | None:
"""Get a gateway by code."""
return self._gateways.get(code)
def get_available_gateways(self) -> list[GatewayInfo]:
"""Get list of all available gateways with their status."""
result = []
for code, gateway in self._gateways.items():
result.append(
GatewayInfo(
code=code,
name=gateway.name,
status=GatewayStatus.ACTIVE if code in self._enabled_gateways else GatewayStatus.INACTIVE,
enabled=code in self._enabled_gateways,
supports_refunds=True,
supports_recurring=code == "stripe",
supported_currencies=["EUR", "USD", "GBP"] if code != "bank_transfer" else ["EUR"],
)
)
return result
def enable_gateway(self, code: str) -> bool:
"""Enable a gateway."""
if code in self._gateways:
self._enabled_gateways.add(code)
logger.info(f"Enabled gateway: {code}")
return True
return False
def disable_gateway(self, code: str) -> bool:
"""Disable a gateway."""
if code in self._enabled_gateways:
self._enabled_gateways.remove(code)
logger.info(f"Disabled gateway: {code}")
return True
return False
async def check_gateway_health(self, code: str) -> dict[str, Any]:
"""Check the health of a specific gateway."""
gateway = self._gateways.get(code)
if not gateway:
return {"status": "unknown", "message": f"Gateway {code} not found"}
try:
is_healthy = await gateway.health_check()
return {
"status": "healthy" if is_healthy else "unhealthy",
"gateway": code,
}
except Exception as e:
logger.exception(f"Gateway health check failed: {code}")
return {
"status": "error",
"gateway": code,
"message": str(e),
}
async def check_all_gateways(self) -> list[dict[str, Any]]:
"""Check health of all enabled gateways."""
results = []
for code in self._enabled_gateways:
result = await self.check_gateway_health(code)
results.append(result)
return results
# Singleton instance
gateway_service = GatewayService()
__all__ = [
"GatewayService",
"GatewayStatus",
"GatewayInfo",
"GatewayProtocol",
"BaseGateway",
"StripeGateway",
"PayPalGateway",
"BankTransferGateway",
"gateway_service",
]

View File

@@ -0,0 +1,232 @@
# app/modules/payments/services/payment_service.py
"""
Payment service for processing payments through configured gateways.
This service provides a unified interface for payment operations
regardless of the underlying gateway (Stripe, PayPal, etc.).
Usage:
from app.modules.payments.services import PaymentService
payment_service = PaymentService()
# Process a payment
result = await payment_service.process_payment(
amount=1000, # Amount in cents
currency="EUR",
payment_method_id="pm_xxx",
description="Order #123",
)
# Issue a refund
refund = await payment_service.refund(
transaction_id="txn_xxx",
amount=500, # Partial refund
)
"""
import logging
from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal
from enum import Enum
from typing import Any
logger = logging.getLogger(__name__)
class PaymentStatus(str, Enum):
"""Payment transaction status."""
PENDING = "pending"
PROCESSING = "processing"
SUCCEEDED = "succeeded"
FAILED = "failed"
CANCELLED = "cancelled"
REFUNDED = "refunded"
PARTIALLY_REFUNDED = "partially_refunded"
class PaymentGateway(str, Enum):
"""Supported payment gateways."""
STRIPE = "stripe"
PAYPAL = "paypal"
BANK_TRANSFER = "bank_transfer"
@dataclass
class PaymentResult:
"""Result of a payment operation."""
success: bool
transaction_id: str | None = None
gateway: PaymentGateway | None = None
status: PaymentStatus = PaymentStatus.PENDING
amount: int = 0 # Amount in cents
currency: str = "EUR"
error_message: str | None = None
gateway_response: dict[str, Any] | None = None
created_at: datetime | None = None
def to_dict(self) -> dict[str, Any]:
return {
"success": self.success,
"transaction_id": self.transaction_id,
"gateway": self.gateway.value if self.gateway else None,
"status": self.status.value,
"amount": self.amount,
"currency": self.currency,
"error_message": self.error_message,
"created_at": self.created_at.isoformat() if self.created_at else None,
}
@dataclass
class RefundResult:
"""Result of a refund operation."""
success: bool
refund_id: str | None = None
transaction_id: str | None = None
amount: int = 0
status: PaymentStatus = PaymentStatus.PENDING
error_message: str | None = None
class PaymentService:
"""
Service for processing payments through configured gateways.
This service provides a unified interface for:
- Processing payments
- Issuing refunds
- Managing payment methods
- Retrieving transaction history
"""
def __init__(self) -> None:
self._default_gateway = PaymentGateway.STRIPE
async def process_payment(
self,
amount: int,
currency: str = "EUR",
payment_method_id: str | None = None,
gateway: PaymentGateway | None = None,
description: str | None = None,
metadata: dict[str, Any] | None = None,
) -> PaymentResult:
"""
Process a payment through the specified gateway.
Args:
amount: Amount in cents
currency: Currency code (EUR, USD, etc.)
payment_method_id: Stored payment method ID
gateway: Payment gateway to use (default: stripe)
description: Payment description
metadata: Additional metadata for the transaction
Returns:
PaymentResult with transaction details
"""
gateway = gateway or self._default_gateway
logger.info(
f"Processing payment: {amount} {currency} via {gateway.value}",
extra={"amount": amount, "currency": currency, "gateway": gateway.value},
)
# TODO: Implement actual gateway processing
# For now, return a mock successful result
return PaymentResult(
success=True,
transaction_id=f"txn_{gateway.value}_mock",
gateway=gateway,
status=PaymentStatus.SUCCEEDED,
amount=amount,
currency=currency,
created_at=datetime.now(timezone.utc),
)
async def refund(
self,
transaction_id: str,
amount: int | None = None,
reason: str | None = None,
) -> RefundResult:
"""
Issue a refund for a transaction.
Args:
transaction_id: Original transaction ID
amount: Refund amount in cents (None for full refund)
reason: Reason for refund
Returns:
RefundResult with refund details
"""
logger.info(
f"Issuing refund for transaction {transaction_id}",
extra={"transaction_id": transaction_id, "amount": amount, "reason": reason},
)
# TODO: Implement actual refund processing
return RefundResult(
success=True,
refund_id=f"rf_{transaction_id}",
transaction_id=transaction_id,
amount=amount or 0,
status=PaymentStatus.REFUNDED,
)
async def get_transaction(self, transaction_id: str) -> dict[str, Any] | None:
"""
Get transaction details by ID.
Args:
transaction_id: Transaction ID
Returns:
Transaction details or None if not found
"""
# TODO: Implement transaction lookup
return None
async def list_transactions(
self,
vendor_id: int | None = None,
platform_id: int | None = None,
status: PaymentStatus | None = None,
limit: int = 50,
offset: int = 0,
) -> list[dict[str, Any]]:
"""
List transactions with optional filters.
Args:
vendor_id: Filter by vendor
platform_id: Filter by platform
status: Filter by status
limit: Maximum results
offset: Pagination offset
Returns:
List of transaction records
"""
# TODO: Implement transaction listing
return []
# Singleton instance
payment_service = PaymentService()
__all__ = [
"PaymentService",
"PaymentStatus",
"PaymentGateway",
"PaymentResult",
"RefundResult",
"payment_service",
]