diff --git a/Cargo.lock b/Cargo.lock index 5847e137..8eaa8294 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -717,7 +717,7 @@ checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "sentry_protos" -version = "0.27.0" +version = "0.27.1" 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..0035d24b 100644 --- a/proto/sentry_protos/billing/v1/services/charge/v1/charge.proto +++ b/proto/sentry_protos/billing/v1/services/charge/v1/charge.proto @@ -14,7 +14,35 @@ message PlatformCharge { bool paid = 4; uint64 amount = 5; optional string failure_code = 6; - bool refunded = 7; - uint64 amount_refunded = 8; + // DEPRECATED: Source of truth for refund state is ``refunds`` below. + // Consumers should check ``len(refunds) > 0`` (or + // ``sum(refunds[*].amount_cents) == amount``) instead. Left in the proto + // because ``sentry-protos`` policy disallows field deletion. Not + // populated by current producers. + bool refunded = 7 [deprecated = true]; + // DEPRECATED: Source of truth for refund amount is the sum of + // ``refunds[*].amount_cents`` below. Left in the proto because + // ``sentry-protos`` policy disallows field deletion. Not populated by + // current producers. + uint64 amount_refunded = 8 [deprecated = true]; optional string card_last_4 = 9; + // Recorded refunds against this charge, ordered by ``date_added_st`` + // ascending. Source of truth for refund state on a charge -- consumers + // sum ``refunds[*].amount_cents`` if they need the aggregate, and + // ``len(refunds) > 0`` (or sum == amount) signals "refunded." + repeated PlatformRefund refunds = 10; +} + +// Canonical projection of a stored platform refund. One row per recorded +// refund against a ``PlatformCharge``. +message PlatformRefund { + // Stripe id of the refund (e.g. "re_xxx"). + string stripe_id = 1; + uint64 organization_id = 2; + // Refund amount in cents. + uint64 amount_cents = 3; + // Stripe-supplied refund reason. Unset when Stripe did not provide one. + optional string reason = 4; + // Unix epoch seconds when the refund was recorded by the platform. + int64 date_added_st = 5; } 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..cdcd85b0 --- /dev/null +++ b/proto/sentry_protos/billing/v1/services/charge/v1/endpoint_record_charge_refunds.proto @@ -0,0 +1,25 @@ +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. When set, + // ``charge.refunds`` carries the platform refund rows that were recorded + // or already existed, ordered by ``date_added_st`` ascending. + optional PlatformCharge charge = 1; +} diff --git a/rust/src/sentry_protos.billing.v1.common.v1.rs b/rust/src/sentry_protos.billing.v1.common.v1.rs index fdceb88c..48877d60 100644 --- a/rust/src/sentry_protos.billing.v1.common.v1.rs +++ b/rust/src/sentry_protos.billing.v1.common.v1.rs @@ -384,9 +384,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, @@ -405,4 +422,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..36e20b94 100644 --- a/rust/src/sentry_protos.billing.v1.services.charge.v1.rs +++ b/rust/src/sentry_protos.billing.v1.services.charge.v1.rs @@ -2,7 +2,7 @@ /// Canonical projection of a stored platform charge. Returned by charge /// service endpoints that need to expose the persisted charge state to /// callers. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct PlatformCharge { #[prost(string, tag = "1")] pub stripe_id: ::prost::alloc::string::String, @@ -18,12 +18,48 @@ pub struct PlatformCharge { pub amount: u64, #[prost(string, optional, tag = "6")] pub failure_code: ::core::option::Option<::prost::alloc::string::String>, + /// DEPRECATED: Source of truth for refund state is `refunds` below. + /// Consumers should check `len(refunds) > 0` (or + /// `sum(refunds\[*\].amount_cents) == amount`) instead. Left in the proto + /// because `sentry-protos` policy disallows field deletion. Not + /// populated by current producers. + #[deprecated] #[prost(bool, tag = "7")] pub refunded: bool, + /// DEPRECATED: Source of truth for refund amount is the sum of + /// `refunds\[*\].amount_cents` below. Left in the proto because + /// `sentry-protos` policy disallows field deletion. Not populated by + /// current producers. + #[deprecated] #[prost(uint64, tag = "8")] pub amount_refunded: u64, #[prost(string, optional, tag = "9")] pub card_last_4: ::core::option::Option<::prost::alloc::string::String>, + /// Recorded refunds against this charge, ordered by `date_added_st` + /// ascending. Source of truth for refund state on a charge -- consumers + /// sum `refunds\[*\].amount_cents` if they need the aggregate, and + /// `len(refunds) > 0` (or sum == amount) signals "refunded." + #[prost(message, repeated, tag = "10")] + pub refunds: ::prost::alloc::vec::Vec, +} +/// Canonical projection of a stored platform refund. One row per recorded +/// refund against a `PlatformCharge`. +#[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, + #[prost(uint64, tag = "2")] + pub organization_id: u64, + /// Refund amount in cents. + #[prost(uint64, tag = "3")] + pub amount_cents: u64, + /// Stripe-supplied refund reason. Unset when Stripe did not provide one. + #[prost(string, optional, tag = "4")] + pub reason: ::core::option::Option<::prost::alloc::string::String>, + /// Unix epoch seconds when the refund was recorded by the platform. + #[prost(int64, tag = "5")] + pub date_added_st: i64, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct CaptureChargeRequest { @@ -99,7 +135,7 @@ pub struct GetChargeByStripeIdRequest { #[prost(string, tag = "1")] pub stripe_id: ::prost::alloc::string::String, } -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct GetChargeByStripeIdResponse { /// Unset when no platform charge with the given stripe_id exists. #[prost(message, optional, tag = "1")] @@ -126,18 +162,41 @@ pub struct ListChargesForInvoiceResponse { #[prost(message, repeated, tag = "1")] pub charges: ::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. When set, + /// `charge.refunds` carries the platform refund rows that were recorded + /// or already existed, ordered by `date_added_st` ascending. + #[prost(message, optional, tag = "1")] + 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< super::super::super::common::v1::StripeCharge, >, } -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct UpdateChargeResponse { /// Unset when no platform charge with the given stripe_id exists. #[prost(message, optional, tag = "1")] 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..83598408 100644 --- a/rust/src/sentry_protos.billing.v1.services.invoicer.v1.rs +++ b/rust/src/sentry_protos.billing.v1.services.invoicer.v1.rs @@ -112,7 +112,7 @@ pub struct HandleChargeDisputedResponse { } /// 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<