DB Olympics

Queries · q228 · Chief Financial Officer · week 2, night 3

No-Ledger-Entries Gap Trend by Month, 2025

“Is the small 'payment with zero ledger entries' gap an ongoing issue in current operations, or was it confined to earlier in the year and since resolved?”

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 SQLGraceWrites tidy, layered CTEs with descriptive names, one step per CTE, then a final select.

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 3, request 3 of 3

    Report 1 (ledger detail) actually resolves last night's big worry in Tidewick's favor: the "credit = payment + refund" pattern is correct double-entry accounting, not double-counted revenue. On capture: Sales revenue (credit) + VAT payable (credit) = Payment gateway clearing (debit). On refund: Refunds and returns/contra-revenue (debit) + VAT payable (debit) = Payment gateway clearing (credit) — e.g. Ampfeld Elektronik #1727 and every other refunded sample shows this exact structure, balanced to the cent. My "ledger_credit_total" diagnostic from Night 2 was simply summing credits across unrelated accounts (revenue credits at capture + clearing credits at refund), which is apples-to-oranges — the ledger itself is fine. Report 2's scale-up confirms this: 78.55% + 18.74% ≈ 97.3% of 2025's captured payments follow one of these two clean, explainable patterns. That's good news I can tell Tobias with confidence.

    Two smaller things still need a next step before I call #1 fully done: (a) "other mismatch" is 249 payments / €84,443 in payment value (2.1% of count) — Castell Fasteners' SO-100233 in the detail report hints these may be non-gateway payment methods (bank transfer) where VAT/revenue nets differently than my simple gross/net comparison expected, not a real error, but I want to confirm that before saying so; (b) "no ledger entries" is small in money (€3,380 across 67 payments, 0.58%) but is a genuine audit-trail gap spread across 31 merchants — worth knowing whether it's an old, fixed bug or still happening now, since "we found gaps but can't tell if they're ongoing" is a weak answer to give auditors.

    Tonight's three slots: characterize the "other mismatch" bucket by payment method (test the bank-transfer/B2B hypothesis); redo the revenue reconciliation using the correct ledger accounts (Sales/Shipping revenue credits minus Refunds-and-returns debits, compared to payments' net amount) so I have a trustworthy "yes, it ties out" statement built the right way instead of my flawed Night 2 shortcut; and trend the no-ledger-entries gap by month to see if it's historical or still occurring.

    No-Ledger-Entries Gap Trend by Month, 2025
    Question
    Is the small 'payment with zero ledger entries' gap an ongoing issue in current operations, or was it confined to earlier in the year and since resolved?
    One row per
    one calendar month, 2025
    Columns
    • month
    • total_payments
    • payments_without_ledger_entries
    • pct_without_ledger_entries
    • amount_at_risk_eur
  4. Grace writes the SQL

    The engineer · 2 dry runs on the empty schema, then the submission

    Dry run 1 · STATUS: REJECTED
    WITH payments_2025 AS (
      SELECT
        p.id,
        p.captured_at,
        p.amount,
        p.currency_code
      FROM payments p
      WHERE p.status IN ('captured', 'refunded', 'partially_refunded')
        AND p.captured_at >= DATE '2025-01-01'
        AND p.captured_at < DATE '2026-01-01'
    ),
    ledger_counts AS (
      SELECT
        le.payment_id,
        COUNT(*) AS entry_count
      FROM ledger_entries le
      WHERE le.payment_id IS NOT NULL
      GROUP BY le.payment_id
    ),
    payments_with_fx AS (
      SELECT
        p.id,
        p.captured_at,
        p.amount / er.rate AS amount_eur,
        COALESCE(lc.entry_count, 0) AS entry_count
      FROM payments_2025 p
      LEFT JOIN ledger_counts lc ON lc.payment_id = p.id
      LEFT JOIN exchange_rates er
        ON er.base_code = 'EUR'
       AND er.quote_code = p.currency_code
       AND er.as_of = DATE(p.captured_at)
    ),
    monthly AS (
      SELECT
        DATE_TRUNC('month', captured_at)::date AS month_start,
        COUNT(*) AS total_payments,
        SUM(CASE WHEN entry_count = 0 THEN 1 ELSE 0 END) AS payments_without_ledger_entries,
        SUM(CASE WHEN entry_count = 0 THEN amount_eur ELSE 0 END) AS amount_at_risk_eur
      FROM payments_with_fx
      GROUP BY 1
    )
    SELECT
      TO_CHAR(month_start, 'YYYY-MM') AS month,
      total_payments,
      payments_without_ledger_entries,
      ROUND(100.0 * payments_without_ledger_entries / total_payments, 2) AS pct_without_ledger_entries,
      ROUND(amount_at_risk_eur, 2) AS amount_at_risk_eur
    FROM monthly
    ORDER BY month_start
    Dry run 2 · 4 changes from the dry run before · STATUS: COMPILES (dry run on the empty schema, no data read)
    WITH payments_2025 AS (  SELECT    p.id,    p.captured_at,    p.amount,    p.currency_code  FROM payments p  WHERE p.status IN ('captured', 'refunded', 'partially_refunded')    AND p.captured_at >= DATE '2025-01-01'    AND p.captured_at < DATE '2026-01-01'),ledger_counts AS (  SELECT    le.payment_id,    COUNT(*) AS entry_count  FROM ledger_entries le  WHERE le.payment_id IS NOT NULL  GROUP BY le.payment_id),payments_with_fx AS (  SELECT    p.id,    p.captured_at,    p.amount / er.rate AS amount_eur,    COALESCE(lc.entry_count, 0) AS entry_count  FROM payments_2025 p  LEFT JOIN ledger_counts lc ON lc.payment_id = p.id  LEFT JOIN exchange_rates er    ON er.base_code = 'EUR'   AND er.quote_code = p.currency_code   AND er.as_of = DATE(p.captured_at)),payments_with_month AS (  SELECT    cd.month_label,    cd.month_start,    pf.amount_eur,    pf.entry_count  FROM payments_with_fx pf  JOIN calendar_days cd ON cd.calendar_date = DATE(pf.captured_at)),monthly AS (  SELECT    DATE_TRUNC('month', captured_at)::date AS month_start,    month_label,    month_start,    COUNT(*) AS total_payments,    SUM(CASE WHEN entry_count = 0 THEN 1 ELSE 0 END) AS payments_without_ledger_entries,    SUM(CASE WHEN entry_count = 0 THEN amount_eur ELSE 0 END) AS amount_at_risk_eur  FROM payments_with_fx  GROUP BY 1  FROM payments_with_month  GROUP BY month_label, month_start)SELECT  TO_CHAR(month_start, 'YYYY-MM') AS month,  month_label AS month,  total_payments,  payments_without_ledger_entries,  ROUND(100.0 * payments_without_ledger_entries / total_payments, 2) AS pct_without_ledger_entries,  ROUND(amount_at_risk_eur, 2) AS amount_at_risk_eurFROM monthlyORDER BY month_start

    “The 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 groups 2025 captured/refunded/partially_refunded payments by calendar month, left-joins a per-payment ledger-entry count, and flags payments with zero linked ledger entries. Amounts are converted to EUR using the day's EUR→payment-currency exchange rate before summing the at-risk total, giving Kofi a clean month-by-month trend to see whether the gap is ongoing or resolved.”

  5. The report comes back overnight

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

    month   | total_payments | payments_without_ledger_entries | pct_without_ledger_entries | amount_at_risk_eur
    --------+----------------+---------------------------------+----------------------------+-------------------
    2025-01 |           1749 |                              38 |                       2.17 |             1241.0
    2025-02 |           1324 |                              17 |                       1.28 |             606.08
    2025-03 |           1545 |                               6 |                       0.39 |             129.83
    2025-04 |           1487 |                               7 |                       0.47 |             323.67
    2025-05 |           1595 |                               7 |                       0.44 |             290.08
    2025-06 |           1435 |                               3 |                       0.21 |              97.52
    2025-07 |           1554 |                               2 |                       0.13 |             149.21
    2025-08 |           1455 |                               1 |                       0.07 |              52.47
    2025-09 |           1607 |                               4 |                       0.25 |              72.45
    2025-10 |           1766 |                               1 |                       0.06 |                0.0
    2025-11 |           2525 |                               3 |                       0.12 |              119.8
    2025-12 |           3011 |                              11 |                       0.37 |              298.5
    (12 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 q228

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

    • spellingdate cast (2×)
    • determinismtie-breaker: month_label is not a function of month_start by schema
    • 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 (11×)
    • 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 payments_2025 AS (  SELECT    p.id,    p.captured_at,    p.amount,    p.currency_code    p.id AS id,    p.captured_at AS captured_at,    p.amount AS amount,    p.currency_code AS currency_code  FROM payments p  WHERE p.status IN ('captured', 'refunded', 'partially_refunded')    AND p.captured_at >= DATE '2025-01-01'    AND p.captured_at < DATE '2026-01-01'),ledger_counts AS (  SELECT    le.payment_id,    le.payment_id AS payment_id,    COUNT(*) AS entry_count  FROM ledger_entries le  WHERE le.payment_id IS NOT NULL  GROUP BY le.payment_id),payments_with_fx AS (  SELECT    p.id,    p.captured_at,    p.amount / er.rate AS amount_eur,    p.id AS id,    p.captured_at AS captured_at,    CAST(p.amount / er.rate AS NUMERIC(18,6)) AS amount_eur,    COALESCE(lc.entry_count, 0) AS entry_count  FROM payments_2025 p  LEFT JOIN ledger_counts lc ON lc.payment_id = p.id  LEFT JOIN exchange_rates er    ON er.base_code = 'EUR'   AND er.quote_code = p.currency_code   AND er.as_of = DATE(p.captured_at)   AND er.as_of = CAST(p.captured_at AS DATE)),payments_with_month AS (  SELECT    cd.month_label,    cd.month_start,    pf.amount_eur,    pf.entry_count    cd.month_label AS month_label,    cd.month_start AS month_start,    pf.amount_eur AS amount_eur,    pf.entry_count AS entry_count  FROM payments_with_fx pf  JOIN calendar_days cd ON cd.calendar_date = DATE(pf.captured_at)  JOIN calendar_days cd ON cd.calendar_date = CAST(pf.captured_at AS DATE)),monthly AS (  SELECT    month_label,    month_start,    COUNT(*) AS total_payments,    SUM(CASE WHEN entry_count = 0 THEN 1 ELSE 0 END) AS payments_without_ledger_entries,    SUM(CASE WHEN entry_count = 0 THEN amount_eur ELSE 0 END) AS amount_at_risk_eur  FROM payments_with_month  GROUP BY month_label, month_start)SELECT  month_label AS month,  total_payments,  payments_without_ledger_entries,  ROUND(100.0 * payments_without_ledger_entries / total_payments, 2) AS pct_without_ledger_entries,  ROUND(amount_at_risk_eur, 2) AS amount_at_risk_eur  100.0 * payments_without_ledger_entries / total_payments AS pct_without_ledger_entries,  amount_at_risk_eur AS amount_at_risk_eurFROM monthlyORDER BY month_startORDER BY month_start NULLS LAST, month_label NULLS LAST
    WITH payments_2025 AS (
      SELECT
        p.id AS id,
        p.captured_at AS captured_at,
        p.amount AS amount,
        p.currency_code AS currency_code
      FROM payments p
      WHERE p.status IN ('captured', 'refunded', 'partially_refunded')
        AND p.captured_at >= DATE '2025-01-01'
        AND p.captured_at < DATE '2026-01-01'
    ),
    ledger_counts AS (
      SELECT
        le.payment_id AS payment_id,
        COUNT(*) AS entry_count
      FROM ledger_entries le
      WHERE le.payment_id IS NOT NULL
      GROUP BY le.payment_id
    ),
    payments_with_fx AS (
      SELECT
        p.id AS id,
        p.captured_at AS captured_at,
        CAST(p.amount / er.rate AS NUMERIC(18,6)) AS amount_eur,
        COALESCE(lc.entry_count, 0) AS entry_count
      FROM payments_2025 p
      LEFT JOIN ledger_counts lc ON lc.payment_id = p.id
      LEFT JOIN exchange_rates er
        ON er.base_code = 'EUR'
       AND er.quote_code = p.currency_code
       AND er.as_of = CAST(p.captured_at AS DATE)
    ),
    payments_with_month AS (
      SELECT
        cd.month_label AS month_label,
        cd.month_start AS month_start,
        pf.amount_eur AS amount_eur,
        pf.entry_count AS entry_count
      FROM payments_with_fx pf
      JOIN calendar_days cd ON cd.calendar_date = CAST(pf.captured_at AS DATE)
    ),
    monthly AS (
      SELECT
        month_label,
        month_start,
        COUNT(*) AS total_payments,
        SUM(CASE WHEN entry_count = 0 THEN 1 ELSE 0 END) AS payments_without_ledger_entries,
        SUM(CASE WHEN entry_count = 0 THEN amount_eur ELSE 0 END) AS amount_at_risk_eur
      FROM payments_with_month
      GROUP BY month_label, month_start
    )
    SELECT
      month_label AS month,
      total_payments,
      payments_without_ledger_entries,
      100.0 * payments_without_ledger_entries / total_payments AS pct_without_ledger_entries,
      amount_at_risk_eur AS amount_at_risk_eur
    FROM monthly
    ORDER BY month_start NULLS LAST, month_label NULLS LAST
    100 ms1 sHyper525 msDuckDB337 msGizmoSQL304 msclickhouse_brew964 msFirebolterrorDataFusion562 msClickHouseerror
    • Firebolt{ "errors": [ { "description": "Line 24, Column 19: 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": "f8273355-4dc6-4a62-8d7a-0298a21fcfb7", "query_lab
    • ClickHouseCode: 69. DB::Exception: Decimal result's scale is less than argument's one: while executing function divide on arguments __table4.amount Decimal(14, 4) Decimal64(size = 0), __table8.rate Nullable(Decimal(12, 6)) Nullable(size = 0, Decimal64(size = 0), UInt8(size = 0)). (ARGUMENT_OUT_OF_BOUND)
  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

    Grace's whole session, 9 turns
    • The request, as Grace received it
      Report request from Kofi Mensah (analyst for the Chief Financial Officer):
      
      Title: No-Ledger-Entries Gap Trend by Month, 2025
      Question: Is the small 'payment with zero ledger entries' gap an ongoing issue in current operations, or was it confined to earlier in the year and since resolved?
      Grain: one calendar month, 2025
      Columns:
      - month
      - total_payments
      - payments_without_ledger_entries
      - pct_without_ledger_entries
      - amount_at_risk_eur
      Details: Source: payments with status captured/refunded/partially_refunded and captured_at in 2025, LEFT JOIN to a per-payment count of linked ledger_entries via payment_id. Convert payment amounts to EUR via exchange_rates (base_code='EUR', quote_code=payments.currency_code, as_of=date(captured_at)). Group by calendar month of captured_at. amount_at_risk_eur sums payment_amount_eur only for payments with zero linked ledger_entries. Order chronologically 2025-01 to 2025-12 (12 rows).
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Grace calls dry_run
      WITH payments_2025 AS (
        SELECT
          p.id,
          p.captured_at,
          p.amount,
          p.currency_code
        FROM payments p
        WHERE p.status IN ('captured', 'refunded', 'partially_refunded')
          AND p.captured_at >= DATE '2025-01-01'
          AND p.captured_at < DATE '2026-01-01'
      ),
      ledger_counts AS (
        SELECT
          le.payment_id,
          COUNT(*) AS entry_count
        FROM ledger_entries le
        WHERE le.payment_id IS NOT NULL
        GROUP BY le.payment_id
      ),
      payments_with_fx AS (
        SELECT
          p.id,
          p.captured_at,
          p.amount / er.rate AS amount_eur,
          COALESCE(lc.entry_count, 0) AS entry_count
        FROM payments_2025 p
        LEFT JOIN ledger_counts lc ON lc.payment_id = p.id
        LEFT JOIN exchange_rates er
          ON er.base_code = 'EUR'
         AND er.quote_code = p.currency_code
         AND er.as_of = DATE(p.captured_at)
      ),
      monthly AS (
        SELECT
          DATE_TRUNC('month', captured_at)::date AS month_start,
          COUNT(*) AS total_payments,
          SUM(CASE WHEN entry_count = 0 THEN 1 ELSE 0 END) AS payments_without_ledger_entries,
          SUM(CASE WHEN entry_count = 0 THEN amount_eur ELSE 0 END) AS amount_at_risk_eur
        FROM payments_with_fx
        GROUP BY 1
      )
      SELECT
        TO_CHAR(month_start, 'YYYY-MM') AS month,
        total_payments,
        payments_without_ledger_entries,
        ROUND(100.0 * payments_without_ledger_entries / total_payments, 2) AS pct_without_ledger_entries,
        ROUND(amount_at_risk_eur, 2) AS amount_at_risk_eur
      FROM monthly
      ORDER BY month_start
    • The warehouse answers
      STATUS: REJECTED
        - does not compile: Catalog Error: Scalar Function with name to_char does not exist!
    • Grace calls dry_run
      WITH payments_2025 AS (
        SELECT
          p.id,
          p.captured_at,
          p.amount,
          p.currency_code
        FROM payments p
        WHERE p.status IN ('captured', 'refunded', 'partially_refunded')
          AND p.captured_at >= DATE '2025-01-01'
          AND p.captured_at < DATE '2026-01-01'
      ),
      ledger_counts AS (
        SELECT
          le.payment_id,
          COUNT(*) AS entry_count
        FROM ledger_entries le
        WHERE le.payment_id IS NOT NULL
        GROUP BY le.payment_id
      ),
      payments_with_fx AS (
        SELECT
          p.id,
          p.captured_at,
          p.amount / er.rate AS amount_eur,
          COALESCE(lc.entry_count, 0) AS entry_count
        FROM payments_2025 p
        LEFT JOIN ledger_counts lc ON lc.payment_id = p.id
        LEFT JOIN exchange_rates er
          ON er.base_code = 'EUR'
         AND er.quote_code = p.currency_code
         AND er.as_of = DATE(p.captured_at)
      ),
      payments_with_month AS (
        SELECT
          cd.month_label,
          cd.month_start,
          pf.amount_eur,
          pf.entry_count
        FROM payments_with_fx pf
        JOIN calendar_days cd ON cd.calendar_date = DATE(pf.captured_at)
      ),
      monthly AS (
        SELECT
          month_label,
          month_start,
          COUNT(*) AS total_payments,
          SUM(CASE WHEN entry_count = 0 THEN 1 ELSE 0 END) AS payments_without_ledger_entries,
          SUM(CASE WHEN entry_count = 0 THEN amount_eur ELSE 0 END) AS amount_at_risk_eur
        FROM payments_with_month
        GROUP BY month_label, month_start
      )
      SELECT
        month_label AS month,
        total_payments,
        payments_without_ledger_entries,
        ROUND(100.0 * payments_without_ledger_entries / total_payments, 2) AS pct_without_ledger_entries,
        ROUND(amount_at_risk_eur, 2) AS amount_at_risk_eur
      FROM monthly
      ORDER BY month_start
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: month, total_payments, payments_without_ledger_entries, pct_without_ledger_entries, amount_at_risk_eur
    • GraceThe query compiles and returns the expected columns. Submitting.
    • Grace calls submit
      WITH payments_2025 AS (
        SELECT
          p.id,
          p.captured_at,
          p.amount,
          p.currency_code
        FROM payments p
        WHERE p.status IN ('captured', 'refunded', 'partially_refunded')
          AND p.captured_at >= DATE '2025-01-01'
          AND p.captured_at < DATE '2026-01-01'
      ),
      ledger_counts AS (
        SELECT
          le.payment_id,
          COUNT(*) AS entry_count
        FROM ledger_entries le
        WHERE le.payment_id IS NOT NULL
        GROUP BY le.payment_id
      ),
      payments_with_fx AS (
        SELECT
          p.id,
          p.captured_at,
          p.amount / er.rate AS amount_eur,
          COALESCE(lc.entry_count, 0) AS entry_count
        FROM payments_2025 p
        LEFT JOIN ledger_counts lc ON lc.payment_id = p.id
        LEFT JOIN exchange_rates er
          ON er.base_code = 'EUR'
         AND er.quote_code = p.currency_code
         AND er.as_of = DATE(p.captured_at)
      ),
      payments_with_month AS (
        SELECT
          cd.month_label,
          cd.month_start,
          pf.amount_eur,
          pf.entry_count
        FROM payments_with_fx pf
        JOIN calendar_days cd ON cd.calendar_date = DATE(pf.captured_at)
      ),
      monthly AS (
        SELECT
          month_label,
          month_start,
          COUNT(*) AS total_payments,
          SUM(CASE WHEN entry_count = 0 THEN 1 ELSE 0 END) AS payments_without_ledger_entries,
          SUM(CASE WHEN entry_count = 0 THEN amount_eur ELSE 0 END) AS amount_at_risk_eur
        FROM payments_with_month
        GROUP BY month_label, month_start
      )
      SELECT
        month_label AS month,
        total_payments,
        payments_without_ledger_entries,
        ROUND(100.0 * payments_without_ledger_entries / total_payments, 2) AS pct_without_ledger_entries,
        ROUND(amount_at_risk_eur, 2) AS amount_at_risk_eur
      FROM monthly
      ORDER BY month_start
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • GraceSubmitted. The report groups 2025 captured/refunded/partially_refunded payments by calendar month, left-joins a per-payment ledger-entry count, and flags payments with zero linked ledger entries. Amounts are converted to EUR using the day's EUR→payment-currency exchange rate before summing the at-risk total, giving Kofi a clean month-by-month trend to see whether the gap is ongoing or resolved.