feat(logging): implement dedicated logging for Telepagos events and update logging configuration
This commit is contained in:
@@ -31,10 +31,13 @@ AUTH_LOGIN_LOCK_MINUTES=15
|
|||||||
AUTH_LOGIN_RATE_LIMIT_PER_MINUTE=10
|
AUTH_LOGIN_RATE_LIMIT_PER_MINUTE=10
|
||||||
AUTH_LOGIN_IP_RATE_LIMIT_PER_MINUTE=30
|
AUTH_LOGIN_IP_RATE_LIMIT_PER_MINUTE=30
|
||||||
|
|
||||||
LOG_CHANNEL=stack
|
LOG_CHANNEL=daily
|
||||||
LOG_STACK=single
|
LOG_STACK=single
|
||||||
LOG_DEPRECATIONS_CHANNEL=null
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
LOG_DAILY_DAYS=14
|
||||||
|
TELEPAGOS_LOG_LEVEL=info
|
||||||
|
TELEPAGOS_LOG_DAYS=30
|
||||||
|
|
||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=mysql
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
|
|||||||
@@ -81,9 +81,7 @@ class TelepagosIntegrationService extends BaseIntegrationService
|
|||||||
'password' => $password,
|
'password' => $password,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$data = $this->handleResponse($response, 'authentication', [
|
$data = $this->handleResponse($response, 'authentication');
|
||||||
'username' => $username,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$token = $data['token'] ?? null;
|
$token = $data['token'] ?? null;
|
||||||
$expiresAtStr = $data['expires_at'] ?? null;
|
$expiresAtStr = $data['expires_at'] ?? null;
|
||||||
@@ -110,7 +108,12 @@ class TelepagosIntegrationService extends BaseIntegrationService
|
|||||||
$response = $this->client()->$method($endpoint, $data);
|
$response = $this->client()->$method($endpoint, $data);
|
||||||
|
|
||||||
if ($response->status() === 401) {
|
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();
|
$this->clearToken();
|
||||||
|
|
||||||
@@ -175,9 +178,11 @@ class TelepagosIntegrationService extends BaseIntegrationService
|
|||||||
{
|
{
|
||||||
if ($response->failed() || $response->json('status') !== 'ok') {
|
if ($response->failed() || $response->json('status') !== 'ok') {
|
||||||
$errorMessage = $response->json('message') ?? $response->body();
|
$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_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));
|
], $context));
|
||||||
|
|
||||||
throw new Exception("Telepagos {$actionDescription} failed: {$errorMessage}");
|
throw new Exception("Telepagos {$actionDescription} failed: {$errorMessage}");
|
||||||
@@ -186,6 +191,30 @@ class TelepagosIntegrationService extends BaseIntegrationService
|
|||||||
return $response->json() ?? [];
|
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.
|
* Clear the cached token.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ class TelepagosWebhookService
|
|||||||
*/
|
*/
|
||||||
public function handleWebhook(Client $client, string $cashinId): void
|
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 = new TelepagosIntegrationService;
|
||||||
$telepagosService->forClient($client);
|
$telepagosService->forClient($client);
|
||||||
|
|
||||||
@@ -43,7 +48,10 @@ class TelepagosWebhookService
|
|||||||
$cuit = $details['data']['buyer']['cuit'] ?? $details['buyer']['cuit'] ?? null;
|
$cuit = $details['data']['buyer']['cuit'] ?? $details['buyer']['cuit'] ?? null;
|
||||||
|
|
||||||
if (! $cuit) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -66,13 +74,21 @@ class TelepagosWebhookService
|
|||||||
$compra = $purchases->count() === 1 ? $purchases->first() : null;
|
$compra = $purchases->count() === 1 ? $purchases->first() : null;
|
||||||
|
|
||||||
if (! $compra) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
} elseif (in_array((int) $operationId, $qrOperationIds, true)) {
|
} elseif (in_array((int) $operationId, $qrOperationIds, true)) {
|
||||||
if (! $qrOrderId) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -80,7 +96,11 @@ class TelepagosWebhookService
|
|||||||
$telepagosQr = TelepagosQr::where('qr_order_id', $qrOrderId)->first();
|
$telepagosQr = TelepagosQr::where('qr_order_id', $qrOrderId)->first();
|
||||||
|
|
||||||
if (! $telepagosQr) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -88,13 +108,21 @@ class TelepagosWebhookService
|
|||||||
$compra = $telepagosQr->compra;
|
$compra = $telepagosQr->compra;
|
||||||
|
|
||||||
if (! $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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $client->tenants()->where('codigo', $compra->tenant_codigo)->exists()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -102,7 +130,12 @@ class TelepagosWebhookService
|
|||||||
if (! in_array($compra->status, [
|
if (! in_array($compra->status, [
|
||||||
Purchase::STATUS_PENDING_PAYMENT,
|
Purchase::STATUS_PENDING_PAYMENT,
|
||||||
], true)) {
|
], 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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -110,12 +143,22 @@ class TelepagosWebhookService
|
|||||||
$totalAmount = $this->normalizeAmount($compra->getTotalAmount());
|
$totalAmount = $this->normalizeAmount($compra->getTotalAmount());
|
||||||
|
|
||||||
if ($amount !== $totalAmount) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} 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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -138,9 +181,18 @@ class TelepagosWebhookService
|
|||||||
$this->checkoutService->confirmPaidPurchase($compra);
|
$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) {
|
} 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;
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`.
|
- Consulta y configuración por cliente bajo `/clients/{client}/integrations`.
|
||||||
- `POST /webhooks/telepagos/{client}` para notificaciones del proveedor.
|
- `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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -163,6 +163,11 @@ class PurchaseController extends Controller
|
|||||||
try {
|
try {
|
||||||
$accountInfo = $telepagosService->getAccountInfo();
|
$accountInfo = $telepagosService->getAccountInfo();
|
||||||
|
|
||||||
|
Log::channel('telepagos')->info('Telepagos account information retrieved.', [
|
||||||
|
'tenant_code' => $tenant->codigo,
|
||||||
|
'purchase_id' => $compra->id,
|
||||||
|
]);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'payment_method' => 'transfer',
|
'payment_method' => 'transfer',
|
||||||
'transfer_data' => [
|
'transfer_data' => [
|
||||||
@@ -174,9 +179,10 @@ class PurchaseController extends Controller
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
} catch (\Exception $e) {
|
} 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,
|
'purchase_id' => $compra->id,
|
||||||
'exception' => $e,
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@@ -191,7 +197,11 @@ class PurchaseController extends Controller
|
|||||||
$telepagosService->forTenant($tenant->codigo);
|
$telepagosService->forTenant($tenant->codigo);
|
||||||
|
|
||||||
try {
|
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(
|
$qrResponse = $telepagosService->generateQr(
|
||||||
$totalAmount,
|
$totalAmount,
|
||||||
'Compra',
|
'Compra',
|
||||||
@@ -202,10 +212,17 @@ class PurchaseController extends Controller
|
|||||||
'qr_order_id' => (string) ($qrResponse['qr_order_id'] ?? ''),
|
'qr_order_id' => (string) ($qrResponse['qr_order_id'] ?? ''),
|
||||||
'qr_code' => $qrResponse['qr_code'] ?? '',
|
'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,
|
'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([
|
return response()->json([
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'default' => env('LOG_CHANNEL', 'stack'),
|
'default' => env('LOG_CHANNEL', 'daily'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@@ -73,6 +73,14 @@ return [
|
|||||||
'replace_placeholders' => true,
|
'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' => [
|
'slack' => [
|
||||||
'driver' => 'slack',
|
'driver' => 'slack',
|
||||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||||
|
|||||||
@@ -285,15 +285,21 @@ class IntegrationServiceTest extends TestCase
|
|||||||
'https://api.telepagos.com.ar/v2/auth/token' => Http::response([
|
'https://api.telepagos.com.ar/v2/auth/token' => Http::response([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Invalid credentials',
|
'message' => 'Invalid credentials',
|
||||||
|
'token' => 'must-not-be-logged',
|
||||||
], 401),
|
], 401),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Log::shouldReceive('channel')->once()->with('telepagos')->andReturnSelf();
|
||||||
Log::shouldReceive('error')
|
Log::shouldReceive('error')
|
||||||
->once()
|
->once()
|
||||||
->with('Telepagos authentication failed: Invalid credentials', \Mockery::on(function ($context) {
|
->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_status'] === 401
|
||||||
&& $context['response_body'] === ['status' => 'error', 'message' => 'Invalid credentials'];
|
&& $context['response_body'] === [
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Invalid credentials',
|
||||||
|
'token' => '[REDACTED]',
|
||||||
|
];
|
||||||
}));
|
}));
|
||||||
|
|
||||||
$service = new TelepagosIntegrationService('telepagos');
|
$service = new TelepagosIntegrationService('telepagos');
|
||||||
@@ -442,6 +448,7 @@ class IntegrationServiceTest extends TestCase
|
|||||||
], 422),
|
], 422),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Log::shouldReceive('channel')->once()->with('telepagos')->andReturnSelf();
|
||||||
Log::shouldReceive('error')
|
Log::shouldReceive('error')
|
||||||
->once()
|
->once()
|
||||||
->with('Telepagos QR generation failed: Importe inválido', \Mockery::on(function ($context) {
|
->with('Telepagos QR generation failed: Importe inválido', \Mockery::on(function ($context) {
|
||||||
@@ -555,6 +562,7 @@ class IntegrationServiceTest extends TestCase
|
|||||||
], 404),
|
], 404),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Log::shouldReceive('channel')->once()->with('telepagos')->andReturnSelf();
|
||||||
Log::shouldReceive('error')
|
Log::shouldReceive('error')
|
||||||
->once()
|
->once()
|
||||||
->with('Telepagos get cash-in details failed: Cashin no encontrado', \Mockery::on(function ($context) {
|
->with('Telepagos get cash-in details failed: Cashin no encontrado', \Mockery::on(function ($context) {
|
||||||
|
|||||||
Reference in New Issue
Block a user