feat(F-102): completed feature

This commit is contained in:
chattie
2026-08-21 12:02:15 +02:00
parent e46faa869a
commit 027cacd871
31 changed files with 510 additions and 80 deletions

View File

@@ -94,6 +94,8 @@ const newProductSchema = z.object({
categoryIds: z.array(z.uuid()).max(50).optional(),
brandId: z.uuid().optional().nullable(),
expirationDate: z.iso.date().optional().nullable(),
unitWeightKg: z.number().positive().max(1000).optional(),
minPurchaseQty: z.number().int().min(1).max(999).optional(),
});
const productPatchSchema = newProductSchema
@@ -727,6 +729,8 @@ function serializeProduct(product: Product, images: ProductImage[] = []) {
brandId: product.brandId,
brand: product.brand,
expirationDate: product.expirationDate,
unitWeightKg: product.unitWeightKg,
minPurchaseQty: product.minPurchaseQty,
createdAt: product.createdAt.toISOString(),
updatedAt: product.updatedAt.toISOString(),
};

View File

@@ -51,6 +51,10 @@ export interface Product {
brandId: string | null;
brand?: ProductBrandSummary;
expirationDate: string | null;
/** Peso unitario en kg (el peso del pedido es cantidad × este valor). */
unitWeightKg: number;
/** Cantidad mínima de compra; el frontend bloquea pedidos por debajo. */
minPurchaseQty: number;
createdAt: Date;
updatedAt: Date;
}
@@ -68,6 +72,8 @@ export interface NewProduct {
categoryIds?: string[];
brandId?: string | null;
expirationDate?: string | null;
unitWeightKg?: number;
minPurchaseQty?: number;
}
/** Fields a product update may set. Undefined = leave unchanged. */

View File

@@ -26,6 +26,8 @@ export interface ProductRow {
brand_name: string | null;
brand_slug: string | null;
expiration_date: string | null;
unit_weight_kg: string | number;
min_purchase_qty: number;
created_at: Date;
updated_at: Date;
}
@@ -55,6 +57,8 @@ const UPDATABLE: ReadonlyArray<[keyof ProductPatch, string]> = [
['seoDescription', 'seo_description'],
['brandId', 'brand_id'],
['expirationDate', 'expiration_date'],
['unitWeightKg', 'unit_weight_kg'],
['minPurchaseQty', 'min_purchase_qty'],
];
export class PgProductRepository implements ProductRepository {
@@ -93,8 +97,8 @@ export class PgProductRepository implements ProductRepository {
try {
await client.query('BEGIN');
const result = await client.query<ProductRow>(
`INSERT INTO catalog_products (name, slug, description, state, seo_title, seo_description, brand_id, expiration_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`INSERT INTO catalog_products (name, slug, description, state, seo_title, seo_description, brand_id, expiration_date, unit_weight_kg, min_purchase_qty)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *, ARRAY[]::uuid[] AS category_ids`,
[
input.name,
@@ -105,6 +109,8 @@ export class PgProductRepository implements ProductRepository {
input.seoDescription ?? null,
input.brandId ?? null,
input.expirationDate ?? null,
input.unitWeightKg ?? 1,
input.minPurchaseQty ?? 1,
],
);
const row = result.rows[0];
@@ -306,6 +312,8 @@ export function toProduct(row: ProductRow): Product {
: undefined,
categoryIds: row.category_ids ?? [],
expirationDate: row.expiration_date ?? null,
unitWeightKg: Number(row.unit_weight_kg ?? 1),
minPurchaseQty: row.min_purchase_qty ?? 1,
createdAt: row.created_at,
updatedAt: row.updated_at,
};

View File

@@ -17,6 +17,8 @@ function product(input: Partial<Product> & Pick<Product, 'id' | 'name' | 'slug'>
categoryIds: [],
brandId: null,
expirationDate: null,
unitWeightKg: 1,
minPurchaseQty: 1,
createdAt: new Date('2026-01-01T00:00:00Z'),
updatedAt: new Date('2026-01-01T00:00:00Z'),
...input,

View File

@@ -21,6 +21,8 @@ function product(input: Partial<Product> & Pick<Product, 'id' | 'name' | 'slug'>
categoryIds: [],
brandId: null,
expirationDate: null,
unitWeightKg: 1,
minPurchaseQty: 1,
createdAt: new Date('2026-01-01T00:00:00Z'),
updatedAt: new Date('2026-01-01T00:00:00Z'),
...input,