From 3887a4fcba28d89f7f391ce9be360d9c87ed4cfe Mon Sep 17 00:00:00 2001 From: ncoronel Date: Fri, 3 Jul 2026 14:44:18 -0300 Subject: [PATCH] feat: implement integration management with CRUD operations and routes for tenant integration --- .env.example | 1 + .../Casts/EncryptedIntegrationData.php | 66 +++++++++++++++++++ .../Controllers/IntegrationController.php | 42 ++++++++++++ .../TenantIntegrationController.php | 47 +++++++++++++ .../Integration/Models/Integration.php | 25 +++++++ .../Integration/Models/TenantIntegration.php | 26 ++++++++ .../Requests/StoreIntegrationRequest.php | 22 +++++++ .../StoreTenantIntegrationRequest.php | 43 ++++++++++++ .../Requests/UpdateIntegrationRequest.php | 25 +++++++ .../Services/TenantIntegrationService.php | 36 ++++++++++ app/Domains/Integration/routes/api.php | 19 ++++++ app/Domains/Tenant/Models/Tenant.php | 1 + ...07_03_000001_create_integrations_table.php | 30 +++++++++ ...000002_create_tenant_integration_table.php | 36 ++++++++++ .../seeders/TelepagosIntegrationSeeder.php | 26 ++++++++ routes/api.php | 1 + 16 files changed, 446 insertions(+) create mode 100644 app/Domains/Integration/Casts/EncryptedIntegrationData.php create mode 100644 app/Domains/Integration/Controllers/IntegrationController.php create mode 100644 app/Domains/Integration/Controllers/TenantIntegrationController.php create mode 100644 app/Domains/Integration/Models/Integration.php create mode 100644 app/Domains/Integration/Models/TenantIntegration.php create mode 100644 app/Domains/Integration/Requests/StoreIntegrationRequest.php create mode 100644 app/Domains/Integration/Requests/StoreTenantIntegrationRequest.php create mode 100644 app/Domains/Integration/Requests/UpdateIntegrationRequest.php create mode 100644 app/Domains/Integration/Services/TenantIntegrationService.php create mode 100644 app/Domains/Integration/routes/api.php create mode 100644 database/migrations/2026_07_03_000001_create_integrations_table.php create mode 100644 database/migrations/2026_07_03_000002_create_tenant_integration_table.php create mode 100644 database/seeders/TelepagosIntegrationSeeder.php diff --git a/.env.example b/.env.example index a1a70e3..cfcf1c4 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,7 @@ APP_KEY= APP_DEBUG=true APP_URL=http://localhost FRONTEND_URL=http://localhost:4200 +INTEGRATION_SECRET= APP_LOCALE=en APP_FALLBACK_LOCALE=en diff --git a/app/Domains/Integration/Casts/EncryptedIntegrationData.php b/app/Domains/Integration/Casts/EncryptedIntegrationData.php new file mode 100644 index 0000000..1d5db1d --- /dev/null +++ b/app/Domains/Integration/Casts/EncryptedIntegrationData.php @@ -0,0 +1,66 @@ + $attributes + */ + public function get(Model $model, string $key, mixed $value, array $attributes): mixed + { + if ($value === null) { + return null; + } + + try { + $decrypted = $this->getEncrypter()->decryptString($value); + return json_decode($decrypted, true); + } catch (Exception $e) { + // Return null or throw depending on how strict we want to be. + return null; + } + } + + /** + * Prepare the given value for storage. + * + * @param array $attributes + */ + public function set(Model $model, string $key, mixed $value, array $attributes): mixed + { + if ($value === null) { + return null; + } + + $json = json_encode($value); + return $this->getEncrypter()->encryptString($json); + } +} diff --git a/app/Domains/Integration/Controllers/IntegrationController.php b/app/Domains/Integration/Controllers/IntegrationController.php new file mode 100644 index 0000000..907d96d --- /dev/null +++ b/app/Domains/Integration/Controllers/IntegrationController.php @@ -0,0 +1,42 @@ +json(Integration::all()); + } + + public function store(StoreIntegrationRequest $request) + { + $integration = Integration::create($request->validated()); + + return response()->json($integration, 201); + } + + public function show(Integration $integration) + { + return response()->json($integration); + } + + public function update(UpdateIntegrationRequest $request, Integration $integration) + { + $integration->update($request->validated()); + + return response()->json($integration->fresh()); + } + + public function destroy(Integration $integration) + { + $integration->delete(); + + return response()->noContent(); + } +} diff --git a/app/Domains/Integration/Controllers/TenantIntegrationController.php b/app/Domains/Integration/Controllers/TenantIntegrationController.php new file mode 100644 index 0000000..a4ced05 --- /dev/null +++ b/app/Domains/Integration/Controllers/TenantIntegrationController.php @@ -0,0 +1,47 @@ +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(['message' => 'Integration not configured for this tenant'], 404); + } + + return response()->json($integration); + } + + public function store(StoreTenantIntegrationRequest $request, string $tenantCode, string $integrationCode) + { + $integration = Integration::where('integration_code', $integrationCode)->firstOrFail(); + + $tenantIntegration = $this->tenantIntegrationService->updateOrCreateIntegration( + $tenantCode, + $integration, + $request->input('integration_data', []) + ); + + return response()->json($tenantIntegration); + } +} diff --git a/app/Domains/Integration/Models/Integration.php b/app/Domains/Integration/Models/Integration.php new file mode 100644 index 0000000..4d1de62 --- /dev/null +++ b/app/Domains/Integration/Models/Integration.php @@ -0,0 +1,25 @@ + 'array', + ]; + + public function tenantIntegrations() + { + return $this->hasMany(TenantIntegration::class, 'integration_code', 'integration_code'); + } +} diff --git a/app/Domains/Integration/Models/TenantIntegration.php b/app/Domains/Integration/Models/TenantIntegration.php new file mode 100644 index 0000000..29d7862 --- /dev/null +++ b/app/Domains/Integration/Models/TenantIntegration.php @@ -0,0 +1,26 @@ + EncryptedIntegrationData::class, + ]; + + public function integration() + { + return $this->belongsTo(Integration::class, 'integration_code', 'integration_code'); + } +} diff --git a/app/Domains/Integration/Requests/StoreIntegrationRequest.php b/app/Domains/Integration/Requests/StoreIntegrationRequest.php new file mode 100644 index 0000000..4d91605 --- /dev/null +++ b/app/Domains/Integration/Requests/StoreIntegrationRequest.php @@ -0,0 +1,22 @@ + ['required', 'string', 'unique:integrations,integration_code'], + 'name' => ['required', 'string', 'max:255'], + 'integration_data_schema' => ['nullable', 'array'], + ]; + } +} diff --git a/app/Domains/Integration/Requests/StoreTenantIntegrationRequest.php b/app/Domains/Integration/Requests/StoreTenantIntegrationRequest.php new file mode 100644 index 0000000..d6d4d54 --- /dev/null +++ b/app/Domains/Integration/Requests/StoreTenantIntegrationRequest.php @@ -0,0 +1,43 @@ +route('integration_code'); + $this->integrationModel = Integration::where('integration_code', $integrationCode)->first(); + + if (!$this->integrationModel) { + throw ValidationException::withMessages([ + 'integration_code' => 'Integration not found.' + ]); + } + } + + public function rules(): array + { + $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; + } + } + + return $rules; + } +} diff --git a/app/Domains/Integration/Requests/UpdateIntegrationRequest.php b/app/Domains/Integration/Requests/UpdateIntegrationRequest.php new file mode 100644 index 0000000..8883f17 --- /dev/null +++ b/app/Domains/Integration/Requests/UpdateIntegrationRequest.php @@ -0,0 +1,25 @@ +route('integration'); + + return [ + 'name' => ['sometimes', 'required', 'string', 'max:255'], + 'integration_data_schema' => ['nullable', 'array'], + // the code shouldn't ideally be updatable, but if it is: + 'integration_code' => ['sometimes', 'required', 'string', 'unique:integrations,integration_code,' . ($integration->id ?? '')], + ]; + } +} diff --git a/app/Domains/Integration/Services/TenantIntegrationService.php b/app/Domains/Integration/Services/TenantIntegrationService.php new file mode 100644 index 0000000..c400259 --- /dev/null +++ b/app/Domains/Integration/Services/TenantIntegrationService.php @@ -0,0 +1,36 @@ +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 TenantIntegration::updateOrCreate( + [ + 'tenant_code' => $tenantCode, + 'integration_code' => $integration->integration_code, + ], + [ + 'integration_data' => $data, + ] + ); + } +} diff --git a/app/Domains/Integration/routes/api.php b/app/Domains/Integration/routes/api.php new file mode 100644 index 0000000..f63a3cd --- /dev/null +++ b/app/Domains/Integration/routes/api.php @@ -0,0 +1,19 @@ + 'integrations'], function () { + Route::get('/', [IntegrationController::class, 'index']); + Route::post('/', [IntegrationController::class, 'store']); + Route::get('/{integration}', [IntegrationController::class, 'show']); + Route::put('/{integration}', [IntegrationController::class, 'update']); + 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']); +}); diff --git a/app/Domains/Tenant/Models/Tenant.php b/app/Domains/Tenant/Models/Tenant.php index 0e1742b..552cb18 100644 --- a/app/Domains/Tenant/Models/Tenant.php +++ b/app/Domains/Tenant/Models/Tenant.php @@ -3,6 +3,7 @@ namespace App\Domains\Tenant\Models; use App\Domains\Attachable\Models\Attachment; +use App\Domains\BankAccount\Models\BankAccount; use App\Domains\Catalog\Models\Product; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Factories\HasFactory; diff --git a/database/migrations/2026_07_03_000001_create_integrations_table.php b/database/migrations/2026_07_03_000001_create_integrations_table.php new file mode 100644 index 0000000..980502d --- /dev/null +++ b/database/migrations/2026_07_03_000001_create_integrations_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('integration_code')->unique(); + $table->string('name'); + $table->json('integration_data_schema')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('integrations'); + } +}; diff --git a/database/migrations/2026_07_03_000002_create_tenant_integration_table.php b/database/migrations/2026_07_03_000002_create_tenant_integration_table.php new file mode 100644 index 0000000..4d021ff --- /dev/null +++ b/database/migrations/2026_07_03_000002_create_tenant_integration_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('integration_code'); + $table->string('tenant_code'); + $table->longText('integration_data')->nullable(); // Stored as longText because it's encrypted JSON + $table->timestamps(); + + // Foreign keys + $table->foreign('integration_code')->references('integration_code')->on('integrations')->onDelete('cascade'); + $table->foreign('tenant_code')->references('codigo')->on('tenants')->onDelete('cascade'); + + $table->unique(['integration_code', 'tenant_code']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tenant_integration'); + } +}; diff --git a/database/seeders/TelepagosIntegrationSeeder.php b/database/seeders/TelepagosIntegrationSeeder.php new file mode 100644 index 0000000..2a30067 --- /dev/null +++ b/database/seeders/TelepagosIntegrationSeeder.php @@ -0,0 +1,26 @@ + 'telepagos'], + [ + 'name' => 'Telepagos', + 'integration_data_schema' => [ + 'username' => 'required|string', + 'password' => 'required|string', + ] + ] + ); + } +} diff --git a/routes/api.php b/routes/api.php index 9f9ed3b..5a3514c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -10,3 +10,4 @@ require __DIR__.'/../app/Domains/StorageTest/routes/api.php'; require __DIR__.'/../app/Domains/Purchase/routes/api.php'; require __DIR__.'/../app/Domains/Tenant/routes/api.php'; require __DIR__.'/../app/Domains/BankAccount/routes/api.php'; +require __DIR__.'/../app/Domains/Integration/routes/api.php';