DB Olympics

Queries · q221 · Chief Financial Officer · week 2, night 1

Refund Attribution Lag: Purchase Quarter vs Refund Quarter, 2025

“Does the 16-40% quarterly gap between payments-based and order_returns-based refund totals come from refunds simply landing in a later quarter than the original purchase, rather than from missing or double-counted data?”

Manager · asked the questionTobias LindqvistChief Financial Officer. New this month, previously finance director at a retailer. Preparing the 2025 annual accounts and the 2026 budget.
Analyst · planned the reportKofi MensahHypothesis-driven: states what he expects and designs reports that would prove him wrong.
Engineer · wrote the SQLBeaWrites SQL the way the company's BI tool generates it: verbose fully qualified aliases (t0, t1, ...), a subquery per measure joined back on the grain keys, COALESCE everywhere.

Fictional people at a fictional company, each played by a language-model agent. Who they are and what they are told

  1. A business event lands on the Chief Financial Officer's desk

    Week 2

    The external auditors start the 2025 year-end audit in two weeks. They will want to see that the money adds up across the whole chain: what shoppers paid, what was refunded or credited, what merchants received in payouts, what Tidewick kept, and that invoices agree with all of it. They will also trace sample transactions end to end.

  2. Tobias Lindqvist writes to Kofi Mensah

    The manager, at the start of week 2

    Subject: Good progress — but the audit clock is now the priority

    Kofi,

    Thanks — this is useful, and I appreciate you flagging the caveats rather than smoothing them over. A few reactions and then where I want to focus next week.

    What stood out:

    • The correction on the revenue report (off by roughly half, now fixed and cross-checked) is reassuring that you caught it, but it also makes me want a second pair of eyes before anything like this goes near the auditors.
    • The refund numbers disagreeing by 16–40% depending on which source we use is the kind of thing an auditor will ask about directly. I don't want to walk in with two different refund totals and no explanation.
    • Concentration easing as we add merchants is good context for the budget conversation — no urgent follow-up needed there this week.
    • I also saw the notes from pricing and risk this week — the free-delivery giveaway and the refund pattern on that welcome discount are small in isolation, but they're exactly the kind of "where did the money go" questions auditors and I will both ask, so I'd like a rough sense of scale rather than letting them sit as separate anecdotes.

    The big new driver: external audit starts in two weeks. They'll want to walk the full money trail — what a shopper paid, what was refunded or credited back, what the merchant was paid out, what we kept as our fee, and that it all agrees with the invoices and accounts — and they'll pick sample transactions and trace them start to finish. I'd rather we got ahead of that than react to it.

    Questions for this week, in priority order:

    1. Can we demonstrate, for a sample of transactions, that the money actually adds up end to end — shopper payment through refund/credit, payout to the merchant, and our retained fee — so we have something ready to hand the auditors rather than assembling it under time pressure?
    2. Which refund figure should I trust for the 2025 accounts, and why do the two methods disagree? I don't need the full reconciliation solved, but I need to understand the likely cause well enough to explain it credibly if asked.
    3. Where do things stand on getting our own plan-fee revenue into the picture, even approximately, so I'm not presenting half of 2025 revenue as the whole story?
    4. If there's room: roughly how much revenue or margin might be leaking through the control gaps your peers flagged this week (free deliveries to canceled customers, uncapped coupon reuse) — just enough for me to judge whether it's noise or something that belongs in front of the auditors too.

    Audit readiness (#1) is the one I most need — please make that the lead request even if it means FX and B2B receivables slip another week.

    Thanks, Tobias

  3. Kofi Mensah plans the night

    The analyst · week 2, night 1, request 2 of 3

    Audit readiness is explicitly the lead ask, so I'm using one dense report to walk a sample of real transactions end-to-end — payment → refund → merchant payout (gross/fee/net) → ledger balance — in one place, since that's literally what the auditors will do when they pick sample transactions. I folded the payout settlement check and the ledger tie-out into a single payment-grain report (12 columns, at the comfortable limit) rather than two separate reports, to leave room tonight for the other two priorities.

    For priority #2 (refund figure discrepancy), my working hypothesis going in is that this is a structural date-field mismatch rather than a data error: payments.amount_refunded gets bucketed by the original purchase's captured_at quarter, while order_returns.refund_total gets bucketed by refunded_at (when the refund actually happened) — and Yuki's finding that refunds typically land ~3 weeks after settlement supports refunds routinely crossing a quarter boundary. I'm testing that directly by measuring the lag between purchase quarter and refund quarter for 2025 returns, which should explain most of the 16-40% swings without needing a full reconciliation.

    For #4 (room permitting), I'm sizing the uncapped-coupon-reuse gap Oliver flagged, since "no usage/per-customer limit exists" is only a control gap in euros if people are actually exploiting it — I want €-at-risk from repeat redemptions by the same customer, comparable in kind to the €57k free-delivery number, so Tobias can judge scale before deciding if it goes to the auditors.

    #3 (plan-fee revenue) gets no slot tonight: I rechecked the schema and confirmed there is no table that could hold Tidewick's own platform-plan billing of merchants (plans/subscriptions/invoices here are each merchant's billing of their own customers) — this is a true data gap, not something a cleverer query can solve, so I'll report that status in prose and recommend sourcing it from the corporate billing/finance system instead.

    Refund Attribution Lag: Purchase Quarter vs Refund Quarter, 2025
    Question
    Does the 16-40% quarterly gap between payments-based and order_returns-based refund totals come from refunds simply landing in a later quarter than the original purchase, rather than from missing or double-counted data?
    One row per
    one lag category, for order_returns refunded in 2025
    Columns
    • lag_category ('same quarter as purchase', 'purchase 1 quarter earlier', 'purchase 2+ quarters earlier', 'no matching payment found')
    • return_count
    • refund_amount_eur
    • pct_of_total_2025_refund_amount
    • avg_lag_days (refunded_at minus the originating payment's captured_at)
  4. Bea writes the SQL

    The engineer · 1 dry run on the empty schema, then the submission

    Dry run 1 · STATUS: COMPILES (dry run on the empty schema, no data read)
    WITH base AS (
      SELECT t0.id AS return_id, t0.order_id, t0.refund_total, t0.currency_code, t0.refunded_at
      FROM order_returns t0
      WHERE t0.status = 'refunded'
        AND t0.refunded_at >= DATE '2025-01-01'
        AND t0.refunded_at < DATE '2026-01-01'
    ),
    first_payment AS (
      SELECT t1.order_id, MIN(t1.captured_at) AS first_captured_at
      FROM payments t1
      WHERE t1.captured_at IS NOT NULL
      GROUP BY t1.order_id
    ),
    joined AS (
      SELECT
        b.return_id,
        b.refund_total,
        b.currency_code,
        b.refunded_at,
        o.id AS order_id,
        fp.first_captured_at
      FROM base b
      LEFT JOIN orders o ON o.id = b.order_id
      LEFT JOIN first_payment fp ON fp.order_id = o.id
    ),
    rated AS (
      SELECT
        j.*,
        CASE WHEN j.currency_code = 'EUR' THEN 1.0
             ELSE er.rate END AS fx_rate
      FROM joined j
      LEFT JOIN exchange_rates er
        ON er.base_code = 'EUR'
       AND er.quote_code = j.currency_code
       AND er.as_of = CAST(j.refunded_at AS date)
    ),
    categorized AS (
      SELECT
        r.*,
        CASE WHEN r.fx_rate IS NULL OR r.fx_rate = 0 THEN NULL ELSE r.refund_total / r.fx_rate END AS refund_amount_eur,
        CASE WHEN r.first_captured_at IS NULL THEN NULL
             ELSE (EXTRACT(YEAR FROM r.refunded_at)*4 + EXTRACT(QUARTER FROM r.refunded_at))
                - (EXTRACT(YEAR FROM r.first_captured_at)*4 + EXTRACT(QUARTER FROM r.first_captured_at))
        END AS quarter_diff,
        CASE WHEN r.first_captured_at IS NULL THEN NULL
             ELSE EXTRACT(EPOCH FROM (r.refunded_at - r.first_captured_at)) / 86400.0
        END AS lag_days
      FROM rated r
    ),
    bucketed AS (
      SELECT
        c.*,
        CASE
          WHEN c.quarter_diff IS NULL THEN 'no matching payment found'
          WHEN c.quarter_diff <= 0 THEN 'same quarter as purchase'
          WHEN c.quarter_diff = 1 THEN 'purchase 1 quarter earlier'
          ELSE 'purchase 2+ quarters earlier'
        END AS lag_category
      FROM categorized c
    )
    SELECT
      b.lag_category,
      COUNT(*) AS return_count,
      ROUND(SUM(b.refund_amount_eur), 2) AS refund_amount_eur,
      ROUND(100.0 * SUM(b.refund_amount_eur) / SUM(SUM(b.refund_amount_eur)) OVER (), 2) AS pct_of_total_2025_refund_amount,
      ROUND(AVG(b.lag_days), 1) AS avg_lag_days
    FROM bucketed b
    GROUP BY b.lag_category
    ORDER BY return_count DESC, b.lag_category ASC

    “Query compiles and returns the expected columns. Submitting.”

    Submitted · the SQL of the last dry run, unchanged · Submitted to tonight's batch. Your work on this request is done.

    “Submitted. The report buckets 2025 refunded order_returns into four lag categories (same quarter, 1Q earlier, 2+Q earlier, no match), matching each return to its order's earliest captured payment, converting refund totals to EUR via the daily EUR→currency rate, and computing count, EUR amount, % of total, and average lag in days per bucket — which should let the CFO's team see how much of the payments-vs-returns gap is just timing lag versus genuinely unmatched data.”

  5. The report comes back overnight

    As Kofi Mensah saw it, from the small dataset the agents worked on

    lag_category               | return_count | refund_amount_eur | pct_of_total_2025_refund_amount | avg_lag_days
    ---------------------------+--------------+-------------------+---------------------------------+-------------
    same quarter as purchase   |         2529 |         366023.32 |                           78.55 |         21.1
    purchase 1 quarter earlier |          786 |          93035.48 |                           19.96 |         24.7
    no matching payment found  |            3 |           6937.12 |                            1.49 |         NULL
    (3 rows)
  6. What came of it

    The memo and the queries that followed; links found by title, so inferred

    It builds on:

  7. It becomes benchmark query q221

    Changed only where the benchmark needs the same result on every system

    • numericsdocs/NUMERICS.md: rounding removed; results keep full precision
    • numericsdocs/NUMERICS.md: EXTRACT(EPOCH FROM (b - a)) cast to NUMERIC(18,6)
    • numericsdocs/NUMERICS.md: x / exchange rate cast to NUMERIC(18,6)
    • made explicita selected t.col gets AS col, its standard name made explicit (11×)
    • placeholderseconds between
    • determinismthe standard leaves the place of NULLs in a sort to the implementation: NULLS LAST ascending, NULLS FIRST descending, as in the reference results (2×)
    The benchmark text, against the submitted SQL
    WITH base AS (  SELECT t0.id AS return_id, t0.order_id, t0.refund_total, t0.currency_code, t0.refunded_at  SELECT t0.id AS return_id, t0.order_id AS order_id, t0.refund_total AS refund_total, t0.currency_code AS currency_code, t0.refunded_at AS refunded_at  FROM order_returns t0  WHERE t0.status = 'refunded'    AND t0.refunded_at >= DATE '2025-01-01'    AND t0.refunded_at < DATE '2026-01-01'),first_payment AS (  SELECT t1.order_id, MIN(t1.captured_at) AS first_captured_at  SELECT t1.order_id AS order_id, MIN(t1.captured_at) AS first_captured_at  FROM payments t1  WHERE t1.captured_at IS NOT NULL  GROUP BY t1.order_id),joined AS (  SELECT    b.return_id,    b.refund_total,    b.currency_code,    b.refunded_at,    b.return_id AS return_id,    b.refund_total AS refund_total,    b.currency_code AS currency_code,    b.refunded_at AS refunded_at,    o.id AS order_id,    fp.first_captured_at    fp.first_captured_at AS first_captured_at  FROM base b  LEFT JOIN orders o ON o.id = b.order_id  LEFT JOIN first_payment fp ON fp.order_id = o.id),rated AS (  SELECT    j.*,    CASE WHEN j.currency_code = 'EUR' THEN 1.0         ELSE er.rate END AS fx_rate  FROM joined j  LEFT JOIN exchange_rates er    ON er.base_code = 'EUR'   AND er.quote_code = j.currency_code   AND er.as_of = CAST(j.refunded_at AS date)),categorized AS (  SELECT    r.*,    CASE WHEN r.fx_rate IS NULL OR r.fx_rate = 0 THEN NULL ELSE r.refund_total / r.fx_rate END AS refund_amount_eur,    CASE WHEN r.fx_rate IS NULL OR r.fx_rate = 0 THEN NULL ELSE CAST(r.refund_total / r.fx_rate AS NUMERIC(18,6)) END AS refund_amount_eur,    CASE WHEN r.first_captured_at IS NULL THEN NULL         ELSE (EXTRACT(YEAR FROM r.refunded_at)*4 + EXTRACT(QUARTER FROM r.refunded_at))            - (EXTRACT(YEAR FROM r.first_captured_at)*4 + EXTRACT(QUARTER FROM r.first_captured_at))    END AS quarter_diff,    CASE WHEN r.first_captured_at IS NULL THEN NULL         ELSE EXTRACT(EPOCH FROM (r.refunded_at - r.first_captured_at)) / 86400.0         ELSE {{seconds_between(r.first_captured_at, r.refunded_at)}} / 86400.0    END AS lag_days  FROM rated r),bucketed AS (  SELECT    c.*,    CASE      WHEN c.quarter_diff IS NULL THEN 'no matching payment found'      WHEN c.quarter_diff <= 0 THEN 'same quarter as purchase'      WHEN c.quarter_diff = 1 THEN 'purchase 1 quarter earlier'      ELSE 'purchase 2+ quarters earlier'    END AS lag_category  FROM categorized c)SELECT  b.lag_category,  b.lag_category AS lag_category,  COUNT(*) AS return_count,  ROUND(SUM(b.refund_amount_eur), 2) AS refund_amount_eur,  ROUND(100.0 * SUM(b.refund_amount_eur) / SUM(SUM(b.refund_amount_eur)) OVER (), 2) AS pct_of_total_2025_refund_amount,  ROUND(AVG(b.lag_days), 1) AS avg_lag_days  SUM(b.refund_amount_eur) AS refund_amount_eur,  100.0 * SUM(b.refund_amount_eur) / SUM(SUM(b.refund_amount_eur)) OVER () AS pct_of_total_2025_refund_amount,  AVG(b.lag_days) AS avg_lag_daysFROM bucketed bGROUP BY b.lag_categoryORDER BY return_count DESC, b.lag_category ASCORDER BY return_count DESC NULLS FIRST, b.lag_category ASC NULLS LAST
    WITH base AS (
      SELECT t0.id AS return_id, t0.order_id AS order_id, t0.refund_total AS refund_total, t0.currency_code AS currency_code, t0.refunded_at AS refunded_at
      FROM order_returns t0
      WHERE t0.status = 'refunded'
        AND t0.refunded_at >= DATE '2025-01-01'
        AND t0.refunded_at < DATE '2026-01-01'
    ),
    first_payment AS (
      SELECT t1.order_id AS order_id, MIN(t1.captured_at) AS first_captured_at
      FROM payments t1
      WHERE t1.captured_at IS NOT NULL
      GROUP BY t1.order_id
    ),
    joined AS (
      SELECT
        b.return_id AS return_id,
        b.refund_total AS refund_total,
        b.currency_code AS currency_code,
        b.refunded_at AS refunded_at,
        o.id AS order_id,
        fp.first_captured_at AS first_captured_at
      FROM base b
      LEFT JOIN orders o ON o.id = b.order_id
      LEFT JOIN first_payment fp ON fp.order_id = o.id
    ),
    rated AS (
      SELECT
        j.*,
        CASE WHEN j.currency_code = 'EUR' THEN 1.0
             ELSE er.rate END AS fx_rate
      FROM joined j
      LEFT JOIN exchange_rates er
        ON er.base_code = 'EUR'
       AND er.quote_code = j.currency_code
       AND er.as_of = CAST(j.refunded_at AS date)
    ),
    categorized AS (
      SELECT
        r.*,
        CASE WHEN r.fx_rate IS NULL OR r.fx_rate = 0 THEN NULL ELSE CAST(r.refund_total / r.fx_rate AS NUMERIC(18,6)) END AS refund_amount_eur,
        CASE WHEN r.first_captured_at IS NULL THEN NULL
             ELSE (EXTRACT(YEAR FROM r.refunded_at)*4 + EXTRACT(QUARTER FROM r.refunded_at))
                - (EXTRACT(YEAR FROM r.first_captured_at)*4 + EXTRACT(QUARTER FROM r.first_captured_at))
        END AS quarter_diff,
        CASE WHEN r.first_captured_at IS NULL THEN NULL
             ELSE CAST(EXTRACT(EPOCH FROM (r.refunded_at - r.first_captured_at)) AS NUMERIC(18,6)) / 86400.0
        END AS lag_days
      FROM rated r
    ),
    bucketed AS (
      SELECT
        c.*,
        CASE
          WHEN c.quarter_diff IS NULL THEN 'no matching payment found'
          WHEN c.quarter_diff <= 0 THEN 'same quarter as purchase'
          WHEN c.quarter_diff = 1 THEN 'purchase 1 quarter earlier'
          ELSE 'purchase 2+ quarters earlier'
        END AS lag_category
      FROM categorized c
    )
    SELECT
      b.lag_category AS lag_category,
      COUNT(*) AS return_count,
      SUM(b.refund_amount_eur) AS refund_amount_eur,
      100.0 * SUM(b.refund_amount_eur) / SUM(SUM(b.refund_amount_eur)) OVER () AS pct_of_total_2025_refund_amount,
      AVG(b.lag_days) AS avg_lag_days
    FROM bucketed b
    GROUP BY b.lag_category
    ORDER BY return_count DESC NULLS FIRST, b.lag_category ASC NULLS LAST
    100 ms1 sHyper154 msDuckDB999 msGizmoSQL834 msclickhouse_brew689 msFirebolt959 msDataFusion410 msClickHouse518 ms
  8. Everything, as recorded

    The agents' own records, as the role play wrote them; only the personal data of the person who ran it is redacted

    Bea's whole session, 7 turns
    • The request, as Bea received it
      Report request from Kofi Mensah (analyst for the Chief Financial Officer):
      
      Title: Refund Attribution Lag: Purchase Quarter vs Refund Quarter, 2025
      Question: Does the 16-40% quarterly gap between payments-based and order_returns-based refund totals come from refunds simply landing in a later quarter than the original purchase, rather than from missing or double-counted data?
      Grain: one lag category, for order_returns refunded in 2025
      Columns:
      - lag_category ('same quarter as purchase', 'purchase 1 quarter earlier', 'purchase 2+ quarters earlier', 'no matching payment found')
      - return_count
      - refund_amount_eur
      - pct_of_total_2025_refund_amount
      - avg_lag_days (refunded_at minus the originating payment's captured_at)
      Details: Source: order_returns with status='refunded' and refunded_at in calendar year 2025, LEFT JOIN orders on order_returns.order_id, LEFT JOIN payments on payments.order_id = orders.id (if an order has multiple payments, use the earliest captured_at). Convert order_returns.refund_total to EUR via exchange_rates (base_code='EUR', quote_code=order_returns.currency_code, as_of=date(refunded_at)). Compute the calendar quarter of refunded_at and the calendar quarter of the matched payment's captured_at; bucket into the four lag_category values based on how many quarters apart they are (or 'no matching payment found' if the join fails). avg_lag_days is the average of (refunded_at - captured_at) in days within each bucket (null for the no-match bucket). Order by return_count descending.
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Bea calls dry_run
      WITH base AS (
        SELECT t0.id AS return_id, t0.order_id, t0.refund_total, t0.currency_code, t0.refunded_at
        FROM order_returns t0
        WHERE t0.status = 'refunded'
          AND t0.refunded_at >= DATE '2025-01-01'
          AND t0.refunded_at < DATE '2026-01-01'
      ),
      first_payment AS (
        SELECT t1.order_id, MIN(t1.captured_at) AS first_captured_at
        FROM payments t1
        WHERE t1.captured_at IS NOT NULL
        GROUP BY t1.order_id
      ),
      joined AS (
        SELECT
          b.return_id,
          b.refund_total,
          b.currency_code,
          b.refunded_at,
          o.id AS order_id,
          fp.first_captured_at
        FROM base b
        LEFT JOIN orders o ON o.id = b.order_id
        LEFT JOIN first_payment fp ON fp.order_id = o.id
      ),
      rated AS (
        SELECT
          j.*,
          CASE WHEN j.currency_code = 'EUR' THEN 1.0
               ELSE er.rate END AS fx_rate
        FROM joined j
        LEFT JOIN exchange_rates er
          ON er.base_code = 'EUR'
         AND er.quote_code = j.currency_code
         AND er.as_of = CAST(j.refunded_at AS date)
      ),
      categorized AS (
        SELECT
          r.*,
          CASE WHEN r.fx_rate IS NULL OR r.fx_rate = 0 THEN NULL ELSE r.refund_total / r.fx_rate END AS refund_amount_eur,
          CASE WHEN r.first_captured_at IS NULL THEN NULL
               ELSE (EXTRACT(YEAR FROM r.refunded_at)*4 + EXTRACT(QUARTER FROM r.refunded_at))
                  - (EXTRACT(YEAR FROM r.first_captured_at)*4 + EXTRACT(QUARTER FROM r.first_captured_at))
          END AS quarter_diff,
          CASE WHEN r.first_captured_at IS NULL THEN NULL
               ELSE EXTRACT(EPOCH FROM (r.refunded_at - r.first_captured_at)) / 86400.0
          END AS lag_days
        FROM rated r
      ),
      bucketed AS (
        SELECT
          c.*,
          CASE
            WHEN c.quarter_diff IS NULL THEN 'no matching payment found'
            WHEN c.quarter_diff <= 0 THEN 'same quarter as purchase'
            WHEN c.quarter_diff = 1 THEN 'purchase 1 quarter earlier'
            ELSE 'purchase 2+ quarters earlier'
          END AS lag_category
        FROM categorized c
      )
      SELECT
        b.lag_category,
        COUNT(*) AS return_count,
        ROUND(SUM(b.refund_amount_eur), 2) AS refund_amount_eur,
        ROUND(100.0 * SUM(b.refund_amount_eur) / SUM(SUM(b.refund_amount_eur)) OVER (), 2) AS pct_of_total_2025_refund_amount,
        ROUND(AVG(b.lag_days), 1) AS avg_lag_days
      FROM bucketed b
      GROUP BY b.lag_category
      ORDER BY return_count DESC, b.lag_category ASC
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: lag_category, return_count, refund_amount_eur, pct_of_total_2025_refund_amount, avg_lag_days
    • BeaQuery compiles and returns the expected columns. Submitting.
    • Bea calls submit
      WITH base AS (
        SELECT t0.id AS return_id, t0.order_id, t0.refund_total, t0.currency_code, t0.refunded_at
        FROM order_returns t0
        WHERE t0.status = 'refunded'
          AND t0.refunded_at >= DATE '2025-01-01'
          AND t0.refunded_at < DATE '2026-01-01'
      ),
      first_payment AS (
        SELECT t1.order_id, MIN(t1.captured_at) AS first_captured_at
        FROM payments t1
        WHERE t1.captured_at IS NOT NULL
        GROUP BY t1.order_id
      ),
      joined AS (
        SELECT
          b.return_id,
          b.refund_total,
          b.currency_code,
          b.refunded_at,
          o.id AS order_id,
          fp.first_captured_at
        FROM base b
        LEFT JOIN orders o ON o.id = b.order_id
        LEFT JOIN first_payment fp ON fp.order_id = o.id
      ),
      rated AS (
        SELECT
          j.*,
          CASE WHEN j.currency_code = 'EUR' THEN 1.0
               ELSE er.rate END AS fx_rate
        FROM joined j
        LEFT JOIN exchange_rates er
          ON er.base_code = 'EUR'
         AND er.quote_code = j.currency_code
         AND er.as_of = CAST(j.refunded_at AS date)
      ),
      categorized AS (
        SELECT
          r.*,
          CASE WHEN r.fx_rate IS NULL OR r.fx_rate = 0 THEN NULL ELSE r.refund_total / r.fx_rate END AS refund_amount_eur,
          CASE WHEN r.first_captured_at IS NULL THEN NULL
               ELSE (EXTRACT(YEAR FROM r.refunded_at)*4 + EXTRACT(QUARTER FROM r.refunded_at))
                  - (EXTRACT(YEAR FROM r.first_captured_at)*4 + EXTRACT(QUARTER FROM r.first_captured_at))
          END AS quarter_diff,
          CASE WHEN r.first_captured_at IS NULL THEN NULL
               ELSE EXTRACT(EPOCH FROM (r.refunded_at - r.first_captured_at)) / 86400.0
          END AS lag_days
        FROM rated r
      ),
      bucketed AS (
        SELECT
          c.*,
          CASE
            WHEN c.quarter_diff IS NULL THEN 'no matching payment found'
            WHEN c.quarter_diff <= 0 THEN 'same quarter as purchase'
            WHEN c.quarter_diff = 1 THEN 'purchase 1 quarter earlier'
            ELSE 'purchase 2+ quarters earlier'
          END AS lag_category
        FROM categorized c
      )
      SELECT
        b.lag_category,
        COUNT(*) AS return_count,
        ROUND(SUM(b.refund_amount_eur), 2) AS refund_amount_eur,
        ROUND(100.0 * SUM(b.refund_amount_eur) / SUM(SUM(b.refund_amount_eur)) OVER (), 2) AS pct_of_total_2025_refund_amount,
        ROUND(AVG(b.lag_days), 1) AS avg_lag_days
      FROM bucketed b
      GROUP BY b.lag_category
      ORDER BY return_count DESC, b.lag_category ASC
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • BeaSubmitted. The report buckets 2025 refunded `order_returns` into four lag categories (same quarter, 1Q earlier, 2+Q earlier, no match), matching each return to its order's earliest captured payment, converting refund totals to EUR via the daily EUR→currency rate, and computing count, EUR amount, % of total, and average lag in days per bucket — which should let the CFO's team see how much of the payments-vs-returns gap is just timing lag versus genuinely unmatched data.