diff --git a/api/spec/packages/aip-client-javascript/README.md b/api/spec/packages/aip-client-javascript/README.md index d3031a3577..7a6d557137 100644 --- a/api/spec/packages/aip-client-javascript/README.md +++ b/api/spec/packages/aip-client-javascript/README.md @@ -41,6 +41,7 @@ TypeSpec definitions and ships fully-typed request and response models. - [EntitlementAccess](#entitlementaccess) - [Notifications](#notifications) - [Internal Operations](#internal-operations) + - [Internal Customers](#internal-customers) - [Internal Subscriptions](#internal-subscriptions) - [Internal Apps](#internal-apps) - [Internal Notifications](#internal-notifications) @@ -498,6 +499,12 @@ Operations marked internal in the API definition are exposed under intended for customer use: they may require additional permissions, and they can change or be removed without notice or semver consideration. +### Internal Customers + +| Method | HTTP | Description | +| ------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `client.internal.customers.charges.invoice` | `POST /openmeter/customers/{customerId}/charges/invoice` | Invoice a customer's charges outside the normal billing cycle. Collection alignment is bypassed. The customer's progressive billing setting applies unless explicitly overridden. All eligible pending lines are considered. Returns the created standard invoices, one per collected currency. Lines blocked by their owning engine remain pending. If all eligible lines are blocked or no lines are eligible, returns a validation error. | + ### Internal Subscriptions | Method | HTTP | Description | 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 8e12027bf9..1bd8ed111c 100644 --- a/api/spec/packages/aip-client-javascript/src/funcs/customers.ts +++ b/api/spec/packages/aip-client-javascript/src/funcs/customers.ts @@ -73,6 +73,8 @@ import type { ListCustomerChargesResponse, CreateCustomerChargesRequest, CreateCustomerChargesResponse, + InvoiceCustomerChargesRequest, + InvoiceCustomerChargesResponse, } from '../models/operations/customers.js' /** @@ -1607,3 +1609,53 @@ export function createCustomerCharges( }) }) } + +/** + * Invoice a customer's charges + * + * Invoice a customer's charges outside the normal billing cycle. Collection + * alignment is bypassed. The customer's progressive billing setting applies unless + * explicitly overridden. All eligible pending lines are considered. + * + * Returns the created standard invoices, one per collected currency. Lines blocked + * by their owning engine remain pending. If all eligible lines are blocked or no + * lines are eligible, returns a validation error. + * + * POST /openmeter/customers/{customerId}/charges/invoice + */ +export function invoiceCustomerCharges( + client: Client, + req: InvoiceCustomerChargesRequest, + options?: RequestOptions, +): Promise> { + return request(() => { + const pathParamsInput = { + customerId: req.customerId, + } + const pathParams = client._options.validate + ? toPathWire(pathParamsInput, schemas.invoiceCustomerChargesPathParams) + : pathParamsInput + if (client._options.validate) { + assertValid(schemas.invoiceCustomerChargesPathParamsWire, pathParams) + } + const path = `openmeter/customers/${(() => { + if (pathParams.customerId === undefined) { + throw new Error('missing path parameter: customerId') + } + return encodeURIComponent(String(pathParams.customerId)) + })()}/charges/invoice` + const body = toWire(req.body, schemas.invoiceCustomerChargesBody) + if (client._options.validate && body !== undefined) { + assertValid(schemas.invoiceCustomerChargesBodyWire, body) + } + return http(client) + .post(path, { ...options, json: body }) + .json() + .then((data) => { + if (client._options.validate) { + assertValid(schemas.invoiceCustomerChargesResponseWire, data) + } + return fromWire(data, schemas.invoiceCustomerChargesResponse) + }) + }) +} diff --git a/api/spec/packages/aip-client-javascript/src/index.ts b/api/spec/packages/aip-client-javascript/src/index.ts index 7cb416fc5c..15ce587180 100644 --- a/api/spec/packages/aip-client-javascript/src/index.ts +++ b/api/spec/packages/aip-client-javascript/src/index.ts @@ -91,6 +91,7 @@ export type { WorkflowPaymentChargeAutomaticallySettings, WorkflowPaymentSendInvoiceSettings, ChargeFlatFeeDiscounts, + InvoiceLineExternalReferences, SubscriptionEditRemoveItem, SubscriptionEditUnscheduleEdit, UpdateLabels, @@ -99,7 +100,6 @@ export type { InstallAppSandbox, InstallAppExternalInvoicing, WorkflowCollectionAlignmentSubscription, - InvoiceLineExternalReferences, UpdateBillingInvoiceWorkflowInvoicingSettings, UpdateBillingWorkflowPaymentChargeAutomaticallySettings, UpdateBillingWorkflowPaymentSendInvoiceSettings, @@ -166,8 +166,8 @@ export type { SubscriptionReference, AppReference, ChargeRealizationInvoiceReference, - AddonReference, ChargeReference, + AddonReference, UpdateResourceReference, NotificationChannelReference, NotificationEventFeatureReference, @@ -179,6 +179,7 @@ export type { AppStripeCreateCustomerPortalSessionResult, ClosedPeriod, ChargeResolvedCostBasis, + InvoiceCustomerChargesInput, SubscriptionAddonTimelineSegment, UpdateClosedPeriod, CostBasis, @@ -238,6 +239,9 @@ export type { UpdateBillingPartyTaxIdentity, InvoiceAvailableActions, ChargeRealizationPayment, + InvoiceLineAmountDiscount, + InvoiceLineUsageDiscount, + InvoiceLineBaseDiscount, SubscriptionEditRemovePhase, TaxCodeAppMapping, AppCapability, @@ -246,9 +250,6 @@ export type { UpdateAppSandboxRequest, UpdateAppExternalInvoicingRequest, WorkflowInvoicingSettings, - InvoiceLineAmountDiscount, - InvoiceLineUsageDiscount, - InvoiceLineBaseDiscount, ListCurrenciesParamsFilter, EntitlementAccessQueryRequest, EntitlementFeatureAccessReason, @@ -342,12 +343,12 @@ export type { SubscriptionBase, InvoiceStatusDetails, InvoiceWorkflow, + InvoiceLineDiscounts, SubscriptionCancel, SubscriptionMigrate, SubscriptionAddonUpdate, TaxCode, AppCatalogItem, - InvoiceLineDiscounts, UpdateBillingInvoiceWorkflow, EntitlementFeatureAccess, NotificationChannelPagePaginatedResponse, @@ -386,6 +387,7 @@ export type { CreateEntitlementMeteredRequest, EntitlementGrantPagePaginatedResponse, InvoiceWorkflowSettings, + InvoiceDetailedLine, TaxCodePagePaginatedResponse, AppStripe, AppSandbox, @@ -394,7 +396,6 @@ export type { InstalledAppStripe, InstalledAppSandbox, InstalledAppExternalInvoicing, - InvoiceDetailedLine, UpdateInvoiceWorkflowSettings, EntitlementAccessQueryResult, NotificationEventBalanceThresholdPayload, @@ -453,6 +454,7 @@ export type { SubscriptionMigrateResponse, ChargeFlatFee, ChargeUsageBased, + InvoiceCustomerChargesResult, InvoicePagePaginatedResponse, ChargePagePaginatedResponse, StringFieldFilter, @@ -604,6 +606,7 @@ export type { SubscriptionMigrateResponseInput, ChargeFlatFeeInput, ChargeUsageBasedInput, + InvoiceCustomerChargesResultInput, InvoicePagePaginatedResponseInput, ChargePagePaginatedResponseInput, WorkflowPaymentSettingsInput, 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 5ee8491074..1f4d42232d 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,8 @@ import type { EntitlementHistoryWindowSize, EntitlementPagePaginatedResponse, GetCreditBalanceParamsFilter, + InvoiceCustomerChargesInput, + InvoiceCustomerChargesResult, ListCreditGrantsParamsFilter, ListCreditTransactionsParamsFilter, ListCustomerChargesParamsFilter, @@ -391,3 +393,9 @@ export type CreateCustomerChargesRequest = AcceptDateStrings<{ body: CreateChargeRequestInput }> export type CreateCustomerChargesResponse = Charge + +export type InvoiceCustomerChargesRequest = AcceptDateStrings<{ + customerId: string + body?: InvoiceCustomerChargesInput +}> +export type InvoiceCustomerChargesResponse = InvoiceCustomerChargesResult 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 65d9224d33..5425c057c8 100644 --- a/api/spec/packages/aip-client-javascript/src/models/schemas.ts +++ b/api/spec/packages/aip-client-javascript/src/models/schemas.ts @@ -983,6 +983,26 @@ export const featureLlmTokenType = z ]) .describe('Token type for LLM cost lookup.') +export const invoiceDiscountReason = z + .enum(['maximum_spend', 'ratecard_percentage', 'ratecard_usage']) + .describe('The reason a discount was applied to an invoice line.') + +export const invoiceLineExternalReferences = z + .object({ + invoicingId: z + .string() + .optional() + .describe('The ID assigned by the external invoicing app.'), + }) + + .describe( + 'External identifiers for an invoice line item assigned by third-party systems.', + ) + +export const invoiceDetailedLineCostCategory = z + .enum(['regular', 'commitment']) + .describe('Cost category of a detailed invoice line item.') + export const subscriptionCreateTimingEnum = z .enum(['immediate']) @@ -1127,26 +1147,6 @@ export const workflowInvoicingSubscriptionEndProrationMode = z .enum(['bill_full_period', 'bill_actual_period']) .describe('Billing workflow subscription end proration mode.') -export const invoiceDiscountReason = z - .enum(['maximum_spend', 'ratecard_percentage', 'ratecard_usage']) - .describe('The reason a discount was applied to an invoice line.') - -export const invoiceLineExternalReferences = z - .object({ - invoicingId: z - .string() - .optional() - .describe('The ID assigned by the external invoicing app.'), - }) - - .describe( - 'External identifiers for an invoice line item assigned by third-party systems.', - ) - -export const invoiceDetailedLineCostCategory = z - .enum(['regular', 'commitment']) - .describe('Cost category of a detailed invoice line item.') - export const updateBillingInvoiceWorkflowInvoicingSettings = z .object({ autoAdvance: z @@ -2320,17 +2320,17 @@ export const chargeRealizationInvoiceReference = z }) .describe('ChargeRealizationInvoice reference.') -export const addonReference = z +export const chargeReference = z .object({ id: ulid, }) - .describe('Addon reference.') + .describe('Reference to a charge associated with an invoice line.') -export const chargeReference = z +export const addonReference = z .object({ id: ulid, }) - .describe('Reference to a charge associated with an invoice line.') + .describe('Addon reference.') export const updateResourceReference = z .object({ @@ -2570,6 +2570,22 @@ export const chargeResolvedCostBasis = z 'Fiat conversion rate a custom-currency charge is invoiced at. Present once the cost basis is resolved; dynamic cost bases are exposed only after the service period has started.', ) +export const invoiceCustomerChargesInput = z + .object({ + asOf: dateTime.optional(), + progressiveBillingOverride: z + .boolean() + .optional() + + .describe( + "Override the customer's progressive billing setting for this collection. When enabled, eligible usage-based lines can be collected for a partial period.", + ), + }) + + .describe( + "Options for manually collecting a customer's pending lines into invoices.", + ) + export const subscriptionAddonTimelineSegment = z .object({ activeFrom: dateTime, @@ -3514,6 +3530,44 @@ export const chargeRealizationPayment = z }) .describe('Payment state of a charge realization.') +export const invoiceLineAmountDiscount = z + .object({ + id: ulid, + reason: invoiceDiscountReason, + description: z + .string() + .optional() + .describe('Optional human-readable description of the discount.'), + externalReferences: invoiceLineExternalReferences.optional(), + amount: numeric, + }) + .describe('A monetary amount discount applied to an invoice line item.') + +export const invoiceLineUsageDiscount = z + .object({ + id: ulid, + reason: invoiceDiscountReason, + description: z + .string() + .optional() + .describe('Optional human-readable description of the discount.'), + externalReferences: invoiceLineExternalReferences.optional(), + quantity: numeric, + }) + .describe('A usage quantity discount applied to an invoice line item.') + +export const invoiceLineBaseDiscount = z + .object({ + id: ulid, + reason: invoiceDiscountReason, + description: z + .string() + .optional() + .describe('Optional human-readable description of the discount.'), + externalReferences: invoiceLineExternalReferences.optional(), + }) + .describe('Base fields shared by all invoice line item discounts.') + export const subscriptionCreateTiming = z .union([subscriptionCreateTimingEnum, dateTime]) @@ -3680,44 +3734,6 @@ export const workflowInvoicingSettings = z }) .describe('Invoice settings for a billing workflow.') -export const invoiceLineAmountDiscount = z - .object({ - id: ulid, - reason: invoiceDiscountReason, - description: z - .string() - .optional() - .describe('Optional human-readable description of the discount.'), - externalReferences: invoiceLineExternalReferences.optional(), - amount: numeric, - }) - .describe('A monetary amount discount applied to an invoice line item.') - -export const invoiceLineUsageDiscount = z - .object({ - id: ulid, - reason: invoiceDiscountReason, - description: z - .string() - .optional() - .describe('Optional human-readable description of the discount.'), - externalReferences: invoiceLineExternalReferences.optional(), - quantity: numeric, - }) - .describe('A usage quantity discount applied to an invoice line item.') - -export const invoiceLineBaseDiscount = z - .object({ - id: ulid, - reason: invoiceDiscountReason, - description: z - .string() - .optional() - .describe('Optional human-readable description of the discount.'), - externalReferences: invoiceLineExternalReferences.optional(), - }) - .describe('Base fields shared by all invoice line item discounts.') - export const updateBillingWorkflowPaymentSettings = z .discriminatedUnion('collectionMethod', [ updateBillingWorkflowPaymentChargeAutomaticallySettings, @@ -5552,6 +5568,22 @@ export const invoiceWorkflow = z 'Invoice-level snapshot of the workflow configuration. Contains only the settings that are meaningful for an already-created invoice: invoicing behaviour and payment settings. Collection alignment and tax policy are gather-time / profile-wide concerns and are not included.', ) +export const invoiceLineDiscounts = z + .object({ + amount: z + .array(invoiceLineAmountDiscount) + .optional() + + .describe( + 'Monetary amount discounts (e.g. from maximum spend commitments).', + ), + usage: z + .array(invoiceLineUsageDiscount) + .optional() + .describe('Usage quantity discounts (e.g. free tier usage allowances).'), + }) + .describe('Discounts applied to an invoice line item.') + export const subscriptionCancel = z .object({ timing: subscriptionEditTiming.optional().default('immediate'), @@ -5713,22 +5745,6 @@ export const updateAppRequest = z ]) .describe('Request to update an installed app.') -export const invoiceLineDiscounts = z - .object({ - amount: z - .array(invoiceLineAmountDiscount) - .optional() - - .describe( - 'Monetary amount discounts (e.g. from maximum spend commitments).', - ), - usage: z - .array(invoiceLineUsageDiscount) - .optional() - .describe('Usage quantity discounts (e.g. free tier usage allowances).'), - }) - .describe('Discounts applied to an invoice line item.') - export const updateBillingInvoiceWorkflow = z .object({ invoicing: updateBillingInvoiceWorkflowInvoicingSettings.optional(), @@ -6445,6 +6461,43 @@ export const invoiceWorkflowSettings = z 'Snapshot of the billing workflow configuration captured at invoice creation.', ) +export const invoiceDetailedLine = z + .object({ + id: ulid, + name: z + .string() + .min(1) + .max(256) + .describe('Display name of the resource. Between 1 and 256 characters.'), + description: z + .string() + .max(1024) + .optional() + + .describe( + 'Optional description of the resource. Maximum 1024 characters.', + ), + labels: labels.optional(), + createdAt: dateTime, + updatedAt: dateTime, + deletedAt: dateTime.optional(), + servicePeriod: closedPeriod, + totals: totals, + category: invoiceDetailedLineCostCategory.default('regular'), + discounts: invoiceLineDiscounts.optional(), + creditsApplied: z + .array(invoiceLineCreditsApplied) + .optional() + .describe('Credit applied to this detailed line.'), + externalReferences: invoiceLineExternalReferences.optional(), + quantity: numeric, + unitPrice: numeric, + }) + + .describe( + 'A detailed (child) sub-line belonging to a parent invoice line. Detailed lines represent the individual flat-fee components that make up a usage-based parent line after quantity snapshotting.', + ) + export const taxCodePagePaginatedResponse = z .object({ data: z.array(taxCode), @@ -6729,43 +6782,6 @@ export const installedAppExternalInvoicing = z }) .describe('Response of an installed app.') -export const invoiceDetailedLine = z - .object({ - id: ulid, - name: z - .string() - .min(1) - .max(256) - .describe('Display name of the resource. Between 1 and 256 characters.'), - description: z - .string() - .max(1024) - .optional() - - .describe( - 'Optional description of the resource. Maximum 1024 characters.', - ), - labels: labels.optional(), - createdAt: dateTime, - updatedAt: dateTime, - deletedAt: dateTime.optional(), - servicePeriod: closedPeriod, - totals: totals, - category: invoiceDetailedLineCostCategory.default('regular'), - discounts: invoiceLineDiscounts.optional(), - creditsApplied: z - .array(invoiceLineCreditsApplied) - .optional() - .describe('Credit applied to this detailed line.'), - externalReferences: invoiceLineExternalReferences.optional(), - quantity: numeric, - unitPrice: numeric, - }) - - .describe( - 'A detailed (child) sub-line belonging to a parent invoice line. Detailed lines represent the individual flat-fee components that make up a usage-based parent line after quantity snapshotting.', - ) - export const updateInvoiceWorkflowSettings = z .object({ workflow: updateBillingInvoiceWorkflow, @@ -8425,6 +8441,17 @@ export const chargeUsageBased = z }) .describe('A usage-based charge for a customer.') +export const invoiceCustomerChargesResult = z + .object({ + invoices: z + .array(invoice) + .describe('Created standard invoices, one per collected currency.'), + }) + + .describe( + "Result of manually collecting a customer's pending lines into invoices.", + ) + export const invoicePagePaginatedResponse = z .object({ data: z.array(invoice), @@ -8970,6 +8997,14 @@ export const createCustomerChargesBody = createChargeRequest export const createCustomerChargesResponse = charge +export const invoiceCustomerChargesPathParams = z.object({ + customerId: ulid, +}) + +export const invoiceCustomerChargesBody = invoiceCustomerChargesInput + +export const invoiceCustomerChargesResponse = invoiceCustomerChargesResult + export const createSubscriptionBody = subscriptionCreate export const createSubscriptionResponse = subscription @@ -10815,6 +10850,26 @@ export const featureLlmTokenTypeWire = z ]) .describe('Token type for LLM cost lookup.') +export const invoiceDiscountReasonWire = z + .enum(['maximum_spend', 'ratecard_percentage', 'ratecard_usage']) + .describe('The reason a discount was applied to an invoice line.') + +export const invoiceLineExternalReferencesWire = z + .strictObject({ + invoicing_id: z + .string() + .optional() + .describe('The ID assigned by the external invoicing app.'), + }) + + .describe( + 'External identifiers for an invoice line item assigned by third-party systems.', + ) + +export const invoiceDetailedLineCostCategoryWire = z + .enum(['regular', 'commitment']) + .describe('Cost category of a detailed invoice line item.') + export const subscriptionCreateTimingEnumWire = z .enum(['immediate']) @@ -10959,26 +11014,6 @@ export const workflowInvoicingSubscriptionEndProrationModeWire = z .enum(['bill_full_period', 'bill_actual_period']) .describe('Billing workflow subscription end proration mode.') -export const invoiceDiscountReasonWire = z - .enum(['maximum_spend', 'ratecard_percentage', 'ratecard_usage']) - .describe('The reason a discount was applied to an invoice line.') - -export const invoiceLineExternalReferencesWire = z - .strictObject({ - invoicing_id: z - .string() - .optional() - .describe('The ID assigned by the external invoicing app.'), - }) - - .describe( - 'External identifiers for an invoice line item assigned by third-party systems.', - ) - -export const invoiceDetailedLineCostCategoryWire = z - .enum(['regular', 'commitment']) - .describe('Cost category of a detailed invoice line item.') - export const updateBillingInvoiceWorkflowInvoicingSettingsWire = z .strictObject({ auto_advance: z @@ -12148,17 +12183,17 @@ export const chargeRealizationInvoiceReferenceWire = z }) .describe('ChargeRealizationInvoice reference.') -export const addonReferenceWire = z +export const chargeReferenceWire = z .strictObject({ id: ulidWire, }) - .describe('Addon reference.') + .describe('Reference to a charge associated with an invoice line.') -export const chargeReferenceWire = z +export const addonReferenceWire = z .strictObject({ id: ulidWire, }) - .describe('Reference to a charge associated with an invoice line.') + .describe('Addon reference.') export const updateResourceReferenceWire = z .strictObject({ @@ -12396,6 +12431,22 @@ export const chargeResolvedCostBasisWire = z 'Fiat conversion rate a custom-currency charge is invoiced at. Present once the cost basis is resolved; dynamic cost bases are exposed only after the service period has started.', ) +export const invoiceCustomerChargesInputWire = z + .strictObject({ + as_of: dateTimeWire.optional(), + progressive_billing_override: z + .boolean() + .optional() + + .describe( + "Override the customer's progressive billing setting for this collection. When enabled, eligible usage-based lines can be collected for a partial period.", + ), + }) + + .describe( + "Options for manually collecting a customer's pending lines into invoices.", + ) + export const subscriptionAddonTimelineSegmentWire = z .strictObject({ active_from: dateTimeWire, @@ -13332,6 +13383,44 @@ export const chargeRealizationPaymentWire = z }) .describe('Payment state of a charge realization.') +export const invoiceLineAmountDiscountWire = z + .strictObject({ + id: ulidWire, + reason: invoiceDiscountReasonWire, + description: z + .string() + .optional() + .describe('Optional human-readable description of the discount.'), + external_references: invoiceLineExternalReferencesWire.optional(), + amount: numericWire, + }) + .describe('A monetary amount discount applied to an invoice line item.') + +export const invoiceLineUsageDiscountWire = z + .strictObject({ + id: ulidWire, + reason: invoiceDiscountReasonWire, + description: z + .string() + .optional() + .describe('Optional human-readable description of the discount.'), + external_references: invoiceLineExternalReferencesWire.optional(), + quantity: numericWire, + }) + .describe('A usage quantity discount applied to an invoice line item.') + +export const invoiceLineBaseDiscountWire = z + .strictObject({ + id: ulidWire, + reason: invoiceDiscountReasonWire, + description: z + .string() + .optional() + .describe('Optional human-readable description of the discount.'), + external_references: invoiceLineExternalReferencesWire.optional(), + }) + .describe('Base fields shared by all invoice line item discounts.') + export const subscriptionCreateTimingWire = z .union([subscriptionCreateTimingEnumWire, dateTimeWire]) @@ -13496,44 +13585,6 @@ export const workflowInvoicingSettingsWire = z }) .describe('Invoice settings for a billing workflow.') -export const invoiceLineAmountDiscountWire = z - .strictObject({ - id: ulidWire, - reason: invoiceDiscountReasonWire, - description: z - .string() - .optional() - .describe('Optional human-readable description of the discount.'), - external_references: invoiceLineExternalReferencesWire.optional(), - amount: numericWire, - }) - .describe('A monetary amount discount applied to an invoice line item.') - -export const invoiceLineUsageDiscountWire = z - .strictObject({ - id: ulidWire, - reason: invoiceDiscountReasonWire, - description: z - .string() - .optional() - .describe('Optional human-readable description of the discount.'), - external_references: invoiceLineExternalReferencesWire.optional(), - quantity: numericWire, - }) - .describe('A usage quantity discount applied to an invoice line item.') - -export const invoiceLineBaseDiscountWire = z - .strictObject({ - id: ulidWire, - reason: invoiceDiscountReasonWire, - description: z - .string() - .optional() - .describe('Optional human-readable description of the discount.'), - external_references: invoiceLineExternalReferencesWire.optional(), - }) - .describe('Base fields shared by all invoice line item discounts.') - export const updateBillingWorkflowPaymentSettingsWire = z .discriminatedUnion('collection_method', [ updateBillingWorkflowPaymentChargeAutomaticallySettingsWire, @@ -15355,6 +15406,22 @@ export const invoiceWorkflowWire = z 'Invoice-level snapshot of the workflow configuration. Contains only the settings that are meaningful for an already-created invoice: invoicing behaviour and payment settings. Collection alignment and tax policy are gather-time / profile-wide concerns and are not included.', ) +export const invoiceLineDiscountsWire = z + .strictObject({ + amount: z + .array(invoiceLineAmountDiscountWire) + .optional() + + .describe( + 'Monetary amount discounts (e.g. from maximum spend commitments).', + ), + usage: z + .array(invoiceLineUsageDiscountWire) + .optional() + .describe('Usage quantity discounts (e.g. free tier usage allowances).'), + }) + .describe('Discounts applied to an invoice line item.') + export const subscriptionCancelWire = z .strictObject({ timing: subscriptionEditTimingWire.optional(), @@ -15518,22 +15585,6 @@ export const updateAppRequestWire = z ]) .describe('Request to update an installed app.') -export const invoiceLineDiscountsWire = z - .strictObject({ - amount: z - .array(invoiceLineAmountDiscountWire) - .optional() - - .describe( - 'Monetary amount discounts (e.g. from maximum spend commitments).', - ), - usage: z - .array(invoiceLineUsageDiscountWire) - .optional() - .describe('Usage quantity discounts (e.g. free tier usage allowances).'), - }) - .describe('Discounts applied to an invoice line item.') - export const updateBillingInvoiceWorkflowWire = z .strictObject({ invoicing: updateBillingInvoiceWorkflowInvoicingSettingsWire.optional(), @@ -16243,6 +16294,43 @@ export const invoiceWorkflowSettingsWire = z 'Snapshot of the billing workflow configuration captured at invoice creation.', ) +export const invoiceDetailedLineWire = z + .strictObject({ + id: ulidWire, + name: z + .string() + .min(1) + .max(256) + .describe('Display name of the resource. Between 1 and 256 characters.'), + description: z + .string() + .max(1024) + .optional() + + .describe( + 'Optional description of the resource. Maximum 1024 characters.', + ), + labels: labelsWire.optional(), + created_at: dateTimeWire, + updated_at: dateTimeWire, + deleted_at: dateTimeWire.optional(), + service_period: closedPeriodWire, + totals: totalsWire, + category: invoiceDetailedLineCostCategoryWire, + discounts: invoiceLineDiscountsWire.optional(), + credits_applied: z + .array(invoiceLineCreditsAppliedWire) + .optional() + .describe('Credit applied to this detailed line.'), + external_references: invoiceLineExternalReferencesWire.optional(), + quantity: numericWire, + unit_price: numericWire, + }) + + .describe( + 'A detailed (child) sub-line belonging to a parent invoice line. Detailed lines represent the individual flat-fee components that make up a usage-based parent line after quantity snapshotting.', + ) + export const taxCodePagePaginatedResponseWire = z .strictObject({ data: z.array(taxCodeWire), @@ -16527,43 +16615,6 @@ export const installedAppExternalInvoicingWire = z }) .describe('Response of an installed app.') -export const invoiceDetailedLineWire = z - .strictObject({ - id: ulidWire, - name: z - .string() - .min(1) - .max(256) - .describe('Display name of the resource. Between 1 and 256 characters.'), - description: z - .string() - .max(1024) - .optional() - - .describe( - 'Optional description of the resource. Maximum 1024 characters.', - ), - labels: labelsWire.optional(), - created_at: dateTimeWire, - updated_at: dateTimeWire, - deleted_at: dateTimeWire.optional(), - service_period: closedPeriodWire, - totals: totalsWire, - category: invoiceDetailedLineCostCategoryWire, - discounts: invoiceLineDiscountsWire.optional(), - credits_applied: z - .array(invoiceLineCreditsAppliedWire) - .optional() - .describe('Credit applied to this detailed line.'), - external_references: invoiceLineExternalReferencesWire.optional(), - quantity: numericWire, - unit_price: numericWire, - }) - - .describe( - 'A detailed (child) sub-line belonging to a parent invoice line. Detailed lines represent the individual flat-fee components that make up a usage-based parent line after quantity snapshotting.', - ) - export const updateInvoiceWorkflowSettingsWire = z .strictObject({ workflow: updateBillingInvoiceWorkflowWire, @@ -18220,6 +18271,17 @@ export const chargeUsageBasedWire = z }) .describe('A usage-based charge for a customer.') +export const invoiceCustomerChargesResultWire = z + .strictObject({ + invoices: z + .array(invoiceWire) + .describe('Created standard invoices, one per collected currency.'), + }) + + .describe( + "Result of manually collecting a customer's pending lines into invoices.", + ) + export const invoicePagePaginatedResponseWire = z .strictObject({ data: z.array(invoiceWire), @@ -18826,6 +18888,15 @@ export const createCustomerChargesBodyWire = createChargeRequestWire export const createCustomerChargesResponseWire = chargeWire +export const invoiceCustomerChargesPathParamsWire = z.object({ + customerId: ulidWire, +}) + +export const invoiceCustomerChargesBodyWire = invoiceCustomerChargesInputWire + +export const invoiceCustomerChargesResponseWire = + invoiceCustomerChargesResultWire + export const createSubscriptionBodyWire = subscriptionCreateWire export const createSubscriptionResponseWire = subscriptionWire 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 198f35fefa..d8f804f62b 100644 --- a/api/spec/packages/aip-client-javascript/src/models/types.ts +++ b/api/spec/packages/aip-client-javascript/src/models/types.ts @@ -276,6 +276,12 @@ export interface ChargeFlatFeeDiscounts { percentage?: number } +/** External identifiers for an invoice line item assigned by third-party systems. */ +export interface InvoiceLineExternalReferences { + /** The ID assigned by the external invoicing app. */ + invoicingId?: string +} + /** Remove a rate card from a phase. */ export interface SubscriptionEditRemoveItem { /** Discriminator for the remove-item operation. */ @@ -363,12 +369,6 @@ export interface WorkflowCollectionAlignmentSubscription { type: 'subscription' } -/** External identifiers for an invoice line item assigned by third-party systems. */ -export interface InvoiceLineExternalReferences { - /** The ID assigned by the external invoicing app. */ - invoicingId?: string -} - /** * Invoice-level invoicing settings. * @@ -1133,17 +1133,17 @@ export interface ChargeRealizationInvoiceReference { id: string } -/** Addon reference. */ -export interface AddonReference { - id: string -} - /** Reference to a charge associated with an invoice line. */ export interface ChargeReference { /** Unique identifier for the charge. */ id: string } +/** Addon reference. */ +export interface AddonReference { + id: string +} + /** TaxCode reference. */ export interface UpdateResourceReference { id: string @@ -1370,6 +1370,17 @@ export interface ChargeResolvedCostBasis { resolvedAt: Date } +/** Options for manually collecting a customer's pending lines into invoices. */ +export interface InvoiceCustomerChargesInput { + /** Collection cutoff. Defaults to the current time and cannot be in the future. */ + asOf?: Date + /** + * Override the customer's progressive billing setting for this collection. When + * enabled, eligible usage-based lines can be collected for a partial period. + */ + progressiveBillingOverride?: boolean +} + /** A subscription add-on event. */ export interface SubscriptionAddonTimelineSegment { /** An ISO-8601 timestamp representation of the cadence start of the resource. */ @@ -2335,6 +2346,46 @@ export interface ChargeRealizationPayment { status: 'authorized' | 'settled' } +/** A monetary amount discount applied to an invoice line item. */ +export interface InvoiceLineAmountDiscount { + /** Unique identifier for the discount. */ + id: string + /** The reason this discount was applied. */ + reason: 'maximum_spend' | 'ratecard_percentage' | 'ratecard_usage' + /** Optional human-readable description of the discount. */ + description?: string + /** External identifiers for this discount. */ + externalReferences?: InvoiceLineExternalReferences + /** The monetary amount deducted. */ + amount: string +} + +/** A usage quantity discount applied to an invoice line item. */ +export interface InvoiceLineUsageDiscount { + /** Unique identifier for the discount. */ + id: string + /** The reason this discount was applied. */ + reason: 'maximum_spend' | 'ratecard_percentage' | 'ratecard_usage' + /** Optional human-readable description of the discount. */ + description?: string + /** External identifiers for this discount. */ + externalReferences?: InvoiceLineExternalReferences + /** The usage quantity deducted (in billing units). */ + quantity: string +} + +/** Base fields shared by all invoice line item discounts. */ +export interface InvoiceLineBaseDiscount { + /** Unique identifier for the discount. */ + id: string + /** The reason this discount was applied. */ + reason: 'maximum_spend' | 'ratecard_percentage' | 'ratecard_usage' + /** Optional human-readable description of the discount. */ + description?: string + /** External identifiers for this discount. */ + externalReferences?: InvoiceLineExternalReferences +} + /** Remove a phase from the subscription. */ export interface SubscriptionEditRemovePhase { /** Discriminator for the remove-phase operation. */ @@ -2473,46 +2524,6 @@ export interface WorkflowInvoicingSettings { subscriptionEndProrationMode: 'bill_full_period' | 'bill_actual_period' } -/** A monetary amount discount applied to an invoice line item. */ -export interface InvoiceLineAmountDiscount { - /** Unique identifier for the discount. */ - id: string - /** The reason this discount was applied. */ - reason: 'maximum_spend' | 'ratecard_percentage' | 'ratecard_usage' - /** Optional human-readable description of the discount. */ - description?: string - /** External identifiers for this discount. */ - externalReferences?: InvoiceLineExternalReferences - /** The monetary amount deducted. */ - amount: string -} - -/** A usage quantity discount applied to an invoice line item. */ -export interface InvoiceLineUsageDiscount { - /** Unique identifier for the discount. */ - id: string - /** The reason this discount was applied. */ - reason: 'maximum_spend' | 'ratecard_percentage' | 'ratecard_usage' - /** Optional human-readable description of the discount. */ - description?: string - /** External identifiers for this discount. */ - externalReferences?: InvoiceLineExternalReferences - /** The usage quantity deducted (in billing units). */ - quantity: string -} - -/** Base fields shared by all invoice line item discounts. */ -export interface InvoiceLineBaseDiscount { - /** Unique identifier for the discount. */ - id: string - /** The reason this discount was applied. */ - reason: 'maximum_spend' | 'ratecard_percentage' | 'ratecard_usage' - /** Optional human-readable description of the discount. */ - description?: string - /** External identifiers for this discount. */ - externalReferences?: InvoiceLineExternalReferences -} - /** Filter options for listing currencies. */ export interface ListCurrenciesParamsFilter { type?: 'fiat' | 'custom' @@ -4574,6 +4585,14 @@ export interface InvoiceWorkflow { payment?: WorkflowPaymentSettings } +/** Discounts applied to an invoice line item. */ +export interface InvoiceLineDiscounts { + /** Monetary amount discounts (e.g. from maximum spend commitments). */ + amount?: InvoiceLineAmountDiscount[] + /** Usage quantity discounts (e.g. free tier usage allowances). */ + usage?: InvoiceLineUsageDiscount[] +} + /** Request for canceling a subscription. */ export interface SubscriptionCancel { /** If not provided the subscription is canceled immediately. */ @@ -4721,14 +4740,6 @@ export interface AppCatalogItem { installMethods: ('with_oauth2' | 'with_api_key' | 'no_credentials_required')[] } -/** Discounts applied to an invoice line item. */ -export interface InvoiceLineDiscounts { - /** Monetary amount discounts (e.g. from maximum spend commitments). */ - amount?: InvoiceLineAmountDiscount[] - /** Usage quantity discounts (e.g. free tier usage allowances). */ - usage?: InvoiceLineUsageDiscount[] -} - /** * Invoice-level snapshot of the workflow configuration. * @@ -5502,6 +5513,51 @@ export interface InvoiceWorkflowSettings { workflow: InvoiceWorkflow } +/** + * A detailed (child) sub-line belonging to a parent invoice line. + * + * Detailed lines represent the individual flat-fee components that make up a + * usage-based parent line after quantity snapshotting. + */ +export interface InvoiceDetailedLine { + id: string + /** + * Display name of the resource. + * + * Between 1 and 256 characters. + */ + name: string + /** + * Optional description of the resource. + * + * Maximum 1024 characters. + */ + description?: string + labels?: Labels + /** An ISO-8601 timestamp representation of entity creation date. */ + createdAt: Date + /** An ISO-8601 timestamp representation of entity last update date. */ + updatedAt: Date + /** An ISO-8601 timestamp representation of entity deletion date. */ + deletedAt?: Date + /** The service period covered by this detailed line. */ + servicePeriod: ClosedPeriod + /** Aggregated financial totals for the detailed line. */ + totals: Totals + /** The cost category of this detailed line. */ + category: 'regular' | 'commitment' + /** Discounts applied to this detailed line. */ + discounts?: InvoiceLineDiscounts + /** Credit applied to this detailed line. */ + creditsApplied?: InvoiceLineCreditsApplied[] + /** External identifiers for this detailed line. */ + externalReferences?: InvoiceLineExternalReferences + /** The quantity of the detailed line. */ + quantity: string + /** The unit price of the detailed line. */ + unitPrice: string +} + /** Page paginated response. */ export interface TaxCodePagePaginatedResponse { data: TaxCode[] @@ -5826,51 +5882,6 @@ export interface InstalledAppExternalInvoicing { )[] } -/** - * A detailed (child) sub-line belonging to a parent invoice line. - * - * Detailed lines represent the individual flat-fee components that make up a - * usage-based parent line after quantity snapshotting. - */ -export interface InvoiceDetailedLine { - id: string - /** - * Display name of the resource. - * - * Between 1 and 256 characters. - */ - name: string - /** - * Optional description of the resource. - * - * Maximum 1024 characters. - */ - description?: string - labels?: Labels - /** An ISO-8601 timestamp representation of entity creation date. */ - createdAt: Date - /** An ISO-8601 timestamp representation of entity last update date. */ - updatedAt: Date - /** An ISO-8601 timestamp representation of entity deletion date. */ - deletedAt?: Date - /** The service period covered by this detailed line. */ - servicePeriod: ClosedPeriod - /** Aggregated financial totals for the detailed line. */ - totals: Totals - /** The cost category of this detailed line. */ - category: 'regular' | 'commitment' - /** Discounts applied to this detailed line. */ - discounts?: InvoiceLineDiscounts - /** Credit applied to this detailed line. */ - creditsApplied?: InvoiceLineCreditsApplied[] - /** External identifiers for this detailed line. */ - externalReferences?: InvoiceLineExternalReferences - /** The quantity of the detailed line. */ - quantity: string - /** The unit price of the detailed line. */ - unitPrice: string -} - /** Snapshot of the billing workflow configuration captured at invoice creation. */ export interface UpdateInvoiceWorkflowSettings { /** @@ -7997,6 +8008,12 @@ export interface ChargeUsageBased { systemIntent?: ChargeUsageBasedSystemIntent } +/** Result of manually collecting a customer's pending lines into invoices. */ +export interface InvoiceCustomerChargesResult { + /** Created standard invoices, one per collected currency. */ + invoices: Invoice[] +} + /** Page paginated response. */ export interface InvoicePagePaginatedResponse { data: Invoice[] @@ -11346,6 +11363,12 @@ export interface ChargeUsageBasedInput { systemIntent?: ChargeUsageBasedSystemIntentInput } +/** Result of manually collecting a customer's pending lines into invoices. */ +export interface InvoiceCustomerChargesResultInput { + /** Created standard invoices, one per collected currency. */ + invoices: InvoiceInput[] +} + /** Page paginated response. */ export interface InvoicePagePaginatedResponseInput { data: InvoiceInput[] diff --git a/api/spec/packages/aip-client-javascript/src/sdk/internal.ts b/api/spec/packages/aip-client-javascript/src/sdk/internal.ts index 5abd235a2a..9e669d9f75 100644 --- a/api/spec/packages/aip-client-javascript/src/sdk/internal.ts +++ b/api/spec/packages/aip-client-javascript/src/sdk/internal.ts @@ -3,6 +3,7 @@ import { type Client } from '../core.js' import { unwrap, type RequestOptions } from '../lib/types.js' import { paginatePages } from '../lib/paginate.js' +import { invoiceCustomerCharges } from '../funcs/customers.js' import { createSubscriptionAddon, updateSubscriptionAddon, @@ -19,6 +20,10 @@ import { getBillingNotificationEvent, resendBillingNotificationEvent, } from '../funcs/notifications.js' +import type { + InvoiceCustomerChargesRequest, + InvoiceCustomerChargesResponse, +} from '../models/operations/customers.js' import type { CreateSubscriptionAddonRequest, CreateSubscriptionAddonResponse, @@ -59,6 +64,11 @@ import type { NotificationEvent, NotificationRule } from '../models/types.js' export class Internal { constructor(private readonly _client: Client) {} + private _customers?: InternalCustomers + get customers(): InternalCustomers { + return (this._customers ??= new InternalCustomers(this._client)) + } + private _subscriptions?: InternalSubscriptions get subscriptions(): InternalSubscriptions { return (this._subscriptions ??= new InternalSubscriptions(this._client)) @@ -75,6 +85,39 @@ export class Internal { } } +export class InternalCustomers { + constructor(private readonly _client: Client) {} + + private _charges?: InternalCustomersCharges + get charges(): InternalCustomersCharges { + return (this._charges ??= new InternalCustomersCharges(this._client)) + } +} + +export class InternalCustomersCharges { + constructor(private readonly _client: Client) {} + + /** + * Invoice a customer's charges + * + * Invoice a customer's charges outside the normal billing cycle. Collection + * alignment is bypassed. The customer's progressive billing setting applies unless + * explicitly overridden. All eligible pending lines are considered. + * + * Returns the created standard invoices, one per collected currency. Lines blocked + * by their owning engine remain pending. If all eligible lines are blocked or no + * lines are eligible, returns a validation error. + * + * POST /openmeter/customers/{customerId}/charges/invoice + */ + async invoice( + request: InvoiceCustomerChargesRequest, + options?: RequestOptions, + ): Promise { + return unwrap(await invoiceCustomerCharges(this._client, request, options)) + } +} + export class InternalSubscriptions { constructor(private readonly _client: Client) {} diff --git a/api/spec/packages/aip-client-javascript/tests/nesting.spec.ts b/api/spec/packages/aip-client-javascript/tests/nesting.spec.ts index 79f63abd52..a52423845a 100644 --- a/api/spec/packages/aip-client-javascript/tests/nesting.spec.ts +++ b/api/spec/packages/aip-client-javascript/tests/nesting.spec.ts @@ -15,6 +15,30 @@ function lastUrl(): string { } describe('nested sub-clients', () => { + it('returns collected invoices inside the customer charges result', async () => { + fetchMock.route('*', { + status: 201, + body: { invoices: [] }, + headers: { 'Content-Type': 'application/json' }, + }) + const sdk = new OpenMeter({ + baseUrl: 'https://eu.api.konghq.com/v3', + apiKey: 'k', + fetch, + validate: true, + }) + + const result = await sdk.customers.charges.invoice({ + customerId: '01ARZ3NDEKTSV4RRFFQ69G5FAV', + body: {}, + }) + + expect(result.invoices).toEqual([]) + expect(lastUrl()).toBe( + 'https://eu.api.konghq.com/v3/openmeter/customers/01ARZ3NDEKTSV4RRFFQ69G5FAV/charges/invoice', + ) + }) + it('routes customers.charges.list() to the charges sub-resource', async () => { fetchMock.route('*', { body: { data: [], meta: { page: { number: 1, size: 10, total: 0 } } }, diff --git a/api/spec/packages/aip/src/customers/charges/operations.tsp b/api/spec/packages/aip/src/customers/charges/operations.tsp index 206f5e76c6..394f75db49 100644 --- a/api/spec/packages/aip/src/customers/charges/operations.tsp +++ b/api/spec/packages/aip/src/customers/charges/operations.tsp @@ -6,6 +6,7 @@ import "../../common/pagination.tsp"; import "../../shared/index.tsp"; import "../../common/parameters.tsp"; import "./charges.tsp"; +import "../../invoices/index.tsp"; using TypeSpec.Http; using TypeSpec.OpenAPI; @@ -53,6 +54,34 @@ model ListCustomerChargesParamsFilter { service_period_to?: Common.DateTimeFieldFilter; } +/** + * Options for manually collecting a customer's pending lines into invoices. + */ +@friendlyName("InvoiceCustomerChargesInput") +model InvoiceCustomerChargesInput { + /** + * Collection cutoff. Defaults to the current time and cannot be in the future. + */ + as_of?: Shared.DateTime; + + /** + * Override the customer's progressive billing setting for this collection. When + * enabled, eligible usage-based lines can be collected for a partial period. + */ + progressive_billing_override?: boolean; +} + +/** + * Result of manually collecting a customer's pending lines into invoices. + */ +@friendlyName("InvoiceCustomerChargesResult") +model InvoiceCustomerChargesResult { + /** + * Created standard invoices, one per collected currency. + */ + invoices: Invoices.Invoice[]; +} + namespace Charges { interface CustomerChargesOperations { /** @@ -129,5 +158,33 @@ namespace Charges { create(@path customerId: Shared.ULID, @body charge: CreateCharge): | Shared.CreateResponse | Common.ErrorResponses; + + /** + * Invoice a customer's charges outside the normal billing cycle. Collection + * alignment is bypassed. The customer's progressive billing setting applies unless + * explicitly overridden. All eligible pending lines are considered. + * + * Returns the created standard invoices, one per collected currency. Lines blocked + * by their owning engine remain pending. If all eligible lines are blocked or no + * lines are eligible, returns a validation error. + */ + @post + @route("/invoice") + @operationId("invoice-customer-charges") + @summary("Invoice a customer's charges") + @extension(Shared.RPCEndpointExtension, true) + @extension(Shared.UnstableExtension, true) + @extension(Shared.PrivateExtension, true) + @extension(Shared.InternalExtension, true) + invoiceCharges( + @path customerId: Shared.ULID, + @body request?: InvoiceCustomerChargesInput, + ): + | { + @statusCode _: 201; + @body body: InvoiceCustomerChargesResult; + } + | Common.NotFound + | Common.ErrorResponses; } } diff --git a/api/spec/packages/typespec-typescript/templates/tests/nesting.spec.ts b/api/spec/packages/typespec-typescript/templates/tests/nesting.spec.ts index feb73019f3..fe5a815f5c 100644 --- a/api/spec/packages/typespec-typescript/templates/tests/nesting.spec.ts +++ b/api/spec/packages/typespec-typescript/templates/tests/nesting.spec.ts @@ -13,6 +13,30 @@ function lastUrl(): string { } describe('nested sub-clients', () => { + it('returns collected invoices inside the customer charges result', async () => { + fetchMock.route('*', { + status: 201, + body: { invoices: [] }, + headers: { 'Content-Type': 'application/json' }, + }) + const sdk = new OpenMeter({ + baseUrl: 'https://eu.api.konghq.com/v3', + apiKey: 'k', + fetch, + validate: true, + }) + + const result = await sdk.customers.charges.invoice({ + customerId: '01ARZ3NDEKTSV4RRFFQ69G5FAV', + body: {}, + }) + + expect(result.invoices).toEqual([]) + expect(lastUrl()).toBe( + 'https://eu.api.konghq.com/v3/openmeter/customers/01ARZ3NDEKTSV4RRFFQ69G5FAV/charges/invoice', + ) + }) + it('routes customers.charges.list() to the charges sub-resource', async () => { fetchMock.route('*', { body: { data: [], meta: { page: { number: 1, size: 10, total: 0 } } }, diff --git a/api/v3/api.gen.go b/api/v3/api.gen.go index 4a198e16fd..36707155ba 100644 --- a/api/v3/api.gen.go +++ b/api/v3/api.gen.go @@ -9601,6 +9601,22 @@ type InvalidParameters_Item struct { // InvalidRules invalid parameters rules type InvalidRules string +// InvoiceCustomerChargesInput Options for manually collecting a customer's pending lines into invoices. +type InvoiceCustomerChargesInput struct { + // AsOf Collection cutoff. Defaults to the current time and cannot be in the future. + AsOf *DateTime `json:"as_of,omitempty"` + + // ProgressiveBillingOverride Override the customer's progressive billing setting for this collection. When + // enabled, eligible usage-based lines can be collected for a partial period. + ProgressiveBillingOverride *bool `json:"progressive_billing_override,omitempty"` +} + +// InvoiceCustomerChargesResult Result of manually collecting a customer's pending lines into invoices. +type InvoiceCustomerChargesResult struct { + // Invoices Created standard invoices, one per collected currency. + Invoices []BillingInvoice `json:"invoices"` +} + // InvoicePagePaginatedResponse Page paginated response. type InvoicePagePaginatedResponse struct { Data []BillingInvoice `json:"data"` @@ -12393,6 +12409,9 @@ type CreateCustomerStripePortalSessionJSONRequestBody = BillingCustomerStripeCre // CreateCustomerChargesJSONRequestBody defines body for CreateCustomerCharges for application/json ContentType. type CreateCustomerChargesJSONRequestBody = CreateChargeRequest +// InvoiceCustomerChargesJSONRequestBody defines body for InvoiceCustomerCharges for application/json ContentType. +type InvoiceCustomerChargesJSONRequestBody = InvoiceCustomerChargesInput + // CreateCreditAdjustmentJSONRequestBody defines body for CreateCreditAdjustment for application/json ContentType. type CreateCreditAdjustmentJSONRequestBody = CreateCreditAdjustmentRequest @@ -16488,6 +16507,9 @@ type ServerInterface interface { // Create customer charge // (POST /openmeter/customers/{customerId}/charges) CreateCustomerCharges(w http.ResponseWriter, r *http.Request, customerId ULID) + // Invoice a customer's charges + // (POST /openmeter/customers/{customerId}/charges/invoice) + InvoiceCustomerCharges(w http.ResponseWriter, r *http.Request, customerId ULID) // Create a credit adjustment // (POST /openmeter/customers/{customerId}/credits/adjustments) CreateCreditAdjustment(w http.ResponseWriter, r *http.Request, customerId ULID) @@ -17037,6 +17059,12 @@ func (_ Unimplemented) CreateCustomerCharges(w http.ResponseWriter, r *http.Requ w.WriteHeader(http.StatusNotImplemented) } +// Invoice a customer's charges +// (POST /openmeter/customers/{customerId}/charges/invoice) +func (_ Unimplemented) InvoiceCustomerCharges(w http.ResponseWriter, r *http.Request, customerId ULID) { + w.WriteHeader(http.StatusNotImplemented) +} + // Create a credit adjustment // (POST /openmeter/customers/{customerId}/credits/adjustments) func (_ Unimplemented) CreateCreditAdjustment(w http.ResponseWriter, r *http.Request, customerId ULID) { @@ -18947,6 +18975,32 @@ func (siw *ServerInterfaceWrapper) CreateCustomerCharges(w http.ResponseWriter, handler.ServeHTTP(w, r) } +// InvoiceCustomerCharges operation middleware +func (siw *ServerInterfaceWrapper) InvoiceCustomerCharges(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- 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 + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.InvoiceCustomerCharges(w, r, customerId) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + // CreateCreditAdjustment operation middleware func (siw *ServerInterfaceWrapper) CreateCreditAdjustment(w http.ResponseWriter, r *http.Request) { @@ -22476,6 +22530,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/openmeter/customers/{customerId}/charges", wrapper.CreateCustomerCharges) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/openmeter/customers/{customerId}/charges/invoice", wrapper.InvoiceCustomerCharges) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/openmeter/customers/{customerId}/credits/adjustments", wrapper.CreateCreditAdjustment) }) @@ -23657,480 +23714,487 @@ var swaggerSpec = []string{ "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", - "z6Q29d0xg4/vP/5/AwA=", + "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", + "SMQWQJwl4lKeugHgFxMJxTOpTX13zODTh0//3wA=", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/api/v3/client/README.md b/api/v3/client/README.md index 71d2218c3a..389058b1c7 100644 --- a/api/v3/client/README.md +++ b/api/v3/client/README.md @@ -188,6 +188,7 @@ The full call path, HTTP route, and a short description are listed below. | `om.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. | | `om.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. | | `om.Customers.Charges.Create` | `POST /openmeter/customers/{customerId}/charges` | Create customer charge. | +| `om.Customers.Charges.Invoice` | `POST /openmeter/customers/{customerId}/charges/invoice` | Invoice a customer's charges outside the normal billing cycle. Collection alignment is bypassed. The customer's progressive billing setting applies unless explicitly overridden. All eligible pending lines are considered. Returns the created standard invoices, one per collected currency. Lines blocked by their owning engine remain pending. If all eligible lines are blocked or no lines are eligible, returns a validation error. | ### Entitlements diff --git a/api/v3/client/customers_charges.go b/api/v3/client/customers_charges.go index e8d77a679a..682eb76d88 100644 --- a/api/v3/client/customers_charges.go +++ b/api/v3/client/customers_charges.go @@ -133,3 +133,32 @@ func (s *CustomersChargesService) Create(ctx context.Context, customerID string, return &out, nil } + +// Invoice a customer's charges outside the normal billing cycle. Collection +// alignment is bypassed. The customer's progressive billing setting applies unless +// explicitly overridden. All eligible pending lines are considered. +// +// Returns the created standard invoices, one per collected currency. Lines blocked +// by their owning engine remain pending. If all eligible lines are blocked or no +// lines are eligible, returns a validation error. +func (s *CustomersChargesService) Invoice(ctx context.Context, customerID string, request *InvoiceCustomerChargesInput) (*InvoiceCustomerChargesResult, error) { + if customerID == "" { + return nil, fmt.Errorf("openmeter: %s must not be empty: %w", "customerID", ErrEmptyID) + } + + path := "/openmeter/customers/{customerId}/charges/invoice" + + path = replacePathParam(path, "customerId", customerID) + + req, err := s.client.newRequestWithContentType(ctx, http.MethodPost, path, nil, optionalBody(request), "application/json", "application/json") + if err != nil { + return nil, err + } + + var out InvoiceCustomerChargesResult + if err := s.client.doJSON(req, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/api/v3/client/models_customers.go b/api/v3/client/models_customers.go index 58aecf53b2..2a7194bac1 100644 --- a/api/v3/client/models_customers.go +++ b/api/v3/client/models_customers.go @@ -1753,6 +1753,21 @@ func (value EntitlementMeasureUsageFromPreset) Valid() bool { } } +// Options for manually collecting a customer's pending lines into invoices. +type InvoiceCustomerChargesInput struct { + // Collection cutoff. Defaults to the current time and cannot be in the future. + AsOf *time.Time `json:"as_of,omitempty"` + // Override the customer's progressive billing setting for this collection. When + // enabled, eligible usage-based lines can be collected for a partial period. + ProgressiveBillingOverride *bool `json:"progressive_billing_override,omitempty"` +} + +// Result of manually collecting a customer's pending lines into invoices. +type InvoiceCustomerChargesResult struct { + // Created standard invoices, one per collected currency. + Invoices []Invoice `json:"invoices"` +} + // Recurring period input. The anchor is optional; the owning resource defines the // default, typically its creation time. type RecurringPeriodInput struct { diff --git a/api/v3/client/models_invoices.go b/api/v3/client/models_invoices.go index 2b93208b45..4f18c1a959 100644 --- a/api/v3/client/models_invoices.go +++ b/api/v3/client/models_invoices.go @@ -5,462 +5,14 @@ package openmeter import ( "encoding/json" "fmt" - "time" ) -// Reference to a charge associated with an invoice line. -type ChargeReference struct { - // Unique identifier for the charge. - ID string `json:"id"` -} - -// An invoice issued to a customer. -// -// The `type` field determines the concrete variant: -// -// - `standard`: a standard invoice for charges owed. -// -// Invoice is a JSON-preserving tagged union: its zero value marshals as JSON null, and values must be built with the InvoiceFrom* constructors. -// The exported Type field is decode-side metadata; MarshalJSON round-trips the original payload and ignores writes to it. -type Invoice struct { - Type string `json:"type"` - raw json.RawMessage -} - -func (u *Invoice) UnmarshalJSON(data []byte) error { - u.raw = append([]byte(nil), data...) - if string(data) == "null" { - u.Type = "" - return nil - } - - var envelope struct { - Value string `json:"type"` - } - if err := json.Unmarshal(data, &envelope); err != nil { - return err - } - u.Type = envelope.Value - return nil -} - -func (u Invoice) MarshalJSON() ([]byte, error) { - if len(u.raw) == 0 { - return []byte("null"), nil - } - return append([]byte(nil), u.raw...), nil -} - -func (u Invoice) AsInvoiceStandard() (*InvoiceStandard, error) { - if u.Type != "standard" { - return nil, fmt.Errorf("Invoice: expected type %q, got %q", "standard", u.Type) - } - var value InvoiceStandard - if err := json.Unmarshal(u.raw, &value); err != nil { - return nil, err - } - return &value, nil -} - -func InvoiceFromInvoiceStandard(value InvoiceStandard) (Invoice, error) { - value.Type = "standard" - raw, err := json.Marshal(value) - if err != nil { - return Invoice{}, err - } - var result Invoice - if err := result.UnmarshalJSON(raw); err != nil { - return Invoice{}, err - } - return result, nil -} - -// Snapshot of the customer's information at the time the invoice was issued. -type InvoiceCustomer struct { - // Display name of the resource. - // - // Between 1 and 256 characters. - Name string `json:"name"` - // Mapping to attribute metered usage to the customer by the event subject. - UsageAttribution *CustomerUsageAttribution `json:"usage_attribution,omitempty"` - // The billing address of the customer. Used for tax and invoicing. - BillingAddress *Address `json:"billing_address,omitempty"` - ID string `json:"id"` - // Optional external resource key for the customer. - // - // Omitted when the customer was created without a key. Unlike on the customer - // resource itself, the key is optional here because the invoice snapshot may - // predate or omit it. - Key *string `json:"key,omitempty"` -} - -// A detailed (child) sub-line belonging to a parent invoice line. -// -// Detailed lines represent the individual flat-fee components that make up a -// usage-based parent line after quantity snapshotting. -type InvoiceDetailedLine struct { - ID string `json:"id"` - // Display name of the resource. - // - // Between 1 and 256 characters. - Name string `json:"name"` - // Optional description of the resource. - // - // Maximum 1024 characters. - Description *string `json:"description,omitempty"` - Labels map[string]string `json:"labels,omitempty"` - // An ISO-8601 timestamp representation of entity creation date. - CreatedAt time.Time `json:"created_at"` - // An ISO-8601 timestamp representation of entity last update date. - UpdatedAt time.Time `json:"updated_at"` - // An ISO-8601 timestamp representation of entity deletion date. - DeletedAt *time.Time `json:"deleted_at,omitempty"` - // The service period covered by this detailed line. - ServicePeriod ClosedPeriod `json:"service_period"` - // Aggregated financial totals for the detailed line. - Totals Totals `json:"totals"` - // The cost category of this detailed line. - Category InvoiceDetailedLineCostCategory `json:"category"` - // Discounts applied to this detailed line. - Discounts *InvoiceLineDiscounts `json:"discounts,omitempty"` - // Credit applied to this detailed line. - CreditsApplied []InvoiceLineCreditsApplied `json:"credits_applied,omitempty"` - // External identifiers for this detailed line. - ExternalReferences *InvoiceLineExternalReferences `json:"external_references,omitempty"` - // The quantity of the detailed line. - Quantity Numeric `json:"quantity"` - // The unit price of the detailed line. - UnitPrice Numeric `json:"unit_price"` -} - -// Cost category of a detailed invoice line item. -type InvoiceDetailedLineCostCategory string - -const ( - InvoiceDetailedLineCostCategoryRegular InvoiceDetailedLineCostCategory = "regular" - InvoiceDetailedLineCostCategoryCommitment InvoiceDetailedLineCostCategory = "commitment" -) - -func (value InvoiceDetailedLineCostCategory) Valid() bool { - switch value { - case InvoiceDetailedLineCostCategoryRegular, InvoiceDetailedLineCostCategoryCommitment: - return true - default: - return false - } -} - -// The reason a discount was applied to an invoice line. -type InvoiceDiscountReason string - -const ( - InvoiceDiscountReasonMaximumSpend InvoiceDiscountReason = "maximum_spend" - InvoiceDiscountReasonRatecardPercentage InvoiceDiscountReason = "ratecard_percentage" - InvoiceDiscountReasonRatecardUsage InvoiceDiscountReason = "ratecard_usage" -) - -func (value InvoiceDiscountReason) Valid() bool { - switch value { - case InvoiceDiscountReasonMaximumSpend, InvoiceDiscountReasonRatecardPercentage, InvoiceDiscountReasonRatecardUsage: - return true - default: - return false - } -} - -// A top-level line item on an invoice. -// -// Each line represents a single charge, typically associated with a rate card from -// a subscription. Detailed (child) lines are nested under `detailed_lines` when -// present. -// -// InvoiceLine is a JSON-preserving tagged union: its zero value marshals as JSON null, and values must be built with the InvoiceLineFrom* constructors. -// The exported Type field is decode-side metadata; MarshalJSON round-trips the original payload and ignores writes to it. -type InvoiceLine struct { - Type string `json:"type"` - raw json.RawMessage -} - -func (u *InvoiceLine) UnmarshalJSON(data []byte) error { - u.raw = append([]byte(nil), data...) - if string(data) == "null" { - u.Type = "" - return nil - } - - var envelope struct { - Value string `json:"type"` - } - if err := json.Unmarshal(data, &envelope); err != nil { - return err - } - u.Type = envelope.Value - return nil -} - -func (u InvoiceLine) MarshalJSON() ([]byte, error) { - if len(u.raw) == 0 { - return []byte("null"), nil - } - return append([]byte(nil), u.raw...), nil -} - -func (u InvoiceLine) AsInvoiceStandardLine() (*InvoiceStandardLine, error) { - if u.Type != "standard_line" { - return nil, fmt.Errorf("InvoiceLine: expected type %q, got %q", "standard_line", u.Type) - } - var value InvoiceStandardLine - if err := json.Unmarshal(u.raw, &value); err != nil { - return nil, err - } - return &value, nil -} - -func InvoiceLineFromInvoiceStandardLine(value InvoiceStandardLine) (InvoiceLine, error) { - value.Type = "standard_line" - raw, err := json.Marshal(value) - if err != nil { - return InvoiceLine{}, err - } - var result InvoiceLine - if err := result.UnmarshalJSON(raw); err != nil { - return InvoiceLine{}, err - } - return result, nil -} - -// A monetary amount discount applied to an invoice line item. -type InvoiceLineAmountDiscount struct { - // Unique identifier for the discount. - ID string `json:"id"` - // The reason this discount was applied. - Reason InvoiceDiscountReason `json:"reason"` - // Optional human-readable description of the discount. - Description *string `json:"description,omitempty"` - // External identifiers for this discount. - ExternalReferences *InvoiceLineExternalReferences `json:"external_references,omitempty"` - // The monetary amount deducted. - Amount Numeric `json:"amount"` -} - -// A credit allocation applied to an invoice line item. -type InvoiceLineCreditsApplied struct { - // The monetary amount credited. - Amount Numeric `json:"amount"` - // Optional human-readable description of the credit allocation. - Description *string `json:"description,omitempty"` -} - -// Discounts applied to an invoice line item. -type InvoiceLineDiscounts struct { - // Monetary amount discounts (e.g. from maximum spend commitments). - Amount []InvoiceLineAmountDiscount `json:"amount,omitempty"` - // Usage quantity discounts (e.g. free tier usage allowances). - Usage []InvoiceLineUsageDiscount `json:"usage,omitempty"` -} - -// External identifiers for an invoice line item assigned by third-party systems. -type InvoiceLineExternalReferences struct { - // The ID assigned by the external invoicing app. - InvoicingID *string `json:"invoicing_id,omitempty"` -} - -// Rate card configuration snapshot for a usage-based invoice line. -type InvoiceLineRateCard struct { - // The price definition used to calculate charges for this line. - Price Price `json:"price"` - // Tax configuration snapshot for this line. - TaxConfig *TaxCodeConfig `json:"tax_config,omitempty"` - // The feature key associated with this line's rate card. - FeatureKey *string `json:"feature_key,omitempty"` - // Discount configuration from the rate card. - Discounts *RateCardDiscounts `json:"discounts,omitempty"` - // Unit config snapshot applied when this line was billed, converting the raw - // metered quantity into the billed quantity. Frozen at billing time; read-only. - UnitConfig *UnitConfig `json:"unit_config,omitempty"` -} - -// Line item type discriminator. -type InvoiceLineType string - -const ( - InvoiceLineTypeStandardLine InvoiceLineType = "standard_line" -) - -func (value InvoiceLineType) Valid() bool { - switch value { - case InvoiceLineTypeStandardLine: - return true - default: - return false - } -} - -// A usage quantity discount applied to an invoice line item. -type InvoiceLineUsageDiscount struct { - // Unique identifier for the discount. - ID string `json:"id"` - // The reason this discount was applied. - Reason InvoiceDiscountReason `json:"reason"` - // Optional human-readable description of the discount. - Description *string `json:"description,omitempty"` - // External identifiers for this discount. - ExternalReferences *InvoiceLineExternalReferences `json:"external_references,omitempty"` - // The usage quantity deducted (in billing units). - Quantity Numeric `json:"quantity"` -} - // Page paginated response. type InvoicePagePaginatedResponse struct { Data []Invoice `json:"data"` Meta PaginatedMeta `json:"meta"` } -// A standard invoice for charges owed by the customer. -type InvoiceStandard struct { - ID string `json:"id"` - // Optional description of the resource. - // - // Maximum 1024 characters. - Description *string `json:"description,omitempty"` - Labels map[string]string `json:"labels,omitempty"` - // An ISO-8601 timestamp representation of entity creation date. - CreatedAt time.Time `json:"created_at"` - // An ISO-8601 timestamp representation of entity last update date. - UpdatedAt time.Time `json:"updated_at"` - // An ISO-8601 timestamp representation of entity deletion date. - DeletedAt *time.Time `json:"deleted_at,omitempty"` - // Human-readable invoice number generated by the invoicing app. - Number string `json:"number"` - // Three-letter ISO 4217 currency code for the invoice. - Currency string `json:"currency"` - // Snapshot of the supplier's contact information at the time the invoice was - // issued. - Supplier Supplier `json:"supplier"` - // Snapshot of the customer's information at the time the invoice was issued. - Customer InvoiceCustomer `json:"customer"` - // Aggregated financial totals for the invoice. - Totals Totals `json:"totals"` - // The service period covered by this invoice. - // - // For flat fee the service period can be empty which means `from` will be equals - // to `to`. In other cases those fields will be filled with the actual service - // period. - ServicePeriod ClosedPeriod `json:"service_period"` - // Validation issues found during invoice processing. - // - // Present only when there are one or more validation findings. An empty list is - // omitted. - ValidationIssues []ValidationIssue `json:"validation_issues,omitempty"` - // External identifiers assigned to this invoice by third-party systems. - ExternalReferences *InvoiceExternalReferences `json:"external_references,omitempty"` - // Discriminator field identifying this as a standard invoice. - Type InvoiceType `json:"type"` - // Current lifecycle status of the invoice. - Status InvoiceStandardStatus `json:"status"` - // Detailed status information including available actions and workflow state. - StatusDetails InvoiceStatusDetails `json:"status_details"` - // Timestamp when the invoice was issued to the customer. - IssuedAt *time.Time `json:"issued_at,omitempty"` - // Timestamp until which the invoice remains in draft state. - // - // The invoice advances automatically once this time is reached. - DraftUntil *time.Time `json:"draft_until,omitempty"` - // Timestamp when usage quantities were last snapshotted for this invoice. - QuantitySnapshottedAt *time.Time `json:"quantity_snapshotted_at,omitempty"` - // Timestamp when collection was initiated for this invoice. - CollectionAt *time.Time `json:"collection_at,omitempty"` - // Timestamp when payment is due. - DueAt *time.Time `json:"due_at,omitempty"` - // Timestamp when the invoice was sent to the customer. - SentToCustomerAt *time.Time `json:"sent_to_customer_at,omitempty"` - // Workflow configuration snapshot captured at invoice creation time. - Workflow InvoiceWorkflowSettings `json:"workflow"` - // Line items on this invoice. - // - // Always returned on single-resource GET; omitted on list endpoints unless - // explicitly expanded. Editable via update: existing lines are matched by `id`, - // lines without an `id` are created, and lines present on the invoice but omitted - // from the update request are deleted. Detailed (child) lines are always computed - // and cannot be edited directly. - Lines []InvoiceLine `json:"lines,omitempty"` -} - -// A top-level line item on an invoice. -// -// Each line represents a single charge, typically associated with a rate card from -// a subscription. Detailed (child) lines are nested under `detailed_lines` when -// present. -type InvoiceStandardLine struct { - // Display name of the resource. - // - // Between 1 and 256 characters. - Name string `json:"name"` - // Optional description of the resource. - // - // Maximum 1024 characters. - Description *string `json:"description,omitempty"` - Labels map[string]string `json:"labels,omitempty"` - // An ISO-8601 timestamp representation of entity creation date. - CreatedAt time.Time `json:"created_at"` - // An ISO-8601 timestamp representation of entity last update date. - UpdatedAt time.Time `json:"updated_at"` - // An ISO-8601 timestamp representation of entity deletion date. - DeletedAt *time.Time `json:"deleted_at,omitempty"` - // ID of the line. - // - // Optional on update: omit to create a new line, or supply the ID of an existing - // line to edit it. Existing lines omitted from an update's `lines` array are - // deleted. - ID *string `json:"id,omitempty"` - // The type of charge this line item represents. - Type InvoiceLineType `json:"type"` - // Indicates whether this line item's lifecycle is controlled by OpenMeter or - // manually overridden by the API user. - LifecycleController LifecycleController `json:"lifecycle_controller"` - // The service period covered by this invoice, spanning the earliest line start to - // the latest line end across all of its lines. - // - // For an invoice with no lines the period is empty, which means `from` will be - // equal to `to`. - ServicePeriod ClosedPeriod `json:"service_period"` - // Aggregated financial totals for the line item. - Totals Totals `json:"totals"` - // Discounts applied to this line item. - Discounts *InvoiceLineDiscounts `json:"discounts,omitempty"` - // Credit applied to this line item. - CreditsApplied []InvoiceLineCreditsApplied `json:"credits_applied,omitempty"` - // External identifiers for this line item assigned by third-party systems. - ExternalReferences *InvoiceLineExternalReferences `json:"external_references,omitempty"` - // Reference to the subscription item that generated this line. - Subscription *SubscriptionReference `json:"subscription,omitempty"` - // The rate card configuration snapshot used to price this line item. - RateCard InvoiceLineRateCard `json:"rate_card"` - // Detailed sub-lines that this line has been broken down into. - // - // Present when line has individual details. - DetailedLines []InvoiceDetailedLine `json:"detailed_lines"` - // Usage quantity details for this line when UnitConfig is in effect. - // - // Read-only; omitted for lines without unit conversion. - UsageQuantityDetail *InvoiceUsageQuantityDetail `json:"usage_quantity_detail,omitempty"` - // Reference to the charge associated with this line item. - Charge *ChargeReference `json:"charge,omitempty"` -} - -// Usage quantity details on an invoice line item when UnitConfig is in effect. -// -// Provides the full audit trail from raw meter output to the invoiced amount. -type InvoiceUsageQuantityDetail struct { - // The raw quantity as reported by the meter (native units). - RawQuantity Numeric `json:"raw_quantity"` - // The net billed quantity for this line in converted units, after rounding and any - // usage discounts. - InvoicedQuantity Numeric `json:"invoiced_quantity"` - // The display unit label (e.g., "GB", "hours", "M tokens"). - DisplayUnit *string `json:"display_unit,omitempty"` -} - // Invoice-level snapshot of the workflow configuration. // // Contains only the settings that are meaningful for an already-created invoice: diff --git a/api/v3/client/models_shared.go b/api/v3/client/models_shared.go index 3eb7c6565a..8ed0afc5a2 100644 --- a/api/v3/client/models_shared.go +++ b/api/v3/client/models_shared.go @@ -766,6 +766,12 @@ func (value ChargeRealizationType) Valid() bool { } } +// Reference to a charge associated with an invoice line. +type ChargeReference struct { + // Unique identifier for the charge. + ID string `json:"id"` +} + // Fiat conversion rate a custom-currency charge is invoiced at. Present once the // cost basis is resolved; dynamic cost bases are exposed only after the service // period has started. @@ -1811,6 +1817,67 @@ func (value FeatureUnitCostType) Valid() bool { } } +// An invoice issued to a customer. +// +// The `type` field determines the concrete variant: +// +// - `standard`: a standard invoice for charges owed. +// +// Invoice is a JSON-preserving tagged union: its zero value marshals as JSON null, and values must be built with the InvoiceFrom* constructors. +// The exported Type field is decode-side metadata; MarshalJSON round-trips the original payload and ignores writes to it. +type Invoice struct { + Type string `json:"type"` + raw json.RawMessage +} + +func (u *Invoice) UnmarshalJSON(data []byte) error { + u.raw = append([]byte(nil), data...) + if string(data) == "null" { + u.Type = "" + return nil + } + + var envelope struct { + Value string `json:"type"` + } + if err := json.Unmarshal(data, &envelope); err != nil { + return err + } + u.Type = envelope.Value + return nil +} + +func (u Invoice) MarshalJSON() ([]byte, error) { + if len(u.raw) == 0 { + return []byte("null"), nil + } + return append([]byte(nil), u.raw...), nil +} + +func (u Invoice) AsInvoiceStandard() (*InvoiceStandard, error) { + if u.Type != "standard" { + return nil, fmt.Errorf("Invoice: expected type %q, got %q", "standard", u.Type) + } + var value InvoiceStandard + if err := json.Unmarshal(u.raw, &value); err != nil { + return nil, err + } + return &value, nil +} + +func InvoiceFromInvoiceStandard(value InvoiceStandard) (Invoice, error) { + value.Type = "standard" + raw, err := json.Marshal(value) + if err != nil { + return Invoice{}, err + } + var result Invoice + if err := result.UnmarshalJSON(raw); err != nil { + return Invoice{}, err + } + return result, nil +} + // Details about an available invoice action including the resulting state. type InvoiceAvailableActionDetails struct { // The extended status the invoice will transition to after performing this action. @@ -1834,6 +1901,99 @@ type InvoiceAvailableActions struct { SnapshotQuantities *InvoiceAvailableActionDetails `json:"snapshot_quantities,omitempty"` } +// Snapshot of the customer's information at the time the invoice was issued. +type InvoiceCustomer struct { + // Display name of the resource. + // + // Between 1 and 256 characters. + Name string `json:"name"` + // Mapping to attribute metered usage to the customer by the event subject. + UsageAttribution *CustomerUsageAttribution `json:"usage_attribution,omitempty"` + // The billing address of the customer. Used for tax and invoicing. + BillingAddress *Address `json:"billing_address,omitempty"` + ID string `json:"id"` + // Optional external resource key for the customer. + // + // Omitted when the customer was created without a key. Unlike on the customer + // resource itself, the key is optional here because the invoice snapshot may + // predate or omit it. + Key *string `json:"key,omitempty"` +} + +// A detailed (child) sub-line belonging to a parent invoice line. +// +// Detailed lines represent the individual flat-fee components that make up a +// usage-based parent line after quantity snapshotting. +type InvoiceDetailedLine struct { + ID string `json:"id"` + // Display name of the resource. + // + // Between 1 and 256 characters. + Name string `json:"name"` + // Optional description of the resource. + // + // Maximum 1024 characters. + Description *string `json:"description,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + // An ISO-8601 timestamp representation of entity creation date. + CreatedAt time.Time `json:"created_at"` + // An ISO-8601 timestamp representation of entity last update date. + UpdatedAt time.Time `json:"updated_at"` + // An ISO-8601 timestamp representation of entity deletion date. + DeletedAt *time.Time `json:"deleted_at,omitempty"` + // The service period covered by this detailed line. + ServicePeriod ClosedPeriod `json:"service_period"` + // Aggregated financial totals for the detailed line. + Totals Totals `json:"totals"` + // The cost category of this detailed line. + Category InvoiceDetailedLineCostCategory `json:"category"` + // Discounts applied to this detailed line. + Discounts *InvoiceLineDiscounts `json:"discounts,omitempty"` + // Credit applied to this detailed line. + CreditsApplied []InvoiceLineCreditsApplied `json:"credits_applied,omitempty"` + // External identifiers for this detailed line. + ExternalReferences *InvoiceLineExternalReferences `json:"external_references,omitempty"` + // The quantity of the detailed line. + Quantity Numeric `json:"quantity"` + // The unit price of the detailed line. + UnitPrice Numeric `json:"unit_price"` +} + +// Cost category of a detailed invoice line item. +type InvoiceDetailedLineCostCategory string + +const ( + InvoiceDetailedLineCostCategoryRegular InvoiceDetailedLineCostCategory = "regular" + InvoiceDetailedLineCostCategoryCommitment InvoiceDetailedLineCostCategory = "commitment" +) + +func (value InvoiceDetailedLineCostCategory) Valid() bool { + switch value { + case InvoiceDetailedLineCostCategoryRegular, InvoiceDetailedLineCostCategoryCommitment: + return true + default: + return false + } +} + +// The reason a discount was applied to an invoice line. +type InvoiceDiscountReason string + +const ( + InvoiceDiscountReasonMaximumSpend InvoiceDiscountReason = "maximum_spend" + InvoiceDiscountReasonRatecardPercentage InvoiceDiscountReason = "ratecard_percentage" + InvoiceDiscountReasonRatecardUsage InvoiceDiscountReason = "ratecard_usage" +) + +func (value InvoiceDiscountReason) Valid() bool { + switch value { + case InvoiceDiscountReasonMaximumSpend, InvoiceDiscountReasonRatecardPercentage, InvoiceDiscountReasonRatecardUsage: + return true + default: + return false + } +} + // External identifiers assigned to an invoice by third-party systems. type InvoiceExternalReferences struct { // The ID assigned by the external invoicing app (e.g. Stripe invoice ID). @@ -1842,6 +2002,280 @@ type InvoiceExternalReferences struct { PaymentID *string `json:"payment_id,omitempty"` } +// A top-level line item on an invoice. +// +// Each line represents a single charge, typically associated with a rate card from +// a subscription. Detailed (child) lines are nested under `detailed_lines` when +// present. +// +// InvoiceLine is a JSON-preserving tagged union: its zero value marshals as JSON null, and values must be built with the InvoiceLineFrom* constructors. +// The exported Type field is decode-side metadata; MarshalJSON round-trips the original payload and ignores writes to it. +type InvoiceLine struct { + Type string `json:"type"` + raw json.RawMessage +} + +func (u *InvoiceLine) UnmarshalJSON(data []byte) error { + u.raw = append([]byte(nil), data...) + if string(data) == "null" { + u.Type = "" + return nil + } + + var envelope struct { + Value string `json:"type"` + } + if err := json.Unmarshal(data, &envelope); err != nil { + return err + } + u.Type = envelope.Value + return nil +} + +func (u InvoiceLine) MarshalJSON() ([]byte, error) { + if len(u.raw) == 0 { + return []byte("null"), nil + } + return append([]byte(nil), u.raw...), nil +} + +func (u InvoiceLine) AsInvoiceStandardLine() (*InvoiceStandardLine, error) { + if u.Type != "standard_line" { + return nil, fmt.Errorf("InvoiceLine: expected type %q, got %q", "standard_line", u.Type) + } + var value InvoiceStandardLine + if err := json.Unmarshal(u.raw, &value); err != nil { + return nil, err + } + return &value, nil +} + +func InvoiceLineFromInvoiceStandardLine(value InvoiceStandardLine) (InvoiceLine, error) { + value.Type = "standard_line" + raw, err := json.Marshal(value) + if err != nil { + return InvoiceLine{}, err + } + var result InvoiceLine + if err := result.UnmarshalJSON(raw); err != nil { + return InvoiceLine{}, err + } + return result, nil +} + +// A monetary amount discount applied to an invoice line item. +type InvoiceLineAmountDiscount struct { + // Unique identifier for the discount. + ID string `json:"id"` + // The reason this discount was applied. + Reason InvoiceDiscountReason `json:"reason"` + // Optional human-readable description of the discount. + Description *string `json:"description,omitempty"` + // External identifiers for this discount. + ExternalReferences *InvoiceLineExternalReferences `json:"external_references,omitempty"` + // The monetary amount deducted. + Amount Numeric `json:"amount"` +} + +// A credit allocation applied to an invoice line item. +type InvoiceLineCreditsApplied struct { + // The monetary amount credited. + Amount Numeric `json:"amount"` + // Optional human-readable description of the credit allocation. + Description *string `json:"description,omitempty"` +} + +// Discounts applied to an invoice line item. +type InvoiceLineDiscounts struct { + // Monetary amount discounts (e.g. from maximum spend commitments). + Amount []InvoiceLineAmountDiscount `json:"amount,omitempty"` + // Usage quantity discounts (e.g. free tier usage allowances). + Usage []InvoiceLineUsageDiscount `json:"usage,omitempty"` +} + +// External identifiers for an invoice line item assigned by third-party systems. +type InvoiceLineExternalReferences struct { + // The ID assigned by the external invoicing app. + InvoicingID *string `json:"invoicing_id,omitempty"` +} + +// Rate card configuration snapshot for a usage-based invoice line. +type InvoiceLineRateCard struct { + // The price definition used to calculate charges for this line. + Price Price `json:"price"` + // Tax configuration snapshot for this line. + TaxConfig *TaxCodeConfig `json:"tax_config,omitempty"` + // The feature key associated with this line's rate card. + FeatureKey *string `json:"feature_key,omitempty"` + // Discount configuration from the rate card. + Discounts *RateCardDiscounts `json:"discounts,omitempty"` + // Unit config snapshot applied when this line was billed, converting the raw + // metered quantity into the billed quantity. Frozen at billing time; read-only. + UnitConfig *UnitConfig `json:"unit_config,omitempty"` +} + +// Line item type discriminator. +type InvoiceLineType string + +const ( + InvoiceLineTypeStandardLine InvoiceLineType = "standard_line" +) + +func (value InvoiceLineType) Valid() bool { + switch value { + case InvoiceLineTypeStandardLine: + return true + default: + return false + } +} + +// A usage quantity discount applied to an invoice line item. +type InvoiceLineUsageDiscount struct { + // Unique identifier for the discount. + ID string `json:"id"` + // The reason this discount was applied. + Reason InvoiceDiscountReason `json:"reason"` + // Optional human-readable description of the discount. + Description *string `json:"description,omitempty"` + // External identifiers for this discount. + ExternalReferences *InvoiceLineExternalReferences `json:"external_references,omitempty"` + // The usage quantity deducted (in billing units). + Quantity Numeric `json:"quantity"` +} + +// A standard invoice for charges owed by the customer. +type InvoiceStandard struct { + ID string `json:"id"` + // Optional description of the resource. + // + // Maximum 1024 characters. + Description *string `json:"description,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + // An ISO-8601 timestamp representation of entity creation date. + CreatedAt time.Time `json:"created_at"` + // An ISO-8601 timestamp representation of entity last update date. + UpdatedAt time.Time `json:"updated_at"` + // An ISO-8601 timestamp representation of entity deletion date. + DeletedAt *time.Time `json:"deleted_at,omitempty"` + // Human-readable invoice number generated by the invoicing app. + Number string `json:"number"` + // Three-letter ISO 4217 currency code for the invoice. + Currency string `json:"currency"` + // Snapshot of the supplier's contact information at the time the invoice was + // issued. + Supplier Supplier `json:"supplier"` + // Snapshot of the customer's information at the time the invoice was issued. + Customer InvoiceCustomer `json:"customer"` + // Aggregated financial totals for the invoice. + Totals Totals `json:"totals"` + // The service period covered by this invoice. + // + // For flat fee the service period can be empty which means `from` will be equals + // to `to`. In other cases those fields will be filled with the actual service + // period. + ServicePeriod ClosedPeriod `json:"service_period"` + // Validation issues found during invoice processing. + // + // Present only when there are one or more validation findings. An empty list is + // omitted. + ValidationIssues []ValidationIssue `json:"validation_issues,omitempty"` + // External identifiers assigned to this invoice by third-party systems. + ExternalReferences *InvoiceExternalReferences `json:"external_references,omitempty"` + // Discriminator field identifying this as a standard invoice. + Type InvoiceType `json:"type"` + // Current lifecycle status of the invoice. + Status InvoiceStandardStatus `json:"status"` + // Detailed status information including available actions and workflow state. + StatusDetails InvoiceStatusDetails `json:"status_details"` + // Timestamp when the invoice was issued to the customer. + IssuedAt *time.Time `json:"issued_at,omitempty"` + // Timestamp until which the invoice remains in draft state. + // + // The invoice advances automatically once this time is reached. + DraftUntil *time.Time `json:"draft_until,omitempty"` + // Timestamp when usage quantities were last snapshotted for this invoice. + QuantitySnapshottedAt *time.Time `json:"quantity_snapshotted_at,omitempty"` + // Timestamp when collection was initiated for this invoice. + CollectionAt *time.Time `json:"collection_at,omitempty"` + // Timestamp when payment is due. + DueAt *time.Time `json:"due_at,omitempty"` + // Timestamp when the invoice was sent to the customer. + SentToCustomerAt *time.Time `json:"sent_to_customer_at,omitempty"` + // Workflow configuration snapshot captured at invoice creation time. + Workflow InvoiceWorkflowSettings `json:"workflow"` + // Line items on this invoice. + // + // Always returned on single-resource GET; omitted on list endpoints unless + // explicitly expanded. Editable via update: existing lines are matched by `id`, + // lines without an `id` are created, and lines present on the invoice but omitted + // from the update request are deleted. Detailed (child) lines are always computed + // and cannot be edited directly. + Lines []InvoiceLine `json:"lines,omitempty"` +} + +// A top-level line item on an invoice. +// +// Each line represents a single charge, typically associated with a rate card from +// a subscription. Detailed (child) lines are nested under `detailed_lines` when +// present. +type InvoiceStandardLine struct { + // Display name of the resource. + // + // Between 1 and 256 characters. + Name string `json:"name"` + // Optional description of the resource. + // + // Maximum 1024 characters. + Description *string `json:"description,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + // An ISO-8601 timestamp representation of entity creation date. + CreatedAt time.Time `json:"created_at"` + // An ISO-8601 timestamp representation of entity last update date. + UpdatedAt time.Time `json:"updated_at"` + // An ISO-8601 timestamp representation of entity deletion date. + DeletedAt *time.Time `json:"deleted_at,omitempty"` + // ID of the line. + // + // Optional on update: omit to create a new line, or supply the ID of an existing + // line to edit it. Existing lines omitted from an update's `lines` array are + // deleted. + ID *string `json:"id,omitempty"` + // The type of charge this line item represents. + Type InvoiceLineType `json:"type"` + // Indicates whether this line item's lifecycle is controlled by OpenMeter or + // manually overridden by the API user. + LifecycleController LifecycleController `json:"lifecycle_controller"` + // The service period covered by this invoice, spanning the earliest line start to + // the latest line end across all of its lines. + // + // For an invoice with no lines the period is empty, which means `from` will be + // equal to `to`. + ServicePeriod ClosedPeriod `json:"service_period"` + // Aggregated financial totals for the line item. + Totals Totals `json:"totals"` + // Discounts applied to this line item. + Discounts *InvoiceLineDiscounts `json:"discounts,omitempty"` + // Credit applied to this line item. + CreditsApplied []InvoiceLineCreditsApplied `json:"credits_applied,omitempty"` + // External identifiers for this line item assigned by third-party systems. + ExternalReferences *InvoiceLineExternalReferences `json:"external_references,omitempty"` + // Reference to the subscription item that generated this line. + Subscription *SubscriptionReference `json:"subscription,omitempty"` + // The rate card configuration snapshot used to price this line item. + RateCard InvoiceLineRateCard `json:"rate_card"` + // Detailed sub-lines that this line has been broken down into. + // + // Present when line has individual details. + DetailedLines []InvoiceDetailedLine `json:"detailed_lines"` + // Usage quantity details for this line when UnitConfig is in effect. + // + // Read-only; omitted for lines without unit conversion. + UsageQuantityDetail *InvoiceUsageQuantityDetail `json:"usage_quantity_detail,omitempty"` + // Reference to the charge associated with this line item. + Charge *ChargeReference `json:"charge,omitempty"` +} + // Lifecycle status of a standard invoice. type InvoiceStandardStatus string @@ -1894,6 +2328,19 @@ func (value InvoiceType) Valid() bool { } } +// Usage quantity details on an invoice line item when UnitConfig is in effect. +// +// Provides the full audit trail from raw meter output to the invoiced amount. +type InvoiceUsageQuantityDetail struct { + // The raw quantity as reported by the meter (native units). + RawQuantity Numeric `json:"raw_quantity"` + // The net billed quantity for this line in converted units, after rounding and any + // usage discounts. + InvoicedQuantity Numeric `json:"invoiced_quantity"` + // The display unit label (e.g., "GB", "hours", "M tokens"). + DisplayUnit *string `json:"display_unit,omitempty"` +} + // Invoice-level snapshot of the workflow configuration. // // Contains only the settings that are meaningful for an already-created invoice: diff --git a/api/v3/client/transport_test.go b/api/v3/client/transport_test.go index 77fc101106..13962edcbd 100644 --- a/api/v3/client/transport_test.go +++ b/api/v3/client/transport_test.go @@ -82,6 +82,52 @@ func newTestClient(t *testing.T, handler http.Handler, opts ...openmeter.Option) return c } +func TestInvoiceCustomerChargesResponse(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + body string + invoiceIDs []string + }{ + { + name: "collected invoices", + body: `{"invoices":[{"type":"standard","id":"invoice-1"}]}`, + invoiceIDs: []string{"invoice-1"}, + }, + { + name: "empty invoices", + body: `{"invoices":[]}`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + rec := &requestRecorder{} + om := newTestClient(t, rec.handler(http.StatusCreated, test.body)) + + result, err := om.Customers.Charges.Invoice(t.Context(), "customer-1", nil) + if err != nil { + t.Fatalf("Customers.Charges.Invoice: %v", err) + } + + if result == nil || result.Invoices == nil || len(result.Invoices) != len(test.invoiceIDs) { + t.Fatalf("unexpected invoice result: %+v", result) + } + + for i, id := range test.invoiceIDs { + invoice, err := result.Invoices[i].AsInvoiceStandard() + if err != nil { + t.Fatalf("decoding collected invoice: %v", err) + } + + if invoice.ID != id { + t.Errorf("invoice ID = %q, want %q", invoice.ID, id) + } + } + }) + } +} + func TestBaseURLJoining(t *testing.T) { t.Parallel() diff --git a/api/v3/handlers/billinginvoices/handler.go b/api/v3/handlers/billinginvoices/handler.go index 92fc1b28ce..7b2d7317bc 100644 --- a/api/v3/handlers/billinginvoices/handler.go +++ b/api/v3/handlers/billinginvoices/handler.go @@ -8,6 +8,7 @@ import ( ) type Handler interface { + InvoiceCustomerCharges() InvoiceCustomerChargesHandler ListBillingInvoices() ListBillingInvoicesHandler GetBillingInvoice() GetBillingInvoiceHandler UpdateBillingInvoice() UpdateBillingInvoiceHandler diff --git a/api/v3/handlers/billinginvoices/invoice_charges.go b/api/v3/handlers/billinginvoices/invoice_charges.go new file mode 100644 index 0000000000..1cd7bf4429 --- /dev/null +++ b/api/v3/handlers/billinginvoices/invoice_charges.go @@ -0,0 +1,83 @@ +package billinginvoices + +import ( + "context" + "fmt" + "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" + "github.com/openmeterio/openmeter/openmeter/customer" + "github.com/openmeterio/openmeter/pkg/framework/commonhttp" + "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" + "github.com/openmeterio/openmeter/pkg/slicesx" + "github.com/samber/lo" +) + +type ( + InvoiceCustomerChargesRequest struct { + billing.InvoicePendingLinesInput + Options []billing.InvoicePendingLinesOption + } + InvoiceCustomerChargesResponse = api.InvoiceCustomerChargesResult + InvoiceCustomerChargesHandler = httptransport.HandlerWithArgs[InvoiceCustomerChargesRequest, InvoiceCustomerChargesResponse, string] +) + +func (h *handler) InvoiceCustomerCharges() InvoiceCustomerChargesHandler { + return httptransport.NewHandlerWithArgs( + func(ctx context.Context, r *http.Request, customerID string) (InvoiceCustomerChargesRequest, error) { + ns, err := h.resolveNamespace(ctx) + if err != nil { + return InvoiceCustomerChargesRequest{}, err + } + + var body api.InvoiceCustomerChargesInput + if err := request.ParseOptionalBody(r, &body); err != nil { + return InvoiceCustomerChargesRequest{}, err + } + + input := InvoiceCustomerChargesRequest{ + Customer: customer.CustomerID{ + Namespace: ns, + ID: customerID, + }, + AsOf: body.AsOf, + Options: []billing.InvoicePendingLinesOption{billing.WithBypassCollectionAlignment()}, + } + + if body.ProgressiveBillingOverride != nil { + if *body.ProgressiveBillingOverride { + input.Options = append(input.Options, billing.WithPartialInvoiceLinesEnabled()) + } else { + input.Options = append(input.Options, billing.WithPartialInvoiceLinesDisabled()) + } + } + + return input, nil + }, + func(ctx context.Context, input InvoiceCustomerChargesRequest) (InvoiceCustomerChargesResponse, error) { + invoices, err := h.service.InvoicePendingLines(ctx, input.InvoicePendingLinesInput, input.Options...) + if err != nil { + return InvoiceCustomerChargesResponse{}, fmt.Errorf("collecting customer pending lines: %w", err) + } + + apiInvoices, err := lo.MapErr(invoices, slicesx.WrapMapFn(ToAPIStandardInvoice)) + if err != nil { + return InvoiceCustomerChargesResponse{}, fmt.Errorf("mapping collected invoice: %w", err) + } + + return InvoiceCustomerChargesResponse{ + Invoices: apiInvoices, + }, nil + }, + commonhttp.JSONResponseEncoderWithStatus[InvoiceCustomerChargesResponse](http.StatusCreated), + httptransport.AppendOptions( + h.options, + httptransport.WithOperationName("invoice-customer-charges"), + httptransport.WithErrorEncoder(apierrors.GenericErrorEncoder()), + httptransport.WithErrorEncoder(errorEncoder()), + )..., + ) +} diff --git a/api/v3/handlers/billinginvoices/invoice_charges_test.go b/api/v3/handlers/billinginvoices/invoice_charges_test.go new file mode 100644 index 0000000000..c39785e2df --- /dev/null +++ b/api/v3/handlers/billinginvoices/invoice_charges_test.go @@ -0,0 +1,198 @@ +package billinginvoices + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/samber/lo" + "github.com/stretchr/testify/require" + + api "github.com/openmeterio/openmeter/api/v3" + "github.com/openmeterio/openmeter/openmeter/billing" + "github.com/openmeterio/openmeter/openmeter/customer" +) + +const invoiceChargesCustomerID = "01K3Q8QG4B9YJYJVQ3MZ3V8KT6" + +type invoiceChargesBillingService struct { + billing.Service + collect func(context.Context, billing.InvoicePendingLinesInput, ...billing.InvoicePendingLinesOption) ([]billing.StandardInvoice, error) +} + +func (s invoiceChargesBillingService) InvoicePendingLines(ctx context.Context, input billing.InvoicePendingLinesInput, opts ...billing.InvoicePendingLinesOption) ([]billing.StandardInvoice, error) { + return s.collect(ctx, input, opts...) +} + +type invoiceChargesServer struct { + api.Unimplemented + handler Handler +} + +func (s invoiceChargesServer) InvoiceCustomerCharges(w http.ResponseWriter, r *http.Request, customerID api.ULID) { + s.handler.InvoiceCustomerCharges().With(customerID).ServeHTTP(w, r) +} + +func TestInvoiceCustomerChargesManualCollection(t *testing.T) { + asOf := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) + tests := []struct { + name string + body string + asOf *time.Time + progressive *bool + }{ + { + name: "empty body uses customer settings", + }, + { + name: "empty object uses customer settings", + body: `{}`, + }, + { + name: "cutoff and progressive billing enabled", + body: `{"as_of":"2026-01-01T12:00:00Z","progressive_billing_override":true}`, + asOf: &asOf, + progressive: lo.ToPtr(true), + }, + { + name: "progressive billing explicitly disabled", + body: `{"progressive_billing_override":false}`, + progressive: lo.ToPtr(false), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // given: the customer has eligible lines in two currencies. + called := false + service := invoiceChargesBillingService{ + collect: func(_ context.Context, input billing.InvoicePendingLinesInput, opts ...billing.InvoicePendingLinesOption) ([]billing.StandardInvoice, error) { + called = true + require.Equal(t, customer.CustomerID{Namespace: "ns", ID: invoiceChargesCustomerID}, input.Customer) + require.Equal(t, test.asOf, input.AsOf) + require.False(t, input.IncludePendingLines.IsPresent()) + require.False(t, input.ForceAsyncAdvance) + require.Equal(t, billing.InvoicePendingLinesOptions{ + BypassCollectionAlignment: true, + PartialInvoiceLinesEnabled: test.progressive, + }, billing.NewInvoicePendingLinesOptions(opts...)) + + return []billing.StandardInvoice{ + {StandardInvoiceBase: billing.StandardInvoiceBase{ID: "usd-invoice", Currency: "USD", Status: billing.StandardInvoiceStatusDraftCreated}}, + {StandardInvoiceBase: billing.StandardInvoiceBase{ID: "eur-invoice", Currency: "EUR", Status: billing.StandardInvoiceStatusDraftCreated}}, + }, nil + }, + } + handler := New(func(context.Context) (string, error) { return "ns", nil }, service) + router := api.Handler(invoiceChargesServer{handler: handler}) + + // when: manually collecting through the generated customer route. + req := httptest.NewRequest(http.MethodPost, "/openmeter/customers/"+invoiceChargesCustomerID+"/charges/invoice", strings.NewReader(test.body)) + req.Header.Set("Content-Type", "application/json") + res := httptest.NewRecorder() + router.ServeHTTP(res, req) + + // then: both standard invoices are returned after forwarding the manual options. + require.Equal(t, http.StatusCreated, res.Code, res.Body.String()) + require.True(t, called) + var result api.InvoiceCustomerChargesResult + require.NoError(t, json.Unmarshal(res.Body.Bytes(), &result)) + require.Len(t, result.Invoices, 2) + usdInvoice, err := result.Invoices[0].AsBillingInvoiceStandard() + require.NoError(t, err) + require.Equal(t, "usd-invoice", usdInvoice.Id) + require.Equal(t, api.CurrencyCode("USD"), usdInvoice.Currency) + eurInvoice, err := result.Invoices[1].AsBillingInvoiceStandard() + require.NoError(t, err) + require.Equal(t, api.CurrencyCode("EUR"), eurInvoice.Currency) + }) + } +} + +func TestInvoiceCustomerChargesAllBlockedReturnsValidationError(t *testing.T) { + handler := New(func(context.Context) (string, error) { return "ns", nil }, invoiceChargesBillingService{ + collect: func(context.Context, billing.InvoicePendingLinesInput, ...billing.InvoicePendingLinesOption) ([]billing.StandardInvoice, error) { + return nil, billing.ValidationError{Err: billing.ErrInvoiceCreateAllLinesGated} + }, + }) + req := httptest.NewRequest(http.MethodPost, "/", nil) + res := httptest.NewRecorder() + + handler.InvoiceCustomerCharges().With(invoiceChargesCustomerID).ServeHTTP(res, req) + + require.Equal(t, http.StatusBadRequest, res.Code, res.Body.String()) + require.Contains(t, res.Body.String(), "all eligible pending lines are blocked from invoice assignment") +} + +func TestInvoiceCustomerChargesErrors(t *testing.T) { + tests := []struct { + name string + body string + serviceErr error + status int + called bool + }{ + { + name: "malformed JSON", + body: `{`, + status: http.StatusBadRequest, + }, + { + name: "malformed cutoff", + body: `{"as_of":"yesterday"}`, + status: http.StatusBadRequest, + }, + { + name: "future cutoff", + body: `{"as_of":"9999-01-01T00:00:00Z"}`, + status: http.StatusBadRequest, + called: true, + }, + { + name: "no eligible lines", + serviceErr: billing.ValidationError{Err: billing.ErrInvoiceCreateNoLines}, + status: http.StatusBadRequest, + called: true, + }, + { + name: "line not found", + serviceErr: billing.NotFoundError{Entity: billing.EntityInvoiceLine, ID: "missing-line"}, + status: http.StatusNotFound, + called: true, + }, + { + name: "service failure", + serviceErr: errors.New("collection failed"), + status: http.StatusInternalServerError, + called: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + called := false + handler := New(func(context.Context) (string, error) { return "ns", nil }, invoiceChargesBillingService{ + collect: func(_ context.Context, input billing.InvoicePendingLinesInput, _ ...billing.InvoicePendingLinesOption) ([]billing.StandardInvoice, error) { + called = true + if err := input.Validate(); err != nil { + return nil, billing.ValidationError{Err: err} + } + + return nil, test.serviceErr + }, + }) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(test.body)) + res := httptest.NewRecorder() + + handler.InvoiceCustomerCharges().With(invoiceChargesCustomerID).ServeHTTP(res, req) + + require.Equal(t, test.status, res.Code, res.Body.String()) + require.Equal(t, test.called, called) + }) + } +} diff --git a/api/v3/openapi.yaml b/api/v3/openapi.yaml index d85912e994..8af6301be1 100644 --- a/api/v3/openapi.yaml +++ b/api/v3/openapi.yaml @@ -1611,6 +1611,51 @@ paths: schema: $ref: '#/components/schemas/CreateChargeRequest' x-unstable: true + /openmeter/customers/{customerId}/charges/invoice: + post: + operationId: invoice-customer-charges + summary: Invoice a customer's charges + description: |- + Invoice a customer's charges outside the normal billing cycle. Collection + alignment is bypassed. The customer's progressive billing setting applies unless + explicitly overridden. All eligible pending lines are considered. + + Returns the created standard invoices, one per collected currency. Lines blocked + by their owning engine remain pending. If all eligible lines are blocked or no + lines are eligible, returns a validation error. + parameters: + - name: customerId + in: path + required: true + schema: + $ref: '#/components/schemas/ULID' + responses: + '201': + description: The request has succeeded and a new resource has been created as a result. + content: + application/json: + schema: + $ref: '#/components/schemas/InvoiceCustomerChargesResult' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + tags: + - OpenMeter Customers + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/InvoiceCustomerChargesInput' + x-internal: true + x-private: true + x-unstable: true + x-rpc-endpoint: true /openmeter/customers/{customerId}/credits/adjustments: post: operationId: create-credit-adjustment @@ -17054,6 +17099,32 @@ components: $ref: '#/components/schemas/CursorMeta' additionalProperties: false description: Cursor paginated response. + InvoiceCustomerChargesInput: + type: object + properties: + as_of: + allOf: + - $ref: '#/components/schemas/DateTime' + description: Collection cutoff. Defaults to the current time and cannot be in the future. + progressive_billing_override: + type: boolean + description: |- + Override the customer's progressive billing setting for this collection. When + enabled, eligible usage-based lines can be collected for a partial period. + additionalProperties: false + description: Options for manually collecting a customer's pending lines into invoices. + InvoiceCustomerChargesResult: + type: object + required: + - invoices + properties: + invoices: + type: array + items: + $ref: '#/components/schemas/BillingInvoice' + description: Created standard invoices, one per collected currency. + additionalProperties: false + description: Result of manually collecting a customer's pending lines into invoices. InvoicePagePaginatedResponse: type: object required: diff --git a/api/v3/server/routes.go b/api/v3/server/routes.go index b37497b633..88fc38a026 100644 --- a/api/v3/server/routes.go +++ b/api/v3/server/routes.go @@ -720,6 +720,10 @@ func (s *Server) CreateCustomerCharges(w http.ResponseWriter, r *http.Request, c }).ServeHTTP(w, r) } +func (s *Server) InvoiceCustomerCharges(w http.ResponseWriter, r *http.Request, customerId api.ULID) { + s.billingInvoicesHandler.InvoiceCustomerCharges().With(customerId).ServeHTTP(w, r) +} + // Organization Default Tax Codes func (s *Server) GetOrganizationDefaultTaxCodes(w http.ResponseWriter, r *http.Request) { diff --git a/api/v3/test/charges_test.go b/api/v3/test/charges_test.go index e743312a62..009707a40d 100644 --- a/api/v3/test/charges_test.go +++ b/api/v3/test/charges_test.go @@ -58,3 +58,41 @@ func TestCreateFlatFeeChargeRejectsUsageBasedPrice(t *testing.T) { require.Equal(t, http.StatusBadRequest, response.Code) require.Contains(t, response.Body.String(), "price") } + +func TestInvoiceCustomerChargesRejectsLineIDs(t *testing.T) { + // given: the public action accepts collection settings without a line filter. + doc, err := api.GetSpec() + require.NoError(t, err) + + validationRouter, err := oasmiddleware.NewValidationRouter(t.Context(), doc, &oasmiddleware.ValidationRouterOpts{ + DeleteServers: true, + }) + require.NoError(t, err) + + router := chi.NewRouter() + router.Use(oasmiddleware.ValidateRequest(validationRouter, oasmiddleware.ValidateRequestOption{ + RouteNotFoundHook: oasmiddleware.OasRouteNotFoundErrorHook, + RouteValidationErrorHook: func(err error, w http.ResponseWriter, request *http.Request) bool { + return oasmiddleware.OasValidationErrorHook(request.Context(), err, w, request) + }, + FilterOptions: &openapi3filter.Options{ + AuthenticationFunc: openapi3filter.NoopAuthenticationFunc, + MultiError: true, + }, + })) + router.Post("/openmeter/customers/{customerId}/charges/invoice", func(http.ResponseWriter, *http.Request) { + t.Fatal("an unsupported line filter must not reach collection") + }) + + // when: a caller supplies the removed field. + request := httptest.NewRequest(http.MethodPost, "/openmeter/customers/01K3Q8QG4B9YJYJVQ3MZ3V8KT6/charges/invoice", strings.NewReader(`{ + "line_ids":["01K3Q8QG4B9YJYJVQ3MZ3V8KT7"] + }`)) + request.Header.Set("Content-Type", "application/json") + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + // then: validation rejects it instead of silently collecting every line. + require.Equal(t, http.StatusBadRequest, response.Code, response.Body.String()) + require.Contains(t, response.Body.String(), "line_ids") +} diff --git a/openmeter/billing/README.md b/openmeter/billing/README.md index c9cd182590..78c895da5c 100644 --- a/openmeter/billing/README.md +++ b/openmeter/billing/README.md @@ -157,9 +157,13 @@ charge engines handle charge-backed lines. Before splitting or materializing otherwise billable gathering lines, billing asks each line engine to gate their invoice assignment. Blocked lines remain on the gathering invoice and do not consume the invoice line limit. A gate may -persist engine-owned state explaining its decision; when every candidate is -blocked, collection succeeds without creating an invoice so those writes can -commit. Gate errors abort collection and roll back its writes. Charge-backed +persist engine-owned state explaining its decision. When every candidate is +blocked, collection finishes its transaction without creating an invoice, then +returns `ErrInvoiceCreateAllLinesGated` as a validation error. Callers with an +enclosing transaction must handle this deferred outcome so gate diagnostics can +commit. Automatic collection, subscription sync, and invoice-now during charge +creation treat it as deferred work. Gate errors abort collection and roll back +its writes. Charge-backed usage lines whose required feature or meter is unavailable are blocked here; the charge records the dependency issue and a later collection attempt re-evaluates it. diff --git a/openmeter/billing/charges/service/create.go b/openmeter/billing/charges/service/create.go index e205e7c940..c338973a59 100644 --- a/openmeter/billing/charges/service/create.go +++ b/openmeter/billing/charges/service/create.go @@ -2,6 +2,7 @@ package service import ( "context" + "errors" "fmt" "sync" "time" @@ -453,6 +454,10 @@ func (s *service) invokeInvoiceNowOnCreate(ctx context.Context, invoiceNowLines }, billing.WithBypassCollectionAlignment(), ); err != nil { + if errors.Is(err, billing.ErrInvoiceCreateAllLinesGated) { + continue + } + return fmt.Errorf("invoking invoice now on create: %w", err) } } diff --git a/openmeter/billing/charges/service/pendinglines_test.go b/openmeter/billing/charges/service/pendinglines_test.go index 7b4d3b697c..acca61fbc4 100644 --- a/openmeter/billing/charges/service/pendinglines_test.go +++ b/openmeter/billing/charges/service/pendinglines_test.go @@ -1,31 +1,193 @@ package service import ( + "bytes" "context" "encoding/json" "errors" + "fmt" + "net/http" + "net/http/httptest" + "testing" "time" "github.com/alpacahq/alpacadecimal" + "github.com/oklog/ulid/v2" "github.com/samber/lo" + "github.com/stretchr/testify/require" + api "github.com/openmeterio/openmeter/api/v3" + "github.com/openmeterio/openmeter/api/v3/handlers/billinginvoices" "github.com/openmeterio/openmeter/openmeter/app" "github.com/openmeterio/openmeter/openmeter/billing" "github.com/openmeterio/openmeter/openmeter/billing/charges" "github.com/openmeterio/openmeter/openmeter/billing/charges/flatfee" "github.com/openmeterio/openmeter/openmeter/billing/charges/meta" "github.com/openmeterio/openmeter/openmeter/billing/charges/models/creditrealization" + "github.com/openmeterio/openmeter/openmeter/billing/charges/models/ledgertransaction" + "github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased" + "github.com/openmeterio/openmeter/openmeter/customer" + "github.com/openmeterio/openmeter/openmeter/meter" "github.com/openmeterio/openmeter/openmeter/productcatalog" featurepkg "github.com/openmeterio/openmeter/openmeter/productcatalog/feature" "github.com/openmeterio/openmeter/openmeter/taxcode" "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/currencyx" "github.com/openmeterio/openmeter/pkg/datetime" + "github.com/openmeterio/openmeter/pkg/framework/transaction" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/timeutil" billingtest "github.com/openmeterio/openmeter/test/billing" ) +type invoiceNowErrorBillingService struct { + billing.Service + err error + calls int +} + +func (s *invoiceNowErrorBillingService) InvoicePendingLines(context.Context, billing.InvoicePendingLinesInput, ...billing.InvoicePendingLinesOption) ([]billing.StandardInvoice, error) { + s.calls++ + return nil, s.err +} + +func TestInvoiceNowOnCreateDefersFullyGatedCollection(t *testing.T) { + unexpectedErr := errors.New("unexpected collection failure") + tests := []struct { + name string + err error + wantErr error + wantCalls int + }{ + {name: "fully gated customers are deferred", err: fmt.Errorf("wrapped: %w", billing.ValidationError{Err: billing.ErrInvoiceCreateAllLinesGated}), wantCalls: 2}, + {name: "no eligible lines remains an error", err: billing.ValidationError{Err: billing.ErrInvoiceCreateNoLines}, wantErr: billing.ErrInvoiceCreateNoLines, wantCalls: 1}, + {name: "operational failure", err: unexpectedErr, wantErr: unexpectedErr, wantCalls: 1}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + billingService := &invoiceNowErrorBillingService{err: test.err} + svc := &service{billingService: billingService} + + err := svc.invokeInvoiceNowOnCreate(t.Context(), []invoicePendingLinesInput{ + {CustomerID: customer.CustomerID{Namespace: "ns", ID: "customer-1"}, LineID: "line-1"}, + {CustomerID: customer.CustomerID{Namespace: "ns", ID: "customer-2"}, LineID: "line-2"}, + }) + require.ErrorIs(t, err, test.wantErr) + require.Equal(t, test.wantCalls, billingService.calls) + }) + } +} + +func (s *InvoicableChargesTestSuite) TestInvoicePendingLinesAllGatedReturnsErrorAndPersistsChargeIssues() { + tests := []struct { + name string + enclosingTransaction bool + }{ + {name: "standalone transaction"}, + {name: "caller handles deferral in enclosing transaction", enclosingTransaction: true}, + } + for _, test := range tests { + s.Run(test.name, func() { + // given: a due charge's required meter becomes unavailable. + ctx := s.T().Context() + ns := s.GetUniqueNamespace("all-gated-charge-validation") + s.TaxCodeEnv.ProvisionDefaultTaxCodes(s.T(), ns) + customInvoicing := s.SetupCustomInvoicing(ns) + s.ProvisionBillingProfile(ctx, ns, customInvoicing.App.GetID(), billingtest.WithManualApproval()) + cust := s.CreateTestCustomer(ns, "test-subject") + period := timeutil.ClosedPeriod{ + From: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), + To: time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC), + } + clock.FreezeTime(period.To) + defer clock.UnFreeze() + + featureKey := "temporarily-unavailable-meter" + featureMeters := s.createFeatureMeters(ctx, ns, featureKey) + meters := []meter.Meter{*featureMeters.ByKey[featureKey].Meter} + pending, err := s.Charges.CreatePendingInvoiceLines(ctx, charges.CreatePendingInvoiceLinesInput{ + Customer: cust.GetID(), + Currency: currencyx.FiatCode(USD), + Lines: billing.CreatePendingInvoiceLines{{ + GatheringLine: billing.GatheringLine{ + GatheringLineBase: billing.GatheringLineBase{ + ManagedResource: models.NewManagedResource(models.ManagedResourceInput{ + Namespace: ns, + Name: "blocked usage", + }), + ManagedBy: billing.ManuallyManagedLine, + Currency: currencyx.FiatCode(USD), + ServicePeriod: period, + InvoiceAt: period.To, + FeatureKey: featureKey, + Price: *productcatalog.NewPriceFrom(productcatalog.UnitPrice{ + Amount: alpacadecimal.NewFromInt(2), + }), + }, + }, + }}, + }) + s.Require().NoError(err) + s.Require().Len(pending.Lines, 1) + s.Require().NotNil(pending.Lines[0].ChargeID) + s.Require().NoError(s.MeterAdapter.ReplaceMeters(ctx, nil)) + defer func() { + s.Require().NoError(s.MeterAdapter.ReplaceMeters(ctx, meters)) + }() + chargeID := meta.ChargeID{Namespace: ns, ID: *pending.Lines[0].ChargeID} + input := billing.InvoicePendingLinesInput{Customer: cust.GetID(), AsOf: &period.To} + + // when: collection is fully gated, including when the caller owns the transaction. + var collectionErr error + if test.enclosingTransaction { + _, err = transaction.Run(ctx, s.BillingAdapter, func(ctx context.Context) (struct{}, error) { + _, collectionErr = s.BillingService.InvoicePendingLines(ctx, input, billing.WithBypassCollectionAlignment()) + if errors.Is(collectionErr, billing.ErrInvoiceCreateAllLinesGated) { + return struct{}{}, nil + } + + return struct{}{}, collectionErr + }) + s.Require().NoError(err) + } else { + _, collectionErr = s.BillingService.InvoicePendingLines(ctx, input, billing.WithBypassCollectionAlignment()) + } + + // then: collection returns an error, the charge stores its validation issues, and the line remains pending. + s.Require().ErrorIs(collectionErr, billing.ErrInvoiceCreateAllLinesGated) + s.ErrorAs(collectionErr, &billing.ValidationError{}) + charge, err := s.UsageBasedAdapter.GetByID(ctx, usagebased.GetByIDInput{ChargeID: chargeID}) + s.Require().NoError(err) + s.Require().True(lo.ContainsBy(charge.ValidationIssues, func(issue billing.ValidationIssue) bool { + return issue.Code == billing.ErrInvoiceLineFeatureHasNoMeters.Code + })) + gathering, err := s.BillingService.GetGatheringInvoiceById(ctx, billing.GetGatheringInvoiceByIdInput{ + Invoice: pending.Invoice.GetInvoiceID(), + Expand: billing.GatheringInvoiceExpands{billing.GatheringInvoiceExpandLines}, + }) + s.Require().NoError(err) + s.Require().Len(gathering.Lines.OrEmpty(), 1) + s.Equal(pending.Lines[0].ID, gathering.Lines.OrEmpty()[0].ID) + + // when: the missing dependency is repaired and collection is retried. + s.Require().NoError(s.MeterAdapter.ReplaceMeters(ctx, meters)) + s.MockStreamingConnector.AddSimpleEvent(meters[0].Key, 1, period.From.Add(time.Hour)) + s.UsageBasedTestHandler.onCreditsOnlyUsageAccrued, _ = newCappedCreditAllocator(0) + defer s.UsageBasedTestHandler.Reset() + invoices, err := s.BillingService.InvoicePendingLines(ctx, input, billing.WithBypassCollectionAlignment()) + s.Require().NoError(err) + + // then: collection creates one invoice and clears the stale dependency issue. + s.Require().Len(invoices, 1) + charge, err = s.UsageBasedAdapter.GetByID(ctx, usagebased.GetByIDInput{ChargeID: chargeID}) + s.Require().NoError(err) + s.False(lo.ContainsBy(charge.ValidationIssues, func(issue billing.ValidationIssue) bool { + return issue.Code == billing.ErrInvoiceLineFeatureHasNoMeters.Code + })) + }) + } +} + func (s *InvoicableChargesTestSuite) TestCreatePendingInvoiceLinesResolvesStripeTaxCodeBeforeCreatingCharge() { ctx := s.T().Context() ns := s.GetUniqueNamespace("charges-service-create-pending-lines-stripe-tax") @@ -716,3 +878,111 @@ func (s *InvoicableChargesTestSuite) TestCreatePendingInvoiceLinesRollsBackParti s.NoError(err) s.Empty(listedInvoices.Items) } + +func (s *InvoicableChargesTestSuite) TestInvoiceCustomerChargesAPIUsesChargeEngine() { + // given: credits are enabled, with a due charge and a future charge pending. + ctx := s.T().Context() + ns := s.GetUniqueNamespace("manual-collection-api") + s.TaxCodeEnv.ProvisionDefaultTaxCodes(s.T(), ns) + customInvoicing := s.SetupCustomInvoicing(ns) + s.ProvisionBillingProfile(ctx, ns, customInvoicing.App.GetID(), billingtest.WithManualApproval()) + cust := s.CreateTestCustomer(ns, "test-subject") + period := timeutil.ClosedPeriod{ + From: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), + To: time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC), + } + clock.FreezeTime(period.To) + defer clock.UnFreeze() + + allocationCalls := 0 + s.FlatFeeTestHandler.onAllocateCredits = func(_ context.Context, input flatfee.OnAllocateCreditsInput) (creditrealization.CreateAllocationInputs, error) { + allocationCalls++ + + return creditrealization.CreateAllocationInputs{ + { + ServicePeriod: input.ServicePeriod, + LedgerTransaction: ledgertransaction.GroupReference{ + TransactionGroupID: ulid.Make().String(), + }, + Amount: alpacadecimal.NewFromInt(4), + }, + }, nil + } + defer s.FlatFeeTestHandler.Reset() + + dueLine := billing.NewFlatFeeGatheringLine(billing.NewFlatFeeLineInput{ + Namespace: ns, + Period: period, + InvoiceAt: period.To, + ManagedBy: billing.ManuallyManagedLine, + Name: "due charge", + Currency: currencyx.FiatCode(USD), + PerUnitAmount: alpacadecimal.NewFromInt(10), + PaymentTerm: productcatalog.InAdvancePaymentTerm, + }) + futureLine := billing.NewFlatFeeGatheringLine(billing.NewFlatFeeLineInput{ + Namespace: ns, + Period: period, + InvoiceAt: period.To.Add(time.Hour), + ManagedBy: billing.ManuallyManagedLine, + Name: "future charge", + Currency: currencyx.FiatCode(USD), + PerUnitAmount: alpacadecimal.NewFromInt(10), + PaymentTerm: productcatalog.InAdvancePaymentTerm, + }) + pending, err := s.Charges.CreatePendingInvoiceLines(ctx, charges.CreatePendingInvoiceLinesInput{ + Customer: cust.GetID(), + Currency: currencyx.FiatCode(USD), + Lines: billing.NewCreatePendingInvoiceLines([]billing.GatheringLine{dueLine, futureLine}), + }) + s.Require().NoError(err) + s.Require().NotNil(pending) + s.Require().Len(pending.Lines, 2) + s.Equal(billing.LineEngineTypeChargeFlatFee, pending.Lines[0].Engine) + s.Require().NotNil(pending.Lines[0].ChargeID) + + // when: manually collecting through the v3 handler at the due line's cutoff. + body, err := json.Marshal(api.InvoiceCustomerChargesInput{ + AsOf: lo.ToPtr(period.To), + ProgressiveBillingOverride: lo.ToPtr(false), + }) + s.Require().NoError(err) + req := httptest.NewRequest(http.MethodPost, "/openmeter/customers/"+cust.ID+"/charges/invoice", bytes.NewReader(body)).WithContext(ctx) + req.Header.Set("Content-Type", "application/json") + res := httptest.NewRecorder() + handler := billinginvoices.New(func(context.Context) (string, error) { return ns, nil }, s.BillingService) + handler.InvoiceCustomerCharges().With(cust.ID).ServeHTTP(res, req) + + // then: collection invokes the charge engine, applies credits, and retains the future line. + s.Require().Equal(http.StatusCreated, res.Code, res.Body.String()) + var result api.InvoiceCustomerChargesResult + s.Require().NoError(json.Unmarshal(res.Body.Bytes(), &result)) + s.Require().Len(result.Invoices, 1) + invoice, err := result.Invoices[0].AsBillingInvoiceStandard() + s.Require().NoError(err) + s.Equal("6", invoice.Totals.Total) + s.Equal("4", invoice.Totals.CreditsTotal) + s.Equal(1, allocationCalls) + s.Require().NotNil(invoice.Lines) + s.Require().Len(*invoice.Lines, 1) + line, err := (*invoice.Lines)[0].AsBillingInvoiceStandardLine() + s.Require().NoError(err) + s.Require().NotNil(line.Charge) + s.Equal(*pending.Lines[0].ChargeID, line.Charge.Id) + s.Equal(lo.ToPtr(pending.Lines[0].ID), line.Id) + + charge := s.mustGetChargeByID(meta.ChargeID{Namespace: ns, ID: *pending.Lines[0].ChargeID}) + flatFeeCharge, err := charge.AsFlatFeeCharge() + s.Require().NoError(err) + s.Require().NotNil(flatFeeCharge.Realizations.CurrentRun) + s.Equal(lo.ToPtr(invoice.Id), flatFeeCharge.Realizations.CurrentRun.InvoiceID) + s.Equal(lo.ToPtr(pending.Lines[0].ID), flatFeeCharge.Realizations.CurrentRun.LineID) + + gathering, err := s.BillingService.GetGatheringInvoiceById(ctx, billing.GetGatheringInvoiceByIdInput{ + Invoice: pending.Invoice.GetInvoiceID(), + Expand: billing.GatheringInvoiceExpands{billing.GatheringInvoiceExpandLines}, + }) + s.Require().NoError(err) + s.Require().Len(gathering.Lines.OrEmpty(), 1) + s.Equal(pending.Lines[1].ID, gathering.Lines.OrEmpty()[0].ID) +} diff --git a/openmeter/billing/charges/service/usagebased_test.go b/openmeter/billing/charges/service/usagebased_test.go index 88357b2431..8ee71c6d07 100644 --- a/openmeter/billing/charges/service/usagebased_test.go +++ b/openmeter/billing/charges/service/usagebased_test.go @@ -2028,7 +2028,7 @@ func (s *UsageBasedChargesTestSuite) TestUsageBasedCreditThenInvoicePartialInvoi }) // then - s.NoError(err) + s.Require().ErrorIs(err, billing.ErrInvoiceCreateAllLinesGated) s.Empty(invoices) charge := s.mustGetUsageBasedChargeByID(usageBasedChargeID) @@ -2417,7 +2417,7 @@ func (s *UsageBasedChargesTestSuite) TestUsageBasedCreditThenInvoicePendingParti }) // then - s.NoError(err) + s.Require().ErrorIs(err, billing.ErrInvoiceCreateAllLinesGated) s.Empty(invoices) charge := s.mustGetUsageBasedChargeByID(usageBasedChargeID) @@ -2439,7 +2439,7 @@ func (s *UsageBasedChargesTestSuite) TestUsageBasedCreditThenInvoicePendingParti // then // the gate remains idempotent - s.NoError(err) + s.Require().ErrorIs(err, billing.ErrInvoiceCreateAllLinesGated) s.Empty(invoices) charge = s.mustGetUsageBasedChargeByID(usageBasedChargeID) s.Len(charge.ValidationIssues, 1) diff --git a/openmeter/billing/errors.go b/openmeter/billing/errors.go index e88fd2e094..3a429021e4 100644 --- a/openmeter/billing/errors.go +++ b/openmeter/billing/errors.go @@ -54,6 +54,7 @@ var ( ErrInvoiceLineNoPeriodChangeForSubscriptionManagedLine = NewValidationError("invoice_line_no_period_change_for_subscription_managed_line", "invoice line period cannot be changed for subscription managed lines") ErrInvoiceLineProgressiveBillingUsageDiscountUpdateForbidden = NewValidationError("invoice_line_progressive_billing_usage_discount_update_forbidden", "usage discount cannot be updated on a partially invoiced line") ErrInvoiceCreateNoLines = NewValidationError("invoice_create_no_lines", "the new invoice would have no lines") + ErrInvoiceCreateAllLinesGated = NewValidationError("invoice_create_all_lines_gated", "all eligible pending lines are blocked from invoice assignment") ErrInvoiceCreateUBPLineCustomerUsageAttributionInvalid = NewValidationError("invoice_create_ubp_line_customer_has_no_subjects", "creating an usage based line: customer usage attribution is invalid") ErrInvoiceCreateUBPLinePeriodIsEmpty = NewValidationError("invoice_create_ubp_line_period_is_empty", "creating an usage based line: truncated period is empty") ErrInvoiceLineCurrencyMismatch = NewValidationError("invoice_line_currency_mismatch", "invoice line currency mismatch") diff --git a/openmeter/billing/service/gatheringinvoicependinglines.go b/openmeter/billing/service/gatheringinvoicependinglines.go index 7f940c0165..ddc4019414 100644 --- a/openmeter/billing/service/gatheringinvoicependinglines.go +++ b/openmeter/billing/service/gatheringinvoicependinglines.go @@ -59,7 +59,7 @@ func (s *Service) InvoicePendingLines(ctx context.Context, input billing.Invoice } } - return transactionForInvoiceManipulation( + invoices, err := transactionForInvoiceManipulation( ctx, s, input.Customer, @@ -96,6 +96,17 @@ func (s *Service) InvoicePendingLines(ctx context.Context, input billing.Invoice return createdInvoices, nil }, ) + if err != nil { + return nil, err + } + + // Gates record why a charge is blocked. Finish their transaction before + // reporting a fully gated collection, so those reasons remain available. + if len(invoices) == 0 { + return nil, billing.ValidationError{Err: billing.ErrInvoiceCreateAllLinesGated} + } + + return invoices, nil } func (s *Service) prepareBillableLines(ctx context.Context, input billing.PrepareBillableLinesInput, options billing.InvoicePendingLinesOptions) (*billing.PrepareBillableLinesResult, error) { diff --git a/openmeter/billing/worker/collect/asynccollect_test.go b/openmeter/billing/worker/collect/asynccollect_test.go index eb9f23cd75..dcf735a5ef 100644 --- a/openmeter/billing/worker/collect/asynccollect_test.go +++ b/openmeter/billing/worker/collect/asynccollect_test.go @@ -1,13 +1,44 @@ package billingworkercollect import ( + "context" + "fmt" + "log/slog" "testing" + "time" "github.com/stretchr/testify/require" "github.com/openmeterio/openmeter/openmeter/billing" + "github.com/openmeterio/openmeter/openmeter/customer" ) +type gatedCollectionBillingService struct { + billing.Service +} + +func (gatedCollectionBillingService) InvoicePendingLines(context.Context, billing.InvoicePendingLinesInput, ...billing.InvoicePendingLinesOption) ([]billing.StandardInvoice, error) { + return nil, fmt.Errorf("collection deferred: %w", billing.ValidationError{Err: billing.ErrInvoiceCreateAllLinesGated}) +} + +func TestInvoiceCollectorDefersFullyGatedCollection(t *testing.T) { + // given: a fully gated collection has already recorded its diagnostics. + collector := &InvoiceCollector{ + billingService: gatedCollectionBillingService{}, + logger: slog.Default(), + } + + // when: the automatic collector encounters that deferred outcome. + invoices, err := collector.CollectCustomerInvoice(t.Context(), CollectCustomerInvoiceInput{ + CustomerID: customer.CustomerID{Namespace: "ns", ID: "customer-id"}, + AsOf: time.Now(), + }) + require.NoError(t, err) + + // then: it neither fails nor calls gathering-invoice recalculation. + require.Empty(t, invoices) +} + func TestInvoiceCollectorHandleCollectCustomerInvoicesEvent(t *testing.T) { collector := &InvoiceCollector{} diff --git a/openmeter/billing/worker/collect/collect.go b/openmeter/billing/worker/collect/collect.go index fe3135ed9b..4b2e4048b8 100644 --- a/openmeter/billing/worker/collect/collect.go +++ b/openmeter/billing/worker/collect/collect.go @@ -104,6 +104,10 @@ func (a *InvoiceCollector) CollectCustomerInvoice(ctx context.Context, params Co return nil, nil } + if errors.Is(err, billing.ErrInvoiceCreateAllLinesGated) { + return nil, nil + } + if errors.Is(err, billing.ErrInvoiceCreateNoLines) { a.logger.WarnContext(ctx, "no invoices generated for customer during collection (possible data inconsistency), recalculating gathering invoices", "customer", params.CustomerID) diff --git a/openmeter/billing/worker/subscriptionsync/service/sync.go b/openmeter/billing/worker/subscriptionsync/service/sync.go index aaebc1566b..55a975e5bb 100644 --- a/openmeter/billing/worker/subscriptionsync/service/sync.go +++ b/openmeter/billing/worker/subscriptionsync/service/sync.go @@ -44,7 +44,7 @@ func (s *Service) invoicePendingLines(ctx context.Context, customer customer.Cus billing.WithMaxLinesPerInvoice(s.featureFlags.MaxLinesPerCollectedInvoice), ) if err != nil { - if errors.Is(err, billing.ErrInvoiceCreateNoLines) { + if errors.Is(err, billing.ErrInvoiceCreateNoLines) || errors.Is(err, billing.ErrInvoiceCreateAllLinesGated) { return nil } diff --git a/openmeter/billing/worker/subscriptionsync/service/sync_credittheninvoice_test.go b/openmeter/billing/worker/subscriptionsync/service/sync_credittheninvoice_test.go index c386ad2fd2..ac6c5f1d23 100644 --- a/openmeter/billing/worker/subscriptionsync/service/sync_credittheninvoice_test.go +++ b/openmeter/billing/worker/subscriptionsync/service/sync_credittheninvoice_test.go @@ -301,7 +301,7 @@ func (s *CreditThenInvoiceTestSuite) TestSubscriptionSyncPersistsMeterlessUsageC // then: // - the gathering line remains pending and no financial processing starts - s.Require().NoError(err) + s.Require().ErrorIs(err, billing.ErrInvoiceCreateAllLinesGated) s.Empty(invoices) s.Require().Len(s.gatheringInvoice(ctx, s.Namespace, s.Customer.ID).Lines.OrEmpty(), 1) @@ -472,7 +472,7 @@ func (s *CreditThenInvoiceTestSuite) TestSubscriptionSyncPersistsMissingFeatureU // then: // - the gathering line remains pending and no financial processing starts - s.Require().NoError(err) + s.Require().ErrorIs(err, billing.ErrInvoiceCreateAllLinesGated) s.Empty(invoices) s.Require().Len(s.gatheringInvoice(ctx, s.Namespace, s.Customer.ID).Lines.OrEmpty(), 1) diff --git a/openmeter/billing/worker/subscriptionsync/service/sync_test.go b/openmeter/billing/worker/subscriptionsync/service/sync_test.go index d741fdefb3..b618f3acc1 100644 --- a/openmeter/billing/worker/subscriptionsync/service/sync_test.go +++ b/openmeter/billing/worker/subscriptionsync/service/sync_test.go @@ -2,6 +2,7 @@ package service import ( "context" + "errors" "fmt" "testing" "time" @@ -41,6 +42,40 @@ func TestSubscriptionHandlerScenarios(t *testing.T) { suite.Run(t, new(SubscriptionHandlerTestSuite)) } +type pendingLinesErrorBillingService struct { + billing.Service + err error +} + +func (s pendingLinesErrorBillingService) InvoicePendingLines(context.Context, billing.InvoicePendingLinesInput, ...billing.InvoicePendingLinesOption) ([]billing.StandardInvoice, error) { + return nil, s.err +} + +func (s *SubscriptionHandlerTestSuite) TestInvoicePendingLinesDeferredOutcomes() { + unexpectedErr := errors.New("unexpected collection failure") + tests := []struct { + name string + err error + wantErr error + }{ + {name: "all lines gated", err: fmt.Errorf("wrapped: %w", billing.ValidationError{Err: billing.ErrInvoiceCreateAllLinesGated})}, + {name: "no billable lines", err: billing.ValidationError{Err: billing.ErrInvoiceCreateNoLines}}, + {name: "operational failure", err: unexpectedErr, wantErr: unexpectedErr}, + } + for _, test := range tests { + s.Run(test.name, func() { + billingService := s.Service.billingService + s.Service.billingService = pendingLinesErrorBillingService{err: test.err} + defer func() { + s.Service.billingService = billingService + }() + + err := s.Service.invoicePendingLines(s.T().Context(), s.Customer.GetID()) + s.Require().ErrorIs(err, test.wantErr) + }) + } +} + func (s *SubscriptionHandlerTestSuite) TestSyncByIDWithLegacyItemCadence() { ctx := s.T().Context() start := s.mustParseTime("2024-01-01T00:00:00Z") diff --git a/test/billing/lineengine_test.go b/test/billing/lineengine_test.go index f20346fad8..2092338f6e 100644 --- a/test/billing/lineengine_test.go +++ b/test/billing/lineengine_test.go @@ -288,7 +288,7 @@ func (s *LineEngineTestSuite) TestGateInvoiceAssignmentDuringCollection() { s.registerMockLineEngine(s.T(), mockEngine) defer s.unregisterLineEngine(s.T(), mockEngine) - s.Run("all otherwise billable lines blocked returns an empty successful result", func() { + s.Run("all otherwise billable lines blocked returns a validation error", func() { // given mockEngine.Reset() namespace := s.GetUniqueNamespace("ns-line-engine-gate-all-blocked") @@ -318,7 +318,8 @@ func (s *LineEngineTestSuite) TestGateInvoiceAssignmentDuringCollection() { }, ombilling.WithBypassCollectionAlignment()) // then - s.Require().NoError(err) + s.Require().ErrorIs(err, ombilling.ErrInvoiceCreateAllLinesGated) + s.ErrorAs(err, &ombilling.ValidationError{}) s.Empty(invoices) s.True(gateCalled) s.False(buildCalled) @@ -379,6 +380,55 @@ func (s *LineEngineTestSuite) TestGateInvoiceAssignmentDuringCollection() { s.Equal(pendingLines[1].ID, invoices[0].Lines.OrEmpty()[0].ID) }) + s.Run("a blocked currency does not prevent another currency from invoicing", func() { + // given: the USD line is blocked while the EUR line is allowed. + mockEngine.Reset() + namespace := s.GetUniqueNamespace("ns-line-engine-gate-currencies") + customerID, blockedLines := s.createInvoiceAssignmentGateFixture(ctx, namespace, mockEngine.GetLineEngineType(), now) + allowedLine := ombilling.NewFlatFeeGatheringLine(ombilling.NewFlatFeeLineInput{ + Namespace: namespace, + Period: timeutil.ClosedPeriod{From: now.Add(-time.Hour), To: now}, + InvoiceAt: now, + Name: "allowed EUR line", + ManagedBy: ombilling.ManuallyManagedLine, + PerUnitAmount: alpacadecimal.NewFromInt(1), + PaymentTerm: productcatalog.InArrearsPaymentTerm, + }) + allowedLine.Currency = currencyx.FiatCode(currency.EUR) + allowedLine.Engine = mockEngine.GetLineEngineType() + _, err := s.BillingService.CreatePendingInvoiceLines(ctx, ombilling.CreatePendingInvoiceLinesInput{ + Customer: customerID, + Currency: currencyx.FiatCode(currency.EUR), + Lines: ombilling.NewCreatePendingInvoiceLines([]ombilling.GatheringLine{allowedLine}), + }) + s.Require().NoError(err) + mockEngine.gateInvoiceAssignment = func(_ context.Context, input ombilling.GateInvoiceAssignmentInput) (ombilling.GateInvoiceAssignmentResult, error) { + result := make(ombilling.GateInvoiceAssignmentResult) + for _, line := range input.Lines { + if line.ID == blockedLines[0].ID { + result[line.GetLineID()] = ombilling.InvoiceAssignmentGateResponse{ExcludeFromInvoice: true} + } + } + + return result, nil + } + mockEngine.buildStandardInvoiceLines = func(_ context.Context, input ombilling.BuildStandardInvoiceLinesInput) (ombilling.StandardLines, error) { + return mustAsNewStandardLines(input), nil + } + + // when: collection considers both currencies. + invoices, err := s.BillingService.InvoicePendingLines(ctx, ombilling.InvoicePendingLinesInput{ + Customer: customerID, + AsOf: &now, + ForceAsyncAdvance: true, + }, ombilling.WithBypassCollectionAlignment()) + s.Require().NoError(err) + + // then: the allowed currency produces its invoice. + s.Require().Len(invoices, 1) + s.Equal(currencyx.FiatCode(currency.EUR), invoices[0].Currency) + }) + s.Run("gate errors abort invoice creation", func() { // given mockEngine.Reset() diff --git a/test/credits/credit_then_invoice_test.go b/test/credits/credit_then_invoice_test.go index 644e6fe455..ab70500f76 100644 --- a/test/credits/credit_then_invoice_test.go +++ b/test/credits/credit_then_invoice_test.go @@ -233,7 +233,7 @@ func (s *CreditThenInvoiceTestSuite) TestUsageBasedCreditThenInvoiceCollectionPe Customer: cust.GetID(), AsOf: lo.ToPtr(servicePeriod.To), }) - s.Require().NoError(err) + s.Require().ErrorIs(err, billing.ErrInvoiceCreateAllLinesGated) s.Empty(invoices) charge := s.RequireUsageBasedChargeStatus(usageBasedChargeID, usagebased.StatusCreated) @@ -552,7 +552,7 @@ func (s *CreditThenInvoiceTestSuite) TestUsageBasedCreditThenInvoiceCollectionPe // then: // - collection excludes both lines and records a distinct critical issue on each charge - s.Require().NoError(err) + s.Require().ErrorIs(err, billing.ErrInvoiceCreateAllLinesGated) s.Empty(invoices) issueFeatureKeys := make([]string, 0, len(created)) @@ -850,7 +850,7 @@ func (s *CreditThenInvoiceTestSuite) TestUsageBasedCreditThenInvoiceCollectionPe Customer: cust.GetID(), AsOf: lo.ToPtr(servicePeriod.To), }) - s.Require().NoError(err) + s.Require().ErrorIs(err, billing.ErrInvoiceCreateAllLinesGated) s.Empty(invoices) charge := s.RequireUsageBasedChargeStatus(usageBasedChargeID, usagebased.StatusActiveRealizationProcessing)