Compare commits
12 Commits
4162e2c1cc
...
07d2129410
| Author | SHA1 | Date | |
|---|---|---|---|
| 07d2129410 | |||
| ceb1d1f1af | |||
| 885ab2a6e3 | |||
| 47196a572a | |||
| 45bba9516d | |||
| c6da71894e | |||
| 75d3c819d9 | |||
| bd6d672df2 | |||
| 6b06028771 | |||
| 0aa423e90e | |||
| e7bf2f887a | |||
| 80a02fdc52 |
@@ -172,18 +172,30 @@ class CatalogItem extends Model
|
||||
}
|
||||
|
||||
/** @param Builder<CatalogItem> $query */
|
||||
public function scopeWhereVariantsAvailable(Builder $query): Builder
|
||||
public function scopeWhereAvailable(Builder $query): Builder
|
||||
{
|
||||
return $query->where(function (Builder $query): void {
|
||||
$query
|
||||
->whereDoesntHave('variants')
|
||||
->orWhere('catalog_items.inventory_policy', InventoryPolicy::Unlimited->value)
|
||||
->where('catalog_items.inventory_policy', InventoryPolicy::Unlimited->value)
|
||||
->orWhereHas(
|
||||
'variants.inventory',
|
||||
fn (Builder $inventoryQuery): Builder => $inventoryQuery
|
||||
->whereColumn('inventories.real_stock', '>', 'inventories.reserved_stock')
|
||||
)
|
||||
->orWhere(function (Builder $directItemQuery): void {
|
||||
$directItemQuery
|
||||
->whereDoesntHave('variants')
|
||||
->where(function (Builder $inventoryQuery): void {
|
||||
$inventoryQuery
|
||||
->whereNull('catalog_items.inventory_id')
|
||||
->orWhereHas(
|
||||
'inventory',
|
||||
fn (Builder $availableInventoryQuery): Builder => $availableInventoryQuery
|
||||
->whereColumn('inventories.real_stock', '>', 'inventories.reserved_stock')
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** @return Collection<int, Variant> */
|
||||
|
||||
@@ -46,7 +46,7 @@ class CatalogFeaturedItemResource extends JsonResource
|
||||
$availableStock,
|
||||
$remainingUserQuota,
|
||||
),
|
||||
'variants' => $catalogItem->variants
|
||||
'variants' => $catalogItem->visibleVariants()
|
||||
->map(function (Variant $variant) use ($catalogItem, $remainingUserQuota): array {
|
||||
$variantStock = $catalogItem->inventory_policy === InventoryPolicy::Unlimited
|
||||
? null
|
||||
@@ -115,7 +115,7 @@ class CatalogFeaturedItemResource extends JsonResource
|
||||
private function firstImageUrl(CatalogItem $catalogItem): ?string
|
||||
{
|
||||
$attachment = $catalogItem->attachments->first()
|
||||
?? $catalogItem->variants
|
||||
?? $catalogItem->visibleVariants()
|
||||
->flatMap(fn (Variant $variant) => $variant->attachments)
|
||||
->first();
|
||||
|
||||
|
||||
@@ -16,8 +16,9 @@ class CatalogSearchItemResource extends JsonResource
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$availableStock = $this->availableStock();
|
||||
$visibleVariants = $this->visibleVariants();
|
||||
$attachment = $this->attachments->first()
|
||||
?? $this->variants
|
||||
?? $visibleVariants
|
||||
->flatMap(fn (Variant $variant) => $variant->attachments)
|
||||
->first();
|
||||
|
||||
@@ -30,7 +31,7 @@ class CatalogSearchItemResource extends JsonResource
|
||||
'image' => $attachment?->getTemporaryUrl(1440),
|
||||
'maximum_addable_quantity' => $this->maximumAddable($availableStock),
|
||||
'unavailable_message' => $this->unavailableMessage($availableStock),
|
||||
'variants' => $this->variants
|
||||
'variants' => $visibleVariants
|
||||
->map(function (Variant $variant): array {
|
||||
$variantStock = $this->inventory_policy === InventoryPolicy::Unlimited
|
||||
? null
|
||||
|
||||
@@ -205,6 +205,13 @@ class CatalogService
|
||||
]);
|
||||
|
||||
$visibleVariants = $catalogItem->visibleVariants();
|
||||
if ($catalogItem->type === CatalogItemType::Standard
|
||||
&& ($catalogItem->inventory_id !== null || $catalogItem->variants->isNotEmpty())
|
||||
&& ! $catalogItem->isAvailable()) {
|
||||
throw new NotFoundHttpException('Catalog item is out of stock.');
|
||||
}
|
||||
|
||||
$catalogItem->setRelation('variants', $visibleVariants);
|
||||
$selectedVariant = $variantId === null
|
||||
? $visibleVariants->first()
|
||||
: $visibleVariants->firstWhere('id', $variantId);
|
||||
@@ -231,6 +238,7 @@ class CatalogService
|
||||
|
||||
$paginator = CatalogItem::query()
|
||||
->where('tenant_code', $tenant->codigo)
|
||||
->whereAvailable()
|
||||
->where(function (Builder $query) use ($containsPattern): void {
|
||||
$query
|
||||
->whereRaw('LOWER(nombre) LIKE ?', [$containsPattern])
|
||||
@@ -280,6 +288,7 @@ class CatalogService
|
||||
return CatalogItem::query()
|
||||
->where('tenant_code', $tenant->codigo)
|
||||
->where('category_id', $category->id)
|
||||
->whereAvailable()
|
||||
->with([
|
||||
'attachments',
|
||||
'inventory',
|
||||
|
||||
@@ -49,6 +49,7 @@ class FeaturedGroupService
|
||||
{
|
||||
$query = CatalogItem::query()
|
||||
->where('catalog_items.tenant_code', $featuredGroup->tenant_code)
|
||||
->whereAvailable()
|
||||
->where(function (Builder $query): void {
|
||||
$query
|
||||
->whereDoesntHave('category')
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Domains\Sale\Controllers\AdminApp;
|
||||
|
||||
use App\Domains\Sale\Requests\AdminAppSaleIndexRequest;
|
||||
use App\Domains\Sale\Requests\AdminAppSaleModificationPdfRequest;
|
||||
use App\Domains\Sale\Requests\AdminAppSalePdfRequest;
|
||||
use App\Domains\Sale\Resources\AdminApp\SaleDetailResource;
|
||||
use App\Domains\Sale\Resources\AdminApp\SaleModificationResource;
|
||||
use App\Domains\Sale\Resources\AdminApp\SaleResource;
|
||||
@@ -71,23 +73,25 @@ class SaleController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
public function downloadPdf(AdminAppSaleIndexRequest $request): Response
|
||||
public function downloadPdf(AdminAppSalePdfRequest $request): Response
|
||||
{
|
||||
$tenant = $request->user()->tenant()->firstOrFail();
|
||||
|
||||
return $this->salePdfService->downloadSales(
|
||||
$tenant,
|
||||
$this->saleService->salesForExport($tenant, $request->validated()),
|
||||
$request->validated('timezone'),
|
||||
);
|
||||
}
|
||||
|
||||
public function downloadModificationsPdf(Request $request): Response
|
||||
public function downloadModificationsPdf(AdminAppSaleModificationPdfRequest $request): Response
|
||||
{
|
||||
$tenant = $request->user()->tenant()->firstOrFail();
|
||||
|
||||
return $this->salePdfService->downloadModifications(
|
||||
$tenant,
|
||||
$this->saleService->modificationsForExport($tenant),
|
||||
$request->validated('timezone'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sale\Requests;
|
||||
|
||||
use App\Domains\Shared\Rules\ValidTimezone;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AdminAppSaleModificationPdfRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @return array<string, list<string>> */
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'timezone' => ['required', 'string', new ValidTimezone],
|
||||
];
|
||||
}
|
||||
}
|
||||
17
app/Domains/Sale/Requests/AdminAppSalePdfRequest.php
Normal file
17
app/Domains/Sale/Requests/AdminAppSalePdfRequest.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sale\Requests;
|
||||
|
||||
use App\Domains\Shared\Rules\ValidTimezone;
|
||||
|
||||
class AdminAppSalePdfRequest extends AdminAppSaleIndexRequest
|
||||
{
|
||||
/** @return array<string, list<string>> */
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
...parent::rules(),
|
||||
'timezone' => ['required', 'string', new ValidTimezone],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,14 @@ use Illuminate\Support\Collection;
|
||||
class AdminAppSalePdfService
|
||||
{
|
||||
/** @param Collection<int, Purchase> $sales */
|
||||
public function downloadSales(Tenant $tenant, Collection $sales): Response
|
||||
public function downloadSales(Tenant $tenant, Collection $sales, string $timeZone): Response
|
||||
{
|
||||
$generatedAt = now();
|
||||
$pdf = Pdf::loadView('pdf.adminapp.sales', [
|
||||
'tenant' => $tenant,
|
||||
'sales' => $sales,
|
||||
'generatedAt' => now(),
|
||||
'generatedAt' => $generatedAt,
|
||||
'timeZone' => $timeZone,
|
||||
'confirmedSalesTotal' => number_format(
|
||||
(float) $sales->where('status', Purchase::STATUS_PAID)->sum('total'),
|
||||
2,
|
||||
@@ -30,23 +32,30 @@ class AdminAppSalePdfService
|
||||
$this->addPageNumbers($pdf);
|
||||
|
||||
return $pdf->download(
|
||||
'ventas_'.$tenant->codigo.'_'.now()->format('Ymd_His').'.pdf'
|
||||
'ventas_'.$tenant->codigo.'_'
|
||||
.$generatedAt->copy()->timezone($timeZone)->format('Ymd_His').'.pdf'
|
||||
);
|
||||
}
|
||||
|
||||
/** @param Collection<int, ValueChange> $modifications */
|
||||
public function downloadModifications(Tenant $tenant, Collection $modifications): Response
|
||||
{
|
||||
public function downloadModifications(
|
||||
Tenant $tenant,
|
||||
Collection $modifications,
|
||||
string $timeZone,
|
||||
): Response {
|
||||
$generatedAt = now();
|
||||
$pdf = Pdf::loadView('pdf.adminapp.sale-modifications', [
|
||||
'tenant' => $tenant,
|
||||
'modifications' => $modifications,
|
||||
'generatedAt' => now(),
|
||||
'generatedAt' => $generatedAt,
|
||||
'timeZone' => $timeZone,
|
||||
])->setPaper('a4', 'landscape');
|
||||
|
||||
$this->addPageNumbers($pdf);
|
||||
|
||||
return $pdf->download(
|
||||
'historial_modificaciones_'.$tenant->codigo.'_'.now()->format('Ymd_His').'.pdf'
|
||||
'historial_modificaciones_'.$tenant->codigo.'_'
|
||||
.$generatedAt->copy()->timezone($timeZone)->format('Ymd_His').'.pdf'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
24
app/Domains/Shared/Rules/ValidTimezone.php
Normal file
24
app/Domains/Shared/Rules/ValidTimezone.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Shared\Rules;
|
||||
|
||||
use Closure;
|
||||
use DateTimeZone;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
|
||||
class ValidTimezone implements ValidationRule
|
||||
{
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
if (! is_string($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
new DateTimeZone($value);
|
||||
} catch (Exception) {
|
||||
$fail(__('validation.timezone'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ return [
|
||||
'required_with' => 'The :attribute field is required when :values is present.',
|
||||
'required_without' => 'The :attribute field is required when :values is not present.',
|
||||
'string' => 'The :attribute must be a string.',
|
||||
'timezone' => 'The timezone sent by the browser is invalid.',
|
||||
'unique' => 'The :attribute has already been taken.',
|
||||
'url' => 'The :attribute must be a valid URL.',
|
||||
'uuid' => 'The :attribute must be a valid UUID.',
|
||||
@@ -52,6 +53,7 @@ return [
|
||||
'password_confirmation' => 'password confirmation',
|
||||
'telefono' => 'phone number',
|
||||
'tenant_codigo' => 'tenant',
|
||||
'timezone' => 'timezone',
|
||||
'variant_id' => 'variant',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -37,6 +37,7 @@ return [
|
||||
'required_with' => 'El campo :attribute es obligatorio cuando :values está presente.',
|
||||
'required_without' => 'El campo :attribute es obligatorio cuando :values no está presente.',
|
||||
'string' => ':Attribute debe ser texto.',
|
||||
'timezone' => 'La zona horaria enviada por el navegador no es válida.',
|
||||
'unique' => 'El :attribute ya está en uso.',
|
||||
'url' => ':Attribute debe ser una URL válida.',
|
||||
'uuid' => ':Attribute debe ser un UUID válido.',
|
||||
@@ -52,6 +53,7 @@ return [
|
||||
'password_confirmation' => 'confirmación de contraseña',
|
||||
'telefono' => 'teléfono',
|
||||
'tenant_codigo' => 'tenant',
|
||||
'timezone' => 'zona horaria',
|
||||
'variant_id' => 'variante',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1>Historial de modificaciones de ventas</h1>
|
||||
<p class="subtitle">{{ $tenant->nombre }} · Generado el {{ $generatedAt->format('d/m/Y H:i') }}</p>
|
||||
<p class="subtitle">{{ $tenant->nombre }} · Generado el {{ $generatedAt->copy()->timezone($timeZone)->format('d/m/Y H:i') }}</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
@@ -44,8 +44,8 @@
|
||||
@forelse ($modifications as $modification)
|
||||
@php($sale = $modification->trackable)
|
||||
<tr>
|
||||
<td>{{ $modification->changed_at->format('d/m/Y') }}</td>
|
||||
<td>{{ $modification->changed_at->format('H:i:s') }}</td>
|
||||
<td>{{ $modification->changed_at->copy()->timezone($timeZone)->format('d/m/Y') }}</td>
|
||||
<td>{{ $modification->changed_at->copy()->timezone($timeZone)->format('H:i:s') }}</td>
|
||||
<td>#{{ $modification->trackable_id }}</td>
|
||||
<td>{{ $sale?->nombre_apellido ?: 'Sin nombre' }}</td>
|
||||
<td>{{ $modification->attribute }}</td>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1>Historial de ventas</h1>
|
||||
<p class="subtitle">{{ $tenant->nombre }} · Generado el {{ $generatedAt->format('d/m/Y H:i') }}</p>
|
||||
<p class="subtitle">{{ $tenant->nombre }} · Generado el {{ $generatedAt->copy()->timezone($timeZone)->format('d/m/Y H:i') }}</p>
|
||||
|
||||
<div class="summary">
|
||||
Total de ventas confirmadas en este reporte: <strong>${{ number_format((float) $confirmedSalesTotal, 2, ',', '.') }}</strong>
|
||||
@@ -44,7 +44,7 @@
|
||||
@forelse ($sales as $sale)
|
||||
<tr>
|
||||
<td>#{{ $sale->id }}</td>
|
||||
<td>{{ $sale->created_at?->format('d/m/Y H:i') ?? '-' }}</td>
|
||||
<td>{{ $sale->created_at?->copy()->timezone($timeZone)->format('d/m/Y H:i') ?? '-' }}</td>
|
||||
<td>{{ $sale->nombre_apellido ?: 'Sin nombre' }}</td>
|
||||
<td class="center">{{ (int) ($sale->quantity ?? 0) }}</td>
|
||||
<td>{{ match ($sale->status) {
|
||||
|
||||
@@ -73,19 +73,18 @@ class CatalogControllerTest extends TestCase
|
||||
->assertJsonPath('0.items.0.descripcion', 'Variants description')
|
||||
->assertJsonPath('0.items.0.precio', '100.00')
|
||||
->assertJsonPath('0.items.0.maximum_addable_quantity', 7)
|
||||
->assertJsonCount(3, '0.items.0.variants')
|
||||
->assertJsonCount(2, '0.items.0.variants')
|
||||
->assertJsonPath('0.items.0.variants.0.maximum_addable_quantity', 4)
|
||||
->assertJsonPath('0.items.0.variants.1.maximum_addable_quantity', 3)
|
||||
->assertJsonPath('0.items.0.variants.2.id', $unavailableVariant->id)
|
||||
->assertJsonPath('0.items.0.variants.2.maximum_addable_quantity', 0)
|
||||
->assertJsonPath(
|
||||
'0.items.0.variants.2.unavailable_message',
|
||||
'Este producto no tiene stock disponible.',
|
||||
)
|
||||
->assertJsonPath('1.title', 'Row')
|
||||
->assertJsonPath('1.items.data.0.maximum_addable_quantity', 8)
|
||||
->assertJsonMissingPath('1.items.data.0.stock_tecnico')
|
||||
->assertJsonCount(0, '1.items.data.0.variants');
|
||||
|
||||
$this->assertNotContains(
|
||||
$unavailableVariant->id,
|
||||
collect($response->json('0.items.0.variants'))->pluck('id')->all(),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_maximum_addable_quantity_shares_the_authenticated_user_quota_between_variants(): void
|
||||
@@ -133,7 +132,7 @@ class CatalogControllerTest extends TestCase
|
||||
->assertJsonMissingPath('0.items.0.variants.1.stock_tecnico');
|
||||
}
|
||||
|
||||
public function test_it_includes_out_of_stock_items_with_an_unavailable_message(): void
|
||||
public function test_it_excludes_out_of_stock_items(): void
|
||||
{
|
||||
$tenant = $this->createTenant('catalog-available-variants');
|
||||
$group = $this->createGroup(
|
||||
@@ -161,15 +160,10 @@ class CatalogControllerTest extends TestCase
|
||||
|
||||
$this->getJson("/api/tenants/{$tenant->codigo}/catalog")
|
||||
->assertOk()
|
||||
->assertJsonCount(2, '0.items')
|
||||
->assertJsonPath('0.items.0.nombre', 'Unavailable')
|
||||
->assertJsonPath('0.items.0.maximum_addable_quantity', 0)
|
||||
->assertJsonPath(
|
||||
'0.items.0.unavailable_message',
|
||||
'Este producto no tiene stock disponible.',
|
||||
)
|
||||
->assertJsonPath('0.items.1.nombre', 'Available')
|
||||
->assertJsonPath('0.items.1.unavailable_message', null);
|
||||
->assertJsonCount(1, '0.items')
|
||||
->assertJsonPath('0.items.0.nombre', 'Available')
|
||||
->assertJsonPath('0.items.0.unavailable_message', null)
|
||||
->assertJsonMissing(['nombre' => 'Unavailable']);
|
||||
}
|
||||
|
||||
public function test_column_with_image_uses_item_image_then_variant_image_then_null(): void
|
||||
|
||||
@@ -48,7 +48,7 @@ class CatalogItemDetailControllerTest extends TestCase
|
||||
$this->assertStringContainsString($itemImage->path, $response->json('data.images.0'));
|
||||
}
|
||||
|
||||
public function test_it_lists_unavailable_variants_and_selects_the_first_available_one(): void
|
||||
public function test_it_omits_unavailable_variants_and_selects_the_first_available_one(): void
|
||||
{
|
||||
Storage::fake('s3');
|
||||
$tenant = $this->createTenant('detail-default');
|
||||
@@ -69,14 +69,8 @@ class CatalogItemDetailControllerTest extends TestCase
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertJsonCount(2, 'data.variants')
|
||||
->assertJsonPath('data.variants.0.id', $firstVariant->id)
|
||||
->assertJsonPath('data.variants.0.maximum_addable_quantity', 0)
|
||||
->assertJsonPath(
|
||||
'data.variants.0.unavailable_message',
|
||||
'Este producto no tiene stock disponible.',
|
||||
)
|
||||
->assertJsonPath('data.variants.1.id', $secondVariant->id)
|
||||
->assertJsonCount(1, 'data.variants')
|
||||
->assertJsonPath('data.variants.0.id', $secondVariant->id)
|
||||
->assertJsonPath('data.selected_variant.id', $secondVariant->id)
|
||||
->assertJsonPath('data.selected_variant.maximum_addable_quantity', 6)
|
||||
->assertJsonMissingPath('data.selected_variant.stock_tecnico')
|
||||
@@ -84,6 +78,10 @@ class CatalogItemDetailControllerTest extends TestCase
|
||||
$response
|
||||
->assertJsonMissingPath('data.stock_tecnico')
|
||||
->assertJsonMissingPath('data.images');
|
||||
$this->assertNotContains(
|
||||
$firstVariant->id,
|
||||
collect($response->json('data.variants'))->pluck('id')->all(),
|
||||
);
|
||||
$this->assertStringContainsString($secondImage->path, $response->json('data.selected_variant.images.0'));
|
||||
$this->assertStringNotContainsString($firstImage->path, $response->json('data.selected_variant.images.0'));
|
||||
$this->assertStringNotContainsString($itemImage->path, $response->json('data.selected_variant.images.0'));
|
||||
@@ -93,6 +91,19 @@ class CatalogItemDetailControllerTest extends TestCase
|
||||
)->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_it_does_not_return_an_out_of_stock_item(): void
|
||||
{
|
||||
$tenant = $this->createTenant('detail-out-of-stock');
|
||||
$inventory = Inventory::query()->create([
|
||||
'real_stock' => 5,
|
||||
'reserved_stock' => 5,
|
||||
]);
|
||||
$item = $this->createItem($tenant, 'Sold out item', $inventory);
|
||||
|
||||
$this->getJson("/api/tenants/{$tenant->codigo}/catalog-items/{$item->id}")
|
||||
->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_it_selects_the_requested_variant_and_lists_variant_values_and_stock(): void
|
||||
{
|
||||
Storage::fake('s3');
|
||||
|
||||
@@ -62,6 +62,24 @@ class CatalogSearchTest extends TestCase
|
||||
$this->createCatalogItem($tenant, "Running {$number}");
|
||||
}
|
||||
$exactMatch = $this->createCatalogItem($tenant, 'Running');
|
||||
$outOfStock = CatalogItem::query()->create([
|
||||
'tenant_code' => $tenant->codigo,
|
||||
'slug' => 'running-sold-out',
|
||||
'nombre' => 'Running sold out',
|
||||
'descripcion' => 'Running sold out description',
|
||||
'precio' => 100,
|
||||
]);
|
||||
$outOfStock->variants()->create([
|
||||
'inventory_id' => Inventory::query()->create(['real_stock' => 0])->id,
|
||||
]);
|
||||
CatalogItem::query()->create([
|
||||
'tenant_code' => $tenant->codigo,
|
||||
'inventory_id' => Inventory::query()->create(['real_stock' => 2, 'reserved_stock' => 2])->id,
|
||||
'slug' => 'running-direct-sold-out',
|
||||
'nombre' => 'Running direct sold out',
|
||||
'descripcion' => 'Running direct sold out description',
|
||||
'precio' => 100,
|
||||
]);
|
||||
$this->createCatalogItem($tenant, 'Unrelated');
|
||||
$this->createCatalogItem($otherTenant, 'Running foreign');
|
||||
|
||||
@@ -76,6 +94,8 @@ class CatalogSearchTest extends TestCase
|
||||
->assertJsonPath('meta.total', 6)
|
||||
->assertJsonCount(4, 'data')
|
||||
->assertJsonPath('data.0.id', $exactMatch->id)
|
||||
->assertJsonMissing(['nombre' => 'Running sold out'])
|
||||
->assertJsonMissing(['nombre' => 'Running direct sold out'])
|
||||
->assertJsonMissing(['nombre' => 'Running foreign'])
|
||||
->assertJsonMissing(['nombre' => 'Unrelated']);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,17 @@ class CategoryDetailTest extends TestCase
|
||||
$this->createCatalogItem($tenant, $category, 'Remera C');
|
||||
$firstItem = $this->createCatalogItem($tenant, $category, 'Remera A');
|
||||
$secondItem = $this->createCatalogItem($tenant, $category, 'Remera B');
|
||||
$outOfStock = CatalogItem::query()->create([
|
||||
'tenant_code' => $tenant->codigo,
|
||||
'category_id' => $category->id,
|
||||
'slug' => 'remera-agotada',
|
||||
'nombre' => 'Remera agotada',
|
||||
'descripcion' => 'Sin stock',
|
||||
'precio' => 100,
|
||||
]);
|
||||
$outOfStock->variants()->create([
|
||||
'inventory_id' => Inventory::query()->create(['real_stock' => 0])->id,
|
||||
]);
|
||||
$this->createCatalogItem($tenant, $otherCategory, 'Pantalón');
|
||||
|
||||
$this->getJson("/api/tenants/{$tenant->codigo}/categories/{$category->id}")
|
||||
@@ -44,6 +55,7 @@ class CategoryDetailTest extends TestCase
|
||||
->assertJsonCount(2, 'data')
|
||||
->assertJsonPath('data.0.id', $firstItem->id)
|
||||
->assertJsonPath('data.1.id', $secondItem->id)
|
||||
->assertJsonMissing(['nombre' => 'Remera agotada'])
|
||||
->assertJsonMissing(['nombre' => 'Pantalón']);
|
||||
}
|
||||
|
||||
|
||||
55
tests/Unit/Sale/AdminAppSalePdfRequestTest.php
Normal file
55
tests/Unit/Sale/AdminAppSalePdfRequestTest.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Sale;
|
||||
|
||||
use App\Domains\Sale\Requests\AdminAppSaleModificationPdfRequest;
|
||||
use App\Domains\Sale\Requests\AdminAppSalePdfRequest;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminAppSalePdfRequestTest extends TestCase
|
||||
{
|
||||
public function test_pdf_requests_accept_php_timezone_identifiers_including_browser_aliases(): void
|
||||
{
|
||||
$requests = [
|
||||
new AdminAppSalePdfRequest,
|
||||
new AdminAppSaleModificationPdfRequest,
|
||||
];
|
||||
|
||||
foreach ($requests as $request) {
|
||||
$this->assertTrue(Validator::make([
|
||||
'timezone' => 'America/Argentina/Buenos_Aires',
|
||||
], $request->rules())->passes());
|
||||
|
||||
$this->assertTrue(Validator::make([
|
||||
'timezone' => 'America/Buenos_Aires',
|
||||
], $request->rules())->passes());
|
||||
|
||||
$this->assertFalse(Validator::make([
|
||||
'timezone' => 'Invalid/Timezone',
|
||||
], $request->rules())->passes());
|
||||
|
||||
$this->assertFalse(Validator::make([], $request->rules())->passes());
|
||||
}
|
||||
}
|
||||
|
||||
public function test_invalid_timezone_message_uses_the_application_locale(): void
|
||||
{
|
||||
$request = new AdminAppSalePdfRequest;
|
||||
|
||||
App::setLocale('es');
|
||||
$spanishValidator = Validator::make(['timezone' => 'Invalid/Timezone'], $request->rules());
|
||||
$this->assertSame(
|
||||
'La zona horaria enviada por el navegador no es válida.',
|
||||
$spanishValidator->errors()->first('timezone'),
|
||||
);
|
||||
|
||||
App::setLocale('en');
|
||||
$englishValidator = Validator::make(['timezone' => 'Invalid/Timezone'], $request->rules());
|
||||
$this->assertSame(
|
||||
'The timezone sent by the browser is invalid.',
|
||||
$englishValidator->errors()->first('timezone'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use App\Domains\Purchase\Models\Purchase;
|
||||
use App\Domains\Sale\Services\AdminAppSalePdfService;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use Barryvdh\DomPDF\ServiceProvider;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminAppSalePdfServiceTest extends TestCase
|
||||
@@ -17,6 +18,14 @@ class AdminAppSalePdfServiceTest extends TestCase
|
||||
parent::setUp();
|
||||
|
||||
$this->app->register(ServiceProvider::class);
|
||||
Carbon::setTestNow(Carbon::parse('2026-08-24 17:53:00', 'UTC'));
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Carbon::setTestNow();
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_it_downloads_the_sales_report_as_a_pdf(): void
|
||||
@@ -24,11 +33,12 @@ class AdminAppSalePdfServiceTest extends TestCase
|
||||
$response = app(AdminAppSalePdfService::class)->downloadSales(
|
||||
$this->tenant(),
|
||||
collect([$this->sale()]),
|
||||
'America/La_Paz',
|
||||
);
|
||||
|
||||
$this->assertSame('application/pdf', $response->headers->get('content-type'));
|
||||
$this->assertStringContainsString(
|
||||
'attachment; filename=ventas_acme_',
|
||||
'attachment; filename=ventas_acme_20260824_135300.pdf',
|
||||
(string) $response->headers->get('content-disposition'),
|
||||
);
|
||||
$this->assertStringStartsWith('%PDF', $response->getContent());
|
||||
@@ -57,16 +67,52 @@ class AdminAppSalePdfServiceTest extends TestCase
|
||||
$response = app(AdminAppSalePdfService::class)->downloadModifications(
|
||||
$this->tenant(),
|
||||
collect([$modification]),
|
||||
'America/La_Paz',
|
||||
);
|
||||
|
||||
$this->assertSame('application/pdf', $response->headers->get('content-type'));
|
||||
$this->assertStringContainsString(
|
||||
'attachment; filename=historial_modificaciones_acme_',
|
||||
'attachment; filename=historial_modificaciones_acme_20260824_135300.pdf',
|
||||
(string) $response->headers->get('content-disposition'),
|
||||
);
|
||||
$this->assertStringStartsWith('%PDF', $response->getContent());
|
||||
}
|
||||
|
||||
public function test_it_renders_pdf_dates_in_the_requested_timezone(): void
|
||||
{
|
||||
$sale = $this->sale();
|
||||
$modification = (new ValueChange)->forceFill([
|
||||
'id' => 1,
|
||||
'trackable_id' => $sale->id,
|
||||
'attribute' => 'status',
|
||||
'old_value' => Purchase::STATUS_PENDING_PAYMENT,
|
||||
'new_value' => Purchase::STATUS_PAID,
|
||||
'changed_at' => now(),
|
||||
'actor_type' => 'system',
|
||||
]);
|
||||
$modification->setRelation('trackable', $sale);
|
||||
$modification->setRelation('user', null);
|
||||
|
||||
$salesHtml = view('pdf.adminapp.sales', [
|
||||
'tenant' => $this->tenant(),
|
||||
'sales' => collect([$sale]),
|
||||
'generatedAt' => now(),
|
||||
'timeZone' => 'America/La_Paz',
|
||||
'confirmedSalesTotal' => '25000.00',
|
||||
])->render();
|
||||
$modificationsHtml = view('pdf.adminapp.sale-modifications', [
|
||||
'tenant' => $this->tenant(),
|
||||
'modifications' => collect([$modification]),
|
||||
'generatedAt' => now(),
|
||||
'timeZone' => 'America/La_Paz',
|
||||
])->render();
|
||||
|
||||
$this->assertStringContainsString('Generado el 24/08/2026 13:53', $salesHtml);
|
||||
$this->assertStringContainsString('24/08/2026 13:53', $salesHtml);
|
||||
$this->assertStringContainsString('Generado el 24/08/2026 13:53', $modificationsHtml);
|
||||
$this->assertStringContainsString('13:53:00', $modificationsHtml);
|
||||
}
|
||||
|
||||
private function tenant(): Tenant
|
||||
{
|
||||
return (new Tenant)->forceFill([
|
||||
|
||||
Reference in New Issue
Block a user