Files
zemailnator/app/Services/SubscriptionMigrationService.php
idevakk 27ac13948c 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
2025-11-19 09:37:00 -08:00

326 lines
9.4 KiB
PHP

<?php
namespace App\Services;
use App\Models\Plan;
use App\Models\Subscription;
use App\Models\SubscriptionChange;
use App\Services\Payments\PaymentOrchestrator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class SubscriptionMigrationService
{
public function __construct(
private PaymentOrchestrator $orchestrator
) {}
/**
* Migrate subscription to a new plan
*/
public function migrateToPlan(Subscription $subscription, Plan $newPlan, string $reason = ''): bool
{
try {
DB::beginTransaction();
$oldPlan = $subscription->plan;
$oldValues = [
'plan_id' => $subscription->plan_id,
'plan_name' => $oldPlan?->name,
'price' => $oldPlan?->price,
];
// Update subscription with new plan
$subscription->update([
'plan_id' => $newPlan->id,
'updated_at' => now(),
]);
// Record the change
SubscriptionChange::createRecord(
$subscription,
'plan_change',
"Migrated from {$oldPlan?->name} to {$newPlan->name}",
$oldValues,
[
'plan_id' => $newPlan->id,
'plan_name' => $newPlan->name,
'price' => $newPlan->price,
],
$reason
);
DB::commit();
Log::info('Subscription plan migration completed', [
'subscription_id' => $subscription->id,
'old_plan_id' => $oldPlan?->id,
'new_plan_id' => $newPlan->id,
'reason' => $reason,
]);
return true;
} catch (\Exception $e) {
DB::rollBack();
Log::error('Subscription plan migration failed', [
'subscription_id' => $subscription->id,
'new_plan_id' => $newPlan->id,
'error' => $e->getMessage(),
]);
return false;
}
}
/**
* Migrate subscription to a new provider
*/
public function migrateToProvider(Subscription $subscription, string $newProvider, array $providerData = []): bool
{
try {
DB::beginTransaction();
$oldProvider = $subscription->provider;
$oldValues = [
'provider' => $oldProvider,
'provider_subscription_id' => $subscription->provider_subscription_id,
];
// Cancel subscription with old provider if needed
if ($oldProvider && $subscription->isActive()) {
$this->orchestrator->cancelSubscription($subscription, 'Provider migration');
}
// Update subscription with new provider
$subscription->update([
'provider' => $newProvider,
'provider_subscription_id' => $providerData['subscription_id'] ?? null,
'provider_data' => array_merge($subscription->provider_data ?? [], $providerData),
'last_provider_sync' => now(),
]);
// Record the change
SubscriptionChange::createRecord(
$subscription,
'provider_change',
"Migrated from {$oldProvider} to {$newProvider}",
$oldValues,
[
'provider' => $newProvider,
'provider_subscription_id' => $providerData['subscription_id'] ?? null,
],
'Provider migration for better service'
);
DB::commit();
Log::info('Subscription provider migration completed', [
'subscription_id' => $subscription->id,
'old_provider' => $oldProvider,
'new_provider' => $newProvider,
]);
return true;
} catch (\Exception $e) {
DB::rollBack();
Log::error('Subscription provider migration failed', [
'subscription_id' => $subscription->id,
'new_provider' => $newProvider,
'error' => $e->getMessage(),
]);
return false;
}
}
/**
* Pause subscription
*/
public function pauseSubscription(Subscription $subscription, string $reason = ''): bool
{
try {
DB::beginTransaction();
$oldValues = [
'status' => $subscription->status,
'paused_at' => null,
];
// Update subscription status
$subscription->update([
'status' => 'paused',
'paused_at' => now(),
]);
// Record the change
SubscriptionChange::createRecord(
$subscription,
'pause',
'Subscription paused',
$oldValues,
[
'status' => 'paused',
'paused_at' => now()->format('Y-m-d H:i:s'),
],
$reason
);
DB::commit();
Log::info('Subscription paused', [
'subscription_id' => $subscription->id,
'reason' => $reason,
]);
return true;
} catch (\Exception $e) {
DB::rollBack();
Log::error('Failed to pause subscription', [
'subscription_id' => $subscription->id,
'error' => $e->getMessage(),
]);
return false;
}
}
/**
* Resume subscription
*/
public function resumeSubscription(Subscription $subscription, string $reason = ''): bool
{
try {
DB::beginTransaction();
$oldValues = [
'status' => $subscription->status,
'paused_at' => $subscription->paused_at,
'resumed_at' => null,
];
// Update subscription status
$subscription->update([
'status' => 'active',
'resumed_at' => now(),
]);
// Record the change
SubscriptionChange::createRecord(
$subscription,
'resume',
'Subscription resumed',
$oldValues,
[
'status' => 'active',
'resumed_at' => now()->format('Y-m-d H:i:s'),
],
$reason
);
DB::commit();
Log::info('Subscription resumed', [
'subscription_id' => $subscription->id,
'reason' => $reason,
]);
return true;
} catch (\Exception $e) {
DB::rollBack();
Log::error('Failed to resume subscription', [
'subscription_id' => $subscription->id,
'error' => $e->getMessage(),
]);
return false;
}
}
/**
* Bulk migrate subscriptions
*/
public function bulkMigrate(array $subscriptionIds, callable $migrationCallback): array
{
$results = [
'success' => 0,
'failed' => 0,
'errors' => [],
];
foreach ($subscriptionIds as $subscriptionId) {
try {
$subscription = Subscription::findOrFail($subscriptionId);
if ($migrationCallback($subscription)) {
$results['success']++;
} else {
$results['failed']++;
$results['errors'][] = "Migration failed for subscription {$subscriptionId}";
}
} catch (\Exception $e) {
$results['failed']++;
$results['errors'][] = "Error processing subscription {$subscriptionId}: {$e->getMessage()}";
}
}
return $results;
}
/**
* Get migration history for a subscription
*/
public function getMigrationHistory(Subscription $subscription): \Illuminate\Database\Eloquent\Collection
{
return $subscription->subscriptionChanges()
->whereIn('change_type', ['plan_change', 'provider_change', 'migration'])
->orderBy('effective_at', 'desc')
->get();
}
/**
* Get pending migrations
*/
public function getPendingMigrations(): \Illuminate\Database\Eloquent\Collection
{
return SubscriptionChange::query()
->whereIn('change_type', ['plan_change', 'provider_change', 'migration'])
->pending()
->with(['subscription', 'user'])
->orderBy('effective_at', 'asc')
->get();
}
/**
* Process pending migrations
*/
public function processPendingMigrations(): int
{
$pending = $this->getPendingMigrations();
$processedCount = 0;
foreach ($pending as $change) {
try {
// Here you would implement the actual migration logic
// based on the change type and data
$change->markAsProcessed();
$processedCount++;
} catch (\Exception $e) {
Log::error('Failed to process pending migration', [
'change_id' => $change->id,
'error' => $e->getMessage(),
]);
}
}
return $processedCount;
}
}