feat(logging): implement dedicated logging for Telepagos events and update logging configuration

This commit is contained in:
2026-08-19 13:37:21 -03:00
parent 16bc657a31
commit d6574bfd0d
7 changed files with 148 additions and 27 deletions

View File

@@ -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

View File

@@ -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.
*/

View File

@@ -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;
}
}

View File

@@ -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.

View File

@@ -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([

View File

@@ -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'),

View File

@@ -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) {