965 lines
34 KiB
PHP
965 lines
34 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Payments\Providers;
|
|
|
|
use App\Contracts\Payments\PaymentProviderContract;
|
|
use App\Models\PaymentProvider as PaymentProviderModel;
|
|
use App\Models\Plan;
|
|
use App\Models\Subscription;
|
|
use App\Models\User;
|
|
use App\NotifyMe;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class OxapayProvider implements PaymentProviderContract
|
|
{
|
|
use NotifyMe;
|
|
|
|
protected array $config;
|
|
|
|
protected bool $sandbox;
|
|
|
|
protected string $baseUrl;
|
|
|
|
protected string $merchantApiKey;
|
|
|
|
public function __construct(array $config = [])
|
|
{
|
|
// Load configuration from PaymentProvider model to get the latest data
|
|
$dbConfig = $this->loadConfigurationFromModel();
|
|
|
|
// Merge with any passed config (passed config takes precedence)
|
|
$config = array_merge($dbConfig, $config);
|
|
|
|
$this->config = $config;
|
|
$this->sandbox = $config['sandbox'] === "true" ?? false;
|
|
$this->merchantApiKey = $this->sandbox ? ($config['sandbox_merchant_api_key'] ?? '') : ($config['merchant_api_key'] ?? '');
|
|
|
|
$this->baseUrl = 'https://api.oxapay.com/v1';
|
|
|
|
Log::info('OxaPayProvider configuration loaded', [
|
|
'sandbox' => $this->sandbox,
|
|
'has_merchant_api_key' => ! empty($this->merchantApiKey),
|
|
'base_url' => $this->baseUrl,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Load configuration from PaymentProvider model
|
|
*/
|
|
protected function loadConfigurationFromModel(): array
|
|
{
|
|
try {
|
|
$providerModel = PaymentProviderModel::where('name', 'oxapay')->first();
|
|
|
|
if ($providerModel && $providerModel->configuration) {
|
|
return $providerModel->configuration;
|
|
}
|
|
|
|
Log::warning('OxaPayProvider configuration not found in database, using defaults');
|
|
|
|
return $this->getDefaultConfiguration();
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error('OxaPayProvider failed to load configuration from database', [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return $this->getDefaultConfiguration();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get default configuration
|
|
*/
|
|
protected function getDefaultConfiguration(): array
|
|
{
|
|
return [
|
|
'merchant_api_key' => '',
|
|
'sandbox_merchant_api_key' => '',
|
|
'sandbox' => false,
|
|
'webhook_url' => route('webhook.payment', 'oxapay'),
|
|
'success_url' => route('payment.success'),
|
|
'cancel_url' => route('payment.cancel'),
|
|
'default_lifetime' => 60, // minutes
|
|
'default_under_paid_coverage' => 5, // percentage
|
|
'fee_paid_by_payer' => 0, // merchant pays by default
|
|
];
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'oxapay';
|
|
}
|
|
|
|
public function isActive(): bool
|
|
{
|
|
return ! empty($this->merchantApiKey);
|
|
}
|
|
|
|
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->merchantApiKey,
|
|
])->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
|
|
{
|
|
try {
|
|
// For oxapay, we don't actually cancel since it's a one-time activation
|
|
// We can deactivate the subscription if needed
|
|
$subscription->update([
|
|
'status' => 'cancelled',
|
|
'cancelled_at' => now(),
|
|
'cancellation_reason' => $reason,
|
|
]);
|
|
|
|
// Notify subscription cancelled
|
|
$this->notifySubscriptionCancelled($subscription, $reason ?: 'Manual cancellation');
|
|
|
|
return true;
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error('Oxapay subscription cancellation failed', [
|
|
'subscription_id' => $subscription->id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
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 = (float) $plan->price;
|
|
$currency = (string) ($this->config['currency'] ?? 'USD');
|
|
|
|
// Build simple invoice request payload
|
|
$payload = [
|
|
'amount' => $amount, // number · decimal
|
|
'currency' => $currency, // string
|
|
'lifetime' => (int) ($this->config['lifetime'] ?? 30), // integer · min: 15 · max: 2880
|
|
'fee_paid_by_payer' => (int) ($this->config['fee_paid_by_payer'] ?? 1), // number · decimal · max: 1
|
|
'under_paid_coverage' => (float) ($this->config['under_paid_coverage'] ?? 2.5), // number · decimal · max: 60
|
|
'auto_withdrawal' => (bool) ($this->config['auto_withdrawal'] ?? false), // boolean
|
|
'mixed_payment' => (bool) ($this->config['mixed_payment'] ?? false), // boolean
|
|
'return_url' => (string) ($this->config['success_url'] ?? route('payment.success')), // string
|
|
'callback_url' => (string) ($this->config['callback_url'] ?? route('webhook.oxapay')), // string
|
|
'order_id' => (string) ($this->config['order_id'] ?? 'order_'.$user->id.'_'.time()), // string
|
|
'thanks_message' => (string) ($this->config['thanks_message'] ?? 'Thank you for your payment!'), // string
|
|
'description' => (string) ($this->config['description'] ?? "Payment for plan: {$plan->name}"), // string
|
|
'sandbox' => $this->sandbox, // boolean
|
|
];
|
|
|
|
// Add to_currency only if it's properly configured
|
|
$configuredToCurrency = $this->config['to_currency'] ?? null;
|
|
if (! empty($configuredToCurrency)) {
|
|
$toCurrency = (string) $configuredToCurrency;
|
|
$payload['to_currency'] = $toCurrency;
|
|
}
|
|
|
|
Log::info('Creating OxaPay invoice', [
|
|
'user_id' => $user->id,
|
|
'plan_id' => $plan->id,
|
|
'payload_keys' => array_keys($payload),
|
|
]);
|
|
|
|
$response = Http::withHeaders([
|
|
'merchant_api_key' => $this->merchantApiKey,
|
|
'Content-Type' => 'application/json',
|
|
])->post("{$this->baseUrl}/payment/invoice", $payload);
|
|
|
|
if (! $response->successful()) {
|
|
$errorData = $response->json();
|
|
$errorMessage = $errorData['message'] ?? 'Unknown error';
|
|
|
|
Log::error('OxaPay invoice creation failed', [
|
|
'status' => $response->status(),
|
|
'response' => $errorData,
|
|
'payload' => $payload,
|
|
]);
|
|
|
|
throw new \Exception("Failed to create OxaPay invoice: {$errorMessage}");
|
|
}
|
|
|
|
$data = $response->json();
|
|
|
|
if (! isset($data['data']) || $data['status'] !== 200) {
|
|
throw new \Exception('Invalid response from OxaPay API');
|
|
}
|
|
|
|
$paymentData = $data['data'];
|
|
|
|
// Create local subscription record
|
|
$subscription = Subscription::create([
|
|
'user_id' => $user->id,
|
|
'plan_id' => $plan->id,
|
|
'provider' => 'oxapay',
|
|
'type' => 'one_time', // OxaPay doesn't support recurring
|
|
'provider_subscription_id' => $paymentData['track_id'],
|
|
'provider_checkout_id' => $paymentData['track_id'],
|
|
'status' => 'pending_payment',
|
|
'unified_status' => 'pending_payment',
|
|
'quantity' => 1,
|
|
'provider_data' => $paymentData,
|
|
'starts_at' => now(),
|
|
'ends_at' => $this->calculateSubscriptionEndDate($plan),
|
|
'last_provider_sync' => now(),
|
|
]);
|
|
|
|
// Notify payment created
|
|
$this->notifyPaymentCreated($user, $plan, [
|
|
'amount' => $paymentData['amount'] ?? $amount,
|
|
'currency' => $paymentData['currency'] ?? $currency,
|
|
'payment_id' => $paymentData['track_id'] ?? null,
|
|
]);
|
|
|
|
Log::info('OxaPay subscription created', [
|
|
'subscription_id' => $subscription->id,
|
|
'track_id' => $paymentData['track_id'],
|
|
'user_id' => $user->id,
|
|
'plan_id' => $plan->id,
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'checkout_url' => $paymentData['payment_url'] ?? null,
|
|
'payment_id' => $paymentData['track_id'] ?? null,
|
|
'track_id' => $paymentData['track_id'] ?? null,
|
|
'subscription_id' => $subscription->id,
|
|
'amount' => $paymentData['amount'] ?? $amount,
|
|
'currency' => $paymentData['currency'] ?? $currency,
|
|
'pay_amount' => $paymentData['pay_amount'] ?? null,
|
|
'pay_currency' => $paymentData['pay_currency'] ?? null,
|
|
'expires_at' => isset($paymentData['expired_at']) ? Carbon::createFromTimestamp($paymentData['expired_at']) : null,
|
|
'lifetime' => $paymentData['lifetime'] ?? $payload['lifetime'],
|
|
'under_paid_coverage' => $paymentData['under_paid_coverage'] ?? $payload['under_paid_coverage'],
|
|
'fee_paid_by_payer' => $paymentData['fee_paid_by_payer'] ?? $payload['fee_paid_by_payer'],
|
|
'provider' => 'oxapay',
|
|
'provider_data' => $paymentData,
|
|
];
|
|
|
|
} 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');
|
|
|
|
$data = $request->json()->all();
|
|
$status = strtolower($data['status'] ?? 'unknown');
|
|
$trackId = $data['track_id'] ?? null;
|
|
$type = $data['type'] ?? 'payment';
|
|
|
|
if (! $trackId) {
|
|
throw new \Exception('Missing track_id in webhook payload');
|
|
}
|
|
Log::debug('before send webhook status');
|
|
// Notify webhook received
|
|
$this->notifyWebhookReceived($status, $trackId);
|
|
Log::debug('before send webhook status with payload'.$status.' '.$trackId);
|
|
|
|
if (! $this->validateWebhook($request)) {
|
|
$this->notifyWebhookError($status, $trackId, 'Invalid webhook signature');
|
|
throw new \Exception('Invalid webhook signature');
|
|
}
|
|
|
|
Log::info('OxaPay webhook received', [
|
|
'track_id' => $trackId,
|
|
'status' => $status,
|
|
'type' => $type,
|
|
]);
|
|
|
|
// Process the webhook based on status
|
|
$result = $this->handleWebhookStatus($status, $data);
|
|
|
|
// Notify webhook processed successfully
|
|
$this->notifyWebhookProcessed($status, $trackId, $data['payment_id'] ?? null);
|
|
|
|
return [
|
|
'success' => true,
|
|
'event_type' => $status,
|
|
'provider_transaction_id' => $trackId,
|
|
'processed' => true,
|
|
'data' => $data,
|
|
'type' => $type,
|
|
'result' => $result,
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error('OxaPay webhook processing failed', [
|
|
'error' => $e->getMessage(),
|
|
'payload' => $request->getContent(),
|
|
]);
|
|
|
|
// Notify webhook processing error
|
|
$this->notifyWebhookError($data['status'] ?? 'unknown', $data['track_id'] ?? 'unknown', $e->getMessage());
|
|
|
|
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'); // Use lowercase 'hmac' as per official implementation
|
|
|
|
if (empty($signature)) {
|
|
Log::warning('OxaPay webhook validation failed: missing HMAC header');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Get webhook data to determine type
|
|
$data = $request->json()->all();
|
|
$type = $data['type'] ?? '';
|
|
|
|
// Resolve API key based on webhook type (following official implementation)
|
|
$apiKey = $this->resolveApiKeyByType($type);
|
|
|
|
if (empty($apiKey)) {
|
|
Log::warning('OxaPay webhook validation failed: no API key available for type', [
|
|
'type' => $type,
|
|
]);
|
|
|
|
return false;
|
|
}
|
|
|
|
$expectedSignature = hash_hmac('sha512', $payload, $apiKey);
|
|
|
|
$isValid = hash_equals($expectedSignature, $signature);
|
|
|
|
Log::info('OxaPay webhook validation', [
|
|
'type' => $type,
|
|
'signature_provided' => ! empty($signature),
|
|
'api_key_available' => ! empty($apiKey),
|
|
'valid' => $isValid,
|
|
]);
|
|
|
|
return $isValid;
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error('OxaPay webhook validation failed', [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve API key based on webhook type (following official OxaPay implementation)
|
|
*/
|
|
private function resolveApiKeyByType(string $type): string
|
|
{
|
|
// Map webhook types to API key groups per official implementation
|
|
$merchantTypes = ['invoice', 'white_label', 'static_address', 'payment_link', 'donation'];
|
|
$payoutTypes = ['payout'];
|
|
|
|
if (in_array($type, $merchantTypes, true)) {
|
|
return $this->config['merchant_api_key'] ?? '';
|
|
}
|
|
|
|
if (in_array($type, $payoutTypes, true)) {
|
|
return $this->config['payout_api_key'] ?? '';
|
|
}
|
|
|
|
// Default to merchant API key for unknown types
|
|
return $this->config['merchant_api_key'] ?? '';
|
|
}
|
|
|
|
/**
|
|
* Handle different webhook statuses
|
|
*/
|
|
protected function handleWebhookStatus(string $status, array $data): array
|
|
{
|
|
$trackId = $data['track_id'];
|
|
$orderId = $data['order_id'] ?? null;
|
|
|
|
switch ($status) {
|
|
case 'paying':
|
|
// Payment is being processed, show as pending
|
|
return $this->updatePaymentStatus($trackId, 'paying', $data);
|
|
|
|
case 'paid':
|
|
// Payment completed successfully
|
|
return $this->updatePaymentStatus($trackId, 'paid', $data);
|
|
|
|
case 'underpaid':
|
|
// Partial payment received
|
|
return $this->updatePaymentStatus($trackId, 'underpaid', $data);
|
|
|
|
case 'expired':
|
|
// Payment expired
|
|
return $this->updatePaymentStatus($trackId, 'expired', $data);
|
|
|
|
case 'refunded':
|
|
// Payment was refunded
|
|
return $this->updatePaymentStatus($trackId, 'refunded', $data);
|
|
|
|
case 'manual_accept':
|
|
// Manually accepted
|
|
return $this->updatePaymentStatus($trackId, 'manual_accept', $data);
|
|
|
|
default:
|
|
Log::warning('Unknown OxaPay webhook status', [
|
|
'status' => $status,
|
|
'track_id' => $trackId,
|
|
]);
|
|
|
|
return ['status' => 'unknown', 'message' => "Unknown status: {$status}"];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update payment status in local system
|
|
*/
|
|
protected function updatePaymentStatus(string $trackId, string $status, array $data): array
|
|
{
|
|
try {
|
|
// Find subscription by provider subscription ID (track_id)
|
|
$subscription = Subscription::where('provider', 'oxapay')
|
|
->where('provider_subscription_id', $trackId)
|
|
->first();
|
|
|
|
if (! $subscription) {
|
|
Log::warning('OxaPay webhook: Subscription not found', [
|
|
'track_id' => $trackId,
|
|
'status' => $status,
|
|
]);
|
|
|
|
return [
|
|
'status' => 'not_found',
|
|
'message' => 'Subscription not found for track_id: '.$trackId,
|
|
];
|
|
}
|
|
|
|
// Update subscription based on payment status
|
|
$updateData = [
|
|
'provider_data' => array_merge($subscription->provider_data ?? [], $data),
|
|
'last_provider_sync' => now(),
|
|
];
|
|
|
|
// Map OxaPay statuses to Laravel subscription statuses
|
|
switch ($status) {
|
|
case 'paying':
|
|
case 'underpaid':
|
|
$updateData['status'] = 'pending_payment';
|
|
$updateData['unified_status'] = 'pending_payment';
|
|
break;
|
|
|
|
case 'paid':
|
|
case 'manual_accept':
|
|
$updateData['status'] = 'active';
|
|
$updateData['unified_status'] = 'active';
|
|
$updateData['starts_at'] = now();
|
|
break;
|
|
|
|
case 'refunded':
|
|
$updateData['status'] = 'cancelled';
|
|
$updateData['unified_status'] = 'cancelled';
|
|
$updateData['cancelled_at'] = now();
|
|
break;
|
|
|
|
case 'expired':
|
|
$updateData['status'] = 'expired';
|
|
$updateData['unified_status'] = 'expired';
|
|
$updateData['ends_at'] = now();
|
|
break;
|
|
}
|
|
|
|
$subscription->update($updateData);
|
|
|
|
// Notify payment success for paid payments
|
|
if (in_array($status, ['paid', 'manual_accept'])) {
|
|
$this->notifyPaymentSuccess($subscription->user, $subscription->plan, [
|
|
'amount' => $data['amount'] ?? 0,
|
|
'currency' => $data['currency'] ?? 'USD',
|
|
'transaction_id' => $trackId,
|
|
]);
|
|
}
|
|
|
|
Log::info('OxaPay subscription updated', [
|
|
'subscription_id' => $subscription->id,
|
|
'track_id' => $trackId,
|
|
'status' => $status,
|
|
'new_status' => $updateData['status'],
|
|
]);
|
|
|
|
return [
|
|
'status' => 'updated',
|
|
'subscription_id' => $subscription->id,
|
|
'new_status' => $updateData['status'],
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error('OxaPay payment status update failed', [
|
|
'track_id' => $trackId,
|
|
'status' => $status,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return [
|
|
'status' => 'error',
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
public function getConfiguration(): array
|
|
{
|
|
return $this->config;
|
|
}
|
|
|
|
public function syncSubscriptionStatus(Subscription $subscription): array
|
|
{
|
|
try {
|
|
$trackId = $subscription->provider_subscription_id;
|
|
|
|
if (empty($trackId)) {
|
|
throw new \Exception('No track_id found for subscription');
|
|
}
|
|
|
|
Log::info('Syncing OxaPay subscription status', [
|
|
'subscription_id' => $subscription->id,
|
|
'track_id' => $trackId,
|
|
]);
|
|
|
|
$response = Http::withHeaders([
|
|
'merchant_api_key' => $this->merchantApiKey,
|
|
'Content-Type' => 'application/json',
|
|
])->get("{$this->baseUrl}/payment/{$trackId}");
|
|
|
|
if (! $response->successful()) {
|
|
throw new \Exception('Failed to fetch payment status from OxaPay');
|
|
}
|
|
|
|
$data = $response->json();
|
|
|
|
if (! isset($data['data'])) {
|
|
throw new \Exception('Invalid response from OxaPay API');
|
|
}
|
|
|
|
$paymentData = $data['data'];
|
|
$oxapayStatus = $paymentData['status'] ?? 'unknown';
|
|
|
|
// Map OxaPay status to Laravel subscription status
|
|
$statusMapping = [
|
|
'paying' => 'pending_payment',
|
|
'underpaid' => 'pending_payment',
|
|
'paid' => 'active',
|
|
'manual_accept' => 'active',
|
|
'expired' => 'expired',
|
|
'refunded' => 'cancelled',
|
|
];
|
|
|
|
$newStatus = $statusMapping[$oxapayStatus] ?? 'pending_payment';
|
|
|
|
// Update subscription with latest data
|
|
$updateData = [
|
|
'status' => $newStatus,
|
|
'unified_status' => $newStatus,
|
|
'provider_data' => array_merge($subscription->provider_data ?? [], $paymentData),
|
|
'last_provider_sync' => now(),
|
|
];
|
|
|
|
// Add timestamps based on status
|
|
switch ($newStatus) {
|
|
case 'active':
|
|
if (! $subscription->starts_at) {
|
|
$updateData['starts_at'] = now();
|
|
}
|
|
break;
|
|
case 'expired':
|
|
$updateData['ends_at'] = now();
|
|
break;
|
|
case 'cancelled':
|
|
$updateData['cancelled_at'] = now();
|
|
break;
|
|
}
|
|
|
|
$subscription->update($updateData);
|
|
|
|
Log::info('OxaPay subscription synced successfully', [
|
|
'subscription_id' => $subscription->id,
|
|
'track_id' => $trackId,
|
|
'oxapay_status' => $oxapayStatus,
|
|
'new_status' => $newStatus,
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'status' => $newStatus,
|
|
'oxapay_status' => $oxapayStatus,
|
|
'provider_data' => $paymentData,
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error('OxaPay subscription sync failed', [
|
|
'subscription_id' => $subscription->id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate subscription end date based on plan billing cycle days
|
|
*/
|
|
protected function calculateSubscriptionEndDate(Plan $plan): ?Carbon
|
|
{
|
|
try {
|
|
// Use billing_cycle_days from plans table
|
|
$billingCycleDays = $plan->billing_cycle_days ?? 30;
|
|
|
|
return now()->addDays($billingCycleDays);
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error('Failed to calculate subscription end date', [
|
|
'plan_id' => $plan->id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
// Safe fallback: 30 days
|
|
return now()->addDays(30);
|
|
}
|
|
}
|
|
|
|
public function getPaymentMethodDetails(string $paymentId): array
|
|
{
|
|
try {
|
|
$response = Http::withHeaders([
|
|
'merchant_api_key' => $this->merchantApiKey,
|
|
])->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->merchantApiKey,
|
|
])->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');
|
|
}
|
|
|
|
// Notification methods
|
|
protected function notifyPaymentSuccess(User $user, Plan $plan, array $paymentDetails): void
|
|
{
|
|
$message = "💰 PAYMENT SUCCESS\n".
|
|
"📧 Customer: {$user->name} ({$user->email})\n".
|
|
"📦 Plan: {$plan->name}\n".
|
|
'💵 Amount: $'.number_format($paymentDetails['amount'] ?? 0, 2).' '.($paymentDetails['currency'] ?? 'USD')."\n".
|
|
"🏪 Provider: OxaPay\n".
|
|
'🆔 Transaction: '.($paymentDetails['transaction_id'] ?? 'N/A')."\n".
|
|
'⏰ Time: '.now()->format('Y-m-d H:i:s');
|
|
|
|
$this->sendTelegramNotification($message);
|
|
}
|
|
|
|
protected function notifyWebhookReceived(string $eventType, ?string $webhookId): void
|
|
{
|
|
$message = "🔔 WEBHOOK RECEIVED\n".
|
|
"🏪 Provider: OxaPay\n".
|
|
"📋 Event: {$eventType}\n".
|
|
'🆔 Webhook ID: '.($webhookId ?? 'N/A')."\n".
|
|
"📊 Status: Processing...\n".
|
|
'⏰ Received: '.now()->format('Y-m-d H:i:s');
|
|
|
|
Log::warning($message);
|
|
|
|
$res = $this->sendTelegramNotification($message);
|
|
|
|
Log::warning('result : '.$res);
|
|
}
|
|
|
|
protected function notifyWebhookProcessed(string $eventType, ?string $webhookId, ?string $paymentId = null): void
|
|
{
|
|
$message = "✅ WEBHOOK PROCESSED\n".
|
|
"🏪 Provider: OxaPay\n".
|
|
"📋 Event: {$eventType}\n".
|
|
'🆔 Webhook ID: '.($webhookId ?? 'N/A')."\n".
|
|
($paymentId ? "💳 Payment ID: {$paymentId}\n" : '').
|
|
"📊 Status: Completed\n".
|
|
'⏰ Processed: '.now()->format('Y-m-d H:i:s');
|
|
|
|
$this->sendTelegramNotification($message);
|
|
}
|
|
|
|
protected function notifyWebhookError(string $eventType, ?string $webhookId, string $error): void
|
|
{
|
|
$message = "❌ WEBHOOK ERROR\n".
|
|
"🏪 Provider: OxaPay\n".
|
|
"📋 Event: {$eventType}\n".
|
|
'🆔 Webhook ID: '.($webhookId ?? 'N/A')."\n".
|
|
"💥 Error: {$error}\n".
|
|
'⏰ Time: '.now()->format('Y-m-d H:i:s');
|
|
|
|
$this->sendTelegramNotification($message);
|
|
}
|
|
|
|
protected function notifyProviderError(string $operation, string $error, array $context = []): void
|
|
{
|
|
$contextStr = '';
|
|
if (! empty($context)) {
|
|
$contextStr = '📝 Details: '.json_encode(array_slice($context, 0, 3, true), JSON_UNESCAPED_SLASHES)."\n";
|
|
if (count($context) > 3) {
|
|
$contextStr .= '📝 Additional: '.(count($context) - 3).' more items'."\n";
|
|
}
|
|
}
|
|
|
|
$message = "🚨 PROVIDER ERROR\n".
|
|
"🏪 Provider: OxaPay\n".
|
|
"📡 Operation: {$operation}\n".
|
|
"💥 Error: {$error}\n".
|
|
$contextStr.
|
|
'⏰ Time: '.now()->format('Y-m-d H:i:s');
|
|
|
|
$this->sendTelegramNotification($message);
|
|
}
|
|
|
|
protected function notifyPaymentCreated(User $user, Plan $plan, array $paymentDetails): void
|
|
{
|
|
$message = "🆕 PAYMENT CREATED\n".
|
|
"👤 User: {$user->name} ({$user->email})\n".
|
|
"📋 Plan: {$plan->name}\n".
|
|
'💰 Amount: $'.number_format($paymentDetails['amount'] ?? 0, 2).' '.($paymentDetails['currency'] ?? 'USD')."\n".
|
|
"🏪 Provider: OxaPay\n".
|
|
'🆔 Payment ID: '.($paymentDetails['payment_id'] ?? 'N/A')."\n".
|
|
'📅 Created: '.now()->format('Y-m-d H:i:s');
|
|
|
|
$this->sendTelegramNotification($message);
|
|
}
|
|
|
|
protected function notifySubscriptionCancelled(Subscription $subscription, string $reason): void
|
|
{
|
|
$user = $subscription->user;
|
|
$plan = $subscription->plan;
|
|
|
|
$message = "❌ SUBSCRIPTION CANCELLED\n".
|
|
"👤 User: {$user->name} ({$user->email})\n".
|
|
"📋 Plan: {$plan->name}\n".
|
|
"🏪 Provider: OxaPay\n".
|
|
"💭 Reason: {$reason}\n".
|
|
"🆔 Subscription ID: {$subscription->provider_subscription_id}\n".
|
|
($subscription->ends_at ? '📅 Effective: '.$subscription->ends_at->format('Y-m-d')."\n" : '').
|
|
'⏰ Cancelled: '.now()->format('Y-m-d H:i:s');
|
|
|
|
$this->sendTelegramNotification($message);
|
|
}
|
|
}
|