Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Tip: Each challenge folder includes its own README with the question, expected o
| Revenue analytics | 005, 007, 011, 013, 014, 030 |
| Date spines and rolling windows | 008, 016, 028 |
| Product and customer behavior | 012, 015, 017, 018, 021, 022, 023, 024, 025, 026 |
| Data engineering SQL patterns | 035, 036, 037 |
| Data engineering SQL patterns | 035, 036, 037, 038, 039, 040 |

For ordered study plans, see [Learning paths](docs/learning-paths.md).

Expand Down Expand Up @@ -72,6 +72,9 @@ For ordered study plans, see [Learning paths](docs/learning-paths.md).
35. Subscription renewals + missed renewals + winback (30 days) — [challenges/035_subscription_renewal_winback](challenges/035_subscription_renewal_winback)
36. SCD Type 2 customer dimension (history table) — [challenges/036_scd2_customer_dimension](challenges/036_scd2_customer_dimension)
37. Incremental fact upsert (staging → fact, insert + update) — [challenges/037_incremental_fact_upsert](challenges/037_incremental_fact_upsert)
38. Inventory stockout risk (velocity + lead time scoring) — [challenges/038_inventory_stockout_risk](challenges/038_inventory_stockout_risk)
39. Trial to paid conversion (cohorts + conversion windows) — [challenges/039_trial_to_paid_conversion](challenges/039_trial_to_paid_conversion)
40. Revenue leakage audit (invoice + payment reconciliation) — [challenges/040_revenue_leakage_audit](challenges/040_revenue_leakage_audit)

## How to use
You can copy/paste the SQL into SQLite, Postgres, or any SQL runner with minor tweaks.
Expand Down
37 changes: 37 additions & 0 deletions challenges/038_inventory_stockout_risk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Challenge 038: Inventory stockout risk

Goal: flag products that may stock out based on recent sales velocity, current inventory, lead time, and the next open purchase order.

Use an as-of date of `2024-05-15`.

Rules:
- `net_available_units` = on hand units minus reserved units
- `recent_7d_units` = units sold from `2024-05-09` through `2024-05-15`
- `avg_daily_units` = `recent_7d_units / 7`
- `days_of_cover` = `net_available_units / avg_daily_units`
- Risk is `critical` when days of cover is at or below lead time, or net availability is at or below safety stock
- Risk is `watch` when days of cover is within 7 days after lead time
- Products with no recent demand should be labeled `no_recent_demand`

## Files
- `schema.sql` creates product, inventory, sales, and purchase order tables
- `solution.sql` computes stockout risk per product

## Run (SQLite)

Windows CMD:
```bat
type challenges\038_inventory_stockout_risk\schema.sql challenges\038_inventory_stockout_risk\solution.sql | sqlite3
```

## Output

Columns:
- product_id
- sku
- net_available_units
- recent_7d_units
- avg_daily_units
- days_of_cover
- next_open_po_date
- risk_level
59 changes: 59 additions & 0 deletions challenges/038_inventory_stockout_risk/expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"challenge": "038_inventory_stockout_risk",
"result_sets": [
{
"columns": [
"product_id",
"sku",
"net_available_units",
"recent_7d_units",
"avg_daily_units",
"days_of_cover",
"next_open_po_date",
"risk_level"
],
"rows": [
[
101,
"WIDGET-A",
60,
70,
10.0,
6.0,
"2024-05-22",
"critical"
],
[
102,
"FILTER-B",
120,
42,
6.0,
20.0,
"2024-05-25",
"healthy"
],
[
103,
"PUMP-C",
50,
21,
3.0,
16.67,
"2024-06-01",
"watch"
],
[
104,
"SENSOR-D",
8,
0,
0.0,
null,
"2024-05-25",
"no_recent_demand"
]
]
}
]
}
88 changes: 88 additions & 0 deletions challenges/038_inventory_stockout_risk/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
-- Challenge 038: Inventory stockout risk
--
-- Scenario:
-- A warehouse team wants to find products that may stock out before replenishment.
-- Combine recent sales velocity, current available inventory, supplier lead time,
-- safety stock, and the next open purchase order.
--
-- Use as-of date: 2024-05-15
-- Recent sales window: 2024-05-09 through 2024-05-15
--
-- Output per product:
-- product_id | sku | net_available_units | recent_7d_units | avg_daily_units | days_of_cover | next_open_po_date | risk_level

DROP TABLE IF EXISTS purchase_orders;
DROP TABLE IF EXISTS sales;
DROP TABLE IF EXISTS inventory;
DROP TABLE IF EXISTS products;

CREATE TABLE products (
product_id INTEGER PRIMARY KEY,
sku TEXT NOT NULL,
category TEXT NOT NULL,
lead_time_days INTEGER NOT NULL,
safety_stock_units INTEGER NOT NULL
);

CREATE TABLE inventory (
product_id INTEGER PRIMARY KEY,
on_hand_units INTEGER NOT NULL,
reserved_units INTEGER NOT NULL
);

CREATE TABLE sales (
sale_id INTEGER PRIMARY KEY,
product_id INTEGER NOT NULL,
sold_date TEXT NOT NULL,
units_sold INTEGER NOT NULL
);

CREATE TABLE purchase_orders (
po_id INTEGER PRIMARY KEY,
product_id INTEGER NOT NULL,
due_date TEXT NOT NULL,
units_ordered INTEGER NOT NULL,
status TEXT NOT NULL
);

INSERT INTO products (product_id, sku, category, lead_time_days, safety_stock_units) VALUES
(101, 'WIDGET-A', 'widgets', 10, 20),
(102, 'FILTER-B', 'filters', 6, 10),
(103, 'PUMP-C', 'pumps', 14, 15),
(104, 'SENSOR-D', 'sensors', 7, 5);

INSERT INTO inventory (product_id, on_hand_units, reserved_units) VALUES
(101, 72, 12),
(102, 130, 10),
(103, 65, 15),
(104, 8, 0);

INSERT INTO sales (sale_id, product_id, sold_date, units_sold) VALUES
(1, 101, '2024-05-02', 30),
(2, 101, '2024-05-09', 8),
(3, 101, '2024-05-10', 10),
(4, 101, '2024-05-11', 12),
(5, 101, '2024-05-12', 9),
(6, 101, '2024-05-13', 11),
(7, 101, '2024-05-14', 10),
(8, 101, '2024-05-15', 10),
(9, 102, '2024-05-09', 4),
(10, 102, '2024-05-10', 8),
(11, 102, '2024-05-12', 6),
(12, 102, '2024-05-14', 10),
(13, 102, '2024-05-15', 14),
(14, 103, '2024-05-09', 2),
(15, 103, '2024-05-10', 3),
(16, 103, '2024-05-11', 4),
(17, 103, '2024-05-12', 3),
(18, 103, '2024-05-14', 5),
(19, 103, '2024-05-15', 4),
(20, 104, '2024-05-07', 5);

INSERT INTO purchase_orders (po_id, product_id, due_date, units_ordered, status) VALUES
(1, 101, '2024-05-22', 80, 'open'),
(2, 101, '2024-06-05', 120, 'open'),
(3, 102, '2024-05-25', 60, 'open'),
(4, 102, '2024-05-18', 30, 'closed'),
(5, 103, '2024-06-01', 40, 'open'),
(6, 104, '2024-05-25', 20, 'open');
77 changes: 77 additions & 0 deletions challenges/038_inventory_stockout_risk/solution.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
-- Output columns:
-- product_id | sku | net_available_units | recent_7d_units | avg_daily_units | days_of_cover | next_open_po_date | risk_level

WITH params AS (
SELECT date('2024-05-15') AS as_of_date
),
recent_sales AS (
SELECT
p.product_id,
COALESCE(SUM(
CASE
WHEN date(s.sold_date) BETWEEN date(params.as_of_date, '-6 day') AND params.as_of_date
THEN s.units_sold
ELSE 0
END
), 0) AS recent_7d_units
FROM products p
CROSS JOIN params
LEFT JOIN sales s ON s.product_id = p.product_id
GROUP BY p.product_id
),
next_purchase_order AS (
SELECT
product_id,
MIN(date(due_date)) AS next_open_po_date
FROM purchase_orders
WHERE status = 'open'
GROUP BY product_id
),
metrics AS (
SELECT
p.product_id,
p.sku,
p.lead_time_days,
p.safety_stock_units,
i.on_hand_units - i.reserved_units AS net_available_units,
rs.recent_7d_units,
ROUND(rs.recent_7d_units / 7.0, 2) AS avg_daily_units,
CASE
WHEN rs.recent_7d_units = 0 THEN NULL
ELSE ROUND((i.on_hand_units - i.reserved_units) * 7.0 / rs.recent_7d_units, 2)
END AS days_of_cover,
npo.next_open_po_date
FROM products p
JOIN inventory i ON i.product_id = p.product_id
JOIN recent_sales rs ON rs.product_id = p.product_id
LEFT JOIN next_purchase_order npo ON npo.product_id = p.product_id
),
scored AS (
SELECT
product_id,
sku,
net_available_units,
recent_7d_units,
avg_daily_units,
days_of_cover,
next_open_po_date,
CASE
WHEN recent_7d_units = 0 THEN 'no_recent_demand'
WHEN net_available_units <= safety_stock_units THEN 'critical'
WHEN days_of_cover <= lead_time_days THEN 'critical'
WHEN days_of_cover <= lead_time_days + 7 THEN 'watch'
ELSE 'healthy'
END AS risk_level
FROM metrics
)
SELECT
product_id,
sku,
net_available_units,
recent_7d_units,
avg_daily_units,
days_of_cover,
next_open_po_date,
risk_level
FROM scored
ORDER BY product_id;
34 changes: 34 additions & 0 deletions challenges/039_trial_to_paid_conversion/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Challenge 039: Trial to paid conversion

Goal: measure trial conversion by signup cohort and trial plan using 14-day and 30-day conversion windows.

For each signup cohort month and trial plan, calculate:
- number of trials
- users who converted to paid within 14 days
- users who converted to paid within 30 days
- 30-day conversion rate
- average days to paid conversion within 30 days
- new monthly recurring revenue from 30-day conversions

## Files
- `schema.sql` creates trial signup and paid subscription tables
- `solution.sql` finds each user's first paid subscription and rolls conversion metrics up by cohort

## Run (SQLite)

Windows CMD:
```bat
type challenges\039_trial_to_paid_conversion\schema.sql challenges\039_trial_to_paid_conversion\solution.sql | sqlite3
```

## Output

Columns:
- cohort_month
- trial_plan
- trials
- converted_14d
- converted_30d
- conversion_30d_pct
- avg_days_to_paid_30d
- new_mrr_30d
Loading
Loading