Compare commits

...

12 Commits

Author SHA1 Message Date
a79f58d520 feat: implement cart service and resource to manage user and guest shopping carts 2026-07-01 14:56:12 -03:00
a7dba4dc10 feat: implement CartService to manage cart lifecycle and guest identity resolution 2026-07-01 12:38:46 -03:00
4aedf7ca86 feat: add tenant success color 2026-07-01 11:55:08 -03:00
a150a9f694 feat: implement product and variant CRUD management service with attachment handling 2026-07-01 11:44:56 -03:00
6d0d600b4b feat: add product catalog image seeder and support assets 2026-07-01 11:01:48 -03:00
67c2a7350e feat: create ProductService to handle product, variant, and attribute lifecycle management 2026-07-01 10:22:02 -03:00
a0a0cc7255 feat: add ProductVariant model and service layer for CRUD operations with attachment handling 2026-07-01 10:09:04 -03:00
d6a17cb449 feat: add is_default field to product variants and implement default variant management logic in ProductService 2026-07-01 10:02:26 -03:00
83f42f3cf9 test: add feature tests for product creation, variant management, and attribute synchronization 2026-07-01 09:58:49 -03:00
bbfe632926 test: add feature tests for product and variant management with attribute associations 2026-07-01 09:51:24 -03:00
e78e64ed2e feat: implement product seeding logic from image catalog and add supporting service methods and requests 2026-07-01 09:48:43 -03:00
35384b48ab feat: add filtering for product detail attribute options based on available variant values 2026-07-01 08:55:27 -03:00
26 changed files with 571 additions and 133 deletions

View File

@@ -2,7 +2,6 @@
namespace App\Domains\Cart\Resources;
use App\Domains\Catalog\Resources\ProductVariantDefinitionResource;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
@@ -19,19 +18,39 @@ class CartItemResource extends JsonResource
$variant = $this->variant;
$product = $variant?->product;
$attributesText = '';
if ($variant && $variant->relationLoaded('definitions')) {
$attributesText = $variant->definitions
->map(function ($definition) {
$attrName = $definition->productAttribute?->attribute?->nombre;
$value = $definition->value;
return $attrName ? "{$attrName}: {$value}" : $value;
})
->filter()
->implode(', ');
}
$productName = $product?->nombre;
if ($productName && $attributesText !== '') {
$productName .= " ({$attributesText})";
}
$imageUrl = null;
if ($variant && $variant->relationLoaded('attachments')) {
$firstAttachment = $variant->attachments->first();
if ($firstAttachment) {
$imageUrl = $firstAttachment->getTemporaryUrl(1440);
}
}
return [
'id' => $this->id,
'cantidad' => $this->cantidad,
'precio_unitario' => $this->formatMoney($product?->precio),
'subtotal' => $this->formatMoney(($product?->precio ?? 0) * $this->cantidad),
'product_id' => $product?->id,
'product' => $product === null ? null : [
'id' => $product->id,
'nombre' => $product->nombre,
'slug' => $product->slug,
],
'variant' => $variant === null ? null : [
'id' => $variant->id,
'definitions' => ProductVariantDefinitionResource::collection($variant->definitions),
'nombre' => $productName,
'imagen' => $imageUrl,
],
];
}

View File

@@ -14,13 +14,13 @@ class CartService
{
public function show(Tenant $tenant, Request $request): Cart
{
$identity = $this->resolveIdentity($request);
$resolvedIdentity = $this->resolveIdentity($request);
if ($identity === null) {
if ($resolvedIdentity === null) {
return $this->makeEmptyCart($tenant);
}
$cart = $this->findCart($tenant, $identity);
$cart = $this->findCart($tenant, $resolvedIdentity['identity']);
if ($cart === null) {
return $this->makeEmptyCart($tenant);
@@ -95,6 +95,7 @@ class CartService
return $cart->fresh()->load([
'items.variant.product',
'items.variant.definitions.productAttribute.attribute',
'items.variant.attachments',
]);
}

View File

@@ -3,6 +3,7 @@
namespace App\Domains\Catalog\Controllers;
use App\Domains\Catalog\Models\Product;
use App\Domains\Catalog\Requests\ProductDetailRequest;
use App\Domains\Catalog\Requests\StoreProductRequest;
use App\Domains\Catalog\Requests\UpdateProductRequest;
use App\Domains\Catalog\Resources\ProductResource;
@@ -30,11 +31,11 @@ class ProductController extends Controller
return ProductResource::make($product)->response()->setStatusCode(201);
}
public function show(Request $request, Tenant $tenant, Product $producto, ProductService $productService): ProductResource
public function show(ProductDetailRequest $request, Tenant $tenant, Product $producto, ProductService $productService): ProductResource
{
$producto = $this->resolveScopedProduct($tenant, $producto);
$variantId = $request->query('variant_id');
$variantId = is_numeric($variantId) ? (int) $variantId : null;
$variantId = $variantId !== null ? (int) $variantId : null;
$producto = $productService->getProductDetail($tenant, $producto, $variantId);
return ProductResource::make($producto);

View File

@@ -13,6 +13,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable([
'producto_id',
'stock',
'is_placeholder',
])]
class ProductVariant extends Model
{
@@ -25,6 +26,7 @@ class ProductVariant extends Model
return [
'producto_id' => 'integer',
'stock' => 'integer',
'is_placeholder' => 'boolean',
];
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Domains\Catalog\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ProductDetailRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'variant_id' => ['sometimes', 'integer'],
];
}
}

View File

@@ -31,6 +31,7 @@ class StoreProductRequest extends FormRequest
'nombre' => ['required', 'string', 'max:255'],
'descripcion' => ['nullable', 'string'],
'precio' => ['required', 'numeric', 'min:0'],
'stock' => ['sometimes', 'integer', 'min:0'],
'attribute_ids' => ['sometimes', 'array'],
'attribute_ids.*' => [
'required',

View File

@@ -10,6 +10,7 @@ use App\Domains\Tenant\Models\Tenant;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ProductService
@@ -26,7 +27,8 @@ class ProductService
return DB::transaction(function () use ($tenant, $data) {
$attributeIds = $data['attribute_ids'] ?? [];
$images = $data['images'] ?? [];
unset($data['attribute_ids'], $data['images']);
$stock = $data['stock'] ?? 0;
unset($data['attribute_ids'], $data['images'], $data['stock']);
/** @var Product $product */
$product = Product::query()->create([
@@ -40,6 +42,13 @@ class ProductService
$this->syncProductImages($product, $images);
}
// Create default variant with stock
$this->createVariant($product, [
'stock' => $stock,
'is_placeholder' => true,
'definitions' => [],
]);
return $product->load(['attributes.options', 'attachments', 'brand', 'category']);
});
}
@@ -109,6 +118,18 @@ class ProductService
$images = $data['images'] ?? [];
unset($data['images']);
// Determine if the variant being created is a placeholder one
$hasDefinitions = ! empty($data['definitions']);
$isPlaceholder = $data['is_placeholder'] ?? (! $hasDefinitions);
$data['is_placeholder'] = $isPlaceholder;
// Remove any existing placeholder variants
$defaultVariants = $product->variants()->where('is_placeholder', true)->get();
foreach ($defaultVariants as $defaultVariant) {
$this->deleteVariantAttachments($defaultVariant);
$product->deleteVariant($defaultVariant);
}
$variant = $product->createVariant($data);
if (! empty($images)) {
@@ -153,6 +174,15 @@ class ProductService
$product = $variant->product;
$this->deleteVariantAttachments($variant);
$product->deleteVariant($variant);
// Re-create a default variant with stock 0 if it has no variants left
if ($product->variants()->count() === 0) {
$product->createVariant([
'stock' => 0,
'is_placeholder' => true,
'definitions' => [],
]);
}
});
}
@@ -293,6 +323,8 @@ class ProductService
'variants.definitions.productAttribute.attribute.options',
]);
$this->filterProductDetailAttributeOptions($product);
$selectedVariant = $variantId !== null
? $product->variants->firstWhere('id', $variantId)
: $product->variants->first(fn (ProductVariant $variant) => $variant->stock > 0);
@@ -301,6 +333,12 @@ class ProductService
throw new NotFoundHttpException('Product variant not found for product.');
}
if ($variantId !== null && $selectedVariant->stock <= 0) {
throw ValidationException::withMessages([
'variant_id' => 'La variante seleccionada no tiene stock.',
]);
}
$selectedVariant ??= $product->variants->first();
if ($selectedVariant !== null) {
@@ -313,4 +351,36 @@ class ProductService
return $product;
}
protected function filterProductDetailAttributeOptions(Product $product): void
{
$availableValuesByAttributeId = [];
foreach ($product->variants as $variant) {
foreach ($variant->definitions as $definition) {
$attributeId = $definition->productAttribute?->attribute_id;
if ($attributeId === null || $definition->value === null) {
continue;
}
$availableValuesByAttributeId[$attributeId][$definition->value] = true;
}
}
foreach ($product->attributes as $attribute) {
if (! $attribute->relationLoaded('options')) {
continue;
}
$availableValues = $availableValuesByAttributeId[$attribute->id] ?? [];
$attribute->setRelation(
'options',
$attribute->options
->filter(fn ($option): bool => array_key_exists($option->value, $availableValues))
->values()
);
}
}
}

View File

@@ -17,6 +17,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
'primary_color',
'secondary_color',
'danger_color',
'success_color',
'header_footer_bg_color',
'header_logo_id',
'footer_logo_id',

View File

@@ -56,6 +56,7 @@ class StoreTenantRequest extends FormRequest
'primary_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'secondary_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'danger_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'success_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'header_footer_bg_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'header_logo' => $logoRule,
'footer_logo' => $logoRule,

View File

@@ -67,6 +67,7 @@ class UpdateTenantRequest extends FormRequest
'primary_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'secondary_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'danger_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'success_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'header_footer_bg_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'header_logo' => $logoRule,
'footer_logo' => $logoRule,

View File

@@ -23,6 +23,7 @@ class TenantResource extends JsonResource
'primary_color' => $this->primary_color,
'secondary_color' => $this->secondary_color,
'danger_color' => $this->danger_color,
'success_color' => $this->success_color,
'header_footer_bg_color' => $this->header_footer_bg_color,
// 1 day
'header_logo' => $this->headerLogo?->getTemporaryUrl(1440),

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('productos_variantes', function (Blueprint $table) {
$table->boolean('is_placeholder')->default(false)->after('stock');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('productos_variantes', function (Blueprint $table) {
$table->dropColumn('is_placeholder');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('tenants', function (Blueprint $table) {
$table->string('success_color')->default('#28a745')->after('danger_color');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('tenants', function (Blueprint $table) {
$table->dropColumn('success_color');
});
}
};

View File

@@ -18,6 +18,52 @@ class AttributeSeeder extends Seeder
$tenants = Tenant::all();
foreach ($tenants as $tenant) {
// Seed Color attribute
$existingColor = Attribute::query()
->where('tenant_codigo', $tenant->codigo)
->where('codigo', 'color')
->first();
if ($existingColor) {
Product::deleteAttribute($existingColor);
}
Product::createAttribute($tenant, [
'codigo' => 'color',
'nombre' => 'Color',
'type' => FieldType::Select->value,
'is_required' => true,
'metadata_schema' => [
'hex' => ['type' => 'string'],
],
'options' => [
[
'value' => 'Negro',
'label' => 'Negro',
'sort_order' => 1,
'metadata' => ['hex' => '#000000'],
],
[
'value' => 'Gris',
'label' => 'Gris',
'sort_order' => 2,
'metadata' => ['hex' => '#808080'],
],
[
'value' => 'Blanco',
'label' => 'Blanco',
'sort_order' => 3,
'metadata' => ['hex' => '#FFFFFF'],
],
[
'value' => 'Azul',
'label' => 'Azul',
'sort_order' => 4,
'metadata' => ['hex' => '#0000FF'],
],
],
]);
// Seed Talle (Size - Text options) attribute
$existingTalle = Attribute::query()
->where('tenant_codigo', $tenant->codigo)
@@ -64,51 +110,6 @@ class AttributeSeeder extends Seeder
],
]);
// Seed Color attribute
$existingColor = Attribute::query()
->where('tenant_codigo', $tenant->codigo)
->where('codigo', 'color')
->first();
if ($existingColor) {
Product::deleteAttribute($existingColor);
}
Product::createAttribute($tenant, [
'codigo' => 'color',
'nombre' => 'Color',
'type' => FieldType::Select->value,
'is_required' => true,
'metadata_schema' => [
'hex' => ['type' => 'string'],
],
'options' => [
[
'value' => 'Negro',
'label' => 'Negro',
'sort_order' => 1,
'metadata' => ['hex' => '#000000'],
],
[
'value' => 'Gris',
'label' => 'Gris',
'sort_order' => 2,
'metadata' => ['hex' => '#808080'],
],
[
'value' => 'Blanco',
'label' => 'Blanco',
'sort_order' => 3,
'metadata' => ['hex' => '#FFFFFF'],
],
[
'value' => 'Azul',
'label' => 'Azul',
'sort_order' => 4,
'metadata' => ['hex' => '#0000FF'],
],
],
]);
}
}
}

View File

@@ -16,7 +16,8 @@ class CategorySeeder extends Seeder
'Remeras',
'Pantalones',
'Zapatillas',
'Buzos'
'Buzos',
'Accesorios'
];
foreach ($categories as $nombre) {

View File

@@ -31,6 +31,7 @@ class ProductCatalogFromImagesSeeder extends Seeder
'variant_groups' => [
'pantalon_blanco' => ['stock' => 14],
'pantalon_negro' => ['stock' => 14],
'pantalon_azul' => ['stock' => 0],
],
],
'remera' => [
@@ -38,10 +39,12 @@ class ProductCatalogFromImagesSeeder extends Seeder
'variant_groups' => [
'remera_blanca' => ['stock' => 22],
'remera_negra' => ['stock' => 22],
'remera_azul' => ['stock' => 0],
],
],
'zapatillas_lecoq' => ['price' => 109999, 'stock' => 6],
'zapatillas_topper' => ['price' => 104999, 'stock' => 7],
'pelota_mundial' => ['price' => 45000, 'stock' => 15],
];
/**
@@ -220,6 +223,15 @@ class ProductCatalogFromImagesSeeder extends Seeder
$variantGroup['files']
);
if ($attributeIds->isEmpty()) {
$this->productService->createVariant($product, [
'stock' => $variantGroup['stock'],
'definitions' => [],
'images' => $images,
]);
continue;
}
foreach ($sizes as $size) {
$definitions = [];
@@ -251,8 +263,23 @@ class ProductCatalogFromImagesSeeder extends Seeder
'value' => $size,
];
// Override stock for specific variants as requested by the user
$stock = $variantGroup['stock'];
$productType = $metadata['type'];
$color = $variantGroup['metadata']['color'];
if ($productType === 'remera' && $color === 'Blanco' && $size === 'L') {
$stock = 0;
} elseif ($productType === 'remera' && $color === 'Negro' && $size === 'S') {
$stock = 0;
} elseif ($productType === 'pantalon' && $color === 'Negro' && $size === 'XL') {
$stock = 0;
} elseif ($productType === 'pantalon' && $color === 'Blanco' && $size === 'M') {
$stock = 0;
}
$this->productService->createVariant($product, [
'stock' => $variantGroup['stock'],
'stock' => $stock,
'definitions' => $definitions,
'images' => $images,
]);
@@ -301,6 +328,19 @@ class ProductCatalogFromImagesSeeder extends Seeder
];
}
if ($groupKey === 'pelota_mundial') {
return [
'type' => 'pelota',
'category_name' => 'Accesorios',
'brand_name' => 'Adidas',
'color' => null,
'attribute_codes' => [],
'product_slug' => $groupKey,
'product_name' => 'Pelota Mundial 2026',
'description' => 'Pelota oficial del mundial 2026 sin variantes.',
];
}
$segments = explode('_', $groupKey);
$type = array_shift($segments);
@@ -311,7 +351,7 @@ class ProductCatalogFromImagesSeeder extends Seeder
$color = null;
$brandSegments = $segments;
if (isset($segments[0]) && in_array($segments[0], ['blanco', 'blanca', 'negro', 'negra', 'gris'], true)) {
if (isset($segments[0]) && in_array($segments[0], ['blanco', 'blanca', 'negro', 'negra', 'gris', 'azul'], true)) {
$color = $this->normalizeColor($segments[0]);
$brandSegments = array_slice($segments, 1);
}
@@ -361,6 +401,7 @@ class ProductCatalogFromImagesSeeder extends Seeder
'blanco', 'blanca' => 'Blanco',
'negro', 'negra' => 'Negro',
'gris' => 'Gris',
'azul' => 'Azul',
default => throw new RuntimeException("Unsupported color '{$color}'."),
};
}

View File

@@ -74,6 +74,7 @@ class TenantSeeder extends Seeder
'primary_color' => '#6376F3',
'secondary_color' => '#A0A0A0',
'danger_color' => '#FF8888',
'success_color' => '#198754',
'header_footer_bg_color' => '#313131',
'header_logo' => $headerLogo,
'footer_logo' => $footerLogo,

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -5,6 +5,7 @@ namespace Tests\Feature\Cart;
use App\Domains\Catalog\Models\Product;
use App\Domains\Catalog\Models\Attribute;
use App\Domains\Catalog\Models\ProductVariant;
use App\Domains\Catalog\Models\ProductAttribute;
use App\Domains\Catalog\Models\ProductVariantDefinition;
use App\Domains\Tenant\Models\Tenant;
use App\Models\User;
@@ -22,11 +23,13 @@ class CartControllerTest extends TestCase
$this->getJson('/api/tenants/acme/cart')
->assertOk()
->assertJson([
'id' => null,
'tenant_codigo' => 'acme',
'status' => 'active',
'items' => [],
'subtotal' => '0.00',
'data' => [
'id' => null,
'tenant_codigo' => 'acme',
'status' => 'active',
'items' => [],
'subtotal' => '0.00',
]
]);
}
@@ -40,9 +43,14 @@ class CartControllerTest extends TestCase
'type' => 'string',
]);
$productAttribute = ProductAttribute::query()->create([
'product_id' => $variant->producto_id,
'attribute_id' => $attribute->id,
]);
ProductVariantDefinition::query()->create([
'producto_variante_id' => $variant->id,
'attribute_id' => $attribute->id,
'products_attribute_id' => $productAttribute->id,
'value' => 'Red',
]);
@@ -54,14 +62,13 @@ class CartControllerTest extends TestCase
$response
->assertOk()
->assertCookie('guest_token')
->assertJsonPath('tenant_codigo', 'acme')
->assertJsonPath('items.0.cantidad', 2)
->assertJsonPath('items.0.precio_unitario', '49.90')
->assertJsonPath('items.0.subtotal', '99.80')
->assertJsonPath('items.0.product.id', $variant->product->id)
->assertJsonPath('items.0.variant.id', $variant->id)
->assertJsonPath('items.0.variant.definitions.0.attribute.codigo', 'color')
->assertJsonPath('subtotal', '99.80');
->assertJsonPath('data.tenant_codigo', 'acme')
->assertJsonPath('data.items.0.cantidad', 2)
->assertJsonPath('data.items.0.precio_unitario', '49.90')
->assertJsonPath('data.items.0.product_id', $variant->product->id)
->assertJsonPath('data.items.0.product.nombre', 'Shirt acme (Color: Red)')
->assertJsonPath('data.items.0.product.imagen', null)
->assertJsonPath('data.subtotal', '99.80');
$this->assertDatabaseHas('carritos', [
'tenant_codigo' => 'acme',
@@ -97,9 +104,8 @@ class CartControllerTest extends TestCase
'cantidad' => 3,
])
->assertOk()
->assertJsonPath('items.0.cantidad', 5)
->assertJsonPath('items.0.subtotal', '125.00')
->assertJsonPath('subtotal', '125.00');
->assertJsonPath('data.items.0.cantidad', 5)
->assertJsonPath('data.subtotal', '125.00');
$this->assertDatabaseCount('carritos', 1);
$this->assertDatabaseCount('carrito_items', 1);
@@ -129,9 +135,8 @@ class CartControllerTest extends TestCase
'cantidad' => 5,
])
->assertOk()
->assertJsonPath('items.0.cantidad', 5)
->assertJsonPath('items.0.subtotal', '75.00')
->assertJsonPath('subtotal', '75.00');
->assertJsonPath('data.items.0.cantidad', 5)
->assertJsonPath('data.subtotal', '75.00');
$this->assertDatabaseHas('carrito_items', [
'producto_variante_id' => $variant->id,
@@ -157,8 +162,8 @@ class CartControllerTest extends TestCase
$this->withCookie('guest_token', $guestToken)
->deleteJson("/api/tenants/acme/cart/items/{$variant->id}")
->assertOk()
->assertJsonPath('items', [])
->assertJsonPath('subtotal', '0.00');
->assertJsonPath('data.items', [])
->assertJsonPath('data.subtotal', '0.00');
$this->assertDatabaseCount('carrito_items', 0);
$this->assertDatabaseHas('productos_variantes', [
@@ -187,7 +192,7 @@ class CartControllerTest extends TestCase
'cantidad' => 2,
])
->assertOk()
->assertJsonPath('subtotal', '50.00');
->assertJsonPath('data.subtotal', '50.00');
$this->actingAs($user)
->postJson('/api/tenants/globex/cart/items', [
@@ -324,6 +329,7 @@ class CartControllerTest extends TestCase
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'success_color' => '#28a745',
'header_footer_bg_color' => '#444444',
'header_logo_id' => $headerAttachment->id,
'footer_logo_id' => $footerAttachment->id,

View File

@@ -122,6 +122,7 @@ class AttributeControllerTest extends TestCase
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'success_color' => '#28a745',
'header_footer_bg_color' => '#444444',
'header_logo_id' => $headerAttachment->id,
'footer_logo_id' => $footerAttachment->id,

View File

@@ -5,6 +5,7 @@ namespace Tests\Feature\Catalog;
use App\Domains\Attachable\Enums\AttachmentType;
use App\Domains\Attachable\Models\Attachment;
use App\Domains\Catalog\Models\Attribute;
use App\Domains\Catalog\Models\Brand;
use App\Domains\Catalog\Models\Product;
use App\Domains\Catalog\Models\ProductAttribute;
use App\Domains\Catalog\Models\ProductVariant;
@@ -19,6 +20,8 @@ class ProductControllerTest extends TestCase
private Tenant $tenant;
private Brand $brand;
private Attribute $sizeAttr;
private Attribute $colorAttr;
@@ -31,6 +34,11 @@ class ProductControllerTest extends TestCase
$this->tenant = $this->createTenant('acme', 'Acme Inc.', 'acme.com');
$this->brand = Brand::create([
'tenant_codigo' => $this->tenant->codigo,
'nombre' => 'Adidas',
]);
// Create attributes for variants
$this->sizeAttr = Attribute::create([
'tenant_codigo' => $this->tenant->codigo,
@@ -69,6 +77,7 @@ class ProductControllerTest extends TestCase
{
$payload = [
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'remera-sport',
'nombre' => 'Remera Sport',
'descripcion' => 'Remera para hacer deportes',
@@ -109,44 +118,43 @@ class ProductControllerTest extends TestCase
'attribute_id' => $this->colorAttr->id,
]);
$productAttributes = [
'size' => $product->productAttributes()->where('attribute_id', $this->sizeAttr->id)->firstOrFail(),
'color' => $product->productAttributes()->where('attribute_id', $this->colorAttr->id)->firstOrFail(),
];
// Create a variant
$variantPayload = [
'producto_id' => $product->id,
'slug' => 'remera-sport-s-azul',
'nombre' => 'Remera Sport S Azul',
'stock' => 10,
'precio' => 15000.00,
'definitions' => [
[
'attribute_id' => $this->sizeAttr->id,
'products_attribute_id' => $productAttributes['size']->id,
'value' => 'S',
],
[
'attribute_id' => $this->colorAttr->id,
'products_attribute_id' => $productAttributes['color']->id,
'value' => 'Azul',
],
],
];
$variantResponse = $this->postJson("/api/tenants/{$this->tenant->codigo}/product-variants", $variantPayload);
$variantResponse = $this->postJson("/api/tenants/{$this->tenant->codigo}/productos/{$product->id}/variants", $variantPayload);
$variantResponse->assertCreated();
$this->assertDatabaseHas('productos_variantes', [
'producto_id' => $product->id,
'slug' => 'remera-sport-s-azul',
'stock' => 10,
'precio' => 15000.00,
]);
$variantS = ProductVariant::where('slug', 'remera-sport-s-azul')->firstOrFail();
$variantS = ProductVariant::where('producto_id', $product->id)->where('stock', 10)->firstOrFail();
$this->assertDatabaseHas('productos_variantes_values', [
'producto_variante_id' => $variantS->id,
'attribute_id' => $this->sizeAttr->id,
'products_attribute_id' => $productAttributes['size']->id,
'value' => 'S',
]);
$this->assertDatabaseHas('productos_variantes_values', [
'producto_variante_id' => $variantS->id,
'attribute_id' => $this->colorAttr->id,
'products_attribute_id' => $productAttributes['color']->id,
'value' => 'Azul',
]);
}
@@ -157,6 +165,7 @@ class ProductControllerTest extends TestCase
$product = Product::create([
'tenant_codigo' => $this->tenant->codigo,
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'pantalon-cargo',
'nombre' => 'Pantalon Cargo',
'precio' => 20000.00,
@@ -167,6 +176,7 @@ class ProductControllerTest extends TestCase
// 2. Perform update payload - change name and update attribute_ids to sizeAttr
$payload = [
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'pantalon-cargo-new-slug',
'nombre' => 'Pantalon Cargo V2',
'precio' => 22000.00,
@@ -202,35 +212,33 @@ class ProductControllerTest extends TestCase
$product = Product::create([
'tenant_codigo' => $this->tenant->codigo,
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'pantalon-cargo',
'nombre' => 'Pantalon Cargo',
'precio' => 20000.00,
]);
$product->attributes()->sync([$this->sizeAttr->id]);
$sizeProductAttr = $product->productAttributes()->where('attribute_id', $this->sizeAttr->id)->firstOrFail();
// Attempt to create variant with colorAttr (which is not associated with the product)
$variantPayload = [
'producto_id' => $product->id,
'slug' => 'pantalon-cargo-38-rojo',
'nombre' => 'Pantalon Cargo 38 Rojo',
'stock' => 5,
'precio' => 20000.00,
'definitions' => [
[
'attribute_id' => $this->sizeAttr->id,
'products_attribute_id' => $sizeProductAttr->id,
'value' => '38',
],
[
'attribute_id' => $this->colorAttr->id, // not associated!
'products_attribute_id' => 99999, // not associated!
'value' => 'Rojo',
],
],
];
$response = $this->postJson("/api/tenants/{$this->tenant->codigo}/product-variants", $variantPayload);
$response = $this->postJson("/api/tenants/{$this->tenant->codigo}/productos/{$product->id}/variants", $variantPayload);
$response->assertUnprocessable();
$response->assertJsonValidationErrors(['definitions.1.attribute_id']);
$response->assertJsonValidationErrors(['definitions.1.products_attribute_id']);
}
public function test_it_rejects_variant_update_with_attributes_not_associated_with_product(): void
@@ -239,46 +247,41 @@ class ProductControllerTest extends TestCase
$product = Product::create([
'tenant_codigo' => $this->tenant->codigo,
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'pantalon-cargo',
'nombre' => 'Pantalon Cargo',
'precio' => 20000.00,
]);
$product->attributes()->sync([$this->sizeAttr->id]);
$sizeProductAttr = $product->productAttributes()->where('attribute_id', $this->sizeAttr->id)->firstOrFail();
$variant = $product->variants()->create([
'slug' => 'pantalon-cargo-38',
'nombre' => 'Pantalon Cargo 38',
'stock' => 5,
'precio' => 20000.00,
]);
$variant->definitions()->create([
'attribute_id' => $this->sizeAttr->id,
'products_attribute_id' => $sizeProductAttr->id,
'value' => '38',
]);
// Attempt to update variant with colorAttr (which is not associated with the product)
$variantPayload = [
'producto_id' => $product->id,
'slug' => 'pantalon-cargo-38-rojo',
'nombre' => 'Pantalon Cargo 38 Rojo',
'stock' => 5,
'precio' => 20000.00,
'definitions' => [
[
'attribute_id' => $this->sizeAttr->id,
'products_attribute_id' => $sizeProductAttr->id,
'value' => '38',
],
[
'attribute_id' => $this->colorAttr->id, // not associated!
'products_attribute_id' => 99999, // not associated!
'value' => 'Rojo',
],
],
];
$response = $this->putJson("/api/tenants/{$this->tenant->codigo}/product-variants/{$variant->id}", $variantPayload);
$response = $this->putJson("/api/tenants/{$this->tenant->codigo}/productos/{$product->id}/variants/{$variant->id}", $variantPayload);
$response->assertUnprocessable();
$response->assertJsonValidationErrors(['definitions.1.attribute_id']);
$response->assertJsonValidationErrors(['definitions.1.products_attribute_id']);
}
public function test_it_does_not_modify_variants_if_not_present_in_update_payload(): void
@@ -286,20 +289,19 @@ class ProductControllerTest extends TestCase
$product = Product::create([
'tenant_codigo' => $this->tenant->codigo,
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'short-running',
'nombre' => 'Short Running',
'precio' => 8000.00,
]);
$v1 = $product->variants()->create([
'slug' => 'short-running-m',
'nombre' => 'Short Running M',
'stock' => 5,
'precio' => 8000.00,
]);
$payload = [
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'short-running',
'nombre' => 'Short Running Updated',
'precio' => 9000.00,
@@ -319,7 +321,6 @@ class ProductControllerTest extends TestCase
// Variant should still exist untouched
$this->assertDatabaseHas('productos_variantes', [
'id' => $v1->id,
'slug' => 'short-running-m',
'stock' => 5,
]);
}
@@ -329,29 +330,26 @@ class ProductControllerTest extends TestCase
$product = Product::create([
'tenant_codigo' => $this->tenant->codigo,
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'pantalon-cargo',
'nombre' => 'Pantalon Cargo',
'precio' => 20000.00,
]);
$payload = [
'producto_id' => $product->id,
'slug' => 'remera-sport-s-azul',
'nombre' => 'Remera Sport S Azul',
'stock' => 10,
'precio' => 15000.00,
'definitions' => [
[
'attribute_id' => 99999, // Non-existent ID
'products_attribute_id' => 99999, // Non-existent ID
'value' => 'S',
],
],
];
$response = $this->postJson("/api/tenants/{$this->tenant->codigo}/product-variants", $payload);
$response = $this->postJson("/api/tenants/{$this->tenant->codigo}/productos/{$product->id}/variants", $payload);
$response->assertUnprocessable();
$response->assertJsonValidationErrors(['definitions.0.attribute_id']);
$response->assertJsonValidationErrors(['definitions.0.products_attribute_id']);
}
public function test_it_throws_exception_when_variant_value_does_not_belong_to_attribute_options(): void
@@ -371,24 +369,23 @@ class ProductControllerTest extends TestCase
$product = Product::create([
'tenant_codigo' => $this->tenant->codigo,
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'test-prod-validation',
'nombre' => 'Test Prod Validation',
'precio' => 100.00,
]);
$product->attributes()->sync([$selectAttr->id]);
$productAttr = $product->productAttributes()->where('attribute_id', $selectAttr->id)->firstOrFail();
// 3. Expect exception when creating variant with invalid value 'G'
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The value 'G' is not a valid option for the select attribute 'Tamanho'.");
$product->createVariant([
'slug' => 'test-prod-validation-g',
'nombre' => 'Test Prod Validation G',
'stock' => 5,
'precio' => 100.00,
'definitions' => [
[
'attribute_id' => $selectAttr->id,
'products_attribute_id' => $productAttr->id,
'value' => 'G', // Invalid value
],
],
@@ -400,11 +397,16 @@ class ProductControllerTest extends TestCase
$product = Product::create([
'tenant_codigo' => $this->tenant->codigo,
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'test-product-show',
'nombre' => 'Test Product Show',
'precio' => 100.00,
]);
$productAttributes = $this->syncVariantAttributes($product);
$this->colorAttr->options()->create([
'value' => 'Verde',
'label' => 'Verde',
]);
$variant = $product->createVariant([
'stock' => 0,
@@ -479,6 +481,23 @@ class ProductControllerTest extends TestCase
$response->assertJsonPath('data.variant.definitions.color', 'Rojo');
$response->assertJsonCount(1, 'data.variant.images');
$this->assertStringContainsString($variantAttachment->path, $response->json('data.variant.images.0'));
$colorAttribute = collect($response->json('data.attributes'))->firstWhere('codigo', 'color');
$this->assertNotNull($colorAttribute);
$this->assertEqualsCanonicalizing(
['Azul', 'Rojo'],
collect($colorAttribute['options'])->pluck('value')->all()
);
$attributesResponse = $this->getJson("/api/tenants/{$this->tenant->codigo}/attributes");
$attributesResponse->assertOk();
$masterColorAttribute = collect($attributesResponse->json('data'))->firstWhere('codigo', 'color');
$this->assertNotNull($masterColorAttribute);
$this->assertEqualsCanonicalizing(
['Azul', 'Rojo', 'Verde'],
collect($masterColorAttribute['options'])->pluck('value')->all()
);
}
public function test_it_returns_requested_variant_in_product_detail(): void
@@ -486,6 +505,7 @@ class ProductControllerTest extends TestCase
$product = Product::create([
'tenant_codigo' => $this->tenant->codigo,
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'test-product-requested-variant',
'nombre' => 'Test Product Requested Variant',
'precio' => 100.00,
@@ -524,6 +544,7 @@ class ProductControllerTest extends TestCase
$product = Product::create([
'tenant_codigo' => $this->tenant->codigo,
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'test-product-invalid-variant',
'nombre' => 'Test Product Invalid Variant',
'precio' => 100.00,
@@ -531,6 +552,7 @@ class ProductControllerTest extends TestCase
$otherProduct = Product::create([
'tenant_codigo' => $this->tenant->codigo,
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'test-product-other-variant',
'nombre' => 'Test Product Other Variant',
'precio' => 100.00,
@@ -548,6 +570,7 @@ class ProductControllerTest extends TestCase
$product = Product::create([
'tenant_codigo' => $this->tenant->codigo,
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'test-product-fallback-images',
'nombre' => 'Test Product Fallback Images',
'precio' => 100.00,
@@ -591,6 +614,160 @@ class ProductControllerTest extends TestCase
];
}
public function test_it_creates_default_variant_on_product_creation(): void
{
$payload = [
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'pelota-mundial-test',
'nombre' => 'Pelota Mundial Test',
'precio' => 45000.00,
'stock' => 15,
];
$response = $this->postJson("/api/tenants/{$this->tenant->codigo}/productos", $payload);
$response->assertCreated();
$product = Product::where('slug', 'pelota-mundial-test')->firstOrFail();
// Should have exactly 1 variant
$this->assertEquals(1, $product->variants()->count());
$variant = $product->variants()->first();
$this->assertEquals(15, $variant->stock);
$this->assertTrue($variant->is_placeholder);
// Should have no definitions
$this->assertEquals(0, $variant->definitions()->count());
}
public function test_it_removes_default_variant_when_creating_real_variant(): void
{
$product = Product::create([
'tenant_codigo' => $this->tenant->codigo,
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'test-default-variant-lifecycle',
'nombre' => 'Test Default Variant Lifecycle',
'precio' => 100.00,
]);
$productAttributes = $this->syncVariantAttributes($product);
$defaultVariant = $product->variants()->create([
'stock' => 10,
'is_placeholder' => true,
]);
$this->assertEquals(1, $product->variants()->count());
// Create a real variant (with definitions)
$variantPayload = [
'stock' => 5,
'definitions' => [
[
'products_attribute_id' => $productAttributes['size']->id,
'value' => 'S',
],
],
];
$response = $this->postJson("/api/tenants/{$this->tenant->codigo}/productos/{$product->id}/variants", $variantPayload);
$response->assertCreated();
// The default variant should be deleted
$this->assertDatabaseMissing('productos_variantes', [
'id' => $defaultVariant->id,
]);
// Only the new variant should remain
$this->assertEquals(1, $product->variants()->count());
$newVariant = $product->variants()->first();
$this->assertEquals(5, $newVariant->stock);
$this->assertFalse($newVariant->is_placeholder);
}
public function test_it_restores_default_variant_when_all_variants_are_deleted(): void
{
$product = Product::create([
'tenant_codigo' => $this->tenant->codigo,
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'test-default-variant-restore',
'nombre' => 'Test Default Variant Restore',
'precio' => 100.00,
]);
$productAttributes = $this->syncVariantAttributes($product);
$realVariant = $product->createVariant([
'stock' => 5,
'is_placeholder' => false,
'definitions' => [
[
'products_attribute_id' => $productAttributes['size']->id,
'value' => 'S',
],
],
]);
$this->assertEquals(1, $product->variants()->count());
// Delete the real variant via API
$response = $this->deleteJson("/api/tenants/{$this->tenant->codigo}/productos/{$product->id}/variants/{$realVariant->id}");
$response->assertNoContent();
// A default variant should be recreated with stock 0 and is_placeholder = true
$this->assertEquals(1, $product->variants()->count());
$defaultVariant = $product->variants()->first();
$this->assertEquals(0, $defaultVariant->stock);
$this->assertTrue($defaultVariant->is_placeholder);
$this->assertEquals(0, $defaultVariant->definitions()->count());
}
public function test_it_rejects_product_detail_when_requested_variant_has_no_stock(): void
{
$product = Product::create([
'tenant_codigo' => $this->tenant->codigo,
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'test-product-no-stock',
'nombre' => 'Test Product No Stock',
'precio' => 100.00,
]);
$productAttributes = $this->syncVariantAttributes($product);
$outOfStockVariant = $product->createVariant([
'stock' => 0,
'definitions' => [
[
'products_attribute_id' => $productAttributes['size']->id,
'value' => 'S',
],
],
]);
$response = $this->getJson("/api/tenants/{$this->tenant->codigo}/productos/{$product->id}?variant_id={$outOfStockVariant->id}");
$response->assertUnprocessable();
$response->assertJsonValidationErrors(['variant_id']);
$response->assertJsonPath('errors.variant_id.0', 'La variante seleccionada no tiene stock.');
}
public function test_it_rejects_product_detail_when_requested_variant_id_is_invalid_format(): void
{
$product = Product::create([
'tenant_codigo' => $this->tenant->codigo,
'categoria_id' => 1,
'brand_id' => $this->brand->id,
'slug' => 'test-product-invalid-format',
'nombre' => 'Test Product Invalid Format',
'precio' => 100.00,
]);
$response = $this->getJson("/api/tenants/{$this->tenant->codigo}/productos/{$product->id}?variant_id=abc");
$response->assertUnprocessable();
$response->assertJsonValidationErrors(['variant_id']);
}
private function createAttachment(string $path): Attachment
{
return Attachment::create([
@@ -629,6 +806,7 @@ class ProductControllerTest extends TestCase
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'success_color' => '#28a745',
'header_footer_bg_color' => '#444444',
'header_logo_id' => $headerAttachment->id,
'footer_logo_id' => $footerAttachment->id,

View File

@@ -41,6 +41,7 @@ class ProductVariantAttachmentTest extends TestCase
'primary_color' => '#ffffff',
'secondary_color' => '#ffffff',
'danger_color' => '#ffffff',
'success_color' => '#ffffff',
'header_footer_bg_color' => '#ffffff',
'header_logo_id' => $headerAttachment->id,
'footer_logo_id' => $footerAttachment->id,

View File

@@ -40,6 +40,7 @@ class BootstrapTenantControllerTest extends TestCase
'primary_color' => '#ff0000',
'secondary_color' => '#00ff00',
'danger_color' => '#0000ff',
'success_color' => '#00ff00',
'header_footer_bg_color' => '#ffffff',
'header_logo_id' => $headerAttachment->id,
'footer_logo_id' => $footerAttachment->id,
@@ -54,6 +55,7 @@ class BootstrapTenantControllerTest extends TestCase
->assertJsonPath('data.primary_color', '#ff0000')
->assertJsonPath('data.secondary_color', '#00ff00')
->assertJsonPath('data.danger_color', '#0000ff')
->assertJsonPath('data.success_color', '#00ff00')
->assertJsonPath('data.header_footer_bg_color', '#ffffff');
$headerUrl = $response->json('data.header_logo');
@@ -107,6 +109,7 @@ class BootstrapTenantControllerTest extends TestCase
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'success_color' => '#555555',
'header_footer_bg_color' => '#444444',
'header_logo' => $base64Image,
'footer_logo' => $base64Image,
@@ -118,6 +121,7 @@ class BootstrapTenantControllerTest extends TestCase
->assertJsonPath('data.primary_color', '#111111')
->assertJsonPath('data.secondary_color', '#222222')
->assertJsonPath('data.danger_color', '#333333')
->assertJsonPath('data.success_color', '#555555')
->assertJsonPath('data.header_footer_bg_color', '#444444');
$tenant = Tenant::query()->with(['headerLogo', 'footerLogo'])->where('codigo', 'acme')->firstOrFail();
@@ -130,6 +134,7 @@ class BootstrapTenantControllerTest extends TestCase
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'success_color' => '#555555',
'header_footer_bg_color' => '#444444',
'header_logo_id' => $tenant->header_logo_id,
'footer_logo_id' => $tenant->footer_logo_id,
@@ -154,6 +159,7 @@ class BootstrapTenantControllerTest extends TestCase
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'success_color' => '#555555',
'header_footer_bg_color' => '#444444',
'header_logo' => (string) Str::uuid(),
'footer_logo' => (string) Str::uuid(),
@@ -203,6 +209,7 @@ class BootstrapTenantControllerTest extends TestCase
'primary_color' => '#555555',
'secondary_color' => '#666666',
'danger_color' => '#777777',
'success_color' => '#999999',
'header_footer_bg_color' => '#888888',
'header_logo' => $hdrUuid,
'footer_logo' => $ftrUuid,
@@ -215,6 +222,7 @@ class BootstrapTenantControllerTest extends TestCase
->assertJsonPath('data.primary_color', '#555555')
->assertJsonPath('data.secondary_color', '#666666')
->assertJsonPath('data.danger_color', '#777777')
->assertJsonPath('data.success_color', '#999999')
->assertJsonPath('data.header_footer_bg_color', '#888888');
$headerUrl = $successfulResponse->json('data.header_logo');
@@ -234,6 +242,7 @@ class BootstrapTenantControllerTest extends TestCase
'primary_color' => '#555555',
'secondary_color' => '#666666',
'danger_color' => '#777777',
'success_color' => '#999999',
'header_footer_bg_color' => '#888888',
'header_logo_id' => $hdrAttachment->id,
'footer_logo_id' => $ftrAttachment->id,
@@ -286,6 +295,7 @@ class BootstrapTenantControllerTest extends TestCase
'primary_color' => 'invalid-color',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'success_color' => '#555555',
'header_footer_bg_color' => '#444444',
'header_logo' => (string) Str::uuid(),
'footer_logo' => (string) Str::uuid(),
@@ -300,12 +310,28 @@ class BootstrapTenantControllerTest extends TestCase
'primary_color' => '#12345',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'success_color' => '#555555',
'header_footer_bg_color' => '#444444',
'header_logo' => (string) Str::uuid(),
'footer_logo' => (string) Str::uuid(),
]);
$response2->assertJsonValidationErrors(['primary_color']);
$response3 = $this->postJson('/api/tenants', [
'codigo' => 'acme',
'nombre' => 'Acme',
'dominio' => 'acme.com',
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'success_color' => 'invalid-color',
'header_footer_bg_color' => '#444444',
'header_logo' => (string) Str::uuid(),
'footer_logo' => (string) Str::uuid(),
]);
$response3->assertJsonValidationErrors(['success_color']);
}
public function test_it_validates_logo_must_be_image_or_svg(): void
@@ -319,6 +345,7 @@ class BootstrapTenantControllerTest extends TestCase
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'success_color' => '#555555',
'header_footer_bg_color' => '#444444',
'header_logo' => UploadedFile::fake()->create('document.pdf', 10, 'application/pdf'),
'footer_logo' => (string) Str::uuid(),
@@ -333,6 +360,7 @@ class BootstrapTenantControllerTest extends TestCase
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'success_color' => '#555555',
'header_footer_bg_color' => '#444444',
'header_logo' => 'data:application/pdf;base64,JVBERi0xLjQKJdcfqksKMSAwIG9iagogIDw8IC9UeXBlIC9DYXRhbG9nCiAgICAvUGFnZXMgMiAwIFI...',
'footer_logo' => (string) Str::uuid(),
@@ -355,6 +383,7 @@ class BootstrapTenantControllerTest extends TestCase
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'success_color' => '#555555',
'header_footer_bg_color' => '#444444',
'header_logo' => $header,
'footer_logo' => $footer,
@@ -410,6 +439,7 @@ class BootstrapTenantControllerTest extends TestCase
'primary_color' => '#ff0000',
'secondary_color' => '#00ff00',
'danger_color' => '#0000ff',
'success_color' => '#00ff00',
'header_footer_bg_color' => '#ffffff',
'header_logo_id' => $headerAttachment->id,
'footer_logo_id' => $footerAttachment->id,
@@ -438,6 +468,7 @@ class BootstrapTenantControllerTest extends TestCase
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'success_color' => '#555555',
'header_footer_bg_color' => '#444444',
'header_logo' => $base64Image,
'footer_logo' => $ftrUuid,