Add Products/Catalog module (MVP): migration, repository, API
Ports the legacy products table's core shape (eden.md §2 §1.1, Master Prompt §6.3) faithfully but MVP-scoped: product_code sequence, the 5 fixed price tiers (price_0/12/24/36/48), impl_unit_price, and stock/serial/ asset-tag/MAC tracking flags. Leaves out legacy telecom-billing specifics (service_type, metered, tariff_rates, ixc_product_code) for a later phase. Mirrors the Companies module's structure and conventions exactly: COALESCE partial updates, JwtAuthGuard/PermissionGuard with open read / gated write (products:edit) per ADR-0004, product.created/product.updated audit events. Money fields are validated as non-negative NUMERIC in the DTO only — no pricing/calculation logic here, that belongs to the future Quotes phase. Does not touch apps/api/src/app.module.ts or packages/database/src/index.ts (wiring is left to integration) to avoid conflicting with sibling agents' concurrent edits to those files; the ProductsService deep-imports the repository module directly instead of via the shared barrel, noted in a code comment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
157
packages/database/src/repositories/products.ts
Normal file
157
packages/database/src/repositories/products.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { query } from "../pool.js";
|
||||
|
||||
export interface Product {
|
||||
id: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
created_by: string | null;
|
||||
product_code: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
product_type: string;
|
||||
product_group: string | null;
|
||||
product_subgroup: string | null;
|
||||
brand: string | null;
|
||||
unit: string;
|
||||
active: boolean;
|
||||
discontinued: boolean;
|
||||
tracks_stock: boolean;
|
||||
tracks_serial: boolean;
|
||||
tracks_asset_tag: boolean;
|
||||
tracks_mac: boolean;
|
||||
price_0: string | null;
|
||||
price_12: string | null;
|
||||
price_24: string | null;
|
||||
price_36: string | null;
|
||||
price_48: string | null;
|
||||
impl_unit_price: string;
|
||||
}
|
||||
|
||||
export async function listProducts(includeInactive = false): Promise<Product[]> {
|
||||
const { rows } = await query<Product>(
|
||||
includeInactive
|
||||
? "SELECT * FROM products ORDER BY created_at ASC"
|
||||
: "SELECT * FROM products WHERE active = true ORDER BY created_at ASC",
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
export async function getProductById(id: string): Promise<Product | null> {
|
||||
const { rows } = await query<Product>("SELECT * FROM products WHERE id = $1", [id]);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
export interface CreateProductInput {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
productType: string;
|
||||
productGroup?: string | null;
|
||||
productSubgroup?: string | null;
|
||||
brand?: string | null;
|
||||
unit?: string;
|
||||
active?: boolean;
|
||||
discontinued?: boolean;
|
||||
tracksStock?: boolean;
|
||||
tracksSerial?: boolean;
|
||||
tracksAssetTag?: boolean;
|
||||
tracksMac?: boolean;
|
||||
price0?: number | null;
|
||||
price12?: number | null;
|
||||
price24?: number | null;
|
||||
price36?: number | null;
|
||||
price48?: number | null;
|
||||
implUnitPrice?: number;
|
||||
createdBy?: string | null;
|
||||
}
|
||||
|
||||
export async function createProduct(input: CreateProductInput): Promise<Product> {
|
||||
const { rows } = await query<Product>(
|
||||
`INSERT INTO products (
|
||||
name, description, product_type, product_group, product_subgroup, brand, unit,
|
||||
active, discontinued, tracks_stock, tracks_serial, tracks_asset_tag, tracks_mac,
|
||||
price_0, price_12, price_24, price_36, price_48, impl_unit_price, created_by
|
||||
) VALUES (
|
||||
$1,$2,$3,$4,$5,$6,COALESCE($7,'unidade'),
|
||||
COALESCE($8,true),COALESCE($9,false),COALESCE($10,false),COALESCE($11,false),COALESCE($12,false),COALESCE($13,false),
|
||||
$14,$15,$16,$17,$18,COALESCE($19,0),$20
|
||||
)
|
||||
RETURNING *`,
|
||||
[
|
||||
input.name,
|
||||
input.description ?? null,
|
||||
input.productType,
|
||||
input.productGroup ?? null,
|
||||
input.productSubgroup ?? null,
|
||||
input.brand ?? null,
|
||||
input.unit ?? null,
|
||||
input.active ?? null,
|
||||
input.discontinued ?? null,
|
||||
input.tracksStock ?? null,
|
||||
input.tracksSerial ?? null,
|
||||
input.tracksAssetTag ?? null,
|
||||
input.tracksMac ?? null,
|
||||
input.price0 ?? null,
|
||||
input.price12 ?? null,
|
||||
input.price24 ?? null,
|
||||
input.price36 ?? null,
|
||||
input.price48 ?? null,
|
||||
input.implUnitPrice ?? null,
|
||||
input.createdBy ?? null,
|
||||
],
|
||||
);
|
||||
const product = rows[0];
|
||||
if (!product) throw new Error("failed to create product");
|
||||
return product;
|
||||
}
|
||||
|
||||
export type UpdateProductInput = Partial<Omit<CreateProductInput, "createdBy">>;
|
||||
|
||||
export async function updateProduct(id: string, input: UpdateProductInput): Promise<Product | null> {
|
||||
const { rows } = await query<Product>(
|
||||
`UPDATE products SET
|
||||
name = COALESCE($2, name),
|
||||
description = COALESCE($3, description),
|
||||
product_type = COALESCE($4, product_type),
|
||||
product_group = COALESCE($5, product_group),
|
||||
product_subgroup = COALESCE($6, product_subgroup),
|
||||
brand = COALESCE($7, brand),
|
||||
unit = COALESCE($8, unit),
|
||||
active = COALESCE($9, active),
|
||||
discontinued = COALESCE($10, discontinued),
|
||||
tracks_stock = COALESCE($11, tracks_stock),
|
||||
tracks_serial = COALESCE($12, tracks_serial),
|
||||
tracks_asset_tag = COALESCE($13, tracks_asset_tag),
|
||||
tracks_mac = COALESCE($14, tracks_mac),
|
||||
price_0 = COALESCE($15, price_0),
|
||||
price_12 = COALESCE($16, price_12),
|
||||
price_24 = COALESCE($17, price_24),
|
||||
price_36 = COALESCE($18, price_36),
|
||||
price_48 = COALESCE($19, price_48),
|
||||
impl_unit_price = COALESCE($20, impl_unit_price)
|
||||
WHERE id = $1
|
||||
RETURNING *`,
|
||||
[
|
||||
id,
|
||||
input.name ?? null,
|
||||
input.description ?? null,
|
||||
input.productType ?? null,
|
||||
input.productGroup ?? null,
|
||||
input.productSubgroup ?? null,
|
||||
input.brand ?? null,
|
||||
input.unit ?? null,
|
||||
input.active ?? null,
|
||||
input.discontinued ?? null,
|
||||
input.tracksStock ?? null,
|
||||
input.tracksSerial ?? null,
|
||||
input.tracksAssetTag ?? null,
|
||||
input.tracksMac ?? null,
|
||||
input.price0 ?? null,
|
||||
input.price12 ?? null,
|
||||
input.price24 ?? null,
|
||||
input.price36 ?? null,
|
||||
input.price48 ?? null,
|
||||
input.implUnitPrice ?? null,
|
||||
],
|
||||
);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
Reference in New Issue
Block a user