- sql/118-stock-velocity.sql: new stock_velocity (UPSERT per tx×vendor) and stock_velocity_events tables with TimescaleDB-compatible indexes - stock-velocity-analyzer.ts: computes sell-through from stock_observations time-series; detects sold/zulauf/data_gap events, trims top-10% outliers, predicts stockout date, assigns high/medium/low/insufficient confidence - scheduler.ts: analyze:stock:velocity job at 04:30/12:30/20:30 UTC - stock.ts: GET /api/stock/velocity (paginated, filterable by vendor/confidence/ stockout_days) + GET /api/stock/velocity/:id (per-product with event history) - First run: 208 products, 979 sell events, 2811 Zulauf events written
85 lines
4.6 KiB
SQL
85 lines
4.6 KiB
SQL
-- ══════════════════════════════════════════════════════════════════════════════
|
||
-- 118 — Stock Velocity & Sell-Through Analysis
|
||
--
|
||
-- Evaluates implied Abverkauf (sell-through) from time-series stock_observations:
|
||
-- • Negative stock delta → implied units sold (sell event)
|
||
-- • Positive stock delta after backorder → Zulauf (incoming replenishment)
|
||
-- • FS.com units_sold counter delta → high-confidence sell signal
|
||
--
|
||
-- Stores per-product velocity results in stock_velocity for API / dashboard use.
|
||
-- ══════════════════════════════════════════════════════════════════════════════
|
||
|
||
-- ── Main results table ────────────────────────────────────────────────────────
|
||
CREATE TABLE IF NOT EXISTS stock_velocity (
|
||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||
transceiver_id UUID NOT NULL REFERENCES transceivers(id) ON DELETE CASCADE,
|
||
vendor_id UUID NOT NULL REFERENCES vendors(id) ON DELETE CASCADE,
|
||
computed_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||
|
||
-- Observation window
|
||
window_start TIMESTAMPTZ NOT NULL,
|
||
window_end TIMESTAMPTZ NOT NULL,
|
||
obs_count INTEGER NOT NULL,
|
||
|
||
-- Sell-through metrics
|
||
avg_daily_sell_rate NUMERIC(12, 2), -- units/day (implied)
|
||
peak_daily_sell_rate NUMERIC(12, 2), -- highest single-interval rate
|
||
total_sell_events INTEGER DEFAULT 0,
|
||
total_units_sold_implied INTEGER DEFAULT 0,
|
||
|
||
-- FS.com direct counter (more reliable when available)
|
||
units_sold_counter_delta BIGINT, -- delta in FS.com units_sold between first/last obs
|
||
units_sold_daily_rate NUMERIC(12, 2), -- counter_delta / window_days
|
||
|
||
-- Zulauf (incoming stock / replenishment)
|
||
total_zulauf_events INTEGER DEFAULT 0,
|
||
total_units_zulauf INTEGER DEFAULT 0,
|
||
last_zulauf_at TIMESTAMPTZ,
|
||
next_expected_delivery DATE, -- backorder_estimated_date from latest obs
|
||
|
||
-- Current stock state (from latest observation)
|
||
current_qty INTEGER,
|
||
current_backorder_qty INTEGER,
|
||
current_price_net NUMERIC(10, 2),
|
||
|
||
-- Sell-through prediction
|
||
estimated_stockout_days NUMERIC(8, 1), -- NULL if no velocity or stock = 0
|
||
estimated_stockout_date DATE,
|
||
|
||
-- Signal quality
|
||
velocity_confidence TEXT CHECK (velocity_confidence IN ('high', 'medium', 'low', 'insufficient')),
|
||
-- high = ≥14 observations with meaningful deltas
|
||
-- medium = ≥5 observations
|
||
-- low = 2–4 observations
|
||
-- insufficient = only 1 observation or no change detected
|
||
|
||
UNIQUE (transceiver_id, vendor_id)
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_stock_velocity_vendor ON stock_velocity (vendor_id);
|
||
CREATE INDEX IF NOT EXISTS idx_stock_velocity_computed ON stock_velocity (computed_at);
|
||
CREATE INDEX IF NOT EXISTS idx_stock_velocity_stockout ON stock_velocity (estimated_stockout_date)
|
||
WHERE estimated_stockout_date IS NOT NULL;
|
||
CREATE INDEX IF NOT EXISTS idx_stock_velocity_confidence ON stock_velocity (velocity_confidence);
|
||
|
||
COMMENT ON TABLE stock_velocity IS
|
||
'Computed sell-through velocity per transceiver per vendor, derived from '
|
||
'time-series stock_observations. Refreshed by analyze:stock:velocity job.';
|
||
|
||
-- ── Sell event log (raw events for trend analysis) ────────────────────────────
|
||
CREATE TABLE IF NOT EXISTS stock_velocity_events (
|
||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||
transceiver_id UUID NOT NULL REFERENCES transceivers(id) ON DELETE CASCADE,
|
||
vendor_id UUID NOT NULL REFERENCES vendors(id) ON DELETE CASCADE,
|
||
event_at TIMESTAMPTZ NOT NULL,
|
||
event_type TEXT NOT NULL CHECK (event_type IN ('sold', 'zulauf', 'unchanged', 'data_gap')),
|
||
units_delta INTEGER, -- negative = sold, positive = arrived
|
||
daily_rate NUMERIC(10, 2), -- implied rate for this interval
|
||
qty_before INTEGER,
|
||
qty_after INTEGER,
|
||
hours_elapsed NUMERIC(8, 2)
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_velocity_events_tx ON stock_velocity_events (transceiver_id, vendor_id, event_at);
|
||
CREATE INDEX IF NOT EXISTS idx_velocity_events_type ON stock_velocity_events (event_type, event_at);
|