DB Olympics

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

Coupon Reuse Exposure: Redemptions Beyond First Use per Customer, 2025

“Roughly how much discount euro value comes from the same customer reusing a coupon code multiple times, given no usage or per-customer limit is enforced?”

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 SQLOmarPragmatic: the shortest correct query, aggregates first and joins dimensions afterwards, FILTER clauses for conditional counts.

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 3 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.

    Coupon Reuse Exposure: Redemptions Beyond First Use per Customer, 2025
    Question
    Roughly how much discount euro value comes from the same customer reusing a coupon code multiple times, given no usage or per-customer limit is enforced?
    One row per
    one coupon code, for redemptions in 2025
    Columns
    • coupon_code
    • discount_kind
    • total_redemptions_2025
    • distinct_customers_2025
    • redemptions_beyond_first_use (total_redemptions - distinct_customers)
    • total_discount_amount_eur
    • discount_amount_beyond_first_use_eur (excess discount from a customer's 2nd+ redemption of the same code)
    • pct_of_discount_from_repeat_use
  4. Omar 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 redemptions as (
      select
        cr.coupon_id,
        c.code as coupon_code,
        d.kind as discount_kind,
        cr.user_id,
        cr.redeemed_at,
        cr.discount_amount / er.rate as discount_amount_eur,
        row_number() over (partition by cr.coupon_id, cr.user_id order by cr.redeemed_at asc, cr.id asc) as rn
      from coupon_redemptions cr
      join coupons c on c.id = cr.coupon_id
      join discounts d on d.id = c.discount_id
      join exchange_rates er
        on er.base_code = 'EUR'
       and er.quote_code = cr.currency_code
       and er.as_of = cast(cr.redeemed_at as date)
      where cr.redeemed_at >= timestamp '2025-01-01'
        and cr.redeemed_at < timestamp '2026-01-01'
    )
    select
      coupon_code,
      discount_kind,
      count(*) as total_redemptions_2025,
      count(distinct user_id) as distinct_customers_2025,
      count(*) - count(distinct user_id) as redemptions_beyond_first_use,
      round(sum(discount_amount_eur), 2) as total_discount_amount_eur,
      round(sum(discount_amount_eur) filter (where rn > 1), 2) as discount_amount_beyond_first_use_eur,
      round(100.0 * sum(discount_amount_eur) filter (where rn > 1) / nullif(sum(discount_amount_eur), 0), 2) as pct_of_discount_from_repeat_use
    from redemptions
    group by coupon_id, coupon_code, discount_kind
    order by discount_amount_beyond_first_use_eur desc nulls last, coupon_code asc
    limit 20
    Submitted · the SQL of the last dry run, unchanged · Submitted to tonight's batch. Your work on this request is done.

    “Submitted. Summary of what the query does: - Joins coupon_redemptions → coupons (for code) → discounts (for kind), restricted to redeemed_at in 2025. - Converts each redemption's discount_amount to EUR via exchange_rates (EUR→currency rate on the redemption date), as specified. - Uses ROW_NUMBER() per (coupon, user) ordered by redeemed_at to identify each customer's first vs. repeat redemption of a given coupon. - Aggregates per coupon: total redemptions, distinct customers, the repeat-use gap, total EUR discount, and the EUR discount attributable only to 2nd+ uses per customer (via FILTER (WHERE rn > 1)), plus that as a percentage of the coupon's total discount. - Orders by repeat-use discount exposure descending, capped at the top 20 rows as requested.”

  5. The report comes back overnight

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

    coupon_code | discount_kind | total_redemptions_2025 | distinct_customers_2025 | redemptions_beyond_first_use | total_discount_amount_eur | discount_amount_beyond_first_use_eur | pct_of_discount_from_repeat_use
    ------------+---------------+------------------------+-------------------------+------------------------------+---------------------------+--------------------------------------+--------------------------------
    BF2530      | percentage    |                     23 |                      22 |                            1 |                   3314.68 |                               224.96 |                            6.79
    BF2525      | percentage    |                     23 |                      19 |                            4 |                    964.81 |                               195.82 |                            20.3
    BF2515      | percentage    |                      4 |                       2 |                            2 |                    384.04 |                               133.82 |                           34.85
    BF2530      | percentage    |                     16 |                      14 |                            2 |                    651.53 |                               113.29 |                           17.39
    BF2525      | percentage    |                     31 |                      30 |                            1 |                   1135.09 |                                85.97 |                            7.57
    BF2530      | percentage    |                     11 |                       9 |                            2 |                    337.85 |                                75.31 |                           22.29
    BF2520      | percentage    |                     45 |                      41 |                            4 |                    682.36 |                                57.81 |                            8.47
    BF2525      | percentage    |                      2 |                       1 |                            1 |                     77.15 |                                56.89 |                           73.74
    BF2530      | percentage    |                      8 |                       6 |                            2 |                    105.36 |                                31.93 |                            30.3
    BF2530      | percentage    |                      6 |                       5 |                            1 |                    421.65 |                                15.96 |                            3.79
    BF2515      | percentage    |                     13 |                      12 |                            1 |                    189.91 |                                13.98 |                            7.36
    BF2520      | percentage    |                     20 |                      18 |                            2 |                    152.97 |                                 5.06 |                            3.31
    BF2530      | percentage    |                      4 |                       3 |                            1 |                     38.66 |                                 3.36 |                             8.7
    BF2515      | percentage    |                      2 |                       2 |                            0 |                      6.49 |                                 NULL |                            NULL
    BF2515      | percentage    |                      1 |                       1 |                            0 |                     23.66 |                                 NULL |                            NULL
    BF2515      | percentage    |                      8 |                       8 |                            0 |                     93.94 |                                 NULL |                            NULL
    BF2515      | percentage    |                      1 |                       1 |                            0 |                     23.73 |                                 NULL |                            NULL
    BF2515      | percentage    |                      1 |                       1 |                            0 |                     13.61 |                                 NULL |                            NULL
    BF2520      | percentage    |                      6 |                       6 |                            0 |                    212.02 |                                 NULL |                            NULL
    BF2520      | percentage    |                      1 |                       1 |                            0 |                     52.82 |                                 NULL |                            NULL
    (20 rows)
  6. It becomes benchmark query q222

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

    • spellingfetch first
    • determinismtie-breaker: coupon codes are unique only per tenant; coupon_id is the group key
    • numericsdocs/NUMERICS.md: rounding removed; results keep full precision
    • numericsdocs/NUMERICS.md: x / exchange rate cast to NUMERIC(18,6)
    • made explicita selected t.col gets AS col, its standard name made explicit (3×)
    • determinismthe standard leaves the place of NULLs in a sort to the implementation: NULLS LAST ascending, NULLS FIRST descending, as in the reference results (4×)
    • placeholderfetch first
    The benchmark text, against the submitted SQL
    with redemptions as (  select    cr.coupon_id,    cr.coupon_id AS coupon_id,    c.code as coupon_code,    d.kind as discount_kind,    cr.user_id,    cr.redeemed_at,    cr.discount_amount / er.rate as discount_amount_eur,    row_number() over (partition by cr.coupon_id, cr.user_id order by cr.redeemed_at asc, cr.id asc) as rn    cr.user_id AS user_id,    cr.redeemed_at AS redeemed_at,    CAST(cr.discount_amount / er.rate AS NUMERIC(18,6)) as discount_amount_eur,    row_number() over (partition by cr.coupon_id, cr.user_id order by cr.redeemed_at asc NULLS LAST, cr.id asc NULLS LAST) as rn  from coupon_redemptions cr  join coupons c on c.id = cr.coupon_id  join discounts d on d.id = c.discount_id  join exchange_rates er    on er.base_code = 'EUR'   and er.quote_code = cr.currency_code   and er.as_of = cast(cr.redeemed_at as date)  where cr.redeemed_at >= timestamp '2025-01-01'    and cr.redeemed_at < timestamp '2026-01-01')select  coupon_code,  discount_kind,  count(*) as total_redemptions_2025,  count(distinct user_id) as distinct_customers_2025,  count(*) - count(distinct user_id) as redemptions_beyond_first_use,  round(sum(discount_amount_eur), 2) as total_discount_amount_eur,  round(sum(discount_amount_eur) filter (where rn > 1), 2) as discount_amount_beyond_first_use_eur,  round(100.0 * sum(discount_amount_eur) filter (where rn > 1) / nullif(sum(discount_amount_eur), 0), 2) as pct_of_discount_from_repeat_use  sum(discount_amount_eur) as total_discount_amount_eur,  sum(discount_amount_eur) filter (where rn > 1) as discount_amount_beyond_first_use_eur,  100.0 * sum(discount_amount_eur) filter (where rn > 1) / nullif(sum(discount_amount_eur), 0) as pct_of_discount_from_repeat_usefrom redemptionsgroup by coupon_id, coupon_code, discount_kindorder by discount_amount_beyond_first_use_eur desc nulls last, coupon_code asclimit 20order by discount_amount_beyond_first_use_eur desc nulls last, coupon_code asc NULLS LAST, coupon_id asc NULLS LAST{{fetch_first(20)}}
    with redemptions as (
      select
        cr.coupon_id AS coupon_id,
        c.code as coupon_code,
        d.kind as discount_kind,
        cr.user_id AS user_id,
        cr.redeemed_at AS redeemed_at,
        CAST(cr.discount_amount / er.rate AS NUMERIC(18,6)) as discount_amount_eur,
        row_number() over (partition by cr.coupon_id, cr.user_id order by cr.redeemed_at asc NULLS LAST, cr.id asc NULLS LAST) as rn
      from coupon_redemptions cr
      join coupons c on c.id = cr.coupon_id
      join discounts d on d.id = c.discount_id
      join exchange_rates er
        on er.base_code = 'EUR'
       and er.quote_code = cr.currency_code
       and er.as_of = cast(cr.redeemed_at as date)
      where cr.redeemed_at >= timestamp '2025-01-01'
        and cr.redeemed_at < timestamp '2026-01-01'
    )
    select
      coupon_code,
      discount_kind,
      count(*) as total_redemptions_2025,
      count(distinct user_id) as distinct_customers_2025,
      count(*) - count(distinct user_id) as redemptions_beyond_first_use,
      sum(discount_amount_eur) as total_discount_amount_eur,
      sum(discount_amount_eur) filter (where rn > 1) as discount_amount_beyond_first_use_eur,
      100.0 * sum(discount_amount_eur) filter (where rn > 1) / nullif(sum(discount_amount_eur), 0) as pct_of_discount_from_repeat_use
    from redemptions
    group by coupon_id, coupon_code, discount_kind
    order by discount_amount_beyond_first_use_eur desc nulls last, coupon_code asc NULLS LAST, coupon_id asc NULLS LAST
    FETCH FIRST 20 ROWS ONLY
    10 ms100 ms1 sHyper65.2 msDuckDB58.7 msGizmoSQL57.2 msclickhouse_brew182 msFirebolterrorDataFusion284 msClickHouseerror
    • Firebolt{ "errors": [ { "description": "Line 8, Column 29: Operations between decimals with different precision and scale is not supported at the moment. Explicitly cast one of the decimals to other decimal precision and scale." } ], "query": { "query_id": "ec3fc3a9-ed19-45fe-834c-3cf82487d376", "query_labe
    • ClickHouseCode: 69. DB::Exception: Decimal result's scale is less than argument's one: while executing function divide on arguments __table2.discount_amount Decimal(14, 4) Decimal64(size = 0), __table5.rate Decimal(12, 6) Decimal64(size = 0). (ARGUMENT_OUT_OF_BOUND)
  7. 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

    Omar's whole session, 6 turns
    • The request, as Omar received it
      Report request from Kofi Mensah (analyst for the Chief Financial Officer):
      
      Title: Coupon Reuse Exposure: Redemptions Beyond First Use per Customer, 2025
      Question: Roughly how much discount euro value comes from the same customer reusing a coupon code multiple times, given no usage or per-customer limit is enforced?
      Grain: one coupon code, for redemptions in 2025
      Columns:
      - coupon_code
      - discount_kind
      - total_redemptions_2025
      - distinct_customers_2025
      - redemptions_beyond_first_use (total_redemptions - distinct_customers)
      - total_discount_amount_eur
      - discount_amount_beyond_first_use_eur (excess discount from a customer's 2nd+ redemption of the same code)
      - pct_of_discount_from_repeat_use
      Details: Source: coupon_redemptions joined to coupons (for code, and discount_id -> discounts for discount_kind), filtered to redeemed_at in calendar year 2025. Convert discount_amount to EUR via exchange_rates (base_code='EUR', quote_code=coupon_redemptions.currency_code, as_of=date(redeemed_at)). Group by coupon_id/code. distinct_customers_2025 counts distinct user_id redeeming that code in 2025. redemptions_beyond_first_use = total_redemptions_2025 - distinct_customers_2025 (i.e., repeat uses by the same customer beyond their first). discount_amount_beyond_first_use_eur: for each (coupon, user_id) pair, sum discount_amount_eur from all but the earliest redemption (by redeemed_at) for that user, then sum across users for the coupon. pct_of_discount_from_repeat_use = discount_amount_beyond_first_use_eur / total_discount_amount_eur * 100. Order by discount_amount_beyond_first_use_eur descending; print top 20 coupons and note total number of distinct coupons redeemed in 2025.
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Omar calls dry_run
      with redemptions as (
        select
          cr.coupon_id,
          c.code as coupon_code,
          d.kind as discount_kind,
          cr.user_id,
          cr.redeemed_at,
          cr.discount_amount / er.rate as discount_amount_eur,
          row_number() over (partition by cr.coupon_id, cr.user_id order by cr.redeemed_at asc, cr.id asc) as rn
        from coupon_redemptions cr
        join coupons c on c.id = cr.coupon_id
        join discounts d on d.id = c.discount_id
        join exchange_rates er
          on er.base_code = 'EUR'
         and er.quote_code = cr.currency_code
         and er.as_of = cast(cr.redeemed_at as date)
        where cr.redeemed_at >= timestamp '2025-01-01'
          and cr.redeemed_at < timestamp '2026-01-01'
      )
      select
        coupon_code,
        discount_kind,
        count(*) as total_redemptions_2025,
        count(distinct user_id) as distinct_customers_2025,
        count(*) - count(distinct user_id) as redemptions_beyond_first_use,
        round(sum(discount_amount_eur), 2) as total_discount_amount_eur,
        round(sum(discount_amount_eur) filter (where rn > 1), 2) as discount_amount_beyond_first_use_eur,
        round(100.0 * sum(discount_amount_eur) filter (where rn > 1) / nullif(sum(discount_amount_eur), 0), 2) as pct_of_discount_from_repeat_use
      from redemptions
      group by coupon_id, coupon_code, discount_kind
      order by discount_amount_beyond_first_use_eur desc nulls last, coupon_code asc
      limit 20
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: coupon_code, discount_kind, total_redemptions_2025, distinct_customers_2025, redemptions_beyond_first_use, total_discount_amount_eur, discount_amount_beyond_first_use_eur, pct_of_discount_from_repeat_use
    • Omar calls submit
      with redemptions as (
        select
          cr.coupon_id,
          c.code as coupon_code,
          d.kind as discount_kind,
          cr.user_id,
          cr.redeemed_at,
          cr.discount_amount / er.rate as discount_amount_eur,
          row_number() over (partition by cr.coupon_id, cr.user_id order by cr.redeemed_at asc, cr.id asc) as rn
        from coupon_redemptions cr
        join coupons c on c.id = cr.coupon_id
        join discounts d on d.id = c.discount_id
        join exchange_rates er
          on er.base_code = 'EUR'
         and er.quote_code = cr.currency_code
         and er.as_of = cast(cr.redeemed_at as date)
        where cr.redeemed_at >= timestamp '2025-01-01'
          and cr.redeemed_at < timestamp '2026-01-01'
      )
      select
        coupon_code,
        discount_kind,
        count(*) as total_redemptions_2025,
        count(distinct user_id) as distinct_customers_2025,
        count(*) - count(distinct user_id) as redemptions_beyond_first_use,
        round(sum(discount_amount_eur), 2) as total_discount_amount_eur,
        round(sum(discount_amount_eur) filter (where rn > 1), 2) as discount_amount_beyond_first_use_eur,
        round(100.0 * sum(discount_amount_eur) filter (where rn > 1) / nullif(sum(discount_amount_eur), 0), 2) as pct_of_discount_from_repeat_use
      from redemptions
      group by coupon_id, coupon_code, discount_kind
      order by discount_amount_beyond_first_use_eur desc nulls last, coupon_code asc
      limit 20
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • OmarSubmitted. Summary of what the query does: - Joins `coupon_redemptions` → `coupons` (for code) → `discounts` (for kind), restricted to `redeemed_at` in 2025. - Converts each redemption's `discount_amount` to EUR via `exchange_rates` (EUR→currency rate on the redemption date), as specified. - Uses `ROW_NUMBER()` per (coupon, user) ordered by `redeemed_at` to identify each customer's first vs. repeat redemption of a given coupon. - Aggregates per coupon: total redemptions, distinct customers, the repeat-use gap, total EUR discount, and the EUR discount attributable only to 2nd+ uses per customer (via `FILTER (WHERE rn > 1)`), plus that as a percentage of the coupon's total discount. - Orders by repeat-use discount exposure descending, capped at the top 20 rows as requested.