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:
410
tests/Unit/OxapayProviderTest.php
Normal file
410
tests/Unit/OxapayProviderTest.php
Normal file
@@ -0,0 +1,410 @@
|
||||
<?php
|
||||
|
||||
use App\Services\Payments\Providers\OxapayProvider;
|
||||
|
||||
test('oxapay provider has correct name and capabilities', function () {
|
||||
$provider = new OxapayProvider;
|
||||
|
||||
expect($provider->getName())->toBe('oxapay');
|
||||
expect($provider->supportsRecurring())->toBeFalse();
|
||||
expect($provider->supportsOneTime())->toBeTrue();
|
||||
expect($provider->getSupportedCurrencies())->toBeArray();
|
||||
});
|
||||
|
||||
test('oxapay provider is inactive without api key', function () {
|
||||
$provider = new OxapayProvider;
|
||||
|
||||
expect($provider->isActive())->toBeFalse();
|
||||
});
|
||||
|
||||
test('oxapay provider is active with merchant api key', function () {
|
||||
$provider = new OxapayProvider(['merchant_api_key' => 'test-api-key']);
|
||||
|
||||
expect($provider->isActive())->toBeTrue();
|
||||
});
|
||||
|
||||
test('can calculate fees correctly', function () {
|
||||
$provider = new OxapayProvider;
|
||||
|
||||
$fees = $provider->calculateFees(100.00);
|
||||
|
||||
expect($fees['fixed_fee'])->toBe(0);
|
||||
expect($fees['percentage_fee'])->toBe(0.5); // 0.5% fee
|
||||
expect($fees['total_fee'])->toBe(0.5);
|
||||
expect($fees['net_amount'])->toBe(99.5);
|
||||
});
|
||||
|
||||
test('can get configuration', function () {
|
||||
$config = [
|
||||
'merchant_api_key' => 'test-api-key',
|
||||
'webhook_url' => 'https://example.com/webhook',
|
||||
'sandbox' => true,
|
||||
];
|
||||
$provider = new OxapayProvider($config);
|
||||
|
||||
expect($provider->getConfiguration())->toHaveKey('merchant_api_key', 'test-api-key');
|
||||
expect($provider->getConfiguration())->toHaveKey('webhook_url', 'https://example.com/webhook');
|
||||
expect($provider->getConfiguration())->toHaveKey('sandbox', true);
|
||||
});
|
||||
|
||||
test('can validate webhook signature', function () {
|
||||
$provider = new OxapayProvider(['merchant_api_key' => 'test-secret']);
|
||||
|
||||
$request = new \Illuminate\Http\Request([], [], [], [], [], [], '{"test": "payload"}');
|
||||
$signature = hash_hmac('sha512', '{"test": "payload"}', 'test-secret');
|
||||
$request->headers->set('HMAC', $signature);
|
||||
|
||||
expect($provider->validateWebhook($request))->toBeTrue();
|
||||
});
|
||||
|
||||
test('webhook validation fails with invalid signature', function () {
|
||||
$provider = new OxapayProvider(['merchant_api_key' => 'test-secret']);
|
||||
|
||||
$request = new \Illuminate\Http\Request([], [], [], [], [], [], '{"test": "payload"}');
|
||||
$request->headers->set('HMAC', 'invalid-signature');
|
||||
|
||||
expect($provider->validateWebhook($request))->toBeFalse();
|
||||
});
|
||||
|
||||
test('webhook validation fails without signature', function () {
|
||||
$provider = new OxapayProvider(['merchant_api_key' => 'test-secret']);
|
||||
|
||||
$request = new \Illuminate\Http\Request([], [], [], [], [], [], '{"test": "payload"}');
|
||||
|
||||
expect($provider->validateWebhook($request))->toBeFalse();
|
||||
});
|
||||
|
||||
test('can process webhook with valid signature', function () {
|
||||
$provider = new OxapayProvider(['merchant_api_key' => 'test-secret']);
|
||||
|
||||
$payload = json_encode([
|
||||
'status' => 'Paid',
|
||||
'track_id' => 'test-track-id',
|
||||
'type' => 'payment',
|
||||
'amount' => 100,
|
||||
'currency' => 'USDT',
|
||||
]);
|
||||
|
||||
$request = new \Illuminate\Http\Request([], [], [], [], [], [], $payload);
|
||||
$signature = hash_hmac('sha512', $payload, 'test-secret');
|
||||
$request->headers->set('HMAC', $signature);
|
||||
|
||||
$result = $provider->processWebhook($request);
|
||||
|
||||
expect($result['success'])->toBeTrue();
|
||||
expect($result['event_type'])->toBe('Paid');
|
||||
expect($result['provider_transaction_id'])->toBe('test-track-id');
|
||||
expect($result['processed'])->toBeTrue();
|
||||
expect($result['type'])->toBe('payment');
|
||||
});
|
||||
|
||||
test('webhook processing fails with invalid signature', function () {
|
||||
$provider = new OxapayProvider(['merchant_api_key' => 'test-secret']);
|
||||
|
||||
$payload = json_encode([
|
||||
'status' => 'Paid',
|
||||
'track_id' => 'test-track-id',
|
||||
]);
|
||||
|
||||
$request = new \Illuminate\Http\Request([], [], [], [], [], [], $payload);
|
||||
$request->headers->set('HMAC', 'invalid-signature');
|
||||
|
||||
$result = $provider->processWebhook($request);
|
||||
|
||||
expect($result['success'])->toBeFalse();
|
||||
expect($result['processed'])->toBeFalse();
|
||||
expect($result['error'])->toBe('Invalid webhook signature');
|
||||
});
|
||||
|
||||
test('cannot create recurring subscription', function () {
|
||||
$provider = new OxapayProvider;
|
||||
$mockUser = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
$mockPlan = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
|
||||
expect(fn () => $provider->createSubscription($mockUser, $mockPlan))
|
||||
->toThrow('OxaPay does not support recurring subscriptions');
|
||||
});
|
||||
|
||||
test('cannot cancel subscription', function () {
|
||||
$provider = new OxapayProvider;
|
||||
$mockSubscription = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
|
||||
expect(fn () => $provider->cancelSubscription($mockSubscription, 'test'))
|
||||
->toThrow('OxaPay does not support recurring subscriptions');
|
||||
});
|
||||
|
||||
test('cannot update subscription', function () {
|
||||
$provider = new OxapayProvider;
|
||||
$mockSubscription = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
$mockPlan = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
|
||||
expect(fn () => $provider->updateSubscription($mockSubscription, $mockPlan))
|
||||
->toThrow('OxaPay does not support recurring subscriptions');
|
||||
});
|
||||
|
||||
test('cannot pause subscription', function () {
|
||||
$provider = new OxapayProvider;
|
||||
$mockSubscription = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
|
||||
expect(fn () => $provider->pauseSubscription($mockSubscription))
|
||||
->toThrow('OxaPay does not support recurring subscriptions');
|
||||
});
|
||||
|
||||
test('cannot resume subscription', function () {
|
||||
$provider = new OxapayProvider;
|
||||
$mockSubscription = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
|
||||
expect(fn () => $provider->resumeSubscription($mockSubscription))
|
||||
->toThrow('OxaPay does not support recurring subscriptions');
|
||||
});
|
||||
|
||||
test('cannot get subscription details', function () {
|
||||
$provider = new OxapayProvider;
|
||||
|
||||
expect(fn () => $provider->getSubscriptionDetails('test-id'))
|
||||
->toThrow('OxaPay does not support recurring subscriptions');
|
||||
});
|
||||
|
||||
test('cannot create customer portal session', function () {
|
||||
$provider = new OxapayProvider;
|
||||
$mockUser = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
|
||||
expect(fn () => $provider->createCustomerPortalSession($mockUser))
|
||||
->toThrow('OxaPay does not provide customer portal functionality');
|
||||
});
|
||||
|
||||
test('cannot sync subscription status', function () {
|
||||
$provider = new OxapayProvider;
|
||||
$mockSubscription = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
|
||||
expect(fn () => $provider->syncSubscriptionStatus($mockSubscription))
|
||||
->toThrow('OxaPay does not support recurring subscriptions');
|
||||
});
|
||||
|
||||
test('cannot process refunds through api', function () {
|
||||
$provider = new OxapayProvider;
|
||||
|
||||
$result = $provider->processRefund('test-id', 100.00, 'User requested');
|
||||
|
||||
expect($result['success'])->toBeFalse();
|
||||
expect($result['error'])->toBe('OxaPay refunds must be processed manually via payouts');
|
||||
});
|
||||
|
||||
test('cannot apply coupon to subscription', function () {
|
||||
$provider = new OxapayProvider;
|
||||
$mockSubscription = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
|
||||
expect(fn () => $provider->applyCoupon($mockSubscription, 'DISCOUNT10'))
|
||||
->toThrow('OxaPay does not support recurring subscriptions');
|
||||
});
|
||||
|
||||
test('cannot remove coupon from subscription', function () {
|
||||
$provider = new OxapayProvider;
|
||||
$mockSubscription = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
|
||||
expect($provider->removeCoupon($mockSubscription))->toBeFalse();
|
||||
});
|
||||
|
||||
test('cannot get upcoming invoice', function () {
|
||||
$provider = new OxapayProvider;
|
||||
$mockSubscription = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
|
||||
expect(fn () => $provider->getUpcomingInvoice($mockSubscription))
|
||||
->toThrow('OxaPay does not support recurring subscriptions');
|
||||
});
|
||||
|
||||
test('cannot retry failed payment', function () {
|
||||
$provider = new OxapayProvider;
|
||||
$mockSubscription = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
|
||||
expect(fn () => $provider->retryFailedPayment($mockSubscription))
|
||||
->toThrow('OxaPay does not support recurring subscriptions');
|
||||
});
|
||||
|
||||
test('cannot start trial', function () {
|
||||
$provider = new OxapayProvider;
|
||||
$mockSubscription = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
|
||||
expect($provider->startTrial($mockSubscription, 7))->toBeFalse();
|
||||
});
|
||||
|
||||
test('cannot modify subscription', function () {
|
||||
$provider = new OxapayProvider;
|
||||
$mockSubscription = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
|
||||
expect($provider->canModifySubscription($mockSubscription))->toBeFalse();
|
||||
});
|
||||
|
||||
test('has correct cancellation terms', function () {
|
||||
$provider = new OxapayProvider;
|
||||
$mockSubscription = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
|
||||
$terms = $provider->getCancellationTerms($mockSubscription);
|
||||
|
||||
expect($terms['immediate_cancellation'])->toBeTrue();
|
||||
expect($terms['refund_policy'])->toBe('no_refunds_crypto');
|
||||
expect($terms['cancellation_effective'])->toBe('immediately');
|
||||
expect($terms['billing_cycle_proration'])->toBeFalse();
|
||||
});
|
||||
|
||||
test('cannot export subscription data', function () {
|
||||
$provider = new OxapayProvider;
|
||||
$mockSubscription = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
|
||||
expect(fn () => $provider->exportSubscriptionData($mockSubscription))
|
||||
->toThrow('OxaPay does not support recurring subscriptions');
|
||||
});
|
||||
|
||||
test('cannot import subscription data', function () {
|
||||
$provider = new OxapayProvider;
|
||||
$mockUser = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
$subscriptionData = ['provider' => 'oxapay', 'data' => []];
|
||||
|
||||
expect(fn () => $provider->importSubscriptionData($mockUser, $subscriptionData))
|
||||
->toThrow('OxaPay does not support recurring subscriptions');
|
||||
});
|
||||
|
||||
test('cannot get subscription metadata', function () {
|
||||
$provider = new OxapayProvider;
|
||||
$mockSubscription = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
|
||||
expect(fn () => $provider->getSubscriptionMetadata($mockSubscription))
|
||||
->toThrow('OxaPay does not support recurring subscriptions');
|
||||
});
|
||||
|
||||
test('cannot update subscription metadata', function () {
|
||||
$provider = new OxapayProvider;
|
||||
$mockSubscription = new class
|
||||
{
|
||||
public $id = 1;
|
||||
};
|
||||
|
||||
expect(fn () => $provider->updateSubscriptionMetadata($mockSubscription, ['test' => 'data']))
|
||||
->toThrow('OxaPay does not support recurring subscriptions');
|
||||
});
|
||||
|
||||
test('has correct method signatures', function () {
|
||||
$provider = new OxapayProvider;
|
||||
|
||||
// Test that all required methods exist and have correct signatures
|
||||
$reflection = new ReflectionClass($provider);
|
||||
|
||||
$methods = [
|
||||
'getName' => 0,
|
||||
'isActive' => 0,
|
||||
'supportsRecurring' => 0,
|
||||
'supportsOneTime' => 0,
|
||||
'getSupportedCurrencies' => 0,
|
||||
'calculateFees' => 1,
|
||||
'createSubscription' => 3,
|
||||
'cancelSubscription' => 2,
|
||||
'updateSubscription' => 2,
|
||||
'pauseSubscription' => 1,
|
||||
'resumeSubscription' => 1,
|
||||
'getSubscriptionDetails' => 1,
|
||||
'createCheckoutSession' => 3,
|
||||
'createCustomerPortalSession' => 1,
|
||||
'processWebhook' => 1,
|
||||
'validateWebhook' => 1,
|
||||
'getConfiguration' => 0,
|
||||
'syncSubscriptionStatus' => 1,
|
||||
'getPaymentMethodDetails' => 1,
|
||||
'processRefund' => 3,
|
||||
'getTransactionHistory' => 2,
|
||||
'getSubscriptionMetadata' => 1,
|
||||
'updateSubscriptionMetadata' => 2,
|
||||
'startTrial' => 2,
|
||||
'applyCoupon' => 2,
|
||||
'removeCoupon' => 1,
|
||||
'getUpcomingInvoice' => 1,
|
||||
'retryFailedPayment' => 1,
|
||||
'canModifySubscription' => 1,
|
||||
'getCancellationTerms' => 1,
|
||||
'exportSubscriptionData' => 1,
|
||||
'importSubscriptionData' => 2,
|
||||
];
|
||||
|
||||
foreach ($methods as $methodName => $expectedParams) {
|
||||
expect($reflection->hasMethod($methodName))->toBeTrue();
|
||||
expect($reflection->getMethod($methodName)->getNumberOfParameters())->toBe($expectedParams);
|
||||
}
|
||||
});
|
||||
|
||||
test('uses correct base url for production', function () {
|
||||
$provider = new OxapayProvider(['sandbox' => false]);
|
||||
|
||||
// Access the baseUrl property via reflection
|
||||
$reflection = new ReflectionClass($provider);
|
||||
$baseUrlProperty = $reflection->getProperty('baseUrl');
|
||||
$baseUrlProperty->setAccessible(true);
|
||||
|
||||
expect($baseUrlProperty->getValue($provider))->toBe('https://api.oxapay.com/v1');
|
||||
});
|
||||
|
||||
test('uses correct base url for sandbox', function () {
|
||||
$provider = new OxapayProvider(['sandbox' => true]);
|
||||
|
||||
// Access the baseUrl property via reflection
|
||||
$reflection = new ReflectionClass($provider);
|
||||
$baseUrlProperty = $reflection->getProperty('baseUrl');
|
||||
$baseUrlProperty->setAccessible(true);
|
||||
|
||||
expect($baseUrlProperty->getValue($provider))->toBe('https://api-sandbox.oxapay.com/v1');
|
||||
});
|
||||
Reference in New Issue
Block a user