diff --git a/app/Domains/Tenant/Controllers/BankAccountController.php b/app/Domains/Tenant/Controllers/BankAccountController.php new file mode 100644 index 0000000..885ff7f --- /dev/null +++ b/app/Domains/Tenant/Controllers/BankAccountController.php @@ -0,0 +1,67 @@ +bankAccounts()->latest()->get(); + return BankAccountResource::collection($accounts)->response(); + } + + public function store(StoreBankAccountRequest $request, Tenant $tenant): JsonResponse + { + $data = $request->validated(); + $data['tenant_code'] = $tenant->codigo; + + $account = BankAccount::query()->create($data); + + return BankAccountResource::make($account)->response()->setStatusCode(201); + } + + public function show(Tenant $tenant, BankAccount $bankAccount): BankAccountResource + { + abort_if($bankAccount->tenant_code !== $tenant->codigo, 404); + + return BankAccountResource::make($bankAccount); + } + + public function update(UpdateBankAccountRequest $request, Tenant $tenant, BankAccount $bankAccount): BankAccountResource + { + abort_if($bankAccount->tenant_code !== $tenant->codigo, 404); + + $bankAccount->update($request->validated()); + + return BankAccountResource::make($bankAccount); + } + + public function destroy(Tenant $tenant, BankAccount $bankAccount): Response + { + abort_if($bankAccount->tenant_code !== $tenant->codigo, 404); + + $bankAccount->delete(); + + return response()->noContent(); + } + + public function select(Tenant $tenant, BankAccount $bankAccount): TenantResource + { + abort_if($bankAccount->tenant_code !== $tenant->codigo, 400, 'Bank account does not belong to this tenant.'); + + $tenant->selected_bank_account_id = $bankAccount->id; + $tenant->save(); + + return TenantResource::make($tenant->loadMissing(['headerLogo', 'footerLogo', 'selectedBankAccount'])); + } +} diff --git a/app/Domains/Tenant/Controllers/BootstrapTenantController.php b/app/Domains/Tenant/Controllers/BootstrapTenantController.php index f9803b2..29dcced 100644 --- a/app/Domains/Tenant/Controllers/BootstrapTenantController.php +++ b/app/Domains/Tenant/Controllers/BootstrapTenantController.php @@ -15,7 +15,7 @@ class BootstrapTenantController extends Controller $dominio = $request->validated('dominio'); return TenantResource::make( - Tenant::query()->with(['headerLogo', 'footerLogo'])->where('dominio', $dominio)->firstOrFail() + Tenant::query()->with(['headerLogo', 'footerLogo', 'selectedBankAccount'])->where('dominio', $dominio)->firstOrFail() ); } } diff --git a/app/Domains/Tenant/Models/BankAccount.php b/app/Domains/Tenant/Models/BankAccount.php new file mode 100644 index 0000000..e8d2be5 --- /dev/null +++ b/app/Domains/Tenant/Models/BankAccount.php @@ -0,0 +1,28 @@ + + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo'); + } +} diff --git a/app/Domains/Tenant/Models/Tenant.php b/app/Domains/Tenant/Models/Tenant.php index 4a29673..0e1742b 100644 --- a/app/Domains/Tenant/Models/Tenant.php +++ b/app/Domains/Tenant/Models/Tenant.php @@ -55,5 +55,21 @@ class Tenant extends Model { return $this->hasMany(Product::class, 'tenant_codigo', 'codigo'); } + + /** + * @return HasMany + */ + public function bankAccounts(): HasMany + { + return $this->hasMany(BankAccount::class, 'tenant_code', 'codigo'); + } + + /** + * @return BelongsTo + */ + public function selectedBankAccount(): BelongsTo + { + return $this->belongsTo(BankAccount::class, 'selected_bank_account_id'); + } } diff --git a/app/Domains/Tenant/Requests/StoreBankAccountRequest.php b/app/Domains/Tenant/Requests/StoreBankAccountRequest.php new file mode 100644 index 0000000..8ed40fc --- /dev/null +++ b/app/Domains/Tenant/Requests/StoreBankAccountRequest.php @@ -0,0 +1,26 @@ + + */ + public function rules(): array + { + return [ + 'titular' => ['required', 'string', 'max:255'], + 'entidad' => ['required', 'string', 'max:255'], + 'alias' => ['required', 'string', 'max:255'], + 'cvu' => ['required', 'string', 'max:255'], + ]; + } +} diff --git a/app/Domains/Tenant/Requests/UpdateBankAccountRequest.php b/app/Domains/Tenant/Requests/UpdateBankAccountRequest.php new file mode 100644 index 0000000..ead00b8 --- /dev/null +++ b/app/Domains/Tenant/Requests/UpdateBankAccountRequest.php @@ -0,0 +1,26 @@ + + */ + public function rules(): array + { + return [ + 'titular' => ['sometimes', 'string', 'max:255'], + 'entidad' => ['sometimes', 'string', 'max:255'], + 'alias' => ['sometimes', 'string', 'max:255'], + 'cvu' => ['sometimes', 'string', 'max:255'], + ]; + } +} diff --git a/app/Domains/Tenant/Resources/BankAccountResource.php b/app/Domains/Tenant/Resources/BankAccountResource.php new file mode 100644 index 0000000..15ae7fa --- /dev/null +++ b/app/Domains/Tenant/Resources/BankAccountResource.php @@ -0,0 +1,27 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'tenant_code' => $this->tenant_code, + 'titular' => $this->titular, + 'entidad' => $this->entidad, + 'alias' => $this->alias, + 'cvu' => $this->cvu, + ]; + } +} diff --git a/app/Domains/Tenant/Resources/TenantResource.php b/app/Domains/Tenant/Resources/TenantResource.php index 908a9ef..987c659 100644 --- a/app/Domains/Tenant/Resources/TenantResource.php +++ b/app/Domains/Tenant/Resources/TenantResource.php @@ -29,6 +29,8 @@ class TenantResource extends JsonResource // 1 day 'header_logo' => $this->headerLogo?->getTemporaryUrl(1440), 'footer_logo' => $this->footerLogo?->getTemporaryUrl(1440 ), + 'selected_bank_account_id' => $this->selected_bank_account_id, + 'selected_bank_account' => BankAccountResource::make($this->whenLoaded('selectedBankAccount')), ]; } } diff --git a/app/Domains/Tenant/routes/api.php b/app/Domains/Tenant/routes/api.php index 69464a8..087c9c3 100644 --- a/app/Domains/Tenant/routes/api.php +++ b/app/Domains/Tenant/routes/api.php @@ -8,3 +8,9 @@ Route::get('tenants/bootstrap/{dominio}', BootstrapTenantController::class) ->where('dominio', '.*'); Route::apiResource('tenants', TenantController::class); + +Route::prefix('tenants/{tenant:codigo}')->group(function (): void { + Route::post('bank-accounts/{bank_account}/select', [\App\Domains\Tenant\Controllers\BankAccountController::class, 'select']); + Route::apiResource('bank-accounts', \App\Domains\Tenant\Controllers\BankAccountController::class) + ->parameters(['bank-accounts' => 'bank_account']); +}); diff --git a/database/migrations/2026_07_03_100000_create_bank_accounts_table.php b/database/migrations/2026_07_03_100000_create_bank_accounts_table.php new file mode 100644 index 0000000..dc5cc9c --- /dev/null +++ b/database/migrations/2026_07_03_100000_create_bank_accounts_table.php @@ -0,0 +1,38 @@ +id(); + $table->string('tenant_code'); + $table->string('titular'); + $table->string('entidad'); + $table->string('alias'); + $table->string('cvu'); + $table->timestamps(); + + $table->foreign('tenant_code') + ->references('codigo') + ->on('tenants') + ->cascadeOnUpdate() + ->cascadeOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('bank_accounts'); + } +}; diff --git a/database/migrations/2026_07_03_100100_add_selected_bank_account_id_to_tenants_table.php b/database/migrations/2026_07_03_100100_add_selected_bank_account_id_to_tenants_table.php new file mode 100644 index 0000000..c7519d7 --- /dev/null +++ b/database/migrations/2026_07_03_100100_add_selected_bank_account_id_to_tenants_table.php @@ -0,0 +1,34 @@ +unsignedBigInteger('selected_bank_account_id')->nullable()->after('footer_logo_id'); + + $table->foreign('selected_bank_account_id') + ->references('id') + ->on('bank_accounts') + ->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('tenants', function (Blueprint $table) { + $table->dropForeign(['selected_bank_account_id']); + $table->dropColumn('selected_bank_account_id'); + }); + } +}; diff --git a/database/seeders/TenantSeeder.php b/database/seeders/TenantSeeder.php index f31bb21..2f86644 100644 --- a/database/seeders/TenantSeeder.php +++ b/database/seeders/TenantSeeder.php @@ -72,7 +72,7 @@ class TenantSeeder extends Seeder true ); - $this->tenantService->create([ + $tenant = $this->tenantService->create([ 'codigo' => 'sonder', 'nombre' => 'Sonder', 'dominio' => 'localhost', @@ -85,5 +85,32 @@ class TenantSeeder extends Seeder 'header_logo' => $headerLogo, 'footer_logo' => $footerLogo, ]); + + $bankAccount1 = \App\Domains\Tenant\Models\BankAccount::query()->create([ + 'tenant_code' => $tenant->codigo, + 'titular' => 'Sonder S.A. (Principal)', + 'entidad' => 'Banco de la Nación Argentina', + 'alias' => 'sonder.indumentaria', + 'cvu' => '0110065420006540987654', + ]); + + \App\Domains\Tenant\Models\BankAccount::query()->create([ + 'tenant_code' => $tenant->codigo, + 'titular' => 'Sonder S.A. (Secundaria)', + 'entidad' => 'Banco de Galicia y Buenos Aires', + 'alias' => 'sonder.indumentaria.galicia', + 'cvu' => '0070012345678901234567', + ]); + + \App\Domains\Tenant\Models\BankAccount::query()->create([ + 'tenant_code' => $tenant->codigo, + 'titular' => 'Sonder S.A. (Mercado Pago)', + 'entidad' => 'Mercado Pago', + 'alias' => 'sonder.indumentaria.mp', + 'cvu' => '0000003100012345678901', + ]); + + $tenant->selected_bank_account_id = $bankAccount1->id; + $tenant->save(); } } diff --git a/tests/Feature/Tenant/BankAccountControllerTest.php b/tests/Feature/Tenant/BankAccountControllerTest.php new file mode 100644 index 0000000..da43707 --- /dev/null +++ b/tests/Feature/Tenant/BankAccountControllerTest.php @@ -0,0 +1,198 @@ + $hdrKey, + 'path' => 'tenants/' . $hdrKey . '.png', + 'filename' => 'logo_header.png', + 'type' => \App\Domains\Attachable\Enums\AttachmentType::Image, + 'mime_type' => 'image/png', + ]); + $footerAttachment = \App\Domains\Attachable\Models\Attachment::create([ + 'key' => $ftrKey, + 'path' => 'tenants/' . $ftrKey . '.png', + 'filename' => 'logo_footer.png', + 'type' => \App\Domains\Attachable\Enums\AttachmentType::Image, + 'mime_type' => 'image/png', + ]); + + $this->tenant = Tenant::create([ + 'codigo' => 'acme', + 'nombre' => 'Acme', + 'dominio' => 'acme.com', + 'primary_color' => '#ff0000', + 'secondary_color' => '#00ff00', + 'danger_color' => '#0000ff', + 'success_color' => '#00ff00', + 'header_bg_color' => '#ffffff', + 'footer_bg_color' => '#ffffff', + 'header_logo_id' => $headerAttachment->id, + 'footer_logo_id' => $footerAttachment->id, + ]); + } + + public function test_it_lists_bank_accounts_for_a_tenant(): void + { + BankAccount::query()->create([ + 'tenant_code' => 'acme', + 'titular' => 'John Doe', + 'entidad' => 'Banco Galicia', + 'alias' => 'john.doe.galicia', + 'cvu' => '1234567890123456789012', + ]); + + $response = $this->getJson("/api/tenants/acme/bank-accounts"); + + $response->assertOk() + ->assertJsonCount(1, 'data') + ->assertJsonPath('data.0.titular', 'John Doe'); + } + + public function test_it_creates_a_bank_account_for_a_tenant(): void + { + $response = $this->postJson("/api/tenants/acme/bank-accounts", [ + 'titular' => 'Jane Doe', + 'entidad' => 'Banco Nación', + 'alias' => 'jane.doe.nacion', + 'cvu' => '9876543210987654321098', + ]); + + $response->assertCreated() + ->assertJsonPath('data.titular', 'Jane Doe'); + + $this->assertDatabaseHas('bank_accounts', [ + 'tenant_code' => 'acme', + 'titular' => 'Jane Doe', + ]); + } + + public function test_it_shows_a_bank_account(): void + { + $account = BankAccount::query()->create([ + 'tenant_code' => 'acme', + 'titular' => 'John Doe', + 'entidad' => 'Banco Galicia', + 'alias' => 'john.doe.galicia', + 'cvu' => '1234567890123456789012', + ]); + + $response = $this->getJson("/api/tenants/acme/bank-accounts/{$account->id}"); + + $response->assertOk() + ->assertJsonPath('data.titular', 'John Doe'); + } + + public function test_it_updates_a_bank_account(): void + { + $account = BankAccount::query()->create([ + 'tenant_code' => 'acme', + 'titular' => 'John Doe', + 'entidad' => 'Banco Galicia', + 'alias' => 'john.doe.galicia', + 'cvu' => '1234567890123456789012', + ]); + + $response = $this->putJson("/api/tenants/acme/bank-accounts/{$account->id}", [ + 'titular' => 'John Doe Updated', + ]); + + $response->assertOk() + ->assertJsonPath('data.titular', 'John Doe Updated'); + + $this->assertDatabaseHas('bank_accounts', [ + 'id' => $account->id, + 'titular' => 'John Doe Updated', + ]); + } + + public function test_it_deletes_a_bank_account(): void + { + $account = BankAccount::query()->create([ + 'tenant_code' => 'acme', + 'titular' => 'John Doe', + 'entidad' => 'Banco Galicia', + 'alias' => 'john.doe.galicia', + 'cvu' => '1234567890123456789012', + ]); + + $response = $this->deleteJson("/api/tenants/acme/bank-accounts/{$account->id}"); + + $response->assertNoContent(); + + $this->assertDatabaseMissing('bank_accounts', ['id' => $account->id]); + } + + public function test_it_selects_a_bank_account_for_a_tenant(): void + { + $account = BankAccount::query()->create([ + 'tenant_code' => 'acme', + 'titular' => 'John Doe', + 'entidad' => 'Banco Galicia', + 'alias' => 'john.doe.galicia', + 'cvu' => '1234567890123456789012', + ]); + + $response = $this->postJson("/api/tenants/acme/bank-accounts/{$account->id}/select"); + + $response->assertOk() + ->assertJsonPath('data.selected_bank_account_id', $account->id); + + $this->tenant->refresh(); + $this->assertEquals($account->id, $this->tenant->selected_bank_account_id); + } + + public function test_it_deletes_related_bank_accounts_when_tenant_is_deleted(): void + { + $account = BankAccount::query()->create([ + 'tenant_code' => 'acme', + 'titular' => 'John Doe', + 'entidad' => 'Banco Galicia', + 'alias' => 'john.doe.galicia', + 'cvu' => '1234567890123456789012', + ]); + + $this->assertDatabaseHas('bank_accounts', ['id' => $account->id]); + + $this->tenant->delete(); + + $this->assertDatabaseMissing('bank_accounts', ['id' => $account->id]); + } + + public function test_it_sets_selected_bank_account_id_to_null_when_selected_bank_account_is_deleted(): void + { + $account = BankAccount::query()->create([ + 'tenant_code' => 'acme', + 'titular' => 'John Doe', + 'entidad' => 'Banco Galicia', + 'alias' => 'john.doe.galicia', + 'cvu' => '1234567890123456789012', + ]); + + $this->tenant->selected_bank_account_id = $account->id; + $this->tenant->save(); + + $account->delete(); + + $this->tenant->refresh(); + $this->assertNull($this->tenant->selected_bank_account_id); + } +}