Phân tích hệ thống
#
1. TỔNG QUAN KIẾN TRÚC
1.1. Kiến trúc hệ thống
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ SmartPost │────▶│ Payment Service │────▶│ Payment Gates │
│ Projects │◀────│ (Multi-tenant) │◀────│ VNPay|MoMo|Zalo │
│ [SM,TMS,TICKET] │ └─────────────────┘ └─────────────────┘
└─────────────────┘ │ │
│ ▼ ▼
▼ ┌─────────────────┐ ┌─────────────────┐
┌─────────────────┐ │ Database │ │ External │
│ Load Balancer │ │ (MySQL 8.0) │ │ Services │
│ (Nginx/HAProxy)│ └─────────────────┘ │ [Kafka/Redis] │
└─────────────────┘ │ └─────────────────┘
▼
┌─────────────────┐
│ Monitoring │
│ [ELK + Grafana] │
└─────────────────┘
```
1.2. Tech Stack
- Framework: Laravel 11.x
- Architecture: Domain Driven Design (DDD) + CQRS
- Database: MySQL 8.0 (Master-Slave for read scaling)
- Cache: Redis Cluster
- Queue: Redis + Laravel Horizon
- Search: Elasticsearch 8.x
- Monitoring: ELK Stack + Grafana + Prometheus
- Container: Docker + Kubernetes
- Authentication: Internal service (Network security)
- API
Documentation**Documentation: OpenAPI 3.0 + Swagger UI - Testing: PHPUnit + Pest + Feature Tests
1.3. Security Model
- Network
Security**Security: Service được bảo vệ bởi VPN, Firewall và Service Mesh - Request
Validation**Validation: Validate project_code và app_code từ internal registry - Webhook
Security**Security: Multi-layer signature validation - Rate
Limiting**Limiting: Intelligent rate limiting với Redis - Data
Encryption**Encryption: AES-256 cho sensitive data - Audit
Trail**Trail: Comprehensive audit logging - GDPR
Compliance**Compliance: Data retention và deletion policies
1.4. Deployment Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Production │ │ Staging │ │ Development │
│ │ │ │ │ │
│ - 3 App Servers │ │ - 1 App Server │ │ - Local Docker │
│ - 2 DB Servers │ │ - 1 DB Server │ │ - SQLite/MySQL │
│ - 3 Redis Nodes │ │ - 1 Redis Node │ │ - Redis Local │
│ - Load Balancer │ │ - Simple LB │ │ - No LB │
│ - Auto Scaling │ │ - Manual Scale │ │ - Manual │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
2. CẤU TRÚC THƯ MỤC LARAVEL + DDD
src/
├── app/
│ ├── Console/
│ │ ├── Commands/
│ │ │ ├── System/
│ │ │ │ ├── CheckSystemHealthCommand.php
│ │ │ │ ├── CleanupOldLogsCommand.php
│ │ │ │ ├── GenerateSystemReportCommand.php
│ │ │ │ └── UpdateSystemConfigCommand.php
│ │ │ ├── Transaction/
│ │ │ │ ├── ProcessFailedTransactionsCommand.php
│ │ │ │ ├── ReconcileTransactionsCommand.php
│ │ │ │ ├── RetryFailedCallbacksCommand.php
│ │ │ │ └── CleanupExpiredTransactionsCommand.php
│ │ │ ├── Webhook/
│ │ │ │ ├── ProcessFailedWebhooksCommand.php
│ │ │ │ ├── RetryWebhookDeliveryCommand.php
│ │ │ │ └── ValidateWebhookConfigCommand.php
│ │ │ └── Maintenance/
│ │ │ ├── ArchiveOldDataCommand.php
│ │ │ ├── OptimizeDatabaseCommand.php
│ │ │ └── BackupCriticalDataCommand.php
│ │ └── Kernel.php
│ │
│ ├── Domain/
│ │ ├── Project/
│ │ │ ├── Entities/
│ │ │ │ ├── Project.php
│ │ │ │ └── ProjectConfiguration.php
│ │ │ ├── ValueObjects/
│ │ │ │ ├── ProjectCode.php
│ │ │ │ ├── ProjectStatus.php
│ │ │ │ └── Currency.php
│ │ │ ├── Repositories/
│ │ │ │ └── ProjectRepositoryInterface.php
│ │ │ ├── Services/
│ │ │ │ ├── ProjectService.php
│ │ │ │ ├── ProjectConfigurationService.php
│ │ │ │ └── ProjectValidationService.php
│ │ │ └── Events/
│ │ │ ├── ProjectCreated.php
│ │ │ ├── ProjectActivated.php
│ │ │ └── ProjectDeactivated.php
│ │ │
│ │ ├── Tenant/
│ │ │ ├── Entities/
│ │ │ │ ├── Tenant.php
│ │ │ │ └── TenantPaymentProvider.php
│ │ │ ├── ValueObjects/
│ │ │ │ ├── AppCode.php
│ │ │ │ ├── TenantStatus.php
│ │ │ │ └── BusinessType.php
│ │ │ ├── Repositories/
│ │ │ │ ├── TenantRepositoryInterface.php
│ │ │ │ └── TenantPaymentProviderRepositoryInterface.php
│ │ │ ├── Services/
│ │ │ │ ├── TenantService.php
│ │ │ │ ├── TenantOnboardingService.php
│ │ │ │ └── TenantConfigurationService.php
│ │ │ └── Events/
│ │ │ ├── TenantCreated.php
│ │ │ ├── TenantProviderConfigured.php
│ │ │ └── TenantLimitUpdated.php
│ │ │
│ │ ├── Payment/
│ │ │ ├── Entities/
│ │ │ │ ├── Transaction.php
│ │ │ │ ├── PaymentProvider.php
│ │ │ │ ├── TransactionEvent.php
│ │ │ │ └── Refund.php
│ │ │ ├── ValueObjects/
│ │ │ │ ├── Money.php
│ │ │ │ ├── TransactionStatus.php
│ │ │ │ ├── PaymentMethod.php
│ │ │ │ └── TransactionReference.php
│ │ │ ├── Repositories/
│ │ │ │ ├── TransactionRepositoryInterface.php
│ │ │ │ ├── PaymentProviderRepositoryInterface.php
│ │ │ │ └── RefundRepositoryInterface.php
│ │ │ ├── Services/
│ │ │ │ ├── TransactionService.php
│ │ │ │ ├── PaymentProviderService.php
│ │ │ │ ├── RefundService.php
│ │ │ │ ├── FraudDetectionService.php
│ │ │ │ └── PaymentProviderFactory.php
│ │ │ ├── Adapters/
│ │ │ │ ├── VNPayAdapter.php
│ │ │ │ ├── MoMoAdapter.php
│ │ │ │ ├── ZaloPayAdapter.php
│ │ │ │ └── NapasAdapter.php
│ │ │ └── Events/
│ │ │ ├── TransactionCreated.php
│ │ │ ├── TransactionCompleted.php
│ │ │ ├── TransactionFailed.php
│ │ │ ├── RefundInitiated.php
│ │ │ └── FraudDetected.php
│ │ │
│ │ ├── Webhook/
│ │ │ ├── Entities/
│ │ │ │ ├── WebhookLog.php
│ │ │ │ ├── WebhookDelivery.php
│ │ │ │ └── WebhookConfiguration.php
│ │ │ ├── ValueObjects/
│ │ │ │ ├── WebhookSignature.php
│ │ │ │ ├── WebhookStatus.php
│ │ │ │ └── WebhookType.php
│ │ │ ├── Repositories/
│ │ │ │ ├── WebhookLogRepositoryInterface.php
│ │ │ │ └── WebhookConfigurationRepositoryInterface.php
│ │ │ ├── Services/
│ │ │ │ ├── WebhookService.php
│ │ │ │ ├── WebhookValidationService.php
│ │ │ │ ├── WebhookDeliveryService.php
│ │ │ │ └── WebhookSecurityService.php
│ │ │ └── Events/
│ │ │ ├── WebhookReceived.php
│ │ │ ├── WebhookProcessed.php
│ │ │ └── WebhookFailed.php
│ │ │
│ │ ├── Reconciliation/
│ │ │ ├── Entities/
│ │ │ │ ├── PaymentReconciliation.php
│ │ │ │ ├── ReconciliationDetail.php
│ │ │ │ └── ReconciliationReport.php
│ │ │ ├── ValueObjects/
│ │ │ │ ├── ReconciliationStatus.php
│ │ │ │ ├── MismatchType.php
│ │ │ │ └── ReconciliationPeriod.php
│ │ │ ├── Repositories/
│ │ │ │ ├── ReconciliationRepositoryInterface.php
│ │ │ │ └── ReconciliationDetailRepositoryInterface.php
│ │ │ ├── Services/
│ │ │ │ ├── ReconciliationService.php
│ │ │ │ ├── ReconciliationReportService.php
│ │ │ │ ├── MismatchResolutionService.php
│ │ │ │ └── AutoReconciliationService.php
│ │ │ └── Events/
│ │ │ ├── ReconciliationStarted.php
│ │ │ ├── ReconciliationCompleted.php
│ │ │ └── MismatchDetected.php
│ │ │
│ │ └── Shared/
│ │ ├── ValueObjects/
│ │ │ ├── EntityId.php
│ │ │ ├── Timestamp.php
│ │ │ ├── IpAddress.php
│ │ │ └── UserAgent.php
│ │ ├── Contracts/
│ │ │ ├── EventPublisherInterface.php
│ │ │ ├── AuditableInterface.php
│ │ │ └── CacheableInterface.php
│ │ ├── Traits/
│ │ │ ├── HasUuid.php
│ │ │ ├── Auditable.php
│ │ │ ├── SoftDeletable.php
│ │ │ └── Cacheable.php
│ │ └── Exceptions/
│ │ ├── DomainException.php
│ │ ├── ValidationException.php
│ │ └── BusinessRuleException.php
│ │
│ ├── Infrastructure/
│ │ ├── Persistence/
│ │ │ ├── Repositories/
│ │ │ │ ├── Eloquent/
│ │ │ │ │ ├── EloquentProjectRepository.php
│ │ │ │ │ ├── EloquentTenantRepository.php
│ │ │ │ │ ├── EloquentTransactionRepository.php
│ │ │ │ │ ├── EloquentWebhookLogRepository.php
│ │ │ │ │ └── EloquentReconciliationRepository.php
│ │ │ │ └── Cache/
│ │ │ │ ├── CachedProjectRepository.php
│ │ │ │ ├── CachedTenantRepository.php
│ │ │ │ └── CachedConfigurationRepository.php
│ │ │ └── Models/
│ │ │ ├── Project.php
│ │ │ ├── Tenant.php
│ │ │ ├── TenantPaymentProvider.php
│ │ │ ├── PaymentProvider.php
│ │ │ ├── Transaction.php
│ │ │ ├── TransactionEvent.php
│ │ │ ├── WebhookLog.php
│ │ │ ├── PaymentReconciliation.php
│ │ │ ├── ReconciliationDetail.php
│ │ │ ├── AuditLog.php
│ │ │ ├── SystemConfiguration.php
│ │ │ └── IdempotencyKey.php
│ │ │
│ │ ├── External/
│ │ │ ├── PaymentProviders/
│ │ │ │ ├── VNPay/
│ │ │ │ │ ├── VNPayClient.php
│ │ │ │ │ ├── VNPaySignature.php
│ │ │ │ │ └── VNPayResponseParser.php
│ │ │ │ ├── MoMo/
│ │ │ │ │ ├── MoMoClient.php
│ │ │ │ │ ├── MoMoSignature.php
│ │ │ │ │ └── MoMoResponseParser.php
│ │ │ │ └── Common/
│ │ │ │ ├── HttpClient.php
│ │ │ │ ├── SignatureValidator.php
│ │ │ │ └── ResponseNormalizer.php
│ │ │ └── Notifications/
│ │ │ ├── SlackNotifier.php
│ │ │ ├── EmailNotifier.php
│ │ │ └── SmsNotifier.php
│ │ │
│ │ ├── Cache/
│ │ │ ├── RedisCache.php
│ │ │ ├── CacheManager.php
│ │ │ └── CacheKeys.php
│ │ │
│ │ └── Queue/
│ │ ├── Jobs/
│ │ │ ├── ProcessWebhookJob.php
│ │ │ ├── SendCallbackJob.php
│ │ │ ├── ProcessReconciliationJob.php
│ │ │ ├── SendNotificationJob.php
│ │ │ └── CleanupExpiredDataJob.php
│ │ └── Processors/
│ │ ├── WebhookProcessor.php
│ │ ├── CallbackProcessor.php
│ │ └── NotificationProcessor.php
│ │
│ ├── Application/
│ │ ├── Commands/
│ │ │ ├── Project/
│ │ │ │ ├── CreateProjectCommand.php
│ │ │ │ ├── UpdateProjectCommand.php
│ │ │ │ └── DeleteProjectCommand.php
│ │ │ ├── Tenant/
│ │ │ │ ├── CreateTenantCommand.php
│ │ │ │ ├── UpdateTenantCommand.php
│ │ │ │ ├── ConfigureTenantProviderCommand.php
│ │ │ │ └── DeactivateTenantCommand.php
│ │ │ ├── Transaction/
│ │ │ │ ├── CreateTransactionCommand.php
│ │ │ │ ├── ProcessTransactionCallbackCommand.php
│ │ │ │ ├── RefundTransactionCommand.php
│ │ │ │ └── CancelTransactionCommand.php
│ │ │ └── Webhook/
│ │ │ ├── ProcessWebhookCommand.php
│ │ │ └── ResendWebhookCommand.php
│ │ │
│ │ ├── Queries/
│ │ │ ├── Project/
│ │ │ │ ├── GetProjectQuery.php
│ │ │ │ └── ListProjectsQuery.php
│ │ │ ├── Tenant/
│ │ │ │ ├── GetTenantQuery.php
│ │ │ │ └── ListTenantsQuery.php
│ │ │ ├── Transaction/
│ │ │ │ ├── GetTransactionQuery.php
│ │ │ │ ├── ListTransactionsQuery.php
│ │ │ │ └── GetTransactionStatisticsQuery.php
│ │ │ └── Reporting/
│ │ │ ├── GenerateReconciliationReportQuery.php
│ │ │ ├── GenerateTransactionReportQuery.php
│ │ │ └── GenerateSystemHealthReportQuery.php
│ │ │
│ │ ├── Handlers/
│ │ │ ├── CommandHandlers/
│ │ │ │ ├── Project/
│ │ │ │ ├── Tenant/
│ │ │ │ ├── Transaction/
│ │ │ │ └── Webhook/
│ │ │ └── QueryHandlers/
│ │ │ ├── Project/
│ │ │ ├── Tenant/
│ │ │ ├── Transaction/
│ │ │ └── Reporting/
│ │ │
│ │ └── Services/
│ │ ├── ApplicationService.php
│ │ ├── CommandBusService.php
│ │ ├── QueryBusService.php
│ │ └── EventBusService.php
│ │
│ ├── Http/
│ │ ├── Controllers/
│ │ │ ├── API/
│ │ │ │ ├── V1/
│ │ │ │ │ ├── Admin/
│ │ │ │ │ │ ├── ProjectController.php
│ │ │ │ │ │ ├── TenantController.php
│ │ │ │ │ │ ├── SystemController.php
│ │ │ │ │ │ ├── ReconciliationController.php
│ │ │ │ │ │ └── MonitoringController.php
│ │ │ │ │ ├── Client/
│ │ │ │ │ │ ├── TransactionController.php
│ │ │ │ │ │ ├── PaymentController.php
│ │ │ │ │ │ └── StatusController.php
│ │ │ │ │ └── Webhook/
│ │ │ │ │ ├── VNPayWebhookController.php
│ │ │ │ │ ├── MoMoWebhookController.php
│ │ │ │ │ ├── ZaloPayWebhookController.php
│ │ │ │ │ └── BaseWebhookController.php
│ │ │ │ └── BaseApiController.php
│ │ │ └── Web/
│ │ │ ├── DashboardController.php
│ │ │ └── HealthCheckController.php
│ │ │
│ │ ├── Middleware/
│ │ │ ├── Security/
│ │ │ │ ├── RateLimitMiddleware.php
│ │ │ │ ├── IpWhitelistMiddleware.php
│ │ │ │ ├── ValidateSignatureMiddleware.php
│ │ │ │ └── CorsMiddleware.php
│ │ │ ├── Request/
│ │ │ │ ├── ResolveProjectAndTenantMiddleware.php
│ │ │ │ ├── ValidateRequestMiddleware.php
│ │ │ │ ├── TransformRequestMiddleware.php
│ │ │ │ └── IdempotencyMiddleware.php
│ │ │ ├── Logging/
│ │ │ │ ├── AuditLogMiddleware.php
│ │ │ │ ├── RequestLoggingMiddleware.php
│ │ │ │ └── PerformanceLoggingMiddleware.php
│ │ │ └── Maintenance/
│ │ │ ├── MaintenanceModeMiddleware.php
│ │ │ └── FeatureToggleMiddleware.php
│ │ │
│ │ ├── Requests/
│ │ │ ├── BaseRequest.php
│ │ │ ├── Project/
│ │ │ │ ├── CreateProjectRequest.php
│ │ │ │ ├── UpdateProjectRequest.php
│ │ │ │ └── ConfigureProjectProviderRequest.php
│ │ │ ├── Tenant/
│ │ │ │ ├── CreateTenantRequest.php
│ │ │ │ ├── UpdateTenantRequest.php
│ │ │ │ └── ConfigureTenantProviderRequest.php
│ │ │ ├── Transaction/
│ │ │ │ ├── CreateTransactionRequest.php
│ │ │ │ ├── QueryTransactionRequest.php
│ │ │ │ ├── RefundTransactionRequest.php
│ │ │ │ └── CancelTransactionRequest.php
│ │ │ └── Webhook/
│ │ │ ├── VNPayWebhookRequest.php
│ │ │ ├── MoMoWebhookRequest.php
│ │ │ └── BaseWebhookRequest.php
│ │ │
│ │ ├── Resources/
│ │ │ ├── ProjectResource.php
│ │ │ ├── TenantResource.php
│ │ │ ├── TransactionResource.php
│ │ │ ├── PaymentProviderResource.php
│ │ │ ├── ReconciliationResource.php
│ │ │ └── Collections/
│ │ │ ├── ProjectCollection.php
│ │ │ ├── TenantCollection.php
│ │ │ └── TransactionCollection.php
│ │ │
│ │ └── Responses/
│ │ ├── ApiResponse.php
│ │ ├── ErrorResponse.php
│ │ ├── SuccessResponse.php
│ │ └── PaginatedResponse.php
│ │
│ ├── Exceptions/
│ │ ├── Handler.php
│ │ ├── BaseException.php
│ │ ├── BusinessException.php
│ │ ├── ValidationException.php
│ │ ├── PaymentProviderException.php
│ │ ├── WebhookException.php
│ │ ├── ReconciliationException.php
│ │ └── SystemException.php
│ │
│ └── Providers/
│ ├── AppServiceProvider.php
│ ├── RouteServiceProvider.php
│ ├── EventServiceProvider.php
│ ├── BroadcastServiceProvider.php
│ ├── DomainServiceProvider.php
│ ├── InfrastructureServiceProvider.php
│ └── ApplicationServiceProvider.php
│
├── config/
│ ├── app.php
│ ├── database.php
│ ├── cache.php
│ ├── queue.php
│ ├── payment-providers.php
│ ├── webhook.php
│ ├── reconciliation.php
│ ├── audit.php
│ ├── monitoring.php
│ └── security.php
│
├── database/
│ ├── migrations/
│ ├── seeders/
│ ├── factories/
│ └── schema/
│ └── smartpost_payment_service_v4.sql
│
├── routes/
│ ├── api.php
│ ├── web.php
│ ├── webhook.php
│ └── admin.php
│
├── resources/
│ ├── views/
│ │ ├── dashboard/
│ │ ├── reports/
│ │ └── emails/
│ └── lang/
│ ├── en/
│ └── vi/
│
├── storage/
│ ├── app/
│ │ ├── reconciliation-reports/
│ │ ├── transaction-exports/
│ │ └── audit-logs/
│ ├── framework/
│ └── logs/
│
└── tests/
├── Unit/
│ ├── Domain/
│ ├── Application/
│ └── Infrastructure/
├── Feature/
│ ├── API/
│ ├── Webhook/
│ └── Integration/
└── Helpers/
```
3. DANH SÁCH API ENDPOINTS
3.1. Admin APIs (Internal Management)
3.1.1. Project Management
POST /api/v1/admin/projects # Tạo dự án mới
GET /api/v1/admin/projects # Danh sách dự án
GET /api/v1/admin/projects/{project_id} # Chi tiết dự án
PUT /api/v1/admin/projects/{project_id} # Cập nhật dự án
DELETE /api/v1/admin/projects/{project_id} # Xóa dự án
POST /api/v1/admin/projects/{project_id}/providers # Cấu hình cổng thanh toán cho dự án
```
3.1.2. Tenant Management
POST /api/v1/admin/tenants # Tạo tenant mới
GET /api/v1/admin/tenants # Danh sách tenant
GET /api/v1/admin/tenants/{tenant_id} # Chi tiết tenant
PUT /api/v1/admin/tenants/{tenant_id} # Cập nhật tenant
DELETE /api/v1/admin/tenants/{tenant_id} # Xóa tenant
POST /api/v1/admin/tenants/{tenant_id}/providers # Cấu hình cổng thanh toán cho tenant
PUT /api/v1/admin/tenants/{tenant_id}/limits # Cập nhật hạn mức tenant
```
3.1.3. System Management
GET /api/v1/admin/system/health # Kiểm tra sức khỏe hệ thống
GET /api/v1/admin/system/metrics # Metrics hệ thống
GET /api/v1/admin/system/configurations # Danh sách cấu hình hệ thống
PUT /api/v1/admin/system/configurations/{key} # Cập nhật cấu hình
POST /api/v1/admin/system/maintenance # Bật/tắt chế độ bảo trì
```
3.2. Client APIs (Business Operations)
3.2.1. Transaction APIs
POST /api/v1/{project_code}/{app_code}/transactions # Tạo giao dịch
GET /api/v1/{project_code}/{app_code}/transactions/{transaction_id} # Chi tiết giao dịch
GET /api/v1/{project_code}/{app_code}/transactions/ref/{reference_id} # Tìm giao dịch theo reference
POST /api/v1/{project_code}/{app_code}/transactions/{transaction_id}/refund # Hoàn tiền
POST /api/v1/{project_code}/{app_code}/transactions/{transaction_id}/cancel # Hủy giao dịch
GET /api/v1/{project_code}/{app_code}/transactions # Danh sách giao dịch (có filter)
```
3.2.2. Payment Provider APIs
GET /api/v1/{project_code}/{app_code}/providers # Danh sách cổng thanh toán khả dụng
GET /api/v1/{project_code}/{app_code}/providers/{provider_code} # Chi tiết cổng thanh toán
```
3.2.3. Status & Health APIs
GET /api/v1/{project_code}/{app_code}/status # Trạng thái tenant
GET /api/v1/status # Health check chung
GET /api/v1/version # Thông tin version
```
3.3. Webhook APIs
3.3.1. Payment Provider Webhooks
POST /api/v1/webhooks/vnpay # VNPay webhook
GET /api/v1/webhooks/vnpay/return # VNPay return URL
POST /api/v1/webhooks/momo # MoMo webhook
GET /api/v1/webhooks/momo/return # MoMo return URL
POST /api/v1/webhooks/zalopay # ZaloPay webhook
GET /api/v1/webhooks/zalopay/return # ZaloPay return URL
POST /api/v1/webhooks/napas # NAPAS webhook
GET /api/v1/webhooks/napas/return # NAPAS return URL
```
3.3.2. Generic Webhook APIs
POST /api/v1/webhooks/{provider_code} # Generic webhook handler
POST /api/v1/webhooks/{provider_code}/test # Test webhook
GET /api/v1/webhooks/logs # Webhook logs
POST /api/v1/webhooks/{webhook_id}/retry # Retry webhook
```
3.4. Reconciliation APIs
3.4.1. Reconciliation Management
GET /api/v1/admin/reconciliations # Danh sách đối soát
POST /api/v1/admin/reconciliations # Tạo đối soát mới
GET /api/v1/admin/reconciliations/{reconciliation_id} # Chi tiết đối soát
PUT /api/v1/admin/reconciliations/{reconciliation_id}/resolve # Giải quyết mismatch
GET /api/v1/admin/reconciliations/{reconciliation_id}/details # Chi tiết mismatch
POST /api/v1/admin/reconciliations/{reconciliation_id}/export # Export báo cáo
```
3.5. Monitoring & Reporting APIs
3.5.1. Audit & Logging
GET /api/v1/admin/audit-logs # Audit logs
GET /api/v1/admin/webhook-logs # Webhook logs
GET /api/v1/admin/transaction-logs # Transaction logs
GET /api/v1/admin/system-logs # System logs
```
3.5.2. Analytics & Reports
GET /api/v1/admin/reports/transactions # Báo cáo giao dịch
GET /api/v1/admin/reports/revenue # Báo cáo doanh thu
GET /api/v1/admin/reports/providers # Báo cáo theo cổng thanh toán
GET /api/v1/admin/reports/tenants # Báo cáo theo tenant
POST /api/v1/admin/reports/custom # Báo cáo tùy chỉnh
```
4. CHI TIẾT API REQUEST/RESPONSE
4.1. Transaction Management APIs
4.1.1. Tạo giao dịch mới
POST `/api/v1/{project_code}/{app_code}/transactions`transactions
Request:**
{
"order_id": "ORD-20250703-001",
"amount": 100000,
"currency": "VND",
"description": "Thanh toán đơn hàng #12345",
"provider_code": "vnpay",
"payment_method": "card",
"customer_info": {
"name": "Nguyễn Văn A",
"email": "nguyenvana@example.com",
"phone": "0901234567",
"address": "123 Đường ABC, Quận 1, TP.HCM"
},
"return_url": "https://yourapp.com/payment/success",
"cancel_url": "https://yourapp.com/payment/cancel",
"callback_url": "https://yourapp.com/api/webhooks/payment",
"expires_in_minutes": 15,
"metadata": {
"internal_order_id": "12345",
"promotion_code": "SUMMER2025",
"user_id": "USER-001"
}
}
```
Response Success (201):**
{
"success": true,
"message": "Transaction created successfully",
"data": {
"transaction_id": "txn_7k8l9m0n1o2p3q4r5s6t",
"order_id": "ORD-20250703-001",
"amount": 100000,
"currency": "VND",
"status": "pending",
"provider_transaction_id": null,
"payment_url": "https://sandbox.vnpayment.vn/paymentv2/vpcpay.html?...",
"expires_at": "2025-07-03T10:30:00+07:00",
"created_at": "2025-07-03T10:15:00+07:00"
},
"request_id": "req_1a2b3c4d5e6f7g8h9i0j"
}
```
Response Error (400):**
{
"success": false,
"error_code": "INVALID_AMOUNT",
"message": "Số tiền phải lớn hơn 10,000 VND và nhỏ hơn 500,000,000 VND",
"details": {
"field": "amount",
"value": 5000,
"min_amount": 10000,
"max_amount": 500000000
},
"request_id": "req_1a2b3c4d5e6f7g8h9i0j"
}
```
4.1.2. Lấy thông tin giao dịch
GET `/api/v1/{project_code}/{app_code}/transactions/{transaction_id}`
Response Success (200):**
{
"success": true,
"data": {
"transaction_id": "txn_7k8l9m0n1o2p3q4r5s6t",
"order_id": "ORD-20250703-001",
"amount": 100000,
"currency": "VND",
"description": "Thanh toán đơn hàng #12345",
"status": "completed",
"payment_method": "card",
"provider_code": "vnpay",
"provider_transaction_id": "14339599",
"provider_order_id": "ORD20250703001",
"customer_info": {
"name": "Nguyễn Văn A",
"email": "nguyenvana@example.com",
"phone": "0901234567"
},
"transaction_fee": 2300,
"net_amount": 97700,
"paid_at": "2025-07-03T10:18:32+07:00",
"events": [
{
"event_type": "created",
"created_at": "2025-07-03T10:15:00+07:00",
"data": {}
},
{
"event_type": "payment_initiated",
"created_at": "2025-07-03T10:15:30+07:00",
"data": {
"payment_url": "https://sandbox.vnpayment.vn/..."
}
},
{
"event_type": "paid",
"created_at": "2025-07-03T10:18:32+07:00",
"data": {
"provider_response_code": "00",
"bank_code": "NCB"
}
}
],
"metadata": {
"internal_order_id": "12345",
"promotion_code": "SUMMER2025",
"user_id": "USER-001"
},
"created_at": "2025-07-03T10:15:00+07:00",
"updated_at": "2025-07-03T10:18:32+07:00"
},
"request_id": "req_2b3c4d5e6f7g8h9i0j1k"
}
```
4.1.3. Hoàn tiền giao dịch
POST `/api/v1/{project_code}/{app_code}/transactions/{transaction_id}/refund`refund
Request:**
{
"refund_amount": 50000,
"reason": "Khách hàng yêu cầu hoàn trả một phần",
"internal_refund_id": "REFUND-001",
"notification_url": "https://yourapp.com/api/webhooks/refund"
}
```
Response Success (200):**
{
"success": true,
"message": "Refund initiated successfully",
"data": {
"refund_id": "rfn_9a8b7c6d5e4f3g2h1i0j",
"transaction_id": "txn_7k8l9m0n1o2p3q4r5s6t",
"refund_amount": 50000,
"remaining_amount": 50000,
"status": "processing",
"provider_refund_id": null,
"reason": "Khách hàng yêu cầu hoàn trả một phần",
"created_at": "2025-07-03T14:30:00+07:00"
},
"request_id": "req_3c4d5e6f7g8h9i0j1k2l"
}
```
4.2. Webhook Processing
4.2.1. VNPay Webhook
POST `/api/v1/webhooks/vnpay`vnpay
Request từ VNPay:**
{
"vnp_Amount": "10000000",
"vnp_BankCode": "NCB",
"vnp_BankTranNo": "VNP14339599",
"vnp_CardType": "ATM",
"vnp_OrderInfo": "Thanh toan don hang %3A ORD-20250703-001",
"vnp_PayDate": "20250703101832",
"vnp_ResponseCode": "00",
"vnp_TmnCode": "VNPAY_TMN",
"vnp_TransactionNo": "14339599",
"vnp_TransactionStatus": "00",
"vnp_TxnRef": "ORD20250703001",
"vnp_SecureHash": "3d4f8b2a1c5e7f9d0a2b4c6e8f1a3b5c"
}
```
Response tới VNPay:**
{
"RspCode": "00",
"Message": "Confirm Success"
}
```
4.3. Error Responses
4.3.1. Standard Error Format
{
"success": false,
"error_code": "ERROR_CODE_CONSTANT",
"message": "Human readable error message",
"details": {
"field": "field_name",
"value": "invalid_value",
"constraint": "validation_rule"
},
"request_id": "req_unique_identifier",
"timestamp": "2025-07-03T10:15:00+07:00",
"trace_id": "trace_for_debugging"
}
```
4.3.2. Common Error Codes
{
"SYSTEM_ERROR": "Lỗi hệ thống, vui lòng thử lại sau",
"VALIDATION_ERROR": "Dữ liệu đầu vào không hợp lệ",
"PROJECT_NOT_FOUND": "Không tìm thấy dự án",
"TENANT_NOT_FOUND": "Không tìm thấy tenant",
"TRANSACTION_NOT_FOUND": "Không tìm thấy giao dịch",
"PROVIDER_NOT_CONFIGURED": "Cổng thanh toán chưa được cấu hình",
"AMOUNT_INVALID": "Số tiền không hợp lệ",
"TRANSACTION_EXPIRED": "Giao dịch đã hết hạn",
"DUPLICATE_ORDER_ID": "Mã đơn hàng đã tồn tại",
"INSUFFICIENT_BALANCE": "Số dư không đủ",
"RATE_LIMIT_EXCEEDED": "Vượt quá giới hạn số lượng request",
"MAINTENANCE_MODE": "Hệ thống đang bảo trì"
}
```
5. MIDDLEWARE VÀ AUTHENTICATION
5.1. Middleware Chain
5.1.1. API Request Middleware Stack
// Cho các API request từ client applications
[
'cors', // CORS headers
'throttle:api', // Rate limiting (100 req/min per IP)
'request.logging', // Log incoming requests
'resolve.project.tenant', // Resolve project và tenant context
'validate.project.tenant', // Validate project/tenant permissions
'idempotency', // Idempotency check
'audit.log', // Audit logging
'maintenance.check', // Check maintenance mode
'feature.toggle' // Feature toggle check
]
```
5.1.2. Webhook Middleware Stack
// Cho webhook từ payment providers
[
'webhook.rate.limit', // Webhook-specific rate limiting
'webhook.ip.whitelist', // IP whitelist validation
'webhook.signature.validation', // Signature verification
'webhook.logging', // Log webhook requests
'webhook.idempotency', // Prevent duplicate webhook processing
'audit.webhook' // Webhook audit logging
]
```
5.1.3. Admin Middleware Stack
// Cho admin/management APIs
[
'cors',
'throttle:admin', // Higher rate limit for admin (1000 req/min)
'admin.ip.whitelist', // Admin IP whitelist
'request.logging',
'audit.admin', // Admin action logging
'maintenance.bypass' // Allow admin during maintenance
]
```
5.2. Middleware Implementation Details
5.2.1. ResolveProjectAndTenantMiddleware
<?php
namespace App\Http\Middleware\Request;
use Closure;
use Illuminate\Http\Request;
use App\Domain\Project\Repositories\ProjectRepositoryInterface;
use App\Domain\Tenant\Repositories\TenantRepositoryInterface;
use App\Exceptions\ProjectNotFoundException;
use App\Exceptions\TenantNotFoundException;
class ResolveProjectAndTenantMiddleware
{
private $projectRepository;
private $tenantRepository;
public function __construct(
ProjectRepositoryInterface $projectRepository,
TenantRepositoryInterface $tenantRepository
) {
$this->projectRepository = $projectRepository;
$this->tenantRepository = $tenantRepository;
}
public function handle(Request $request, Closure $next)
{
$projectCode = $request->route('project_code');
$appCode = $request->route('app_code');
if (!$projectCode || !$appCode) {
return response()->json([
'success' => false,
'error_code' => 'MISSING_PROJECT_OR_APP_CODE',
'message' => 'Project code và app code là bắt buộc'
], 400);
}
// Resolve project
$project = $this->projectRepository->findByCode($projectCode);
if (!$project) {
throw new ProjectNotFoundException("Project not found: {$projectCode}");
}
if (!$project->isActive()) {
return response()->json([
'success' => false,
'error_code' => 'PROJECT_INACTIVE',
'message' => 'Dự án đã bị vô hiệu hóa'
], 403);
}
// Resolve tenant
$tenant = $this->tenantRepository->findByAppCode($appCode, $project->getId());
if (!$tenant) {
throw new TenantNotFoundException("Tenant not found: {$appCode}");
}
if (!$tenant->isActive()) {
return response()->json([
'success' => false,
'error_code' => 'TENANT_INACTIVE',
'message' => 'Tenant đã bị vô hiệu hóa'
], 403);
}
// Add to request context
$request->merge([
'resolved_project' => $project,
'resolved_tenant' => $tenant,
'project_id' => $project->getId(),
'tenant_id' => $tenant->getId()
]);
// Add to app context for easy access
app()->instance('current.project', $project);
app()->instance('current.tenant', $tenant);
return $next($request);
}
}
```
5.2.2. AuditLogMiddleware
<?php
namespace App\Http\Middleware\Logging;
use Closure;
use Illuminate\Http\Request;
use App\Domain\Shared\Services\AuditLogService;
use Illuminate\Support\Str;
class AuditLogMiddleware
{
private $auditService;
public function __construct(AuditLogService $auditService)
{
$this->auditService = $auditService;
}
public function handle(Request $request, Closure $next)
{
$startTime = microtime(true);
$requestId = Str::uuid();
// Add request ID to request for tracking
$request->headers->set('X-Request-ID', $requestId);
// Log request start
$this->auditService->logRequest([
'request_id' => $requestId,
'entity_type' => $this->getEntityType($request),
'action' => $this->getAction($request),
'endpoint' => $request->path(),
'method' => $request->method(),
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
'project_code' => $request->route('project_code'),
'app_code' => $request->route('app_code'),
'tenant_id' => $request->get('tenant_id'),
'request_headers' => $this->sanitizeHeaders($request->headers->all()),
'request_body' => $this->sanitizeRequestBody($request->all()),
'received_at' => now()
]);
// Process request
$response = $next($request);
// Calculate response time
$responseTime = round((microtime(true) - $startTime) * 1000);
// Log response
$this->auditService->logResponse([
'request_id' => $requestId,
'response_status' => $response->getStatusCode(),
'response_body' => $this->sanitizeResponseBody($response),
'response_time_ms' => $responseTime,
'completed_at' => now()
]);
// Add request ID to response headers
$response->headers->set('X-Request-ID', $requestId);
return $response;
}
private function getEntityType(Request $request): string
{
$path = $request->path();
if (str_contains($path, 'transactions')) return 'transaction';
if (str_contains($path, 'projects')) return 'project';
if (str_contains($path, 'tenants')) return 'tenant';
if (str_contains($path, 'webhooks')) return 'webhook';
if (str_contains($path, 'reconciliations')) return 'reconciliation';
return 'unknown';
}
private function getAction(Request $request): string
{
$method = $request->method();
$path = $request->path();
return match($method) {
'GET' => str_contains($path, '/') && !str_ends_with($path, 's') ? 'view' : 'list',
'POST' => 'create',
'PUT', 'PATCH' => 'update',
'DELETE' => 'delete',
default => 'unknown'
};
}
private function sanitizeHeaders(array $headers): array
{
$sensitive = ['authorization', 'cookie', 'x-api-key', 'x-signature'];
$sanitized = [];
foreach ($headers as $key => $value) {
if (in_array(strtolower($key), $sensitive)) {
$sanitized[$key] = '[REDACTED]';
} else {
$sanitized[$key] = is_array($value) ? $value[0] : $value;
}
}
return $sanitized;
}
private function sanitizeRequestBody(array $body): array
{
$sensitive = [
'password', 'secret_key', 'api_key', 'secret', 'token',
'webhook_secret', 'signature', 'hash', 'private_key'
];
return $this->recursiveSanitize($body, $sensitive);
}
private function sanitizeResponseBody($response): ?array
{
if (!$response instanceof \Illuminate\Http\JsonResponse) {
return null;
}
$data = $response->getData(true);
// Don't log large response bodies
if (json_encode($data) && strlen(json_encode($data)) > 10000) {
return ['message' => 'Response body too large to log'];
}
$sensitive = ['secret_key', 'api_key', 'token', 'signature'];
return $this->recursiveSanitize($data, $sensitive);
}
private function recursiveSanitize(array $data, array $sensitive): array
{
$sanitized = [];
foreach ($data as $key => $value) {
if (in_array(strtolower($key), $sensitive)) {
$sanitized[$key] = '[REDACTED]';
} elseif (is_array($value)) {
$sanitized[$key] = $this->recursiveSanitize($value, $sensitive);
} else {
$sanitized[$key] = $value;
}
}
return $sanitized;
}
}
```
5.2.3. IdempotencyMiddleware
<?php
namespace App\Http\Middleware\Request;
use Closure;
use Illuminate\Http\Request;
use App\Infrastructure\Cache\CacheManager;
use App\Domain\Shared\Services\IdempotencyService;
class IdempotencyMiddleware
{
private $idempotencyService;
public function __construct(IdempotencyService $idempotencyService)
{
$this->idempotencyService = $idempotencyService;
}
public function handle(Request $request, Closure $next)
{
// Only check for POST, PUT, PATCH requests
if (!in_array($request->method(), ['POST', 'PUT', 'PATCH'])) {
return $next($request);
}
$idempotencyKey = $request->header('X-Idempotency-Key');
if (!$idempotencyKey) {
// Generate one for internal use
$idempotencyKey = $this->generateIdempotencyKey($request);
}
// Check if this request was already processed
$existingResult = $this->idempotencyService->getResult(
$idempotencyKey,
$request->get('tenant_id')
);
if ($existingResult) {
// Return the cached result
return response()->json($existingResult['response'], $existingResult['status_code'])
->withHeaders(['X-Idempotency-Cache' => 'HIT']);
}
// Process the request
$response = $next($request);
// Cache the result for successful operations
if ($response->getStatusCode() < 400) {
$this->idempotencyService->storeResult(
$idempotencyKey,
$request->get('tenant_id'),
$response->getData(true),
$response->getStatusCode(),
now()->addHours(24) // Cache for 24 hours
);
}
return $response->withHeaders(['X-Idempotency-Cache' => 'MISS']);
}
private function generateIdempotencyKey(Request $request): string
{
$components = [
$request->method(),
$request->path(),
$request->get('tenant_id'),
md5(json_encode($request->all()))
];
return 'auto_' . md5(implode('|', $components));
}
}
```
6. XỬ LÝ WEBHOOK
6.1. Webhook Security
6.1.1. Multi-layer Validation
<?php
namespace App\Http\Middleware\Security;
use Closure;
use Illuminate\Http\Request;
use App\Domain\Webhook\Services\WebhookSecurityService;
class ValidateWebhookSignatureMiddleware
{
private $webhookSecurity;
public function __construct(WebhookSecurityService $webhookSecurity)
{
$this->webhookSecurity = $webhookSecurity;
}
public function handle(Request $request, Closure $next)
{
$providerCode = $request->route('provider') ?? $this->detectProvider($request);
if (!$providerCode) {
return response()->json([
'error' => 'Provider not detected'
], 400);
}
// Step 1: IP Whitelist validation
if (!$this->webhookSecurity->validateIP($providerCode, $request->ip())) {
\Log::warning('Webhook IP validation failed', [
'provider' => $providerCode,
'ip' => $request->ip(),
'user_agent' => $request->userAgent()
]);
return response()->json(['error' => 'Invalid source'], 403);
}
// Step 2: Signature validation
if (!$this->webhookSecurity->validateSignature($providerCode, $request)) {
\Log::warning('Webhook signature validation failed', [
'provider' => $providerCode,
'ip' => $request->ip(),
'headers' => $request->headers->all()
]);
return response()->json(['error' => 'Invalid signature'], 403);
}
// Step 3: Timestamp validation (prevent replay attacks)
if (!$this->webhookSecurity->validateTimestamp($request)) {
\Log::warning('Webhook timestamp validation failed', [
'provider' => $providerCode,
'timestamp' => $request->header('timestamp')
]);
return response()->json(['error' => 'Request too old'], 403);
}
$request->merge(['validated_provider' => $providerCode]);
return $next($request);
}
private function detectProvider(Request $request): ?string
{
$path = $request->path();
$userAgent = $request->userAgent();
if (str_contains($path, 'vnpay')) return 'vnpay';
if (str_contains($path, 'momo')) return 'momo';
if (str_contains($path, 'zalopay')) return 'zalopay';
if (str_contains($path, 'napas')) return 'napas';
// Detect by User-Agent
if (str_contains($userAgent, 'VNPay')) return 'vnpay';
if (str_contains($userAgent, 'MoMo')) return 'momo';
return null;
}
}
```
6.2. Webhook Processing
6.2.1. Generic Webhook Controller
<?php
namespace App\Http\Controllers\API\V1\Webhook;
use Illuminate\Http\Request;
use App\Http\Controllers\API\BaseApiController;
use App\Domain\Webhook\Services\WebhookService;
use App\Infrastructure\Queue\Jobs\ProcessWebhookJob;
class BaseWebhookController extends BaseApiController
{
private $webhookService;
public function __construct(WebhookService $webhookService)
{
$this->webhookService = $webhookService;
}
public function handle(Request $request, string $provider)
{
try {
// Log incoming webhook
$webhookLog = $this->webhookService->logIncomingWebhook([
'provider_code' => $provider,
'source_ip' => $request->ip(),
'headers' => $request->headers->all(),
'payload' => $request->all(),
'received_at' => now()
]);
// Process webhook asynchronously
ProcessWebhookJob::dispatch($webhookLog->id, $provider, $request->all())
->onQueue('webhooks');
// Return immediate response to provider
return $this->getProviderSuccessResponse($provider);
} catch (\Exception $e) {
\Log::error('Webhook processing error', [
'provider' => $provider,
'error' => $e->getMessage(),
'payload' => $request->all()
]);
return $this->getProviderErrorResponse($provider, $e->getMessage());
}
}
private function getProviderSuccessResponse(string $provider): \Illuminate\Http\JsonResponse
{
return match($provider) {
'vnpay' => response()->json(['RspCode' => '00', 'Message' => 'Confirm Success']),
'momo' => response()->json(['status' => 0, 'message' => 'success']),
'zalopay' => response()->json(['return_code' => 1, 'return_message' => 'success']),
'napas' => response()->json(['responseCode' => '00', 'desc' => 'success']),
default => response()->json(['status' => 'success'])
};
}
private function getProviderErrorResponse(string $provider, string $error): \Illuminate\Http\JsonResponse
{
return match($provider) {
'vnpay' => response()->json(['RspCode' => '99', 'Message' => 'Error'], 500),
'momo' => response()->json(['status' => -1, 'message' => 'error']),
'zalopay' => response()->json(['return_code' => 0, 'return_message' => 'error']),
'napas' => response()->json(['responseCode' => '99', 'desc' => 'error']),
default => response()->json(['status' => 'error', 'message' => $error], 500)
};
}
}
```
6.2.2. Webhook Processing Job
<?php
namespace App\Infrastructure\Queue\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Domain\Webhook\Services\WebhookService;
use App\Domain\Payment\Services\TransactionService;
use App\Infrastructure\Queue\Jobs\SendCallbackJob;
class ProcessWebhookJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $backoff = [10, 30, 60]; // seconds
private $webhookLogId;
private $provider;
private $payload;
public function __construct(string $webhookLogId, string $provider, array $payload)
{
$this->webhookLogId = $webhookLogId;
$this->provider = $provider;
$this->payload = $payload;
}
public function handle(
WebhookService $webhookService,
TransactionService $transactionService
) {
try {
// Update webhook log status
$webhookService->updateStatus($this->webhookLogId, 'processing');
// Process based on provider
$result = match($this->provider) {
'vnpay' => $this->processVNPayWebhook($transactionService),
'momo' => $this->processMoMoWebhook($transactionService),
'zalopay' => $this->processZaloPayWebhook($transactionService),
'napas' => $this->processNapasWebhook($transactionService),
default => throw new \Exception("Unsupported provider: {$this->provider}")
};
// Update webhook log with result
$webhookService->updateStatus($this->webhookLogId, 'processed', $result);
// Send callback to tenant if needed
if ($result['transaction_id'] && $result['callback_url']) {
SendCallbackJob::dispatch(
$result['transaction_id'],
$result['callback_url'],
$result['callback_data']
)->onQueue('callbacks');
}
} catch (\Exception $e) {
$webhookService->updateStatus($this->webhookLogId, 'failed', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
throw $e; // Re-throw for retry mechanism
}
}
private function processVNPayWebhook(TransactionService $transactionService): array
{
$transactionRef = $this->payload['vnp_TxnRef'] ?? null;
$responseCode = $this->payload['vnp_ResponseCode'] ?? null;
$transactionNo = $this->payload['vnp_TransactionNo'] ?? null;
$amount = $this->payload['vnp_Amount'] ?? null;
if (!$transactionRef) {
throw new \Exception('Missing vnp_TxnRef in webhook');
}
// Find transaction by provider reference
$transaction = $transactionService->findByProviderReference($transactionRef);
if (!$transaction) {
throw new \Exception("Transaction not found: {$transactionRef}");
}
// Update transaction based on response code
$status = $responseCode === '00' ? 'completed' : 'failed';
$updateData = [
'status' => $status,
'provider_transaction_id' => $transactionNo,
'provider_response' => $this->payload,
'processed_at' => now()
];
if ($status === 'completed') {
$updateData['paid_at'] = now();
$updateData['net_amount'] = ($amount / 100) - $transaction->transaction_fee;
} else {
$updateData['failed_at'] = now();
$updateData['failure_reason'] = $this->getVNPayErrorMessage($responseCode);
}
$transactionService->updateTransaction($transaction->id, $updateData);
return [
'transaction_id' => $transaction->id,
'status' => $status,
'callback_url' => $transaction->callback_url,
'callback_data' => [
'transaction_id' => $transaction->id,
'order_id' => $transaction->order_id,
'status' => $status,
'amount' => $transaction->amount,
'provider_transaction_id' => $transactionNo,
'processed_at' => now()->toISOString()
]
];
}
private function getVNPayErrorMessage(string $responseCode): string
{
return match($responseCode) {
'07' => 'Trừ tiền thành công. Giao dịch bị nghi ngờ (liên quan tới lừa đảo, giao dịch bất thường).',
'09' => 'Giao dịch không thành công do: Thẻ/Tài khoản của khách hàng chưa đăng ký dịch vụ InternetBanking tại ngân hàng.',
'10' => 'Giao dịch không thành công do: Khách hàng xác thực thông tin thẻ/tài khoản không đúng quá 3 lần',
'11' => 'Giao dịch không thành công do: Đã hết hạn chờ thanh toán. Xin quý khách vui lòng thực hiện lại giao dịch.',
'12' => 'Giao dịch không thành công do: Thẻ/Tài khoản của khách hàng bị khóa.',
'13' => 'Giao dịch không thành công do Quý khách nhập sai mật khẩu xác thực giao dịch (OTP).',
'24' => 'Giao dịch không thành công do: Khách hàng hủy giao dịch',
'51' => 'Giao dịch không thành công do: Tài khoản của quý khách không đủ số dư để thực hiện giao dịch.',
'65' => 'Giao dịch không thành công do: Tài khoản của Quý khách đã vượt quá hạn mức giao dịch trong ngày.',
'75' => 'Ngân hàng thanh toán đang bảo trì.',
'79' => 'Giao dịch không thành công do: KH nhập sai mật khẩu thanh toán quá số lần quy định.',
'99' => 'Các lỗi khác (lỗi còn lại, không có trong danh sách mã lỗi đã liệt kê)',
default => "Giao dịch thất bại với mã lỗi: {$responseCode}"
};
}
}
```
7. XỬ LÝ CÁC TRƯỜNG HỢP ĐẶC BIỆT
7.1. Fraud Detection
7.1.1. Real-time Fraud Detection Service
<?php
namespace App\Domain\Payment\Services;
use App\Domain\Payment\Entities\Transaction;
use App\Domain\Payment\Events\FraudDetected;
class FraudDetectionService
{
private $rules = [];
public function __construct()
{
$this->initializeRules();
}
public function analyzeTransaction(Transaction $transaction): array
{
$riskScore = 0;
$flags = [];
foreach ($this->rules as $rule) {
$result = $rule->analyze($transaction);
$riskScore += $result['score'];
if ($result['triggered']) {
$flags[] = $result['flag'];
}
}
$riskLevel = $this->calculateRiskLevel($riskScore);
if ($riskLevel === 'HIGH') {
event(new FraudDetected($transaction, $riskScore, $flags));
}
return [
'risk_score' => $riskScore,
'risk_level' => $riskLevel,
'flags' => $flags,
'action' => $this->getRecommendedAction($riskLevel)
];
}
private function initializeRules(): void
{
$this->rules = [
new VelocityRule(), // Tần suất giao dịch
new AmountRule(), // Số tiền bất thường
new GeolocationRule(), // Địa lý bất thường
new DeviceFingerprintRule(), // Thiết bị lạ
new TimePatternRule(), // Thời gian bất thường
new BehaviorRule() // Hành vi bất thường
];
}
private function calculateRiskLevel(int $score): string
{
return match(true) {
$score >= 80 => 'HIGH',
$score >= 50 => 'MEDIUM',
$score >= 20 => 'LOW',
default => 'MINIMAL'
};
}
private function getRecommendedAction(string $riskLevel): string
{
return match($riskLevel) {
'HIGH' => 'BLOCK',
'MEDIUM' => 'REVIEW',
'LOW' => 'MONITOR',
default => 'ALLOW'
};
}
}
```
7.2. Circuit Breaker Pattern
7.2.2. Payment Provider Circuit Breaker
<?php
namespace App\Infrastructure\External\Common;
use App\Infrastructure\Cache\CacheManager;
class CircuitBreaker
{
private $cache;
private $failureThreshold;
private $recoveryTimeout;
private $monitoringPeriod;
public function __construct(
CacheManager $cache,
int $failureThreshold = 5,
int $recoveryTimeout = 60,
int $monitoringPeriod = 300
) {
$this->cache = $cache;
$this->failureThreshold = $failureThreshold;
$this->recoveryTimeout = $recoveryTimeout;
$this->monitoringPeriod = $monitoringPeriod;
}
public function call(string $service, callable $callback)
{
$state = $this->getState($service);
if ($state === 'OPEN') {
if ($this->shouldAttemptReset($service)) {
$this->setState($service, 'HALF_OPEN');
} else {
throw new \Exception("Circuit breaker is OPEN for service: {$service}");
}
}
try {
$result = $callback();
$this->recordSuccess($service);
if ($state === 'HALF_OPEN') {
$this->setState($service, 'CLOSED');
}
return $result;
} catch (\Exception $e) {
$this->recordFailure($service);
if ($this->shouldOpenCircuit($service)) {
$this->setState($service, 'OPEN');
$this->setLastFailureTime($service, time());
}
throw $e;
}
}
private function getState(string $service): string
{
return $this->cache->get("circuit_breaker:{$service}:state", 'CLOSED');
}
private function setState(string $service, string $state): void
{
$this->cache->put("circuit_breaker:{$service}:state", $state, $this->monitoringPeriod);
}
private function recordFailure(string $service): void
{
$key = "circuit_breaker:{$service}:failures";
$failures = $this->cache->get($key, 0);
$this->cache->put($key, $failures + 1, $this->monitoringPeriod);
}
private function recordSuccess(string $service): void
{
$this->cache->forget("circuit_breaker:{$service}:failures");
}
private function shouldOpenCircuit(string $service): bool
{
$failures = $this->cache->get("circuit_breaker:{$service}:failures", 0);
return $failures >= $this->failureThreshold;
}
private function shouldAttemptReset(string $service): bool
{
$lastFailure = $this->cache->get("circuit_breaker:{$service}:last_failure", 0);
return (time() - $lastFailure) >= $this->recoveryTimeout;
}
private function setLastFailureTime(string $service, int $time): void
{
$this->cache->put("circuit_breaker:{$service}:last_failure", $time, $this->recoveryTimeout * 2);
}
}
```
7.3. Retry Mechanism
7.3.1. Exponential Backoff Retry
<?php
namespace App\Infrastructure\External\Common;
class RetryHandler
{
private $maxAttempts;
private $baseDelay;
private $maxDelay;
private $multiplier;
public function __construct(
int $maxAttempts = 3,
int $baseDelay = 1000, // milliseconds
int $maxDelay = 30000, // milliseconds
float $multiplier = 2.0
) {
$this->maxAttempts = $maxAttempts;
$this->baseDelay = $baseDelay;
$this->maxDelay = $maxDelay;
$this->multiplier = $multiplier;
}
public function execute(callable $callback, array $retryableExceptions = [])
{
$attempt = 1;
$lastException = null;
while ($attempt <= $this->maxAttempts) {
try {
return $callback();
} catch (\Exception $e) {
$lastException = $e;
// Check if this exception is retryable
if (!$this->isRetryable($e, $retryableExceptions)) {
throw $e;
}
// Don't retry on last attempt
if ($attempt === $this->maxAttempts) {
break;
}
// Calculate delay with jitter
$delay = $this->calculateDelay($attempt);
$this->sleep($delay);
\Log::warning('Retrying operation', [
'attempt' => $attempt,
'max_attempts' => $this->maxAttempts,
'delay_ms' => $delay,
'exception' => $e->getMessage()
]);
$attempt++;
}
}
throw $lastException;
}
private function isRetryable(\Exception $e, array $retryableExceptions): bool
{
if (empty($retryableExceptions)) {
// Default retryable exceptions
return $e instanceof \GuzzleHttp\Exception\ConnectException ||
$e instanceof \GuzzleHttp\Exception\ServerException ||
$e instanceof \Illuminate\Http\Client\ConnectionException;
}
foreach ($retryableExceptions as $exceptionClass) {
if ($e instanceof $exceptionClass) {
return true;
}
}
return false;
}
private function calculateDelay(int $attempt): int
{
// Exponential backoff with jitter
$exponentialDelay = $this->baseDelay * pow($this->multiplier, $attempt - 1);
$delayWithJitter = $exponentialDelay + random_int(0, (int)($exponentialDelay * 0.1));
return min($delayWithJitter, $this->maxDelay);
}
private function sleep(int $milliseconds): void
{
usleep($milliseconds * 1000);
}
}
```
8. AUDIT LOG VÀ MONITORING
8.1. Comprehensive Audit System
8.1.1. Audit Log Service
<?php
namespace App\Domain\Shared\Services;
use App\Infrastructure\Persistence\Models\AuditLog;
use Illuminate\Support\Facades\Auth;
class AuditLogService
{
public function logAction(array $data): AuditLog
{
return AuditLog::create([
'id' => \Str::uuid(),
'tenant_id' => $data['tenant_id'] ?? null,
'entity_type' => $data['entity_type'],
'entity_id' => $data['entity_id'],
'action' => $data['action'],
'old_values' => $data['old_values'] ?? null,
'new_values' => $data['new_values'] ?? null,
'changed_fields' => $data['changed_fields'] ?? null,
'user_id' => $data['user_id'] ?? 'system',
'user_type' => $data['user_type'] ?? 'system',
'ip_address' => $data['ip_address'] ?? request()->ip(),
'user_agent' => $data['user_agent'] ?? request()->userAgent(),
'request_id' => $data['request_id'] ?? request()->header('X-Request-ID'),
'additional_data' => $data['additional_data'] ?? null,
'created_at' => now()
]);
}
public function logEntityChange(
string $entityType,
string $entityId,
string $action,
array $oldValues = null,
array $newValues = null,
string $tenantId = null
): AuditLog {
$changedFields = null;
if ($oldValues && $newValues) {
$changedFields = array_keys(array_diff_assoc($newValues, $oldValues));
}
return $this->logAction([
'tenant_id' => $tenantId,
'entity_type' => $entityType,
'entity_id' => $entityId,
'action' => $action,
'old_values' => $oldValues,
'new_values' => $newValues,
'changed_fields' => $changedFields
]);
}
public function logTransactionEvent(
string $transactionId,
string $eventType,
array $eventData = [],
string $source = 'system'
): AuditLog {
return $this->logAction([
'tenant_id' => app('current.tenant')->id ?? null,
'entity_type' => 'transaction',
'entity_id' => $transactionId,
'action' => $eventType,
'new_values' => $eventData,
'additional_data' => [
'source' => $source,
'timestamp' => now()->toISOString()
]
]);
}
public function logWebhookEvent(
string $webhookId,
string $provider,
array $payload,
string $status = 'received'
): AuditLog {
return $this->logAction([
'entity_type' => 'webhook',
'entity_id' => $webhookId,
'action' => "webhook_{$status}",
'new_values' => [
'provider' => $provider,
'payload_hash' => md5(json_encode($payload)),
'status' => $status
],
'additional_data' => [
'provider' => $provider,
'payload_size' => strlen(json_encode($payload))
]
]);
}
}
```
8.2. System Health Monitoring
8.2.1. Health Check Service
<?php
namespace App\Domain\System\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
use App\Infrastructure\External\PaymentProviders\VNPay\VNPayClient;
class SystemHealthService
{
public function getSystemHealth(): array
{
return [
'overall_status' => $this->getOverallStatus(),
'timestamp' => now()->toISOString(),
'version' => config('app.version'),
'environment' => config('app.env'),
'checks' => [
'database' => $this->checkDatabase(),
'redis' => $this->checkRedis(),
'queue' => $this->checkQueue(),
'storage' => $this->checkStorage(),
'payment_providers' => $this->checkPaymentProviders(),
'external_apis' => $this->checkExternalAPIs()
]
];
}
private function checkDatabase(): array
{
try {
$start = microtime(true);
DB::select('SELECT 1');
$responseTime = round((microtime(true) - $start) * 1000, 2);
return [
'status' => 'healthy',
'response_time_ms' => $responseTime,
'connection' => DB::connection()->getName()
];
} catch (\Exception $e) {
return [
'status' => 'unhealthy',
'error' => $e->getMessage()
];
}
}
private function checkRedis(): array
{
try {
$start = microtime(true);
Redis::ping();
$responseTime = round((microtime(true) - $start) * 1000, 2);
return [
'status' => 'healthy',
'response_time_ms' => $responseTime,
'memory_usage' => Redis::info('memory')['used_memory_human'] ?? 'unknown'
];
} catch (\Exception $e) {
return [
'status' => 'unhealthy',
'error' => $e->getMessage()
];
}
}
private function checkQueue(): array
{
try {
$queueSizes = [
'default' => \Queue::size('default'),
'webhooks' => \Queue::size('webhooks'),
'callbacks' => \Queue::size('callbacks'),
'reconciliation' => \Queue::size('reconciliation')
];
$totalPending = array_sum($queueSizes);
$status = $totalPending > 1000 ? 'warning' : 'healthy';
return [
'status' => $status,
'queue_sizes' => $queueSizes,
'total_pending' => $totalPending
];
} catch (\Exception $e) {
return [
'status' => 'unhealthy',
'error' => $e->getMessage()
];
}
}
private function checkPaymentProviders(): array
{
$providers = ['vnpay', 'momo', 'zalopay', 'napas'];
$results = [];
foreach ($providers as $provider) {
$results[$provider] = $this->checkProviderHealth($provider);
}
return $results;
}
private function checkProviderHealth(string $provider): array
{
try {
// Test với health check endpoint nếu có
$start = microtime(true);
switch ($provider) {
case 'vnpay':
// VNPay không có health check endpoint public
// Kiểm tra connectivity thông qua DNS resolution
$host = parse_url(config("payment-providers.{$provider}.api_url"), PHP_URL_HOST);
if (!gethostbyname($host)) {
throw new \Exception('Cannot resolve hostname');
}
break;
default:
// Generic connectivity check
$host = parse_url(config("payment-providers.{$provider}.api_url"), PHP_URL_HOST);
if (!gethostbyname($host)) {
throw new \Exception('Cannot resolve hostname');
}
break;
}
$responseTime = round((microtime(true) - $start) * 1000, 2);
return [
'status' => 'healthy',
'response_time_ms' => $responseTime
];
} catch (\Exception $e) {
return [
'status' => 'unhealthy',
'error' => $e->getMessage()
];
}
}
private function getOverallStatus(): string
{
$checks = [
$this->checkDatabase()['status'],
$this->checkRedis()['status'],
$this->checkQueue()['status']
];
if (in_array('unhealthy', $checks)) {
return 'unhealthy';
}
if (in_array('warning', $checks)) {
return 'warning';
}
return 'healthy';
}
}
```
9. JOBS VÀ QUEUE PROCESSING
9.1. Queue Configuration
9.1.1. Queue Structure
// config/queue.php
return [
'default' => env('QUEUE_CONNECTION', 'redis'),
'connections' => [
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'default',
'retry_after' => 90,
'block_for' => null,
],
],
'batching' => [
'database' => env('DB_CONNECTION', 'mysql'),
'table' => 'job_batches',
],
// Custom queue definitions
'queues' => [
'high_priority' => ['webhooks', 'callbacks'],
'medium_priority' => ['reconciliation', 'notifications'],
'low_priority' => ['reports', 'cleanup']
]
];
```
9.2. Critical Jobs
9.2.1. SendCallbackJob
<?php
namespace App\Infrastructure\Queue\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use App\Domain\Payment\Repositories\TransactionRepositoryInterface;
use App\Domain\Shared\Services\AuditLogService;
use App\Infrastructure\External\Common\RetryHandler;
class SendCallbackJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 5;
public $backoff = [10, 30, 60, 300, 900]; // seconds: 10s, 30s, 1m, 5m, 15m
public $timeout = 30;
private $transactionId;
private $callbackUrl;
private $callbackData;
private $attempt;
public function __construct(
string $transactionId,
string $callbackUrl,
array $callbackData,
int $attempt = 1
) {
$this->transactionId = $transactionId;
$this->callbackUrl = $callbackUrl;
$this->callbackData = $callbackData;
$this->attempt = $attempt;
// Set queue based on priority
$this->onQueue('callbacks');
}
public function handle(
TransactionRepositoryInterface $transactionRepository,
AuditLogService $auditService,
RetryHandler $retryHandler
) {
$transaction = $transactionRepository->findById($this->transactionId);
if (!$transaction) {
\Log::error('Transaction not found for callback', [
'transaction_id' => $this->transactionId
]);
return;
}
try {
// Log callback attempt
$auditService->logAction([
'tenant_id' => $transaction->tenant_id,
'entity_type' => 'transaction_callback',
'entity_id' => $this->transactionId,
'action' => 'callback_attempt',
'additional_data' => [
'callback_url' => $this->callbackUrl,
'attempt' => $this->attempt,
'max_attempts' => $this->tries
]
]);
// Send callback with retry mechanism
$response = $retryHandler->execute(function() {
return Http::timeout($this->timeout)
->withHeaders([
'Content-Type' => 'application/json',
'User-Agent' => 'SmartPost-Payment-Service/1.0',
'X-Callback-Signature' => $this->generateSignature(),
'X-Callback-Timestamp' => now()->timestamp,
'X-Callback-Attempt' => $this->attempt
])
->post($this->callbackUrl, $this->callbackData);
});
// Check response
if ($response->successful()) {
$auditService->logAction([
'tenant_id' => $transaction->tenant_id,
'entity_type' => 'transaction_callback',
'entity_id' => $this->transactionId,
'action' => 'callback_success',
'additional_data' => [
'response_status' => $response->status(),
'response_time_ms' => $response->handlerStats()['total_time'] * 1000,
'attempt' => $this->attempt
]
]);
// Update transaction callback status
$transactionRepository->updateCallback($this->transactionId, [
'callback_status' => 'delivered',
'callback_delivered_at' => now(),
'callback_attempts' => $this->attempt,
'callback_response' => [
'status' => $response->status(),
'body' => $response->body()
]
]);
} else {
throw new \Exception("HTTP {$response->status()}: {$response->body()}");
}
} catch (\Exception $e) {
\Log::error('Callback delivery failed', [
'transaction_id' => $this->transactionId,
'callback_url' => $this->callbackUrl,
'attempt' => $this->attempt,
'error' => $e->getMessage()
]);
$auditService->logAction([
'tenant_id' => $transaction->tenant_id,
'entity_type' => 'transaction_callback',
'entity_id' => $this->transactionId,
'action' => 'callback_failed',
'additional_data' => [
'error' => $e->getMessage(),
'attempt' => $this->attempt,
'will_retry' => $this->attempt < $this->tries
]
]);
// Update callback status on final failure
if ($this->attempt >= $this->tries) {
$transactionRepository->updateCallback($this->transactionId, [
'callback_status' => 'failed',
'callback_failed_at' => now(),
'callback_attempts' => $this->attempt,
'callback_error' => $e->getMessage()
]);
}
throw $e; // Re-throw for retry mechanism
}
}
public function failed(\Exception $exception)
{
\Log::error('Callback job finally failed', [
'transaction_id' => $this->transactionId,
'callback_url' => $this->callbackUrl,
'attempts' => $this->tries,
'error' => $exception->getMessage()
]);
// Send alert to monitoring system
\Event::dispatch(new \App\Events\CallbackDeliveryFailed(
$this->transactionId,
$this->callbackUrl,
$exception->getMessage()
));
}
private function generateSignature(): string
{
$secret = config('app.callback_secret');
$payload = json_encode($this->callbackData);
return hash_hmac('sha256', $payload, $secret);
}
}
```
9.2.2. ProcessReconciliationJob
<?php
namespace App\Infrastructure\Queue\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Domain\Reconciliation\Services\ReconciliationService;
use Carbon\Carbon;
class ProcessReconciliationJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $timeout = 600; // 10 minutes
private $tenantId;
private $providerId;
private $reconciliationDate;
public function __construct(string $tenantId, string $providerId, Carbon $reconciliationDate)
{
$this->tenantId = $tenantId;
$this->providerId = $providerId;
$this->reconciliationDate = $reconciliationDate;
$this->onQueue('reconciliation');
}
public function handle(ReconciliationService $reconciliationService)
{
try {
\Log::info('Starting reconciliation process', [
'tenant_id' => $this->tenantId,
'provider_id' => $this->providerId,
'date' => $this->reconciliationDate->toDateString()
]);
$result = $reconciliationService->reconcileForTenant(
$this->tenantId,
$this->providerId,
$this->reconciliationDate
);
\Log::info('Reconciliation completed', [
'tenant_id' => $this->tenantId,
'provider_id' => $this->providerId,
'result' => $result
]);
// Send notification if there are mismatches
if ($result['mismatched_transactions'] > 0) {
\Event::dispatch(new \App\Events\ReconciliationMismatchDetected(
$this->tenantId,
$this->providerId,
$result
));
}
} catch (\Exception $e) {
\Log::error('Reconciliation process failed', [
'tenant_id' => $this->tenantId,
'provider_id' => $this->providerId,
'error' => $e->getMessage()
]);
throw $e;
}
}
}
```
9.2.3. CleanupExpiredDataJob
<?php
namespace App\Infrastructure\Queue\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Domain\Shared\Services\DataRetentionService;
class CleanupExpiredDataJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $timeout = 3600; // 1 hour
public function __construct()
{
$this->onQueue('cleanup');
}
public function handle(DataRetentionService $dataRetentionService)
{
try {
\Log::info('Starting data cleanup process');
$results = [
'audit_logs' => $dataRetentionService->cleanupAuditLogs(),
'webhook_logs' => $dataRetentionService->cleanupWebhookLogs(),
'transaction_events' => $dataRetentionService->cleanupTransactionEvents(),
'idempotency_keys' => $dataRetentionService->cleanupIdempotencyKeys(),
'expired_transactions' => $dataRetentionService->cleanupExpiredTransactions()
];
\Log::info('Data cleanup completed', $results);
} catch (\Exception $e) {
\Log::error('Data cleanup failed', [
'error' => $e->getMessage()
]);
throw $e;
}
}
}
```
10. ERROR HANDLING
10.1. Exception Hierarchy
10.1.1. Base Exception Classes
<?php
namespace App\Exceptions;
abstract class BaseException extends \Exception
{
protected $errorCode;
protected $errorData;
protected $httpStatusCode;
public function __construct(
string $message = '',
string $errorCode = '',
array $errorData = [],
int $httpStatusCode = 400,
\Exception $previous = null
) {
parent::__construct($message, 0, $previous);
$this->errorCode = $errorCode ?: static::getDefaultErrorCode();
$this->errorData = $errorData;
$this->httpStatusCode = $httpStatusCode;
}
abstract protected static function getDefaultErrorCode(): string;
public function getErrorCode(): string
{
return $this->errorCode;
}
public function getErrorData(): array
{
return $this->errorData;
}
public function getHttpStatusCode(): int
{
return $this->httpStatusCode;
}
public function toArray(): array
{
return [
'success' => false,
'error_code' => $this->getErrorCode(),
'message' => $this->getMessage(),
'details' => $this->getErrorData(),
'timestamp' => now()->toISOString()
];
}
}
class BusinessException extends BaseException
{
protected static function getDefaultErrorCode(): string
{
return 'BUSINESS_RULE_VIOLATION';
}
}
class ValidationException extends BaseException
{
protected static function getDefaultErrorCode(): string
{
return 'VALIDATION_ERROR';
}
}
class PaymentProviderException extends BaseException
{
protected static function getDefaultErrorCode(): string
{
return 'PAYMENT_PROVIDER_ERROR';
}
}
```
10.2. Global Exception Handler
10.2.1. Custom Exception Handler
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Validation\ValidationException as LaravelValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Throwable;
class Handler extends ExceptionHandler
{
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
'secret_key',
'api_key',
'webhook_secret'
];
protected $dontReport = [
ValidationException::class,
LaravelValidationException::class,
NotFoundHttpException::class,
MethodNotAllowedHttpException::class
];
public function register()
{
$this->reportable(function (Throwable $e) {
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
});
}
public function render($request, Throwable $e)
{
// API requests should always return JSON
if ($request->expectsJson() || $request->is('api/*')) {
return $this->renderApiException($request, $e);
}
return parent::render($request, $e);
}
private function renderApiException(Request $request, Throwable $e): JsonResponse
{
// Handle custom exceptions
if ($e instanceof BaseException) {
return response()->json($e->toArray(), $e->getHttpStatusCode());
}
// Handle Laravel validation exceptions
if ($e instanceof LaravelValidationException) {
return response()->json([
'success' => false,
'error_code' => 'VALIDATION_ERROR',
'message' => 'Dữ liệu đầu vào không hợp lệ',
'details' => [
'errors' => $e->errors()
],
'timestamp' => now()->toISOString()
], 422);
}
// Handle 404 errors
if ($e instanceof NotFoundHttpException) {
return response()->json([
'success' => false,
'error_code' => 'NOT_FOUND',
'message' => 'Không tìm thấy tài nguyên được yêu cầu',
'timestamp' => now()->toISOString()
], 404);
}
// Handle 405 errors
if ($e instanceof MethodNotAllowedHttpException) {
return response()->json([
'success' => false,
'error_code' => 'METHOD_NOT_ALLOWED',
'message' => 'Phương thức HTTP không được hỗ trợ',
'timestamp' => now()->toISOString()
], 405);
}
// Handle generic exceptions
$statusCode = method_exists($e, 'getStatusCode') ? $e->getStatusCode() : 500;
$errorCode = $statusCode === 500 ? 'INTERNAL_SERVER_ERROR' : 'CLIENT_ERROR';
$response = [
'success' => false,
'error_code' => $errorCode,
'message' => $statusCode === 500 ? 'Đã xảy ra lỗi hệ thống' : $e->getMessage(),
'timestamp' => now()->toISOString()
];
// Add debug info in non-production environments
if (config('app.debug') && $statusCode === 500) {
$response['debug'] = [
'exception' => get_class($e),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
];
}
// Add request ID for tracking
if ($requestId = $request->header('X-Request-ID')) {
$response['request_id'] = $requestId;
}
return response()->json($response, $statusCode);
}
protected function context()
{
return array_merge(parent::context(), [
'tenant_id' => app('current.tenant')->id ?? null,
'project_id' => app('current.project')->id ?? null,
'request_id' => request()->header('X-Request-ID')
]);
}
}
```
10.3. Error Code Registry
10.3.1. Centralized Error Codes
<?php
namespace App\Constants;
class ErrorCodes
{
// System Errors (1000-1999)
const SYSTEM_ERROR = 'SYSTEM_ERROR';
const DATABASE_ERROR = 'DATABASE_ERROR';
const NETWORK_ERROR = 'NETWORK_ERROR';
const CACHE_ERROR = 'CACHE_ERROR';
const QUEUE_ERROR = 'QUEUE_ERROR';
// Authentication & Authorization Errors (2000-2999)
const UNAUTHORIZED = 'UNAUTHORIZED';
const FORBIDDEN = 'FORBIDDEN';
const INVALID_API_KEY = 'INVALID_API_KEY';
const RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED';
// Validation Errors (3000-3999)
const VALIDATION_ERROR = 'VALIDATION_ERROR';
const INVALID_AMOUNT = 'INVALID_AMOUNT';
const INVALID_CURRENCY = 'INVALID_CURRENCY';
const INVALID_PAYMENT_METHOD = 'INVALID_PAYMENT_METHOD';
const DUPLICATE_ORDER_ID = 'DUPLICATE_ORDER_ID';
// Business Logic Errors (4000-4999)
const PROJECT_NOT_FOUND = 'PROJECT_NOT_FOUND';
const PROJECT_INACTIVE = 'PROJECT_INACTIVE';
const PROJECT_HAS_TENANTS = 'PROJECT_HAS_TENANTS';
const TENANT_NOT_FOUND = 'TENANT_NOT_FOUND';
const TENANT_INACTIVE = 'TENANT_INACTIVE';
const TENANT_LIMIT_EXCEEDED = 'TENANT_LIMIT_EXCEEDED';
const TRANSACTION_NOT_FOUND = 'TRANSACTION_NOT_FOUND';
const TRANSACTION_EXPIRED = 'TRANSACTION_EXPIRED';
const TRANSACTION_ALREADY_PROCESSED = 'TRANSACTION_ALREADY_PROCESSED';
const TRANSACTION_CANNOT_BE_REFUNDED = 'TRANSACTION_CANNOT_BE_REFUNDED';
// Payment Provider Errors (5000-5999)
const PROVIDER_NOT_CONFIGURED = 'PROVIDER_NOT_CONFIGURED';
const PROVIDER_INACTIVE = 'PROVIDER_INACTIVE';
const PROVIDER_CONNECTION_ERROR = 'PROVIDER_CONNECTION_ERROR';
const PROVIDER_INVALID_RESPONSE = 'PROVIDER_INVALID_RESPONSE';
const PROVIDER_AUTHENTICATION_FAILED = 'PROVIDER_AUTHENTICATION_FAILED';
// Webhook Errors (6000-6999)
const WEBHOOK_INVALID_SIGNATURE = 'WEBHOOK_INVALID_SIGNATURE';
const WEBHOOK_INVALID_IP = 'WEBHOOK_INVALID_IP';
const WEBHOOK_PROCESSING_ERROR = 'WEBHOOK_PROCESSING_ERROR';
const WEBHOOK_DELIVERY_FAILED = 'WEBHOOK_DELIVERY_FAILED';
// Reconciliation Errors (7000-7999)
const RECONCILIATION_IN_PROGRESS = 'RECONCILIATION_IN_PROGRESS';
const RECONCILIATION_FAILED = 'RECONCILIATION_FAILED';
const RECONCILIATION_DATA_MISMATCH = 'RECONCILIATION_DATA_MISMATCH';
public static function getMessage(string $code): string
{
return match($code) {
// System Errors
self::SYSTEM_ERROR => 'Đã xảy ra lỗi hệ thống, vui lòng thử lại sau',
self::DATABASE_ERROR => 'Lỗi kết nối cơ sở dữ liệu',
self::NETWORK_ERROR => 'Lỗi kết nối mạng',
self::CACHE_ERROR => 'Lỗi hệ thống cache',
self::QUEUE_ERROR => 'Lỗi hệ thống queue',
// Authentication & Authorization
self::UNAUTHORIZED => 'Không có quyền truy cập',
self::FORBIDDEN => 'Truy cập bị từ chối',
self::INVALID_API_KEY => 'API key không hợp lệ',
self::RATE_LIMIT_EXCEEDED => 'Vượt quá giới hạn số lượng request',
// Validation
self::VALIDATION_ERROR => 'Dữ liệu đầu vào không hợp lệ',
self::INVALID_AMOUNT => 'Số tiền không hợp lệ',
self::INVALID_CURRENCY => 'Đơn vị tiền tệ không được hỗ trợ',
self::INVALID_PAYMENT_METHOD => 'Phương thức thanh toán không hợp lệ',
self::DUPLICATE_ORDER_ID => 'Mã đơn hàng đã tồn tại',
// Business Logic
self::PROJECT_NOT_FOUND => 'Không tìm thấy dự án',
self::PROJECT_INACTIVE => 'Dự án đã bị vô hiệu hóa',
self::PROJECT_HAS_TENANTS => 'Không thể xóa dự án vì còn tồn tại tenant',
self::TENANT_NOT_FOUND => 'Không tìm thấy tenant',
self::TENANT_INACTIVE => 'Tenant đã bị vô hiệu hóa',
self::TENANT_LIMIT_EXCEEDED => 'Vượt quá hạn mức của tenant',
self::TRANSACTION_NOT_FOUND => 'Không tìm thấy giao dịch',
self::TRANSACTION_EXPIRED => 'Giao dịch đã hết hạn',
self::TRANSACTION_ALREADY_PROCESSED => 'Giao dịch đã được xử lý',
self::TRANSACTION_CANNOT_BE_REFUNDED => 'Giao dịch không thể hoàn tiền',
// Payment Provider
self::PROVIDER_NOT_CONFIGURED => 'Cổng thanh toán chưa được cấu hình',
self::PROVIDER_INACTIVE => 'Cổng thanh toán đã bị vô hiệu hóa',
self::PROVIDER_CONNECTION_ERROR => 'Không thể kết nối đến cổng thanh toán',
self::PROVIDER_INVALID_RESPONSE => 'Phản hồi từ cổng thanh toán không hợp lệ',
self::PROVIDER_AUTHENTICATION_FAILED => 'Xác thực với cổng thanh toán thất bại',
// Webhook
self::WEBHOOK_INVALID_SIGNATURE => 'Chữ ký webhook không hợp lệ',
self::WEBHOOK_INVALID_IP => 'IP address không được phép',
self::WEBHOOK_PROCESSING_ERROR => 'Lỗi xử lý webhook',
self::WEBHOOK_DELIVERY_FAILED => 'Gửi webhook thất bại',
// Reconciliation
self::RECONCILIATION_IN_PROGRESS => 'Đối soát đang được thực hiện',
self::RECONCILIATION_FAILED => 'Đối soát thất bại',
self::RECONCILIATION_DATA_MISMATCH => 'Dữ liệu đối soát không khớp',
default => 'Đã xảy ra lỗi không xác định'
};
}
public static function getHttpStatusCode(string $code): int
{
return match($code) {
// 400 Bad Request
self::VALIDATION_ERROR,
self::INVALID_AMOUNT,
self::INVALID_CURRENCY,
self::INVALID_PAYMENT_METHOD,
self::DUPLICATE_ORDER_ID,
self::TRANSACTION_EXPIRED,
self::TRANSACTION_ALREADY_PROCESSED => 400,
// 401 Unauthorized
self::UNAUTHORIZED,
self::INVALID_API_KEY => 401,
// 403 Forbidden
self::FORBIDDEN,
self::PROJECT_INACTIVE,
self::TENANT_INACTIVE,
self::WEBHOOK_INVALID_IP => 403,
// 404 Not Found
self::PROJECT_NOT_FOUND,
self::TENANT_NOT_FOUND,
self::TRANSACTION_NOT_FOUND => 404,
// 409 Conflict
self::PROJECT_HAS_TENANTS,
self::RECONCILIATION_IN_PROGRESS => 409,
// 422 Unprocessable Entity
self::PROVIDER_NOT_CONFIGURED,
self::TRANSACTION_CANNOT_BE_REFUNDED => 422,
// 429 Too Many Requests
self::RATE_LIMIT_EXCEEDED,
self::TENANT_LIMIT_EXCEEDED => 429,
// 502 Bad Gateway
self::PROVIDER_CONNECTION_ERROR,
self::PROVIDER_INVALID_RESPONSE => 502,
// 503 Service Unavailable
self::PROVIDER_INACTIVE => 503,
// 500 Internal Server Error (default)
default => 500
};
}
}
```
11. PERFORMANCE VÀ SCALING
11.1. Database Optimization
11.1.1. Query Optimization Strategies
<?php
namespace App\Infrastructure\Persistence\Repositories\Eloquent;
use App\Domain\Payment\Repositories\TransactionRepositoryInterface;
use App\Infrastructure\Persistence\Models\Transaction;
use Illuminate\Pagination\LengthAwarePaginator;
class EloquentTransactionRepository implements TransactionRepositoryInterface
{
public function findWithPagination(
array $filters = [],
int $perPage = 20,
int $page = 1,
array $with = []
): LengthAwarePaginator {
$query = Transaction::query();
// Always eager load commonly used relationships
$defaultWith = ['tenant', 'paymentProvider', 'events'];
$with = array_merge($defaultWith, $with);
$query->with($with);
// Apply filters with proper indexing
if (!empty($filters['tenant_id'])) {
$query->where('tenant_id', $filters['tenant_id']);
}
if (!empty($filters['status'])) {
$query->where('status', $filters['status']);
}
if (!empty($filters['date_from'])) {
$query->where('created_at', '>=', $filters['date_from']);
}
if (!empty($filters['date_to'])) {
$query->where('created_at', '<=', $filters['date_to']);
}
if (!empty($filters['amount_from'])) {
$query->where('amount', '>=', $filters['amount_from']);
}
if (!empty($filters['amount_to'])) {
$query->where('amount', '<=', $filters['amount_to']);
}
if (!empty($filters['provider_code'])) {
$query->whereHas('paymentProvider', function($q) use ($filters) {
$q->where('code', $filters['provider_code']);
});
}
if (!empty($filters['search'])) {
$query->where(function($q) use ($filters) {
$q->where('order_id', 'like', "%{$filters['search']}%")
->orWhere('provider_transaction_id', 'like', "%{$filters['search']}%")
->orWhereJsonContains('customer_info->email', $filters['search']);
});
}
// Use appropriate indexes for sorting
$sortField = $filters['sort'] ?? 'created_at';
$sortDirection = $filters['direction'] ?? 'desc';
$query->orderBy($sortField, $sortDirection);
// Add secondary sort for consistency
if ($sortField !== 'id') {
$query->orderBy('id', 'desc');
}
return $query->paginate($perPage, ['*'], 'page', $page);
}
public function getStatistics(array $filters = []): array
{
$query = Transaction::query();
// Apply same filters as pagination
$this->applyFilters($query, $filters);
// Use database aggregation for better performance
$stats = $query->selectRaw('
COUNT(*) as total_transactions,
COUNT(CASE WHEN status = "completed" THEN 1 END) as completed_count,
COUNT(CASE WHEN status = "failed" THEN 1 END) as failed_count,
COUNT(CASE WHEN status = "pending" THEN 1 END) as pending_count,
SUM(CASE WHEN status = "completed" THEN amount ELSE 0 END) as total_amount,
SUM(CASE WHEN status = "completed" THEN transaction_fee ELSE 0 END) as total_fees,
AVG(CASE WHEN status = "completed" THEN amount END) as avg_amount
')->first();
return [
'total_transactions' => (int) $stats->total_transactions,
'completed_count' => (int) $stats->completed_count,
'failed_count' => (int) $stats->failed_count,
'pending_count' => (int) $stats->pending_count,
'total_amount' => (float) $stats->total_amount,
'total_fees' => (float) $stats->total_fees,
'average_amount' => (float) $stats->avg_amount,
'success_rate' => $stats->total_transactions > 0
? round(($stats->completed_count / $stats->total_transactions) * 100, 2)
: 0
];
}
}
```
11.2. Caching Strategy
11.2.1. Multi-layer Caching Implementation
<?php
namespace App\Infrastructure\Cache;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
class CacheManager
{
private const DEFAULT_TTL = 3600; // 1 hour
private const SHORT_TTL = 300; // 5 minutes
private const LONG_TTL = 86400; // 24 hours
// Cache configuration for different entities
private const CACHE_CONFIG = [
'projects' => ['ttl' => self::LONG_TTL, 'tags' => ['projects']],
'tenants' => ['ttl' => self::LONG_TTL, 'tags' => ['tenants']],
'payment_providers' => ['ttl' => self::LONG_TTL, 'tags' => ['providers']],
'tenant_providers' => ['ttl' => self::DEFAULT_TTL, 'tags' => ['tenants', 'providers']],
'transactions' => ['ttl' => self::SHORT_TTL, 'tags' => ['transactions']],
'system_config' => ['ttl' => self::LONG_TTL, 'tags' => ['config']],
'statistics' => ['ttl' => self::SHORT_TTL, 'tags' => ['stats']]
];
public function get(string $key, $default = null)
{
return Cache::get($key, $default);
}
public function put(string $key, $value, string $type = 'default', int $ttl = null): bool
{
$config = self::CACHE_CONFIG[$type] ?? ['ttl' => self::DEFAULT_TTL, 'tags' => []];
$ttl = $ttl ?? $config['ttl'];
if (!empty($config['tags'])) {
return Cache::tags($config['tags'])->put($key, $value, $ttl);
}
return Cache::put($key, $value, $ttl);
}
public function remember(string $key, callable $callback, string $type = 'default', int $ttl = null)
{
$config = self::CACHE_CONFIG[$type] ?? ['ttl' => self::DEFAULT_TTL, 'tags' => []];
$ttl = $ttl ?? $config['ttl'];
if (!empty($config['tags'])) {
return Cache::tags($config['tags'])->remember($key, $ttl, $callback);
}
return Cache::remember($key, $ttl, $callback);
}
public function forget(string $key): bool
{
return Cache::forget($key);
}
public function flush(array $tags = []): bool
{
if (empty($tags)) {
return Cache::flush();
}
return Cache::tags($tags)->flush();
}
// High-performance methods using Redis directly
public function getFromRedis(string $key, $default = null)
{
$value = Redis::get($key);
if ($value === null) {
return $default;
}
return json_decode($value, true);
}
public function putToRedis(string $key, $value, int $ttl = self::DEFAULT_TTL): bool
{
return Redis::setex($key, $ttl, json_encode($value));
}
// Atomic operations for counters
public function increment(string $key, int $value = 1): int
{
return Redis::incrby($key, $value);
}
public function decrement(string $key, int $value = 1): int
{
return Redis::decrby($key, $value);
}
// Lock mechanism for critical sections
public function lock(string $key, int $ttl = 30): bool
{
return Redis::set("lock:{$key}", 1, 'EX', $ttl, 'NX');
}
public function unlock(string $key): bool
{
return Redis::del("lock:{$key}") > 0;
}
// Bulk operations for better performance
public function multiGet(array $keys): array
{
$values = Redis::mget($keys);
$result = [];
foreach ($keys as $index => $key) {
$result[$key] = $values[$index] ? json_decode($values[$index], true) : null;
}
return $result;
}
public function multiSet(array $data, int $ttl = self::DEFAULT_TTL): bool
{
$pipeline = Redis::pipeline();
foreach ($data as $key => $value) {
$pipeline->setex($key, $ttl, json_encode($value));
}
$results = $pipeline->execute();
return !in_array(false, $results, true);
}
}
```
11.3. Auto-scaling Configuration
11.3.1. Kubernetes Deployment Configuration
# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: smartpost-payment-service
labels:
app: smartpost-payment-service
spec:
replicas: 3
selector:
matchLabels:
app: smartpost-payment-service
template:
metadata:
labels:
app: smartpost-payment-service
spec:
containers:
- name: app
image: smartpost/payment-service:latest
ports:
- containerPort: 80
env:
- name: APP_ENV
value: "production"
- name: DB_HOST
valueFrom:
secretKeyRef:
name: database-secret
key: host
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /api/v1/status
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/v1/status
port: 80
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: smartpost-payment-service
spec:
selector:
app: smartpost-payment-service
ports:
- port: 80
targetPort: 80
type: LoadBalancer
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: smartpost-payment-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: smartpost-payment-service
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 100
periodSeconds: 60
- type: Pods
value: 5
periodSeconds: 60
selectPolicy: Max
```
12. SECURITY VÀ COMPLIANCE
12.1. Data Encryption
12.1.1. Sensitive Data Encryption Service
<?php
namespace App\Domain\Shared\Services;
use Illuminate\Encryption\Encrypter;
class EncryptionService
{
private $encrypter;
private $keyRotationEnabled;
public function __construct()
{
$this->encrypter = new Encrypter(
config('app.encryption_key'),
config('app.cipher')
);
$this->keyRotationEnabled = config('app.key_rotation_enabled', false);
}
public function encrypt(string $value, string $context = ''): string
{
$data = [
'value' => $value,
'context' => $context,
'timestamp' => now()->timestamp,
'version' => config('app.encryption_version', 1)
];
return $this->encrypter->encrypt($data);
}
public function decrypt(string $encryptedValue): string
{
$data = $this->encrypter->decrypt($encryptedValue);
// Handle key rotation
if ($this->keyRotationEnabled && $data['version'] < config('app.encryption_version')) {
// Re-encrypt with new key
$newEncrypted = $this->encrypt($data['value'], $data['context']);
// Update database record (implement based on context)
$this->updateEncryptedValue($encryptedValue, $newEncrypted, $data['context']);
}
return $data['value'];
}
public function hash(string $value, string $salt = ''): string
{
return hash_hmac('sha256', $value, config('app.key') . $salt);
}
public function generateSecureToken(int $length = 32): string
{
return bin2hex(random_bytes($length));
}
private function updateEncryptedValue(string $old, string $new, string $context): void
{
// Implement based on context - update database records
// This would typically involve identifying the record and updating it
\Log::info('Key rotation: Re-encrypted value', [
'context' => $context,
'old_hash' => substr(md5($old), 0, 8),
'new_hash' => substr(md5($new), 0, 8)
]);
}
}
```
12.2. Compliance Framework
12.2.1. GDPR Compliance Service
<?php
namespace App\Domain\Shared\Services;
use App\Infrastructure\Persistence\Models\AuditLog;
use Carbon\Carbon;
class ComplianceService
{
private $auditService;
private $encryptionService;
public function __construct(
AuditLogService $auditService,
EncryptionService $encryptionService
) {
$this->auditService = $auditService;
$this->encryptionService = $encryptionService;
}
public function anonymizePersonalData(string $tenantId, string $dataSubject): array
{
$results = [];
// Anonymize transaction data
$results['transactions'] = $this->anonymizeTransactionData($tenantId, $dataSubject);
// Anonymize audit logs
$results['audit_logs'] = $this->anonymizeAuditLogs($tenantId, $dataSubject);
// Anonymize webhook logs
$results['webhook_logs'] = $this->anonymizeWebhookLogs($tenantId, $dataSubject);
// Log the anonymization action
$this->auditService->logAction([
'tenant_id' => $tenantId,
'entity_type' => 'personal_data',
'entity_id' => $dataSubject,
'action' => 'anonymized',
'additional_data' => [
'anonymization_results' => $results,
'requested_at' => now()->toISOString(),
'retention_policy' => 'gdpr_article_17'
]
]);
return $results;
}
public function exportPersonalData(string $tenantId, string $dataSubject): array
{
$exportData = [
'data_subject' => $dataSubject,
'tenant_id' => $tenantId,
'export_date' => now()->toISOString(),
'data_categories' => []
];
// Export transaction data
$exportData['data_categories']['transactions'] = $this->exportTransactionData($tenantId, $dataSubject);
// Export customer information
$exportData['data_categories']['customer_info'] = $this->exportCustomerInfo($tenantId, $dataSubject);
// Export audit trail
$exportData['data_categories']['audit_trail'] = $this->exportAuditTrail($tenantId, $dataSubject);
// Log the export action
$this->auditService->logAction([
'tenant_id' => $tenantId,
'entity_type' => 'personal_data',
'entity_id' => $dataSubject,
'action' => 'exported',
'additional_data' => [
'export_size' => strlen(json_encode($exportData)),
'categories_count' => count($exportData['data_categories']),
'gdpr_article' => 'article_15'
]
]);
return $exportData;
}
public function deletePersonalData(string $tenantId, string $dataSubject, string $legalBasis): array
{
$deletionResults = [];
// Delete from transactions (customer_info)
$deletionResults['transactions'] = \DB::table('transactions')
->where('tenant_id', $tenantId)
->whereJsonContains('customer_info->email', $dataSubject)
->update([
'customer_info' => json_encode(['deleted' => true, 'deleted_at' => now()]),
'updated_at' => now()
]);
// Delete from audit logs
$deletionResults['audit_logs'] = \DB::table('audit_logs')
->where('tenant_id', $tenantId)
->where('additional_data->customer_email', $dataSubject)
->delete();
// Log the deletion
$this->auditService->logAction([
'tenant_id' => $tenantId,
'entity_type' => 'personal_data',
'entity_id' => $dataSubject,
'action' => 'deleted',
'additional_data' => [
'legal_basis' => $legalBasis,
'deletion_results' => $deletionResults,
'gdpr_article' => 'article_17'
]
]);
return $deletionResults;
}
public function applyDataRetentionPolicy(): array
{
$retentionDays = config('app.data_retention_days', 2555); // 7 years default
$cutoffDate = now()->subDays($retentionDays);
$results = [
'cutoff_date' => $cutoffDate->toISOString(),
'deleted_records' => []
];
// Archive old transactions
$results['deleted_records']['transactions'] = $this->archiveOldTransactions($cutoffDate);
// Clean up old audit logs
$results['deleted_records']['audit_logs'] = $this->cleanupOldAuditLogs($cutoffDate);
// Clean up old webhook logs
$results['deleted_records']['webhook_logs'] = $this->cleanupOldWebhookLogs($cutoffDate);
return $results;
}
private function anonymizeTransactionData(string $tenantId, string $dataSubject): int
{
return \DB::table('transactions')
->where('tenant_id', $tenantId)
->whereJsonContains('customer_info->email', $dataSubject)
->update([
'customer_info' => json_encode([
'name' => 'ANONYMIZED',
'email' => 'anonymized@example.com',
'phone' => 'ANONYMIZED',
'address' => 'ANONYMIZED',
'anonymized_at' => now()->toISOString()
]),
'updated_at' => now()
]);
}
private function exportTransactionData(string $tenantId, string $dataSubject): array
{
return \DB::table('transactions')
->where('tenant_id', $tenantId)
->whereJsonContains('customer_info->email', $dataSubject)
->select([
'id', 'order_id', 'amount', 'currency', 'status',
'customer_info', 'created_at', 'paid_at'
])
->get()
->toArray();
}
}
``` ---