Skip to content

feat(billing-platform): add refund types + record_charge_refunds endpoint (REVENG-157)#303

Merged
armcknight merged 11 commits into
mainfrom
andrewmcknight/reveng-157-protos-record-charge-refunds
Jun 12, 2026
Merged

feat(billing-platform): add refund types + record_charge_refunds endpoint (REVENG-157)#303
armcknight merged 11 commits into
mainfrom
andrewmcknight/reveng-157-protos-record-charge-refunds

Conversation

@armcknight

@armcknight armcknight commented Jun 11, 2026

Copy link
Copy Markdown
Member

Summary

Shared refund types + the endpoint driven by the charge.refunded webhook flow:

  • common/v1/stripe_charge.proto — new StripeRefund message + repeated refunds field on StripeCharge. Webhook handlers need per-refund metadata (id, amount, reason) for idempotent ingestion; the aggregate amount_refunded on the charge alone isn't enough.
  • services/charge/v1/charge.proto — new PlatformRefund canonical projection (parallels PlatformCharge), embedded as repeated refunds on PlatformCharge. The previously-cached aggregate scalars refunded (field 7) and amount_refunded (field 8) stay in the message marked [deprecated = true] (per sentry-protos policy that disallows field deletion) and consumers should derive both from the refunds list — len(refunds) > 0 for "any refund happened" and sum(refunds[*].amount_cents) for the total.
  • services/charge/v1/endpoint_record_charge_refunds.proto (new) — ChargeService.record_charge_refunds, called directly by the charge.refunded webhook layer to record per-refund rows idempotently and return the charge with its refund list embedded.

Proto stack

Proto PR Paired getsentry PR Contents
#303 (this PR, merged) getsentry#20611 StripeRefund + PlatformRefund shared types, record_charge_refunds
#305 getsentry#20613 endpoint_list_refunds.proto (for customer + admin refund rendering)

Test plan

  • Rust bindings regenerated by CI's Build Rust bindings step + auto-committed
  • Python bindings regenerated by CI
  • Consumed downstream by getsentry#20611

🤖 Generated with Claude Code

@linear-code

linear-code Bot commented Jun 11, 2026

Copy link
Copy Markdown

REVENG-157

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow ci / buf-checks (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedJun 12, 2026, 12:34 AM

…oint

Adds the shared refund types + the charge-service endpoint driven by the
`charge.refunded` webhook flow (REVENG-157):

- ``common/v1/stripe_charge.proto``: new ``StripeRefund`` message +
  ``repeated refunds`` field on ``StripeCharge`` so webhook handlers
  receive per-refund metadata (id, amount, reason) and can record refunds
  idempotently — the aggregate ``amount_refunded`` on the charge alone
  isn't enough.
- ``services/charge/v1/charge.proto``: new ``PlatformRefund`` canonical
  projection, paralleling ``PlatformCharge``. The shared response type
  across all three refund endpoints, so it ships here in the base PR.
- ``services/charge/v1/endpoint_record_charge_refunds.proto``:
  ``ChargeService.record_charge_refunds`` — records per-refund rows from
  a Stripe webhook payload idempotently (keyed on refund stripe_id) and
  syncs the aggregate ``amount_refunded`` / ``refunded`` state.

The webhook handler in getsentry calls ``ChargeService.record_charge_refunds``
directly. We considered an ``InvoicerService.handle_charge_refunded`` wrapper
(mirroring the ``charge.succeeded`` / ``charge.dispute.created`` pattern)
but the wrapper would have been a passthrough -- no multi-service
orchestration to coordinate, just a single call. ``charge.updated`` set
the precedent for skipping the presentation layer when only one data
service is involved (getsentry#20559).

The other two refund endpoints (``refund_charge``, ``list_refunds_by_invoice``)
are split into separate PRs paired with their getsentry consumers.

Rust + Python bindings regenerated by CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@armcknight armcknight force-pushed the andrewmcknight/reveng-157-protos-record-charge-refunds branch from fc673f0 to 83023b0 Compare June 11, 2026 05:29
@armcknight armcknight marked this pull request as ready for review June 11, 2026 05:43
@armcknight armcknight requested a review from a team as a code owner June 11, 2026 05:43
Comment thread proto/sentry_protos/billing/v1/common/v1/stripe_charge.proto
Comment thread proto/sentry_protos/billing/v1/services/charge/v1/charge.proto Outdated
Comment thread proto/sentry_protos/billing/v1/services/charge/v1/charge.proto Outdated
Andrew McKnight and others added 4 commits June 11, 2026 10:51
…e_stripe_id

Per review feedback on #303:

- ``PlatformRefund.charge_stripe_id`` removed (field 2). Nothing consumes
  this field today; if a future caller needs to identify the parent charge
  of a standalone ``PlatformRefund`` (e.g. grouping by charge in a UI),
  we can add it back as a new field number then.

- ``PlatformCharge.refunds`` added (field 10): the recorded refund rows
  against the charge, ordered by ``date_added_st``. Populated by
  endpoints that have already loaded the refund rows; left empty by
  endpoints that only need the aggregate ``amount_refunded`` cache.

- ``RecordChargeRefundsResponse.refunds`` removed (field 2). Refunds now
  live under ``response.charge.refunds`` -- the response only needs the
  single ``charge`` field. Matches the DB relationship (refunds belong
  to a specific charge) and avoids duplicating the same data at two
  levels of the response.

Remaining ``PlatformRefund`` field numbers compacted (3..6 -> 2..5) since
this message hasn't shipped to any consumer yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Make the unit explicit on the canonical refund projection. Matches the
unit-explicit naming used on the underlying Django model
(``PlatformRefund.amount_cents``) and avoids consumers having to guess
whether the value is dollars, cents, or something else.

Field number is unchanged (3), so wire-compatible with any in-flight
serialized messages from earlier #303 iterations.

The sibling ``PlatformCharge.amount`` / ``StripeRefund.amount`` /
``StripeCharge.amount`` aren't renamed -- their values come from Stripe
or from the legacy ``Charge`` model, where the unit-implicit naming is
already a settled convention. Following up on those would be a broader
audit (#REVENG-???) and out of scope for this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread rust/src/sentry_protos.billing.v1.services.charge.v1.rs
Andrew McKnight and others added 5 commits June 11, 2026 15:18
…arge

Per review feedback on getsentry#20611: the cached ``amount_refunded`` /
``refunded`` fields on ``PlatformCharge`` are derivable from the
embedded ``refunds`` list -- consumers can sum ``refunds[*].amount_cents``
for the aggregate, and check ``len(refunds) > 0`` (or sum == amount)
for "is this refunded." Storing them as separate fields creates a drift
risk between the cache and the rows (sentry-protos#303 bugbot).

Removing fields 7 (``refunded``) and 8 (``amount_refunded``) from the
``PlatformCharge`` proto. Field numbers reserved so they can't be
re-used for an incompatible meaning.

The corresponding service-side change (drop the cache write in
``record_charge_refunds``, populate ``refunds`` instead of the
aggregate fields) lands in getsentry#20611.

The DB columns on ``accounts_platformcharge`` are inherited from
``AbstractCharge`` and remain in place since the legacy ``Charge``
model still uses them; splitting the abstract class to fully drop the
columns from ``accounts_platformcharge`` is a separate follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…stead of removing

The previous attempt (commit 67ce9cf) removed fields 7 (``refunded``) and
8 (``amount_refunded``) and replaced them with ``reserved 7, 8;`` so the
field numbers couldn't be re-used. That tripped buf's ``FIELD_NO_DELETE``
rule, which sentry-protos enables deliberately: PR #168 lifted it once
to clean up unused prototype protos, and PR #169 immediately restored
it as a permanent guardrail.

Restoring the fields with ``[deprecated = true]`` so:

- buf is happy (no field deletion).
- Consumers get a deprecation marker steering them toward
  ``refunds[*].amount_cents`` as the source of truth.
- No drift risk in practice: the getsentry-side producer
  (``_charge_to_proto``) is already not populating these fields
  (getsentry#20611 commit ``96fd005``), and a search confirms no
  getsentry code reads ``PlatformCharge.refunded`` /
  ``PlatformCharge.amount_refunded`` -- the proto is internal to the
  billing platform service.

This matches the spirit of Noah's review feedback (don't carry a cache
that can drift from the rows) while respecting the repo's field-
deletion policy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3167124. Configure here.

Comment thread proto/sentry_protos/billing/v1/services/charge/v1/charge.proto
@armcknight armcknight merged commit 06694eb into main Jun 12, 2026
16 checks passed
@armcknight armcknight changed the title feat(billing-platform): add refund types + record_charge_refunds/handle_charge_refunded endpoints (REVENG-157) feat(billing-platform): add refund types + record_charge_refunds endpoint (REVENG-157) Jun 12, 2026
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