diff --git a/app/Domains/Sale/Controllers/AdminApp/SaleController.php b/app/Domains/Sale/Controllers/AdminApp/SaleController.php index 8e5c302..8091932 100644 --- a/app/Domains/Sale/Controllers/AdminApp/SaleController.php +++ b/app/Domains/Sale/Controllers/AdminApp/SaleController.php @@ -3,6 +3,7 @@ namespace App\Domains\Sale\Controllers\AdminApp; use App\Domains\Sale\Requests\AdminAppSaleIndexRequest; +use App\Domains\Sale\Resources\AdminApp\SaleDetailResource; use App\Domains\Sale\Resources\AdminApp\SaleModificationResource; use App\Domains\Sale\Resources\AdminApp\SaleResource; use App\Domains\Sale\Services\AdminAppSalePdfService; @@ -30,6 +31,13 @@ class SaleController extends Controller ]); } + public function show(Request $request, int $sale): SaleDetailResource + { + $tenant = $request->user()->tenant()->firstOrFail(); + + return new SaleDetailResource($this->saleService->detail($tenant, $sale)); + } + public function modifications(Request $request): AnonymousResourceCollection { return SaleModificationResource::collection( diff --git a/app/Domains/Sale/Resources/AdminApp/SaleDetailResource.php b/app/Domains/Sale/Resources/AdminApp/SaleDetailResource.php new file mode 100644 index 0000000..d389d1a --- /dev/null +++ b/app/Domains/Sale/Resources/AdminApp/SaleDetailResource.php @@ -0,0 +1,50 @@ + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'items' => $this->items->map(fn (PurchaseItem $item): array => [ + 'id' => $item->id, + 'product' => $item->item_nombre, + 'event_dates' => $this->eventDates($item), + 'quantity' => (int) $item->cantidad, + 'unit_price' => $this->formatMoney($item->precio_unitario), + 'total' => $this->formatMoney($item->total), + ])->values(), + 'total' => $this->formatMoney($this->total), + ]; + } + + /** @return list */ + private function eventDates(PurchaseItem $item): array + { + return collect($item->variant_attributes ?? []) + ->filter(fn (mixed $attribute): bool => is_array($attribute) + && mb_strtolower(trim((string) ($attribute['name'] ?? ''))) === 'fecha') + ->flatMap(function (array $attribute): array { + $value = $attribute['value'] ?? []; + + return is_array($value) ? $value : [$value]; + }) + ->filter(fn (mixed $date): bool => is_string($date) && $date !== '') + ->values() + ->all(); + } + + private function formatMoney(float|int|string|null $amount): string + { + return number_format((float) ($amount ?? 0), 2, '.', ''); + } +} diff --git a/app/Domains/Sale/Services/AdminAppSaleService.php b/app/Domains/Sale/Services/AdminAppSaleService.php index fda53e6..6202ccc 100644 --- a/app/Domains/Sale/Services/AdminAppSaleService.php +++ b/app/Domains/Sale/Services/AdminAppSaleService.php @@ -39,6 +39,14 @@ class AdminAppSaleService ->withQueryString(); } + public function detail(Tenant $tenant, int $saleId): Purchase + { + return Purchase::query() + ->where('tenant_codigo', $tenant->codigo) + ->with('items') + ->findOrFail($saleId); + } + /** * @param array $filters * @return Collection diff --git a/app/Domains/Sale/routes/adminapp.php b/app/Domains/Sale/routes/adminapp.php index 3eea7bd..c5d4325 100644 --- a/app/Domains/Sale/routes/adminapp.php +++ b/app/Domains/Sale/routes/adminapp.php @@ -10,4 +10,5 @@ Route::prefix('v1/adminapp/tenant') Route::get('sales/pdf', [SaleController::class, 'downloadPdf']); Route::get('sales/modifications', [SaleController::class, 'modifications']); Route::get('sales/modifications/pdf', [SaleController::class, 'downloadModificationsPdf']); + Route::get('sales/{sale}', [SaleController::class, 'show'])->whereNumber('sale'); }); diff --git a/tests/Feature/Sale/AdminAppSaleControllerTest.php b/tests/Feature/Sale/AdminAppSaleControllerTest.php new file mode 100644 index 0000000..c0c2e40 --- /dev/null +++ b/tests/Feature/Sale/AdminAppSaleControllerTest.php @@ -0,0 +1,102 @@ +seed(AuthorizationSeeder::class); + WebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); + } + + public function test_authentication_is_required_to_read_a_sale_detail(): void + { + $this->getJson('/api/v1/adminapp/tenant/sales/1')->assertUnauthorized(); + } + + public function test_an_adminapp_user_can_read_a_sale_detail_from_its_tenant(): void + { + $tenant = $this->createTenant('acme'); + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + $purchase = Purchase::query()->create([ + 'tenant_codigo' => $tenant->codigo, + 'status' => Purchase::STATUS_PAID, + 'total' => '40000.00', + 'nombre_apellido' => 'Ada Lovelace', + ]); + PurchaseItem::query()->create([ + 'compra_id' => $purchase->id, + 'source_catalog_item_id' => 10, + 'nombre' => 'Comida', + 'item_nombre' => 'Comida', + 'variant_attributes' => [ + ['name' => 'Fecha', 'value' => ['2026-10-12']], + ['name' => 'Servicio', 'value' => 'Almuerzo'], + ], + 'cantidad' => 2, + 'precio_unitario' => '10000.00', + 'total' => '20000.00', + ]); + + $this->getJson("/api/v1/adminapp/tenant/sales/{$purchase->id}") + ->assertOk() + ->assertJsonPath('data.id', $purchase->id) + ->assertJsonPath('data.items.0.product', 'Comida') + ->assertJsonPath('data.items.0.event_dates.0', '2026-10-12') + ->assertJsonPath('data.items.0.quantity', 2) + ->assertJsonPath('data.items.0.unit_price', '10000.00') + ->assertJsonPath('data.items.0.total', '20000.00') + ->assertJsonPath('data.total', '40000.00'); + } + + public function test_an_adminapp_user_cannot_read_a_sale_from_another_tenant(): void + { + $tenant = $this->createTenant('acme'); + $otherTenant = $this->createTenant('other'); + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + $foreignPurchase = Purchase::query()->create([ + 'tenant_codigo' => $otherTenant->codigo, + 'status' => Purchase::STATUS_PAID, + 'total' => '10000.00', + ]); + + $this->getJson("/api/v1/adminapp/tenant/sales/{$foreignPurchase->id}") + ->assertNotFound(); + } + + private function createTenant(string $code): Tenant + { + return Tenant::query()->create([ + 'codigo' => $code, + 'nombre' => ucfirst($code), + 'dominio' => "{$code}.test", + 'website_type_code' => 'onticket', + ]); + } + + private function createAdminAppUser(Tenant $tenant): User + { + return User::factory()->create([ + 'rol_codigo' => RoleCode::AdminApp->value, + 'tenant_codigo' => $tenant->codigo, + ]); + } +}