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:
431
app/Services/Payments/PaymentLogger.php
Normal file
431
app/Services/Payments/PaymentLogger.php
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user