🐛 PROBLEMA IDENTIFICADO: - Cliente pagava 1 assinatura mas recebia 2 faturas - Fatura 1: subscription_create (método confirm()) - Fatura 2: subscription_cycle (webhook PAYMENT.SALE.COMPLETED) - Ambas criadas com 8 segundos de diferença ✅ SOLUÇÃO IMPLEMENTADA: handlePaymentCompleted() agora verifica: 1. Se já existe fatura com mesmo paypal_payment_id - Evita duplicação por webhook reprocessado 2. Se já existe fatura paga HOJE para esta subscription - Evita duplicação do pagamento inicial - Webhook vem depois do confirm() 3. Só cria nova fatura se for pagamento recorrente genuíno 📊 RESULTADO: - Pagamento inicial: 1 fatura (subscription_create) - Renovação mensal/anual: 1 fatura (subscription_cycle) - Webhooks duplicados: ignorados com log 🗑️ LIMPEZA: - Removida fatura duplicada WM-2025-000002 do user_id 35
1298 lines
48 KiB
PHP
Executable File
1298 lines
48 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Plan;
|
|
use App\Models\Subscription;
|
|
use App\Models\Invoice;
|
|
use App\Services\PayPalService;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\JsonResponse;
|
|
use App\Models\EmailVerificationToken;
|
|
use App\Mail\AccountActivationMail;
|
|
use App\Mail\SubscriptionCancelledMail;
|
|
use App\Mail\CancellationRetentionOfferMail;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Carbon\Carbon;
|
|
|
|
class SubscriptionController extends Controller
|
|
{
|
|
protected PayPalService $paypal;
|
|
|
|
public function __construct(PayPalService $paypal)
|
|
{
|
|
$this->paypal = $paypal;
|
|
}
|
|
|
|
/**
|
|
* Get current subscription status
|
|
*/
|
|
public function status(Request $request): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
$subscription = $user->subscriptions()->active()->with('plan')->first();
|
|
$currentPlan = $user->currentPlan();
|
|
|
|
// Get current usage
|
|
$usage = [
|
|
'accounts' => $user->accounts()->count(),
|
|
'categories' => $user->categories()->count(),
|
|
'budgets' => $user->budgets()->count(),
|
|
'transactions' => $user->transactions()->count(),
|
|
];
|
|
|
|
// Calculate usage percentages if plan has limits
|
|
$limits = $currentPlan?->limits ?? [];
|
|
$usagePercentages = [];
|
|
foreach ($usage as $resource => $count) {
|
|
$limit = $limits[$resource] ?? null;
|
|
if ($limit !== null && $limit > 0) {
|
|
$usagePercentages[$resource] = round(($count / $limit) * 100, 1);
|
|
} else {
|
|
$usagePercentages[$resource] = null; // unlimited
|
|
}
|
|
}
|
|
|
|
// Calculate guarantee period info (7 days from subscription creation)
|
|
$withinGuaranteePeriod = false;
|
|
$guaranteeDaysRemaining = 0;
|
|
$guaranteeEndsAt = null;
|
|
|
|
if ($subscription && $subscription->created_at) {
|
|
$guaranteeEndsAt = $subscription->created_at->copy()->addDays(7);
|
|
$withinGuaranteePeriod = now()->lt($guaranteeEndsAt);
|
|
$guaranteeDaysRemaining = $withinGuaranteePeriod
|
|
? (int) ceil(now()->diffInHours($guaranteeEndsAt) / 24)
|
|
: 0;
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'has_subscription' => $subscription !== null,
|
|
'subscription' => $subscription ? [
|
|
'id' => $subscription->id,
|
|
'status' => $subscription->status,
|
|
'trial_ends_at' => $subscription->trial_ends_at,
|
|
'current_period_start' => $subscription->current_period_start,
|
|
'current_period_end' => $subscription->current_period_end,
|
|
'canceled_at' => $subscription->canceled_at,
|
|
'ends_at' => $subscription->ends_at,
|
|
'on_trial' => $subscription->isOnTrial(),
|
|
'on_grace_period' => $subscription->onGracePeriod(),
|
|
] : null,
|
|
'on_trial' => $subscription?->isOnTrial() ?? false,
|
|
'trial_ends_at' => $subscription?->trial_ends_at,
|
|
'days_until_trial_ends' => $subscription?->days_until_trial_ends,
|
|
'current_period_start' => $subscription?->current_period_start,
|
|
'current_period_end' => $subscription?->current_period_end,
|
|
'status' => $subscription?->status,
|
|
'status_label' => $subscription?->status_label,
|
|
'canceled_at' => $subscription?->canceled_at,
|
|
'on_grace_period' => $subscription?->onGracePeriod() ?? false,
|
|
'within_guarantee_period' => $withinGuaranteePeriod,
|
|
'guarantee_days_remaining' => $guaranteeDaysRemaining,
|
|
'guarantee_ends_at' => $guaranteeEndsAt?->toIso8601String(),
|
|
'plan' => $currentPlan ? [
|
|
'id' => $currentPlan->id,
|
|
'slug' => $currentPlan->slug,
|
|
'name' => $currentPlan->name,
|
|
'price' => $currentPlan->price,
|
|
'formatted_price' => $currentPlan->formatted_price,
|
|
'billing_period' => $currentPlan->billing_period,
|
|
'is_free' => $currentPlan->is_free,
|
|
'features' => $currentPlan->features,
|
|
'limits' => $currentPlan->limits,
|
|
] : null,
|
|
'usage' => $usage,
|
|
'usage_percentages' => $usagePercentages,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Create a subscription checkout
|
|
*/
|
|
public function subscribe(Request $request): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'plan_slug' => 'required|string|exists:plans,slug',
|
|
]);
|
|
|
|
$user = $request->user();
|
|
$plan = Plan::where('slug', $request->plan_slug)->where('is_active', true)->first();
|
|
|
|
if (!$plan) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Plan not found or inactive',
|
|
], 404);
|
|
}
|
|
|
|
// Check if plan is free
|
|
if ($plan->is_free) {
|
|
return $this->subscribeFree($user, $plan);
|
|
}
|
|
|
|
// Check if PayPal is configured
|
|
if (!$this->paypal->isConfigured()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Payment gateway not configured',
|
|
], 500);
|
|
}
|
|
|
|
// Check if plan has PayPal plan ID
|
|
if (!$plan->paypal_plan_id) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Plan not configured for payments yet',
|
|
], 500);
|
|
}
|
|
|
|
// Create PayPal subscription
|
|
$frontendUrl = config('app.frontend_url', 'https://webmoney.cnxifly.com');
|
|
$returnUrl = "{$frontendUrl}/billing?success=true&plan={$plan->slug}";
|
|
$cancelUrl = "{$frontendUrl}/pricing?canceled=true";
|
|
|
|
$paypalSubscription = $this->paypal->createSubscription($plan, $returnUrl, $cancelUrl);
|
|
|
|
if (!$paypalSubscription) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Failed to create subscription',
|
|
], 500);
|
|
}
|
|
|
|
// Find approve link
|
|
$approveUrl = collect($paypalSubscription['links'] ?? [])
|
|
->firstWhere('rel', 'approve')['href'] ?? null;
|
|
|
|
if (!$approveUrl) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'No approval URL received',
|
|
], 500);
|
|
}
|
|
|
|
// Create pending subscription in our DB
|
|
$subscription = Subscription::create([
|
|
'user_id' => $user->id,
|
|
'plan_id' => $plan->id,
|
|
'status' => Subscription::STATUS_TRIALING,
|
|
'paypal_subscription_id' => $paypalSubscription['id'],
|
|
'paypal_status' => $paypalSubscription['status'],
|
|
'paypal_data' => $paypalSubscription,
|
|
'price_paid' => $plan->price,
|
|
'currency' => $plan->currency,
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'subscription_id' => $subscription->id,
|
|
'paypal_subscription_id' => $paypalSubscription['id'],
|
|
'approve_url' => $approveUrl,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Start subscription for newly registered user (public - no auth required)
|
|
* Used immediately after registration, before user is logged in
|
|
*/
|
|
public function startSubscription(Request $request): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'plan_id' => 'required|integer|exists:plans,id',
|
|
'user_email' => 'required|email|exists:users,email',
|
|
]);
|
|
|
|
$user = \App\Models\User::where('email', $request->user_email)->first();
|
|
|
|
if (!$user) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'User not found',
|
|
], 404);
|
|
}
|
|
|
|
// Verify user hasn't already verified email (prevent abuse)
|
|
if ($user->email_verified_at) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'User already activated. Please login.',
|
|
], 400);
|
|
}
|
|
|
|
$plan = Plan::where('id', $request->plan_id)->where('is_active', true)->first();
|
|
|
|
if (!$plan) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Plan not found or inactive',
|
|
], 404);
|
|
}
|
|
|
|
// All plans are paid now - no free subscriptions during registration
|
|
if ($plan->is_free || $plan->price <= 0) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'All plans require payment',
|
|
], 400);
|
|
}
|
|
|
|
// Check if PayPal is configured
|
|
if (!$this->paypal->isConfigured()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Payment gateway not configured',
|
|
], 500);
|
|
}
|
|
|
|
// Check if plan has PayPal plan ID
|
|
if (!$plan->paypal_plan_id) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Plan not configured for payments yet',
|
|
], 500);
|
|
}
|
|
|
|
// Create PayPal subscription
|
|
$frontendUrl = config('app.frontend_url', 'https://webmoney.cnxifly.com');
|
|
$returnUrl = "{$frontendUrl}/payment-success?user_email={$user->email}&plan={$plan->slug}";
|
|
$cancelUrl = "{$frontendUrl}/register?payment_canceled=true";
|
|
|
|
$paypalSubscription = $this->paypal->createSubscription($plan, $returnUrl, $cancelUrl);
|
|
|
|
if (!$paypalSubscription) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Failed to create subscription',
|
|
], 500);
|
|
}
|
|
|
|
// Find approve link
|
|
$approveUrl = collect($paypalSubscription['links'] ?? [])
|
|
->firstWhere('rel', 'approve')['href'] ?? null;
|
|
|
|
if (!$approveUrl) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'No approval URL received',
|
|
], 500);
|
|
}
|
|
|
|
// Create pending subscription in our DB
|
|
$subscription = Subscription::create([
|
|
'user_id' => $user->id,
|
|
'plan_id' => $plan->id,
|
|
'status' => Subscription::STATUS_PENDING,
|
|
'paypal_subscription_id' => $paypalSubscription['id'],
|
|
'paypal_status' => $paypalSubscription['status'],
|
|
'paypal_data' => $paypalSubscription,
|
|
'price_paid' => $plan->price,
|
|
'currency' => $plan->currency,
|
|
]);
|
|
|
|
\Illuminate\Support\Facades\Log::info("Started subscription for user {$user->email}, PayPal ID: {$paypalSubscription['id']}");
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'subscription_id' => $subscription->id,
|
|
'paypal_subscription_id' => $paypalSubscription['id'],
|
|
'approve_url' => $approveUrl,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Subscribe to free plan
|
|
*/
|
|
private function subscribeFree($user, Plan $plan): JsonResponse
|
|
{
|
|
// Cancel any existing subscriptions
|
|
$user->subscriptions()->active()->update([
|
|
'status' => Subscription::STATUS_CANCELED,
|
|
'canceled_at' => now(),
|
|
'ends_at' => now(),
|
|
'cancel_reason' => 'Downgraded to free plan',
|
|
]);
|
|
|
|
// Create free subscription
|
|
$subscription = Subscription::createForUser($user, $plan);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Subscribed to free plan',
|
|
'data' => [
|
|
'subscription_id' => $subscription->id,
|
|
'status' => $subscription->status,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Confirm subscription after PayPal approval
|
|
*/
|
|
public function confirm(Request $request): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'subscription_id' => 'required|string',
|
|
]);
|
|
|
|
$user = $request->user();
|
|
$subscription = Subscription::where('paypal_subscription_id', $request->subscription_id)
|
|
->where('user_id', $user->id)
|
|
->first();
|
|
|
|
if (!$subscription) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Subscription not found',
|
|
], 404);
|
|
}
|
|
|
|
// Get subscription details from PayPal
|
|
$paypalData = $this->paypal->getSubscription($request->subscription_id);
|
|
|
|
if (!$paypalData) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Failed to verify subscription',
|
|
], 500);
|
|
}
|
|
|
|
// Update subscription based on PayPal status
|
|
$this->updateSubscriptionFromPayPal($subscription, $paypalData);
|
|
|
|
// Cancel other active subscriptions
|
|
$user->subscriptions()
|
|
->where('id', '!=', $subscription->id)
|
|
->active()
|
|
->update([
|
|
'status' => Subscription::STATUS_CANCELED,
|
|
'canceled_at' => now(),
|
|
'ends_at' => now(),
|
|
'cancel_reason' => 'Replaced by new subscription',
|
|
]);
|
|
|
|
// Create invoice for the subscription
|
|
if ($subscription->isActive() && !$subscription->plan->is_free) {
|
|
Invoice::createForSubscription(
|
|
$subscription,
|
|
Invoice::REASON_SUBSCRIPTION_CREATE,
|
|
"{$subscription->plan->name} - Nueva suscripción"
|
|
)->markAsPaid($paypalData['id'] ?? null);
|
|
}
|
|
|
|
// Send activation email if subscription is active and user not verified yet
|
|
$activationSent = false;
|
|
if ($subscription->isActive() && !$user->email_verified_at) {
|
|
try {
|
|
$verificationToken = EmailVerificationToken::createForUser($user);
|
|
$frontendUrl = config('app.frontend_url', 'https://webmoney.cnxifly.com');
|
|
$activationUrl = "{$frontendUrl}/activate?token={$verificationToken->token}";
|
|
|
|
Mail::to($user->email)->send(new AccountActivationMail(
|
|
$user,
|
|
$activationUrl,
|
|
$subscription->plan->name
|
|
));
|
|
|
|
$activationSent = true;
|
|
Log::info("Activation email sent to {$user->email}");
|
|
} catch (\Exception $e) {
|
|
Log::error("Failed to send activation email: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => $activationSent
|
|
? 'Suscripción confirmada. Revisa tu email para activar tu cuenta.'
|
|
: 'Subscription confirmed',
|
|
'data' => [
|
|
'status' => $subscription->status,
|
|
'status_label' => $subscription->status_label,
|
|
'plan' => $subscription->plan->name,
|
|
'activation_email_sent' => $activationSent,
|
|
'email_verified' => $user->email_verified_at !== null,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Confirm subscription after PayPal approval (public - no auth required)
|
|
* Used for new user registration flow
|
|
*/
|
|
public function confirmPublic(Request $request): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'subscription_id' => 'required|string',
|
|
'user_email' => 'required|email',
|
|
]);
|
|
|
|
$user = \App\Models\User::where('email', $request->user_email)->first();
|
|
|
|
if (!$user) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'User not found',
|
|
], 404);
|
|
}
|
|
|
|
$subscription = Subscription::where('paypal_subscription_id', $request->subscription_id)
|
|
->where('user_id', $user->id)
|
|
->first();
|
|
|
|
if (!$subscription) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Subscription not found',
|
|
], 404);
|
|
}
|
|
|
|
// Get subscription details from PayPal
|
|
$paypalData = $this->paypal->getSubscription($request->subscription_id);
|
|
|
|
if (!$paypalData) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Failed to verify subscription with PayPal',
|
|
], 500);
|
|
}
|
|
|
|
\Illuminate\Support\Facades\Log::info("PayPal confirmation for {$user->email}: " . json_encode($paypalData));
|
|
|
|
// Update subscription based on PayPal status
|
|
$this->updateSubscriptionFromPayPal($subscription, $paypalData);
|
|
|
|
// Cancel other active subscriptions
|
|
$user->subscriptions()
|
|
->where('id', '!=', $subscription->id)
|
|
->active()
|
|
->update([
|
|
'status' => Subscription::STATUS_CANCELED,
|
|
'canceled_at' => now(),
|
|
'ends_at' => now(),
|
|
'cancel_reason' => 'Replaced by new subscription',
|
|
]);
|
|
|
|
// Create invoice for the subscription
|
|
if ($subscription->isActive() && !$subscription->plan->is_free) {
|
|
Invoice::createForSubscription(
|
|
$subscription,
|
|
Invoice::REASON_SUBSCRIPTION_CREATE,
|
|
"{$subscription->plan->name} - Nueva suscripción"
|
|
)->markAsPaid($paypalData['id'] ?? null);
|
|
}
|
|
|
|
// Send activation email if subscription is active and user not verified yet
|
|
$activationSent = false;
|
|
if ($subscription->isActive() && !$user->email_verified_at) {
|
|
try {
|
|
$verificationToken = EmailVerificationToken::createForUser($user);
|
|
$frontendUrl = config('app.frontend_url', 'https://webmoney.cnxifly.com');
|
|
$activationUrl = "{$frontendUrl}/activate?token={$verificationToken->token}";
|
|
|
|
Mail::to($user->email)->send(new AccountActivationMail(
|
|
$user,
|
|
$activationUrl,
|
|
$subscription->plan->name
|
|
));
|
|
|
|
$activationSent = true;
|
|
\Illuminate\Support\Facades\Log::info("Activation email sent to {$user->email}");
|
|
} catch (\Exception $e) {
|
|
\Illuminate\Support\Facades\Log::error("Failed to send activation email: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => $activationSent
|
|
? 'Pagamento confirmado! Verifique seu email para ativar sua conta.'
|
|
: 'Payment confirmed',
|
|
'data' => [
|
|
'status' => $subscription->status,
|
|
'status_label' => $subscription->status_label,
|
|
'plan' => $subscription->plan->name,
|
|
'activation_email_sent' => $activationSent,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Cancel subscription
|
|
*/
|
|
public function cancel(Request $request): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'reason' => 'nullable|string|max:500',
|
|
'immediately' => 'nullable|boolean',
|
|
'request_refund' => 'nullable|boolean',
|
|
]);
|
|
|
|
$user = $request->user();
|
|
$subscription = $user->subscriptions()->active()->first();
|
|
|
|
if (!$subscription) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'No active subscription found',
|
|
], 404);
|
|
}
|
|
|
|
$refundResult = null;
|
|
$isWithinGuaranteePeriod = false;
|
|
|
|
// Check if within 7-day guarantee period (from subscription creation date)
|
|
if ($subscription->created_at) {
|
|
$daysSinceCreation = now()->diffInDays($subscription->created_at);
|
|
$isWithinGuaranteePeriod = $daysSinceCreation <= 7;
|
|
}
|
|
|
|
// If it's a paid plan with PayPal subscription
|
|
if ($subscription->paypal_subscription_id && !$subscription->plan->is_free) {
|
|
|
|
// If user requests refund and is within guarantee period, cancel and refund
|
|
if ($request->boolean('request_refund') && $isWithinGuaranteePeriod) {
|
|
$refundResult = $this->paypal->cancelAndRefund(
|
|
$subscription->paypal_subscription_id,
|
|
$request->reason ?? 'Refund within 7-day guarantee period'
|
|
);
|
|
|
|
if (!$refundResult['canceled']) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Failed to cancel subscription on PayPal',
|
|
], 500);
|
|
}
|
|
|
|
Log::info('Subscription canceled with refund', [
|
|
'user_id' => $user->id,
|
|
'subscription_id' => $subscription->id,
|
|
'refund_result' => $refundResult,
|
|
]);
|
|
} else {
|
|
// Just cancel without refund
|
|
$canceled = $this->paypal->cancelSubscription(
|
|
$subscription->paypal_subscription_id,
|
|
$request->reason ?? 'User requested cancellation'
|
|
);
|
|
|
|
if (!$canceled) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Failed to cancel subscription on PayPal',
|
|
], 500);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cancel in our DB - always immediately when refunded
|
|
$cancelImmediately = $request->boolean('request_refund') || $request->boolean('immediately', false);
|
|
$subscription->cancel(
|
|
$request->reason,
|
|
$cancelImmediately
|
|
);
|
|
|
|
// Send cancellation confirmation email
|
|
try {
|
|
$wasRefunded = $refundResult && $refundResult['refunded'];
|
|
$refundAmount = $wasRefunded && isset($refundResult['refund_amount'])
|
|
? number_format($refundResult['refund_amount'], 2) . ' ' . ($subscription->plan->currency ?? 'EUR')
|
|
: null;
|
|
|
|
Mail::to($user->email)->send(new SubscriptionCancelledMail(
|
|
$user,
|
|
$subscription->plan->name,
|
|
$wasRefunded,
|
|
$refundAmount
|
|
));
|
|
|
|
Log::info('Cancellation email sent', [
|
|
'user_id' => $user->id,
|
|
'email' => $user->email,
|
|
'refunded' => $wasRefunded,
|
|
]);
|
|
} catch (\Exception $e) {
|
|
Log::error('Failed to send cancellation email', [
|
|
'user_id' => $user->id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
// Don't fail the cancellation just because email failed
|
|
}
|
|
|
|
// Build response message
|
|
$message = 'Subscription canceled';
|
|
if ($refundResult && $refundResult['refunded']) {
|
|
$message = 'Subscription canceled and refund processed';
|
|
} elseif ($refundResult && !$refundResult['refunded']) {
|
|
$message = 'Subscription canceled. Refund could not be processed automatically - please contact support.';
|
|
} elseif ($cancelImmediately) {
|
|
$message = 'Subscription canceled immediately';
|
|
} else {
|
|
$message = 'Subscription will be canceled at period end';
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => $message,
|
|
'data' => [
|
|
'status' => $subscription->status,
|
|
'ends_at' => $subscription->ends_at,
|
|
'refunded' => $refundResult['refunded'] ?? false,
|
|
'refund_id' => $refundResult['refund_id'] ?? null,
|
|
'within_guarantee_period' => $isWithinGuaranteePeriod,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get invoices
|
|
*/
|
|
public function invoices(Request $request): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
$invoices = $user->invoices()
|
|
->with('subscription.plan')
|
|
->recent()
|
|
->paginate(20);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'invoices' => $invoices->map(function ($invoice) {
|
|
return [
|
|
'id' => $invoice->id,
|
|
'number' => $invoice->number,
|
|
'status' => $invoice->status,
|
|
'status_label' => $invoice->status_label,
|
|
'status_color' => $invoice->status_color,
|
|
'description' => $invoice->description,
|
|
'subtotal' => $invoice->subtotal,
|
|
'tax' => $invoice->tax,
|
|
'tax_percent' => $invoice->tax_percent,
|
|
'total' => $invoice->total,
|
|
'formatted_total' => $invoice->formatted_total,
|
|
'currency' => $invoice->currency,
|
|
'billing_reason' => $invoice->billing_reason,
|
|
'billing_reason_label' => $invoice->billing_reason_label,
|
|
'paid_at' => $invoice->paid_at,
|
|
'created_at' => $invoice->created_at,
|
|
'plan_name' => $invoice->subscription?->plan?->name,
|
|
];
|
|
}),
|
|
'pagination' => [
|
|
'current_page' => $invoices->currentPage(),
|
|
'last_page' => $invoices->lastPage(),
|
|
'per_page' => $invoices->perPage(),
|
|
'total' => $invoices->total(),
|
|
],
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get PayPal client ID for frontend
|
|
*/
|
|
public function paypalConfig(): JsonResponse
|
|
{
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'client_id' => $this->paypal->getClientId(),
|
|
'sandbox' => $this->paypal->isSandbox(),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* PayPal webhook handler
|
|
*/
|
|
public function webhook(Request $request): JsonResponse
|
|
{
|
|
// Log all incoming webhooks for debugging
|
|
Log::channel('single')->info('=== PAYPAL WEBHOOK RECEIVED ===', [
|
|
'event_type' => $request->input('event_type'),
|
|
'content' => $request->all(),
|
|
]);
|
|
|
|
$webhookId = config('services.paypal.webhook_id');
|
|
|
|
// Verify webhook signature (skip in sandbox for testing)
|
|
if (!$this->paypal->isSandbox() && $webhookId) {
|
|
$verified = $this->paypal->verifyWebhookSignature(
|
|
$request->headers->all(),
|
|
$request->getContent(),
|
|
$webhookId
|
|
);
|
|
|
|
if (!$verified) {
|
|
Log::warning('PayPal webhook signature verification failed');
|
|
return response()->json(['status' => 'signature_invalid'], 400);
|
|
}
|
|
}
|
|
|
|
$event = $request->all();
|
|
$eventType = $event['event_type'] ?? '';
|
|
$resource = $event['resource'] ?? [];
|
|
|
|
Log::info('PayPal webhook received', ['event_type' => $eventType]);
|
|
|
|
switch ($eventType) {
|
|
case 'BILLING.SUBSCRIPTION.ACTIVATED':
|
|
$this->handleSubscriptionActivated($resource);
|
|
break;
|
|
|
|
case 'BILLING.SUBSCRIPTION.CANCELLED':
|
|
$this->handleSubscriptionCancelled($resource);
|
|
break;
|
|
|
|
case 'BILLING.SUBSCRIPTION.EXPIRED':
|
|
$this->handleSubscriptionExpired($resource);
|
|
break;
|
|
|
|
case 'BILLING.SUBSCRIPTION.SUSPENDED':
|
|
$this->handleSubscriptionSuspended($resource);
|
|
break;
|
|
|
|
case 'PAYMENT.SALE.COMPLETED':
|
|
$this->handlePaymentCompleted($resource);
|
|
break;
|
|
|
|
case 'PAYMENT.SALE.DENIED':
|
|
case 'PAYMENT.SALE.REFUNDED':
|
|
$this->handlePaymentFailed($resource);
|
|
break;
|
|
|
|
default:
|
|
Log::info('Unhandled PayPal webhook event', ['event_type' => $eventType]);
|
|
}
|
|
|
|
return response()->json(['status' => 'ok']);
|
|
}
|
|
|
|
// ==================== WEBHOOK HANDLERS ====================
|
|
|
|
private function handleSubscriptionActivated(array $resource): void
|
|
{
|
|
$subscription = Subscription::where('paypal_subscription_id', $resource['id'])->first();
|
|
if (!$subscription) return;
|
|
|
|
$this->updateSubscriptionFromPayPal($subscription, $resource);
|
|
|
|
Log::info('Subscription activated', ['subscription_id' => $subscription->id]);
|
|
}
|
|
|
|
private function handleSubscriptionCancelled(array $resource): void
|
|
{
|
|
$subscription = Subscription::where('paypal_subscription_id', $resource['id'])->first();
|
|
if (!$subscription) return;
|
|
|
|
$subscription->update([
|
|
'status' => Subscription::STATUS_CANCELED,
|
|
'canceled_at' => now(),
|
|
'paypal_status' => $resource['status'] ?? 'CANCELLED',
|
|
'paypal_data' => $resource,
|
|
]);
|
|
|
|
Log::info('Subscription cancelled via webhook', ['subscription_id' => $subscription->id]);
|
|
}
|
|
|
|
private function handleSubscriptionExpired(array $resource): void
|
|
{
|
|
$subscription = Subscription::where('paypal_subscription_id', $resource['id'])->first();
|
|
if (!$subscription) return;
|
|
|
|
$subscription->markAsExpired();
|
|
$subscription->update([
|
|
'paypal_status' => $resource['status'] ?? 'EXPIRED',
|
|
'paypal_data' => $resource,
|
|
]);
|
|
|
|
Log::info('Subscription expired via webhook', ['subscription_id' => $subscription->id]);
|
|
}
|
|
|
|
private function handleSubscriptionSuspended(array $resource): void
|
|
{
|
|
$subscription = Subscription::where('paypal_subscription_id', $resource['id'])->first();
|
|
if (!$subscription) return;
|
|
|
|
$subscription->update([
|
|
'status' => Subscription::STATUS_PAST_DUE,
|
|
'paypal_status' => $resource['status'] ?? 'SUSPENDED',
|
|
'paypal_data' => $resource,
|
|
]);
|
|
|
|
Log::info('Subscription suspended via webhook', ['subscription_id' => $subscription->id]);
|
|
}
|
|
|
|
private function handlePaymentCompleted(array $resource): void
|
|
{
|
|
$billingAgreementId = $resource['billing_agreement_id'] ?? null;
|
|
if (!$billingAgreementId) return;
|
|
|
|
$subscription = Subscription::where('paypal_subscription_id', $billingAgreementId)->first();
|
|
if (!$subscription) return;
|
|
|
|
$paymentId = $resource['id'] ?? null;
|
|
|
|
// Check if invoice already exists for this payment (avoid duplicates)
|
|
if ($paymentId) {
|
|
$existingInvoice = Invoice::where('paypal_payment_id', $paymentId)
|
|
->orWhere('paypal_capture_id', $paymentId)
|
|
->first();
|
|
|
|
if ($existingInvoice) {
|
|
Log::info('Invoice already exists for this payment', [
|
|
'payment_id' => $paymentId,
|
|
'invoice_id' => $existingInvoice->id,
|
|
]);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check if a paid invoice was already created for this subscription today (initial payment)
|
|
$todayInvoice = Invoice::where('subscription_id', $subscription->id)
|
|
->where('status', Invoice::STATUS_PAID)
|
|
->whereDate('created_at', today())
|
|
->first();
|
|
|
|
if ($todayInvoice) {
|
|
Log::info('Invoice already created today for subscription, skipping duplicate', [
|
|
'subscription_id' => $subscription->id,
|
|
'existing_invoice_id' => $todayInvoice->id,
|
|
'payment_id' => $paymentId,
|
|
]);
|
|
return;
|
|
}
|
|
|
|
// Create invoice for recurring payment
|
|
$invoice = Invoice::createForSubscription(
|
|
$subscription,
|
|
Invoice::REASON_SUBSCRIPTION_CYCLE,
|
|
"{$subscription->plan->name} - " . now()->format('F Y')
|
|
);
|
|
|
|
$invoice->update([
|
|
'paypal_payment_id' => $resource['id'] ?? null,
|
|
'paypal_data' => $resource,
|
|
]);
|
|
$invoice->markAsPaid($resource['id'] ?? null);
|
|
|
|
// Renew subscription period
|
|
$subscription->renew();
|
|
|
|
Log::info('Payment completed, subscription renewed', [
|
|
'subscription_id' => $subscription->id,
|
|
'invoice_id' => $invoice->id,
|
|
]);
|
|
}
|
|
|
|
private function handlePaymentFailed(array $resource): void
|
|
{
|
|
$billingAgreementId = $resource['billing_agreement_id'] ?? null;
|
|
if (!$billingAgreementId) return;
|
|
|
|
$subscription = Subscription::where('paypal_subscription_id', $billingAgreementId)->first();
|
|
if (!$subscription) return;
|
|
|
|
$subscription->update([
|
|
'status' => Subscription::STATUS_PAST_DUE,
|
|
]);
|
|
|
|
Log::warning('Payment failed', ['subscription_id' => $subscription->id]);
|
|
}
|
|
|
|
// ==================== HELPERS ====================
|
|
|
|
private function updateSubscriptionFromPayPal(Subscription $subscription, array $paypalData): void
|
|
{
|
|
$status = $paypalData['status'] ?? '';
|
|
|
|
$subscription->paypal_status = $status;
|
|
$subscription->paypal_data = $paypalData;
|
|
|
|
switch ($status) {
|
|
case 'ACTIVE':
|
|
$subscription->status = Subscription::STATUS_ACTIVE;
|
|
|
|
// Always set current_period_start to now() on activation if not set
|
|
if (!$subscription->current_period_start) {
|
|
$subscription->current_period_start = now();
|
|
}
|
|
|
|
// Calculate period end based on plan interval
|
|
$plan = $subscription->plan;
|
|
if ($plan) {
|
|
$periodEnd = now();
|
|
if ($plan->interval === 'year') {
|
|
$periodEnd = now()->addYear();
|
|
} else {
|
|
$periodEnd = now()->addMonth();
|
|
}
|
|
$subscription->current_period_end = $periodEnd;
|
|
}
|
|
|
|
// Only use PayPal dates if they make sense (within reasonable range)
|
|
if (isset($paypalData['billing_info']['last_payment']['time'])) {
|
|
$lastPayment = Carbon::parse($paypalData['billing_info']['last_payment']['time']);
|
|
// Accept if within last 30 days
|
|
if ($lastPayment->gte(now()->subDays(30)) && $lastPayment->lte(now()->addDay())) {
|
|
$subscription->current_period_start = $lastPayment;
|
|
}
|
|
}
|
|
|
|
if (isset($paypalData['billing_info']['next_billing_time'])) {
|
|
$nextBilling = Carbon::parse($paypalData['billing_info']['next_billing_time']);
|
|
// Accept if within next 13 months (reasonable for monthly/yearly plans)
|
|
if ($nextBilling->gt(now()) && $nextBilling->lt(now()->addMonths(13))) {
|
|
$subscription->current_period_end = $nextBilling;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'APPROVAL_PENDING':
|
|
// Keep as trialing until approved
|
|
break;
|
|
|
|
case 'APPROVED':
|
|
// Subscription approved but not yet active
|
|
if ($subscription->plan->has_trial) {
|
|
$subscription->startTrial($subscription->plan->trial_days);
|
|
} else {
|
|
$subscription->activate();
|
|
}
|
|
break;
|
|
|
|
case 'SUSPENDED':
|
|
$subscription->status = Subscription::STATUS_PAST_DUE;
|
|
break;
|
|
|
|
case 'CANCELLED':
|
|
$subscription->status = Subscription::STATUS_CANCELED;
|
|
$subscription->canceled_at = now();
|
|
break;
|
|
|
|
case 'EXPIRED':
|
|
$subscription->status = Subscription::STATUS_EXPIRED;
|
|
break;
|
|
}
|
|
|
|
$subscription->save();
|
|
}
|
|
|
|
// ==================== CANCELLATION WIZARD ====================
|
|
|
|
/**
|
|
* Get cancellation eligibility and options
|
|
*/
|
|
public function cancellationEligibility(Request $request): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
$subscription = $user->subscriptions()->active()->with('plan')->first();
|
|
|
|
if (!$subscription) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'No active subscription found',
|
|
], 404);
|
|
}
|
|
|
|
// Calculate subscription age (months paid)
|
|
$subscriptionAge = $subscription->created_at->diffInMonths(now());
|
|
$monthsPaid = max(0, $subscriptionAge);
|
|
|
|
// Check if eligible for 3-month free offer
|
|
$eligibleForFreeMonths = $monthsPaid >= 3;
|
|
|
|
// Check if already used the 3-month offer
|
|
$hasUsedFreeMonthsOffer = DB::table('subscription_retention_offers')
|
|
->where('user_id', $user->id)
|
|
->where('offer_type', 'free_3_months')
|
|
->where('status', 'accepted')
|
|
->exists();
|
|
|
|
// Calculate refund eligibility (7 days guarantee)
|
|
$withinGuaranteePeriod = $subscription->created_at->diffInDays(now()) <= 7;
|
|
$guaranteeDaysRemaining = max(0, 7 - $subscription->created_at->diffInDays(now()));
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'subscription' => [
|
|
'id' => $subscription->id,
|
|
'plan_name' => $subscription->plan->name,
|
|
'price' => $subscription->plan->price,
|
|
'formatted_price' => $subscription->plan->formatted_price,
|
|
'billing_period' => $subscription->plan->billing_period,
|
|
'status' => $subscription->status,
|
|
'created_at' => $subscription->created_at->toIso8601String(),
|
|
'current_period_end' => $subscription->current_period_end?->toIso8601String(),
|
|
],
|
|
'subscription_age' => [
|
|
'months' => $monthsPaid,
|
|
'days' => $subscription->created_at->diffInDays(now()),
|
|
],
|
|
'retention_offer' => [
|
|
'eligible_for_free_months' => $eligibleForFreeMonths && !$hasUsedFreeMonthsOffer,
|
|
'has_used_offer' => $hasUsedFreeMonthsOffer,
|
|
'months_offered' => 3,
|
|
'reason_required' => 'price',
|
|
],
|
|
'guarantee' => [
|
|
'within_period' => $withinGuaranteePeriod,
|
|
'days_remaining' => $guaranteeDaysRemaining,
|
|
'refund_eligible' => $withinGuaranteePeriod,
|
|
],
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Process cancellation with retention attempt
|
|
*/
|
|
public function processCancellation(Request $request): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'reason' => 'required|string|in:price,features,not_using,switching,other',
|
|
'feedback' => 'nullable|string|max:500',
|
|
'accept_retention_offer' => 'nullable|boolean',
|
|
]);
|
|
|
|
$user = $request->user();
|
|
$subscription = $user->subscriptions()->active()->with('plan')->first();
|
|
|
|
if (!$subscription) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'No active subscription found',
|
|
], 404);
|
|
}
|
|
|
|
$reason = $request->reason;
|
|
$acceptOffer = $request->boolean('accept_retention_offer', false);
|
|
|
|
// Check if user accepted retention offer (3 free months)
|
|
if ($acceptOffer && $reason === 'price') {
|
|
return $this->applyRetentionOffer($user, $subscription, $request->feedback);
|
|
}
|
|
|
|
// Process cancellation
|
|
return $this->executeCancellation($user, $subscription, $reason, $request->feedback);
|
|
}
|
|
|
|
/**
|
|
* Apply retention offer (3 free months)
|
|
*/
|
|
private function applyRetentionOffer($user, Subscription $subscription, $feedback): JsonResponse
|
|
{
|
|
$monthsPaid = $subscription->created_at->diffInMonths(now());
|
|
|
|
// Validate eligibility
|
|
if ($monthsPaid < 3) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Not eligible for this offer. Need at least 3 months paid.',
|
|
], 400);
|
|
}
|
|
|
|
// Check if already used
|
|
$hasUsed = DB::table('subscription_retention_offers')
|
|
->where('user_id', $user->id)
|
|
->where('offer_type', 'free_3_months')
|
|
->where('status', 'accepted')
|
|
->exists();
|
|
|
|
if ($hasUsed) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'This offer can only be used once.',
|
|
], 400);
|
|
}
|
|
|
|
DB::beginTransaction();
|
|
try {
|
|
// Record the offer acceptance
|
|
DB::table('subscription_retention_offers')->insert([
|
|
'user_id' => $user->id,
|
|
'subscription_id' => $subscription->id,
|
|
'offer_type' => 'free_3_months',
|
|
'status' => 'accepted',
|
|
'original_cancel_reason' => 'price',
|
|
'user_feedback' => $feedback,
|
|
'offer_start_date' => now(),
|
|
'offer_end_date' => now()->addMonths(3),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
// Extend subscription period by 3 months
|
|
$subscription->current_period_end = $subscription->current_period_end
|
|
? $subscription->current_period_end->addMonths(3)
|
|
: now()->addMonths(3);
|
|
$subscription->save();
|
|
|
|
// Suspend PayPal subscription for 3 months (skip admin-granted subscriptions)
|
|
if ($subscription->paypal_subscription_id && !str_starts_with($subscription->paypal_subscription_id, 'ADMIN_GRANTED_')) {
|
|
try {
|
|
$this->paypal->suspendSubscription($subscription->paypal_subscription_id);
|
|
} catch (\Exception $e) {
|
|
Log::warning('Failed to suspend PayPal subscription', [
|
|
'subscription_id' => $subscription->id,
|
|
'paypal_id' => $subscription->paypal_subscription_id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
// Send confirmation email
|
|
try {
|
|
Mail::to($user->email)->send(new CancellationRetentionOfferMail(
|
|
$user,
|
|
$subscription,
|
|
3, // months
|
|
now()->addMonths(3)
|
|
));
|
|
} catch (\Exception $e) {
|
|
Log::warning('Failed to send retention offer email', [
|
|
'user_id' => $user->id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
|
|
DB::commit();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '3 free months applied successfully!',
|
|
'data' => [
|
|
'subscription' => [
|
|
'id' => $subscription->id,
|
|
'status' => $subscription->status,
|
|
'current_period_end' => $subscription->current_period_end->toIso8601String(),
|
|
],
|
|
'offer' => [
|
|
'type' => 'free_3_months',
|
|
'end_date' => now()->addMonths(3)->toIso8601String(),
|
|
],
|
|
],
|
|
]);
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
Log::error('Failed to apply retention offer', [
|
|
'user_id' => $user->id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Failed to apply offer',
|
|
'error' => $e->getMessage(),
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute subscription cancellation
|
|
*/
|
|
private function executeCancellation($user, Subscription $subscription, $reason, $feedback): JsonResponse
|
|
{
|
|
DB::beginTransaction();
|
|
try {
|
|
$withinGuaranteePeriod = $subscription->created_at->diffInDays(now()) <= 7;
|
|
|
|
// Cancel on PayPal if exists (skip admin-granted subscriptions)
|
|
if ($subscription->paypal_subscription_id && !str_starts_with($subscription->paypal_subscription_id, 'ADMIN_GRANTED_')) {
|
|
try {
|
|
$this->paypal->cancelSubscription($subscription->paypal_subscription_id);
|
|
} catch (\Exception $e) {
|
|
Log::warning('Failed to cancel PayPal subscription', [
|
|
'subscription_id' => $subscription->id,
|
|
'paypal_id' => $subscription->paypal_subscription_id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
// Update subscription
|
|
$subscription->status = Subscription::STATUS_CANCELED;
|
|
$subscription->canceled_at = now();
|
|
$subscription->ends_at = $withinGuaranteePeriod ? now() : $subscription->current_period_end;
|
|
$subscription->cancel_reason = $reason;
|
|
$subscription->save();
|
|
|
|
// Record cancellation
|
|
DB::table('subscription_cancellations')->insert([
|
|
'user_id' => $user->id,
|
|
'subscription_id' => $subscription->id,
|
|
'reason' => $reason,
|
|
'feedback' => $feedback,
|
|
'canceled_at' => now(),
|
|
'within_guarantee_period' => $withinGuaranteePeriod,
|
|
'refund_issued' => false, // Manual process
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
// Send cancellation email
|
|
try {
|
|
Mail::to($user->email)->send(new SubscriptionCancelledMail(
|
|
$user,
|
|
$subscription->plan->name,
|
|
$withinGuaranteePeriod,
|
|
$withinGuaranteePeriod ? $subscription->plan->formatted_price : null
|
|
));
|
|
} catch (\Exception $e) {
|
|
Log::warning('Failed to send cancellation email', [
|
|
'user_id' => $user->id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
|
|
DB::commit();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Subscription cancelled successfully',
|
|
'data' => [
|
|
'subscription' => [
|
|
'id' => $subscription->id,
|
|
'status' => $subscription->status,
|
|
'canceled_at' => $subscription->canceled_at->toIso8601String(),
|
|
'ends_at' => $subscription->ends_at?->toIso8601String(),
|
|
'access_until' => $subscription->ends_at?->toIso8601String(),
|
|
],
|
|
'guarantee' => [
|
|
'within_period' => $withinGuaranteePeriod,
|
|
'refund_eligible' => $withinGuaranteePeriod,
|
|
'refund_message' => $withinGuaranteePeriod
|
|
? 'Your refund will be processed within 5-7 business days'
|
|
: null,
|
|
],
|
|
],
|
|
]);
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
Log::error('Failed to cancel subscription', [
|
|
'user_id' => $user->id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Failed to cancel subscription',
|
|
'error' => $e->getMessage(),
|
|
], 500);
|
|
}
|
|
}
|
|
}
|