From 387e136707a0dccf05a521032dde3b1667a24bed Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 6 Jul 2026 08:48:40 -0300 Subject: [PATCH] feat: update environment configuration for production and enhance TelepagosIntegrationService with token caching and error handling --- .env.example | 4 +- .../Services/TelepagosIntegrationService.php | 84 ++++++++++++++- .../Integration/IntegrationServiceTest.php | 101 +++++++++++++++--- 3 files changed, 166 insertions(+), 23 deletions(-) diff --git a/.env.example b/.env.example index cfcf1c4..91e9d99 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ APP_NAME=Laravel -APP_ENV=local +APP_ENV=production APP_KEY= -APP_DEBUG=true +APP_DEBUG=false APP_URL=http://localhost FRONTEND_URL=http://localhost:4200 INTEGRATION_SECRET= diff --git a/app/Domains/Integration/Services/TelepagosIntegrationService.php b/app/Domains/Integration/Services/TelepagosIntegrationService.php index 7eaba78..4460f59 100644 --- a/app/Domains/Integration/Services/TelepagosIntegrationService.php +++ b/app/Domains/Integration/Services/TelepagosIntegrationService.php @@ -3,6 +3,9 @@ namespace App\Domains\Integration\Services; use Exception; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Http; +use Carbon\Carbon; class TelepagosIntegrationService extends BaseIntegrationService { @@ -13,6 +16,11 @@ class TelepagosIntegrationService extends BaseIntegrationService */ public function __construct(string $integrationCode = 'telepagos') { + // Force homologation code if not in production and using default + if ($integrationCode === 'telepagos' && !app()->environment('production')) { + $integrationCode = 'telepagos_homo'; + } + $this->integrationCode = $integrationCode; } @@ -23,11 +31,45 @@ class TelepagosIntegrationService extends BaseIntegrationService * @throws Exception */ public function getHeaders(): array + { + return [ + 'Authorization' => 'Bearer ' . $this->getToken(), + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ]; + } + + /** + * Get a valid token, either from cache or by performing a login. + * + * @return string + * @throws Exception + */ + public function getToken(): string { if (!$this->tenantIntegration) { throw new Exception("Tenant integration is not loaded. Call forTenant() first."); } + $cacheKey = "integration_token:{$this->tenantCode}:{$this->integrationCode}"; + + $token = Cache::get($cacheKey); + + if ($token) { + return $token; + } + + return $this->login(); + } + + /** + * Authenticate with Telepagos and cache the returned token. + * + * @return string + * @throws Exception + */ + public function login(): string + { $username = $this->getIntegrationSetting('username'); $password = $this->getIntegrationSetting('password'); @@ -35,10 +77,42 @@ class TelepagosIntegrationService extends BaseIntegrationService throw new Exception("Missing username or password in Telepagos integration settings."); } - return [ - 'Authorization' => 'Basic ' . base64_encode($username . ':' . $password), - 'Content-Type' => 'application/json', - 'Accept' => 'application/json', - ]; + $url = $this->getUrl('/v2/auth/token'); + + $response = Http::post($url, [ + 'username' => $username, + 'password' => $password, + ]); + + if ($response->failed() || $response->json('status') !== 'ok') { + throw new Exception("Telepagos authentication failed: " . ($response->json('message') ?? $response->body())); + } + + $token = $response->json('token'); + $expiresAtStr = $response->json('expires_at'); + + if (!$token || !$expiresAtStr) { + throw new Exception("Telepagos authentication response is missing token or expires_at."); + } + + $expiresAt = Carbon::parse($expiresAtStr); + // Calculate TTL and subtract a buffer of 60 seconds + $ttlSeconds = max(1, $expiresAt->diffInSeconds(now()) - 60); + + $cacheKey = "integration_token:{$this->tenantCode}:{$this->integrationCode}"; + Cache::put($cacheKey, $token, $ttlSeconds); + + return $token; + } + + /** + * Clear the cached token. + * + * @return void + */ + public function clearToken(): void + { + $cacheKey = "integration_token:{$this->tenantCode}:{$this->integrationCode}"; + Cache::forget($cacheKey); } } diff --git a/tests/Feature/Integration/IntegrationServiceTest.php b/tests/Feature/Integration/IntegrationServiceTest.php index c95d831..4e2b9ea 100644 --- a/tests/Feature/Integration/IntegrationServiceTest.php +++ b/tests/Feature/Integration/IntegrationServiceTest.php @@ -7,6 +7,8 @@ use App\Domains\Integration\Models\TenantIntegration; use App\Domains\Integration\Services\TelepagosIntegrationService; use App\Domains\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Cache; use Tests\TestCase; use Exception; @@ -23,6 +25,9 @@ class IntegrationServiceTest extends TestCase // Set the integrations secret for tests config(['services.integrations.secret' => 'base64:' . base64_encode(random_bytes(32))]); + // Clear cache to prevent test pollution + Cache::flush(); + $hdrKey = (string) \Illuminate\Support\Str::uuid(); $ftrKey = (string) \Illuminate\Support\Str::uuid(); @@ -71,7 +76,7 @@ class IntegrationServiceTest extends TestCase { // Seed integration but don't configure for tenant Integration::create([ - 'integration_code' => 'telepagos', + 'integration_code' => 'telepagos_homo', 'name' => 'Telepagos', 'url' => 'https://api.telepagos.com.ar', 'integration_data_schema' => [ @@ -80,10 +85,10 @@ class IntegrationServiceTest extends TestCase ] ]); - $service = new TelepagosIntegrationService(); + $service = new TelepagosIntegrationService('telepagos'); $this->expectException(Exception::class); - $this->expectExceptionMessage("Tenant 'test-tenant' does not have integration 'telepagos' configured."); + $this->expectExceptionMessage("Tenant 'test-tenant' does not have integration 'telepagos_homo' configured."); $service->forTenant($this->tenant->codigo); } @@ -91,7 +96,7 @@ class IntegrationServiceTest extends TestCase public function test_it_resolves_base_url_and_paths(): void { Integration::create([ - 'integration_code' => 'telepagos', + 'integration_code' => 'telepagos_homo', 'name' => 'Telepagos', 'url' => 'https://api.telepagos.com.ar/', // Trailing slash to test trimming 'integration_data_schema' => [ @@ -102,14 +107,14 @@ class IntegrationServiceTest extends TestCase TenantIntegration::create([ 'tenant_code' => $this->tenant->codigo, - 'integration_code' => 'telepagos', + 'integration_code' => 'telepagos_homo', 'integration_data' => [ 'username' => 'user123', 'password' => 'pass123', ] ]); - $service = new TelepagosIntegrationService(); + $service = new TelepagosIntegrationService('telepagos'); $service->forTenant($this->tenant->codigo); $this->assertEquals('https://api.telepagos.com.ar', $service->getUrl()); @@ -117,10 +122,10 @@ class IntegrationServiceTest extends TestCase $this->assertEquals('https://api.telepagos.com.ar/v1/payments', $service->getUrl('/v1/payments')); } - public function test_it_generates_correct_headers_for_telepagos(): void + public function test_it_generates_correct_headers_for_telepagos_using_cached_token(): void { Integration::create([ - 'integration_code' => 'telepagos', + 'integration_code' => 'telepagos_homo', 'name' => 'Telepagos', 'url' => 'https://api.telepagos.com.ar', 'integration_data_schema' => [ @@ -131,28 +136,55 @@ class IntegrationServiceTest extends TestCase TenantIntegration::create([ 'tenant_code' => $this->tenant->codigo, - 'integration_code' => 'telepagos', + 'integration_code' => 'telepagos_homo', 'integration_data' => [ 'username' => 'tele_user', 'password' => 'tele_pass', ] ]); - $service = new TelepagosIntegrationService(); + // Mock HTTP response sequence for authentication + Http::fake([ + 'https://api.telepagos.com.ar/v2/auth/token' => Http::sequence() + ->push([ + 'status' => 'ok', + 'token' => 'mock-jwt-token-123', + 'expires_at' => now()->addHour()->toDateTimeString() + ], 200) + ->push([ + 'status' => 'ok', + 'token' => 'new-mock-jwt-token', + 'expires_at' => now()->addHour()->toDateTimeString() + ], 200) + ]); + + $service = new TelepagosIntegrationService('telepagos'); $service->forTenant($this->tenant->codigo); + // Fetch headers first time (triggers API login) $headers = $service->getHeaders(); - $expectedAuth = 'Basic ' . base64_encode('tele_user:tele_pass'); - $this->assertEquals($expectedAuth, $headers['Authorization']); + $this->assertEquals('Bearer mock-jwt-token-123', $headers['Authorization']); $this->assertEquals('application/json', $headers['Content-Type']); $this->assertEquals('application/json', $headers['Accept']); + + // Assert HTTP call was made once + Http::assertSentCount(1); + + // Retrieve token again, should be same (cached) + $this->assertEquals('mock-jwt-token-123', $service->getToken()); + Http::assertSentCount(1); // Still 1 since it's cached! + + // Clear token, should trigger another API login (sequence returns second token) + $service->clearToken(); + $this->assertEquals('new-mock-jwt-token', $service->getToken()); + Http::assertSentCount(2); } public function test_it_throws_exception_if_credentials_are_missing(): void { Integration::create([ - 'integration_code' => 'telepagos', + 'integration_code' => 'telepagos_homo', 'name' => 'Telepagos', 'url' => 'https://api.telepagos.com.ar', 'integration_data_schema' => [ @@ -163,19 +195,56 @@ class IntegrationServiceTest extends TestCase TenantIntegration::create([ 'tenant_code' => $this->tenant->codigo, - 'integration_code' => 'telepagos', + 'integration_code' => 'telepagos_homo', 'integration_data' => [ 'username' => '', 'password' => 'tele_pass', ] ]); - $service = new TelepagosIntegrationService(); + $service = new TelepagosIntegrationService('telepagos'); $service->forTenant($this->tenant->codigo); $this->expectException(Exception::class); $this->expectExceptionMessage("Missing username or password in Telepagos integration settings."); - $service->getHeaders(); + $service->getToken(); + } + + public function test_it_throws_exception_if_api_fails(): void + { + Integration::create([ + 'integration_code' => 'telepagos_homo', + 'name' => 'Telepagos', + 'url' => 'https://api.telepagos.com.ar', + 'integration_data_schema' => [ + 'username' => 'required|string', + 'password' => 'required|string', + ] + ]); + + TenantIntegration::create([ + 'tenant_code' => $this->tenant->codigo, + 'integration_code' => 'telepagos_homo', + 'integration_data' => [ + 'username' => 'tele_user', + 'password' => 'tele_pass', + ] + ]); + + Http::fake([ + 'https://api.telepagos.com.ar/v2/auth/token' => Http::response([ + 'status' => 'error', + 'message' => 'Invalid credentials' + ], 401) + ]); + + $service = new TelepagosIntegrationService('telepagos'); + $service->forTenant($this->tenant->codigo); + + $this->expectException(Exception::class); + $this->expectExceptionMessage("Telepagos authentication failed: Invalid credentials"); + + $service->getToken(); } }