diff --git a/app/Domains/Client/Controllers/ClientController.php b/app/Domains/Client/Controllers/ClientController.php new file mode 100644 index 0000000..1ab9da3 --- /dev/null +++ b/app/Domains/Client/Controllers/ClientController.php @@ -0,0 +1,47 @@ +with('tenants')->latest()->paginateFromRequest() + )->response(); + } + + public function store(StoreClientRequest $request): JsonResponse + { + return ClientResource::make( + Client::query()->create($request->validated())->load('tenants') + )->response()->setStatusCode(201); + } + + public function show(Client $client): ClientResource + { + return ClientResource::make($client->load('tenants')); + } + + public function update(UpdateClientRequest $request, Client $client): ClientResource + { + $client->update($request->validated()); + + return ClientResource::make($client->fresh()->load('tenants')); + } + + public function destroy(Client $client): Response + { + $client->delete(); + + return response()->noContent(); + } +} diff --git a/app/Domains/Client/Models/Client.php b/app/Domains/Client/Models/Client.php new file mode 100644 index 0000000..e0bf9e1 --- /dev/null +++ b/app/Domains/Client/Models/Client.php @@ -0,0 +1,30 @@ + */ + public function tenants(): HasMany + { + return $this->hasMany(Tenant::class); + } + + /** @return HasMany */ + public function integrations(): HasMany + { + return $this->hasMany(ClientIntegration::class); + } +} diff --git a/app/Domains/Client/Requests/StoreClientRequest.php b/app/Domains/Client/Requests/StoreClientRequest.php new file mode 100644 index 0000000..b0ab979 --- /dev/null +++ b/app/Domains/Client/Requests/StoreClientRequest.php @@ -0,0 +1,22 @@ + ['required', 'string', 'max:255', Rule::unique('clients', 'code')], + 'name' => ['required', 'string', 'max:255'], + ]; + } +} diff --git a/app/Domains/Client/Requests/UpdateClientRequest.php b/app/Domains/Client/Requests/UpdateClientRequest.php new file mode 100644 index 0000000..513376b --- /dev/null +++ b/app/Domains/Client/Requests/UpdateClientRequest.php @@ -0,0 +1,26 @@ +route('client'); + + return [ + 'code' => ['sometimes', 'string', 'max:255', Rule::unique('clients', 'code')->ignore($client?->id)], + 'name' => ['sometimes', 'string', 'max:255'], + ]; + } +} diff --git a/app/Domains/Client/Resources/ClientResource.php b/app/Domains/Client/Resources/ClientResource.php new file mode 100644 index 0000000..caf724e --- /dev/null +++ b/app/Domains/Client/Resources/ClientResource.php @@ -0,0 +1,26 @@ + $this->id, + 'code' => $this->code, + 'name' => $this->name, + 'tenants' => $this->whenLoaded('tenants', fn () => $this->tenants->map(fn ($tenant): array => [ + 'id' => $tenant->id, + 'codigo' => $tenant->codigo, + 'nombre' => $tenant->nombre, + 'dominio' => $tenant->dominio, + ])), + ]; + } +} diff --git a/app/Domains/Client/routes/api.php b/app/Domains/Client/routes/api.php new file mode 100644 index 0000000..ee8461c --- /dev/null +++ b/app/Domains/Client/routes/api.php @@ -0,0 +1,6 @@ +json($this->clientIntegrationService->getAllForClient($client)); + } + + public function show(Client $client, string $integrationCode): JsonResponse + { + $integration = $this->clientIntegrationService->getClientIntegration($client, $integrationCode); + + if (! $integration) { + return response()->json([ + 'code' => 'integration.not_configured', + 'message' => __('api.integration.not_configured'), + ], 404); + } + + return response()->json($integration); + } + + public function store( + StoreClientIntegrationRequest $request, + Client $client, + string $integrationCode, + ): JsonResponse { + $integration = Integration::query() + ->where('integration_code', $integrationCode) + ->firstOrFail(); + + try { + $this->clientIntegrationService->updateOrCreateIntegration( + $client, + $integration, + $request->input('integration_data', []), + ); + + return response()->json([ + 'code' => 'integration.configured', + 'message' => __('api.integration.configured'), + ]); + } catch (\Exception $exception) { + return response()->json([ + 'code' => 'integration.validation_failed', + 'message' => __('api.integration.validation_failed', ['error' => $exception->getMessage()]), + ], 400); + } + } +} diff --git a/app/Domains/Integration/Controllers/TelepagosWebhookController.php b/app/Domains/Integration/Controllers/TelepagosWebhookController.php index 47c3a5b..b0e90a6 100644 --- a/app/Domains/Integration/Controllers/TelepagosWebhookController.php +++ b/app/Domains/Integration/Controllers/TelepagosWebhookController.php @@ -2,6 +2,7 @@ namespace App\Domains\Integration\Controllers; +use App\Domains\Client\Models\Client; use App\Domains\Integration\Requests\TelepagosWebhookRequest; use App\Domains\Integration\Services\TelepagosWebhookService; use App\Http\Controllers\Controller; @@ -12,12 +13,12 @@ class TelepagosWebhookController extends Controller /** * Handle the incoming Telepagos webhook. */ - public function handle(TelepagosWebhookRequest $request, string $tenantCodigo, TelepagosWebhookService $service): JsonResponse + public function handle(TelepagosWebhookRequest $request, Client $client, TelepagosWebhookService $service): JsonResponse { try { $cashinId = $request->validated('id'); - $service->handleWebhook($tenantCodigo, $cashinId); + $service->handleWebhook($client, $cashinId); return response()->json(['status' => 'success']); } catch (\Exception $e) { diff --git a/app/Domains/Integration/Controllers/TenantIntegrationController.php b/app/Domains/Integration/Controllers/TenantIntegrationController.php deleted file mode 100644 index b89f1b8..0000000 --- a/app/Domains/Integration/Controllers/TenantIntegrationController.php +++ /dev/null @@ -1,60 +0,0 @@ -tenantIntegrationService = $tenantIntegrationService; - } - - public function index(string $tenantCode) - { - return response()->json($this->tenantIntegrationService->getAllForTenant($tenantCode)); - } - - public function show(string $tenantCode, string $integrationCode) - { - $integration = $this->tenantIntegrationService->getTenantIntegration($tenantCode, $integrationCode); - - if (! $integration) { - return response()->json([ - 'code' => 'integration.not_configured', - 'message' => __('api.integration.not_configured'), - ], 404); - } - - return response()->json($integration); - } - - public function store(StoreTenantIntegrationRequest $request, string $tenantCode, string $integrationCode) - { - $integration = Integration::where('integration_code', $integrationCode)->firstOrFail(); - - try { - $this->tenantIntegrationService->updateOrCreateIntegration( - $tenantCode, - $integration, - $request->input('integration_data', []) - ); - - return response()->json([ - 'code' => 'integration.configured', - 'message' => __('api.integration.configured'), - ]); - } catch (\Exception $e) { - return response()->json([ - 'code' => 'integration.validation_failed', - 'message' => __('api.integration.validation_failed', ['error' => $e->getMessage()]), - ], 400); - } - } -} diff --git a/app/Domains/Integration/Models/TenantIntegration.php b/app/Domains/Integration/Models/ClientIntegration.php similarity index 50% rename from app/Domains/Integration/Models/TenantIntegration.php rename to app/Domains/Integration/Models/ClientIntegration.php index 29d7862..676bddb 100644 --- a/app/Domains/Integration/Models/TenantIntegration.php +++ b/app/Domains/Integration/Models/ClientIntegration.php @@ -2,16 +2,18 @@ namespace App\Domains\Integration\Models; +use App\Domains\Client\Models\Client; use App\Domains\Integration\Casts\EncryptedIntegrationData; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; -class TenantIntegration extends Model +class ClientIntegration extends Model { - protected $table = 'tenant_integration'; + protected $hidden = ['integration_data']; protected $fillable = [ + 'client_id', 'integration_code', - 'tenant_code', 'integration_data', ]; @@ -19,7 +21,14 @@ class TenantIntegration extends Model 'integration_data' => EncryptedIntegrationData::class, ]; - public function integration() + /** @return BelongsTo */ + public function client(): BelongsTo + { + return $this->belongsTo(Client::class); + } + + /** @return BelongsTo */ + public function integration(): BelongsTo { return $this->belongsTo(Integration::class, 'integration_code', 'integration_code'); } diff --git a/app/Domains/Integration/Models/Integration.php b/app/Domains/Integration/Models/Integration.php index 04a08f6..c384cbf 100644 --- a/app/Domains/Integration/Models/Integration.php +++ b/app/Domains/Integration/Models/Integration.php @@ -13,16 +13,16 @@ class Integration extends Model 'name', 'url', 'integration_data_schema', - 'requires_tenant_configuration', + 'requires_client_configuration', ]; protected $casts = [ 'integration_data_schema' => 'array', - 'requires_tenant_configuration' => 'boolean', + 'requires_client_configuration' => 'boolean', ]; - - public function tenantIntegrations() + + public function clientIntegrations() { - return $this->hasMany(TenantIntegration::class, 'integration_code', 'integration_code'); + return $this->hasMany(ClientIntegration::class, 'integration_code', 'integration_code'); } } diff --git a/app/Domains/Integration/Requests/StoreTenantIntegrationRequest.php b/app/Domains/Integration/Requests/StoreClientIntegrationRequest.php similarity index 56% rename from app/Domains/Integration/Requests/StoreTenantIntegrationRequest.php rename to app/Domains/Integration/Requests/StoreClientIntegrationRequest.php index ddddd02..40cd43c 100644 --- a/app/Domains/Integration/Requests/StoreTenantIntegrationRequest.php +++ b/app/Domains/Integration/Requests/StoreClientIntegrationRequest.php @@ -6,7 +6,7 @@ use App\Domains\Integration\Models\Integration; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\ValidationException; -class StoreTenantIntegrationRequest extends FormRequest +class StoreClientIntegrationRequest extends FormRequest { protected ?Integration $integrationModel = null; @@ -15,10 +15,12 @@ class StoreTenantIntegrationRequest extends FormRequest return true; } - protected function prepareForValidation() + protected function prepareForValidation(): void { $integrationCode = $this->route('integration_code'); - $this->integrationModel = Integration::where('integration_code', $integrationCode)->first(); + $this->integrationModel = Integration::query() + ->where('integration_code', $integrationCode) + ->first(); if (! $this->integrationModel) { throw ValidationException::withMessages([ @@ -31,11 +33,8 @@ class StoreTenantIntegrationRequest extends FormRequest { $rules = []; - // Dynamic validation rules based on the integration data schema - if ($this->integrationModel && $this->integrationModel->integration_data_schema) { - foreach ($this->integrationModel->integration_data_schema as $field => $rule) { - $rules['integration_data.'.$field] = $rule; - } + foreach ($this->integrationModel?->integration_data_schema ?? [] as $field => $rule) { + $rules['integration_data.'.$field] = $rule; } return $rules; diff --git a/app/Domains/Integration/Requests/StoreIntegrationRequest.php b/app/Domains/Integration/Requests/StoreIntegrationRequest.php index a5f1b99..9fb824d 100644 --- a/app/Domains/Integration/Requests/StoreIntegrationRequest.php +++ b/app/Domains/Integration/Requests/StoreIntegrationRequest.php @@ -18,7 +18,7 @@ class StoreIntegrationRequest extends FormRequest 'name' => ['required', 'string', 'max:255'], 'url' => ['nullable', 'url', 'max:255'], 'integration_data_schema' => ['nullable', 'array'], - 'requires_tenant_configuration' => ['sometimes', 'boolean'], + 'requires_client_configuration' => ['sometimes', 'boolean'], ]; } } diff --git a/app/Domains/Integration/Requests/UpdateIntegrationRequest.php b/app/Domains/Integration/Requests/UpdateIntegrationRequest.php index 40dbfce..210d27e 100644 --- a/app/Domains/Integration/Requests/UpdateIntegrationRequest.php +++ b/app/Domains/Integration/Requests/UpdateIntegrationRequest.php @@ -14,14 +14,14 @@ class UpdateIntegrationRequest extends FormRequest public function rules(): array { $integration = $this->route('integration'); - + return [ 'name' => ['sometimes', 'required', 'string', 'max:255'], 'url' => ['nullable', 'url', 'max:255'], 'integration_data_schema' => ['nullable', 'array'], - 'requires_tenant_configuration' => ['sometimes', 'boolean'], + 'requires_client_configuration' => ['sometimes', 'boolean'], // the code shouldn't ideally be updatable, but if it is: - 'integration_code' => ['sometimes', 'required', 'string', 'unique:integrations,integration_code,' . ($integration->id ?? '')], + 'integration_code' => ['sometimes', 'required', 'string', 'unique:integrations,integration_code,'.($integration->id ?? '')], ]; } } diff --git a/app/Domains/Integration/Services/BaseIntegrationService.php b/app/Domains/Integration/Services/BaseIntegrationService.php index d1354a6..daa638c 100644 --- a/app/Domains/Integration/Services/BaseIntegrationService.php +++ b/app/Domains/Integration/Services/BaseIntegrationService.php @@ -2,58 +2,54 @@ namespace App\Domains\Integration\Services; +use App\Domains\Client\Models\Client; +use App\Domains\Integration\Models\ClientIntegration; use App\Domains\Integration\Models\Integration; -use App\Domains\Integration\Models\TenantIntegration; -use Illuminate\Support\Facades\Http; -use Illuminate\Http\Client\PendingRequest; +use App\Domains\Tenant\Models\Tenant; use Exception; +use Illuminate\Http\Client\PendingRequest; +use Illuminate\Support\Facades\Http; abstract class BaseIntegrationService { /** * The unique code of the integration. - * - * @var string */ protected string $integrationCode; /** * The current tenant code. - * - * @var string */ protected string $tenantCode; + protected ?Tenant $tenant = null; + + protected ?Client $clientContext = null; + /** * The integration model instance. - * - * @var Integration|null */ protected ?Integration $integration = null; /** - * The tenant-specific integration model instance. - * - * @var TenantIntegration|null + * The client-owned integration configuration. */ - protected ?TenantIntegration $tenantIntegration = null; + protected ?ClientIntegration $clientIntegration = null; /** * Set the integration code. * - * @param string $integrationCode * @return $this */ public function setIntegrationCode(string $integrationCode): self { $this->integrationCode = $integrationCode; + return $this; } /** * Get the integration code. - * - * @return string */ public function getIntegrationCode(): string { @@ -63,53 +59,69 @@ abstract class BaseIntegrationService /** * Set the tenant code and load the integration models. * - * @param string $tenantCode * @return $this + * * @throws Exception */ public function forTenant(string $tenantCode): self { $this->tenantCode = $tenantCode; + $this->tenant = Tenant::query()->with('client')->where('codigo', $tenantCode)->firstOrFail(); + $this->clientContext = $this->tenant->client; $this->loadIntegration(); + + return $this; + } + + public function forClient(Client|string $client): self + { + $this->clientContext = $client instanceof Client + ? $client + : Client::query()->where('code', $client)->firstOrFail(); + $this->tenant = null; + $this->loadIntegration(); + return $this; } /** - * Load the Integration and TenantIntegration models. + * Load the integration definition and its client-owned configuration. * * @throws Exception */ protected function loadIntegration(): void { if (empty($this->integrationCode)) { - throw new Exception("Integration code is not set."); + throw new Exception('Integration code is not set.'); } $this->integration = Integration::where('integration_code', $this->integrationCode)->first(); - if (!$this->integration) { + if (! $this->integration) { throw new Exception("Integration with code '{$this->integrationCode}' not found."); } - $this->tenantIntegration = TenantIntegration::where('tenant_code', $this->tenantCode) + if (! $this->clientContext) { + throw new Exception('Client context is not set.'); + } + + $this->clientIntegration = ClientIntegration::where('client_id', $this->clientContext->id) ->where('integration_code', $this->integrationCode) ->first(); - if (!$this->tenantIntegration && $this->integration->requires_tenant_configuration) { - throw new Exception("Tenant '{$this->tenantCode}' does not have integration '{$this->integrationCode}' configured."); + if (! $this->clientIntegration && $this->integration->requires_client_configuration) { + throw new Exception("Client '{$this->clientContext->code}' does not have integration '{$this->integrationCode}' configured."); } } /** * Build the request URL. * - * @param string $path - * @return string * @throws Exception */ public function getUrl(string $path = ''): string { - if (!$this->integration) { - throw new Exception("Integration is not loaded. Call forTenant() first."); + if (! $this->integration) { + throw new Exception('Integration is not loaded. Call forTenant() or forClient() first.'); } $baseUrl = rtrim($this->integration->url, '/'); @@ -119,25 +131,20 @@ abstract class BaseIntegrationService } /** - * Get integration setting by key from tenant's integration data. - * - * @param string $key - * @param mixed $default - * @return mixed + * Get an integration setting from the client-owned configuration. */ protected function getIntegrationSetting(string $key, mixed $default = null): mixed { - if (!$this->tenantIntegration || !$this->tenantIntegration->integration_data) { + if (! $this->clientIntegration || ! $this->clientIntegration->integration_data) { return $default; } - return $this->tenantIntegration->integration_data[$key] ?? $default; + return $this->clientIntegration->integration_data[$key] ?? $default; } /** * Get a pre-configured HTTP client builder. * - * @return PendingRequest * @throws Exception */ public function client(): PendingRequest @@ -148,17 +155,13 @@ abstract class BaseIntegrationService /** * Get the headers for the integration. - * - * @return array */ abstract public function getHeaders(): array; /** - * Hook called after the integration is configured for the tenant. + * Hook called after the integration is configured for the client. * Can be used to validate credentials or perform initial setups. * Throw an Exception on failure. - * - * @return void */ public function onSetup(): void { diff --git a/app/Domains/Integration/Services/ClientIntegrationService.php b/app/Domains/Integration/Services/ClientIntegrationService.php new file mode 100644 index 0000000..1657259 --- /dev/null +++ b/app/Domains/Integration/Services/ClientIntegrationService.php @@ -0,0 +1,55 @@ +integrations() + ->where('integration_code', $integrationCode) + ->first(); + } + + /** @return Collection */ + public function getAllForClient(Client $client): Collection + { + return $client->integrations()->with('integration')->get(); + } + + public function updateOrCreateIntegration( + Client $client, + Integration $integration, + array $data, + ): ClientIntegration { + return DB::transaction(function () use ($client, $integration, $data): ClientIntegration { + $clientIntegration = ClientIntegration::query()->updateOrCreate( + [ + 'client_id' => $client->id, + 'integration_code' => $integration->integration_code, + ], + ['integration_data' => $data], + ); + + $service = $this->resolveService($integration->integration_code); + $service?->forClient($client)->onSetup(); + + return $clientIntegration; + }); + } + + protected function resolveService(string $integrationCode): ?BaseIntegrationService + { + return match ($integrationCode) { + 'email' => new MailService, + 'telepagos', 'telepagos_homo' => new TelepagosIntegrationService($integrationCode), + default => null, + }; + } +} diff --git a/app/Domains/Integration/Services/MailService.php b/app/Domains/Integration/Services/MailService.php index 37a1e1b..6be767d 100644 --- a/app/Domains/Integration/Services/MailService.php +++ b/app/Domains/Integration/Services/MailService.php @@ -2,7 +2,7 @@ namespace App\Domains\Integration\Services; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Client\Models\Client; use Exception; use Illuminate\Contracts\Mail\Factory as MailFactory; use Illuminate\Contracts\Mail\Mailer; @@ -27,9 +27,7 @@ class MailService extends BaseIntegrationService private ?Mailer $mailer = null; - private ?Tenant $tenant = null; - - private bool $usesTenantMailer = false; + private bool $usesClientMailer = false; public function __construct(?MailFactory $mailFactory = null) { @@ -40,16 +38,28 @@ class MailService extends BaseIntegrationService { parent::forTenant($tenantCode); - $this->tenant = Tenant::query() - ->where('codigo', $tenantCode) - ->firstOrFail(); - - if ($this->tenantIntegration) { + if ($this->clientIntegration) { $this->mailer = $this->resolveMailer(); - $this->usesTenantMailer = true; + $this->usesClientMailer = true; } else { $this->mailer = $this->mailFactory->mailer(); - $this->usesTenantMailer = false; + $this->usesClientMailer = false; + } + + return $this; + } + + public function forClient(Client|string $client): self + { + parent::forClient($client); + $this->tenant = $this->clientContext?->tenants()->first(); + + if ($this->clientIntegration) { + $this->mailer = $this->resolveMailer(); + $this->usesClientMailer = true; + } else { + $this->mailer = $this->mailFactory->mailer(); + $this->usesClientMailer = false; } return $this; @@ -63,7 +73,7 @@ class MailService extends BaseIntegrationService public function send(string|array $recipient, string $subject, string $content): void { if (! $this->mailer || ! $this->tenant) { - throw new Exception('MailService no está configurado. Llamá a forTenant() primero.'); + throw new Exception('MailService no está configurado. Llamá a forTenant() o forClient() primero.'); } $this->tenant->loadMissing(['headerLogo', 'footerLogo']); @@ -91,43 +101,50 @@ class MailService extends BaseIntegrationService public function mailerName(): string { - return $this->usesTenantMailer - ? 'tenant-smtp' + return $this->usesClientMailer + ? 'client-smtp' : (string) config('mail.default'); } public function onSetup(): void { - if (! $this->tenant) { - throw new Exception('MailService no está configurado. Llamá a forTenant() primero.'); + if (! $this->mailer || ! $this->clientContext) { + throw new Exception('MailService no está configurado. Llamá a forTenant() o forClient() primero.'); } $recipient = $this->getIntegrationSetting('MAIL_FROM_ADDRESS'); if (! is_string($recipient) || $recipient === '') { - throw new InvalidArgumentException('Falta MAIL_FROM_ADDRESS en la configuración SMTP del tenant.'); + throw new InvalidArgumentException('Falta MAIL_FROM_ADDRESS en la configuración SMTP del cliente.'); } - $this->send( - $recipient, - 'Configuración de correo validada', - '

Configuración de correo validada

' - .'

La integración SMTP de '.e($this->tenant->nombre).' fue configurada correctamente.

' - .'

Este mensaje fue enviado automáticamente para validar las credenciales de correo.

', + $subject = 'Configuración de correo validada'; + $content = '

Configuración de correo validada

' + .'

La integración SMTP de '.e($this->clientContext->name).' fue configurada correctamente.

' + .'

Este mensaje fue enviado automáticamente para validar las credenciales de correo.

'; + + if ($this->tenant) { + $this->send($recipient, $subject, $content); + + return; + } + + $this->mailer->to($recipient)->send( + (new Mailable)->subject($subject)->html($content) ); } private function resolveMailer(): Mailer { - $data = $this->tenantIntegration?->integration_data; + $data = $this->clientIntegration?->integration_data; if (! is_array($data)) { - throw new InvalidArgumentException('La configuración SMTP del tenant no es válida.'); + throw new InvalidArgumentException('La configuración SMTP del cliente no es válida.'); } foreach (self::REQUIRED_SMTP_FIELDS as $field) { if (! array_key_exists($field, $data) || $data[$field] === null || $data[$field] === '') { - throw new InvalidArgumentException("Falta {$field} en la configuración SMTP del tenant."); + throw new InvalidArgumentException("Falta {$field} en la configuración SMTP del cliente."); } } @@ -137,7 +154,7 @@ class MailService extends BaseIntegrationService } $mailer = $this->mailFactory->build([ - 'name' => "tenant-smtp-{$this->tenantCode}", + 'name' => 'client-smtp-'.$this->clientContext?->id, 'transport' => 'smtp', 'scheme' => $data['MAIL_SCHEME'] ?? null, 'host' => $data['MAIL_HOST'], @@ -150,7 +167,7 @@ class MailService extends BaseIntegrationService $mailer->alwaysFrom( $data['MAIL_FROM_ADDRESS'], - $data['MAIL_FROM_NAME'] ?? $this->tenant?->nombre, + $data['MAIL_FROM_NAME'] ?? $this->clientContext?->name, ); return $mailer; diff --git a/app/Domains/Integration/Services/TelepagosIntegrationService.php b/app/Domains/Integration/Services/TelepagosIntegrationService.php index cde949c..a6b917d 100644 --- a/app/Domains/Integration/Services/TelepagosIntegrationService.php +++ b/app/Domains/Integration/Services/TelepagosIntegrationService.php @@ -2,39 +2,37 @@ namespace App\Domains\Integration\Services; +use Carbon\Carbon; use Exception; +use Illuminate\Http\Client\Response; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; -use Carbon\Carbon; class TelepagosIntegrationService extends BaseIntegrationService { /** * TelepagosIntegrationService constructor. - * - * @param string $integrationCode */ public function __construct(string $integrationCode = 'telepagos') { // Force homologation code if not in production and using default - if ($integrationCode === 'telepagos' && !app()->environment('production')) { + if ($integrationCode === 'telepagos' && ! app()->environment('production')) { $integrationCode = 'telepagos_homo'; } - + $this->integrationCode = $integrationCode; } /** * Get the headers for Telepagos integration. * - * @return array * @throws Exception */ public function getHeaders(): array { return [ - 'Authorization' => 'Bearer ' . $this->getToken(), + 'Authorization' => 'Bearer '.$this->getToken(), 'Content-Type' => 'application/json', 'Accept' => 'application/json', ]; @@ -43,16 +41,15 @@ class TelepagosIntegrationService extends BaseIntegrationService /** * 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."); + if (! $this->clientIntegration || ! $this->clientContext) { + throw new Exception('Client integration is not loaded. Call forTenant() or forClient() first.'); } - $cacheKey = "integration_token:{$this->tenantCode}:{$this->integrationCode}"; + $cacheKey = "integration_token:{$this->clientContext->id}:{$this->integrationCode}"; $token = Cache::get($cacheKey); @@ -66,7 +63,6 @@ class TelepagosIntegrationService extends BaseIntegrationService /** * Authenticate with Telepagos and cache the returned token. * - * @return string * @throws Exception */ public function login(): string @@ -75,7 +71,7 @@ class TelepagosIntegrationService extends BaseIntegrationService $password = $this->getIntegrationSetting('password'); if (empty($username) || empty($password)) { - throw new Exception("Missing username or password in Telepagos integration settings."); + throw new Exception('Missing username or password in Telepagos integration settings.'); } $url = $this->getUrl('/v2/auth/token'); @@ -92,15 +88,15 @@ class TelepagosIntegrationService extends BaseIntegrationService $token = $data['token'] ?? null; $expiresAtStr = $data['expires_at'] ?? null; - if (!$token || !$expiresAtStr) { - throw new Exception("Telepagos authentication response is missing token or 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}"; + $cacheKey = "integration_token:{$this->clientContext->id}:{$this->integrationCode}"; Cache::put($cacheKey, $token, $ttlSeconds); return $token; @@ -108,21 +104,16 @@ class TelepagosIntegrationService extends BaseIntegrationService /** * Send a request to Telepagos, handling 401 Unauthorized for token refresh. - * - * @param string $method - * @param string $endpoint - * @param array $data - * @return \Illuminate\Http\Client\Response */ - protected function sendRequest(string $method, string $endpoint, array $data = []): \Illuminate\Http\Client\Response + protected function sendRequest(string $method, string $endpoint, array $data = []): Response { $response = $this->client()->$method($endpoint, $data); if ($response->status() === 401) { - Log::info("Telepagos 401 Unauthorized. Refreshing token and retrying..."); - + Log::info('Telepagos 401 Unauthorized. Refreshing token and retrying...'); + $this->clearToken(); - + $response = $this->client()->$method($endpoint, $data); } @@ -132,10 +123,6 @@ class TelepagosIntegrationService extends BaseIntegrationService /** * Generate a QR code for cash-in. * - * @param float $amount - * @param string $concept - * @param string $description - * @return array * @throws Exception */ public function generateQr(float $amount, string $concept, string $description): array @@ -154,8 +141,8 @@ class TelepagosIntegrationService extends BaseIntegrationService /** * Get the details of a cash-in payment. * - * @param int $cashinId - * @return array + * @param int $cashinId + * * @throws Exception */ public function getCashinDetails(string $cashinId): array @@ -170,25 +157,21 @@ class TelepagosIntegrationService extends BaseIntegrationService /** * Get the account info. * - * @return array * @throws Exception */ public function getAccountInfo(): array { $response = $this->sendRequest('get', '/v2/account/info'); + return $this->handleResponse($response, 'get account info'); } /** * Handle the Telepagos API response, logging any failures and throwing Exceptions. * - * @param \Illuminate\Http\Client\Response $response - * @param string $actionDescription - * @param array $context - * @return array * @throws Exception */ - protected function handleResponse(\Illuminate\Http\Client\Response $response, string $actionDescription, array $context = []): array + protected function handleResponse(Response $response, string $actionDescription, array $context = []): array { if ($response->failed() || $response->json('status') !== 'ok') { $errorMessage = $response->json('message') ?? $response->body(); @@ -205,19 +188,20 @@ class TelepagosIntegrationService extends BaseIntegrationService /** * Clear the cached token. - * - * @return void */ public function clearToken(): void { - $cacheKey = "integration_token:{$this->tenantCode}:{$this->integrationCode}"; + if (! $this->clientContext) { + return; + } + + $cacheKey = "integration_token:{$this->clientContext->id}:{$this->integrationCode}"; Cache::forget($cacheKey); } /** * Perform initial setup validation for Telepagos. * - * @return void * @throws Exception */ public function onSetup(): void diff --git a/app/Domains/Integration/Services/TelepagosWebhookService.php b/app/Domains/Integration/Services/TelepagosWebhookService.php index 821ccee..4fdda97 100644 --- a/app/Domains/Integration/Services/TelepagosWebhookService.php +++ b/app/Domains/Integration/Services/TelepagosWebhookService.php @@ -2,11 +2,11 @@ namespace App\Domains\Integration\Services; +use App\Domains\Client\Models\Client; use App\Domains\Purchase\Models\Purchase; use App\Domains\Purchase\Models\TelepagosPayment; use App\Domains\Purchase\Models\TelepagosQr; use App\Domains\Purchase\Services\CheckoutService; -use App\Domains\Tenant\Models\Tenant; use Exception; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -22,12 +22,10 @@ class TelepagosWebhookService * * @throws Exception */ - public function handleWebhook(string $tenantCodigo, string $cashinId): void + public function handleWebhook(Client $client, string $cashinId): void { - $tenant = Tenant::where('codigo', $tenantCodigo)->firstOrFail(); - $telepagosService = new TelepagosIntegrationService; - $telepagosService->forTenant($tenant->codigo); + $telepagosService->forClient($client); try { $details = $telepagosService->getCashinDetails($cashinId); @@ -52,7 +50,8 @@ class TelepagosWebhookService $dni = substr($cuit, 2, -1); - $compra = Purchase::where('tenant_codigo', $tenantCodigo) + $tenantCodes = $client->tenants()->pluck('codigo'); + $purchases = Purchase::whereIn('tenant_codigo', $tenantCodes) ->where('transfer_payer_dni', $dni) ->whereIn('status', [ Purchase::STATUS_CREATED, @@ -61,10 +60,13 @@ class TelepagosWebhookService ->where('payment_method', 'transfer') ->where('total', $amount) ->latest() - ->first(); + ->limit(2) + ->get(); + + $compra = $purchases->count() === 1 ? $purchases->first() : null; if (! $compra) { - Log::warning("Telepagos webhook: No matching purchase found for DNI {$dni} and amount {$amount} for cashin {$cashinId}"); + Log::warning("Telepagos webhook: Expected one matching purchase for client {$client->code}, DNI {$dni}, amount {$amount} and cashin {$cashinId}; found {$purchases->count()}"); return; } @@ -91,6 +93,12 @@ class TelepagosWebhookService return; } + if (! $client->tenants()->where('codigo', $compra->tenant_codigo)->exists()) { + Log::warning("Telepagos webhook: Purchase {$compra->id} does not belong to client {$client->code}"); + + return; + } + if (! in_array($compra->status, [ Purchase::STATUS_PENDING_PAYMENT, ], true)) { diff --git a/app/Domains/Integration/Services/TenantIntegrationService.php b/app/Domains/Integration/Services/TenantIntegrationService.php deleted file mode 100644 index de3cf1a..0000000 --- a/app/Domains/Integration/Services/TenantIntegrationService.php +++ /dev/null @@ -1,59 +0,0 @@ -where('integration_code', $integrationCode) - ->first(); - } - - public function getAllForTenant(string $tenantCode) - { - return TenantIntegration::with('integration') - ->where('tenant_code', $tenantCode) - ->get(); - } - - public function updateOrCreateIntegration(string $tenantCode, Integration $integration, array $data): TenantIntegration - { - return DB::transaction(function () use ($tenantCode, $integration, $data) { - $tenantIntegration = TenantIntegration::updateOrCreate( - [ - 'tenant_code' => $tenantCode, - 'integration_code' => $integration->integration_code, - ], - [ - 'integration_data' => $data, - ] - ); - - $service = $this->resolveService($integration->integration_code); - if ($service) { - $service->forTenant($tenantCode)->onSetup(); - } - - return $tenantIntegration; - }); - } - - protected function resolveService(string $integrationCode): ?BaseIntegrationService - { - switch ($integrationCode) { - case 'email': - return new MailService; - case 'telepagos': - case 'telepagos_homo': - return new TelepagosIntegrationService($integrationCode); - default: - return null; - } - } -} diff --git a/app/Domains/Integration/documentacion/README.md b/app/Domains/Integration/documentacion/README.md index 0975bb3..91a807f 100644 --- a/app/Domains/Integration/documentacion/README.md +++ b/app/Domains/Integration/documentacion/README.md @@ -2,18 +2,18 @@ ## Propósito -Gestiona integraciones externas disponibles y su configuración por tenant. Incluye correo y pagos mediante Telepagos. +Gestiona integraciones externas disponibles y su configuración por cliente. Un cliente puede agrupar múltiples tenants que comparten las mismas credenciales. Incluye correo y pagos mediante Telepagos. ## Modelo y seguridad - `Integration`: definición global de una integración. -- `TenantIntegration`: configuración y credenciales de una integración para un tenant. +- `ClientIntegration`: configuración y credenciales de una integración para un cliente. - `EncryptedIntegrationData`: cast que protege los datos sensibles persistidos. -- `TenantIntegrationService`: consulta y configura integraciones del tenant. +- `ClientIntegrationService`: consulta y configura integraciones del cliente. ## Servicios externos -- `BaseIntegrationService`: base para resolver configuración, URL y cliente del tenant. +- `BaseIntegrationService`: resuelve el cliente desde el tenant operativo y carga exclusivamente la configuración del cliente. - `MailService`: envío de correo usando la integración configurada. - `TelepagosIntegrationService`: autenticación, caché de token, generación de QR y consulta de cobros. - `TelepagosWebhookService`: procesa notificaciones recibidas desde Telepagos. @@ -21,9 +21,9 @@ Gestiona integraciones externas disponibles y su configuración por tenant. Incl ## Endpoints - CRUD global bajo `/integrations`. -- Consulta y configuración por tenant bajo `/{tenant_code}/integrations`. -- `POST /webhooks/telepagos/{tenant_codigo}` para notificaciones del proveedor. +- Consulta y configuración por cliente bajo `/clients/{client}/integrations`. +- `POST /webhooks/telepagos/{client}` para notificaciones del proveedor. ## Dependencias y reglas -Se integra con `Tenant` y con el checkout de `Purchase`. `Notification` utiliza `MailService`. Las credenciales no deben exponerse 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. diff --git a/app/Domains/Integration/routes/api.php b/app/Domains/Integration/routes/api.php index 910cbda..0fe672c 100644 --- a/app/Domains/Integration/routes/api.php +++ b/app/Domains/Integration/routes/api.php @@ -1,7 +1,8 @@ 'integrations'], function () { @@ -12,10 +13,10 @@ Route::group(['prefix' => 'integrations'], function () { Route::delete('/{integration}', [IntegrationController::class, 'destroy']); }); -Route::group(['prefix' => '{tenant_code}/integrations'], function () { - Route::get('/', [TenantIntegrationController::class, 'index']); - Route::get('/{integration_code}', [TenantIntegrationController::class, 'show']); - Route::post('/{integration_code}', [TenantIntegrationController::class, 'store']); +Route::group(['prefix' => 'clients/{client}/integrations'], function () { + Route::get('/', [ClientIntegrationController::class, 'index']); + Route::get('/{integration_code}', [ClientIntegrationController::class, 'show']); + Route::put('/{integration_code}', [ClientIntegrationController::class, 'store']); }); -Route::post('webhooks/telepagos/{tenant_codigo}', [\App\Domains\Integration\Controllers\TelepagosWebhookController::class, 'handle']); +Route::post('webhooks/telepagos/{client}', [TelepagosWebhookController::class, 'handle']); diff --git a/app/Domains/Tenant/Models/Tenant.php b/app/Domains/Tenant/Models/Tenant.php index dc50201..504e3e7 100644 --- a/app/Domains/Tenant/Models/Tenant.php +++ b/app/Domains/Tenant/Models/Tenant.php @@ -7,6 +7,7 @@ use App\Domains\Catalog\Enums\GroupLayout; use App\Domains\Catalog\Enums\ProductLayout; use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\Category; +use App\Domains\Client\Models\Client; use App\Domains\Event\Models\EventDate; use App\Domains\Menu\Models\Menu; use App\Domains\Menu\Models\TenantMenu; @@ -16,8 +17,10 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Facades\Schema; #[Fillable([ + 'client_id', 'codigo', 'nombre', 'dominio', @@ -64,6 +67,28 @@ class Tenant extends Model return 'codigo'; } + protected static function booted(): void + { + static::creating(function (Tenant $tenant): void { + if ($tenant->client_id !== null || ! Schema::hasTable('clients')) { + return; + } + + $client = Client::query()->firstOrCreate( + ['code' => $tenant->codigo], + ['name' => $tenant->nombre], + ); + + $tenant->client()->associate($client); + }); + } + + /** @return BelongsTo */ + public function client(): BelongsTo + { + return $this->belongsTo(Client::class); + } + public function requiresScannerCategoryValidation(): bool { return $this->scanner_category_validation_enabled; diff --git a/app/Domains/Tenant/Requests/StoreTenantRequest.php b/app/Domains/Tenant/Requests/StoreTenantRequest.php index 2fc1e26..ec158da 100644 --- a/app/Domains/Tenant/Requests/StoreTenantRequest.php +++ b/app/Domains/Tenant/Requests/StoreTenantRequest.php @@ -41,6 +41,7 @@ class StoreTenantRequest extends FormRequest $logoRule = ['required', new ImageOrBase64Rule]; return array_merge([ + 'client_id' => ['sometimes', 'integer', Rule::exists('clients', 'id')], 'codigo' => ['required', 'string', 'max:255', Rule::unique('tenants', 'codigo')], 'nombre' => ['required', 'string', 'max:255'], 'dominio' => [ diff --git a/app/Domains/Tenant/Requests/UpdateTenantRequest.php b/app/Domains/Tenant/Requests/UpdateTenantRequest.php index bc289ba..6061493 100644 --- a/app/Domains/Tenant/Requests/UpdateTenantRequest.php +++ b/app/Domains/Tenant/Requests/UpdateTenantRequest.php @@ -46,6 +46,7 @@ class UpdateTenantRequest extends FormRequest $logoRule = ['nullable', new ImageOrBase64Rule]; return [ + 'client_id' => ['sometimes', 'integer', Rule::exists('clients', 'id')], 'codigo' => [ 'nullable', 'string', diff --git a/app/Domains/Tenant/Resources/TenantResource.php b/app/Domains/Tenant/Resources/TenantResource.php index 08f92c4..b26428b 100644 --- a/app/Domains/Tenant/Resources/TenantResource.php +++ b/app/Domains/Tenant/Resources/TenantResource.php @@ -24,6 +24,7 @@ class TenantResource extends JsonResource { return [ 'id' => $this->id, + 'client_id' => $this->client_id, 'codigo' => $this->codigo, 'nombre' => $this->nombre, 'dominio' => $this->dominio, diff --git a/database/migrations/2026_08_18_040000_create_clients_and_assign_tenants.php b/database/migrations/2026_08_18_040000_create_clients_and_assign_tenants.php new file mode 100644 index 0000000..94f623f --- /dev/null +++ b/database/migrations/2026_08_18_040000_create_clients_and_assign_tenants.php @@ -0,0 +1,50 @@ +id(); + $table->string('code')->unique(); + $table->string('name'); + $table->timestamps(); + }); + + Schema::table('tenants', function (Blueprint $table): void { + $table->foreignId('client_id')->nullable()->after('id')->constrained('clients')->restrictOnDelete(); + }); + + DB::table('tenants') + ->select(['id', 'codigo', 'nombre', 'created_at', 'updated_at']) + ->orderBy('id') + ->each(function (object $tenant): void { + $clientId = DB::table('clients')->insertGetId([ + 'code' => $tenant->codigo, + 'name' => $tenant->nombre, + 'created_at' => $tenant->created_at ?? now(), + 'updated_at' => $tenant->updated_at ?? now(), + ]); + + DB::table('tenants')->where('id', $tenant->id)->update(['client_id' => $clientId]); + }); + + Schema::table('tenants', function (Blueprint $table): void { + $table->unsignedBigInteger('client_id')->nullable(false)->change(); + }); + } + + public function down(): void + { + Schema::table('tenants', function (Blueprint $table): void { + $table->dropConstrainedForeignId('client_id'); + }); + + Schema::dropIfExists('clients'); + } +}; diff --git a/database/migrations/2026_08_18_041000_move_integrations_from_tenants_to_clients.php b/database/migrations/2026_08_18_041000_move_integrations_from_tenants_to_clients.php new file mode 100644 index 0000000..2f54c62 --- /dev/null +++ b/database/migrations/2026_08_18_041000_move_integrations_from_tenants_to_clients.php @@ -0,0 +1,107 @@ +id(); + $table->foreignId('client_id')->constrained('clients')->cascadeOnDelete(); + $table->string('integration_code'); + $table->longText('integration_data')->nullable(); + $table->timestamps(); + + $table->foreign('integration_code') + ->references('integration_code') + ->on('integrations') + ->cascadeOnDelete(); + $table->unique(['client_id', 'integration_code']); + }); + + DB::table('tenant_integration') + ->join('tenants', 'tenants.codigo', '=', 'tenant_integration.tenant_code') + ->select([ + 'tenants.client_id', + 'tenant_integration.integration_code', + 'tenant_integration.integration_data', + 'tenant_integration.created_at', + 'tenant_integration.updated_at', + ]) + ->orderBy('tenant_integration.id') + ->each(function (object $configuration): void { + DB::table('client_integrations')->updateOrInsert( + [ + 'client_id' => $configuration->client_id, + 'integration_code' => $configuration->integration_code, + ], + [ + 'integration_data' => $configuration->integration_data, + 'created_at' => $configuration->created_at, + 'updated_at' => $configuration->updated_at, + ], + ); + }); + + Schema::table('integrations', function (Blueprint $table): void { + $table->boolean('requires_client_configuration')->default(true); + }); + + DB::table('integrations')->update([ + 'requires_client_configuration' => DB::raw('requires_tenant_configuration'), + ]); + + Schema::table('integrations', function (Blueprint $table): void { + $table->dropColumn('requires_tenant_configuration'); + }); + + Schema::dropIfExists('tenant_integration'); + } + + public function down(): void + { + Schema::table('integrations', function (Blueprint $table): void { + $table->boolean('requires_tenant_configuration')->default(true); + }); + + DB::table('integrations')->update([ + 'requires_tenant_configuration' => DB::raw('requires_client_configuration'), + ]); + + Schema::table('integrations', function (Blueprint $table): void { + $table->dropColumn('requires_client_configuration'); + }); + + Schema::create('tenant_integration', function (Blueprint $table): void { + $table->id(); + $table->string('integration_code'); + $table->string('tenant_code'); + $table->longText('integration_data')->nullable(); + $table->timestamps(); + + $table->foreign('integration_code')->references('integration_code')->on('integrations')->cascadeOnDelete(); + $table->foreign('tenant_code')->references('codigo')->on('tenants')->cascadeOnDelete(); + $table->unique(['integration_code', 'tenant_code']); + }); + + DB::table('client_integrations') + ->join('tenants', 'tenants.client_id', '=', 'client_integrations.client_id') + ->select([ + 'tenants.codigo as tenant_code', + 'client_integrations.integration_code', + 'client_integrations.integration_data', + 'client_integrations.created_at', + 'client_integrations.updated_at', + ]) + ->orderBy('client_integrations.id') + ->each(function (object $configuration): void { + DB::table('tenant_integration')->insert((array) $configuration); + }); + + Schema::dropIfExists('client_integrations'); + } +}; diff --git a/database/migrations/2026_08_18_050000_group_onticket_tenants_under_shared_client.php b/database/migrations/2026_08_18_050000_group_onticket_tenants_under_shared_client.php new file mode 100644 index 0000000..2b0fe77 --- /dev/null +++ b/database/migrations/2026_08_18_050000_group_onticket_tenants_under_shared_client.php @@ -0,0 +1,101 @@ +where('code', self::CLIENT_CODE)->value('id'); + + if ($clientId === null) { + $clientId = DB::table('clients')->insertGetId([ + 'code' => self::CLIENT_CODE, + 'name' => 'OnTicket', + 'created_at' => $now, + 'updated_at' => $now, + ]); + } else { + DB::table('clients')->where('id', $clientId)->update([ + 'name' => 'OnTicket', + 'updated_at' => $now, + ]); + } + + $tenants = DB::table('tenants') + ->whereIn('codigo', self::TENANT_CODES) + ->orderByRaw("CASE codigo WHEN 'fiesta_futbol_infantil' THEN 0 ELSE 1 END") + ->get(['codigo', 'client_id']); + + foreach ($tenants as $tenant) { + DB::table('client_integrations') + ->where('client_id', $tenant->client_id) + ->orderBy('id') + ->get() + ->each(function (object $integration) use ($clientId): void { + DB::table('client_integrations')->insertOrIgnore([ + 'client_id' => $clientId, + 'integration_code' => $integration->integration_code, + 'integration_data' => $integration->integration_data, + 'created_at' => $integration->created_at, + 'updated_at' => $integration->updated_at, + ]); + }); + } + + DB::table('tenants') + ->whereIn('codigo', self::TENANT_CODES) + ->update(['client_id' => $clientId]); + } + + public function down(): void + { + foreach (self::TENANT_CODES as $tenantCode) { + $tenant = DB::table('tenants')->where('codigo', $tenantCode)->first(['id']); + + if (! $tenant) { + continue; + } + + DB::table('clients')->insertOrIgnore([ + 'code' => $tenantCode, + 'name' => $tenantCode === 'fiesta_futbol_infantil' + ? 'Fiesta Fútbol Infantil' + : 'Desfile Pura Tendencia', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $individualClientId = DB::table('clients')->where('code', $tenantCode)->value('id'); + $sharedClientId = DB::table('clients')->where('code', self::CLIENT_CODE)->value('id'); + + DB::table('client_integrations') + ->where('client_id', $sharedClientId) + ->orderBy('id') + ->get() + ->each(function (object $integration) use ($individualClientId): void { + DB::table('client_integrations')->insertOrIgnore([ + 'client_id' => $individualClientId, + 'integration_code' => $integration->integration_code, + 'integration_data' => $integration->integration_data, + 'created_at' => $integration->created_at, + 'updated_at' => $integration->updated_at, + ]); + }); + + DB::table('tenants')->where('id', $tenant->id)->update([ + 'client_id' => $individualClientId, + ]); + } + } +}; diff --git a/database/seeders/DesfilePuraTendenciaSeeder.php b/database/seeders/DesfilePuraTendenciaSeeder.php index 1c684ef..553789c 100644 --- a/database/seeders/DesfilePuraTendenciaSeeder.php +++ b/database/seeders/DesfilePuraTendenciaSeeder.php @@ -11,6 +11,7 @@ use App\Domains\Catalog\Models\Attribute; use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\FeaturedGroup; use App\Domains\Catalog\Services\CatalogService; +use App\Domains\Client\Models\Client; use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Services\TenantService; use Illuminate\Database\Seeder; @@ -20,6 +21,8 @@ use RuntimeException; class DesfilePuraTendenciaSeeder extends Seeder { + private const ONTICKET_CLIENT_CODE = 'onticket'; + private const TENANT_CODE = 'desfile_pura_tendencia'; public function __construct( @@ -29,20 +32,30 @@ class DesfilePuraTendenciaSeeder extends Seeder public function run(): void { - if (Tenant::query()->where('codigo', self::TENANT_CODE)->exists()) { + $client = Client::query()->firstOrCreate( + ['code' => self::ONTICKET_CLIENT_CODE], + ['name' => 'OnTicket'], + ); + + $tenant = Tenant::query()->where('codigo', self::TENANT_CODE)->first(); + + if ($tenant) { + $tenant->update(['client_id' => $client->id]); + return; } - DB::transaction(function (): void { - $this->createTenant(); + DB::transaction(function () use ($client): void { + $this->createTenant($client); $this->createEventDate(); $this->createEntryCatalog(); }); } - private function createTenant(): void + private function createTenant(Client $client): void { $this->tenantService->create([ + 'client_id' => $client->id, 'codigo' => self::TENANT_CODE, 'nombre' => 'Desfile Pura Tendencia', 'dominio' => 'desfile-pura-tendencia.localhost', diff --git a/database/seeders/EmailIntegrationSeeder.php b/database/seeders/EmailIntegrationSeeder.php index a7d7a9c..6217bcf 100644 --- a/database/seeders/EmailIntegrationSeeder.php +++ b/database/seeders/EmailIntegrationSeeder.php @@ -17,7 +17,7 @@ class EmailIntegrationSeeder extends Seeder [ 'name' => 'Email', 'url' => null, - 'requires_tenant_configuration' => false, + 'requires_client_configuration' => false, 'integration_data_schema' => [ 'MAIL_MAILER' => 'required|string|in:smtp', 'MAIL_SCHEME' => 'required|string|in:smtp', diff --git a/database/seeders/TelepagosIntegrationSeeder.php b/database/seeders/TelepagosIntegrationSeeder.php index 6e7c917..fad8b4d 100644 --- a/database/seeders/TelepagosIntegrationSeeder.php +++ b/database/seeders/TelepagosIntegrationSeeder.php @@ -17,11 +17,11 @@ class TelepagosIntegrationSeeder extends Seeder [ 'name' => 'Telepagos', 'url' => 'https://api.telepagos.com.ar', - 'requires_tenant_configuration' => true, + 'requires_client_configuration' => true, 'integration_data_schema' => [ 'username' => 'required|string', 'password' => 'required|string', - ] + ], ] ); @@ -30,11 +30,11 @@ class TelepagosIntegrationSeeder extends Seeder [ 'name' => 'Telepagos Homologación', 'url' => 'https://api.homo.telepagos.com.ar', - 'requires_tenant_configuration' => true, + 'requires_client_configuration' => true, 'integration_data_schema' => [ 'username' => 'required|string', 'password' => 'required|string', - ] + ], ] ); } diff --git a/database/seeders/TenantSeeder.php b/database/seeders/TenantSeeder.php index 62be038..ac45a0f 100644 --- a/database/seeders/TenantSeeder.php +++ b/database/seeders/TenantSeeder.php @@ -4,6 +4,7 @@ namespace Database\Seeders; use App\Domains\Attachable\Models\Attachment; use App\Domains\Attachable\Services\AttachmentService; +use App\Domains\Client\Models\Client; use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Services\TenantService; use Illuminate\Database\Seeder; @@ -13,6 +14,8 @@ use Throwable; class TenantSeeder extends Seeder { + private const ONTICKET_CLIENT_CODE = 'onticket'; + private const SOCIAL_MEDIA = [ [ 'code' => 'instagram', @@ -40,6 +43,11 @@ class TenantSeeder extends Seeder public function run(): void { + $onTicketClient = Client::query()->firstOrCreate( + ['code' => self::ONTICKET_CLIENT_CODE], + ['name' => 'OnTicket'], + ); + $this->deleteTenant('sonder'); Tenant::query() @@ -101,6 +109,7 @@ class TenantSeeder extends Seeder ->delete(); $this->tenantService->create([ + 'client_id' => $onTicketClient->id, 'codigo' => 'fiesta_futbol_infantil', 'nombre' => 'Fiesta Fútbol Infantil', 'dominio' => $fiestaDomain, diff --git a/lang/en/api.php b/lang/en/api.php index 9eeb9bc..e632e90 100644 --- a/lang/en/api.php +++ b/lang/en/api.php @@ -73,7 +73,7 @@ return [ 'scanner_category_forbidden' => 'The scanner is not assigned to the ticket category.', ], 'integration' => [ - 'not_configured' => 'The integration is not configured for this tenant.', + 'not_configured' => 'The integration is not configured for this client.', 'configured' => 'Integration configured successfully.', 'validation_failed' => 'Configuration validation failed: :error', 'invalid_payment_method' => 'Invalid payment method.', diff --git a/lang/es/api.php b/lang/es/api.php index a4722e9..c968c0d 100644 --- a/lang/es/api.php +++ b/lang/es/api.php @@ -73,7 +73,7 @@ return [ 'scanner_category_forbidden' => 'El scanner no está asignado a la categoría del ticket.', ], 'integration' => [ - 'not_configured' => 'La integración no está configurada para este tenant.', + 'not_configured' => 'La integración no está configurada para este cliente.', 'configured' => 'Integración configurada correctamente.', 'validation_failed' => 'Error validando la configuración: :error', 'invalid_payment_method' => 'Método de pago inválido.', diff --git a/routes/api.php b/routes/api.php index 9e83804..61a93b5 100644 --- a/routes/api.php +++ b/routes/api.php @@ -9,6 +9,7 @@ require __DIR__.'/../app/Domains/Purchase/routes/api.php'; require __DIR__.'/../app/Domains/Sale/routes/api.php'; require __DIR__.'/../app/Domains/Bootstrap/routes/api.php'; require __DIR__.'/../app/Domains/Tenant/routes/api.php'; +require __DIR__.'/../app/Domains/Client/routes/api.php'; require __DIR__.'/../app/Domains/Integration/routes/api.php'; require __DIR__.'/../app/Domains/Menu/routes/api.php'; require __DIR__.'/../app/Domains/Ticket/routes/api.php'; diff --git a/tests/Feature/Integration/TenantIntegrationControllerTest.php b/tests/Feature/Integration/ClientIntegrationControllerTest.php similarity index 64% rename from tests/Feature/Integration/TenantIntegrationControllerTest.php rename to tests/Feature/Integration/ClientIntegrationControllerTest.php index 744f3f8..fce9b5f 100644 --- a/tests/Feature/Integration/TenantIntegrationControllerTest.php +++ b/tests/Feature/Integration/ClientIntegrationControllerTest.php @@ -2,14 +2,15 @@ namespace Tests\Feature\Integration; +use App\Domains\Client\Models\Client; +use App\Domains\Integration\Models\ClientIntegration; use App\Domains\Integration\Models\Integration; -use App\Domains\Integration\Models\TenantIntegration; -use App\Domains\Integration\Services\TenantIntegrationService; +use App\Domains\Integration\Services\ClientIntegrationService; use Illuminate\Foundation\Testing\RefreshDatabase; use Mockery; use Tests\TestCase; -class TenantIntegrationControllerTest extends TestCase +class ClientIntegrationControllerTest extends TestCase { use RefreshDatabase; @@ -22,19 +23,20 @@ class TenantIntegrationControllerTest extends TestCase 'api_key' => 'required|string', ], ]); + $client = Client::query()->create(['code' => 'test-client', 'name' => 'Test Client']); - $this->mock(TenantIntegrationService::class, function ($mock) use ($integration) { + $this->mock(ClientIntegrationService::class, function ($mock) use ($client, $integration) { $mock->shouldReceive('updateOrCreateIntegration') ->once() ->with( - 'test-tenant', + Mockery::on(fn (Client $argument) => $argument->is($client)), Mockery::on(fn (Integration $argument) => $argument->is($integration)), ['api_key' => 'secret'] ) - ->andReturn(new TenantIntegration); + ->andReturn(new ClientIntegration); }); - $this->postJson('/api/test-tenant/integrations/test_integration', [ + $this->putJson('/api/clients/test-client/integrations/test_integration', [ 'integration_data' => [ 'api_key' => 'secret', ], diff --git a/tests/Feature/Integration/IntegrationServiceTest.php b/tests/Feature/Integration/IntegrationServiceTest.php index 54a24b9..a3f8ab6 100644 --- a/tests/Feature/Integration/IntegrationServiceTest.php +++ b/tests/Feature/Integration/IntegrationServiceTest.php @@ -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); } diff --git a/tests/Feature/Integration/MailServiceTest.php b/tests/Feature/Integration/MailServiceTest.php index fb34a89..622e976 100644 --- a/tests/Feature/Integration/MailServiceTest.php +++ b/tests/Feature/Integration/MailServiceTest.php @@ -4,10 +4,10 @@ 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\ClientIntegrationService; use App\Domains\Integration\Services\MailService; -use App\Domains\Integration\Services\TenantIntegrationService; use App\Domains\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Mail\Mailable; @@ -21,12 +21,12 @@ class MailServiceTest extends TestCase { use RefreshDatabase; - public function test_it_builds_an_isolated_smtp_mailer_from_the_tenant_integration(): void + public function test_it_builds_an_isolated_smtp_mailer_from_the_client_integration(): void { $tenant = $this->createTenant(); $this->createEmailIntegration(); - TenantIntegration::create([ - 'tenant_code' => $tenant->codigo, + ClientIntegration::create([ + 'client_id' => $tenant->client_id, 'integration_code' => 'email', 'integration_data' => $this->emailData(), ]); @@ -40,7 +40,7 @@ class MailServiceTest extends TestCase $manager->shouldReceive('build') ->once() ->with(Mockery::on(fn (array $config): bool => $config === [ - 'name' => 'tenant-smtp-acme', + 'name' => 'client-smtp-'.$tenant->client_id, 'transport' => 'smtp', 'scheme' => 'smtp', 'host' => 'smtp.example.com', @@ -54,10 +54,10 @@ class MailServiceTest extends TestCase $service = (new MailService($manager))->forTenant($tenant->codigo); - $this->assertSame('tenant-smtp', $service->mailerName()); + $this->assertSame('client-smtp', $service->mailerName()); } - public function test_it_uses_the_default_mailer_when_tenant_configuration_is_not_required(): void + public function test_it_uses_the_default_mailer_when_client_configuration_is_not_required(): void { Mail::fake(); config(['mail.default' => 'array']); @@ -65,7 +65,7 @@ class MailServiceTest extends TestCase Integration::create([ 'integration_code' => 'email', 'name' => 'Email', - 'requires_tenant_configuration' => false, + 'requires_client_configuration' => false, ]); $service = (new MailService)->forTenant($tenant->codigo); @@ -84,8 +84,8 @@ class MailServiceTest extends TestCase Mail::fake(); $tenant = $this->createTenant(); $this->createEmailIntegration(); - TenantIntegration::create([ - 'tenant_code' => $tenant->codigo, + ClientIntegration::create([ + 'client_id' => $tenant->client_id, 'integration_code' => 'email', 'integration_data' => $this->emailData(), ]); @@ -109,8 +109,8 @@ class MailServiceTest extends TestCase $tenant = $this->createTenant(); $integration = $this->createEmailIntegration(); - app(TenantIntegrationService::class)->updateOrCreateIntegration( - $tenant->codigo, + app(ClientIntegrationService::class)->updateOrCreateIntegration( + $tenant->client, $integration, $this->emailData(), ); diff --git a/tests/Feature/Integration/TelepagosWebhookTest.php b/tests/Feature/Integration/TelepagosWebhookTest.php index 7fd1162..b0f3f80 100644 --- a/tests/Feature/Integration/TelepagosWebhookTest.php +++ b/tests/Feature/Integration/TelepagosWebhookTest.php @@ -11,8 +11,8 @@ use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\Category; use App\Domains\Catalog\Models\Inventory; use App\Domains\Catalog\Models\Variant; +use App\Domains\Integration\Models\ClientIntegration; use App\Domains\Integration\Models\Integration; -use App\Domains\Integration\Models\TenantIntegration; use App\Domains\Purchase\Models\Purchase; use App\Domains\Purchase\Services\CheckoutService; use App\Domains\Tenant\Models\Tenant; @@ -357,8 +357,8 @@ class TelepagosWebhookTest extends TestCase ], ]); - TenantIntegration::create([ - 'tenant_code' => $tenant->codigo, + ClientIntegration::create([ + 'client_id' => $tenant->client_id, 'integration_code' => 'telepagos_homo', 'integration_data' => [ 'username' => 'user123', diff --git a/tests/Feature/MailTest/MailTestControllerTest.php b/tests/Feature/MailTest/MailTestControllerTest.php index 3f9b8d2..05a3bab 100644 --- a/tests/Feature/MailTest/MailTestControllerTest.php +++ b/tests/Feature/MailTest/MailTestControllerTest.php @@ -4,8 +4,8 @@ namespace Tests\Feature\MailTest; 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\MailTest\Mailables\TestMail; use App\Domains\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -171,8 +171,8 @@ class MailTestControllerTest extends TestCase 'integration_code' => 'email', 'name' => 'Email', ]); - TenantIntegration::create([ - 'tenant_code' => $tenant->codigo, + ClientIntegration::create([ + 'client_id' => $tenant->client_id, 'integration_code' => 'email', 'integration_data' => [ 'MAIL_SCHEME' => 'smtp', diff --git a/tests/Feature/Notification/NotificationMailServiceTest.php b/tests/Feature/Notification/NotificationMailServiceTest.php index 11b5fa5..a343e1e 100644 --- a/tests/Feature/Notification/NotificationMailServiceTest.php +++ b/tests/Feature/Notification/NotificationMailServiceTest.php @@ -35,7 +35,7 @@ class NotificationMailServiceTest extends TestCase 'integration_code' => 'email', 'name' => 'Email', 'url' => null, - 'requires_tenant_configuration' => false, + 'requires_client_configuration' => false, 'integration_data_schema' => [], ]); $header = Attachment::query()->create([