Queries · q287 · VP Product · week 2, night 1
“Does basket size, price point, or discount usage differ between carts that convert quickly and carts that stall for hours or days?”
Fictional people at a fictional company, each played by a language-model agent. Who they are and what they are told
Week 1
We are redesigning the shopper journey for 2026. We need to understand the whole path, from the first visit to checkout, delivery, returns and the next purchase, and where shoppers drop out and why.
The manager, at the start of week 2
Subject: Good start — let's dig into the cart leak
Emeka, this is exactly the kind of clarity I needed. The cart-stage bleed is the headline, and I agree we should lead with it — but a two-thirds drop-off that lingers for days, converting at under 20%, isn't something we can act on until we know why people stall. "Add cart recovery emails" is a guess until we understand the shape of the hesitation. That's my main ask for this week.
I also want to flag one thing before we build a roadmap slide on repeat purchase and returns: the return-vs-repeat gap and the delivery/returns pattern are both flagged as not-yet-causal, confounded by vertical. I don't want to present an 8-point "returns hurt loyalty" number to the exec team if it's really just "apparel behaves differently." Let's firm that up before it goes in a deck, but it's not urgent this week.
Questions for this week:
Keep the returns/delivery vertical-control work and the causal retention question on the list for whenever a slot frees up, but the cart story is what I want to walk into the roadmap review with real conviction on.
Thanks, Emeka — strong week.
Jonas
The analyst · week 2, night 1, request 1 of 3
Jonas's email has an implicit priority: Q1 (what's in a stalled cart) and Q2 (is the bleed trending worse) are "my main ask for this week"; Q3 (organic recovery) is also wanted but reads as a natural follow-up once we understand the shape; Q4 is explicitly "a quick gut-check... no need for deep investigation." So tonight I'm doing Q1, Q2, and a lightweight version of Q4, saving Q3 for night 2 since it benefits from what we learn tonight (e.g., whether stalled carts later convert with a discount attached is itself a clue about organic recovery).
Design notes:
All three use the 6 retail tenants (Q1, Q2) or 4 subscription tenants (Q4) established in last week's work, so the engineer has a concrete, named population to filter on rather than re-deriving "top merchants."
The engineer · 1 dry run on the empty schema, then the submission
WITH tenant_filter AS ( SELECT id FROM tenants WHERE name IN ('Kibblewick','Studio Vrij','Petalcourt Beauty','Marrowby & Vane','Cragsley Outfitters','Ampfeld Elektronik') ), cart_base AS ( SELECT c.id, c.tenant_id, c.status, c.item_count, c.subtotal, c.currency_code, c.created_at, coalesce(c.converted_at, c.abandoned_at, c.expires_at) AS resolved_at, er.rate AS eur_rate FROM carts c JOIN tenant_filter tf ON tf.id = c.tenant_id LEFT JOIN exchange_rates er ON er.base_code='EUR' AND er.quote_code = c.currency_code AND er.as_of = c.created_at::date WHERE c.created_at >= DATE '2025-01-01' AND c.created_at < DATE '2026-01-01' ), cart_calc AS ( SELECT *, CASE WHEN currency_code='EUR' THEN subtotal ELSE subtotal / NULLIF(eur_rate,0) END AS subtotal_eur, CASE WHEN resolved_at IS NULL THEN NULL ELSE EXTRACT(EPOCH FROM (resolved_at - created_at))/3600.0 END AS resolution_hours FROM cart_base ), bucketed AS ( SELECT *, CASE WHEN resolution_hours IS NULL THEN 'unresolved/still active' WHEN resolution_hours < 1 THEN '<1 hour' WHEN resolution_hours < 24 THEN '1-24 hours' WHEN resolution_hours < 168 THEN '1-7 days' ELSE '>7 days' END AS duration_bucket FROM cart_calc ), order_discount_flag AS ( SELECT o.cart_id, CASE WHEN EXISTS (SELECT 1 FROM order_discounts od WHERE od.order_id = o.id) THEN 1 ELSE 0 END AS has_discount FROM orders o WHERE o.cart_id IS NOT NULL ) SELECT b.duration_bucket AS duration_bucket, COUNT(*) AS cart_count, ROUND(AVG(b.item_count)::numeric, 2) AS avg_item_count, ROUND(AVG(b.subtotal_eur)::numeric, 2) AS avg_subtotal_eur, ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY b.subtotal_eur)::numeric, 2) AS median_subtotal_eur, ROUND(100.0 * COUNT(*) FILTER (WHERE b.status='converted') / COUNT(*), 1) AS pct_converted, ROUND(100.0 * COUNT(*) FILTER (WHERE b.status='converted' AND odf.has_discount=1) / NULLIF(COUNT(*) FILTER (WHERE b.status='converted'),0), 1) AS pct_converted_with_discount FROM bucketed b LEFT JOIN order_discount_flag odf ON odf.cart_id = b.id GROUP BY b.duration_bucket ORDER BY CASE b.duration_bucket WHEN '<1 hour' THEN 1 WHEN '1-24 hours' THEN 2 WHEN '1-7 days' THEN 3 WHEN '>7 days' THEN 4 WHEN 'unresolved/still active' THEN 5 END
“Looks good — compiles with the expected columns, grain, and ordering.”
“Submitted. The report is grained by duration bucket (5 rows, fixed order), covering the 6 named retail tenants' 2025 carts, with EUR-normalized subtotal averages/medians, conversion rate, and discount-usage rate among converted carts per bucket.”
As Emeka Obi saw it, from the small dataset the agents worked on
duration_bucket | cart_count | avg_item_count | avg_subtotal_eur | median_subtotal_eur | pct_converted | pct_converted_with_discount ----------------+------------+----------------+------------------+---------------------+---------------+---------------------------- <1 hour | 5992 | 2.79 | 242.44 | 130.92 | 100.0 | 17.2 1-24 hours | 6577 | 1.61 | 175.87 | 75.06 | 1.8 | 16.2 1-7 days | 8293 | 1.74 | 168.84 | 77.63 | 16.0 | 10.7 >7 days | 335 | 1.79 | 209.01 | 84.40 | 31.6 | 15.1 (4 rows)
The memo and the queries that followed; links found by title, so inferred
Later queries that build on this report:
It builds on:
Changed only where the benchmark needs the same result on every system
WITH tenant_filter AS ( SELECT id FROM tenants WHERE name IN ('Kibblewick','Studio Vrij','Petalcourt Beauty','Marrowby & Vane','Cragsley Outfitters','Ampfeld Elektronik')),cart_base AS ( SELECT c.id, c.tenant_id, c.status, c.item_count, c.subtotal, c.currency_code, c.created_at, SELECT c.id AS id, c.tenant_id AS tenant_id, c.status AS status, c.item_count AS item_count, c.subtotal AS subtotal, c.currency_code AS currency_code, c.created_at AS created_at, coalesce(c.converted_at, c.abandoned_at, c.expires_at) AS resolved_at, er.rate AS eur_rate FROM carts c JOIN tenant_filter tf ON tf.id = c.tenant_id LEFT JOIN exchange_rates er ON er.base_code='EUR' AND er.quote_code = c.currency_code AND er.as_of = c.created_at::date LEFT JOIN exchange_rates er ON er.base_code='EUR' AND er.quote_code = c.currency_code AND er.as_of = CAST(c.created_at AS date) WHERE c.created_at >= DATE '2025-01-01' AND c.created_at < DATE '2026-01-01'),cart_calc AS ( SELECT *, CASE WHEN currency_code='EUR' THEN subtotal ELSE subtotal / NULLIF(eur_rate,0) END AS subtotal_eur, CASE WHEN resolved_at IS NULL THEN NULL ELSE EXTRACT(EPOCH FROM (resolved_at - created_at))/3600.0 END AS resolution_hours CASE WHEN currency_code='EUR' THEN subtotal ELSE CAST(subtotal / NULLIF(eur_rate,0) AS NUMERIC(18,6)) END AS subtotal_eur, CASE WHEN resolved_at IS NULL THEN NULL ELSE {{seconds_between(created_at, resolved_at)}}/3600.0 END AS resolution_hours FROM cart_base),bucketed AS ( SELECT *, CASE WHEN resolution_hours IS NULL THEN 'unresolved/still active' WHEN resolution_hours < 1 THEN '<1 hour' WHEN resolution_hours < 24 THEN '1-24 hours' WHEN resolution_hours < 168 THEN '1-7 days' ELSE '>7 days' END AS duration_bucket FROM cart_calc),order_discount_flag AS ( SELECT o.cart_id, CASE WHEN EXISTS (SELECT 1 FROM order_discounts od WHERE od.order_id = o.id) THEN 1 ELSE 0 END AS has_discount SELECT o.cart_id AS cart_id, CASE WHEN EXISTS (SELECT 1 FROM order_discounts od WHERE od.order_id = o.id) THEN 1 ELSE 0 END AS has_discount FROM orders o WHERE o.cart_id IS NOT NULL)SELECT b.duration_bucket AS duration_bucket, COUNT(*) AS cart_count, ROUND(AVG(b.item_count)::numeric, 2) AS avg_item_count, ROUND(AVG(b.subtotal_eur)::numeric, 2) AS avg_subtotal_eur, ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY b.subtotal_eur)::numeric, 2) AS median_subtotal_eur, ROUND(100.0 * COUNT(*) FILTER (WHERE b.status='converted') / COUNT(*), 1) AS pct_converted, ROUND(100.0 * COUNT(*) FILTER (WHERE b.status='converted' AND odf.has_discount=1) / NULLIF(COUNT(*) FILTER (WHERE b.status='converted'),0), 1) AS pct_converted_with_discount AVG(CAST(b.item_count AS NUMERIC(18,6))) AS avg_item_count, AVG(b.subtotal_eur) AS avg_subtotal_eur, {{percentile_cont(0.5, b.subtotal_eur)}} AS median_subtotal_eur, 100.0 * COUNT(*) FILTER (WHERE b.status='converted') / COUNT(*) AS pct_converted, 100.0 * COUNT(*) FILTER (WHERE b.status='converted' AND odf.has_discount=1) / NULLIF(COUNT(*) FILTER (WHERE b.status='converted'),0) AS pct_converted_with_discountFROM bucketed bLEFT JOIN order_discount_flag odf ON odf.cart_id = b.idGROUP BY b.duration_bucketORDER BY CASE b.duration_bucket WHEN '<1 hour' THEN 1 WHEN '1-24 hours' THEN 2 WHEN '1-7 days' THEN 3 WHEN '>7 days' THEN 4 WHEN 'unresolved/still active' THEN 5ENDEND NULLS LAST
WITH tenant_filter AS ( SELECT id FROM tenants WHERE name IN ('Kibblewick','Studio Vrij','Petalcourt Beauty','Marrowby & Vane','Cragsley Outfitters','Ampfeld Elektronik') ), cart_base AS ( SELECT c.id AS id, c.tenant_id AS tenant_id, c.status AS status, c.item_count AS item_count, c.subtotal AS subtotal, c.currency_code AS currency_code, c.created_at AS created_at, coalesce(c.converted_at, c.abandoned_at, c.expires_at) AS resolved_at, er.rate AS eur_rate FROM carts c JOIN tenant_filter tf ON tf.id = c.tenant_id LEFT JOIN exchange_rates er ON er.base_code='EUR' AND er.quote_code = c.currency_code AND er.as_of = CAST(c.created_at AS date) WHERE c.created_at >= DATE '2025-01-01' AND c.created_at < DATE '2026-01-01' ), cart_calc AS ( SELECT *, CASE WHEN currency_code='EUR' THEN subtotal ELSE CAST(subtotal / NULLIF(eur_rate,0) AS NUMERIC(18,6)) END AS subtotal_eur, CASE WHEN resolved_at IS NULL THEN NULL ELSE CAST(EXTRACT(EPOCH FROM (resolved_at - created_at)) AS NUMERIC(18,6))/3600.0 END AS resolution_hours FROM cart_base ), bucketed AS ( SELECT *, CASE WHEN resolution_hours IS NULL THEN 'unresolved/still active' WHEN resolution_hours < 1 THEN '<1 hour' WHEN resolution_hours < 24 THEN '1-24 hours' WHEN resolution_hours < 168 THEN '1-7 days' ELSE '>7 days' END AS duration_bucket FROM cart_calc ), order_discount_flag AS ( SELECT o.cart_id AS cart_id, CASE WHEN EXISTS (SELECT 1 FROM order_discounts od WHERE od.order_id = o.id) THEN 1 ELSE 0 END AS has_discount FROM orders o WHERE o.cart_id IS NOT NULL ) SELECT b.duration_bucket AS duration_bucket, COUNT(*) AS cart_count, AVG(CAST(b.item_count AS NUMERIC(18,6))) AS avg_item_count, AVG(b.subtotal_eur) AS avg_subtotal_eur, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY b.subtotal_eur) AS median_subtotal_eur, 100.0 * COUNT(*) FILTER (WHERE b.status='converted') / COUNT(*) AS pct_converted, 100.0 * COUNT(*) FILTER (WHERE b.status='converted' AND odf.has_discount=1) / NULLIF(COUNT(*) FILTER (WHERE b.status='converted'),0) AS pct_converted_with_discount FROM bucketed b LEFT JOIN order_discount_flag odf ON odf.cart_id = b.id GROUP BY b.duration_bucket ORDER BY CASE b.duration_bucket WHEN '<1 hour' THEN 1 WHEN '1-24 hours' THEN 2 WHEN '1-7 days' THEN 3 WHEN '>7 days' THEN 4 WHEN 'unresolved/still active' THEN 5 END NULLS LAST
WITH tenant_filter AS ( SELECT id FROM tenants WHERE name IN ('Kibblewick','Studio Vrij','Petalcourt Beauty','Marrowby & Vane','Cragsley Outfitters','Ampfeld Elektronik') ), cart_base AS ( SELECT c.id AS id, c.tenant_id AS tenant_id, c.status AS status, c.item_count AS item_count, c.subtotal AS subtotal, c.currency_code AS currency_code, c.created_at AS created_at, coalesce(c.converted_at, c.abandoned_at, c.expires_at) AS resolved_at, er.rate AS eur_rate FROM carts c JOIN tenant_filter tf ON tf.id = c.tenant_id LEFT JOIN exchange_rates er ON er.base_code='EUR' AND er.quote_code = c.currency_code AND er.as_of = CAST(c.created_at AS date) WHERE c.created_at >= DATE '2025-01-01' AND c.created_at < DATE '2026-01-01' ), cart_calc AS ( SELECT *, CASE WHEN currency_code='EUR' THEN subtotal ELSE CAST(subtotal / NULLIF(eur_rate,0) AS NUMERIC(18,6)) END AS subtotal_eur, CASE WHEN resolved_at IS NULL THEN NULL ELSE CAST(EXTRACT(EPOCH FROM (resolved_at - created_at)) AS NUMERIC(18,6))/3600.0 END AS resolution_hours FROM cart_base ), bucketed AS ( SELECT *, CASE WHEN resolution_hours IS NULL THEN 'unresolved/still active' WHEN resolution_hours < 1 THEN '<1 hour' WHEN resolution_hours < 24 THEN '1-24 hours' WHEN resolution_hours < 168 THEN '1-7 days' ELSE '>7 days' END AS duration_bucket FROM cart_calc ), order_discount_flag AS ( SELECT o.cart_id AS cart_id, CASE WHEN EXISTS (SELECT 1 FROM order_discounts od WHERE od.order_id = o.id) THEN 1 ELSE 0 END AS has_discount FROM orders o WHERE o.cart_id IS NOT NULL ) SELECT b.duration_bucket AS duration_bucket, COUNT(*) AS cart_count, AVG(CAST(b.item_count AS NUMERIC(18,6))) AS avg_item_count, AVG(b.subtotal_eur) AS avg_subtotal_eur, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY CAST(b.subtotal_eur AS DOUBLE PRECISION)) AS median_subtotal_eur, 100.0 * COUNT(*) FILTER (WHERE b.status='converted') / COUNT(*) AS pct_converted, 100.0 * COUNT(*) FILTER (WHERE b.status='converted' AND odf.has_discount=1) / NULLIF(COUNT(*) FILTER (WHERE b.status='converted'),0) AS pct_converted_with_discount FROM bucketed b LEFT JOIN order_discount_flag odf ON odf.cart_id = b.id GROUP BY b.duration_bucket ORDER BY CASE b.duration_bucket WHEN '<1 hour' THEN 1 WHEN '1-24 hours' THEN 2 WHEN '1-7 days' THEN 3 WHEN '>7 days' THEN 4 WHEN 'unresolved/still active' THEN 5 END NULLS LAST
WITH tenant_filter AS ( SELECT id FROM tenants WHERE name IN ('Kibblewick','Studio Vrij','Petalcourt Beauty','Marrowby & Vane','Cragsley Outfitters','Ampfeld Elektronik') ), cart_base AS ( SELECT c.id AS id, c.tenant_id AS tenant_id, c.status AS status, c.item_count AS item_count, c.subtotal AS subtotal, c.currency_code AS currency_code, c.created_at AS created_at, coalesce(c.converted_at, c.abandoned_at, c.expires_at) AS resolved_at, er.rate AS eur_rate FROM carts c JOIN tenant_filter tf ON tf.id = c.tenant_id LEFT JOIN exchange_rates er ON er.base_code='EUR' AND er.quote_code = c.currency_code AND er.as_of = CAST(c.created_at AS date) WHERE c.created_at >= DATE '2025-01-01' AND c.created_at < DATE '2026-01-01' ), cart_calc AS ( SELECT *, CASE WHEN currency_code='EUR' THEN subtotal ELSE CAST(subtotal / NULLIF(eur_rate,0) AS NUMERIC(18,6)) END AS subtotal_eur, CASE WHEN resolved_at IS NULL THEN NULL ELSE CAST(divideDecimal(toDecimal128(dateDiff('microsecond', created_at, resolved_at), 0), toDecimal128(1000000, 0), 6) AS Nullable(Decimal(18, 6)))/3600.0 END AS resolution_hours FROM cart_base ), bucketed AS ( SELECT *, CASE WHEN resolution_hours IS NULL THEN 'unresolved/still active' WHEN resolution_hours < 1 THEN '<1 hour' WHEN resolution_hours < 24 THEN '1-24 hours' WHEN resolution_hours < 168 THEN '1-7 days' ELSE '>7 days' END AS duration_bucket FROM cart_calc ), order_discount_flag AS ( SELECT o.cart_id AS cart_id, CASE WHEN EXISTS (SELECT 1 FROM order_discounts od WHERE od.order_id = o.id) THEN 1 ELSE 0 END AS has_discount FROM orders o WHERE o.cart_id IS NOT NULL ) SELECT b.duration_bucket AS duration_bucket, COUNT(*) AS cart_count, AVG(CAST(b.item_count AS NUMERIC(18,6))) AS avg_item_count, AVG(b.subtotal_eur) AS avg_subtotal_eur, quantileExactInclusive(0.5)(toFloat64(b.subtotal_eur)) AS median_subtotal_eur, 100.0 * COUNT(*) FILTER (WHERE b.status='converted') / COUNT(*) AS pct_converted, 100.0 * COUNT(*) FILTER (WHERE b.status='converted' AND odf.has_discount=1) / NULLIF(COUNT(*) FILTER (WHERE b.status='converted'),0) AS pct_converted_with_discount FROM bucketed b LEFT JOIN order_discount_flag odf ON odf.cart_id = b.id GROUP BY b.duration_bucket ORDER BY CASE b.duration_bucket WHEN '<1 hour' THEN 1 WHEN '1-24 hours' THEN 2 WHEN '1-7 days' THEN 3 WHEN '>7 days' THEN 4 WHEN 'unresolved/still active' THEN 5 END NULLS LAST
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 Emeka Obi (analyst for the VP Product): Title: Cart contents: stalled vs. fast-converting carts, 2025 Question: Does basket size, price point, or discount usage differ between carts that convert quickly and carts that stall for hours or days? Grain: one row per cart duration bucket Columns: - duration bucket - cart count - avg item count - avg subtotal (EUR) - median subtotal (EUR) - pct of bucket that converted to an order - pct of converted carts (in this bucket) whose order has a discount or coupon applied Details: Tenants: the 6 retail merchants (Kibblewick, Studio Vrij, Petalcourt Beauty, Marrowby & Vane, Cragsley Outfitters, Ampfeld Elektronik). Population: carts with created_at in 2025-01-01 to 2025-12-31. Buckets (same definitions as last week): resolution_time = coalesce(converted_at, abandoned_at, expires_at) - created_at in hours; '<1 hour', '1-24 hours', '1-7 days', '>7 days', 'unresolved/still active' (no resolution timestamp). avg/median subtotal: carts.subtotal converted to EUR via exchange_rates (base_code='EUR', quote_code=carts.currency_code, as_of = carts.created_at date; amount_eur = subtotal / rate). avg item count = carts.item_count. pct_converted = share of carts in the bucket with status='converted'. Discount/coupon presence can only be observed on carts that converted (abandoned carts never reach checkout, so there's no discount record) — compute it only among converted carts in each bucket, as the % whose resulting order (orders.cart_id = carts.id) has at least one row in order_discounts. Order rows in the fixed sequence: '<1 hour', '1-24 hours', '1-7 days', '>7 days', 'unresolved/still active'. Check your query with the warehouse's dry_run tool, then hand it in with submit.
dry_runWITH tenant_filter AS ( SELECT id FROM tenants WHERE name IN ('Kibblewick','Studio Vrij','Petalcourt Beauty','Marrowby & Vane','Cragsley Outfitters','Ampfeld Elektronik') ), cart_base AS ( SELECT c.id, c.tenant_id, c.status, c.item_count, c.subtotal, c.currency_code, c.created_at, coalesce(c.converted_at, c.abandoned_at, c.expires_at) AS resolved_at, er.rate AS eur_rate FROM carts c JOIN tenant_filter tf ON tf.id = c.tenant_id LEFT JOIN exchange_rates er ON er.base_code='EUR' AND er.quote_code = c.currency_code AND er.as_of = c.created_at::date WHERE c.created_at >= DATE '2025-01-01' AND c.created_at < DATE '2026-01-01' ), cart_calc AS ( SELECT *, CASE WHEN currency_code='EUR' THEN subtotal ELSE subtotal / NULLIF(eur_rate,0) END AS subtotal_eur, CASE WHEN resolved_at IS NULL THEN NULL ELSE EXTRACT(EPOCH FROM (resolved_at - created_at))/3600.0 END AS resolution_hours FROM cart_base ), bucketed AS ( SELECT *, CASE WHEN resolution_hours IS NULL THEN 'unresolved/still active' WHEN resolution_hours < 1 THEN '<1 hour' WHEN resolution_hours < 24 THEN '1-24 hours' WHEN resolution_hours < 168 THEN '1-7 days' ELSE '>7 days' END AS duration_bucket FROM cart_calc ), order_discount_flag AS ( SELECT o.cart_id, CASE WHEN EXISTS (SELECT 1 FROM order_discounts od WHERE od.order_id = o.id) THEN 1 ELSE 0 END AS has_discount FROM orders o WHERE o.cart_id IS NOT NULL ) SELECT b.duration_bucket AS duration_bucket, COUNT(*) AS cart_count, ROUND(AVG(b.item_count)::numeric, 2) AS avg_item_count, ROUND(AVG(b.subtotal_eur)::numeric, 2) AS avg_subtotal_eur, ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY b.subtotal_eur)::numeric, 2) AS median_subtotal_eur, ROUND(100.0 * COUNT(*) FILTER (WHERE b.status='converted') / COUNT(*), 1) AS pct_converted, ROUND(100.0 * COUNT(*) FILTER (WHERE b.status='converted' AND odf.has_discount=1) / NULLIF(COUNT(*) FILTER (WHERE b.status='converted'),0), 1) AS pct_converted_with_discount FROM bucketed b LEFT JOIN order_discount_flag odf ON odf.cart_id = b.id GROUP BY b.duration_bucket ORDER BY CASE b.duration_bucket WHEN '<1 hour' THEN 1 WHEN '1-24 hours' THEN 2 WHEN '1-7 days' THEN 3 WHEN '>7 days' THEN 4 WHEN 'unresolved/still active' THEN 5 END
STATUS: COMPILES (dry run on the empty schema, no data read) COLUMNS: duration_bucket, cart_count, avg_item_count, avg_subtotal_eur, median_subtotal_eur, pct_converted, pct_converted_with_discount
submitWITH tenant_filter AS ( SELECT id FROM tenants WHERE name IN ('Kibblewick','Studio Vrij','Petalcourt Beauty','Marrowby & Vane','Cragsley Outfitters','Ampfeld Elektronik') ), cart_base AS ( SELECT c.id, c.tenant_id, c.status, c.item_count, c.subtotal, c.currency_code, c.created_at, coalesce(c.converted_at, c.abandoned_at, c.expires_at) AS resolved_at, er.rate AS eur_rate FROM carts c JOIN tenant_filter tf ON tf.id = c.tenant_id LEFT JOIN exchange_rates er ON er.base_code='EUR' AND er.quote_code = c.currency_code AND er.as_of = c.created_at::date WHERE c.created_at >= DATE '2025-01-01' AND c.created_at < DATE '2026-01-01' ), cart_calc AS ( SELECT *, CASE WHEN currency_code='EUR' THEN subtotal ELSE subtotal / NULLIF(eur_rate,0) END AS subtotal_eur, CASE WHEN resolved_at IS NULL THEN NULL ELSE EXTRACT(EPOCH FROM (resolved_at - created_at))/3600.0 END AS resolution_hours FROM cart_base ), bucketed AS ( SELECT *, CASE WHEN resolution_hours IS NULL THEN 'unresolved/still active' WHEN resolution_hours < 1 THEN '<1 hour' WHEN resolution_hours < 24 THEN '1-24 hours' WHEN resolution_hours < 168 THEN '1-7 days' ELSE '>7 days' END AS duration_bucket FROM cart_calc ), order_discount_flag AS ( SELECT o.cart_id, CASE WHEN EXISTS (SELECT 1 FROM order_discounts od WHERE od.order_id = o.id) THEN 1 ELSE 0 END AS has_discount FROM orders o WHERE o.cart_id IS NOT NULL ) SELECT b.duration_bucket AS duration_bucket, COUNT(*) AS cart_count, ROUND(AVG(b.item_count)::numeric, 2) AS avg_item_count, ROUND(AVG(b.subtotal_eur)::numeric, 2) AS avg_subtotal_eur, ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY b.subtotal_eur)::numeric, 2) AS median_subtotal_eur, ROUND(100.0 * COUNT(*) FILTER (WHERE b.status='converted') / COUNT(*), 1) AS pct_converted, ROUND(100.0 * COUNT(*) FILTER (WHERE b.status='converted' AND odf.has_discount=1) / NULLIF(COUNT(*) FILTER (WHERE b.status='converted'),0), 1) AS pct_converted_with_discount FROM bucketed b LEFT JOIN order_discount_flag odf ON odf.cart_id = b.id GROUP BY b.duration_bucket ORDER BY CASE b.duration_bucket WHEN '<1 hour' THEN 1 WHEN '1-24 hours' THEN 2 WHEN '1-7 days' THEN 3 WHEN '>7 days' THEN 4 WHEN 'unresolved/still active' THEN 5 END
Submitted to tonight's batch. Your work on this request is done.