<?php namespace App\Integrations\Celcoin; use App\Models\CelcoinSubscription; use App\Models\Membership; use Exception; /** * Serviço de gerenciamento de assinaturas recorrentes na Celcoin * Cria, gerencia e cancela assinaturas automáticas */ class CelcoinSubscriptionService { private CelcoinHttpClient $httpClient; private CelcoinAuthService $authService; private CelcoinCustomerService $customerService; private int $companyId; private int $gatewaySettingId; public function __construct(int $companyId, int $gatewaySettingId) { $this->companyId = $companyId; $this->gatewaySettingId = $gatewaySettingId; $this->authService = new CelcoinAuthService($companyId, $gatewaySettingId); $this->httpClient = new CelcoinHttpClient($companyId); $this->customerService = new CelcoinCustomerService($companyId, $gatewaySettingId); } /** * Cria uma nova assinatura recorrente */ public function createSubscription(array $subscriptionData): array { try { // Validar dados obrigatórios $this->validateSubscriptionData($subscriptionData); // Sincronizar cliente se necessário $customerId = $this->ensureCustomerExists($subscriptionData['user_id']); // Obter token válido $accessToken = $this->authService->getValidAccessToken(); $this->httpClient->setAccessToken($accessToken); // Preparar dados para a API da Celcoin $apiData = $this->prepareSubscriptionData($subscriptionData, $customerId); // Criar assinatura na Celcoin $response = $this->httpClient->post('/v5/subscriptions', $apiData); if (!$response['success']) { throw new Exception('Failed to create subscription in Celcoin API'); } $celcoinSubscription = $response['data']; // Salvar assinatura no banco local $localSubscriptionId = $this->saveSubscription($subscriptionData, $celcoinSubscription); // Log de sucesso $this->logSubscriptionSuccess('Subscription created', $localSubscriptionId, $celcoinSubscription['id']); return [ 'success' => true, 'subscription_id' => $localSubscriptionId, 'celcoin_subscription_id' => $celcoinSubscription['id'], 'initial_charge_id' => $celcoinSubscription['initialChargeId'] ?? null ]; } catch (Exception $e) { $this->logSubscriptionError('Subscription creation failed', $subscriptionData['user_id'], $e->getMessage()); throw new Exception('Failed to create subscription: ' . $e->getMessage()); } } /** * Cancela uma assinatura */ public function cancelSubscription(string $celcoinSubscriptionId, string $reason = null): bool { try { $accessToken = $this->authService->getValidAccessToken(); $this->httpClient->setAccessToken($accessToken); $cancelData = []; if ($reason) { $cancelData['reason'] = $reason; } $response = $this->httpClient->delete("/v5/subscriptions/{$celcoinSubscriptionId}", $cancelData); if ($response['success']) { // Atualizar status local $this->updateSubscriptionStatus($celcoinSubscriptionId, 'canceled', null, $reason); $this->logSubscriptionSuccess('Subscription canceled', 0, $celcoinSubscriptionId); return true; } return false; } catch (Exception $e) { $this->logSubscriptionError('Subscription cancellation failed', 0, $e->getMessage()); return false; } } /** * Consulta status de uma assinatura */ public function getSubscriptionStatus(string $celcoinSubscriptionId): ?array { try { $accessToken = $this->authService->getValidAccessToken(); $this->httpClient->setAccessToken($accessToken); $response = $this->httpClient->get("/v5/subscriptions/{$celcoinSubscriptionId}"); if (!$response['success']) { return null; } return $response['data']; } catch (Exception $e) { $this->logSubscriptionError('Subscription status check failed', 0, $e->getMessage()); return null; } } /** * Lista assinaturas com filtros */ public function listSubscriptions(array $filters = [], int $page = 1, int $limit = 50): array { try { $accessToken = $this->authService->getValidAccessToken(); $this->httpClient->setAccessToken($accessToken); $params = array_merge($filters, [ 'page' => $page, 'limit' => $limit ]); $response = $this->httpClient->get('/v5/subscriptions?' . http_build_query($params)); if (!$response['success']) { throw new Exception('Failed to list subscriptions'); } return $response['data']; } catch (Exception $e) { $this->logSubscriptionError('Subscription list failed', 0, $e->getMessage()); throw $e; } } /** * Atualiza método de pagamento de uma assinatura */ public function updatePaymentMethod(string $celcoinSubscriptionId, array $paymentMethodData): bool { try { $accessToken = $this->authService->getValidAccessToken(); $this->httpClient->setAccessToken($accessToken); $response = $this->httpClient->put("/v5/subscriptions/{$celcoinSubscriptionId}/payment-method", $paymentMethodData); if ($response['success']) { // Atualizar método de pagamento local $this->updateSubscriptionPaymentMethod($celcoinSubscriptionId, $paymentMethodData); $this->logSubscriptionSuccess('Payment method updated', 0, $celcoinSubscriptionId); return true; } return false; } catch (Exception $e) { $this->logSubscriptionError('Payment method update failed', 0, $e->getMessage()); return false; } } /** * Processa webhook de assinatura */ public function processSubscriptionWebhook(array $webhookData): array { try { $celcoinSubscriptionId = $webhookData['id'] ?? null; $event = $webhookData['event'] ?? null; if (!$celcoinSubscriptionId || !$event) { throw new Exception('Invalid webhook data: missing subscription ID or event'); } // Processar evento específico $result = $this->handleSubscriptionEvent($celcoinSubscriptionId, $event, $webhookData); $this->logSubscriptionSuccess('Subscription webhook processed', 0, $celcoinSubscriptionId); return [ 'success' => true, 'processed' => true, 'event' => $event, 'result' => $result ]; } catch (Exception $e) { $this->logSubscriptionError('Subscription webhook processing failed', 0, $e->getMessage()); return ['success' => false, 'processed' => false, 'error' => $e->getMessage()]; } } /** * Sincroniza assinatura com membership local */ public function syncWithMembership(int $membershipId): array { try { // Buscar dados da membership $membershipModel = new Membership(); $membership = $membershipModel->find($membershipId); if (!$membership) { throw new Exception("Membership not found: {$membershipId}"); } // Verificar se já existe assinatura na Celcoin $existingSubscription = $this->getExistingSubscription($membershipId); if ($existingSubscription) { // Atualizar assinatura existente return $this->updateExistingSubscription($existingSubscription, $membership); } else { // Criar nova assinatura return $this->createSubscriptionFromMembership($membership); } } catch (Exception $e) { $this->logSubscriptionError('Membership sync failed', 0, $e->getMessage()); throw new Exception('Failed to sync membership: ' . $e->getMessage()); } } /** * Valida dados da assinatura */ private function validateSubscriptionData(array $subscriptionData): void { $required = ['user_id', 'amount', 'billing_cycle']; foreach ($required as $field) { if (!isset($subscriptionData[$field]) || empty($subscriptionData[$field])) { throw new Exception("Missing required field: {$field}"); } } // Validar ciclo de cobrança $validCycles = ['daily', 'weekly', 'monthly', 'quarterly', 'semiannual', 'annual']; if (!in_array($subscriptionData['billing_cycle'], $validCycles)) { throw new Exception("Invalid billing cycle: {$subscriptionData['billing_cycle']}"); } // Validar valor if (!is_numeric($subscriptionData['amount']) || $subscriptionData['amount'] <= 0) { throw new Exception('Invalid amount: must be a positive number'); } } /** * Garante que o cliente existe na Celcoin */ private function ensureCustomerExists(int $userId): string { $result = $this->customerService->syncCustomer($userId); if (!$result['success']) { throw new Exception('Failed to sync customer for subscription creation'); } return $result['customer_id']; } /** * Prepara dados da assinatura para a API da Celcoin */ private function prepareSubscriptionData(array $subscriptionData, string $customerId): array { $apiData = [ 'customerId' => $customerId, 'amount' => $subscriptionData['amount'], 'currency' => 'BRL', 'billingCycle' => $subscriptionData['billing_cycle'], 'description' => $subscriptionData['description'] ?? 'Assinatura recorrente', 'startDate' => $subscriptionData['start_date'] ?? date('Y-m-d'), 'paymentMethod' => $subscriptionData['payment_method'] ?? 'credit_card' ]; // Adicionar período de trial se especificado if (isset($subscriptionData['trial_days']) && $subscriptionData['trial_days'] > 0) { $apiData['trialDays'] = $subscriptionData['trial_days']; } // Adicionar limite de ciclos se especificado if (isset($subscriptionData['max_cycles']) && $subscriptionData['max_cycles'] > 0) { $apiData['maxCycles'] = $subscriptionData['max_cycles']; } // Adicionar dados do método de pagamento if (isset($subscriptionData['payment_method_data'])) { $apiData['paymentMethodData'] = $subscriptionData['payment_method_data']; } return $apiData; } /** * Salva assinatura no banco local */ private function saveSubscription(array $subscriptionData, array $celcoinSubscription): int { $subscriptionModel = new CelcoinSubscription(); // Calcular datas $startDate = $subscriptionData['start_date'] ?? date('Y-m-d'); $currentPeriodEnd = $this->calculatePeriodEnd($startDate, $subscriptionData['billing_cycle']); return $subscriptionModel->create([ 'company_id' => $this->companyId, 'user_id' => $subscriptionData['user_id'], 'membership_id' => $subscriptionData['membership_id'] ?? null, 'celcoin_customer_id' => $celcoinSubscription['customerId'] ?? null, 'celcoin_subscription_id' => $celcoinSubscription['id'], 'celcoin_charge_id' => $celcoinSubscription['initialChargeId'] ?? null, 'amount' => $subscriptionData['amount'], 'currency' => 'BRL', 'billing_cycle' => $subscriptionData['billing_cycle'], 'status' => 'active', 'current_period_start' => $startDate, 'current_period_end' => $currentPeriodEnd, 'trial_end' => $subscriptionData['trial_days'] ? $this->calculateTrialEnd($startDate, $subscriptionData['trial_days']) : null, 'next_billing_date' => $currentPeriodEnd, 'max_cycles' => $subscriptionData['max_cycles'] ?? null, 'payment_method' => json_encode($subscriptionData['payment_method_data'] ?? []), 'metadata' => json_encode($subscriptionData['metadata'] ?? []) ]); } /** * Atualiza status da assinatura */ private function updateSubscriptionStatus(string $celcoinSubscriptionId, string $status, ?string $canceledAt = null, ?string $cancelReason = null): bool { $subscriptionModel = new CelcoinSubscription(); $subscription = $subscriptionModel->findByCelcoinId($this->companyId, $celcoinSubscriptionId); if (!$subscription) { return false; } $updateData = [ 'status' => $this->mapCelcoinStatus($status), 'updated_at' => date('Y-m-d H:i:s') ]; if ($canceledAt) { $updateData['canceled_at'] = $canceledAt; } if ($cancelReason) { $updateData['cancel_reason'] = $cancelReason; } return $subscriptionModel->update($subscription['id'], $updateData); } /** * Atualiza método de pagamento da assinatura */ private function updateSubscriptionPaymentMethod(string $celcoinSubscriptionId, array $paymentMethodData): bool { $subscriptionModel = new CelcoinSubscription(); $subscription = $subscriptionModel->findByCelcoinId($this->companyId, $celcoinSubscriptionId); if (!$subscription) { return false; } return $subscriptionModel->update($subscription['id'], [ 'payment_method' => json_encode($paymentMethodData), 'updated_at' => date('Y-m-d H:i:s') ]); } /** * Mapeia status da Celcoin para status local */ private function mapCelcoinStatus(string $celcoinStatus): string { $statusMap = [ 'active' => 'active', 'past_due' => 'past_due', 'canceled' => 'canceled', 'expired' => 'expired', 'suspended' => 'suspended' ]; return $statusMap[$celcoinStatus] ?? 'active'; } /** * Calcula fim do período baseado no ciclo */ private function calculatePeriodEnd(string $startDate, string $billingCycle): string { $start = strtotime($startDate); switch ($billingCycle) { case 'daily': return date('Y-m-d', strtotime('+1 day', $start)); case 'weekly': return date('Y-m-d', strtotime('+1 week', $start)); case 'monthly': return date('Y-m-d', strtotime('+1 month', $start)); case 'quarterly': return date('Y-m-d', strtotime('+3 months', $start)); case 'semiannual': return date('Y-m-d', strtotime('+6 months', $start)); case 'annual': return date('Y-m-d', strtotime('+1 year', $start)); default: return date('Y-m-d', strtotime('+1 month', $start)); } } /** * Calcula fim do período de trial */ private function calculateTrialEnd(string $startDate, int $trialDays): string { return date('Y-m-d', strtotime("+{$trialDays} days", strtotime($startDate))); } /** * Trata eventos específicos de webhook */ private function handleSubscriptionEvent(string $celcoinSubscriptionId, string $event, array $webhookData): array { switch ($event) { case 'subscription.canceled': return $this->handleSubscriptionCanceled($celcoinSubscriptionId, $webhookData); case 'subscription.expired': return $this->handleSubscriptionExpired($celcoinSubscriptionId, $webhookData); case 'subscription.payment_failed': return $this->handleSubscriptionPaymentFailed($celcoinSubscriptionId, $webhookData); case 'subscription.payment_succeeded': return $this->handleSubscriptionPaymentSucceeded($celcoinSubscriptionId, $webhookData); default: return ['event' => $event, 'handled' => false]; } } /** * Trata cancelamento de assinatura */ private function handleSubscriptionCanceled(string $celcoinSubscriptionId, array $webhookData): array { $canceledAt = isset($webhookData['canceledAt']) ? date('Y-m-d H:i:s', strtotime($webhookData['canceledAt'])) : date('Y-m-d H:i:s'); $reason = $webhookData['cancelReason'] ?? 'Canceled via webhook'; $this->updateSubscriptionStatus($celcoinSubscriptionId, 'canceled', $canceledAt, $reason); return ['status' => 'canceled', 'handled' => true]; } /** * Trata expiração de assinatura */ private function handleSubscriptionExpired(string $celcoinSubscriptionId, array $webhookData): array { $this->updateSubscriptionStatus($celcoinSubscriptionId, 'expired'); return ['status' => 'expired', 'handled' => true]; } /** * Trata falha de pagamento */ private function handleSubscriptionPaymentFailed(string $celcoinSubscriptionId, array $webhookData): array { // Pode implementar lógica de retry ou notificação return ['payment' => 'failed', 'handled' => true]; } /** * Trata sucesso de pagamento */ private function handleSubscriptionPaymentSucceeded(string $celcoinSubscriptionId, array $webhookData): array { // Atualizar ciclo atual e próxima cobrança $this->updateSubscriptionCycle($celcoinSubscriptionId); return ['payment' => 'succeeded', 'handled' => true]; } /** * Atualiza ciclo da assinatura após pagamento */ private function updateSubscriptionCycle(string $celcoinSubscriptionId): void { $subscriptionModel = new CelcoinSubscription(); $subscription = $subscriptionModel->findByCelcoinId($this->companyId, $celcoinSubscriptionId); if (!$subscription) { return; } $nextBillingDate = $this->calculatePeriodEnd($subscription['next_billing_date'], $subscription['billing_cycle']); $subscriptionModel->update($subscription['id'], [ 'current_cycle' => $subscription['current_cycle'] + 1, 'current_period_start' => $subscription['next_billing_date'], 'current_period_end' => $nextBillingDate, 'next_billing_date' => $nextBillingDate, 'total_paid' => $subscription['total_paid'] + $subscription['amount'], 'updated_at' => date('Y-m-d H:i:s') ]); } /** * Verifica se já existe assinatura para a membership */ private function getExistingSubscription(int $membershipId): ?array { $subscriptionModel = new CelcoinSubscription(); return $subscriptionModel->findByMembershipId($this->companyId, $membershipId); } /** * Cria assinatura a partir de uma membership */ private function createSubscriptionFromMembership(array $membership): array { $subscriptionData = [ 'user_id' => $membership['client_id'], 'membership_id' => $membership['id'], 'amount' => $membership['price'], 'billing_cycle' => $membership['billing_cycle'], 'start_date' => $membership['start_date'], 'trial_days' => $membership['trial_days'] ?? 0, 'description' => "Assinatura - Plano {$membership['plan_name']}" ]; return $this->createSubscription($subscriptionData); } /** * Atualiza assinatura existente */ private function updateExistingSubscription(array $existingSubscription, array $membership): array { // Implementar lógica de atualização se necessário return [ 'success' => true, 'subscription_id' => $existingSubscription['id'], 'action' => 'already_exists' ]; } /** * Log de sucesso na assinatura */ private function logSubscriptionSuccess(string $action, int $subscriptionId, string $celcoinId): void { error_log("[Celcoin Subscription] {$action} - Subscription ID: {$subscriptionId}, Celcoin ID: {$celcoinId}"); } /** * Log de erro na assinatura */ private function logSubscriptionError(string $action, int $userId, string $error): void { error_log("[Celcoin Subscription Error] {$action} - User: {$userId}, Error: {$error}"); } }