diff --git a/.env.example b/.env.example index c848aea..1f4f388 100644 --- a/.env.example +++ b/.env.example @@ -31,10 +31,13 @@ AUTH_LOGIN_LOCK_MINUTES=15 AUTH_LOGIN_RATE_LIMIT_PER_MINUTE=10 AUTH_LOGIN_IP_RATE_LIMIT_PER_MINUTE=30 -LOG_CHANNEL=stack +LOG_CHANNEL=daily LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug +LOG_DAILY_DAYS=14 +TELEPAGOS_LOG_LEVEL=info +TELEPAGOS_LOG_DAYS=30 DB_CONNECTION=mysql DB_HOST=127.0.0.1 diff --git a/app/Domains/Integration/Services/TelepagosIntegrationService.php b/app/Domains/Integration/Services/TelepagosIntegrationService.php index a6b917d..528690b 100644 --- a/app/Domains/Integration/Services/TelepagosIntegrationService.php +++ b/app/Domains/Integration/Services/TelepagosIntegrationService.php @@ -81,9 +81,7 @@ class TelepagosIntegrationService extends BaseIntegrationService 'password' => $password, ]); - $data = $this->handleResponse($response, 'authentication', [ - 'username' => $username, - ]); + $data = $this->handleResponse($response, 'authentication'); $token = $data['token'] ?? null; $expiresAtStr = $data['expires_at'] ?? null; @@ -110,7 +108,12 @@ class TelepagosIntegrationService extends BaseIntegrationService $response = $this->client()->$method($endpoint, $data); if ($response->status() === 401) { - Log::info('Telepagos 401 Unauthorized. Refreshing token and retrying...'); + Log::channel('telepagos')->info('Telepagos request returned 401; refreshing token and retrying.', [ + 'method' => strtoupper($method), + 'endpoint' => $endpoint, + 'client_id' => $this->clientContext?->id, + 'integration_code' => $this->integrationCode, + ]); $this->clearToken(); @@ -175,9 +178,11 @@ class TelepagosIntegrationService extends BaseIntegrationService { if ($response->failed() || $response->json('status') !== 'ok') { $errorMessage = $response->json('message') ?? $response->body(); - Log::error("Telepagos {$actionDescription} failed: {$errorMessage}", array_merge([ + Log::channel('telepagos')->error("Telepagos {$actionDescription} failed: {$errorMessage}", array_merge([ 'response_status' => $response->status(), - 'response_body' => $response->json() ?? $response->body(), + 'response_body' => $this->sanitizeForLog($response->json() ?? $response->body()), + 'client_id' => $this->clientContext?->id, + 'integration_code' => $this->integrationCode, ], $context)); throw new Exception("Telepagos {$actionDescription} failed: {$errorMessage}"); @@ -186,6 +191,30 @@ class TelepagosIntegrationService extends BaseIntegrationService return $response->json() ?? []; } + /** + * Remove credentials and tokens before serializing provider responses. + */ + protected function sanitizeForLog(mixed $value): mixed + { + if (! is_array($value)) { + return $value; + } + + $sensitiveKeys = ['authorization', 'password', 'token', 'access_token', 'refresh_token']; + + foreach ($value as $key => $item) { + if (in_array(strtolower((string) $key), $sensitiveKeys, true)) { + $value[$key] = '[REDACTED]'; + + continue; + } + + $value[$key] = $this->sanitizeForLog($item); + } + + return $value; + } + /** * Clear the cached token. */ diff --git a/app/Domains/Integration/Services/TelepagosWebhookService.php b/app/Domains/Integration/Services/TelepagosWebhookService.php index 4fdda97..e74e4bf 100644 --- a/app/Domains/Integration/Services/TelepagosWebhookService.php +++ b/app/Domains/Integration/Services/TelepagosWebhookService.php @@ -24,6 +24,11 @@ class TelepagosWebhookService */ public function handleWebhook(Client $client, string $cashinId): void { + Log::channel('telepagos')->info('Telepagos webhook received.', [ + 'client_code' => $client->code, + 'cashin_id' => $cashinId, + ]); + $telepagosService = new TelepagosIntegrationService; $telepagosService->forClient($client); @@ -43,7 +48,10 @@ class TelepagosWebhookService $cuit = $details['data']['buyer']['cuit'] ?? $details['buyer']['cuit'] ?? null; if (! $cuit) { - Log::warning("Telepagos webhook: CUIT not found for Transferencia cashin {$cashinId}"); + Log::channel('telepagos')->warning('Telepagos webhook: CUIT not found for transfer.', [ + 'client_code' => $client->code, + 'cashin_id' => $cashinId, + ]); return; } @@ -66,13 +74,21 @@ class TelepagosWebhookService $compra = $purchases->count() === 1 ? $purchases->first() : null; if (! $compra) { - Log::warning("Telepagos webhook: Expected one matching purchase for client {$client->code}, DNI {$dni}, amount {$amount} and cashin {$cashinId}; found {$purchases->count()}"); + Log::channel('telepagos')->warning('Telepagos webhook: Expected exactly one matching purchase.', [ + 'client_code' => $client->code, + 'cashin_id' => $cashinId, + 'amount' => $amount, + 'matches' => $purchases->count(), + ]); return; } } elseif (in_array((int) $operationId, $qrOperationIds, true)) { if (! $qrOrderId) { - Log::warning("Telepagos webhook: qr_order_id not found for QR cashin {$cashinId}"); + Log::channel('telepagos')->warning('Telepagos webhook: qr_order_id not present in provider response.', [ + 'client_code' => $client->code, + 'cashin_id' => $cashinId, + ]); return; } @@ -80,7 +96,11 @@ class TelepagosWebhookService $telepagosQr = TelepagosQr::where('qr_order_id', $qrOrderId)->first(); if (! $telepagosQr) { - Log::warning("Telepagos webhook: QR {$qrOrderId} not found in database for cashin {$cashinId}"); + Log::channel('telepagos')->warning('Telepagos webhook: QR not found in database.', [ + 'client_code' => $client->code, + 'cashin_id' => $cashinId, + 'qr_order_id' => $qrOrderId, + ]); return; } @@ -88,13 +108,21 @@ class TelepagosWebhookService $compra = $telepagosQr->compra; if (! $compra) { - Log::warning("Telepagos webhook: Purchase not found for QR {$qrOrderId}"); + Log::channel('telepagos')->warning('Telepagos webhook: Purchase not found for QR.', [ + 'client_code' => $client->code, + 'cashin_id' => $cashinId, + 'qr_order_id' => $qrOrderId, + ]); return; } if (! $client->tenants()->where('codigo', $compra->tenant_codigo)->exists()) { - Log::warning("Telepagos webhook: Purchase {$compra->id} does not belong to client {$client->code}"); + Log::channel('telepagos')->warning('Telepagos webhook: Purchase does not belong to client.', [ + 'client_code' => $client->code, + 'cashin_id' => $cashinId, + 'purchase_id' => $compra->id, + ]); return; } @@ -102,7 +130,12 @@ class TelepagosWebhookService if (! in_array($compra->status, [ Purchase::STATUS_PENDING_PAYMENT, ], true)) { - Log::warning("Telepagos webhook: Purchase {$compra->id} is not awaiting payment confirmation"); + Log::channel('telepagos')->warning('Telepagos webhook: Purchase is not awaiting payment confirmation.', [ + 'client_code' => $client->code, + 'cashin_id' => $cashinId, + 'purchase_id' => $compra->id, + 'purchase_status' => $compra->status, + ]); return; } @@ -110,12 +143,22 @@ class TelepagosWebhookService $totalAmount = $this->normalizeAmount($compra->getTotalAmount()); if ($amount !== $totalAmount) { - Log::warning("Telepagos webhook: Amount mismatch. Cashin amount: {$amount}, Purchase amount: {$totalAmount}"); + Log::channel('telepagos')->warning('Telepagos webhook: Amount mismatch.', [ + 'client_code' => $client->code, + 'cashin_id' => $cashinId, + 'purchase_id' => $compra->id, + 'cashin_amount' => $amount, + 'purchase_amount' => $totalAmount, + ]); return; } } else { - Log::warning("Telepagos webhook: Unknown operation_id {$operationId} for cashin {$cashinId}"); + Log::channel('telepagos')->warning('Telepagos webhook: Unknown operation_id.', [ + 'client_code' => $client->code, + 'cashin_id' => $cashinId, + 'operation_id' => $operationId, + ]); return; } @@ -138,9 +181,18 @@ class TelepagosWebhookService $this->checkoutService->confirmPaidPurchase($compra); }); - Log::info("Telepagos webhook: Successfully processed cashin {$cashinId} for purchase {$compra->id}"); + Log::channel('telepagos')->info('Telepagos webhook processed successfully.', [ + 'client_code' => $client->code, + 'cashin_id' => $cashinId, + 'purchase_id' => $compra->id, + 'transaction_id' => $paymentData['transaction_id'], + ]); } catch (Exception $e) { - Log::error('Telepagos webhook error: '.$e->getMessage()); + Log::channel('telepagos')->error('Telepagos webhook processing failed.', [ + 'client_code' => $client->code, + 'cashin_id' => $cashinId, + 'error' => $e->getMessage(), + ]); throw $e; } } diff --git a/app/Domains/Integration/documentacion/README.md b/app/Domains/Integration/documentacion/README.md index 91a807f..64e71ac 100644 --- a/app/Domains/Integration/documentacion/README.md +++ b/app/Domains/Integration/documentacion/README.md @@ -24,6 +24,10 @@ Gestiona integraciones externas disponibles y su configuración por cliente. Un - Consulta y configuración por cliente bajo `/clients/{client}/integrations`. - `POST /webhooks/telepagos/{client}` para notificaciones del proveedor. +## Logging de Telepagos + +Los eventos de autenticación, QR, consultas de cuenta y procesamiento de webhooks se escriben en el canal diario `telepagos`, separado del log general. Los archivos se generan en `storage/logs/telepagos-YYYY-MM-DD.log`; el nivel y la retención se configuran con `TELEPAGOS_LOG_LEVEL` y `TELEPAGOS_LOG_DAYS`. Tokens y credenciales se eliminan del contexto antes de registrar respuestas del proveedor. + ## Dependencias y reglas Se integra con `Client`, `Tenant` y con el checkout de `Purchase`. `Notification` utiliza `MailService`. El tenant conserva el contexto operativo y de branding, pero nunca es dueño de credenciales. Las credenciales no se exponen en respuestas ni logs; los webhooks deben validar su contrato antes de alterar una compra. diff --git a/app/Domains/Purchase/Controllers/PurchaseController.php b/app/Domains/Purchase/Controllers/PurchaseController.php index 5439563..fbb56f4 100644 --- a/app/Domains/Purchase/Controllers/PurchaseController.php +++ b/app/Domains/Purchase/Controllers/PurchaseController.php @@ -163,6 +163,11 @@ class PurchaseController extends Controller try { $accountInfo = $telepagosService->getAccountInfo(); + Log::channel('telepagos')->info('Telepagos account information retrieved.', [ + 'tenant_code' => $tenant->codigo, + 'purchase_id' => $compra->id, + ]); + return response()->json([ 'payment_method' => 'transfer', 'transfer_data' => [ @@ -174,9 +179,10 @@ class PurchaseController extends Controller ], ]); } catch (\Exception $e) { - Log::error('Unable to retrieve TelePagos account information.', [ + Log::channel('telepagos')->error('Unable to retrieve TelePagos account information.', [ + 'tenant_code' => $tenant->codigo, 'purchase_id' => $compra->id, - 'exception' => $e, + 'error' => $e->getMessage(), ]); return response()->json([ @@ -191,7 +197,11 @@ class PurchaseController extends Controller $telepagosService->forTenant($tenant->codigo); try { - Log::info("Generating QR for purchase ID: {$compra->id}, amount: {$totalAmount}"); + Log::channel('telepagos')->info('Generating Telepagos QR.', [ + 'tenant_code' => $tenant->codigo, + 'purchase_id' => $compra->id, + 'amount' => $totalAmount, + ]); $qrResponse = $telepagosService->generateQr( $totalAmount, 'Compra', @@ -202,10 +212,17 @@ class PurchaseController extends Controller 'qr_order_id' => (string) ($qrResponse['qr_order_id'] ?? ''), 'qr_code' => $qrResponse['qr_code'] ?? '', ]); - } catch (\Exception $e) { - Log::error('Unable to generate TelePagos QR code.', [ + + Log::channel('telepagos')->info('Telepagos QR generated successfully.', [ + 'tenant_code' => $tenant->codigo, 'purchase_id' => $compra->id, - 'exception' => $e, + 'qr_order_id' => $telepagosQr->qr_order_id, + ]); + } catch (\Exception $e) { + Log::channel('telepagos')->error('Unable to generate TelePagos QR code.', [ + 'tenant_code' => $tenant->codigo, + 'purchase_id' => $compra->id, + 'error' => $e->getMessage(), ]); return response()->json([ diff --git a/config/logging.php b/config/logging.php index b09cb25..d716938 100644 --- a/config/logging.php +++ b/config/logging.php @@ -18,7 +18,7 @@ return [ | */ - 'default' => env('LOG_CHANNEL', 'stack'), + 'default' => env('LOG_CHANNEL', 'daily'), /* |-------------------------------------------------------------------------- @@ -73,6 +73,14 @@ return [ 'replace_placeholders' => true, ], + 'telepagos' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/telepagos.log'), + 'level' => env('TELEPAGOS_LOG_LEVEL', 'info'), + 'days' => env('TELEPAGOS_LOG_DAYS', 30), + 'replace_placeholders' => true, + ], + 'slack' => [ 'driver' => 'slack', 'url' => env('LOG_SLACK_WEBHOOK_URL'), diff --git a/tests/Feature/Integration/IntegrationServiceTest.php b/tests/Feature/Integration/IntegrationServiceTest.php index a3f8ab6..b14440c 100644 --- a/tests/Feature/Integration/IntegrationServiceTest.php +++ b/tests/Feature/Integration/IntegrationServiceTest.php @@ -285,15 +285,21 @@ class IntegrationServiceTest extends TestCase 'https://api.telepagos.com.ar/v2/auth/token' => Http::response([ 'status' => 'error', 'message' => 'Invalid credentials', + 'token' => 'must-not-be-logged', ], 401), ]); + Log::shouldReceive('channel')->once()->with('telepagos')->andReturnSelf(); Log::shouldReceive('error') ->once() ->with('Telepagos authentication failed: Invalid credentials', \Mockery::on(function ($context) { - return $context['username'] === 'tele_user' + return ! array_key_exists('username', $context) && $context['response_status'] === 401 - && $context['response_body'] === ['status' => 'error', 'message' => 'Invalid credentials']; + && $context['response_body'] === [ + 'status' => 'error', + 'message' => 'Invalid credentials', + 'token' => '[REDACTED]', + ]; })); $service = new TelepagosIntegrationService('telepagos'); @@ -442,6 +448,7 @@ class IntegrationServiceTest extends TestCase ], 422), ]); + Log::shouldReceive('channel')->once()->with('telepagos')->andReturnSelf(); Log::shouldReceive('error') ->once() ->with('Telepagos QR generation failed: Importe inválido', \Mockery::on(function ($context) { @@ -555,6 +562,7 @@ class IntegrationServiceTest extends TestCase ], 404), ]); + Log::shouldReceive('channel')->once()->with('telepagos')->andReturnSelf(); Log::shouldReceive('error') ->once() ->with('Telepagos get cash-in details failed: Cashin no encontrado', \Mockery::on(function ($context) {