Queries · q269 · Head of Merchant Success · week 2, night 2
“In the months before a churned merchant's last order, did sales, shopper repeat-buying, refund rates, or support ticket volume already show warning signs?”
Fictional people at a fictional company, each played by a language-model agent. Who they are and what they are told
Week 2; shared with Chloé Marchetti, Head of Growth Marketing and Daniel Achterberg, VP Customer Experience
Churn has become a board topic, for merchants and for their shoppers. A cross-functional task force wants to understand which experiences precede leaving, and which early signals would let us act before it happens.
The manager, at the start of week 2
Subject: Next week's priorities — QBR follow-ups + the churn task force
Hi Arjun,
Really useful memo — thank you. A few things jumped out that I want to understand better, plus a new cross-functional ask that's going to need a slot or two this week.
What surprised me / needs explaining first:
My questions for this week:
New context — the churn task force: Churn is now a board topic, both merchant churn and end-shopper churn, and I'm leading this jointly with Chloé Marchetti and Daniel Achterberg. The deliverable the three of us owe the board is a shortlist of early-warning signals — things we can observe weeks or months before a merchant (or a merchant's shoppers) leaves — and a recommendation on which of those signals is reliable enough to act on. It feeds directly into two decisions: whether Merchant Success should get a proactive outreach trigger for at-risk accounts, and whether Growth Marketing and CX get an equivalent retention trigger for end-shoppers. So anything you can tell me this week about what preceded past departures — on the merchant side especially, since that's my piece — is directly useful input, even if it's partial. I'll compare notes with Chloé's and Daniel's analysts before we draft anything for the board.
Given the slot limits, prioritize in this order: Castell explanation, then the task-force churn pattern, then support/CSAT if a slot remains.
Thanks, Samuel
Subject: CX signals for the churn task force
Arjun,
A few things I'd want the task force to look at from the shopper/support side:
If we can pin down even one or two of these as reliable early signals, it changes what Samuel's team and I should be monitoring week to week rather than finding out after the fact.
Daniel
Subject: Churn task force — what I'd like us to look at from Growth
Hi Arjun,
Glad this is getting cross-functional attention. From the marketing side, here's what I think matters:
Happy to jump on a call if useful — mainly want to make sure whatever "early warning" signals come out of this include the marketing-touch side, not just support or payment signals.
Best, Chloé
The analyst · week 2, night 2, request 1 of 1
Isolating a possible length issue - submitting one longer report alone.
The engineer · 1 dry run on the empty schema, then the submission
WITH churned_tenants AS ( SELECT id AS tenant_id, name, industry FROM tenants WHERE status = 'churned' ), anchor_dates AS ( SELECT ct.tenant_id, MAX(o.placed_at) AS anchor_at FROM churned_tenants ct JOIN orders o ON o.tenant_id = ct.tenant_id AND o.status <> 'cancelled' GROUP BY ct.tenant_id ), windows AS ( SELECT tenant_id, anchor_at - interval '90 days' AS final_start, anchor_at AS final_end, anchor_at - interval '180 days' AS prior_start, anchor_at - interval '91 days' AS prior_end FROM anchor_dates ), orders_with_fx AS ( SELECT o.tenant_id, o.id AS order_id, o.user_id, o.placed_at, o.currency_code, o.grand_total, er.rate FROM orders o JOIN churned_tenants ct ON ct.tenant_id = o.tenant_id LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of = o.placed_at::date WHERE o.status <> 'cancelled' ), orders_classified AS ( SELECT ow.tenant_id, ow.order_id, ow.user_id, CASE WHEN ow.currency_code = 'EUR' THEN ow.grand_total ELSE ow.grand_total / ow.rate END AS revenue_eur, CASE WHEN ow.placed_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN ow.placed_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM orders_with_fx ow JOIN windows w ON w.tenant_id = ow.tenant_id ), order_metrics AS ( SELECT tenant_id, COUNT(*) FILTER (WHERE period = 'final') AS orders_final, COUNT(*) FILTER (WHERE period = 'prior') AS orders_prior, SUM(revenue_eur) FILTER (WHERE period = 'final') AS revenue_final, SUM(revenue_eur) FILTER (WHERE period = 'prior') AS revenue_prior FROM orders_classified GROUP BY tenant_id ), user_period_counts AS ( SELECT tenant_id, user_id, period, COUNT(*) AS order_cnt FROM orders_classified WHERE period IS NOT NULL GROUP BY tenant_id, user_id, period ), repeat_metrics AS ( SELECT tenant_id, period, COUNT(*) AS customers_total, COUNT(*) FILTER (WHERE order_cnt >= 2) AS customers_repeat FROM user_period_counts GROUP BY tenant_id, period ), repeat_pivot AS ( SELECT tenant_id, MAX(CASE WHEN period = 'final' THEN customers_repeat::numeric / NULLIF(customers_total, 0) END) AS repeat_rate_final, MAX(CASE WHEN period = 'prior' THEN customers_repeat::numeric / NULLIF(customers_total, 0) END) AS repeat_rate_prior FROM repeat_metrics GROUP BY tenant_id ), payments_with_fx AS ( SELECT p.tenant_id, p.captured_at, p.currency_code, p.amount, er.rate FROM payments p JOIN churned_tenants ct ON ct.tenant_id = p.tenant_id LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = p.currency_code AND er.as_of = p.captured_at::date WHERE p.captured_at IS NOT NULL ), payments_classified AS ( SELECT pw.tenant_id, CASE WHEN pw.currency_code = 'EUR' THEN pw.amount ELSE pw.amount / pw.rate END AS amount_eur, CASE WHEN pw.captured_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN pw.captured_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM payments_with_fx pw JOIN windows w ON w.tenant_id = pw.tenant_id ), payment_metrics AS ( SELECT tenant_id, SUM(amount_eur) FILTER (WHERE period = 'final') AS payments_final, SUM(amount_eur) FILTER (WHERE period = 'prior') AS payments_prior FROM payments_classified GROUP BY tenant_id ), refunds_with_fx AS ( SELECT r.tenant_id, r.processed_at, r.currency_code, r.amount, er.rate FROM payment_refunds r JOIN churned_tenants ct ON ct.tenant_id = r.tenant_id LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = r.currency_code AND er.as_of = r.processed_at::date WHERE r.status = 'succeeded' AND r.processed_at IS NOT NULL ), refunds_classified AS ( SELECT rw.tenant_id, CASE WHEN rw.currency_code = 'EUR' THEN rw.amount ELSE rw.amount / rw.rate END AS amount_eur, CASE WHEN rw.processed_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN rw.processed_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM refunds_with_fx rw JOIN windows w ON w.tenant_id = rw.tenant_id ), refund_metrics AS ( SELECT tenant_id, SUM(amount_eur) FILTER (WHERE period = 'final') AS refunds_final, SUM(amount_eur) FILTER (WHERE period = 'prior') AS refunds_prior FROM refunds_classified GROUP BY tenant_id ), tickets_classified AS ( SELECT t.tenant_id, CASE WHEN t.created_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN t.created_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM tickets t JOIN windows w ON w.tenant_id = t.tenant_id ), ticket_metrics AS ( SELECT tenant_id, COUNT(*) FILTER (WHERE period = 'final') AS tickets_final, COUNT(*) FILTER (WHERE period = 'prior') AS tickets_prior FROM tickets_classified GROUP BY tenant_id ), report AS ( SELECT ct.tenant_id, ct.name AS merchant_name, ct.industry, COALESCE(om.orders_final, 0) AS orders_final_90d, COALESCE(om.orders_prior, 0) AS orders_prior_90d, CASE WHEN om.revenue_prior IS NULL OR om.revenue_prior = 0 THEN NULL ELSE ROUND(((om.revenue_final - om.revenue_prior) / om.revenue_prior) * 100, 1) END AS revenue_growth_pct, ROUND(rp.repeat_rate_final * 100, 1) AS repeat_rate_final_pct, ROUND(rp.repeat_rate_prior * 100, 1) AS repeat_rate_prior_pct, CASE WHEN pm.payments_final IS NULL OR pm.payments_final = 0 THEN NULL ELSE ROUND(COALESCE(rfm.refunds_final, 0) / pm.payments_final * 100, 1) END AS refund_rate_final_pct, CASE WHEN pm.payments_prior IS NULL OR pm.payments_prior = 0 THEN NULL ELSE ROUND(COALESCE(rfm.refunds_prior, 0) / pm.payments_prior * 100, 1) END AS refund_rate_prior_pct, COALESCE(tm.tickets_final, 0) AS tickets_final_90d, COALESCE(tm.tickets_prior, 0) AS tickets_prior_90d, COUNT(*) OVER () AS total_churned_merchants FROM churned_tenants ct LEFT JOIN order_metrics om ON om.tenant_id = ct.tenant_id LEFT JOIN repeat_pivot rp ON rp.tenant_id = ct.tenant_id LEFT JOIN payment_metrics pm ON pm.tenant_id = ct.tenant_id LEFT JOIN refund_metrics rfm ON rfm.tenant_id = ct.tenant_id LEFT JOIN ticket_metrics tm ON tm.tenant_id = ct.tenant_id ) SELECT merchant_name, industry, orders_final_90d, orders_prior_90d, revenue_growth_pct, repeat_rate_final_pct, repeat_rate_prior_pct, refund_rate_final_pct, refund_rate_prior_pct, tickets_final_90d, tickets_prior_90d, total_churned_merchants FROM report ORDER BY orders_final_90d ASC, tenant_id ASC LIMIT 50
“Query compiles and produces the expected columns. Submitting now.”
“The report has been submitted to tonight's batch.
Summary of approach:
- Anchored each churned tenant's analysis window on their own last non-cancelled order date, then built final (0-90 days before) and prior (91-180 days before) windows.
- Converted all money (order revenue, payments, refunds) to EUR using each row's own currency and the exchange rate as of that row's date.
- Repeat rate = share of distinct customers with ≥2 orders in the window.
- Refund rate = succeeded refunds (EUR) ÷ captured payments (EUR) within the same window.
- Tickets counted by created_at falling in each window.
- Added a total_churned_merchants column so the fixed 50-row cap is traceable to the true population size, as the handbook asks for.”
As Arjun Patel saw it, from the small dataset the agents worked on
merchant_name | industry | orders_final_90d | orders_prior_90d | revenue_growth_pct | repeat_rate_final_pct | repeat_rate_prior_pct | refund_rate_final_pct | refund_rate_prior_pct | tickets_final_90d | tickets_prior_90d | total_churned_merchants -------------------+-----------------------+------------------+------------------+--------------------+-----------------------+-----------------------+-----------------------+-----------------------+-------------------+-------------------+------------------------ Paddock & Pine | Apparel & Footwear | 1 | 0 | NULL | 0.0 | NULL | NULL | NULL | 0 | 0 | 9 Petal & Pestle | Beauty | 3 | 4 | 41.4 | 0.0 | 50.0 | 0.0 | 0.0 | 0 | 0 | 9 Klara Beauty | Beauty | 3 | 0 | NULL | 100.0 | NULL | 0.0 | NULL | 0 | 0 | 9 Dewdrop Studio | Beauty | 3 | 0 | NULL | 100.0 | NULL | 0.0 | NULL | 0 | 0 | 9 Aurum & Ash | Jewelry & Accessories | 4 | 2 | 280.1 | 33.3 | 100.0 | 0.0 | 0.0 | 0 | 0 | 9 Halden Boots | Apparel & Footwear | 5 | 2 | -57.6 | 100.0 | 0.0 | 0.0 | 0.0 | 1 | 0 | 9 Marginalia | Books & Stationery | 5 | 1 | 117.2 | 33.3 | 0.0 | 0.0 | 0.0 | 0 | 1 | 9 Northwire Audio | Consumer Electronics | 7 | 0 | NULL | 100.0 | NULL | 21.3 | NULL | 0 | 0 | 9 Saltmarsh Knitwear | Apparel & Footwear | 11 | 3 | 73.5 | 25.0 | 0.0 | 28.8 | 13.1 | 0 | 1 | 9 (9 rows)
The memo and the queries that followed; links found by title, so inferred
Later queries that build on this report:
Changed only where the benchmark needs the same result on every system
WITH churned_tenants AS ( SELECT id AS tenant_id, name, industry FROM tenants WHERE status = 'churned'),anchor_dates AS ( SELECT ct.tenant_id, MAX(o.placed_at) AS anchor_at SELECT ct.tenant_id AS tenant_id, MAX(o.placed_at) AS anchor_at FROM churned_tenants ct JOIN orders o ON o.tenant_id = ct.tenant_id AND o.status <> 'cancelled' GROUP BY ct.tenant_id),windows AS ( SELECT tenant_id, anchor_at - interval '90 days' AS final_start, anchor_at AS final_end, anchor_at - interval '180 days' AS prior_start, anchor_at - interval '91 days' AS prior_end FROM anchor_dates),orders_with_fx AS ( SELECT o.tenant_id, o.id AS order_id, o.user_id, o.placed_at, o.currency_code, o.grand_total, er.rate o.tenant_id AS tenant_id, o.id AS order_id, o.user_id AS user_id, o.placed_at AS placed_at, o.currency_code AS currency_code, o.grand_total AS grand_total, er.rate AS rate FROM orders o JOIN churned_tenants ct ON ct.tenant_id = o.tenant_id LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of = o.placed_at::date ON er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of = CAST(o.placed_at AS date) WHERE o.status <> 'cancelled'),orders_classified AS ( SELECT ow.tenant_id, ow.order_id, ow.user_id, CASE WHEN ow.currency_code = 'EUR' THEN ow.grand_total ELSE ow.grand_total / ow.rate END AS revenue_eur, ow.tenant_id AS tenant_id, ow.order_id AS order_id, ow.user_id AS user_id, CASE WHEN ow.currency_code = 'EUR' THEN ow.grand_total ELSE CAST(ow.grand_total / ow.rate AS NUMERIC(18,6)) END AS revenue_eur, CASE WHEN ow.placed_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN ow.placed_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM orders_with_fx ow JOIN windows w ON w.tenant_id = ow.tenant_id),order_metrics AS ( SELECT tenant_id, COUNT(*) FILTER (WHERE period = 'final') AS orders_final, COUNT(*) FILTER (WHERE period = 'prior') AS orders_prior, SUM(revenue_eur) FILTER (WHERE period = 'final') AS revenue_final, SUM(revenue_eur) FILTER (WHERE period = 'prior') AS revenue_prior FROM orders_classified GROUP BY tenant_id),user_period_counts AS ( SELECT tenant_id, user_id, period, COUNT(*) AS order_cnt FROM orders_classified WHERE period IS NOT NULL GROUP BY tenant_id, user_id, period),repeat_metrics AS ( SELECT tenant_id, period, COUNT(*) AS customers_total, COUNT(*) FILTER (WHERE order_cnt >= 2) AS customers_repeat FROM user_period_counts GROUP BY tenant_id, period),repeat_pivot AS ( SELECT tenant_id, MAX(CASE WHEN period = 'final' THEN customers_repeat::numeric / NULLIF(customers_total, 0) END) AS repeat_rate_final, MAX(CASE WHEN period = 'prior' THEN customers_repeat::numeric / NULLIF(customers_total, 0) END) AS repeat_rate_prior MAX(CASE WHEN period = 'final' THEN CAST(customers_repeat AS numeric(18,6)) / NULLIF(customers_total, 0) END) AS repeat_rate_final, MAX(CASE WHEN period = 'prior' THEN CAST(customers_repeat AS numeric(18,6)) / NULLIF(customers_total, 0) END) AS repeat_rate_prior FROM repeat_metrics GROUP BY tenant_id),payments_with_fx AS ( SELECT p.tenant_id, p.captured_at, p.currency_code, p.amount, er.rate p.tenant_id AS tenant_id, p.captured_at AS captured_at, p.currency_code AS currency_code, p.amount AS amount, er.rate AS rate FROM payments p JOIN churned_tenants ct ON ct.tenant_id = p.tenant_id LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = p.currency_code AND er.as_of = p.captured_at::date ON er.base_code = 'EUR' AND er.quote_code = p.currency_code AND er.as_of = CAST(p.captured_at AS date) WHERE p.captured_at IS NOT NULL),payments_classified AS ( SELECT pw.tenant_id, CASE WHEN pw.currency_code = 'EUR' THEN pw.amount ELSE pw.amount / pw.rate END AS amount_eur, pw.tenant_id AS tenant_id, CASE WHEN pw.currency_code = 'EUR' THEN pw.amount ELSE CAST(pw.amount / pw.rate AS NUMERIC(18,6)) END AS amount_eur, CASE WHEN pw.captured_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN pw.captured_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM payments_with_fx pw JOIN windows w ON w.tenant_id = pw.tenant_id),payment_metrics AS ( SELECT tenant_id, SUM(amount_eur) FILTER (WHERE period = 'final') AS payments_final, SUM(amount_eur) FILTER (WHERE period = 'prior') AS payments_prior FROM payments_classified GROUP BY tenant_id),refunds_with_fx AS ( SELECT r.tenant_id, r.processed_at, r.currency_code, r.amount, er.rate r.tenant_id AS tenant_id, r.processed_at AS processed_at, r.currency_code AS currency_code, r.amount AS amount, er.rate AS rate FROM payment_refunds r JOIN churned_tenants ct ON ct.tenant_id = r.tenant_id LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = r.currency_code AND er.as_of = r.processed_at::date ON er.base_code = 'EUR' AND er.quote_code = r.currency_code AND er.as_of = CAST(r.processed_at AS date) WHERE r.status = 'succeeded' AND r.processed_at IS NOT NULL),refunds_classified AS ( SELECT rw.tenant_id, CASE WHEN rw.currency_code = 'EUR' THEN rw.amount ELSE rw.amount / rw.rate END AS amount_eur, rw.tenant_id AS tenant_id, CASE WHEN rw.currency_code = 'EUR' THEN rw.amount ELSE CAST(rw.amount / rw.rate AS NUMERIC(18,6)) END AS amount_eur, CASE WHEN rw.processed_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN rw.processed_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM refunds_with_fx rw JOIN windows w ON w.tenant_id = rw.tenant_id),refund_metrics AS ( SELECT tenant_id, SUM(amount_eur) FILTER (WHERE period = 'final') AS refunds_final, SUM(amount_eur) FILTER (WHERE period = 'prior') AS refunds_prior FROM refunds_classified GROUP BY tenant_id),tickets_classified AS ( SELECT t.tenant_id, t.tenant_id AS tenant_id, CASE WHEN t.created_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN t.created_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM tickets t JOIN windows w ON w.tenant_id = t.tenant_id),ticket_metrics AS ( SELECT tenant_id, COUNT(*) FILTER (WHERE period = 'final') AS tickets_final, COUNT(*) FILTER (WHERE period = 'prior') AS tickets_prior FROM tickets_classified GROUP BY tenant_id),report AS ( SELECT ct.tenant_id, ct.tenant_id AS tenant_id, ct.name AS merchant_name, ct.industry, ct.industry AS industry, COALESCE(om.orders_final, 0) AS orders_final_90d, COALESCE(om.orders_prior, 0) AS orders_prior_90d, CASE WHEN om.revenue_prior IS NULL OR om.revenue_prior = 0 THEN NULL ELSE ROUND(((om.revenue_final - om.revenue_prior) / om.revenue_prior) * 100, 1) ELSE ((om.revenue_final - om.revenue_prior) / om.revenue_prior) * 100 END AS revenue_growth_pct, ROUND(rp.repeat_rate_final * 100, 1) AS repeat_rate_final_pct, ROUND(rp.repeat_rate_prior * 100, 1) AS repeat_rate_prior_pct, rp.repeat_rate_final * 100 AS repeat_rate_final_pct, rp.repeat_rate_prior * 100 AS repeat_rate_prior_pct, CASE WHEN pm.payments_final IS NULL OR pm.payments_final = 0 THEN NULL ELSE ROUND(COALESCE(rfm.refunds_final, 0) / pm.payments_final * 100, 1) ELSE COALESCE(rfm.refunds_final, 0) / pm.payments_final * 100 END AS refund_rate_final_pct, CASE WHEN pm.payments_prior IS NULL OR pm.payments_prior = 0 THEN NULL ELSE ROUND(COALESCE(rfm.refunds_prior, 0) / pm.payments_prior * 100, 1) ELSE COALESCE(rfm.refunds_prior, 0) / pm.payments_prior * 100 END AS refund_rate_prior_pct, COALESCE(tm.tickets_final, 0) AS tickets_final_90d, COALESCE(tm.tickets_prior, 0) AS tickets_prior_90d, COUNT(*) OVER () AS total_churned_merchants FROM churned_tenants ct LEFT JOIN order_metrics om ON om.tenant_id = ct.tenant_id LEFT JOIN repeat_pivot rp ON rp.tenant_id = ct.tenant_id LEFT JOIN payment_metrics pm ON pm.tenant_id = ct.tenant_id LEFT JOIN refund_metrics rfm ON rfm.tenant_id = ct.tenant_id LEFT JOIN ticket_metrics tm ON tm.tenant_id = ct.tenant_id)SELECT merchant_name, industry, orders_final_90d, orders_prior_90d, revenue_growth_pct, repeat_rate_final_pct, repeat_rate_prior_pct, refund_rate_final_pct, refund_rate_prior_pct, tickets_final_90d, tickets_prior_90d, total_churned_merchantsFROM reportORDER BY orders_final_90d ASC, tenant_id ASCLIMIT 50ORDER BY orders_final_90d ASC NULLS LAST, tenant_id ASC NULLS LAST{{fetch_first(50)}}
WITH churned_tenants AS ( SELECT id AS tenant_id, name, industry FROM tenants WHERE status = 'churned' ), anchor_dates AS ( SELECT ct.tenant_id AS tenant_id, MAX(o.placed_at) AS anchor_at FROM churned_tenants ct JOIN orders o ON o.tenant_id = ct.tenant_id AND o.status <> 'cancelled' GROUP BY ct.tenant_id ), windows AS ( SELECT tenant_id, anchor_at - interval '90 days' AS final_start, anchor_at AS final_end, anchor_at - interval '180 days' AS prior_start, anchor_at - interval '91 days' AS prior_end FROM anchor_dates ), orders_with_fx AS ( SELECT o.tenant_id AS tenant_id, o.id AS order_id, o.user_id AS user_id, o.placed_at AS placed_at, o.currency_code AS currency_code, o.grand_total AS grand_total, er.rate AS rate FROM orders o JOIN churned_tenants ct ON ct.tenant_id = o.tenant_id LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of = CAST(o.placed_at AS date) WHERE o.status <> 'cancelled' ), orders_classified AS ( SELECT ow.tenant_id AS tenant_id, ow.order_id AS order_id, ow.user_id AS user_id, CASE WHEN ow.currency_code = 'EUR' THEN ow.grand_total ELSE CAST(ow.grand_total / ow.rate AS NUMERIC(18,6)) END AS revenue_eur, CASE WHEN ow.placed_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN ow.placed_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM orders_with_fx ow JOIN windows w ON w.tenant_id = ow.tenant_id ), order_metrics AS ( SELECT tenant_id, COUNT(*) FILTER (WHERE period = 'final') AS orders_final, COUNT(*) FILTER (WHERE period = 'prior') AS orders_prior, SUM(revenue_eur) FILTER (WHERE period = 'final') AS revenue_final, SUM(revenue_eur) FILTER (WHERE period = 'prior') AS revenue_prior FROM orders_classified GROUP BY tenant_id ), user_period_counts AS ( SELECT tenant_id, user_id, period, COUNT(*) AS order_cnt FROM orders_classified WHERE period IS NOT NULL GROUP BY tenant_id, user_id, period ), repeat_metrics AS ( SELECT tenant_id, period, COUNT(*) AS customers_total, COUNT(*) FILTER (WHERE order_cnt >= 2) AS customers_repeat FROM user_period_counts GROUP BY tenant_id, period ), repeat_pivot AS ( SELECT tenant_id, MAX(CASE WHEN period = 'final' THEN CAST(customers_repeat AS numeric(18,6)) / NULLIF(customers_total, 0) END) AS repeat_rate_final, MAX(CASE WHEN period = 'prior' THEN CAST(customers_repeat AS numeric(18,6)) / NULLIF(customers_total, 0) END) AS repeat_rate_prior FROM repeat_metrics GROUP BY tenant_id ), payments_with_fx AS ( SELECT p.tenant_id AS tenant_id, p.captured_at AS captured_at, p.currency_code AS currency_code, p.amount AS amount, er.rate AS rate FROM payments p JOIN churned_tenants ct ON ct.tenant_id = p.tenant_id LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = p.currency_code AND er.as_of = CAST(p.captured_at AS date) WHERE p.captured_at IS NOT NULL ), payments_classified AS ( SELECT pw.tenant_id AS tenant_id, CASE WHEN pw.currency_code = 'EUR' THEN pw.amount ELSE CAST(pw.amount / pw.rate AS NUMERIC(18,6)) END AS amount_eur, CASE WHEN pw.captured_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN pw.captured_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM payments_with_fx pw JOIN windows w ON w.tenant_id = pw.tenant_id ), payment_metrics AS ( SELECT tenant_id, SUM(amount_eur) FILTER (WHERE period = 'final') AS payments_final, SUM(amount_eur) FILTER (WHERE period = 'prior') AS payments_prior FROM payments_classified GROUP BY tenant_id ), refunds_with_fx AS ( SELECT r.tenant_id AS tenant_id, r.processed_at AS processed_at, r.currency_code AS currency_code, r.amount AS amount, er.rate AS rate FROM payment_refunds r JOIN churned_tenants ct ON ct.tenant_id = r.tenant_id LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = r.currency_code AND er.as_of = CAST(r.processed_at AS date) WHERE r.status = 'succeeded' AND r.processed_at IS NOT NULL ), refunds_classified AS ( SELECT rw.tenant_id AS tenant_id, CASE WHEN rw.currency_code = 'EUR' THEN rw.amount ELSE CAST(rw.amount / rw.rate AS NUMERIC(18,6)) END AS amount_eur, CASE WHEN rw.processed_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN rw.processed_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM refunds_with_fx rw JOIN windows w ON w.tenant_id = rw.tenant_id ), refund_metrics AS ( SELECT tenant_id, SUM(amount_eur) FILTER (WHERE period = 'final') AS refunds_final, SUM(amount_eur) FILTER (WHERE period = 'prior') AS refunds_prior FROM refunds_classified GROUP BY tenant_id ), tickets_classified AS ( SELECT t.tenant_id AS tenant_id, CASE WHEN t.created_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN t.created_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM tickets t JOIN windows w ON w.tenant_id = t.tenant_id ), ticket_metrics AS ( SELECT tenant_id, COUNT(*) FILTER (WHERE period = 'final') AS tickets_final, COUNT(*) FILTER (WHERE period = 'prior') AS tickets_prior FROM tickets_classified GROUP BY tenant_id ), report AS ( SELECT ct.tenant_id AS tenant_id, ct.name AS merchant_name, ct.industry AS industry, COALESCE(om.orders_final, 0) AS orders_final_90d, COALESCE(om.orders_prior, 0) AS orders_prior_90d, CASE WHEN om.revenue_prior IS NULL OR om.revenue_prior = 0 THEN NULL ELSE ((om.revenue_final - om.revenue_prior) / om.revenue_prior) * 100 END AS revenue_growth_pct, rp.repeat_rate_final * 100 AS repeat_rate_final_pct, rp.repeat_rate_prior * 100 AS repeat_rate_prior_pct, CASE WHEN pm.payments_final IS NULL OR pm.payments_final = 0 THEN NULL ELSE COALESCE(rfm.refunds_final, 0) / pm.payments_final * 100 END AS refund_rate_final_pct, CASE WHEN pm.payments_prior IS NULL OR pm.payments_prior = 0 THEN NULL ELSE COALESCE(rfm.refunds_prior, 0) / pm.payments_prior * 100 END AS refund_rate_prior_pct, COALESCE(tm.tickets_final, 0) AS tickets_final_90d, COALESCE(tm.tickets_prior, 0) AS tickets_prior_90d, COUNT(*) OVER () AS total_churned_merchants FROM churned_tenants ct LEFT JOIN order_metrics om ON om.tenant_id = ct.tenant_id LEFT JOIN repeat_pivot rp ON rp.tenant_id = ct.tenant_id LEFT JOIN payment_metrics pm ON pm.tenant_id = ct.tenant_id LEFT JOIN refund_metrics rfm ON rfm.tenant_id = ct.tenant_id LEFT JOIN ticket_metrics tm ON tm.tenant_id = ct.tenant_id ) SELECT merchant_name, industry, orders_final_90d, orders_prior_90d, revenue_growth_pct, repeat_rate_final_pct, repeat_rate_prior_pct, refund_rate_final_pct, refund_rate_prior_pct, tickets_final_90d, tickets_prior_90d, total_churned_merchants FROM report ORDER BY orders_final_90d ASC NULLS LAST, tenant_id ASC NULLS LAST FETCH FIRST 50 ROWS ONLY
WITH churned_tenants AS ( SELECT id AS tenant_id, name, industry FROM tenants WHERE status = 'churned' ), anchor_dates AS ( SELECT ct.tenant_id AS tenant_id, MAX(o.placed_at) AS anchor_at FROM churned_tenants ct JOIN orders o ON o.tenant_id = ct.tenant_id AND o.status <> 'cancelled' GROUP BY ct.tenant_id ), windows AS ( SELECT tenant_id, anchor_at - interval '90 days' AS final_start, anchor_at AS final_end, anchor_at - interval '180 days' AS prior_start, anchor_at - interval '91 days' AS prior_end FROM anchor_dates ), orders_with_fx AS ( SELECT o.tenant_id AS tenant_id, o.id AS order_id, o.user_id AS user_id, o.placed_at AS placed_at, o.currency_code AS currency_code, o.grand_total AS grand_total, er.rate AS rate FROM orders o JOIN churned_tenants ct ON ct.tenant_id = o.tenant_id LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of = CAST(o.placed_at AS date) WHERE o.status <> 'cancelled' ), orders_classified AS ( SELECT ow.tenant_id AS tenant_id, ow.order_id AS order_id, ow.user_id AS user_id, CASE WHEN ow.currency_code = 'EUR' THEN ow.grand_total ELSE CAST(ow.grand_total / ow.rate AS NUMERIC(18,6)) END AS revenue_eur, CASE WHEN ow.placed_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN ow.placed_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM orders_with_fx ow JOIN windows w ON w.tenant_id = ow.tenant_id ), order_metrics AS ( SELECT tenant_id, COUNT(*) FILTER (WHERE period = 'final') AS orders_final, COUNT(*) FILTER (WHERE period = 'prior') AS orders_prior, SUM(revenue_eur) FILTER (WHERE period = 'final') AS revenue_final, SUM(revenue_eur) FILTER (WHERE period = 'prior') AS revenue_prior FROM orders_classified GROUP BY tenant_id ), user_period_counts AS ( SELECT tenant_id, user_id, period, COUNT(*) AS order_cnt FROM orders_classified WHERE period IS NOT NULL GROUP BY tenant_id, user_id, period ), repeat_metrics AS ( SELECT tenant_id, period, COUNT(*) AS customers_total, COUNT(*) FILTER (WHERE order_cnt >= 2) AS customers_repeat FROM user_period_counts GROUP BY tenant_id, period ), repeat_pivot AS ( SELECT tenant_id, MAX(CASE WHEN period = 'final' THEN CAST(customers_repeat AS numeric(18,6)) / NULLIF(customers_total, 0) END) AS repeat_rate_final, MAX(CASE WHEN period = 'prior' THEN CAST(customers_repeat AS numeric(18,6)) / NULLIF(customers_total, 0) END) AS repeat_rate_prior FROM repeat_metrics GROUP BY tenant_id ), payments_with_fx AS ( SELECT p.tenant_id AS tenant_id, p.captured_at AS captured_at, p.currency_code AS currency_code, p.amount AS amount, er.rate AS rate FROM payments p JOIN churned_tenants ct ON ct.tenant_id = p.tenant_id LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = p.currency_code AND er.as_of = CAST(p.captured_at AS date) WHERE p.captured_at IS NOT NULL ), payments_classified AS ( SELECT pw.tenant_id AS tenant_id, CASE WHEN pw.currency_code = 'EUR' THEN pw.amount ELSE CAST(pw.amount / pw.rate AS NUMERIC(18,6)) END AS amount_eur, CASE WHEN pw.captured_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN pw.captured_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM payments_with_fx pw JOIN windows w ON w.tenant_id = pw.tenant_id ), payment_metrics AS ( SELECT tenant_id, SUM(amount_eur) FILTER (WHERE period = 'final') AS payments_final, SUM(amount_eur) FILTER (WHERE period = 'prior') AS payments_prior FROM payments_classified GROUP BY tenant_id ), refunds_with_fx AS ( SELECT r.tenant_id AS tenant_id, r.processed_at AS processed_at, r.currency_code AS currency_code, r.amount AS amount, er.rate AS rate FROM payment_refunds r JOIN churned_tenants ct ON ct.tenant_id = r.tenant_id LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = r.currency_code AND er.as_of = CAST(r.processed_at AS date) WHERE r.status = 'succeeded' AND r.processed_at IS NOT NULL ), refunds_classified AS ( SELECT rw.tenant_id AS tenant_id, CASE WHEN rw.currency_code = 'EUR' THEN rw.amount ELSE CAST(rw.amount / rw.rate AS NUMERIC(18,6)) END AS amount_eur, CASE WHEN rw.processed_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN rw.processed_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM refunds_with_fx rw JOIN windows w ON w.tenant_id = rw.tenant_id ), refund_metrics AS ( SELECT tenant_id, SUM(amount_eur) FILTER (WHERE period = 'final') AS refunds_final, SUM(amount_eur) FILTER (WHERE period = 'prior') AS refunds_prior FROM refunds_classified GROUP BY tenant_id ), tickets_classified AS ( SELECT t.tenant_id AS tenant_id, CASE WHEN t.created_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN t.created_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM tickets t JOIN windows w ON w.tenant_id = t.tenant_id ), ticket_metrics AS ( SELECT tenant_id, COUNT(*) FILTER (WHERE period = 'final') AS tickets_final, COUNT(*) FILTER (WHERE period = 'prior') AS tickets_prior FROM tickets_classified GROUP BY tenant_id ), report AS ( SELECT ct.tenant_id AS tenant_id, ct.name AS merchant_name, ct.industry AS industry, COALESCE(om.orders_final, 0) AS orders_final_90d, COALESCE(om.orders_prior, 0) AS orders_prior_90d, CASE WHEN om.revenue_prior IS NULL OR om.revenue_prior = 0 THEN NULL ELSE ((om.revenue_final - om.revenue_prior) / om.revenue_prior) * 100 END AS revenue_growth_pct, rp.repeat_rate_final * 100 AS repeat_rate_final_pct, rp.repeat_rate_prior * 100 AS repeat_rate_prior_pct, CASE WHEN pm.payments_final IS NULL OR pm.payments_final = 0 THEN NULL ELSE COALESCE(rfm.refunds_final, 0) / pm.payments_final * 100 END AS refund_rate_final_pct, CASE WHEN pm.payments_prior IS NULL OR pm.payments_prior = 0 THEN NULL ELSE COALESCE(rfm.refunds_prior, 0) / pm.payments_prior * 100 END AS refund_rate_prior_pct, COALESCE(tm.tickets_final, 0) AS tickets_final_90d, COALESCE(tm.tickets_prior, 0) AS tickets_prior_90d, COUNT(*) OVER () AS total_churned_merchants FROM churned_tenants ct LEFT JOIN order_metrics om ON om.tenant_id = ct.tenant_id LEFT JOIN repeat_pivot rp ON rp.tenant_id = ct.tenant_id LEFT JOIN payment_metrics pm ON pm.tenant_id = ct.tenant_id LEFT JOIN refund_metrics rfm ON rfm.tenant_id = ct.tenant_id LEFT JOIN ticket_metrics tm ON tm.tenant_id = ct.tenant_id ) SELECT merchant_name, industry, orders_final_90d, orders_prior_90d, revenue_growth_pct, repeat_rate_final_pct, repeat_rate_prior_pct, refund_rate_final_pct, refund_rate_prior_pct, tickets_final_90d, tickets_prior_90d, total_churned_merchants FROM report ORDER BY orders_final_90d ASC NULLS LAST, tenant_id ASC NULLS LAST LIMIT 50
The agents' own records, as the role play wrote them; only the personal data of the person who ran it is redacted
Report request from Arjun Patel (analyst for the Head of Merchant Success): Title: Pre-churn warning signs across sales, repeat buying, refunds, and support tickets Question: In the months before a churned merchant's last order, did sales, shopper repeat-buying, refund rates, or support ticket volume already show warning signs? Grain: one row per merchant (tenant) with status equal to churned Columns: - merchant name - industry - orders final 90 days - orders prior 90 days - revenue growth pct - repeat rate final 90 days - repeat rate prior 90 days - refund rate final 90 days - refund rate prior 90 days - tickets final 90 days - tickets prior 90 days Details: For each churned tenant, anchor date is that tenant's own max orders.placed_at excluding cancelled orders. Final window is anchor minus 90 days through anchor. Prior window is anchor minus 180 days through anchor minus 91 days. Orders and revenue from orders table, converted to EUR via exchange_rates, excluding cancelled orders; revenue growth pct is final revenue minus prior revenue, divided by prior revenue, blank if prior is zero. Repeat rate is distinct customers with at least two orders divided by distinct customers with at least one order, per window. Refund rate is sum of payment_refunds.amount EUR divided by sum of payments.amount EUR captured in that window. Tickets is count of tickets rows with created_at in that window for that tenant. Order by orders final 90 days ascending, tie-break tenant_id. Cap 50 rows, note total count. Check your query with the warehouse's dry_run tool, then hand it in with submit.
dry_runWITH churned_tenants AS ( SELECT id AS tenant_id, name, industry FROM tenants WHERE status = 'churned' ), anchor_dates AS ( SELECT ct.tenant_id, MAX(o.placed_at) AS anchor_at FROM churned_tenants ct JOIN orders o ON o.tenant_id = ct.tenant_id AND o.status <> 'cancelled' GROUP BY ct.tenant_id ), windows AS ( SELECT tenant_id, anchor_at - interval '90 days' AS final_start, anchor_at AS final_end, anchor_at - interval '180 days' AS prior_start, anchor_at - interval '91 days' AS prior_end FROM anchor_dates ), orders_with_fx AS ( SELECT o.tenant_id, o.id AS order_id, o.user_id, o.placed_at, o.currency_code, o.grand_total, er.rate FROM orders o JOIN churned_tenants ct ON ct.tenant_id = o.tenant_id LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of = o.placed_at::date WHERE o.status <> 'cancelled' ), orders_classified AS ( SELECT ow.tenant_id, ow.order_id, ow.user_id, CASE WHEN ow.currency_code = 'EUR' THEN ow.grand_total ELSE ow.grand_total / ow.rate END AS revenue_eur, CASE WHEN ow.placed_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN ow.placed_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM orders_with_fx ow JOIN windows w ON w.tenant_id = ow.tenant_id ), order_metrics AS ( SELECT tenant_id, COUNT(*) FILTER (WHERE period = 'final') AS orders_final, COUNT(*) FILTER (WHERE period = 'prior') AS orders_prior, SUM(revenue_eur) FILTER (WHERE period = 'final') AS revenue_final, SUM(revenue_eur) FILTER (WHERE period = 'prior') AS revenue_prior FROM orders_classified GROUP BY tenant_id ), user_period_counts AS ( SELECT tenant_id, user_id, period, COUNT(*) AS order_cnt FROM orders_classified WHERE period IS NOT NULL GROUP BY tenant_id, user_id, period ), repeat_metrics AS ( SELECT tenant_id, period, COUNT(*) AS customers_total, COUNT(*) FILTER (WHERE order_cnt >= 2) AS customers_repeat FROM user_period_counts GROUP BY tenant_id, period ), repeat_pivot AS ( SELECT tenant_id, MAX(CASE WHEN period = 'final' THEN customers_repeat::numeric / NULLIF(customers_total, 0) END) AS repeat_rate_final, MAX(CASE WHEN period = 'prior' THEN customers_repeat::numeric / NULLIF(customers_total, 0) END) AS repeat_rate_prior FROM repeat_metrics GROUP BY tenant_id ), payments_with_fx AS ( SELECT p.tenant_id, p.captured_at, p.currency_code, p.amount, er.rate FROM payments p JOIN churned_tenants ct ON ct.tenant_id = p.tenant_id LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = p.currency_code AND er.as_of = p.captured_at::date WHERE p.captured_at IS NOT NULL ), payments_classified AS ( SELECT pw.tenant_id, CASE WHEN pw.currency_code = 'EUR' THEN pw.amount ELSE pw.amount / pw.rate END AS amount_eur, CASE WHEN pw.captured_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN pw.captured_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM payments_with_fx pw JOIN windows w ON w.tenant_id = pw.tenant_id ), payment_metrics AS ( SELECT tenant_id, SUM(amount_eur) FILTER (WHERE period = 'final') AS payments_final, SUM(amount_eur) FILTER (WHERE period = 'prior') AS payments_prior FROM payments_classified GROUP BY tenant_id ), refunds_with_fx AS ( SELECT r.tenant_id, r.processed_at, r.currency_code, r.amount, er.rate FROM payment_refunds r JOIN churned_tenants ct ON ct.tenant_id = r.tenant_id LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = r.currency_code AND er.as_of = r.processed_at::date WHERE r.status = 'succeeded' AND r.processed_at IS NOT NULL ), refunds_classified AS ( SELECT rw.tenant_id, CASE WHEN rw.currency_code = 'EUR' THEN rw.amount ELSE rw.amount / rw.rate END AS amount_eur, CASE WHEN rw.processed_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN rw.processed_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM refunds_with_fx rw JOIN windows w ON w.tenant_id = rw.tenant_id ), refund_metrics AS ( SELECT tenant_id, SUM(amount_eur) FILTER (WHERE period = 'final') AS refunds_final, SUM(amount_eur) FILTER (WHERE period = 'prior') AS refunds_prior FROM refunds_classified GROUP BY tenant_id ), tickets_classified AS ( SELECT t.tenant_id, CASE WHEN t.created_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN t.created_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM tickets t JOIN windows w ON w.tenant_id = t.tenant_id ), ticket_metrics AS ( SELECT tenant_id, COUNT(*) FILTER (WHERE period = 'final') AS tickets_final, COUNT(*) FILTER (WHERE period = 'prior') AS tickets_prior FROM tickets_classified GROUP BY tenant_id ), report AS ( SELECT ct.tenant_id, ct.name AS merchant_name, ct.industry, COALESCE(om.orders_final, 0) AS orders_final_90d, COALESCE(om.orders_prior, 0) AS orders_prior_90d, CASE WHEN om.revenue_prior IS NULL OR om.revenue_prior = 0 THEN NULL ELSE ROUND(((om.revenue_final - om.revenue_prior) / om.revenue_prior) * 100, 1) END AS revenue_growth_pct, ROUND(rp.repeat_rate_final * 100, 1) AS repeat_rate_final_pct, ROUND(rp.repeat_rate_prior * 100, 1) AS repeat_rate_prior_pct, CASE WHEN pm.payments_final IS NULL OR pm.payments_final = 0 THEN NULL ELSE ROUND(COALESCE(rfm.refunds_final, 0) / pm.payments_final * 100, 1) END AS refund_rate_final_pct, CASE WHEN pm.payments_prior IS NULL OR pm.payments_prior = 0 THEN NULL ELSE ROUND(COALESCE(rfm.refunds_prior, 0) / pm.payments_prior * 100, 1) END AS refund_rate_prior_pct, COALESCE(tm.tickets_final, 0) AS tickets_final_90d, COALESCE(tm.tickets_prior, 0) AS tickets_prior_90d, COUNT(*) OVER () AS total_churned_merchants FROM churned_tenants ct LEFT JOIN order_metrics om ON om.tenant_id = ct.tenant_id LEFT JOIN repeat_pivot rp ON rp.tenant_id = ct.tenant_id LEFT JOIN payment_metrics pm ON pm.tenant_id = ct.tenant_id LEFT JOIN refund_metrics rfm ON rfm.tenant_id = ct.tenant_id LEFT JOIN ticket_metrics tm ON tm.tenant_id = ct.tenant_id ) SELECT merchant_name, industry, orders_final_90d, orders_prior_90d, revenue_growth_pct, repeat_rate_final_pct, repeat_rate_prior_pct, refund_rate_final_pct, refund_rate_prior_pct, tickets_final_90d, tickets_prior_90d, total_churned_merchants FROM report ORDER BY orders_final_90d ASC, tenant_id ASC LIMIT 50
STATUS: COMPILES (dry run on the empty schema, no data read) COLUMNS: merchant_name, industry, orders_final_90d, orders_prior_90d, revenue_growth_pct, repeat_rate_final_pct, repeat_rate_prior_pct, refund_rate_final_pct, refund_rate_prior_pct, tickets_final_90d, tickets_prior_90d, total_churned_merchants
submitWITH churned_tenants AS ( SELECT id AS tenant_id, name, industry FROM tenants WHERE status = 'churned' ), anchor_dates AS ( SELECT ct.tenant_id, MAX(o.placed_at) AS anchor_at FROM churned_tenants ct JOIN orders o ON o.tenant_id = ct.tenant_id AND o.status <> 'cancelled' GROUP BY ct.tenant_id ), windows AS ( SELECT tenant_id, anchor_at - interval '90 days' AS final_start, anchor_at AS final_end, anchor_at - interval '180 days' AS prior_start, anchor_at - interval '91 days' AS prior_end FROM anchor_dates ), orders_with_fx AS ( SELECT o.tenant_id, o.id AS order_id, o.user_id, o.placed_at, o.currency_code, o.grand_total, er.rate FROM orders o JOIN churned_tenants ct ON ct.tenant_id = o.tenant_id LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of = o.placed_at::date WHERE o.status <> 'cancelled' ), orders_classified AS ( SELECT ow.tenant_id, ow.order_id, ow.user_id, CASE WHEN ow.currency_code = 'EUR' THEN ow.grand_total ELSE ow.grand_total / ow.rate END AS revenue_eur, CASE WHEN ow.placed_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN ow.placed_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM orders_with_fx ow JOIN windows w ON w.tenant_id = ow.tenant_id ), order_metrics AS ( SELECT tenant_id, COUNT(*) FILTER (WHERE period = 'final') AS orders_final, COUNT(*) FILTER (WHERE period = 'prior') AS orders_prior, SUM(revenue_eur) FILTER (WHERE period = 'final') AS revenue_final, SUM(revenue_eur) FILTER (WHERE period = 'prior') AS revenue_prior FROM orders_classified GROUP BY tenant_id ), user_period_counts AS ( SELECT tenant_id, user_id, period, COUNT(*) AS order_cnt FROM orders_classified WHERE period IS NOT NULL GROUP BY tenant_id, user_id, period ), repeat_metrics AS ( SELECT tenant_id, period, COUNT(*) AS customers_total, COUNT(*) FILTER (WHERE order_cnt >= 2) AS customers_repeat FROM user_period_counts GROUP BY tenant_id, period ), repeat_pivot AS ( SELECT tenant_id, MAX(CASE WHEN period = 'final' THEN customers_repeat::numeric / NULLIF(customers_total, 0) END) AS repeat_rate_final, MAX(CASE WHEN period = 'prior' THEN customers_repeat::numeric / NULLIF(customers_total, 0) END) AS repeat_rate_prior FROM repeat_metrics GROUP BY tenant_id ), payments_with_fx AS ( SELECT p.tenant_id, p.captured_at, p.currency_code, p.amount, er.rate FROM payments p JOIN churned_tenants ct ON ct.tenant_id = p.tenant_id LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = p.currency_code AND er.as_of = p.captured_at::date WHERE p.captured_at IS NOT NULL ), payments_classified AS ( SELECT pw.tenant_id, CASE WHEN pw.currency_code = 'EUR' THEN pw.amount ELSE pw.amount / pw.rate END AS amount_eur, CASE WHEN pw.captured_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN pw.captured_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM payments_with_fx pw JOIN windows w ON w.tenant_id = pw.tenant_id ), payment_metrics AS ( SELECT tenant_id, SUM(amount_eur) FILTER (WHERE period = 'final') AS payments_final, SUM(amount_eur) FILTER (WHERE period = 'prior') AS payments_prior FROM payments_classified GROUP BY tenant_id ), refunds_with_fx AS ( SELECT r.tenant_id, r.processed_at, r.currency_code, r.amount, er.rate FROM payment_refunds r JOIN churned_tenants ct ON ct.tenant_id = r.tenant_id LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = r.currency_code AND er.as_of = r.processed_at::date WHERE r.status = 'succeeded' AND r.processed_at IS NOT NULL ), refunds_classified AS ( SELECT rw.tenant_id, CASE WHEN rw.currency_code = 'EUR' THEN rw.amount ELSE rw.amount / rw.rate END AS amount_eur, CASE WHEN rw.processed_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN rw.processed_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM refunds_with_fx rw JOIN windows w ON w.tenant_id = rw.tenant_id ), refund_metrics AS ( SELECT tenant_id, SUM(amount_eur) FILTER (WHERE period = 'final') AS refunds_final, SUM(amount_eur) FILTER (WHERE period = 'prior') AS refunds_prior FROM refunds_classified GROUP BY tenant_id ), tickets_classified AS ( SELECT t.tenant_id, CASE WHEN t.created_at BETWEEN w.final_start AND w.final_end THEN 'final' WHEN t.created_at BETWEEN w.prior_start AND w.prior_end THEN 'prior' ELSE NULL END AS period FROM tickets t JOIN windows w ON w.tenant_id = t.tenant_id ), ticket_metrics AS ( SELECT tenant_id, COUNT(*) FILTER (WHERE period = 'final') AS tickets_final, COUNT(*) FILTER (WHERE period = 'prior') AS tickets_prior FROM tickets_classified GROUP BY tenant_id ), report AS ( SELECT ct.tenant_id, ct.name AS merchant_name, ct.industry, COALESCE(om.orders_final, 0) AS orders_final_90d, COALESCE(om.orders_prior, 0) AS orders_prior_90d, CASE WHEN om.revenue_prior IS NULL OR om.revenue_prior = 0 THEN NULL ELSE ROUND(((om.revenue_final - om.revenue_prior) / om.revenue_prior) * 100, 1) END AS revenue_growth_pct, ROUND(rp.repeat_rate_final * 100, 1) AS repeat_rate_final_pct, ROUND(rp.repeat_rate_prior * 100, 1) AS repeat_rate_prior_pct, CASE WHEN pm.payments_final IS NULL OR pm.payments_final = 0 THEN NULL ELSE ROUND(COALESCE(rfm.refunds_final, 0) / pm.payments_final * 100, 1) END AS refund_rate_final_pct, CASE WHEN pm.payments_prior IS NULL OR pm.payments_prior = 0 THEN NULL ELSE ROUND(COALESCE(rfm.refunds_prior, 0) / pm.payments_prior * 100, 1) END AS refund_rate_prior_pct, COALESCE(tm.tickets_final, 0) AS tickets_final_90d, COALESCE(tm.tickets_prior, 0) AS tickets_prior_90d, COUNT(*) OVER () AS total_churned_merchants FROM churned_tenants ct LEFT JOIN order_metrics om ON om.tenant_id = ct.tenant_id LEFT JOIN repeat_pivot rp ON rp.tenant_id = ct.tenant_id LEFT JOIN payment_metrics pm ON pm.tenant_id = ct.tenant_id LEFT JOIN refund_metrics rfm ON rfm.tenant_id = ct.tenant_id LEFT JOIN ticket_metrics tm ON tm.tenant_id = ct.tenant_id ) SELECT merchant_name, industry, orders_final_90d, orders_prior_90d, revenue_growth_pct, repeat_rate_final_pct, repeat_rate_prior_pct, refund_rate_final_pct, refund_rate_prior_pct, tickets_final_90d, tickets_prior_90d, total_churned_merchants FROM report ORDER BY orders_final_90d ASC, tenant_id ASC LIMIT 50
Submitted to tonight's batch. Your work on this request is done.