feat(payment): implement OxaPay provider and non-recurring subscription sync

- Add comprehensive OxaPay payment provider with invoice creation, webhook processing, and subscription status sync
  - Implement conditional payload fields (to_currency, callback_url) based on configuration
  - Create universal sync command for all non-recurring payment providers
  - Add subscription model fields for payment tracking
  - Implement proper status mapping between OxaPay and Laravel subscription states
  - Add webhook signature validation using HMAC SHA512
This commit is contained in:
idevakk
2025-12-07 08:20:51 -08:00
parent 5fabec1f9d
commit 9a32511e97
5 changed files with 628 additions and 60 deletions

View File

@@ -0,0 +1,142 @@
<?php
namespace App\Console\Commands\Payment;
use App\Models\PaymentProvider;
use App\Models\Subscription;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class SyncNonRecurringSubscriptions extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'payment:sync-non-recurring-subscriptions {--dry-run}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sync status of subscriptions for non-recurring payment providers (activation_key, oxapay, etc.) based on ends_at timestamp';
/**
* Execute the console command.
*/
public function handle()
{
$dryRun = $this->option('dry-run');
$this->info('🔄 Syncing non-recurring payment provider subscriptions...');
if ($dryRun) {
$this->warn('📋 DRY RUN MODE - No actual updates will be made');
}
// Get all active non-recurring payment providers
$nonRecurringProviders = PaymentProvider::where('is_active', true)
->where('supports_recurring', false)
->where('supports_one_time', true)
->pluck('name')
->toArray();
$this->info('📋 Non-recurring providers found: '.implode(', ', $nonRecurringProviders));
$totalProcessed = 0;
$totalExpired = 0;
foreach ($nonRecurringProviders as $providerName) {
$this->info("🔍 Processing provider: {$providerName}");
$result = $this->syncProviderSubscriptions($providerName, $dryRun);
$totalProcessed += $result['processed'];
$totalExpired += $result['expired'];
}
$this->info('✅ Sync completed!');
$this->info("📊 Total subscriptions processed: {$totalProcessed}");
$this->info("⏰ Total subscriptions expired: {$totalExpired}");
if (! $dryRun && $totalExpired > 0) {
$this->info('💡 Tip: Set up a cron job to run this command every few hours:');
$this->info(' */4 * * * * php artisan app:sync-non-recurring-subscriptions');
}
}
/**
* Sync subscriptions for a specific provider
*/
protected function syncProviderSubscriptions(string $providerName, bool $dryRun): array
{
$query = Subscription::where('provider', $providerName)
->where('status', '!=', 'expired')
->where('status', '!=', 'cancelled')
->whereNotNull('ends_at');
if ($dryRun) {
$subscriptions = $query->get();
$expiredCount = $subscriptions->filter(function ($sub) {
return $sub->ends_at->isPast();
})->count();
$this->line(" 📊 Found {$subscriptions->count()} subscriptions");
$this->line(" ⏰ Would expire {$expiredCount} subscriptions");
return [
'processed' => $subscriptions->count(),
'expired' => $expiredCount,
];
}
// Get subscriptions that should be expired
$expiredSubscriptions = $query->where('ends_at', '<', now())->get();
$expiredCount = 0;
foreach ($expiredSubscriptions as $subscription) {
try {
$subscription->update([
'status' => 'expired',
'unified_status' => 'expired',
'updated_at' => now(),
]);
$expiredCount++;
$this->line(" ✅ Expired subscription #{$subscription->id} (User: {$subscription->user_id})");
Log::info('Non-recurring subscription expired via sync command', [
'subscription_id' => $subscription->id,
'provider' => $providerName,
'user_id' => $subscription->user_id,
'ends_at' => $subscription->ends_at,
'command' => 'app:sync-non-recurring-subscriptions',
]);
} catch (\Exception $e) {
$this->error(" ❌ Failed to expire subscription #{$subscription->id}: {$e->getMessage()}");
Log::error('Failed to expire non-recurring subscription', [
'subscription_id' => $subscription->id,
'provider' => $providerName,
'error' => $e->getMessage(),
]);
}
}
$totalProcessed = $query->count();
if ($expiredCount > 0) {
$this->info(" ✅ Expired {$expiredCount} subscriptions for {$providerName}");
} else {
$this->info(" No expired subscriptions found for {$providerName}");
}
return [
'processed' => $totalProcessed,
'expired' => $expiredCount,
];
}
}