- 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
384 lines
12 KiB
PHP
384 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Payments\Providers;
|
|
|
|
use App\Contracts\Payments\PaymentProviderContract;
|
|
use App\Models\Plan;
|
|
use App\Models\Subscription;
|
|
use App\Models\User;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class OxapayProvider implements PaymentProviderContract
|
|
{
|
|
protected array $config;
|
|
|
|
protected string $baseUrl;
|
|
|
|
public function __construct(array $config = [])
|
|
{
|
|
$this->config = $config;
|
|
$this->baseUrl = $config['sandbox'] ?? false
|
|
? 'https://api-sandbox.oxapay.com/v1'
|
|
: 'https://api.oxapay.com/v1';
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'oxapay';
|
|
}
|
|
|
|
public function isActive(): bool
|
|
{
|
|
return ! empty($this->config['merchant_api_key']);
|
|
}
|
|
|
|
public function supportsRecurring(): bool
|
|
{
|
|
return false; // OxaPay doesn't support recurring payments
|
|
}
|
|
|
|
public function supportsOneTime(): bool
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public function getSupportedCurrencies(): array
|
|
{
|
|
return Cache::remember('oxapay_currencies', now()->addHour(), function () {
|
|
try {
|
|
$response = Http::withHeaders([
|
|
'merchant_api_key' => $this->config['merchant_api_key'],
|
|
])->get("{$this->baseUrl}/info/currencies");
|
|
|
|
if ($response->successful()) {
|
|
$data = $response->json();
|
|
|
|
return $data['data'] ?? [];
|
|
}
|
|
} catch (\Exception $e) {
|
|
Log::error('Failed to fetch OxaPay currencies', [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
|
|
return ['BTC', 'ETH', 'USDT', 'USDC', 'LTC', 'BCH']; // Default common cryptos
|
|
});
|
|
}
|
|
|
|
public function calculateFees(float $amount): array
|
|
{
|
|
// OxaPay fees vary by currency and network
|
|
// Using average estimates - actual fees should be fetched from API
|
|
$percentageFee = 0.5; // 0.5% average
|
|
$fixedFee = 0.0; // No fixed fee for most cryptos
|
|
$totalFee = ($amount * $percentageFee / 100) + $fixedFee;
|
|
|
|
return [
|
|
'fixed_fee' => $fixedFee,
|
|
'percentage_fee' => $percentageFee,
|
|
'total_fee' => $totalFee,
|
|
'net_amount' => $amount - $totalFee,
|
|
];
|
|
}
|
|
|
|
public function createSubscription(User $user, Plan $plan, array $options = []): array
|
|
{
|
|
throw new \Exception('OxaPay does not support recurring subscriptions');
|
|
}
|
|
|
|
public function cancelSubscription(Subscription $subscription, string $reason = ''): bool
|
|
{
|
|
throw new \Exception('OxaPay does not support recurring subscriptions');
|
|
}
|
|
|
|
public function updateSubscription(Subscription $subscription, Plan $newPlan): array
|
|
{
|
|
throw new \Exception('OxaPay does not support recurring subscriptions');
|
|
}
|
|
|
|
public function pauseSubscription(Subscription $subscription): bool
|
|
{
|
|
throw new \Exception('OxaPay does not support recurring subscriptions');
|
|
}
|
|
|
|
public function resumeSubscription(Subscription $subscription): bool
|
|
{
|
|
throw new \Exception('OxaPay does not support recurring subscriptions');
|
|
}
|
|
|
|
public function getSubscriptionDetails(string $subscriptionId): array
|
|
{
|
|
throw new \Exception('OxaPay does not support recurring subscriptions');
|
|
}
|
|
|
|
public function createCheckoutSession(User $user, Plan $plan, array $options = []): array
|
|
{
|
|
try {
|
|
$amount = $options['amount'] ?? $plan->price;
|
|
$currency = $options['currency'] ?? 'USD';
|
|
$toCurrency = $options['to_currency'] ?? 'USDT';
|
|
|
|
$payload = [
|
|
'amount' => $amount,
|
|
'currency' => $currency,
|
|
'to_currency' => $toCurrency,
|
|
'lifetime' => $options['lifetime'] ?? 60, // 60 minutes default
|
|
'fee_paid_by_payer' => $options['fee_paid_by_payer'] ?? 0,
|
|
'callback_url' => $this->config['webhook_url'] ?? route('webhooks.oxapay'),
|
|
'return_url' => $this->config['success_url'] ?? route('payment.success'),
|
|
'email' => $user->email,
|
|
'order_id' => $options['order_id'] ?? null,
|
|
'description' => $options['description'] ?? "Payment for {$plan->name}",
|
|
'sandbox' => $this->config['sandbox'] ?? false,
|
|
];
|
|
|
|
$response = Http::withHeaders([
|
|
'merchant_api_key' => $this->config['merchant_api_key'],
|
|
'Content-Type' => 'application/json',
|
|
])->post("{$this->baseUrl}/payment/invoice", $payload);
|
|
|
|
if (! $response->successful()) {
|
|
throw new \Exception('Failed to create OxaPay invoice: '.$response->body());
|
|
}
|
|
|
|
$data = $response->json();
|
|
|
|
return [
|
|
'success' => true,
|
|
'checkout_url' => $data['data']['payment_url'] ?? null,
|
|
'payment_id' => $data['data']['track_id'] ?? null,
|
|
'expires_at' => $data['data']['expired_at'] ?? null,
|
|
'amount' => $amount,
|
|
'currency' => $currency,
|
|
'provider' => 'oxapay',
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error('OxaPay checkout session creation failed', [
|
|
'error' => $e->getMessage(),
|
|
'user_id' => $user->id,
|
|
'plan_id' => $plan->id,
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
public function createCustomerPortalSession(User $user): array
|
|
{
|
|
throw new \Exception('OxaPay does not provide customer portal functionality');
|
|
}
|
|
|
|
public function processWebhook(Request $request): array
|
|
{
|
|
try {
|
|
$payload = $request->getContent();
|
|
$signature = $request->header('HMAC');
|
|
|
|
if (! $this->validateWebhook($request)) {
|
|
throw new \Exception('Invalid webhook signature');
|
|
}
|
|
|
|
$data = $request->json()->all();
|
|
$status = $data['status'] ?? 'unknown';
|
|
$trackId = $data['track_id'] ?? null;
|
|
$type = $data['type'] ?? 'payment';
|
|
|
|
return [
|
|
'success' => true,
|
|
'event_type' => $status,
|
|
'provider_transaction_id' => $trackId,
|
|
'processed' => true,
|
|
'data' => $data,
|
|
'type' => $type,
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error('OxaPay webhook processing failed', [
|
|
'error' => $e->getMessage(),
|
|
'payload' => $request->getContent(),
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'event_type' => 'error',
|
|
'provider_transaction_id' => null,
|
|
'processed' => false,
|
|
'error' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
public function validateWebhook(Request $request): bool
|
|
{
|
|
try {
|
|
$payload = $request->getContent();
|
|
$signature = $request->header('HMAC');
|
|
$apiSecret = $this->config['merchant_api_key'];
|
|
|
|
if (empty($signature) || empty($apiSecret)) {
|
|
return false;
|
|
}
|
|
|
|
$expectedSignature = hash_hmac('sha512', $payload, $apiSecret);
|
|
|
|
return hash_equals($expectedSignature, $signature);
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error('OxaPay webhook validation failed', [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function getConfiguration(): array
|
|
{
|
|
return $this->config;
|
|
}
|
|
|
|
public function syncSubscriptionStatus(Subscription $subscription): array
|
|
{
|
|
throw new \Exception('OxaPay does not support recurring subscriptions');
|
|
}
|
|
|
|
public function getPaymentMethodDetails(string $paymentId): array
|
|
{
|
|
try {
|
|
$response = Http::withHeaders([
|
|
'merchant_api_key' => $this->config['merchant_api_key'],
|
|
])->get("{$this->baseUrl}/payment/info", [
|
|
'track_id' => $paymentId,
|
|
]);
|
|
|
|
if ($response->successful()) {
|
|
$data = $response->json();
|
|
|
|
return [
|
|
'success' => true,
|
|
'details' => $data['data'] ?? [],
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Payment not found',
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
public function processRefund(string $paymentIntentId, float $amount, string $reason = ''): array
|
|
{
|
|
// OxaPay doesn't support traditional refunds in crypto
|
|
// Would need manual payout process
|
|
return [
|
|
'success' => false,
|
|
'error' => 'OxaPay refunds must be processed manually via payouts',
|
|
];
|
|
}
|
|
|
|
public function getTransactionHistory(User $user, array $filters = []): array
|
|
{
|
|
try {
|
|
$response = Http::withHeaders([
|
|
'merchant_api_key' => $this->config['merchant_api_key'],
|
|
])->get("{$this->baseUrl}/payment/history", array_merge([
|
|
'email' => $user->email,
|
|
], $filters));
|
|
|
|
if ($response->successful()) {
|
|
$data = $response->json();
|
|
|
|
return [
|
|
'success' => true,
|
|
'transactions' => $data['data'] ?? [],
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Failed to fetch transaction history',
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
public function getSubscriptionMetadata(Subscription $subscription): array
|
|
{
|
|
throw new \Exception('OxaPay does not support recurring subscriptions');
|
|
}
|
|
|
|
public function updateSubscriptionMetadata(Subscription $subscription, array $metadata): bool
|
|
{
|
|
throw new \Exception('OxaPay does not support recurring subscriptions');
|
|
}
|
|
|
|
public function startTrial(Subscription $subscription, int $trialDays): bool
|
|
{
|
|
throw new \Exception('OxaPay does not support recurring subscriptions');
|
|
}
|
|
|
|
public function applyCoupon(Subscription $subscription, string $couponCode): array
|
|
{
|
|
throw new \Exception('OxaPay does not support recurring subscriptions');
|
|
}
|
|
|
|
public function removeCoupon(Subscription $subscription): bool
|
|
{
|
|
throw new \Exception('OxaPay does not support recurring subscriptions');
|
|
}
|
|
|
|
public function getUpcomingInvoice(Subscription $subscription): array
|
|
{
|
|
throw new \Exception('OxaPay does not support recurring subscriptions');
|
|
}
|
|
|
|
public function retryFailedPayment(Subscription $subscription): array
|
|
{
|
|
throw new \Exception('OxaPay does not support recurring subscriptions');
|
|
}
|
|
|
|
public function canModifySubscription(Subscription $subscription): bool
|
|
{
|
|
return false; // No recurring support
|
|
}
|
|
|
|
public function getCancellationTerms(Subscription $subscription): array
|
|
{
|
|
return [
|
|
'immediate_cancellation' => true,
|
|
'refund_policy' => 'no_refunds_crypto',
|
|
'cancellation_effective' => 'immediately',
|
|
'billing_cycle_proration' => false,
|
|
];
|
|
}
|
|
|
|
public function exportSubscriptionData(Subscription $subscription): array
|
|
{
|
|
throw new \Exception('OxaPay does not support recurring subscriptions');
|
|
}
|
|
|
|
public function importSubscriptionData(User $user, array $subscriptionData): array
|
|
{
|
|
throw new \Exception('OxaPay does not support recurring subscriptions');
|
|
}
|
|
}
|