Add ERP order import endpoint and n8n order-ingest flow export
This commit is contained in:
347
docs/SCHEMA_PHASE1.sql
Normal file
347
docs/SCHEMA_PHASE1.sql
Normal file
@@ -0,0 +1,347 @@
|
||||
-- ERP Naurua - Phase 1 Draft Schema
|
||||
-- Status: Draft (Intentional minimal constraints; to be hardened during implementation)
|
||||
-- Target DB: PostgreSQL-compatible SQL
|
||||
-- Scope: Lagerverwaltung (Chargen/MHD), Bestellerfassung, Kontaktangaben
|
||||
|
||||
-- NOTE
|
||||
-- - Use this as a starting point for migrations, not as final truth.
|
||||
-- - Add stricter constraints once business rules are confirmed.
|
||||
|
||||
CREATE TABLE party (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
type TEXT NOT NULL DEFAULT 'customer', -- customer | supplier | both
|
||||
name TEXT,
|
||||
email TEXT,
|
||||
phone TEXT,
|
||||
phone_alt TEXT,
|
||||
tax_id TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_party_email ON party(email);
|
||||
|
||||
CREATE TABLE address (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
party_id BIGINT NOT NULL REFERENCES party(id) ON DELETE CASCADE,
|
||||
type TEXT NOT NULL, -- billing | shipping
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
company_name TEXT,
|
||||
street TEXT,
|
||||
house_number TEXT,
|
||||
zip TEXT,
|
||||
city TEXT,
|
||||
state_code TEXT,
|
||||
country_name TEXT,
|
||||
country_iso2 CHAR(2),
|
||||
raw_payload JSONB, -- preserves source formatting variants
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_address_party ON address(party_id);
|
||||
CREATE INDEX idx_address_country_iso2 ON address(country_iso2);
|
||||
|
||||
CREATE TABLE contact (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
party_id BIGINT NOT NULL REFERENCES party(id) ON DELETE CASCADE,
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
email TEXT,
|
||||
phone TEXT,
|
||||
position_title TEXT,
|
||||
note TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_contact_party ON contact(party_id);
|
||||
|
||||
-- Lagergefuehrtes Produkt (Bestandsfuehrung, Charge, MHD)
|
||||
CREATE TABLE product (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
sku TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
uom TEXT NOT NULL DEFAULT 'unit',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_product_sku ON product(sku);
|
||||
|
||||
-- Verkaufbarer Shop-Artikel (kann Bundle sein)
|
||||
CREATE TABLE sellable_item (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
item_code TEXT NOT NULL, -- interne stabile Kennung
|
||||
display_name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_sellable_item_code ON sellable_item(item_code);
|
||||
|
||||
-- Mapping externer Shop-Daten auf internen Artikel
|
||||
CREATE TABLE external_item_alias (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
source_system TEXT NOT NULL DEFAULT 'wix',
|
||||
external_article_number TEXT,
|
||||
external_title TEXT,
|
||||
title_normalized TEXT,
|
||||
sellable_item_id BIGINT NOT NULL REFERENCES sellable_item(id) ON DELETE CASCADE,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_ext_alias_article_number ON external_item_alias(external_article_number);
|
||||
CREATE INDEX idx_ext_alias_title_norm ON external_item_alias(title_normalized);
|
||||
|
||||
-- Stueckliste: welcher Artikel enthaelt welche lagergefuehrten Produkte
|
||||
CREATE TABLE sellable_item_component (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
sellable_item_id BIGINT NOT NULL REFERENCES sellable_item(id) ON DELETE CASCADE,
|
||||
product_id BIGINT NOT NULL REFERENCES product(id) ON DELETE RESTRICT,
|
||||
qty_per_item NUMERIC(14, 4) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_component_qty_positive CHECK (qty_per_item > 0)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_item_component ON sellable_item_component(sellable_item_id, product_id);
|
||||
CREATE INDEX idx_item_component_product ON sellable_item_component(product_id);
|
||||
|
||||
CREATE TABLE warehouse (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
code TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_warehouse_code ON warehouse(code);
|
||||
|
||||
CREATE TABLE location (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
warehouse_id BIGINT NOT NULL REFERENCES warehouse(id) ON DELETE RESTRICT,
|
||||
code TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL, -- storage | receiving | dispatch | adjustment
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_location_warehouse ON location(warehouse_id);
|
||||
CREATE UNIQUE INDEX uq_location_code_per_warehouse ON location(warehouse_id, code);
|
||||
|
||||
CREATE TABLE stock_lot (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
product_id BIGINT NOT NULL REFERENCES product(id) ON DELETE RESTRICT,
|
||||
lot_number TEXT,
|
||||
mfg_date DATE,
|
||||
expiry_date DATE,
|
||||
status TEXT NOT NULL DEFAULT 'open', -- open | current | closed
|
||||
sellout_date DATE,
|
||||
warning_state TEXT NOT NULL DEFAULT 'none', -- none | due_60d | due_now
|
||||
supplier_lot_number TEXT,
|
||||
supplier_name TEXT,
|
||||
purchase_date DATE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_stock_lot_status CHECK (status IN ('open', 'current', 'closed')),
|
||||
CONSTRAINT chk_stock_lot_warning_state CHECK (warning_state IN ('none', 'due_60d', 'due_now')),
|
||||
CONSTRAINT chk_stock_lot_number_required_for_non_open CHECK (
|
||||
status = 'open' OR lot_number IS NOT NULL
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_stock_lot_product ON stock_lot(product_id);
|
||||
CREATE UNIQUE INDEX uq_stock_lot_product_number ON stock_lot(product_id, lot_number);
|
||||
CREATE UNIQUE INDEX uq_stock_lot_one_current_per_product ON stock_lot(product_id) WHERE status = 'current';
|
||||
CREATE UNIQUE INDEX uq_stock_lot_one_open_per_product ON stock_lot(product_id) WHERE status = 'open';
|
||||
|
||||
CREATE TABLE payment_method (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
code TEXT NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_payment_method_code ON payment_method(code);
|
||||
|
||||
CREATE TABLE shipping_method (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
code TEXT NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_shipping_method_code ON shipping_method(code);
|
||||
|
||||
CREATE TABLE sales_order (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
external_ref TEXT NOT NULL, -- Wix BestellungNr oder ERP-Ref (DIR-...)
|
||||
order_source TEXT NOT NULL DEFAULT 'wix', -- wix | direct
|
||||
party_id BIGINT REFERENCES party(id) ON DELETE RESTRICT, -- bei direct optional
|
||||
order_date TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
order_status TEXT NOT NULL DEFAULT 'received', -- received | imported | fulfilled | cancelled
|
||||
payment_status TEXT NOT NULL DEFAULT 'paid', -- phase 1 fuehrt nur bezahlte Auftraege
|
||||
payment_method_id BIGINT REFERENCES payment_method(id) ON DELETE RESTRICT,
|
||||
shipping_method_id BIGINT REFERENCES shipping_method(id) ON DELETE RESTRICT,
|
||||
amount_net NUMERIC(14, 2),
|
||||
amount_shipping NUMERIC(14, 2),
|
||||
amount_tax NUMERIC(14, 2),
|
||||
amount_discount NUMERIC(14, 2),
|
||||
total_amount NUMERIC(14, 2),
|
||||
currency TEXT NOT NULL DEFAULT 'CHF',
|
||||
webhook_payload JSONB,
|
||||
imported_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
cancelled_at TIMESTAMP,
|
||||
cancelled_reason TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_sales_order_status CHECK (order_status IN ('received', 'imported', 'fulfilled', 'cancelled')),
|
||||
CONSTRAINT chk_sales_order_payment_status CHECK (payment_status IN ('paid')),
|
||||
CONSTRAINT chk_sales_order_source CHECK (order_source IN ('wix', 'direct')),
|
||||
CONSTRAINT chk_sales_order_direct_ref_prefix CHECK (
|
||||
order_source <> 'direct' OR external_ref LIKE 'DIR-%'
|
||||
)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_sales_order_external_ref ON sales_order(external_ref);
|
||||
CREATE INDEX idx_sales_order_party ON sales_order(party_id);
|
||||
CREATE INDEX idx_sales_order_source ON sales_order(order_source);
|
||||
|
||||
CREATE TABLE sales_order_line (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
sales_order_id BIGINT NOT NULL REFERENCES sales_order(id) ON DELETE CASCADE,
|
||||
line_no INTEGER NOT NULL,
|
||||
sellable_item_id BIGINT REFERENCES sellable_item(id) ON DELETE RESTRICT,
|
||||
raw_external_article_number TEXT,
|
||||
raw_external_title TEXT,
|
||||
qty NUMERIC(14, 4) NOT NULL,
|
||||
qty_cancelled NUMERIC(14, 4) NOT NULL DEFAULT 0,
|
||||
line_status TEXT NOT NULL DEFAULT 'allocated', -- allocated | partially_cancelled | cancelled
|
||||
unit_price NUMERIC(14, 4),
|
||||
line_total NUMERIC(14, 2),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_sales_order_line_qty_positive CHECK (qty > 0),
|
||||
CONSTRAINT chk_sales_order_line_qty_cancelled_range CHECK (qty_cancelled >= 0 AND qty_cancelled <= qty),
|
||||
CONSTRAINT chk_sales_order_line_status CHECK (line_status IN ('allocated', 'partially_cancelled', 'cancelled'))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_sales_order_line_no ON sales_order_line(sales_order_id, line_no);
|
||||
CREATE INDEX idx_sales_order_line_order ON sales_order_line(sales_order_id);
|
||||
CREATE INDEX idx_sales_order_line_sellable_item ON sales_order_line(sellable_item_id);
|
||||
CREATE INDEX idx_sales_order_line_status ON sales_order_line(line_status);
|
||||
|
||||
CREATE TABLE stock_move (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
product_id BIGINT NOT NULL REFERENCES product(id) ON DELETE RESTRICT,
|
||||
lot_id BIGINT NOT NULL REFERENCES stock_lot(id) ON DELETE RESTRICT,
|
||||
from_location_id BIGINT REFERENCES location(id) ON DELETE RESTRICT,
|
||||
to_location_id BIGINT REFERENCES location(id) ON DELETE RESTRICT,
|
||||
qty NUMERIC(14, 4) NOT NULL,
|
||||
move_type TEXT NOT NULL, -- in | out | transfer | adjustment
|
||||
move_date TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
note TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_stock_move_qty_positive CHECK (qty > 0),
|
||||
CONSTRAINT chk_stock_move_type CHECK (move_type IN ('in', 'out', 'transfer', 'adjustment'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_stock_move_product ON stock_move(product_id);
|
||||
CREATE INDEX idx_stock_move_lot ON stock_move(lot_id);
|
||||
CREATE INDEX idx_stock_move_from_location ON stock_move(from_location_id);
|
||||
CREATE INDEX idx_stock_move_to_location ON stock_move(to_location_id);
|
||||
|
||||
-- Explizite Rueckverfolgung: welche Charge wurde fuer welche Bestellposition verwendet
|
||||
CREATE TABLE sales_order_line_lot_allocation (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
sales_order_line_id BIGINT NOT NULL REFERENCES sales_order_line(id) ON DELETE CASCADE,
|
||||
product_id BIGINT NOT NULL REFERENCES product(id) ON DELETE RESTRICT,
|
||||
lot_id BIGINT NOT NULL REFERENCES stock_lot(id) ON DELETE RESTRICT,
|
||||
qty NUMERIC(14, 4) NOT NULL,
|
||||
allocation_status TEXT NOT NULL DEFAULT 'allocated', -- reserved | allocated | released | cancelled
|
||||
released_at TIMESTAMP,
|
||||
stock_move_id BIGINT REFERENCES stock_move(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_line_lot_qty_positive CHECK (qty > 0),
|
||||
CONSTRAINT chk_line_lot_alloc_status CHECK (allocation_status IN ('reserved', 'allocated', 'released', 'cancelled'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_line_lot_alloc_line ON sales_order_line_lot_allocation(sales_order_line_id);
|
||||
CREATE INDEX idx_line_lot_alloc_product ON sales_order_line_lot_allocation(product_id);
|
||||
CREATE INDEX idx_line_lot_alloc_lot ON sales_order_line_lot_allocation(lot_id);
|
||||
CREATE INDEX idx_line_lot_alloc_stock_move ON sales_order_line_lot_allocation(stock_move_id);
|
||||
CREATE INDEX idx_line_lot_alloc_status ON sales_order_line_lot_allocation(allocation_status);
|
||||
|
||||
-- Berechnete Chargensalden (Lagereingang/-ausgang/-netto)
|
||||
CREATE VIEW v_stock_lot_balance AS
|
||||
SELECT
|
||||
sl.id AS stock_lot_id,
|
||||
sl.product_id,
|
||||
COALESCE(SUM(CASE WHEN sm.move_type = 'in' THEN sm.qty ELSE 0 END), 0) AS qty_in,
|
||||
COALESCE(SUM(CASE WHEN sm.move_type = 'out' THEN sm.qty ELSE 0 END), 0) AS qty_out,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN sm.move_type = 'in' THEN sm.qty
|
||||
WHEN sm.move_type = 'out' THEN -sm.qty
|
||||
ELSE 0
|
||||
END
|
||||
), 0) AS qty_net
|
||||
FROM stock_lot sl
|
||||
LEFT JOIN stock_move sm ON sm.lot_id = sl.id
|
||||
GROUP BY sl.id, sl.product_id;
|
||||
|
||||
-- Operative Guardrail-Trigger:
|
||||
-- 1) keine negativen Chargensalden
|
||||
-- 2) wenn aktuelle Charge <= 0, auf offene Charge umschalten und neue offene Charge vorbereiten
|
||||
-- Hinweis: Die konkrete Trigger-Implementierung erfolgt in der Migrationsphase.
|
||||
|
||||
-- Standard Audit Trail (vorbereitet fuer spaetere Module)
|
||||
CREATE TABLE audit_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
entity_name TEXT NOT NULL,
|
||||
entity_id TEXT NOT NULL,
|
||||
action TEXT NOT NULL, -- insert | update | delete | import | allocate
|
||||
changed_by TEXT,
|
||||
changed_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
before_data JSONB,
|
||||
after_data JSONB,
|
||||
context JSONB
|
||||
);
|
||||
|
||||
CREATE INDEX idx_audit_log_entity ON audit_log(entity_name, entity_id);
|
||||
CREATE INDEX idx_audit_log_changed_at ON audit_log(changed_at);
|
||||
|
||||
-- Outbound-Webhook Queue (ERP -> n8n)
|
||||
CREATE TABLE outbound_webhook_event (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
event_type TEXT NOT NULL, -- order.imported | order.cancelled.partial | order.cancelled.full | lot.auto_switched
|
||||
event_key TEXT NOT NULL, -- idempotency key for consumers
|
||||
aggregate_type TEXT NOT NULL, -- sales_order | stock_lot | sales_order_line
|
||||
aggregate_id TEXT NOT NULL,
|
||||
payload JSONB NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending', -- pending | processing | sent | failed | dead_letter
|
||||
attempt_count INTEGER NOT NULL DEFAULT 0,
|
||||
next_attempt_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
last_attempt_at TIMESTAMP,
|
||||
last_error TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
sent_at TIMESTAMP,
|
||||
CONSTRAINT chk_outbound_webhook_status CHECK (status IN ('pending', 'processing', 'sent', 'failed', 'dead_letter'))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_outbound_webhook_event_key ON outbound_webhook_event(event_key);
|
||||
CREATE INDEX idx_outbound_webhook_status_next ON outbound_webhook_event(status, next_attempt_at);
|
||||
Reference in New Issue
Block a user