From 3f9bcd84c4955c2c1c8d515ef6af92886c7ff519 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Tue, 4 Aug 2026 10:27:33 -0300 Subject: [PATCH] feat: Add tenant code to value changes; implement PurchaseController and related services for tenant-specific purchase management --- .../Models/Concerns/LogsValueChanges.php | 3 ++ app/Domains/Logging/Models/ValueChange.php | 8 +++ .../AdminApp/PurchaseController.php | 35 +++++++++++++ app/Domains/Purchase/Models/Purchase.php | 5 ++ .../AdminApp/PurchaseModificationResource.php | 40 +++++++++++++++ .../Services/AdminAppPurchaseService.php | 46 +++++++++++++++++ app/Domains/Purchase/routes/adminapp.php | 11 ++++ app/Domains/Purchase/routes/api.php | 2 + ...add_tenant_code_to_value_changes_table.php | 50 +++++++++++++++++++ .../Feature/Logging/LogsValueChangesTest.php | 18 +++++++ tests/Unit/Logging/ValueChangeTest.php | 4 ++ 11 files changed, 222 insertions(+) create mode 100644 app/Domains/Purchase/Controllers/AdminApp/PurchaseController.php create mode 100644 app/Domains/Purchase/Resources/AdminApp/PurchaseModificationResource.php create mode 100644 app/Domains/Purchase/Services/AdminAppPurchaseService.php create mode 100644 app/Domains/Purchase/routes/adminapp.php create mode 100644 database/migrations/2026_08_04_000000_add_tenant_code_to_value_changes_table.php diff --git a/app/Domains/Logging/Models/Concerns/LogsValueChanges.php b/app/Domains/Logging/Models/Concerns/LogsValueChanges.php index a9d8b15..ec35fb8 100644 --- a/app/Domains/Logging/Models/Concerns/LogsValueChanges.php +++ b/app/Domains/Logging/Models/Concerns/LogsValueChanges.php @@ -11,6 +11,8 @@ use LogicException; trait LogsValueChanges { + abstract protected function valueChangeTenantCode(): string; + public static function bootLogsValueChanges(): void { static::updated(function (Model $model): void { @@ -30,6 +32,7 @@ trait LogsValueChanges foreach ($changedAttributes as $attribute) { $model->valueChanges()->create([ + 'tenant_code' => $model->valueChangeTenantCode(), 'attribute' => $attribute, 'old_value' => $model->getRawOriginal($attribute), 'new_value' => $model->getAttributes()[$attribute] ?? null, diff --git a/app/Domains/Logging/Models/ValueChange.php b/app/Domains/Logging/Models/ValueChange.php index d9a60ea..d3f81a9 100644 --- a/app/Domains/Logging/Models/ValueChange.php +++ b/app/Domains/Logging/Models/ValueChange.php @@ -4,12 +4,14 @@ namespace App\Domains\Logging\Models; use App\Domains\Auth\Models\User; use App\Domains\Logging\Enums\ValueChangeActorType; +use App\Domains\Tenant\Models\Tenant; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; #[Fillable([ + 'tenant_code', 'trackable_type', 'trackable_id', 'attribute', @@ -35,6 +37,12 @@ class ValueChange extends Model return $this->belongsTo(User::class); } + /** @return BelongsTo */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo'); + } + protected function casts(): array { return [ diff --git a/app/Domains/Purchase/Controllers/AdminApp/PurchaseController.php b/app/Domains/Purchase/Controllers/AdminApp/PurchaseController.php new file mode 100644 index 0000000..feb1c35 --- /dev/null +++ b/app/Domains/Purchase/Controllers/AdminApp/PurchaseController.php @@ -0,0 +1,35 @@ +user()->tenant()->firstOrFail(); + + return PurchaseResource::collection( + $this->purchaseService->purchases($tenant) + )->additional([ + 'confirmed_sales_total' => $this->purchaseService->confirmedSalesTotal($tenant), + ]); + } + + public function modifications(Request $request): AnonymousResourceCollection + { + return PurchaseModificationResource::collection( + $this->purchaseService->modifications( + $request->user()->tenant()->firstOrFail() + ) + ); + } +} diff --git a/app/Domains/Purchase/Models/Purchase.php b/app/Domains/Purchase/Models/Purchase.php index ff1b79d..273b823 100644 --- a/app/Domains/Purchase/Models/Purchase.php +++ b/app/Domains/Purchase/Models/Purchase.php @@ -137,6 +137,11 @@ class Purchase extends Model return (float) $this->items()->sum('total'); } + protected function valueChangeTenantCode(): string + { + return $this->tenant_codigo; + } + public function markAsPendingPayment(): void { $this->update([ diff --git a/app/Domains/Purchase/Resources/AdminApp/PurchaseModificationResource.php b/app/Domains/Purchase/Resources/AdminApp/PurchaseModificationResource.php new file mode 100644 index 0000000..f42b219 --- /dev/null +++ b/app/Domains/Purchase/Resources/AdminApp/PurchaseModificationResource.php @@ -0,0 +1,40 @@ + */ + public function toArray(Request $request): array + { + /** @var Purchase|null $purchase */ + $purchase = $this->whenLoaded('trackable'); + $user = $this->whenLoaded('user'); + + return [ + 'id' => $this->id, + 'purchase_id' => $this->trackable_id, + 'attribute' => $this->attribute, + 'old_value' => $this->old_value, + 'new_value' => $this->new_value, + 'changed_at' => $this->changed_at, + 'actor_type' => $this->actor_type->value, + 'purchase' => $purchase instanceof Purchase ? [ + 'id' => $purchase->id, + 'customer_name' => $purchase->nombre_apellido, + 'status' => $purchase->status, + ] : null, + 'modified_by' => $user ? [ + 'id' => $user->id, + 'name' => $user->nombre_apellido, + 'email' => $user->email, + ] : null, + ]; + } +} diff --git a/app/Domains/Purchase/Services/AdminAppPurchaseService.php b/app/Domains/Purchase/Services/AdminAppPurchaseService.php new file mode 100644 index 0000000..78bc441 --- /dev/null +++ b/app/Domains/Purchase/Services/AdminAppPurchaseService.php @@ -0,0 +1,46 @@ +where('tenant_codigo', $tenant->codigo) + ->where('status', Purchase::STATUS_PAID) + ->sum('total'); + + return number_format((float) $total, 2, '.', ''); + } + + /** @return LengthAwarePaginator */ + public function purchases(Tenant $tenant): LengthAwarePaginator + { + return Purchase::query() + ->where('tenant_codigo', $tenant->codigo) + ->with(['items.imageAttachment']) + ->withCount('tickets') + ->latest() + ->paginateFromRequest() + ->withQueryString(); + } + + /** @return LengthAwarePaginator */ + public function modifications(Tenant $tenant): LengthAwarePaginator + { + return ValueChange::query() + ->where('tenant_code', $tenant->codigo) + ->where('trackable_type', (new Purchase)->getMorphClass()) + ->with(['trackable', 'user']) + ->orderByDesc('changed_at') + ->orderByDesc('id') + ->paginateFromRequest() + ->withQueryString(); + } +} diff --git a/app/Domains/Purchase/routes/adminapp.php b/app/Domains/Purchase/routes/adminapp.php new file mode 100644 index 0000000..e560ddc --- /dev/null +++ b/app/Domains/Purchase/routes/adminapp.php @@ -0,0 +1,11 @@ +middleware(['auth:sanctum', 'adminapp.tenant']) + ->group(function (): void { + Route::get('purchases', [PurchaseController::class, 'index']); + Route::get('purchases/modifications', [PurchaseController::class, 'modifications']); + }); diff --git a/app/Domains/Purchase/routes/api.php b/app/Domains/Purchase/routes/api.php index 41cc919..314a680 100644 --- a/app/Domains/Purchase/routes/api.php +++ b/app/Domains/Purchase/routes/api.php @@ -15,3 +15,5 @@ Route::prefix('tenants/{tenant:codigo}')->middleware('auth:sanctum')->group(func Route::post('compras/{compra}/review', [PurchaseController::class, 'submitForReview']); Route::post('compras/{compra}/cancel', [PurchaseController::class, 'cancel']); }); + +require __DIR__.'/adminapp.php'; diff --git a/database/migrations/2026_08_04_000000_add_tenant_code_to_value_changes_table.php b/database/migrations/2026_08_04_000000_add_tenant_code_to_value_changes_table.php new file mode 100644 index 0000000..8c5ae52 --- /dev/null +++ b/database/migrations/2026_08_04_000000_add_tenant_code_to_value_changes_table.php @@ -0,0 +1,50 @@ +string('tenant_code')->nullable()->after('id'); + $table->foreign('tenant_code') + ->references('codigo') + ->on('tenants') + ->cascadeOnUpdate() + ->nullOnDelete(); + $table->index(['tenant_code', 'changed_at']); + }); + + DB::table('value_changes') + ->where('trackable_type', (new Purchase)->getMorphClass()) + ->whereNull('tenant_code') + ->orderBy('id') + ->chunkById(500, function ($changes): void { + foreach ($changes as $change) { + $tenantCode = DB::table('compras') + ->where('id', $change->trackable_id) + ->value('tenant_codigo'); + + if ($tenantCode !== null) { + DB::table('value_changes') + ->where('id', $change->id) + ->update(['tenant_code' => $tenantCode]); + } + } + }); + } + + public function down(): void + { + Schema::table('value_changes', function (Blueprint $table): void { + $table->dropForeign(['tenant_code']); + $table->dropIndex(['tenant_code', 'changed_at']); + $table->dropColumn('tenant_code'); + }); + } +}; diff --git a/tests/Feature/Logging/LogsValueChangesTest.php b/tests/Feature/Logging/LogsValueChangesTest.php index 4951219..bd4dc8c 100644 --- a/tests/Feature/Logging/LogsValueChangesTest.php +++ b/tests/Feature/Logging/LogsValueChangesTest.php @@ -30,6 +30,13 @@ class LogsValueChangesTest extends TestCase $table->id(); }); + Schema::create('tenants', function (Blueprint $table): void { + $table->id(); + $table->string('codigo')->unique(); + }); + + Schema::getConnection()->table('tenants')->insert(['codigo' => 'test']); + Schema::create('logging_test_products', function (Blueprint $table): void { $table->id(); $table->string('name'); @@ -40,12 +47,15 @@ class LogsValueChangesTest extends TestCase Schema::create('compras', function (Blueprint $table): void { $table->id(); + $table->string('tenant_codigo'); $table->string('status')->default(Purchase::STATUS_CREATED); $table->timestamps(); }); $migration = require database_path('migrations/2026_08_03_000200_create_value_changes_table.php'); $migration->up(); + $tenantMigration = require database_path('migrations/2026_08_04_000000_add_tenant_code_to_value_changes_table.php'); + $tenantMigration->up(); } public function test_it_creates_one_system_record_per_configured_change(): void @@ -64,6 +74,7 @@ class LogsValueChangesTest extends TestCase $this->assertDatabaseCount('value_changes', 2); $this->assertDatabaseHas('value_changes', [ + 'tenant_code' => 'test', 'attribute' => 'name', 'old_value' => 'Original', 'new_value' => 'Updated', @@ -114,6 +125,7 @@ class LogsValueChangesTest extends TestCase public function test_purchase_logs_its_status_changes(): void { $purchase = Purchase::query()->create([ + 'tenant_codigo' => 'test', 'status' => Purchase::STATUS_CREATED, ]); @@ -122,6 +134,7 @@ class LogsValueChangesTest extends TestCase ]); $this->assertDatabaseHas('value_changes', [ + 'tenant_code' => 'test', 'trackable_type' => $purchase->getMorphClass(), 'trackable_id' => $purchase->id, 'attribute' => 'status', @@ -145,4 +158,9 @@ class LoggingTestProduct extends Model 'name', 'price', ]; + + protected function valueChangeTenantCode(): string + { + return 'test'; + } } diff --git a/tests/Unit/Logging/ValueChangeTest.php b/tests/Unit/Logging/ValueChangeTest.php index 9fa0991..8add79d 100644 --- a/tests/Unit/Logging/ValueChangeTest.php +++ b/tests/Unit/Logging/ValueChangeTest.php @@ -5,6 +5,7 @@ namespace Tests\Unit\Logging; use App\Domains\Auth\Models\User; use App\Domains\Logging\Enums\ValueChangeActorType; use App\Domains\Logging\Models\ValueChange; +use App\Domains\Tenant\Models\Tenant; use Illuminate\Database\Eloquent\Relations\MorphTo; use Tests\TestCase; @@ -14,6 +15,7 @@ class ValueChangeTest extends TestCase { $valueChange = new ValueChange; $valueChange->setRawAttributes([ + 'tenant_code' => 'test', 'trackable_id' => '10', 'attribute' => 'status', 'old_value' => 'pending', @@ -25,6 +27,7 @@ class ValueChangeTest extends TestCase $this->assertSame('value_changes', $valueChange->getTable()); $this->assertFalse($valueChange->usesTimestamps()); + $this->assertSame('test', $valueChange->tenant_code); $this->assertSame(10, $valueChange->trackable_id); $this->assertSame('status', $valueChange->attribute); $this->assertSame('pending', $valueChange->old_value); @@ -34,5 +37,6 @@ class ValueChangeTest extends TestCase $this->assertSame(20, $valueChange->user_id); $this->assertInstanceOf(MorphTo::class, $valueChange->trackable()); $this->assertInstanceOf(User::class, $valueChange->user()->getRelated()); + $this->assertInstanceOf(Tenant::class, $valueChange->tenant()->getRelated()); } }