feat(purchase,sale,ticket): expose refunded total summary in admin sales and tickets

This commit is contained in:
ncoronel 2026-09-11 16:23:23 -03:00
parent 4bb4f526e4
commit 4f7ede1072
8 changed files with 84 additions and 4 deletions

View File

@ -0,0 +1,22 @@
<?php
namespace App\Domains\Purchase\Services;
use App\Domains\Purchase\Models\PurchaseItem;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Database\Eloquent\Builder;
class PurchaseRefundSummaryService
{
public function totalForTenant(Tenant $tenant): string
{
$total = PurchaseItem::query()
->whereHas(
'purchase',
fn (Builder $query): Builder => $query->where('tenant_codigo', $tenant->codigo)
)
->sum('refunded_amount');
return number_format((float) $total, 2, '.', '');
}
}

View File

@ -35,6 +35,7 @@ class SaleController extends Controller
$this->saleService->sales($tenant, $request->validated()) $this->saleService->sales($tenant, $request->validated())
)->additional([ )->additional([
'confirmed_sales_total' => $this->saleService->confirmedSalesTotal($tenant), 'confirmed_sales_total' => $this->saleService->confirmedSalesTotal($tenant),
'refunded_total' => $this->saleService->refundedTotal($tenant),
]); ]);
} }

View File

@ -5,6 +5,7 @@ namespace App\Domains\Sale\Services;
use App\Domains\Logging\Models\ValueChange; use App\Domains\Logging\Models\ValueChange;
use App\Domains\Purchase\Models\Purchase; use App\Domains\Purchase\Models\Purchase;
use App\Domains\Purchase\Services\CheckoutService; use App\Domains\Purchase\Services\CheckoutService;
use App\Domains\Purchase\Services\PurchaseRefundSummaryService;
use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Models\Tenant;
use App\Domains\Ticket\Models\Ticket; use App\Domains\Ticket\Models\Ticket;
use App\Domains\Ticket\Services\TicketPresentationResolver; use App\Domains\Ticket\Services\TicketPresentationResolver;
@ -17,6 +18,7 @@ class AdminAppSaleService
{ {
public function __construct( public function __construct(
protected CheckoutService $checkoutService, protected CheckoutService $checkoutService,
protected PurchaseRefundSummaryService $refundSummaryService,
) {} ) {}
public function confirmedSalesTotal(Tenant $tenant): string public function confirmedSalesTotal(Tenant $tenant): string
@ -29,6 +31,11 @@ class AdminAppSaleService
return number_format((float) $total, 2, '.', ''); return number_format((float) $total, 2, '.', '');
} }
public function refundedTotal(Tenant $tenant): string
{
return $this->refundSummaryService->totalForTenant($tenant);
}
/** /**
* @param array{ * @param array{
* q?: string|null, * q?: string|null,

View File

@ -15,20 +15,24 @@ class AdminAppTicketCollection extends ResourceCollection
private readonly int $totalTickets; private readonly int $totalTickets;
private readonly string $refundedTotal;
public function __construct(AdminAppTicketResult $result) public function __construct(AdminAppTicketResult $result)
{ {
parent::__construct($result->tickets); parent::__construct($result->tickets);
$this->scannedTickets = $result->scannedTickets; $this->scannedTickets = $result->scannedTickets;
$this->totalTickets = $result->totalTickets; $this->totalTickets = $result->totalTickets;
$this->refundedTotal = $result->refundedTotal;
} }
/** @return array{scanned_tickets: int, total_tickets: int} */ /** @return array{scanned_tickets: int, total_tickets: int, refunded_total: string} */
public function with(Request $request): array public function with(Request $request): array
{ {
return [ return [
'scanned_tickets' => $this->scannedTickets, 'scanned_tickets' => $this->scannedTickets,
'total_tickets' => $this->totalTickets, 'total_tickets' => $this->totalTickets,
'refunded_total' => $this->refundedTotal,
]; ];
} }
} }

View File

@ -12,5 +12,6 @@ final readonly class AdminAppTicketResult
public LengthAwarePaginator $tickets, public LengthAwarePaginator $tickets,
public int $scannedTickets, public int $scannedTickets,
public int $totalTickets, public int $totalTickets,
public string $refundedTotal,
) {} ) {}
} }

View File

@ -4,6 +4,7 @@ namespace App\Domains\Ticket\Services;
use App\Domains\Auth\Models\User; use App\Domains\Auth\Models\User;
use App\Domains\Purchase\Models\PurchaseItem; use App\Domains\Purchase\Models\PurchaseItem;
use App\Domains\Purchase\Services\PurchaseRefundSummaryService;
use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Models\Tenant;
use App\Domains\Ticket\Models\Ticket; use App\Domains\Ticket\Models\Ticket;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@ -27,6 +28,7 @@ class AdminAppTicketService
public function __construct( public function __construct(
private readonly AdminAppTicketColumnService $columnService, private readonly AdminAppTicketColumnService $columnService,
private readonly AdminAppTicketRowService $rowService, private readonly AdminAppTicketRowService $rowService,
private readonly PurchaseRefundSummaryService $refundSummaryService,
) {} ) {}
/** /**
@ -68,6 +70,7 @@ class AdminAppTicketService
tickets: $tickets, tickets: $tickets,
scannedTickets: $scannedTickets, scannedTickets: $scannedTickets,
totalTickets: $totalTickets, totalTickets: $totalTickets,
refundedTotal: $this->refundSummaryService->totalForTenant($tenant),
); );
} }

View File

@ -163,6 +163,7 @@ class AdminAppSaleControllerTest extends TestCase
'cantidad' => 3, 'cantidad' => 3,
'precio_unitario' => '10000.00', 'precio_unitario' => '10000.00',
'total' => '30000.00', 'total' => '30000.00',
'refunded_amount' => '1250.00',
]); ]);
$pendingCart = Cart::query()->create([ $pendingCart = Cart::query()->create([
@ -203,6 +204,7 @@ class AdminAppSaleControllerTest extends TestCase
'cantidad' => 2, 'cantidad' => 2,
'precio_unitario' => '10000.00', 'precio_unitario' => '10000.00',
'total' => '20000.00', 'total' => '20000.00',
'refunded_amount' => '2500.00',
]); ]);
$supersededPurchase = Purchase::query()->create([ $supersededPurchase = Purchase::query()->create([
@ -237,7 +239,8 @@ class AdminAppSaleControllerTest extends TestCase
->assertJsonPath('data.2.status_label', 'Confirmado') ->assertJsonPath('data.2.status_label', 'Confirmado')
->assertJsonPath('data.3.id', $supersededPurchase->id) ->assertJsonPath('data.3.id', $supersededPurchase->id)
->assertJsonPath('data.3.admin_status', Purchase::ADMIN_STATUS_CANCELLED) ->assertJsonPath('data.3.admin_status', Purchase::ADMIN_STATUS_CANCELLED)
->assertJsonPath('data.3.status_label', 'Anulado'); ->assertJsonPath('data.3.status_label', 'Anulado')
->assertJsonPath('refunded_total', '3750.00');
$this->getJson('/api/v1/adminapp/tenant/sales?status='.Purchase::STATUS_SUPERSEDED) $this->getJson('/api/v1/adminapp/tenant/sales?status='.Purchase::STATUS_SUPERSEDED)
->assertUnprocessable(); ->assertUnprocessable();

View File

@ -541,17 +541,56 @@ class AdminAppTicketControllerTest extends TestCase
$this->createTicket($tenant, $admin)->update(['disabled_at' => now()]); $this->createTicket($tenant, $admin)->update(['disabled_at' => now()]);
$this->createTicket($otherTenant, $otherUser)->update(['used_at' => now()]); $this->createTicket($otherTenant, $otherUser)->update(['used_at' => now()]);
$catalogItem = CatalogItem::query()->create([
'tenant_code' => $tenant->codigo,
'slug' => 'refund-summary-item',
'nombre' => 'Entrada',
'precio' => '1000.00',
]);
$purchase = Purchase::query()->create([
'tenant_codigo' => $tenant->codigo,
'status' => Purchase::STATUS_PAID,
'total' => '1000.00',
]);
PurchaseItem::query()->create([
'compra_id' => $purchase->id,
'source_catalog_item_id' => $catalogItem->id,
'nombre' => 'Entrada',
'item_nombre' => 'Entrada',
'cantidad' => 1,
'precio_unitario' => '1000.00',
'total' => '1000.00',
'refunded_amount' => '250.00',
]);
$otherPurchase = Purchase::query()->create([
'tenant_codigo' => $otherTenant->codigo,
'status' => Purchase::STATUS_PAID,
'total' => '2000.00',
]);
PurchaseItem::query()->create([
'compra_id' => $otherPurchase->id,
'source_catalog_item_id' => $catalogItem->id,
'nombre' => 'Otra entrada',
'item_nombre' => 'Otra entrada',
'cantidad' => 1,
'precio_unitario' => '2000.00',
'total' => '2000.00',
'refunded_amount' => '2000.00',
]);
$this->getJson('/api/v1/adminapp/tenant/tickets?q=does-not-match') $this->getJson('/api/v1/adminapp/tenant/tickets?q=does-not-match')
->assertOk() ->assertOk()
->assertJsonCount(0, 'data') ->assertJsonCount(0, 'data')
->assertJsonPath('scanned_tickets', 0) ->assertJsonPath('scanned_tickets', 0)
->assertJsonPath('total_tickets', 0); ->assertJsonPath('total_tickets', 0)
->assertJsonPath('refunded_total', '250.00');
$this->getJson('/api/v1/adminapp/tenant/tickets') $this->getJson('/api/v1/adminapp/tenant/tickets')
->assertOk() ->assertOk()
->assertJsonCount(5, 'data') ->assertJsonCount(5, 'data')
->assertJsonPath('scanned_tickets', 1) ->assertJsonPath('scanned_tickets', 1)
->assertJsonPath('total_tickets', 2); ->assertJsonPath('total_tickets', 2)
->assertJsonPath('refunded_total', '250.00');
} }
public function test_it_returns_structured_variant_properties(): void public function test_it_returns_structured_variant_properties(): void