Files
graphs/backend/db.js
Cauê Faleiros 3a0cc9ffe8
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m13s
Allow updating production plan responsibility
2026-08-25 16:42:51 -03:00

914 lines
43 KiB
JavaScript

const { Pool } = require('pg');
const { DATABASE_URL } = require('./config');
const pool = new Pool({
connectionString: DATABASE_URL
});
const defaultCatalogCategories = [
['Camiseta regular', 'Bases lisas e camisetas adultas.'],
['Camiseta infantil', 'Produtos infantis por cor e tamanho.'],
['Moletom', 'Moletons, cangurus e produtos de frio.'],
['Oversized', 'Modelagens oversized e variações relacionadas.'],
['Acessórios', 'Bonés, itens complementares e produtos não têxteis.'],
['DTF', 'Insumos e serviços relacionados a impressão DTF.'],
['Malha', 'Tecidos e malhas usados como matéria-prima.'],
['Aviamentos', 'Ribanas, linhas, ilhós e componentes de costura.'],
['Embalagens', 'Sacos, etiquetas, tags e materiais de expedição.'],
['Insumos gerais', 'Materiais de apoio sem família operacional específica.']
];
const seedDefaultCatalogCategories = async () => {
for (const [name, description] of defaultCatalogCategories) {
await pool.query(`
INSERT INTO catalog_categories (name, description, updated_at)
VALUES ($1, $2, CURRENT_TIMESTAMP)
ON CONFLICT (name) DO NOTHING;
`, [name, description]);
}
};
const initDB = async () => {
try {
await pool.query(`SET TIME ZONE 'America/Sao_Paulo';`);
await pool.query(`
CREATE TABLE IF NOT EXISTS orders (
id SERIAL PRIMARY KEY,
cliente_nome VARCHAR(255),
data_pedido VARCHAR(50),
data_pedido_date DATE,
valor_pedido NUMERIC(10, 2),
produto_id VARCHAR(100),
produto_descricao TEXT,
quantidade INTEGER,
valor_unitario NUMERIC(10, 5),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS pedido_id VARCHAR(100);`).catch(() => {});
await pool.query(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS cliente_fone VARCHAR(50);`).catch(() => {});
await pool.query(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS data_pedido_date DATE;`).catch(() => {});
await pool.query(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS cliente_nome_fantasia VARCHAR(255);`).catch(() => {});
await pool.query(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS id_vendedor VARCHAR(100);`).catch(() => {});
await pool.query(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS nome_vendedor VARCHAR(255);`).catch(() => {});
await pool.query(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS marketplace VARCHAR(255);`).catch(() => {});
await pool.query(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS canal_venda VARCHAR(255);`).catch(() => {});
await pool.query(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS numero_ecommerce VARCHAR(100);`).catch(() => {});
await pool.query(`
ALTER TABLE orders
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP;
`).catch(() => {});
await pool.query(`
UPDATE orders
SET data_pedido_date = CASE
WHEN data_pedido ~ '^\\d{4}[-/]\\d{1,2}[-/]\\d{1,2}' THEN to_date(replace(left(data_pedido, 10), '/', '-'), 'YYYY-MM-DD')
WHEN data_pedido ~ '^\\d{1,2}[-/]\\d{1,2}[-/]\\d{4}' THEN to_date(replace(left(data_pedido, 10), '/', '-'), 'DD-MM-YYYY')
ELSE NULL
END
WHERE data_pedido_date IS NULL
AND data_pedido IS NOT NULL
AND data_pedido != '';
`).catch(err => {
console.error('Notice: Could not backfill normalized order dates:', err.message);
});
await pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS unique_order_product ON orders (pedido_id, produto_id);`).catch(err => {
console.error('Notice: Could not create unique index (might already exist or there are duplicates):', err.message);
});
await pool.query(`
CREATE TABLE IF NOT EXISTS stock (
produto_id VARCHAR(100) PRIMARY KEY,
nome TEXT,
saldo INTEGER DEFAULT 0,
delta_estoque INTEGER DEFAULT 0,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
ALTER TABLE stock
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
`).catch(() => {});
// Tiny/Olist remains the imported source balance. Graphs physical balances
// are stored separately so planning can use a verified count without
// overwriting the audit value received from Tiny.
await pool.query(`
CREATE TABLE IF NOT EXISTS stock_graphs_balances (
produto_id VARCHAR(100) PRIMARY KEY,
balance NUMERIC(14, 4) NOT NULL,
reason TEXT NOT NULL,
effective_at DATE,
updated_by VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS stock_graphs_balance_history (
id SERIAL PRIMARY KEY,
produto_id VARCHAR(100) NOT NULL,
tiny_balance NUMERIC(14, 4),
graphs_balance NUMERIC(14, 4),
reason TEXT,
effective_at DATE,
action VARCHAR(20) NOT NULL DEFAULT 'set',
changed_by VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS stock_campaign_queue (
id SERIAL PRIMARY KEY,
base_product_name TEXT NOT NULL,
produto_id VARCHAR(100) NOT NULL,
nome TEXT NOT NULL,
saldo INTEGER DEFAULT 0,
delta_estoque INTEGER DEFAULT 0,
status VARCHAR(20) DEFAULT 'pending',
attempts INTEGER DEFAULT 0,
last_error TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
sent_at TIMESTAMPTZ
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS production_orders (
id SERIAL PRIMARY KEY,
tiny_id VARCHAR(100) UNIQUE,
number VARCHAR(100),
status VARCHAR(40) DEFAULT 'open',
order_reference TEXT,
issue_date DATE,
expected_date DATE,
product_sku VARCHAR(255),
product_description TEXT NOT NULL,
quantity NUMERIC(14, 4) DEFAULT 0,
unit VARCHAR(20) DEFAULT 'UN',
integration_status VARCHAR(100),
notes TEXT,
supplier TEXT,
lot_code VARCHAR(120),
roll_quantity NUMERIC(14, 4),
fabric_kg NUMERIC(14, 4),
rib_kg NUMERIC(14, 4),
yield_pieces_per_kg NUMERIC(14, 4),
tiny_payload JSONB,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
// A production plan is the PCP container above the individual OPs. The
// calculated cut screen may change tomorrow; a confirmed plan must not.
await pool.query(`
CREATE TABLE IF NOT EXISTS production_plans (
id SERIAL PRIMARY KEY,
number VARCHAR(40) UNIQUE,
name TEXT NOT NULL,
status VARCHAR(30) NOT NULL DEFAULT 'open',
contractor TEXT,
planned_start_date DATE,
expected_date DATE,
notes TEXT,
source VARCHAR(40) NOT NULL DEFAULT 'cut_plan',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMPTZ
);
`);
await pool.query(`
ALTER TABLE production_orders
ADD COLUMN IF NOT EXISTS production_plan_id INTEGER REFERENCES production_plans(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS contractor TEXT;
`).catch(() => {});
await pool.query(`
CREATE TABLE IF NOT EXISTS production_order_markers (
id SERIAL PRIMARY KEY,
production_order_id INTEGER NOT NULL REFERENCES production_orders(id) ON DELETE CASCADE,
label VARCHAR(100) NOT NULL,
color VARCHAR(40),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE (production_order_id, label)
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS production_order_components (
id SERIAL PRIMARY KEY,
production_order_id INTEGER NOT NULL REFERENCES production_orders(id) ON DELETE CASCADE,
component_tiny_id VARCHAR(100),
component_sku VARCHAR(255),
component_name TEXT NOT NULL,
quantity_per_unit NUMERIC(14, 4) DEFAULT 0,
total_quantity NUMERIC(14, 4) DEFAULT 0,
unit VARCHAR(30),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS production_order_steps (
id SERIAL PRIMARY KEY,
production_order_id INTEGER NOT NULL REFERENCES production_orders(id) ON DELETE CASCADE,
step_number INTEGER,
name VARCHAR(160) NOT NULL,
start_date DATE,
end_date DATE,
status VARCHAR(80),
color VARCHAR(40),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS production_workstations (
id SERIAL PRIMARY KEY,
code VARCHAR(60) NOT NULL UNIQUE,
name VARCHAR(160) NOT NULL,
color VARCHAR(40) NOT NULL DEFAULT '#38bdf8',
sort_order INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
INSERT INTO production_workstations (code, name, color, sort_order)
VALUES
('cutting', 'Corte', '#fbbf24', 10),
('sewing', 'Costura', '#38bdf8', 20),
('review', 'Revisão', '#a78bfa', 30),
('sac', 'SAC', '#fb7185', 40)
ON CONFLICT (code) DO NOTHING;
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS production_order_defects (
id SERIAL PRIMARY KEY,
production_order_id INTEGER NOT NULL REFERENCES production_orders(id) ON DELETE CASCADE,
workstation_id INTEGER REFERENCES production_workstations(id) ON DELETE SET NULL,
defect_type VARCHAR(160) NOT NULL,
origin VARCHAR(160),
quantity NUMERIC(14, 4) NOT NULL CHECK (quantity > 0),
lot_code VARCHAR(120),
notes TEXT,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS product_compositions (
id SERIAL PRIMARY KEY,
source VARCHAR(60) NOT NULL,
external_source_id VARCHAR(120) NOT NULL,
finished_product_identity VARCHAR(255) NOT NULL,
finished_product_sku VARCHAR(255),
finished_product_description TEXT NOT NULL,
finished_product_unit VARCHAR(30) NOT NULL DEFAULT 'UN',
finished_tiny_product_id VARCHAR(100),
source_metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
last_synced_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE (source, finished_product_identity)
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS product_composition_components (
id SERIAL PRIMARY KEY,
product_composition_id INTEGER NOT NULL REFERENCES product_compositions(id) ON DELETE CASCADE,
component_identity VARCHAR(255) NOT NULL,
component_tiny_id VARCHAR(100),
component_sku VARCHAR(255),
component_name TEXT NOT NULL,
quantity_per_unit NUMERIC(14, 4) NOT NULL DEFAULT 0,
unit VARCHAR(30),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE (product_composition_id, component_identity)
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS cutting_family_rules (
family_key VARCHAR(20) PRIMARY KEY,
units_per_roll NUMERIC(14, 4),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS cutting_product_overrides (
product_id VARCHAR(100) PRIMARY KEY,
family_key VARCHAR(20),
color VARCHAR(100),
size VARCHAR(40),
product_type VARCHAR(40),
planning_notes TEXT,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS catalog_categories (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS catalog_products (
id SERIAL PRIMARY KEY,
type VARCHAR(40) NOT NULL DEFAULT 'finished_product',
sku VARCHAR(100) NOT NULL UNIQUE,
name TEXT NOT NULL,
category_id INTEGER REFERENCES catalog_categories(id) ON DELETE SET NULL,
composition TEXT,
notes TEXT,
gramature NUMERIC(14, 4),
material_yield NUMERIC(14, 4),
width_cm NUMERIC(14, 4),
color VARCHAR(100),
subcategory VARCHAR(80),
sizes TEXT[] DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS supply_material_details (
material_key VARCHAR(255) PRIMARY KEY,
display_name TEXT,
preferred_supplier VARCHAR(255),
notes TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS olist_connections (
id SMALLINT PRIMARY KEY CHECK (id = 1),
access_token_ciphertext TEXT NOT NULL,
access_token_iv VARCHAR(64) NOT NULL,
refresh_token_ciphertext TEXT NOT NULL,
refresh_token_iv VARCHAR(64) NOT NULL,
token_expires_at TIMESTAMPTZ,
connected_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS olist_oauth_states (
state_hash VARCHAR(128) PRIMARY KEY,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS olist_sync_runs (
id SERIAL PRIMARY KEY,
trigger VARCHAR(30) NOT NULL,
mode VARCHAR(20) NOT NULL,
status VARCHAR(20) NOT NULL,
started_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMPTZ,
products_scanned INTEGER NOT NULL DEFAULT 0,
manufactured_products INTEGER NOT NULL DEFAULT 0,
imported INTEGER NOT NULL DEFAULT 0,
component_count INTEGER NOT NULL DEFAULT 0,
reference_count INTEGER NOT NULL DEFAULT 0,
skipped_reference_count INTEGER NOT NULL DEFAULT 0,
failed INTEGER NOT NULL DEFAULT 0,
error_message TEXT,
details JSONB NOT NULL DEFAULT '[]'::jsonb
);
`);
await pool.query(`
ALTER TABLE olist_sync_runs
ADD COLUMN IF NOT EXISTS cancel_requested_at TIMESTAMPTZ;
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS olist_sync_events (
id BIGSERIAL PRIMARY KEY,
run_id INTEGER NOT NULL REFERENCES olist_sync_runs(id) ON DELETE CASCADE,
level VARCHAR(20) NOT NULL DEFAULT 'info',
event_type VARCHAR(40) NOT NULL,
message TEXT NOT NULL,
olist_product_id BIGINT,
sku VARCHAR(100),
product_description TEXT,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS olist_product_cache (
olist_product_id BIGINT PRIMARY KEY,
sku VARCHAR(100),
description TEXT NOT NULL,
unit VARCHAR(30) NOT NULL DEFAULT 'UN',
last_changed_at TIMESTAMPTZ,
structure_checked_at TIMESTAMPTZ,
is_manufactured BOOLEAN,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS olist_sync_state (
id SMALLINT PRIMARY KEY CHECK (id = 1),
last_catalog_sync_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS consumption_references (
id SERIAL PRIMARY KEY,
product_id INTEGER NOT NULL REFERENCES catalog_products(id) ON DELETE CASCADE,
material_product_id INTEGER REFERENCES catalog_products(id) ON DELETE SET NULL,
color VARCHAR(100),
general_yield NUMERIC(14, 4),
size_yields JSONB DEFAULT '{}'::jsonb,
size_areas JSONB DEFAULT '{}'::jsonb,
gramature NUMERIC(14, 4),
efficiency_percent NUMERIC(7, 3),
rib_g_per_piece NUMERIC(14, 4),
material_cost_per_kg NUMERIC(14, 4),
consumption_quantity NUMERIC(14, 4),
consumption_unit VARCHAR(30),
source VARCHAR(60) DEFAULT 'manual',
last_production_order_id INTEGER REFERENCES production_orders(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS supply_receipts (
id SERIAL PRIMARY KEY,
category VARCHAR(120) NOT NULL,
product TEXT NOT NULL,
quantity NUMERIC(14, 4) NOT NULL,
unit VARCHAR(30) NOT NULL DEFAULT 'kg',
supplier TEXT,
invoice VARCHAR(120),
notes TEXT,
status VARCHAR(30) NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
approved_at TIMESTAMPTZ
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS supply_stock_lots (
id SERIAL PRIMARY KEY,
receipt_id INTEGER REFERENCES supply_receipts(id) ON DELETE SET NULL,
category VARCHAR(120) NOT NULL,
product TEXT NOT NULL,
quantity NUMERIC(14, 4) NOT NULL,
unit VARCHAR(30) NOT NULL DEFAULT 'kg',
supplier TEXT,
invoice VARCHAR(120),
status VARCHAR(30) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS cut_plan_lot_reservations (
id SERIAL PRIMARY KEY,
cut_plan_reference TEXT NOT NULL,
production_plan_id INTEGER REFERENCES production_plans(id) ON DELETE SET NULL,
supply_lot_id INTEGER NOT NULL REFERENCES supply_stock_lots(id) ON DELETE RESTRICT,
material_key VARCHAR(255) NOT NULL,
quantity NUMERIC(14, 4) NOT NULL CHECK (quantity > 0),
consumed_quantity NUMERIC(14, 4) NOT NULL DEFAULT 0 CHECK (consumed_quantity >= 0),
status VARCHAR(30) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
released_at TIMESTAMPTZ
);
`);
await pool.query(`
ALTER TABLE cut_plan_lot_reservations
ADD COLUMN IF NOT EXISTS production_plan_id INTEGER REFERENCES production_plans(id) ON DELETE SET NULL;
`).catch(() => {});
await pool.query(`CREATE INDEX IF NOT EXISTS idx_cut_plan_lot_reservations_lot_status ON cut_plan_lot_reservations (supply_lot_id, status);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_cut_plan_lot_reservations_reference ON cut_plan_lot_reservations (cut_plan_reference, status);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_cut_plan_lot_reservations_plan ON cut_plan_lot_reservations (production_plan_id, status);`);
await pool.query(`
CREATE TABLE IF NOT EXISTS supply_movements (
id SERIAL PRIMARY KEY,
receipt_id INTEGER REFERENCES supply_receipts(id) ON DELETE SET NULL,
lot_id INTEGER REFERENCES supply_stock_lots(id) ON DELETE SET NULL,
type VARCHAR(40) NOT NULL,
category VARCHAR(120) NOT NULL,
product TEXT NOT NULL,
quantity NUMERIC(14, 4) NOT NULL,
unit VARCHAR(30) NOT NULL DEFAULT 'kg',
reason TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS supply_fabric_plans (
id SERIAL PRIMARY KEY,
material TEXT NOT NULL,
color VARCHAR(120),
quantity_kg NUMERIC(14, 4) NOT NULL,
supplier TEXT,
priority VARCHAR(40) NOT NULL DEFAULT 'Normal',
status VARCHAR(30) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS supply_purchase_orders (
id SERIAL PRIMARY KEY,
number VARCHAR(40) UNIQUE,
supplier TEXT NOT NULL,
status VARCHAR(30) NOT NULL DEFAULT 'draft',
expected_date DATE,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS supply_purchase_order_items (
id SERIAL PRIMARY KEY,
purchase_order_id INTEGER NOT NULL REFERENCES supply_purchase_orders(id) ON DELETE CASCADE,
material TEXT NOT NULL,
quantity NUMERIC(14, 4) NOT NULL CHECK (quantity > 0),
unit VARCHAR(30) NOT NULL DEFAULT 'kg',
unit_cost NUMERIC(14, 4),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_supply_purchase_orders_status ON supply_purchase_orders (status, created_at DESC);`);
await pool.query(`
ALTER TABLE production_orders
ADD COLUMN IF NOT EXISTS notes TEXT,
ADD COLUMN IF NOT EXISTS supplier TEXT,
ADD COLUMN IF NOT EXISTS lot_code VARCHAR(120),
ADD COLUMN IF NOT EXISTS roll_quantity NUMERIC(14, 4),
ADD COLUMN IF NOT EXISTS fabric_kg NUMERIC(14, 4),
ADD COLUMN IF NOT EXISTS rib_kg NUMERIC(14, 4),
ADD COLUMN IF NOT EXISTS yield_pieces_per_kg NUMERIC(14, 4);
`).catch(() => {});
await pool.query(`
ALTER TABLE production_orders
ADD COLUMN IF NOT EXISTS completed_date DATE;
`).catch(() => {});
await pool.query(`
ALTER TABLE production_orders
ADD COLUMN IF NOT EXISTS actual_cut_quantity NUMERIC(14, 4),
ADD COLUMN IF NOT EXISTS rejected_quantity NUMERIC(14, 4) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS finished_quantity NUMERIC(14, 4),
ADD COLUMN IF NOT EXISTS quality_notes TEXT;
`).catch(() => {});
await pool.query(`
CREATE TABLE IF NOT EXISTS production_order_import_runs (
id SERIAL PRIMARY KEY,
filename TEXT NOT NULL,
status VARCHAR(40) NOT NULL DEFAULT 'running',
total_rows INTEGER NOT NULL DEFAULT 0,
imported_count INTEGER NOT NULL DEFAULT 0,
updated_count INTEGER NOT NULL DEFAULT 0,
skipped_count INTEGER NOT NULL DEFAULT 0,
failed_count INTEGER NOT NULL DEFAULT 0,
error_message TEXT,
started_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMPTZ
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS production_yield_recommendations (
family_key VARCHAR(20) PRIMARY KEY,
sample_count INTEGER NOT NULL DEFAULT 0,
yield_sample_count INTEGER NOT NULL DEFAULT 0,
roll_sample_count INTEGER NOT NULL DEFAULT 0,
avg_yield_pieces_per_kg NUMERIC(14, 4),
avg_kg_per_roll NUMERIC(14, 4),
recommended_units_per_roll NUMERIC(14, 4),
last_completed_date DATE,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS production_yield_supplier_recommendations (
family_key VARCHAR(20) NOT NULL,
supplier VARCHAR(255) NOT NULL,
sample_count INTEGER NOT NULL DEFAULT 0,
yield_sample_count INTEGER NOT NULL DEFAULT 0,
roll_sample_count INTEGER NOT NULL DEFAULT 0,
avg_yield_pieces_per_kg NUMERIC(14, 4),
avg_kg_per_roll NUMERIC(14, 4),
recommended_units_per_roll NUMERIC(14, 4),
last_completed_date DATE,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (family_key, supplier)
);
`);
await pool.query(`
ALTER TABLE production_orders
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP,
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
`).catch(() => {});
await pool.query(`
ALTER TABLE production_order_components
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP,
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
`).catch(() => {});
await pool.query(`
ALTER TABLE production_order_steps
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP,
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
`).catch(() => {});
await pool.query(`
ALTER TABLE cutting_family_rules
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
`).catch(() => {});
await pool.query(`
ALTER TABLE cutting_product_overrides
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
`).catch(() => {});
await pool.query(`
ALTER TABLE cutting_product_overrides
ADD COLUMN IF NOT EXISTS product_type VARCHAR(40),
ADD COLUMN IF NOT EXISTS planning_notes TEXT;
`).catch(() => {});
await pool.query(`
ALTER TABLE catalog_categories
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP,
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
`).catch(() => {});
await pool.query(`
ALTER TABLE catalog_products
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP,
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
`).catch(() => {});
await pool.query(`
ALTER TABLE consumption_references
ADD COLUMN IF NOT EXISTS consumption_quantity NUMERIC(14, 4),
ADD COLUMN IF NOT EXISTS consumption_unit VARCHAR(30),
ADD COLUMN IF NOT EXISTS source VARCHAR(60) DEFAULT 'manual',
ADD COLUMN IF NOT EXISTS last_production_order_id INTEGER REFERENCES production_orders(id) ON DELETE SET NULL;
`).catch(() => {});
await pool.query(`
ALTER TABLE consumption_references
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP,
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
`).catch(() => {});
await seedDefaultCatalogCategories();
await pool.query(`
ALTER TABLE supply_receipts
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP,
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP,
ALTER COLUMN approved_at TYPE TIMESTAMPTZ USING approved_at AT TIME ZONE 'America/Sao_Paulo';
`).catch(() => {});
await pool.query(`
ALTER TABLE supply_stock_lots
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP,
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
`).catch(() => {});
await pool.query(`
ALTER TABLE supply_movements
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP;
`).catch(() => {});
await pool.query(`
ALTER TABLE supply_fabric_plans
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP,
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
`).catch(() => {});
await pool.query(`
CREATE TABLE IF NOT EXISTS app_users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS client_identity_tokens (
customer_key TEXT PRIMARY KEY,
token TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
`);
await pool.query(`
ALTER TABLE app_users
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP,
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
`).catch(() => {});
// A production plan is operated by a Graphs user. The ID link will let
// role-based assignments be added without changing historical plans.
await pool.query(`
ALTER TABLE production_plans
ADD COLUMN IF NOT EXISTS responsible_user_id INTEGER REFERENCES app_users(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS responsible_name TEXT;
`).catch(() => {});
await pool.query(`
ALTER TABLE stock_campaign_queue
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP,
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP,
ALTER COLUMN sent_at TYPE TIMESTAMPTZ USING sent_at AT TIME ZONE 'America/Sao_Paulo';
`).catch(() => {});
await pool.query(`
UPDATE stock_campaign_queue
SET base_product_name = TRIM(regexp_replace(
base_product_name,
'\\s+-\\s+(?:(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\\d{2})(?:/(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\\d{2}))*)$',
'',
'i'
))
WHERE status IN ('pending', 'failed', 'processing')
AND base_product_name ~* '\\s+-\\s+(?:(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\\d{2})(?:/(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\\d{2}))*)$';
`).catch(err => {
console.error('Notice: Could not normalize queued campaign product names:', err.message);
});
await pool.query(`
UPDATE stock_campaign_queue
SET base_product_name = nome
WHERE status IN ('pending', 'failed', 'processing')
AND nome ILIKE 'ETIQUETA%'
AND base_product_name != nome;
`).catch(err => {
console.error('Notice: Could not restore queued etiqueta product names:', err.message);
});
await pool.query(`CREATE INDEX IF NOT EXISTS idx_stock_campaign_queue_status ON stock_campaign_queue (status);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_stock_graphs_balance_history_product_created ON stock_graphs_balance_history (produto_id, created_at DESC);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_orders_status ON production_orders (status);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_orders_issue_date ON production_orders (issue_date DESC);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_orders_expected_date ON production_orders (expected_date DESC);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_orders_completed_date ON production_orders (completed_date DESC);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_order_import_runs_started_at ON production_order_import_runs (started_at DESC);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_order_markers_order_id ON production_order_markers (production_order_id);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_order_components_order_id ON production_order_components (production_order_id);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_order_components_sku ON production_order_components (component_sku);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_order_steps_order_id ON production_order_steps (production_order_id);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_product_compositions_finished_tiny_id ON product_compositions (finished_tiny_product_id);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_product_composition_components_tiny_id ON product_composition_components (component_tiny_id);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_cutting_product_overrides_family_key ON cutting_product_overrides (family_key);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_catalog_products_type ON catalog_products (type);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_catalog_products_category_id ON catalog_products (category_id);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_olist_sync_runs_started_at ON olist_sync_runs (started_at DESC);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_olist_sync_events_run_created ON olist_sync_events (run_id, created_at DESC);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_olist_sync_events_affected_products ON olist_sync_events (run_id, event_type, created_at DESC);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_olist_product_cache_pending_structure ON olist_product_cache (structure_checked_at) WHERE structure_checked_at IS NULL;`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_consumption_references_product_id ON consumption_references (product_id);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_consumption_references_material_product_id ON consumption_references (material_product_id);`);
await pool.query(`
CREATE UNIQUE INDEX IF NOT EXISTS unique_consumption_reference_source
ON consumption_references (
product_id,
COALESCE(material_product_id, 0),
COALESCE(color, ''),
COALESCE(source, 'manual')
);
`).catch(err => {
console.error('Notice: Could not create unique consumption reference source index:', err.message);
});
await pool.query(`CREATE INDEX IF NOT EXISTS idx_supply_receipts_status ON supply_receipts (status);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_supply_receipts_created_at ON supply_receipts (created_at DESC);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_supply_stock_lots_status ON supply_stock_lots (status);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_supply_movements_created_at ON supply_movements (created_at DESC);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_supply_fabric_plans_status ON supply_fabric_plans (status);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_orders_cliente_fone ON orders (cliente_fone);`);
await pool.query(`
CREATE INDEX IF NOT EXISTS idx_orders_normalized_cliente_nome
ON orders ((NULLIF(LOWER(TRIM(regexp_replace(COALESCE(cliente_nome, ''), '\\s+', ' ', 'g'))), '')));
`).catch(err => {
console.error('Notice: Could not create normalized client name index:', err.message);
});
await pool.query(`
CREATE INDEX IF NOT EXISTS idx_orders_normalized_cliente_nome_phone_date
ON orders (
(NULLIF(LOWER(TRIM(regexp_replace(COALESCE(cliente_nome, ''), '\\s+', ' ', 'g'))), '')),
data_pedido_date DESC,
id DESC
)
WHERE NULLIF(cliente_fone, '') IS NOT NULL;
`).catch(err => {
console.error('Notice: Could not create normalized client phone lookup index:', err.message);
});
await pool.query(`
CREATE INDEX IF NOT EXISTS idx_orders_customer_key_date
ON orders (
(COALESCE(NULLIF(cliente_fone, ''), 'name:' || COALESCE(NULLIF(cliente_nome, ''), 'Cliente Desconhecido'))),
data_pedido_date
);
`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_orders_produto_id ON orders (produto_id);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_orders_data_pedido_date ON orders (data_pedido_date);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_app_users_email ON app_users (LOWER(email));`);
console.log('Database initialized successfully.');
} catch (err) {
console.error('Failed to initialize database:', err);
throw err;
}
};
module.exports = {
pool,
initDB
};