diff --git a/AGENTS.md b/AGENTS.md index ce3a9a37e6..24185d257d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -96,7 +96,7 @@ All generated files have `// Code generated by X, DO NOT EDIT.` headers — neve 2. Run `make gen-api` to regenerate OpenAPI spec and SDKs 3. Run `make generate` to regenerate Go server/client code -The TypeSpec JS client emitted from `api/spec/packages/aip` now lands in `api/spec/packages/aip-client-javascript/`. The emitter regenerates `src/`, `README.md`, and five conformance test files (`tests/client.spec.ts`, `tests/meters.spec.ts`, `tests/errors.spec.ts`, `tests/nesting.spec.ts`, `tests/internal.spec.ts`); every regenerated file carries a `Code generated by @openmeter/typespec-typescript. DO NOT EDIT.` header — treat files without that header as hand-written. `package.json` is **stable, hand-maintained, and committed** (the emitter's `writeOutput` only writes the paths it lists, so the manifest survives regeneration). The test suite is vitest + `@fetch-mock/vitest`; keep hand-written tests and helpers in `tests/` (never `src/`), and put test-runner dependencies/scripts in the `api/spec/package.json` workspace root rather than the client package manifest. Static publish metadata (`name`, `license`, `homepage`, `repository`) lives directly in the client `package.json`; only the per-release `version` is injected at publish time. Operations marked `x-private` in the TypeSpec source are omitted from the generated SDK (they stay in the OpenAPI output); operations marked `x-internal` (without `x-private`) are emitted but quarantined under `client.internal..` (`src/sdk/internal.ts`, an `Internal` aggregate with `Internal` facade classes; internal ops share their group's `funcs/` and `models/operations/` modules, a group whose ops are all internal gets no public facade, and the `Internal` class is deliberately not re-exported from the package root — the name belongs to the Internal Server Error model). Removing an operation from the spec (or marking it private) leaves stale generated files behind — the emitter never deletes outputs, so `git rm` them. +The TypeSpec JS client emitted from `api/spec/packages/aip` now lands in `api/spec/packages/aip-client-javascript/`. The emitter regenerates `src/`, `README.md`, and five conformance test files (`tests/client.spec.ts`, `tests/meters.spec.ts`, `tests/errors.spec.ts`, `tests/nesting.spec.ts`, `tests/internal.spec.ts`); every regenerated file carries a `Code generated by @openmeter/typespec-typescript. DO NOT EDIT.` header — treat files without that header as hand-written. `package.json` is **stable, hand-maintained, and committed** (the emitter's `writeOutput` only writes the paths it lists, so the manifest survives regeneration). The test suite is vitest + `@fetch-mock/vitest`; keep hand-written tests and helpers in `tests/` (never `src/`), and put test-runner dependencies/scripts in the `api/spec/package.json` workspace root rather than the client package manifest. Static publish metadata (`name`, `license`, `homepage`, `repository`) lives directly in the client `package.json`; only the per-release `version` is injected at publish time. Operations marked `x-internal` or `x-private` in the TypeSpec source are emitted but quarantined under `client.internal..` (`src/sdk/internal.ts`, an `Internal` aggregate with `Internal` facade classes; internal ops share their group's `funcs/` and `models/operations/` modules, a group whose ops are all internal gets no public facade, and the `Internal` class is deliberately not re-exported from the package root — the name belongs to the Internal Server Error model). Removing an operation from the spec leaves stale generated files behind — the emitter never deletes outputs, so `git rm` them (and `rm` the gitignored `*.assert.ts` companions, which `git rm` cannot clean up on collaborators' checkouts). The emitted `api/spec/packages/aip-client-javascript/src/sdk/sdk.ts` exposes aggregated sub-client getters on the `OpenMeter` class (e.g. `events`, `meters`, `customers`, `entitlements`, `subscriptions`, `billing`, `features`, `plans`, `addons`, `planAddons`, `tax`, `defaults`). Access operations through those getters, e.g. `sdk.meters.list()`, `sdk.customers.create(...)`, `sdk.plans.create(...)`. These grouped-client methods throw `HTTPError` on failure; the same operations are also available as tree-shakeable standalone functions under `src/funcs/` that return a `Result` instead of throwing. diff --git a/api/spec/AGENTS.md b/api/spec/AGENTS.md index b787896be6..a1e1aa271d 100644 --- a/api/spec/AGENTS.md +++ b/api/spec/AGENTS.md @@ -312,8 +312,8 @@ Structural rules the interface emitter follows: - **`extends`** the base interface when the model has a `baseModel`, so inherited fields/docs propagate (`BadRequest extends BaseError`). - **Open records** (`...Record<…>`) get an index signature (`[key: string]: V`). -- **Named union aliases.** Every named TypeSpec `union` that is reachable from a - non-`x-private` operation on an included service gets its own +- **Named union aliases.** Every named TypeSpec `union` that is reachable from + an operation on an included service gets its own `export type = | | …` in `types.ts` (`interface-types.ts`, `unionVariantsType` in `ts-types.ts`) — variants resolve through the same `RefName`/`refNameInput` machinery as model properties, so a model-variant is @@ -328,18 +328,15 @@ Structural rules the interface emitter follows: anything in the actual SDK surface referencing it — `computeReachableUnions` in `emitter.tsx` walks every collected operation's request body, query parameters, and response body (success and error) and only aliases unions it - reaches. Collected means non-`x-private`: `x-internal` operations count as - reachability roots because they are emitted under the `client.internal.*` - surface. `PriceUsageBased`, `ULIDOrResourceKey`, and - `ULIDOrExternalResourceKey` are declared but never referenced by anything, so - they stay zod-only (aliasing them would export a degenerate type like - `string | string`); `Invoice`/`InvoiceLine`/`UpdateInvoiceRequest` are - reachable only through the invoice operations marked `x-private`, which are - excluded from the generated client entirely, so their unions are excluded too - even though the underlying models (e.g. `InvoiceStandard`) still get - interfaces (models are never reachability-gated — only the new union alias - pass is). `Currency` is reachable through the `x-internal` currency - operations, so its alias is emitted. This is a deliberately narrower policy + reaches. Every operation counts as a reachability root: `x-internal` and + `x-private` operations are emitted under the `client.internal.*` surface, so + the unions they reach are aliased too (`Invoice`/`InvoiceLine`/ + `UpdateInvoiceRequest` via the `x-private` invoice operations, `Currency` via + the `x-internal` currency operations). `PriceUsageBased`, + `ULIDOrResourceKey`, and `ULIDOrExternalResourceKey` are declared but never + referenced by anything, so they stay zod-only (aliasing them would export a + degenerate type like `string | string`); models are never reachability-gated + — only the union alias pass is. This is a deliberately narrower policy than models', to avoid exporting unions nothing in the shipped client can ever produce or accept. - **Response wiring picks up named unions too.** Because a named union now @@ -694,8 +691,8 @@ TypeSpec program in-memory, runs the emitter through the compiler's real emit pipeline, and returns the emitted files as `outputs: Record` (paths relative to the emitter output dir, e.g. `src/sdk/internal.ts`). Use it to pin generator behavior that should be caught before regenerating the real -client — `test/internal-surface.test.ts` (the x-private/x-internal -partitioning) is the model. Constraints: +client — `test/internal-surface.test.ts` (the x-private/x-internal routing to +the `client.internal.*` surface) is the model. Constraints: - The tester resolves the emitter by its package name through `package.json` exports, i.e. it runs the **built** `dist/` — the package `test` script runs diff --git a/api/spec/packages/aip-client-javascript/README.md b/api/spec/packages/aip-client-javascript/README.md index d7d989e765..5fdbd8150a 100644 --- a/api/spec/packages/aip-client-javascript/README.md +++ b/api/spec/packages/aip-client-javascript/README.md @@ -36,7 +36,9 @@ TypeSpec definitions and ships fully-typed request and response models. - [Defaults](#defaults) - [Internal Operations](#internal-operations) - [Internal Subscriptions](#internal-subscriptions) + - [Internal Invoices](#internal-invoices) - [Internal Currencies](#internal-currencies) + - [Internal Governance](#internal-governance) - [Runtime Validation (validate option)](#runtime-validation-validate-option) - [Zod Schemas (./zod export)](#zod-schemas-zod-export) - [Error Handling](#error-handling) @@ -420,6 +422,15 @@ they can change or be removed without notice or semver consideration. | ------------------------------------------- | ------------------------------------------------------- | ----------------------------- | | `client.internal.subscriptions.createAddon` | `POST /openmeter/subscriptions/{subscriptionId}/addons` | Add add-on to a subscription. | +### Internal Invoices + +| Method | HTTP | Description | +| --------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `client.internal.invoices.list` | `GET /openmeter/billing/invoices` | List billing invoices. Returns a page of invoices. Gathering invoices are never included. Use `filter` to narrow by status, customer, dates, or service period start. Use `sort` to control ordering. | +| `client.internal.invoices.get` | `GET /openmeter/billing/invoices/{invoiceId}` | Get a billing invoice by ID. Returns the full invoice resource including line items, status details, totals, and workflow configuration snapshot. | +| `client.internal.invoices.update` | `PUT /openmeter/billing/invoices/{invoiceId}` | Update a billing invoice. Only the mutable fields of the invoice can be edited: description, labels, supplier, customer, workflow settings, and top-level lines. Top-level lines are matched by `id`; lines without an `id` are created, and existing lines omitted from `lines` are deleted. Detailed (child) lines are always computed and cannot be edited directly. Only invoices in draft status can be updated. | +| `client.internal.invoices.delete` | `DELETE /openmeter/billing/invoices/{invoiceId}` | Delete a billing invoice. Only standard invoices in draft status can be deleted. Deleting an invoice will also delete all associated line items and workflow configuration. | + ### Internal Currencies | Method | HTTP | Description | @@ -429,6 +440,12 @@ they can change or be removed without notice or semver consideration. | `client.internal.currencies.listCostBases` | `GET /openmeter/currencies/custom/{currencyId}/cost-bases` | List cost bases for a currency. For custom currencies, there can be multiple cost bases with different `effective_from` dates. | | `client.internal.currencies.createCostBasis` | `POST /openmeter/currencies/custom/{currencyId}/cost-bases` | Create a cost basis for a currency. | +### Internal Governance + +| Method | HTTP | Description | +| ---------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `client.internal.governance.queryAccess` | `POST /openmeter/governance/query` | Query feature access for a list of customers. The endpoint resolves each provided identifier to a customer and returns the access status for the requested features, plus optional credit balance availability. _Designed to be called on a fixed refresh interval and the query response is intended to be cached._ | + ## Runtime Validation (validate option) `validate` is off by default. The SDK's normal request/response mapping diff --git a/api/spec/packages/aip-client-javascript/src/funcs/governance.ts b/api/spec/packages/aip-client-javascript/src/funcs/governance.ts new file mode 100644 index 0000000000..50a55c9e81 --- /dev/null +++ b/api/spec/packages/aip-client-javascript/src/funcs/governance.ts @@ -0,0 +1,62 @@ +// Code generated by @openmeter/typespec-typescript. DO NOT EDIT. + +import { type Client, http } from '../core.js' +import { type Result, type RequestOptions } from '../lib/types.js' +import { request } from '../lib/request.js' +import { toURLSearchParams, encodeSort } from '../lib/encodings.js' +import { toWire, fromWire, assertValid } from '../lib/wire.js' +import * as schemas from '../models/schemas.js' +import type { + QueryGovernanceAccessRequest, + QueryGovernanceAccessResponse, +} from '../models/operations/governance.js' + +/** + * Query governance access + * + * Query feature access for a list of customers. + * + * The endpoint resolves each provided identifier to a customer and returns the + * access status for the requested features, plus optional credit balance + * availability. + * + * _Designed to be called on a fixed refresh interval and the query response is + * intended to be cached._ + * + * POST /openmeter/governance/query + */ +export function queryGovernanceAccess( + client: Client, + req: QueryGovernanceAccessRequest, + options?: RequestOptions, +): Promise> { + return request(() => { + const body = toWire(req.body, schemas.queryGovernanceAccessBody) + if (client._options.validate) { + assertValid(schemas.queryGovernanceAccessBodyWire, body) + } + const query = toWire( + { + page: req.page, + }, + schemas.queryGovernanceAccessQueryParams, + ) + if (client._options.validate) { + assertValid(schemas.queryGovernanceAccessQueryParamsWire, query) + } + const searchParams = toURLSearchParams(query) + return http(client) + .post('openmeter/governance/query', { + ...options, + searchParams, + json: body, + }) + .json() + .then((data) => { + if (client._options.validate) { + assertValid(schemas.queryGovernanceAccessResponseWire, data) + } + return fromWire(data, schemas.queryGovernanceAccessResponse) + }) + }) +} diff --git a/api/spec/packages/aip-client-javascript/src/funcs/index.ts b/api/spec/packages/aip-client-javascript/src/funcs/index.ts index 11b36454ff..ac12b17def 100644 --- a/api/spec/packages/aip-client-javascript/src/funcs/index.ts +++ b/api/spec/packages/aip-client-javascript/src/funcs/index.ts @@ -7,6 +7,7 @@ export * from './entitlements.js' export * from './subscriptions.js' export * from './apps.js' export * from './billing.js' +export * from './invoices.js' export * from './tax.js' export * from './currencies.js' export * from './features.js' @@ -15,3 +16,4 @@ export * from './plans.js' export * from './addons.js' export * from './planAddons.js' export * from './defaults.js' +export * from './governance.js' diff --git a/api/spec/packages/aip-client-javascript/src/funcs/invoices.ts b/api/spec/packages/aip-client-javascript/src/funcs/invoices.ts new file mode 100644 index 0000000000..81c415d882 --- /dev/null +++ b/api/spec/packages/aip-client-javascript/src/funcs/invoices.ts @@ -0,0 +1,160 @@ +// Code generated by @openmeter/typespec-typescript. DO NOT EDIT. + +import { type Client, http } from '../core.js' +import { type Result, type RequestOptions } from '../lib/types.js' +import { request } from '../lib/request.js' +import { toURLSearchParams, encodeSort } from '../lib/encodings.js' +import { toWire, fromWire, assertValid, toSnakeCase } from '../lib/wire.js' +import * as schemas from '../models/schemas.js' +import type { + ListInvoicesRequest, + ListInvoicesResponse, + GetInvoiceRequest, + GetInvoiceResponse, + UpdateInvoiceRequest, + UpdateInvoiceResponse, + DeleteInvoiceRequest, + DeleteInvoiceResponse, +} from '../models/operations/invoices.js' + +/** + * List billing invoices + * + * List billing invoices. + * + * Returns a page of invoices. Gathering invoices are never included. Use `filter` + * to narrow by status, customer, dates, or service period start. Use `sort` to + * control ordering. + * + * GET /openmeter/billing/invoices + */ +export function listInvoices( + client: Client, + req: ListInvoicesRequest = {}, + options?: RequestOptions, +): Promise> { + return request(() => { + const query = toWire( + { + page: req.page, + sort: encodeSort(req.sort, toSnakeCase), + filter: req.filter, + }, + schemas.listInvoicesQueryParams, + ) + if (client._options.validate) { + assertValid(schemas.listInvoicesQueryParamsWire, query) + } + const searchParams = toURLSearchParams(query) + return http(client) + .get('openmeter/billing/invoices', { ...options, searchParams }) + .json() + .then((data) => { + if (client._options.validate) { + assertValid(schemas.listInvoicesResponseWire, data) + } + return fromWire(data, schemas.listInvoicesResponse) + }) + }) +} + +/** + * Get a billing invoice + * + * Get a billing invoice by ID. + * + * Returns the full invoice resource including line items, status details, totals, + * and workflow configuration snapshot. + * + * GET /openmeter/billing/invoices/{invoiceId} + */ +export function getInvoice( + client: Client, + req: GetInvoiceRequest, + options?: RequestOptions, +): Promise> { + return request(() => { + const path = `openmeter/billing/invoices/${(() => { + if (req.invoiceId === undefined) { + throw new Error('missing path parameter: invoiceId') + } + return encodeURIComponent(String(req.invoiceId)) + })()}` + return http(client) + .get(path, options) + .json() + .then((data) => { + if (client._options.validate) { + assertValid(schemas.getInvoiceResponseWire, data) + } + return fromWire(data, schemas.getInvoiceResponse) + }) + }) +} + +/** + * Update a billing invoice + * + * Update a billing invoice. + * + * Only the mutable fields of the invoice can be edited: description, labels, + * supplier, customer, workflow settings, and top-level lines. Top-level lines are + * matched by `id`; lines without an `id` are created, and existing lines omitted + * from `lines` are deleted. Detailed (child) lines are always computed and cannot + * be edited directly. Only invoices in draft status can be updated. + * + * PUT /openmeter/billing/invoices/{invoiceId} + */ +export function updateInvoice( + client: Client, + req: UpdateInvoiceRequest, + options?: RequestOptions, +): Promise> { + return request(() => { + const path = `openmeter/billing/invoices/${(() => { + if (req.invoiceId === undefined) { + throw new Error('missing path parameter: invoiceId') + } + return encodeURIComponent(String(req.invoiceId)) + })()}` + const body = toWire(req.body, schemas.updateInvoiceBody) + if (client._options.validate) { + assertValid(schemas.updateInvoiceBodyWire, body) + } + return http(client) + .put(path, { ...options, json: body }) + .json() + .then((data) => { + if (client._options.validate) { + assertValid(schemas.updateInvoiceResponseWire, data) + } + return fromWire(data, schemas.updateInvoiceResponse) + }) + }) +} + +/** + * Delete a billing invoice + * + * Delete a billing invoice. + * + * Only standard invoices in draft status can be deleted. Deleting an invoice will + * also delete all associated line items and workflow configuration. + * + * DELETE /openmeter/billing/invoices/{invoiceId} + */ +export function deleteInvoice( + client: Client, + req: DeleteInvoiceRequest, + options?: RequestOptions, +): Promise> { + return request(async () => { + const path = `openmeter/billing/invoices/${(() => { + if (req.invoiceId === undefined) { + throw new Error('missing path parameter: invoiceId') + } + return encodeURIComponent(String(req.invoiceId)) + })()}` + await http(client).delete(path, options) + }) +} diff --git a/api/spec/packages/aip-client-javascript/src/index.ts b/api/spec/packages/aip-client-javascript/src/index.ts index 3a99e578a9..1f480021d5 100644 --- a/api/spec/packages/aip-client-javascript/src/index.ts +++ b/api/spec/packages/aip-client-javascript/src/index.ts @@ -46,6 +46,7 @@ export type * from './models/operations/entitlements.js' export type * from './models/operations/subscriptions.js' export type * from './models/operations/apps.js' export type * from './models/operations/billing.js' +export type * from './models/operations/invoices.js' export type * from './models/operations/tax.js' export type * from './models/operations/currencies.js' export type * from './models/operations/features.js' @@ -54,6 +55,7 @@ export type * from './models/operations/plans.js' export type * from './models/operations/addons.js' export type * from './models/operations/planAddons.js' export type * from './models/operations/defaults.js' +export type * from './models/operations/governance.js' export type { Labels, @@ -341,6 +343,7 @@ export type { DateTimeFieldFilter, SubscriptionEditTiming, WorkflowPaymentSettings, + UpdateBillingWorkflowPaymentSettings, InvalidParameter, RateCardEntitlement, Currency, @@ -348,8 +351,12 @@ export type { WorkflowCollectionAlignment, App, Price, + UpdatePrice, CreateChargeRequest, Charge, + InvoiceLine, + UpdateInvoiceLine, + Invoice, SortQueryInput, BaseErrorInput, WorkflowPaymentSendInvoiceSettingsInput, @@ -418,5 +425,9 @@ export type { UpdateInvoiceStandardRequestInput, InvoicePagePaginatedResponseInput, WorkflowPaymentSettingsInput, + UpdateBillingWorkflowPaymentSettingsInput, RateCardEntitlementInput, + InvoiceLineInput, + InvoiceInput, + UpdateInvoiceRequestInput, } from './models/types.js' diff --git a/api/spec/packages/aip-client-javascript/src/models/operations/governance.ts b/api/spec/packages/aip-client-javascript/src/models/operations/governance.ts new file mode 100644 index 0000000000..792046fc68 --- /dev/null +++ b/api/spec/packages/aip-client-javascript/src/models/operations/governance.ts @@ -0,0 +1,21 @@ +// Code generated by @openmeter/typespec-typescript. DO NOT EDIT. + +import { z } from 'zod' +import * as schemas from '../schemas.js' +import type { AcceptDateStrings } from '../../lib/wire.js' +import type { + CursorPaginationQueryPage, + GovernanceQueryRequestInput, + GovernanceQueryResponse, +} from '../types.js' + +export interface QueryGovernanceAccessQuery { + page?: CursorPaginationQueryPage +} + +export type QueryGovernanceAccessRequest = AcceptDateStrings< + { + body: GovernanceQueryRequestInput + } & QueryGovernanceAccessQuery +> +export type QueryGovernanceAccessResponse = GovernanceQueryResponse diff --git a/api/spec/packages/aip-client-javascript/src/models/operations/invoices.ts b/api/spec/packages/aip-client-javascript/src/models/operations/invoices.ts new file mode 100644 index 0000000000..d0f9d05104 --- /dev/null +++ b/api/spec/packages/aip-client-javascript/src/models/operations/invoices.ts @@ -0,0 +1,57 @@ +// Code generated by @openmeter/typespec-typescript. DO NOT EDIT. + +import { z } from 'zod' +import * as schemas from '../schemas.js' +import type { AcceptDateStrings } from '../../lib/wire.js' +import type { + Invoice, + InvoicePagePaginatedResponse, + ListInvoicesParamsFilter, + SortQueryInput, + UpdateInvoiceRequestInput, +} from '../types.js' + +export interface ListInvoicesQuery { + /** Determines which page of the collection to retrieve. */ + page?: { size?: number; number?: number } + /** + * Sort invoices returned in the response. Supported sort attributes: + * + * - `issued_at` + * - `created_at` (default) + * - `service_period_start` + * + * The `asc` suffix is optional as the default sort order is ascending. The `desc` + * suffix is used to specify a descending order. + */ + sort?: SortQueryInput + /** + * Filter invoices returned in the response. + * + * Examples: + * + * - `filter[status][oeq]=draft,issued` + * - `filter[customer_id]=01KPDB8K...` + * - `filter[issued_at][gte]=2024-01-01T00:00:00Z` + */ + filter?: ListInvoicesParamsFilter +} + +export type ListInvoicesRequest = AcceptDateStrings +export type ListInvoicesResponse = InvoicePagePaginatedResponse + +export type GetInvoiceRequest = { + invoiceId: string +} +export type GetInvoiceResponse = Invoice + +export type UpdateInvoiceRequest = AcceptDateStrings<{ + invoiceId: string + body: UpdateInvoiceRequestInput +}> +export type UpdateInvoiceResponse = Invoice + +export type DeleteInvoiceRequest = { + invoiceId: string +} +export type DeleteInvoiceResponse = void diff --git a/api/spec/packages/aip-client-javascript/src/models/schemas.ts b/api/spec/packages/aip-client-javascript/src/models/schemas.ts index c757e123e3..8ffb0b04ce 100644 --- a/api/spec/packages/aip-client-javascript/src/models/schemas.ts +++ b/api/spec/packages/aip-client-javascript/src/models/schemas.ts @@ -6086,6 +6086,45 @@ export const deleteBillingProfilePathParams = z.object({ id: ulid, }) +export const listInvoicesQueryParams = z.object({ + page: z + .object({ + size: z.coerce + .number() + .int() + .optional() + .describe('The number of items to include per page.'), + number: z.coerce.number().int().optional().describe('The page number.'), + }) + .optional() + .describe('Determines which page of the collection to retrieve.'), + sort: sortQuery.optional(), + filter: listInvoicesParamsFilter.optional(), +}) + +export const listInvoicesResponse = z.object({ + data: z.array(invoice), + meta: paginatedMeta, +}) + +export const getInvoicePathParams = z.object({ + invoiceId: ulid, +}) + +export const getInvoiceResponse = invoice + +export const updateInvoicePathParams = z.object({ + invoiceId: ulid, +}) + +export const updateInvoiceBody = updateInvoiceRequest + +export const updateInvoiceResponse = invoice + +export const deleteInvoicePathParams = z.object({ + invoiceId: ulid, +}) + export const createTaxCodeBody = createTaxCodeRequest export const createTaxCodeResponse = taxCode @@ -6465,6 +6504,14 @@ export const updateOrganizationDefaultTaxCodesBody = export const updateOrganizationDefaultTaxCodesResponse = organizationDefaultTaxCodes +export const queryGovernanceAccessQueryParams = z.object({ + page: cursorPaginationQueryPage.optional(), +}) + +export const queryGovernanceAccessBody = governanceQueryRequest + +export const queryGovernanceAccessResponse = governanceQueryResponse + export const labelsWire = z .record(z.string(), z.string()) @@ -12577,6 +12624,45 @@ export const deleteBillingProfilePathParamsWire = z.object({ id: ulidWire, }) +export const listInvoicesQueryParamsWire = z.object({ + page: z + .strictObject({ + size: z.coerce + .number() + .int() + .optional() + .describe('The number of items to include per page.'), + number: z.coerce.number().int().optional().describe('The page number.'), + }) + .optional() + .describe('Determines which page of the collection to retrieve.'), + sort: sortQueryWire.optional(), + filter: listInvoicesParamsFilterWire.optional(), +}) + +export const listInvoicesResponseWire = z.strictObject({ + data: z.array(invoiceWire), + meta: paginatedMetaWire, +}) + +export const getInvoicePathParamsWire = z.object({ + invoiceId: ulidWire, +}) + +export const getInvoiceResponseWire = invoiceWire + +export const updateInvoicePathParamsWire = z.object({ + invoiceId: ulidWire, +}) + +export const updateInvoiceBodyWire = updateInvoiceRequestWire + +export const updateInvoiceResponseWire = invoiceWire + +export const deleteInvoicePathParamsWire = z.object({ + invoiceId: ulidWire, +}) + export const createTaxCodeBodyWire = createTaxCodeRequestWire export const createTaxCodeResponseWire = taxCodeWire @@ -12955,3 +13041,11 @@ export const updateOrganizationDefaultTaxCodesBodyWire = export const updateOrganizationDefaultTaxCodesResponseWire = organizationDefaultTaxCodesWire + +export const queryGovernanceAccessQueryParamsWire = z.object({ + page: cursorPaginationQueryPageWire.optional(), +}) + +export const queryGovernanceAccessBodyWire = governanceQueryRequestWire + +export const queryGovernanceAccessResponseWire = governanceQueryResponseWire diff --git a/api/spec/packages/aip-client-javascript/src/models/types.ts b/api/spec/packages/aip-client-javascript/src/models/types.ts index bc736de831..336d6a2d5d 100644 --- a/api/spec/packages/aip-client-javascript/src/models/types.ts +++ b/api/spec/packages/aip-client-javascript/src/models/types.ts @@ -3314,9 +3314,7 @@ export interface UpdateBillingInvoiceWorkflow { /** Invoicing settings for this invoice. */ invoicing?: UpdateBillingInvoiceWorkflowInvoicingSettings /** Payment settings for this invoice. */ - payment?: - | UpdateBillingWorkflowPaymentChargeAutomaticallySettings - | UpdateBillingWorkflowPaymentSendInvoiceSettings + payment?: UpdateBillingWorkflowPaymentSettings } /** Access status for a single feature. */ @@ -4391,12 +4389,7 @@ export interface InvoiceLineRateCard { /** Rate card configuration snapshot for a usage-based invoice line. */ export interface UpdateInvoiceLineRateCard { /** The price definition used to calculate charges for this line. */ - price: - | UpdatePriceFree - | UpdatePriceFlat - | UpdatePriceUnit - | UpdatePriceGraduated - | UpdatePriceVolume + price: UpdatePrice /** Tax configuration snapshot for this line. */ taxConfig?: UpdateRateCardTaxConfig /** The feature key associated with this line's rate card. */ @@ -5150,7 +5143,7 @@ export interface InvoiceStandard { * from the update request are deleted. Detailed (child) lines are always computed * and cannot be edited directly. */ - lines?: InvoiceStandardLine[] + lines?: InvoiceLine[] } /** InvoiceStandard update request. */ @@ -5182,12 +5175,12 @@ export interface UpdateInvoiceStandardRequest { * from the update request are deleted. Detailed (child) lines are always computed * and cannot be edited directly. */ - lines?: UpdateInvoiceStandardLine[] + lines?: UpdateInvoiceLine[] } /** Page paginated response. */ export interface InvoicePagePaginatedResponse { - data: InvoiceStandard[] + data: Invoice[] meta: PaginatedMeta } @@ -5271,6 +5264,11 @@ export type WorkflowPaymentSettings = | WorkflowPaymentChargeAutomaticallySettings | WorkflowPaymentSendInvoiceSettings +/** Payment settings for a billing workflow. */ +export type UpdateBillingWorkflowPaymentSettings = + | UpdateBillingWorkflowPaymentChargeAutomaticallySettings + | UpdateBillingWorkflowPaymentSendInvoiceSettings + /** A parameter that failed validation. */ export type InvalidParameter = | InvalidParameterStandard @@ -5319,6 +5317,14 @@ export type Price = | PriceGraduated | PriceVolume +/** Price. */ +export type UpdatePrice = + | UpdatePriceFree + | UpdatePriceFlat + | UpdatePriceUnit + | UpdatePriceGraduated + | UpdatePriceVolume + /** Customer charge. */ export type CreateChargeRequest = | CreateChargeFlatFeeRequest @@ -5327,6 +5333,36 @@ export type CreateChargeRequest = /** Customer charge. */ export type Charge = ChargeFlatFee | ChargeUsageBased +/** + * 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. + */ +export type InvoiceLine = InvoiceStandardLine + +/** + * 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. + */ +export type UpdateInvoiceLine = UpdateInvoiceStandardLine + +/** + * An invoice issued to a customer. + * + * The `type` field determines the concrete variant: + * + * - `standard`: a standard invoice for charges owed. + */ +export type Invoice = InvoiceStandard + +/** UpdateInvoiceRequest update request. */ +export type UpdateInvoiceRequest = UpdateInvoiceStandardRequest + /** * Sort query. * @@ -5745,9 +5781,7 @@ export interface UpdateBillingInvoiceWorkflowInput { /** Invoicing settings for this invoice. */ invoicing?: UpdateBillingInvoiceWorkflowInvoicingSettingsInput /** Payment settings for this invoice. */ - payment?: - | UpdateBillingWorkflowPaymentChargeAutomaticallySettings - | UpdateBillingWorkflowPaymentSendInvoiceSettingsInput + payment?: UpdateBillingWorkflowPaymentSettingsInput } /** CreditGrant create request. */ @@ -6821,7 +6855,7 @@ export interface InvoiceStandardInput { * from the update request are deleted. Detailed (child) lines are always computed * and cannot be edited directly. */ - lines?: InvoiceStandardLineInput[] + lines?: InvoiceLineInput[] } /** InvoiceStandard update request. */ @@ -6853,12 +6887,12 @@ export interface UpdateInvoiceStandardRequestInput { * from the update request are deleted. Detailed (child) lines are always computed * and cannot be edited directly. */ - lines?: UpdateInvoiceStandardLine[] + lines?: UpdateInvoiceLine[] } /** Page paginated response. */ export interface InvoicePagePaginatedResponseInput { - data: InvoiceStandardInput[] + data: InvoiceInput[] meta: PaginatedMeta } @@ -6867,6 +6901,11 @@ export type WorkflowPaymentSettingsInput = | WorkflowPaymentChargeAutomaticallySettings | WorkflowPaymentSendInvoiceSettingsInput +/** Payment settings for a billing workflow. */ +export type UpdateBillingWorkflowPaymentSettingsInput = + | UpdateBillingWorkflowPaymentChargeAutomaticallySettings + | UpdateBillingWorkflowPaymentSendInvoiceSettingsInput + /** * Entitlement template configured on a rate card. The feature is taken from the * rate card itself, so it is omitted here. @@ -6875,3 +6914,24 @@ export type RateCardEntitlementInput = | RateCardMeteredEntitlementInput | RateCardStaticEntitlement | RateCardBooleanEntitlement + +/** + * 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. + */ +export type InvoiceLineInput = InvoiceStandardLineInput + +/** + * An invoice issued to a customer. + * + * The `type` field determines the concrete variant: + * + * - `standard`: a standard invoice for charges owed. + */ +export type InvoiceInput = InvoiceStandardInput + +/** UpdateInvoiceRequest update request. */ +export type UpdateInvoiceRequestInput = UpdateInvoiceStandardRequestInput 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 1a27e79a10..d0468b85d6 100644 --- a/api/spec/packages/aip-client-javascript/src/sdk/internal.ts +++ b/api/spec/packages/aip-client-javascript/src/sdk/internal.ts @@ -4,16 +4,33 @@ import { type Client } from '../core.js' import { unwrap, type RequestOptions } from '../lib/types.js' import { paginatePages } from '../lib/paginate.js' import { createSubscriptionAddon } from '../funcs/subscriptions.js' +import { + listInvoices, + getInvoice, + updateInvoice, + deleteInvoice, +} from '../funcs/invoices.js' import { listCurrencies, createCustomCurrency, listCostBases, createCostBasis, } from '../funcs/currencies.js' +import { queryGovernanceAccess } from '../funcs/governance.js' import type { CreateSubscriptionAddonRequest, CreateSubscriptionAddonResponse, } from '../models/operations/subscriptions.js' +import type { + ListInvoicesRequest, + ListInvoicesResponse, + GetInvoiceRequest, + GetInvoiceResponse, + UpdateInvoiceRequest, + UpdateInvoiceResponse, + DeleteInvoiceRequest, + DeleteInvoiceResponse, +} from '../models/operations/invoices.js' import type { ListCurrenciesRequest, ListCurrenciesResponse, @@ -24,7 +41,11 @@ import type { CreateCostBasisRequest, CreateCostBasisResponse, } from '../models/operations/currencies.js' -import type { CostBasis, Currency } from '../models/types.js' +import type { + QueryGovernanceAccessRequest, + QueryGovernanceAccessResponse, +} from '../models/operations/governance.js' +import type { CostBasis, Currency, Invoice } from '../models/types.js' /** * Operations marked internal in the API definition. They are not part of @@ -39,10 +60,20 @@ export class Internal { return (this._subscriptions ??= new InternalSubscriptions(this._client)) } + private _invoices?: InternalInvoices + get invoices(): InternalInvoices { + return (this._invoices ??= new InternalInvoices(this._client)) + } + private _currencies?: InternalCurrencies get currencies(): InternalCurrencies { return (this._currencies ??= new InternalCurrencies(this._client)) } + + private _governance?: InternalGovernance + get governance(): InternalGovernance { + return (this._governance ??= new InternalGovernance(this._client)) + } } export class InternalSubscriptions { @@ -63,6 +94,106 @@ export class InternalSubscriptions { } } +export class InternalInvoices { + constructor(private readonly _client: Client) {} + + /** + * List billing invoices + * + * List billing invoices. + * + * Returns a page of invoices. Gathering invoices are never included. Use `filter` + * to narrow by status, customer, dates, or service period start. Use `sort` to + * control ordering. + * + * GET /openmeter/billing/invoices + */ + async list( + request?: ListInvoicesRequest, + options?: RequestOptions, + ): Promise { + return unwrap(await listInvoices(this._client, request, options)) + } + + /** + * List billing invoices + * + * List billing invoices. + * + * Returns a page of invoices. Gathering invoices are never included. Use `filter` + * to narrow by status, customer, dates, or service period start. Use `sort` to + * control ordering. + * + * Iterates every item across all pages, fetching more as the returned iterable is consumed. + * + * GET /openmeter/billing/invoices + */ + listAll( + request?: ListInvoicesRequest, + options?: RequestOptions, + ): AsyncIterable { + return paginatePages( + (req, opts) => listInvoices(this._client, req, opts), + request ?? {}, + options, + ) + } + + /** + * Get a billing invoice + * + * Get a billing invoice by ID. + * + * Returns the full invoice resource including line items, status details, totals, + * and workflow configuration snapshot. + * + * GET /openmeter/billing/invoices/{invoiceId} + */ + async get( + request: GetInvoiceRequest, + options?: RequestOptions, + ): Promise { + return unwrap(await getInvoice(this._client, request, options)) + } + + /** + * Update a billing invoice + * + * Update a billing invoice. + * + * Only the mutable fields of the invoice can be edited: description, labels, + * supplier, customer, workflow settings, and top-level lines. Top-level lines are + * matched by `id`; lines without an `id` are created, and existing lines omitted + * from `lines` are deleted. Detailed (child) lines are always computed and cannot + * be edited directly. Only invoices in draft status can be updated. + * + * PUT /openmeter/billing/invoices/{invoiceId} + */ + async update( + request: UpdateInvoiceRequest, + options?: RequestOptions, + ): Promise { + return unwrap(await updateInvoice(this._client, request, options)) + } + + /** + * Delete a billing invoice + * + * Delete a billing invoice. + * + * Only standard invoices in draft status can be deleted. Deleting an invoice will + * also delete all associated line items and workflow configuration. + * + * DELETE /openmeter/billing/invoices/{invoiceId} + */ + async delete( + request: DeleteInvoiceRequest, + options?: RequestOptions, + ): Promise { + return unwrap(await deleteInvoice(this._client, request, options)) + } +} + export class InternalCurrencies { constructor(private readonly _client: Client) {} @@ -165,3 +296,28 @@ export class InternalCurrencies { return unwrap(await createCostBasis(this._client, request, options)) } } + +export class InternalGovernance { + constructor(private readonly _client: Client) {} + + /** + * Query governance access + * + * Query feature access for a list of customers. + * + * The endpoint resolves each provided identifier to a customer and returns the + * access status for the requested features, plus optional credit balance + * availability. + * + * _Designed to be called on a fixed refresh interval and the query response is + * intended to be cached._ + * + * POST /openmeter/governance/query + */ + async queryAccess( + request: QueryGovernanceAccessRequest, + options?: RequestOptions, + ): Promise { + return unwrap(await queryGovernanceAccess(this._client, request, options)) + } +} diff --git a/api/spec/packages/typespec-typescript/src/ZodOperations.tsx b/api/spec/packages/typespec-typescript/src/ZodOperations.tsx index fccab0de00..b289b4cb5e 100644 --- a/api/spec/packages/typespec-typescript/src/ZodOperations.tsx +++ b/api/spec/packages/typespec-typescript/src/ZodOperations.tsx @@ -53,13 +53,12 @@ export interface OperationSchema { // Customer-visibility markers from the spec's shared/consts.tsp: x-private is // "private and should not be exposed to customers", x-internal is "internal and -// should not be used by customers". The published SDK is the customer surface, -// so x-private operations are omitted from it entirely (they remain in the -// OpenAPI output, which serves internal consumers too). x-internal operations -// are emitted but quarantined under the `client.internal.*` sub-client so the -// audience split stays visible at every call site. x-unstable operations stay -// in the public surface: most of the young v3 API carries that marker, and it -// flags maturity, not audience. +// should not be used by customers". Both are emitted but quarantined under the +// `client.internal.*` sub-client so the audience split stays visible at every +// call site — the SDK is also how internal consumers call the API, so dropping +// x-private operations would just push those callers back to hand-rolled HTTP. +// x-unstable operations stay in the public surface: most of the young v3 API +// carries that marker, and it flags maturity, not audience. function hasExtension( program: Program, op: Operation, @@ -80,27 +79,27 @@ function hasExtension( return false } -function isHiddenFromSdk(program: Program, op: Operation): boolean { - return hasExtension(program, op, 'x-private') -} - /** - * Whether an operation is marked x-internal in the spec. Internal operations - * are emitted like any other (funcs, envelope types, zod schemas), but their - * grouped-client methods live under `client.internal.*` instead of the public - * sub-clients (see emitter.tsx). Operations that are both x-internal and - * x-private never reach this check — x-private drops them at collection. + * Whether an operation belongs to the `client.internal.*` surface. Internal + * operations are emitted like any other (funcs, envelope types, zod schemas), + * but their grouped-client methods live under `client.internal.*` instead of + * the public sub-clients (see emitter.tsx). x-private implies the internal + * surface too: it marks a stricter audience than x-internal, so it must never + * surface publicly, but internal consumers still call it through the SDK. */ export function isInternalOperation(program: Program, op: Operation): boolean { - return hasExtension(program, op, 'x-internal') + return ( + hasExtension(program, op, 'x-internal') || + hasExtension(program, op, 'x-private') + ) } /** * Collect every HTTP operation in the program, de-duplicated by the underlying * TypeSpec `Operation` (the same operation surfaces under multiple service * namespaces — OpenMeter and MeteringAndBilling — and must not be emitted - * twice). Operations marked x-private never enter the SDK; x-internal - * operations are collected and later routed to the `client.internal.*` surface. + * twice). Operations marked x-internal or x-private are collected like any + * other and later routed to the `client.internal.*` surface. */ export function collectHttpOperations( program: Program, @@ -120,9 +119,6 @@ export function collectHttpOperations( const result: Operation[] = [] for (const service of included) { for (const httpOp of service.operations) { - if (isHiddenFromSdk(program, httpOp.operation)) { - continue - } const id = operationBaseName(program, httpOp.operation) if (seen.has(id)) { continue diff --git a/api/spec/packages/typespec-typescript/test/internal-surface.test.ts b/api/spec/packages/typespec-typescript/test/internal-surface.test.ts index 3297a83d00..dc86418f30 100644 --- a/api/spec/packages/typespec-typescript/test/internal-surface.test.ts +++ b/api/spec/packages/typespec-typescript/test/internal-surface.test.ts @@ -5,7 +5,7 @@ import { EmitterTester } from './emit.js' // same extends pattern the real spec uses (grouping resolves through the // source interface's namespace). Widgets mixes public and internal operations; // Gadgets is entirely internal; delete-widget is private and update-widget -// carries both markers (private wins). +// carries both markers (both route to the internal surface). const FIXTURE = ` import "@typespec/http"; import "@typespec/openapi"; @@ -99,28 +99,26 @@ describe('internal operation surface', () => { outputs = result.outputs }) - it('drops x-private operations entirely, even when also marked x-internal', () => { - for (const content of Object.values(outputs)) { - expect(content).not.toContain('deleteWidget') - expect(content).not.toContain('updateWidget') - } - }) - - it('keeps internal operations out of the public facade', () => { + it('keeps internal and private operations out of the public facade', () => { const widgets = file('src/sdk/widgets.ts') expect(widgets).toContain('export class Widgets {') expect(widgets).toContain('async list(') expect(widgets).toContain('listAll(') expect(widgets).not.toContain('create') + expect(widgets).not.toContain('delete') + expect(widgets).not.toContain('update') }) - it('quarantines internal operations under Internal facades', () => { + it('quarantines internal and private operations under Internal facades', () => { const internal = file('src/sdk/internal.ts') expect(internal).toContain('export class Internal {') expect(internal).toContain('get widgets(): InternalWidgets {') expect(internal).toContain('get gadgets(): InternalGadgets {') expect(internal).toContain('export class InternalWidgets {') expect(internal).toContain('async create(') + // x-private routes to the internal surface, with or without x-internal. + expect(internal).toContain('async delete(') + expect(internal).toContain('async update(') expect(internal).toContain('export class InternalGadgets {') expect(internal).toContain('async list(') // Pagination companions are emitted for internal operations too. @@ -165,6 +163,8 @@ describe('internal operation surface', () => { const readme = file('README.md') expect(readme).toContain('## Internal Operations') expect(readme).toContain('`client.internal.widgets.create`') + expect(readme).toContain('`client.internal.widgets.delete`') + expect(readme).toContain('`client.internal.widgets.update`') expect(readme).toContain('### Internal Gadgets') expect(readme).toContain('`client.internal.gadgets.list`') // The public table lists only the public operation.