From 67deca095e7e5085b6d2df953c513ef721147a0b Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 14 Sep 2026 12:29:13 -0300 Subject: [PATCH] refactor(ticket): use refunds as amount source --- app/Domains/Purchase/Models/PurchaseItem.php | 2 - .../Resources/PurchaseItemResource.php | 1 - .../Services/PurchaseRefundSummaryService.php | 8 +-- .../Resources/AdminApp/SaleDetailResource.php | 1 - .../Services/AdminAppTicketRowService.php | 1 - .../Ticket/Services/AdminAppTicketService.php | 18 ++++-- ...ve_refunded_amount_from_purchase_items.php | 22 +++++++ .../Sale/AdminAppSaleControllerTest.php | 38 +++++++++++-- .../Ticket/AdminAppTicketControllerTest.php | 57 ++++++++++++++----- 9 files changed, 114 insertions(+), 34 deletions(-) create mode 100644 database/migrations/2026_09_14_040000_remove_refunded_amount_from_purchase_items.php diff --git a/app/Domains/Purchase/Models/PurchaseItem.php b/app/Domains/Purchase/Models/PurchaseItem.php index 956b6dc..7a6d52b 100644 --- a/app/Domains/Purchase/Models/PurchaseItem.php +++ b/app/Domains/Purchase/Models/PurchaseItem.php @@ -28,7 +28,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany; 'discount_total', 'tax_total', 'total', - 'refunded_amount', ])] class PurchaseItem extends Model { @@ -49,7 +48,6 @@ class PurchaseItem extends Model 'discount_total' => 'decimal:2', 'tax_total' => 'decimal:2', 'total' => 'decimal:2', - 'refunded_amount' => 'decimal:2', ]; } diff --git a/app/Domains/Purchase/Resources/PurchaseItemResource.php b/app/Domains/Purchase/Resources/PurchaseItemResource.php index f83ecf6..1bd67d5 100644 --- a/app/Domains/Purchase/Resources/PurchaseItemResource.php +++ b/app/Domains/Purchase/Resources/PurchaseItemResource.php @@ -25,7 +25,6 @@ class PurchaseItemResource extends JsonResource 'quantity' => (int) $this->cantidad, 'unit_price' => $this->formatMoney($this->precio_unitario), 'line_total' => $this->formatMoney($this->total), - 'refunded_amount' => $this->formatMoney($this->refunded_amount), 'source_catalog_item_id' => $this->source_catalog_item_id, 'source_variant_id' => $this->source_variant_id, 'item_details' => [ diff --git a/app/Domains/Purchase/Services/PurchaseRefundSummaryService.php b/app/Domains/Purchase/Services/PurchaseRefundSummaryService.php index 9546810..7b67fbc 100644 --- a/app/Domains/Purchase/Services/PurchaseRefundSummaryService.php +++ b/app/Domains/Purchase/Services/PurchaseRefundSummaryService.php @@ -2,20 +2,20 @@ namespace App\Domains\Purchase\Services; -use App\Domains\Purchase\Models\PurchaseItem; use App\Domains\Tenant\Models\Tenant; +use App\Domains\Ticket\Models\TicketRefund; use Illuminate\Database\Eloquent\Builder; class PurchaseRefundSummaryService { public function totalForTenant(Tenant $tenant): string { - $total = PurchaseItem::query() + $total = TicketRefund::query() ->whereHas( - 'purchase', + 'purchaseItem.purchase', fn (Builder $query): Builder => $query->where('tenant_codigo', $tenant->codigo) ) - ->sum('refunded_amount'); + ->sum('amount'); return number_format((float) $total, 2, '.', ''); } diff --git a/app/Domains/Sale/Resources/AdminApp/SaleDetailResource.php b/app/Domains/Sale/Resources/AdminApp/SaleDetailResource.php index 08f1736..d389d1a 100644 --- a/app/Domains/Sale/Resources/AdminApp/SaleDetailResource.php +++ b/app/Domains/Sale/Resources/AdminApp/SaleDetailResource.php @@ -22,7 +22,6 @@ class SaleDetailResource extends JsonResource 'quantity' => (int) $item->cantidad, 'unit_price' => $this->formatMoney($item->precio_unitario), 'total' => $this->formatMoney($item->total), - 'refunded_amount' => $this->formatMoney($item->refunded_amount), ])->values(), 'total' => $this->formatMoney($this->total), ]; diff --git a/app/Domains/Ticket/Services/AdminAppTicketRowService.php b/app/Domains/Ticket/Services/AdminAppTicketRowService.php index 7b8b8a6..ed19924 100644 --- a/app/Domains/Ticket/Services/AdminAppTicketRowService.php +++ b/app/Domains/Ticket/Services/AdminAppTicketRowService.php @@ -32,7 +32,6 @@ class AdminAppTicketRowService ?? $ticket->sourceCatalogItem?->nombre ?? $ticket->name, 'amount' => $purchaseItem?->precio_unitario, - 'refunded_amount' => $refund?->amount ?? $purchaseItem?->refunded_amount, 'refund_type' => $refund?->type, 'refund_type_label' => $refund?->typeLabel(), 'client' => $purchaseItem?->purchase?->nombre_apellido ?? $ticket->user?->nombre_apellido, diff --git a/app/Domains/Ticket/Services/AdminAppTicketService.php b/app/Domains/Ticket/Services/AdminAppTicketService.php index 19cac45..9960fe3 100644 --- a/app/Domains/Ticket/Services/AdminAppTicketService.php +++ b/app/Domains/Ticket/Services/AdminAppTicketService.php @@ -142,7 +142,7 @@ class AdminAppTicketService $unitPrice = (float) $purchaseItem->precio_unitario; $itemTotal = (float) $purchaseItem->total; - $itemRefundedAmount = (float) ($purchaseItem->refunded_amount ?? 0); + $itemRefundedAmount = $this->refundedAmountForPurchaseItem($purchaseItem); $remainingItemAmount = max(0.0, round($itemTotal - $itemRefundedAmount, 2)); $total = null; @@ -201,7 +201,10 @@ class AdminAppTicketService } $refundAmount = $this->refundAmount($purchaseItem, $tenant, $refundType); - $refundedAmount = round((float) $purchaseItem->refunded_amount + $refundAmount, 2); + $refundedAmount = round( + $this->refundedAmountForPurchaseItem($purchaseItem) + $refundAmount, + 2, + ); if ($refundedAmount > (float) $purchaseItem->total) { throw ValidationException::withMessages([ @@ -220,14 +223,17 @@ class AdminAppTicketService 'amount' => number_format($refundAmount, 2, '.', ''), ]); - $purchaseItem->update([ - 'refunded_amount' => number_format($refundedAmount, 2, '.', ''), - ]); - return $ticket->refresh()->load(self::RELATIONS); }); } + private function refundedAmountForPurchaseItem(PurchaseItem $purchaseItem): float + { + return round((float) TicketRefund::query() + ->where('purchase_item_id', $purchaseItem->id) + ->sum('amount'), 2); + } + private function ensureRefundIsAllowed(Tenant $tenant, string $refundType): void { $isAllowed = match ($refundType) { diff --git a/database/migrations/2026_09_14_040000_remove_refunded_amount_from_purchase_items.php b/database/migrations/2026_09_14_040000_remove_refunded_amount_from_purchase_items.php new file mode 100644 index 0000000..f331d8c --- /dev/null +++ b/database/migrations/2026_09_14_040000_remove_refunded_amount_from_purchase_items.php @@ -0,0 +1,22 @@ +dropColumn('refunded_amount'); + }); + } + + public function down(): void + { + Schema::table('compra_items', function (Blueprint $table): void { + $table->decimal('refunded_amount', 10, 2)->default(0)->after('total'); + }); + } +}; diff --git a/tests/Feature/Sale/AdminAppSaleControllerTest.php b/tests/Feature/Sale/AdminAppSaleControllerTest.php index 81b21e9..f995133 100644 --- a/tests/Feature/Sale/AdminAppSaleControllerTest.php +++ b/tests/Feature/Sale/AdminAppSaleControllerTest.php @@ -20,6 +20,7 @@ use App\Domains\Purchase\Services\CheckoutService; use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Models\WebsiteType; use App\Domains\Ticket\Models\Ticket; +use App\Domains\Ticket\Models\TicketRefund; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; @@ -131,7 +132,8 @@ class AdminAppSaleControllerTest extends TestCase public function test_sales_list_uses_purchase_item_snapshots_for_every_status(): void { $tenant = $this->createTenant('acme'); - Sanctum::actingAs($this->createAdminAppUser($tenant)); + $admin = $this->createAdminAppUser($tenant); + Sanctum::actingAs($admin); $catalogItem = CatalogItem::query()->create([ 'tenant_code' => $tenant->codigo, @@ -155,7 +157,7 @@ class AdminAppSaleControllerTest extends TestCase 'status' => Purchase::STATUS_CREATED, 'total' => '30000.00', ]); - PurchaseItem::query()->create([ + $createdPurchaseItem = PurchaseItem::query()->create([ 'compra_id' => $createdPurchase->id, 'source_catalog_item_id' => $catalogItem->id, 'nombre' => $catalogItem->nombre, @@ -163,7 +165,20 @@ class AdminAppSaleControllerTest extends TestCase 'cantidad' => 3, 'precio_unitario' => '10000.00', 'total' => '30000.00', - 'refunded_amount' => '1250.00', + ]); + $createdRefundTicket = Ticket::query()->create([ + 'tenant_code' => $tenant->codigo, + 'ticket' => 'created-purchase-refund', + 'user_id' => $admin->id, + 'source_purchase_item_id' => $createdPurchaseItem->id, + 'refunded_at' => now(), + ]); + TicketRefund::query()->create([ + 'ticket_id' => $createdRefundTicket->id, + 'purchase_item_id' => $createdPurchaseItem->id, + 'created_by_user_id' => $admin->id, + 'type' => TicketRefund::TYPE_PARTIAL, + 'amount' => '1250.00', ]); $pendingCart = Cart::query()->create([ @@ -196,7 +211,7 @@ class AdminAppSaleControllerTest extends TestCase 'status' => Purchase::STATUS_PAID, 'total' => '20000.00', ]); - PurchaseItem::query()->create([ + $paidPurchaseItem = PurchaseItem::query()->create([ 'compra_id' => $paidPurchase->id, 'source_catalog_item_id' => $catalogItem->id, 'nombre' => $catalogItem->nombre, @@ -204,7 +219,20 @@ class AdminAppSaleControllerTest extends TestCase 'cantidad' => 2, 'precio_unitario' => '10000.00', 'total' => '20000.00', - 'refunded_amount' => '2500.00', + ]); + $paidRefundTicket = Ticket::query()->create([ + 'tenant_code' => $tenant->codigo, + 'ticket' => 'paid-purchase-refund', + 'user_id' => $admin->id, + 'source_purchase_item_id' => $paidPurchaseItem->id, + 'refunded_at' => now(), + ]); + TicketRefund::query()->create([ + 'ticket_id' => $paidRefundTicket->id, + 'purchase_item_id' => $paidPurchaseItem->id, + 'created_by_user_id' => $admin->id, + 'type' => TicketRefund::TYPE_PARTIAL, + 'amount' => '2500.00', ]); $supersededPurchase = Purchase::query()->create([ diff --git a/tests/Feature/Ticket/AdminAppTicketControllerTest.php b/tests/Feature/Ticket/AdminAppTicketControllerTest.php index 89adb78..a9ea96e 100644 --- a/tests/Feature/Ticket/AdminAppTicketControllerTest.php +++ b/tests/Feature/Ticket/AdminAppTicketControllerTest.php @@ -194,14 +194,13 @@ class AdminAppTicketControllerTest extends TestCase ->assertJsonPath('data.id', $ticket->id) ->assertJsonPath('data.status', Ticket::STATUS_REFUNDED) ->assertJsonPath('data.status_label', 'Reembolso total') - ->assertJsonPath('data.refunded_amount', '100.00') + ->assertJsonMissingPath('data.refunded_amount') ->assertJsonPath('data.refund.type', TicketRefund::TYPE_TOTAL) ->assertJsonPath('data.refund.type_label', 'Reembolso total') ->assertJsonPath('data.refund.amount', '100.00') ->assertJsonPath('data.refund.created_by', $admin->nombre_apellido); $this->assertNotNull($ticket->fresh()->refunded_at); - $this->assertSame('100.00', $purchaseItem->fresh()->refunded_amount); $this->assertDatabaseHas('ticket_refunds', [ 'ticket_id' => $ticket->id, 'purchase_item_id' => $purchaseItem->id, @@ -230,11 +229,10 @@ class AdminAppTicketControllerTest extends TestCase ->assertOk() ->assertJsonPath('data.status', Ticket::STATUS_REFUNDED) ->assertJsonPath('data.status_label', 'Reembolso parcial') - ->assertJsonPath('data.refunded_amount', '25.50') + ->assertJsonMissingPath('data.refunded_amount') ->assertJsonPath('data.refund.type', TicketRefund::TYPE_PARTIAL) ->assertJsonPath('data.refund.amount', '25.50'); - $this->assertSame('25.50', $purchaseItem->fresh()->refunded_amount); $this->assertDatabaseHas('ticket_refunds', [ 'ticket_id' => $ticket->id, 'type' => TicketRefund::TYPE_PARTIAL, @@ -270,7 +268,10 @@ class AdminAppTicketControllerTest extends TestCase 'refund_type' => TicketRefund::TYPE_TOTAL, ])->assertOk()->assertJsonPath('data.status_label', 'Reembolso total'); - $this->assertSame('125.00', $purchaseItem->fresh()->refunded_amount); + $this->assertSame( + '125.00', + number_format((float) $purchaseItem->ticketRefunds()->sum('amount'), 2, '.', ''), + ); $this->assertDatabaseHas('ticket_refunds', [ 'ticket_id' => $partialTicket->id, 'type' => TicketRefund::TYPE_PARTIAL, @@ -298,7 +299,7 @@ class AdminAppTicketControllerTest extends TestCase ->assertJsonValidationErrors('refund_type'); $this->assertNull($ticket->fresh()->refunded_at); - $this->assertSame('0.00', $purchaseItem->fresh()->refunded_amount); + $this->assertSame(0, $purchaseItem->ticketRefunds()->count()); } public function test_it_validates_the_refund_type(): void @@ -369,8 +370,18 @@ class AdminAppTicketControllerTest extends TestCase Sanctum::actingAs($admin); [$ticket, $purchaseItem] = $this->createRefundableTicket($tenant, $admin, '100.00'); - // Simulate 70 already refunded out of 100 on the item (remaining is 30) - $purchaseItem->update(['refunded_amount' => '70.00']); + // Simulate 70 already refunded out of 100 on the item (remaining is 30). + $previousTicket = $this->createTicket($tenant, $admin, [ + 'source_purchase_item_id' => $purchaseItem->id, + 'refunded_at' => now(), + ]); + TicketRefund::query()->create([ + 'ticket_id' => $previousTicket->id, + 'purchase_item_id' => $purchaseItem->id, + 'created_by_user_id' => $admin->id, + 'type' => TicketRefund::TYPE_PARTIAL, + 'amount' => '70.00', + ]); $this->getJson("/api/v1/adminapp/tenant/tickets/{$ticket->id}/refund") ->assertOk() @@ -599,7 +610,8 @@ class AdminAppTicketControllerTest extends TestCase $this->createTicket($tenant, $admin)->update(['used_at' => now()]); $this->createTicket($tenant, $admin); $this->createTicket($tenant, $admin)->update(['cancelled_at' => now()]); - $this->createTicket($tenant, $admin)->update(['refunded_at' => now()]); + $refundedTicket = $this->createTicket($tenant, $admin); + $refundedTicket->update(['refunded_at' => now()]); $this->createTicket($tenant, $admin)->update(['disabled_at' => now()]); $this->createTicket($otherTenant, $otherUser)->update(['used_at' => now()]); @@ -614,7 +626,7 @@ class AdminAppTicketControllerTest extends TestCase 'status' => Purchase::STATUS_PAID, 'total' => '1000.00', ]); - PurchaseItem::query()->create([ + $purchaseItem = PurchaseItem::query()->create([ 'compra_id' => $purchase->id, 'source_catalog_item_id' => $catalogItem->id, 'nombre' => 'Entrada', @@ -622,14 +634,21 @@ class AdminAppTicketControllerTest extends TestCase 'cantidad' => 1, 'precio_unitario' => '1000.00', 'total' => '1000.00', - 'refunded_amount' => '250.00', + ]); + $refundedTicket->update(['source_purchase_item_id' => $purchaseItem->id]); + TicketRefund::query()->create([ + 'ticket_id' => $refundedTicket->id, + 'purchase_item_id' => $purchaseItem->id, + 'created_by_user_id' => $admin->id, + 'type' => TicketRefund::TYPE_PARTIAL, + 'amount' => '250.00', ]); $otherPurchase = Purchase::query()->create([ 'tenant_codigo' => $otherTenant->codigo, 'status' => Purchase::STATUS_PAID, 'total' => '2000.00', ]); - PurchaseItem::query()->create([ + $otherPurchaseItem = PurchaseItem::query()->create([ 'compra_id' => $otherPurchase->id, 'source_catalog_item_id' => $catalogItem->id, 'nombre' => 'Otra entrada', @@ -637,7 +656,17 @@ class AdminAppTicketControllerTest extends TestCase 'cantidad' => 1, 'precio_unitario' => '2000.00', 'total' => '2000.00', - 'refunded_amount' => '2000.00', + ]); + $otherRefundedTicket = $this->createTicket($otherTenant, $otherUser, [ + 'source_purchase_item_id' => $otherPurchaseItem->id, + 'refunded_at' => now(), + ]); + TicketRefund::query()->create([ + 'ticket_id' => $otherRefundedTicket->id, + 'purchase_item_id' => $otherPurchaseItem->id, + 'created_by_user_id' => $otherUser->id, + 'type' => TicketRefund::TYPE_TOTAL, + 'amount' => '2000.00', ]); $this->getJson('/api/v1/adminapp/tenant/tickets?q=does-not-match') @@ -722,7 +751,7 @@ class AdminAppTicketControllerTest extends TestCase ->assertJsonPath('data.0.order_number', $purchase->id) ->assertJsonPath('data.0.product', 'Remera') ->assertJsonPath('data.0.amount', '8000.00') - ->assertJsonPath('data.0.refunded_amount', '0.00') + ->assertJsonMissingPath('data.0.refunded_amount') ->assertJsonPath('data.0.status', Ticket::STATUS_USED) ->assertJsonPath('data.0.scanned_by', $admin->nombre_apellido) ->assertJsonPath('data.0.variant_properties.0.code', 'size')