feat: implement comprehensive multi-provider payment processing system

- Add unified payment provider architecture with contract-based design
  - Implement 6 payment providers: Stripe, Lemon Squeezy, Polar, Oxapay, Crypto, Activation Keys
  - Create subscription management with lifecycle handling (create, cancel, pause, resume, update)
  - Add coupon system with usage tracking and trial extensions
  - Build Filament admin resources for payment providers, subscriptions, coupons, and trials
  - Implement payment orchestration service with provider registry and configuration management
  - Add comprehensive payment logging and webhook handling for all providers
  - Create customer analytics dashboard with revenue, churn, and lifetime value metrics
  - Add subscription migration service for provider switching
  - Include extensive test coverage for all payment functionality
This commit is contained in:
idevakk
2025-11-19 09:37:00 -08:00
parent 0560016f33
commit 27ac13948c
83 changed files with 15613 additions and 103 deletions

View File

@@ -0,0 +1,431 @@
<?php
namespace App\Services\Payments;
use App\Models\PaymentEvent;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class PaymentLogger
{
protected array $context = [];
public function __construct()
{
$this->context = [
'request_id' => uniqid('pay_', true),
'timestamp' => now()->toISOString(),
'user_agent' => $this->getUserAgent(),
'ip_address' => $this->getIpAddress(),
];
}
/**
* Log a payment event
*/
public function logEvent(string $eventType, array $data = [], ?string $level = 'info'): void
{
$eventData = array_merge($this->context, [
'event_type' => $eventType,
'user_id' => $this->getUserId(),
'data' => $data,
'level' => $level,
]);
// Log to Laravel logs
$this->logToFile("Payment event: {$eventType}", $eventData, $level);
// Store in database for audit trail
$this->storeEvent($eventType, $data, $level);
}
/**
* Log an error event
*/
public function logError(string $eventType, array $data = [], ?\Exception $exception = null): void
{
$errorData = $data;
if ($exception) {
$errorData['exception'] = [
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString(),
];
}
$this->logEvent($eventType, $errorData, 'error');
}
/**
* Log a security event
*/
public function logSecurityEvent(string $eventType, array $data = []): void
{
$securityData = array_merge($data, [
'security_level' => 'high',
'requires_review' => true,
]);
$this->logEvent("security_{$eventType}", $securityData, 'warning');
// Additional security logging
$this->logToFile("Security payment event: {$eventType}", array_merge($this->context, $securityData), 'warning');
}
/**
* Log webhook events
*/
public function logWebhook(string $provider, string $eventType, array $payload, bool $success = true): void
{
$webhookData = [
'provider' => $provider,
'webhook_event_type' => $eventType,
'payload_size' => strlen(json_encode($payload)),
'payload_hash' => hash('sha256', json_encode($payload)),
'success' => $success,
];
// Don't store full payload in logs for security/size reasons
$this->logEvent('webhook_received', $webhookData, $success ? 'info' : 'error');
// Store full payload in database for debugging (with retention policy)
$this->storeWebhookPayload($provider, $eventType, $payload, $success);
}
/**
* Log subscription lifecycle events
*/
public function logSubscriptionEvent(string $action, int $subscriptionId, array $data = []): void
{
$subscriptionData = array_merge($data, [
'subscription_id' => $subscriptionId,
'action' => $action,
]);
$this->logEvent("subscription_{$action}", $subscriptionData);
}
/**
* Log payment method events
*/
public function logPaymentMethodEvent(string $action, array $data = []): void
{
$this->logEvent("payment_method_{$action}", $data);
}
/**
* Log provider events
*/
public function logProviderEvent(string $provider, string $action, array $data = []): void
{
$providerData = array_merge($data, [
'provider' => $provider,
'provider_action' => $action,
]);
$this->logEvent("provider_{$action}", $providerData);
}
/**
* Log admin actions
*/
public function logAdminAction(string $action, array $data = []): void
{
$adminData = array_merge($data, [
'admin_user_id' => Auth::id(),
'admin_action' => $action,
'requires_review' => in_array($action, ['refund', 'subscription_override', 'provider_config_change']),
]);
$this->logEvent("admin_{$action}", $adminData);
}
/**
* Log migration events
*/
public function logMigrationEvent(string $action, array $data = []): void
{
$migrationData = array_merge($data, [
'migration_action' => $action,
'batch_id' => $data['batch_id'] ?? null,
]);
$this->logEvent("migration_{$action}", $migrationData);
}
/**
* Log compliance events
*/
public function logComplianceEvent(string $type, array $data = []): void
{
$complianceData = array_merge($data, [
'compliance_type' => $type,
'retention_required' => true,
'gdpr_relevant' => in_array($type, ['data_access', 'data_deletion', 'consent_withdrawn']),
]);
$this->logEvent("compliance_{$type}", $complianceData);
}
/**
* Get audit trail for a specific user
*/
public function getUserAuditTrail(int $userId, array $filters = []): array
{
$query = PaymentEvent::where('user_id', $userId);
if (! empty($filters['event_type'])) {
$query->where('event_type', $filters['event_type']);
}
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['level'])) {
$query->where('level', $filters['level']);
}
return $query->orderBy('created_at', 'desc')
->limit($filters['limit'] ?? 1000)
->get()
->toArray();
}
/**
* Get audit trail for a subscription
*/
public function getSubscriptionAuditTrail(int $subscriptionId): array
{
return PaymentEvent::whereJsonContains('data->subscription_id', $subscriptionId)
->orderBy('created_at', 'desc')
->get()
->toArray();
}
/**
* Get provider audit trail
*/
public function getProviderAuditTrail(string $provider, array $filters = []): array
{
$query = PaymentEvent::whereJsonContains('data->provider', $provider);
if (! empty($filters['date_from'])) {
$query->where('created_at', '>=', $filters['date_from']);
}
if (! empty($filters['date_to'])) {
$query->where('created_at', '<=', $filters['date_to']);
}
return $query->orderBy('created_at', 'desc')
->limit($filters['limit'] ?? 1000)
->get()
->toArray();
}
/**
* Generate compliance report
*/
public function generateComplianceReport(array $criteria = []): array
{
$query = PaymentEvent::query();
if (! empty($criteria['date_from'])) {
$query->where('created_at', '>=', $criteria['date_from']);
}
if (! empty($criteria['date_to'])) {
$query->where('created_at', '<=', $criteria['date_to']);
}
if (! empty($criteria['event_types'])) {
$query->whereIn('event_type', $criteria['event_types']);
}
$events = $query->get();
return [
'report_generated_at' => now()->toISOString(),
'criteria' => $criteria,
'total_events' => $events->count(),
'events_by_type' => $events->groupBy('event_type')->map->count(),
'events_by_level' => $events->groupBy('level')->map->count(),
'security_events' => $events->filter(fn ($e) => str_contains($e->event_type, 'security'))->count(),
'compliance_events' => $events->filter(fn ($e) => str_contains($e->event_type, 'compliance'))->count(),
'retention_summary' => $this->getRetentionSummary($events),
];
}
/**
* Store event in database
*/
protected function storeEvent(string $eventType, array $data, string $level): void
{
try {
PaymentEvent::create([
'event_type' => $eventType,
'user_id' => Auth::id(),
'level' => $level,
'data' => array_merge($this->context, $data),
'ip_address' => $this->context['ip_address'],
'user_agent' => $this->context['user_agent'],
'request_id' => $this->context['request_id'],
]);
} catch (\Exception $e) {
// Fallback to file logging if database fails
$this->logToFile('Failed to store payment event in database', [
'event_type' => $eventType,
'error' => $e->getMessage(),
'data' => $data,
], 'error');
}
}
/**
* Store webhook payload
*/
protected function storeWebhookPayload(string $provider, string $eventType, array $payload, bool $success): void
{
try {
PaymentEvent::create([
'event_type' => 'webhook_payload',
'level' => $success ? 'info' : 'error',
'data' => [
'provider' => $provider,
'webhook_event_type' => $eventType,
'payload' => $payload,
'success' => $success,
'stored_at' => now()->toISOString(),
] + $this->context,
'ip_address' => $this->context['ip_address'],
'user_agent' => $this->context['user_agent'],
'request_id' => $this->context['request_id'],
'expires_at' => now()->addDays(30), // Webhook payloads expire after 30 days
]);
} catch (\Exception $e) {
$this->logToFile('Failed to store webhook payload', [
'provider' => $provider,
'event_type' => $eventType,
'error' => $e->getMessage(),
], 'error');
}
}
/**
* Get retention summary for compliance
*/
protected function getRetentionSummary($events): array
{
$now = now();
$retentionPeriods = [
'30_days' => $now->copy()->subDays(30),
'90_days' => $now->copy()->subDays(90),
'1_year' => $now->copy()->subYear(),
'7_years' => $now->copy()->subYears(7),
];
return array_map(static function ($date) use ($events) {
return $events->where('created_at', '>=', $date)->count();
}, $retentionPeriods);
}
/**
* Clean up old events based on retention policy
*/
public function cleanupOldEvents(): array
{
$cleanupResults = [];
// Clean up webhook payloads after 30 days
$webhookCleanup = PaymentEvent::where('event_type', 'webhook_payload')
->where('expires_at', '<', now())
->delete();
$cleanupResults['webhook_payloads'] = $webhookCleanup;
// Clean up debug events after 90 days
$debugCleanup = PaymentEvent::where('level', 'debug')
->where('created_at', '<', now()->subDays(90))
->delete();
$cleanupResults['debug_events'] = $debugCleanup;
// Keep compliance and security events for 7 years
// This is handled by database retention policies
$this->logToFile('Payment event cleanup completed', $cleanupResults, 'info');
return $cleanupResults;
}
/**
* Set additional context for logging
*/
public function setContext(array $context): void
{
$this->context = array_merge($this->context, $context);
}
/**
* Get current context
*/
public function getContext(): array
{
return $this->context;
}
/**
* Get user agent safely
*/
protected function getUserAgent(): ?string
{
try {
return request()->userAgent();
} catch (\Exception $e) {
return null;
}
}
/**
* Get IP address safely
*/
protected function getIpAddress(): ?string
{
try {
return request()->ip();
} catch (\Exception $e) {
return null;
}
}
/**
* Get user ID safely
*/
protected function getUserId(): ?int
{
try {
return Auth::id();
} catch (\Exception $e) {
return null;
}
}
/**
* Log to file safely
*/
protected function logToFile(string $message, array $context, string $level): void
{
try {
Log::{$level}($message, $context);
} catch (\Exception $e) {
// Silently fail if logging isn't available
}
}
}