<?php namespace App\Integrations\Celcoin; use App\Models\CelcoinCharge; use App\Models\CelcoinSubscription; use App\Models\CelcoinSyncLog; use Exception; /** * Serviço de reconciliação para sincronização manual de status * Sincroniza dados locais com a API da Celcoin */ class CelcoinReconciliationService { private CelcoinHttpClient $httpClient; private CelcoinAuthService $authService; 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); } /** * Sincroniza todas as cobranças pendentes */ public function syncPendingCharges(): array { try { $syncLogId = $this->startSyncLog('charges_sync'); $accessToken = $this->authService->getValidAccessToken(); $this->httpClient->setAccessToken($accessToken); $chargeModel = new CelcoinCharge(); $pendingCharges = $chargeModel->findPending($this->companyId); $results = [ 'total' => count($pendingCharges), 'updated' => 0, 'errors' => 0, 'details' => [] ]; foreach ($pendingCharges as $charge) { try { $updated = $this->syncChargeStatus($charge); if ($updated) { $results['updated']++; $results['details'][] = [ 'charge_id' => $charge['id'], 'celcoin_id' => $charge['celcoin_charge_id'], 'status' => 'updated' ]; } } catch (Exception $e) { $results['errors']++; $results['details'][] = [ 'charge_id' => $charge['id'], 'celcoin_id' => $charge['celcoin_charge_id'], 'error' => $e->getMessage() ]; } } $this->endSyncLog($syncLogId, $results); return $results; } catch (Exception $e) { $this->logSyncError('Charges sync failed', $e->getMessage()); throw new Exception('Failed to sync pending charges: ' . $e->getMessage()); } } /** * Sincroniza todas as assinaturas ativas */ public function syncActiveSubscriptions(): array { try { $syncLogId = $this->startSyncLog('subscriptions_sync'); $accessToken = $this->authService->getValidAccessToken(); $this->httpClient->setAccessToken($accessToken); $subscriptionModel = new CelcoinSubscription(); $activeSubscriptions = $subscriptionModel->findActive($this->companyId); $results = [ 'total' => count($activeSubscriptions), 'updated' => 0, 'errors' => 0, 'details' => [] ]; foreach ($activeSubscriptions as $subscription) { try { $updated = $this->syncSubscriptionStatus($subscription); if ($updated) { $results['updated']++; $results['details'][] = [ 'subscription_id' => $subscription['id'], 'celcoin_id' => $subscription['celcoin_subscription_id'], 'status' => 'updated' ]; } } catch (Exception $e) { $results['errors']++; $results['details'][] = [ 'subscription_id' => $subscription['id'], 'celcoin_id' => $subscription['celcoin_subscription_id'], 'error' => $e->getMessage() ]; } } $this->endSyncLog($syncLogId, $results); return $results; } catch (Exception $e) { $this->logSyncError('Subscriptions sync failed', $e->getMessage()); throw new Exception('Failed to sync active subscriptions: ' . $e->getMessage()); } } /** * Sincroniza cobrança específica */ public function syncCharge(string $celcoinChargeId): array { try { $accessToken = $this->authService->getValidAccessToken(); $this->httpClient->setAccessToken($accessToken); $response = $this->httpClient->get("/v5/charges/{$celcoinChargeId}"); if (!$response['success']) { throw new Exception('Failed to fetch charge from Celcoin API'); } $celcoinCharge = $response['data']; // Atualizar cobrança local $updated = $this->updateLocalCharge($celcoinCharge); return [ 'success' => true, 'updated' => $updated, 'charge' => $celcoinCharge ]; } catch (Exception $e) { $this->logSyncError('Charge sync failed', $e->getMessage()); return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * Sincroniza assinatura específica */ public function syncSubscription(string $celcoinSubscriptionId): array { try { $accessToken = $this->authService->getValidAccessToken(); $this->httpClient->setAccessToken($accessToken); $response = $this->httpClient->get("/v5/subscriptions/{$celcoinSubscriptionId}"); if (!$response['success']) { throw new Exception('Failed to fetch subscription from Celcoin API'); } $celcoinSubscription = $response['data']; // Atualizar assinatura local $updated = $this->updateLocalSubscription($celcoinSubscription); return [ 'success' => true, 'updated' => $updated, 'subscription' => $celcoinSubscription ]; } catch (Exception $e) { $this->logSyncError('Subscription sync failed', $e->getMessage()); return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * Executa reconciliação completa */ public function fullReconciliation(): array { try { $syncLogId = $this->startSyncLog('full_reconciliation'); $results = [ 'charges' => $this->syncPendingCharges(), 'subscriptions' => $this->syncActiveSubscriptions(), 'timestamp' => date('Y-m-d H:i:s') ]; $this->endSyncLog($syncLogId, $results); return $results; } catch (Exception $e) { $this->logSyncError('Full reconciliation failed', $e->getMessage()); throw new Exception('Failed to execute full reconciliation: ' . $e->getMessage()); } } /** * Sincroniza status de uma cobrança específica */ private function syncChargeStatus(array $localCharge): bool { $celcoinChargeId = $localCharge['celcoin_charge_id']; if (!$celcoinChargeId) { return false; } $response = $this->httpClient->get("/v5/charges/{$celcoinChargeId}"); if (!$response['success']) { throw new Exception("Failed to fetch charge {$celcoinChargeId}"); } $celcoinCharge = $response['data']; // Verificar se status mudou $currentStatus = $localCharge['status']; $newStatus = $this->mapCelcoinChargeStatus($celcoinCharge['status']); if ($currentStatus !== $newStatus) { $this->updateLocalCharge($celcoinCharge); return true; } return false; } /** * Sincroniza status de uma assinatura específica */ private function syncSubscriptionStatus(array $localSubscription): bool { $celcoinSubscriptionId = $localSubscription['celcoin_subscription_id']; if (!$celcoinSubscriptionId) { return false; } $response = $this->httpClient->get("/v5/subscriptions/{$celcoinSubscriptionId}"); if (!$response['success']) { throw new Exception("Failed to fetch subscription {$celcoinSubscriptionId}"); } $celcoinSubscription = $response['data']; // Verificar se status mudou $currentStatus = $localSubscription['status']; $newStatus = $this->mapCelcoinSubscriptionStatus($celcoinSubscription['status']); if ($currentStatus !== $newStatus) { $this->updateLocalSubscription($celcoinSubscription); return true; } return false; } /** * Atualiza cobrança local com dados da Celcoin */ private function updateLocalCharge(array $celcoinCharge): bool { $chargeModel = new CelcoinCharge(); // Buscar cobrança local $localCharge = $chargeModel->findByCelcoinId($this->companyId, $celcoinCharge['id']); if (!$localCharge) { // Se não existe localmente, talvez criar ou logar return false; } $updateData = [ 'status' => $this->mapCelcoinChargeStatus($celcoinCharge['status']), 'paid_at' => isset($celcoinCharge['paidAt']) ? date('Y-m-d H:i:s', strtotime($celcoinCharge['paidAt'])) : null, 'failed_at' => isset($celcoinCharge['failedAt']) ? date('Y-m-d H:i:s', strtotime($celcoinCharge['failedAt'])) : null, 'refunded_at' => isset($celcoinCharge['refundedAt']) ? date('Y-m-d H:i:s', strtotime($celcoinCharge['refundedAt'])) : null, 'celcoin_response' => json_encode($celcoinCharge), 'updated_at' => date('Y-m-d H:i:s') ]; // Adicionar dados específicos do método de pagamento if (isset($celcoinCharge['paymentMethod'])) { $updateData['payment_method_details'] = json_encode($celcoinCharge['paymentMethod']); } return $chargeModel->update($localCharge['id'], $updateData); } /** * Atualiza assinatura local com dados da Celcoin */ private function updateLocalSubscription(array $celcoinSubscription): bool { $subscriptionModel = new CelcoinSubscription(); // Buscar assinatura local $localSubscription = $subscriptionModel->findByCelcoinId($this->companyId, $celcoinSubscription['id']); if (!$localSubscription) { return false; } $updateData = [ 'status' => $this->mapCelcoinSubscriptionStatus($celcoinSubscription['status']), 'current_cycle' => $celcoinSubscription['currentCycle'] ?? $localSubscription['current_cycle'], 'next_billing_date' => isset($celcoinSubscription['nextBillingDate']) ? date('Y-m-d', strtotime($celcoinSubscription['nextBillingDate'])) : $localSubscription['next_billing_date'], 'canceled_at' => isset($celcoinSubscription['canceledAt']) ? date('Y-m-d H:i:s', strtotime($celcoinSubscription['canceledAt'])) : $localSubscription['canceled_at'], 'celcoin_response' => json_encode($celcoinSubscription), 'updated_at' => date('Y-m-d H:i:s') ]; return $subscriptionModel->update($localSubscription['id'], $updateData); } /** * Mapeia status de cobrança da Celcoin */ private function mapCelcoinChargeStatus(string $celcoinStatus): string { $statusMap = [ 'pending' => 'pending', 'paid' => 'paid', 'failed' => 'failed', 'expired' => 'expired', 'canceled' => 'canceled', 'refunded' => 'refunded' ]; return $statusMap[$celcoinStatus] ?? 'pending'; } /** * Mapeia status de assinatura da Celcoin */ private function mapCelcoinSubscriptionStatus(string $celcoinStatus): string { $statusMap = [ 'active' => 'active', 'past_due' => 'past_due', 'canceled' => 'canceled', 'expired' => 'expired', 'suspended' => 'suspended' ]; return $statusMap[$celcoinStatus] ?? 'active'; } /** * Inicia log de sincronização */ private function startSyncLog(string $syncType): int { $syncLogModel = new CelcoinSyncLog(); return $syncLogModel->create([ 'company_id' => $this->companyId, 'sync_type' => $syncType, 'status' => 'running', 'started_at' => date('Y-m-d H:i:s'), 'details' => json_encode(['message' => 'Sync started']) ]); } /** * Finaliza log de sincronização */ private function endSyncLog(int $syncLogId, array $results): void { $syncLogModel = new CelcoinSyncLog(); $syncLogModel->update($syncLogId, [ 'status' => 'completed', 'completed_at' => date('Y-m-d H:i:s'), 'results' => json_encode($results) ]); } /** * Lista logs de sincronização */ public function listSyncLogs(array $filters = [], int $page = 1, int $limit = 50): array { $syncLogModel = new CelcoinSyncLog(); $offset = ($page - 1) * $limit; $where = ['company_id' => $this->companyId]; // Adicionar filtros if (isset($filters['sync_type'])) { $where['sync_type'] = $filters['sync_type']; } if (isset($filters['status'])) { $where['status'] = $filters['status']; } if (isset($filters['date_from'])) { $where['started_at >='] = $filters['date_from']; } if (isset($filters['date_to'])) { $where['started_at <='] = $filters['date_to']; } $logs = $syncLogModel->findAll($where, $limit, $offset, 'started_at DESC'); $total = $syncLogModel->count($where); return [ 'logs' => $logs, 'total' => $total, 'page' => $page, 'limit' => $limit, 'pages' => ceil($total / $limit) ]; } /** * Executa sincronização manual por período */ public function syncByDateRange(string $startDate, string $endDate, string $type = 'charges'): array { try { $syncLogId = $this->startSyncLog("{$type}_sync_by_date"); $accessToken = $this->authService->getValidAccessToken(); $this->httpClient->setAccessToken($accessToken); $results = [ 'type' => $type, 'start_date' => $startDate, 'end_date' => $endDate, 'processed' => 0, 'updated' => 0, 'errors' => 0 ]; // Buscar dados da API por período $apiData = $this->fetchDataByDateRange($type, $startDate, $endDate); foreach ($apiData as $item) { try { $results['processed']++; if ($type === 'charges') { $updated = $this->updateLocalCharge($item); } else { $updated = $this->updateLocalSubscription($item); } if ($updated) { $results['updated']++; } } catch (Exception $e) { $results['errors']++; error_log("Error syncing {$type} {$item['id']}: " . $e->getMessage()); } } $this->endSyncLog($syncLogId, $results); return $results; } catch (Exception $e) { $this->logSyncError("{$type} sync by date range failed", $e->getMessage()); throw new Exception("Failed to sync {$type} by date range: " . $e->getMessage()); } } /** * Busca dados da API por período */ private function fetchDataByDateRange(string $type, string $startDate, string $endDate): array { $endpoint = $type === 'charges' ? '/v5/charges' : '/v5/subscriptions'; $params = [ 'created_after' => $startDate, 'created_before' => $endDate, 'limit' => 100 ]; $allData = []; $page = 1; do { $params['page'] = $page; $response = $this->httpClient->get($endpoint . '?' . http_build_query($params)); if (!$response['success']) { break; } $data = $response['data']['data'] ?? []; $allData = array_merge($allData, $data); $page++; $hasMore = count($data) === 100; // Assumindo que 100 é o limite por página } while ($hasMore && $page <= 10); // Limitar a 10 páginas para evitar loops infinitos return $allData; } /** * Log de erro de sincronização */ private function logSyncError(string $action, string $error): void { error_log("[Celcoin Reconciliation Error] {$action} - Company: {$this->companyId}, Error: {$error}"); } }