Skip to content

fix(transaction): surface refund processor fee in balance.refund event metadata - #14166

Closed
detail-app[bot] wants to merge 3 commits into
mainfrom
detail/bug-fix/fix-transaction-surface-refund-processor-fee-in-ba-7a3477
Closed

detail-app[bot] wants to merge 3 commits into
mainfrom
detail/bug-fix/fix-transaction-surface-refund-processor-fee-in-ba-7a3477

Conversation

@detail-app

@detail-app detail-app Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Detail bug report: View on Detail

Summary

Related Issue: polarsource/feedback#432

Surface the computed Stripe refund processor fee in the balance.refund system event metadata so the merchant-facing GET /v1/events/ and GET /v1/metrics/ (net revenue) endpoints report it correctly.

What

  • server/polar/transaction/service/refund.py: in RefundTransactionService.create, replaced the hardcoded "fee": 0 in the BalanceRefundMetadata with "fee": sum(-fee.amount for fee in transaction_fees).
  • server/tests/transaction/service/test_refund.py: added test_valid_surfaces_processor_fee_in_event_metadata to TestCreate, which overrides the autouse create_refund_fees mock to return a processor_fee Transaction with amount=-100 and asserts the emitted balance.refund event's user_metadata["fee"] == 100.

Why

create already computes the refund processor fees one statement earlier via processor_fee_transaction_service.create_refund_fees(...) and stores them on refund_transaction.incurred_transactions, but then builds the event metadata with a hardcoded "fee": 0, discarding the in-scope transaction_fees.

The dispute sibling (dispute.py) surfaces its computed fee with the same sum(-fee.amount for fee in …) pattern; refund never matched it after the dispute fee field was retconned to processor-fee semantics in e01437f1b1. As a result, whenever Stripe reports a non-zero fee on a refund's balance transaction (cross-border / currency-conversion refunds, or accounts on plans that charge a fixed refund fee), the merchant's net_revenue / net_cumulative_revenue metrics were overstated by that fee amount (the Tinybird metrics_revenue pipe subtracts COALESCE(e.fee, 0), which was always 0), and the wrong fee: 0 was returned directly via the events API. For the common domestic refund where Stripe reports fee=0, the corrected formula is a no-op.

How

Mirrored the dispute path exactly: sum(-fee.amount for fee in transaction_fees), where transaction_fees is the list already computed and attached to refund_transaction.incurred_transactions. Each processor_fee Transaction's amount is -balance_transaction.fee, so negating and summing yields balance_transaction.fee0 for domestic refunds and the real fee otherwise. No schema or migration change is required: BalanceRefundMetadata.fee is already typed int and the downstream consumers (tinybird.pop("fee"), COALESCE(e.fee, 0), events API serialization) already handle a non-zero int.

The revert path's literal "fee": 0 is left unchanged — there is no reversal-fee producer today, so that 0 is correct.

Checklist

  • This PR addresses a single concern (one bug fix, one feature, one refactor)
  • The diff is reasonably sized and easy to review
  • New functionality is covered by tests
  • Linting and type checking pass (uv run task lint && uv run task lint_types)
  • No unrelated changes or drive-by fixes are included

Testing

  • Added test_valid_surfaces_processor_fee_in_event_metadata overrides the create_refund_fees mock to return a processor_fee Transaction with amount=-100, drives refund_transaction_service.create, and asserts balance.refund event user_metadata["fee"] == 100. Confirmed bi-directionally: it FAILS against the buggy hardcoded "fee": 0 (assert 0 == 100) and passes with the fix.
  • Unit tests, typecheck, and lint all pass: the full tests/transaction/service/ suite (78 tests), ruff format --check + ruff check on the repo (1711 files), mypy across all 1631 source files, and the custom AST lints (lint_check) are clean.
  • End-to-end verification of the downstream surfaces: with the tinybirdco/tinybird-local container and the tb CLI (installed via the same tinybird.co/install.sh step CI uses), the Tinybird metrics suite (tests/metrics/test_tinybird_metrics.py, 18 tests), the metrics service suite (tests/metrics/test_service.py, 100 tests), and the events suite (tests/event/, 142 tests) all pass, confirming the COALESCE(e.fee, 0) subtraction path and events API serialization remain correct with a non-zero fee.

Automatic Fixes PRs can be configured here.

Review in cubic

@detail-app
detail-app Bot requested a review from Yopi September 7, 2026 01:22
@detail-app detail-app Bot assigned Yopi Sep 7, 2026
@vercel

vercel Bot commented Sep 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
orbit Ready Ready Preview Sep 9, 2026 2:11pm UTC
polar-test Ready Ready Preview Sep 9, 2026 2:11pm UTC

Request Review

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

OpenAPI Changes

No changes detected in the OpenAPI schema.

"tax_country": payment_transaction.tax_country,
"tax_state": payment_transaction.tax_state,
"fee": 0,
"fee": sum(-fee.amount for fee in transaction_fees),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't entirely correct, because it will deduct the fees Polar take as well, which is not something you will get back

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 3c72f09. The filter now explicitly excludes any fee without a processor_fee_type (i.e. platform fees), ensuring only Stripe's refund processing fee is counted in the fee metadata field.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants