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:
86
tests/Feature/Feature/PaymentLoggerTest.php
Normal file
86
tests/Feature/Feature/PaymentLoggerTest.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
use App\Models\PaymentEvent;
|
||||
use App\Models\User;
|
||||
use App\Services\Payments\PaymentLogger;
|
||||
|
||||
test('can log payment event', function () {
|
||||
$logger = new PaymentLogger;
|
||||
|
||||
$logger->logEvent('test_subscription_created', [
|
||||
'subscription_id' => 123,
|
||||
'provider' => 'stripe',
|
||||
'amount' => 10.00,
|
||||
]);
|
||||
|
||||
$event = PaymentEvent::where('event_type', 'test_subscription_created')->first();
|
||||
|
||||
expect($event)->not->toBeNull();
|
||||
expect($event->level)->toBe('info');
|
||||
expect($event->data['subscription_id'])->toBe(123);
|
||||
expect($event->data['provider'])->toBe('stripe');
|
||||
expect($event->data['amount'])->toBe(10.00);
|
||||
});
|
||||
|
||||
test('can log error event', function () {
|
||||
$logger = new PaymentLogger;
|
||||
|
||||
$logger->logError('test_payment_failed', [
|
||||
'subscription_id' => 456,
|
||||
'error' => 'Payment declined',
|
||||
]);
|
||||
|
||||
$event = PaymentEvent::where('event_type', 'test_payment_failed')->first();
|
||||
|
||||
expect($event)->not->toBeNull();
|
||||
expect($event->level)->toBe('error');
|
||||
expect($event->data['error'])->toBe('Payment declined');
|
||||
});
|
||||
|
||||
test('can log security event', function () {
|
||||
$logger = new PaymentLogger;
|
||||
|
||||
$logger->logSecurityEvent('suspicious_activity', [
|
||||
'ip_address' => '192.168.1.1',
|
||||
'user_agent' => 'Test Agent',
|
||||
]);
|
||||
|
||||
$event = PaymentEvent::where('event_type', 'security_suspicious_activity')->first();
|
||||
|
||||
expect($event)->not->toBeNull();
|
||||
expect($event->level)->toBe('warning');
|
||||
expect($event->data['requires_review'])->toBeTrue();
|
||||
});
|
||||
|
||||
test('can get user audit trail', function () {
|
||||
$user = User::factory()->create();
|
||||
$logger = new PaymentLogger;
|
||||
|
||||
// Log some events for the user
|
||||
$logger->logEvent('test_event_1', ['user_id' => $user->id]);
|
||||
$logger->logEvent('test_event_2', ['user_id' => $user->id]);
|
||||
|
||||
$trail = $logger->getUserAuditTrail($user->id);
|
||||
|
||||
expect($trail)->toHaveCount(2);
|
||||
expect($trail[0]['event_type'])->toBe('test_event_2'); // Latest first
|
||||
expect($trail[1]['event_type'])->toBe('test_event_1');
|
||||
});
|
||||
|
||||
test('can generate compliance report', function () {
|
||||
$logger = new PaymentLogger;
|
||||
|
||||
// Log some events
|
||||
$logger->logEvent('compliance_data_access', ['user_id' => 1]);
|
||||
$logger->logEvent('subscription_created', ['user_id' => 2]);
|
||||
$logger->logError('payment_failed', ['user_id' => 3]);
|
||||
|
||||
$report = $logger->generateComplianceReport();
|
||||
|
||||
expect($report['total_events'])->toBe(3);
|
||||
expect($report['events_by_type']['compliance_data_access'])->toBe(1);
|
||||
expect($report['events_by_type']['subscription_created'])->toBe(1);
|
||||
expect($report['events_by_type']['payment_failed'])->toBe(1);
|
||||
expect($report['events_by_level']['info'])->toBe(2);
|
||||
expect($report['events_by_level']['error'])->toBe(1);
|
||||
});
|
||||
147
tests/Feature/PaymentProviderControllerTest.php
Normal file
147
tests/Feature/PaymentProviderControllerTest.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
use App\Models\ActivationKey;
|
||||
use App\Models\User;
|
||||
use App\Services\Payments\PaymentConfigurationManager;
|
||||
use App\Services\Payments\PaymentOrchestrator;
|
||||
use App\Services\Payments\ProviderRegistry;
|
||||
|
||||
test('can redeem activation key', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
// Create an activation key
|
||||
$activationKey = ActivationKey::factory()->create([
|
||||
'activation_key' => 'TEST-KEY-123',
|
||||
'is_activated' => false,
|
||||
]);
|
||||
|
||||
$response = actingAs($user)
|
||||
->postJson('/api/activation-keys/redeem', [
|
||||
'activation_key' => 'TEST-KEY-123',
|
||||
]);
|
||||
|
||||
expect($response->status())->toBe(200);
|
||||
$data = $response->json();
|
||||
expect($data['success'])->toBeTrue();
|
||||
expect($data['data'])->toHaveKey('subscription_id');
|
||||
|
||||
// Verify the key is now activated
|
||||
$activationKey->refresh();
|
||||
expect($activationKey->is_activated)->toBeTrue();
|
||||
});
|
||||
|
||||
test('requires authentication to redeem activation key', function () {
|
||||
$response = postJson('/api/activation-keys/redeem', [
|
||||
'activation_key' => 'TEST-KEY-123',
|
||||
]);
|
||||
|
||||
expect($response->status())->toBe(401);
|
||||
});
|
||||
|
||||
test('can validate activation key', function () {
|
||||
// Create an activation key
|
||||
$activationKey = ActivationKey::factory()->create([
|
||||
'activation_key' => 'VALIDATE-KEY-123',
|
||||
'is_activated' => false,
|
||||
]);
|
||||
|
||||
$response = getJson('/api/activation-keys/validate/VALIDATE-KEY-123');
|
||||
|
||||
expect($response->status())->toBe(200);
|
||||
$data = $response->json();
|
||||
expect($data['valid'])->toBeTrue();
|
||||
expect($data['is_activated'])->toBeFalse();
|
||||
});
|
||||
|
||||
test('returns invalid for non-existent activation key', function () {
|
||||
$response = getJson('/api/activation-keys/validate/NON-EXISTENT');
|
||||
|
||||
expect($response->status())->toBe(200);
|
||||
$data = $response->json();
|
||||
expect($data['valid'])->toBeFalse();
|
||||
expect($data['reason'])->toBe('Activation key not found');
|
||||
});
|
||||
|
||||
test('can get crypto exchange rate', function () {
|
||||
$response = getJson('/api/crypto/rates/btc');
|
||||
|
||||
expect($response->status())->toBe(200);
|
||||
$response->assertJsonStructure([
|
||||
'crypto',
|
||||
'rate_usd_per_crypto',
|
||||
'rate_crypto_per_usd',
|
||||
'updated_at',
|
||||
]);
|
||||
|
||||
$data = $response->json();
|
||||
expect($data['crypto'])->toBe('BTC');
|
||||
expect($data['rate_usd_per_crypto'])->toBeNumeric();
|
||||
expect($data['rate_crypto_per_usd'])->toBeNumeric();
|
||||
});
|
||||
|
||||
test('can convert usd to crypto', function () {
|
||||
$response = getJson('/api/crypto/convert?usd_amount=100&crypto=btc');
|
||||
|
||||
expect($response->status())->toBe(200);
|
||||
$response->assertJsonStructure([
|
||||
'usd_amount',
|
||||
'crypto',
|
||||
'crypto_amount',
|
||||
'fees',
|
||||
'net_crypto_amount',
|
||||
'updated_at',
|
||||
]);
|
||||
|
||||
$data = $response->json();
|
||||
expect($data['usd_amount'])->toBe(100.0);
|
||||
expect($data['crypto'])->toBe('BTC');
|
||||
expect($data['crypto_amount'])->toBeNumeric();
|
||||
expect($data['fees'])->toHaveKey('total_fee');
|
||||
});
|
||||
|
||||
test('validates crypto conversion parameters', function () {
|
||||
// Invalid crypto type
|
||||
$response = getJson('/api/crypto/convert?usd_amount=100&crypto=invalid');
|
||||
|
||||
expect($response->status())->toBe(422);
|
||||
|
||||
// Invalid amount
|
||||
$response = getJson('/api/crypto/convert?usd_amount=0&crypto=btc');
|
||||
|
||||
expect($response->status())->toBe(422);
|
||||
});
|
||||
|
||||
test('payment routes exist', function () {
|
||||
// Test that basic payment routes exist
|
||||
$response = getJson('/api/payment/success');
|
||||
expect($response->status())->toBe(200);
|
||||
|
||||
$response = getJson('/api/payment/cancel');
|
||||
expect($response->status())->toBe(200);
|
||||
});
|
||||
|
||||
test('can create simple provider registry', function () {
|
||||
$registry = new ProviderRegistry;
|
||||
expect($registry)->toBeInstanceOf(ProviderRegistry::class);
|
||||
expect($registry->getAllProviders())->toHaveCount(0);
|
||||
});
|
||||
|
||||
test('can create configuration manager', function () {
|
||||
$registry = new ProviderRegistry;
|
||||
$configManager = new PaymentConfigurationManager($registry);
|
||||
|
||||
expect($configManager)->toBeInstanceOf(PaymentConfigurationManager::class);
|
||||
|
||||
// Test that activation key is always available
|
||||
$configManager->initializeProviders();
|
||||
expect($registry->has('activation_key'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('can create payment orchestrator', function () {
|
||||
$registry = new ProviderRegistry;
|
||||
$configManager = new PaymentConfigurationManager($registry);
|
||||
$orchestrator = new PaymentOrchestrator($registry, $configManager);
|
||||
|
||||
expect($orchestrator)->toBeInstanceOf(PaymentOrchestrator::class);
|
||||
expect($orchestrator->getRegistry())->toBe($registry);
|
||||
});
|
||||
86
tests/Feature/Unit/PaymentLoggerTest.php
Normal file
86
tests/Feature/Unit/PaymentLoggerTest.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
use App\Models\PaymentEvent;
|
||||
use App\Models\User;
|
||||
use App\Services\Payments\PaymentLogger;
|
||||
|
||||
test('can log payment event', function () {
|
||||
$logger = new PaymentLogger;
|
||||
|
||||
$logger->logEvent('test_subscription_created', [
|
||||
'subscription_id' => 123,
|
||||
'provider' => 'stripe',
|
||||
'amount' => 10.00,
|
||||
]);
|
||||
|
||||
$event = PaymentEvent::where('event_type', 'test_subscription_created')->first();
|
||||
|
||||
expect($event)->not->toBeNull();
|
||||
expect($event->level)->toBe('info');
|
||||
expect($event->data['subscription_id'])->toBe(123);
|
||||
expect($event->data['provider'])->toBe('stripe');
|
||||
expect($event->data['amount'])->toBe(10.00);
|
||||
});
|
||||
|
||||
test('can log error event', function () {
|
||||
$logger = new PaymentLogger;
|
||||
|
||||
$logger->logError('test_payment_failed', [
|
||||
'subscription_id' => 456,
|
||||
'error' => 'Payment declined',
|
||||
]);
|
||||
|
||||
$event = PaymentEvent::where('event_type', 'test_payment_failed')->first();
|
||||
|
||||
expect($event)->not->toBeNull();
|
||||
expect($event->level)->toBe('error');
|
||||
expect($event->data['error'])->toBe('Payment declined');
|
||||
});
|
||||
|
||||
test('can log security event', function () {
|
||||
$logger = new PaymentLogger;
|
||||
|
||||
$logger->logSecurityEvent('suspicious_activity', [
|
||||
'ip_address' => '192.168.1.1',
|
||||
'user_agent' => 'Test Agent',
|
||||
]);
|
||||
|
||||
$event = PaymentEvent::where('event_type', 'security_suspicious_activity')->first();
|
||||
|
||||
expect($event)->not->toBeNull();
|
||||
expect($event->level)->toBe('warning');
|
||||
expect($event->data['requires_review'])->toBeTrue();
|
||||
});
|
||||
|
||||
test('can get user audit trail', function () {
|
||||
$user = User::factory()->create();
|
||||
$logger = new PaymentLogger;
|
||||
|
||||
// Log some events for the user
|
||||
$logger->logEvent('test_event_1', ['user_id' => $user->id]);
|
||||
$logger->logEvent('test_event_2', ['user_id' => $user->id]);
|
||||
|
||||
$trail = $logger->getUserAuditTrail($user->id);
|
||||
|
||||
expect($trail)->toHaveCount(2);
|
||||
expect($trail[0]['event_type'])->toBe('test_event_2'); // Latest first
|
||||
expect($trail[1]['event_type'])->toBe('test_event_1');
|
||||
});
|
||||
|
||||
test('can generate compliance report', function () {
|
||||
$logger = new PaymentLogger;
|
||||
|
||||
// Log some events
|
||||
$logger->logEvent('compliance_data_access', ['user_id' => 1]);
|
||||
$logger->logEvent('subscription_created', ['user_id' => 2]);
|
||||
$logger->logError('payment_failed', ['user_id' => 3]);
|
||||
|
||||
$report = $logger->generateComplianceReport();
|
||||
|
||||
expect($report['total_events'])->toBe(3);
|
||||
expect($report['events_by_type']['compliance_data_access'])->toBe(1);
|
||||
expect($report['events_by_type']['subscription_created'])->toBe(1);
|
||||
expect($report['events_by_type']['payment_failed'])->toBe(1);
|
||||
expect($report['events_by_level']['info'])->toBe(2);
|
||||
expect($report['events_by_level']['error'])->toBe(1);
|
||||
});
|
||||
77
tests/Feature/Unit/ProviderRegistryTest.php
Normal file
77
tests/Feature/Unit/ProviderRegistryTest.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
use App\Services\Payments\ProviderRegistry;
|
||||
use App\Services\Payments\Providers\StripeProvider;
|
||||
|
||||
test('can register payment provider', function () {
|
||||
$registry = new ProviderRegistry;
|
||||
$provider = new StripeProvider;
|
||||
|
||||
$registry->register('stripe', $provider);
|
||||
|
||||
expect($registry->has('stripe'))->toBeTrue();
|
||||
expect($registry->get('stripe'))->toBe($provider);
|
||||
});
|
||||
|
||||
test('can get all providers', function () {
|
||||
$registry = new ProviderRegistry;
|
||||
$stripeProvider = new StripeProvider;
|
||||
|
||||
$registry->register('stripe', $stripeProvider);
|
||||
|
||||
$providers = $registry->getAllProviders();
|
||||
|
||||
expect($providers)->toHaveCount(1);
|
||||
expect($providers->get('stripe'))->toBe($stripeProvider);
|
||||
});
|
||||
|
||||
test('can get active providers only', function () {
|
||||
$registry = new ProviderRegistry;
|
||||
$stripeProvider = new StripeProvider;
|
||||
|
||||
$registry->register('stripe', $stripeProvider);
|
||||
|
||||
$activeProviders = $registry->getActiveProviders();
|
||||
|
||||
expect($activeProviders)->toHaveCount(0); // Stripe is inactive without API key
|
||||
});
|
||||
|
||||
test('can unregister provider', function () {
|
||||
$registry = new ProviderRegistry;
|
||||
$provider = new StripeProvider;
|
||||
|
||||
$registry->register('stripe', $provider);
|
||||
expect($registry->has('stripe'))->toBeTrue();
|
||||
|
||||
$result = $registry->unregister('stripe');
|
||||
expect($result)->toBeTrue();
|
||||
expect($registry->has('stripe'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('can validate providers', function () {
|
||||
$registry = new ProviderRegistry;
|
||||
$stripeProvider = new StripeProvider;
|
||||
|
||||
$registry->register('stripe', $stripeProvider);
|
||||
|
||||
$results = $registry->validateProviders();
|
||||
|
||||
expect($results)->toHaveKey('stripe');
|
||||
expect($results['stripe']['active'])->toBeFalse();
|
||||
expect($results['stripe']['supports_recurring'])->toBeTrue();
|
||||
expect($results['stripe']['supports_one_time'])->toBeTrue();
|
||||
});
|
||||
|
||||
test('can get provider statistics', function () {
|
||||
$registry = new ProviderRegistry;
|
||||
$stripeProvider = new StripeProvider;
|
||||
|
||||
$registry->register('stripe', $stripeProvider);
|
||||
|
||||
$stats = $registry->getProviderStats();
|
||||
|
||||
expect($stats['total_providers'])->toBe(1);
|
||||
expect($stats['active_providers'])->toBe(0);
|
||||
expect($stats['recurring_providers'])->toBe(0);
|
||||
expect($stats['one_time_providers'])->toBe(0);
|
||||
});
|
||||
Reference in New Issue
Block a user