Merge: Products/Catalog module (agente eden-commercial)
This commit is contained in:
68
packages/database/migrations/1788438907074_products.cjs
Normal file
68
packages/database/migrations/1788438907074_products.cjs
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Commercial bounded context, first slice: products (sellable catalog
|
||||
* items) — Master Prompt §6.3, eden.md §2 §1.1. MVP scope only: the shape
|
||||
* below ports the legacy `products` table's core identity/pricing/flag
|
||||
* columns faithfully (product_code sequence, the 5 fixed price tiers,
|
||||
* impl_unit_price), but deliberately leaves out service_type/metered/
|
||||
* tariff_rates/ixc_product_code — those are legacy telecom-billing
|
||||
* specifics that belong to a later phase, not invented here. `product_type`
|
||||
* is free text for now (no CHECK) since the enum of types isn't finalized;
|
||||
* tightening it to a CHECK/lookup table is future work once Quotes needs
|
||||
* to branch behavior on it.
|
||||
*
|
||||
* Quotes/pricing-tier selection logic is explicitly out of scope here —
|
||||
* this migration only stores the data, it computes nothing.
|
||||
*/
|
||||
|
||||
exports.shorthands = undefined;
|
||||
|
||||
exports.up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE SEQUENCE products_product_code_seq START 1;
|
||||
|
||||
CREATE TABLE products (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
created_by UUID REFERENCES users(id),
|
||||
product_code INTEGER NOT NULL UNIQUE DEFAULT nextval('products_product_code_seq'),
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
product_type TEXT NOT NULL,
|
||||
product_group TEXT,
|
||||
product_subgroup TEXT,
|
||||
brand TEXT,
|
||||
unit TEXT NOT NULL DEFAULT 'unidade',
|
||||
active BOOLEAN NOT NULL DEFAULT true,
|
||||
discontinued BOOLEAN NOT NULL DEFAULT false,
|
||||
tracks_stock BOOLEAN NOT NULL DEFAULT false,
|
||||
tracks_serial BOOLEAN NOT NULL DEFAULT false,
|
||||
tracks_asset_tag BOOLEAN NOT NULL DEFAULT false,
|
||||
tracks_mac BOOLEAN NOT NULL DEFAULT false,
|
||||
price_0 NUMERIC(12,2),
|
||||
price_12 NUMERIC(12,2),
|
||||
price_24 NUMERIC(12,2),
|
||||
price_36 NUMERIC(12,2),
|
||||
price_48 NUMERIC(12,2),
|
||||
impl_unit_price NUMERIC(12,2) NOT NULL DEFAULT 0
|
||||
);
|
||||
COMMENT ON TABLE products IS
|
||||
'Catálogo de produtos vendáveis — eden.md §2 §1.1, Master Prompt §6.3. product_code é a sequência legada (distinta do UUID); price_0/12/24/36/48 são as 5 faixas fixas de preço por período de fidelidade, não uma tabela relacionada. Leitura aberta a qualquer autenticado (catálogo precisa ser navegável por qualquer vendedor); escrita restrita à permissão products:edit.';
|
||||
COMMENT ON COLUMN products.impl_unit_price IS
|
||||
'Valor de implantação por unidade, multiplicado pela quantidade do item na oferta (fase futura — Quotes). NUMERIC sempre, nunca float.';
|
||||
|
||||
CREATE TRIGGER trg_products_updated_at BEFORE UPDATE ON products
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE INDEX idx_products_active ON products(active);
|
||||
|
||||
ALTER SEQUENCE products_product_code_seq OWNED BY products.product_code;
|
||||
`);
|
||||
};
|
||||
|
||||
exports.down = (pgm) => {
|
||||
pgm.sql(`
|
||||
DROP TABLE IF EXISTS products;
|
||||
DROP SEQUENCE IF EXISTS products_product_code_seq;
|
||||
`);
|
||||
};
|
||||
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