feat: Introduce client management and integration updates

- Added client_id field to StoreTenantRequest and UpdateTenantRequest for tenant management.
- Updated TenantResource to include client_id in the response.
- Created migration to establish clients table and link tenants to clients.
- Migrated existing tenant integrations to client_integrations table.
- Grouped specific tenants under a shared client (OnTicket) in a new migration.
- Updated seeders to reflect new client structure and relationships.
- Adjusted integration configurations to require client instead of tenant.
- Added tests for client integration functionality and ensured existing tests reflect the new client structure.
This commit is contained in:
2026-08-18 16:18:34 -03:00
parent e6785eb5af
commit d73b4d1daf
42 changed files with 931 additions and 392 deletions

View File

@@ -2,15 +2,20 @@
namespace Tests\Feature\Integration;
use App\Domains\Attachable\Enums\AttachmentType;
use App\Domains\Attachable\Models\Attachment;
use App\Domains\Integration\Models\ClientIntegration;
use App\Domains\Integration\Models\Integration;
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;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Tests\TestCase;
class IntegrationServiceTest extends TestCase
{
@@ -23,26 +28,26 @@ class IntegrationServiceTest extends TestCase
parent::setUp();
// Set the integrations secret for tests
config(['services.integrations.secret' => 'base64:' . base64_encode(random_bytes(32))]);
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();
$hdrKey = (string) Str::uuid();
$ftrKey = (string) Str::uuid();
$headerAttachment = \App\Domains\Attachable\Models\Attachment::create([
$headerAttachment = Attachment::create([
'key' => $hdrKey,
'path' => 'tenants/' . $hdrKey . '.png',
'path' => 'tenants/'.$hdrKey.'.png',
'filename' => 'logo_header.png',
'type' => \App\Domains\Attachable\Enums\AttachmentType::Image,
'type' => AttachmentType::Image,
'mime_type' => 'image/png',
]);
$footerAttachment = \App\Domains\Attachable\Models\Attachment::create([
$footerAttachment = Attachment::create([
'key' => $ftrKey,
'path' => 'tenants/' . $ftrKey . '.png',
'path' => 'tenants/'.$ftrKey.'.png',
'filename' => 'logo_footer.png',
'type' => \App\Domains\Attachable\Enums\AttachmentType::Image,
'type' => AttachmentType::Image,
'mime_type' => 'image/png',
]);
@@ -72,7 +77,7 @@ class IntegrationServiceTest extends TestCase
$service->forTenant($this->tenant->codigo);
}
public function test_it_throws_exception_if_tenant_integration_is_not_configured(): void
public function test_it_throws_exception_if_client_integration_is_not_configured(): void
{
// Seed integration but don't configure for tenant
Integration::create([
@@ -82,13 +87,13 @@ class IntegrationServiceTest extends TestCase
'integration_data_schema' => [
'username' => 'required|string',
'password' => 'required|string',
]
],
]);
$service = new TelepagosIntegrationService('telepagos');
$this->expectException(Exception::class);
$this->expectExceptionMessage("Tenant 'test-tenant' does not have integration 'telepagos_homo' configured.");
$this->expectExceptionMessage("Client 'test-tenant' does not have integration 'telepagos_homo' configured.");
$service->forTenant($this->tenant->codigo);
}
@@ -102,16 +107,16 @@ class IntegrationServiceTest extends TestCase
'integration_data_schema' => [
'username' => 'required|string',
'password' => 'required|string',
]
],
]);
TenantIntegration::create([
'tenant_code' => $this->tenant->codigo,
ClientIntegration::create([
'client_id' => $this->tenant->client_id,
'integration_code' => 'telepagos_homo',
'integration_data' => [
'username' => 'user123',
'password' => 'pass123',
]
],
]);
$service = new TelepagosIntegrationService('telepagos');
@@ -131,16 +136,16 @@ class IntegrationServiceTest extends TestCase
'integration_data_schema' => [
'username' => 'required|string',
'password' => 'required|string',
]
],
]);
TenantIntegration::create([
'tenant_code' => $this->tenant->codigo,
ClientIntegration::create([
'client_id' => $this->tenant->client_id,
'integration_code' => 'telepagos_homo',
'integration_data' => [
'username' => 'tele_user',
'password' => 'tele_pass',
]
],
]);
// Mock HTTP response sequence for authentication
@@ -149,13 +154,13 @@ class IntegrationServiceTest extends TestCase
->push([
'status' => 'ok',
'token' => 'mock-jwt-token-123',
'expires_at' => now()->addHour()->toDateTimeString()
'expires_at' => now()->addHour()->toDateTimeString(),
], 200)
->push([
'status' => 'ok',
'token' => 'new-mock-jwt-token',
'expires_at' => now()->addHour()->toDateTimeString()
], 200)
'expires_at' => now()->addHour()->toDateTimeString(),
], 200),
]);
$service = new TelepagosIntegrationService('telepagos');
@@ -181,6 +186,50 @@ class IntegrationServiceTest extends TestCase
Http::assertSentCount(2);
}
public function test_tenants_from_the_same_client_share_configuration_and_cached_token(): void
{
Integration::create([
'integration_code' => 'telepagos_homo',
'name' => 'Telepagos',
'url' => 'https://api.telepagos.com.ar',
'integration_data_schema' => [
'username' => 'required|string',
'password' => 'required|string',
],
]);
ClientIntegration::create([
'client_id' => $this->tenant->client_id,
'integration_code' => 'telepagos_homo',
'integration_data' => [
'username' => 'shared-user',
'password' => 'shared-password',
],
]);
$secondTenant = $this->tenant->replicate()->fill([
'codigo' => 'second-tenant',
'nombre' => 'Second Tenant',
'dominio' => 'second.test.com',
]);
$secondTenant->save();
Http::fake([
'https://api.telepagos.com.ar/v2/auth/token' => Http::response([
'status' => 'ok',
'token' => 'shared-token',
'expires_at' => now()->addHour()->toDateTimeString(),
]),
]);
$firstService = (new TelepagosIntegrationService('telepagos'))->forTenant($this->tenant->codigo);
$secondService = (new TelepagosIntegrationService('telepagos'))->forTenant($secondTenant->codigo);
$this->assertSame('shared-token', $firstService->getToken());
$this->assertSame('shared-token', $secondService->getToken());
Http::assertSentCount(1);
}
public function test_it_throws_exception_if_credentials_are_missing(): void
{
Integration::create([
@@ -190,23 +239,23 @@ class IntegrationServiceTest extends TestCase
'integration_data_schema' => [
'username' => 'required|string',
'password' => 'required|string',
]
],
]);
TenantIntegration::create([
'tenant_code' => $this->tenant->codigo,
ClientIntegration::create([
'client_id' => $this->tenant->client_id,
'integration_code' => 'telepagos_homo',
'integration_data' => [
'username' => '',
'password' => 'tele_pass',
]
],
]);
$service = new TelepagosIntegrationService('telepagos');
$service->forTenant($this->tenant->codigo);
$this->expectException(Exception::class);
$this->expectExceptionMessage("Missing username or password in Telepagos integration settings.");
$this->expectExceptionMessage('Missing username or password in Telepagos integration settings.');
$service->getToken();
}
@@ -220,26 +269,26 @@ class IntegrationServiceTest extends TestCase
'integration_data_schema' => [
'username' => 'required|string',
'password' => 'required|string',
]
],
]);
TenantIntegration::create([
'tenant_code' => $this->tenant->codigo,
ClientIntegration::create([
'client_id' => $this->tenant->client_id,
'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)
'message' => 'Invalid credentials',
], 401),
]);
\Illuminate\Support\Facades\Log::shouldReceive('error')
Log::shouldReceive('error')
->once()
->with('Telepagos authentication failed: Invalid credentials', \Mockery::on(function ($context) {
return $context['username'] === 'tele_user'
@@ -251,7 +300,7 @@ class IntegrationServiceTest extends TestCase
$service->forTenant($this->tenant->codigo);
$this->expectException(Exception::class);
$this->expectExceptionMessage("Telepagos authentication failed: Invalid credentials");
$this->expectExceptionMessage('Telepagos authentication failed: Invalid credentials');
$service->getToken();
}
@@ -265,28 +314,28 @@ class IntegrationServiceTest extends TestCase
'integration_data_schema' => [
'username' => 'required|string',
'password' => 'required|string',
]
],
]);
TenantIntegration::create([
'tenant_code' => $this->tenant->codigo,
ClientIntegration::create([
'client_id' => $this->tenant->client_id,
'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' => 'ok',
'token' => 'mock-jwt-token-123',
'expires_at' => now()->addHour()->toDateTimeString()
'expires_at' => now()->addHour()->toDateTimeString(),
], 200),
'https://api.telepagos.com.ar/v1/payments' => Http::response([
'status' => 'success',
'payment_id' => 999
], 200)
'payment_id' => 999,
], 200),
]);
$service = new TelepagosIntegrationService('telepagos');
@@ -294,7 +343,7 @@ class IntegrationServiceTest extends TestCase
// Get configured client and perform GET request
$client = $service->client();
$this->assertInstanceOf(\Illuminate\Http\Client\PendingRequest::class, $client);
$this->assertInstanceOf(PendingRequest::class, $client);
$response = $client->get('/v1/payments');
@@ -317,29 +366,29 @@ class IntegrationServiceTest extends TestCase
'integration_data_schema' => [
'username' => 'required|string',
'password' => 'required|string',
]
],
]);
TenantIntegration::create([
'tenant_code' => $this->tenant->codigo,
ClientIntegration::create([
'client_id' => $this->tenant->client_id,
'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' => 'ok',
'token' => 'mock-jwt-token-123',
'expires_at' => now()->addHour()->toDateTimeString()
'expires_at' => now()->addHour()->toDateTimeString(),
], 200),
'https://api.telepagos.com.ar/v2/payment/cashin/qr/generate' => Http::response([
'status' => 'ok',
'qr_code' => 'mock-qr-code-data',
'qr_order_id' => 6353
], 200)
'qr_order_id' => 6353,
], 200),
]);
$service = new TelepagosIntegrationService('telepagos');
@@ -369,31 +418,31 @@ class IntegrationServiceTest extends TestCase
'integration_data_schema' => [
'username' => 'required|string',
'password' => 'required|string',
]
],
]);
TenantIntegration::create([
'tenant_code' => $this->tenant->codigo,
ClientIntegration::create([
'client_id' => $this->tenant->client_id,
'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' => 'ok',
'token' => 'mock-jwt-token-123',
'expires_at' => now()->addHour()->toDateTimeString()
'expires_at' => now()->addHour()->toDateTimeString(),
], 200),
'https://api.telepagos.com.ar/v2/payment/cashin/qr/generate' => Http::response([
'status' => 'error',
'message' => 'Importe inválido'
], 422)
'message' => 'Importe inválido',
], 422),
]);
\Illuminate\Support\Facades\Log::shouldReceive('error')
Log::shouldReceive('error')
->once()
->with('Telepagos QR generation failed: Importe inválido', \Mockery::on(function ($context) {
return $context['amount'] === 1200.00
@@ -407,7 +456,7 @@ class IntegrationServiceTest extends TestCase
$service->forTenant($this->tenant->codigo);
$this->expectException(Exception::class);
$this->expectExceptionMessage("Telepagos QR generation failed: Importe inválido");
$this->expectExceptionMessage('Telepagos QR generation failed: Importe inválido');
$service->generateQr(1200.00, 'Test Concept', 'Test Description');
}
@@ -421,29 +470,29 @@ class IntegrationServiceTest extends TestCase
'integration_data_schema' => [
'username' => 'required|string',
'password' => 'required|string',
]
],
]);
TenantIntegration::create([
'tenant_code' => $this->tenant->codigo,
ClientIntegration::create([
'client_id' => $this->tenant->client_id,
'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' => 'ok',
'token' => 'mock-jwt-token-123',
'expires_at' => now()->addHour()->toDateTimeString()
'expires_at' => now()->addHour()->toDateTimeString(),
], 200),
'https://api.telepagos.com.ar/v2/payment/cashin/6351' => Http::response([
'status' => 'ok',
'buyer' => [
'cuit' => '20416561398',
'cvu' => '0000124900000000011974'
'cvu' => '0000124900000000011974',
],
'amount' => 1200,
'concept' => 'VAR',
@@ -452,8 +501,8 @@ class IntegrationServiceTest extends TestCase
'description' => 'Pago prueba',
'transaction_id' => '2026070364',
'qr_order_id' => 6351,
'link_id' => null
], 200)
'link_id' => null,
], 200),
]);
$service = new TelepagosIntegrationService('telepagos');
@@ -482,31 +531,31 @@ class IntegrationServiceTest extends TestCase
'integration_data_schema' => [
'username' => 'required|string',
'password' => 'required|string',
]
],
]);
TenantIntegration::create([
'tenant_code' => $this->tenant->codigo,
ClientIntegration::create([
'client_id' => $this->tenant->client_id,
'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' => 'ok',
'token' => 'mock-jwt-token-123',
'expires_at' => now()->addHour()->toDateTimeString()
'expires_at' => now()->addHour()->toDateTimeString(),
], 200),
'https://api.telepagos.com.ar/v2/payment/cashin/6351' => Http::response([
'status' => 'error',
'message' => 'Cashin no encontrado'
], 404)
'message' => 'Cashin no encontrado',
], 404),
]);
\Illuminate\Support\Facades\Log::shouldReceive('error')
Log::shouldReceive('error')
->once()
->with('Telepagos get cash-in details failed: Cashin no encontrado', \Mockery::on(function ($context) {
return $context['cashin_id'] === 6351
@@ -518,7 +567,7 @@ class IntegrationServiceTest extends TestCase
$service->forTenant($this->tenant->codigo);
$this->expectException(Exception::class);
$this->expectExceptionMessage("Telepagos get cash-in details failed: Cashin no encontrado");
$this->expectExceptionMessage('Telepagos get cash-in details failed: Cashin no encontrado');
$service->getCashinDetails(6351);
}