feat(sales): scope sales actions totals history and exports by event
This commit is contained in:
@@ -8,12 +8,13 @@ use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class PurchaseRefundSummaryService
|
||||
{
|
||||
public function totalForTenant(Tenant $tenant): string
|
||||
public function totalForTenant(Tenant $tenant, ?int $eventId = null): string
|
||||
{
|
||||
$total = TicketRefund::query()
|
||||
->whereHas(
|
||||
'purchaseItem.purchase',
|
||||
fn (Builder $query): Builder => $query->where('tenant_codigo', $tenant->codigo)
|
||||
->when($eventId !== null, fn (Builder $purchase) => $purchase->where('event_id', $eventId))
|
||||
)
|
||||
->sum('amount');
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Domains\Commerce\Sale\Resources\AdminApp\SaleTicketResource;
|
||||
use App\Domains\Commerce\Sale\Services\AdminAppSaleExcelService;
|
||||
use App\Domains\Commerce\Sale\Services\AdminAppSalePdfService;
|
||||
use App\Domains\Commerce\Sale\Services\AdminAppSaleService;
|
||||
use App\Domains\Core\Auth\Services\EventScopeService;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
@@ -27,15 +28,20 @@ class SaleController extends Controller
|
||||
protected AdminAppSaleExcelService $saleExcelService,
|
||||
) {}
|
||||
|
||||
private function eventId(Request $request): ?int
|
||||
{
|
||||
return app(EventScopeService::class)->eventId($request->user());
|
||||
}
|
||||
|
||||
public function index(AdminAppSaleIndexRequest $request): AnonymousResourceCollection
|
||||
{
|
||||
$tenant = $request->user()->tenant()->firstOrFail();
|
||||
|
||||
return SaleResource::collection(
|
||||
$this->saleService->sales($tenant, $request->validated())
|
||||
$this->saleService->sales($tenant, $request->validated(), $this->eventId($request))
|
||||
)->additional([
|
||||
'confirmed_sales_total' => $this->saleService->confirmedSalesTotal($tenant),
|
||||
'refunded_total' => $this->saleService->refundedTotal($tenant),
|
||||
'confirmed_sales_total' => $this->saleService->confirmedSalesTotal($tenant, $this->eventId($request)),
|
||||
'refunded_total' => $this->saleService->refundedTotal($tenant, $this->eventId($request)),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -43,7 +49,7 @@ class SaleController extends Controller
|
||||
{
|
||||
$tenant = $request->user()->tenant()->firstOrFail();
|
||||
|
||||
return new SaleDetailResource($this->saleService->detail($tenant, $sale));
|
||||
return new SaleDetailResource($this->saleService->detail($tenant, $sale, $this->eventId($request)));
|
||||
}
|
||||
|
||||
public function tickets(Request $request, int $sale): AnonymousResourceCollection
|
||||
@@ -51,7 +57,7 @@ class SaleController extends Controller
|
||||
$tenant = $request->user()->tenant()->firstOrFail();
|
||||
|
||||
return SaleTicketResource::collection(
|
||||
$this->saleService->tickets($tenant, $sale)
|
||||
$this->saleService->tickets($tenant, $sale, $this->eventId($request))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,14 +65,14 @@ class SaleController extends Controller
|
||||
{
|
||||
$tenant = $request->user()->tenant()->firstOrFail();
|
||||
|
||||
return new SaleResource($this->saleService->confirm($tenant, $sale));
|
||||
return new SaleResource($this->saleService->confirm($tenant, $sale, $this->eventId($request)));
|
||||
}
|
||||
|
||||
public function cancel(Request $request, int $sale): SaleResource
|
||||
{
|
||||
$tenant = $request->user()->tenant()->firstOrFail();
|
||||
|
||||
return new SaleResource($this->saleService->cancel($tenant, $sale));
|
||||
return new SaleResource($this->saleService->cancel($tenant, $sale, $this->eventId($request)));
|
||||
}
|
||||
|
||||
public function modifications(
|
||||
@@ -76,6 +82,7 @@ class SaleController extends Controller
|
||||
$this->saleService->modifications(
|
||||
$request->user()->tenant()->firstOrFail(),
|
||||
$request->validated(),
|
||||
$this->eventId($request),
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -86,7 +93,7 @@ class SaleController extends Controller
|
||||
|
||||
return $this->salePdfService->downloadSales(
|
||||
$tenant,
|
||||
$this->saleService->salesForExport($tenant, $request->validated()),
|
||||
$this->saleService->salesForExport($tenant, $request->validated(), $this->eventId($request)),
|
||||
$request->validated('timezone'),
|
||||
);
|
||||
}
|
||||
@@ -97,7 +104,7 @@ class SaleController extends Controller
|
||||
|
||||
return $this->salePdfService->downloadModifications(
|
||||
$tenant,
|
||||
$this->saleService->modificationsForExport($tenant, $request->validated()),
|
||||
$this->saleService->modificationsForExport($tenant, $request->validated(), $this->eventId($request)),
|
||||
$request->validated('timezone'),
|
||||
);
|
||||
}
|
||||
@@ -108,7 +115,7 @@ class SaleController extends Controller
|
||||
|
||||
return $this->saleExcelService->downloadSales(
|
||||
$tenant,
|
||||
$this->saleService->salesForExport($tenant, $request->validated()),
|
||||
$this->saleService->salesForExport($tenant, $request->validated(), $this->eventId($request)),
|
||||
$request->validated('timezone'),
|
||||
);
|
||||
}
|
||||
@@ -120,7 +127,7 @@ class SaleController extends Controller
|
||||
|
||||
return $this->saleExcelService->downloadModifications(
|
||||
$tenant,
|
||||
$this->saleService->modificationsForExport($tenant, $request->validated()),
|
||||
$this->saleService->modificationsForExport($tenant, $request->validated(), $this->eventId($request)),
|
||||
$request->validated('timezone'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Domains\Commerce\Sale\Services;
|
||||
|
||||
use App\Shared\Logging\Models\ValueChange;
|
||||
use App\Domains\Commerce\Purchase\Models\Purchase;
|
||||
use App\Domains\Commerce\Purchase\Services\CheckoutService;
|
||||
use App\Domains\Commerce\Purchase\Services\PurchaseRefundSummaryService;
|
||||
@@ -10,6 +9,7 @@ use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
||||
use App\Domains\Ticketing\Ticket\Services\TicketPresentationResolver;
|
||||
use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver;
|
||||
use App\Shared\Logging\Models\ValueChange;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
@@ -21,19 +21,20 @@ class AdminAppSaleService
|
||||
protected PurchaseRefundSummaryService $refundSummaryService,
|
||||
) {}
|
||||
|
||||
public function confirmedSalesTotal(Tenant $tenant): string
|
||||
public function confirmedSalesTotal(Tenant $tenant, ?int $eventId = null): string
|
||||
{
|
||||
$total = Purchase::query()
|
||||
->where('tenant_codigo', $tenant->codigo)
|
||||
->when($eventId !== null, fn (Builder $query) => $query->where('event_id', $eventId))
|
||||
->where('status', Purchase::STATUS_PAID)
|
||||
->sum('total');
|
||||
|
||||
return number_format((float) $total, 2, '.', '');
|
||||
}
|
||||
|
||||
public function refundedTotal(Tenant $tenant): string
|
||||
public function refundedTotal(Tenant $tenant, ?int $eventId = null): string
|
||||
{
|
||||
return $this->refundSummaryService->totalForTenant($tenant);
|
||||
return $this->refundSummaryService->totalForTenant($tenant, $eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,43 +48,44 @@ class AdminAppSaleService
|
||||
* } $filters
|
||||
* @return LengthAwarePaginator<Purchase>
|
||||
*/
|
||||
public function sales(Tenant $tenant, array $filters = []): LengthAwarePaginator
|
||||
public function sales(Tenant $tenant, array $filters = [], ?int $eventId = null): LengthAwarePaginator
|
||||
{
|
||||
return $this->salesQuery($tenant, $filters)
|
||||
return $this->salesQuery($tenant, $filters, $eventId)
|
||||
->paginateFromRequest()
|
||||
->withQueryString();
|
||||
}
|
||||
|
||||
public function detail(Tenant $tenant, int $saleId): Purchase
|
||||
public function detail(Tenant $tenant, int $saleId, ?int $eventId = null): Purchase
|
||||
{
|
||||
return Purchase::query()
|
||||
->where('tenant_codigo', $tenant->codigo)
|
||||
->when($eventId !== null, fn (Builder $query) => $query->where('event_id', $eventId))
|
||||
->with('items')
|
||||
->findOrFail($saleId);
|
||||
}
|
||||
|
||||
/** @return Collection<int, Ticket> */
|
||||
public function tickets(Tenant $tenant, int $saleId): Collection
|
||||
public function tickets(Tenant $tenant, int $saleId, ?int $eventId = null): Collection
|
||||
{
|
||||
return $this->findForTenant($tenant, $saleId)
|
||||
return $this->findForTenant($tenant, $saleId, $eventId)
|
||||
->tickets()
|
||||
->with([...TicketValidityResolver::RELATIONS, ...TicketPresentationResolver::RELATIONS, 'refund'])
|
||||
->orderBy('id')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function confirm(Tenant $tenant, int $saleId): Purchase
|
||||
public function confirm(Tenant $tenant, int $saleId, ?int $eventId = null): Purchase
|
||||
{
|
||||
$sale = $this->findForTenant($tenant, $saleId);
|
||||
$sale = $this->findForTenant($tenant, $saleId, $eventId);
|
||||
|
||||
return $this->saleForResponse(
|
||||
$this->checkoutService->confirmPaidPurchase($sale)
|
||||
);
|
||||
}
|
||||
|
||||
public function cancel(Tenant $tenant, int $saleId): Purchase
|
||||
public function cancel(Tenant $tenant, int $saleId, ?int $eventId = null): Purchase
|
||||
{
|
||||
$sale = $this->findForTenant($tenant, $saleId);
|
||||
$sale = $this->findForTenant($tenant, $saleId, $eventId);
|
||||
|
||||
return $this->saleForResponse(
|
||||
$this->checkoutService->cancelPurchaseFromAdmin($sale)
|
||||
@@ -94,18 +96,18 @@ class AdminAppSaleService
|
||||
* @param array<string, mixed> $filters
|
||||
* @return Collection<int, Purchase>
|
||||
*/
|
||||
public function salesForExport(Tenant $tenant, array $filters = []): Collection
|
||||
public function salesForExport(Tenant $tenant, array $filters = [], ?int $eventId = null): Collection
|
||||
{
|
||||
return $this->salesQuery($tenant, $filters)->get();
|
||||
return $this->salesQuery($tenant, $filters, $eventId)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $filters
|
||||
* @return LengthAwarePaginator<ValueChange>
|
||||
*/
|
||||
public function modifications(Tenant $tenant, array $filters = []): LengthAwarePaginator
|
||||
public function modifications(Tenant $tenant, array $filters = [], ?int $eventId = null): LengthAwarePaginator
|
||||
{
|
||||
return $this->modificationsQuery($tenant, $filters)
|
||||
return $this->modificationsQuery($tenant, $filters, $eventId)
|
||||
->paginateFromRequest()
|
||||
->withQueryString();
|
||||
}
|
||||
@@ -114,13 +116,13 @@ class AdminAppSaleService
|
||||
* @param array<string, mixed> $filters
|
||||
* @return Collection<int, ValueChange>
|
||||
*/
|
||||
public function modificationsForExport(Tenant $tenant, array $filters = []): Collection
|
||||
public function modificationsForExport(Tenant $tenant, array $filters = [], ?int $eventId = null): Collection
|
||||
{
|
||||
return $this->modificationsQuery($tenant, $filters)->get();
|
||||
return $this->modificationsQuery($tenant, $filters, $eventId)->get();
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $filters */
|
||||
protected function salesQuery(Tenant $tenant, array $filters): Builder
|
||||
protected function salesQuery(Tenant $tenant, array $filters, ?int $eventId = null): Builder
|
||||
{
|
||||
$sortColumns = [
|
||||
'id' => 'id',
|
||||
@@ -139,6 +141,7 @@ class AdminAppSaleService
|
||||
|
||||
return Purchase::query()
|
||||
->where('tenant_codigo', $tenant->codigo)
|
||||
->when($eventId !== null, fn (Builder $query) => $query->where('event_id', $eventId))
|
||||
->when($filters['q'] ?? null, function (Builder $query, string $search): void {
|
||||
$term = trim($search);
|
||||
|
||||
@@ -176,11 +179,14 @@ class AdminAppSaleService
|
||||
* @param array<string, mixed> $filters
|
||||
* @return Builder<ValueChange>
|
||||
*/
|
||||
protected function modificationsQuery(Tenant $tenant, array $filters): Builder
|
||||
protected function modificationsQuery(Tenant $tenant, array $filters, ?int $eventId = null): Builder
|
||||
{
|
||||
return ValueChange::query()
|
||||
->where('tenant_code', $tenant->codigo)
|
||||
->where('trackable_type', (new Purchase)->getMorphClass())
|
||||
->when($eventId !== null, fn (Builder $query) => $query->whereHasMorph(
|
||||
'trackable', [Purchase::class],
|
||||
fn (Builder $sales) => $sales->where('event_id', $eventId)))
|
||||
->when($filters['q'] ?? null, function (Builder $query, string $search): void {
|
||||
$term = trim($search);
|
||||
|
||||
@@ -221,10 +227,11 @@ class AdminAppSaleService
|
||||
->orderByDesc('id');
|
||||
}
|
||||
|
||||
protected function findForTenant(Tenant $tenant, int $saleId): Purchase
|
||||
protected function findForTenant(Tenant $tenant, int $saleId, ?int $eventId = null): Purchase
|
||||
{
|
||||
return Purchase::query()
|
||||
->where('tenant_codigo', $tenant->codigo)
|
||||
->when($eventId !== null, fn (Builder $query) => $query->where('event_id', $eventId))
|
||||
->findOrFail($saleId);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,3 +30,10 @@ Consume compras de `Purchase`, datos del tenant y entradas de `Logging`. No es d
|
||||
La consulta paginada y la colección de exportación deben aplicar los mismos filtros para evitar diferencias entre pantalla, PDF y Excel.
|
||||
|
||||
El historial comparte con ventas los filtros de búsqueda, ID, fecha de venta y estado. En el historial, el estado se evalúa sobre `ValueChange.new_value`: representa el resultado de esa modificación y no el estado actual de la venta.
|
||||
|
||||
## Alcance por evento
|
||||
|
||||
Las rutas usan `adminapp.tenant:event`. Para admins de evento, listados, totales,
|
||||
detalles, tickets de compras, confirmación, cancelación, historial y exportaciones
|
||||
se filtran por `user.event_id` además del tenant. Las compras pertenecen a un
|
||||
único evento. Un admin de tenant conserva acceso a sus compras de todos los eventos.
|
||||
|
||||
@@ -4,7 +4,7 @@ use App\Domains\Commerce\Sale\Controllers\AdminApp\SaleController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::prefix('v1/adminapp/tenant')
|
||||
->middleware(['auth:sanctum', 'adminapp.tenant'])
|
||||
->middleware(['auth:sanctum', 'adminapp.tenant:event'])
|
||||
->group(function (): void {
|
||||
Route::get('sales', [SaleController::class, 'index']);
|
||||
Route::get('sales/pdf', [SaleController::class, 'downloadPdf']);
|
||||
|
||||
@@ -20,7 +20,7 @@ Route::prefix('v1/adminapp/forms')
|
||||
DesfileEntryReservationFormController::class
|
||||
)->middleware('adminapp.tenant')->middleware('tenant.menu:adminapp.desfile.reservas')
|
||||
->name('adminapp.forms.desfile.entry-reservation');
|
||||
Route::get('sale', SaleFormController::class)->middleware('adminapp.tenant');
|
||||
Route::get('sale', SaleFormController::class);
|
||||
Route::get('staff', StaffFormController::class)->middleware('adminapp.tenant');
|
||||
Route::get('tickets-filter', TicketFilterFormController::class)->middleware('adminapp.tenant')
|
||||
->middleware('tenant.menu:adminapp.tickets')
|
||||
|
||||
Reference in New Issue
Block a user