Move sales seasonality table to config schema and finish up standardizing scripts

This commit is contained in:
2025-01-28 23:57:09 -05:00
parent ebebd37f11
commit d56f1e1437
4 changed files with 74 additions and 44 deletions

View File

@@ -88,6 +88,16 @@ CREATE TABLE IF NOT EXISTS turnover_config (
UNIQUE KEY unique_category_vendor (category_id, vendor) UNIQUE KEY unique_category_vendor (category_id, vendor)
); );
-- Create table for sales seasonality factors
CREATE TABLE IF NOT EXISTS sales_seasonality (
month INT NOT NULL,
seasonality_factor DECIMAL(5,3) DEFAULT 0,
last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (month),
CHECK (month BETWEEN 1 AND 12),
CHECK (seasonality_factor BETWEEN -1.0 AND 1.0)
);
-- Insert default global thresholds if not exists -- Insert default global thresholds if not exists
INSERT INTO stock_thresholds (id, category_id, vendor, critical_days, reorder_days, overstock_days) INSERT INTO stock_thresholds (id, category_id, vendor, critical_days, reorder_days, overstock_days)
VALUES (1, NULL, NULL, 7, 14, 90) VALUES (1, NULL, NULL, 7, 14, 90)
@@ -129,6 +139,13 @@ ON DUPLICATE KEY UPDATE
calculation_period_days = VALUES(calculation_period_days), calculation_period_days = VALUES(calculation_period_days),
target_rate = VALUES(target_rate); target_rate = VALUES(target_rate);
-- Insert default seasonality factors (neutral)
INSERT INTO sales_seasonality (month, seasonality_factor)
VALUES
(1, 0), (2, 0), (3, 0), (4, 0), (5, 0), (6, 0),
(7, 0), (8, 0), (9, 0), (10, 0), (11, 0), (12, 0)
ON DUPLICATE KEY UPDATE last_updated = CURRENT_TIMESTAMP;
-- View to show thresholds with category names -- View to show thresholds with category names
CREATE OR REPLACE VIEW stock_thresholds_view AS CREATE OR REPLACE VIEW stock_thresholds_view AS
SELECT SELECT

View File

@@ -287,26 +287,6 @@ CREATE TABLE IF NOT EXISTS category_forecasts (
INDEX idx_category_forecast_last_calculated (last_calculated_at) INDEX idx_category_forecast_last_calculated (last_calculated_at)
); );
-- Create table for sales seasonality factors
CREATE TABLE IF NOT EXISTS sales_seasonality (
month INT NOT NULL,
seasonality_factor DECIMAL(5,3) DEFAULT 0,
last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (month),
CHECK (month BETWEEN 1 AND 12),
CHECK (seasonality_factor BETWEEN -1.0 AND 1.0)
);
-- Insert default seasonality factors (neutral)
INSERT INTO sales_seasonality (month, seasonality_factor)
VALUES
(1, 0), (2, 0), (3, 0), (4, 0), (5, 0), (6, 0),
(7, 0), (8, 0), (9, 0), (10, 0), (11, 0), (12, 0)
ON DUPLICATE KEY UPDATE last_updated = CURRENT_TIMESTAMP;
-- Re-enable foreign key checks
SET FOREIGN_KEY_CHECKS = 1;
-- Create view for inventory health -- Create view for inventory health
CREATE OR REPLACE VIEW inventory_health AS CREATE OR REPLACE VIEW inventory_health AS
WITH product_thresholds AS ( WITH product_thresholds AS (
@@ -428,3 +408,23 @@ LEFT JOIN
categories p ON c.parent_id = p.cat_id categories p ON c.parent_id = p.cat_id
LEFT JOIN LEFT JOIN
category_metrics cm ON c.cat_id = cm.category_id; category_metrics cm ON c.cat_id = cm.category_id;
-- Re-enable foreign key checks
SET FOREIGN_KEY_CHECKS = 1;
-- Create table for sales seasonality factors
CREATE TABLE IF NOT EXISTS sales_seasonality (
month INT NOT NULL,
seasonality_factor DECIMAL(5,3) DEFAULT 0,
last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (month),
CHECK (month BETWEEN 1 AND 12),
CHECK (seasonality_factor BETWEEN -1.0 AND 1.0)
);
-- Insert default seasonality factors (neutral)
INSERT INTO sales_seasonality (month, seasonality_factor)
VALUES
(1, 0), (2, 0), (3, 0), (4, 0), (5, 0), (6, 0),
(7, 0), (8, 0), (9, 0), (10, 0), (11, 0), (12, 0)
ON DUPLICATE KEY UPDATE last_updated = CURRENT_TIMESTAMP;

View File

@@ -40,6 +40,7 @@ const CONFIG_TABLES = [
'sales_velocity_config', 'sales_velocity_config',
'abc_classification_config', 'abc_classification_config',
'safety_stock_config', 'safety_stock_config',
'sales_seasonality',
'turnover_config' 'turnover_config'
]; ];

View File

@@ -32,7 +32,6 @@ const METRICS_TABLES = [
'product_metrics', 'product_metrics',
'product_time_aggregates', 'product_time_aggregates',
'sales_forecasts', 'sales_forecasts',
'sales_seasonality',
'temp_purchase_metrics', 'temp_purchase_metrics',
'temp_sales_metrics', 'temp_sales_metrics',
'vendor_metrics', //before vendor_details for foreign key 'vendor_metrics', //before vendor_details for foreign key
@@ -103,15 +102,15 @@ async function resetMetrics() {
// First verify current state // First verify current state
const [initialTables] = await connection.query(` const [initialTables] = await connection.query(`
SELECT table_name SELECT TABLE_NAME as name
FROM information_schema.tables FROM information_schema.tables
WHERE table_schema = DATABASE() WHERE TABLE_SCHEMA = DATABASE()
AND table_name IN (?) AND TABLE_NAME IN (?)
`, [METRICS_TABLES]); `, [METRICS_TABLES]);
outputProgress({ outputProgress({
operation: 'Initial state', operation: 'Initial state',
message: `Found ${initialTables.length} existing metrics tables: ${initialTables.map(t => t.table_name).join(', ')}` message: `Found ${initialTables.length} existing metrics tables: ${initialTables.map(t => t.name).join(', ')}`
}); });
// Disable foreign key checks at the start // Disable foreign key checks at the start
@@ -131,8 +130,8 @@ async function resetMetrics() {
const [checkDrop] = await connection.query(` const [checkDrop] = await connection.query(`
SELECT COUNT(*) as count SELECT COUNT(*) as count
FROM information_schema.tables FROM information_schema.tables
WHERE table_schema = DATABASE() WHERE TABLE_SCHEMA = DATABASE()
AND table_name = ? AND TABLE_NAME = ?
`, [table]); `, [table]);
if (checkDrop[0].count > 0) { if (checkDrop[0].count > 0) {
@@ -155,14 +154,14 @@ async function resetMetrics() {
// Verify all tables were dropped // Verify all tables were dropped
const [afterDrop] = await connection.query(` const [afterDrop] = await connection.query(`
SELECT table_name SELECT TABLE_NAME as name
FROM information_schema.tables FROM information_schema.tables
WHERE table_schema = DATABASE() WHERE TABLE_SCHEMA = DATABASE()
AND table_name IN (?) AND TABLE_NAME IN (?)
`, [METRICS_TABLES]); `, [METRICS_TABLES]);
if (afterDrop.length > 0) { if (afterDrop.length > 0) {
throw new Error(`Failed to drop all tables. Remaining tables: ${afterDrop.map(t => t.table_name).join(', ')}`); throw new Error(`Failed to drop all tables. Remaining tables: ${afterDrop.map(t => t.name).join(', ')}`);
} }
// Read metrics schema // Read metrics schema
@@ -188,7 +187,7 @@ async function resetMetrics() {
for (let i = 0; i < statements.length; i++) { for (let i = 0; i < statements.length; i++) {
const stmt = statements[i]; const stmt = statements[i];
try { try {
const [result] = await connection.query(stmt); await connection.query(stmt);
// Check for warnings // Check for warnings
const [warnings] = await connection.query('SHOW WARNINGS'); const [warnings] = await connection.query('SHOW WARNINGS');
@@ -208,15 +207,20 @@ async function resetMetrics() {
const tableName = stmt.match(/create\s+table\s+(?:if\s+not\s+exists\s+)?`?(\w+)`?/i)?.[1]; const tableName = stmt.match(/create\s+table\s+(?:if\s+not\s+exists\s+)?`?(\w+)`?/i)?.[1];
if (tableName) { if (tableName) {
const [checkCreate] = await connection.query(` const [checkCreate] = await connection.query(`
SELECT COUNT(*) as count SELECT TABLE_NAME as name, CREATE_TIME as created
FROM information_schema.tables FROM information_schema.tables
WHERE table_schema = DATABASE() WHERE TABLE_SCHEMA = DATABASE()
AND table_name = ? AND TABLE_NAME = ?
`, [tableName]); `, [tableName]);
if (checkCreate[0].count === 0) { if (checkCreate.length === 0) {
throw new Error(`Failed to create table ${tableName} - table does not exist after CREATE statement`); throw new Error(`Failed to create table ${tableName} - table does not exist after CREATE statement`);
} }
outputProgress({
operation: 'Table created',
message: `Successfully created table: ${tableName} at ${checkCreate[0].created}`
});
} }
} }
@@ -225,8 +229,7 @@ async function resetMetrics() {
message: { message: {
statement: i + 1, statement: i + 1,
total: statements.length, total: statements.length,
preview: stmt.substring(0, 100) + (stmt.length > 100 ? '...' : ''), preview: stmt.substring(0, 100) + (stmt.length > 100 ? '...' : '')
affectedRows: result.affectedRows
} }
}); });
} catch (sqlError) { } catch (sqlError) {
@@ -255,23 +258,32 @@ async function resetMetrics() {
}); });
const [metricsTablesResult] = await connection.query(` const [metricsTablesResult] = await connection.query(`
SELECT table_name, table_rows, create_time SELECT
TABLE_NAME as name,
TABLE_ROWS as \`rows\`,
CREATE_TIME as created
FROM information_schema.tables FROM information_schema.tables
WHERE table_schema = DATABASE() WHERE TABLE_SCHEMA = DATABASE()
AND table_name IN (?) AND TABLE_NAME IN (?)
`, [METRICS_TABLES]); `, [METRICS_TABLES]);
outputProgress({ outputProgress({
operation: 'Tables found', operation: 'Tables found',
message: `Found ${metricsTablesResult.length} tables: ${metricsTablesResult.map(t => message: `Found ${metricsTablesResult.length} tables: ${metricsTablesResult.map(t =>
`${t.table_name} (created: ${t.create_time})` `${t.name} (created: ${t.created})`
).join(', ')}` ).join(', ')}`
}); });
const existingMetricsTables = metricsTablesResult.map(t => t.table_name); const existingMetricsTables = metricsTablesResult.map(t => t.name);
const missingMetricsTables = METRICS_TABLES.filter(t => !existingMetricsTables.includes(t)); const missingMetricsTables = METRICS_TABLES.filter(t => !existingMetricsTables.includes(t));
if (missingMetricsTables.length > 0) { if (missingMetricsTables.length > 0) {
// Do one final check of the actual tables
const [finalCheck] = await connection.query('SHOW TABLES');
outputProgress({
operation: 'Final table check',
message: `All database tables: ${finalCheck.map(t => Object.values(t)[0]).join(', ')}`
});
throw new Error(`Failed to create metrics tables: ${missingMetricsTables.join(', ')}`); throw new Error(`Failed to create metrics tables: ${missingMetricsTables.join(', ')}`);
} }