diff --git a/api/spec/packages/aip-client-javascript/README.md b/api/spec/packages/aip-client-javascript/README.md index d7d989e765..ffd03ed54e 100644 --- a/api/spec/packages/aip-client-javascript/README.md +++ b/api/spec/packages/aip-client-javascript/README.md @@ -277,27 +277,28 @@ The full call path, HTTP route, and a short description are listed below. ### Customers -| Method | HTTP | Description | -| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `client.customers.create` | `POST /openmeter/customers` | Create customer | -| `client.customers.get` | `GET /openmeter/customers/{customerId}` | Get customer | -| `client.customers.list` | `GET /openmeter/customers` | List customers | -| `client.customers.upsert` | `PUT /openmeter/customers/{customerId}` | Upsert customer | -| `client.customers.delete` | `DELETE /openmeter/customers/{customerId}` | Delete customer | -| `client.customers.billing.get` | `GET /openmeter/customers/{customerId}/billing` | Get customer billing data | -| `client.customers.billing.update` | `PUT /openmeter/customers/{customerId}/billing` | Update customer billing data | -| `client.customers.billing.updateAppData` | `PUT /openmeter/customers/{customerId}/billing/app-data` | Update customer billing app data | -| `client.customers.billing.createStripeCheckoutSession` | `POST /openmeter/customers/{customerId}/billing/stripe/checkout-sessions` | Create a [Stripe Checkout Session](https://docs.stripe.com/payments/checkout) for the customer. Creates a Checkout Session for collecting payment method information from customers. The session operates in "setup" mode, which collects payment details without charging the customer immediately. The collected payment method can be used for future subscription billing. For hosted checkout sessions, redirect customers to the returned URL. For embedded sessions, use the client_secret to initialize Stripe.js in your application. | -| `client.customers.billing.createStripePortalSession` | `POST /openmeter/customers/{customerId}/billing/stripe/portal-sessions` | Create Stripe Customer Portal Session. Useful to redirect the customer to the Stripe Customer Portal to manage their payment methods, change their billing address and access their invoice history. Only returns URL if the customer billing profile is linked to a stripe app and customer. | -| `client.customers.credits.grants.create` | `POST /openmeter/customers/{customerId}/credits/grants` | Create a new credit grant. A credit grant represents an allocation of prepaid credits to a customer. | -| `client.customers.credits.grants.get` | `GET /openmeter/customers/{customerId}/credits/grants/{creditGrantId}` | Get a credit grant. | -| `client.customers.credits.grants.list` | `GET /openmeter/customers/{customerId}/credits/grants` | List credit grants. | -| `client.customers.credits.balance.get` | `GET /openmeter/customers/{customerId}/credits/balance` | Get a credit balance. | -| `client.customers.credits.adjustments.create` | `POST /openmeter/customers/{customerId}/credits/adjustments` | A credit adjustment can be used to make manual adjustments to a customer's credit balance. Supported use-cases: - Usage correction | -| `client.customers.credits.grants.updateExternalSettlement` | `POST /openmeter/customers/{customerId}/credits/grants/{creditGrantId}/settlement/external` | Update the payment settlement status of an externally funded credit grant. Use this endpoint to synchronize the payment state of an external payment with the system so that revenue recognition and credit availability work as expected. | -| `client.customers.credits.transactions.list` | `GET /openmeter/customers/{customerId}/credits/transactions` | List credit transactions for a customer. Returns an immutable, chronological record of credit movements: funded credits and consumed credits. Transactions are returned in reverse chronological order by default. | -| `client.customers.charges.list` | `GET /openmeter/customers/{customerId}/charges` | List customer charges. Returns the customer's charges that are represented as either flat fee or usage-based charges. | -| `client.customers.charges.create` | `POST /openmeter/customers/{customerId}/charges` | Create customer charge. | +| Method | HTTP | Description | +| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `client.customers.create` | `POST /openmeter/customers` | Create customer | +| `client.customers.get` | `GET /openmeter/customers/{customerId}` | Get customer | +| `client.customers.list` | `GET /openmeter/customers` | List customers | +| `client.customers.upsert` | `PUT /openmeter/customers/{customerId}` | Upsert customer | +| `client.customers.delete` | `DELETE /openmeter/customers/{customerId}` | Delete customer | +| `client.customers.billing.get` | `GET /openmeter/customers/{customerId}/billing` | Get customer billing data | +| `client.customers.billing.update` | `PUT /openmeter/customers/{customerId}/billing` | Update customer billing data | +| `client.customers.billing.updateAppData` | `PUT /openmeter/customers/{customerId}/billing/app-data` | Update customer billing app data | +| `client.customers.billing.createStripeCheckoutSession` | `POST /openmeter/customers/{customerId}/billing/stripe/checkout-sessions` | Create a [Stripe Checkout Session](https://docs.stripe.com/payments/checkout) for the customer. Creates a Checkout Session for collecting payment method information from customers. The session operates in "setup" mode, which collects payment details without charging the customer immediately. The collected payment method can be used for future subscription billing. For hosted checkout sessions, redirect customers to the returned URL. For embedded sessions, use the client_secret to initialize Stripe.js in your application. | +| `client.customers.billing.createStripePortalSession` | `POST /openmeter/customers/{customerId}/billing/stripe/portal-sessions` | Create Stripe Customer Portal Session. Useful to redirect the customer to the Stripe Customer Portal to manage their payment methods, change their billing address and access their invoice history. Only returns URL if the customer billing profile is linked to a stripe app and customer. | +| `client.customers.credits.grants.create` | `POST /openmeter/customers/{customerId}/credits/grants` | Create a new credit grant. A credit grant represents an allocation of prepaid credits to a customer. | +| `client.customers.credits.grants.get` | `GET /openmeter/customers/{customerId}/credits/grants/{creditGrantId}` | Get a credit grant. | +| `client.customers.credits.grants.list` | `GET /openmeter/customers/{customerId}/credits/grants` | List credit grants. | +| `client.customers.credits.balance.get` | `GET /openmeter/customers/{customerId}/credits/balance` | Get a credit balance. | +| `client.customers.credits.adjustments.create` | `POST /openmeter/customers/{customerId}/credits/adjustments` | A credit adjustment can be used to make manual adjustments to a customer's credit balance. Supported use-cases: - Usage correction | +| `client.customers.credits.grants.void` | `POST /openmeter/customers/{customerId}/credits/grants/{creditGrantId}/void` | Void a credit grant, forfeiting the remaining unused balance. Voiding is a forward-looking, irreversible operation. Credits already consumed by usage remain unaffected — only the remaining balance is forfeited. The grant reads as `voided` status afterwards. Payment state is not adjusted when `payment_adjustment` is `none`, so invoice-backed or externally collected payments may still collect the original amount. Only `active` grants can be voided; voiding a pending, expired, or fully consumed grant returns a conflict. Retrying a successful void is an idempotent success. | +| `client.customers.credits.grants.updateExternalSettlement` | `POST /openmeter/customers/{customerId}/credits/grants/{creditGrantId}/settlement/external` | Update the payment settlement status of an externally funded credit grant. Use this endpoint to synchronize the payment state of an external payment with the system so that revenue recognition and credit availability work as expected. | +| `client.customers.credits.transactions.list` | `GET /openmeter/customers/{customerId}/credits/transactions` | List credit transactions for a customer. Returns an immutable, chronological record of credit movements: funded credits and consumed credits. Transactions are returned in reverse chronological order by default. | +| `client.customers.charges.list` | `GET /openmeter/customers/{customerId}/charges` | List customer charges. Returns the customer's charges that are represented as either flat fee or usage-based charges. | +| `client.customers.charges.create` | `POST /openmeter/customers/{customerId}/charges` | Create customer charge. | ### Entitlements diff --git a/api/spec/packages/aip-client-javascript/src/funcs/customers.ts b/api/spec/packages/aip-client-javascript/src/funcs/customers.ts index bef89f27ba..540e509032 100644 --- a/api/spec/packages/aip-client-javascript/src/funcs/customers.ts +++ b/api/spec/packages/aip-client-javascript/src/funcs/customers.ts @@ -37,6 +37,8 @@ import type { GetCustomerCreditBalanceResponse, CreateCreditAdjustmentRequest, CreateCreditAdjustmentResponse, + VoidCreditGrantRequest, + VoidCreditGrantResponse, UpdateCreditGrantExternalSettlementRequest, UpdateCreditGrantExternalSettlementResponse, ListCreditTransactionsRequest, @@ -580,6 +582,54 @@ export function createCreditAdjustment( }) } +/** + * Void credit grant + * + * Void a credit grant, forfeiting the remaining unused balance. + * + * Voiding is a forward-looking, irreversible operation. Credits already consumed + * by usage remain unaffected — only the remaining balance is forfeited. The grant + * reads as `voided` status afterwards. Payment state is not adjusted when + * `payment_adjustment` is `none`, so invoice-backed or externally collected + * payments may still collect the original amount. Only `active` grants can be + * voided; voiding a pending, expired, or fully consumed grant returns a conflict. + * Retrying a successful void is an idempotent success. + * + * POST /openmeter/customers/{customerId}/credits/grants/{creditGrantId}/void + */ +export function voidCreditGrant( + client: Client, + req: VoidCreditGrantRequest, + options?: RequestOptions, +): Promise> { + return request(() => { + const path = `openmeter/customers/${(() => { + if (req.customerId === undefined) { + throw new Error('missing path parameter: customerId') + } + return encodeURIComponent(String(req.customerId)) + })()}/credits/grants/${(() => { + if (req.creditGrantId === undefined) { + throw new Error('missing path parameter: creditGrantId') + } + return encodeURIComponent(String(req.creditGrantId)) + })()}/void` + const body = toWire(req.body, schemas.voidCreditGrantBody) + if (client._options.validate) { + assertValid(schemas.voidCreditGrantBodyWire, body) + } + return http(client) + .post(path, { ...options, json: body }) + .json() + .then((data) => { + if (client._options.validate) { + assertValid(schemas.voidCreditGrantResponseWire, data) + } + return fromWire(data, schemas.voidCreditGrantResponse) + }) + }) +} + /** * Update credit grant external settlement status * diff --git a/api/spec/packages/aip-client-javascript/src/index.ts b/api/spec/packages/aip-client-javascript/src/index.ts index 3a99e578a9..a94251973c 100644 --- a/api/spec/packages/aip-client-javascript/src/index.ts +++ b/api/spec/packages/aip-client-javascript/src/index.ts @@ -374,6 +374,7 @@ export type { CreateCreditGrantPurchaseInput, RateCardMeteredEntitlementInput, CreditGrantPurchaseInput, + VoidCreditGrantRequestInput, UnitConfigInput, WorkflowInvoicingSettingsInput, GovernanceQueryRequestInput, diff --git a/api/spec/packages/aip-client-javascript/src/models/operations/customers.ts b/api/spec/packages/aip-client-javascript/src/models/operations/customers.ts index db274409ce..01f824aaf0 100644 --- a/api/spec/packages/aip-client-javascript/src/models/operations/customers.ts +++ b/api/spec/packages/aip-client-javascript/src/models/operations/customers.ts @@ -34,6 +34,7 @@ import type { UpsertAppCustomerDataRequest, UpsertCustomerBillingDataRequest, UpsertCustomerRequest as UpsertCustomerRequestBody, + VoidCreditGrantRequestInput, } from '../types.js' export type CreateCustomerRequest = AcceptDateStrings @@ -156,6 +157,13 @@ export type CreateCreditAdjustmentRequest = AcceptDateStrings<{ }> export type CreateCreditAdjustmentResponse = CreditAdjustment +export type VoidCreditGrantRequest = AcceptDateStrings<{ + customerId: string + creditGrantId: string + body: VoidCreditGrantRequestInput +}> +export type VoidCreditGrantResponse = CreditGrant + export type UpdateCreditGrantExternalSettlementRequest = AcceptDateStrings<{ customerId: string creditGrantId: string diff --git a/api/spec/packages/aip-client-javascript/src/models/schemas.ts b/api/spec/packages/aip-client-javascript/src/models/schemas.ts index c757e123e3..e90f863544 100644 --- a/api/spec/packages/aip-client-javascript/src/models/schemas.ts +++ b/api/spec/packages/aip-client-javascript/src/models/schemas.ts @@ -571,11 +571,18 @@ export const stringFieldFilterExact = z 'Filters on the given string field value by exact match. All properties are optional; provide exactly one to specify the comparison.', ) +export const creditGrantVoidPaymentAdjustment = z + .enum(['none']) + + .describe( + 'Describes how voiding a credit grant adjusts related payment state. - `none`: Voiding does not adjust invoices, payment authorization, settlement, payment intents, or external collection state.', + ) + export const creditTransactionType = z - .enum(['funded', 'consumed', 'expired']) + .enum(['funded', 'consumed', 'expired', 'voided']) .describe( - 'The type of the credit transaction. - `funded`: Credit granted and available for consumption. - `consumed`: Credit consumed by usage or fees. - `expired`: Credit removed because it expired before being used.', + 'The type of the credit transaction. - `funded`: Credit granted and available for consumption. - `consumed`: Credit consumed by usage or fees. - `expired`: Credit removed because it expired before being used. - `voided`: Credit removed because the grant was voided before being used.', ) export const chargesExpand = z @@ -2678,6 +2685,14 @@ export const listPlansParamsFilter = z }) .describe('Filter options for listing plans.') +export const voidCreditGrantRequest = z + .object({ + paymentAdjustment: creditGrantVoidPaymentAdjustment + .optional() + .default('none'), + }) + .describe('Request body for voiding a credit grant.') + export const subscriptionCreate = z .object({ labels: labels.optional(), @@ -5864,6 +5879,15 @@ export const createCreditAdjustmentBody = createCreditAdjustmentRequest export const createCreditAdjustmentResponse = creditAdjustment +export const voidCreditGrantPathParams = z.object({ + customerId: ulid, + creditGrantId: ulid, +}) + +export const voidCreditGrantBody = voidCreditGrantRequest + +export const voidCreditGrantResponse = creditGrant + export const updateCreditGrantExternalSettlementPathParams = z.object({ customerId: ulid, creditGrantId: ulid, @@ -7036,11 +7060,18 @@ export const stringFieldFilterExactWire = z 'Filters on the given string field value by exact match. All properties are optional; provide exactly one to specify the comparison.', ) +export const creditGrantVoidPaymentAdjustmentWire = z + .enum(['none']) + + .describe( + 'Describes how voiding a credit grant adjusts related payment state. - `none`: Voiding does not adjust invoices, payment authorization, settlement, payment intents, or external collection state.', + ) + export const creditTransactionTypeWire = z - .enum(['funded', 'consumed', 'expired']) + .enum(['funded', 'consumed', 'expired', 'voided']) .describe( - 'The type of the credit transaction. - `funded`: Credit granted and available for consumption. - `consumed`: Credit consumed by usage or fees. - `expired`: Credit removed because it expired before being used.', + 'The type of the credit transaction. - `funded`: Credit granted and available for consumption. - `consumed`: Credit consumed by usage or fees. - `expired`: Credit removed because it expired before being used. - `voided`: Credit removed because the grant was voided before being used.', ) export const chargesExpandWire = z @@ -9145,6 +9176,14 @@ export const listPlansParamsFilterWire = z }) .describe('Filter options for listing plans.') +export const voidCreditGrantRequestWire = z + .strictObject({ + payment_adjustment: creditGrantVoidPaymentAdjustmentWire + .optional() + .default('none'), + }) + .describe('Request body for voiding a credit grant.') + export const subscriptionCreateWire = z .strictObject({ labels: labelsWire.optional(), @@ -12354,6 +12393,15 @@ export const createCreditAdjustmentBodyWire = createCreditAdjustmentRequestWire export const createCreditAdjustmentResponseWire = creditAdjustmentWire +export const voidCreditGrantPathParamsWire = z.object({ + customerId: ulidWire, + creditGrantId: ulidWire, +}) + +export const voidCreditGrantBodyWire = voidCreditGrantRequestWire + +export const voidCreditGrantResponseWire = creditGrantWire + export const updateCreditGrantExternalSettlementPathParamsWire = z.object({ customerId: ulidWire, creditGrantId: ulidWire, diff --git a/api/spec/packages/aip-client-javascript/src/models/types.ts b/api/spec/packages/aip-client-javascript/src/models/types.ts index bc736de831..e52b5fc903 100644 --- a/api/spec/packages/aip-client-javascript/src/models/types.ts +++ b/api/spec/packages/aip-client-javascript/src/models/types.ts @@ -1768,6 +1768,18 @@ export interface ListPlansParamsFilter { currency?: StringFieldFilterExact } +/** Request body for voiding a credit grant. */ +export interface VoidCreditGrantRequest { + /** + * How voiding adjusts payment state related to the grant. + * + * Currently only `none` is supported: voiding does not adjust invoices, payment + * authorization, settlement, payment intents, or external collection state. If + * payment later completes, the original invoiced amount may still be collected. + */ + paymentAdjustment: 'none' +} + /** Subscription create request. */ export interface SubscriptionCreate { labels?: Labels @@ -2198,7 +2210,7 @@ export interface CreateCreditAdjustmentRequest { /** Filter options for listing credit transactions. */ export interface ListCreditTransactionsParamsFilter { /** Filter credit transactions by type. */ - type?: 'funded' | 'consumed' | 'expired' + type?: 'funded' | 'consumed' | 'expired' | 'voided' /** Filter credit transactions by currency. */ currency?: BillingCurrencyCode /** @@ -2235,7 +2247,7 @@ export interface CreditTransaction { /** The date and time the transaction was booked. */ bookedAt: Date /** The type of credit transaction. */ - type: 'funded' | 'consumed' | 'expired' + type: 'funded' | 'consumed' | 'expired' | 'voided' /** Currency of the balance affected by the transaction. */ currency: BillingCurrencyCode /** @@ -5592,6 +5604,18 @@ export interface CreditGrantPurchaseInput { settlementStatus?: 'pending' | 'authorized' | 'settled' } +/** Request body for voiding a credit grant. */ +export interface VoidCreditGrantRequestInput { + /** + * How voiding adjusts payment state related to the grant. + * + * Currently only `none` is supported: voiding does not adjust invoices, payment + * authorization, settlement, payment intents, or external collection state. If + * payment later completes, the original invoiced amount may still be collected. + */ + paymentAdjustment?: 'none' +} + /** * Unit conversion configuration. * diff --git a/api/spec/packages/aip-client-javascript/src/sdk/customers.ts b/api/spec/packages/aip-client-javascript/src/sdk/customers.ts index 4b54847a9c..0d95f9b9f9 100644 --- a/api/spec/packages/aip-client-javascript/src/sdk/customers.ts +++ b/api/spec/packages/aip-client-javascript/src/sdk/customers.ts @@ -19,6 +19,7 @@ import { listCreditGrants, getCustomerCreditBalance, createCreditAdjustment, + voidCreditGrant, updateCreditGrantExternalSettlement, listCreditTransactions, listCustomerCharges, @@ -55,6 +56,8 @@ import type { GetCustomerCreditBalanceResponse, CreateCreditAdjustmentRequest, CreateCreditAdjustmentResponse, + VoidCreditGrantRequest, + VoidCreditGrantResponse, UpdateCreditGrantExternalSettlementRequest, UpdateCreditGrantExternalSettlementResponse, ListCreditTransactionsRequest, @@ -349,6 +352,28 @@ export class CustomersCreditsGrants { ) } + /** + * Void credit grant + * + * Void a credit grant, forfeiting the remaining unused balance. + * + * Voiding is a forward-looking, irreversible operation. Credits already consumed + * by usage remain unaffected — only the remaining balance is forfeited. The grant + * reads as `voided` status afterwards. Payment state is not adjusted when + * `payment_adjustment` is `none`, so invoice-backed or externally collected + * payments may still collect the original amount. Only `active` grants can be + * voided; voiding a pending, expired, or fully consumed grant returns a conflict. + * Retrying a successful void is an idempotent success. + * + * POST /openmeter/customers/{customerId}/credits/grants/{creditGrantId}/void + */ + async void( + request: VoidCreditGrantRequest, + options?: RequestOptions, + ): Promise { + return unwrap(await voidCreditGrant(this._client, request, options)) + } + /** * Update credit grant external settlement status * diff --git a/api/spec/packages/aip/src/customers/credits/grant.tsp b/api/spec/packages/aip/src/customers/credits/grant.tsp index 7c1c3fb9af..01394230df 100644 --- a/api/spec/packages/aip/src/customers/credits/grant.tsp +++ b/api/spec/packages/aip/src/customers/credits/grant.tsp @@ -92,6 +92,19 @@ enum CreditGrantStatus { Voided: "voided", } +/** + * Describes how voiding a credit grant adjusts related payment state. + * + * - `none`: Voiding does not adjust invoices, payment authorization, settlement, + * payment intents, or external collection state. + */ +@friendlyName("BillingCreditGrantVoidPaymentAdjustment") +@summary("Credit grant void payment adjustment") +enum CreditGrantVoidPaymentAdjustment { + // No payment state is adjusted by the void operation. + None: "none", +} + /** * Tax configuration for a credit grant. * diff --git a/api/spec/packages/aip/src/customers/credits/ledger.tsp b/api/spec/packages/aip/src/customers/credits/ledger.tsp index 8a9679b038..309792944a 100644 --- a/api/spec/packages/aip/src/customers/credits/ledger.tsp +++ b/api/spec/packages/aip/src/customers/credits/ledger.tsp @@ -9,6 +9,7 @@ namespace Customers; * - `funded`: Credit granted and available for consumption. * - `consumed`: Credit consumed by usage or fees. * - `expired`: Credit removed because it expired before being used. + * - `voided`: Credit removed because the grant was voided before being used. */ @friendlyName("BillingCreditTransactionType") enum CreditTransactionType { @@ -20,6 +21,9 @@ enum CreditTransactionType { // Credit removed because it expired before being used. Expired: "expired", + + // Credit removed because the grant was voided before being used. + Voided: "voided", } // /** diff --git a/api/spec/packages/aip/src/customers/credits/operations.tsp b/api/spec/packages/aip/src/customers/credits/operations.tsp index d3a98a47cd..6c1c79a58e 100644 --- a/api/spec/packages/aip/src/customers/credits/operations.tsp +++ b/api/spec/packages/aip/src/customers/credits/operations.tsp @@ -90,24 +90,49 @@ namespace Credits.Grants { } } -// interface CustomerCreditGrantVoidOperations { -// /** -// * Void a credit grant, forfeiting the remaining unused balance. -// * -// * Voiding is a forward-looking, irreversible operation. Credits already -// * consumed by usage remain unaffected — only the remaining balance is -// * forfeited. The grant transitions to `voided` status and a breakage -// * ledger entry is recorded for the forfeited amount. Only `pending` -// * and `active` grants can be voided. -// */ -// @post -// @operationId("void-credit-grant") -// @summary("Void credit grant") -// voidGrant( -// @path customerId: Shared.ULID, -// @path creditGrantId: Shared.ULID, -// ): Shared.UpdateResponse | Common.NotFound | Common.ErrorResponses; -// } +namespace Credits.Grants { + /** + * Request body for voiding a credit grant. + */ + @friendlyName("VoidCreditGrantRequest") + model VoidCreditGrantRequest { + /** + * How voiding adjusts payment state related to the grant. + * + * Currently only `none` is supported: voiding does not adjust invoices, payment + * authorization, settlement, payment intents, or external collection state. If + * payment later completes, the original invoiced amount may still be collected. + */ + payment_adjustment?: CreditGrantVoidPaymentAdjustment = CreditGrantVoidPaymentAdjustment.None; + } + + interface CustomerCreditGrantVoidOperations { + /** + * Void a credit grant, forfeiting the remaining unused balance. + * + * Voiding is a forward-looking, irreversible operation. Credits already consumed + * by usage remain unaffected — only the remaining balance is forfeited. The grant + * reads as `voided` status afterwards. Payment state is not adjusted when + * `payment_adjustment` is `none`, so invoice-backed or externally collected + * payments may still collect the original amount. Only `active` grants can be + * voided; voiding a pending, expired, or fully consumed grant returns a conflict. + * Retrying a successful void is an idempotent success. + */ + @extension(Shared.UnstableExtension, true) + @post + @operationId("void-credit-grant") + @summary("Void credit grant") + voidGrant( + @path customerId: Shared.ULID, + @path creditGrantId: Shared.ULID, + @body request?: VoidCreditGrantRequest, + ): + | Shared.UpdateResponse + | Common.NotFound + | Common.Conflict + | Common.ErrorResponses; + } +} /** * Request body for updating the external payment settlement status of a credit diff --git a/api/spec/packages/aip/src/konnect.tsp b/api/spec/packages/aip/src/konnect.tsp index ff4a926f57..e0bd1fc7c9 100644 --- a/api/spec/packages/aip/src/konnect.tsp +++ b/api/spec/packages/aip/src/konnect.tsp @@ -111,8 +111,8 @@ interface CustomerCreditAdjustmentsEndpoints @route("/openmeter/customers/{customerId}/credits/grants/{creditGrantId}") @tag(Shared.CustomersTag) interface CustomerCreditGrantEndpoints { - // @route("/void") - // voidGrant is Customers.CustomerCreditGrantVoidOperations.voidGrant; + @route("/void") + voidGrant is Customers.Credits.Grants.CustomerCreditGrantVoidOperations.voidGrant; @route("/settlement/external") updateExternalSettlement is Customers.Credits.Grants.CustomerCreditGrantExternalSettlementOperations.updateExternalSettlement; diff --git a/api/spec/packages/aip/src/openmeter.tsp b/api/spec/packages/aip/src/openmeter.tsp index d37a5d2b5c..95a2977c6b 100644 --- a/api/spec/packages/aip/src/openmeter.tsp +++ b/api/spec/packages/aip/src/openmeter.tsp @@ -116,8 +116,8 @@ interface CustomerCreditAdjustmentsEndpoints @route("/openmeter/customers/{customerId}/credits/grants/{creditGrantId}") @tag(Shared.CustomersTag) interface CustomerCreditGrantEndpoints { - // @route("/void") - // voidGrant is Customers.CustomerCreditGrantVoidOperations.voidGrant; + @route("/void") + voidGrant is Customers.Credits.Grants.CustomerCreditGrantVoidOperations.voidGrant; @route("/settlement/external") updateExternalSettlement is Customers.Credits.Grants.CustomerCreditGrantExternalSettlementOperations.updateExternalSettlement; diff --git a/api/v3/api.gen.go b/api/v3/api.gen.go index b41a7c42b8..874ad23a8b 100644 --- a/api/v3/api.gen.go +++ b/api/v3/api.gen.go @@ -437,6 +437,21 @@ func (e BillingCreditGrantStatus) Valid() bool { } } +// Defines values for BillingCreditGrantVoidPaymentAdjustment. +const ( + BillingCreditGrantVoidPaymentAdjustmentNone BillingCreditGrantVoidPaymentAdjustment = "none" +) + +// Valid indicates whether the value is a known member of the BillingCreditGrantVoidPaymentAdjustment enum. +func (e BillingCreditGrantVoidPaymentAdjustment) Valid() bool { + switch e { + case BillingCreditGrantVoidPaymentAdjustmentNone: + return true + default: + return false + } +} + // Defines values for BillingCreditPurchasePaymentSettlementStatus. const ( BillingCreditPurchasePaymentSettlementStatusAuthorized BillingCreditPurchasePaymentSettlementStatus = "authorized" @@ -463,6 +478,7 @@ const ( BillingCreditTransactionTypeConsumed BillingCreditTransactionType = "consumed" BillingCreditTransactionTypeExpired BillingCreditTransactionType = "expired" BillingCreditTransactionTypeFunded BillingCreditTransactionType = "funded" + BillingCreditTransactionTypeVoided BillingCreditTransactionType = "voided" ) // Valid indicates whether the value is a known member of the BillingCreditTransactionType enum. @@ -474,6 +490,8 @@ func (e BillingCreditTransactionType) Valid() bool { return true case BillingCreditTransactionTypeFunded: return true + case BillingCreditTransactionTypeVoided: + return true default: return false } @@ -2880,6 +2898,12 @@ type BillingCreditGrantTaxConfig struct { TaxCode *TaxCodeReference `json:"tax_code,omitempty"` } +// BillingCreditGrantVoidPaymentAdjustment Describes how voiding a credit grant adjusts related payment state. +// +// - `none`: Voiding does not adjust invoices, payment authorization, settlement, +// payment intents, or external collection state. +type BillingCreditGrantVoidPaymentAdjustment string + // BillingCreditPurchasePaymentSettlementStatus Credit purchase payment settlement status. // // - `pending`: Payment has been initiated and is not yet authorized. @@ -2939,6 +2963,7 @@ type BillingCreditTransaction struct { // - `funded`: Credit granted and available for consumption. // - `consumed`: Credit consumed by usage or fees. // - `expired`: Credit removed because it expired before being used. +// - `voided`: Credit removed because the grant was voided before being used. type BillingCreditTransactionType string // BillingCurrency Fiat or custom currency. @@ -7341,6 +7366,16 @@ type UpsertTaxCodeRequest struct { // UsageAttributionSubjectKey Subject key. type UsageAttributionSubjectKey = string +// VoidCreditGrantRequest Request body for voiding a credit grant. +type VoidCreditGrantRequest struct { + // PaymentAdjustment How voiding adjusts payment state related to the grant. + // + // Currently only `none` is supported: voiding does not adjust invoices, payment + // authorization, settlement, payment intents, or external collection state. If + // payment later completes, the original invoiced amount may still be collected. + PaymentAdjustment *BillingCreditGrantVoidPaymentAdjustment `json:"payment_adjustment,omitempty"` +} + // CursorPaginationQuery Determines which page of the collection to retrieve. type CursorPaginationQuery = CursorPaginationQueryPage @@ -7764,6 +7799,9 @@ type CreateCreditGrantJSONRequestBody = CreateCreditGrantRequest // UpdateCreditGrantExternalSettlementJSONRequestBody defines body for UpdateCreditGrantExternalSettlement for application/json ContentType. type UpdateCreditGrantExternalSettlementJSONRequestBody = UpdateCreditGrantExternalSettlementRequest +// VoidCreditGrantJSONRequestBody defines body for VoidCreditGrant for application/json ContentType. +type VoidCreditGrantJSONRequestBody = VoidCreditGrantRequest + // UpdateOrganizationDefaultTaxCodesJSONRequestBody defines body for UpdateOrganizationDefaultTaxCodes for application/json ContentType. type UpdateOrganizationDefaultTaxCodesJSONRequestBody = UpdateOrganizationDefaultTaxCodesRequest @@ -9600,6 +9638,9 @@ type ServerInterface interface { // Update credit grant external settlement status // (POST /openmeter/customers/{customerId}/credits/grants/{creditGrantId}/settlement/external) UpdateCreditGrantExternalSettlement(w http.ResponseWriter, r *http.Request, customerId ULID, creditGrantId ULID) + // Void credit grant + // (POST /openmeter/customers/{customerId}/credits/grants/{creditGrantId}/void) + VoidCreditGrant(w http.ResponseWriter, r *http.Request, customerId ULID, creditGrantId ULID) // List credit transactions // (GET /openmeter/customers/{customerId}/credits/transactions) ListCreditTransactions(w http.ResponseWriter, r *http.Request, customerId ULID, params ListCreditTransactionsParams) @@ -9981,6 +10022,12 @@ func (_ Unimplemented) UpdateCreditGrantExternalSettlement(w http.ResponseWriter w.WriteHeader(http.StatusNotImplemented) } +// Void credit grant +// (POST /openmeter/customers/{customerId}/credits/grants/{creditGrantId}/void) +func (_ Unimplemented) VoidCreditGrant(w http.ResponseWriter, r *http.Request, customerId ULID, creditGrantId ULID) { + w.WriteHeader(http.StatusNotImplemented) +} + // List credit transactions // (GET /openmeter/customers/{customerId}/credits/transactions) func (_ Unimplemented) ListCreditTransactions(w http.ResponseWriter, r *http.Request, customerId ULID, params ListCreditTransactionsParams) { @@ -11346,6 +11393,40 @@ func (siw *ServerInterfaceWrapper) UpdateCreditGrantExternalSettlement(w http.Re handler.ServeHTTP(w, r) } +// VoidCreditGrant operation middleware +func (siw *ServerInterfaceWrapper) VoidCreditGrant(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "customerId" ------------- + var customerId ULID + + err = runtime.BindStyledParameterWithOptions("simple", "customerId", chi.URLParam(r, "customerId"), &customerId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "customerId", Err: err}) + return + } + + // ------------- Path parameter "creditGrantId" ------------- + var creditGrantId ULID + + err = runtime.BindStyledParameterWithOptions("simple", "creditGrantId", chi.URLParam(r, "creditGrantId"), &creditGrantId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "creditGrantId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.VoidCreditGrant(w, r, customerId, creditGrantId) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + // ListCreditTransactions operation middleware func (siw *ServerInterfaceWrapper) ListCreditTransactions(w http.ResponseWriter, r *http.Request) { @@ -13046,6 +13127,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/openmeter/customers/{customerId}/credits/grants/{creditGrantId}/settlement/external", wrapper.UpdateCreditGrantExternalSettlement) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/openmeter/customers/{customerId}/credits/grants/{creditGrantId}/void", wrapper.VoidCreditGrant) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/openmeter/customers/{customerId}/credits/transactions", wrapper.ListCreditTransactions) }) @@ -13387,693 +13471,703 @@ var swaggerSpec = []string{ "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", + "//9wu2aaUkAFAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v3/client/README.md b/api/v3/client/README.md index f327703279..d30ba7331b 100644 --- a/api/v3/client/README.md +++ b/api/v3/client/README.md @@ -168,6 +168,7 @@ The full call path, HTTP route, and a short description are listed below. | `om.Customers.Credits.Grants.Create` | `POST /openmeter/customers/{customerId}/credits/grants` | Create a new credit grant. A credit grant represents an allocation of prepaid credits to a customer. | | `om.Customers.Credits.Grants.Get` | `GET /openmeter/customers/{customerId}/credits/grants/{creditGrantId}` | Get a credit grant. | | `om.Customers.Credits.Grants.List` | `GET /openmeter/customers/{customerId}/credits/grants` | List credit grants. | +| `om.Customers.Credits.Grants.Void` | `POST /openmeter/customers/{customerId}/credits/grants/{creditGrantId}/void` | Void a credit grant, forfeiting the remaining unused balance. Voiding is a forward-looking, irreversible operation. Credits already consumed by usage remain unaffected — only the remaining balance is forfeited. The grant reads as `voided` status afterwards. Payment state is not adjusted when `payment_adjustment` is `none`, so invoice-backed or externally collected payments may still collect the original amount. Only `active` grants can be voided; voiding a pending, expired, or fully consumed grant returns a conflict. Retrying a successful void is an idempotent success. | | `om.Customers.Credits.Grants.UpdateExternalSettlement` | `POST /openmeter/customers/{customerId}/credits/grants/{creditGrantId}/settlement/external` | Update the payment settlement status of an externally funded credit grant. Use this endpoint to synchronize the payment state of an external payment with the system so that revenue recognition and credit availability work as expected. | | `om.Customers.Credits.Balance.Get` | `GET /openmeter/customers/{customerId}/credits/balance` | Get a credit balance. | | `om.Customers.Credits.Adjustments.Create` | `POST /openmeter/customers/{customerId}/credits/adjustments` | A credit adjustment can be used to make manual adjustments to a customer's credit balance. Supported use-cases: - Usage correction | diff --git a/api/v3/client/customers_credits_grants.go b/api/v3/client/customers_credits_grants.go index dd2d15918d..23097e0626 100644 --- a/api/v3/client/customers_credits_grants.go +++ b/api/v3/client/customers_credits_grants.go @@ -137,6 +137,43 @@ func (s *CustomersCreditsGrantsService) ListAll(ctx context.Context, customerID }) } +// Void a credit grant, forfeiting the remaining unused balance. +// +// Voiding is a forward-looking, irreversible operation. Credits already consumed +// by usage remain unaffected — only the remaining balance is forfeited. The grant +// reads as `voided` status afterwards. Payment state is not adjusted when +// `payment_adjustment` is `none`, so invoice-backed or externally collected +// payments may still collect the original amount. Only `active` grants can be +// voided; voiding a pending, expired, or fully consumed grant returns a conflict. +// Retrying a successful void is an idempotent success. +func (s *CustomersCreditsGrantsService) Void(ctx context.Context, customerID string, creditGrantID string, request *VoidCreditGrantRequest) (*CreditGrant, error) { + if customerID == "" { + return nil, fmt.Errorf("openmeter: %s must not be empty: %w", "customerID", ErrEmptyID) + } + + if creditGrantID == "" { + return nil, fmt.Errorf("openmeter: %s must not be empty: %w", "creditGrantID", ErrEmptyID) + } + + path := "/openmeter/customers/{customerId}/credits/grants/{creditGrantId}/void" + + path = replacePathParam(path, "customerId", customerID) + + path = replacePathParam(path, "creditGrantId", creditGrantID) + + req, err := s.client.newRequestWithContentType(ctx, http.MethodPost, path, nil, optionalBody(request), "application/json", "application/json") + if err != nil { + return nil, err + } + + var out CreditGrant + if err := s.client.doJSON(req, &out); err != nil { + return nil, err + } + + return &out, nil +} + // Update the payment settlement status of an externally funded credit grant. // // Use this endpoint to synchronize the payment state of an external payment with diff --git a/api/v3/client/models_customers.go b/api/v3/client/models_customers.go index b79722a9fd..3fe5d8d7fa 100644 --- a/api/v3/client/models_customers.go +++ b/api/v3/client/models_customers.go @@ -1340,6 +1340,25 @@ type CreditGrantTaxConfig struct { TaxCode *TaxCodeReference `json:"tax_code,omitempty"` } +// Describes how voiding a credit grant adjusts related payment state. +// +// - `none`: Voiding does not adjust invoices, payment authorization, settlement, +// payment intents, or external collection state. +type CreditGrantVoidPaymentAdjustment string + +const ( + CreditGrantVoidPaymentAdjustmentNone CreditGrantVoidPaymentAdjustment = "none" +) + +func (value CreditGrantVoidPaymentAdjustment) Valid() bool { + switch value { + case CreditGrantVoidPaymentAdjustmentNone: + return true + default: + return false + } +} + // Credit purchase payment settlement status. // // - `pending`: Payment has been initiated and is not yet authorized. @@ -1408,17 +1427,19 @@ type CreditTransactionPaginatedResponse struct { // - `funded`: Credit granted and available for consumption. // - `consumed`: Credit consumed by usage or fees. // - `expired`: Credit removed because it expired before being used. +// - `voided`: Credit removed because the grant was voided before being used. type CreditTransactionType string const ( CreditTransactionTypeFunded CreditTransactionType = "funded" CreditTransactionTypeConsumed CreditTransactionType = "consumed" CreditTransactionTypeExpired CreditTransactionType = "expired" + CreditTransactionTypeVoided CreditTransactionType = "voided" ) func (value CreditTransactionType) Valid() bool { switch value { - case CreditTransactionTypeFunded, CreditTransactionTypeConsumed, CreditTransactionTypeExpired: + case CreditTransactionTypeFunded, CreditTransactionTypeConsumed, CreditTransactionTypeExpired, CreditTransactionTypeVoided: return true default: return false @@ -1545,3 +1566,13 @@ type UpsertCustomerRequest struct { // The billing address of the customer. Used for tax and invoicing. BillingAddress *Address `json:"billing_address,omitempty"` } + +// Request body for voiding a credit grant. +type VoidCreditGrantRequest struct { + // How voiding adjusts payment state related to the grant. + // + // Currently only `none` is supported: voiding does not adjust invoices, payment + // authorization, settlement, payment intents, or external collection state. If + // payment later completes, the original invoiced amount may still be collected. + PaymentAdjustment *CreditGrantVoidPaymentAdjustment `json:"payment_adjustment,omitempty"` +} diff --git a/api/v3/handlers/customers/credits/convert.go b/api/v3/handlers/customers/credits/convert.go index edaa4b1635..ee0aa53e84 100644 --- a/api/v3/handlers/customers/credits/convert.go +++ b/api/v3/handlers/customers/credits/convert.go @@ -12,12 +12,12 @@ import ( api "github.com/openmeterio/openmeter/api/v3" "github.com/openmeterio/openmeter/api/v3/labels" "github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase" - "github.com/openmeterio/openmeter/openmeter/billing/charges/meta" "github.com/openmeterio/openmeter/openmeter/billing/charges/models/payment" "github.com/openmeterio/openmeter/openmeter/billing/creditgrant" "github.com/openmeterio/openmeter/openmeter/ledger" "github.com/openmeterio/openmeter/openmeter/ledger/customerbalance" "github.com/openmeterio/openmeter/openmeter/productcatalog" + "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/currencyx" "github.com/openmeterio/openmeter/pkg/datetime" "github.com/openmeterio/openmeter/pkg/models" @@ -34,6 +34,7 @@ func toAPIBillingCreditGrant(charge creditpurchase.Charge) (api.BillingCreditGra ExpiresAt: charge.Intent.ExpiresAt, FundingMethod: toAPIBillingCreditFundingMethod(charge.Intent.Settlement), Status: toAPIBillingCreditGrantStatus(charge), + VoidedAt: charge.State.VoidedAt, CreatedAt: charge.CreatedAt, UpdatedAt: charge.UpdatedAt, DeletedAt: charge.DeletedAt, @@ -70,6 +71,13 @@ func toAPIBillingCreditFundingMethod(settlement creditpurchase.Settlement) api.B } func toAPIBillingCreditGrantStatus(charge creditpurchase.Charge) api.BillingCreditGrantStatus { + if charge.State.VoidedAt != nil { + return api.BillingCreditGrantStatusVoided + } + if charge.Intent.ExpiresAt != nil && !charge.Intent.ExpiresAt.After(clock.Now()) { + return api.BillingCreditGrantStatusExpired + } + switch charge.Status { case creditpurchase.StatusActive, creditpurchase.StatusFinal: return api.BillingCreditGrantStatusActive @@ -262,17 +270,16 @@ func fromAPIBillingCreditGrantFilters(filters *api.CreateCreditGrantFilters) (*c }, nil } -func fromAPIBillingCreditGrantStatus(status api.BillingCreditGrantStatus) (meta.ChargeStatus, error) { +func fromAPIBillingCreditGrantStatus(status api.BillingCreditGrantStatus) (creditgrant.GrantStatus, error) { switch status { case api.BillingCreditGrantStatusActive: - return meta.ChargeStatusActive, nil + return creditgrant.GrantStatusActive, nil case api.BillingCreditGrantStatusPending: - return meta.ChargeStatusCreated, nil + return creditgrant.GrantStatusPending, nil case api.BillingCreditGrantStatusVoided: - return meta.ChargeStatusDeleted, nil + return creditgrant.GrantStatusVoided, nil case api.BillingCreditGrantStatusExpired: - // Expired maps to final (terminal state, no further actions). - return meta.ChargeStatusFinal, nil + return creditgrant.GrantStatusExpired, nil default: return "", fmt.Errorf("unsupported credit grant status: %s", status) } @@ -393,6 +400,25 @@ func fromAPICreateCreditGrantRequest(ns string, customerID api.ULID, body api.Cr return req, nil } +func fromAPIVoidCreditGrantRequest( + ns string, + customerID api.ULID, + creditGrantID api.ULID, + body api.VoidCreditGrantRequest, +) (creditgrant.VoidInput, error) { + paymentAdjustment, err := fromAPIBillingCreditGrantVoidPaymentAdjustment(body.PaymentAdjustment) + if err != nil { + return creditgrant.VoidInput{}, err + } + + return creditgrant.VoidInput{ + Namespace: ns, + CustomerID: customerID, + ChargeID: creditGrantID, + PaymentAdjustment: paymentAdjustment, + }, nil +} + func fromAPIUpdateCreditGrantExternalSettlementRequest( ns string, customerID api.ULID, @@ -412,6 +438,19 @@ func fromAPIUpdateCreditGrantExternalSettlementRequest( }, nil } +func fromAPIBillingCreditGrantVoidPaymentAdjustment(adjustment *api.BillingCreditGrantVoidPaymentAdjustment) (creditgrant.VoidPaymentAdjustment, error) { + if adjustment == nil { + return creditgrant.VoidPaymentAdjustmentNone, nil + } + + switch *adjustment { + case api.BillingCreditGrantVoidPaymentAdjustmentNone: + return creditgrant.VoidPaymentAdjustmentNone, nil + default: + return "", newCreditGrantVoidPaymentAdjustmentInvalid(string(*adjustment)) + } +} + func fromAPIBillingCreditPurchasePaymentSettlementStatus(status api.BillingCreditPurchasePaymentSettlementStatus) (payment.Status, error) { switch status { case api.BillingCreditPurchasePaymentSettlementStatusAuthorized: @@ -445,6 +484,8 @@ func fromAPIBillingCreditTransactionType(filter *api.BillingCreditTransactionTyp txType = customerbalance.CreditTransactionTypeConsumed case api.BillingCreditTransactionTypeExpired: txType = customerbalance.CreditTransactionTypeExpired + case api.BillingCreditTransactionTypeVoided: + txType = customerbalance.CreditTransactionTypeVoided default: return nil } @@ -496,6 +537,8 @@ func toAPIBillingCreditTransactionType(txType customerbalance.CreditTransactionT return api.BillingCreditTransactionTypeFunded case customerbalance.CreditTransactionTypeExpired: return api.BillingCreditTransactionTypeExpired + case customerbalance.CreditTransactionTypeVoided: + return api.BillingCreditTransactionTypeVoided default: return api.BillingCreditTransactionTypeConsumed } diff --git a/api/v3/handlers/customers/credits/convert_test.go b/api/v3/handlers/customers/credits/convert_test.go index 1401f0c89c..9902a98b65 100644 --- a/api/v3/handlers/customers/credits/convert_test.go +++ b/api/v3/handlers/customers/credits/convert_test.go @@ -11,6 +11,7 @@ import ( api "github.com/openmeterio/openmeter/api/v3" "github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase" "github.com/openmeterio/openmeter/openmeter/billing/charges/meta" + "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/currencyx" "github.com/openmeterio/openmeter/pkg/models" ) @@ -52,6 +53,83 @@ func TestToAPIBillingCreditGrantPromotional(t *testing.T) { require.Equal(t, api.BillingCreditFundingMethodNone, grant.FundingMethod) require.Nil(t, grant.Purchase) require.Equal(t, "25", grant.Amount) + require.Equal(t, api.BillingCreditGrantStatusActive, grant.Status) + require.Nil(t, grant.VoidedAt) + + t.Run("ledger-derived voiding overrides the charge status", func(t *testing.T) { + voidedAt := now.Add(time.Hour) + + voidedCharge := charge + voidedCharge.State.VoidedAt = &voidedAt + + grant, err := toAPIBillingCreditGrant(voidedCharge) + require.NoError(t, err) + require.Equal(t, api.BillingCreditGrantStatusVoided, grant.Status) + require.Equal(t, lo.ToPtr(voidedAt), grant.VoidedAt) + }) +} + +func TestToAPIBillingCreditGrantStatusUsesExpiry(t *testing.T) { + now := time.Date(2026, time.April, 17, 10, 0, 0, 0, time.UTC) + clock.FreezeTime(now) + defer clock.UnFreeze() + + charge := creditpurchase.Charge{ + ChargeBase: creditpurchase.ChargeBase{ + Intent: creditpurchase.Intent{ + Intent: meta.Intent{ + CustomerID: "cust-1", + Currency: currencyx.Code("USD"), + }, + IntentMutableFields: creditpurchase.IntentMutableFields{ + CreditAmount: alpacadecimal.RequireFromString("25"), + Settlement: creditpurchase.NewSettlement(creditpurchase.PromotionalSettlement{}), + }, + }, + Status: creditpurchase.StatusActive, + }, + } + + t.Run("past expiry is public expired", func(t *testing.T) { + expiredAt := now.Add(-time.Nanosecond) + expiredCharge := charge + expiredCharge.Intent.ExpiresAt = &expiredAt + + grant, err := toAPIBillingCreditGrant(expiredCharge) + require.NoError(t, err) + require.Equal(t, api.BillingCreditGrantStatusExpired, grant.Status) + }) + + t.Run("expiry at now is public expired", func(t *testing.T) { + expiredCharge := charge + expiredCharge.Intent.ExpiresAt = &now + + grant, err := toAPIBillingCreditGrant(expiredCharge) + require.NoError(t, err) + require.Equal(t, api.BillingCreditGrantStatusExpired, grant.Status) + }) + + t.Run("future expiry stays active", func(t *testing.T) { + expiresAt := now.Add(time.Nanosecond) + activeCharge := charge + activeCharge.Intent.ExpiresAt = &expiresAt + + grant, err := toAPIBillingCreditGrant(activeCharge) + require.NoError(t, err) + require.Equal(t, api.BillingCreditGrantStatusActive, grant.Status) + }) + + t.Run("voided wins over expired", func(t *testing.T) { + expiredAt := now.Add(-time.Nanosecond) + voidedAt := now + voidedCharge := charge + voidedCharge.Intent.ExpiresAt = &expiredAt + voidedCharge.State.VoidedAt = &voidedAt + + grant, err := toAPIBillingCreditGrant(voidedCharge) + require.NoError(t, err) + require.Equal(t, api.BillingCreditGrantStatusVoided, grant.Status) + }) } func TestToAPIBillingCreditGrantKey(t *testing.T) { diff --git a/api/v3/handlers/customers/credits/errors.go b/api/v3/handlers/customers/credits/errors.go index 1b75d1544f..e26d8b9264 100644 --- a/api/v3/handlers/customers/credits/errors.go +++ b/api/v3/handlers/customers/credits/errors.go @@ -20,3 +20,16 @@ func newCreditGrantExternalSettlementStatusInvalid(status string) error { models.WithAttribute("status", status), ) } + +const errCodeCreditGrantVoidPaymentAdjustmentInvalid models.ErrorCode = "credit_grant_void_payment_adjustment_invalid" + +func newCreditGrantVoidPaymentAdjustmentInvalid(adjustment string) error { + return models.NewValidationIssue( + errCodeCreditGrantVoidPaymentAdjustmentInvalid, + fmt.Sprintf("unsupported credit grant void payment adjustment: %s", adjustment), + models.WithCriticalSeverity(), + commonhttp.WithHTTPStatusCodeAttribute(http.StatusBadRequest), + models.WithFieldString("payment_adjustment"), + models.WithAttribute("payment_adjustment", adjustment), + ) +} diff --git a/api/v3/handlers/customers/credits/handler.go b/api/v3/handlers/customers/credits/handler.go index 9b7f0370ce..3998e275db 100644 --- a/api/v3/handlers/customers/credits/handler.go +++ b/api/v3/handlers/customers/credits/handler.go @@ -23,6 +23,7 @@ type Handler interface { ListCreditGrants() ListCreditGrantsHandler CreateCreditGrant() CreateCreditGrantHandler GetCreditGrant() GetCreditGrantHandler + VoidCreditGrant() VoidCreditGrantHandler UpdateCreditGrantExternalSettlement() UpdateCreditGrantExternalSettlementHandler ListCreditTransactions() ListCreditTransactionsHandler } diff --git a/api/v3/handlers/customers/credits/void_grant.go b/api/v3/handlers/customers/credits/void_grant.go new file mode 100644 index 0000000000..a5a62b2dfb --- /dev/null +++ b/api/v3/handlers/customers/credits/void_grant.go @@ -0,0 +1,55 @@ +package customerscredits + +import ( + "context" + "net/http" + + api "github.com/openmeterio/openmeter/api/v3" + "github.com/openmeterio/openmeter/api/v3/apierrors" + "github.com/openmeterio/openmeter/api/v3/request" + "github.com/openmeterio/openmeter/openmeter/billing/creditgrant" + "github.com/openmeterio/openmeter/pkg/framework/commonhttp" + "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" +) + +type ( + VoidCreditGrantRequest = creditgrant.VoidInput + VoidCreditGrantResponse = api.BillingCreditGrant + VoidCreditGrantParams struct { + CustomerID api.ULID + CreditGrantID api.ULID + } + VoidCreditGrantHandler = httptransport.HandlerWithArgs[VoidCreditGrantRequest, VoidCreditGrantResponse, VoidCreditGrantParams] +) + +func (h *handler) VoidCreditGrant() VoidCreditGrantHandler { + return httptransport.NewHandlerWithArgs( + func(ctx context.Context, r *http.Request, args VoidCreditGrantParams) (VoidCreditGrantRequest, error) { + ns, err := h.resolveNamespace(ctx) + if err != nil { + return VoidCreditGrantRequest{}, err + } + + var body api.VoidCreditGrantRequest + if err := request.ParseOptionalBody(r, &body); err != nil { + return VoidCreditGrantRequest{}, err + } + + return fromAPIVoidCreditGrantRequest(ns, args.CustomerID, args.CreditGrantID, body) + }, + func(ctx context.Context, request VoidCreditGrantRequest) (VoidCreditGrantResponse, error) { + grant, err := h.creditGrantService.Void(ctx, request) + if err != nil { + return VoidCreditGrantResponse{}, err + } + + return toAPIBillingCreditGrant(grant) + }, + commonhttp.JSONResponseEncoderWithStatus[VoidCreditGrantResponse](http.StatusOK), + httptransport.AppendOptions( + h.options, + httptransport.WithOperationName("void-credit-grant"), + httptransport.WithErrorEncoder(apierrors.GenericErrorEncoder()), + )..., + ) +} diff --git a/api/v3/handlers/customers/credits/void_grant_test.go b/api/v3/handlers/customers/credits/void_grant_test.go new file mode 100644 index 0000000000..e0eeb54074 --- /dev/null +++ b/api/v3/handlers/customers/credits/void_grant_test.go @@ -0,0 +1,71 @@ +package customerscredits + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + + api "github.com/openmeterio/openmeter/api/v3" + "github.com/openmeterio/openmeter/api/v3/apierrors" + "github.com/openmeterio/openmeter/openmeter/billing/creditgrant" + "github.com/openmeterio/openmeter/pkg/models" +) + +func TestConvertAPIVoidCreditGrantRequestDefaultsPaymentAdjustment(t *testing.T) { + req, err := fromAPIVoidCreditGrantRequest( + "ns", + "cust-1", + "grant-1", + api.VoidCreditGrantRequest{}, + ) + + require.NoError(t, err) + require.Equal(t, "ns", req.Namespace) + require.Equal(t, "cust-1", req.CustomerID) + require.Equal(t, "grant-1", req.ChargeID) + require.Equal(t, creditgrant.VoidPaymentAdjustmentNone, req.PaymentAdjustment) +} + +func TestConvertAPIVoidCreditGrantRequestAcceptsNonePaymentAdjustment(t *testing.T) { + adjustment := api.BillingCreditGrantVoidPaymentAdjustmentNone + + req, err := fromAPIVoidCreditGrantRequest( + "ns", + "cust-1", + "grant-1", + api.VoidCreditGrantRequest{ + PaymentAdjustment: &adjustment, + }, + ) + + require.NoError(t, err) + require.Equal(t, creditgrant.VoidPaymentAdjustmentNone, req.PaymentAdjustment) +} + +func TestConvertAPIVoidCreditGrantRequestRejectsUnsupportedPaymentAdjustment(t *testing.T) { + adjustment := api.BillingCreditGrantVoidPaymentAdjustment("external") + + _, err := fromAPIVoidCreditGrantRequest( + "ns", + "cust-1", + "grant-1", + api.VoidCreditGrantRequest{ + PaymentAdjustment: &adjustment, + }, + ) + + require.Error(t, err) + issues, convErr := models.AsValidationIssues(err) + require.NoError(t, convErr) + require.Len(t, issues, 1) + require.ErrorContains(t, err, "unsupported credit grant void payment adjustment") + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/api/v3/customers/cust-1/credits/grants/grant-1/void", nil) + handled := apierrors.GenericErrorEncoder()(t.Context(), err, rec, req) + + require.True(t, handled) + require.Equal(t, http.StatusBadRequest, rec.Code) +} diff --git a/api/v3/openapi.yaml b/api/v3/openapi.yaml index a3ec08010c..d2c3142efc 100644 --- a/api/v3/openapi.yaml +++ b/api/v3/openapi.yaml @@ -1338,6 +1338,57 @@ paths: schema: $ref: '#/components/schemas/UpdateCreditGrantExternalSettlementRequest' x-unstable: true + /openmeter/customers/{customerId}/credits/grants/{creditGrantId}/void: + post: + operationId: void-credit-grant + summary: Void credit grant + description: |- + Void a credit grant, forfeiting the remaining unused balance. + + Voiding is a forward-looking, irreversible operation. Credits already consumed + by usage remain unaffected — only the remaining balance is forfeited. The grant + reads as `voided` status afterwards. Payment state is not adjusted when + `payment_adjustment` is `none`, so invoice-backed or externally collected + payments may still collect the original amount. Only `active` grants can be + voided; voiding a pending, expired, or fully consumed grant returns a conflict. + Retrying a successful void is an idempotent success. + parameters: + - name: customerId + in: path + required: true + schema: + $ref: '#/components/schemas/ULID' + - name: creditGrantId + in: path + required: true + schema: + $ref: '#/components/schemas/ULID' + responses: + '200': + description: CreditGrant updated response. + content: + application/json: + schema: + $ref: '#/components/schemas/BillingCreditGrant' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + tags: + - OpenMeter Customers + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/VoidCreditGrantRequest' + x-unstable: true /openmeter/customers/{customerId}/credits/transactions: get: operationId: list-credit-transactions @@ -5207,6 +5258,16 @@ components: Tax configuration should be provided to ensure correct revenue recognition, including for externally funded grants. title: Tax configuration for a credit grant + BillingCreditGrantVoidPaymentAdjustment: + type: string + enum: + - none + description: |- + Describes how voiding a credit grant adjusts related payment state. + + - `none`: Voiding does not adjust invoices, payment authorization, settlement, + payment intents, or external collection state. + title: Credit grant void payment adjustment BillingCreditPurchasePaymentSettlementStatus: type: string enum: @@ -5306,12 +5367,14 @@ components: - funded - consumed - expired + - voided description: |- The type of the credit transaction. - `funded`: Credit granted and available for consumption. - `consumed`: Credit consumed by usage or fees. - `expired`: Credit removed because it expired before being used. + - `voided`: Credit removed because the grant was voided before being used. BillingCurrency: type: object oneOf: @@ -12405,6 +12468,21 @@ components: type: string minLength: 1 description: Subject key. + VoidCreditGrantRequest: + type: object + properties: + payment_adjustment: + allOf: + - $ref: '#/components/schemas/BillingCreditGrantVoidPaymentAdjustment' + description: |- + How voiding adjusts payment state related to the grant. + + Currently only `none` is supported: voiding does not adjust invoices, payment + authorization, settlement, payment intents, or external collection state. If + payment later completes, the original invoiced amount may still be collected. + default: none + additionalProperties: false + description: Request body for voiding a credit grant. SortQuery: title: SortQuery type: string diff --git a/api/v3/server/routes.go b/api/v3/server/routes.go index 178eb83842..f5650ba460 100644 --- a/api/v3/server/routes.go +++ b/api/v3/server/routes.go @@ -458,6 +458,18 @@ func (s *Server) CreateCreditAdjustment(w http.ResponseWriter, r *http.Request, unimplemented.CreateCreditAdjustment(w, r, customerId) } +func (s *Server) VoidCreditGrant(w http.ResponseWriter, r *http.Request, customerId api.ULID, creditGrantId api.ULID) { + if !s.Credits.Enabled || s.customersCreditsHandler == nil || s.CreditGrantService == nil { + unimplemented.VoidCreditGrant(w, r, customerId, creditGrantId) + return + } + + s.customersCreditsHandler.VoidCreditGrant().With(customerscreditshandler.VoidCreditGrantParams{ + CustomerID: customerId, + CreditGrantID: creditGrantId, + }).ServeHTTP(w, r) +} + func (s *Server) UpdateCreditGrantExternalSettlement(w http.ResponseWriter, r *http.Request, customerId api.ULID, creditGrantId api.ULID) { if !s.Credits.Enabled || s.customersCreditsHandler == nil || s.CreditGrantService == nil { unimplemented.UpdateCreditGrantExternalSettlement(w, r, customerId, creditGrantId) diff --git a/app/common/creditgrant.go b/app/common/creditgrant.go index bb94e0e29a..131cb89e89 100644 --- a/app/common/creditgrant.go +++ b/app/common/creditgrant.go @@ -5,18 +5,69 @@ import ( "github.com/google/wire" + "github.com/openmeterio/openmeter/app/config" "github.com/openmeterio/openmeter/openmeter/billing/creditgrant" creditgrantservice "github.com/openmeterio/openmeter/openmeter/billing/creditgrant/service" "github.com/openmeterio/openmeter/openmeter/customer" + entdb "github.com/openmeterio/openmeter/openmeter/ent/db" + enttx "github.com/openmeterio/openmeter/openmeter/ent/tx" + "github.com/openmeterio/openmeter/openmeter/ledger" + ledgeraccount "github.com/openmeterio/openmeter/openmeter/ledger/account" + ledgerbreakage "github.com/openmeterio/openmeter/openmeter/ledger/breakage" + "github.com/openmeterio/openmeter/openmeter/ledger/creditvoid" + creditvoidadapter "github.com/openmeterio/openmeter/openmeter/ledger/creditvoid/adapter" + "github.com/openmeterio/openmeter/openmeter/ledger/transactions" ) var CreditGrant = wire.NewSet( + NewCreditVoidService, NewCreditGrantService, ) +func NewCreditVoidService( + creditsConfig config.CreditsConfiguration, + db *entdb.Client, + ledgerService ledger.Ledger, + balanceQuerier ledger.BalanceQuerier, + accountResolver ledger.AccountResolver, + accountService ledgeraccount.Service, + breakageService ledgerbreakage.Service, +) (creditvoid.Service, error) { + if !creditsConfig.Enabled { + return creditvoid.NewNoopService(), nil + } + + adapter, err := creditvoidadapter.New(creditvoidadapter.Config{ + Client: db, + }) + if err != nil { + return nil, fmt.Errorf("failed to create credit void adapter: %w", err) + } + + svc, err := creditvoid.NewService(creditvoid.Config{ + Adapter: adapter, + Ledger: ledgerService, + Dependencies: transactions.ResolverDependencies{ + AccountService: accountResolver, + AccountCatalog: accountService, + BalanceQuerier: balanceQuerier, + }, + Breakage: breakageService, + AccountLocker: accountService, + TransactionManager: enttx.NewCreator(db), + }) + if err != nil { + return nil, fmt.Errorf("failed to create credit void service: %w", err) + } + + return svc, nil +} + func NewCreditGrantService( + db *entdb.Client, billingRegistry BillingRegistry, customerService customer.Service, + creditVoidService creditvoid.Service, ) (creditgrant.Service, error) { if billingRegistry.Charges == nil { return nil, nil @@ -27,6 +78,8 @@ func NewCreditGrantService( ChargesService: billingRegistry.Charges.Service, BillingService: billingRegistry.Billing, CustomerService: customerService, + CreditVoidService: creditVoidService, + TransactionManager: enttx.NewCreator(db), }) if err != nil { return nil, fmt.Errorf("failed to create credit grant service: %w", err) diff --git a/app/common/customerbalance.go b/app/common/customerbalance.go index bd07e13fb2..7d739c65a7 100644 --- a/app/common/customerbalance.go +++ b/app/common/customerbalance.go @@ -7,6 +7,7 @@ import ( "github.com/openmeterio/openmeter/openmeter/ledger" ledgeraccount "github.com/openmeterio/openmeter/openmeter/ledger/account" ledgerbreakage "github.com/openmeterio/openmeter/openmeter/ledger/breakage" + "github.com/openmeterio/openmeter/openmeter/ledger/creditvoid" "github.com/openmeterio/openmeter/openmeter/ledger/customerbalance" ) @@ -23,6 +24,7 @@ func NewCustomerBalanceService( accountService ledgeraccount.Service, billingRegistry BillingRegistry, breakageService ledgerbreakage.Service, + creditVoidService creditvoid.Service, ) (customerbalance.Service, error) { if !creditsConfig.Enabled { return customerbalance.NewNoopService(), nil @@ -37,6 +39,7 @@ func NewCustomerBalanceService( Ledger: historicalLedger, BalanceQuerier: balanceQuerier, Breakage: breakageService, + CreditVoid: creditVoidService, }) } diff --git a/cmd/server/wire_gen.go b/cmd/server/wire_gen.go index ce3f2ade99..443ab8633c 100644 --- a/cmd/server/wire_gen.go +++ b/cmd/server/wire_gen.go @@ -525,7 +525,7 @@ func initializeApplication(ctx context.Context, conf config.Configuration) (Appl cleanup() return Application{}, nil, err } - creditgrantService, err := common.NewCreditGrantService(billingRegistry, customerService) + creditvoidService, err := common.NewCreditVoidService(creditsConfiguration, client, ledger, balanceQuerier, accountResolver, accountService, breakageService) if err != nil { cleanup7() cleanup6() @@ -536,7 +536,18 @@ func initializeApplication(ctx context.Context, conf config.Configuration) (Appl cleanup() return Application{}, nil, err } - customerbalanceService, err := common.NewCustomerBalanceService(creditsConfiguration, ledger, balanceQuerier, accountResolver, accountService, billingRegistry, breakageService) + creditgrantService, err := common.NewCreditGrantService(client, billingRegistry, customerService, creditvoidService) + if err != nil { + cleanup7() + cleanup6() + cleanup5() + cleanup4() + cleanup3() + cleanup2() + cleanup() + return Application{}, nil, err + } + customerbalanceService, err := common.NewCustomerBalanceService(creditsConfiguration, ledger, balanceQuerier, accountResolver, accountService, billingRegistry, breakageService, creditvoidService) if err != nil { cleanup7() cleanup6() diff --git a/e2e/customer_credits_v3_test.go b/e2e/customer_credits_v3_test.go index 4deea046e1..32ca7f2a7c 100644 --- a/e2e/customer_credits_v3_test.go +++ b/e2e/customer_credits_v3_test.go @@ -1,6 +1,7 @@ package e2e import ( + "encoding/json" "net/http" "net/url" "strings" @@ -152,6 +153,182 @@ func TestV3CreateCreditGrantIdempotencyKey(t *testing.T) { }) } +func voidCreditGrantRaw(t testing.TB, c *v3Client, customerID, creditGrantID string, body any) (int, *v3sdk.CreditGrant, *v3Problem) { + t.Helper() + + if body == nil { + body = map[string]any{} + } + + status, raw, problem := c.doMalformedRequest(http.MethodPost, "/customers/"+customerID+"/credits/grants/"+creditGrantID+"/void", body) + if status != http.StatusOK { + return status, nil, problem + } + + var grant v3sdk.CreditGrant + require.NoError(t, json.Unmarshal(raw, &grant)) + + return status, &grant, nil +} + +func TestV3VoidCreditGrant(t *testing.T) { + c := newV3Client(t) + currency := v3sdk.BillingCurrencyCode("USD") + + customer, err := c.Customers.Create(t.Context(), v3sdk.CreateCustomerRequest{ + Key: uniqueKey("credit_grant_void_customer"), + Name: "Credit Grant Void Test Customer", + Currency: lo.ToPtr("USD"), + }) + c.requireStatus(http.StatusCreated, err) + require.NotNil(t, customer) + customerID := customer.ID + + // given: + // - an active promotional grant of 25 and a second grant that stays untouched + grant, err := c.Customers.Credits.Grants.Create(t.Context(), customerID, v3sdk.CreateCreditGrantRequest{ + Name: "grant to void", + Amount: v3sdk.Numeric("25"), + Currency: currency, + FundingMethod: v3sdk.CreditFundingMethodNone, + }) + c.requireStatus(http.StatusCreated, err) + require.NotNil(t, grant) + require.Equal(t, v3sdk.CreditGrantStatusActive, grant.Status) + require.Nil(t, grant.VoidedAt) + + keptGrant, err := c.Customers.Credits.Grants.Create(t.Context(), customerID, v3sdk.CreateCreditGrantRequest{ + Name: "grant to keep", + Amount: v3sdk.Numeric("40"), + Currency: currency, + FundingMethod: v3sdk.CreditFundingMethodNone, + }) + c.requireStatus(http.StatusCreated, err) + require.NotNil(t, keptGrant) + + // when: + // - the first grant is voided + status, voided, problem := voidCreditGrantRaw(t, c, customerID, grant.ID, map[string]any{}) + require.Equal(t, http.StatusOK, status, "problem: %+v", problem) + require.NotNil(t, voided) + + // then: + // - the response reads as voided with a voided_at timestamp + require.Equal(t, v3sdk.CreditGrantStatusVoided, voided.Status) + require.NotNil(t, voided.VoidedAt) + + t.Run("get derives the voided status", func(t *testing.T) { + got, err := c.Customers.Credits.Grants.Get(t.Context(), customerID, grant.ID) + c.requireStatus(http.StatusOK, err) + require.NotNil(t, got) + require.Equal(t, v3sdk.CreditGrantStatusVoided, got.Status) + require.NotNil(t, got.VoidedAt) + assert.True(t, voided.VoidedAt.Equal(*got.VoidedAt), "voided_at must be stable across reads") + + kept, err := c.Customers.Credits.Grants.Get(t.Context(), customerID, keptGrant.ID) + c.requireStatus(http.StatusOK, err) + require.NotNil(t, kept) + require.Equal(t, v3sdk.CreditGrantStatusActive, kept.Status) + require.Nil(t, kept.VoidedAt) + }) + + t.Run("retrying the void is an idempotent success", func(t *testing.T) { + status, again, problem := voidCreditGrantRaw(t, c, customerID, grant.ID, map[string]any{}) + require.Equal(t, http.StatusOK, status, "problem: %+v", problem) + require.NotNil(t, again) + require.Equal(t, v3sdk.CreditGrantStatusVoided, again.Status) + require.NotNil(t, again.VoidedAt) + assert.True(t, voided.VoidedAt.Equal(*again.VoidedAt), "a retry must return the original void time") + }) + + t.Run("credit balance only carries the untouched grant", func(t *testing.T) { + balances, err := c.Customers.Credits.Balance.Get(t.Context(), customerID, v3sdk.GetCustomerCreditBalanceParams{}) + c.requireStatus(http.StatusOK, err) + require.NotNil(t, balances) + require.Len(t, balances.Balances, 1) + assert.Equal(t, v3sdk.Numeric("40"), balances.Balances[0].Settled) + }) + + t.Run("status filter separates voided from active grants", func(t *testing.T) { + voidedStatus := v3sdk.CreditGrantStatusVoided + voidedList, err := c.Customers.Credits.Grants.List(t.Context(), customerID, v3sdk.CreditGrantListParams{ + Filter: &v3sdk.CreditGrantFilter{Status: &voidedStatus}, + }) + c.requireStatus(http.StatusOK, err) + require.NotNil(t, voidedList) + require.Len(t, voidedList.Data, 1) + assert.Equal(t, grant.ID, voidedList.Data[0].ID) + + activeStatus := v3sdk.CreditGrantStatusActive + activeList, err := c.Customers.Credits.Grants.List(t.Context(), customerID, v3sdk.CreditGrantListParams{ + Filter: &v3sdk.CreditGrantFilter{Status: &activeStatus}, + }) + c.requireStatus(http.StatusOK, err) + require.NotNil(t, activeList) + require.Len(t, activeList.Data, 1) + assert.Equal(t, keptGrant.ID, activeList.Data[0].ID) + }) + + t.Run("transaction listing surfaces the forfeiture as voided", func(t *testing.T) { + txType := v3sdk.CreditTransactionType("voided") + transactions, err := c.Customers.Credits.Transactions.List(t.Context(), customerID, v3sdk.CreditTransactionListParams{ + Filter: &v3sdk.CreditTransactionFilter{Type: &txType}, + }) + c.requireStatus(http.StatusOK, err) + require.NotNil(t, transactions) + require.Len(t, transactions.Data, 1) + assert.Equal(t, txType, transactions.Data[0].Type) + assert.Equal(t, v3sdk.Numeric("-25"), transactions.Data[0].Amount) + + // The unfiltered listing carries both fundings and the void. + all, err := c.Customers.Credits.Transactions.List(t.Context(), customerID, v3sdk.CreditTransactionListParams{}) + c.requireStatus(http.StatusOK, err) + require.NotNil(t, all) + + typeCounts := map[v3sdk.CreditTransactionType]int{} + for _, tx := range all.Data { + typeCounts[tx.Type]++ + } + assert.Equal(t, 2, typeCounts[v3sdk.CreditTransactionTypeFunded]) + assert.Equal(t, 1, typeCounts[txType]) + }) + + t.Run("voiding an unknown grant is a 404", func(t *testing.T) { + status, _, problem := voidCreditGrantRaw(t, c, customerID, ulid.Make().String(), map[string]any{}) + require.Equal(t, http.StatusNotFound, status, "problem: %+v", problem) + }) +} + +func TestV3VoidCreditGrantPaymentAdjustmentNone(t *testing.T) { + c := newV3Client(t) + currency := v3sdk.BillingCurrencyCode("USD") + + customer, err := c.Customers.Create(t.Context(), v3sdk.CreateCustomerRequest{ + Key: uniqueKey("credit_grant_void_adjustment_customer"), + Name: "Credit Grant Void Payment Adjustment Test Customer", + Currency: lo.ToPtr("USD"), + }) + c.requireStatus(http.StatusCreated, err) + require.NotNil(t, customer) + + grant, err := c.Customers.Credits.Grants.Create(t.Context(), customer.ID, v3sdk.CreateCreditGrantRequest{ + Name: "grant to void with explicit payment adjustment", + Amount: v3sdk.Numeric("15"), + Currency: currency, + FundingMethod: v3sdk.CreditFundingMethodNone, + }) + c.requireStatus(http.StatusCreated, err) + require.NotNil(t, grant) + + status, voided, problem := voidCreditGrantRaw(t, c, customer.ID, grant.ID, map[string]string{ + "payment_adjustment": "none", + }) + require.Equal(t, http.StatusOK, status, "problem: %+v", problem) + require.NotNil(t, voided) + require.Equal(t, v3sdk.CreditGrantStatusVoided, voided.Status) + require.NotNil(t, voided.VoidedAt) +} + // TestV3CreditGrantKeyReadAndFilter verifies that the idempotency key is exposed // on the get/list read responses and that list credit grants can be filtered by // key. The key column carries a unique partial index per namespace, so an diff --git a/openmeter/billing/charges/creditpurchase/adapter.go b/openmeter/billing/charges/creditpurchase/adapter.go index ca18824011..3927a366af 100644 --- a/openmeter/billing/charges/creditpurchase/adapter.go +++ b/openmeter/billing/charges/creditpurchase/adapter.go @@ -28,6 +28,7 @@ type Adapter interface { type ChargeAdapter interface { CreateCharge(ctx context.Context, in CreateChargeInput) (Charge, error) UpdateCharge(ctx context.Context, charge ChargeBase) (ChargeBase, error) + MarkVoided(ctx context.Context, input MarkVoidedInput) (ChargeBase, error) GetByIDs(ctx context.Context, ids GetByIDsInput) ([]Charge, error) GetByID(ctx context.Context, id GetByIDInput) (Charge, error) ListCharges(ctx context.Context, input ListChargesInput) (pagination.Result[Charge], error) @@ -93,6 +94,25 @@ func (i GetByIDInput) Validate() error { return models.NewNillableGenericValidationError(errors.Join(errs...)) } +type MarkVoidedInput struct { + ChargeID meta.ChargeID + VoidedAt time.Time +} + +func (i MarkVoidedInput) Validate() error { + var errs []error + + if err := i.ChargeID.Validate(); err != nil { + errs = append(errs, fmt.Errorf("charge ID: %w", err)) + } + + if i.VoidedAt.IsZero() { + errs = append(errs, errors.New("voided at is required")) + } + + return models.NewNillableGenericValidationError(errors.Join(errs...)) +} + type CreateChargeInput struct { Namespace string Intent Intent @@ -121,11 +141,28 @@ type ListChargesInput struct { Statuses []meta.ChargeStatus Currencies []currencyx.Code Key *filter.FilterString + // Voided filters by whether the charge has been voided. + Voided *bool + // Expiration filters by whether expires_at has passed as of a point in time. + Expiration *ListChargesExpirationFilter IncludeDeleted bool Expands meta.Expands } +type ListChargesExpirationFilter struct { + AsOf time.Time + Expired bool +} + +func (f ListChargesExpirationFilter) Validate() error { + if f.AsOf.IsZero() { + return errors.New("as of is required") + } + + return nil +} + func (i ListChargesInput) Validate() error { var errs []error @@ -157,6 +194,12 @@ func (i ListChargesInput) Validate() error { } } + if i.Expiration != nil { + if err := i.Expiration.Validate(); err != nil { + errs = append(errs, fmt.Errorf("expiration: %w", err)) + } + } + if err := i.Expands.Validate(); err != nil { errs = append(errs, fmt.Errorf("expands: %w", err)) } diff --git a/openmeter/billing/charges/creditpurchase/adapter/charge.go b/openmeter/billing/charges/creditpurchase/adapter/charge.go index 280679b7c1..8e33e4ba09 100644 --- a/openmeter/billing/charges/creditpurchase/adapter/charge.go +++ b/openmeter/billing/charges/creditpurchase/adapter/charge.go @@ -113,6 +113,24 @@ func (a *adapter) CreateCharge(ctx context.Context, in creditpurchase.CreateChar }) } +func (a *adapter) MarkVoided(ctx context.Context, input creditpurchase.MarkVoidedInput) (creditpurchase.ChargeBase, error) { + if err := input.Validate(); err != nil { + return creditpurchase.ChargeBase{}, err + } + + return entutils.TransactingRepo(ctx, a, func(ctx context.Context, tx *adapter) (creditpurchase.ChargeBase, error) { + dbCreditPurchase, err := tx.db.ChargeCreditPurchase.UpdateOneID(input.ChargeID.ID). + Where(dbchargecreditpurchase.NamespaceEQ(input.ChargeID.Namespace)). + SetVoidedAt(input.VoidedAt). + Save(ctx) + if err != nil { + return creditpurchase.ChargeBase{}, fmt.Errorf("marking credit purchase charge voided [id=%s]: %w", input.ChargeID.ID, err) + } + + return MapChargeBaseFromDB(dbCreditPurchase), nil + }) +} + func (a *adapter) GetByID(ctx context.Context, input creditpurchase.GetByIDInput) (creditpurchase.Charge, error) { if err := input.Validate(); err != nil { return creditpurchase.Charge{}, err @@ -189,6 +207,25 @@ func (a *adapter) ListCharges(ctx context.Context, input creditpurchase.ListChar query = query.Where(dbchargecreditpurchase.CurrencyIn(input.Currencies...)) } + if input.Voided != nil { + if *input.Voided { + query = query.Where(dbchargecreditpurchase.VoidedAtNotNil()) + } else { + query = query.Where(dbchargecreditpurchase.VoidedAtIsNil()) + } + } + + if input.Expiration != nil { + if input.Expiration.Expired { + query = query.Where(dbchargecreditpurchase.ExpiresAtLTE(input.Expiration.AsOf)) + } else { + query = query.Where(dbchargecreditpurchase.Or( + dbchargecreditpurchase.ExpiresAtIsNil(), + dbchargecreditpurchase.ExpiresAtGT(input.Expiration.AsOf), + )) + } + } + query = filter.ApplyToQuery(query, input.Key, dbchargecreditpurchase.FieldKey) query = withExpands(query, input.Expands) diff --git a/openmeter/billing/charges/creditpurchase/adapter/mapper.go b/openmeter/billing/charges/creditpurchase/adapter/mapper.go index a7d24ddbb5..b728db2890 100644 --- a/openmeter/billing/charges/creditpurchase/adapter/mapper.go +++ b/openmeter/billing/charges/creditpurchase/adapter/mapper.go @@ -34,6 +34,9 @@ func MapChargeBaseFromDB(dbEntity *entdb.ChargeCreditPurchase) creditpurchase.Ch }, Key: dbEntity.Key, }, + State: creditpurchase.State{ + VoidedAt: convert.SafeToUTC(dbEntity.VoidedAt), + }, } } diff --git a/openmeter/billing/charges/creditpurchase/charge.go b/openmeter/billing/charges/creditpurchase/charge.go index 80541d9ef1..350ce0953c 100644 --- a/openmeter/billing/charges/creditpurchase/charge.go +++ b/openmeter/billing/charges/creditpurchase/charge.go @@ -242,8 +242,11 @@ func (i Intent) Validate() error { } // State holds durable base-row scheduling fields for the credit purchase charge. -// Currently empty — all lifecycle outcomes live in Realizations. -type State struct{} +type State struct { + // VoidedAt is set when the remaining value was forfeited through the + // ledger void flow; the breakage records stay the accounting source of truth. + VoidedAt *time.Time `json:"voidedAt,omitempty"` +} func (s State) Validate() error { return nil diff --git a/openmeter/billing/charges/creditpurchase/service.go b/openmeter/billing/charges/creditpurchase/service.go index 8121757e21..36567f7b5b 100644 --- a/openmeter/billing/charges/creditpurchase/service.go +++ b/openmeter/billing/charges/creditpurchase/service.go @@ -31,6 +31,9 @@ type CreditPurchaseService interface { // ListFundedCreditActivities reports grant-side activity funded by credit // purchases, not invoice-line override state. ListFundedCreditActivities(ctx context.Context, input ListFundedCreditActivitiesInput) (ListFundedCreditActivitiesResult, error) + // MarkVoided records the void time on the charge row. Callers run it in the + // same transaction as the ledger void booking. + MarkVoided(ctx context.Context, input MarkVoidedInput) (ChargeBase, error) } type ChargeWithGatheringLine struct { diff --git a/openmeter/billing/charges/creditpurchase/service/get.go b/openmeter/billing/charges/creditpurchase/service/get.go index b89d603172..3ba25807ee 100644 --- a/openmeter/billing/charges/creditpurchase/service/get.go +++ b/openmeter/billing/charges/creditpurchase/service/get.go @@ -27,3 +27,13 @@ func (s *service) List(ctx context.Context, input creditpurchase.ListChargesInpu return s.adapter.ListCharges(ctx, input) }) } + +func (s *service) MarkVoided(ctx context.Context, input creditpurchase.MarkVoidedInput) (creditpurchase.ChargeBase, error) { + if err := input.Validate(); err != nil { + return creditpurchase.ChargeBase{}, err + } + + return transaction.Run(ctx, s.adapter, func(ctx context.Context) (creditpurchase.ChargeBase, error) { + return s.adapter.MarkVoided(ctx, input) + }) +} diff --git a/openmeter/billing/creditgrant/noop.go b/openmeter/billing/creditgrant/noop.go index c0179a1574..6d3bb04b01 100644 --- a/openmeter/billing/creditgrant/noop.go +++ b/openmeter/billing/creditgrant/noop.go @@ -30,3 +30,7 @@ func (NoopService) List(context.Context, ListInput) (pagination.Result[creditpur func (NoopService) UpdateExternalSettlement(context.Context, UpdateExternalSettlementInput) (creditpurchase.Charge, error) { return creditpurchase.Charge{}, nil } + +func (NoopService) Void(context.Context, VoidInput) (creditpurchase.Charge, error) { + return creditpurchase.Charge{}, nil +} diff --git a/openmeter/billing/creditgrant/service.go b/openmeter/billing/creditgrant/service.go index ff8ec77598..4a170eb654 100644 --- a/openmeter/billing/creditgrant/service.go +++ b/openmeter/billing/creditgrant/service.go @@ -9,7 +9,6 @@ import ( "github.com/alpacahq/alpacadecimal" "github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase" - "github.com/openmeterio/openmeter/openmeter/billing/charges/meta" "github.com/openmeterio/openmeter/openmeter/billing/charges/models/payment" "github.com/openmeterio/openmeter/openmeter/productcatalog" "github.com/openmeterio/openmeter/pkg/currencyx" @@ -25,6 +24,45 @@ type Service interface { Get(ctx context.Context, input GetInput) (creditpurchase.Charge, error) List(ctx context.Context, input ListInput) (pagination.Result[creditpurchase.Charge], error) UpdateExternalSettlement(ctx context.Context, input UpdateExternalSettlementInput) (creditpurchase.Charge, error) + // Void forfeits the grant's remaining unused value by correcting the + // original receivable issuance at the current server time. + Void(ctx context.Context, input VoidInput) (creditpurchase.Charge, error) +} + +// GrantStatus is the public lifecycle status of a grant. +type GrantStatus string + +const ( + GrantStatusPending GrantStatus = "pending" + GrantStatusActive GrantStatus = "active" + GrantStatusExpired GrantStatus = "expired" + GrantStatusVoided GrantStatus = "voided" +) + +type VoidPaymentAdjustment string + +const ( + // VoidPaymentAdjustmentNone leaves invoice, payment authorization, + // settlement, payment intent, and external collection state unchanged. + VoidPaymentAdjustmentNone VoidPaymentAdjustment = "none" +) + +func (s GrantStatus) Validate() error { + switch s { + case GrantStatusPending, GrantStatusActive, GrantStatusExpired, GrantStatusVoided: + return nil + default: + return fmt.Errorf("invalid grant status: %s", s) + } +} + +func (a VoidPaymentAdjustment) Validate() error { + switch a { + case "", VoidPaymentAdjustmentNone: + return nil + default: + return fmt.Errorf("invalid payment adjustment: %s", a) + } } // FundingMethod represents how a credit grant is funded. @@ -170,7 +208,7 @@ type ListInput struct { CustomerID string // Optional filters - Status *meta.ChargeStatus + Status *GrantStatus Currency *currencyx.Code Key *filter.FilterString } @@ -207,6 +245,38 @@ func (i ListInput) Validate() error { return models.NewNillableGenericValidationError(errors.Join(errs...)) } +type VoidInput struct { + Namespace string + CustomerID string + ChargeID string + + // PaymentAdjustment is currently a no-op: voiding leaves invoice, payment + // authorization, settlement, payment intent, and external collection state unchanged. + PaymentAdjustment VoidPaymentAdjustment +} + +func (i VoidInput) Validate() error { + var errs []error + + if i.Namespace == "" { + errs = append(errs, errors.New("namespace is required")) + } + + if i.CustomerID == "" { + errs = append(errs, errors.New("customer ID is required")) + } + + if i.ChargeID == "" { + errs = append(errs, errors.New("charge ID is required")) + } + + if err := i.PaymentAdjustment.Validate(); err != nil { + errs = append(errs, fmt.Errorf("payment adjustment: %w", err)) + } + + return models.NewNillableGenericValidationError(errors.Join(errs...)) +} + type UpdateExternalSettlementInput struct { Namespace string CustomerID string diff --git a/openmeter/billing/creditgrant/service/service.go b/openmeter/billing/creditgrant/service/service.go index db80b83c44..4f66ade878 100644 --- a/openmeter/billing/creditgrant/service/service.go +++ b/openmeter/billing/creditgrant/service/service.go @@ -18,11 +18,14 @@ import ( "github.com/openmeterio/openmeter/openmeter/billing/creditgrant" customerbilling "github.com/openmeterio/openmeter/openmeter/billing/validators/customerbilling" "github.com/openmeterio/openmeter/openmeter/customer" + "github.com/openmeterio/openmeter/openmeter/ledger" + "github.com/openmeterio/openmeter/openmeter/ledger/creditvoid" "github.com/openmeterio/openmeter/openmeter/productcatalog" "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/currencyx" "github.com/openmeterio/openmeter/pkg/datetime" "github.com/openmeterio/openmeter/pkg/framework/commonhttp" + "github.com/openmeterio/openmeter/pkg/framework/transaction" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/pagination" "github.com/openmeterio/openmeter/pkg/timeutil" @@ -33,6 +36,8 @@ type Config struct { ChargesService charges.Service BillingService billing.Service CustomerService customer.Service + CreditVoidService creditvoid.Service + TransactionManager transaction.Creator } func (c Config) Validate() error { @@ -54,6 +59,14 @@ func (c Config) Validate() error { errs = append(errs, errors.New("customer service is required")) } + if c.CreditVoidService == nil { + errs = append(errs, errors.New("credit void service is required")) + } + + if c.TransactionManager == nil { + errs = append(errs, errors.New("transaction manager is required")) + } + return errors.Join(errs...) } @@ -67,6 +80,8 @@ func New(config Config) (creditgrant.Service, error) { chargesService: config.ChargesService, billingService: config.BillingService, customerService: config.CustomerService, + creditVoidService: config.CreditVoidService, + transactionManager: config.TransactionManager, }, nil } @@ -75,6 +90,8 @@ type service struct { chargesService charges.Service billingService billing.Service customerService customer.Service + creditVoidService creditvoid.Service + transactionManager transaction.Creator } func (s *service) Create(ctx context.Context, input creditgrant.CreateInput) (creditpurchase.Charge, error) { @@ -131,20 +148,11 @@ func (s *service) Create(ctx context.Context, input creditgrant.CreateInput) (cr return creditpurchase.Charge{}, fmt.Errorf("get created charge id: %w", err) } - charge, err := s.chargesService.GetByID(ctx, charges.GetByIDInput{ - ChargeID: createdChargeID, - Expands: meta.Expands{meta.ExpandRealizations}, + return s.Get(ctx, creditgrant.GetInput{ + Namespace: input.Namespace, + CustomerID: input.CustomerID, + ChargeID: createdChargeID.ID, }) - if err != nil { - return creditpurchase.Charge{}, fmt.Errorf("get created credit grant charge: %w", err) - } - - cpCharge, err := charge.AsCreditPurchaseCharge() - if err != nil { - return creditpurchase.Charge{}, fmt.Errorf("charge is not a credit purchase: %w", err) - } - - return cpCharge, nil } func (s *service) Get(ctx context.Context, input creditgrant.GetInput) (creditpurchase.Charge, error) { @@ -191,7 +199,29 @@ func (s *service) List(ctx context.Context, input creditgrant.ListInput) (pagina } if input.Status != nil { - listInput.Statuses = []meta.ChargeStatus{*input.Status} + switch *input.Status { + case creditgrant.GrantStatusPending: + listInput.Statuses = []meta.ChargeStatus{meta.ChargeStatusCreated} + case creditgrant.GrantStatusActive: + // Final charges read as public status active (promotional grants + // settle straight to final) until their optional expiry time passes. + listInput.Statuses = []meta.ChargeStatus{meta.ChargeStatusActive, meta.ChargeStatusFinal} + listInput.Voided = lo.ToPtr(false) + listInput.Expiration = &creditpurchase.ListChargesExpirationFilter{ + AsOf: clock.Now().UTC(), + Expired: false, + } + case creditgrant.GrantStatusExpired: + listInput.Voided = lo.ToPtr(false) + listInput.Expiration = &creditpurchase.ListChargesExpirationFilter{ + AsOf: clock.Now().UTC(), + Expired: true, + } + case creditgrant.GrantStatusVoided: + listInput.Voided = lo.ToPtr(true) + default: + return pagination.Result[creditpurchase.Charge]{}, models.NewGenericValidationError(fmt.Errorf("invalid grant status filter: %s", *input.Status)) + } } if input.Currency != nil { @@ -241,6 +271,86 @@ func (s *service) UpdateExternalSettlement(ctx context.Context, input creditgran return updated, nil } +func (s *service) Void(ctx context.Context, input creditgrant.VoidInput) (creditpurchase.Charge, error) { + if err := input.Validate(); err != nil { + return creditpurchase.Charge{}, fmt.Errorf("invalid input: %w", err) + } + + charge, err := s.Get(ctx, creditgrant.GetInput{ + Namespace: input.Namespace, + CustomerID: input.CustomerID, + ChargeID: input.ChargeID, + }) + if err != nil { + return creditpurchase.Charge{}, err + } + + if charge.State.VoidedAt != nil { + return charge, nil + } + + if err := validateChargeVoidable(charge); err != nil { + return creditpurchase.Charge{}, err + } + + return transaction.Run(ctx, s.transactionManager, func(ctx context.Context) (creditpurchase.Charge, error) { + result, err := s.creditVoidService.VoidCreditPurchase(ctx, creditvoid.VoidCreditPurchaseInput{ + CustomerID: customer.CustomerID{ + Namespace: input.Namespace, + ID: input.CustomerID, + }, + ChargeID: charge.ID, + Currency: charge.Intent.Currency, + Annotations: ledger.ChargeAnnotations(models.NamespacedID{ + Namespace: charge.Namespace, + ID: charge.ID, + }), + }) + if err != nil { + return creditpurchase.Charge{}, fmt.Errorf("void credit purchase: %w", err) + } + + voided, err := s.creditPurchaseService.MarkVoided(ctx, creditpurchase.MarkVoidedInput{ + ChargeID: charge.GetChargeID(), + VoidedAt: result.VoidedAt, + }) + if err != nil { + return creditpurchase.Charge{}, fmt.Errorf("mark charge voided: %w", err) + } + + return charge.WithBase(voided), nil + }) +} + +// validateChargeVoidable rejects charges whose lifecycle state makes a +// ledger-only void dishonest: a created (pending) charge has not funded FBO +// yet and would still fund later, a deleted charge is not readable as a +// grant, and an already expired charge has nothing left to void. +func validateChargeVoidable(charge creditpurchase.Charge) error { + if charge.DeletedAt != nil { + return models.NewGenericNotFoundError(fmt.Errorf("credit grant %s not found", charge.ID)) + } + + metaStatus, err := charge.Status.ToMetaChargeStatus() + if err != nil { + return fmt.Errorf("charge status: %w", err) + } + + switch metaStatus { + case meta.ChargeStatusActive, meta.ChargeStatusFinal: + case meta.ChargeStatusCreated: + return models.NewGenericConflictError(fmt.Errorf("credit grant %s is pending and cannot be voided", charge.ID)) + default: + return models.NewGenericConflictError(fmt.Errorf("credit grant %s cannot be voided in status %s", charge.ID, charge.Status)) + } + + if charge.Intent.ExpiresAt != nil && !charge.Intent.ExpiresAt.After(clock.Now()) { + return models.NewGenericConflictError(fmt.Errorf("credit grant %s has already expired", charge.ID)) + } + + return nil +} + func toIntent(input creditgrant.CreateInput) creditpurchase.Intent { effectiveAt := lo.FromPtrOr(input.EffectiveAt, clock.Now()).UTC() period := timeutil.ClosedPeriod{From: effectiveAt, To: effectiveAt} diff --git a/openmeter/billing/creditgrant/service/service_test.go b/openmeter/billing/creditgrant/service/service_test.go new file mode 100644 index 0000000000..d6eccb41e1 --- /dev/null +++ b/openmeter/billing/creditgrant/service/service_test.go @@ -0,0 +1,77 @@ +package service + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase" + "github.com/openmeterio/openmeter/pkg/clock" + "github.com/openmeterio/openmeter/pkg/models" +) + +// The charge fixtures are hand-assembled because some rejected lifecycle +// states (notably a still-created charge) cannot be produced through the +// public grant creation flows today, but the guard must stay defensive. +func TestValidateChargeVoidable(t *testing.T) { + now := time.Date(2026, time.March, 1, 0, 0, 0, 0, time.UTC) + clock.FreezeTime(now) + defer clock.UnFreeze() + + newCharge := func(status creditpurchase.Status, mutate func(*creditpurchase.Charge)) creditpurchase.Charge { + charge := creditpurchase.Charge{ + ChargeBase: creditpurchase.ChargeBase{ + Status: status, + }, + } + charge.ID = "grant-1" + if mutate != nil { + mutate(&charge) + } + + return charge + } + + t.Run("active charge is voidable", func(t *testing.T) { + require.NoError(t, validateChargeVoidable(newCharge(creditpurchase.StatusActive, nil))) + }) + + t.Run("final charge is voidable", func(t *testing.T) { + require.NoError(t, validateChargeVoidable(newCharge(creditpurchase.StatusFinal, nil))) + }) + + t.Run("detailed active payment state is voidable", func(t *testing.T) { + require.NoError(t, validateChargeVoidable(newCharge(creditpurchase.StatusActivePaymentPending, nil))) + }) + + t.Run("created (pending) charge is rejected with conflict", func(t *testing.T) { + err := validateChargeVoidable(newCharge(creditpurchase.StatusCreated, nil)) + require.Error(t, err) + require.True(t, models.IsGenericConflictError(err)) + }) + + t.Run("deleted charge is rejected as not found", func(t *testing.T) { + err := validateChargeVoidable(newCharge(creditpurchase.StatusDeleted, func(charge *creditpurchase.Charge) { + charge.DeletedAt = &now + })) + require.Error(t, err) + require.True(t, models.IsGenericNotFoundError(err)) + }) + + t.Run("already expired charge is rejected with conflict", func(t *testing.T) { + expiresAt := now.Add(-time.Hour) + err := validateChargeVoidable(newCharge(creditpurchase.StatusActive, func(charge *creditpurchase.Charge) { + charge.Intent.ExpiresAt = &expiresAt + })) + require.Error(t, err) + require.True(t, models.IsGenericConflictError(err)) + }) + + t.Run("future expiry stays voidable", func(t *testing.T) { + expiresAt := now.Add(time.Hour) + require.NoError(t, validateChargeVoidable(newCharge(creditpurchase.StatusActive, func(charge *creditpurchase.Charge) { + charge.Intent.ExpiresAt = &expiresAt + }))) + }) +} diff --git a/openmeter/ent/db/chargecreditpurchase.go b/openmeter/ent/db/chargecreditpurchase.go index 5f7c1dd44d..57fcff54e1 100644 --- a/openmeter/ent/db/chargecreditpurchase.go +++ b/openmeter/ent/db/chargecreditpurchase.go @@ -101,6 +101,8 @@ type ChargeCreditPurchase struct { StatusDetailed creditpurchase.Status `json:"status_detailed,omitempty"` // Key holds the value of the "key" field. Key *string `json:"key,omitempty"` + // VoidedAt holds the value of the "voided_at" field. + VoidedAt *time.Time `json:"voided_at,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the ChargeCreditPurchaseQuery when eager-loading is set. Edges ChargeCreditPurchaseEdges `json:"edges"` @@ -246,7 +248,7 @@ func (*ChargeCreditPurchase) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullInt64) case chargecreditpurchase.FieldID, chargecreditpurchase.FieldCustomerID, chargecreditpurchase.FieldStatus, chargecreditpurchase.FieldUniqueReferenceID, chargecreditpurchase.FieldCurrency, chargecreditpurchase.FieldManagedBy, chargecreditpurchase.FieldSubscriptionID, chargecreditpurchase.FieldSubscriptionPhaseID, chargecreditpurchase.FieldSubscriptionItemID, chargecreditpurchase.FieldTaxCodeID, chargecreditpurchase.FieldTaxBehavior, chargecreditpurchase.FieldNamespace, chargecreditpurchase.FieldName, chargecreditpurchase.FieldDescription, chargecreditpurchase.FieldStatusDetailed, chargecreditpurchase.FieldKey: values[i] = new(sql.NullString) - case chargecreditpurchase.FieldServicePeriodFrom, chargecreditpurchase.FieldServicePeriodTo, chargecreditpurchase.FieldBillingPeriodFrom, chargecreditpurchase.FieldBillingPeriodTo, chargecreditpurchase.FieldFullServicePeriodFrom, chargecreditpurchase.FieldFullServicePeriodTo, chargecreditpurchase.FieldAdvanceAfter, chargecreditpurchase.FieldCreatedAt, chargecreditpurchase.FieldUpdatedAt, chargecreditpurchase.FieldDeletedAt, chargecreditpurchase.FieldEffectiveAt, chargecreditpurchase.FieldExpiresAt: + case chargecreditpurchase.FieldServicePeriodFrom, chargecreditpurchase.FieldServicePeriodTo, chargecreditpurchase.FieldBillingPeriodFrom, chargecreditpurchase.FieldBillingPeriodTo, chargecreditpurchase.FieldFullServicePeriodFrom, chargecreditpurchase.FieldFullServicePeriodTo, chargecreditpurchase.FieldAdvanceAfter, chargecreditpurchase.FieldCreatedAt, chargecreditpurchase.FieldUpdatedAt, chargecreditpurchase.FieldDeletedAt, chargecreditpurchase.FieldEffectiveAt, chargecreditpurchase.FieldExpiresAt, chargecreditpurchase.FieldVoidedAt: values[i] = new(sql.NullTime) case chargecreditpurchase.FieldSettlement: values[i] = chargecreditpurchase.ValueScanner.Settlement.ScanValue() @@ -485,6 +487,13 @@ func (_m *ChargeCreditPurchase) assignValues(columns []string, values []any) err _m.Key = new(string) *_m.Key = value.String } + case chargecreditpurchase.FieldVoidedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field voided_at", values[i]) + } else if value.Valid { + _m.VoidedAt = new(time.Time) + *_m.VoidedAt = value.Time + } default: _m.selectValues.Set(columns[i], values[i]) } @@ -688,6 +697,11 @@ func (_m *ChargeCreditPurchase) String() string { builder.WriteString("key=") builder.WriteString(*v) } + builder.WriteString(", ") + if v := _m.VoidedAt; v != nil { + builder.WriteString("voided_at=") + builder.WriteString(v.Format(time.ANSIC)) + } builder.WriteByte(')') return builder.String() } diff --git a/openmeter/ent/db/chargecreditpurchase/chargecreditpurchase.go b/openmeter/ent/db/chargecreditpurchase/chargecreditpurchase.go index 075c042894..d3a93e20a8 100644 --- a/openmeter/ent/db/chargecreditpurchase/chargecreditpurchase.go +++ b/openmeter/ent/db/chargecreditpurchase/chargecreditpurchase.go @@ -86,6 +86,8 @@ const ( FieldStatusDetailed = "status_detailed" // FieldKey holds the string denoting the key field in the database. FieldKey = "key" + // FieldVoidedAt holds the string denoting the voided_at field in the database. + FieldVoidedAt = "voided_at" // EdgeExternalPayment holds the string denoting the external_payment edge name in mutations. EdgeExternalPayment = "external_payment" // EdgeInvoicedPayment holds the string denoting the invoiced_payment edge name in mutations. @@ -207,6 +209,7 @@ var Columns = []string{ FieldSettlement, FieldStatusDetailed, FieldKey, + FieldVoidedAt, } // ValidColumn reports if the column name is valid (part of the table columns). @@ -445,6 +448,11 @@ func ByKey(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldKey, opts...).ToFunc() } +// ByVoidedAt orders the results by the voided_at field. +func ByVoidedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldVoidedAt, opts...).ToFunc() +} + // ByExternalPaymentField orders the results by external_payment field. func ByExternalPaymentField(field string, opts ...sql.OrderTermOption) OrderOption { return func(s *sql.Selector) { diff --git a/openmeter/ent/db/chargecreditpurchase/where.go b/openmeter/ent/db/chargecreditpurchase/where.go index 1440a53a1d..67d39b566f 100644 --- a/openmeter/ent/db/chargecreditpurchase/where.go +++ b/openmeter/ent/db/chargecreditpurchase/where.go @@ -203,6 +203,11 @@ func Key(v string) predicate.ChargeCreditPurchase { return predicate.ChargeCreditPurchase(sql.FieldEQ(FieldKey, v)) } +// VoidedAt applies equality check predicate on the "voided_at" field. It's identical to VoidedAtEQ. +func VoidedAt(v time.Time) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldEQ(FieldVoidedAt, v)) +} + // CustomerIDEQ applies the EQ predicate on the "customer_id" field. func CustomerIDEQ(v string) predicate.ChargeCreditPurchase { return predicate.ChargeCreditPurchase(sql.FieldEQ(FieldCustomerID, v)) @@ -1807,6 +1812,56 @@ func KeyContainsFold(v string) predicate.ChargeCreditPurchase { return predicate.ChargeCreditPurchase(sql.FieldContainsFold(FieldKey, v)) } +// VoidedAtEQ applies the EQ predicate on the "voided_at" field. +func VoidedAtEQ(v time.Time) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldEQ(FieldVoidedAt, v)) +} + +// VoidedAtNEQ applies the NEQ predicate on the "voided_at" field. +func VoidedAtNEQ(v time.Time) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldNEQ(FieldVoidedAt, v)) +} + +// VoidedAtIn applies the In predicate on the "voided_at" field. +func VoidedAtIn(vs ...time.Time) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldIn(FieldVoidedAt, vs...)) +} + +// VoidedAtNotIn applies the NotIn predicate on the "voided_at" field. +func VoidedAtNotIn(vs ...time.Time) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldNotIn(FieldVoidedAt, vs...)) +} + +// VoidedAtGT applies the GT predicate on the "voided_at" field. +func VoidedAtGT(v time.Time) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldGT(FieldVoidedAt, v)) +} + +// VoidedAtGTE applies the GTE predicate on the "voided_at" field. +func VoidedAtGTE(v time.Time) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldGTE(FieldVoidedAt, v)) +} + +// VoidedAtLT applies the LT predicate on the "voided_at" field. +func VoidedAtLT(v time.Time) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldLT(FieldVoidedAt, v)) +} + +// VoidedAtLTE applies the LTE predicate on the "voided_at" field. +func VoidedAtLTE(v time.Time) predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldLTE(FieldVoidedAt, v)) +} + +// VoidedAtIsNil applies the IsNil predicate on the "voided_at" field. +func VoidedAtIsNil() predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldIsNull(FieldVoidedAt)) +} + +// VoidedAtNotNil applies the NotNil predicate on the "voided_at" field. +func VoidedAtNotNil() predicate.ChargeCreditPurchase { + return predicate.ChargeCreditPurchase(sql.FieldNotNull(FieldVoidedAt)) +} + // HasExternalPayment applies the HasEdge predicate on the "external_payment" edge. func HasExternalPayment() predicate.ChargeCreditPurchase { return predicate.ChargeCreditPurchase(func(s *sql.Selector) { diff --git a/openmeter/ent/db/chargecreditpurchase_create.go b/openmeter/ent/db/chargecreditpurchase_create.go index 9d9c4bde1b..c09f51c503 100644 --- a/openmeter/ent/db/chargecreditpurchase_create.go +++ b/openmeter/ent/db/chargecreditpurchase_create.go @@ -350,6 +350,20 @@ func (_c *ChargeCreditPurchaseCreate) SetNillableKey(v *string) *ChargeCreditPur return _c } +// SetVoidedAt sets the "voided_at" field. +func (_c *ChargeCreditPurchaseCreate) SetVoidedAt(v time.Time) *ChargeCreditPurchaseCreate { + _c.mutation.SetVoidedAt(v) + return _c +} + +// SetNillableVoidedAt sets the "voided_at" field if the given value is not nil. +func (_c *ChargeCreditPurchaseCreate) SetNillableVoidedAt(v *time.Time) *ChargeCreditPurchaseCreate { + if v != nil { + _c.SetVoidedAt(*v) + } + return _c +} + // SetID sets the "id" field. func (_c *ChargeCreditPurchaseCreate) SetID(v string) *ChargeCreditPurchaseCreate { _c.mutation.SetID(v) @@ -776,6 +790,10 @@ func (_c *ChargeCreditPurchaseCreate) createSpec() (*ChargeCreditPurchase, *sqlg _spec.SetField(chargecreditpurchase.FieldKey, field.TypeString, value) _node.Key = &value } + if value, ok := _c.mutation.VoidedAt(); ok { + _spec.SetField(chargecreditpurchase.FieldVoidedAt, field.TypeTime, value) + _node.VoidedAt = &value + } if nodes := _c.mutation.ExternalPaymentIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2O, @@ -1229,6 +1247,24 @@ func (u *ChargeCreditPurchaseUpsert) UpdateStatusDetailed() *ChargeCreditPurchas return u } +// SetVoidedAt sets the "voided_at" field. +func (u *ChargeCreditPurchaseUpsert) SetVoidedAt(v time.Time) *ChargeCreditPurchaseUpsert { + u.Set(chargecreditpurchase.FieldVoidedAt, v) + return u +} + +// UpdateVoidedAt sets the "voided_at" field to the value that was provided on create. +func (u *ChargeCreditPurchaseUpsert) UpdateVoidedAt() *ChargeCreditPurchaseUpsert { + u.SetExcluded(chargecreditpurchase.FieldVoidedAt) + return u +} + +// ClearVoidedAt clears the value of the "voided_at" field. +func (u *ChargeCreditPurchaseUpsert) ClearVoidedAt() *ChargeCreditPurchaseUpsert { + u.SetNull(chargecreditpurchase.FieldVoidedAt) + return u +} + // UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. // Using this option is equivalent to using: // @@ -1616,6 +1652,27 @@ func (u *ChargeCreditPurchaseUpsertOne) UpdateStatusDetailed() *ChargeCreditPurc }) } +// SetVoidedAt sets the "voided_at" field. +func (u *ChargeCreditPurchaseUpsertOne) SetVoidedAt(v time.Time) *ChargeCreditPurchaseUpsertOne { + return u.Update(func(s *ChargeCreditPurchaseUpsert) { + s.SetVoidedAt(v) + }) +} + +// UpdateVoidedAt sets the "voided_at" field to the value that was provided on create. +func (u *ChargeCreditPurchaseUpsertOne) UpdateVoidedAt() *ChargeCreditPurchaseUpsertOne { + return u.Update(func(s *ChargeCreditPurchaseUpsert) { + s.UpdateVoidedAt() + }) +} + +// ClearVoidedAt clears the value of the "voided_at" field. +func (u *ChargeCreditPurchaseUpsertOne) ClearVoidedAt() *ChargeCreditPurchaseUpsertOne { + return u.Update(func(s *ChargeCreditPurchaseUpsert) { + s.ClearVoidedAt() + }) +} + // Exec executes the query. func (u *ChargeCreditPurchaseUpsertOne) Exec(ctx context.Context) error { if len(u.create.conflict) == 0 { @@ -2173,6 +2230,27 @@ func (u *ChargeCreditPurchaseUpsertBulk) UpdateStatusDetailed() *ChargeCreditPur }) } +// SetVoidedAt sets the "voided_at" field. +func (u *ChargeCreditPurchaseUpsertBulk) SetVoidedAt(v time.Time) *ChargeCreditPurchaseUpsertBulk { + return u.Update(func(s *ChargeCreditPurchaseUpsert) { + s.SetVoidedAt(v) + }) +} + +// UpdateVoidedAt sets the "voided_at" field to the value that was provided on create. +func (u *ChargeCreditPurchaseUpsertBulk) UpdateVoidedAt() *ChargeCreditPurchaseUpsertBulk { + return u.Update(func(s *ChargeCreditPurchaseUpsert) { + s.UpdateVoidedAt() + }) +} + +// ClearVoidedAt clears the value of the "voided_at" field. +func (u *ChargeCreditPurchaseUpsertBulk) ClearVoidedAt() *ChargeCreditPurchaseUpsertBulk { + return u.Update(func(s *ChargeCreditPurchaseUpsert) { + s.ClearVoidedAt() + }) +} + // Exec executes the query. func (u *ChargeCreditPurchaseUpsertBulk) Exec(ctx context.Context) error { if u.create.err != nil { diff --git a/openmeter/ent/db/chargecreditpurchase_update.go b/openmeter/ent/db/chargecreditpurchase_update.go index 70821058ac..8d75912117 100644 --- a/openmeter/ent/db/chargecreditpurchase_update.go +++ b/openmeter/ent/db/chargecreditpurchase_update.go @@ -300,6 +300,26 @@ func (_u *ChargeCreditPurchaseUpdate) SetNillableStatusDetailed(v *creditpurchas return _u } +// SetVoidedAt sets the "voided_at" field. +func (_u *ChargeCreditPurchaseUpdate) SetVoidedAt(v time.Time) *ChargeCreditPurchaseUpdate { + _u.mutation.SetVoidedAt(v) + return _u +} + +// SetNillableVoidedAt sets the "voided_at" field if the given value is not nil. +func (_u *ChargeCreditPurchaseUpdate) SetNillableVoidedAt(v *time.Time) *ChargeCreditPurchaseUpdate { + if v != nil { + _u.SetVoidedAt(*v) + } + return _u +} + +// ClearVoidedAt clears the value of the "voided_at" field. +func (_u *ChargeCreditPurchaseUpdate) ClearVoidedAt() *ChargeCreditPurchaseUpdate { + _u.mutation.ClearVoidedAt() + return _u +} + // SetExternalPaymentID sets the "external_payment" edge to the ChargeCreditPurchaseExternalPayment entity by ID. func (_u *ChargeCreditPurchaseUpdate) SetExternalPaymentID(id string) *ChargeCreditPurchaseUpdate { _u.mutation.SetExternalPaymentID(id) @@ -556,6 +576,12 @@ func (_u *ChargeCreditPurchaseUpdate) sqlSave(ctx context.Context) (_node int, e if _u.mutation.KeyCleared() { _spec.ClearField(chargecreditpurchase.FieldKey, field.TypeString) } + if value, ok := _u.mutation.VoidedAt(); ok { + _spec.SetField(chargecreditpurchase.FieldVoidedAt, field.TypeTime, value) + } + if _u.mutation.VoidedAtCleared() { + _spec.ClearField(chargecreditpurchase.FieldVoidedAt, field.TypeTime) + } if _u.mutation.ExternalPaymentCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2O, @@ -956,6 +982,26 @@ func (_u *ChargeCreditPurchaseUpdateOne) SetNillableStatusDetailed(v *creditpurc return _u } +// SetVoidedAt sets the "voided_at" field. +func (_u *ChargeCreditPurchaseUpdateOne) SetVoidedAt(v time.Time) *ChargeCreditPurchaseUpdateOne { + _u.mutation.SetVoidedAt(v) + return _u +} + +// SetNillableVoidedAt sets the "voided_at" field if the given value is not nil. +func (_u *ChargeCreditPurchaseUpdateOne) SetNillableVoidedAt(v *time.Time) *ChargeCreditPurchaseUpdateOne { + if v != nil { + _u.SetVoidedAt(*v) + } + return _u +} + +// ClearVoidedAt clears the value of the "voided_at" field. +func (_u *ChargeCreditPurchaseUpdateOne) ClearVoidedAt() *ChargeCreditPurchaseUpdateOne { + _u.mutation.ClearVoidedAt() + return _u +} + // SetExternalPaymentID sets the "external_payment" edge to the ChargeCreditPurchaseExternalPayment entity by ID. func (_u *ChargeCreditPurchaseUpdateOne) SetExternalPaymentID(id string) *ChargeCreditPurchaseUpdateOne { _u.mutation.SetExternalPaymentID(id) @@ -1242,6 +1288,12 @@ func (_u *ChargeCreditPurchaseUpdateOne) sqlSave(ctx context.Context) (_node *Ch if _u.mutation.KeyCleared() { _spec.ClearField(chargecreditpurchase.FieldKey, field.TypeString) } + if value, ok := _u.mutation.VoidedAt(); ok { + _spec.SetField(chargecreditpurchase.FieldVoidedAt, field.TypeTime, value) + } + if _u.mutation.VoidedAtCleared() { + _spec.ClearField(chargecreditpurchase.FieldVoidedAt, field.TypeTime) + } if _u.mutation.ExternalPaymentCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2O, diff --git a/openmeter/ent/db/client.go b/openmeter/ent/db/client.go index 00b2ab3fec..439611c943 100644 --- a/openmeter/ent/db/client.go +++ b/openmeter/ent/db/client.go @@ -70,6 +70,7 @@ import ( dbgrant "github.com/openmeterio/openmeter/openmeter/ent/db/grant" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgeraccount" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgerbreakagerecord" + "github.com/openmeterio/openmeter/openmeter/ent/db/ledgercreditvoidrecord" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgercustomeraccount" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgerentry" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgersubaccount" @@ -219,6 +220,8 @@ type Client struct { LedgerAccount *LedgerAccountClient // LedgerBreakageRecord is the client for interacting with the LedgerBreakageRecord builders. LedgerBreakageRecord *LedgerBreakageRecordClient + // LedgerCreditVoidRecord is the client for interacting with the LedgerCreditVoidRecord builders. + LedgerCreditVoidRecord *LedgerCreditVoidRecordClient // LedgerCustomerAccount is the client for interacting with the LedgerCustomerAccount builders. LedgerCustomerAccount *LedgerCustomerAccountClient // LedgerEntry is the client for interacting with the LedgerEntry builders. @@ -337,6 +340,7 @@ func (c *Client) init() { c.LLMCostPrice = NewLLMCostPriceClient(c.config) c.LedgerAccount = NewLedgerAccountClient(c.config) c.LedgerBreakageRecord = NewLedgerBreakageRecordClient(c.config) + c.LedgerCreditVoidRecord = NewLedgerCreditVoidRecordClient(c.config) c.LedgerCustomerAccount = NewLedgerCustomerAccountClient(c.config) c.LedgerEntry = NewLedgerEntryClient(c.config) c.LedgerSubAccount = NewLedgerSubAccountClient(c.config) @@ -511,6 +515,7 @@ func (c *Client) Tx(ctx context.Context) (*Tx, error) { LLMCostPrice: NewLLMCostPriceClient(cfg), LedgerAccount: NewLedgerAccountClient(cfg), LedgerBreakageRecord: NewLedgerBreakageRecordClient(cfg), + LedgerCreditVoidRecord: NewLedgerCreditVoidRecordClient(cfg), LedgerCustomerAccount: NewLedgerCustomerAccountClient(cfg), LedgerEntry: NewLedgerEntryClient(cfg), LedgerSubAccount: NewLedgerSubAccountClient(cfg), @@ -612,6 +617,7 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) LLMCostPrice: NewLLMCostPriceClient(cfg), LedgerAccount: NewLedgerAccountClient(cfg), LedgerBreakageRecord: NewLedgerBreakageRecordClient(cfg), + LedgerCreditVoidRecord: NewLedgerCreditVoidRecordClient(cfg), LedgerCustomerAccount: NewLedgerCustomerAccountClient(cfg), LedgerEntry: NewLedgerEntryClient(cfg), LedgerSubAccount: NewLedgerSubAccountClient(cfg), @@ -687,10 +693,10 @@ func (c *Client) Use(hooks ...Hook) { c.CreditRealizationLineage, c.CreditRealizationLineageSegment, c.CurrencyCostBasis, c.CustomCurrency, c.Customer, c.CustomerSubjects, c.Entitlement, c.Feature, c.Grant, c.LLMCostPrice, c.LedgerAccount, - c.LedgerBreakageRecord, c.LedgerCustomerAccount, c.LedgerEntry, - c.LedgerSubAccount, c.LedgerSubAccountRoute, c.LedgerTransaction, - c.LedgerTransactionGroup, c.Meter, c.NotificationChannel, c.NotificationEvent, - c.NotificationEventDeliveryStatus, c.NotificationRule, + c.LedgerBreakageRecord, c.LedgerCreditVoidRecord, c.LedgerCustomerAccount, + c.LedgerEntry, c.LedgerSubAccount, c.LedgerSubAccountRoute, + c.LedgerTransaction, c.LedgerTransactionGroup, c.Meter, c.NotificationChannel, + c.NotificationEvent, c.NotificationEventDeliveryStatus, c.NotificationRule, c.OrganizationDefaultTaxCodes, c.Plan, c.PlanAddon, c.PlanPhase, c.PlanRateCard, c.Subject, c.Subscription, c.SubscriptionAddon, c.SubscriptionAddonQuantity, c.SubscriptionBillingSyncState, @@ -725,10 +731,10 @@ func (c *Client) Intercept(interceptors ...Interceptor) { c.CreditRealizationLineage, c.CreditRealizationLineageSegment, c.CurrencyCostBasis, c.CustomCurrency, c.Customer, c.CustomerSubjects, c.Entitlement, c.Feature, c.Grant, c.LLMCostPrice, c.LedgerAccount, - c.LedgerBreakageRecord, c.LedgerCustomerAccount, c.LedgerEntry, - c.LedgerSubAccount, c.LedgerSubAccountRoute, c.LedgerTransaction, - c.LedgerTransactionGroup, c.Meter, c.NotificationChannel, c.NotificationEvent, - c.NotificationEventDeliveryStatus, c.NotificationRule, + c.LedgerBreakageRecord, c.LedgerCreditVoidRecord, c.LedgerCustomerAccount, + c.LedgerEntry, c.LedgerSubAccount, c.LedgerSubAccountRoute, + c.LedgerTransaction, c.LedgerTransactionGroup, c.Meter, c.NotificationChannel, + c.NotificationEvent, c.NotificationEventDeliveryStatus, c.NotificationRule, c.OrganizationDefaultTaxCodes, c.Plan, c.PlanAddon, c.PlanPhase, c.PlanRateCard, c.Subject, c.Subscription, c.SubscriptionAddon, c.SubscriptionAddonQuantity, c.SubscriptionBillingSyncState, @@ -853,6 +859,8 @@ func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) { return c.LedgerAccount.mutate(ctx, m) case *LedgerBreakageRecordMutation: return c.LedgerBreakageRecord.mutate(ctx, m) + case *LedgerCreditVoidRecordMutation: + return c.LedgerCreditVoidRecord.mutate(ctx, m) case *LedgerCustomerAccountMutation: return c.LedgerCustomerAccount.mutate(ctx, m) case *LedgerEntryMutation: @@ -11538,6 +11546,139 @@ func (c *LedgerBreakageRecordClient) mutate(ctx context.Context, m *LedgerBreaka } } +// LedgerCreditVoidRecordClient is a client for the LedgerCreditVoidRecord schema. +type LedgerCreditVoidRecordClient struct { + config +} + +// NewLedgerCreditVoidRecordClient returns a client for the LedgerCreditVoidRecord from the given config. +func NewLedgerCreditVoidRecordClient(c config) *LedgerCreditVoidRecordClient { + return &LedgerCreditVoidRecordClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `ledgercreditvoidrecord.Hooks(f(g(h())))`. +func (c *LedgerCreditVoidRecordClient) Use(hooks ...Hook) { + c.hooks.LedgerCreditVoidRecord = append(c.hooks.LedgerCreditVoidRecord, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `ledgercreditvoidrecord.Intercept(f(g(h())))`. +func (c *LedgerCreditVoidRecordClient) Intercept(interceptors ...Interceptor) { + c.inters.LedgerCreditVoidRecord = append(c.inters.LedgerCreditVoidRecord, interceptors...) +} + +// Create returns a builder for creating a LedgerCreditVoidRecord entity. +func (c *LedgerCreditVoidRecordClient) Create() *LedgerCreditVoidRecordCreate { + mutation := newLedgerCreditVoidRecordMutation(c.config, OpCreate) + return &LedgerCreditVoidRecordCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of LedgerCreditVoidRecord entities. +func (c *LedgerCreditVoidRecordClient) CreateBulk(builders ...*LedgerCreditVoidRecordCreate) *LedgerCreditVoidRecordCreateBulk { + return &LedgerCreditVoidRecordCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *LedgerCreditVoidRecordClient) MapCreateBulk(slice any, setFunc func(*LedgerCreditVoidRecordCreate, int)) *LedgerCreditVoidRecordCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &LedgerCreditVoidRecordCreateBulk{err: fmt.Errorf("calling to LedgerCreditVoidRecordClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*LedgerCreditVoidRecordCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &LedgerCreditVoidRecordCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for LedgerCreditVoidRecord. +func (c *LedgerCreditVoidRecordClient) Update() *LedgerCreditVoidRecordUpdate { + mutation := newLedgerCreditVoidRecordMutation(c.config, OpUpdate) + return &LedgerCreditVoidRecordUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *LedgerCreditVoidRecordClient) UpdateOne(_m *LedgerCreditVoidRecord) *LedgerCreditVoidRecordUpdateOne { + mutation := newLedgerCreditVoidRecordMutation(c.config, OpUpdateOne, withLedgerCreditVoidRecord(_m)) + return &LedgerCreditVoidRecordUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *LedgerCreditVoidRecordClient) UpdateOneID(id string) *LedgerCreditVoidRecordUpdateOne { + mutation := newLedgerCreditVoidRecordMutation(c.config, OpUpdateOne, withLedgerCreditVoidRecordID(id)) + return &LedgerCreditVoidRecordUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for LedgerCreditVoidRecord. +func (c *LedgerCreditVoidRecordClient) Delete() *LedgerCreditVoidRecordDelete { + mutation := newLedgerCreditVoidRecordMutation(c.config, OpDelete) + return &LedgerCreditVoidRecordDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *LedgerCreditVoidRecordClient) DeleteOne(_m *LedgerCreditVoidRecord) *LedgerCreditVoidRecordDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *LedgerCreditVoidRecordClient) DeleteOneID(id string) *LedgerCreditVoidRecordDeleteOne { + builder := c.Delete().Where(ledgercreditvoidrecord.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &LedgerCreditVoidRecordDeleteOne{builder} +} + +// Query returns a query builder for LedgerCreditVoidRecord. +func (c *LedgerCreditVoidRecordClient) Query() *LedgerCreditVoidRecordQuery { + return &LedgerCreditVoidRecordQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeLedgerCreditVoidRecord}, + inters: c.Interceptors(), + } +} + +// Get returns a LedgerCreditVoidRecord entity by its id. +func (c *LedgerCreditVoidRecordClient) Get(ctx context.Context, id string) (*LedgerCreditVoidRecord, error) { + return c.Query().Where(ledgercreditvoidrecord.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *LedgerCreditVoidRecordClient) GetX(ctx context.Context, id string) *LedgerCreditVoidRecord { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *LedgerCreditVoidRecordClient) Hooks() []Hook { + return c.hooks.LedgerCreditVoidRecord +} + +// Interceptors returns the client interceptors. +func (c *LedgerCreditVoidRecordClient) Interceptors() []Interceptor { + return c.inters.LedgerCreditVoidRecord +} + +func (c *LedgerCreditVoidRecordClient) mutate(ctx context.Context, m *LedgerCreditVoidRecordMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&LedgerCreditVoidRecordCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&LedgerCreditVoidRecordUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&LedgerCreditVoidRecordUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&LedgerCreditVoidRecordDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("db: unknown LedgerCreditVoidRecord mutation op: %q", m.Op()) + } +} + // LedgerCustomerAccountClient is a client for the LedgerCustomerAccount schema. type LedgerCustomerAccountClient struct { config @@ -16181,13 +16322,13 @@ type ( ChargeUsageBasedRunPayment, ChargeUsageBasedRuns, CreditRealizationLineage, CreditRealizationLineageSegment, CurrencyCostBasis, CustomCurrency, Customer, CustomerSubjects, Entitlement, Feature, Grant, LLMCostPrice, LedgerAccount, - LedgerBreakageRecord, LedgerCustomerAccount, LedgerEntry, LedgerSubAccount, - LedgerSubAccountRoute, LedgerTransaction, LedgerTransactionGroup, Meter, - NotificationChannel, NotificationEvent, NotificationEventDeliveryStatus, - NotificationRule, OrganizationDefaultTaxCodes, Plan, PlanAddon, PlanPhase, - PlanRateCard, Subject, Subscription, SubscriptionAddon, - SubscriptionAddonQuantity, SubscriptionBillingSyncState, SubscriptionItem, - SubscriptionPhase, TaxCode, UsageReset []ent.Hook + LedgerBreakageRecord, LedgerCreditVoidRecord, LedgerCustomerAccount, + LedgerEntry, LedgerSubAccount, LedgerSubAccountRoute, LedgerTransaction, + LedgerTransactionGroup, Meter, NotificationChannel, NotificationEvent, + NotificationEventDeliveryStatus, NotificationRule, OrganizationDefaultTaxCodes, + Plan, PlanAddon, PlanPhase, PlanRateCard, Subject, Subscription, + SubscriptionAddon, SubscriptionAddonQuantity, SubscriptionBillingSyncState, + SubscriptionItem, SubscriptionPhase, TaxCode, UsageReset []ent.Hook } inters struct { Addon, AddonRateCard, App, AppCustomInvoicing, AppCustomInvoicingCustomer, @@ -16209,13 +16350,14 @@ type ( ChargeUsageBasedRunPayment, ChargeUsageBasedRuns, ChargesSearchV1, CreditRealizationLineage, CreditRealizationLineageSegment, CurrencyCostBasis, CustomCurrency, Customer, CustomerSubjects, Entitlement, Feature, Grant, - LLMCostPrice, LedgerAccount, LedgerBreakageRecord, LedgerCustomerAccount, - LedgerEntry, LedgerSubAccount, LedgerSubAccountRoute, LedgerTransaction, - LedgerTransactionGroup, Meter, NotificationChannel, NotificationEvent, - NotificationEventDeliveryStatus, NotificationRule, OrganizationDefaultTaxCodes, - Plan, PlanAddon, PlanPhase, PlanRateCard, Subject, Subscription, - SubscriptionAddon, SubscriptionAddonQuantity, SubscriptionBillingSyncState, - SubscriptionItem, SubscriptionPhase, TaxCode, UsageReset []ent.Interceptor + LLMCostPrice, LedgerAccount, LedgerBreakageRecord, LedgerCreditVoidRecord, + LedgerCustomerAccount, LedgerEntry, LedgerSubAccount, LedgerSubAccountRoute, + LedgerTransaction, LedgerTransactionGroup, Meter, NotificationChannel, + NotificationEvent, NotificationEventDeliveryStatus, NotificationRule, + OrganizationDefaultTaxCodes, Plan, PlanAddon, PlanPhase, PlanRateCard, Subject, + Subscription, SubscriptionAddon, SubscriptionAddonQuantity, + SubscriptionBillingSyncState, SubscriptionItem, SubscriptionPhase, TaxCode, + UsageReset []ent.Interceptor } ) diff --git a/openmeter/ent/db/cursor.go b/openmeter/ent/db/cursor.go index b15fcfa70f..b06c64cd14 100644 --- a/openmeter/ent/db/cursor.go +++ b/openmeter/ent/db/cursor.go @@ -2561,6 +2561,57 @@ func (_m *LedgerBreakageRecordQuery) Cursor(ctx context.Context, cursor *paginat return result, nil } +// Cursor runs the query and returns a cursor-paginated response. +// Ordering is always by created_at asc, id asc. +func (_m *LedgerCreditVoidRecordQuery) Cursor(ctx context.Context, cursor *pagination.Cursor) (pagination.Result[*LedgerCreditVoidRecord], error) { + if cursor != nil { + if err := cursor.Validate(); err != nil { + return pagination.Result[*LedgerCreditVoidRecord]{}, fmt.Errorf("invalid cursor: %w", err) + } + + _m.Where(func(s *sql.Selector) { + s.Where( + sql.Or( + sql.GT(s.C("created_at"), cursor.Time), + sql.And( + sql.EQ(s.C("created_at"), cursor.Time), + sql.P(func(b *sql.Builder) { + b.WriteString("CAST(") + b.WriteString(s.C("id")) + b.WriteString(" AS TEXT) > ") + b.Args(cursor.ID) + }), + ), + ), + ) + }) + } + + _m.Order(func(s *sql.Selector) { + s.OrderBy(sql.Asc(s.C("created_at")), sql.Asc(s.C("id"))) + }) + + items, err := _m.All(ctx) + if err != nil { + return pagination.Result[*LedgerCreditVoidRecord]{}, err + } + + if items == nil { + items = make([]*LedgerCreditVoidRecord, 0) + } + + result := pagination.Result[*LedgerCreditVoidRecord]{ + Items: items, + } + + if len(items) > 0 { + last := items[len(items)-1] + result.NextCursor = lo.ToPtr(pagination.NewCursor(last.CreatedAt, fmt.Sprint(last.ID))) + } + + return result, nil +} + // Cursor runs the query and returns a cursor-paginated response. // Ordering is always by created_at asc, id asc. func (_m *LedgerCustomerAccountQuery) Cursor(ctx context.Context, cursor *pagination.Cursor) (pagination.Result[*LedgerCustomerAccount], error) { diff --git a/openmeter/ent/db/ent.go b/openmeter/ent/db/ent.go index b995c2fc33..43d484e43c 100644 --- a/openmeter/ent/db/ent.go +++ b/openmeter/ent/db/ent.go @@ -71,6 +71,7 @@ import ( dbgrant "github.com/openmeterio/openmeter/openmeter/ent/db/grant" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgeraccount" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgerbreakagerecord" + "github.com/openmeterio/openmeter/openmeter/ent/db/ledgercreditvoidrecord" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgercustomeraccount" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgerentry" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgersubaccount" @@ -216,6 +217,7 @@ func checkColumn(t, c string) error { llmcostprice.Table: llmcostprice.ValidColumn, ledgeraccount.Table: ledgeraccount.ValidColumn, ledgerbreakagerecord.Table: ledgerbreakagerecord.ValidColumn, + ledgercreditvoidrecord.Table: ledgercreditvoidrecord.ValidColumn, ledgercustomeraccount.Table: ledgercustomeraccount.ValidColumn, ledgerentry.Table: ledgerentry.ValidColumn, ledgersubaccount.Table: ledgersubaccount.ValidColumn, diff --git a/openmeter/ent/db/entmixinaccessor.go b/openmeter/ent/db/entmixinaccessor.go index 566bd218d9..63c874edf9 100644 --- a/openmeter/ent/db/entmixinaccessor.go +++ b/openmeter/ent/db/entmixinaccessor.go @@ -2313,6 +2313,30 @@ func (e *LedgerBreakageRecord) GetDeletedAt() *time.Time { return e.DeletedAt } +func (e *LedgerCreditVoidRecord) GetID() string { + return e.ID +} + +func (e *LedgerCreditVoidRecord) GetNamespace() string { + return e.Namespace +} + +func (e *LedgerCreditVoidRecord) GetAnnotations() models.Annotations { + return e.Annotations +} + +func (e *LedgerCreditVoidRecord) GetCreatedAt() time.Time { + return e.CreatedAt +} + +func (e *LedgerCreditVoidRecord) GetUpdatedAt() time.Time { + return e.UpdatedAt +} + +func (e *LedgerCreditVoidRecord) GetDeletedAt() *time.Time { + return e.DeletedAt +} + func (e *LedgerCustomerAccount) GetID() string { return e.ID } diff --git a/openmeter/ent/db/expose.go b/openmeter/ent/db/expose.go index 580592a866..a3d2ce4046 100644 --- a/openmeter/ent/db/expose.go +++ b/openmeter/ent/db/expose.go @@ -203,6 +203,8 @@ func NewTxClientFromRawConfig(ctx context.Context, cfg entutils.RawEntConfig) *T LedgerBreakageRecord: NewLedgerBreakageRecordClient(config), + LedgerCreditVoidRecord: NewLedgerCreditVoidRecordClient(config), + LedgerCustomerAccount: NewLedgerCustomerAccountClient(config), LedgerEntry: NewLedgerEntryClient(config), diff --git a/openmeter/ent/db/hook/hook.go b/openmeter/ent/db/hook/hook.go index 26344196a5..f93c242cc1 100644 --- a/openmeter/ent/db/hook/hook.go +++ b/openmeter/ent/db/hook/hook.go @@ -681,6 +681,18 @@ func (f LedgerBreakageRecordFunc) Mutate(ctx context.Context, m db.Mutation) (db return nil, fmt.Errorf("unexpected mutation type %T. expect *db.LedgerBreakageRecordMutation", m) } +// The LedgerCreditVoidRecordFunc type is an adapter to allow the use of ordinary +// function as LedgerCreditVoidRecord mutator. +type LedgerCreditVoidRecordFunc func(context.Context, *db.LedgerCreditVoidRecordMutation) (db.Value, error) + +// Mutate calls f(ctx, m). +func (f LedgerCreditVoidRecordFunc) Mutate(ctx context.Context, m db.Mutation) (db.Value, error) { + if mv, ok := m.(*db.LedgerCreditVoidRecordMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *db.LedgerCreditVoidRecordMutation", m) +} + // The LedgerCustomerAccountFunc type is an adapter to allow the use of ordinary // function as LedgerCustomerAccount mutator. type LedgerCustomerAccountFunc func(context.Context, *db.LedgerCustomerAccountMutation) (db.Value, error) diff --git a/openmeter/ent/db/ledgercreditvoidrecord.go b/openmeter/ent/db/ledgercreditvoidrecord.go new file mode 100644 index 0000000000..e75fbfadf4 --- /dev/null +++ b/openmeter/ent/db/ledgercreditvoidrecord.go @@ -0,0 +1,260 @@ +// Code generated by ent, DO NOT EDIT. + +package db + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/alpacahq/alpacadecimal" + "github.com/openmeterio/openmeter/openmeter/ent/db/ledgercreditvoidrecord" + "github.com/openmeterio/openmeter/pkg/currencyx" + "github.com/openmeterio/openmeter/pkg/models" +) + +// LedgerCreditVoidRecord is the model entity for the LedgerCreditVoidRecord schema. +type LedgerCreditVoidRecord struct { + config `json:"-"` + // ID of the ent. + ID string `json:"id,omitempty"` + // Namespace holds the value of the "namespace" field. + Namespace string `json:"namespace,omitempty"` + // Annotations holds the value of the "annotations" field. + Annotations models.Annotations `json:"annotations,omitempty"` + // CreatedAt holds the value of the "created_at" field. + CreatedAt time.Time `json:"created_at,omitempty"` + // UpdatedAt holds the value of the "updated_at" field. + UpdatedAt time.Time `json:"updated_at,omitempty"` + // DeletedAt holds the value of the "deleted_at" field. + DeletedAt *time.Time `json:"deleted_at,omitempty"` + // Amount holds the value of the "amount" field. + Amount alpacadecimal.Decimal `json:"amount,omitempty"` + // CustomerID holds the value of the "customer_id" field. + CustomerID string `json:"customer_id,omitempty"` + // Currency holds the value of the "currency" field. + Currency currencyx.Code `json:"currency,omitempty"` + // VoidedAt holds the value of the "voided_at" field. + VoidedAt time.Time `json:"voided_at,omitempty"` + // SourceChargeID holds the value of the "source_charge_id" field. + SourceChargeID string `json:"source_charge_id,omitempty"` + // VoidTransactionGroupID holds the value of the "void_transaction_group_id" field. + VoidTransactionGroupID string `json:"void_transaction_group_id,omitempty"` + // VoidTransactionID holds the value of the "void_transaction_id" field. + VoidTransactionID string `json:"void_transaction_id,omitempty"` + // FboSubAccountID holds the value of the "fbo_sub_account_id" field. + FboSubAccountID string `json:"fbo_sub_account_id,omitempty"` + // ReceivableSubAccountID holds the value of the "receivable_sub_account_id" field. + ReceivableSubAccountID string `json:"receivable_sub_account_id,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*LedgerCreditVoidRecord) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case ledgercreditvoidrecord.FieldAnnotations: + values[i] = new([]byte) + case ledgercreditvoidrecord.FieldAmount: + values[i] = new(alpacadecimal.Decimal) + case ledgercreditvoidrecord.FieldID, ledgercreditvoidrecord.FieldNamespace, ledgercreditvoidrecord.FieldCustomerID, ledgercreditvoidrecord.FieldCurrency, ledgercreditvoidrecord.FieldSourceChargeID, ledgercreditvoidrecord.FieldVoidTransactionGroupID, ledgercreditvoidrecord.FieldVoidTransactionID, ledgercreditvoidrecord.FieldFboSubAccountID, ledgercreditvoidrecord.FieldReceivableSubAccountID: + values[i] = new(sql.NullString) + case ledgercreditvoidrecord.FieldCreatedAt, ledgercreditvoidrecord.FieldUpdatedAt, ledgercreditvoidrecord.FieldDeletedAt, ledgercreditvoidrecord.FieldVoidedAt: + values[i] = new(sql.NullTime) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the LedgerCreditVoidRecord fields. +func (_m *LedgerCreditVoidRecord) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case ledgercreditvoidrecord.FieldID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value.Valid { + _m.ID = value.String + } + case ledgercreditvoidrecord.FieldNamespace: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field namespace", values[i]) + } else if value.Valid { + _m.Namespace = value.String + } + case ledgercreditvoidrecord.FieldAnnotations: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field annotations", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &_m.Annotations); err != nil { + return fmt.Errorf("unmarshal field annotations: %w", err) + } + } + case ledgercreditvoidrecord.FieldCreatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created_at", values[i]) + } else if value.Valid { + _m.CreatedAt = value.Time + } + case ledgercreditvoidrecord.FieldUpdatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field updated_at", values[i]) + } else if value.Valid { + _m.UpdatedAt = value.Time + } + case ledgercreditvoidrecord.FieldDeletedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field deleted_at", values[i]) + } else if value.Valid { + _m.DeletedAt = new(time.Time) + *_m.DeletedAt = value.Time + } + case ledgercreditvoidrecord.FieldAmount: + if value, ok := values[i].(*alpacadecimal.Decimal); !ok { + return fmt.Errorf("unexpected type %T for field amount", values[i]) + } else if value != nil { + _m.Amount = *value + } + case ledgercreditvoidrecord.FieldCustomerID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field customer_id", values[i]) + } else if value.Valid { + _m.CustomerID = value.String + } + case ledgercreditvoidrecord.FieldCurrency: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field currency", values[i]) + } else if value.Valid { + _m.Currency = currencyx.Code(value.String) + } + case ledgercreditvoidrecord.FieldVoidedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field voided_at", values[i]) + } else if value.Valid { + _m.VoidedAt = value.Time + } + case ledgercreditvoidrecord.FieldSourceChargeID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field source_charge_id", values[i]) + } else if value.Valid { + _m.SourceChargeID = value.String + } + case ledgercreditvoidrecord.FieldVoidTransactionGroupID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field void_transaction_group_id", values[i]) + } else if value.Valid { + _m.VoidTransactionGroupID = value.String + } + case ledgercreditvoidrecord.FieldVoidTransactionID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field void_transaction_id", values[i]) + } else if value.Valid { + _m.VoidTransactionID = value.String + } + case ledgercreditvoidrecord.FieldFboSubAccountID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field fbo_sub_account_id", values[i]) + } else if value.Valid { + _m.FboSubAccountID = value.String + } + case ledgercreditvoidrecord.FieldReceivableSubAccountID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field receivable_sub_account_id", values[i]) + } else if value.Valid { + _m.ReceivableSubAccountID = value.String + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the LedgerCreditVoidRecord. +// This includes values selected through modifiers, order, etc. +func (_m *LedgerCreditVoidRecord) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this LedgerCreditVoidRecord. +// Note that you need to call LedgerCreditVoidRecord.Unwrap() before calling this method if this LedgerCreditVoidRecord +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *LedgerCreditVoidRecord) Update() *LedgerCreditVoidRecordUpdateOne { + return NewLedgerCreditVoidRecordClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the LedgerCreditVoidRecord entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *LedgerCreditVoidRecord) Unwrap() *LedgerCreditVoidRecord { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("db: LedgerCreditVoidRecord is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *LedgerCreditVoidRecord) String() string { + var builder strings.Builder + builder.WriteString("LedgerCreditVoidRecord(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("namespace=") + builder.WriteString(_m.Namespace) + builder.WriteString(", ") + builder.WriteString("annotations=") + builder.WriteString(fmt.Sprintf("%v", _m.Annotations)) + builder.WriteString(", ") + builder.WriteString("created_at=") + builder.WriteString(_m.CreatedAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("updated_at=") + builder.WriteString(_m.UpdatedAt.Format(time.ANSIC)) + builder.WriteString(", ") + if v := _m.DeletedAt; v != nil { + builder.WriteString("deleted_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + builder.WriteString("amount=") + builder.WriteString(fmt.Sprintf("%v", _m.Amount)) + builder.WriteString(", ") + builder.WriteString("customer_id=") + builder.WriteString(_m.CustomerID) + builder.WriteString(", ") + builder.WriteString("currency=") + builder.WriteString(fmt.Sprintf("%v", _m.Currency)) + builder.WriteString(", ") + builder.WriteString("voided_at=") + builder.WriteString(_m.VoidedAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("source_charge_id=") + builder.WriteString(_m.SourceChargeID) + builder.WriteString(", ") + builder.WriteString("void_transaction_group_id=") + builder.WriteString(_m.VoidTransactionGroupID) + builder.WriteString(", ") + builder.WriteString("void_transaction_id=") + builder.WriteString(_m.VoidTransactionID) + builder.WriteString(", ") + builder.WriteString("fbo_sub_account_id=") + builder.WriteString(_m.FboSubAccountID) + builder.WriteString(", ") + builder.WriteString("receivable_sub_account_id=") + builder.WriteString(_m.ReceivableSubAccountID) + builder.WriteByte(')') + return builder.String() +} + +// LedgerCreditVoidRecords is a parsable slice of LedgerCreditVoidRecord. +type LedgerCreditVoidRecords []*LedgerCreditVoidRecord diff --git a/openmeter/ent/db/ledgercreditvoidrecord/ledgercreditvoidrecord.go b/openmeter/ent/db/ledgercreditvoidrecord/ledgercreditvoidrecord.go new file mode 100644 index 0000000000..d74bb1e89c --- /dev/null +++ b/openmeter/ent/db/ledgercreditvoidrecord/ledgercreditvoidrecord.go @@ -0,0 +1,175 @@ +// Code generated by ent, DO NOT EDIT. + +package ledgercreditvoidrecord + +import ( + "time" + + "entgo.io/ent/dialect/sql" +) + +const ( + // Label holds the string label denoting the ledgercreditvoidrecord type in the database. + Label = "ledger_credit_void_record" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldNamespace holds the string denoting the namespace field in the database. + FieldNamespace = "namespace" + // FieldAnnotations holds the string denoting the annotations field in the database. + FieldAnnotations = "annotations" + // FieldCreatedAt holds the string denoting the created_at field in the database. + FieldCreatedAt = "created_at" + // FieldUpdatedAt holds the string denoting the updated_at field in the database. + FieldUpdatedAt = "updated_at" + // FieldDeletedAt holds the string denoting the deleted_at field in the database. + FieldDeletedAt = "deleted_at" + // FieldAmount holds the string denoting the amount field in the database. + FieldAmount = "amount" + // FieldCustomerID holds the string denoting the customer_id field in the database. + FieldCustomerID = "customer_id" + // FieldCurrency holds the string denoting the currency field in the database. + FieldCurrency = "currency" + // FieldVoidedAt holds the string denoting the voided_at field in the database. + FieldVoidedAt = "voided_at" + // FieldSourceChargeID holds the string denoting the source_charge_id field in the database. + FieldSourceChargeID = "source_charge_id" + // FieldVoidTransactionGroupID holds the string denoting the void_transaction_group_id field in the database. + FieldVoidTransactionGroupID = "void_transaction_group_id" + // FieldVoidTransactionID holds the string denoting the void_transaction_id field in the database. + FieldVoidTransactionID = "void_transaction_id" + // FieldFboSubAccountID holds the string denoting the fbo_sub_account_id field in the database. + FieldFboSubAccountID = "fbo_sub_account_id" + // FieldReceivableSubAccountID holds the string denoting the receivable_sub_account_id field in the database. + FieldReceivableSubAccountID = "receivable_sub_account_id" + // Table holds the table name of the ledgercreditvoidrecord in the database. + Table = "ledger_credit_void_records" +) + +// Columns holds all SQL columns for ledgercreditvoidrecord fields. +var Columns = []string{ + FieldID, + FieldNamespace, + FieldAnnotations, + FieldCreatedAt, + FieldUpdatedAt, + FieldDeletedAt, + FieldAmount, + FieldCustomerID, + FieldCurrency, + FieldVoidedAt, + FieldSourceChargeID, + FieldVoidTransactionGroupID, + FieldVoidTransactionID, + FieldFboSubAccountID, + FieldReceivableSubAccountID, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // NamespaceValidator is a validator for the "namespace" field. It is called by the builders before save. + NamespaceValidator func(string) error + // DefaultCreatedAt holds the default value on creation for the "created_at" field. + DefaultCreatedAt func() time.Time + // DefaultUpdatedAt holds the default value on creation for the "updated_at" field. + DefaultUpdatedAt func() time.Time + // UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field. + UpdateDefaultUpdatedAt func() time.Time + // CustomerIDValidator is a validator for the "customer_id" field. It is called by the builders before save. + CustomerIDValidator func(string) error + // CurrencyValidator is a validator for the "currency" field. It is called by the builders before save. + CurrencyValidator func(string) error + // SourceChargeIDValidator is a validator for the "source_charge_id" field. It is called by the builders before save. + SourceChargeIDValidator func(string) error + // VoidTransactionGroupIDValidator is a validator for the "void_transaction_group_id" field. It is called by the builders before save. + VoidTransactionGroupIDValidator func(string) error + // VoidTransactionIDValidator is a validator for the "void_transaction_id" field. It is called by the builders before save. + VoidTransactionIDValidator func(string) error + // FboSubAccountIDValidator is a validator for the "fbo_sub_account_id" field. It is called by the builders before save. + FboSubAccountIDValidator func(string) error + // ReceivableSubAccountIDValidator is a validator for the "receivable_sub_account_id" field. It is called by the builders before save. + ReceivableSubAccountIDValidator func(string) error + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() string +) + +// OrderOption defines the ordering options for the LedgerCreditVoidRecord queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByNamespace orders the results by the namespace field. +func ByNamespace(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldNamespace, opts...).ToFunc() +} + +// ByCreatedAt orders the results by the created_at field. +func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() +} + +// ByUpdatedAt orders the results by the updated_at field. +func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc() +} + +// ByDeletedAt orders the results by the deleted_at field. +func ByDeletedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDeletedAt, opts...).ToFunc() +} + +// ByAmount orders the results by the amount field. +func ByAmount(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAmount, opts...).ToFunc() +} + +// ByCustomerID orders the results by the customer_id field. +func ByCustomerID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCustomerID, opts...).ToFunc() +} + +// ByCurrency orders the results by the currency field. +func ByCurrency(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCurrency, opts...).ToFunc() +} + +// ByVoidedAt orders the results by the voided_at field. +func ByVoidedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldVoidedAt, opts...).ToFunc() +} + +// BySourceChargeID orders the results by the source_charge_id field. +func BySourceChargeID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSourceChargeID, opts...).ToFunc() +} + +// ByVoidTransactionGroupID orders the results by the void_transaction_group_id field. +func ByVoidTransactionGroupID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldVoidTransactionGroupID, opts...).ToFunc() +} + +// ByVoidTransactionID orders the results by the void_transaction_id field. +func ByVoidTransactionID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldVoidTransactionID, opts...).ToFunc() +} + +// ByFboSubAccountID orders the results by the fbo_sub_account_id field. +func ByFboSubAccountID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldFboSubAccountID, opts...).ToFunc() +} + +// ByReceivableSubAccountID orders the results by the receivable_sub_account_id field. +func ByReceivableSubAccountID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldReceivableSubAccountID, opts...).ToFunc() +} diff --git a/openmeter/ent/db/ledgercreditvoidrecord/where.go b/openmeter/ent/db/ledgercreditvoidrecord/where.go new file mode 100644 index 0000000000..a9fc3cd231 --- /dev/null +++ b/openmeter/ent/db/ledgercreditvoidrecord/where.go @@ -0,0 +1,907 @@ +// Code generated by ent, DO NOT EDIT. + +package ledgercreditvoidrecord + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/alpacahq/alpacadecimal" + "github.com/openmeterio/openmeter/openmeter/ent/db/predicate" + "github.com/openmeterio/openmeter/pkg/currencyx" +) + +// ID filters vertices based on their ID field. +func ID(id string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLTE(FieldID, id)) +} + +// IDEqualFold applies the EqualFold predicate on the ID field. +func IDEqualFold(id string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEqualFold(FieldID, id)) +} + +// IDContainsFold applies the ContainsFold predicate on the ID field. +func IDContainsFold(id string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldContainsFold(FieldID, id)) +} + +// Namespace applies equality check predicate on the "namespace" field. It's identical to NamespaceEQ. +func Namespace(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldNamespace, v)) +} + +// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. +func CreatedAt(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldCreatedAt, v)) +} + +// UpdatedAt applies equality check predicate on the "updated_at" field. It's identical to UpdatedAtEQ. +func UpdatedAt(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldUpdatedAt, v)) +} + +// DeletedAt applies equality check predicate on the "deleted_at" field. It's identical to DeletedAtEQ. +func DeletedAt(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldDeletedAt, v)) +} + +// Amount applies equality check predicate on the "amount" field. It's identical to AmountEQ. +func Amount(v alpacadecimal.Decimal) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldAmount, v)) +} + +// CustomerID applies equality check predicate on the "customer_id" field. It's identical to CustomerIDEQ. +func CustomerID(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldCustomerID, v)) +} + +// Currency applies equality check predicate on the "currency" field. It's identical to CurrencyEQ. +func Currency(v currencyx.Code) predicate.LedgerCreditVoidRecord { + vc := string(v) + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldCurrency, vc)) +} + +// VoidedAt applies equality check predicate on the "voided_at" field. It's identical to VoidedAtEQ. +func VoidedAt(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldVoidedAt, v)) +} + +// SourceChargeID applies equality check predicate on the "source_charge_id" field. It's identical to SourceChargeIDEQ. +func SourceChargeID(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldSourceChargeID, v)) +} + +// VoidTransactionGroupID applies equality check predicate on the "void_transaction_group_id" field. It's identical to VoidTransactionGroupIDEQ. +func VoidTransactionGroupID(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldVoidTransactionGroupID, v)) +} + +// VoidTransactionID applies equality check predicate on the "void_transaction_id" field. It's identical to VoidTransactionIDEQ. +func VoidTransactionID(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldVoidTransactionID, v)) +} + +// FboSubAccountID applies equality check predicate on the "fbo_sub_account_id" field. It's identical to FboSubAccountIDEQ. +func FboSubAccountID(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldFboSubAccountID, v)) +} + +// ReceivableSubAccountID applies equality check predicate on the "receivable_sub_account_id" field. It's identical to ReceivableSubAccountIDEQ. +func ReceivableSubAccountID(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldReceivableSubAccountID, v)) +} + +// NamespaceEQ applies the EQ predicate on the "namespace" field. +func NamespaceEQ(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldNamespace, v)) +} + +// NamespaceNEQ applies the NEQ predicate on the "namespace" field. +func NamespaceNEQ(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNEQ(FieldNamespace, v)) +} + +// NamespaceIn applies the In predicate on the "namespace" field. +func NamespaceIn(vs ...string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldIn(FieldNamespace, vs...)) +} + +// NamespaceNotIn applies the NotIn predicate on the "namespace" field. +func NamespaceNotIn(vs ...string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNotIn(FieldNamespace, vs...)) +} + +// NamespaceGT applies the GT predicate on the "namespace" field. +func NamespaceGT(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGT(FieldNamespace, v)) +} + +// NamespaceGTE applies the GTE predicate on the "namespace" field. +func NamespaceGTE(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGTE(FieldNamespace, v)) +} + +// NamespaceLT applies the LT predicate on the "namespace" field. +func NamespaceLT(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLT(FieldNamespace, v)) +} + +// NamespaceLTE applies the LTE predicate on the "namespace" field. +func NamespaceLTE(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLTE(FieldNamespace, v)) +} + +// NamespaceContains applies the Contains predicate on the "namespace" field. +func NamespaceContains(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldContains(FieldNamespace, v)) +} + +// NamespaceHasPrefix applies the HasPrefix predicate on the "namespace" field. +func NamespaceHasPrefix(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldHasPrefix(FieldNamespace, v)) +} + +// NamespaceHasSuffix applies the HasSuffix predicate on the "namespace" field. +func NamespaceHasSuffix(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldHasSuffix(FieldNamespace, v)) +} + +// NamespaceEqualFold applies the EqualFold predicate on the "namespace" field. +func NamespaceEqualFold(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEqualFold(FieldNamespace, v)) +} + +// NamespaceContainsFold applies the ContainsFold predicate on the "namespace" field. +func NamespaceContainsFold(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldContainsFold(FieldNamespace, v)) +} + +// AnnotationsIsNil applies the IsNil predicate on the "annotations" field. +func AnnotationsIsNil() predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldIsNull(FieldAnnotations)) +} + +// AnnotationsNotNil applies the NotNil predicate on the "annotations" field. +func AnnotationsNotNil() predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNotNull(FieldAnnotations)) +} + +// CreatedAtEQ applies the EQ predicate on the "created_at" field. +func CreatedAtEQ(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldCreatedAt, v)) +} + +// CreatedAtNEQ applies the NEQ predicate on the "created_at" field. +func CreatedAtNEQ(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNEQ(FieldCreatedAt, v)) +} + +// CreatedAtIn applies the In predicate on the "created_at" field. +func CreatedAtIn(vs ...time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldIn(FieldCreatedAt, vs...)) +} + +// CreatedAtNotIn applies the NotIn predicate on the "created_at" field. +func CreatedAtNotIn(vs ...time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNotIn(FieldCreatedAt, vs...)) +} + +// CreatedAtGT applies the GT predicate on the "created_at" field. +func CreatedAtGT(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGT(FieldCreatedAt, v)) +} + +// CreatedAtGTE applies the GTE predicate on the "created_at" field. +func CreatedAtGTE(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGTE(FieldCreatedAt, v)) +} + +// CreatedAtLT applies the LT predicate on the "created_at" field. +func CreatedAtLT(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLT(FieldCreatedAt, v)) +} + +// CreatedAtLTE applies the LTE predicate on the "created_at" field. +func CreatedAtLTE(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLTE(FieldCreatedAt, v)) +} + +// UpdatedAtEQ applies the EQ predicate on the "updated_at" field. +func UpdatedAtEQ(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldUpdatedAt, v)) +} + +// UpdatedAtNEQ applies the NEQ predicate on the "updated_at" field. +func UpdatedAtNEQ(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNEQ(FieldUpdatedAt, v)) +} + +// UpdatedAtIn applies the In predicate on the "updated_at" field. +func UpdatedAtIn(vs ...time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldIn(FieldUpdatedAt, vs...)) +} + +// UpdatedAtNotIn applies the NotIn predicate on the "updated_at" field. +func UpdatedAtNotIn(vs ...time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNotIn(FieldUpdatedAt, vs...)) +} + +// UpdatedAtGT applies the GT predicate on the "updated_at" field. +func UpdatedAtGT(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGT(FieldUpdatedAt, v)) +} + +// UpdatedAtGTE applies the GTE predicate on the "updated_at" field. +func UpdatedAtGTE(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGTE(FieldUpdatedAt, v)) +} + +// UpdatedAtLT applies the LT predicate on the "updated_at" field. +func UpdatedAtLT(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLT(FieldUpdatedAt, v)) +} + +// UpdatedAtLTE applies the LTE predicate on the "updated_at" field. +func UpdatedAtLTE(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLTE(FieldUpdatedAt, v)) +} + +// DeletedAtEQ applies the EQ predicate on the "deleted_at" field. +func DeletedAtEQ(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldDeletedAt, v)) +} + +// DeletedAtNEQ applies the NEQ predicate on the "deleted_at" field. +func DeletedAtNEQ(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNEQ(FieldDeletedAt, v)) +} + +// DeletedAtIn applies the In predicate on the "deleted_at" field. +func DeletedAtIn(vs ...time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldIn(FieldDeletedAt, vs...)) +} + +// DeletedAtNotIn applies the NotIn predicate on the "deleted_at" field. +func DeletedAtNotIn(vs ...time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNotIn(FieldDeletedAt, vs...)) +} + +// DeletedAtGT applies the GT predicate on the "deleted_at" field. +func DeletedAtGT(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGT(FieldDeletedAt, v)) +} + +// DeletedAtGTE applies the GTE predicate on the "deleted_at" field. +func DeletedAtGTE(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGTE(FieldDeletedAt, v)) +} + +// DeletedAtLT applies the LT predicate on the "deleted_at" field. +func DeletedAtLT(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLT(FieldDeletedAt, v)) +} + +// DeletedAtLTE applies the LTE predicate on the "deleted_at" field. +func DeletedAtLTE(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLTE(FieldDeletedAt, v)) +} + +// DeletedAtIsNil applies the IsNil predicate on the "deleted_at" field. +func DeletedAtIsNil() predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldIsNull(FieldDeletedAt)) +} + +// DeletedAtNotNil applies the NotNil predicate on the "deleted_at" field. +func DeletedAtNotNil() predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNotNull(FieldDeletedAt)) +} + +// AmountEQ applies the EQ predicate on the "amount" field. +func AmountEQ(v alpacadecimal.Decimal) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldAmount, v)) +} + +// AmountNEQ applies the NEQ predicate on the "amount" field. +func AmountNEQ(v alpacadecimal.Decimal) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNEQ(FieldAmount, v)) +} + +// AmountIn applies the In predicate on the "amount" field. +func AmountIn(vs ...alpacadecimal.Decimal) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldIn(FieldAmount, vs...)) +} + +// AmountNotIn applies the NotIn predicate on the "amount" field. +func AmountNotIn(vs ...alpacadecimal.Decimal) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNotIn(FieldAmount, vs...)) +} + +// AmountGT applies the GT predicate on the "amount" field. +func AmountGT(v alpacadecimal.Decimal) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGT(FieldAmount, v)) +} + +// AmountGTE applies the GTE predicate on the "amount" field. +func AmountGTE(v alpacadecimal.Decimal) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGTE(FieldAmount, v)) +} + +// AmountLT applies the LT predicate on the "amount" field. +func AmountLT(v alpacadecimal.Decimal) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLT(FieldAmount, v)) +} + +// AmountLTE applies the LTE predicate on the "amount" field. +func AmountLTE(v alpacadecimal.Decimal) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLTE(FieldAmount, v)) +} + +// CustomerIDEQ applies the EQ predicate on the "customer_id" field. +func CustomerIDEQ(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldCustomerID, v)) +} + +// CustomerIDNEQ applies the NEQ predicate on the "customer_id" field. +func CustomerIDNEQ(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNEQ(FieldCustomerID, v)) +} + +// CustomerIDIn applies the In predicate on the "customer_id" field. +func CustomerIDIn(vs ...string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldIn(FieldCustomerID, vs...)) +} + +// CustomerIDNotIn applies the NotIn predicate on the "customer_id" field. +func CustomerIDNotIn(vs ...string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNotIn(FieldCustomerID, vs...)) +} + +// CustomerIDGT applies the GT predicate on the "customer_id" field. +func CustomerIDGT(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGT(FieldCustomerID, v)) +} + +// CustomerIDGTE applies the GTE predicate on the "customer_id" field. +func CustomerIDGTE(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGTE(FieldCustomerID, v)) +} + +// CustomerIDLT applies the LT predicate on the "customer_id" field. +func CustomerIDLT(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLT(FieldCustomerID, v)) +} + +// CustomerIDLTE applies the LTE predicate on the "customer_id" field. +func CustomerIDLTE(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLTE(FieldCustomerID, v)) +} + +// CustomerIDContains applies the Contains predicate on the "customer_id" field. +func CustomerIDContains(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldContains(FieldCustomerID, v)) +} + +// CustomerIDHasPrefix applies the HasPrefix predicate on the "customer_id" field. +func CustomerIDHasPrefix(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldHasPrefix(FieldCustomerID, v)) +} + +// CustomerIDHasSuffix applies the HasSuffix predicate on the "customer_id" field. +func CustomerIDHasSuffix(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldHasSuffix(FieldCustomerID, v)) +} + +// CustomerIDEqualFold applies the EqualFold predicate on the "customer_id" field. +func CustomerIDEqualFold(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEqualFold(FieldCustomerID, v)) +} + +// CustomerIDContainsFold applies the ContainsFold predicate on the "customer_id" field. +func CustomerIDContainsFold(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldContainsFold(FieldCustomerID, v)) +} + +// CurrencyEQ applies the EQ predicate on the "currency" field. +func CurrencyEQ(v currencyx.Code) predicate.LedgerCreditVoidRecord { + vc := string(v) + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldCurrency, vc)) +} + +// CurrencyNEQ applies the NEQ predicate on the "currency" field. +func CurrencyNEQ(v currencyx.Code) predicate.LedgerCreditVoidRecord { + vc := string(v) + return predicate.LedgerCreditVoidRecord(sql.FieldNEQ(FieldCurrency, vc)) +} + +// CurrencyIn applies the In predicate on the "currency" field. +func CurrencyIn(vs ...currencyx.Code) predicate.LedgerCreditVoidRecord { + v := make([]any, len(vs)) + for i := range v { + v[i] = string(vs[i]) + } + return predicate.LedgerCreditVoidRecord(sql.FieldIn(FieldCurrency, v...)) +} + +// CurrencyNotIn applies the NotIn predicate on the "currency" field. +func CurrencyNotIn(vs ...currencyx.Code) predicate.LedgerCreditVoidRecord { + v := make([]any, len(vs)) + for i := range v { + v[i] = string(vs[i]) + } + return predicate.LedgerCreditVoidRecord(sql.FieldNotIn(FieldCurrency, v...)) +} + +// CurrencyGT applies the GT predicate on the "currency" field. +func CurrencyGT(v currencyx.Code) predicate.LedgerCreditVoidRecord { + vc := string(v) + return predicate.LedgerCreditVoidRecord(sql.FieldGT(FieldCurrency, vc)) +} + +// CurrencyGTE applies the GTE predicate on the "currency" field. +func CurrencyGTE(v currencyx.Code) predicate.LedgerCreditVoidRecord { + vc := string(v) + return predicate.LedgerCreditVoidRecord(sql.FieldGTE(FieldCurrency, vc)) +} + +// CurrencyLT applies the LT predicate on the "currency" field. +func CurrencyLT(v currencyx.Code) predicate.LedgerCreditVoidRecord { + vc := string(v) + return predicate.LedgerCreditVoidRecord(sql.FieldLT(FieldCurrency, vc)) +} + +// CurrencyLTE applies the LTE predicate on the "currency" field. +func CurrencyLTE(v currencyx.Code) predicate.LedgerCreditVoidRecord { + vc := string(v) + return predicate.LedgerCreditVoidRecord(sql.FieldLTE(FieldCurrency, vc)) +} + +// CurrencyContains applies the Contains predicate on the "currency" field. +func CurrencyContains(v currencyx.Code) predicate.LedgerCreditVoidRecord { + vc := string(v) + return predicate.LedgerCreditVoidRecord(sql.FieldContains(FieldCurrency, vc)) +} + +// CurrencyHasPrefix applies the HasPrefix predicate on the "currency" field. +func CurrencyHasPrefix(v currencyx.Code) predicate.LedgerCreditVoidRecord { + vc := string(v) + return predicate.LedgerCreditVoidRecord(sql.FieldHasPrefix(FieldCurrency, vc)) +} + +// CurrencyHasSuffix applies the HasSuffix predicate on the "currency" field. +func CurrencyHasSuffix(v currencyx.Code) predicate.LedgerCreditVoidRecord { + vc := string(v) + return predicate.LedgerCreditVoidRecord(sql.FieldHasSuffix(FieldCurrency, vc)) +} + +// CurrencyEqualFold applies the EqualFold predicate on the "currency" field. +func CurrencyEqualFold(v currencyx.Code) predicate.LedgerCreditVoidRecord { + vc := string(v) + return predicate.LedgerCreditVoidRecord(sql.FieldEqualFold(FieldCurrency, vc)) +} + +// CurrencyContainsFold applies the ContainsFold predicate on the "currency" field. +func CurrencyContainsFold(v currencyx.Code) predicate.LedgerCreditVoidRecord { + vc := string(v) + return predicate.LedgerCreditVoidRecord(sql.FieldContainsFold(FieldCurrency, vc)) +} + +// VoidedAtEQ applies the EQ predicate on the "voided_at" field. +func VoidedAtEQ(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldVoidedAt, v)) +} + +// VoidedAtNEQ applies the NEQ predicate on the "voided_at" field. +func VoidedAtNEQ(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNEQ(FieldVoidedAt, v)) +} + +// VoidedAtIn applies the In predicate on the "voided_at" field. +func VoidedAtIn(vs ...time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldIn(FieldVoidedAt, vs...)) +} + +// VoidedAtNotIn applies the NotIn predicate on the "voided_at" field. +func VoidedAtNotIn(vs ...time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNotIn(FieldVoidedAt, vs...)) +} + +// VoidedAtGT applies the GT predicate on the "voided_at" field. +func VoidedAtGT(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGT(FieldVoidedAt, v)) +} + +// VoidedAtGTE applies the GTE predicate on the "voided_at" field. +func VoidedAtGTE(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGTE(FieldVoidedAt, v)) +} + +// VoidedAtLT applies the LT predicate on the "voided_at" field. +func VoidedAtLT(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLT(FieldVoidedAt, v)) +} + +// VoidedAtLTE applies the LTE predicate on the "voided_at" field. +func VoidedAtLTE(v time.Time) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLTE(FieldVoidedAt, v)) +} + +// SourceChargeIDEQ applies the EQ predicate on the "source_charge_id" field. +func SourceChargeIDEQ(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldSourceChargeID, v)) +} + +// SourceChargeIDNEQ applies the NEQ predicate on the "source_charge_id" field. +func SourceChargeIDNEQ(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNEQ(FieldSourceChargeID, v)) +} + +// SourceChargeIDIn applies the In predicate on the "source_charge_id" field. +func SourceChargeIDIn(vs ...string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldIn(FieldSourceChargeID, vs...)) +} + +// SourceChargeIDNotIn applies the NotIn predicate on the "source_charge_id" field. +func SourceChargeIDNotIn(vs ...string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNotIn(FieldSourceChargeID, vs...)) +} + +// SourceChargeIDGT applies the GT predicate on the "source_charge_id" field. +func SourceChargeIDGT(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGT(FieldSourceChargeID, v)) +} + +// SourceChargeIDGTE applies the GTE predicate on the "source_charge_id" field. +func SourceChargeIDGTE(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGTE(FieldSourceChargeID, v)) +} + +// SourceChargeIDLT applies the LT predicate on the "source_charge_id" field. +func SourceChargeIDLT(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLT(FieldSourceChargeID, v)) +} + +// SourceChargeIDLTE applies the LTE predicate on the "source_charge_id" field. +func SourceChargeIDLTE(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLTE(FieldSourceChargeID, v)) +} + +// SourceChargeIDContains applies the Contains predicate on the "source_charge_id" field. +func SourceChargeIDContains(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldContains(FieldSourceChargeID, v)) +} + +// SourceChargeIDHasPrefix applies the HasPrefix predicate on the "source_charge_id" field. +func SourceChargeIDHasPrefix(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldHasPrefix(FieldSourceChargeID, v)) +} + +// SourceChargeIDHasSuffix applies the HasSuffix predicate on the "source_charge_id" field. +func SourceChargeIDHasSuffix(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldHasSuffix(FieldSourceChargeID, v)) +} + +// SourceChargeIDEqualFold applies the EqualFold predicate on the "source_charge_id" field. +func SourceChargeIDEqualFold(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEqualFold(FieldSourceChargeID, v)) +} + +// SourceChargeIDContainsFold applies the ContainsFold predicate on the "source_charge_id" field. +func SourceChargeIDContainsFold(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldContainsFold(FieldSourceChargeID, v)) +} + +// VoidTransactionGroupIDEQ applies the EQ predicate on the "void_transaction_group_id" field. +func VoidTransactionGroupIDEQ(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldVoidTransactionGroupID, v)) +} + +// VoidTransactionGroupIDNEQ applies the NEQ predicate on the "void_transaction_group_id" field. +func VoidTransactionGroupIDNEQ(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNEQ(FieldVoidTransactionGroupID, v)) +} + +// VoidTransactionGroupIDIn applies the In predicate on the "void_transaction_group_id" field. +func VoidTransactionGroupIDIn(vs ...string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldIn(FieldVoidTransactionGroupID, vs...)) +} + +// VoidTransactionGroupIDNotIn applies the NotIn predicate on the "void_transaction_group_id" field. +func VoidTransactionGroupIDNotIn(vs ...string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNotIn(FieldVoidTransactionGroupID, vs...)) +} + +// VoidTransactionGroupIDGT applies the GT predicate on the "void_transaction_group_id" field. +func VoidTransactionGroupIDGT(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGT(FieldVoidTransactionGroupID, v)) +} + +// VoidTransactionGroupIDGTE applies the GTE predicate on the "void_transaction_group_id" field. +func VoidTransactionGroupIDGTE(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGTE(FieldVoidTransactionGroupID, v)) +} + +// VoidTransactionGroupIDLT applies the LT predicate on the "void_transaction_group_id" field. +func VoidTransactionGroupIDLT(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLT(FieldVoidTransactionGroupID, v)) +} + +// VoidTransactionGroupIDLTE applies the LTE predicate on the "void_transaction_group_id" field. +func VoidTransactionGroupIDLTE(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLTE(FieldVoidTransactionGroupID, v)) +} + +// VoidTransactionGroupIDContains applies the Contains predicate on the "void_transaction_group_id" field. +func VoidTransactionGroupIDContains(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldContains(FieldVoidTransactionGroupID, v)) +} + +// VoidTransactionGroupIDHasPrefix applies the HasPrefix predicate on the "void_transaction_group_id" field. +func VoidTransactionGroupIDHasPrefix(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldHasPrefix(FieldVoidTransactionGroupID, v)) +} + +// VoidTransactionGroupIDHasSuffix applies the HasSuffix predicate on the "void_transaction_group_id" field. +func VoidTransactionGroupIDHasSuffix(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldHasSuffix(FieldVoidTransactionGroupID, v)) +} + +// VoidTransactionGroupIDEqualFold applies the EqualFold predicate on the "void_transaction_group_id" field. +func VoidTransactionGroupIDEqualFold(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEqualFold(FieldVoidTransactionGroupID, v)) +} + +// VoidTransactionGroupIDContainsFold applies the ContainsFold predicate on the "void_transaction_group_id" field. +func VoidTransactionGroupIDContainsFold(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldContainsFold(FieldVoidTransactionGroupID, v)) +} + +// VoidTransactionIDEQ applies the EQ predicate on the "void_transaction_id" field. +func VoidTransactionIDEQ(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldVoidTransactionID, v)) +} + +// VoidTransactionIDNEQ applies the NEQ predicate on the "void_transaction_id" field. +func VoidTransactionIDNEQ(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNEQ(FieldVoidTransactionID, v)) +} + +// VoidTransactionIDIn applies the In predicate on the "void_transaction_id" field. +func VoidTransactionIDIn(vs ...string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldIn(FieldVoidTransactionID, vs...)) +} + +// VoidTransactionIDNotIn applies the NotIn predicate on the "void_transaction_id" field. +func VoidTransactionIDNotIn(vs ...string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNotIn(FieldVoidTransactionID, vs...)) +} + +// VoidTransactionIDGT applies the GT predicate on the "void_transaction_id" field. +func VoidTransactionIDGT(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGT(FieldVoidTransactionID, v)) +} + +// VoidTransactionIDGTE applies the GTE predicate on the "void_transaction_id" field. +func VoidTransactionIDGTE(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGTE(FieldVoidTransactionID, v)) +} + +// VoidTransactionIDLT applies the LT predicate on the "void_transaction_id" field. +func VoidTransactionIDLT(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLT(FieldVoidTransactionID, v)) +} + +// VoidTransactionIDLTE applies the LTE predicate on the "void_transaction_id" field. +func VoidTransactionIDLTE(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLTE(FieldVoidTransactionID, v)) +} + +// VoidTransactionIDContains applies the Contains predicate on the "void_transaction_id" field. +func VoidTransactionIDContains(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldContains(FieldVoidTransactionID, v)) +} + +// VoidTransactionIDHasPrefix applies the HasPrefix predicate on the "void_transaction_id" field. +func VoidTransactionIDHasPrefix(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldHasPrefix(FieldVoidTransactionID, v)) +} + +// VoidTransactionIDHasSuffix applies the HasSuffix predicate on the "void_transaction_id" field. +func VoidTransactionIDHasSuffix(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldHasSuffix(FieldVoidTransactionID, v)) +} + +// VoidTransactionIDEqualFold applies the EqualFold predicate on the "void_transaction_id" field. +func VoidTransactionIDEqualFold(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEqualFold(FieldVoidTransactionID, v)) +} + +// VoidTransactionIDContainsFold applies the ContainsFold predicate on the "void_transaction_id" field. +func VoidTransactionIDContainsFold(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldContainsFold(FieldVoidTransactionID, v)) +} + +// FboSubAccountIDEQ applies the EQ predicate on the "fbo_sub_account_id" field. +func FboSubAccountIDEQ(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldFboSubAccountID, v)) +} + +// FboSubAccountIDNEQ applies the NEQ predicate on the "fbo_sub_account_id" field. +func FboSubAccountIDNEQ(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNEQ(FieldFboSubAccountID, v)) +} + +// FboSubAccountIDIn applies the In predicate on the "fbo_sub_account_id" field. +func FboSubAccountIDIn(vs ...string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldIn(FieldFboSubAccountID, vs...)) +} + +// FboSubAccountIDNotIn applies the NotIn predicate on the "fbo_sub_account_id" field. +func FboSubAccountIDNotIn(vs ...string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNotIn(FieldFboSubAccountID, vs...)) +} + +// FboSubAccountIDGT applies the GT predicate on the "fbo_sub_account_id" field. +func FboSubAccountIDGT(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGT(FieldFboSubAccountID, v)) +} + +// FboSubAccountIDGTE applies the GTE predicate on the "fbo_sub_account_id" field. +func FboSubAccountIDGTE(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGTE(FieldFboSubAccountID, v)) +} + +// FboSubAccountIDLT applies the LT predicate on the "fbo_sub_account_id" field. +func FboSubAccountIDLT(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLT(FieldFboSubAccountID, v)) +} + +// FboSubAccountIDLTE applies the LTE predicate on the "fbo_sub_account_id" field. +func FboSubAccountIDLTE(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLTE(FieldFboSubAccountID, v)) +} + +// FboSubAccountIDContains applies the Contains predicate on the "fbo_sub_account_id" field. +func FboSubAccountIDContains(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldContains(FieldFboSubAccountID, v)) +} + +// FboSubAccountIDHasPrefix applies the HasPrefix predicate on the "fbo_sub_account_id" field. +func FboSubAccountIDHasPrefix(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldHasPrefix(FieldFboSubAccountID, v)) +} + +// FboSubAccountIDHasSuffix applies the HasSuffix predicate on the "fbo_sub_account_id" field. +func FboSubAccountIDHasSuffix(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldHasSuffix(FieldFboSubAccountID, v)) +} + +// FboSubAccountIDEqualFold applies the EqualFold predicate on the "fbo_sub_account_id" field. +func FboSubAccountIDEqualFold(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEqualFold(FieldFboSubAccountID, v)) +} + +// FboSubAccountIDContainsFold applies the ContainsFold predicate on the "fbo_sub_account_id" field. +func FboSubAccountIDContainsFold(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldContainsFold(FieldFboSubAccountID, v)) +} + +// ReceivableSubAccountIDEQ applies the EQ predicate on the "receivable_sub_account_id" field. +func ReceivableSubAccountIDEQ(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEQ(FieldReceivableSubAccountID, v)) +} + +// ReceivableSubAccountIDNEQ applies the NEQ predicate on the "receivable_sub_account_id" field. +func ReceivableSubAccountIDNEQ(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNEQ(FieldReceivableSubAccountID, v)) +} + +// ReceivableSubAccountIDIn applies the In predicate on the "receivable_sub_account_id" field. +func ReceivableSubAccountIDIn(vs ...string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldIn(FieldReceivableSubAccountID, vs...)) +} + +// ReceivableSubAccountIDNotIn applies the NotIn predicate on the "receivable_sub_account_id" field. +func ReceivableSubAccountIDNotIn(vs ...string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldNotIn(FieldReceivableSubAccountID, vs...)) +} + +// ReceivableSubAccountIDGT applies the GT predicate on the "receivable_sub_account_id" field. +func ReceivableSubAccountIDGT(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGT(FieldReceivableSubAccountID, v)) +} + +// ReceivableSubAccountIDGTE applies the GTE predicate on the "receivable_sub_account_id" field. +func ReceivableSubAccountIDGTE(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldGTE(FieldReceivableSubAccountID, v)) +} + +// ReceivableSubAccountIDLT applies the LT predicate on the "receivable_sub_account_id" field. +func ReceivableSubAccountIDLT(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLT(FieldReceivableSubAccountID, v)) +} + +// ReceivableSubAccountIDLTE applies the LTE predicate on the "receivable_sub_account_id" field. +func ReceivableSubAccountIDLTE(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldLTE(FieldReceivableSubAccountID, v)) +} + +// ReceivableSubAccountIDContains applies the Contains predicate on the "receivable_sub_account_id" field. +func ReceivableSubAccountIDContains(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldContains(FieldReceivableSubAccountID, v)) +} + +// ReceivableSubAccountIDHasPrefix applies the HasPrefix predicate on the "receivable_sub_account_id" field. +func ReceivableSubAccountIDHasPrefix(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldHasPrefix(FieldReceivableSubAccountID, v)) +} + +// ReceivableSubAccountIDHasSuffix applies the HasSuffix predicate on the "receivable_sub_account_id" field. +func ReceivableSubAccountIDHasSuffix(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldHasSuffix(FieldReceivableSubAccountID, v)) +} + +// ReceivableSubAccountIDEqualFold applies the EqualFold predicate on the "receivable_sub_account_id" field. +func ReceivableSubAccountIDEqualFold(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldEqualFold(FieldReceivableSubAccountID, v)) +} + +// ReceivableSubAccountIDContainsFold applies the ContainsFold predicate on the "receivable_sub_account_id" field. +func ReceivableSubAccountIDContainsFold(v string) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.FieldContainsFold(FieldReceivableSubAccountID, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.LedgerCreditVoidRecord) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.LedgerCreditVoidRecord) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.LedgerCreditVoidRecord) predicate.LedgerCreditVoidRecord { + return predicate.LedgerCreditVoidRecord(sql.NotPredicates(p)) +} diff --git a/openmeter/ent/db/ledgercreditvoidrecord_create.go b/openmeter/ent/db/ledgercreditvoidrecord_create.go new file mode 100644 index 0000000000..bb595c218a --- /dev/null +++ b/openmeter/ent/db/ledgercreditvoidrecord_create.go @@ -0,0 +1,966 @@ +// Code generated by ent, DO NOT EDIT. + +package db + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/alpacahq/alpacadecimal" + "github.com/openmeterio/openmeter/openmeter/ent/db/ledgercreditvoidrecord" + "github.com/openmeterio/openmeter/pkg/currencyx" + "github.com/openmeterio/openmeter/pkg/models" +) + +// LedgerCreditVoidRecordCreate is the builder for creating a LedgerCreditVoidRecord entity. +type LedgerCreditVoidRecordCreate struct { + config + mutation *LedgerCreditVoidRecordMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetNamespace sets the "namespace" field. +func (_c *LedgerCreditVoidRecordCreate) SetNamespace(v string) *LedgerCreditVoidRecordCreate { + _c.mutation.SetNamespace(v) + return _c +} + +// SetAnnotations sets the "annotations" field. +func (_c *LedgerCreditVoidRecordCreate) SetAnnotations(v models.Annotations) *LedgerCreditVoidRecordCreate { + _c.mutation.SetAnnotations(v) + return _c +} + +// SetCreatedAt sets the "created_at" field. +func (_c *LedgerCreditVoidRecordCreate) SetCreatedAt(v time.Time) *LedgerCreditVoidRecordCreate { + _c.mutation.SetCreatedAt(v) + return _c +} + +// SetNillableCreatedAt sets the "created_at" field if the given value is not nil. +func (_c *LedgerCreditVoidRecordCreate) SetNillableCreatedAt(v *time.Time) *LedgerCreditVoidRecordCreate { + if v != nil { + _c.SetCreatedAt(*v) + } + return _c +} + +// SetUpdatedAt sets the "updated_at" field. +func (_c *LedgerCreditVoidRecordCreate) SetUpdatedAt(v time.Time) *LedgerCreditVoidRecordCreate { + _c.mutation.SetUpdatedAt(v) + return _c +} + +// SetNillableUpdatedAt sets the "updated_at" field if the given value is not nil. +func (_c *LedgerCreditVoidRecordCreate) SetNillableUpdatedAt(v *time.Time) *LedgerCreditVoidRecordCreate { + if v != nil { + _c.SetUpdatedAt(*v) + } + return _c +} + +// SetDeletedAt sets the "deleted_at" field. +func (_c *LedgerCreditVoidRecordCreate) SetDeletedAt(v time.Time) *LedgerCreditVoidRecordCreate { + _c.mutation.SetDeletedAt(v) + return _c +} + +// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil. +func (_c *LedgerCreditVoidRecordCreate) SetNillableDeletedAt(v *time.Time) *LedgerCreditVoidRecordCreate { + if v != nil { + _c.SetDeletedAt(*v) + } + return _c +} + +// SetAmount sets the "amount" field. +func (_c *LedgerCreditVoidRecordCreate) SetAmount(v alpacadecimal.Decimal) *LedgerCreditVoidRecordCreate { + _c.mutation.SetAmount(v) + return _c +} + +// SetCustomerID sets the "customer_id" field. +func (_c *LedgerCreditVoidRecordCreate) SetCustomerID(v string) *LedgerCreditVoidRecordCreate { + _c.mutation.SetCustomerID(v) + return _c +} + +// SetCurrency sets the "currency" field. +func (_c *LedgerCreditVoidRecordCreate) SetCurrency(v currencyx.Code) *LedgerCreditVoidRecordCreate { + _c.mutation.SetCurrency(v) + return _c +} + +// SetVoidedAt sets the "voided_at" field. +func (_c *LedgerCreditVoidRecordCreate) SetVoidedAt(v time.Time) *LedgerCreditVoidRecordCreate { + _c.mutation.SetVoidedAt(v) + return _c +} + +// SetSourceChargeID sets the "source_charge_id" field. +func (_c *LedgerCreditVoidRecordCreate) SetSourceChargeID(v string) *LedgerCreditVoidRecordCreate { + _c.mutation.SetSourceChargeID(v) + return _c +} + +// SetVoidTransactionGroupID sets the "void_transaction_group_id" field. +func (_c *LedgerCreditVoidRecordCreate) SetVoidTransactionGroupID(v string) *LedgerCreditVoidRecordCreate { + _c.mutation.SetVoidTransactionGroupID(v) + return _c +} + +// SetVoidTransactionID sets the "void_transaction_id" field. +func (_c *LedgerCreditVoidRecordCreate) SetVoidTransactionID(v string) *LedgerCreditVoidRecordCreate { + _c.mutation.SetVoidTransactionID(v) + return _c +} + +// SetFboSubAccountID sets the "fbo_sub_account_id" field. +func (_c *LedgerCreditVoidRecordCreate) SetFboSubAccountID(v string) *LedgerCreditVoidRecordCreate { + _c.mutation.SetFboSubAccountID(v) + return _c +} + +// SetReceivableSubAccountID sets the "receivable_sub_account_id" field. +func (_c *LedgerCreditVoidRecordCreate) SetReceivableSubAccountID(v string) *LedgerCreditVoidRecordCreate { + _c.mutation.SetReceivableSubAccountID(v) + return _c +} + +// SetID sets the "id" field. +func (_c *LedgerCreditVoidRecordCreate) SetID(v string) *LedgerCreditVoidRecordCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *LedgerCreditVoidRecordCreate) SetNillableID(v *string) *LedgerCreditVoidRecordCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the LedgerCreditVoidRecordMutation object of the builder. +func (_c *LedgerCreditVoidRecordCreate) Mutation() *LedgerCreditVoidRecordMutation { + return _c.mutation +} + +// Save creates the LedgerCreditVoidRecord in the database. +func (_c *LedgerCreditVoidRecordCreate) Save(ctx context.Context) (*LedgerCreditVoidRecord, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *LedgerCreditVoidRecordCreate) SaveX(ctx context.Context) *LedgerCreditVoidRecord { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *LedgerCreditVoidRecordCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *LedgerCreditVoidRecordCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *LedgerCreditVoidRecordCreate) defaults() { + if _, ok := _c.mutation.CreatedAt(); !ok { + v := ledgercreditvoidrecord.DefaultCreatedAt() + _c.mutation.SetCreatedAt(v) + } + if _, ok := _c.mutation.UpdatedAt(); !ok { + v := ledgercreditvoidrecord.DefaultUpdatedAt() + _c.mutation.SetUpdatedAt(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := ledgercreditvoidrecord.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *LedgerCreditVoidRecordCreate) check() error { + if _, ok := _c.mutation.Namespace(); !ok { + return &ValidationError{Name: "namespace", err: errors.New(`db: missing required field "LedgerCreditVoidRecord.namespace"`)} + } + if v, ok := _c.mutation.Namespace(); ok { + if err := ledgercreditvoidrecord.NamespaceValidator(v); err != nil { + return &ValidationError{Name: "namespace", err: fmt.Errorf(`db: validator failed for field "LedgerCreditVoidRecord.namespace": %w`, err)} + } + } + if _, ok := _c.mutation.CreatedAt(); !ok { + return &ValidationError{Name: "created_at", err: errors.New(`db: missing required field "LedgerCreditVoidRecord.created_at"`)} + } + if _, ok := _c.mutation.UpdatedAt(); !ok { + return &ValidationError{Name: "updated_at", err: errors.New(`db: missing required field "LedgerCreditVoidRecord.updated_at"`)} + } + if _, ok := _c.mutation.Amount(); !ok { + return &ValidationError{Name: "amount", err: errors.New(`db: missing required field "LedgerCreditVoidRecord.amount"`)} + } + if _, ok := _c.mutation.CustomerID(); !ok { + return &ValidationError{Name: "customer_id", err: errors.New(`db: missing required field "LedgerCreditVoidRecord.customer_id"`)} + } + if v, ok := _c.mutation.CustomerID(); ok { + if err := ledgercreditvoidrecord.CustomerIDValidator(v); err != nil { + return &ValidationError{Name: "customer_id", err: fmt.Errorf(`db: validator failed for field "LedgerCreditVoidRecord.customer_id": %w`, err)} + } + } + if _, ok := _c.mutation.Currency(); !ok { + return &ValidationError{Name: "currency", err: errors.New(`db: missing required field "LedgerCreditVoidRecord.currency"`)} + } + if v, ok := _c.mutation.Currency(); ok { + if err := ledgercreditvoidrecord.CurrencyValidator(string(v)); err != nil { + return &ValidationError{Name: "currency", err: fmt.Errorf(`db: validator failed for field "LedgerCreditVoidRecord.currency": %w`, err)} + } + } + if _, ok := _c.mutation.VoidedAt(); !ok { + return &ValidationError{Name: "voided_at", err: errors.New(`db: missing required field "LedgerCreditVoidRecord.voided_at"`)} + } + if _, ok := _c.mutation.SourceChargeID(); !ok { + return &ValidationError{Name: "source_charge_id", err: errors.New(`db: missing required field "LedgerCreditVoidRecord.source_charge_id"`)} + } + if v, ok := _c.mutation.SourceChargeID(); ok { + if err := ledgercreditvoidrecord.SourceChargeIDValidator(v); err != nil { + return &ValidationError{Name: "source_charge_id", err: fmt.Errorf(`db: validator failed for field "LedgerCreditVoidRecord.source_charge_id": %w`, err)} + } + } + if _, ok := _c.mutation.VoidTransactionGroupID(); !ok { + return &ValidationError{Name: "void_transaction_group_id", err: errors.New(`db: missing required field "LedgerCreditVoidRecord.void_transaction_group_id"`)} + } + if v, ok := _c.mutation.VoidTransactionGroupID(); ok { + if err := ledgercreditvoidrecord.VoidTransactionGroupIDValidator(v); err != nil { + return &ValidationError{Name: "void_transaction_group_id", err: fmt.Errorf(`db: validator failed for field "LedgerCreditVoidRecord.void_transaction_group_id": %w`, err)} + } + } + if _, ok := _c.mutation.VoidTransactionID(); !ok { + return &ValidationError{Name: "void_transaction_id", err: errors.New(`db: missing required field "LedgerCreditVoidRecord.void_transaction_id"`)} + } + if v, ok := _c.mutation.VoidTransactionID(); ok { + if err := ledgercreditvoidrecord.VoidTransactionIDValidator(v); err != nil { + return &ValidationError{Name: "void_transaction_id", err: fmt.Errorf(`db: validator failed for field "LedgerCreditVoidRecord.void_transaction_id": %w`, err)} + } + } + if _, ok := _c.mutation.FboSubAccountID(); !ok { + return &ValidationError{Name: "fbo_sub_account_id", err: errors.New(`db: missing required field "LedgerCreditVoidRecord.fbo_sub_account_id"`)} + } + if v, ok := _c.mutation.FboSubAccountID(); ok { + if err := ledgercreditvoidrecord.FboSubAccountIDValidator(v); err != nil { + return &ValidationError{Name: "fbo_sub_account_id", err: fmt.Errorf(`db: validator failed for field "LedgerCreditVoidRecord.fbo_sub_account_id": %w`, err)} + } + } + if _, ok := _c.mutation.ReceivableSubAccountID(); !ok { + return &ValidationError{Name: "receivable_sub_account_id", err: errors.New(`db: missing required field "LedgerCreditVoidRecord.receivable_sub_account_id"`)} + } + if v, ok := _c.mutation.ReceivableSubAccountID(); ok { + if err := ledgercreditvoidrecord.ReceivableSubAccountIDValidator(v); err != nil { + return &ValidationError{Name: "receivable_sub_account_id", err: fmt.Errorf(`db: validator failed for field "LedgerCreditVoidRecord.receivable_sub_account_id": %w`, err)} + } + } + return nil +} + +func (_c *LedgerCreditVoidRecordCreate) sqlSave(ctx context.Context) (*LedgerCreditVoidRecord, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(string); ok { + _node.ID = id + } else { + return nil, fmt.Errorf("unexpected LedgerCreditVoidRecord.ID type: %T", _spec.ID.Value) + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *LedgerCreditVoidRecordCreate) createSpec() (*LedgerCreditVoidRecord, *sqlgraph.CreateSpec) { + var ( + _node = &LedgerCreditVoidRecord{config: _c.config} + _spec = sqlgraph.NewCreateSpec(ledgercreditvoidrecord.Table, sqlgraph.NewFieldSpec(ledgercreditvoidrecord.FieldID, field.TypeString)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = id + } + if value, ok := _c.mutation.Namespace(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldNamespace, field.TypeString, value) + _node.Namespace = value + } + if value, ok := _c.mutation.Annotations(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldAnnotations, field.TypeJSON, value) + _node.Annotations = value + } + if value, ok := _c.mutation.CreatedAt(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldCreatedAt, field.TypeTime, value) + _node.CreatedAt = value + } + if value, ok := _c.mutation.UpdatedAt(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldUpdatedAt, field.TypeTime, value) + _node.UpdatedAt = value + } + if value, ok := _c.mutation.DeletedAt(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldDeletedAt, field.TypeTime, value) + _node.DeletedAt = &value + } + if value, ok := _c.mutation.Amount(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldAmount, field.TypeOther, value) + _node.Amount = value + } + if value, ok := _c.mutation.CustomerID(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldCustomerID, field.TypeString, value) + _node.CustomerID = value + } + if value, ok := _c.mutation.Currency(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldCurrency, field.TypeString, value) + _node.Currency = value + } + if value, ok := _c.mutation.VoidedAt(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldVoidedAt, field.TypeTime, value) + _node.VoidedAt = value + } + if value, ok := _c.mutation.SourceChargeID(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldSourceChargeID, field.TypeString, value) + _node.SourceChargeID = value + } + if value, ok := _c.mutation.VoidTransactionGroupID(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldVoidTransactionGroupID, field.TypeString, value) + _node.VoidTransactionGroupID = value + } + if value, ok := _c.mutation.VoidTransactionID(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldVoidTransactionID, field.TypeString, value) + _node.VoidTransactionID = value + } + if value, ok := _c.mutation.FboSubAccountID(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldFboSubAccountID, field.TypeString, value) + _node.FboSubAccountID = value + } + if value, ok := _c.mutation.ReceivableSubAccountID(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldReceivableSubAccountID, field.TypeString, value) + _node.ReceivableSubAccountID = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.LedgerCreditVoidRecord.Create(). +// SetNamespace(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.LedgerCreditVoidRecordUpsert) { +// SetNamespace(v+v). +// }). +// Exec(ctx) +func (_c *LedgerCreditVoidRecordCreate) OnConflict(opts ...sql.ConflictOption) *LedgerCreditVoidRecordUpsertOne { + _c.conflict = opts + return &LedgerCreditVoidRecordUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.LedgerCreditVoidRecord.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *LedgerCreditVoidRecordCreate) OnConflictColumns(columns ...string) *LedgerCreditVoidRecordUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &LedgerCreditVoidRecordUpsertOne{ + create: _c, + } +} + +type ( + // LedgerCreditVoidRecordUpsertOne is the builder for "upsert"-ing + // one LedgerCreditVoidRecord node. + LedgerCreditVoidRecordUpsertOne struct { + create *LedgerCreditVoidRecordCreate + } + + // LedgerCreditVoidRecordUpsert is the "OnConflict" setter. + LedgerCreditVoidRecordUpsert struct { + *sql.UpdateSet + } +) + +// SetAnnotations sets the "annotations" field. +func (u *LedgerCreditVoidRecordUpsert) SetAnnotations(v models.Annotations) *LedgerCreditVoidRecordUpsert { + u.Set(ledgercreditvoidrecord.FieldAnnotations, v) + return u +} + +// UpdateAnnotations sets the "annotations" field to the value that was provided on create. +func (u *LedgerCreditVoidRecordUpsert) UpdateAnnotations() *LedgerCreditVoidRecordUpsert { + u.SetExcluded(ledgercreditvoidrecord.FieldAnnotations) + return u +} + +// ClearAnnotations clears the value of the "annotations" field. +func (u *LedgerCreditVoidRecordUpsert) ClearAnnotations() *LedgerCreditVoidRecordUpsert { + u.SetNull(ledgercreditvoidrecord.FieldAnnotations) + return u +} + +// SetUpdatedAt sets the "updated_at" field. +func (u *LedgerCreditVoidRecordUpsert) SetUpdatedAt(v time.Time) *LedgerCreditVoidRecordUpsert { + u.Set(ledgercreditvoidrecord.FieldUpdatedAt, v) + return u +} + +// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create. +func (u *LedgerCreditVoidRecordUpsert) UpdateUpdatedAt() *LedgerCreditVoidRecordUpsert { + u.SetExcluded(ledgercreditvoidrecord.FieldUpdatedAt) + return u +} + +// SetDeletedAt sets the "deleted_at" field. +func (u *LedgerCreditVoidRecordUpsert) SetDeletedAt(v time.Time) *LedgerCreditVoidRecordUpsert { + u.Set(ledgercreditvoidrecord.FieldDeletedAt, v) + return u +} + +// UpdateDeletedAt sets the "deleted_at" field to the value that was provided on create. +func (u *LedgerCreditVoidRecordUpsert) UpdateDeletedAt() *LedgerCreditVoidRecordUpsert { + u.SetExcluded(ledgercreditvoidrecord.FieldDeletedAt) + return u +} + +// ClearDeletedAt clears the value of the "deleted_at" field. +func (u *LedgerCreditVoidRecordUpsert) ClearDeletedAt() *LedgerCreditVoidRecordUpsert { + u.SetNull(ledgercreditvoidrecord.FieldDeletedAt) + return u +} + +// SetAmount sets the "amount" field. +func (u *LedgerCreditVoidRecordUpsert) SetAmount(v alpacadecimal.Decimal) *LedgerCreditVoidRecordUpsert { + u.Set(ledgercreditvoidrecord.FieldAmount, v) + return u +} + +// UpdateAmount sets the "amount" field to the value that was provided on create. +func (u *LedgerCreditVoidRecordUpsert) UpdateAmount() *LedgerCreditVoidRecordUpsert { + u.SetExcluded(ledgercreditvoidrecord.FieldAmount) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.LedgerCreditVoidRecord.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(ledgercreditvoidrecord.FieldID) +// }), +// ). +// Exec(ctx) +func (u *LedgerCreditVoidRecordUpsertOne) UpdateNewValues() *LedgerCreditVoidRecordUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldID) + } + if _, exists := u.create.mutation.Namespace(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldNamespace) + } + if _, exists := u.create.mutation.CreatedAt(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldCreatedAt) + } + if _, exists := u.create.mutation.CustomerID(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldCustomerID) + } + if _, exists := u.create.mutation.Currency(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldCurrency) + } + if _, exists := u.create.mutation.VoidedAt(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldVoidedAt) + } + if _, exists := u.create.mutation.SourceChargeID(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldSourceChargeID) + } + if _, exists := u.create.mutation.VoidTransactionGroupID(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldVoidTransactionGroupID) + } + if _, exists := u.create.mutation.VoidTransactionID(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldVoidTransactionID) + } + if _, exists := u.create.mutation.FboSubAccountID(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldFboSubAccountID) + } + if _, exists := u.create.mutation.ReceivableSubAccountID(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldReceivableSubAccountID) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.LedgerCreditVoidRecord.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *LedgerCreditVoidRecordUpsertOne) Ignore() *LedgerCreditVoidRecordUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *LedgerCreditVoidRecordUpsertOne) DoNothing() *LedgerCreditVoidRecordUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the LedgerCreditVoidRecordCreate.OnConflict +// documentation for more info. +func (u *LedgerCreditVoidRecordUpsertOne) Update(set func(*LedgerCreditVoidRecordUpsert)) *LedgerCreditVoidRecordUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&LedgerCreditVoidRecordUpsert{UpdateSet: update}) + })) + return u +} + +// SetAnnotations sets the "annotations" field. +func (u *LedgerCreditVoidRecordUpsertOne) SetAnnotations(v models.Annotations) *LedgerCreditVoidRecordUpsertOne { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.SetAnnotations(v) + }) +} + +// UpdateAnnotations sets the "annotations" field to the value that was provided on create. +func (u *LedgerCreditVoidRecordUpsertOne) UpdateAnnotations() *LedgerCreditVoidRecordUpsertOne { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.UpdateAnnotations() + }) +} + +// ClearAnnotations clears the value of the "annotations" field. +func (u *LedgerCreditVoidRecordUpsertOne) ClearAnnotations() *LedgerCreditVoidRecordUpsertOne { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.ClearAnnotations() + }) +} + +// SetUpdatedAt sets the "updated_at" field. +func (u *LedgerCreditVoidRecordUpsertOne) SetUpdatedAt(v time.Time) *LedgerCreditVoidRecordUpsertOne { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.SetUpdatedAt(v) + }) +} + +// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create. +func (u *LedgerCreditVoidRecordUpsertOne) UpdateUpdatedAt() *LedgerCreditVoidRecordUpsertOne { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.UpdateUpdatedAt() + }) +} + +// SetDeletedAt sets the "deleted_at" field. +func (u *LedgerCreditVoidRecordUpsertOne) SetDeletedAt(v time.Time) *LedgerCreditVoidRecordUpsertOne { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.SetDeletedAt(v) + }) +} + +// UpdateDeletedAt sets the "deleted_at" field to the value that was provided on create. +func (u *LedgerCreditVoidRecordUpsertOne) UpdateDeletedAt() *LedgerCreditVoidRecordUpsertOne { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.UpdateDeletedAt() + }) +} + +// ClearDeletedAt clears the value of the "deleted_at" field. +func (u *LedgerCreditVoidRecordUpsertOne) ClearDeletedAt() *LedgerCreditVoidRecordUpsertOne { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.ClearDeletedAt() + }) +} + +// SetAmount sets the "amount" field. +func (u *LedgerCreditVoidRecordUpsertOne) SetAmount(v alpacadecimal.Decimal) *LedgerCreditVoidRecordUpsertOne { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.SetAmount(v) + }) +} + +// UpdateAmount sets the "amount" field to the value that was provided on create. +func (u *LedgerCreditVoidRecordUpsertOne) UpdateAmount() *LedgerCreditVoidRecordUpsertOne { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.UpdateAmount() + }) +} + +// Exec executes the query. +func (u *LedgerCreditVoidRecordUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("db: missing options for LedgerCreditVoidRecordCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *LedgerCreditVoidRecordUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *LedgerCreditVoidRecordUpsertOne) ID(ctx context.Context) (id string, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("db: LedgerCreditVoidRecordUpsertOne.ID is not supported by MySQL driver. Use LedgerCreditVoidRecordUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *LedgerCreditVoidRecordUpsertOne) IDX(ctx context.Context) string { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// LedgerCreditVoidRecordCreateBulk is the builder for creating many LedgerCreditVoidRecord entities in bulk. +type LedgerCreditVoidRecordCreateBulk struct { + config + err error + builders []*LedgerCreditVoidRecordCreate + conflict []sql.ConflictOption +} + +// Save creates the LedgerCreditVoidRecord entities in the database. +func (_c *LedgerCreditVoidRecordCreateBulk) Save(ctx context.Context) ([]*LedgerCreditVoidRecord, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*LedgerCreditVoidRecord, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*LedgerCreditVoidRecordMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *LedgerCreditVoidRecordCreateBulk) SaveX(ctx context.Context) []*LedgerCreditVoidRecord { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *LedgerCreditVoidRecordCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *LedgerCreditVoidRecordCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.LedgerCreditVoidRecord.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.LedgerCreditVoidRecordUpsert) { +// SetNamespace(v+v). +// }). +// Exec(ctx) +func (_c *LedgerCreditVoidRecordCreateBulk) OnConflict(opts ...sql.ConflictOption) *LedgerCreditVoidRecordUpsertBulk { + _c.conflict = opts + return &LedgerCreditVoidRecordUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.LedgerCreditVoidRecord.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *LedgerCreditVoidRecordCreateBulk) OnConflictColumns(columns ...string) *LedgerCreditVoidRecordUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &LedgerCreditVoidRecordUpsertBulk{ + create: _c, + } +} + +// LedgerCreditVoidRecordUpsertBulk is the builder for "upsert"-ing +// a bulk of LedgerCreditVoidRecord nodes. +type LedgerCreditVoidRecordUpsertBulk struct { + create *LedgerCreditVoidRecordCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.LedgerCreditVoidRecord.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(ledgercreditvoidrecord.FieldID) +// }), +// ). +// Exec(ctx) +func (u *LedgerCreditVoidRecordUpsertBulk) UpdateNewValues() *LedgerCreditVoidRecordUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldID) + } + if _, exists := b.mutation.Namespace(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldNamespace) + } + if _, exists := b.mutation.CreatedAt(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldCreatedAt) + } + if _, exists := b.mutation.CustomerID(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldCustomerID) + } + if _, exists := b.mutation.Currency(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldCurrency) + } + if _, exists := b.mutation.VoidedAt(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldVoidedAt) + } + if _, exists := b.mutation.SourceChargeID(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldSourceChargeID) + } + if _, exists := b.mutation.VoidTransactionGroupID(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldVoidTransactionGroupID) + } + if _, exists := b.mutation.VoidTransactionID(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldVoidTransactionID) + } + if _, exists := b.mutation.FboSubAccountID(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldFboSubAccountID) + } + if _, exists := b.mutation.ReceivableSubAccountID(); exists { + s.SetIgnore(ledgercreditvoidrecord.FieldReceivableSubAccountID) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.LedgerCreditVoidRecord.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *LedgerCreditVoidRecordUpsertBulk) Ignore() *LedgerCreditVoidRecordUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *LedgerCreditVoidRecordUpsertBulk) DoNothing() *LedgerCreditVoidRecordUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the LedgerCreditVoidRecordCreateBulk.OnConflict +// documentation for more info. +func (u *LedgerCreditVoidRecordUpsertBulk) Update(set func(*LedgerCreditVoidRecordUpsert)) *LedgerCreditVoidRecordUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&LedgerCreditVoidRecordUpsert{UpdateSet: update}) + })) + return u +} + +// SetAnnotations sets the "annotations" field. +func (u *LedgerCreditVoidRecordUpsertBulk) SetAnnotations(v models.Annotations) *LedgerCreditVoidRecordUpsertBulk { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.SetAnnotations(v) + }) +} + +// UpdateAnnotations sets the "annotations" field to the value that was provided on create. +func (u *LedgerCreditVoidRecordUpsertBulk) UpdateAnnotations() *LedgerCreditVoidRecordUpsertBulk { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.UpdateAnnotations() + }) +} + +// ClearAnnotations clears the value of the "annotations" field. +func (u *LedgerCreditVoidRecordUpsertBulk) ClearAnnotations() *LedgerCreditVoidRecordUpsertBulk { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.ClearAnnotations() + }) +} + +// SetUpdatedAt sets the "updated_at" field. +func (u *LedgerCreditVoidRecordUpsertBulk) SetUpdatedAt(v time.Time) *LedgerCreditVoidRecordUpsertBulk { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.SetUpdatedAt(v) + }) +} + +// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create. +func (u *LedgerCreditVoidRecordUpsertBulk) UpdateUpdatedAt() *LedgerCreditVoidRecordUpsertBulk { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.UpdateUpdatedAt() + }) +} + +// SetDeletedAt sets the "deleted_at" field. +func (u *LedgerCreditVoidRecordUpsertBulk) SetDeletedAt(v time.Time) *LedgerCreditVoidRecordUpsertBulk { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.SetDeletedAt(v) + }) +} + +// UpdateDeletedAt sets the "deleted_at" field to the value that was provided on create. +func (u *LedgerCreditVoidRecordUpsertBulk) UpdateDeletedAt() *LedgerCreditVoidRecordUpsertBulk { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.UpdateDeletedAt() + }) +} + +// ClearDeletedAt clears the value of the "deleted_at" field. +func (u *LedgerCreditVoidRecordUpsertBulk) ClearDeletedAt() *LedgerCreditVoidRecordUpsertBulk { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.ClearDeletedAt() + }) +} + +// SetAmount sets the "amount" field. +func (u *LedgerCreditVoidRecordUpsertBulk) SetAmount(v alpacadecimal.Decimal) *LedgerCreditVoidRecordUpsertBulk { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.SetAmount(v) + }) +} + +// UpdateAmount sets the "amount" field to the value that was provided on create. +func (u *LedgerCreditVoidRecordUpsertBulk) UpdateAmount() *LedgerCreditVoidRecordUpsertBulk { + return u.Update(func(s *LedgerCreditVoidRecordUpsert) { + s.UpdateAmount() + }) +} + +// Exec executes the query. +func (u *LedgerCreditVoidRecordUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("db: OnConflict was set for builder %d. Set it on the LedgerCreditVoidRecordCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("db: missing options for LedgerCreditVoidRecordCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *LedgerCreditVoidRecordUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/openmeter/ent/db/ledgercreditvoidrecord_delete.go b/openmeter/ent/db/ledgercreditvoidrecord_delete.go new file mode 100644 index 0000000000..456f2f2afc --- /dev/null +++ b/openmeter/ent/db/ledgercreditvoidrecord_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package db + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/openmeterio/openmeter/openmeter/ent/db/ledgercreditvoidrecord" + "github.com/openmeterio/openmeter/openmeter/ent/db/predicate" +) + +// LedgerCreditVoidRecordDelete is the builder for deleting a LedgerCreditVoidRecord entity. +type LedgerCreditVoidRecordDelete struct { + config + hooks []Hook + mutation *LedgerCreditVoidRecordMutation +} + +// Where appends a list predicates to the LedgerCreditVoidRecordDelete builder. +func (_d *LedgerCreditVoidRecordDelete) Where(ps ...predicate.LedgerCreditVoidRecord) *LedgerCreditVoidRecordDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *LedgerCreditVoidRecordDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *LedgerCreditVoidRecordDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *LedgerCreditVoidRecordDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(ledgercreditvoidrecord.Table, sqlgraph.NewFieldSpec(ledgercreditvoidrecord.FieldID, field.TypeString)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// LedgerCreditVoidRecordDeleteOne is the builder for deleting a single LedgerCreditVoidRecord entity. +type LedgerCreditVoidRecordDeleteOne struct { + _d *LedgerCreditVoidRecordDelete +} + +// Where appends a list predicates to the LedgerCreditVoidRecordDelete builder. +func (_d *LedgerCreditVoidRecordDeleteOne) Where(ps ...predicate.LedgerCreditVoidRecord) *LedgerCreditVoidRecordDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *LedgerCreditVoidRecordDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{ledgercreditvoidrecord.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *LedgerCreditVoidRecordDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/openmeter/ent/db/ledgercreditvoidrecord_query.go b/openmeter/ent/db/ledgercreditvoidrecord_query.go new file mode 100644 index 0000000000..5d1965e81e --- /dev/null +++ b/openmeter/ent/db/ledgercreditvoidrecord_query.go @@ -0,0 +1,564 @@ +// Code generated by ent, DO NOT EDIT. + +package db + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/openmeterio/openmeter/openmeter/ent/db/ledgercreditvoidrecord" + "github.com/openmeterio/openmeter/openmeter/ent/db/predicate" +) + +// LedgerCreditVoidRecordQuery is the builder for querying LedgerCreditVoidRecord entities. +type LedgerCreditVoidRecordQuery struct { + config + ctx *QueryContext + order []ledgercreditvoidrecord.OrderOption + inters []Interceptor + predicates []predicate.LedgerCreditVoidRecord + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the LedgerCreditVoidRecordQuery builder. +func (_q *LedgerCreditVoidRecordQuery) Where(ps ...predicate.LedgerCreditVoidRecord) *LedgerCreditVoidRecordQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *LedgerCreditVoidRecordQuery) Limit(limit int) *LedgerCreditVoidRecordQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *LedgerCreditVoidRecordQuery) Offset(offset int) *LedgerCreditVoidRecordQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *LedgerCreditVoidRecordQuery) Unique(unique bool) *LedgerCreditVoidRecordQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *LedgerCreditVoidRecordQuery) Order(o ...ledgercreditvoidrecord.OrderOption) *LedgerCreditVoidRecordQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first LedgerCreditVoidRecord entity from the query. +// Returns a *NotFoundError when no LedgerCreditVoidRecord was found. +func (_q *LedgerCreditVoidRecordQuery) First(ctx context.Context) (*LedgerCreditVoidRecord, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{ledgercreditvoidrecord.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *LedgerCreditVoidRecordQuery) FirstX(ctx context.Context) *LedgerCreditVoidRecord { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first LedgerCreditVoidRecord ID from the query. +// Returns a *NotFoundError when no LedgerCreditVoidRecord ID was found. +func (_q *LedgerCreditVoidRecordQuery) FirstID(ctx context.Context) (id string, err error) { + var ids []string + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{ledgercreditvoidrecord.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *LedgerCreditVoidRecordQuery) FirstIDX(ctx context.Context) string { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single LedgerCreditVoidRecord entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one LedgerCreditVoidRecord entity is found. +// Returns a *NotFoundError when no LedgerCreditVoidRecord entities are found. +func (_q *LedgerCreditVoidRecordQuery) Only(ctx context.Context) (*LedgerCreditVoidRecord, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{ledgercreditvoidrecord.Label} + default: + return nil, &NotSingularError{ledgercreditvoidrecord.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *LedgerCreditVoidRecordQuery) OnlyX(ctx context.Context) *LedgerCreditVoidRecord { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only LedgerCreditVoidRecord ID in the query. +// Returns a *NotSingularError when more than one LedgerCreditVoidRecord ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *LedgerCreditVoidRecordQuery) OnlyID(ctx context.Context) (id string, err error) { + var ids []string + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{ledgercreditvoidrecord.Label} + default: + err = &NotSingularError{ledgercreditvoidrecord.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *LedgerCreditVoidRecordQuery) OnlyIDX(ctx context.Context) string { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of LedgerCreditVoidRecords. +func (_q *LedgerCreditVoidRecordQuery) All(ctx context.Context) ([]*LedgerCreditVoidRecord, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*LedgerCreditVoidRecord, *LedgerCreditVoidRecordQuery]() + return withInterceptors[[]*LedgerCreditVoidRecord](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *LedgerCreditVoidRecordQuery) AllX(ctx context.Context) []*LedgerCreditVoidRecord { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of LedgerCreditVoidRecord IDs. +func (_q *LedgerCreditVoidRecordQuery) IDs(ctx context.Context) (ids []string, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(ledgercreditvoidrecord.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *LedgerCreditVoidRecordQuery) IDsX(ctx context.Context) []string { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *LedgerCreditVoidRecordQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*LedgerCreditVoidRecordQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *LedgerCreditVoidRecordQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *LedgerCreditVoidRecordQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("db: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *LedgerCreditVoidRecordQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the LedgerCreditVoidRecordQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *LedgerCreditVoidRecordQuery) Clone() *LedgerCreditVoidRecordQuery { + if _q == nil { + return nil + } + return &LedgerCreditVoidRecordQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]ledgercreditvoidrecord.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.LedgerCreditVoidRecord{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// Namespace string `json:"namespace,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.LedgerCreditVoidRecord.Query(). +// GroupBy(ledgercreditvoidrecord.FieldNamespace). +// Aggregate(db.Count()). +// Scan(ctx, &v) +func (_q *LedgerCreditVoidRecordQuery) GroupBy(field string, fields ...string) *LedgerCreditVoidRecordGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &LedgerCreditVoidRecordGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = ledgercreditvoidrecord.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// Namespace string `json:"namespace,omitempty"` +// } +// +// client.LedgerCreditVoidRecord.Query(). +// Select(ledgercreditvoidrecord.FieldNamespace). +// Scan(ctx, &v) +func (_q *LedgerCreditVoidRecordQuery) Select(fields ...string) *LedgerCreditVoidRecordSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &LedgerCreditVoidRecordSelect{LedgerCreditVoidRecordQuery: _q} + sbuild.label = ledgercreditvoidrecord.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a LedgerCreditVoidRecordSelect configured with the given aggregations. +func (_q *LedgerCreditVoidRecordQuery) Aggregate(fns ...AggregateFunc) *LedgerCreditVoidRecordSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *LedgerCreditVoidRecordQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("db: uninitialized interceptor (forgotten import db/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !ledgercreditvoidrecord.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("db: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *LedgerCreditVoidRecordQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*LedgerCreditVoidRecord, error) { + var ( + nodes = []*LedgerCreditVoidRecord{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*LedgerCreditVoidRecord).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &LedgerCreditVoidRecord{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *LedgerCreditVoidRecordQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *LedgerCreditVoidRecordQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(ledgercreditvoidrecord.Table, ledgercreditvoidrecord.Columns, sqlgraph.NewFieldSpec(ledgercreditvoidrecord.FieldID, field.TypeString)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, ledgercreditvoidrecord.FieldID) + for i := range fields { + if fields[i] != ledgercreditvoidrecord.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *LedgerCreditVoidRecordQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(ledgercreditvoidrecord.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = ledgercreditvoidrecord.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *LedgerCreditVoidRecordQuery) ForUpdate(opts ...sql.LockOption) *LedgerCreditVoidRecordQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *LedgerCreditVoidRecordQuery) ForShare(opts ...sql.LockOption) *LedgerCreditVoidRecordQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// LedgerCreditVoidRecordGroupBy is the group-by builder for LedgerCreditVoidRecord entities. +type LedgerCreditVoidRecordGroupBy struct { + selector + build *LedgerCreditVoidRecordQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *LedgerCreditVoidRecordGroupBy) Aggregate(fns ...AggregateFunc) *LedgerCreditVoidRecordGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *LedgerCreditVoidRecordGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*LedgerCreditVoidRecordQuery, *LedgerCreditVoidRecordGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *LedgerCreditVoidRecordGroupBy) sqlScan(ctx context.Context, root *LedgerCreditVoidRecordQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// LedgerCreditVoidRecordSelect is the builder for selecting fields of LedgerCreditVoidRecord entities. +type LedgerCreditVoidRecordSelect struct { + *LedgerCreditVoidRecordQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *LedgerCreditVoidRecordSelect) Aggregate(fns ...AggregateFunc) *LedgerCreditVoidRecordSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *LedgerCreditVoidRecordSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*LedgerCreditVoidRecordQuery, *LedgerCreditVoidRecordSelect](ctx, _s.LedgerCreditVoidRecordQuery, _s, _s.inters, v) +} + +func (_s *LedgerCreditVoidRecordSelect) sqlScan(ctx context.Context, root *LedgerCreditVoidRecordQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/openmeter/ent/db/ledgercreditvoidrecord_update.go b/openmeter/ent/db/ledgercreditvoidrecord_update.go new file mode 100644 index 0000000000..bbb4094860 --- /dev/null +++ b/openmeter/ent/db/ledgercreditvoidrecord_update.go @@ -0,0 +1,336 @@ +// Code generated by ent, DO NOT EDIT. + +package db + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/alpacahq/alpacadecimal" + "github.com/openmeterio/openmeter/openmeter/ent/db/ledgercreditvoidrecord" + "github.com/openmeterio/openmeter/openmeter/ent/db/predicate" + "github.com/openmeterio/openmeter/pkg/models" +) + +// LedgerCreditVoidRecordUpdate is the builder for updating LedgerCreditVoidRecord entities. +type LedgerCreditVoidRecordUpdate struct { + config + hooks []Hook + mutation *LedgerCreditVoidRecordMutation +} + +// Where appends a list predicates to the LedgerCreditVoidRecordUpdate builder. +func (_u *LedgerCreditVoidRecordUpdate) Where(ps ...predicate.LedgerCreditVoidRecord) *LedgerCreditVoidRecordUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetAnnotations sets the "annotations" field. +func (_u *LedgerCreditVoidRecordUpdate) SetAnnotations(v models.Annotations) *LedgerCreditVoidRecordUpdate { + _u.mutation.SetAnnotations(v) + return _u +} + +// ClearAnnotations clears the value of the "annotations" field. +func (_u *LedgerCreditVoidRecordUpdate) ClearAnnotations() *LedgerCreditVoidRecordUpdate { + _u.mutation.ClearAnnotations() + return _u +} + +// SetUpdatedAt sets the "updated_at" field. +func (_u *LedgerCreditVoidRecordUpdate) SetUpdatedAt(v time.Time) *LedgerCreditVoidRecordUpdate { + _u.mutation.SetUpdatedAt(v) + return _u +} + +// SetDeletedAt sets the "deleted_at" field. +func (_u *LedgerCreditVoidRecordUpdate) SetDeletedAt(v time.Time) *LedgerCreditVoidRecordUpdate { + _u.mutation.SetDeletedAt(v) + return _u +} + +// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil. +func (_u *LedgerCreditVoidRecordUpdate) SetNillableDeletedAt(v *time.Time) *LedgerCreditVoidRecordUpdate { + if v != nil { + _u.SetDeletedAt(*v) + } + return _u +} + +// ClearDeletedAt clears the value of the "deleted_at" field. +func (_u *LedgerCreditVoidRecordUpdate) ClearDeletedAt() *LedgerCreditVoidRecordUpdate { + _u.mutation.ClearDeletedAt() + return _u +} + +// SetAmount sets the "amount" field. +func (_u *LedgerCreditVoidRecordUpdate) SetAmount(v alpacadecimal.Decimal) *LedgerCreditVoidRecordUpdate { + _u.mutation.SetAmount(v) + return _u +} + +// SetNillableAmount sets the "amount" field if the given value is not nil. +func (_u *LedgerCreditVoidRecordUpdate) SetNillableAmount(v *alpacadecimal.Decimal) *LedgerCreditVoidRecordUpdate { + if v != nil { + _u.SetAmount(*v) + } + return _u +} + +// Mutation returns the LedgerCreditVoidRecordMutation object of the builder. +func (_u *LedgerCreditVoidRecordUpdate) Mutation() *LedgerCreditVoidRecordMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *LedgerCreditVoidRecordUpdate) Save(ctx context.Context) (int, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *LedgerCreditVoidRecordUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *LedgerCreditVoidRecordUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *LedgerCreditVoidRecordUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *LedgerCreditVoidRecordUpdate) defaults() { + if _, ok := _u.mutation.UpdatedAt(); !ok { + v := ledgercreditvoidrecord.UpdateDefaultUpdatedAt() + _u.mutation.SetUpdatedAt(v) + } +} + +func (_u *LedgerCreditVoidRecordUpdate) sqlSave(ctx context.Context) (_node int, err error) { + _spec := sqlgraph.NewUpdateSpec(ledgercreditvoidrecord.Table, ledgercreditvoidrecord.Columns, sqlgraph.NewFieldSpec(ledgercreditvoidrecord.FieldID, field.TypeString)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Annotations(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldAnnotations, field.TypeJSON, value) + } + if _u.mutation.AnnotationsCleared() { + _spec.ClearField(ledgercreditvoidrecord.FieldAnnotations, field.TypeJSON) + } + if value, ok := _u.mutation.UpdatedAt(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldUpdatedAt, field.TypeTime, value) + } + if value, ok := _u.mutation.DeletedAt(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldDeletedAt, field.TypeTime, value) + } + if _u.mutation.DeletedAtCleared() { + _spec.ClearField(ledgercreditvoidrecord.FieldDeletedAt, field.TypeTime) + } + if value, ok := _u.mutation.Amount(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldAmount, field.TypeOther, value) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{ledgercreditvoidrecord.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// LedgerCreditVoidRecordUpdateOne is the builder for updating a single LedgerCreditVoidRecord entity. +type LedgerCreditVoidRecordUpdateOne struct { + config + fields []string + hooks []Hook + mutation *LedgerCreditVoidRecordMutation +} + +// SetAnnotations sets the "annotations" field. +func (_u *LedgerCreditVoidRecordUpdateOne) SetAnnotations(v models.Annotations) *LedgerCreditVoidRecordUpdateOne { + _u.mutation.SetAnnotations(v) + return _u +} + +// ClearAnnotations clears the value of the "annotations" field. +func (_u *LedgerCreditVoidRecordUpdateOne) ClearAnnotations() *LedgerCreditVoidRecordUpdateOne { + _u.mutation.ClearAnnotations() + return _u +} + +// SetUpdatedAt sets the "updated_at" field. +func (_u *LedgerCreditVoidRecordUpdateOne) SetUpdatedAt(v time.Time) *LedgerCreditVoidRecordUpdateOne { + _u.mutation.SetUpdatedAt(v) + return _u +} + +// SetDeletedAt sets the "deleted_at" field. +func (_u *LedgerCreditVoidRecordUpdateOne) SetDeletedAt(v time.Time) *LedgerCreditVoidRecordUpdateOne { + _u.mutation.SetDeletedAt(v) + return _u +} + +// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil. +func (_u *LedgerCreditVoidRecordUpdateOne) SetNillableDeletedAt(v *time.Time) *LedgerCreditVoidRecordUpdateOne { + if v != nil { + _u.SetDeletedAt(*v) + } + return _u +} + +// ClearDeletedAt clears the value of the "deleted_at" field. +func (_u *LedgerCreditVoidRecordUpdateOne) ClearDeletedAt() *LedgerCreditVoidRecordUpdateOne { + _u.mutation.ClearDeletedAt() + return _u +} + +// SetAmount sets the "amount" field. +func (_u *LedgerCreditVoidRecordUpdateOne) SetAmount(v alpacadecimal.Decimal) *LedgerCreditVoidRecordUpdateOne { + _u.mutation.SetAmount(v) + return _u +} + +// SetNillableAmount sets the "amount" field if the given value is not nil. +func (_u *LedgerCreditVoidRecordUpdateOne) SetNillableAmount(v *alpacadecimal.Decimal) *LedgerCreditVoidRecordUpdateOne { + if v != nil { + _u.SetAmount(*v) + } + return _u +} + +// Mutation returns the LedgerCreditVoidRecordMutation object of the builder. +func (_u *LedgerCreditVoidRecordUpdateOne) Mutation() *LedgerCreditVoidRecordMutation { + return _u.mutation +} + +// Where appends a list predicates to the LedgerCreditVoidRecordUpdate builder. +func (_u *LedgerCreditVoidRecordUpdateOne) Where(ps ...predicate.LedgerCreditVoidRecord) *LedgerCreditVoidRecordUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *LedgerCreditVoidRecordUpdateOne) Select(field string, fields ...string) *LedgerCreditVoidRecordUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated LedgerCreditVoidRecord entity. +func (_u *LedgerCreditVoidRecordUpdateOne) Save(ctx context.Context) (*LedgerCreditVoidRecord, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *LedgerCreditVoidRecordUpdateOne) SaveX(ctx context.Context) *LedgerCreditVoidRecord { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *LedgerCreditVoidRecordUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *LedgerCreditVoidRecordUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *LedgerCreditVoidRecordUpdateOne) defaults() { + if _, ok := _u.mutation.UpdatedAt(); !ok { + v := ledgercreditvoidrecord.UpdateDefaultUpdatedAt() + _u.mutation.SetUpdatedAt(v) + } +} + +func (_u *LedgerCreditVoidRecordUpdateOne) sqlSave(ctx context.Context) (_node *LedgerCreditVoidRecord, err error) { + _spec := sqlgraph.NewUpdateSpec(ledgercreditvoidrecord.Table, ledgercreditvoidrecord.Columns, sqlgraph.NewFieldSpec(ledgercreditvoidrecord.FieldID, field.TypeString)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`db: missing "LedgerCreditVoidRecord.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, ledgercreditvoidrecord.FieldID) + for _, f := range fields { + if !ledgercreditvoidrecord.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("db: invalid field %q for query", f)} + } + if f != ledgercreditvoidrecord.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Annotations(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldAnnotations, field.TypeJSON, value) + } + if _u.mutation.AnnotationsCleared() { + _spec.ClearField(ledgercreditvoidrecord.FieldAnnotations, field.TypeJSON) + } + if value, ok := _u.mutation.UpdatedAt(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldUpdatedAt, field.TypeTime, value) + } + if value, ok := _u.mutation.DeletedAt(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldDeletedAt, field.TypeTime, value) + } + if _u.mutation.DeletedAtCleared() { + _spec.ClearField(ledgercreditvoidrecord.FieldDeletedAt, field.TypeTime) + } + if value, ok := _u.mutation.Amount(); ok { + _spec.SetField(ledgercreditvoidrecord.FieldAmount, field.TypeOther, value) + } + _node = &LedgerCreditVoidRecord{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{ledgercreditvoidrecord.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/openmeter/ent/db/migrate/schema.go b/openmeter/ent/db/migrate/schema.go index 7ca969c29a..8558b625b9 100644 --- a/openmeter/ent/db/migrate/schema.go +++ b/openmeter/ent/db/migrate/schema.go @@ -1650,6 +1650,7 @@ var ( {Name: "settlement", Type: field.TypeString, SchemaType: map[string]string{"postgres": "jsonb"}}, {Name: "status_detailed", Type: field.TypeEnum, Enums: []string{"created", "active", "active.initial_credit_grant", "active.payment.pending", "active.payment.authorized", "active.payment.paid_and_authorized", "active.payment.settled", "final", "deleted"}}, {Name: "key", Type: field.TypeString, Nullable: true}, + {Name: "voided_at", Type: field.TypeTime, Nullable: true}, {Name: "customer_id", Type: field.TypeString, SchemaType: map[string]string{"postgres": "char(26)"}}, {Name: "subscription_id", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "char(26)"}}, {Name: "subscription_item_id", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "char(26)"}}, @@ -1664,31 +1665,31 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "charge_credit_purchases_customers_charges_credit_purchase", - Columns: []*schema.Column{ChargeCreditPurchasesColumns[29]}, + Columns: []*schema.Column{ChargeCreditPurchasesColumns[30]}, RefColumns: []*schema.Column{CustomersColumns[0]}, OnDelete: schema.NoAction, }, { Symbol: "charge_credit_purchases_subscriptions_charges_credit_purchase", - Columns: []*schema.Column{ChargeCreditPurchasesColumns[30]}, + Columns: []*schema.Column{ChargeCreditPurchasesColumns[31]}, RefColumns: []*schema.Column{SubscriptionsColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "charge_credit_purchases_subscription_items_charges_credit_purchase", - Columns: []*schema.Column{ChargeCreditPurchasesColumns[31]}, + Columns: []*schema.Column{ChargeCreditPurchasesColumns[32]}, RefColumns: []*schema.Column{SubscriptionItemsColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "charge_credit_purchases_subscription_phases_charges_credit_purchase", - Columns: []*schema.Column{ChargeCreditPurchasesColumns[32]}, + Columns: []*schema.Column{ChargeCreditPurchasesColumns[33]}, RefColumns: []*schema.Column{SubscriptionPhasesColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "charge_credit_purchases_tax_codes_charge_credit_purchases", - Columns: []*schema.Column{ChargeCreditPurchasesColumns[33]}, + Columns: []*schema.Column{ChargeCreditPurchasesColumns[34]}, RefColumns: []*schema.Column{TaxCodesColumns[0]}, OnDelete: schema.NoAction, }, @@ -1697,7 +1698,7 @@ var ( { Name: "chargecreditpurchase_namespace_customer_id_unique_reference_id", Unique: true, - Columns: []*schema.Column{ChargeCreditPurchasesColumns[14], ChargeCreditPurchasesColumns[29], ChargeCreditPurchasesColumns[8]}, + Columns: []*schema.Column{ChargeCreditPurchasesColumns[14], ChargeCreditPurchasesColumns[30], ChargeCreditPurchasesColumns[8]}, Annotation: &entsql.IndexAnnotation{ Where: "unique_reference_id IS NOT NULL AND deleted_at IS NULL", }, @@ -1730,7 +1731,7 @@ var ( { Name: "chargecreditpurchases_tax_code_id", Unique: false, - Columns: []*schema.Column{ChargeCreditPurchasesColumns[33]}, + Columns: []*schema.Column{ChargeCreditPurchasesColumns[34]}, }, { Name: "chargecreditpurchase_namespace_key", @@ -3807,6 +3808,67 @@ var ( }, }, } + // LedgerCreditVoidRecordsColumns holds the columns for the "ledger_credit_void_records" table. + LedgerCreditVoidRecordsColumns = []*schema.Column{ + {Name: "id", Type: field.TypeString, Unique: true, SchemaType: map[string]string{"postgres": "char(26)"}}, + {Name: "namespace", Type: field.TypeString}, + {Name: "annotations", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, + {Name: "created_at", Type: field.TypeTime}, + {Name: "updated_at", Type: field.TypeTime}, + {Name: "deleted_at", Type: field.TypeTime, Nullable: true}, + {Name: "amount", Type: field.TypeOther, SchemaType: map[string]string{"postgres": "numeric"}}, + {Name: "customer_id", Type: field.TypeString, SchemaType: map[string]string{"postgres": "char(26)"}}, + {Name: "currency", Type: field.TypeString, SchemaType: map[string]string{"postgres": "varchar(3)"}}, + {Name: "voided_at", Type: field.TypeTime}, + {Name: "source_charge_id", Type: field.TypeString, SchemaType: map[string]string{"postgres": "char(26)"}}, + {Name: "void_transaction_group_id", Type: field.TypeString, SchemaType: map[string]string{"postgres": "char(26)"}}, + {Name: "void_transaction_id", Type: field.TypeString, SchemaType: map[string]string{"postgres": "char(26)"}}, + {Name: "fbo_sub_account_id", Type: field.TypeString, SchemaType: map[string]string{"postgres": "char(26)"}}, + {Name: "receivable_sub_account_id", Type: field.TypeString, SchemaType: map[string]string{"postgres": "char(26)"}}, + } + // LedgerCreditVoidRecordsTable holds the schema information for the "ledger_credit_void_records" table. + LedgerCreditVoidRecordsTable = &schema.Table{ + Name: "ledger_credit_void_records", + Columns: LedgerCreditVoidRecordsColumns, + PrimaryKey: []*schema.Column{LedgerCreditVoidRecordsColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "ledgercreditvoidrecord_id", + Unique: true, + Columns: []*schema.Column{LedgerCreditVoidRecordsColumns[0]}, + }, + { + Name: "ledgercreditvoidrecord_namespace", + Unique: false, + Columns: []*schema.Column{LedgerCreditVoidRecordsColumns[1]}, + }, + { + Name: "ledgercreditvoidrecord_annotations", + Unique: false, + Columns: []*schema.Column{LedgerCreditVoidRecordsColumns[2]}, + Annotation: &entsql.IndexAnnotation{ + Types: map[string]string{ + "postgres": "GIN", + }, + }, + }, + { + Name: "ledgercreditvoidrecord_namespace_customer_currency_voided", + Unique: false, + Columns: []*schema.Column{LedgerCreditVoidRecordsColumns[1], LedgerCreditVoidRecordsColumns[7], LedgerCreditVoidRecordsColumns[8], LedgerCreditVoidRecordsColumns[9], LedgerCreditVoidRecordsColumns[0]}, + }, + { + Name: "ledgercreditvoidrecord_namespace_source_charge_id", + Unique: false, + Columns: []*schema.Column{LedgerCreditVoidRecordsColumns[1], LedgerCreditVoidRecordsColumns[10]}, + }, + { + Name: "ledgercreditvoidrecord_namespace_void_transaction_group_id", + Unique: false, + Columns: []*schema.Column{LedgerCreditVoidRecordsColumns[1], LedgerCreditVoidRecordsColumns[11]}, + }, + }, + } // LedgerCustomerAccountsColumns holds the columns for the "ledger_customer_accounts" table. LedgerCustomerAccountsColumns = []*schema.Column{ {Name: "id", Type: field.TypeString, Unique: true, SchemaType: map[string]string{"postgres": "char(26)"}}, @@ -5423,6 +5485,7 @@ var ( LlmCostPricesTable, LedgerAccountsTable, LedgerBreakageRecordsTable, + LedgerCreditVoidRecordsTable, LedgerCustomerAccountsTable, LedgerEntriesTable, LedgerSubAccountsTable, diff --git a/openmeter/ent/db/mutation.go b/openmeter/ent/db/mutation.go index 0d6bac06cd..e8cecd860f 100644 --- a/openmeter/ent/db/mutation.go +++ b/openmeter/ent/db/mutation.go @@ -80,6 +80,7 @@ import ( dbgrant "github.com/openmeterio/openmeter/openmeter/ent/db/grant" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgeraccount" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgerbreakagerecord" + "github.com/openmeterio/openmeter/openmeter/ent/db/ledgercreditvoidrecord" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgercustomeraccount" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgerentry" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgersubaccount" @@ -185,6 +186,7 @@ const ( TypeLLMCostPrice = "LLMCostPrice" TypeLedgerAccount = "LedgerAccount" TypeLedgerBreakageRecord = "LedgerBreakageRecord" + TypeLedgerCreditVoidRecord = "LedgerCreditVoidRecord" TypeLedgerCustomerAccount = "LedgerCustomerAccount" TypeLedgerEntry = "LedgerEntry" TypeLedgerSubAccount = "LedgerSubAccount" @@ -36381,6 +36383,7 @@ type ChargeCreditPurchaseMutation struct { settlement *creditpurchase.Settlement status_detailed *creditpurchase.Status key *string + voided_at *time.Time clearedFields map[string]struct{} external_payment *string clearedexternal_payment bool @@ -37913,6 +37916,55 @@ func (m *ChargeCreditPurchaseMutation) ResetKey() { delete(m.clearedFields, chargecreditpurchase.FieldKey) } +// SetVoidedAt sets the "voided_at" field. +func (m *ChargeCreditPurchaseMutation) SetVoidedAt(t time.Time) { + m.voided_at = &t +} + +// VoidedAt returns the value of the "voided_at" field in the mutation. +func (m *ChargeCreditPurchaseMutation) VoidedAt() (r time.Time, exists bool) { + v := m.voided_at + if v == nil { + return + } + return *v, true +} + +// OldVoidedAt returns the old "voided_at" field's value of the ChargeCreditPurchase entity. +// If the ChargeCreditPurchase object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ChargeCreditPurchaseMutation) OldVoidedAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldVoidedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldVoidedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldVoidedAt: %w", err) + } + return oldValue.VoidedAt, nil +} + +// ClearVoidedAt clears the value of the "voided_at" field. +func (m *ChargeCreditPurchaseMutation) ClearVoidedAt() { + m.voided_at = nil + m.clearedFields[chargecreditpurchase.FieldVoidedAt] = struct{}{} +} + +// VoidedAtCleared returns if the "voided_at" field was cleared in this mutation. +func (m *ChargeCreditPurchaseMutation) VoidedAtCleared() bool { + _, ok := m.clearedFields[chargecreditpurchase.FieldVoidedAt] + return ok +} + +// ResetVoidedAt resets all changes to the "voided_at" field. +func (m *ChargeCreditPurchaseMutation) ResetVoidedAt() { + m.voided_at = nil + delete(m.clearedFields, chargecreditpurchase.FieldVoidedAt) +} + // SetExternalPaymentID sets the "external_payment" edge to the ChargeCreditPurchaseExternalPayment entity by id. func (m *ChargeCreditPurchaseMutation) SetExternalPaymentID(id string) { m.external_payment = &id @@ -38238,7 +38290,7 @@ func (m *ChargeCreditPurchaseMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *ChargeCreditPurchaseMutation) Fields() []string { - fields := make([]string, 0, 33) + fields := make([]string, 0, 34) if m.customer != nil { fields = append(fields, chargecreditpurchase.FieldCustomerID) } @@ -38338,6 +38390,9 @@ func (m *ChargeCreditPurchaseMutation) Fields() []string { if m.key != nil { fields = append(fields, chargecreditpurchase.FieldKey) } + if m.voided_at != nil { + fields = append(fields, chargecreditpurchase.FieldVoidedAt) + } return fields } @@ -38412,6 +38467,8 @@ func (m *ChargeCreditPurchaseMutation) Field(name string) (ent.Value, bool) { return m.StatusDetailed() case chargecreditpurchase.FieldKey: return m.Key() + case chargecreditpurchase.FieldVoidedAt: + return m.VoidedAt() } return nil, false } @@ -38487,6 +38544,8 @@ func (m *ChargeCreditPurchaseMutation) OldField(ctx context.Context, name string return m.OldStatusDetailed(ctx) case chargecreditpurchase.FieldKey: return m.OldKey(ctx) + case chargecreditpurchase.FieldVoidedAt: + return m.OldVoidedAt(ctx) } return nil, fmt.Errorf("unknown ChargeCreditPurchase field %s", name) } @@ -38727,6 +38786,13 @@ func (m *ChargeCreditPurchaseMutation) SetField(name string, value ent.Value) er } m.SetKey(v) return nil + case chargecreditpurchase.FieldVoidedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetVoidedAt(v) + return nil } return fmt.Errorf("unknown ChargeCreditPurchase field %s", name) } @@ -38817,6 +38883,9 @@ func (m *ChargeCreditPurchaseMutation) ClearedFields() []string { if m.FieldCleared(chargecreditpurchase.FieldKey) { fields = append(fields, chargecreditpurchase.FieldKey) } + if m.FieldCleared(chargecreditpurchase.FieldVoidedAt) { + fields = append(fields, chargecreditpurchase.FieldVoidedAt) + } return fields } @@ -38876,6 +38945,9 @@ func (m *ChargeCreditPurchaseMutation) ClearField(name string) error { case chargecreditpurchase.FieldKey: m.ClearKey() return nil + case chargecreditpurchase.FieldVoidedAt: + m.ClearVoidedAt() + return nil } return fmt.Errorf("unknown ChargeCreditPurchase nullable field %s", name) } @@ -38983,6 +39055,9 @@ func (m *ChargeCreditPurchaseMutation) ResetField(name string) error { case chargecreditpurchase.FieldKey: m.ResetKey() return nil + case chargecreditpurchase.FieldVoidedAt: + m.ResetVoidedAt() + return nil } return fmt.Errorf("unknown ChargeCreditPurchase field %s", name) } @@ -85408,6 +85483,1081 @@ func (m *LedgerBreakageRecordMutation) ResetEdge(name string) error { return fmt.Errorf("unknown LedgerBreakageRecord edge %s", name) } +// LedgerCreditVoidRecordMutation represents an operation that mutates the LedgerCreditVoidRecord nodes in the graph. +type LedgerCreditVoidRecordMutation struct { + config + op Op + typ string + id *string + namespace *string + annotations *models.Annotations + created_at *time.Time + updated_at *time.Time + deleted_at *time.Time + amount *alpacadecimal.Decimal + customer_id *string + currency *currencyx.Code + voided_at *time.Time + source_charge_id *string + void_transaction_group_id *string + void_transaction_id *string + fbo_sub_account_id *string + receivable_sub_account_id *string + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*LedgerCreditVoidRecord, error) + predicates []predicate.LedgerCreditVoidRecord +} + +var _ ent.Mutation = (*LedgerCreditVoidRecordMutation)(nil) + +// ledgercreditvoidrecordOption allows management of the mutation configuration using functional options. +type ledgercreditvoidrecordOption func(*LedgerCreditVoidRecordMutation) + +// newLedgerCreditVoidRecordMutation creates new mutation for the LedgerCreditVoidRecord entity. +func newLedgerCreditVoidRecordMutation(c config, op Op, opts ...ledgercreditvoidrecordOption) *LedgerCreditVoidRecordMutation { + m := &LedgerCreditVoidRecordMutation{ + config: c, + op: op, + typ: TypeLedgerCreditVoidRecord, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withLedgerCreditVoidRecordID sets the ID field of the mutation. +func withLedgerCreditVoidRecordID(id string) ledgercreditvoidrecordOption { + return func(m *LedgerCreditVoidRecordMutation) { + var ( + err error + once sync.Once + value *LedgerCreditVoidRecord + ) + m.oldValue = func(ctx context.Context) (*LedgerCreditVoidRecord, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().LedgerCreditVoidRecord.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withLedgerCreditVoidRecord sets the old LedgerCreditVoidRecord of the mutation. +func withLedgerCreditVoidRecord(node *LedgerCreditVoidRecord) ledgercreditvoidrecordOption { + return func(m *LedgerCreditVoidRecordMutation) { + m.oldValue = func(context.Context) (*LedgerCreditVoidRecord, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m LedgerCreditVoidRecordMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m LedgerCreditVoidRecordMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("db: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of LedgerCreditVoidRecord entities. +func (m *LedgerCreditVoidRecordMutation) SetID(id string) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *LedgerCreditVoidRecordMutation) ID() (id string, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *LedgerCreditVoidRecordMutation) IDs(ctx context.Context) ([]string, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []string{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().LedgerCreditVoidRecord.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetNamespace sets the "namespace" field. +func (m *LedgerCreditVoidRecordMutation) SetNamespace(s string) { + m.namespace = &s +} + +// Namespace returns the value of the "namespace" field in the mutation. +func (m *LedgerCreditVoidRecordMutation) Namespace() (r string, exists bool) { + v := m.namespace + if v == nil { + return + } + return *v, true +} + +// OldNamespace returns the old "namespace" field's value of the LedgerCreditVoidRecord entity. +// If the LedgerCreditVoidRecord object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *LedgerCreditVoidRecordMutation) OldNamespace(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldNamespace is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldNamespace requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldNamespace: %w", err) + } + return oldValue.Namespace, nil +} + +// ResetNamespace resets all changes to the "namespace" field. +func (m *LedgerCreditVoidRecordMutation) ResetNamespace() { + m.namespace = nil +} + +// SetAnnotations sets the "annotations" field. +func (m *LedgerCreditVoidRecordMutation) SetAnnotations(value models.Annotations) { + m.annotations = &value +} + +// Annotations returns the value of the "annotations" field in the mutation. +func (m *LedgerCreditVoidRecordMutation) Annotations() (r models.Annotations, exists bool) { + v := m.annotations + if v == nil { + return + } + return *v, true +} + +// OldAnnotations returns the old "annotations" field's value of the LedgerCreditVoidRecord entity. +// If the LedgerCreditVoidRecord object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *LedgerCreditVoidRecordMutation) OldAnnotations(ctx context.Context) (v models.Annotations, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAnnotations is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAnnotations requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAnnotations: %w", err) + } + return oldValue.Annotations, nil +} + +// ClearAnnotations clears the value of the "annotations" field. +func (m *LedgerCreditVoidRecordMutation) ClearAnnotations() { + m.annotations = nil + m.clearedFields[ledgercreditvoidrecord.FieldAnnotations] = struct{}{} +} + +// AnnotationsCleared returns if the "annotations" field was cleared in this mutation. +func (m *LedgerCreditVoidRecordMutation) AnnotationsCleared() bool { + _, ok := m.clearedFields[ledgercreditvoidrecord.FieldAnnotations] + return ok +} + +// ResetAnnotations resets all changes to the "annotations" field. +func (m *LedgerCreditVoidRecordMutation) ResetAnnotations() { + m.annotations = nil + delete(m.clearedFields, ledgercreditvoidrecord.FieldAnnotations) +} + +// SetCreatedAt sets the "created_at" field. +func (m *LedgerCreditVoidRecordMutation) SetCreatedAt(t time.Time) { + m.created_at = &t +} + +// CreatedAt returns the value of the "created_at" field in the mutation. +func (m *LedgerCreditVoidRecordMutation) CreatedAt() (r time.Time, exists bool) { + v := m.created_at + if v == nil { + return + } + return *v, true +} + +// OldCreatedAt returns the old "created_at" field's value of the LedgerCreditVoidRecord entity. +// If the LedgerCreditVoidRecord object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *LedgerCreditVoidRecordMutation) OldCreatedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedAt: %w", err) + } + return oldValue.CreatedAt, nil +} + +// ResetCreatedAt resets all changes to the "created_at" field. +func (m *LedgerCreditVoidRecordMutation) ResetCreatedAt() { + m.created_at = nil +} + +// SetUpdatedAt sets the "updated_at" field. +func (m *LedgerCreditVoidRecordMutation) SetUpdatedAt(t time.Time) { + m.updated_at = &t +} + +// UpdatedAt returns the value of the "updated_at" field in the mutation. +func (m *LedgerCreditVoidRecordMutation) UpdatedAt() (r time.Time, exists bool) { + v := m.updated_at + if v == nil { + return + } + return *v, true +} + +// OldUpdatedAt returns the old "updated_at" field's value of the LedgerCreditVoidRecord entity. +// If the LedgerCreditVoidRecord object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *LedgerCreditVoidRecordMutation) OldUpdatedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdatedAt: %w", err) + } + return oldValue.UpdatedAt, nil +} + +// ResetUpdatedAt resets all changes to the "updated_at" field. +func (m *LedgerCreditVoidRecordMutation) ResetUpdatedAt() { + m.updated_at = nil +} + +// SetDeletedAt sets the "deleted_at" field. +func (m *LedgerCreditVoidRecordMutation) SetDeletedAt(t time.Time) { + m.deleted_at = &t +} + +// DeletedAt returns the value of the "deleted_at" field in the mutation. +func (m *LedgerCreditVoidRecordMutation) DeletedAt() (r time.Time, exists bool) { + v := m.deleted_at + if v == nil { + return + } + return *v, true +} + +// OldDeletedAt returns the old "deleted_at" field's value of the LedgerCreditVoidRecord entity. +// If the LedgerCreditVoidRecord object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *LedgerCreditVoidRecordMutation) OldDeletedAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDeletedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDeletedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDeletedAt: %w", err) + } + return oldValue.DeletedAt, nil +} + +// ClearDeletedAt clears the value of the "deleted_at" field. +func (m *LedgerCreditVoidRecordMutation) ClearDeletedAt() { + m.deleted_at = nil + m.clearedFields[ledgercreditvoidrecord.FieldDeletedAt] = struct{}{} +} + +// DeletedAtCleared returns if the "deleted_at" field was cleared in this mutation. +func (m *LedgerCreditVoidRecordMutation) DeletedAtCleared() bool { + _, ok := m.clearedFields[ledgercreditvoidrecord.FieldDeletedAt] + return ok +} + +// ResetDeletedAt resets all changes to the "deleted_at" field. +func (m *LedgerCreditVoidRecordMutation) ResetDeletedAt() { + m.deleted_at = nil + delete(m.clearedFields, ledgercreditvoidrecord.FieldDeletedAt) +} + +// SetAmount sets the "amount" field. +func (m *LedgerCreditVoidRecordMutation) SetAmount(a alpacadecimal.Decimal) { + m.amount = &a +} + +// Amount returns the value of the "amount" field in the mutation. +func (m *LedgerCreditVoidRecordMutation) Amount() (r alpacadecimal.Decimal, exists bool) { + v := m.amount + if v == nil { + return + } + return *v, true +} + +// OldAmount returns the old "amount" field's value of the LedgerCreditVoidRecord entity. +// If the LedgerCreditVoidRecord object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *LedgerCreditVoidRecordMutation) OldAmount(ctx context.Context) (v alpacadecimal.Decimal, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAmount is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAmount requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAmount: %w", err) + } + return oldValue.Amount, nil +} + +// ResetAmount resets all changes to the "amount" field. +func (m *LedgerCreditVoidRecordMutation) ResetAmount() { + m.amount = nil +} + +// SetCustomerID sets the "customer_id" field. +func (m *LedgerCreditVoidRecordMutation) SetCustomerID(s string) { + m.customer_id = &s +} + +// CustomerID returns the value of the "customer_id" field in the mutation. +func (m *LedgerCreditVoidRecordMutation) CustomerID() (r string, exists bool) { + v := m.customer_id + if v == nil { + return + } + return *v, true +} + +// OldCustomerID returns the old "customer_id" field's value of the LedgerCreditVoidRecord entity. +// If the LedgerCreditVoidRecord object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *LedgerCreditVoidRecordMutation) OldCustomerID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCustomerID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCustomerID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCustomerID: %w", err) + } + return oldValue.CustomerID, nil +} + +// ResetCustomerID resets all changes to the "customer_id" field. +func (m *LedgerCreditVoidRecordMutation) ResetCustomerID() { + m.customer_id = nil +} + +// SetCurrency sets the "currency" field. +func (m *LedgerCreditVoidRecordMutation) SetCurrency(c currencyx.Code) { + m.currency = &c +} + +// Currency returns the value of the "currency" field in the mutation. +func (m *LedgerCreditVoidRecordMutation) Currency() (r currencyx.Code, exists bool) { + v := m.currency + if v == nil { + return + } + return *v, true +} + +// OldCurrency returns the old "currency" field's value of the LedgerCreditVoidRecord entity. +// If the LedgerCreditVoidRecord object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *LedgerCreditVoidRecordMutation) OldCurrency(ctx context.Context) (v currencyx.Code, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCurrency is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCurrency requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCurrency: %w", err) + } + return oldValue.Currency, nil +} + +// ResetCurrency resets all changes to the "currency" field. +func (m *LedgerCreditVoidRecordMutation) ResetCurrency() { + m.currency = nil +} + +// SetVoidedAt sets the "voided_at" field. +func (m *LedgerCreditVoidRecordMutation) SetVoidedAt(t time.Time) { + m.voided_at = &t +} + +// VoidedAt returns the value of the "voided_at" field in the mutation. +func (m *LedgerCreditVoidRecordMutation) VoidedAt() (r time.Time, exists bool) { + v := m.voided_at + if v == nil { + return + } + return *v, true +} + +// OldVoidedAt returns the old "voided_at" field's value of the LedgerCreditVoidRecord entity. +// If the LedgerCreditVoidRecord object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *LedgerCreditVoidRecordMutation) OldVoidedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldVoidedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldVoidedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldVoidedAt: %w", err) + } + return oldValue.VoidedAt, nil +} + +// ResetVoidedAt resets all changes to the "voided_at" field. +func (m *LedgerCreditVoidRecordMutation) ResetVoidedAt() { + m.voided_at = nil +} + +// SetSourceChargeID sets the "source_charge_id" field. +func (m *LedgerCreditVoidRecordMutation) SetSourceChargeID(s string) { + m.source_charge_id = &s +} + +// SourceChargeID returns the value of the "source_charge_id" field in the mutation. +func (m *LedgerCreditVoidRecordMutation) SourceChargeID() (r string, exists bool) { + v := m.source_charge_id + if v == nil { + return + } + return *v, true +} + +// OldSourceChargeID returns the old "source_charge_id" field's value of the LedgerCreditVoidRecord entity. +// If the LedgerCreditVoidRecord object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *LedgerCreditVoidRecordMutation) OldSourceChargeID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSourceChargeID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSourceChargeID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSourceChargeID: %w", err) + } + return oldValue.SourceChargeID, nil +} + +// ResetSourceChargeID resets all changes to the "source_charge_id" field. +func (m *LedgerCreditVoidRecordMutation) ResetSourceChargeID() { + m.source_charge_id = nil +} + +// SetVoidTransactionGroupID sets the "void_transaction_group_id" field. +func (m *LedgerCreditVoidRecordMutation) SetVoidTransactionGroupID(s string) { + m.void_transaction_group_id = &s +} + +// VoidTransactionGroupID returns the value of the "void_transaction_group_id" field in the mutation. +func (m *LedgerCreditVoidRecordMutation) VoidTransactionGroupID() (r string, exists bool) { + v := m.void_transaction_group_id + if v == nil { + return + } + return *v, true +} + +// OldVoidTransactionGroupID returns the old "void_transaction_group_id" field's value of the LedgerCreditVoidRecord entity. +// If the LedgerCreditVoidRecord object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *LedgerCreditVoidRecordMutation) OldVoidTransactionGroupID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldVoidTransactionGroupID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldVoidTransactionGroupID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldVoidTransactionGroupID: %w", err) + } + return oldValue.VoidTransactionGroupID, nil +} + +// ResetVoidTransactionGroupID resets all changes to the "void_transaction_group_id" field. +func (m *LedgerCreditVoidRecordMutation) ResetVoidTransactionGroupID() { + m.void_transaction_group_id = nil +} + +// SetVoidTransactionID sets the "void_transaction_id" field. +func (m *LedgerCreditVoidRecordMutation) SetVoidTransactionID(s string) { + m.void_transaction_id = &s +} + +// VoidTransactionID returns the value of the "void_transaction_id" field in the mutation. +func (m *LedgerCreditVoidRecordMutation) VoidTransactionID() (r string, exists bool) { + v := m.void_transaction_id + if v == nil { + return + } + return *v, true +} + +// OldVoidTransactionID returns the old "void_transaction_id" field's value of the LedgerCreditVoidRecord entity. +// If the LedgerCreditVoidRecord object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *LedgerCreditVoidRecordMutation) OldVoidTransactionID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldVoidTransactionID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldVoidTransactionID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldVoidTransactionID: %w", err) + } + return oldValue.VoidTransactionID, nil +} + +// ResetVoidTransactionID resets all changes to the "void_transaction_id" field. +func (m *LedgerCreditVoidRecordMutation) ResetVoidTransactionID() { + m.void_transaction_id = nil +} + +// SetFboSubAccountID sets the "fbo_sub_account_id" field. +func (m *LedgerCreditVoidRecordMutation) SetFboSubAccountID(s string) { + m.fbo_sub_account_id = &s +} + +// FboSubAccountID returns the value of the "fbo_sub_account_id" field in the mutation. +func (m *LedgerCreditVoidRecordMutation) FboSubAccountID() (r string, exists bool) { + v := m.fbo_sub_account_id + if v == nil { + return + } + return *v, true +} + +// OldFboSubAccountID returns the old "fbo_sub_account_id" field's value of the LedgerCreditVoidRecord entity. +// If the LedgerCreditVoidRecord object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *LedgerCreditVoidRecordMutation) OldFboSubAccountID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldFboSubAccountID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldFboSubAccountID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldFboSubAccountID: %w", err) + } + return oldValue.FboSubAccountID, nil +} + +// ResetFboSubAccountID resets all changes to the "fbo_sub_account_id" field. +func (m *LedgerCreditVoidRecordMutation) ResetFboSubAccountID() { + m.fbo_sub_account_id = nil +} + +// SetReceivableSubAccountID sets the "receivable_sub_account_id" field. +func (m *LedgerCreditVoidRecordMutation) SetReceivableSubAccountID(s string) { + m.receivable_sub_account_id = &s +} + +// ReceivableSubAccountID returns the value of the "receivable_sub_account_id" field in the mutation. +func (m *LedgerCreditVoidRecordMutation) ReceivableSubAccountID() (r string, exists bool) { + v := m.receivable_sub_account_id + if v == nil { + return + } + return *v, true +} + +// OldReceivableSubAccountID returns the old "receivable_sub_account_id" field's value of the LedgerCreditVoidRecord entity. +// If the LedgerCreditVoidRecord object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *LedgerCreditVoidRecordMutation) OldReceivableSubAccountID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldReceivableSubAccountID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldReceivableSubAccountID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldReceivableSubAccountID: %w", err) + } + return oldValue.ReceivableSubAccountID, nil +} + +// ResetReceivableSubAccountID resets all changes to the "receivable_sub_account_id" field. +func (m *LedgerCreditVoidRecordMutation) ResetReceivableSubAccountID() { + m.receivable_sub_account_id = nil +} + +// Where appends a list predicates to the LedgerCreditVoidRecordMutation builder. +func (m *LedgerCreditVoidRecordMutation) Where(ps ...predicate.LedgerCreditVoidRecord) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the LedgerCreditVoidRecordMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *LedgerCreditVoidRecordMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.LedgerCreditVoidRecord, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *LedgerCreditVoidRecordMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *LedgerCreditVoidRecordMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (LedgerCreditVoidRecord). +func (m *LedgerCreditVoidRecordMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *LedgerCreditVoidRecordMutation) Fields() []string { + fields := make([]string, 0, 14) + if m.namespace != nil { + fields = append(fields, ledgercreditvoidrecord.FieldNamespace) + } + if m.annotations != nil { + fields = append(fields, ledgercreditvoidrecord.FieldAnnotations) + } + if m.created_at != nil { + fields = append(fields, ledgercreditvoidrecord.FieldCreatedAt) + } + if m.updated_at != nil { + fields = append(fields, ledgercreditvoidrecord.FieldUpdatedAt) + } + if m.deleted_at != nil { + fields = append(fields, ledgercreditvoidrecord.FieldDeletedAt) + } + if m.amount != nil { + fields = append(fields, ledgercreditvoidrecord.FieldAmount) + } + if m.customer_id != nil { + fields = append(fields, ledgercreditvoidrecord.FieldCustomerID) + } + if m.currency != nil { + fields = append(fields, ledgercreditvoidrecord.FieldCurrency) + } + if m.voided_at != nil { + fields = append(fields, ledgercreditvoidrecord.FieldVoidedAt) + } + if m.source_charge_id != nil { + fields = append(fields, ledgercreditvoidrecord.FieldSourceChargeID) + } + if m.void_transaction_group_id != nil { + fields = append(fields, ledgercreditvoidrecord.FieldVoidTransactionGroupID) + } + if m.void_transaction_id != nil { + fields = append(fields, ledgercreditvoidrecord.FieldVoidTransactionID) + } + if m.fbo_sub_account_id != nil { + fields = append(fields, ledgercreditvoidrecord.FieldFboSubAccountID) + } + if m.receivable_sub_account_id != nil { + fields = append(fields, ledgercreditvoidrecord.FieldReceivableSubAccountID) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *LedgerCreditVoidRecordMutation) Field(name string) (ent.Value, bool) { + switch name { + case ledgercreditvoidrecord.FieldNamespace: + return m.Namespace() + case ledgercreditvoidrecord.FieldAnnotations: + return m.Annotations() + case ledgercreditvoidrecord.FieldCreatedAt: + return m.CreatedAt() + case ledgercreditvoidrecord.FieldUpdatedAt: + return m.UpdatedAt() + case ledgercreditvoidrecord.FieldDeletedAt: + return m.DeletedAt() + case ledgercreditvoidrecord.FieldAmount: + return m.Amount() + case ledgercreditvoidrecord.FieldCustomerID: + return m.CustomerID() + case ledgercreditvoidrecord.FieldCurrency: + return m.Currency() + case ledgercreditvoidrecord.FieldVoidedAt: + return m.VoidedAt() + case ledgercreditvoidrecord.FieldSourceChargeID: + return m.SourceChargeID() + case ledgercreditvoidrecord.FieldVoidTransactionGroupID: + return m.VoidTransactionGroupID() + case ledgercreditvoidrecord.FieldVoidTransactionID: + return m.VoidTransactionID() + case ledgercreditvoidrecord.FieldFboSubAccountID: + return m.FboSubAccountID() + case ledgercreditvoidrecord.FieldReceivableSubAccountID: + return m.ReceivableSubAccountID() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *LedgerCreditVoidRecordMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case ledgercreditvoidrecord.FieldNamespace: + return m.OldNamespace(ctx) + case ledgercreditvoidrecord.FieldAnnotations: + return m.OldAnnotations(ctx) + case ledgercreditvoidrecord.FieldCreatedAt: + return m.OldCreatedAt(ctx) + case ledgercreditvoidrecord.FieldUpdatedAt: + return m.OldUpdatedAt(ctx) + case ledgercreditvoidrecord.FieldDeletedAt: + return m.OldDeletedAt(ctx) + case ledgercreditvoidrecord.FieldAmount: + return m.OldAmount(ctx) + case ledgercreditvoidrecord.FieldCustomerID: + return m.OldCustomerID(ctx) + case ledgercreditvoidrecord.FieldCurrency: + return m.OldCurrency(ctx) + case ledgercreditvoidrecord.FieldVoidedAt: + return m.OldVoidedAt(ctx) + case ledgercreditvoidrecord.FieldSourceChargeID: + return m.OldSourceChargeID(ctx) + case ledgercreditvoidrecord.FieldVoidTransactionGroupID: + return m.OldVoidTransactionGroupID(ctx) + case ledgercreditvoidrecord.FieldVoidTransactionID: + return m.OldVoidTransactionID(ctx) + case ledgercreditvoidrecord.FieldFboSubAccountID: + return m.OldFboSubAccountID(ctx) + case ledgercreditvoidrecord.FieldReceivableSubAccountID: + return m.OldReceivableSubAccountID(ctx) + } + return nil, fmt.Errorf("unknown LedgerCreditVoidRecord field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *LedgerCreditVoidRecordMutation) SetField(name string, value ent.Value) error { + switch name { + case ledgercreditvoidrecord.FieldNamespace: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetNamespace(v) + return nil + case ledgercreditvoidrecord.FieldAnnotations: + v, ok := value.(models.Annotations) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAnnotations(v) + return nil + case ledgercreditvoidrecord.FieldCreatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedAt(v) + return nil + case ledgercreditvoidrecord.FieldUpdatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdatedAt(v) + return nil + case ledgercreditvoidrecord.FieldDeletedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDeletedAt(v) + return nil + case ledgercreditvoidrecord.FieldAmount: + v, ok := value.(alpacadecimal.Decimal) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAmount(v) + return nil + case ledgercreditvoidrecord.FieldCustomerID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCustomerID(v) + return nil + case ledgercreditvoidrecord.FieldCurrency: + v, ok := value.(currencyx.Code) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCurrency(v) + return nil + case ledgercreditvoidrecord.FieldVoidedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetVoidedAt(v) + return nil + case ledgercreditvoidrecord.FieldSourceChargeID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSourceChargeID(v) + return nil + case ledgercreditvoidrecord.FieldVoidTransactionGroupID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetVoidTransactionGroupID(v) + return nil + case ledgercreditvoidrecord.FieldVoidTransactionID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetVoidTransactionID(v) + return nil + case ledgercreditvoidrecord.FieldFboSubAccountID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetFboSubAccountID(v) + return nil + case ledgercreditvoidrecord.FieldReceivableSubAccountID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetReceivableSubAccountID(v) + return nil + } + return fmt.Errorf("unknown LedgerCreditVoidRecord field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *LedgerCreditVoidRecordMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *LedgerCreditVoidRecordMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *LedgerCreditVoidRecordMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown LedgerCreditVoidRecord numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *LedgerCreditVoidRecordMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(ledgercreditvoidrecord.FieldAnnotations) { + fields = append(fields, ledgercreditvoidrecord.FieldAnnotations) + } + if m.FieldCleared(ledgercreditvoidrecord.FieldDeletedAt) { + fields = append(fields, ledgercreditvoidrecord.FieldDeletedAt) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *LedgerCreditVoidRecordMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *LedgerCreditVoidRecordMutation) ClearField(name string) error { + switch name { + case ledgercreditvoidrecord.FieldAnnotations: + m.ClearAnnotations() + return nil + case ledgercreditvoidrecord.FieldDeletedAt: + m.ClearDeletedAt() + return nil + } + return fmt.Errorf("unknown LedgerCreditVoidRecord nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *LedgerCreditVoidRecordMutation) ResetField(name string) error { + switch name { + case ledgercreditvoidrecord.FieldNamespace: + m.ResetNamespace() + return nil + case ledgercreditvoidrecord.FieldAnnotations: + m.ResetAnnotations() + return nil + case ledgercreditvoidrecord.FieldCreatedAt: + m.ResetCreatedAt() + return nil + case ledgercreditvoidrecord.FieldUpdatedAt: + m.ResetUpdatedAt() + return nil + case ledgercreditvoidrecord.FieldDeletedAt: + m.ResetDeletedAt() + return nil + case ledgercreditvoidrecord.FieldAmount: + m.ResetAmount() + return nil + case ledgercreditvoidrecord.FieldCustomerID: + m.ResetCustomerID() + return nil + case ledgercreditvoidrecord.FieldCurrency: + m.ResetCurrency() + return nil + case ledgercreditvoidrecord.FieldVoidedAt: + m.ResetVoidedAt() + return nil + case ledgercreditvoidrecord.FieldSourceChargeID: + m.ResetSourceChargeID() + return nil + case ledgercreditvoidrecord.FieldVoidTransactionGroupID: + m.ResetVoidTransactionGroupID() + return nil + case ledgercreditvoidrecord.FieldVoidTransactionID: + m.ResetVoidTransactionID() + return nil + case ledgercreditvoidrecord.FieldFboSubAccountID: + m.ResetFboSubAccountID() + return nil + case ledgercreditvoidrecord.FieldReceivableSubAccountID: + m.ResetReceivableSubAccountID() + return nil + } + return fmt.Errorf("unknown LedgerCreditVoidRecord field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *LedgerCreditVoidRecordMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *LedgerCreditVoidRecordMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *LedgerCreditVoidRecordMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *LedgerCreditVoidRecordMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *LedgerCreditVoidRecordMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *LedgerCreditVoidRecordMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *LedgerCreditVoidRecordMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown LedgerCreditVoidRecord unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *LedgerCreditVoidRecordMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown LedgerCreditVoidRecord edge %s", name) +} + // LedgerCustomerAccountMutation represents an operation that mutates the LedgerCustomerAccount nodes in the graph. type LedgerCustomerAccountMutation struct { config diff --git a/openmeter/ent/db/paginate.go b/openmeter/ent/db/paginate.go index 6744730d8a..bd7a7c9ce5 100644 --- a/openmeter/ent/db/paginate.go +++ b/openmeter/ent/db/paginate.go @@ -3315,6 +3315,64 @@ func (_m *LedgerBreakageRecordQuery) Paginate(ctx context.Context, page paginati // type check var _ pagination.Paginator[*LedgerBreakageRecord] = (*LedgerBreakageRecordQuery)(nil) +// Paginate runs the query and returns a paginated response. +// If page is its 0 value then it will return all the items and populate the response page accordingly. +func (_m *LedgerCreditVoidRecordQuery) Paginate(ctx context.Context, page pagination.Page) (pagination.Result[*LedgerCreditVoidRecord], error) { + // Get the limit and offset + limit, offset := page.Limit(), page.Offset() + + // Unset previous pagination settings + zero := 0 + _m.ctx.Offset = &zero + _m.ctx.Limit = &zero + + // Create duplicate of the query to run for + countQuery := _m.Clone() + pagedQuery := _m + + // Unset select for count query + countQuery.ctx.Fields = []string{} + + // Unset ordering for count query + countQuery.order = nil + + pagedResponse := pagination.Result[*LedgerCreditVoidRecord]{ + Page: page, + } + + // Get the total count + count, err := countQuery.Count(ctx) + if err != nil { + return pagedResponse, fmt.Errorf("failed to get count: %w", err) + } + pagedResponse.TotalCount = count + + // If there are no items, return the empty response early + if count == 0 { + // Items should be [] not null. + pagedResponse.Items = make([]*LedgerCreditVoidRecord, 0) + return pagedResponse, nil + } + + // If page is its 0 value then return all the items + if page.IsZero() { + offset = 0 + limit = count + } + + // Set the limit and offset + pagedQuery.ctx.Limit = &limit + pagedQuery.ctx.Offset = &offset + + // Get the paged items + items, err := pagedQuery.All(ctx) + pagedResponse.Items = items + return pagedResponse, err +} + +// type check +var _ pagination.Paginator[*LedgerCreditVoidRecord] = (*LedgerCreditVoidRecordQuery)(nil) + // Paginate runs the query and returns a paginated response. // If page is its 0 value then it will return all the items and populate the response page accordingly. func (_m *LedgerCustomerAccountQuery) Paginate(ctx context.Context, page pagination.Page) (pagination.Result[*LedgerCustomerAccount], error) { diff --git a/openmeter/ent/db/predicate/predicate.go b/openmeter/ent/db/predicate/predicate.go index aebcf0d977..49f0762a83 100644 --- a/openmeter/ent/db/predicate/predicate.go +++ b/openmeter/ent/db/predicate/predicate.go @@ -342,6 +342,9 @@ type LedgerAccount func(*sql.Selector) // LedgerBreakageRecord is the predicate function for ledgerbreakagerecord builders. type LedgerBreakageRecord func(*sql.Selector) +// LedgerCreditVoidRecord is the predicate function for ledgercreditvoidrecord builders. +type LedgerCreditVoidRecord func(*sql.Selector) + // LedgerCustomerAccount is the predicate function for ledgercustomeraccount builders. type LedgerCustomerAccount func(*sql.Selector) diff --git a/openmeter/ent/db/runtime.go b/openmeter/ent/db/runtime.go index 0f17936a1d..f76ebd9611 100644 --- a/openmeter/ent/db/runtime.go +++ b/openmeter/ent/db/runtime.go @@ -65,6 +65,7 @@ import ( dbgrant "github.com/openmeterio/openmeter/openmeter/ent/db/grant" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgeraccount" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgerbreakagerecord" + "github.com/openmeterio/openmeter/openmeter/ent/db/ledgercreditvoidrecord" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgercustomeraccount" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgerentry" "github.com/openmeterio/openmeter/openmeter/ent/db/ledgersubaccount" @@ -2091,6 +2092,61 @@ func init() { ledgerbreakagerecordDescID := ledgerbreakagerecordMixinFields0[0].Descriptor() // ledgerbreakagerecord.DefaultID holds the default value on creation for the id field. ledgerbreakagerecord.DefaultID = ledgerbreakagerecordDescID.Default.(func() string) + ledgercreditvoidrecordMixin := schema.LedgerCreditVoidRecord{}.Mixin() + ledgercreditvoidrecordMixinFields0 := ledgercreditvoidrecordMixin[0].Fields() + _ = ledgercreditvoidrecordMixinFields0 + ledgercreditvoidrecordMixinFields1 := ledgercreditvoidrecordMixin[1].Fields() + _ = ledgercreditvoidrecordMixinFields1 + ledgercreditvoidrecordMixinFields3 := ledgercreditvoidrecordMixin[3].Fields() + _ = ledgercreditvoidrecordMixinFields3 + ledgercreditvoidrecordFields := schema.LedgerCreditVoidRecord{}.Fields() + _ = ledgercreditvoidrecordFields + // ledgercreditvoidrecordDescNamespace is the schema descriptor for namespace field. + ledgercreditvoidrecordDescNamespace := ledgercreditvoidrecordMixinFields1[0].Descriptor() + // ledgercreditvoidrecord.NamespaceValidator is a validator for the "namespace" field. It is called by the builders before save. + ledgercreditvoidrecord.NamespaceValidator = ledgercreditvoidrecordDescNamespace.Validators[0].(func(string) error) + // ledgercreditvoidrecordDescCreatedAt is the schema descriptor for created_at field. + ledgercreditvoidrecordDescCreatedAt := ledgercreditvoidrecordMixinFields3[0].Descriptor() + // ledgercreditvoidrecord.DefaultCreatedAt holds the default value on creation for the created_at field. + ledgercreditvoidrecord.DefaultCreatedAt = ledgercreditvoidrecordDescCreatedAt.Default.(func() time.Time) + // ledgercreditvoidrecordDescUpdatedAt is the schema descriptor for updated_at field. + ledgercreditvoidrecordDescUpdatedAt := ledgercreditvoidrecordMixinFields3[1].Descriptor() + // ledgercreditvoidrecord.DefaultUpdatedAt holds the default value on creation for the updated_at field. + ledgercreditvoidrecord.DefaultUpdatedAt = ledgercreditvoidrecordDescUpdatedAt.Default.(func() time.Time) + // ledgercreditvoidrecord.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field. + ledgercreditvoidrecord.UpdateDefaultUpdatedAt = ledgercreditvoidrecordDescUpdatedAt.UpdateDefault.(func() time.Time) + // ledgercreditvoidrecordDescCustomerID is the schema descriptor for customer_id field. + ledgercreditvoidrecordDescCustomerID := ledgercreditvoidrecordFields[1].Descriptor() + // ledgercreditvoidrecord.CustomerIDValidator is a validator for the "customer_id" field. It is called by the builders before save. + ledgercreditvoidrecord.CustomerIDValidator = ledgercreditvoidrecordDescCustomerID.Validators[0].(func(string) error) + // ledgercreditvoidrecordDescCurrency is the schema descriptor for currency field. + ledgercreditvoidrecordDescCurrency := ledgercreditvoidrecordFields[2].Descriptor() + // ledgercreditvoidrecord.CurrencyValidator is a validator for the "currency" field. It is called by the builders before save. + ledgercreditvoidrecord.CurrencyValidator = ledgercreditvoidrecordDescCurrency.Validators[0].(func(string) error) + // ledgercreditvoidrecordDescSourceChargeID is the schema descriptor for source_charge_id field. + ledgercreditvoidrecordDescSourceChargeID := ledgercreditvoidrecordFields[4].Descriptor() + // ledgercreditvoidrecord.SourceChargeIDValidator is a validator for the "source_charge_id" field. It is called by the builders before save. + ledgercreditvoidrecord.SourceChargeIDValidator = ledgercreditvoidrecordDescSourceChargeID.Validators[0].(func(string) error) + // ledgercreditvoidrecordDescVoidTransactionGroupID is the schema descriptor for void_transaction_group_id field. + ledgercreditvoidrecordDescVoidTransactionGroupID := ledgercreditvoidrecordFields[5].Descriptor() + // ledgercreditvoidrecord.VoidTransactionGroupIDValidator is a validator for the "void_transaction_group_id" field. It is called by the builders before save. + ledgercreditvoidrecord.VoidTransactionGroupIDValidator = ledgercreditvoidrecordDescVoidTransactionGroupID.Validators[0].(func(string) error) + // ledgercreditvoidrecordDescVoidTransactionID is the schema descriptor for void_transaction_id field. + ledgercreditvoidrecordDescVoidTransactionID := ledgercreditvoidrecordFields[6].Descriptor() + // ledgercreditvoidrecord.VoidTransactionIDValidator is a validator for the "void_transaction_id" field. It is called by the builders before save. + ledgercreditvoidrecord.VoidTransactionIDValidator = ledgercreditvoidrecordDescVoidTransactionID.Validators[0].(func(string) error) + // ledgercreditvoidrecordDescFboSubAccountID is the schema descriptor for fbo_sub_account_id field. + ledgercreditvoidrecordDescFboSubAccountID := ledgercreditvoidrecordFields[7].Descriptor() + // ledgercreditvoidrecord.FboSubAccountIDValidator is a validator for the "fbo_sub_account_id" field. It is called by the builders before save. + ledgercreditvoidrecord.FboSubAccountIDValidator = ledgercreditvoidrecordDescFboSubAccountID.Validators[0].(func(string) error) + // ledgercreditvoidrecordDescReceivableSubAccountID is the schema descriptor for receivable_sub_account_id field. + ledgercreditvoidrecordDescReceivableSubAccountID := ledgercreditvoidrecordFields[8].Descriptor() + // ledgercreditvoidrecord.ReceivableSubAccountIDValidator is a validator for the "receivable_sub_account_id" field. It is called by the builders before save. + ledgercreditvoidrecord.ReceivableSubAccountIDValidator = ledgercreditvoidrecordDescReceivableSubAccountID.Validators[0].(func(string) error) + // ledgercreditvoidrecordDescID is the schema descriptor for id field. + ledgercreditvoidrecordDescID := ledgercreditvoidrecordMixinFields0[0].Descriptor() + // ledgercreditvoidrecord.DefaultID holds the default value on creation for the id field. + ledgercreditvoidrecord.DefaultID = ledgercreditvoidrecordDescID.Default.(func() string) ledgercustomeraccountMixin := schema.LedgerCustomerAccount{}.Mixin() ledgercustomeraccountMixinFields0 := ledgercustomeraccountMixin[0].Fields() _ = ledgercustomeraccountMixinFields0 diff --git a/openmeter/ent/db/setorclear.go b/openmeter/ent/db/setorclear.go index 7449a05b41..d30ac522a5 100644 --- a/openmeter/ent/db/setorclear.go +++ b/openmeter/ent/db/setorclear.go @@ -2287,6 +2287,20 @@ func (u *ChargeCreditPurchaseUpdateOne) SetOrClearDescription(value *string) *Ch return u.SetDescription(*value) } +func (u *ChargeCreditPurchaseUpdate) SetOrClearVoidedAt(value *time.Time) *ChargeCreditPurchaseUpdate { + if value == nil { + return u.ClearVoidedAt() + } + return u.SetVoidedAt(*value) +} + +func (u *ChargeCreditPurchaseUpdateOne) SetOrClearVoidedAt(value *time.Time) *ChargeCreditPurchaseUpdateOne { + if value == nil { + return u.ClearVoidedAt() + } + return u.SetVoidedAt(*value) +} + func (u *ChargeCreditPurchaseCreditGrantUpdate) SetOrClearDeletedAt(value *time.Time) *ChargeCreditPurchaseCreditGrantUpdate { if value == nil { return u.ClearDeletedAt() @@ -4387,6 +4401,34 @@ func (u *LedgerBreakageRecordUpdateOne) SetOrClearDeletedAt(value *time.Time) *L return u.SetDeletedAt(*value) } +func (u *LedgerCreditVoidRecordUpdate) SetOrClearAnnotations(value *models.Annotations) *LedgerCreditVoidRecordUpdate { + if value == nil { + return u.ClearAnnotations() + } + return u.SetAnnotations(*value) +} + +func (u *LedgerCreditVoidRecordUpdateOne) SetOrClearAnnotations(value *models.Annotations) *LedgerCreditVoidRecordUpdateOne { + if value == nil { + return u.ClearAnnotations() + } + return u.SetAnnotations(*value) +} + +func (u *LedgerCreditVoidRecordUpdate) SetOrClearDeletedAt(value *time.Time) *LedgerCreditVoidRecordUpdate { + if value == nil { + return u.ClearDeletedAt() + } + return u.SetDeletedAt(*value) +} + +func (u *LedgerCreditVoidRecordUpdateOne) SetOrClearDeletedAt(value *time.Time) *LedgerCreditVoidRecordUpdateOne { + if value == nil { + return u.ClearDeletedAt() + } + return u.SetDeletedAt(*value) +} + func (u *LedgerCustomerAccountUpdate) SetOrClearDeletedAt(value *time.Time) *LedgerCustomerAccountUpdate { if value == nil { return u.ClearDeletedAt() diff --git a/openmeter/ent/db/tx.go b/openmeter/ent/db/tx.go index c00c441369..8fa58057f6 100644 --- a/openmeter/ent/db/tx.go +++ b/openmeter/ent/db/tx.go @@ -128,6 +128,8 @@ type Tx struct { LedgerAccount *LedgerAccountClient // LedgerBreakageRecord is the client for interacting with the LedgerBreakageRecord builders. LedgerBreakageRecord *LedgerBreakageRecordClient + // LedgerCreditVoidRecord is the client for interacting with the LedgerCreditVoidRecord builders. + LedgerCreditVoidRecord *LedgerCreditVoidRecordClient // LedgerCustomerAccount is the client for interacting with the LedgerCustomerAccount builders. LedgerCustomerAccount *LedgerCustomerAccountClient // LedgerEntry is the client for interacting with the LedgerEntry builders. @@ -366,6 +368,7 @@ func (tx *Tx) init() { tx.LLMCostPrice = NewLLMCostPriceClient(tx.config) tx.LedgerAccount = NewLedgerAccountClient(tx.config) tx.LedgerBreakageRecord = NewLedgerBreakageRecordClient(tx.config) + tx.LedgerCreditVoidRecord = NewLedgerCreditVoidRecordClient(tx.config) tx.LedgerCustomerAccount = NewLedgerCustomerAccountClient(tx.config) tx.LedgerEntry = NewLedgerEntryClient(tx.config) tx.LedgerSubAccount = NewLedgerSubAccountClient(tx.config) diff --git a/openmeter/ent/schema/chargescreditpurchase.go b/openmeter/ent/schema/chargescreditpurchase.go index 91d2782897..f36a1760b2 100644 --- a/openmeter/ent/schema/chargescreditpurchase.go +++ b/openmeter/ent/schema/chargescreditpurchase.go @@ -68,6 +68,10 @@ func (ChargeCreditPurchase) Fields() []ent.Field { Optional(). Nillable(). Immutable(), + + field.Time("voided_at"). + Optional(). + Nillable(), } } diff --git a/openmeter/ent/schema/ledger_credit_void_record.go b/openmeter/ent/schema/ledger_credit_void_record.go new file mode 100644 index 0000000000..d1eeb94378 --- /dev/null +++ b/openmeter/ent/schema/ledger_credit_void_record.go @@ -0,0 +1,90 @@ +package schema + +import ( + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/alpacahq/alpacadecimal" + + "github.com/openmeterio/openmeter/pkg/currencyx" + "github.com/openmeterio/openmeter/pkg/framework/entutils" +) + +type LedgerCreditVoidRecord struct { + ent.Schema +} + +// Credit void records are projection rows for customer-visible void events. +// Ledger entries remain the accounting source of truth. +func (LedgerCreditVoidRecord) Mixin() []ent.Mixin { + return []ent.Mixin{ + entutils.IDMixin{}, + entutils.NamespaceMixin{}, + entutils.AnnotationsMixin{}, + entutils.TimeMixin{}, + } +} + +func (LedgerCreditVoidRecord) Fields() []ent.Field { + return []ent.Field{ + field.Other("amount", alpacadecimal.Decimal{}). + SchemaType(map[string]string{ + dialect.Postgres: "numeric", + }), + field.String("customer_id"). + SchemaType(map[string]string{ + dialect.Postgres: "char(26)", + }). + NotEmpty(). + Immutable(), + field.String("currency"). + GoType(currencyx.Code("")). + NotEmpty(). + Immutable(). + SchemaType(map[string]string{ + dialect.Postgres: "varchar(3)", + }), + field.Time("voided_at"). + Immutable(), + field.String("source_charge_id"). + SchemaType(map[string]string{ + dialect.Postgres: "char(26)", + }). + NotEmpty(). + Immutable(), + field.String("void_transaction_group_id"). + SchemaType(map[string]string{ + dialect.Postgres: "char(26)", + }). + NotEmpty(). + Immutable(), + field.String("void_transaction_id"). + SchemaType(map[string]string{ + dialect.Postgres: "char(26)", + }). + NotEmpty(). + Immutable(), + field.String("fbo_sub_account_id"). + SchemaType(map[string]string{ + dialect.Postgres: "char(26)", + }). + NotEmpty(). + Immutable(), + field.String("receivable_sub_account_id"). + SchemaType(map[string]string{ + dialect.Postgres: "char(26)", + }). + NotEmpty(). + Immutable(), + } +} + +func (LedgerCreditVoidRecord) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("namespace", "customer_id", "currency", "voided_at", "id"). + StorageKey("ledgercreditvoidrecord_namespace_customer_currency_voided"), + index.Fields("namespace", "source_charge_id"), + index.Fields("namespace", "void_transaction_group_id"), + } +} diff --git a/openmeter/ledger/breakage/breakage_impacts.go b/openmeter/ledger/breakage/breakage_impacts.go index d003c54f64..6b7655f553 100644 --- a/openmeter/ledger/breakage/breakage_impacts.go +++ b/openmeter/ledger/breakage/breakage_impacts.go @@ -137,6 +137,7 @@ func (s *service) ListExpiredBreakageImpacts(ctx context.Context, input ListExpi CustomerID: input.CustomerID, Currency: group.currency, Amount: group.amount.Neg(), + SourceKind: SourceKindCreditPurchase, Annotations: annotations, } diff --git a/openmeter/ledger/breakage/breakage_impacts_test.go b/openmeter/ledger/breakage/breakage_impacts_test.go index 9864c175ca..0b91e70778 100644 --- a/openmeter/ledger/breakage/breakage_impacts_test.go +++ b/openmeter/ledger/breakage/breakage_impacts_test.go @@ -23,17 +23,19 @@ func TestListExpiredBreakageImpactsGroupsBySourceChargeID(t *testing.T) { currency := currencyx.Code("USD") chargeA := "charge-a" chargeB := "charge-b" + planAID := "01KBREAKAGE000000000000000001" svc := &service{ adapter: fakeBreakageAdapter{ expiredRecords: []Record{ { - ID: models.NamespacedID{Namespace: "ns", ID: "01KBREAKAGE000000000000000001"}, + ID: models.NamespacedID{Namespace: "ns", ID: planAID}, Kind: ledger.BreakageKindPlan, Amount: alpacadecimal.NewFromInt(10), CustomerID: customerID, Currency: currency, ExpiresAt: expiresAt, + SourceKind: SourceKindCreditPurchase, SourceChargeID: &chargeA, }, { @@ -43,7 +45,9 @@ func TestListExpiredBreakageImpactsGroupsBySourceChargeID(t *testing.T) { CustomerID: customerID, Currency: currency, ExpiresAt: expiresAt, + SourceKind: SourceKindUsage, SourceChargeID: &chargeA, + PlanID: &planAID, }, { ID: models.NamespacedID{Namespace: "ns", ID: "01KBREAKAGE000000000000000003"}, @@ -52,6 +56,7 @@ func TestListExpiredBreakageImpactsGroupsBySourceChargeID(t *testing.T) { CustomerID: customerID, Currency: currency, ExpiresAt: expiresAt, + SourceKind: SourceKindCreditPurchase, SourceChargeID: &chargeB, }, }, diff --git a/openmeter/ledger/breakage/service.go b/openmeter/ledger/breakage/service.go index a33875bd7a..d667a2c0ff 100644 --- a/openmeter/ledger/breakage/service.go +++ b/openmeter/ledger/breakage/service.go @@ -208,7 +208,7 @@ func (i ReleasePlanInput) Validate() error { } switch i.SourceKind { - case SourceKindUsage, SourceKindUsageCorrection, SourceKindCreditPurchaseCorrection, SourceKindAdvanceBackfill: + case SourceKindCreditPurchase, SourceKindUsage, SourceKindUsageCorrection, SourceKindCreditPurchaseCorrection, SourceKindAdvanceBackfill: default: errs = append(errs, fmt.Errorf("invalid release source kind: %s", i.SourceKind)) } diff --git a/openmeter/ledger/breakage/types.go b/openmeter/ledger/breakage/types.go index c00a33207c..6993d7200b 100644 --- a/openmeter/ledger/breakage/types.go +++ b/openmeter/ledger/breakage/types.go @@ -155,6 +155,7 @@ type BreakageImpact struct { CustomerID customer.CustomerID Currency currencyx.Code Amount alpacadecimal.Decimal + SourceKind SourceKind Annotations models.Annotations } diff --git a/openmeter/ledger/creditvoid/adapter/adapter.go b/openmeter/ledger/creditvoid/adapter/adapter.go new file mode 100644 index 0000000000..207906093c --- /dev/null +++ b/openmeter/ledger/creditvoid/adapter/adapter.go @@ -0,0 +1,62 @@ +package adapter + +import ( + "context" + "database/sql" + "errors" + "fmt" + + entdb "github.com/openmeterio/openmeter/openmeter/ent/db" + "github.com/openmeterio/openmeter/openmeter/ledger/creditvoid" + "github.com/openmeterio/openmeter/pkg/framework/entutils" + "github.com/openmeterio/openmeter/pkg/framework/transaction" +) + +type Config struct { + Client *entdb.Client +} + +func (c Config) Validate() error { + if c.Client == nil { + return errors.New("ent client is required") + } + + return nil +} + +func New(config Config) (creditvoid.Adapter, error) { + if err := config.Validate(); err != nil { + return nil, err + } + + return &adapter{ + db: config.Client, + }, nil +} + +type adapter struct { + db *entdb.Client +} + +func (a *adapter) Tx(ctx context.Context) (context.Context, transaction.Driver, error) { + txCtx, rawConfig, eDriver, err := a.db.HijackTx(ctx, &sql.TxOptions{ + ReadOnly: false, + }) + if err != nil { + return nil, nil, fmt.Errorf("failed to hijack transaction: %w", err) + } + + return txCtx, entutils.NewTxDriver(eDriver, rawConfig), nil +} + +func (a *adapter) WithTx(ctx context.Context, tx *entutils.TxDriver) *adapter { + txDB := entdb.NewTxClientFromRawConfig(ctx, *tx.GetConfig()) + + return &adapter{ + db: txDB.Client(), + } +} + +func (a *adapter) Self() *adapter { + return a +} diff --git a/openmeter/ledger/creditvoid/adapter/query.go b/openmeter/ledger/creditvoid/adapter/query.go new file mode 100644 index 0000000000..50abcbd456 --- /dev/null +++ b/openmeter/ledger/creditvoid/adapter/query.go @@ -0,0 +1,133 @@ +package adapter + +import ( + "entgo.io/ent/dialect" + sql "entgo.io/ent/dialect/sql" + "github.com/lib/pq" + + dbledgercreditvoidrecord "github.com/openmeterio/openmeter/openmeter/ent/db/ledgercreditvoidrecord" + ledgersubaccountdb "github.com/openmeterio/openmeter/openmeter/ent/db/ledgersubaccount" + ledgersubaccountroutedb "github.com/openmeterio/openmeter/openmeter/ent/db/ledgersubaccountroute" + "github.com/openmeterio/openmeter/openmeter/ent/db/predicate" + "github.com/openmeterio/openmeter/openmeter/ledger" +) + +func voidRecordRoutePredicate(route ledger.RouteFilter) predicate.LedgerCreditVoidRecord { + if route.Currency == "" && route.Features.IsAbsent() && route.MatchFeature == "" { + return nil + } + + return func(s *sql.Selector) { + s.Where(voidRecordRouteQuery{Route: route}.predicate(s.C(dbledgercreditvoidrecord.FieldFboSubAccountID))) + } +} + +type voidRecordRouteQuery struct { + Route ledger.RouteFilter +} + +func (q voidRecordRouteQuery) predicate(fboSubAccountIDColumn string) *sql.Predicate { + return sql.In(fboSubAccountIDColumn, q.selector()) +} + +func (q voidRecordRouteQuery) SQL() (string, []any) { + selector := q.selector() + selector.SetDialect(dialect.Postgres) + + return selector.Query() +} + +func (q voidRecordRouteQuery) selector() *sql.Selector { + const ( + subAccountTableAlias = "lsa" + routeTableAlias = "lsar" + ) + + subAccounts := sql.Table(ledgersubaccountdb.Table).As(subAccountTableAlias) + routes := sql.Table(ledgersubaccountroutedb.Table).As(routeTableAlias) + + selector := sql.Select(subAccounts.C(ledgersubaccountdb.FieldID)). + From(subAccounts). + Join(routes). + On(subAccounts.C(ledgersubaccountdb.FieldRouteID), routes.C(ledgersubaccountroutedb.FieldID)) + + for _, predicate := range q.selectorPredicates(routes.C, routeTableAlias) { + selector.Where(predicate) + } + + return selector +} + +func (q voidRecordRouteQuery) selectorPredicates(routeColumn func(string) string, routeTableAlias string) []*sql.Predicate { + predicates := make([]*sql.Predicate, 0, 3) + + if q.Route.Currency != "" { + predicates = append(predicates, sql.EQ(routeColumn(ledgersubaccountroutedb.FieldCurrency), string(q.Route.Currency))) + } + + if q.Route.Features.IsPresent() { + features, _ := q.Route.Features.Get() + features = ledger.SortedFeatures(features) + if len(features) == 0 { + predicates = append(predicates, sql.IsNull(routeColumn(ledgersubaccountroutedb.FieldFeatures))) + } else { + predicates = append(predicates, postgresArrayRouteExpression{ + Column: postgresQualifiedColumn{ + TableAlias: routeTableAlias, + Field: ledgersubaccountroutedb.FieldFeatures, + }, + Operator: postgresArrayRouteOperatorEqual, + Value: pq.StringArray(features), + }.predicate()) + } + } + + if q.Route.MatchFeature != "" { + predicates = append(predicates, sql.Or( + sql.IsNull(routeColumn(ledgersubaccountroutedb.FieldFeatures)), + postgresArrayRouteExpression{ + Column: postgresQualifiedColumn{ + TableAlias: routeTableAlias, + Field: ledgersubaccountroutedb.FieldFeatures, + }, + Operator: postgresArrayRouteOperatorContains, + Value: pq.StringArray{q.Route.MatchFeature}, + }.predicate(), + )) + } + + return predicates +} + +type postgresArrayRouteOperator string + +const ( + postgresArrayRouteOperatorEqual postgresArrayRouteOperator = "=" + postgresArrayRouteOperatorContains postgresArrayRouteOperator = "@>" +) + +type postgresQualifiedColumn struct { + TableAlias string + Field string +} + +func (c postgresQualifiedColumn) ident(b *sql.Builder) { + b.Ident(c.TableAlias).WriteString(".").Ident(c.Field) +} + +type postgresArrayRouteExpression struct { + Column postgresQualifiedColumn + Operator postgresArrayRouteOperator + Value pq.StringArray +} + +func (e postgresArrayRouteExpression) predicate() *sql.Predicate { + return sql.P(func(b *sql.Builder) { + e.appendSQL(b) + }) +} + +func (e postgresArrayRouteExpression) appendSQL(b *sql.Builder) { + e.Column.ident(b) + b.WriteString(" ").WriteString(string(e.Operator)).WriteString(" ").Arg(e.Value) +} diff --git a/openmeter/ledger/creditvoid/adapter/record.go b/openmeter/ledger/creditvoid/adapter/record.go new file mode 100644 index 0000000000..38eb209da0 --- /dev/null +++ b/openmeter/ledger/creditvoid/adapter/record.go @@ -0,0 +1,128 @@ +package adapter + +import ( + "context" + "fmt" + + sql "entgo.io/ent/dialect/sql" + + "github.com/openmeterio/openmeter/openmeter/customer" + entdb "github.com/openmeterio/openmeter/openmeter/ent/db" + dbledgercreditvoidrecord "github.com/openmeterio/openmeter/openmeter/ent/db/ledgercreditvoidrecord" + "github.com/openmeterio/openmeter/openmeter/ent/db/predicate" + "github.com/openmeterio/openmeter/openmeter/ledger/creditvoid" + "github.com/openmeterio/openmeter/pkg/framework/entutils" + "github.com/openmeterio/openmeter/pkg/models" +) + +func (a *adapter) CreateRecords(ctx context.Context, input creditvoid.CreateRecordsInput) error { + if err := input.Validate(); err != nil { + return err + } + + return entutils.TransactingRepoWithNoValue(ctx, a, func(ctx context.Context, tx *adapter) error { + creates := make([]*entdb.LedgerCreditVoidRecordCreate, 0, len(input.Records)) + for _, record := range input.Records { + create := tx.db.LedgerCreditVoidRecord.Create(). + SetID(record.ID.ID). + SetNamespace(record.ID.Namespace). + SetAmount(record.Amount). + SetCustomerID(record.CustomerID.ID). + SetCurrency(record.Currency). + SetVoidedAt(record.VoidedAt). + SetSourceChargeID(record.SourceChargeID). + SetVoidTransactionGroupID(record.VoidTransactionGroupID). + SetVoidTransactionID(record.VoidTransactionID). + SetFboSubAccountID(record.FBOSubAccountID). + SetReceivableSubAccountID(record.ReceivableSubAccountID). + SetAnnotations(record.Annotations) + + creates = append(creates, create) + } + + if len(creates) == 0 { + return nil + } + + if err := tx.db.LedgerCreditVoidRecord.CreateBulk(creates...).Exec(ctx); err != nil { + return fmt.Errorf("create credit void record rows: %w", err) + } + + return nil + }) +} + +func (a *adapter) ListRecords(ctx context.Context, input creditvoid.ListRecordsInput) ([]creditvoid.Record, error) { + if err := input.CustomerID.Validate(); err != nil { + return nil, fmt.Errorf("customer id: %w", err) + } + if input.Currency != nil { + if err := input.Currency.Validate(); err != nil { + return nil, fmt.Errorf("currency: %w", err) + } + } + if input.AsOf.IsZero() { + return nil, fmt.Errorf("as of is required") + } + + return entutils.TransactingRepo(ctx, a, func(ctx context.Context, tx *adapter) ([]creditvoid.Record, error) { + predicates := []predicate.LedgerCreditVoidRecord{ + dbledgercreditvoidrecord.NamespaceEQ(input.CustomerID.Namespace), + dbledgercreditvoidrecord.CustomerIDEQ(input.CustomerID.ID), + dbledgercreditvoidrecord.DeletedAtIsNil(), + dbledgercreditvoidrecord.VoidedAtLTE(input.AsOf), + } + + if input.Currency != nil { + predicates = append(predicates, dbledgercreditvoidrecord.CurrencyEQ(*input.Currency)) + } + if routePredicate := voidRecordRoutePredicate(input.Route); routePredicate != nil { + predicates = append(predicates, routePredicate) + } + + rows, err := tx.db.LedgerCreditVoidRecord.Query(). + Where(predicates...). + Order( + dbledgercreditvoidrecord.ByVoidedAt(sql.OrderDesc()), + dbledgercreditvoidrecord.ByCreatedAt(sql.OrderDesc()), + dbledgercreditvoidrecord.ByID(sql.OrderDesc()), + ). + All(ctx) + if err != nil { + return nil, fmt.Errorf("list credit void records: %w", err) + } + + return mapRecords(rows), nil + }) +} + +func mapRecordFromDB(row *entdb.LedgerCreditVoidRecord) creditvoid.Record { + return creditvoid.Record{ + ID: models.NamespacedID{ + Namespace: row.Namespace, + ID: row.ID, + }, + Amount: row.Amount, + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, + DeletedAt: row.DeletedAt, + CustomerID: customer.CustomerID{Namespace: row.Namespace, ID: row.CustomerID}, + Currency: row.Currency, + VoidedAt: row.VoidedAt, + SourceChargeID: row.SourceChargeID, + VoidTransactionGroupID: row.VoidTransactionGroupID, + VoidTransactionID: row.VoidTransactionID, + FBOSubAccountID: row.FboSubAccountID, + ReceivableSubAccountID: row.ReceivableSubAccountID, + Annotations: row.Annotations, + } +} + +func mapRecords(rows []*entdb.LedgerCreditVoidRecord) []creditvoid.Record { + out := make([]creditvoid.Record, 0, len(rows)) + for _, row := range rows { + out = append(out, mapRecordFromDB(row)) + } + + return out +} diff --git a/openmeter/ledger/creditvoid/noop.go b/openmeter/ledger/creditvoid/noop.go new file mode 100644 index 0000000000..29df06d624 --- /dev/null +++ b/openmeter/ledger/creditvoid/noop.go @@ -0,0 +1,25 @@ +package creditvoid + +import ( + "context" + "errors" + + "github.com/openmeterio/openmeter/pkg/models" +) + +// NoopService is wired when credits are disabled. +type NoopService struct{} + +var _ Service = NoopService{} + +func NewNoopService() Service { + return NoopService{} +} + +func (NoopService) VoidCreditPurchase(context.Context, VoidCreditPurchaseInput) (VoidCreditPurchaseResult, error) { + return VoidCreditPurchaseResult{}, models.NewGenericNotImplementedError(errors.New("credit voiding is not enabled")) +} + +func (NoopService) ListVoidedCreditImpacts(context.Context, ListVoidedCreditImpactsInput) (ListVoidedCreditImpactsResult, error) { + return ListVoidedCreditImpactsResult{Items: []VoidImpact{}}, nil +} diff --git a/openmeter/ledger/creditvoid/service.go b/openmeter/ledger/creditvoid/service.go new file mode 100644 index 0000000000..610673e968 --- /dev/null +++ b/openmeter/ledger/creditvoid/service.go @@ -0,0 +1,730 @@ +package creditvoid + +import ( + "context" + "errors" + "fmt" + "slices" + "time" + + "github.com/alpacahq/alpacadecimal" + "github.com/oklog/ulid/v2" + "github.com/samber/mo" + + "github.com/openmeterio/openmeter/openmeter/customer" + "github.com/openmeterio/openmeter/openmeter/ledger" + "github.com/openmeterio/openmeter/openmeter/ledger/breakage" + "github.com/openmeterio/openmeter/openmeter/ledger/transactions" + "github.com/openmeterio/openmeter/pkg/clock" + "github.com/openmeterio/openmeter/pkg/currencyx" + "github.com/openmeterio/openmeter/pkg/framework/transaction" + "github.com/openmeterio/openmeter/pkg/models" +) + +type Service interface { + VoidCreditPurchase(ctx context.Context, input VoidCreditPurchaseInput) (VoidCreditPurchaseResult, error) + ListVoidedCreditImpacts(ctx context.Context, input ListVoidedCreditImpactsInput) (ListVoidedCreditImpactsResult, error) +} + +type Config struct { + Adapter Adapter + Ledger ledger.Ledger + Dependencies transactions.ResolverDependencies + Breakage breakage.Service + AccountLocker ledger.AccountLocker + TransactionManager transaction.Creator +} + +func (c Config) Validate() error { + var errs []error + + if c.Adapter == nil { + errs = append(errs, errors.New("adapter is required")) + } + if c.Ledger == nil { + errs = append(errs, errors.New("ledger is required")) + } + if c.Dependencies.AccountService == nil { + errs = append(errs, errors.New("account service is required")) + } + if c.Dependencies.AccountCatalog == nil { + errs = append(errs, errors.New("account catalog is required")) + } + if c.Dependencies.BalanceQuerier == nil { + errs = append(errs, errors.New("balance querier is required")) + } + if c.Breakage == nil { + errs = append(errs, errors.New("breakage service is required")) + } + if c.AccountLocker == nil { + errs = append(errs, errors.New("account locker is required")) + } + if c.TransactionManager == nil { + errs = append(errs, errors.New("transaction manager is required")) + } + + return errors.Join(errs...) +} + +func NewService(config Config) (Service, error) { + if err := config.Validate(); err != nil { + return nil, err + } + + return &service{ + adapter: config.Adapter, + ledger: config.Ledger, + deps: config.Dependencies, + breakage: config.Breakage, + accountLocker: config.AccountLocker, + transactionManager: config.TransactionManager, + }, nil +} + +type service struct { + adapter Adapter + ledger ledger.Ledger + deps transactions.ResolverDependencies + breakage breakage.Service + accountLocker ledger.AccountLocker + transactionManager transaction.Creator +} + +type VoidCreditPurchaseInput struct { + CustomerID customer.CustomerID + ChargeID string + Currency currencyx.Code + Annotations models.Annotations +} + +func (i VoidCreditPurchaseInput) Validate() error { + var errs []error + + if err := i.CustomerID.Validate(); err != nil { + errs = append(errs, fmt.Errorf("customer id: %w", err)) + } + + if i.ChargeID == "" { + errs = append(errs, errors.New("charge id is required")) + } + + if err := i.Currency.Validate(); err != nil { + errs = append(errs, fmt.Errorf("currency: %w", err)) + } + + return errors.Join(errs...) +} + +type VoidCreditPurchaseResult struct { + VoidedAt time.Time + Amount alpacadecimal.Decimal + TransactionGroupID string +} + +type ListVoidedCreditImpactsInput struct { + CustomerID customer.CustomerID + Currency *currencyx.Code + AsOf time.Time + After *ledger.TransactionCursor + Before *ledger.TransactionCursor + Limit int + Route ledger.RouteFilter +} + +func (i ListVoidedCreditImpactsInput) Validate() error { + var errs []error + + if err := i.CustomerID.Validate(); err != nil { + errs = append(errs, fmt.Errorf("customer id: %w", err)) + } + if i.Currency != nil { + if err := i.Currency.Validate(); err != nil { + errs = append(errs, fmt.Errorf("currency: %w", err)) + } + } + if i.AsOf.IsZero() { + errs = append(errs, errors.New("as of is required")) + } + if i.After != nil { + if err := i.After.Validate(); err != nil { + errs = append(errs, fmt.Errorf("after: %w", err)) + } + } + if i.Before != nil { + if err := i.Before.Validate(); err != nil { + errs = append(errs, fmt.Errorf("before: %w", err)) + } + } + if i.After != nil && i.Before != nil { + errs = append(errs, errors.New("after and before cannot be set together")) + } + if err := breakage.ValidateExpiredRouteFilter(i.Route); err != nil { + errs = append(errs, fmt.Errorf("route: %w", err)) + } + if i.Limit < 1 { + errs = append(errs, errors.New("limit must be greater than 0")) + } + + return errors.Join(errs...) +} + +type ListVoidedCreditImpactsResult struct { + Items []VoidImpact + HasMore bool +} + +type VoidImpact struct { + ID models.NamespacedID + CreatedAt time.Time + VoidedAt time.Time + CustomerID customer.CustomerID + Currency currencyx.Code + Amount alpacadecimal.Decimal + Annotations models.Annotations +} + +func (i VoidImpact) Cursor() ledger.TransactionCursor { + return ledger.TransactionCursor{ + BookedAt: i.VoidedAt, + CreatedAt: i.CreatedAt, + ID: i.ID, + } +} + +// voidPlan is the read-only outcome of the planning step: the concrete +// remaining balance slices to forfeit, each with the open expiry breakage plan +// it must release. +type voidPlan struct { + voidedAt time.Time + slices []voidSlice +} + +type voidSlice struct { + amount alpacadecimal.Decimal + fboAccount string + fboAddress ledger.PostingAddress + // expiryPlan is nil for non-expiring charges. + expiryPlan *breakage.Plan +} + +func (s *service) VoidCreditPurchase(ctx context.Context, input VoidCreditPurchaseInput) (VoidCreditPurchaseResult, error) { + if err := input.Validate(); err != nil { + return VoidCreditPurchaseResult{}, err + } + + return transaction.Run(ctx, s.transactionManager, func(ctx context.Context) (VoidCreditPurchaseResult, error) { + plan, err := s.planVoid(ctx, input) + if err != nil { + return VoidCreditPurchaseResult{}, err + } + + return s.executeVoid(ctx, input, plan) + }) +} + +// planVoid locks the customer FBO account, reads the charge's remaining +// balance slices, and matches each to the open expiry breakage plan it must +// release. The lock stays held until the surrounding transaction commits, so +// the plan cannot go stale against concurrent collection or voiding. +func (s *service) planVoid(ctx context.Context, input VoidCreditPurchaseInput) (voidPlan, error) { + // Truncate to Postgres timestamp precision so the returned void time + // matches the persisted rows byte-for-byte. + voidedAt := clock.Now().UTC().Truncate(time.Microsecond) + + customerAccounts, err := s.deps.AccountService.GetCustomerAccounts(ctx, input.CustomerID) + if err != nil { + return voidPlan{}, fmt.Errorf("get customer accounts: %w", err) + } + + if err := s.accountLocker.LockAccountsForPosting(ctx, []ledger.Account{customerAccounts.FBOAccount}); err != nil { + return voidPlan{}, fmt.Errorf("lock customer FBO account: %w", err) + } + + fboAccountID := customerAccounts.FBOAccount.ID() + // The as-of read naturally excludes the charge's own future-dated expiry + // breakage entries. + buckets, err := s.deps.BalanceQuerier.GetBalanceBuckets(ctx, ledger.BalanceBucketQuery{ + Namespace: input.CustomerID.Namespace, + Filters: ledger.Filters{ + AccountID: &fboAccountID.ID, + SourceChargeID: mo.Some(&input.ChargeID), + AsOf: &voidedAt, + Route: ledger.RouteFilter{ + Currency: input.Currency, + }, + }, + GroupBy: []string{ledger.BalanceBucketGroupBySourceChargeID}, + }) + if err != nil { + return voidPlan{}, fmt.Errorf("get FBO balance buckets: %w", err) + } + + openPlansBySubAccount, err := s.openExpiryPlansBySubAccount(ctx, input, voidedAt) + if err != nil { + return voidPlan{}, err + } + + plan := voidPlan{voidedAt: voidedAt} + for _, bucket := range buckets { + if !bucket.SettledAmount.IsPositive() { + continue + } + + slice := voidSlice{ + amount: bucket.SettledAmount, + fboAccount: fboAccountID.ID, + fboAddress: bucket.Address, + } + + // Expiring grants should have matching open expiry plans; non-expiring + // grants and already-released plans are absent from this lookup. + expiryPlan, ok := openPlansBySubAccount[bucket.Address.SubAccountID()] + if ok { + if expiryPlan.OpenAmount.LessThan(slice.amount) { + return voidPlan{}, fmt.Errorf("open expiry breakage plan %s amount %s is less than voided amount %s", expiryPlan.ID.ID, expiryPlan.OpenAmount, slice.amount) + } + + slice.expiryPlan = &expiryPlan + } + + plan.slices = append(plan.slices, slice) + } + + if len(plan.slices) == 0 { + return voidPlan{}, models.NewGenericConflictError( + fmt.Errorf("credit purchase %s has no remaining value to void", input.ChargeID), + ) + } + + return plan, nil +} + +func (s *service) openExpiryPlansBySubAccount(ctx context.Context, input VoidCreditPurchaseInput, voidedAt time.Time) (map[string]breakage.Plan, error) { + openPlans, err := s.breakage.ListPlans(ctx, breakage.ListPlansInput{ + CustomerID: input.CustomerID, + Currency: input.Currency, + AsOf: voidedAt, + }) + if err != nil { + return nil, fmt.Errorf("list open breakage plans: %w", err) + } + + out := make(map[string]breakage.Plan) + for _, plan := range openPlans { + if plan.SourceChargeID == nil || *plan.SourceChargeID != input.ChargeID { + continue + } + + out[plan.FBOSubAccountID] = plan + } + + return out, nil +} + +func (s *service) executeVoid(ctx context.Context, input VoidCreditPurchaseInput, plan voidPlan) (VoidCreditPurchaseResult, error) { + var ( + inputs []ledger.TransactionInput + pendingBreakage []breakage.PendingRecord + pendingVoidRecords []pendingVoidRecord + ) + amount := alpacadecimal.Zero + + for _, slice := range plan.slices { + voidTx, voidRecord, err := s.resolveVoidSlice(ctx, input, plan.voidedAt, slice) + if err != nil { + return VoidCreditPurchaseResult{}, err + } + + inputs = append(inputs, voidTx) + pendingVoidRecords = append(pendingVoidRecords, voidRecord) + amount = amount.Add(slice.amount) + + if slice.expiryPlan == nil { + continue + } + + releaseTx, releaseRecord, err := s.breakage.ReleasePlan(ctx, breakage.ReleasePlanInput{ + Plan: *slice.expiryPlan, + Amount: slice.amount, + SourceKind: breakage.SourceKindCreditPurchase, + SourceChargeID: &input.ChargeID, + }) + if err != nil { + return VoidCreditPurchaseResult{}, fmt.Errorf("resolve expiry breakage release: %w", err) + } + + inputs = append(inputs, releaseTx) + pendingBreakage = append(pendingBreakage, releaseRecord) + } + + for i, txInput := range inputs { + if txInput != nil { + inputs[i] = transactions.WithAnnotations(txInput, input.Annotations) + } + } + + transactionGroup, err := s.ledger.CommitGroup(ctx, transactions.GroupInputs( + input.CustomerID.Namespace, + input.Annotations, + inputs..., + )) + if err != nil { + return VoidCreditPurchaseResult{}, fmt.Errorf("commit ledger transaction group: %w", err) + } + + if err := s.breakage.PersistCommittedRecords(ctx, pendingBreakage, transactionGroup); err != nil { + return VoidCreditPurchaseResult{}, fmt.Errorf("persist breakage records: %w", err) + } + + if err := s.persistCommittedVoidRecords(ctx, pendingVoidRecords, transactionGroup); err != nil { + return VoidCreditPurchaseResult{}, fmt.Errorf("persist void records: %w", err) + } + + return VoidCreditPurchaseResult{ + VoidedAt: plan.voidedAt, + Amount: amount, + TransactionGroupID: transactionGroup.ID().ID, + }, nil +} + +func (s *service) resolveVoidSlice(ctx context.Context, input VoidCreditPurchaseInput, voidedAt time.Time, slice voidSlice) (ledger.TransactionInput, pendingVoidRecord, error) { + recordID := newRecordID(input.CustomerID.Namespace) + route := slice.fboAddress.Route().Route() + + issueTx, err := s.originalIssueTransaction(ctx, input, voidedAt, slice) + if err != nil { + return nil, pendingVoidRecord{}, err + } + + inputs, err := transactions.CorrectTransaction(ctx, s.deps, transactions.CorrectionInput{ + At: voidedAt, + Amount: slice.amount, + OriginalTransaction: issueTx, + }) + if err != nil { + return nil, pendingVoidRecord{}, fmt.Errorf("resolve issue correction: %w", err) + } + if len(inputs) != 1 { + return nil, pendingVoidRecord{}, fmt.Errorf("expected one issue correction transaction input, got %d", len(inputs)) + } + + correctedFBO, correctedReceivable, err := correctionEntrySubAccounts(inputs[0]) + if err != nil { + return nil, pendingVoidRecord{}, err + } + if correctedFBO != slice.fboAddress.SubAccountID() { + return nil, pendingVoidRecord{}, fmt.Errorf("issue correction FBO sub-account %s does not match voided FBO sub-account %s", correctedFBO, slice.fboAddress.SubAccountID()) + } + + record := pendingVoidRecord{ + ID: recordID, + Amount: slice.amount, + CustomerID: input.CustomerID, + Currency: route.Currency, + VoidedAt: voidedAt, + SourceChargeID: input.ChargeID, + FBOSubAccountID: correctedFBO, + ReceivableSubAccountID: correctedReceivable, + } + + return transactions.WithAnnotations(inputs[0], creditVoidRecordAnnotations(recordID.ID)), record, nil +} + +func (s *service) originalIssueTransaction(ctx context.Context, input VoidCreditPurchaseInput, voidedAt time.Time, slice voidSlice) (ledger.Transaction, error) { + cursor := (*ledger.TransactionCursor)(nil) + + for { + page, err := s.ledger.ListTransactions(ctx, ledger.ListTransactionsInput{ + Namespace: input.CustomerID.Namespace, + Cursor: cursor, + Limit: 100, + AccountIDs: []string{slice.fboAccount}, + Currency: &input.Currency, + AsOf: &voidedAt, + CreditMovement: ledger.ListTransactionsCreditMovementPositive, + AnnotationFilters: map[string]string{ + ledger.AnnotationTransactionTemplateCode: string(transactions.TemplateCodeIssueCustomerReceivable), + ledger.AnnotationTransactionDirection: string(ledger.TransactionDirectionForward), + }, + }) + if err != nil { + return nil, fmt.Errorf("list issue transactions: %w", err) + } + + for _, tx := range page.Items { + if !transactionIssuedFBOForSource(tx, input.ChargeID, slice.fboAddress.SubAccountID()) { + continue + } + + fullTx, err := s.transactionByID(ctx, tx.ID()) + if err != nil { + return nil, err + } + + return fullTx, nil + } + + if page.NextCursor == nil { + return nil, fmt.Errorf("issue transaction for charge %s FBO sub-account %s not found", input.ChargeID, slice.fboAddress.SubAccountID()) + } + + cursor = page.NextCursor + } +} + +func (s *service) transactionByID(ctx context.Context, id models.NamespacedID) (ledger.Transaction, error) { + page, err := s.ledger.ListTransactions(ctx, ledger.ListTransactionsInput{ + Namespace: id.Namespace, + Limit: 1, + TransactionID: &id, + }) + if err != nil { + return nil, fmt.Errorf("get transaction %s: %w", id.ID, err) + } + if len(page.Items) != 1 { + return nil, fmt.Errorf("transaction %s not found", id.ID) + } + + return page.Items[0], nil +} + +func transactionIssuedFBOForSource(tx ledger.Transaction, sourceChargeID string, fboSubAccountID string) bool { + for _, entry := range tx.Entries() { + if entry.PostingAddress().AccountType() != ledger.AccountTypeCustomerFBO { + continue + } + if entry.PostingAddress().SubAccountID() != fboSubAccountID { + continue + } + if !entry.Amount().IsPositive() { + continue + } + if entry.SourceChargeID() == nil || *entry.SourceChargeID() != sourceChargeID { + continue + } + + return true + } + + return false +} + +func correctionEntrySubAccounts(input ledger.TransactionInput) (string, string, error) { + var fboSubAccountID string + var receivableSubAccountID string + + for _, entry := range input.EntryInputs() { + switch entry.PostingAddress().AccountType() { + case ledger.AccountTypeCustomerFBO: + if !entry.Amount().IsNegative() { + continue + } + fboSubAccountID = entry.PostingAddress().SubAccountID() + case ledger.AccountTypeCustomerReceivable: + if !entry.Amount().IsPositive() { + continue + } + receivableSubAccountID = entry.PostingAddress().SubAccountID() + } + } + + if fboSubAccountID == "" { + return "", "", errors.New("issue correction FBO entry is required") + } + if receivableSubAccountID == "" { + return "", "", errors.New("issue correction receivable entry is required") + } + + return fboSubAccountID, receivableSubAccountID, nil +} + +func (s *service) persistCommittedVoidRecords(ctx context.Context, pending []pendingVoidRecord, group ledger.TransactionGroup) error { + if len(pending) == 0 { + return nil + } + if group == nil { + return errors.New("transaction group is required") + } + + pendingByID := make(map[string]pendingVoidRecord, len(pending)) + for _, item := range pending { + pendingByID[item.ID.ID] = item + } + + records := make([]Record, 0, len(pending)) + groupID := group.ID().ID + for _, tx := range group.Transactions() { + recordID, ok := creditVoidRecordID(tx.Annotations()) + if !ok { + continue + } + + pendingRecord, ok := pendingByID[recordID] + if !ok { + return fmt.Errorf("committed void transaction %s has unknown record id %s", tx.ID().ID, recordID) + } + + record := pendingRecord.committed(groupID, tx.ID().ID, tx.Annotations()) + + records = append(records, record) + delete(pendingByID, recordID) + } + + if len(pendingByID) > 0 { + return fmt.Errorf("missing committed void transactions for %d pending records", len(pendingByID)) + } + + return s.adapter.CreateRecords(ctx, CreateRecordsInput{Records: records}) +} + +func (s *service) ListVoidedCreditImpacts(ctx context.Context, input ListVoidedCreditImpactsInput) (ListVoidedCreditImpactsResult, error) { + if err := input.Validate(); err != nil { + return ListVoidedCreditImpactsResult{}, err + } + + records, err := s.adapter.ListRecords(ctx, ListRecordsInput{ + CustomerID: input.CustomerID, + Currency: input.Currency, + AsOf: input.AsOf, + Route: input.Route, + }) + if err != nil { + return ListVoidedCreditImpactsResult{}, fmt.Errorf("list void records: %w", err) + } + if len(records) == 0 { + return ListVoidedCreditImpactsResult{ + Items: []VoidImpact{}, + }, nil + } + + groups := make(map[voidImpactGroupKey]*voidImpactGroup) + for _, record := range records { + key := voidImpactGroupKey{ + voidedAt: record.VoidedAt, + currency: record.Currency, + sourceChargeID: record.SourceChargeID, + transactionGroupID: record.VoidTransactionGroupID, + } + + group := groups[key] + if group == nil { + group = &voidImpactGroup{ + id: record.ID, + createdAt: record.CreatedAt, + voidedAt: record.VoidedAt, + currency: record.Currency, + annotations: models.Annotations{}, + } + groups[key] = group + } + + group.amount = group.amount.Add(record.Amount) + if group.id.ID == "" || record.ID.ID < group.id.ID { + group.id = record.ID + } + if group.createdAt.IsZero() || record.CreatedAt.Before(group.createdAt) { + group.createdAt = record.CreatedAt + } + for k, v := range record.Annotations { + group.annotations[k] = v + } + group.annotations[ledger.AnnotationChargeID] = record.SourceChargeID + } + + items := make([]VoidImpact, 0, len(groups)) + for _, group := range groups { + if group.amount.IsZero() { + continue + } + if group.amount.IsNegative() { + return ListVoidedCreditImpactsResult{}, fmt.Errorf("void amount is negative for %s %s", group.voidedAt, group.currency) + } + + item := VoidImpact{ + ID: group.id, + CreatedAt: group.createdAt, + VoidedAt: group.voidedAt, + CustomerID: input.CustomerID, + Currency: group.currency, + Amount: group.amount.Neg(), + Annotations: group.annotations, + } + if !voidImpactMatchesCursorWindow(item, input.After, input.Before) { + continue + } + + items = append(items, item) + } + + slices.SortFunc(items, func(a, b VoidImpact) int { + return -a.Cursor().Compare(b.Cursor()) + }) + + hasMore := len(items) > input.Limit + if hasMore { + items = items[:input.Limit] + } + + return ListVoidedCreditImpactsResult{ + Items: items, + HasMore: hasMore, + }, nil +} + +func voidImpactMatchesCursorWindow(item VoidImpact, after, before *ledger.TransactionCursor) bool { + cursor := item.Cursor() + + if after != nil && cursor.Compare(*after) >= 0 { + return false + } + if before != nil && cursor.Compare(*before) <= 0 { + return false + } + + return true +} + +type voidImpactGroupKey struct { + voidedAt time.Time + currency currencyx.Code + sourceChargeID string + transactionGroupID string +} + +type voidImpactGroup struct { + id models.NamespacedID + createdAt time.Time + voidedAt time.Time + currency currencyx.Code + amount alpacadecimal.Decimal + annotations models.Annotations +} + +const AnnotationCreditVoidRecordID = "ledger.credit_void.record_id" + +func creditVoidRecordAnnotations(recordID string) models.Annotations { + return models.Annotations{ + AnnotationCreditVoidRecordID: recordID, + } +} + +func creditVoidRecordID(annotations models.Annotations) (string, bool) { + raw, ok := annotations[AnnotationCreditVoidRecordID] + if !ok { + return "", false + } + + value, ok := raw.(string) + if !ok || value == "" { + return "", false + } + + return value, true +} + +func newRecordID(namespace string) models.NamespacedID { + return models.NamespacedID{ + Namespace: namespace, + ID: ulid.Make().String(), + } +} diff --git a/openmeter/ledger/creditvoid/types.go b/openmeter/ledger/creditvoid/types.go new file mode 100644 index 0000000000..dd546a223d --- /dev/null +++ b/openmeter/ledger/creditvoid/types.go @@ -0,0 +1,131 @@ +package creditvoid + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/alpacahq/alpacadecimal" + + "github.com/openmeterio/openmeter/openmeter/customer" + "github.com/openmeterio/openmeter/openmeter/ledger" + "github.com/openmeterio/openmeter/pkg/currencyx" + "github.com/openmeterio/openmeter/pkg/models" +) + +type Record struct { + ID models.NamespacedID + Amount alpacadecimal.Decimal + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt *time.Time + + CustomerID customer.CustomerID + Currency currencyx.Code + VoidedAt time.Time + + SourceChargeID string + + VoidTransactionGroupID string + VoidTransactionID string + + FBOSubAccountID string + ReceivableSubAccountID string + + Annotations models.Annotations +} + +// pendingVoidRecord is a pre-commit record plan. The committed ledger +// transaction IDs are not known until after CommitGroup returns. +type pendingVoidRecord struct { + ID models.NamespacedID + Amount alpacadecimal.Decimal + CustomerID customer.CustomerID + Currency currencyx.Code + VoidedAt time.Time + + SourceChargeID string + + FBOSubAccountID string + ReceivableSubAccountID string +} + +func (r pendingVoidRecord) committed(groupID, transactionID string, annotations models.Annotations) Record { + return Record{ + ID: r.ID, + Amount: r.Amount, + CustomerID: r.CustomerID, + Currency: r.Currency, + VoidedAt: r.VoidedAt, + SourceChargeID: r.SourceChargeID, + VoidTransactionGroupID: groupID, + VoidTransactionID: transactionID, + FBOSubAccountID: r.FBOSubAccountID, + ReceivableSubAccountID: r.ReceivableSubAccountID, + Annotations: annotations, + } +} + +type CreateRecordsInput struct { + Records []Record +} + +func (i CreateRecordsInput) Validate() error { + for idx, record := range i.Records { + if err := record.Validate(); err != nil { + return fmt.Errorf("records[%d]: %w", idx, err) + } + } + + return nil +} + +func (r Record) Validate() error { + var errs []error + + if err := r.ID.Validate(); err != nil { + errs = append(errs, fmt.Errorf("id: %w", err)) + } + if !r.Amount.IsPositive() { + errs = append(errs, errors.New("amount must be positive")) + } + if err := r.CustomerID.Validate(); err != nil { + errs = append(errs, fmt.Errorf("customer id: %w", err)) + } + if err := r.Currency.Validate(); err != nil { + errs = append(errs, fmt.Errorf("currency: %w", err)) + } + if r.VoidedAt.IsZero() { + errs = append(errs, errors.New("voided at is required")) + } + if r.SourceChargeID == "" { + errs = append(errs, errors.New("source charge id is required")) + } + if r.VoidTransactionGroupID == "" { + errs = append(errs, errors.New("void transaction group id is required")) + } + if r.VoidTransactionID == "" { + errs = append(errs, errors.New("void transaction id is required")) + } + if r.FBOSubAccountID == "" { + errs = append(errs, errors.New("FBO sub-account id is required")) + } + if r.ReceivableSubAccountID == "" { + errs = append(errs, errors.New("receivable sub-account id is required")) + } + + return errors.Join(errs...) +} + +type ListRecordsInput struct { + CustomerID customer.CustomerID + Currency *currencyx.Code + AsOf time.Time + Route ledger.RouteFilter +} + +type Adapter interface { + CreateRecords(ctx context.Context, input CreateRecordsInput) error + ListRecords(ctx context.Context, input ListRecordsInput) ([]Record, error) +} diff --git a/openmeter/ledger/customerbalance/expired_loader.go b/openmeter/ledger/customerbalance/expired_loader.go index ee6d403467..2e7e574e10 100644 --- a/openmeter/ledger/customerbalance/expired_loader.go +++ b/openmeter/ledger/customerbalance/expired_loader.go @@ -7,12 +7,14 @@ import ( "github.com/openmeterio/openmeter/openmeter/ledger/breakage" ) -type expiredCreditTransactionLoader struct { - service *service +func newExpiredCreditTransactionLoader(s *service) creditTransactionLoader { + return &expiredCreditTransactionLoader{ + service: s, + } } -func newExpiredCreditTransactionLoader(s *service) creditTransactionLoader { - return &expiredCreditTransactionLoader{service: s} +type expiredCreditTransactionLoader struct { + service *service } func (l *expiredCreditTransactionLoader) Load(ctx context.Context, input creditTransactionLoaderInput) (creditTransactionLoaderResult, error) { diff --git a/openmeter/ledger/customerbalance/ledger_loader.go b/openmeter/ledger/customerbalance/ledger_loader.go index ed1bcd69c1..adf12767ab 100644 --- a/openmeter/ledger/customerbalance/ledger_loader.go +++ b/openmeter/ledger/customerbalance/ledger_loader.go @@ -4,6 +4,7 @@ import ( "context" "github.com/openmeterio/openmeter/openmeter/ledger" + "github.com/openmeterio/openmeter/openmeter/ledger/creditvoid" ) type ledgerCreditTransactionLoader struct { @@ -19,31 +20,64 @@ func newLedgerCreditTransactionLoader(s *service, movement ledger.ListTransactio } func (l *ledgerCreditTransactionLoader) Load(ctx context.Context, input creditTransactionLoaderInput) (creditTransactionLoaderResult, error) { - result, err := l.service.Ledger.ListTransactions(ctx, ledger.ListTransactionsInput{ - Namespace: input.CustomerID.Namespace, - Cursor: input.After, - Before: input.Before, - Limit: input.Limit, - AccountIDs: []string{input.AccountID}, - Currency: input.Currency, - AsOf: &input.AsOf, - Route: featureFilterRoute(input.FeatureFilter), - CreditMovement: l.movement, - ExcludeAnnotationFilters: map[string]string{ - ledger.AnnotationCollectionType: ledger.CollectionTypeBreakage, - }, - }) - if err != nil { - return creditTransactionLoaderResult{}, err + txs := make([]ledger.Transaction, 0, input.Limit+1) + after := input.After + before := input.Before + hasMore := false + + for len(txs) <= input.Limit { + result, err := l.service.Ledger.ListTransactions(ctx, ledger.ListTransactionsInput{ + Namespace: input.CustomerID.Namespace, + Cursor: after, + Before: before, + Limit: input.Limit, + AccountIDs: []string{input.AccountID}, + Currency: input.Currency, + AsOf: &input.AsOf, + Route: featureFilterRoute(input.FeatureFilter), + CreditMovement: l.movement, + ExcludeAnnotationFilters: map[string]string{ + ledger.AnnotationCollectionType: ledger.CollectionTypeBreakage, + }, + }) + if err != nil { + return creditTransactionLoaderResult{}, err + } + + for _, tx := range result.Items { + if _, ok := tx.Annotations()[creditvoid.AnnotationCreditVoidRecordID]; ok { + continue + } + + txs = append(txs, tx) + } + + if len(txs) > input.Limit { + hasMore = true + break + } + + if result.NextCursor == nil { + break + } + + if before != nil { + before = result.NextCursor + } else { + after = result.NextCursor + } + } + if len(txs) > input.Limit { + txs = txs[:input.Limit] } - items, err := creditTransactionsFromLedgerTransactions(result.Items) + items, err := creditTransactionsFromLedgerTransactions(txs) if err != nil { return creditTransactionLoaderResult{}, err } return creditTransactionLoaderResult{ Items: items, - HasMore: result.NextCursor != nil, + HasMore: hasMore, }, nil } diff --git a/openmeter/ledger/customerbalance/ledger_loader_test.go b/openmeter/ledger/customerbalance/ledger_loader_test.go new file mode 100644 index 0000000000..7699de321a --- /dev/null +++ b/openmeter/ledger/customerbalance/ledger_loader_test.go @@ -0,0 +1,229 @@ +package customerbalance + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/alpacahq/alpacadecimal" + "github.com/stretchr/testify/require" + + "github.com/openmeterio/openmeter/openmeter/customer" + "github.com/openmeterio/openmeter/openmeter/ledger" + "github.com/openmeterio/openmeter/openmeter/ledger/creditvoid" + ledgerhistorical "github.com/openmeterio/openmeter/openmeter/ledger/historical" + "github.com/openmeterio/openmeter/pkg/currencyx" + "github.com/openmeterio/openmeter/pkg/models" +) + +func TestLedgerCreditTransactionLoaderDoesNotRetainHasMoreAfterHiddenFinalPage(t *testing.T) { + now := time.Date(2026, 7, 9, 12, 0, 0, 0, time.UTC) + visible := ledgerLoaderTestTransaction(t, "tx-visible", now.Add(-time.Minute), nil, ledger.AccountTypeCustomerAccrued) + hidden := ledgerLoaderTestTransaction(t, "tx-hidden", now.Add(-2*time.Minute), models.Annotations{ + creditvoid.AnnotationCreditVoidRecordID: "void-record-1", + }, ledger.AccountTypeCustomerReceivable) + + next := visible.Cursor() + fakeLedger := &ledgerLoaderFakeLedger{ + pages: []ledger.ListTransactionsResult{ + { + Items: []ledger.Transaction{visible}, + NextCursor: &next, + }, + { + Items: []ledger.Transaction{hidden}, + }, + }, + } + + loader := newLedgerCreditTransactionLoader( + &service{Ledger: fakeLedger}, + ledger.ListTransactionsCreditMovementNegative, + ) + + currency := currencyx.Code("USD") + got, err := loader.Load(t.Context(), creditTransactionLoaderInput{ + Limit: 1, + CustomerID: customer.CustomerID{Namespace: "ns", ID: "customer-id"}, + AccountID: "fbo-account", + Currency: ¤cy, + AsOf: now, + }) + require.NoError(t, err) + require.False(t, got.HasMore) + require.Len(t, got.Items, 1) + require.Equal(t, "tx-visible", got.Items[0].ID.ID) + require.Len(t, fakeLedger.inputs, 2) + require.NotNil(t, fakeLedger.inputs[1].Cursor) +} + +func TestLedgerCreditTransactionLoaderScansPastHiddenRowsUntilLimit(t *testing.T) { + now := time.Date(2026, 7, 9, 12, 0, 0, 0, time.UTC) + hidden := ledgerLoaderTestTransaction(t, "tx-hidden", now.Add(-time.Minute), models.Annotations{ + creditvoid.AnnotationCreditVoidRecordID: "void-record-1", + }, ledger.AccountTypeCustomerReceivable) + visibleA := ledgerLoaderTestTransaction(t, "tx-visible-a", now.Add(-2*time.Minute), nil, ledger.AccountTypeCustomerAccrued) + visibleB := ledgerLoaderTestTransaction(t, "tx-visible-b", now.Add(-3*time.Minute), nil, ledger.AccountTypeCustomerAccrued) + + next := hidden.Cursor() + fakeLedger := &ledgerLoaderFakeLedger{ + pages: []ledger.ListTransactionsResult{ + { + Items: []ledger.Transaction{hidden}, + NextCursor: &next, + }, + { + Items: []ledger.Transaction{visibleA, visibleB}, + }, + }, + } + + loader := newLedgerCreditTransactionLoader( + &service{Ledger: fakeLedger}, + ledger.ListTransactionsCreditMovementNegative, + ) + + currency := currencyx.Code("USD") + got, err := loader.Load(t.Context(), creditTransactionLoaderInput{ + Limit: 2, + CustomerID: customer.CustomerID{Namespace: "ns", ID: "customer-id"}, + AccountID: "fbo-account", + Currency: ¤cy, + AsOf: now, + }) + require.NoError(t, err) + require.False(t, got.HasMore) + require.Len(t, got.Items, 2) + require.Equal(t, "tx-visible-a", got.Items[0].ID.ID) + require.Equal(t, "tx-visible-b", got.Items[1].ID.ID) + require.Len(t, fakeLedger.inputs, 2) + require.Equal(t, 2, fakeLedger.inputs[0].Limit) + require.NotNil(t, fakeLedger.inputs[1].Cursor) +} + +func TestLedgerCreditTransactionLoaderScansPastHiddenRowsToFindVisibleHasMore(t *testing.T) { + now := time.Date(2026, 7, 9, 12, 0, 0, 0, time.UTC) + visible := ledgerLoaderTestTransaction(t, "tx-visible", now.Add(-time.Minute), nil, ledger.AccountTypeCustomerAccrued) + hidden := ledgerLoaderTestTransaction(t, "tx-hidden", now.Add(-2*time.Minute), models.Annotations{ + creditvoid.AnnotationCreditVoidRecordID: "void-record-1", + }, ledger.AccountTypeCustomerReceivable) + extraVisible := ledgerLoaderTestTransaction(t, "tx-extra-visible", now.Add(-3*time.Minute), nil, ledger.AccountTypeCustomerAccrued) + + nextAfterVisible := visible.Cursor() + nextAfterHidden := hidden.Cursor() + fakeLedger := &ledgerLoaderFakeLedger{ + pages: []ledger.ListTransactionsResult{ + { + Items: []ledger.Transaction{visible}, + NextCursor: &nextAfterVisible, + }, + { + Items: []ledger.Transaction{hidden}, + NextCursor: &nextAfterHidden, + }, + { + Items: []ledger.Transaction{extraVisible}, + }, + }, + } + + loader := newLedgerCreditTransactionLoader( + &service{Ledger: fakeLedger}, + ledger.ListTransactionsCreditMovementNegative, + ) + + currency := currencyx.Code("USD") + got, err := loader.Load(t.Context(), creditTransactionLoaderInput{ + Limit: 1, + CustomerID: customer.CustomerID{Namespace: "ns", ID: "customer-id"}, + AccountID: "fbo-account", + Currency: ¤cy, + AsOf: now, + }) + require.NoError(t, err) + require.True(t, got.HasMore) + require.Len(t, got.Items, 1) + require.Equal(t, "tx-visible", got.Items[0].ID.ID) + require.Len(t, fakeLedger.inputs, 3) + require.Equal(t, 1, fakeLedger.inputs[0].Limit) + require.NotNil(t, fakeLedger.inputs[1].Cursor) + require.NotNil(t, fakeLedger.inputs[2].Cursor) +} + +type ledgerLoaderFakeLedger struct { + pages []ledger.ListTransactionsResult + inputs []ledger.ListTransactionsInput +} + +func (l *ledgerLoaderFakeLedger) CommitGroup(context.Context, ledger.TransactionGroupInput) (ledger.TransactionGroup, error) { + return nil, errors.New("unexpected CommitGroup call") +} + +func (l *ledgerLoaderFakeLedger) GetTransactionGroup(context.Context, models.NamespacedID) (ledger.TransactionGroup, error) { + return nil, errors.New("unexpected GetTransactionGroup call") +} + +func (l *ledgerLoaderFakeLedger) ListTransactions(_ context.Context, input ledger.ListTransactionsInput) (ledger.ListTransactionsResult, error) { + l.inputs = append(l.inputs, input) + if len(l.pages) == 0 { + return ledger.ListTransactionsResult{}, errors.New("unexpected ListTransactions call") + } + + page := l.pages[0] + l.pages = l.pages[1:] + + return page, nil +} + +func ledgerLoaderTestTransaction( + t *testing.T, + id string, + bookedAt time.Time, + annotations models.Annotations, + offsetAccountType ledger.AccountType, +) ledger.Transaction { + t.Helper() + + route := ledger.Route{Currency: currencyx.Code("USD")} + key, err := ledger.BuildRoutingKey(route) + require.NoError(t, err) + + tx, err := ledgerhistorical.NewTransactionFromData(ledgerhistorical.TransactionData{ + ID: id, + Namespace: "ns", + Annotations: annotations, + CreatedAt: bookedAt, + BookedAt: bookedAt, + }, []ledgerhistorical.EntryData{ + { + ID: id + "-fbo", + Namespace: "ns", + CreatedAt: bookedAt, + SubAccountID: "fbo-subaccount", + AccountType: ledger.AccountTypeCustomerFBO, + Route: route, + RouteID: "fbo-route", + RouteKey: key.Value(), + RouteKeyVer: key.Version(), + Amount: alpacadecimal.NewFromInt(-10), + TransactionID: id, + }, + { + ID: id + "-offset", + Namespace: "ns", + CreatedAt: bookedAt, + SubAccountID: "offset-subaccount", + AccountType: offsetAccountType, + Route: route, + RouteID: "offset-route", + RouteKey: key.Value(), + RouteKeyVer: key.Version(), + Amount: alpacadecimal.NewFromInt(10), + TransactionID: id, + }, + }) + require.NoError(t, err) + + return tx +} diff --git a/openmeter/ledger/customerbalance/loaders.go b/openmeter/ledger/customerbalance/loaders.go index 6571a5e598..b665fced59 100644 --- a/openmeter/ledger/customerbalance/loaders.go +++ b/openmeter/ledger/customerbalance/loaders.go @@ -38,6 +38,7 @@ var creditTransactionLoaderOrder = []CreditTransactionType{ CreditTransactionTypeFunded, CreditTransactionTypeConsumed, CreditTransactionTypeExpired, + CreditTransactionTypeVoided, } var creditTransactionLoaderFactories = map[CreditTransactionType]creditTransactionLoaderFactory{ @@ -46,6 +47,7 @@ var creditTransactionLoaderFactories = map[CreditTransactionType]creditTransacti return newLedgerCreditTransactionLoader(s, ledger.ListTransactionsCreditMovementNegative) }, CreditTransactionTypeExpired: newExpiredCreditTransactionLoader, + CreditTransactionTypeVoided: newVoidedCreditTransactionLoader, } func (s *service) creditTransactionLoaders(txType *CreditTransactionType) ([]creditTransactionLoader, error) { diff --git a/openmeter/ledger/customerbalance/service.go b/openmeter/ledger/customerbalance/service.go index db5a6816a0..df4a7ec3c8 100644 --- a/openmeter/ledger/customerbalance/service.go +++ b/openmeter/ledger/customerbalance/service.go @@ -19,6 +19,7 @@ import ( "github.com/openmeterio/openmeter/openmeter/customer" "github.com/openmeterio/openmeter/openmeter/ledger" ledgerbreakage "github.com/openmeterio/openmeter/openmeter/ledger/breakage" + "github.com/openmeterio/openmeter/openmeter/ledger/creditvoid" "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/cmpx" "github.com/openmeterio/openmeter/pkg/currencyx" @@ -78,6 +79,7 @@ type service struct { Ledger ledger.Ledger BalanceQuerier ledger.BalanceQuerier Breakage ledgerbreakage.Service + CreditVoid creditvoid.Service balanceCalculator chargeLiveBalanceCalculator } @@ -93,6 +95,7 @@ type Config struct { Ledger ledger.Ledger BalanceQuerier ledger.BalanceQuerier Breakage ledgerbreakage.Service + CreditVoid creditvoid.Service } type GetBalanceServiceInput struct { @@ -247,6 +250,10 @@ func New(config Config) (*service, error) { if breakageService == nil { breakageService = ledgerbreakage.NewNoopService() } + creditVoidService := config.CreditVoid + if creditVoidService == nil { + creditVoidService = creditvoid.NewNoopService() + } return &service{ AccountResolver: config.AccountResolver, @@ -257,6 +264,7 @@ func New(config Config) (*service, error) { Ledger: config.Ledger, BalanceQuerier: config.BalanceQuerier, Breakage: breakageService, + CreditVoid: creditVoidService, balanceCalculator: chargeLiveBalanceCalculator{}, }, nil } diff --git a/openmeter/ledger/customerbalance/testenv_test.go b/openmeter/ledger/customerbalance/testenv_test.go index 83f82d1107..e47f7ff4b4 100644 --- a/openmeter/ledger/customerbalance/testenv_test.go +++ b/openmeter/ledger/customerbalance/testenv_test.go @@ -35,6 +35,8 @@ import ( ledgerbreakageadapter "github.com/openmeterio/openmeter/openmeter/ledger/breakage/adapter" ledgerchargeadapter "github.com/openmeterio/openmeter/openmeter/ledger/chargeadapter" ledgercollector "github.com/openmeterio/openmeter/openmeter/ledger/collector" + "github.com/openmeterio/openmeter/openmeter/ledger/creditvoid" + creditvoidadapter "github.com/openmeterio/openmeter/openmeter/ledger/creditvoid/adapter" ledgertestutils "github.com/openmeterio/openmeter/openmeter/ledger/testutils" "github.com/openmeterio/openmeter/openmeter/ledger/transactions" "github.com/openmeterio/openmeter/openmeter/meter" @@ -65,6 +67,7 @@ type testEnv struct { *ledgertestutils.IntegrationEnv BreakageService ledgerbreakage.Service + CreditVoidService creditvoid.Service Service *service creditPurchase creditpurchase.Service flatFeeService flatfee.Service @@ -189,6 +192,25 @@ func newTestEnv(t *testing.T) *testEnv { }) require.NoError(t, err) + creditVoidAdapter, err := creditvoidadapter.New(creditvoidadapter.Config{ + Client: base.DB, + }) + require.NoError(t, err) + + creditVoidService, err := creditvoid.NewService(creditvoid.Config{ + Adapter: creditVoidAdapter, + Ledger: base.Deps.HistoricalLedger, + Dependencies: transactions.ResolverDependencies{ + AccountService: base.Deps.ResolversService, + AccountCatalog: base.Deps.AccountService, + BalanceQuerier: base.Deps.HistoricalLedger, + }, + Breakage: breakageService, + AccountLocker: base.Deps.AccountService, + TransactionManager: enttx.NewCreator(base.DB), + }) + require.NoError(t, err) + collectorService, err := ledgercollector.NewService(ledgercollector.Config{ Ledger: base.Deps.HistoricalLedger, Dependencies: transactions.ResolverDependencies{ @@ -301,12 +323,14 @@ func newTestEnv(t *testing.T) *testEnv { Ledger: base.Deps.HistoricalLedger, BalanceQuerier: base.Deps.HistoricalLedger, Breakage: breakageService, + CreditVoid: creditVoidService, }) require.NoError(t, err) env := &testEnv{ IntegrationEnv: base, BreakageService: breakageService, + CreditVoidService: creditVoidService, Service: service, creditPurchase: creditPurchaseService, flatFeeService: flatFeeService, diff --git a/openmeter/ledger/customerbalance/transactions.go b/openmeter/ledger/customerbalance/transactions.go index f5231f5d96..317aafc2be 100644 --- a/openmeter/ledger/customerbalance/transactions.go +++ b/openmeter/ledger/customerbalance/transactions.go @@ -26,11 +26,12 @@ const ( CreditTransactionTypeFunded CreditTransactionType = "funded" CreditTransactionTypeConsumed CreditTransactionType = "consumed" CreditTransactionTypeExpired CreditTransactionType = "expired" + CreditTransactionTypeVoided CreditTransactionType = "voided" ) func (t CreditTransactionType) Validate() error { switch t { - case CreditTransactionTypeFunded, CreditTransactionTypeConsumed, CreditTransactionTypeExpired: + case CreditTransactionTypeFunded, CreditTransactionTypeConsumed, CreditTransactionTypeExpired, CreditTransactionTypeVoided: return nil default: return fmt.Errorf("invalid credit transaction type: %s", t) diff --git a/openmeter/ledger/customerbalance/voided_loader.go b/openmeter/ledger/customerbalance/voided_loader.go new file mode 100644 index 0000000000..6ad7b4b154 --- /dev/null +++ b/openmeter/ledger/customerbalance/voided_loader.go @@ -0,0 +1,54 @@ +package customerbalance + +import ( + "context" + "fmt" + + "github.com/openmeterio/openmeter/openmeter/ledger/creditvoid" +) + +type voidedCreditTransactionLoader struct { + service *service +} + +func newVoidedCreditTransactionLoader(s *service) creditTransactionLoader { + return &voidedCreditTransactionLoader{ + service: s, + } +} + +func (l *voidedCreditTransactionLoader) Load(ctx context.Context, input creditTransactionLoaderInput) (creditTransactionLoaderResult, error) { + result, err := l.service.CreditVoid.ListVoidedCreditImpacts(ctx, creditvoid.ListVoidedCreditImpactsInput{ + CustomerID: input.CustomerID, + Currency: input.Currency, + AsOf: input.AsOf, + After: input.After, + Before: input.Before, + Limit: input.Limit, + Route: featureFilterRoute(input.FeatureFilter), + }) + if err != nil { + return creditTransactionLoaderResult{}, fmt.Errorf("list voided credit impacts: %w", err) + } + + items := make([]CreditTransaction, 0, len(result.Items)) + for _, impact := range result.Items { + balanceAsOf := impact.VoidedAt + items = append(items, CreditTransaction{ + ID: impact.ID, + CreatedAt: impact.CreatedAt, + BookedAt: impact.VoidedAt, + Type: CreditTransactionTypeVoided, + Currency: impact.Currency, + Amount: impact.Amount, + Name: "Voided credits", + Annotations: impact.Annotations, + balanceAsOf: &balanceAsOf, + }) + } + + return creditTransactionLoaderResult{ + Items: items, + HasMore: result.HasMore, + }, nil +} diff --git a/test/app/stripe/invoice_test.go b/test/app/stripe/invoice_test.go index cef1bf05bd..6e398d280c 100644 --- a/test/app/stripe/invoice_test.go +++ b/test/app/stripe/invoice_test.go @@ -31,6 +31,7 @@ import ( ledgerbreakage "github.com/openmeterio/openmeter/openmeter/ledger/breakage" ledgerchargeadapter "github.com/openmeterio/openmeter/openmeter/ledger/chargeadapter" ledgercollector "github.com/openmeterio/openmeter/openmeter/ledger/collector" + "github.com/openmeterio/openmeter/openmeter/ledger/creditvoid" ledgerresolvers "github.com/openmeterio/openmeter/openmeter/ledger/resolvers" ledgertestutils "github.com/openmeterio/openmeter/openmeter/ledger/testutils" "github.com/openmeterio/openmeter/openmeter/ledger/transactions" @@ -159,6 +160,8 @@ func (s *StripeInvoiceTestSuite) SetupSuite() { ChargesService: chargeStack.ChargesService, BillingService: s.BillingService, CustomerService: s.CustomerService, + CreditVoidService: creditvoid.NewNoopService(), + TransactionManager: enttx.NewCreator(s.DBClient), }) s.Require().NoError(err, "failed to initialize credit grant service") s.CreditGrant = creditGrantService diff --git a/test/credits/base.go b/test/credits/base.go index 4d3c73b2ac..a880c4779f 100644 --- a/test/credits/base.go +++ b/test/credits/base.go @@ -29,6 +29,8 @@ import ( ledgerbreakageadapter "github.com/openmeterio/openmeter/openmeter/ledger/breakage/adapter" ledgerchargeadapter "github.com/openmeterio/openmeter/openmeter/ledger/chargeadapter" ledgercollector "github.com/openmeterio/openmeter/openmeter/ledger/collector" + "github.com/openmeterio/openmeter/openmeter/ledger/creditvoid" + creditvoidadapter "github.com/openmeterio/openmeter/openmeter/ledger/creditvoid/adapter" "github.com/openmeterio/openmeter/openmeter/ledger/customerbalance" "github.com/openmeterio/openmeter/openmeter/ledger/recognizer" ledgerresolvers "github.com/openmeterio/openmeter/openmeter/ledger/resolvers" @@ -56,6 +58,7 @@ type BaseSuite struct { LedgerAccountService ledgeraccount.Service LedgerResolver *ledgerresolvers.AccountResolver BreakageService ledgerbreakage.Service + CreditVoidService creditvoid.Service FlatFeeHandler flatfee.Handler LineageService lineage.Service RevenueRecognizer recognizer.Service @@ -103,6 +106,26 @@ func (s *BaseSuite) SetupSuite() { s.NoError(err) s.BreakageService = breakageService + creditVoidAdapter, err := creditvoidadapter.New(creditvoidadapter.Config{ + Client: s.DBClient, + }) + s.NoError(err) + + creditVoidService, err := creditvoid.NewService(creditvoid.Config{ + Adapter: creditVoidAdapter, + Ledger: deps.HistoricalLedger, + Dependencies: transactions.ResolverDependencies{ + AccountService: deps.ResolversService, + AccountCatalog: deps.AccountService, + BalanceQuerier: deps.HistoricalLedger, + }, + Breakage: breakageService, + AccountLocker: deps.AccountService, + TransactionManager: transactionManager, + }) + s.NoError(err) + s.CreditVoidService = creditVoidService + revenueRecognizer, err := recognizer.NewService(recognizer.Config{ Ledger: deps.HistoricalLedger, Dependencies: transactions.ResolverDependencies{ @@ -163,6 +186,7 @@ func (s *BaseSuite) SetupSuite() { Ledger: deps.HistoricalLedger, BalanceQuerier: deps.HistoricalLedger, Breakage: breakageService, + CreditVoid: creditVoidService, }) s.NoError(err) s.CustomerBalanceSvc = customerBalanceSvc diff --git a/test/credits/creditgrant_test.go b/test/credits/creditgrant_test.go index 9156c59568..27126615a1 100644 --- a/test/credits/creditgrant_test.go +++ b/test/credits/creditgrant_test.go @@ -27,6 +27,7 @@ import ( "github.com/openmeterio/openmeter/openmeter/ledger" ledgerbreakage "github.com/openmeterio/openmeter/openmeter/ledger/breakage" ledgerchargeadapter "github.com/openmeterio/openmeter/openmeter/ledger/chargeadapter" + "github.com/openmeterio/openmeter/openmeter/ledger/creditvoid" "github.com/openmeterio/openmeter/openmeter/productcatalog" "github.com/openmeterio/openmeter/openmeter/taxcode" omtestutils "github.com/openmeterio/openmeter/openmeter/testutils" @@ -89,6 +90,8 @@ func (s *CreditGrantTestSuite) SetupSuite() { ChargesService: s.Charges, BillingService: s.BillingService, CustomerService: s.CustomerService, + CreditVoidService: creditvoid.NewNoopService(), + TransactionManager: enttx.NewCreator(s.DBClient), }) s.Require().NoError(err) diff --git a/test/credits/voidgrant_test.go b/test/credits/voidgrant_test.go new file mode 100644 index 0000000000..c96bd3af82 --- /dev/null +++ b/test/credits/voidgrant_test.go @@ -0,0 +1,859 @@ +package credits + +import ( + "context" + "testing" + "time" + + "github.com/alpacahq/alpacadecimal" + "github.com/samber/lo" + "github.com/samber/mo" + "github.com/stretchr/testify/suite" + "golang.org/x/sync/errgroup" + + appcustominvoicing "github.com/openmeterio/openmeter/openmeter/app/custominvoicing" + "github.com/openmeterio/openmeter/openmeter/billing" + "github.com/openmeterio/openmeter/openmeter/billing/charges" + "github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase" + creditgrant "github.com/openmeterio/openmeter/openmeter/billing/creditgrant" + creditgrantservice "github.com/openmeterio/openmeter/openmeter/billing/creditgrant/service" + "github.com/openmeterio/openmeter/openmeter/customer" + enttx "github.com/openmeterio/openmeter/openmeter/ent/tx" + "github.com/openmeterio/openmeter/openmeter/ledger" + "github.com/openmeterio/openmeter/openmeter/ledger/creditvoid" + "github.com/openmeterio/openmeter/openmeter/ledger/customerbalance" + "github.com/openmeterio/openmeter/openmeter/ledger/transactions" + "github.com/openmeterio/openmeter/openmeter/productcatalog" + "github.com/openmeterio/openmeter/pkg/clock" + "github.com/openmeterio/openmeter/pkg/datetime" + "github.com/openmeterio/openmeter/pkg/models" + "github.com/openmeterio/openmeter/pkg/pagination" + "github.com/openmeterio/openmeter/pkg/timeutil" + billingtest "github.com/openmeterio/openmeter/test/billing" +) + +func TestVoidGrantTestSuite(t *testing.T) { + suite.Run(t, new(VoidGrantTestSuite)) +} + +type VoidGrantTestSuite struct { + BaseSuite + + CreditGrantService creditgrant.Service +} + +func (s *VoidGrantTestSuite) SetupSuite() { + s.BaseSuite.SetupSuite() + + creditGrantService, err := creditgrantservice.New(creditgrantservice.Config{ + CreditPurchaseService: s.CreditPurchaseSvc, + ChargesService: s.Charges, + BillingService: s.BillingService, + CustomerService: s.CustomerService, + CreditVoidService: s.CreditVoidService, + TransactionManager: enttx.NewCreator(s.DBClient), + }) + s.Require().NoError(err) + s.CreditGrantService = creditGrantService +} + +func (s *VoidGrantTestSuite) TestVoidFullyUnusedGrant() { + // given: + // - a non-expiring promotional grant of 100 fully unused + // when: + // - the grant is voided + // then: + // - the full remaining value moves from customer FBO back to open receivable + // - the grant derives as voided and the listing shows a voided transaction + ctx := s.T().Context() + ns := s.GetUniqueNamespace("voidgrant-unused") + cust := s.setupVoidTestCustomer(ctx, ns) + + fundedAt := datetime.MustParseTimeInLocation(s.T(), "2026-03-01T00:00:00Z", time.UTC).AsTime() + clock.FreezeTime(fundedAt) + defer clock.UnFreeze() + + funding := s.CreatePromotionalCreditFunding(ctx, CreatePromotionalCreditFundingInput{ + Namespace: ns, + Customer: cust.GetID(), + Amount: alpacadecimal.NewFromInt(100), + At: fundedAt, + CostBasis: alpacadecimal.Zero, + }) + + voidedAt := fundedAt.Add(24 * time.Hour) + clock.FreezeTime(voidedAt) + + grant, err := s.CreditGrantService.Void(ctx, creditgrant.VoidInput{ + Namespace: ns, + CustomerID: cust.ID, + ChargeID: funding.Charge.ID, + }) + s.Require().NoError(err) + s.Require().NotNil(grant.State.VoidedAt) + s.Equal(voidedAt, grant.State.VoidedAt.UTC()) + + s.Equal(float64(0), s.MustCustomerFBOBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal]()).InexactFloat64()) + s.Equal(float64(100), s.MustCustomerReceivableBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal](), ledger.TransactionAuthorizationStatusOpen).InexactFloat64()) + s.Equal(float64(0), s.MustBreakageBalanceAsOf(ns, USD, mo.None[*alpacadecimal.Decimal](), voidedAt).InexactFloat64()) + + s.requireCreditTransactionAmountsByType(cust.GetID(), nil, map[customerbalance.CreditTransactionType]float64{ + customerbalance.CreditTransactionTypeFunded: 100, + customerbalance.CreditTransactionTypeVoided: -100, + }) + s.requireCreditTransactionAmountsByType(cust.GetID(), lo.ToPtr(customerbalance.CreditTransactionTypeVoided), map[customerbalance.CreditTransactionType]float64{ + customerbalance.CreditTransactionTypeVoided: -100, + }) + + voidedImpacts, err := s.CreditVoidService.ListVoidedCreditImpacts(ctx, creditvoid.ListVoidedCreditImpactsInput{ + CustomerID: cust.GetID(), + Currency: lo.ToPtr(USD), + AsOf: voidedAt.Add(time.Second), + Limit: 10, + }) + s.Require().NoError(err) + s.Require().Len(voidedImpacts.Items, 1) + s.Equal(string(transactions.TemplateCodeIssueCustomerReceivable), voidedImpacts.Items[0].Annotations[ledger.AnnotationTransactionTemplateCode]) + s.Equal(string(ledger.TransactionDirectionCorrection), voidedImpacts.Items[0].Annotations[ledger.AnnotationTransactionDirection]) +} + +func (s *VoidGrantTestSuite) TestVoidTwiceIsIdempotent() { + // given: + // - a voided grant + // when: + // - the void is retried + // then: + // - the retry succeeds with the original void time and books no extra ledger movement + ctx := s.T().Context() + ns := s.GetUniqueNamespace("voidgrant-twice") + cust := s.setupVoidTestCustomer(ctx, ns) + + fundedAt := datetime.MustParseTimeInLocation(s.T(), "2026-03-01T00:00:00Z", time.UTC).AsTime() + clock.FreezeTime(fundedAt) + defer clock.UnFreeze() + + funding := s.CreatePromotionalCreditFunding(ctx, CreatePromotionalCreditFundingInput{ + Namespace: ns, + Customer: cust.GetID(), + Amount: alpacadecimal.NewFromInt(100), + At: fundedAt, + CostBasis: alpacadecimal.Zero, + }) + + voidedAt := fundedAt.Add(time.Hour) + clock.FreezeTime(voidedAt) + + first, err := s.CreditGrantService.Void(ctx, creditgrant.VoidInput{ + Namespace: ns, + CustomerID: cust.ID, + ChargeID: funding.Charge.ID, + }) + s.Require().NoError(err) + s.Require().NotNil(first.State.VoidedAt) + + clock.FreezeTime(voidedAt.Add(time.Hour)) + + second, err := s.CreditGrantService.Void(ctx, creditgrant.VoidInput{ + Namespace: ns, + CustomerID: cust.ID, + ChargeID: funding.Charge.ID, + }) + s.Require().NoError(err) + s.Require().NotNil(second.State.VoidedAt) + s.Equal(first.State.VoidedAt.UTC(), second.State.VoidedAt.UTC()) + + s.Equal(float64(0), s.MustBreakageBalanceAsOf(ns, USD, mo.None[*alpacadecimal.Decimal](), clock.Now()).InexactFloat64()) + s.Equal(float64(100), s.MustCustomerReceivableBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal](), ledger.TransactionAuthorizationStatusOpen).InexactFloat64()) +} + +func (s *VoidGrantTestSuite) TestVoidPartiallyConsumedGrant() { + // given: + // - a promotional grant of 100 with 40 consumed by a credit-only usage charge + // when: + // - the grant is voided + // then: + // - only the remaining 60 moves back to receivable and consumed credit stays untouched + ctx := s.T().Context() + ns := s.GetUniqueNamespace("voidgrant-partial") + cust := s.setupVoidTestCustomer(ctx, ns) + + fundedAt := datetime.MustParseTimeInLocation(s.T(), "2026-03-01T00:00:00Z", time.UTC).AsTime() + clock.FreezeTime(fundedAt) + defer clock.UnFreeze() + + funding := s.CreatePromotionalCreditFunding(ctx, CreatePromotionalCreditFundingInput{ + Namespace: ns, + Customer: cust.GetID(), + Amount: alpacadecimal.NewFromInt(100), + At: fundedAt, + CostBasis: alpacadecimal.Zero, + }) + + s.mustConsumeCredits(ctx, ns, cust.GetID(), 40) + + voidedAt := clock.Now().Add(time.Hour) + clock.FreezeTime(voidedAt) + + grant, err := s.CreditGrantService.Void(ctx, creditgrant.VoidInput{ + Namespace: ns, + CustomerID: cust.ID, + ChargeID: funding.Charge.ID, + }) + s.Require().NoError(err) + s.Require().NotNil(grant.State.VoidedAt) + + s.Equal(float64(0), s.MustCustomerFBOBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal]()).InexactFloat64()) + s.Equal(float64(0), s.MustBreakageBalanceAsOf(ns, USD, mo.None[*alpacadecimal.Decimal](), voidedAt).InexactFloat64()) + s.Equal(float64(40), s.MustCustomerAccruedBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal]()).InexactFloat64()) + s.Equal(float64(60), s.MustCustomerReceivableBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal](), ledger.TransactionAuthorizationStatusOpen).InexactFloat64()) +} + +func (s *VoidGrantTestSuite) TestVoidFullyConsumedGrantReturnsConflict() { + // given: + // - a promotional grant of 40 fully consumed by usage + // when: + // - the grant is voided + // then: + // - the void is rejected with a conflict because there is nothing to void + ctx := s.T().Context() + ns := s.GetUniqueNamespace("voidgrant-consumed") + cust := s.setupVoidTestCustomer(ctx, ns) + + fundedAt := datetime.MustParseTimeInLocation(s.T(), "2026-03-01T00:00:00Z", time.UTC).AsTime() + clock.FreezeTime(fundedAt) + defer clock.UnFreeze() + + funding := s.CreatePromotionalCreditFunding(ctx, CreatePromotionalCreditFundingInput{ + Namespace: ns, + Customer: cust.GetID(), + Amount: alpacadecimal.NewFromInt(40), + At: fundedAt, + CostBasis: alpacadecimal.Zero, + }) + + s.mustConsumeCredits(ctx, ns, cust.GetID(), 40) + + clock.FreezeTime(clock.Now().Add(time.Hour)) + + _, err := s.CreditGrantService.Void(ctx, creditgrant.VoidInput{ + Namespace: ns, + CustomerID: cust.ID, + ChargeID: funding.Charge.ID, + }) + s.Require().Error(err) + s.True(models.IsGenericConflictError(err), "expected conflict error, got: %v", err) +} + +func (s *VoidGrantTestSuite) TestVoidExpiringGrantReleasesFutureExpiry() { + // given: + // - an expiring promotional grant of 100 voided before its expiry + // when: + // - the original expiry time passes + // then: + // - expiry does not remove the same value again: breakage stays 0 and FBO 0 + // - the listing shows one voided transaction and no expired transaction + ctx := s.T().Context() + ns := s.GetUniqueNamespace("voidgrant-expiring") + cust := s.setupVoidTestCustomer(ctx, ns) + + fundedAt := datetime.MustParseTimeInLocation(s.T(), "2026-03-01T00:00:00Z", time.UTC).AsTime() + expiresAt := fundedAt.Add(30 * 24 * time.Hour) + clock.FreezeTime(fundedAt) + defer clock.UnFreeze() + + funding := s.CreatePromotionalCreditFunding(ctx, CreatePromotionalCreditFundingInput{ + Namespace: ns, + Customer: cust.GetID(), + Amount: alpacadecimal.NewFromInt(100), + At: fundedAt, + ExpiresAt: &expiresAt, + CostBasis: alpacadecimal.Zero, + }) + + voidedAt := fundedAt.Add(24 * time.Hour) + clock.FreezeTime(voidedAt) + + grant, err := s.CreditGrantService.Void(ctx, creditgrant.VoidInput{ + Namespace: ns, + CustomerID: cust.ID, + ChargeID: funding.Charge.ID, + }) + s.Require().NoError(err) + s.Require().NotNil(grant.State.VoidedAt) + + s.Equal(float64(0), s.MustBreakageBalanceAsOf(ns, USD, mo.None[*alpacadecimal.Decimal](), voidedAt).InexactFloat64()) + s.Equal(float64(100), s.MustCustomerReceivableBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal](), ledger.TransactionAuthorizationStatusOpen).InexactFloat64()) + + afterExpiry := expiresAt.Add(time.Hour) + clock.FreezeTime(afterExpiry) + + s.Equal(float64(0), s.MustCustomerFBOBalanceAsOf(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal](), afterExpiry).InexactFloat64()) + s.Equal(float64(0), s.MustBreakageBalanceAsOf(ns, USD, mo.None[*alpacadecimal.Decimal](), afterExpiry).InexactFloat64()) + + s.requireCreditTransactionAmountsByType(cust.GetID(), nil, map[customerbalance.CreditTransactionType]float64{ + customerbalance.CreditTransactionTypeFunded: 100, + customerbalance.CreditTransactionTypeVoided: -100, + }) +} + +func (s *VoidGrantTestSuite) TestVoidAlreadyExpiredGrantReturnsConflict() { + // given: + // - an expiring promotional grant whose expiry already passed + // when: + // - the grant is voided + // then: + // - the void is rejected with a conflict and books nothing + ctx := s.T().Context() + ns := s.GetUniqueNamespace("voidgrant-expired") + cust := s.setupVoidTestCustomer(ctx, ns) + + fundedAt := datetime.MustParseTimeInLocation(s.T(), "2026-03-01T00:00:00Z", time.UTC).AsTime() + expiresAt := fundedAt.Add(24 * time.Hour) + clock.FreezeTime(fundedAt) + defer clock.UnFreeze() + + funding := s.CreatePromotionalCreditFunding(ctx, CreatePromotionalCreditFundingInput{ + Namespace: ns, + Customer: cust.GetID(), + Amount: alpacadecimal.NewFromInt(100), + At: fundedAt, + ExpiresAt: &expiresAt, + CostBasis: alpacadecimal.Zero, + }) + + clock.FreezeTime(expiresAt.Add(time.Hour)) + + _, err := s.CreditGrantService.Void(ctx, creditgrant.VoidInput{ + Namespace: ns, + CustomerID: cust.ID, + ChargeID: funding.Charge.ID, + }) + s.Require().Error(err) + s.True(models.IsGenericConflictError(err), "expected conflict error, got: %v", err) + + // Ordinary expiry still surfaces as expired, unaffected by the rejected void. + s.requireCreditTransactionAmountsByType(cust.GetID(), nil, map[customerbalance.CreditTransactionType]float64{ + customerbalance.CreditTransactionTypeFunded: 100, + customerbalance.CreditTransactionTypeExpired: -100, + }) + s.requireCreditTransactionAmountsByType(cust.GetID(), lo.ToPtr(customerbalance.CreditTransactionTypeVoided), map[customerbalance.CreditTransactionType]float64{}) +} + +func (s *VoidGrantTestSuite) TestVoidFeatureRestrictedGrantPreservesProvenance() { + // given: + // - a feature-restricted promotional grant of 100 + // when: + // - the grant is voided + // then: + // - the feature-routed FBO bucket empties and the void transaction carries + // the grant's source charge provenance + ctx := s.T().Context() + ns := s.GetUniqueNamespace("voidgrant-features") + cust := s.setupVoidTestCustomer(ctx, ns) + + fundedAt := datetime.MustParseTimeInLocation(s.T(), "2026-03-01T00:00:00Z", time.UTC).AsTime() + clock.FreezeTime(fundedAt) + defer clock.UnFreeze() + + funding := s.CreatePromotionalCreditFunding(ctx, CreatePromotionalCreditFundingInput{ + Namespace: ns, + Customer: cust.GetID(), + Amount: alpacadecimal.NewFromInt(100), + At: fundedAt, + CostBasis: alpacadecimal.Zero, + FeatureFilters: creditpurchase.FeatureFilters{"api_requests_total"}, + }) + + voidedAt := fundedAt.Add(time.Hour) + clock.FreezeTime(voidedAt) + + _, err := s.CreditGrantService.Void(ctx, creditgrant.VoidInput{ + Namespace: ns, + CustomerID: cust.ID, + ChargeID: funding.Charge.ID, + }) + s.Require().NoError(err) + + s.Equal(float64(0), s.MustCustomerFBOBalanceForFeatures(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal](), mo.Some([]string{"api_requests_total"})).InexactFloat64()) + s.Equal(float64(0), s.MustBreakageBalanceAsOf(ns, USD, mo.None[*alpacadecimal.Decimal](), voidedAt).InexactFloat64()) + + voidedType := customerbalance.CreditTransactionTypeVoided + result, err := s.CustomerBalanceSvc.ListCreditTransactions(ctx, customerbalance.ListCreditTransactionsInput{ + CustomerID: cust.GetID(), + Limit: 20, + Type: &voidedType, + FeatureFilter: customerbalance.NewFeatureFilter([]string{"api_requests_total"}), + }) + s.Require().NoError(err) + s.Require().Len(result.Items, 1) + s.Equal(float64(-100), result.Items[0].Amount.InexactFloat64()) + s.Equal(funding.Charge.ID, result.Items[0].Annotations[ledger.AnnotationChargeID]) +} + +func (s *VoidGrantTestSuite) TestVoidedStatusDerivationInReads() { + // given: + // - two grants, one of which is voided + // then: + // - Get returns voided_at for the voided grant only + // - the status filter separates voided from active grants + ctx := s.T().Context() + ns := s.GetUniqueNamespace("voidgrant-status") + cust := s.setupVoidTestCustomer(ctx, ns) + + fundedAt := datetime.MustParseTimeInLocation(s.T(), "2026-03-01T00:00:00Z", time.UTC).AsTime() + clock.FreezeTime(fundedAt) + defer clock.UnFreeze() + + voidedFunding := s.CreatePromotionalCreditFunding(ctx, CreatePromotionalCreditFundingInput{ + Namespace: ns, + Customer: cust.GetID(), + Amount: alpacadecimal.NewFromInt(100), + At: fundedAt, + CostBasis: alpacadecimal.Zero, + }) + + keptFunding := s.CreatePromotionalCreditFunding(ctx, CreatePromotionalCreditFundingInput{ + Namespace: ns, + Customer: cust.GetID(), + Amount: alpacadecimal.NewFromInt(150), + At: fundedAt, + CostBasis: alpacadecimal.Zero, + Priority: lo.ToPtr(5), + }) + + clock.FreezeTime(fundedAt.Add(time.Hour)) + + _, err := s.CreditGrantService.Void(ctx, creditgrant.VoidInput{ + Namespace: ns, + CustomerID: cust.ID, + ChargeID: voidedFunding.Charge.ID, + }) + s.Require().NoError(err) + + voidedGrant, err := s.CreditGrantService.Get(ctx, creditgrant.GetInput{ + Namespace: ns, + CustomerID: cust.ID, + ChargeID: voidedFunding.Charge.ID, + }) + s.Require().NoError(err) + s.NotNil(voidedGrant.State.VoidedAt) + + keptGrant, err := s.CreditGrantService.Get(ctx, creditgrant.GetInput{ + Namespace: ns, + CustomerID: cust.ID, + ChargeID: keptFunding.Charge.ID, + }) + s.Require().NoError(err) + s.Nil(keptGrant.State.VoidedAt) + + s.requireGrantIDsForStatusFilter(ctx, ns, cust.ID, creditgrant.GrantStatusVoided, []string{voidedFunding.Charge.ID}) + + activeIDs := s.mustListGrantIDs(ctx, ns, cust.ID, lo.ToPtr(creditgrant.GrantStatusActive)) + s.NotContains(activeIDs, voidedFunding.Charge.ID) +} + +func (s *VoidGrantTestSuite) TestListGrantStatusFiltersUseGrantLifecycleState() { + // given: + // - pending, active, expired, and voided grants created through service flows + // when: + // - grants are listed by public status + // then: + // - each filter returns only grants in the matching public lifecycle state + ctx := s.T().Context() + ns := s.GetUniqueNamespace("voidgrant-list-status") + cust := s.setupVoidTestCustomer(ctx, ns) + + fundedAt := datetime.MustParseTimeInLocation(s.T(), "2026-03-01T00:00:00Z", time.UTC).AsTime() + clock.FreezeTime(fundedAt) + defer clock.UnFreeze() + + expiresAfter := datetime.MustParseDuration(s.T(), "PT1H") + pendingEffectiveAt := fundedAt.Add(48 * time.Hour) + + activeGrant, err := s.CreditGrantService.Create(ctx, creditgrant.CreateInput{ + Namespace: ns, + CustomerID: cust.ID, + Name: "active grant", + Currency: USD, + Amount: alpacadecimal.NewFromInt(100), + FundingMethod: creditgrant.FundingMethodNone, + }) + s.Require().NoError(err) + + expiredGrant, err := s.CreditGrantService.Create(ctx, creditgrant.CreateInput{ + Namespace: ns, + CustomerID: cust.ID, + Name: "expired grant", + Currency: USD, + Amount: alpacadecimal.NewFromInt(200), + FundingMethod: creditgrant.FundingMethodNone, + ExpiresAfter: &expiresAfter, + }) + s.Require().NoError(err) + + voidedGrant, err := s.CreditGrantService.Create(ctx, creditgrant.CreateInput{ + Namespace: ns, + CustomerID: cust.ID, + Name: "voided grant", + Currency: USD, + Amount: alpacadecimal.NewFromInt(300), + FundingMethod: creditgrant.FundingMethodNone, + }) + s.Require().NoError(err) + + pendingGrant, err := s.CreditGrantService.Create(ctx, creditgrant.CreateInput{ + Namespace: ns, + CustomerID: cust.ID, + Name: "pending grant", + Currency: USD, + Amount: alpacadecimal.NewFromInt(400), + EffectiveAt: &pendingEffectiveAt, + FundingMethod: creditgrant.FundingMethodInvoice, + Purchase: &creditgrant.PurchaseTerms{ + Currency: USD, + PerUnitCostBasis: lo.ToPtr(alpacadecimal.NewFromInt(1)), + }, + }) + s.Require().NoError(err) + + voidedAt := fundedAt.Add(30 * time.Minute) + clock.FreezeTime(voidedAt) + + _, err = s.CreditGrantService.Void(ctx, creditgrant.VoidInput{ + Namespace: ns, + CustomerID: cust.ID, + ChargeID: voidedGrant.ID, + }) + s.Require().NoError(err) + + clock.FreezeTime(fundedAt.Add(2 * time.Hour)) + + s.requireGrantIDsForStatusFilter(ctx, ns, cust.ID, creditgrant.GrantStatusPending, []string{pendingGrant.ID}) + s.requireGrantIDsForStatusFilter(ctx, ns, cust.ID, creditgrant.GrantStatusActive, []string{activeGrant.ID}) + s.requireGrantIDsForStatusFilter(ctx, ns, cust.ID, creditgrant.GrantStatusExpired, []string{expiredGrant.ID}) + s.requireGrantIDsForStatusFilter(ctx, ns, cust.ID, creditgrant.GrantStatusVoided, []string{voidedGrant.ID}) +} + +func (s *VoidGrantTestSuite) TestVoidInvoiceFundedGrantReceivableBalancesAcrossPaymentFlow() { + // given: + // - invoice-funded grants issued through the real invoice-backed credit-purchase flow + // when: + // - each grant is voided at a different payment lifecycle point + // then: + // - voiding corrects the original issuance, while already-booked payment auth/settle entries remain visible + ctx := s.T().Context() + issuedAt := datetime.MustParseTimeInLocation(s.T(), "2026-04-01T00:00:00Z", time.UTC).AsTime() + amount := alpacadecimal.NewFromInt(100) + costBasis := alpacadecimal.NewFromInt(1) + + tests := []struct { + name string + namespace string + advance func(context.Context, billing.StandardInvoice) + wantOpen alpacadecimal.Decimal + wantAuthorized alpacadecimal.Decimal + wantWash alpacadecimal.Decimal + }{ + { + name: "after issue before auth", + namespace: "voidgrant-invoice-before-auth", + advance: func(ctx context.Context, invoice billing.StandardInvoice) { + clock.FreezeTime(issuedAt.Add(time.Hour)) + invoice, err := s.BillingService.ApproveInvoice(ctx, invoice.GetInvoiceID()) + s.Require().NoError(err) + s.Equal(billing.StandardInvoiceStatusPaymentProcessingPending, invoice.Status) + }, + wantOpen: alpacadecimal.Zero, + wantAuthorized: alpacadecimal.Zero, + wantWash: alpacadecimal.Zero, + }, + { + name: "after auth before settle", + namespace: "voidgrant-invoice-after-auth", + advance: func(ctx context.Context, invoice billing.StandardInvoice) { + clock.FreezeTime(issuedAt.Add(time.Hour)) + invoice, err := s.BillingService.ApproveInvoice(ctx, invoice.GetInvoiceID()) + s.Require().NoError(err) + s.Equal(billing.StandardInvoiceStatusPaymentProcessingPending, invoice.Status) + + clock.FreezeTime(issuedAt.Add(2 * time.Hour)) + invoice, err = s.BillingService.PaymentAuthorized(ctx, invoice.GetInvoiceID()) + s.Require().NoError(err) + s.Equal(billing.StandardInvoiceStatusPaymentProcessingAuthorized, invoice.Status) + }, + wantOpen: amount, + wantAuthorized: amount.Neg(), + wantWash: alpacadecimal.Zero, + }, + { + name: "after settle", + namespace: "voidgrant-invoice-after-settle", + advance: func(ctx context.Context, invoice billing.StandardInvoice) { + clock.FreezeTime(issuedAt.Add(time.Hour)) + invoice, err := s.BillingService.ApproveInvoice(ctx, invoice.GetInvoiceID()) + s.Require().NoError(err) + s.Equal(billing.StandardInvoiceStatusPaymentProcessingPending, invoice.Status) + + clock.FreezeTime(issuedAt.Add(2 * time.Hour)) + invoice, err = s.BillingService.PaymentAuthorized(ctx, invoice.GetInvoiceID()) + s.Require().NoError(err) + s.Equal(billing.StandardInvoiceStatusPaymentProcessingAuthorized, invoice.Status) + + clock.FreezeTime(issuedAt.Add(3 * time.Hour)) + invoice, err = s.CustomInvoicingService.HandlePaymentTrigger(ctx, appcustominvoicing.HandlePaymentTriggerInput{ + InvoiceID: invoice.GetInvoiceID(), + Trigger: billing.TriggerPaid, + }) + s.Require().NoError(err) + s.Equal(billing.StandardInvoiceStatusPaid, invoice.Status) + }, + wantOpen: amount, + wantAuthorized: alpacadecimal.Zero, + wantWash: amount.Neg(), + }, + } + + for _, tt := range tests { + s.Run(tt.name, func() { + defer clock.UnFreeze() + + ns := s.GetUniqueNamespace(tt.namespace) + cust, grant, invoice := s.setupInvoiceFundedVoidGrant(ctx, ns, issuedAt, amount, costBasis) + + tt.advance(ctx, invoice) + clock.FreezeTime(issuedAt.Add(4 * time.Hour)) + + voidedGrant, err := s.CreditGrantService.Void(ctx, creditgrant.VoidInput{ + Namespace: ns, + CustomerID: cust.ID, + ChargeID: grant.ID, + }) + s.Require().NoError(err) + s.Require().NotNil(voidedGrant.State.VoidedAt) + + s.AssertDecimalEqual(alpacadecimal.Zero, s.MustCustomerFBOBalance(cust.GetID(), USD, mo.Some(&costBasis)), "void should remove the remaining grant value from FBO") + s.AssertDecimalEqual(tt.wantOpen, s.MustCustomerReceivableBalance(cust.GetID(), USD, mo.Some(&costBasis), ledger.TransactionAuthorizationStatusOpen), "open receivable balance after void") + s.AssertDecimalEqual(tt.wantAuthorized, s.MustCustomerReceivableBalance(cust.GetID(), USD, mo.Some(&costBasis), ledger.TransactionAuthorizationStatusAuthorized), "authorized receivable balance after void") + s.AssertDecimalEqual(tt.wantWash, s.MustWashBalance(ns, USD, mo.Some(&costBasis)), "wash balance after void") + }) + } +} + +func (s *VoidGrantTestSuite) TestConcurrentVoidsDoNotDoubleBook() { + // given: + // - a funded grant of 100 voided concurrently from several goroutines + // then: + // - exactly one call books the void, the rest get a conflict, and FBO is cleared + ctx := s.T().Context() + ns := s.GetUniqueNamespace("voidgrant-concurrent") + cust := s.setupVoidTestCustomer(ctx, ns) + + fundedAt := datetime.MustParseTimeInLocation(s.T(), "2026-03-01T00:00:00Z", time.UTC).AsTime() + clock.FreezeTime(fundedAt) + defer clock.UnFreeze() + + funding := s.CreatePromotionalCreditFunding(ctx, CreatePromotionalCreditFundingInput{ + Namespace: ns, + Customer: cust.GetID(), + Amount: alpacadecimal.NewFromInt(100), + At: fundedAt, + CostBasis: alpacadecimal.Zero, + }) + + clock.FreezeTime(fundedAt.Add(time.Hour)) + + errs := make([]error, 4) + + var group errgroup.Group + for i := range errs { + group.Go(func() error { + _, err := s.CreditVoidService.VoidCreditPurchase(ctx, creditvoid.VoidCreditPurchaseInput{ + CustomerID: cust.GetID(), + ChargeID: funding.Charge.ID, + Currency: USD, + Annotations: ledger.ChargeAnnotations(models.NamespacedID{ + Namespace: ns, + ID: funding.Charge.ID, + }), + }) + errs[i] = err + return nil + }) + } + s.Require().NoError(group.Wait()) + + succeeded := 0 + for _, err := range errs { + if err == nil { + succeeded++ + continue + } + s.True(models.IsGenericConflictError(err), "losing voids must conflict, got: %v", err) + } + s.Equal(1, succeeded) + + s.Equal(float64(0), s.MustBreakageBalanceAsOf(ns, USD, mo.None[*alpacadecimal.Decimal](), clock.Now()).InexactFloat64()) + s.Equal(float64(0), s.MustCustomerFBOBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal]()).InexactFloat64()) + s.Equal(float64(100), s.MustCustomerReceivableBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal](), ledger.TransactionAuthorizationStatusOpen).InexactFloat64()) +} + +func (s *VoidGrantTestSuite) setupVoidTestCustomer(ctx context.Context, ns string) *customer.Customer { + s.T().Helper() + + s.ProvisionDefaultTaxCodes(ctx, ns) + cust := s.CreateLedgerBackedCustomer(ns, "test-subject") + sandboxApp := s.InstallSandboxApp(s.T(), ns) + _ = s.ProvisionBillingProfile(ctx, ns, sandboxApp.GetID(), + billingtest.WithProgressiveBilling(), + billingtest.WithCollectionInterval(datetime.MustParseDuration(s.T(), "P2D")), + billingtest.WithManualApproval(), + ) + + return cust +} + +func (s *VoidGrantTestSuite) setupInvoiceFundedVoidGrant(ctx context.Context, ns string, issuedAt time.Time, amount, costBasis alpacadecimal.Decimal) (*customer.Customer, creditpurchase.Charge, billing.StandardInvoice) { + s.T().Helper() + + s.ProvisionDefaultTaxCodes(ctx, ns) + customInvoicing := s.SetupCustomInvoicing(ns) + cust := s.CreateLedgerBackedCustomer(ns, "test-subject") + _ = s.ProvisionBillingProfile(ctx, ns, customInvoicing.App.GetID(), + billingtest.WithProgressiveBilling(), + billingtest.WithCollectionInterval(datetime.MustParseDuration(s.T(), "PT1H")), + billingtest.WithManualApproval(), + ) + + clock.FreezeTime(issuedAt) + defer clock.UnFreeze() + + grant, err := s.CreditGrantService.Create(ctx, creditgrant.CreateInput{ + Namespace: ns, + CustomerID: cust.ID, + Name: "invoice-funded grant", + Currency: USD, + Amount: amount, + FundingMethod: creditgrant.FundingMethodInvoice, + Purchase: &creditgrant.PurchaseTerms{ + Currency: USD, + PerUnitCostBasis: &costBasis, + }, + }) + s.Require().NoError(err) + s.Equal(creditpurchase.StatusActive, grant.Status) + s.Require().NotNil(grant.Realizations.CreditGrantRealization) + + standardInvoices, err := s.BillingService.ListStandardInvoices(ctx, billing.ListStandardInvoicesInput{ + Namespaces: []string{ns}, + Expand: billing.StandardInvoiceExpandAll, + }) + s.Require().NoError(err) + s.Require().Len(standardInvoices.Items, 1) + invoice := standardInvoices.Items[0] + s.Equal(billing.StandardInvoiceStatusDraftManualApprovalNeeded, invoice.Status) + s.Require().Len(invoice.Lines.OrEmpty(), 1) + s.Equal(grant.ID, lo.FromPtr(invoice.Lines.OrEmpty()[0].ChargeID)) + + s.AssertDecimalEqual(amount, s.MustCustomerFBOBalance(cust.GetID(), USD, mo.Some(&costBasis)), "issued grant should be available in FBO before payment") + s.AssertDecimalEqual(amount.Neg(), s.MustCustomerReceivableBalance(cust.GetID(), USD, mo.Some(&costBasis), ledger.TransactionAuthorizationStatusOpen), "issued grant should create open receivable before payment") + s.AssertDecimalEqual(alpacadecimal.Zero, s.MustCustomerReceivableBalance(cust.GetID(), USD, mo.Some(&costBasis), ledger.TransactionAuthorizationStatusAuthorized), "issued grant should not be authorized yet") + s.AssertDecimalEqual(alpacadecimal.Zero, s.MustWashBalance(ns, USD, mo.Some(&costBasis)), "issued grant should not be settled yet") + + return cust, grant, invoice +} + +// mustConsumeCredits consumes credits from customer FBO by driving a +// unit-priced credit-only usage charge through its full lifecycle. It moves +// the clock past the charge's collection window. +func (s *VoidGrantTestSuite) mustConsumeCredits(ctx context.Context, ns string, customerID customer.CustomerID, units float64) { + s.T().Helper() + + apiRequestsTotal := s.SetupApiRequestsTotalFeature(ctx, ns) + + from := clock.Now() + servicePeriod := timeutil.ClosedPeriod{ + From: from, + To: from.Add(7 * 24 * time.Hour), + } + + _, err := s.Charges.Create(ctx, charges.CreateInput{ + Namespace: ns, + Intents: charges.ChargeIntents{ + s.CreateMockChargeIntent(CreateMockChargeIntentInput{ + Customer: customerID, + Currency: USD, + ServicePeriod: servicePeriod, + SettlementMode: productcatalog.CreditOnlySettlementMode, + Price: productcatalog.NewPriceFrom(productcatalog.UnitPrice{ + Amount: alpacadecimal.NewFromInt(1), + }), + Name: "void-test-usage", + ManagedBy: billing.SubscriptionManagedLine, + UniqueReferenceID: "void-test-usage", + FeatureKey: apiRequestsTotal.Feature.Key, + }), + }, + }) + s.Require().NoError(err) + + s.mustAdvanceUsageBasedCharges(ctx, customerID) + + s.MockStreamingConnector.AddSimpleEvent(apiRequestsTotal.Feature.Key, units, servicePeriod.From.Add(time.Hour)) + + // Advance past the service period and the P2D collection window so the + // charge finalizes and collection settles the consumed credits. + clock.FreezeTime(servicePeriod.To.Add(12 * time.Hour)) + s.mustAdvanceUsageBasedCharges(ctx, customerID) + + clock.FreezeTime(servicePeriod.To.Add(3 * 24 * time.Hour)) + s.mustAdvanceUsageBasedCharges(ctx, customerID) +} + +func (s *VoidGrantTestSuite) mustAdvanceUsageBasedCharges(ctx context.Context, customerID customer.CustomerID) { + s.T().Helper() + + _, err := s.Charges.AdvanceCharges(ctx, charges.AdvanceChargesInput{ + Customer: customerID, + }) + s.Require().NoError(err) +} + +func (s *VoidGrantTestSuite) requireCreditTransactionAmountsByType(customerID customer.CustomerID, txType *customerbalance.CreditTransactionType, expected map[customerbalance.CreditTransactionType]float64) { + s.T().Helper() + + result, err := s.CustomerBalanceSvc.ListCreditTransactions(s.T().Context(), customerbalance.ListCreditTransactionsInput{ + CustomerID: customerID, + Limit: 50, + Type: txType, + }) + s.Require().NoError(err) + + actual := make(map[customerbalance.CreditTransactionType]float64, len(result.Items)) + for _, item := range result.Items { + actual[item.Type] += item.Amount.InexactFloat64() + } + + s.Equal(expected, actual) +} + +func (s *VoidGrantTestSuite) requireGrantIDsForStatusFilter(ctx context.Context, ns, customerID string, status creditgrant.GrantStatus, expected []string) { + s.T().Helper() + + s.Equal(expected, s.mustListGrantIDs(ctx, ns, customerID, &status)) +} + +func (s *VoidGrantTestSuite) mustListGrantIDs(ctx context.Context, ns, customerID string, status *creditgrant.GrantStatus) []string { + s.T().Helper() + + result, err := s.CreditGrantService.List(ctx, creditgrant.ListInput{ + Page: pagination.NewPage(1, 20), + Namespace: ns, + CustomerID: customerID, + Status: status, + }) + s.Require().NoError(err) + + return lo.Map(result.Items, func(item creditpurchase.Charge, _ int) string { + return item.ID + }) +} diff --git a/tools/migrate/migrations/20260709134500_add_credit_purchase_voided_at.down.sql b/tools/migrate/migrations/20260709134500_add_credit_purchase_voided_at.down.sql new file mode 100644 index 0000000000..65024cdc1f --- /dev/null +++ b/tools/migrate/migrations/20260709134500_add_credit_purchase_voided_at.down.sql @@ -0,0 +1,2 @@ +-- reverse: modify "charge_credit_purchases" table +ALTER TABLE "charge_credit_purchases" DROP COLUMN "voided_at"; diff --git a/tools/migrate/migrations/20260709134500_add_credit_purchase_voided_at.up.sql b/tools/migrate/migrations/20260709134500_add_credit_purchase_voided_at.up.sql new file mode 100644 index 0000000000..fb1b349ed0 --- /dev/null +++ b/tools/migrate/migrations/20260709134500_add_credit_purchase_voided_at.up.sql @@ -0,0 +1,2 @@ +-- modify "charge_credit_purchases" table +ALTER TABLE "charge_credit_purchases" ADD COLUMN "voided_at" timestamptz NULL; diff --git a/tools/migrate/migrations/20260709134600_add_ledger_credit_void_records.down.sql b/tools/migrate/migrations/20260709134600_add_ledger_credit_void_records.down.sql new file mode 100644 index 0000000000..2b3a9ac2a8 --- /dev/null +++ b/tools/migrate/migrations/20260709134600_add_ledger_credit_void_records.down.sql @@ -0,0 +1,14 @@ +-- reverse: create index "ledgercreditvoidrecord_namespace_void_transaction_group_id" to table: "ledger_credit_void_records" +DROP INDEX "ledgercreditvoidrecord_namespace_void_transaction_group_id"; +-- reverse: create index "ledgercreditvoidrecord_namespace_source_charge_id" to table: "ledger_credit_void_records" +DROP INDEX "ledgercreditvoidrecord_namespace_source_charge_id"; +-- reverse: create index "ledgercreditvoidrecord_namespace_customer_currency_voided" to table: "ledger_credit_void_records" +DROP INDEX "ledgercreditvoidrecord_namespace_customer_currency_voided"; +-- reverse: create index "ledgercreditvoidrecord_namespace" to table: "ledger_credit_void_records" +DROP INDEX "ledgercreditvoidrecord_namespace"; +-- reverse: create index "ledgercreditvoidrecord_id" to table: "ledger_credit_void_records" +DROP INDEX "ledgercreditvoidrecord_id"; +-- reverse: create index "ledgercreditvoidrecord_annotations" to table: "ledger_credit_void_records" +DROP INDEX "ledgercreditvoidrecord_annotations"; +-- reverse: create "ledger_credit_void_records" table +DROP TABLE "ledger_credit_void_records"; diff --git a/tools/migrate/migrations/20260709134600_add_ledger_credit_void_records.up.sql b/tools/migrate/migrations/20260709134600_add_ledger_credit_void_records.up.sql new file mode 100644 index 0000000000..d133aceaf4 --- /dev/null +++ b/tools/migrate/migrations/20260709134600_add_ledger_credit_void_records.up.sql @@ -0,0 +1,31 @@ +-- create "ledger_credit_void_records" table +CREATE TABLE "ledger_credit_void_records" ( + "id" character(26) NOT NULL, + "namespace" character varying NOT NULL, + "annotations" jsonb NULL, + "created_at" timestamptz NOT NULL, + "updated_at" timestamptz NOT NULL, + "deleted_at" timestamptz NULL, + "amount" numeric NOT NULL, + "customer_id" character(26) NOT NULL, + "currency" character varying(3) NOT NULL, + "voided_at" timestamptz NOT NULL, + "source_charge_id" character(26) NOT NULL, + "void_transaction_group_id" character(26) NOT NULL, + "void_transaction_id" character(26) NOT NULL, + "fbo_sub_account_id" character(26) NOT NULL, + "receivable_sub_account_id" character(26) NOT NULL, + PRIMARY KEY ("id") +); +-- create index "ledgercreditvoidrecord_annotations" to table: "ledger_credit_void_records" +CREATE INDEX "ledgercreditvoidrecord_annotations" ON "ledger_credit_void_records" USING gin ("annotations"); +-- create index "ledgercreditvoidrecord_id" to table: "ledger_credit_void_records" +CREATE UNIQUE INDEX "ledgercreditvoidrecord_id" ON "ledger_credit_void_records" ("id"); +-- create index "ledgercreditvoidrecord_namespace" to table: "ledger_credit_void_records" +CREATE INDEX "ledgercreditvoidrecord_namespace" ON "ledger_credit_void_records" ("namespace"); +-- create index "ledgercreditvoidrecord_namespace_customer_currency_voided" to table: "ledger_credit_void_records" +CREATE INDEX "ledgercreditvoidrecord_namespace_customer_currency_voided" ON "ledger_credit_void_records" ("namespace", "customer_id", "currency", "voided_at", "id"); +-- create index "ledgercreditvoidrecord_namespace_source_charge_id" to table: "ledger_credit_void_records" +CREATE INDEX "ledgercreditvoidrecord_namespace_source_charge_id" ON "ledger_credit_void_records" ("namespace", "source_charge_id"); +-- create index "ledgercreditvoidrecord_namespace_void_transaction_group_id" to table: "ledger_credit_void_records" +CREATE INDEX "ledgercreditvoidrecord_namespace_void_transaction_group_id" ON "ledger_credit_void_records" ("namespace", "void_transaction_group_id"); diff --git a/tools/migrate/migrations/atlas.sum b/tools/migrate/migrations/atlas.sum index 9f053b75ba..d6dfe3b95a 100644 --- a/tools/migrate/migrations/atlas.sum +++ b/tools/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:B9MwpYFJh32EmzJgZD3xN/pXUnEtnug0bOLzTB7wDdw= +h1:dYcsKJwoTpQcsxSs+06+R73lsXc+cAa5+rW89lgBizo= 20240826120919_init.up.sql h1:tc1V91/smlmaeJGQ8h+MzTEeFjjnrrFDbDAjOYJK91o= 20240903155435_entitlement-expired-index.up.sql h1:Hp8u5uckmLXc1cRvWU0AtVnnK8ShlpzZNp8pbiJLhac= 20240917172257_billing-entities.up.sql h1:Q1dAMo0Vjiit76OybClNfYPGC5nmvov2/M2W1ioi4Kw= @@ -227,3 +227,5 @@ h1:B9MwpYFJh32EmzJgZD3xN/pXUnEtnug0bOLzTB7wDdw= 20260707142444_om_399_subscription_unit_config.up.sql h1:NfIgaPJWptTtGaxyMzwUV/jts/Z6AVvbp3COxEgDOtQ= 20260709132435_repair_usagebased_deleted_invoice_runs.up.sql h1:4Wo2RUrKLTkYqOqrRyXku75I04OEuycJfJbDqEo3kfI= 20260709134422_add_ledger_internal_fks.up.sql h1:JE6z1+A8Vg64F0hwyr3RnoLXu/BdFKf3JD6wLJM9LG0= +20260709134500_add_credit_purchase_voided_at.up.sql h1:+LXY/WI+kvHKOCh4VQXdswI7O+PPnqHKk/o7snTxlxk= +20260709134600_add_ledger_credit_void_records.up.sql h1:o5C0cNz9b50BMgQcn1lp3BtlBYx8ETeOs2m8Ck6OiXc=