- 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
326 lines
9.4 KiB
PHP
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;
|
|
}
|
|
}
|