DB Olympics

Queries · q274 · Head of Merchant Success · week 2, night 4

Refund friction and what follows: fast vs slow refunds

“Where refunds take longer to process, do shoppers rate us worse and come back less often afterward?”

Manager · asked the questionSamuel WhitcombeHead of Merchant Success. New in the role, owns the relationship with the largest merchants and quarterly business reviews with them in January.
Analyst · planned the reportArjun PatelSegmenter: breaks totals down by merchant size, vertical, country and channel.
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 Head of Merchant Success's desk

    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.

  2. Samuel Whitcombe writes to Arjun Patel

    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:

    • Castell Fasteners is a contradiction — our #4 merchant, highly sticky customer base (88.6% repeat), but the weakest YoY growth of the top tier and a steep traffic drop. I want to know whether that traffic drop is a real warning sign or just how B2B industrial buying seasonally looks, before I raise it with them in their QBR.
    • Good catch on the refund-rate math — please don't let anything with the >100% problem anywhere near a client-facing page until it's fixed.

    My questions for this week:

    1. Is Castell's traffic decline a real risk or seasonal noise? I want a clear enough answer that I either flag it to them directly in the QBR or leave it alone.
    2. How are our top ~15 merchants' shoppers being supported, and does it show up in how those merchants feel about us? This was cut last week — please pick it back up.
    3. For the task force below: which merchants have actually left the platform (or are clearly heading that way), and what did their last few months look like beforehand — their sales trend, their shoppers' buying behavior, how much support friction they had? I'm less interested in a single root cause and more in whatever pattern shows up consistently across the merchants who left, since that's what would let us catch the next one early.

    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

    A note from the VP Customer Experience, Daniel Achterberg

    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:

    1. Does ticket experience precede merchant churn? For merchants who later left, did their shoppers see slower first responses, more reopened tickets, or more escalations in the months before? I'd want to know how far out a warning shows up.
    2. Do returns/refunds friction and churn move together? Where refunds get delayed, disputed, or partially handled, do we see both unhappy shoppers (low ratings/CSAT) and merchants churning shortly after? I'm curious whether refund delay is a leading indicator or just a symptom.
    3. Are complaint themes shifting before a merchant leaves? Is there a recognizable pattern in what shoppers are complaining about (e.g. shipping, product quality, billing) in the weeks before a merchant churns, versus a stable merchant?

    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

    A note from the Head of Growth Marketing, Chloé Marchetti

    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:

    1. Do shoppers who stop engaging with our marketing touches (emails, campaigns, loyalty) churn sooner than the ones who stay engaged? If disengagement shows up weeks before a churn, that's an early signal worth acting on.
    2. Does a merchant's own marketing activity level relate to whether their shoppers stick around? I want to know if merchants who run fewer campaigns, promotions, or loyalty programs are the ones seeing higher shopper churn — that would tell us whether under-investment in marketing tools is part of the story.
    3. Is there a pattern in how promo- or discount-driven shoppers behave right before leaving — e.g. do they go quiet after a coupon or loyalty redemption, or does spend drop off without any further marketing response from us?

    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é

  3. Arjun Patel plans the night

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

    Key read from last night: no merchant with 20+ lifetime orders has gone silent for 180+ days (0 rows) - established merchants aren't quietly churning, narrowing real merchant churn to the 9 small/new accounts already found. Marketing engagement barely differs between shoppers who later lapse and those who stay active, and support response times before a lapse track the same general degradation active shoppers also saw (likely the holiday ticket surge other teams flagged) rather than a lapse-specific pattern - both look like weak signals so far. Tonight (last night): (A) loosen the silent-churn threshold to check for a softer "slowing down" tier among established merchants, (B) test the refund-friction hypothesis directly, and (C) finally deliver the deferred top-15 support/CSAT read.

    Refund friction and what follows: fast vs slow refunds
    Question
    Where refunds take longer to process, do shoppers rate us worse and come back less often afterward?
    One row per
    refund-speed group (fast: processed within 7 days; slow: more than 7 days)
    Columns
    • group
    • refund count
    • avg processing days
    • pct with another order within 90 days after
    • avg csat score within 30 days after
  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 refund_base AS (
      SELECT
        t_pr.id AS refund_id,
        t_pr.tenant_id AS tenant_id,
        t_o.id AS order_id,
        t_o.user_id AS user_id,
        t_pr.created_at AS created_at,
        t_pr.processed_at AS processed_at,
        CASE
          WHEN (t_pr.processed_at - t_pr.created_at) <= INTERVAL '7 days' THEN 'fast'
          ELSE 'slow'
        END AS refund_group
      FROM payment_refunds t_pr
      JOIN payments t_pm ON t_pm.id = t_pr.payment_id
      JOIN orders t_o ON t_o.id = t_pm.order_id
      WHERE t_pr.status = 'succeeded'
        AND t_pr.processed_at IS NOT NULL
        AND t_pr.created_at IS NOT NULL
    ),
    grain AS (
      SELECT DISTINCT t0.refund_group AS refund_group
      FROM refund_base t0
    ),
    m_count_days AS (
      SELECT
        t1.refund_group AS refund_group,
        COUNT(*) AS refund_count,
        AVG(EXTRACT(EPOCH FROM (t1.processed_at - t1.created_at)) / 86400.0) AS avg_processing_days
      FROM refund_base t1
      GROUP BY t1.refund_group
    ),
    repeat_flags AS (
      SELECT
        t2.refund_id AS refund_id,
        t2.refund_group AS refund_group,
        CASE WHEN EXISTS (
          SELECT 1
          FROM orders t2o
          WHERE t2o.tenant_id = t2.tenant_id
            AND t2o.user_id = t2.user_id
            AND t2o.id <> t2.order_id
            AND t2o.status <> 'cancelled'
            AND t2o.placed_at IS NOT NULL
            AND t2o.placed_at BETWEEN t2.processed_at AND (t2.processed_at + INTERVAL '90 days')
        ) THEN 1 ELSE 0 END AS has_repeat_order
      FROM refund_base t2
    ),
    m_repeat AS (
      SELECT
        t3.refund_group AS refund_group,
        AVG(t3.has_repeat_order) * 100.0 AS pct_with_repeat_order
      FROM repeat_flags t3
      GROUP BY t3.refund_group
    ),
    csat_per_refund AS (
      SELECT
        t4.refund_id AS refund_id,
        t4.refund_group AS refund_group,
        (
          SELECT AVG(t4c.score)
          FROM csat_responses t4c
          WHERE t4c.tenant_id = t4.tenant_id
            AND t4c.respondent_user_id = t4.user_id
            AND t4c.submitted_at BETWEEN t4.processed_at AND (t4.processed_at + INTERVAL '30 days')
        ) AS csat_avg_for_refund
      FROM refund_base t4
    ),
    m_csat AS (
      SELECT
        t5.refund_group AS refund_group,
        AVG(t5.csat_avg_for_refund) AS avg_csat_score
      FROM csat_per_refund t5
      GROUP BY t5.refund_group
    )
    SELECT
      t_g.refund_group AS refund_speed_group,
      COALESCE(t_cd.refund_count, 0) AS refund_count,
      ROUND(COALESCE(t_cd.avg_processing_days, 0), 2) AS avg_processing_days,
      ROUND(COALESCE(t_rp.pct_with_repeat_order, 0), 2) AS pct_with_order_within_90d,
      ROUND(COALESCE(t_cs.avg_csat_score, 0), 2) AS avg_csat_within_30d
    FROM grain t_g
    LEFT JOIN m_count_days t_cd ON t_cd.refund_group = t_g.refund_group
    LEFT JOIN m_repeat t_rp ON t_rp.refund_group = t_g.refund_group
    LEFT JOIN m_csat t_cs ON t_cs.refund_group = t_g.refund_group
    ORDER BY t_g.refund_group

    “Query compiles and returns exactly the requested columns at the fast/slow refund-speed grain.”

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

    “Submitted. The report has one row each for "fast" (≤7 days) and "slow" (>7 days) refund groups, with refund count, average processing days, the share of refund events followed by another non-cancelled order within 90 days, and the average CSAT score within 30 days after — letting Arjun see directly whether slower refunds correlate with worse satisfaction or lower repeat-purchase rates.”

  5. The report comes back overnight

    As Arjun Patel saw it, from the small dataset the agents worked on

    refund_speed_group | refund_count | avg_processing_days | pct_with_order_within_90d | avg_csat_within_30d
    -------------------+--------------+---------------------+---------------------------+--------------------
    fast               |         8035 |                0.04 |                     48.18 |                2.74
    (1 rows)
  6. It becomes benchmark query q274

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

    • numericsdocs/NUMERICS.md: AVG over an integer computed as a decimal: AVG(CAST(x AS NUMERIC(18,6)))
    • numericsdocs/NUMERICS.md: AVG over an integer computed as a decimal: AVG(CAST(x AS NUMERIC(18,6)))
    • numericsdocs/NUMERICS.md: rounding removed; results keep full precision
    • numericsdocs/NUMERICS.md: EXTRACT(EPOCH FROM (b - a)) cast to NUMERIC(18,6)
    • 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
    The benchmark text, against the submitted SQL
    WITH refund_base AS (  SELECT    t_pr.id AS refund_id,    t_pr.tenant_id AS tenant_id,    t_o.id AS order_id,    t_o.user_id AS user_id,    t_pr.created_at AS created_at,    t_pr.processed_at AS processed_at,    CASE      WHEN (t_pr.processed_at - t_pr.created_at) <= INTERVAL '7 days' THEN 'fast'      ELSE 'slow'    END AS refund_group  FROM payment_refunds t_pr  JOIN payments t_pm ON t_pm.id = t_pr.payment_id  JOIN orders t_o ON t_o.id = t_pm.order_id  WHERE t_pr.status = 'succeeded'    AND t_pr.processed_at IS NOT NULL    AND t_pr.created_at IS NOT NULL),grain AS (  SELECT DISTINCT t0.refund_group AS refund_group  FROM refund_base t0),m_count_days AS (  SELECT    t1.refund_group AS refund_group,    COUNT(*) AS refund_count,    AVG(EXTRACT(EPOCH FROM (t1.processed_at - t1.created_at)) / 86400.0) AS avg_processing_days    AVG({{seconds_between(t1.created_at, t1.processed_at)}} / 86400.0) AS avg_processing_days  FROM refund_base t1  GROUP BY t1.refund_group),repeat_flags AS (  SELECT    t2.refund_id AS refund_id,    t2.refund_group AS refund_group,    CASE WHEN EXISTS (      SELECT 1      FROM orders t2o      WHERE t2o.tenant_id = t2.tenant_id        AND t2o.user_id = t2.user_id        AND t2o.id <> t2.order_id        AND t2o.status <> 'cancelled'        AND t2o.placed_at IS NOT NULL        AND t2o.placed_at BETWEEN t2.processed_at AND (t2.processed_at + INTERVAL '90 days')    ) THEN 1 ELSE 0 END AS has_repeat_order  FROM refund_base t2),m_repeat AS (  SELECT    t3.refund_group AS refund_group,    AVG(t3.has_repeat_order) * 100.0 AS pct_with_repeat_order    AVG(CAST(t3.has_repeat_order AS NUMERIC(18,6))) * 100.0 AS pct_with_repeat_order  FROM repeat_flags t3  GROUP BY t3.refund_group),csat_per_refund AS (  SELECT    t4.refund_id AS refund_id,    t4.refund_group AS refund_group,    (      SELECT AVG(t4c.score)      SELECT AVG(CAST(t4c.score AS NUMERIC(18,6)))      FROM csat_responses t4c      WHERE t4c.tenant_id = t4.tenant_id        AND t4c.respondent_user_id = t4.user_id        AND t4c.submitted_at BETWEEN t4.processed_at AND (t4.processed_at + INTERVAL '30 days')    ) AS csat_avg_for_refund  FROM refund_base t4),m_csat AS (  SELECT    t5.refund_group AS refund_group,    AVG(t5.csat_avg_for_refund) AS avg_csat_score  FROM csat_per_refund t5  GROUP BY t5.refund_group)SELECT  t_g.refund_group AS refund_speed_group,  COALESCE(t_cd.refund_count, 0) AS refund_count,  ROUND(COALESCE(t_cd.avg_processing_days, 0), 2) AS avg_processing_days,  ROUND(COALESCE(t_rp.pct_with_repeat_order, 0), 2) AS pct_with_order_within_90d,  ROUND(COALESCE(t_cs.avg_csat_score, 0), 2) AS avg_csat_within_30d  COALESCE(t_cd.avg_processing_days, 0) AS avg_processing_days,  COALESCE(t_rp.pct_with_repeat_order, 0) AS pct_with_order_within_90d,  COALESCE(t_cs.avg_csat_score, 0) AS avg_csat_within_30dFROM grain t_gLEFT JOIN m_count_days t_cd ON t_cd.refund_group = t_g.refund_groupLEFT JOIN m_repeat t_rp ON t_rp.refund_group = t_g.refund_groupLEFT JOIN m_csat t_cs ON t_cs.refund_group = t_g.refund_groupORDER BY t_g.refund_groupORDER BY t_g.refund_group NULLS LAST
    WITH refund_base AS (
      SELECT
        t_pr.id AS refund_id,
        t_pr.tenant_id AS tenant_id,
        t_o.id AS order_id,
        t_o.user_id AS user_id,
        t_pr.created_at AS created_at,
        t_pr.processed_at AS processed_at,
        CASE
          WHEN (t_pr.processed_at - t_pr.created_at) <= INTERVAL '7 days' THEN 'fast'
          ELSE 'slow'
        END AS refund_group
      FROM payment_refunds t_pr
      JOIN payments t_pm ON t_pm.id = t_pr.payment_id
      JOIN orders t_o ON t_o.id = t_pm.order_id
      WHERE t_pr.status = 'succeeded'
        AND t_pr.processed_at IS NOT NULL
        AND t_pr.created_at IS NOT NULL
    ),
    grain AS (
      SELECT DISTINCT t0.refund_group AS refund_group
      FROM refund_base t0
    ),
    m_count_days AS (
      SELECT
        t1.refund_group AS refund_group,
        COUNT(*) AS refund_count,
        AVG(CAST(EXTRACT(EPOCH FROM (t1.processed_at - t1.created_at)) AS NUMERIC(18,6)) / 86400.0) AS avg_processing_days
      FROM refund_base t1
      GROUP BY t1.refund_group
    ),
    repeat_flags AS (
      SELECT
        t2.refund_id AS refund_id,
        t2.refund_group AS refund_group,
        CASE WHEN EXISTS (
          SELECT 1
          FROM orders t2o
          WHERE t2o.tenant_id = t2.tenant_id
            AND t2o.user_id = t2.user_id
            AND t2o.id <> t2.order_id
            AND t2o.status <> 'cancelled'
            AND t2o.placed_at IS NOT NULL
            AND t2o.placed_at BETWEEN t2.processed_at AND (t2.processed_at + INTERVAL '90 days')
        ) THEN 1 ELSE 0 END AS has_repeat_order
      FROM refund_base t2
    ),
    m_repeat AS (
      SELECT
        t3.refund_group AS refund_group,
        AVG(CAST(t3.has_repeat_order AS NUMERIC(18,6))) * 100.0 AS pct_with_repeat_order
      FROM repeat_flags t3
      GROUP BY t3.refund_group
    ),
    csat_per_refund AS (
      SELECT
        t4.refund_id AS refund_id,
        t4.refund_group AS refund_group,
        (
          SELECT AVG(CAST(t4c.score AS NUMERIC(18,6)))
          FROM csat_responses t4c
          WHERE t4c.tenant_id = t4.tenant_id
            AND t4c.respondent_user_id = t4.user_id
            AND t4c.submitted_at BETWEEN t4.processed_at AND (t4.processed_at + INTERVAL '30 days')
        ) AS csat_avg_for_refund
      FROM refund_base t4
    ),
    m_csat AS (
      SELECT
        t5.refund_group AS refund_group,
        AVG(t5.csat_avg_for_refund) AS avg_csat_score
      FROM csat_per_refund t5
      GROUP BY t5.refund_group
    )
    SELECT
      t_g.refund_group AS refund_speed_group,
      COALESCE(t_cd.refund_count, 0) AS refund_count,
      COALESCE(t_cd.avg_processing_days, 0) AS avg_processing_days,
      COALESCE(t_rp.pct_with_repeat_order, 0) AS pct_with_order_within_90d,
      COALESCE(t_cs.avg_csat_score, 0) AS avg_csat_within_30d
    FROM grain t_g
    LEFT JOIN m_count_days t_cd ON t_cd.refund_group = t_g.refund_group
    LEFT JOIN m_repeat t_rp ON t_rp.refund_group = t_g.refund_group
    LEFT JOIN m_csat t_cs ON t_cs.refund_group = t_g.refund_group
    ORDER BY t_g.refund_group NULLS LAST
    100 ms1 s10 sHyper494 msDuckDB514 msGizmoSQL408 msclickhouse_brew5.48 sFirebolt1.94 sDataFusionerrorClickHouseerror
    • DataFusionThis feature is not implemented: Physical plan does not support logical expression ScalarSubquery(<subquery>)
    • ClickHouseCode: 43. DB::Exception: No operation lessOrEquals between Decimal(18, 6) and IntervalDay: while executing 'FUNCTION lessOrEquals(minus(__table25.processed_at, __table25.created_at) :: 8, _CAST(7_IntervalDay, 'IntervalDay'_String) :: 4) -> lessOrEquals(minus(__table25.processed_at, __table25.created
  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

    Bea's whole session, 7 turns
    • The request, as Bea received it
      Report request from Arjun Patel (analyst for the Head of Merchant Success):
      
      Title: Refund friction and what follows: fast vs slow refunds
      Question: Where refunds take longer to process, do shoppers rate us worse and come back less often afterward?
      Grain: one row per refund-speed group (fast: processed within 7 days; slow: more than 7 days)
      Columns:
      - group
      - refund count
      - avg processing days
      - pct with another order within 90 days after
      - avg csat score within 30 days after
      Details: From payment_refunds (status equal to succeeded, created_at, processed_at), joined via payments to orders to get user_id. processing days is processed_at minus created_at; group is fast if 7 or fewer days, slow if more. pct with another order within 90 days after is the share of refund events where that user placed another order (excluding cancelled) with placed_at between processed_at and processed_at plus 90 days. avg csat score within 30 days after is the average csat_responses.score where submitted_at is between processed_at and processed_at plus 30 days for that user. Order by group. Expect 2 rows.
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Bea calls dry_run
      WITH refund_base AS (
        SELECT
          t_pr.id AS refund_id,
          t_pr.tenant_id AS tenant_id,
          t_o.id AS order_id,
          t_o.user_id AS user_id,
          t_pr.created_at AS created_at,
          t_pr.processed_at AS processed_at,
          CASE
            WHEN (t_pr.processed_at - t_pr.created_at) <= INTERVAL '7 days' THEN 'fast'
            ELSE 'slow'
          END AS refund_group
        FROM payment_refunds t_pr
        JOIN payments t_pm ON t_pm.id = t_pr.payment_id
        JOIN orders t_o ON t_o.id = t_pm.order_id
        WHERE t_pr.status = 'succeeded'
          AND t_pr.processed_at IS NOT NULL
          AND t_pr.created_at IS NOT NULL
      ),
      grain AS (
        SELECT DISTINCT t0.refund_group AS refund_group
        FROM refund_base t0
      ),
      m_count_days AS (
        SELECT
          t1.refund_group AS refund_group,
          COUNT(*) AS refund_count,
          AVG(EXTRACT(EPOCH FROM (t1.processed_at - t1.created_at)) / 86400.0) AS avg_processing_days
        FROM refund_base t1
        GROUP BY t1.refund_group
      ),
      repeat_flags AS (
        SELECT
          t2.refund_id AS refund_id,
          t2.refund_group AS refund_group,
          CASE WHEN EXISTS (
            SELECT 1
            FROM orders t2o
            WHERE t2o.tenant_id = t2.tenant_id
              AND t2o.user_id = t2.user_id
              AND t2o.id <> t2.order_id
              AND t2o.status <> 'cancelled'
              AND t2o.placed_at IS NOT NULL
              AND t2o.placed_at BETWEEN t2.processed_at AND (t2.processed_at + INTERVAL '90 days')
          ) THEN 1 ELSE 0 END AS has_repeat_order
        FROM refund_base t2
      ),
      m_repeat AS (
        SELECT
          t3.refund_group AS refund_group,
          AVG(t3.has_repeat_order) * 100.0 AS pct_with_repeat_order
        FROM repeat_flags t3
        GROUP BY t3.refund_group
      ),
      csat_per_refund AS (
        SELECT
          t4.refund_id AS refund_id,
          t4.refund_group AS refund_group,
          (
            SELECT AVG(t4c.score)
            FROM csat_responses t4c
            WHERE t4c.tenant_id = t4.tenant_id
              AND t4c.respondent_user_id = t4.user_id
              AND t4c.submitted_at BETWEEN t4.processed_at AND (t4.processed_at + INTERVAL '30 days')
          ) AS csat_avg_for_refund
        FROM refund_base t4
      ),
      m_csat AS (
        SELECT
          t5.refund_group AS refund_group,
          AVG(t5.csat_avg_for_refund) AS avg_csat_score
        FROM csat_per_refund t5
        GROUP BY t5.refund_group
      )
      SELECT
        t_g.refund_group AS refund_speed_group,
        COALESCE(t_cd.refund_count, 0) AS refund_count,
        ROUND(COALESCE(t_cd.avg_processing_days, 0), 2) AS avg_processing_days,
        ROUND(COALESCE(t_rp.pct_with_repeat_order, 0), 2) AS pct_with_order_within_90d,
        ROUND(COALESCE(t_cs.avg_csat_score, 0), 2) AS avg_csat_within_30d
      FROM grain t_g
      LEFT JOIN m_count_days t_cd ON t_cd.refund_group = t_g.refund_group
      LEFT JOIN m_repeat t_rp ON t_rp.refund_group = t_g.refund_group
      LEFT JOIN m_csat t_cs ON t_cs.refund_group = t_g.refund_group
      ORDER BY t_g.refund_group
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: refund_speed_group, refund_count, avg_processing_days, pct_with_order_within_90d, avg_csat_within_30d
    • BeaQuery compiles and returns exactly the requested columns at the fast/slow refund-speed grain.
    • Bea calls submit
      WITH refund_base AS (
        SELECT
          t_pr.id AS refund_id,
          t_pr.tenant_id AS tenant_id,
          t_o.id AS order_id,
          t_o.user_id AS user_id,
          t_pr.created_at AS created_at,
          t_pr.processed_at AS processed_at,
          CASE
            WHEN (t_pr.processed_at - t_pr.created_at) <= INTERVAL '7 days' THEN 'fast'
            ELSE 'slow'
          END AS refund_group
        FROM payment_refunds t_pr
        JOIN payments t_pm ON t_pm.id = t_pr.payment_id
        JOIN orders t_o ON t_o.id = t_pm.order_id
        WHERE t_pr.status = 'succeeded'
          AND t_pr.processed_at IS NOT NULL
          AND t_pr.created_at IS NOT NULL
      ),
      grain AS (
        SELECT DISTINCT t0.refund_group AS refund_group
        FROM refund_base t0
      ),
      m_count_days AS (
        SELECT
          t1.refund_group AS refund_group,
          COUNT(*) AS refund_count,
          AVG(EXTRACT(EPOCH FROM (t1.processed_at - t1.created_at)) / 86400.0) AS avg_processing_days
        FROM refund_base t1
        GROUP BY t1.refund_group
      ),
      repeat_flags AS (
        SELECT
          t2.refund_id AS refund_id,
          t2.refund_group AS refund_group,
          CASE WHEN EXISTS (
            SELECT 1
            FROM orders t2o
            WHERE t2o.tenant_id = t2.tenant_id
              AND t2o.user_id = t2.user_id
              AND t2o.id <> t2.order_id
              AND t2o.status <> 'cancelled'
              AND t2o.placed_at IS NOT NULL
              AND t2o.placed_at BETWEEN t2.processed_at AND (t2.processed_at + INTERVAL '90 days')
          ) THEN 1 ELSE 0 END AS has_repeat_order
        FROM refund_base t2
      ),
      m_repeat AS (
        SELECT
          t3.refund_group AS refund_group,
          AVG(t3.has_repeat_order) * 100.0 AS pct_with_repeat_order
        FROM repeat_flags t3
        GROUP BY t3.refund_group
      ),
      csat_per_refund AS (
        SELECT
          t4.refund_id AS refund_id,
          t4.refund_group AS refund_group,
          (
            SELECT AVG(t4c.score)
            FROM csat_responses t4c
            WHERE t4c.tenant_id = t4.tenant_id
              AND t4c.respondent_user_id = t4.user_id
              AND t4c.submitted_at BETWEEN t4.processed_at AND (t4.processed_at + INTERVAL '30 days')
          ) AS csat_avg_for_refund
        FROM refund_base t4
      ),
      m_csat AS (
        SELECT
          t5.refund_group AS refund_group,
          AVG(t5.csat_avg_for_refund) AS avg_csat_score
        FROM csat_per_refund t5
        GROUP BY t5.refund_group
      )
      SELECT
        t_g.refund_group AS refund_speed_group,
        COALESCE(t_cd.refund_count, 0) AS refund_count,
        ROUND(COALESCE(t_cd.avg_processing_days, 0), 2) AS avg_processing_days,
        ROUND(COALESCE(t_rp.pct_with_repeat_order, 0), 2) AS pct_with_order_within_90d,
        ROUND(COALESCE(t_cs.avg_csat_score, 0), 2) AS avg_csat_within_30d
      FROM grain t_g
      LEFT JOIN m_count_days t_cd ON t_cd.refund_group = t_g.refund_group
      LEFT JOIN m_repeat t_rp ON t_rp.refund_group = t_g.refund_group
      LEFT JOIN m_csat t_cs ON t_cs.refund_group = t_g.refund_group
      ORDER BY t_g.refund_group
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • BeaSubmitted. The report has one row each for "fast" (≤7 days) and "slow" (>7 days) refund groups, with refund count, average processing days, the share of refund events followed by another non-cancelled order within 90 days, and the average CSAT score within 30 days after — letting Arjun see directly whether slower refunds correlate with worse satisfaction or lower repeat-purchase rates.