refactor(backend): reorganize domains into Core, Commerce, Ticketing and Shared

This commit is contained in:
2026-09-18 10:14:02 -03:00
parent 4659c1049d
commit 1241e1f7e8
425 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,206 @@
<?php
namespace App\Domains\Sale\Services;
use App\Domains\Logging\Models\ValueChange;
use App\Domains\Purchase\Models\Purchase;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Support\Collection;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Symfony\Component\HttpFoundation\StreamedResponse;
class AdminAppSaleExcelService
{
/** @param Collection<int, Purchase> $sales */
public function downloadSales(Tenant $tenant, Collection $sales, string $timeZone): StreamedResponse
{
$generatedAt = now();
$spreadsheet = $this->spreadsheet($tenant, 'Historial de ventas');
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('Ventas');
$sheet->fromArray([
'ID',
'Fecha',
'Cliente',
'Cantidad',
'Estado',
'Importe',
'Tickets',
], null, 'A1');
foreach ($sales->values() as $index => $sale) {
$row = $index + 2;
$sheet->setCellValueExplicit("A{$row}", '#'.$sale->id, DataType::TYPE_STRING);
if ($sale->created_at) {
$sheet->setCellValue(
"B{$row}",
Date::dateTimeToExcel($sale->created_at->copy()->timezone($timeZone)),
);
}
$sheet->setCellValueExplicit(
"C{$row}",
$sale->nombre_apellido ?: 'Sin nombre',
DataType::TYPE_STRING,
);
$sheet->setCellValue("D{$row}", (int) ($sale->quantity ?? 0));
$sheet->setCellValue("E{$row}", $this->saleStatus($sale->status));
$sheet->setCellValue("F{$row}", (float) $sale->total);
$sheet->setCellValue("G{$row}", (int) ($sale->tickets_count ?? 0));
}
$lastRow = max(2, $sales->count() + 1);
$sheet->getStyle("B2:B{$lastRow}")->getNumberFormat()->setFormatCode('dd/mm/yyyy hh:mm');
$sheet->getStyle("F2:F{$lastRow}")->getNumberFormat()->setFormatCode('$ #,##0.00');
$this->formatSheet($spreadsheet, 'A1:G1', "A1:G{$lastRow}", [
'A' => 13,
'B' => 20,
'C' => 32,
'D' => 12,
'E' => 22,
'F' => 16,
'G' => 12,
]);
return $this->download(
$spreadsheet,
'ventas_'.$tenant->codigo.'_'
.$generatedAt->copy()->timezone($timeZone)->format('Ymd_His').'.xlsx',
);
}
/** @param Collection<int, ValueChange> $modifications */
public function downloadModifications(
Tenant $tenant,
Collection $modifications,
string $timeZone,
): StreamedResponse {
$generatedAt = now();
$spreadsheet = $this->spreadsheet($tenant, 'Historial de modificaciones de ventas');
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('Modificaciones');
$sheet->fromArray([
'Fecha',
'Hora',
'Venta',
'Cliente',
'Campo',
'Valor anterior',
'Valor nuevo',
'Modificado por',
], null, 'A1');
foreach ($modifications->values() as $index => $modification) {
$row = $index + 2;
$changedAt = $modification->changed_at->copy()->timezone($timeZone);
$sale = $modification->trackable;
$sheet->setCellValue("A{$row}", Date::dateTimeToExcel($changedAt));
$sheet->setCellValue("B{$row}", Date::dateTimeToExcel($changedAt));
$sheet->setCellValueExplicit(
"C{$row}",
'#'.$modification->trackable_id,
DataType::TYPE_STRING,
);
$sheet->setCellValueExplicit(
"D{$row}",
$sale?->nombre_apellido ?: 'Sin nombre',
DataType::TYPE_STRING,
);
$sheet->setCellValueExplicit(
"E{$row}",
$modification->attribute,
DataType::TYPE_STRING,
);
$sheet->setCellValueExplicit(
"F{$row}",
$modification->old_value ?? '-',
DataType::TYPE_STRING,
);
$sheet->setCellValueExplicit(
"G{$row}",
$modification->new_value ?? '-',
DataType::TYPE_STRING,
);
$sheet->setCellValueExplicit(
"H{$row}",
$modification->user?->nombre_apellido ?? 'Sistema',
DataType::TYPE_STRING,
);
}
$lastRow = max(2, $modifications->count() + 1);
$sheet->getStyle("A2:A{$lastRow}")->getNumberFormat()->setFormatCode('dd/mm/yyyy');
$sheet->getStyle("B2:B{$lastRow}")->getNumberFormat()->setFormatCode('hh:mm:ss');
$this->formatSheet($spreadsheet, 'A1:H1', "A1:H{$lastRow}", [
'A' => 14,
'B' => 12,
'C' => 13,
'D' => 32,
'E' => 20,
'F' => 24,
'G' => 24,
'H' => 28,
]);
return $this->download(
$spreadsheet,
'historial_modificaciones_'.$tenant->codigo.'_'
.$generatedAt->copy()->timezone($timeZone)->format('Ymd_His').'.xlsx',
);
}
private function spreadsheet(Tenant $tenant, string $title): Spreadsheet
{
$spreadsheet = new Spreadsheet;
$spreadsheet->getProperties()
->setCreator('Shopit')
->setTitle($title)
->setSubject($tenant->nombre);
return $spreadsheet;
}
/** @param array<string, int> $widths */
private function formatSheet(
Spreadsheet $spreadsheet,
string $headerRange,
string $filterRange,
array $widths,
): void {
$sheet = $spreadsheet->getActiveSheet();
$sheet->getStyle($headerRange)->applyFromArray([
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
'fill' => [
'fillType' => Fill::FILL_SOLID,
'startColor' => ['rgb' => '26382E'],
],
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER],
]);
$sheet->getRowDimension(1)->setRowHeight(24);
$sheet->freezePane('A2');
$sheet->setAutoFilter($filterRange);
foreach ($widths as $column => $width) {
$sheet->getColumnDimension($column)->setWidth($width);
}
}
private function download(Spreadsheet $spreadsheet, string $filename): StreamedResponse
{
return response()->streamDownload(function () use ($spreadsheet): void {
(new Xlsx($spreadsheet))->save('php://output');
$spreadsheet->disconnectWorksheets();
}, $filename, [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}
private function saleStatus(string $status): string
{
return Purchase::adminStatusNameFor($status) ?? $status;
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Domains\Sale\Services;
use App\Domains\Logging\Models\ValueChange;
use App\Domains\Purchase\Models\Purchase;
use App\Domains\Tenant\Models\Tenant;
use Barryvdh\DomPDF\Facade\Pdf;
use Barryvdh\DomPDF\PDF as DomPdf;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
class AdminAppSalePdfService
{
/** @param Collection<int, Purchase> $sales */
public function downloadSales(Tenant $tenant, Collection $sales, string $timeZone): Response
{
$generatedAt = now();
$pdf = Pdf::loadView('pdf.adminapp.sales', [
'tenant' => $tenant,
'sales' => $sales,
'generatedAt' => $generatedAt,
'timeZone' => $timeZone,
'confirmedSalesTotal' => number_format(
(float) $sales->where('status', Purchase::STATUS_PAID)->sum('total'),
2,
'.',
'',
),
])->setPaper('a4', 'landscape');
$this->addPageNumbers($pdf);
return $pdf->download(
'ventas_'.$tenant->codigo.'_'
.$generatedAt->copy()->timezone($timeZone)->format('Ymd_His').'.pdf'
);
}
/** @param Collection<int, ValueChange> $modifications */
public function downloadModifications(
Tenant $tenant,
Collection $modifications,
string $timeZone,
): Response {
$generatedAt = now();
$pdf = Pdf::loadView('pdf.adminapp.sale-modifications', [
'tenant' => $tenant,
'modifications' => $modifications,
'generatedAt' => $generatedAt,
'timeZone' => $timeZone,
])->setPaper('a4', 'landscape');
$this->addPageNumbers($pdf);
return $pdf->download(
'historial_modificaciones_'.$tenant->codigo.'_'
.$generatedAt->copy()->timezone($timeZone)->format('Ymd_His').'.pdf'
);
}
private function addPageNumbers(DomPdf $pdf): void
{
$pdf->render();
$domPdf = $pdf->getDomPDF();
$font = $domPdf->getFontMetrics()->getFont('DejaVu Sans');
$domPdf->getCanvas()->page_text(
385,
575,
'Página {PAGE_NUM} de {PAGE_COUNT}',
$font,
7,
[0.48, 0.52, 0.49],
);
}
}

View File

@@ -0,0 +1,252 @@
<?php
namespace App\Domains\Sale\Services;
use App\Domains\Logging\Models\ValueChange;
use App\Domains\Purchase\Models\Purchase;
use App\Domains\Purchase\Services\CheckoutService;
use App\Domains\Purchase\Services\PurchaseRefundSummaryService;
use App\Domains\Tenant\Models\Tenant;
use App\Domains\Ticket\Models\Ticket;
use App\Domains\Ticket\Services\TicketPresentationResolver;
use App\Domains\Ticket\Services\TicketValidityResolver;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
class AdminAppSaleService
{
public function __construct(
protected CheckoutService $checkoutService,
protected PurchaseRefundSummaryService $refundSummaryService,
) {}
public function confirmedSalesTotal(Tenant $tenant): string
{
$total = Purchase::query()
->where('tenant_codigo', $tenant->codigo)
->where('status', Purchase::STATUS_PAID)
->sum('total');
return number_format((float) $total, 2, '.', '');
}
public function refundedTotal(Tenant $tenant): string
{
return $this->refundSummaryService->totalForTenant($tenant);
}
/**
* @param array{
* q?: string|null,
* id?: int|null,
* sale_date?: string|null,
* status?: string|null,
* sort_by?: string,
* sort_direction?: string
* } $filters
* @return LengthAwarePaginator<Purchase>
*/
public function sales(Tenant $tenant, array $filters = []): LengthAwarePaginator
{
return $this->salesQuery($tenant, $filters)
->paginateFromRequest()
->withQueryString();
}
public function detail(Tenant $tenant, int $saleId): Purchase
{
return Purchase::query()
->where('tenant_codigo', $tenant->codigo)
->with('items')
->findOrFail($saleId);
}
/** @return Collection<int, Ticket> */
public function tickets(Tenant $tenant, int $saleId): Collection
{
return $this->findForTenant($tenant, $saleId)
->tickets()
->with([...TicketValidityResolver::RELATIONS, ...TicketPresentationResolver::RELATIONS, 'refund'])
->orderBy('id')
->get();
}
public function confirm(Tenant $tenant, int $saleId): Purchase
{
$sale = $this->findForTenant($tenant, $saleId);
return $this->saleForResponse(
$this->checkoutService->confirmPaidPurchase($sale)
);
}
public function cancel(Tenant $tenant, int $saleId): Purchase
{
$sale = $this->findForTenant($tenant, $saleId);
return $this->saleForResponse(
$this->checkoutService->cancelPurchaseFromAdmin($sale)
);
}
/**
* @param array<string, mixed> $filters
* @return Collection<int, Purchase>
*/
public function salesForExport(Tenant $tenant, array $filters = []): Collection
{
return $this->salesQuery($tenant, $filters)->get();
}
/**
* @param array<string, mixed> $filters
* @return LengthAwarePaginator<ValueChange>
*/
public function modifications(Tenant $tenant, array $filters = []): LengthAwarePaginator
{
return $this->modificationsQuery($tenant, $filters)
->paginateFromRequest()
->withQueryString();
}
/**
* @param array<string, mixed> $filters
* @return Collection<int, ValueChange>
*/
public function modificationsForExport(Tenant $tenant, array $filters = []): Collection
{
return $this->modificationsQuery($tenant, $filters)->get();
}
/** @param array<string, mixed> $filters */
protected function salesQuery(Tenant $tenant, array $filters): Builder
{
$sortColumns = [
'id' => 'id',
'date' => 'created_at',
'customer_name' => 'nombre_apellido',
'quantity' => 'quantity',
'status' => 'status',
'total' => 'total',
];
$requestedSort = $filters['sort_by'] ?? 'date';
$sortBy = array_key_exists($requestedSort, $sortColumns) ? $requestedSort : 'date';
$requestedDirection = $filters['sort_direction'] ?? 'desc';
$sortDirection = in_array($requestedDirection, ['asc', 'desc'], true)
? $requestedDirection
: 'desc';
return Purchase::query()
->where('tenant_codigo', $tenant->codigo)
->when($filters['q'] ?? null, function (Builder $query, string $search): void {
$term = trim($search);
$query->where(function (Builder $query) use ($term): void {
$query
->where('id', 'like', "%{$term}%")
->orWhere('nombre_apellido', 'like', "%{$term}%")
->orWhere('created_at', 'like', "%{$term}%");
});
})
->when($filters['id'] ?? null, fn (Builder $query, int $id): Builder => $query->whereKey($id))
->when(
$filters['sale_date'] ?? null,
fn (Builder $query, string $date): Builder => $query->whereDate('created_at', $date)
)
->when(
$filters['status'] ?? null,
fn (Builder $query, string $status): Builder => $query->whereIn(
'status',
Purchase::realStatusesForAdminStatus($status),
)
)
->select('compras.*')
->selectRaw(
'(SELECT COALESCE(SUM(purchase_items.cantidad), 0) '
.'FROM compra_items AS purchase_items '
.'WHERE purchase_items.compra_id = compras.id) AS quantity',
)
->withCount('tickets')
->orderBy($sortColumns[$sortBy], $sortDirection)
->when($sortBy !== 'id', fn (Builder $query): Builder => $query->orderByDesc('id'));
}
/**
* @param array<string, mixed> $filters
* @return Builder<ValueChange>
*/
protected function modificationsQuery(Tenant $tenant, array $filters): Builder
{
return ValueChange::query()
->where('tenant_code', $tenant->codigo)
->where('trackable_type', (new Purchase)->getMorphClass())
->when($filters['q'] ?? null, function (Builder $query, string $search): void {
$term = trim($search);
$query->where(function (Builder $query) use ($term): void {
$query
->where('trackable_id', 'like', "%{$term}%")
->orWhereHasMorph(
'trackable',
[Purchase::class],
function (Builder $sales) use ($term): void {
$sales
->where('nombre_apellido', 'like', "%{$term}%")
->orWhere('created_at', 'like', "%{$term}%");
},
);
});
})
->when(
$filters['id'] ?? null,
fn (Builder $query, int $id): Builder => $query->where('trackable_id', $id)
)
->when(
$filters['sale_date'] ?? null,
fn (Builder $query, string $date): Builder => $query->whereHasMorph(
'trackable',
[Purchase::class],
fn (Builder $sales): Builder => $sales->whereDate('created_at', $date),
)
)
->when(
$filters['status'] ?? null,
fn (Builder $query, string $status): Builder => $query
->where('attribute', 'status')
->whereIn('new_value', Purchase::realStatusesForAdminStatus($status))
)
->with(['trackable', 'user'])
->orderByDesc('changed_at')
->orderByDesc('id');
}
protected function findForTenant(Tenant $tenant, int $saleId): Purchase
{
return Purchase::query()
->where('tenant_codigo', $tenant->codigo)
->findOrFail($saleId);
}
protected function saleForResponse(Purchase $sale): Purchase
{
/** @var Purchase */
return Purchase::query()
->select('compras.*')
->selectRaw($this->quantityExpression())
->withCount('tickets')
->findOrFail($sale->getKey());
}
protected function quantityExpression(): string
{
return <<<'SQL'
COALESCE(
(SELECT SUM(compra_items.cantidad)
FROM compra_items
WHERE compra_items.compra_id = compras.id),
0
) AS quantity
SQL;
}
}