diff --git a/Cargo.lock b/Cargo.lock index 0ba34095..777db98b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -717,7 +717,7 @@ checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "sentry_protos" -version = "0.25.1" +version = "0.26.0" dependencies = [ "prost", "prost-types", diff --git a/proto/sentry_protos/billing/v1/common/v1/stripe_charge.proto b/proto/sentry_protos/billing/v1/common/v1/stripe_charge.proto index 1c527650..ceafdf45 100644 --- a/proto/sentry_protos/billing/v1/common/v1/stripe_charge.proto +++ b/proto/sentry_protos/billing/v1/common/v1/stripe_charge.proto @@ -14,6 +14,20 @@ message PaymentMethodDetails { } } +// A snapshot of a single Stripe refund attached to a charge. Conveys +// per-refund metadata so handlers can record refunds individually and +// dedupe by ``id``; the aggregate ``amount_refunded`` on ``StripeCharge`` +// alone is not enough for idempotent ingestion. +message StripeRefund { + // Stripe id of the refund (e.g. "re_xxx"). + string id = 1; + // Refund amount in the charge's smallest currency unit (cents for USD). + uint64 amount = 2; + // Stripe-supplied reason (e.g. "requested_by_customer", "duplicate", + // "fraudulent"). Unset when Stripe did not provide one. + optional string reason = 3; +} + // A snapshot of a Stripe charge object. Used as the payload when reacting // to Stripe webhook events. message StripeCharge { @@ -26,4 +40,8 @@ message StripeCharge { int64 created_st = 6; optional string failure_code = 7; PaymentMethodDetails payment_method_details = 8; + // Per-refund records attached to this charge. Empty when the charge has + // no refunds. The list reflects the state of refunds at the time the + // webhook was emitted. + repeated StripeRefund refunds = 9; } diff --git a/proto/sentry_protos/billing/v1/services/charge/v1/charge.proto b/proto/sentry_protos/billing/v1/services/charge/v1/charge.proto index 4c355d5e..bd75ca59 100644 --- a/proto/sentry_protos/billing/v1/services/charge/v1/charge.proto +++ b/proto/sentry_protos/billing/v1/services/charge/v1/charge.proto @@ -18,3 +18,21 @@ message PlatformCharge { uint64 amount_refunded = 8; optional string card_last_4 = 9; } + +// Canonical projection of a stored platform refund. One row per recorded +// refund against a ``PlatformCharge``; the aggregate ``amount_refunded`` on +// ``PlatformCharge`` is a cache of the sum of these. +message PlatformRefund { + // Stripe id of the refund (e.g. "re_xxx"). + string stripe_id = 1; + // Stripe id of the charge this refund is against (e.g. "ch_xxx"). Joins + // back to ``PlatformCharge.stripe_id``. + string charge_stripe_id = 2; + uint64 organization_id = 3; + // Refund amount in cents. + uint64 amount = 4; + // Stripe-supplied refund reason. Unset when Stripe did not provide one. + optional string reason = 5; + // Unix epoch seconds when the refund was recorded by the platform. + int64 date_added_st = 6; +} diff --git a/proto/sentry_protos/billing/v1/services/charge/v1/endpoint_list_refunds.proto b/proto/sentry_protos/billing/v1/services/charge/v1/endpoint_list_refunds.proto new file mode 100644 index 00000000..ecbb641e --- /dev/null +++ b/proto/sentry_protos/billing/v1/services/charge/v1/endpoint_list_refunds.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package sentry_protos.billing.v1.services.charge.v1; + +import "sentry_protos/billing/v1/services/charge/v1/charge.proto"; + +// Lists every recorded refund associated with the charges for a single +// platform invoice. Callers in the presentation layer use this to render +// invoice-level refund state without crossing the charge service boundary. +message ListRefundsByInvoiceRequest { + uint64 invoice_id = 1; +} + +message ListRefundsByInvoiceResponse { + // Refunds ordered by ``date_added_st`` ascending. Empty when the invoice + // has no refunds. + repeated PlatformRefund refunds = 1; +} diff --git a/proto/sentry_protos/billing/v1/services/charge/v1/endpoint_record_charge_refunds.proto b/proto/sentry_protos/billing/v1/services/charge/v1/endpoint_record_charge_refunds.proto new file mode 100644 index 00000000..45e65724 --- /dev/null +++ b/proto/sentry_protos/billing/v1/services/charge/v1/endpoint_record_charge_refunds.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package sentry_protos.billing.v1.services.charge.v1; + +import "sentry_protos/billing/v1/common/v1/stripe_charge.proto"; +import "sentry_protos/billing/v1/services/charge/v1/charge.proto"; + +// Records platform refunds for a Stripe charge from a webhook payload. +// Mirrors the contents of ``stripe_charge.refunds`` as ``PlatformRefund`` +// rows idempotently keyed by Stripe refund id, and syncs the aggregate +// ``amount_refunded`` / ``refunded`` state on the stored ``PlatformCharge``. +// Called by the presentation layer that owns the ``charge.refunded`` +// webhook handler; the charge service does not call Stripe in this path +// (Stripe initiated the refund). +message RecordChargeRefundsRequest { + sentry_protos.billing.v1.common.v1.StripeCharge stripe_charge = 1; +} + +message RecordChargeRefundsResponse { + // Unset when no platform charge exists for ``stripe_charge.id``. Callers + // use this to distinguish platform charges from legacy charges. + optional PlatformCharge charge = 1; + // The platform refund rows that were recorded or already existed, + // ordered by ``date_added_st`` ascending. + repeated PlatformRefund refunds = 2; +} diff --git a/proto/sentry_protos/billing/v1/services/charge/v1/endpoint_refund_charge.proto b/proto/sentry_protos/billing/v1/services/charge/v1/endpoint_refund_charge.proto new file mode 100644 index 00000000..4665ce2e --- /dev/null +++ b/proto/sentry_protos/billing/v1/services/charge/v1/endpoint_refund_charge.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +package sentry_protos.billing.v1.services.charge.v1; + +import "sentry_protos/billing/v1/services/charge/v1/charge.proto"; + +// Issues a refund against a platform charge by calling Stripe and +// recording a ``PlatformRefund`` row. Caller supplies the partial-refund +// amount; the service enforces the over-refund guard (sum of recorded +// refunds + new amount <= charge amount) under optimistic locking to +// prevent concurrent double refunds. +message RefundChargeRequest { + // Stripe id of the charge to refund (e.g. "ch_xxx"). Identifies the + // ``PlatformCharge`` to refund against. + string stripe_charge_id = 1; + // Refund amount in cents. Must be > 0 and <= remaining refundable amount + // on the charge. + uint64 amount = 2; + // Optional reason recorded with the refund and forwarded to Stripe. + optional string reason = 3; +} + +message RefundChargeResponse { + // The refund record created by this call. + PlatformRefund refund = 1; + // The charge with refund aggregates synced. + PlatformCharge charge = 2; +} diff --git a/proto/sentry_protos/billing/v1/services/invoicer/v1/endpoint_handle_charge_refunded.proto b/proto/sentry_protos/billing/v1/services/invoicer/v1/endpoint_handle_charge_refunded.proto new file mode 100644 index 00000000..29d27bf2 --- /dev/null +++ b/proto/sentry_protos/billing/v1/services/invoicer/v1/endpoint_handle_charge_refunded.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package sentry_protos.billing.v1.services.invoicer.v1; + +import "sentry_protos/billing/v1/common/v1/stripe_charge.proto"; + +// Request to react to a Stripe `charge.refunded` webhook event for a +// charge created by the billing platform. The included ``stripe_charge`` +// carries the per-refund records on its ``refunds`` field; the handler +// records each refund idempotently by Stripe refund id and syncs the +// aggregate refund state on the stored platform charge. +message HandleChargeRefundedRequest { + sentry_protos.billing.v1.common.v1.StripeCharge stripe_charge = 1; +} + +message HandleChargeRefundedResponse { + // True when the charge was created by the billing platform and the + // service has finished its handling. + bool handled = 1; +} diff --git a/rust/src/sentry_protos.billing.v1.common.v1.rs b/rust/src/sentry_protos.billing.v1.common.v1.rs index 0589afd1..0b96a970 100644 --- a/rust/src/sentry_protos.billing.v1.common.v1.rs +++ b/rust/src/sentry_protos.billing.v1.common.v1.rs @@ -382,9 +382,26 @@ pub mod payment_method_details { Card(super::Card), } } +/// A snapshot of a single Stripe refund attached to a charge. Conveys +/// per-refund metadata so handlers can record refunds individually and +/// dedupe by `id`; the aggregate `amount_refunded` on `StripeCharge` +/// alone is not enough for idempotent ingestion. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct StripeRefund { + /// Stripe id of the refund (e.g. "re_xxx"). + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, + /// Refund amount in the charge's smallest currency unit (cents for USD). + #[prost(uint64, tag = "2")] + pub amount: u64, + /// Stripe-supplied reason (e.g. "requested_by_customer", "duplicate", + /// "fraudulent"). Unset when Stripe did not provide one. + #[prost(string, optional, tag = "3")] + pub reason: ::core::option::Option<::prost::alloc::string::String>, +} /// A snapshot of a Stripe charge object. Used as the payload when reacting /// to Stripe webhook events. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct StripeCharge { #[prost(string, tag = "1")] pub id: ::prost::alloc::string::String, @@ -403,4 +420,9 @@ pub struct StripeCharge { pub failure_code: ::core::option::Option<::prost::alloc::string::String>, #[prost(message, optional, tag = "8")] pub payment_method_details: ::core::option::Option, + /// Per-refund records attached to this charge. Empty when the charge has + /// no refunds. The list reflects the state of refunds at the time the + /// webhook was emitted. + #[prost(message, repeated, tag = "9")] + pub refunds: ::prost::alloc::vec::Vec, } diff --git a/rust/src/sentry_protos.billing.v1.services.charge.v1.rs b/rust/src/sentry_protos.billing.v1.services.charge.v1.rs index d4beade9..9e4c3e81 100644 --- a/rust/src/sentry_protos.billing.v1.services.charge.v1.rs +++ b/rust/src/sentry_protos.billing.v1.services.charge.v1.rs @@ -25,6 +25,30 @@ pub struct PlatformCharge { #[prost(string, optional, tag = "9")] pub card_last_4: ::core::option::Option<::prost::alloc::string::String>, } +/// Canonical projection of a stored platform refund. One row per recorded +/// refund against a `PlatformCharge`; the aggregate `amount_refunded` on +/// `PlatformCharge` is a cache of the sum of these. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct PlatformRefund { + /// Stripe id of the refund (e.g. "re_xxx"). + #[prost(string, tag = "1")] + pub stripe_id: ::prost::alloc::string::String, + /// Stripe id of the charge this refund is against (e.g. "ch_xxx"). Joins + /// back to `PlatformCharge.stripe_id`. + #[prost(string, tag = "2")] + pub charge_stripe_id: ::prost::alloc::string::String, + #[prost(uint64, tag = "3")] + pub organization_id: u64, + /// Refund amount in cents. + #[prost(uint64, tag = "4")] + pub amount: u64, + /// Stripe-supplied refund reason. Unset when Stripe did not provide one. + #[prost(string, optional, tag = "5")] + pub reason: ::core::option::Option<::prost::alloc::string::String>, + /// Unix epoch seconds when the refund was recorded by the platform. + #[prost(int64, tag = "6")] + pub date_added_st: i64, +} #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct CaptureChargeRequest { #[prost(enumeration = "ChargeMethod", tag = "1")] @@ -126,11 +150,79 @@ pub struct ListChargesForInvoiceResponse { #[prost(message, repeated, tag = "1")] pub charges: ::prost::alloc::vec::Vec, } +/// Lists every recorded refund associated with the charges for a single +/// platform invoice. Callers in the presentation layer use this to render +/// invoice-level refund state without crossing the charge service boundary. +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ListRefundsByInvoiceRequest { + #[prost(uint64, tag = "1")] + pub invoice_id: u64, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListRefundsByInvoiceResponse { + /// Refunds ordered by `date_added_st` ascending. Empty when the invoice + /// has no refunds. + #[prost(message, repeated, tag = "1")] + pub refunds: ::prost::alloc::vec::Vec, +} +/// Records platform refunds for a Stripe charge from a webhook payload. +/// Mirrors the contents of `stripe_charge.refunds` as `PlatformRefund` +/// rows idempotently keyed by Stripe refund id, and syncs the aggregate +/// `amount_refunded` / `refunded` state on the stored `PlatformCharge`. +/// Called by the presentation layer that owns the `charge.refunded` +/// webhook handler; the charge service does not call Stripe in this path +/// (Stripe initiated the refund). +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RecordChargeRefundsRequest { + #[prost(message, optional, tag = "1")] + pub stripe_charge: ::core::option::Option< + super::super::super::common::v1::StripeCharge, + >, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RecordChargeRefundsResponse { + /// Unset when no platform charge exists for `stripe_charge.id`. Callers + /// use this to distinguish platform charges from legacy charges. + #[prost(message, optional, tag = "1")] + pub charge: ::core::option::Option, + /// The platform refund rows that were recorded or already existed, + /// ordered by `date_added_st` ascending. + #[prost(message, repeated, tag = "2")] + pub refunds: ::prost::alloc::vec::Vec, +} +/// Issues a refund against a platform charge by calling Stripe and +/// recording a `PlatformRefund` row. Caller supplies the partial-refund +/// amount; the service enforces the over-refund guard (sum of recorded +/// refunds + new amount \<= charge amount) under optimistic locking to +/// prevent concurrent double refunds. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct RefundChargeRequest { + /// Stripe id of the charge to refund (e.g. "ch_xxx"). Identifies the + /// `PlatformCharge` to refund against. + #[prost(string, tag = "1")] + pub stripe_charge_id: ::prost::alloc::string::String, + /// Refund amount in cents. Must be > 0 and \<= remaining refundable amount + /// on the charge. + #[prost(uint64, tag = "2")] + pub amount: u64, + /// Optional reason recorded with the refund and forwarded to Stripe. + #[prost(string, optional, tag = "3")] + pub reason: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct RefundChargeResponse { + /// The refund record created by this call. + #[prost(message, optional, tag = "1")] + pub refund: ::core::option::Option, + /// The charge with refund aggregates synced. + #[prost(message, optional, tag = "2")] + pub charge: ::core::option::Option, +} /// Synchronizes a stored platform charge with the latest snapshot from /// Stripe. The charge is identified by `stripe_charge.id`. Fields like /// `paid`, `failure_code` and refund state are copied onto the stored /// record so the database reflects the current payment-provider state. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct UpdateChargeRequest { #[prost(message, optional, tag = "1")] pub stripe_charge: ::core::option::Option< diff --git a/rust/src/sentry_protos.billing.v1.services.invoicer.v1.rs b/rust/src/sentry_protos.billing.v1.services.invoicer.v1.rs index fc2bc926..be76f95e 100644 --- a/rust/src/sentry_protos.billing.v1.services.invoicer.v1.rs +++ b/rust/src/sentry_protos.billing.v1.services.invoicer.v1.rs @@ -110,9 +110,28 @@ pub struct HandleChargeDisputedResponse { #[prost(bool, tag = "1")] pub handled: bool, } +/// Request to react to a Stripe `charge.refunded` webhook event for a +/// charge created by the billing platform. The included `stripe_charge` +/// carries the per-refund records on its `refunds` field; the handler +/// records each refund idempotently by Stripe refund id and syncs the +/// aggregate refund state on the stored platform charge. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct HandleChargeRefundedRequest { + #[prost(message, optional, tag = "1")] + pub stripe_charge: ::core::option::Option< + super::super::super::common::v1::StripeCharge, + >, +} +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct HandleChargeRefundedResponse { + /// True when the charge was created by the billing platform and the + /// service has finished its handling. + #[prost(bool, tag = "1")] + pub handled: bool, +} /// Request to react to a Stripe `charge.succeeded` webhook event for a /// charge created by the billing platform. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct HandleChargeSucceededRequest { #[prost(message, optional, tag = "1")] pub stripe_charge: ::core::option::Option<