diff --git a/Cargo.lock b/Cargo.lock index 09d2f89c..0af9158c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -717,7 +717,7 @@ checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "sentry_protos" -version = "0.90.0" +version = "0.91.0" dependencies = [ "prost", "prost-types", diff --git a/proto/sentry_protos/billing/v1/services/contract/ledger/v1/endpoint_get_ledger_balance.proto b/proto/sentry_protos/billing/v1/services/contract/ledger/v1/endpoint_get_ledger_balance.proto new file mode 100644 index 00000000..1c2420d3 --- /dev/null +++ b/proto/sentry_protos/billing/v1/services/contract/ledger/v1/endpoint_get_ledger_balance.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package sentry_protos.billing.v1.services.contract.ledger.v1; + +message GetLedgerBalanceRequest { + uint64 organization_id = 1; + string account_id = 2; +} + +message GetLedgerBalanceResponse { + // Paid deposits minus withdrawals, plus adjustments. Zero for an account + // with no entries. + int64 balance_cents = 1; + // Deposits whose invoices are not paid yet. Not part of balance_cents. + int64 pending_deposit_cents = 2; +} diff --git a/proto/sentry_protos/billing/v1/services/contract/ledger/v1/endpoint_list_ledger_entries.proto b/proto/sentry_protos/billing/v1/services/contract/ledger/v1/endpoint_list_ledger_entries.proto new file mode 100644 index 00000000..a6cb9a66 --- /dev/null +++ b/proto/sentry_protos/billing/v1/services/contract/ledger/v1/endpoint_list_ledger_entries.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package sentry_protos.billing.v1.services.contract.ledger.v1; + +import "sentry_protos/billing/v1/services/contract/ledger/v1/ledger_entry.proto"; + +// Lists an account's entries, newest first. +message ListLedgerEntriesRequest { + uint64 organization_id = 1; + string account_id = 2; + // Return only entries older than this one. Pass next_cursor from the + // previous page. + optional uint64 before_entry_id = 3; + // Defaults to 100 when zero; capped at 1000. + uint32 limit = 4; +} + +message ListLedgerEntriesResponse { + repeated LedgerEntry entries = 1; + // Unset on the last page. + optional uint64 next_cursor = 2; +} diff --git a/proto/sentry_protos/billing/v1/services/contract/ledger/v1/endpoint_record_ledger_adjustment.proto b/proto/sentry_protos/billing/v1/services/contract/ledger/v1/endpoint_record_ledger_adjustment.proto new file mode 100644 index 00000000..1e219d3b --- /dev/null +++ b/proto/sentry_protos/billing/v1/services/contract/ledger/v1/endpoint_record_ledger_adjustment.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package sentry_protos.billing.v1.services.contract.ledger.v1; + +import "sentry_protos/billing/v1/services/contract/ledger/v1/ledger_entry.proto"; + +// Records a manual correction. A refund is a negative adjustment: the refunded +// invoice stays paid, so its deposit still counts and the adjustment cancels +// it. +message RecordLedgerAdjustmentRequest { + uint64 organization_id = 1; + string account_id = 2; + // Must not be zero. + int64 amount_cents = 3; + // Unique per organization and account_id. A repeated key writes nothing + // and returns the existing entry with created=false. + string idempotency_key = 4; + // Required, so that the ledger shows why the balance changed. + string description = 5; + // The invoice the adjustment refers to, e.g. the refunded one. + optional uint64 invoice_id = 6; +} + +message RecordLedgerAdjustmentResponse { + LedgerEntry entry = 1; + bool created = 2; + // The balance after the adjustment. + int64 balance_cents = 3; +} diff --git a/proto/sentry_protos/billing/v1/services/contract/ledger/v1/endpoint_record_ledger_withdrawal.proto b/proto/sentry_protos/billing/v1/services/contract/ledger/v1/endpoint_record_ledger_withdrawal.proto new file mode 100644 index 00000000..dde642e1 --- /dev/null +++ b/proto/sentry_protos/billing/v1/services/contract/ledger/v1/endpoint_record_ledger_withdrawal.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +package sentry_protos.billing.v1.services.contract.ledger.v1; + +import "sentry_protos/billing/v1/services/contract/ledger/v1/ledger_entry.proto"; + +// Takes credit out of an account. The full amount is always withdrawn, even +// when that makes the balance negative. +message RecordLedgerWithdrawalRequest { + uint64 organization_id = 1; + string account_id = 2; + // Must be positive. + uint64 amount_cents = 3; + // Unique per organization and account_id. A repeated key writes nothing + // and returns the existing entry with created=false. + string idempotency_key = 4; + // The contract whose usage the withdrawal pays for. Defaults to the + // organization's current contract. + optional uint64 contract_id = 5; + optional string description = 6; +} + +message RecordLedgerWithdrawalResponse { + LedgerEntry entry = 1; + bool created = 2; + // The balance after the withdrawal. + int64 balance_cents = 3; +} diff --git a/proto/sentry_protos/billing/v1/services/contract/ledger/v1/ledger_deposit.proto b/proto/sentry_protos/billing/v1/services/contract/ledger/v1/ledger_deposit.proto new file mode 100644 index 00000000..b41456ac --- /dev/null +++ b/proto/sentry_protos/billing/v1/services/contract/ledger/v1/ledger_deposit.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package sentry_protos.billing.v1.services.contract.ledger.v1; + +// Credit that an off-cycle invoice buys. The deposit is written in the same +// transaction as the invoice, so a crash can not leave a charge without its +// credit. It counts toward the balance once the invoice is paid. +message LedgerDeposit { + // 1 to 64 characters. + string account_id = 1; + // The credit bought. Must be positive and at most the invoice's total + // before tax. + uint64 amount_cents = 2; + // At most 256 characters. + optional string description = 3; +} diff --git a/proto/sentry_protos/billing/v1/services/contract/ledger/v1/ledger_entry.proto b/proto/sentry_protos/billing/v1/services/contract/ledger/v1/ledger_entry.proto new file mode 100644 index 00000000..ff51275e --- /dev/null +++ b/proto/sentry_protos/billing/v1/services/contract/ledger/v1/ledger_entry.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +package sentry_protos.billing.v1.services.contract.ledger.v1; + +import "google/protobuf/timestamp.proto"; + +enum LedgerEntryType { + LEDGER_ENTRY_TYPE_UNSPECIFIED = 0; + // Credit bought through an invoice. It adds to the balance only after the + // invoice is paid. + LEDGER_ENTRY_TYPE_DEPOSIT = 1; + // Credit used, e.g. the credit-only usage tallied at contract rollover. + LEDGER_ENTRY_TYPE_WITHDRAWAL = 2; + // A manual correction, such as a refund. It can be positive or negative. + LEDGER_ENTRY_TYPE_ADJUSTMENT = 3; +} + +// One row of an account's transaction history. Entries are append-only. +message LedgerEntry { + uint64 id = 1; + uint64 organization_id = 2; + // Identifies one balance among the organization's accounts, e.g. "seer". + string account_id = 3; + // The organization's contract when the entry was written, if it had one. + optional uint64 contract_id = 4; + // Signed: deposits are positive, withdrawals are negative. + int64 amount_cents = 5; + LedgerEntryType entry_type = 6; + // The invoice that pays for a deposit, or that an adjustment refers to. + optional uint64 invoice_id = 7; + google.protobuf.Timestamp date_added = 8; + optional string idempotency_key = 9; + optional string description = 10; + // False only for a deposit whose invoice is not paid yet. Such an entry + // does not count toward the balance. + bool counts_toward_balance = 11; +} diff --git a/proto/sentry_protos/billing/v1/services/contract/v2/endpoint_create_off_cycle_invoice.proto b/proto/sentry_protos/billing/v1/services/contract/v2/endpoint_create_off_cycle_invoice.proto index fb1aa957..6d7ac7e7 100644 --- a/proto/sentry_protos/billing/v1/services/contract/v2/endpoint_create_off_cycle_invoice.proto +++ b/proto/sentry_protos/billing/v1/services/contract/v2/endpoint_create_off_cycle_invoice.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package sentry_protos.billing.v1.services.contract.v2; import "sentry_protos/billing/v1/common/v1/address.proto"; +import "sentry_protos/billing/v1/services/contract/ledger/v1/ledger_deposit.proto"; import "sentry_protos/billing/v1/services/contract/v1/invoice.proto"; // Writes an organization-scoped invoice outside of any contract, ready for the @@ -29,6 +30,9 @@ message CreateOffCycleInvoiceRequest { // The tax document the provider opened for this invoice, if any. optional string tax_transaction_code = 8; bool is_reverse_charge = 9; + // Required. The credit the invoice buys. A repeated idempotency_key returns + // the existing invoice and writes no second deposit. + sentry_protos.billing.v1.services.contract.ledger.v1.LedgerDeposit ledger_deposit = 10; } message CreateOffCycleInvoiceResponse { diff --git a/proto/sentry_protos/billing/v1/services/invoicer/v1/endpoint_bill_off_cycle_invoice.proto b/proto/sentry_protos/billing/v1/services/invoicer/v1/endpoint_bill_off_cycle_invoice.proto index 1ec0333c..891fd7a6 100644 --- a/proto/sentry_protos/billing/v1/services/invoicer/v1/endpoint_bill_off_cycle_invoice.proto +++ b/proto/sentry_protos/billing/v1/services/invoicer/v1/endpoint_bill_off_cycle_invoice.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package sentry_protos.billing.v1.services.invoicer.v1; import "sentry_protos/billing/v1/common/v1/stripe_verification.proto"; +import "sentry_protos/billing/v1/services/contract/ledger/v1/ledger_deposit.proto"; import "sentry_protos/billing/v1/services/contract/v1/invoice.proto"; // Bills an organization for line items another service has priced, outside of @@ -26,6 +27,9 @@ message BillOffCycleInvoiceRequest { // requesting service chooses: FAILURE_POLICY_RETRY_ONLY leaves account // status alone and makes the service responsible for non-payment. sentry_protos.billing.v1.services.contract.v1.FailurePolicy failure_policy = 6; + // Required. The credit the invoice buys. The deposit exists before the + // charge starts. + sentry_protos.billing.v1.services.contract.ledger.v1.LedgerDeposit ledger_deposit = 7; } message BillOffCycleInvoiceResponse { diff --git a/rust/src/_include.rs b/rust/src/_include.rs index bfed6562..f773a526 100644 --- a/rust/src/_include.rs +++ b/rust/src/_include.rs @@ -35,6 +35,11 @@ pub mod sentry_protos { } } pub mod contract { + pub mod ledger { + pub mod v1 { + include!("sentry_protos.billing.v1.services.contract.ledger.v1.rs"); + } + } pub mod v1 { include!("sentry_protos.billing.v1.services.contract.v1.rs"); } diff --git a/rust/src/sentry_protos.billing.v1.services.contract.ledger.v1.rs b/rust/src/sentry_protos.billing.v1.services.contract.ledger.v1.rs new file mode 100644 index 00000000..94008824 --- /dev/null +++ b/rust/src/sentry_protos.billing.v1.services.contract.ledger.v1.rs @@ -0,0 +1,190 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct GetLedgerBalanceRequest { + #[prost(uint64, tag = "1")] + pub organization_id: u64, + #[prost(string, tag = "2")] + pub account_id: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct GetLedgerBalanceResponse { + /// Paid deposits minus withdrawals, plus adjustments. Zero for an account + /// with no entries. + #[prost(int64, tag = "1")] + pub balance_cents: i64, + /// Deposits whose invoices are not paid yet. Not part of balance_cents. + #[prost(int64, tag = "2")] + pub pending_deposit_cents: i64, +} +/// One row of an account's transaction history. Entries are append-only. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct LedgerEntry { + #[prost(uint64, tag = "1")] + pub id: u64, + #[prost(uint64, tag = "2")] + pub organization_id: u64, + /// Identifies one balance among the organization's accounts, e.g. "seer". + #[prost(string, tag = "3")] + pub account_id: ::prost::alloc::string::String, + /// The organization's contract when the entry was written, if it had one. + #[prost(uint64, optional, tag = "4")] + pub contract_id: ::core::option::Option, + /// Signed: deposits are positive, withdrawals are negative. + #[prost(int64, tag = "5")] + pub amount_cents: i64, + #[prost(enumeration = "LedgerEntryType", tag = "6")] + pub entry_type: i32, + /// The invoice that pays for a deposit, or that an adjustment refers to. + #[prost(uint64, optional, tag = "7")] + pub invoice_id: ::core::option::Option, + #[prost(message, optional, tag = "8")] + pub date_added: ::core::option::Option<::prost_types::Timestamp>, + #[prost(string, optional, tag = "9")] + pub idempotency_key: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "10")] + pub description: ::core::option::Option<::prost::alloc::string::String>, + /// False only for a deposit whose invoice is not paid yet. Such an entry + /// does not count toward the balance. + #[prost(bool, tag = "11")] + pub counts_toward_balance: bool, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum LedgerEntryType { + Unspecified = 0, + /// Credit bought through an invoice. It adds to the balance only after the + /// invoice is paid. + Deposit = 1, + /// Credit used, e.g. the credit-only usage tallied at contract rollover. + Withdrawal = 2, + /// A manual correction, such as a refund. It can be positive or negative. + Adjustment = 3, +} +impl LedgerEntryType { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "LEDGER_ENTRY_TYPE_UNSPECIFIED", + Self::Deposit => "LEDGER_ENTRY_TYPE_DEPOSIT", + Self::Withdrawal => "LEDGER_ENTRY_TYPE_WITHDRAWAL", + Self::Adjustment => "LEDGER_ENTRY_TYPE_ADJUSTMENT", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "LEDGER_ENTRY_TYPE_UNSPECIFIED" => Some(Self::Unspecified), + "LEDGER_ENTRY_TYPE_DEPOSIT" => Some(Self::Deposit), + "LEDGER_ENTRY_TYPE_WITHDRAWAL" => Some(Self::Withdrawal), + "LEDGER_ENTRY_TYPE_ADJUSTMENT" => Some(Self::Adjustment), + _ => None, + } + } +} +/// Lists an account's entries, newest first. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ListLedgerEntriesRequest { + #[prost(uint64, tag = "1")] + pub organization_id: u64, + #[prost(string, tag = "2")] + pub account_id: ::prost::alloc::string::String, + /// Return only entries older than this one. Pass next_cursor from the + /// previous page. + #[prost(uint64, optional, tag = "3")] + pub before_entry_id: ::core::option::Option, + /// Defaults to 100 when zero; capped at 1000. + #[prost(uint32, tag = "4")] + pub limit: u32, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListLedgerEntriesResponse { + #[prost(message, repeated, tag = "1")] + pub entries: ::prost::alloc::vec::Vec, + /// Unset on the last page. + #[prost(uint64, optional, tag = "2")] + pub next_cursor: ::core::option::Option, +} +/// Records a manual correction. A refund is a negative adjustment: the refunded +/// invoice stays paid, so its deposit still counts and the adjustment cancels +/// it. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct RecordLedgerAdjustmentRequest { + #[prost(uint64, tag = "1")] + pub organization_id: u64, + #[prost(string, tag = "2")] + pub account_id: ::prost::alloc::string::String, + /// Must not be zero. + #[prost(int64, tag = "3")] + pub amount_cents: i64, + /// Unique per organization and account_id. A repeated key writes nothing + /// and returns the existing entry with created=false. + #[prost(string, tag = "4")] + pub idempotency_key: ::prost::alloc::string::String, + /// Required, so that the ledger shows why the balance changed. + #[prost(string, tag = "5")] + pub description: ::prost::alloc::string::String, + /// The invoice the adjustment refers to, e.g. the refunded one. + #[prost(uint64, optional, tag = "6")] + pub invoice_id: ::core::option::Option, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct RecordLedgerAdjustmentResponse { + #[prost(message, optional, tag = "1")] + pub entry: ::core::option::Option, + #[prost(bool, tag = "2")] + pub created: bool, + /// The balance after the adjustment. + #[prost(int64, tag = "3")] + pub balance_cents: i64, +} +/// Takes credit out of an account. The full amount is always withdrawn, even +/// when that makes the balance negative. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct RecordLedgerWithdrawalRequest { + #[prost(uint64, tag = "1")] + pub organization_id: u64, + #[prost(string, tag = "2")] + pub account_id: ::prost::alloc::string::String, + /// Must be positive. + #[prost(uint64, tag = "3")] + pub amount_cents: u64, + /// Unique per organization and account_id. A repeated key writes nothing + /// and returns the existing entry with created=false. + #[prost(string, tag = "4")] + pub idempotency_key: ::prost::alloc::string::String, + /// The contract whose usage the withdrawal pays for. Defaults to the + /// organization's current contract. + #[prost(uint64, optional, tag = "5")] + pub contract_id: ::core::option::Option, + #[prost(string, optional, tag = "6")] + pub description: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct RecordLedgerWithdrawalResponse { + #[prost(message, optional, tag = "1")] + pub entry: ::core::option::Option, + #[prost(bool, tag = "2")] + pub created: bool, + /// The balance after the withdrawal. + #[prost(int64, tag = "3")] + pub balance_cents: i64, +} +/// Credit that an off-cycle invoice buys. The deposit is written in the same +/// transaction as the invoice, so a crash can not leave a charge without its +/// credit. It counts toward the balance once the invoice is paid. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct LedgerDeposit { + /// 1 to 64 characters. + #[prost(string, tag = "1")] + pub account_id: ::prost::alloc::string::String, + /// The credit bought. Must be positive and at most the invoice's total + /// before tax. + #[prost(uint64, tag = "2")] + pub amount_cents: u64, + /// At most 256 characters. + #[prost(string, optional, tag = "3")] + pub description: ::core::option::Option<::prost::alloc::string::String>, +} diff --git a/rust/src/sentry_protos.billing.v1.services.contract.v2.rs b/rust/src/sentry_protos.billing.v1.services.contract.v2.rs index 2e919a67..6516dd7c 100644 --- a/rust/src/sentry_protos.billing.v1.services.contract.v2.rs +++ b/rust/src/sentry_protos.billing.v1.services.contract.v2.rs @@ -255,6 +255,10 @@ pub struct CreateOffCycleInvoiceRequest { pub tax_transaction_code: ::core::option::Option<::prost::alloc::string::String>, #[prost(bool, tag = "9")] pub is_reverse_charge: bool, + /// Required. The credit the invoice buys. A repeated idempotency_key returns + /// the existing invoice and writes no second deposit. + #[prost(message, optional, tag = "10")] + pub ledger_deposit: ::core::option::Option, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct CreateOffCycleInvoiceResponse { 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 c6ea3659..ba68e086 100644 --- a/rust/src/sentry_protos.billing.v1.services.invoicer.v1.rs +++ b/rust/src/sentry_protos.billing.v1.services.invoicer.v1.rs @@ -68,6 +68,12 @@ pub struct BillOffCycleInvoiceRequest { /// status alone and makes the service responsible for non-payment. #[prost(enumeration = "super::super::contract::v1::FailurePolicy", tag = "6")] pub failure_policy: i32, + /// Required. The credit the invoice buys. The deposit exists before the + /// charge starts. + #[prost(message, optional, tag = "7")] + pub ledger_deposit: ::core::option::Option< + super::super::contract::ledger::v1::LedgerDeposit, + >, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct BillOffCycleInvoiceResponse {