diff --git a/api/spec/src/v3/common/pagination.tsp b/api/spec/src/v3/common/pagination.tsp index e49a73b64..39fb5f118 100644 --- a/api/spec/src/v3/common/pagination.tsp +++ b/api/spec/src/v3/common/pagination.tsp @@ -10,8 +10,8 @@ namespace Common; /** * Cursor page query. */ -@friendlyName("CursorPageQuery") -model CursorPageQuery { +@friendlyName("CursorPaginationQuery") +model CursorPaginationQuery { /** * Determines which page of the collection to retrieve. */ @@ -25,6 +25,21 @@ model CursorPageQuery { }; } +/** + * Page pagination query. + */ +@friendlyName("PagePaginationQuery") +model PagePaginationQuery { + /** + * Determines which page of the collection to retrieve. + */ + @query(#{ explode: true, style: "deepObject" }) + page?: { + size?: integer; + number?: integer; + }; +} + /** * Cursor pagination metadata. */ @@ -73,34 +88,6 @@ model CursorMeta { page: CursorMetaPage; } -/** - * Cursor pagination metadata with total. - */ -@friendlyName("CursorMetaWithTotal") -@useRef("../../../../common/definitions/metadatas.yaml#/components/schemas/CursorMetaWithTotal") -model CursorMetaWithTotal { - ...CursorMeta; - - /** - * Total number of items in the collection. - */ - total: integer; -} - -/** - * Cursor pagination metadata with estimated total. - */ -@friendlyName("CursorMetaWithEstimatedTotal") -@useRef("../../../../common/definitions/metadatas.yaml#/components/schemas/CursorMetaWithEstimatedTotal") -model CursorMetaWithEstimatedTotal { - ...CursorMeta; - - /** - * Estimated total number of items in the collection. - */ - estimated_total: integer; -} - /** * Page before. */ @@ -166,11 +153,11 @@ model PageNumber { */ @friendlyName("PaginatedMeta") @useRef("../../../../common/definitions/metadatas.yaml#/components/schemas/PaginatedMeta") -model PaginatedMeta { +model PageMeta { /** * Page metadata. */ - page: PageMeta; + page: PagePaginatedMeta; } /** @@ -178,7 +165,7 @@ model PaginatedMeta { */ @friendlyName("PageMeta") @useRef("../../../../common/definitions/metadatas.yaml#/components/schemas/PageMeta") -model PageMeta { +model PagePaginatedMeta { /** * Page number. */ diff --git a/api/spec/src/v3/customers/customer.tsp b/api/spec/src/v3/customers/customer.tsp new file mode 100644 index 000000000..c148f7c6e --- /dev/null +++ b/api/spec/src/v3/customers/customer.tsp @@ -0,0 +1,116 @@ +import "../shared/index.tsp"; + +namespace Customers; + +/** + * Customers can be individuals or organizations that can subscribe to plans and have access to features. + */ +@friendlyName("BillingCustomer") +model Customer { + ...Shared.ResourceWithKey; + + /** + * Mapping to attribute metered usage to the customer by the event subject. + */ + @visibility(Lifecycle.Create, Lifecycle.Read, Lifecycle.Update) + @summary("Usage Attribution") + usage_attribution?: CustomerUsageAttribution; + + /** + * The primary email address of the customer. + */ + @visibility(Lifecycle.Create, Lifecycle.Read, Lifecycle.Update) + @summary("Primary Email") + primary_email?: string; + + /** + * Currency of the customer. + * Used for billing, tax and invoicing. + */ + @visibility(Lifecycle.Create, Lifecycle.Read, Lifecycle.Update) + @summary("Currency") + currency?: Shared.CurrencyCode; + + /** + * The billing address of the customer. + * Used for tax and invoicing. + */ + @visibility(Lifecycle.Create, Lifecycle.Read, Lifecycle.Update) + @summary("Billing Address") + billing_address?: Address; +} + +/** + * Address + */ +@friendlyName("BillingAddress") +model Address { + /** + * Country code in [ISO 3166-1](https://www.iso.org/iso-3166-country-codes.html) alpha-2 format. + */ + @visibility(Lifecycle.Create, Lifecycle.Read, Lifecycle.Update) + @summary("Country") + country?: Shared.CountryCode; + + /** + * Postal code. + */ + @visibility(Lifecycle.Create, Lifecycle.Read, Lifecycle.Update) + @summary("Postal Code") + postal_code?: string; + + /** + * State or province. + */ + @visibility(Lifecycle.Create, Lifecycle.Read, Lifecycle.Update) + @summary("State") + state?: string; + + /** + * City. + */ + @visibility(Lifecycle.Create, Lifecycle.Read, Lifecycle.Update) + @summary("City") + city?: string; + + /** + * First line of the address. + */ + @visibility(Lifecycle.Create, Lifecycle.Read, Lifecycle.Update) + @summary("Line 1") + line1?: string; + + /** + * Second line of the address. + */ + @visibility(Lifecycle.Create, Lifecycle.Read, Lifecycle.Update) + @summary("Line 2") + line2?: string; + + /** + * Phone number. + */ + @visibility(Lifecycle.Create, Lifecycle.Read, Lifecycle.Update) + @summary("Phone Number") + phone_number?: string; +} + +/** + * Mapping to attribute metered usage to the customer. + * One customer can have zero or more subjects, + * but one subject can only belong to one customer. + */ +@friendlyName("BillingCustomerUsageAttribution") +model CustomerUsageAttribution { + /** + * The subjects that are attributed to the customer. + * Can be empty when no usage event subjects are associated with the customer. + */ + @visibility(Lifecycle.Create, Lifecycle.Read, Lifecycle.Update) + @summary("Subject Keys") + @minItems(0) + subject_keys: UsageAttributionKey[]; +} + +@minLength(1) +scalar UsageAttributionKey extends string; diff --git a/api/spec/src/v3/customers/index.tsp b/api/spec/src/v3/customers/index.tsp new file mode 100644 index 000000000..ebd2919dd --- /dev/null +++ b/api/spec/src/v3/customers/index.tsp @@ -0,0 +1,2 @@ +import "./customer.tsp"; +import "./operations.tsp"; diff --git a/api/spec/src/v3/customers/operations.tsp b/api/spec/src/v3/customers/operations.tsp new file mode 100644 index 000000000..2ebbd553c --- /dev/null +++ b/api/spec/src/v3/customers/operations.tsp @@ -0,0 +1,90 @@ +import "@typespec/http"; +import "@typespec/rest"; +import "@typespec/openapi"; +import "@typespec/openapi3"; +import "../common/error.tsp"; +import "../common/pagination.tsp"; +import "../common/parameters.tsp"; +import "../shared/index.tsp"; +import "./customer.tsp"; + +using TypeSpec.Http; +using TypeSpec.OpenAPI; + +namespace Customers; + +/** + * Query params for listing customers. + */ +@friendlyName("ListCustomersParams") +model ListCustomersParams { + ...Common.PagePaginationQuery, + /** + * Sort customers returned in the response. + * Supported sort attributes are: + * - `key` + * - `id` + * - `name` + * - `primary_email` + * - `created_at` (default) + * - `updated_at` + * - `deleted_at` + * + * The `asc` suffix is optional as the default sort order is ascending. + * The `desc` suffix is used to specify a descending order. + * Multiple sort attributes may be provided via a comma separated list. + */ + @query(#{name: "sort"}) + sort?: Common.SortQuery; +} + +interface CustomersOperations { + @post + @operationId("create-customer") + @summary("Create customer") + @extension(Shared.UnstableExtension, true) + @extension(Shared.InternalExtension, true) + create( + @body + customer: Shared.CreateRequest, + ): Shared.CreateResponse | Common.ErrorResponses; + + @get + @operationId("get-customer") + @summary("Get customer") + @extension(Shared.UnstableExtension, true) + @extension(Shared.InternalExtension, true) + get( + @path customerId: Shared.ULID, + ): Shared.GetResponse | Common.NotFound | Common.ErrorResponses; + + @get + @operationId("list-customers") + @summary("List customers") + @extension(Shared.UnstableExtension, true) + @extension(Shared.InternalExtension, true) + list( + ...ListCustomersParams, + ): Shared.PagePaginatedResponse | Common.ErrorResponses; + + @put + @operationId("upsert-customer") + @summary("Upsert customer") + @extension(Shared.UnstableExtension, true) + @extension(Shared.InternalExtension, true) + upsert( + @path customerId: Shared.ULID, + + @body + customer: Shared.UpsertRequest, + ): Shared.UpsertResponse | Common.NotFound | Common.ErrorResponses; + + @delete + @operationId("delete-customer") + @summary("Delete customer") + @extension(Shared.UnstableExtension, true) + @extension(Shared.InternalExtension, true) + delete( + @path customerId: Shared.ULID, + ): Shared.DeleteResponse | Common.NotFound | Common.ErrorResponses; +} diff --git a/api/spec/src/v3/entitlements/entitlement.tsp b/api/spec/src/v3/entitlements/entitlement.tsp new file mode 100644 index 000000000..36753bf76 --- /dev/null +++ b/api/spec/src/v3/entitlements/entitlement.tsp @@ -0,0 +1,37 @@ +namespace Entitlements; + +/** + * The type of the entitlement. + */ +@friendlyName("BillingEntitlementType") +enum EntitlementType { + metered, + static, + boolean, +} + +/** + * Entitlement check result. + */ +@friendlyName("BillingEntitlementCheck") +model EntitlementCheck { + /** + * The type of the entitlement. + */ + @visibility(Lifecycle.Read) + type: EntitlementType; + + /** + * Whether the customer has access to the feature. + */ + @visibility(Lifecycle.Read) + has_access: boolean; + + /** + * Only available for static entitlements. + */ + @example("{ \"rateLimit\": 100 }") + @encode("json") + @visibility(Lifecycle.Read) + config?: string; +} diff --git a/api/spec/src/v3/entitlements/index.tsp b/api/spec/src/v3/entitlements/index.tsp new file mode 100644 index 000000000..fabf9eee3 --- /dev/null +++ b/api/spec/src/v3/entitlements/index.tsp @@ -0,0 +1,2 @@ +import "./entitlement.tsp"; +import "./operations.tsp"; diff --git a/api/spec/src/v3/entitlements/operations.tsp b/api/spec/src/v3/entitlements/operations.tsp new file mode 100644 index 000000000..4340e8548 --- /dev/null +++ b/api/spec/src/v3/entitlements/operations.tsp @@ -0,0 +1,26 @@ +import "@typespec/http"; +import "@typespec/rest"; +import "@typespec/openapi"; +import "@typespec/openapi3"; +import "../common/error.tsp"; +import "../common/pagination.tsp"; +import "../common/parameters.tsp"; +import "../shared/index.tsp"; +import "./entitlement.tsp"; + +using TypeSpec.Http; +using TypeSpec.OpenAPI; + +namespace Entitlements; + +interface CustomerEntitlementsOperations { + @get + @operationId("check-customer-feature-access") + @summary("Check customer feature access") + @extension(Shared.UnstableExtension, true) + @extension(Shared.InternalExtension, true) + get( + @path customerId: Shared.ULID, + @path featureKey: Shared.ResourceKey, + ): Shared.GetResponse | Common.NotFound | Common.ErrorResponses; +} diff --git a/api/spec/src/v3/konnect.tsp b/api/spec/src/v3/konnect.tsp index 12f90f5f6..49d5e5144 100644 --- a/api/spec/src/v3/konnect.tsp +++ b/api/spec/src/v3/konnect.tsp @@ -4,6 +4,10 @@ import "@typespec/openapi"; import "@typespec/openapi3"; import "./shared/index.tsp"; import "./events/index.tsp"; +import "./customers/index.tsp"; +import "./entitlements/index.tsp"; +import "./meters/index.tsp"; +import "./subscriptions/index.tsp"; using TypeSpec.Http; using TypeSpec.OpenAPI; @@ -24,17 +28,42 @@ using TypeSpec.OpenAPI; // TODO: Uncomment this when the Singapore production region is available // @server("https://sg.api.konghq.com/v3", "Singapore Production region") @server("https://global.api.konghq.com/v3", "Global Production region") +@tagMetadata(Shared.MetersTag, #{ description: Shared.MetersDescription }) @tagMetadata(Shared.EventsTag, #{ description: Shared.EventsDescription }) @tagMetadata(Shared.CustomersTag, #{ description: Shared.CustomersDescription }) -@tagMetadata(Shared.MetersTag, #{ description: Shared.MetersDescription }) +@tagMetadata(Shared.SubscriptionsTag, #{ description: Shared.SubscriptionsDescription }) +@tagMetadata(Shared.EntitlementsTag, #{ description: Shared.EntitlementsDescription }) @useAuth(systemAccountAccessToken | personalAccessToken | konnectAccessToken) namespace MeteringAndBilling; +@route("/openmeter/meters") +@tag(Shared.MetersTag) +@friendlyName("${Shared.MeteringAndBillingTitle}: ${Shared.MetersTag}") +interface MetersEndpoints extends Meters.MetersOperations {} + @route("/openmeter/events") @tag(Shared.EventsTag) @friendlyName("${Shared.MeteringAndBillingTitle}: ${Shared.EventsTag}") interface EventsEndpoints extends Events.EventsOperations {} +@route("/openmeter/customers") +@tag(Shared.CustomersTag) +@friendlyName("${Shared.MeteringAndBillingTitle}: ${Shared.CustomersTag}") +interface CustomersEndpoints extends Customers.CustomersOperations {} + +@route("/openmeter/customers/{customerId}/subscriptions") +@tag(Shared.CustomersTag) +@tag(Shared.SubscriptionsTag) +@friendlyName("${Shared.MeteringAndBillingTitle}: ${Shared.CustomersTag}: ${Shared.SubscriptionsTag}") +interface CustomerSubscriptionsEndpoints extends Subscriptions.CustomerSubscriptionsOperations {} + +@route("/openmeter/customers/{customerId}/entitlements/{featureKey}") +@tag(Shared.CustomersTag) +@tag(Shared.EntitlementsTag) +@friendlyName("${Shared.MeteringAndBillingTitle}: ${Shared.EntitlementsTag}") +interface CustomerEntitlementsEndpoints + extends Entitlements.CustomerEntitlementsOperations {} + /** * The system account access token is meant for automations and integrations that are not directly associated with a human identity. */ diff --git a/api/spec/src/v3/meters/index.tsp b/api/spec/src/v3/meters/index.tsp new file mode 100644 index 000000000..fcf19b921 --- /dev/null +++ b/api/spec/src/v3/meters/index.tsp @@ -0,0 +1,2 @@ +import "./meter.tsp"; +import "./operations.tsp"; diff --git a/api/spec/src/v3/meters/meter.tsp b/api/spec/src/v3/meters/meter.tsp new file mode 100644 index 000000000..58a93cf85 --- /dev/null +++ b/api/spec/src/v3/meters/meter.tsp @@ -0,0 +1,82 @@ +import "../shared/index.tsp"; + +namespace Meters; + +/** + * A meter is a configuration that defines how to match and aggregate events. + */ +@friendlyName("Meter") +@example(#{ + id: "01G65Z755AFWAKHE12NY0CQ9FH", + key: "tokens_total", + name: "Tokens Total", + description: "AI Token Usage", + aggregation: "sum", + event_type_filter: "prompt", + value_property: "$.tokens", + dimensions: #{ `model`: "$.model", type: "$.type" }, + created_at: Shared.DateTime.fromISO("2024-01-01T01:01:01.001Z"), + updated_at: Shared.DateTime.fromISO("2024-01-01T01:01:01.001Z"), +}) +model Meter { + ...Shared.ResourceWithKey; + + /** + * The aggregation type to use for the meter. + */ + @visibility(Lifecycle.Read, Lifecycle.Create) + aggregation: MeterAggregation; + + /** + * The event type to include in the aggregation. + */ + @visibility(Lifecycle.Read, Lifecycle.Create) + @minLength(1) + @example("prompt") + event_type_filter: string; + + /** + * The date since the meter should include events. + * Useful to skip old events. + * If not specified, all historical events are included. + */ + @visibility(Lifecycle.Read, Lifecycle.Create) + event_from?: Shared.DateTime; + + /** + * JSONPath expression to extract the value from the ingested event's data property. + * + * The ingested value for sum, avg, min, and max aggregations is a number or a string that can be parsed to a number. + * + * For unique_count aggregation, the ingested value must be a string. For count aggregation the value_property is ignored. + */ + @visibility(Lifecycle.Read, Lifecycle.Create) + @minLength(1) + @example("$.tokens") + value_property?: string; + + /** + * Named JSONPath expressions to extract the group by values from the event data. + * + * Keys must be unique and consist only alphanumeric and underscore characters. + * + */ + // TODO: add key format enforcement + @visibility(Lifecycle.Read, Lifecycle.Create, Lifecycle.Update) + @example(#{ type: "$.type" }) + dimensions?: Record; +} + +/** + * The aggregation type to use for the meter. + */ +@friendlyName("MeterAggregation") +union MeterAggregation { + sum: "sum", + count: "count", + unique_count: "unique_count", + avg: "avg", + min: "min", + max: "max", + latest: "latest", +} diff --git a/api/spec/src/v3/meters/operations.tsp b/api/spec/src/v3/meters/operations.tsp new file mode 100644 index 000000000..ff3c39b0d --- /dev/null +++ b/api/spec/src/v3/meters/operations.tsp @@ -0,0 +1,46 @@ +import "@typespec/http"; +import "@typespec/rest"; +import "@typespec/openapi"; +import "@typespec/openapi3"; +import "../common/error.tsp"; +import "../common/pagination.tsp"; +import "../common/parameters.tsp"; +import "../shared/index.tsp"; +import "./meter.tsp"; + +using TypeSpec.Http; +using TypeSpec.OpenAPI; + +namespace Meters; + +interface MetersOperations { + /** + * Create a meter. + */ + @post + @operationId("create-meter") + @summary("Create meter") + create( + @body meter: Shared.CreateRequest, + ): Shared.CreateResponse | Common.ErrorResponses; + + /** + * Get a meter by ID. + */ + @get + @operationId("get-meter") + @summary("Get meter") + get( + @path meterId: Shared.ULID, + ): Shared.GetResponse | Common.NotFound | Common.ErrorResponses; + + /** + * List meters. + */ + @get + @operationId("list-meters") + @summary("List meters") + list( + ...Common.PagePaginationQuery, + ): Shared.PagePaginatedResponse | Common.ErrorResponses; +} diff --git a/api/spec/src/v3/openmeter.tsp b/api/spec/src/v3/openmeter.tsp index cc80ed194..6316da95e 100644 --- a/api/spec/src/v3/openmeter.tsp +++ b/api/spec/src/v3/openmeter.tsp @@ -4,6 +4,9 @@ import "@typespec/openapi"; import "@typespec/openapi3"; import "./events/index.tsp"; import "./shared/index.tsp"; +import "./customers/index.tsp"; +import "./entitlements/index.tsp"; +import "./subscriptions/index.tsp"; using TypeSpec.Http; using TypeSpec.OpenAPI; @@ -21,14 +24,38 @@ using TypeSpec.OpenAPI; }, termsOfService: "https://openmeter.cloud/terms-of-service", }) -@server("https://127.0.0.1/api/v3", "Local") @server("https://openmeter.cloud/api/v3", "Cloud") +@tagMetadata(Shared.MetersTag, #{ description: Shared.MetersDescription }) @tagMetadata(Shared.EventsTag, #{ description: Shared.EventsDescription }) @tagMetadata(Shared.CustomersTag, #{ description: Shared.CustomersDescription }) -@tagMetadata(Shared.MetersTag, #{ description: Shared.MetersDescription }) +@tagMetadata(Shared.SubscriptionsTag, #{ description: Shared.SubscriptionsDescription }) +@tagMetadata(Shared.EntitlementsTag, #{ description: Shared.EntitlementsDescription }) namespace OpenMeter; +@route("/openmeter/meters") +@tag(Shared.MetersTag) +@friendlyName("${Shared.OpenMeterTitle}: ${Shared.MetersTag}") +interface MetersEndpoints extends Meters.MetersOperations {} + @route("/openmeter/events") @tag(Shared.EventsTag) @friendlyName("${Shared.OpenMeterTitle}: ${Shared.EventsTag}") interface EventsEndpoints extends Events.EventsOperations {} + +@route("/openmeter/customers") +@tag(Shared.CustomersTag) +@friendlyName("${Shared.OpenMeterTitle}: ${Shared.CustomersTag}") +interface CustomersEndpoints extends Customers.CustomersOperations {} + +@route("/openmeter/customers/{customerId}/subscriptions") +@tag(Shared.CustomersTag) +@tag(Shared.SubscriptionsTag) +@friendlyName("${Shared.OpenMeterTitle}: ${Shared.CustomersTag}: ${Shared.SubscriptionsTag}") +interface CustomerSubscriptionsEndpoints extends Subscriptions.CustomerSubscriptionsOperations {} + +@route("/openmeter/customers/{customerId}/entitlements/{featureKey}") +@tag(Shared.CustomersTag) +@tag(Shared.EntitlementsTag) +@friendlyName("${Shared.OpenMeterTitle}: ${Shared.EntitlementsTag}") +interface CustomerEntitlementsEndpoints + extends Entitlements.CustomerEntitlementsOperations {} diff --git a/api/spec/src/v3/shared/responses.tsp b/api/spec/src/v3/shared/responses.tsp index 015c794a9..87478fa53 100644 --- a/api/spec/src/v3/shared/responses.tsp +++ b/api/spec/src/v3/shared/responses.tsp @@ -18,6 +18,13 @@ model CreateResponse { @Http.body body: T; } +@doc("{name} upsert response.", T) +@friendlyName("Upsert{name}Response", T) +model UpsertResponse { + @Http.statusCode _: 200; + @Http.body body: T; +} + @doc("{name} updated response.", T) @friendlyName("Update{name}Response", T) model UpdateResponse { @@ -54,5 +61,5 @@ model PagePaginatedResponse { @pageItems data: T[]; - meta: Common.PaginatedMeta; + meta: Common.PageMeta; } diff --git a/api/spec/src/v3/subscriptions/index.tsp b/api/spec/src/v3/subscriptions/index.tsp new file mode 100644 index 000000000..8628b1580 --- /dev/null +++ b/api/spec/src/v3/subscriptions/index.tsp @@ -0,0 +1,2 @@ +import "./subscription.tsp"; +import "./operations.tsp"; diff --git a/api/spec/src/v3/subscriptions/operations.tsp b/api/spec/src/v3/subscriptions/operations.tsp new file mode 100644 index 000000000..c7141d6fd --- /dev/null +++ b/api/spec/src/v3/subscriptions/operations.tsp @@ -0,0 +1,36 @@ +import "@typespec/http"; +import "@typespec/rest"; +import "@typespec/openapi"; +import "@typespec/openapi3"; +import "../common/error.tsp"; +import "../common/pagination.tsp"; +import "../common/parameters.tsp"; +import "../shared/index.tsp"; +import "./subscription.tsp"; + +using TypeSpec.Http; +using TypeSpec.OpenAPI; + +namespace Subscriptions; + +interface CustomerSubscriptionsOperations { + @post + @operationId("create-customer-subscription") + @summary("Create customer subscription") + @extension(Shared.UnstableExtension, true) + @extension(Shared.InternalExtension, true) + create( + @path customerId: Shared.ULID, + @body request: Shared.CreateRequest, + ): Shared.CreateResponse | Common.NotFound | Common.Conflict | Common.ErrorResponses; + + @get + @operationId("list-customer-subscriptions") + @summary("List customer subscriptions") + @extension(Shared.UnstableExtension, true) + @extension(Shared.InternalExtension, true) + list( + @path customerId: Shared.ULID, + ...Common.PagePaginationQuery, + ): Shared.PagePaginatedResponse | Common.NotFound | Common.ErrorResponses; +} diff --git a/api/spec/src/v3/subscriptions/subscription.tsp b/api/spec/src/v3/subscriptions/subscription.tsp new file mode 100644 index 000000000..46d0e6a2d --- /dev/null +++ b/api/spec/src/v3/subscriptions/subscription.tsp @@ -0,0 +1,33 @@ +import "@typespec/openapi"; +import "@typespec/openapi3"; +import "../shared/index.tsp"; + +using TypeSpec.OpenAPI; + +namespace Subscriptions; + +/** + * Reference to a plan by its id or key and version. + */ +@oneOf +@friendlyName("PlanReference") +union PlanReference { + id: Shared.ULID, + keyVersion: { + key: Shared.ResourceKey, + version?: integer, + }, +} + +/** + * A subscription is a customer's subscription to a plan. + */ +@friendlyName("BillingSubscription") +model Subscription { + ...Shared.Resource; + + /** + * Plan reference. + */ + plan: PlanReference; +} diff --git a/api/v3/api.gen.go b/api/v3/api.gen.go index cdeb03de2..acfefcd20 100644 --- a/api/v3/api.gen.go +++ b/api/v3/api.gen.go @@ -21,6 +21,13 @@ import ( "github.com/oapi-codegen/runtime" ) +// Defines values for BillingEntitlementType. +const ( + BillingEntitlementTypeBoolean BillingEntitlementType = "boolean" + BillingEntitlementTypeMetered BillingEntitlementType = "metered" + BillingEntitlementTypeStatic BillingEntitlementType = "static" +) + // Defines values for InvalidParameterChoiceItemRule. const ( InvalidParameterChoiceItemRuleEnum InvalidParameterChoiceItemRule = "enum" @@ -75,6 +82,17 @@ const ( InvalidRulesUnknownProperty InvalidRules = "unknown_property" ) +// Defines values for MeterAggregation. +const ( + MeterAggregationAvg MeterAggregation = "avg" + MeterAggregationCount MeterAggregation = "count" + MeterAggregationLatest MeterAggregation = "latest" + MeterAggregationMax MeterAggregation = "max" + MeterAggregationMin MeterAggregation = "min" + MeterAggregationSum MeterAggregation = "sum" + MeterAggregationUniqueCount MeterAggregation = "unique_count" +) + // Defines values for MeteringEventDatacontenttype. const ( MeteringEventDatacontenttypeApplicationjson MeteringEventDatacontenttype = "application/json" @@ -135,6 +153,258 @@ type BaseError struct { Type *string `json:"type,omitempty"` } +// BillingAddress Address +type BillingAddress struct { + // City City. + City *string `json:"city,omitempty"` + + // Country Country code in [ISO 3166-1](https://www.iso.org/iso-3166-country-codes.html) alpha-2 format. + Country *CountryCode `json:"country,omitempty"` + + // Line1 First line of the address. + Line1 *string `json:"line1,omitempty"` + + // Line2 Second line of the address. + Line2 *string `json:"line2,omitempty"` + + // PhoneNumber Phone number. + PhoneNumber *string `json:"phone_number,omitempty"` + + // PostalCode Postal code. + PostalCode *string `json:"postal_code,omitempty"` + + // State State or province. + State *string `json:"state,omitempty"` +} + +// BillingCustomer Customers can be individuals or organizations that can subscribe to plans and have access to features. +type BillingCustomer struct { + // BillingAddress The billing address of the customer. + // Used for tax and invoicing. + BillingAddress *BillingAddress `json:"billing_address,omitempty"` + + // CreatedAt An ISO-8601 timestamp representation of entity creation date. + CreatedAt *DateTime `json:"created_at,omitempty"` + + // Currency Currency of the customer. + // Used for billing, tax and invoicing. + Currency *CurrencyCode `json:"currency,omitempty"` + + // DeletedAt An ISO-8601 timestamp representation of entity deletion date. + DeletedAt *DateTime `json:"deleted_at,omitempty"` + + // Description Optional description of the resource. + // + // Maximum 1024 characters. + Description *string `json:"description,omitempty"` + Id ULID `json:"id"` + + // Key A key is a unique string that is used to identify a resource. + Key ResourceKey `json:"key"` + + // Labels Labels store metadata of an entity that can be used for filtering an entity list or for searching across entity types. + // + // Keys must be of length 1-63 characters, and cannot start with "kong", "konnect", "mesh", "kic", or "_". + Labels *Labels `json:"labels,omitempty"` + + // Name Display name of the resource. + // + // Between 1 and 256 characters. + Name string `json:"name"` + + // PrimaryEmail The primary email address of the customer. + PrimaryEmail *string `json:"primary_email,omitempty"` + + // UpdatedAt An ISO-8601 timestamp representation of entity last update date. + UpdatedAt *DateTime `json:"updated_at,omitempty"` + + // UsageAttribution Mapping to attribute metered usage to the customer by the event subject. + UsageAttribution *BillingCustomerUsageAttribution `json:"usage_attribution,omitempty"` +} + +// BillingCustomerUsageAttribution Mapping to attribute metered usage to the customer. +// One customer can have zero or more subjects, +// but one subject can only belong to one customer. +type BillingCustomerUsageAttribution struct { + // SubjectKeys The subjects that are attributed to the customer. + // Can be empty when no usage event subjects are associated with the customer. + SubjectKeys []CustomersUsageAttributionKey `json:"subject_keys"` +} + +// BillingEntitlementCheck Entitlement check result. +type BillingEntitlementCheck struct { + // Config Only available for static entitlements. + Config json.RawMessage `json:"config,omitempty"` + + // HasAccess Whether the customer has access to the feature. + HasAccess bool `json:"has_access"` + + // Type The type of the entitlement. + Type BillingEntitlementType `json:"type"` +} + +// BillingEntitlementType The type of the entitlement. +type BillingEntitlementType string + +// BillingSubscription A subscription is a customer's subscription to a plan. +type BillingSubscription struct { + // CreatedAt An ISO-8601 timestamp representation of entity creation date. + CreatedAt *DateTime `json:"created_at,omitempty"` + + // DeletedAt An ISO-8601 timestamp representation of entity deletion date. + DeletedAt *DateTime `json:"deleted_at,omitempty"` + + // Description Optional description of the resource. + // + // Maximum 1024 characters. + Description *string `json:"description,omitempty"` + Id ULID `json:"id"` + + // Labels Labels store metadata of an entity that can be used for filtering an entity list or for searching across entity types. + // + // Keys must be of length 1-63 characters, and cannot start with "kong", "konnect", "mesh", "kic", or "_". + Labels *Labels `json:"labels,omitempty"` + + // Name Display name of the resource. + // + // Between 1 and 256 characters. + Name string `json:"name"` + + // Plan Plan reference. + Plan PlanReference `json:"plan"` + + // UpdatedAt An ISO-8601 timestamp representation of entity last update date. + UpdatedAt *DateTime `json:"updated_at,omitempty"` +} + +// ConflictError defines model for ConflictError. +type ConflictError struct { + Detail interface{} `json:"detail"` + Instance interface{} `json:"instance"` + Status interface{} `json:"status"` + Title interface{} `json:"title"` + Type interface{} `json:"type,omitempty"` +} + +// CountryCode [ISO 3166-1](https://www.iso.org/iso-3166-country-codes.html) alpha-2 country code. +// Custom two-letter country codes are also supported for convenience. +type CountryCode = string + +// CreateCustomerRequest Customer create request. +type CreateCustomerRequest struct { + // BillingAddress The billing address of the customer. + // Used for tax and invoicing. + BillingAddress *BillingAddress `json:"billing_address,omitempty"` + + // Currency Currency of the customer. + // Used for billing, tax and invoicing. + Currency *CurrencyCode `json:"currency,omitempty"` + + // Description Optional description of the resource. + // + // Maximum 1024 characters. + Description *string `json:"description,omitempty"` + + // Key A key is a unique string that is used to identify a resource. + Key ResourceKey `json:"key"` + + // Labels Labels store metadata of an entity that can be used for filtering an entity list or for searching across entity types. + // + // Keys must be of length 1-63 characters, and cannot start with "kong", "konnect", "mesh", "kic", or "_". + Labels *Labels `json:"labels,omitempty"` + + // Name Display name of the resource. + // + // Between 1 and 256 characters. + Name string `json:"name"` + + // PrimaryEmail The primary email address of the customer. + PrimaryEmail *string `json:"primary_email,omitempty"` + + // UsageAttribution Mapping to attribute metered usage to the customer by the event subject. + UsageAttribution *BillingCustomerUsageAttribution `json:"usage_attribution,omitempty"` +} + +// CreateMeterRequest Meter create request. +type CreateMeterRequest struct { + // Aggregation The aggregation type to use for the meter. + Aggregation MeterAggregation `json:"aggregation"` + + // Description Optional description of the resource. + // + // Maximum 1024 characters. + Description *string `json:"description,omitempty"` + + // Dimensions Named JSONPath expressions to extract the group by values from the event data. + // + // Keys must be unique and consist only alphanumeric and underscore characters. + Dimensions *map[string]string `json:"dimensions,omitempty"` + + // EventFrom The date since the meter should include events. + // Useful to skip old events. + // If not specified, all historical events are included. + EventFrom *DateTime `json:"event_from,omitempty"` + + // EventTypeFilter The event type to include in the aggregation. + EventTypeFilter string `json:"event_type_filter"` + + // Key A key is a unique string that is used to identify a resource. + Key ResourceKey `json:"key"` + + // Labels Labels store metadata of an entity that can be used for filtering an entity list or for searching across entity types. + // + // Keys must be of length 1-63 characters, and cannot start with "kong", "konnect", "mesh", "kic", or "_". + Labels *Labels `json:"labels,omitempty"` + + // Name Display name of the resource. + // + // Between 1 and 256 characters. + Name string `json:"name"` + + // ValueProperty JSONPath expression to extract the value from the ingested event's data property. + // + // The ingested value for sum, avg, min, and max aggregations is a number or a string that can be parsed to a number. + // + // For unique_count aggregation, the ingested value must be a string. For count aggregation the value_property is ignored. + ValueProperty *string `json:"value_property,omitempty"` +} + +// CreateSubscriptionRequest Subscription create request. +type CreateSubscriptionRequest struct { + // Description Optional description of the resource. + // + // Maximum 1024 characters. + Description *string `json:"description,omitempty"` + + // Labels Labels store metadata of an entity that can be used for filtering an entity list or for searching across entity types. + // + // Keys must be of length 1-63 characters, and cannot start with "kong", "konnect", "mesh", "kic", or "_". + Labels *Labels `json:"labels,omitempty"` + + // Name Display name of the resource. + // + // Between 1 and 256 characters. + Name string `json:"name"` + + // Plan Plan reference. + Plan PlanReference `json:"plan"` +} + +// CurrencyCode Three-letter [ISO4217](https://www.iso.org/iso-4217-currency-codes.html) currency code. +// Custom three-letter currency codes are also supported for convenience. +type CurrencyCode = string + +// CustomerPagePaginatedResponse Page paginated response. +type CustomerPagePaginatedResponse struct { + Data []BillingCustomer `json:"data"` + + // Meta returns the pagination information + Meta PaginatedMeta `json:"meta"` +} + +// CustomersUsageAttributionKey defines model for Customers.UsageAttributionKey. +type CustomersUsageAttributionKey = string + // DateTime [RFC3339](https://tools.ietf.org/html/rfc3339) formatted date-time string in UTC. type DateTime = time.Time @@ -224,6 +494,76 @@ type InvalidParameters_Item struct { // InvalidRules invalid parameters rules type InvalidRules string +// Labels Labels store metadata of an entity that can be used for filtering an entity list or for searching across entity types. +// +// Keys must be of length 1-63 characters, and cannot start with "kong", "konnect", "mesh", "kic", or "_". +type Labels map[string]string + +// Meter A meter is a configuration that defines how to match and aggregate events. +type Meter struct { + // Aggregation The aggregation type to use for the meter. + Aggregation MeterAggregation `json:"aggregation"` + + // CreatedAt An ISO-8601 timestamp representation of entity creation date. + CreatedAt *DateTime `json:"created_at,omitempty"` + + // DeletedAt An ISO-8601 timestamp representation of entity deletion date. + DeletedAt *DateTime `json:"deleted_at,omitempty"` + + // Description Optional description of the resource. + // + // Maximum 1024 characters. + Description *string `json:"description,omitempty"` + + // Dimensions Named JSONPath expressions to extract the group by values from the event data. + // + // Keys must be unique and consist only alphanumeric and underscore characters. + Dimensions *map[string]string `json:"dimensions,omitempty"` + + // EventFrom The date since the meter should include events. + // Useful to skip old events. + // If not specified, all historical events are included. + EventFrom *DateTime `json:"event_from,omitempty"` + + // EventTypeFilter The event type to include in the aggregation. + EventTypeFilter string `json:"event_type_filter"` + Id ULID `json:"id"` + + // Key A key is a unique string that is used to identify a resource. + Key ResourceKey `json:"key"` + + // Labels Labels store metadata of an entity that can be used for filtering an entity list or for searching across entity types. + // + // Keys must be of length 1-63 characters, and cannot start with "kong", "konnect", "mesh", "kic", or "_". + Labels *Labels `json:"labels,omitempty"` + + // Name Display name of the resource. + // + // Between 1 and 256 characters. + Name string `json:"name"` + + // UpdatedAt An ISO-8601 timestamp representation of entity last update date. + UpdatedAt *DateTime `json:"updated_at,omitempty"` + + // ValueProperty JSONPath expression to extract the value from the ingested event's data property. + // + // The ingested value for sum, avg, min, and max aggregations is a number or a string that can be parsed to a number. + // + // For unique_count aggregation, the ingested value must be a string. For count aggregation the value_property is ignored. + ValueProperty *string `json:"value_property,omitempty"` +} + +// MeterAggregation The aggregation type to use for the meter. +type MeterAggregation string + +// MeterPagePaginatedResponse Page paginated response. +type MeterPagePaginatedResponse struct { + Data []Meter `json:"data"` + + // Meta returns the pagination information + Meta PaginatedMeta `json:"meta"` +} + // MeteringEvent Metering event following the CloudEvents specification. type MeteringEvent struct { // Data The event payload. @@ -258,6 +598,60 @@ type MeteringEvent struct { // MeteringEventDatacontenttype Content type of the CloudEvents data value. Only the value "application/json" is allowed over HTTP. type MeteringEventDatacontenttype string +// NotFoundError defines model for NotFoundError. +type NotFoundError struct { + Detail interface{} `json:"detail"` + Instance interface{} `json:"instance"` + Status interface{} `json:"status"` + Title interface{} `json:"title"` + Type interface{} `json:"type,omitempty"` +} + +// PageMeta Contains pagination query parameters and the total number of objects returned. +type PageMeta struct { + Number float32 `json:"number"` + Size float32 `json:"size"` + Total float32 `json:"total"` +} + +// PaginatedMeta returns the pagination information +type PaginatedMeta struct { + // Page Contains pagination query parameters and the total number of objects returned. + Page PageMeta `json:"page"` +} + +// PlanReference Reference to a plan by its id or key and version. +type PlanReference struct { + union json.RawMessage +} + +// PlanReference1 defines model for . +type PlanReference1 struct { + // Key A key is a unique string that is used to identify a resource. + Key ResourceKey `json:"key"` + Version *int `json:"version,omitempty"` +} + +// ResourceKey A key is a unique string that is used to identify a resource. +type ResourceKey = string + +// SortQuery The `asc` suffix is optional as the default sort order is ascending. +// The `desc` suffix is used to specify a descending order. +// Multiple sort attributes may be provided via a comma separated list. +// JSONPath notation may be used to specify a sub-attribute (eg: 'foo.bar desc'). +type SortQuery = string + +// SubscriptionPagePaginatedResponse Page paginated response. +type SubscriptionPagePaginatedResponse struct { + Data []BillingSubscription `json:"data"` + + // Meta returns the pagination information + Meta PaginatedMeta `json:"meta"` +} + +// ULID ULID (Universally Unique Lexicographically Sortable Identifier). +type ULID = string + // UnauthorizedError defines model for UnauthorizedError. type UnauthorizedError struct { Detail interface{} `json:"detail"` @@ -267,9 +661,56 @@ type UnauthorizedError struct { Type interface{} `json:"type,omitempty"` } +// UpsertCustomerRequest Customer upsert request. +type UpsertCustomerRequest struct { + // BillingAddress The billing address of the customer. + // Used for tax and invoicing. + BillingAddress *BillingAddress `json:"billing_address,omitempty"` + + // Currency Currency of the customer. + // Used for billing, tax and invoicing. + Currency *CurrencyCode `json:"currency,omitempty"` + + // Description Optional description of the resource. + // + // Maximum 1024 characters. + Description *string `json:"description,omitempty"` + + // Labels Labels store metadata of an entity that can be used for filtering an entity list or for searching across entity types. + // + // Keys must be of length 1-63 characters, and cannot start with "kong", "konnect", "mesh", "kic", or "_". + Labels *Labels `json:"labels,omitempty"` + + // Name Display name of the resource. + // + // Between 1 and 256 characters. + Name string `json:"name"` + + // PrimaryEmail The primary email address of the customer. + PrimaryEmail *string `json:"primary_email,omitempty"` + + // UsageAttribution Mapping to attribute metered usage to the customer by the event subject. + UsageAttribution *BillingCustomerUsageAttribution `json:"usage_attribution,omitempty"` +} + +// ListCustomersParamsSort The `asc` suffix is optional as the default sort order is ascending. +// The `desc` suffix is used to specify a descending order. +// Multiple sort attributes may be provided via a comma separated list. +// JSONPath notation may be used to specify a sub-attribute (eg: 'foo.bar desc'). +type ListCustomersParamsSort = SortQuery + +// PagePaginationQuery defines model for PagePaginationQuery. +type PagePaginationQuery struct { + Number *int `json:"number,omitempty"` + Size *int `json:"size,omitempty"` +} + // BadRequest defines model for BadRequest. type BadRequest = BadRequestError +// Conflict defines model for Conflict. +type Conflict = ConflictError + // Forbidden defines model for Forbidden. type Forbidden = ForbiddenError @@ -279,9 +720,39 @@ type Internal = BaseError // NotAvailable standard error type NotAvailable = BaseError +// NotFound defines model for NotFound. +type NotFound = NotFoundError + // Unauthorized defines model for Unauthorized. type Unauthorized = UnauthorizedError +// ListCustomersParams defines parameters for ListCustomers. +type ListCustomersParams struct { + // Page Determines which page of the collection to retrieve. + Page *PagePaginationQuery `json:"page,omitempty"` + + // Sort Sort customers returned in the response. + // Supported sort attributes are: + // - `key` + // - `id` + // - `name` + // - `primary_email` + // - `created_at` (default) + // - `updated_at` + // - `deleted_at` + // + // The `asc` suffix is optional as the default sort order is ascending. + // The `desc` suffix is used to specify a descending order. + // Multiple sort attributes may be provided via a comma separated list. + Sort *ListCustomersParamsSort `form:"sort,omitempty" json:"sort,omitempty"` +} + +// ListCustomerSubscriptionsParams defines parameters for ListCustomerSubscriptions. +type ListCustomerSubscriptionsParams struct { + // Page Determines which page of the collection to retrieve. + Page *PagePaginationQuery `json:"page,omitempty"` +} + // IngestMeteringEventsApplicationCloudeventsBatchPlusJSONBody defines parameters for IngestMeteringEvents. type IngestMeteringEventsApplicationCloudeventsBatchPlusJSONBody = []MeteringEvent @@ -293,6 +764,21 @@ type IngestMeteringEventsJSONBody struct { // IngestMeteringEventsJSONBody1 defines parameters for IngestMeteringEvents. type IngestMeteringEventsJSONBody1 = []MeteringEvent +// ListMetersParams defines parameters for ListMeters. +type ListMetersParams struct { + // Page Determines which page of the collection to retrieve. + Page *PagePaginationQuery `json:"page,omitempty"` +} + +// CreateCustomerJSONRequestBody defines body for CreateCustomer for application/json ContentType. +type CreateCustomerJSONRequestBody = CreateCustomerRequest + +// UpsertCustomerJSONRequestBody defines body for UpsertCustomer for application/json ContentType. +type UpsertCustomerJSONRequestBody = UpsertCustomerRequest + +// CreateCustomerSubscriptionJSONRequestBody defines body for CreateCustomerSubscription for application/json ContentType. +type CreateCustomerSubscriptionJSONRequestBody = CreateSubscriptionRequest + // IngestMeteringEventsApplicationCloudeventsPlusJSONRequestBody defines body for IngestMeteringEvents for application/cloudevents+json ContentType. type IngestMeteringEventsApplicationCloudeventsPlusJSONRequestBody = MeteringEvent @@ -302,6 +788,9 @@ type IngestMeteringEventsApplicationCloudeventsBatchPlusJSONRequestBody = Ingest // IngestMeteringEventsJSONRequestBody defines body for IngestMeteringEvents for application/json ContentType. type IngestMeteringEventsJSONRequestBody IngestMeteringEventsJSONBody +// CreateMeterJSONRequestBody defines body for CreateMeter for application/json ContentType. +type CreateMeterJSONRequestBody = CreateMeterRequest + // AsInvalidParameterStandard returns the union data inside the InvalidParameters_Item as a InvalidParameterStandard func (t InvalidParameters_Item) AsInvalidParameterStandard() (InvalidParameterStandard, error) { var body InvalidParameterStandard @@ -442,23 +931,184 @@ func (t *InvalidParameters_Item) UnmarshalJSON(b []byte) error { return err } +// AsULID returns the union data inside the PlanReference as a ULID +func (t PlanReference) AsULID() (ULID, error) { + var body ULID + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromULID overwrites any union data inside the PlanReference as the provided ULID +func (t *PlanReference) FromULID(v ULID) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeULID performs a merge with any union data inside the PlanReference, using the provided ULID +func (t *PlanReference) MergeULID(v ULID) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsPlanReference1 returns the union data inside the PlanReference as a PlanReference1 +func (t PlanReference) AsPlanReference1() (PlanReference1, error) { + var body PlanReference1 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromPlanReference1 overwrites any union data inside the PlanReference as the provided PlanReference1 +func (t *PlanReference) FromPlanReference1(v PlanReference1) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergePlanReference1 performs a merge with any union data inside the PlanReference, using the provided PlanReference1 +func (t *PlanReference) MergePlanReference1(v PlanReference1) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t PlanReference) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *PlanReference) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + // ServerInterface represents all server handlers. type ServerInterface interface { + // List customers + // (GET /openmeter/customers) + ListCustomers(w http.ResponseWriter, r *http.Request, params ListCustomersParams) + // Create customer + // (POST /openmeter/customers) + CreateCustomer(w http.ResponseWriter, r *http.Request) + // Delete customer + // (DELETE /openmeter/customers/{customerId}) + DeleteCustomer(w http.ResponseWriter, r *http.Request, customerId ULID) + // Get customer + // (GET /openmeter/customers/{customerId}) + GetCustomer(w http.ResponseWriter, r *http.Request, customerId ULID) + // Upsert customer + // (PUT /openmeter/customers/{customerId}) + UpsertCustomer(w http.ResponseWriter, r *http.Request, customerId ULID) + // Check customer feature access + // (GET /openmeter/customers/{customerId}/entitlements/{featureKey}) + CheckCustomerFeatureAccess(w http.ResponseWriter, r *http.Request, customerId ULID, featureKey ResourceKey) + // List customer subscriptions + // (GET /openmeter/customers/{customerId}/subscriptions) + ListCustomerSubscriptions(w http.ResponseWriter, r *http.Request, customerId ULID, params ListCustomerSubscriptionsParams) + // Create customer subscription + // (POST /openmeter/customers/{customerId}/subscriptions) + CreateCustomerSubscription(w http.ResponseWriter, r *http.Request, customerId ULID) // Ingest metering events // (POST /openmeter/events) IngestMeteringEvents(w http.ResponseWriter, r *http.Request) + // List meters + // (GET /openmeter/meters) + ListMeters(w http.ResponseWriter, r *http.Request, params ListMetersParams) + // Create meter + // (POST /openmeter/meters) + CreateMeter(w http.ResponseWriter, r *http.Request) + // Get meter + // (GET /openmeter/meters/{meterId}) + GetMeter(w http.ResponseWriter, r *http.Request, meterId ULID) } // Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. type Unimplemented struct{} +// List customers +// (GET /openmeter/customers) +func (_ Unimplemented) ListCustomers(w http.ResponseWriter, r *http.Request, params ListCustomersParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Create customer +// (POST /openmeter/customers) +func (_ Unimplemented) CreateCustomer(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Delete customer +// (DELETE /openmeter/customers/{customerId}) +func (_ Unimplemented) DeleteCustomer(w http.ResponseWriter, r *http.Request, customerId ULID) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Get customer +// (GET /openmeter/customers/{customerId}) +func (_ Unimplemented) GetCustomer(w http.ResponseWriter, r *http.Request, customerId ULID) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Upsert customer +// (PUT /openmeter/customers/{customerId}) +func (_ Unimplemented) UpsertCustomer(w http.ResponseWriter, r *http.Request, customerId ULID) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Check customer feature access +// (GET /openmeter/customers/{customerId}/entitlements/{featureKey}) +func (_ Unimplemented) CheckCustomerFeatureAccess(w http.ResponseWriter, r *http.Request, customerId ULID, featureKey ResourceKey) { + w.WriteHeader(http.StatusNotImplemented) +} + +// List customer subscriptions +// (GET /openmeter/customers/{customerId}/subscriptions) +func (_ Unimplemented) ListCustomerSubscriptions(w http.ResponseWriter, r *http.Request, customerId ULID, params ListCustomerSubscriptionsParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Create customer subscription +// (POST /openmeter/customers/{customerId}/subscriptions) +func (_ Unimplemented) CreateCustomerSubscription(w http.ResponseWriter, r *http.Request, customerId ULID) { + w.WriteHeader(http.StatusNotImplemented) +} + // Ingest metering events // (POST /openmeter/events) func (_ Unimplemented) IngestMeteringEvents(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) } +// List meters +// (GET /openmeter/meters) +func (_ Unimplemented) ListMeters(w http.ResponseWriter, r *http.Request, params ListMetersParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Create meter +// (POST /openmeter/meters) +func (_ Unimplemented) CreateMeter(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Get meter +// (GET /openmeter/meters/{meterId}) +func (_ Unimplemented) GetMeter(w http.ResponseWriter, r *http.Request, meterId ULID) { + w.WriteHeader(http.StatusNotImplemented) +} + // ServerInterfaceWrapper converts contexts to parameters. type ServerInterfaceWrapper struct { Handler ServerInterface @@ -468,6 +1118,225 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(http.Handler) http.Handler +// ListCustomers operation middleware +func (siw *ServerInterfaceWrapper) ListCustomers(w http.ResponseWriter, r *http.Request) { + + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params ListCustomersParams + + // ------------- Optional query parameter "page" ------------- + + err = runtime.BindQueryParameter("deepObject", true, false, "page", r.URL.Query(), ¶ms.Page) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page", Err: err}) + return + } + + // ------------- Optional query parameter "sort" ------------- + + err = runtime.BindQueryParameter("form", false, false, "sort", r.URL.Query(), ¶ms.Sort) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "sort", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ListCustomers(w, r, params) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + +// CreateCustomer operation middleware +func (siw *ServerInterfaceWrapper) CreateCustomer(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.CreateCustomer(w, r) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + +// DeleteCustomer operation middleware +func (siw *ServerInterfaceWrapper) DeleteCustomer(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "customerId" ------------- + var customerId ULID + + err = runtime.BindStyledParameterWithOptions("simple", "customerId", chi.URLParam(r, "customerId"), &customerId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + 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.DeleteCustomer(w, r, customerId) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + +// GetCustomer operation middleware +func (siw *ServerInterfaceWrapper) GetCustomer(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "customerId" ------------- + var customerId ULID + + err = runtime.BindStyledParameterWithOptions("simple", "customerId", chi.URLParam(r, "customerId"), &customerId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + 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.GetCustomer(w, r, customerId) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + +// UpsertCustomer operation middleware +func (siw *ServerInterfaceWrapper) UpsertCustomer(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "customerId" ------------- + var customerId ULID + + err = runtime.BindStyledParameterWithOptions("simple", "customerId", chi.URLParam(r, "customerId"), &customerId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + 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.UpsertCustomer(w, r, customerId) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + +// CheckCustomerFeatureAccess operation middleware +func (siw *ServerInterfaceWrapper) CheckCustomerFeatureAccess(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "customerId" ------------- + var customerId ULID + + err = runtime.BindStyledParameterWithOptions("simple", "customerId", chi.URLParam(r, "customerId"), &customerId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "customerId", Err: err}) + return + } + + // ------------- Path parameter "featureKey" ------------- + var featureKey ResourceKey + + err = runtime.BindStyledParameterWithOptions("simple", "featureKey", chi.URLParam(r, "featureKey"), &featureKey, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "featureKey", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.CheckCustomerFeatureAccess(w, r, customerId, featureKey) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + +// ListCustomerSubscriptions operation middleware +func (siw *ServerInterfaceWrapper) ListCustomerSubscriptions(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "customerId" ------------- + var customerId ULID + + err = runtime.BindStyledParameterWithOptions("simple", "customerId", chi.URLParam(r, "customerId"), &customerId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "customerId", Err: err}) + return + } + + // Parameter object where we will unmarshal all parameters from the context + var params ListCustomerSubscriptionsParams + + // ------------- Optional query parameter "page" ------------- + + err = runtime.BindQueryParameter("deepObject", true, false, "page", r.URL.Query(), ¶ms.Page) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ListCustomerSubscriptions(w, r, customerId, params) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + +// CreateCustomerSubscription operation middleware +func (siw *ServerInterfaceWrapper) CreateCustomerSubscription(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "customerId" ------------- + var customerId ULID + + err = runtime.BindStyledParameterWithOptions("simple", "customerId", chi.URLParam(r, "customerId"), &customerId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + 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.CreateCustomerSubscription(w, r, customerId) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + // IngestMeteringEvents operation middleware func (siw *ServerInterfaceWrapper) IngestMeteringEvents(w http.ResponseWriter, r *http.Request) { @@ -482,6 +1351,72 @@ func (siw *ServerInterfaceWrapper) IngestMeteringEvents(w http.ResponseWriter, r handler.ServeHTTP(w, r) } +// ListMeters operation middleware +func (siw *ServerInterfaceWrapper) ListMeters(w http.ResponseWriter, r *http.Request) { + + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params ListMetersParams + + // ------------- Optional query parameter "page" ------------- + + err = runtime.BindQueryParameter("deepObject", true, false, "page", r.URL.Query(), ¶ms.Page) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ListMeters(w, r, params) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + +// CreateMeter operation middleware +func (siw *ServerInterfaceWrapper) CreateMeter(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.CreateMeter(w, r) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + +// GetMeter operation middleware +func (siw *ServerInterfaceWrapper) GetMeter(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "meterId" ------------- + var meterId ULID + + err = runtime.BindStyledParameterWithOptions("simple", "meterId", chi.URLParam(r, "meterId"), &meterId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "meterId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetMeter(w, r, meterId) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + type UnescapedCookieParamError struct { ParamName string Err error @@ -595,9 +1530,42 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl ErrorHandlerFunc: options.ErrorHandlerFunc, } + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/openmeter/customers", wrapper.ListCustomers) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/openmeter/customers", wrapper.CreateCustomer) + }) + r.Group(func(r chi.Router) { + r.Delete(options.BaseURL+"/openmeter/customers/{customerId}", wrapper.DeleteCustomer) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/openmeter/customers/{customerId}", wrapper.GetCustomer) + }) + r.Group(func(r chi.Router) { + r.Put(options.BaseURL+"/openmeter/customers/{customerId}", wrapper.UpsertCustomer) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/openmeter/customers/{customerId}/entitlements/{featureKey}", wrapper.CheckCustomerFeatureAccess) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/openmeter/customers/{customerId}/subscriptions", wrapper.ListCustomerSubscriptions) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/openmeter/customers/{customerId}/subscriptions", wrapper.CreateCustomerSubscription) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/openmeter/events", wrapper.IngestMeteringEvents) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/openmeter/meters", wrapper.ListMeters) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/openmeter/meters", wrapper.CreateMeter) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/openmeter/meters/{meterId}", wrapper.GetMeter) + }) return r } @@ -605,57 +1573,114 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "yH//UCsAAA==", + "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", + "ThUCp5IJVeeOMhu8/wsAAP//r88DnAyRAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v3/handlers/customers/convert.gen.go b/api/v3/handlers/customers/convert.gen.go new file mode 100644 index 000000000..7641a7419 --- /dev/null +++ b/api/v3/handlers/customers/convert.gen.go @@ -0,0 +1,191 @@ +// Code generated by github.com/jmattheis/goverter, DO NOT EDIT. +//go:build !goverter + +package customers + +import ( + v3 "github.com/openmeterio/openmeter/api/v3" + response "github.com/openmeterio/openmeter/api/v3/response" + customer "github.com/openmeterio/openmeter/openmeter/customer" + currencyx "github.com/openmeterio/openmeter/pkg/currencyx" + models "github.com/openmeterio/openmeter/pkg/models" + "time" +) + +func init() { + ConvertCreateCustomerRequestToCustomerMutate = func(source v3.CreateCustomerRequest) customer.CustomerMutate { + var customerCustomerMutate customer.CustomerMutate + pString := source.Key + customerCustomerMutate.Key = &pString + customerCustomerMutate.Name = source.Name + customerCustomerMutate.Description = source.Description + customerCustomerMutate.UsageAttribution = pV3BillingCustomerUsageAttributionToPCustomerCustomerUsageAttribution(source.UsageAttribution) + customerCustomerMutate.PrimaryEmail = source.PrimaryEmail + if source.Currency != nil { + currencyxCode := currencyx.Code(*source.Currency) + customerCustomerMutate.Currency = ¤cyxCode + } + customerCustomerMutate.BillingAddress = pV3BillingAddressToPModelsAddress(source.BillingAddress) + customerCustomerMutate.Metadata = pV3LabelsToPModelsMetadata(source.Labels) + return customerCustomerMutate + } + ConvertCustomerListResponse = func(source response.PagePaginationResponse[customer.Customer]) v3.CustomerPagePaginatedResponse { + var v3CustomerPagePaginatedResponse v3.CustomerPagePaginatedResponse + if source.Data != nil { + v3CustomerPagePaginatedResponse.Data = make([]v3.BillingCustomer, len(source.Data)) + for i := 0; i < len(source.Data); i++ { + v3CustomerPagePaginatedResponse.Data[i] = ConvertCustomerRequestToBillingCustomer(source.Data[i]) + } + } + v3CustomerPagePaginatedResponse.Meta = responsePageMetaToV3PaginatedMeta(source.Meta) + return v3CustomerPagePaginatedResponse + } + ConvertCustomerRequestToBillingCustomer = func(source customer.Customer) v3.BillingCustomer { + var v3BillingCustomer v3.BillingCustomer + v3BillingCustomer.BillingAddress = pModelsAddressToPV3BillingAddress(source.BillingAddress) + v3BillingCustomer.CreatedAt = timeTimeToPTimeTime(source.ManagedResource.ManagedModel.CreatedAt) + if source.Currency != nil { + xstring := string(*source.Currency) + v3BillingCustomer.Currency = &xstring + } + v3BillingCustomer.DeletedAt = source.ManagedResource.ManagedModel.DeletedAt + v3BillingCustomer.Description = source.ManagedResource.Description + v3BillingCustomer.Id = source.ManagedResource.ID + if source.Key != nil { + v3BillingCustomer.Key = *source.Key + } + v3BillingCustomer.Labels = pModelsMetadataToPV3Labels(source.Metadata) + v3BillingCustomer.Name = source.ManagedResource.Name + v3BillingCustomer.PrimaryEmail = source.PrimaryEmail + v3BillingCustomer.UpdatedAt = timeTimeToPTimeTime(source.ManagedResource.ManagedModel.UpdatedAt) + v3BillingCustomer.UsageAttribution = pCustomerCustomerUsageAttributionToPV3BillingCustomerUsageAttribution(source.UsageAttribution) + return v3BillingCustomer + } + ConvertFromCreateCustomerRequestToCreateCustomerInput = func(context string, source v3.CreateCustomerRequest) customer.CreateCustomerInput { + var customerCreateCustomerInput customer.CreateCustomerInput + customerCreateCustomerInput.Namespace = NamespaceFromContext(context) + customerCreateCustomerInput.CustomerMutate = ConvertCreateCustomerRequestToCustomerMutate(source) + return customerCreateCustomerInput + } + ConvertUpsertCustomerRequestToCustomerMutate = func(source v3.UpsertCustomerRequest) customer.CustomerMutate { + var customerCustomerMutate customer.CustomerMutate + customerCustomerMutate.Name = source.Name + customerCustomerMutate.Description = source.Description + customerCustomerMutate.UsageAttribution = pV3BillingCustomerUsageAttributionToPCustomerCustomerUsageAttribution(source.UsageAttribution) + customerCustomerMutate.PrimaryEmail = source.PrimaryEmail + if source.Currency != nil { + currencyxCode := currencyx.Code(*source.Currency) + customerCustomerMutate.Currency = ¤cyxCode + } + customerCustomerMutate.BillingAddress = pV3BillingAddressToPModelsAddress(source.BillingAddress) + customerCustomerMutate.Metadata = pV3LabelsToPModelsMetadata(source.Labels) + return customerCustomerMutate + } +} +func modelsMetadataToV3Labels(source models.Metadata) v3.Labels { + var v3Labels v3.Labels + if source != nil { + v3Labels = make(v3.Labels, len(source)) + for key, value := range source { + v3Labels[key] = value + } + } + return v3Labels +} +func pCustomerCustomerUsageAttributionToPV3BillingCustomerUsageAttribution(source *customer.CustomerUsageAttribution) *v3.BillingCustomerUsageAttribution { + var pV3BillingCustomerUsageAttribution *v3.BillingCustomerUsageAttribution + if source != nil { + var v3BillingCustomerUsageAttribution v3.BillingCustomerUsageAttribution + v3BillingCustomerUsageAttribution.SubjectKeys = (*source).SubjectKeys + pV3BillingCustomerUsageAttribution = &v3BillingCustomerUsageAttribution + } + return pV3BillingCustomerUsageAttribution +} +func pModelsAddressToPV3BillingAddress(source *models.Address) *v3.BillingAddress { + var pV3BillingAddress *v3.BillingAddress + if source != nil { + var v3BillingAddress v3.BillingAddress + v3BillingAddress.City = (*source).City + if (*source).Country != nil { + xstring := string(*(*source).Country) + v3BillingAddress.Country = &xstring + } + v3BillingAddress.Line1 = (*source).Line1 + v3BillingAddress.Line2 = (*source).Line2 + v3BillingAddress.PhoneNumber = (*source).PhoneNumber + v3BillingAddress.PostalCode = (*source).PostalCode + v3BillingAddress.State = (*source).State + pV3BillingAddress = &v3BillingAddress + } + return pV3BillingAddress +} +func pModelsMetadataToPV3Labels(source *models.Metadata) *v3.Labels { + var pV3Labels *v3.Labels + if source != nil { + v3Labels := modelsMetadataToV3Labels((*source)) + pV3Labels = &v3Labels + } + return pV3Labels +} +func pV3BillingAddressToPModelsAddress(source *v3.BillingAddress) *models.Address { + var pModelsAddress *models.Address + if source != nil { + var modelsAddress models.Address + if (*source).Country != nil { + modelsCountryCode := models.CountryCode(*(*source).Country) + modelsAddress.Country = &modelsCountryCode + } + modelsAddress.PostalCode = (*source).PostalCode + modelsAddress.State = (*source).State + modelsAddress.City = (*source).City + modelsAddress.Line1 = (*source).Line1 + modelsAddress.Line2 = (*source).Line2 + modelsAddress.PhoneNumber = (*source).PhoneNumber + pModelsAddress = &modelsAddress + } + return pModelsAddress +} +func pV3BillingCustomerUsageAttributionToPCustomerCustomerUsageAttribution(source *v3.BillingCustomerUsageAttribution) *customer.CustomerUsageAttribution { + var pCustomerCustomerUsageAttribution *customer.CustomerUsageAttribution + if source != nil { + var customerCustomerUsageAttribution customer.CustomerUsageAttribution + customerCustomerUsageAttribution.SubjectKeys = (*source).SubjectKeys + pCustomerCustomerUsageAttribution = &customerCustomerUsageAttribution + } + return pCustomerCustomerUsageAttribution +} +func pV3LabelsToPModelsMetadata(source *v3.Labels) *models.Metadata { + var pModelsMetadata *models.Metadata + if source != nil { + modelsMetadata := v3LabelsToModelsMetadata((*source)) + pModelsMetadata = &modelsMetadata + } + return pModelsMetadata +} +func responsePageMetaPageToV3PageMeta(source response.PageMetaPage) v3.PageMeta { + var v3PageMeta v3.PageMeta + v3PageMeta.Number = IntToFloat32(source.Number) + v3PageMeta.Size = IntToFloat32(source.Size) + if source.Total != nil { + v3PageMeta.Total = IntToFloat32(*source.Total) + } + return v3PageMeta +} +func responsePageMetaToV3PaginatedMeta(source response.PageMeta) v3.PaginatedMeta { + var v3PaginatedMeta v3.PaginatedMeta + v3PaginatedMeta.Page = responsePageMetaPageToV3PageMeta(source.Page) + return v3PaginatedMeta +} +func timeTimeToPTimeTime(source time.Time) *time.Time { + return &source +} +func v3LabelsToModelsMetadata(source v3.Labels) models.Metadata { + var modelsMetadata models.Metadata + if source != nil { + modelsMetadata = make(models.Metadata, len(source)) + for key, value := range source { + modelsMetadata[key] = value + } + } + return modelsMetadata +} diff --git a/api/v3/handlers/customers/convert.go b/api/v3/handlers/customers/convert.go new file mode 100644 index 000000000..3ebd9c538 --- /dev/null +++ b/api/v3/handlers/customers/convert.go @@ -0,0 +1,47 @@ +//go:generate go run github.com/jmattheis/goverter/cmd/goverter gen ./ +package customers + +import ( + api "github.com/openmeterio/openmeter/api/v3" + "github.com/openmeterio/openmeter/api/v3/response" + "github.com/openmeterio/openmeter/openmeter/customer" +) + +// goverter:variables +// goverter:skipCopySameType +// goverter:output:file ./convert.gen.go +// goverter:useZeroValueOnPointerInconsistency +// goverter:useUnderlyingTypeMethods +// goverter:matchIgnoreCase +// goverter:extend IntToFloat32 +var ( + // goverter:context namespace + // goverter:map Namespace | NamespaceFromContext + // goverter:map . CustomerMutate + ConvertFromCreateCustomerRequestToCreateCustomerInput func(namespace string, createCustomerRequest api.CreateCustomerRequest) customer.CreateCustomerInput + // goverter:map Metadata Labels + // goverter:map ManagedResource.ID Id + // goverter:map ManagedResource.Description Description + // goverter:map ManagedResource.Name Name + // goverter:map ManagedResource.ManagedModel.CreatedAt CreatedAt + // goverter:map ManagedResource.ManagedModel.UpdatedAt UpdatedAt + // goverter:map ManagedResource.ManagedModel.DeletedAt DeletedAt + ConvertCustomerRequestToBillingCustomer func(customer.Customer) api.BillingCustomer + // goverter:map Labels Metadata + // goverter:ignore Annotation + ConvertCreateCustomerRequestToCustomerMutate func(createCustomerRequest api.CreateCustomerRequest) customer.CustomerMutate + // goverter:map Labels Metadata + // goverter:ignore Annotation + // goverter:ignore Key + ConvertUpsertCustomerRequestToCustomerMutate func(updateCustomerRequest api.UpsertCustomerRequest) customer.CustomerMutate + ConvertCustomerListResponse func(customers response.PagePaginationResponse[customer.Customer]) api.CustomerPagePaginatedResponse +) + +//goverter:context namespace +func NamespaceFromContext(namespace string) string { + return namespace +} + +func IntToFloat32(i int) float32 { + return float32(i) +} diff --git a/api/v3/handlers/customers/create.go b/api/v3/handlers/customers/create.go new file mode 100644 index 000000000..84d8bdbab --- /dev/null +++ b/api/v3/handlers/customers/create.go @@ -0,0 +1,59 @@ +package customers + +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/customer" + "github.com/openmeterio/openmeter/pkg/framework/commonhttp" + "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" +) + +type ( + CreateCustomerRequest = customer.CreateCustomerInput + CreateCustomerResponse = api.BillingCustomer + CreateCustomerHandler httptransport.Handler[CreateCustomerRequest, CreateCustomerResponse] +) + +// CreateCustomer returns a new httptransport.Handler for creating a customer. +func (h *handler) CreateCustomer() CreateCustomerHandler { + return httptransport.NewHandler( + func(ctx context.Context, r *http.Request) (CreateCustomerRequest, error) { + body := api.CreateCustomerRequest{} + if err := request.ParseBody(r, &body); err != nil { + return CreateCustomerRequest{}, err + } + + ns, err := h.resolveNamespace(ctx) + if err != nil { + return CreateCustomerRequest{}, err + } + + req := ConvertFromCreateCustomerRequestToCreateCustomerInput(ns, body) + + return req, nil + }, + func(ctx context.Context, request CreateCustomerRequest) (CreateCustomerResponse, error) { + customer, err := h.service.CreateCustomer(ctx, request) + if err != nil { + return CreateCustomerResponse{}, err + } + + if customer == nil { + return CreateCustomerResponse{}, fmt.Errorf("failed to create customer") + } + + return ConvertCustomerRequestToBillingCustomer(*customer), nil + }, + commonhttp.JSONResponseEncoderWithStatus[CreateCustomerResponse](http.StatusCreated), + httptransport.AppendOptions( + h.options, + httptransport.WithOperationName("create-customer"), + httptransport.WithErrorEncoder(apierrors.GenericErrorEncoder()), + )..., + ) +} diff --git a/api/v3/handlers/customers/delete.go b/api/v3/handlers/customers/delete.go new file mode 100644 index 000000000..6089617e3 --- /dev/null +++ b/api/v3/handlers/customers/delete.go @@ -0,0 +1,69 @@ +package customers + +import ( + "context" + "net/http" + + "github.com/openmeterio/openmeter/api/v3/apierrors" + "github.com/openmeterio/openmeter/openmeter/customer" + "github.com/openmeterio/openmeter/pkg/framework/commonhttp" + "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" +) + +type ( + DeleteCustomerRequest struct { + Namespace string + CustomerID string + } + DeleteCustomerResponse = interface{} + DeleteCustomerParams = string + DeleteCustomerHandler httptransport.HandlerWithArgs[DeleteCustomerRequest, DeleteCustomerResponse, DeleteCustomerParams] +) + +// DeleteCustomer returns a handler for deleting a customer. +func (h *handler) DeleteCustomer() DeleteCustomerHandler { + return httptransport.NewHandlerWithArgs( + func(ctx context.Context, r *http.Request, customerID DeleteCustomerParams) (DeleteCustomerRequest, error) { + ns, err := h.resolveNamespace(ctx) + if err != nil { + return DeleteCustomerRequest{}, err + } + + return DeleteCustomerRequest{ + Namespace: ns, + CustomerID: customerID, + }, nil + }, + func(ctx context.Context, request DeleteCustomerRequest) (DeleteCustomerResponse, error) { + // Get the customer + cus, err := h.service.GetCustomer(ctx, customer.GetCustomerInput{ + CustomerID: &customer.CustomerID{ + ID: request.CustomerID, + Namespace: request.Namespace, + }, + }) + if err != nil { + return DeleteCustomerRequest{}, err + } + + // Idempotent operation, we return 204 when a customer is already deleted. + // Rationale: https://kong-aip.netlify.app/aip/135/ + if cus != nil && cus.IsDeleted() { + return nil, nil + } + + err = h.service.DeleteCustomer(ctx, cus.GetID()) + if err != nil { + return nil, err + } + + return nil, nil + }, + commonhttp.JSONResponseEncoderWithStatus[DeleteCustomerResponse](http.StatusNoContent), + httptransport.AppendOptions( + h.options, + httptransport.WithOperationName("delete-customer"), + httptransport.WithErrorEncoder(apierrors.GenericErrorEncoder()), + )..., + ) +} diff --git a/api/v3/handlers/customers/get.go b/api/v3/handlers/customers/get.go new file mode 100644 index 000000000..3bc16d8fe --- /dev/null +++ b/api/v3/handlers/customers/get.go @@ -0,0 +1,60 @@ +package customers + +import ( + "context" + "fmt" + "net/http" + + api "github.com/openmeterio/openmeter/api/v3" + "github.com/openmeterio/openmeter/api/v3/apierrors" + "github.com/openmeterio/openmeter/openmeter/customer" + "github.com/openmeterio/openmeter/pkg/framework/commonhttp" + "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" +) + +type ( + GetCustomerRequest = customer.GetCustomerInput + GetCustomerResponse = api.BillingCustomer + GetCustomerParams = string + GetCustomerHandler httptransport.HandlerWithArgs[GetCustomerRequest, GetCustomerResponse, GetCustomerParams] +) + +// GetCustomer returns a handler for getting a customer. +func (h *handler) GetCustomer() GetCustomerHandler { + return httptransport.NewHandlerWithArgs( + func(ctx context.Context, r *http.Request, customerID GetCustomerParams) (GetCustomerRequest, error) { + ns, err := h.resolveNamespace(ctx) + if err != nil { + return GetCustomerRequest{}, err + } + + // a comment + + return GetCustomerRequest{ + CustomerID: &customer.CustomerID{ + Namespace: ns, + ID: customerID, + }, + }, nil + }, + func(ctx context.Context, request GetCustomerRequest) (GetCustomerResponse, error) { + // Get the customer + cus, err := h.service.GetCustomer(ctx, request) + if err != nil { + return GetCustomerResponse{}, err + } + + if cus == nil { + return GetCustomerResponse{}, fmt.Errorf("failed to get customer") + } + + return ConvertCustomerRequestToBillingCustomer(*cus), nil + }, + commonhttp.JSONResponseEncoderWithStatus[GetCustomerResponse](http.StatusOK), + httptransport.AppendOptions( + h.options, + httptransport.WithOperationName("get-customer"), + httptransport.WithErrorEncoder(apierrors.GenericErrorEncoder()), + )..., + ) +} diff --git a/api/v3/handlers/customers/handler.go b/api/v3/handlers/customers/handler.go new file mode 100644 index 000000000..04db73a6a --- /dev/null +++ b/api/v3/handlers/customers/handler.go @@ -0,0 +1,34 @@ +package customers + +import ( + "context" + + "github.com/openmeterio/openmeter/openmeter/customer" + "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" +) + +type Handler interface { + ListCustomers() ListCustomersHandler + CreateCustomer() CreateCustomerHandler + DeleteCustomer() DeleteCustomerHandler + GetCustomer() GetCustomerHandler + UpsertCustomer() UpsertCustomerHandler +} + +type handler struct { + resolveNamespace func(ctx context.Context) (string, error) + service customer.Service + options []httptransport.HandlerOption +} + +func New( + resolveNamespace func(ctx context.Context) (string, error), + service customer.Service, + options ...httptransport.HandlerOption, +) Handler { + return &handler{ + resolveNamespace: resolveNamespace, + service: service, + options: options, + } +} diff --git a/api/v3/handlers/customers/list.go b/api/v3/handlers/customers/list.go new file mode 100644 index 000000000..98a4b0985 --- /dev/null +++ b/api/v3/handlers/customers/list.go @@ -0,0 +1,95 @@ +package customers + +import ( + "context" + "fmt" + "net/http" + + "github.com/samber/lo" + + 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/api/v3/response" + "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/pagination" + "github.com/openmeterio/openmeter/pkg/sortx" +) + +type ( + ListCustomersRequest = customer.ListCustomersInput + ListCustomersResponse = response.PagePaginationResponse[api.BillingCustomer] + ListCustomersParams = api.ListCustomersParams + ListCustomersHandler httptransport.HandlerWithArgs[ListCustomersRequest, ListCustomersResponse, ListCustomersParams] +) + +func (h *handler) ListCustomers() ListCustomersHandler { + return httptransport.NewHandlerWithArgs( + func(ctx context.Context, r *http.Request, params ListCustomersParams) (ListCustomersRequest, error) { + ns, err := h.resolveNamespace(ctx) + if err != nil { + return ListCustomersRequest{}, err + } + + page := pagination.NewPage(1, 20) + if params.Page != nil { + page = pagination.NewPage( + lo.FromPtrOr(params.Page.Number, 1), + lo.FromPtrOr(params.Page.Size, 20), + ) + } + + var orderBy string + var order sortx.Order + if params.Sort != nil { + sort, err := request.ParseSortBy(*params.Sort) + if err != nil { + return ListCustomersRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Field: "sort", + Reason: err.Error(), + Source: apierrors.InvalidParamSourceQuery, + }, + }) + } + orderBy = sort.Field + order = sort.Order.ToSortxOrder() + } + + req := ListCustomersRequest{ + Namespace: ns, + Page: page, + OrderBy: orderBy, + Order: order, + } + + return req, nil + }, + func(ctx context.Context, request ListCustomersRequest) (ListCustomersResponse, error) { + resp, err := h.service.ListCustomers(ctx, request) + if err != nil { + return ListCustomersResponse{}, fmt.Errorf("failed to list customers: %w", err) + } + + customers := lo.Map(resp.Items, func(item customer.Customer, _ int) api.BillingCustomer { + return ConvertCustomerRequestToBillingCustomer(item) + }) + + r := response.NewPagePaginationResponse(customers, response.PageMetaPage{ + Size: request.Page.PageSize, + Number: request.Page.PageNumber, + Total: lo.ToPtr(resp.TotalCount), + }) + + return r, nil + }, + commonhttp.JSONResponseEncoderWithStatus[ListCustomersResponse](http.StatusOK), + httptransport.AppendOptions( + h.options, + httptransport.WithOperationName("list-customers"), + httptransport.WithErrorEncoder(apierrors.GenericErrorEncoder()), + )..., + ) +} diff --git a/api/v3/handlers/customers/upsert.go b/api/v3/handlers/customers/upsert.go new file mode 100644 index 000000000..2fe4473f7 --- /dev/null +++ b/api/v3/handlers/customers/upsert.go @@ -0,0 +1,95 @@ +package customers + +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/customer" + "github.com/openmeterio/openmeter/pkg/framework/commonhttp" + "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" +) + +type ( + UpsertCustomerRequest struct { + Namespace string + CustomerID string + CustomerMutate customer.CustomerMutate + } + UpsertCustomerParams = string + UpsertCustomerResponse = api.BillingCustomer + UpsertCustomerHandler httptransport.HandlerWithArgs[UpsertCustomerRequest, UpsertCustomerResponse, UpsertCustomerParams] +) + +// UpdateCustomer returns a handler for updating a customer. +func (h *handler) UpsertCustomer() UpsertCustomerHandler { + return httptransport.NewHandlerWithArgs[UpsertCustomerRequest, UpsertCustomerResponse, UpsertCustomerParams]( + func(ctx context.Context, r *http.Request, customerID UpsertCustomerParams) (UpsertCustomerRequest, error) { + body := api.UpsertCustomerRequest{} + if err := request.ParseBody(r, &body); err != nil { + return UpsertCustomerRequest{}, fmt.Errorf("field to decode update customer request: %w", err) + } + + ns, err := h.resolveNamespace(ctx) + if err != nil { + return UpsertCustomerRequest{}, err + } + + req := UpsertCustomerRequest{ + Namespace: ns, + CustomerID: customerID, + // Key cannot be updated according to api.UpsertCustomerRequest. + // Therefore, at this point we don't have a key yet. It is ignored in this conversion. + CustomerMutate: ConvertUpsertCustomerRequestToCustomerMutate(body), + } + + return req, nil + }, + func(ctx context.Context, request UpsertCustomerRequest) (UpsertCustomerResponse, error) { + // Get the customer + cus, err := h.service.GetCustomer(ctx, customer.GetCustomerInput{ + CustomerID: &customer.CustomerID{ + ID: request.CustomerID, + Namespace: request.Namespace, + }, + }) + if err != nil { + return UpsertCustomerResponse{}, err + } + + if cus != nil && cus.IsDeleted() { + return UpsertCustomerResponse{}, + apierrors.NewPreconditionFailedError( + ctx, + fmt.Sprintf("customer is deleted [namespace=%s customer.id=%s]", cus.Namespace, cus.ID), + ) + } + + // Use the key from the just retrieved customer, as it is required by the UpdateCustomer service method. + request.CustomerMutate.Key = cus.Key + + updatedCustomer, err := h.service.UpdateCustomer(ctx, customer.UpdateCustomerInput{ + CustomerID: cus.GetID(), + CustomerMutate: request.CustomerMutate, + }) + if err != nil { + return UpsertCustomerResponse{}, err + } + + if updatedCustomer == nil { + return UpsertCustomerResponse{}, fmt.Errorf("failed to update customer") + } + + return ConvertCustomerRequestToBillingCustomer(*updatedCustomer), nil + }, + commonhttp.JSONResponseEncoderWithStatus[UpsertCustomerResponse](http.StatusOK), + httptransport.AppendOptions( + h.options, + httptransport.WithOperationName("update-customer"), + httptransport.WithErrorEncoder(apierrors.GenericErrorEncoder()), + )..., + ) +} diff --git a/api/v3/openapi.yaml b/api/v3/openapi.yaml index 9332e724f..3d2f1672c 100644 --- a/api/v3/openapi.yaml +++ b/api/v3/openapi.yaml @@ -13,17 +13,290 @@ servers: - url: https://openmeter.cloud/api/v3 description: Cloud variables: {} - - url: https://127.0.0.1/api/v3 - description: Local - variables: {} tags: - - name: Meters - description: Meters specify how to aggregate events for billing and analytics purposes. Meters can be configured with multiple aggregation methods and groupings. Multiple meters can be created for the same event type, enabling flexible metering scenarios. + - name: Billing Entitlements + description: Entitlements are used to control access to features for customers. + - name: Billing Subscriptions + description: Subscriptions are used to track usage of your product or service. Subscriptions can be individuals or organizations that can subscribe to plans and have access to features. - name: Billing Customers description: Customers are used to track usage of your product or service. Customers can be individuals or organizations that can subscribe to plans and have access to features. - name: Metering Events description: Metering events are used to track usage of your product or service. Events are processed asynchronously by the meters, so they may not be immediately available for querying. + - name: Meters + description: Meters specify how to aggregate events for billing and analytics purposes. Meters can be configured with multiple aggregation methods and groupings. Multiple meters can be created for the same event type, enabling flexible metering scenarios. paths: + /openmeter/customers: + post: + operationId: create-customer + summary: Create customer + responses: + '201': + description: Customer created response. + content: + application/json: + schema: + $ref: '#/components/schemas/BillingCustomer' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '500': + $ref: '#/components/responses/Internal' + '503': + $ref: '#/components/responses/NotAvailable' + tags: + - Billing Customers + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateCustomerRequest' + x-internal: true + x-unstable: true + get: + operationId: list-customers + summary: List customers + parameters: + - $ref: '#/components/parameters/PagePaginationQuery' + - $ref: '#/components/parameters/ListCustomersParams.sort' + responses: + '200': + description: Page paginated response. + content: + application/json: + schema: + $ref: '#/components/schemas/CustomerPagePaginatedResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '500': + $ref: '#/components/responses/Internal' + '503': + $ref: '#/components/responses/NotAvailable' + tags: + - Billing Customers + x-internal: true + x-unstable: true + /openmeter/customers/{customerId}: + get: + operationId: get-customer + summary: Get customer + parameters: + - name: customerId + in: path + required: true + schema: + $ref: '#/components/schemas/ULID' + responses: + '200': + description: Customer response. + content: + application/json: + schema: + $ref: '#/components/schemas/BillingCustomer' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/Internal' + '503': + $ref: '#/components/responses/NotAvailable' + tags: + - Billing Customers + x-internal: true + x-unstable: true + put: + operationId: upsert-customer + summary: Upsert customer + parameters: + - name: customerId + in: path + required: true + schema: + $ref: '#/components/schemas/ULID' + responses: + '200': + description: Customer upsert response. + content: + application/json: + schema: + $ref: '#/components/schemas/BillingCustomer' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/Internal' + '503': + $ref: '#/components/responses/NotAvailable' + tags: + - Billing Customers + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpsertCustomerRequest' + x-internal: true + x-unstable: true + delete: + operationId: delete-customer + summary: Delete customer + parameters: + - name: customerId + in: path + required: true + schema: + $ref: '#/components/schemas/ULID' + responses: + '204': + description: Deleted response. + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/Internal' + '503': + $ref: '#/components/responses/NotAvailable' + tags: + - Billing Customers + x-internal: true + x-unstable: true + /openmeter/customers/{customerId}/entitlements/{featureKey}: + get: + operationId: check-customer-feature-access + summary: Check customer feature access + parameters: + - name: customerId + in: path + required: true + schema: + $ref: '#/components/schemas/ULID' + - name: featureKey + in: path + required: true + schema: + $ref: '#/components/schemas/ResourceKey' + responses: + '200': + description: EntitlementCheck response. + content: + application/json: + schema: + $ref: '#/components/schemas/BillingEntitlementCheck' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/Internal' + '503': + $ref: '#/components/responses/NotAvailable' + tags: + - Billing Customers + - Billing Entitlements + x-internal: true + x-unstable: true + /openmeter/customers/{customerId}/subscriptions: + post: + operationId: create-customer-subscription + summary: Create customer subscription + parameters: + - name: customerId + in: path + required: true + schema: + $ref: '#/components/schemas/ULID' + responses: + '201': + description: Subscription created response. + content: + application/json: + schema: + $ref: '#/components/schemas/BillingSubscription' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '500': + $ref: '#/components/responses/Internal' + '503': + $ref: '#/components/responses/NotAvailable' + tags: + - Billing Customers + - Billing Subscriptions + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateSubscriptionRequest' + x-internal: true + x-unstable: true + get: + operationId: list-customer-subscriptions + summary: List customer subscriptions + parameters: + - name: customerId + in: path + required: true + schema: + $ref: '#/components/schemas/ULID' + - $ref: '#/components/parameters/PagePaginationQuery' + responses: + '200': + description: Page paginated response. + content: + application/json: + schema: + $ref: '#/components/schemas/SubscriptionPagePaginatedResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/Internal' + '503': + $ref: '#/components/responses/NotAvailable' + tags: + - Billing Customers + - Billing Subscriptions + x-internal: true + x-unstable: true /openmeter/events: post: operationId: ingest-metering-events @@ -64,14 +337,630 @@ paths: - type: array items: $ref: '#/components/schemas/MeteringEvent' + /openmeter/meters: + post: + operationId: create-meter + summary: Create meter + description: Create a meter. + responses: + '201': + description: Meter created response. + content: + application/json: + schema: + $ref: '#/components/schemas/Meter' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '500': + $ref: '#/components/responses/Internal' + '503': + $ref: '#/components/responses/NotAvailable' + tags: + - Meters + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateMeterRequest' + get: + operationId: list-meters + summary: List meters + description: List meters. + parameters: + - $ref: '#/components/parameters/PagePaginationQuery' + responses: + '200': + description: Page paginated response. + content: + application/json: + schema: + $ref: '#/components/schemas/MeterPagePaginatedResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '500': + $ref: '#/components/responses/Internal' + '503': + $ref: '#/components/responses/NotAvailable' + tags: + - Meters + /openmeter/meters/{meterId}: + get: + operationId: get-meter + summary: Get meter + description: Get a meter by ID. + parameters: + - name: meterId + in: path + required: true + schema: + $ref: '#/components/schemas/ULID' + responses: + '200': + description: Meter response. + content: + application/json: + schema: + $ref: '#/components/schemas/Meter' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/Internal' + '503': + $ref: '#/components/responses/NotAvailable' + tags: + - Meters components: + parameters: + ListCustomersParams.sort: + name: sort + in: query + required: false + description: |- + Sort customers returned in the response. + Supported sort attributes are: + - `key` + - `id` + - `name` + - `primary_email` + - `created_at` (default) + - `updated_at` + - `deleted_at` + + The `asc` suffix is optional as the default sort order is ascending. + The `desc` suffix is used to specify a descending order. + Multiple sort attributes may be provided via a comma separated list. + schema: + $ref: '#/components/schemas/SortQuery' + explode: false + style: form + PagePaginationQuery: + name: page + in: query + required: false + description: Determines which page of the collection to retrieve. + schema: + type: object + properties: + size: + type: integer + number: + type: integer + style: deepObject schemas: + BillingAddress: + type: object + properties: + country: + allOf: + - $ref: '#/components/schemas/CountryCode' + description: Country code in [ISO 3166-1](https://www.iso.org/iso-3166-country-codes.html) alpha-2 format. + title: Country + postal_code: + type: string + description: Postal code. + title: Postal Code + state: + type: string + description: State or province. + title: State + city: + type: string + description: City. + title: City + line1: + type: string + description: First line of the address. + title: Line 1 + line2: + type: string + description: Second line of the address. + title: Line 2 + phone_number: + type: string + description: Phone number. + title: Phone Number + description: Address + BillingCustomer: + type: object + required: + - id + - name + - key + properties: + id: + allOf: + - $ref: '#/components/schemas/ULID' + readOnly: true + name: + type: string + minLength: 1 + maxLength: 256 + description: |- + Display name of the resource. + + Between 1 and 256 characters. + description: + type: string + maxLength: 1024 + description: |- + Optional description of the resource. + + Maximum 1024 characters. + labels: + $ref: '#/components/schemas/Labels' + created_at: + allOf: + - $ref: '#/components/schemas/DateTime' + description: An ISO-8601 timestamp representation of entity creation date. + readOnly: true + updated_at: + allOf: + - $ref: '#/components/schemas/DateTime' + description: An ISO-8601 timestamp representation of entity last update date. + readOnly: true + deleted_at: + allOf: + - $ref: '#/components/schemas/DateTime' + description: An ISO-8601 timestamp representation of entity deletion date. + readOnly: true + key: + $ref: '#/components/schemas/ResourceKey' + usage_attribution: + allOf: + - $ref: '#/components/schemas/BillingCustomerUsageAttribution' + description: Mapping to attribute metered usage to the customer by the event subject. + title: Usage Attribution + primary_email: + type: string + description: The primary email address of the customer. + title: Primary Email + currency: + allOf: + - $ref: '#/components/schemas/CurrencyCode' + description: |- + Currency of the customer. + Used for billing, tax and invoicing. + title: Currency + billing_address: + allOf: + - $ref: '#/components/schemas/BillingAddress' + description: |- + The billing address of the customer. + Used for tax and invoicing. + title: Billing Address + description: Customers can be individuals or organizations that can subscribe to plans and have access to features. + BillingCustomerUsageAttribution: + type: object + required: + - subject_keys + properties: + subject_keys: + type: array + items: + $ref: '#/components/schemas/Customers.UsageAttributionKey' + minItems: 0 + description: |- + The subjects that are attributed to the customer. + Can be empty when no usage event subjects are associated with the customer. + title: Subject Keys + description: |- + Mapping to attribute metered usage to the customer. + One customer can have zero or more subjects, + but one subject can only belong to one customer. + BillingEntitlementCheck: + type: object + required: + - type + - has_access + properties: + type: + allOf: + - $ref: '#/components/schemas/BillingEntitlementType' + description: The type of the entitlement. + readOnly: true + has_access: + type: boolean + description: Whether the customer has access to the feature. + readOnly: true + config: + type: string + format: json + description: Only available for static entitlements. + example: '{ "rateLimit": 100 }' + readOnly: true + description: Entitlement check result. + BillingEntitlementType: + type: string + enum: + - metered + - static + - boolean + description: The type of the entitlement. + BillingSubscription: + type: object + required: + - id + - name + - plan + properties: + id: + allOf: + - $ref: '#/components/schemas/ULID' + readOnly: true + name: + type: string + minLength: 1 + maxLength: 256 + description: |- + Display name of the resource. + + Between 1 and 256 characters. + description: + type: string + maxLength: 1024 + description: |- + Optional description of the resource. + + Maximum 1024 characters. + labels: + $ref: '#/components/schemas/Labels' + created_at: + allOf: + - $ref: '#/components/schemas/DateTime' + description: An ISO-8601 timestamp representation of entity creation date. + readOnly: true + updated_at: + allOf: + - $ref: '#/components/schemas/DateTime' + description: An ISO-8601 timestamp representation of entity last update date. + readOnly: true + deleted_at: + allOf: + - $ref: '#/components/schemas/DateTime' + description: An ISO-8601 timestamp representation of entity deletion date. + readOnly: true + plan: + allOf: + - $ref: '#/components/schemas/PlanReference' + description: Plan reference. + description: A subscription is a customer's subscription to a plan. + CountryCode: + type: string + minLength: 2 + maxLength: 2 + pattern: ^[A-Z]{2}$ + description: |- + [ISO 3166-1](https://www.iso.org/iso-3166-country-codes.html) alpha-2 country code. + Custom two-letter country codes are also supported for convenience. + example: US + CreateCustomerRequest: + type: object + required: + - name + - key + properties: + name: + type: string + minLength: 1 + maxLength: 256 + description: |- + Display name of the resource. + + Between 1 and 256 characters. + description: + type: string + maxLength: 1024 + description: |- + Optional description of the resource. + + Maximum 1024 characters. + labels: + $ref: '#/components/schemas/Labels' + key: + $ref: '#/components/schemas/ResourceKey' + usage_attribution: + allOf: + - $ref: '#/components/schemas/BillingCustomerUsageAttribution' + description: Mapping to attribute metered usage to the customer by the event subject. + title: Usage Attribution + primary_email: + type: string + description: The primary email address of the customer. + title: Primary Email + currency: + allOf: + - $ref: '#/components/schemas/CurrencyCode' + description: |- + Currency of the customer. + Used for billing, tax and invoicing. + title: Currency + billing_address: + allOf: + - $ref: '#/components/schemas/BillingAddress' + description: |- + The billing address of the customer. + Used for tax and invoicing. + title: Billing Address + description: Customer create request. + CreateMeterRequest: + type: object + required: + - name + - key + - aggregation + - event_type_filter + properties: + name: + type: string + minLength: 1 + maxLength: 256 + description: |- + Display name of the resource. + + Between 1 and 256 characters. + description: + type: string + maxLength: 1024 + description: |- + Optional description of the resource. + + Maximum 1024 characters. + labels: + $ref: '#/components/schemas/Labels' + key: + $ref: '#/components/schemas/ResourceKey' + aggregation: + allOf: + - $ref: '#/components/schemas/MeterAggregation' + description: The aggregation type to use for the meter. + event_type_filter: + type: string + minLength: 1 + description: The event type to include in the aggregation. + example: prompt + event_from: + allOf: + - $ref: '#/components/schemas/DateTime' + description: |- + The date since the meter should include events. + Useful to skip old events. + If not specified, all historical events are included. + value_property: + type: string + minLength: 1 + description: |- + JSONPath expression to extract the value from the ingested event's data property. + + The ingested value for sum, avg, min, and max aggregations is a number or a string that can be parsed to a number. + + For unique_count aggregation, the ingested value must be a string. For count aggregation the value_property is ignored. + example: $.tokens + dimensions: + type: object + additionalProperties: + type: string + description: |- + Named JSONPath expressions to extract the group by values from the event data. + + Keys must be unique and consist only alphanumeric and underscore characters. + example: + type: $.type + description: Meter create request. + CreateSubscriptionRequest: + type: object + required: + - name + - plan + properties: + name: + type: string + minLength: 1 + maxLength: 256 + description: |- + Display name of the resource. + + Between 1 and 256 characters. + description: + type: string + maxLength: 1024 + description: |- + Optional description of the resource. + + Maximum 1024 characters. + labels: + $ref: '#/components/schemas/Labels' + plan: + allOf: + - $ref: '#/components/schemas/PlanReference' + description: Plan reference. + description: Subscription create request. + CurrencyCode: + type: string + minLength: 3 + maxLength: 3 + pattern: ^[A-Z]{3}$ + description: |- + Three-letter [ISO4217](https://www.iso.org/iso-4217-currency-codes.html) currency code. + Custom three-letter currency codes are also supported for convenience. + example: USD + CustomerPagePaginatedResponse: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/BillingCustomer' + meta: + $ref: '#/components/schemas/PaginatedMeta' + description: Page paginated response. + Customers.UsageAttributionKey: + type: string + minLength: 1 DateTime: type: string format: date-time description: '[RFC3339](https://tools.ietf.org/html/rfc3339) formatted date-time string in UTC.' title: RFC3339 Date-Time example: '2023-01-01T01:01:01.001Z' + Meter: + type: object + required: + - id + - name + - key + - aggregation + - event_type_filter + properties: + id: + allOf: + - $ref: '#/components/schemas/ULID' + readOnly: true + name: + type: string + minLength: 1 + maxLength: 256 + description: |- + Display name of the resource. + + Between 1 and 256 characters. + description: + type: string + maxLength: 1024 + description: |- + Optional description of the resource. + + Maximum 1024 characters. + labels: + $ref: '#/components/schemas/Labels' + created_at: + allOf: + - $ref: '#/components/schemas/DateTime' + description: An ISO-8601 timestamp representation of entity creation date. + readOnly: true + updated_at: + allOf: + - $ref: '#/components/schemas/DateTime' + description: An ISO-8601 timestamp representation of entity last update date. + readOnly: true + deleted_at: + allOf: + - $ref: '#/components/schemas/DateTime' + description: An ISO-8601 timestamp representation of entity deletion date. + readOnly: true + key: + $ref: '#/components/schemas/ResourceKey' + aggregation: + allOf: + - $ref: '#/components/schemas/MeterAggregation' + description: The aggregation type to use for the meter. + event_type_filter: + type: string + minLength: 1 + description: The event type to include in the aggregation. + example: prompt + event_from: + allOf: + - $ref: '#/components/schemas/DateTime' + description: |- + The date since the meter should include events. + Useful to skip old events. + If not specified, all historical events are included. + value_property: + type: string + minLength: 1 + description: |- + JSONPath expression to extract the value from the ingested event's data property. + + The ingested value for sum, avg, min, and max aggregations is a number or a string that can be parsed to a number. + + For unique_count aggregation, the ingested value must be a string. For count aggregation the value_property is ignored. + example: $.tokens + dimensions: + type: object + additionalProperties: + type: string + description: |- + Named JSONPath expressions to extract the group by values from the event data. + + Keys must be unique and consist only alphanumeric and underscore characters. + example: + type: $.type + description: A meter is a configuration that defines how to match and aggregate events. + example: + id: 01G65Z755AFWAKHE12NY0CQ9FH + key: tokens_total + name: Tokens Total + description: AI Token Usage + aggregation: sum + event_type_filter: prompt + value_property: $.tokens + dimensions: + model: $.model + type: $.type + created_at: '2024-01-01T01:01:01.001Z' + updated_at: '2024-01-01T01:01:01.001Z' + MeterAggregation: + type: string + enum: + - sum + - count + - unique_count + - avg + - min + - max + - latest + description: The aggregation type to use for the meter. + MeterPagePaginatedResponse: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/Meter' + meta: + $ref: '#/components/schemas/PaginatedMeta' + description: Page paginated response. MeteringEvent: type: object required: @@ -149,6 +1038,145 @@ components: tokens: 100 model: gpt-4o type: input + PlanReference: + oneOf: + - $ref: '#/components/schemas/ULID' + - type: object + properties: + key: + $ref: '#/components/schemas/ResourceKey' + version: + type: integer + required: + - key + description: Reference to a plan by its id or key and version. + ResourceKey: + type: string + minLength: 1 + maxLength: 64 + pattern: ^[a-z0-9]+(?:_[a-z0-9]+)*$ + description: A key is a unique string that is used to identify a resource. + title: Resource Key + example: resource_key + SubscriptionPagePaginatedResponse: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/BillingSubscription' + meta: + $ref: '#/components/schemas/PaginatedMeta' + description: Page paginated response. + ULID: + type: string + pattern: ^[0-7][0-9A-HJKMNP-TV-Z]{25}$ + description: ULID (Universally Unique Lexicographically Sortable Identifier). + title: ULID + example: 01G65Z755AFWAKHE12NY0CQ9FH + UpsertCustomerRequest: + type: object + required: + - name + properties: + name: + type: string + minLength: 1 + maxLength: 256 + description: |- + Display name of the resource. + + Between 1 and 256 characters. + description: + type: string + maxLength: 1024 + description: |- + Optional description of the resource. + + Maximum 1024 characters. + labels: + $ref: '#/components/schemas/Labels' + usage_attribution: + allOf: + - $ref: '#/components/schemas/BillingCustomerUsageAttribution' + description: Mapping to attribute metered usage to the customer by the event subject. + title: Usage Attribution + primary_email: + type: string + description: The primary email address of the customer. + title: Primary Email + currency: + allOf: + - $ref: '#/components/schemas/CurrencyCode' + description: |- + Currency of the customer. + Used for billing, tax and invoicing. + title: Currency + billing_address: + allOf: + - $ref: '#/components/schemas/BillingAddress' + description: |- + The billing address of the customer. + Used for tax and invoicing. + title: Billing Address + description: Customer upsert request. + SortQuery: + title: SortQuery + type: string + example: created_at desc + description: | + The `asc` suffix is optional as the default sort order is ascending. + The `desc` suffix is used to specify a descending order. + Multiple sort attributes may be provided via a comma separated list. + JSONPath notation may be used to specify a sub-attribute (eg: 'foo.bar desc'). + Labels: + title: Labels + type: object + example: + env: test + maxProperties: 50 + description: | + Labels store metadata of an entity that can be used for filtering an entity list or for searching across entity types. + + Keys must be of length 1-63 characters, and cannot start with "kong", "konnect", "mesh", "kic", or "_". + additionalProperties: + type: string + pattern: ^[a-z0-9A-Z]{1}([a-z0-9A-Z-._]*[a-z0-9A-Z]+)?$ + minLength: 1 + maxLength: 63 + PageMeta: + type: object + description: Contains pagination query parameters and the total number of objects returned. + required: + - number + - size + - total + properties: + number: + type: number + example: 1 + x-speakeasy-terraform-ignore: true + size: + type: number + example: 10 + x-speakeasy-terraform-ignore: true + total: + type: number + example: 100 + x-speakeasy-terraform-ignore: true + PaginatedMeta: + type: object + title: PaginatedMeta + x-speakeasy-terraform-ignore: true + description: returns the pagination information + properties: + page: + $ref: '#/components/schemas/PageMeta' + required: + - page BaseError: type: object title: Error @@ -426,6 +1454,36 @@ components: example: kong:trace:1234567890 detail: example: Forbidden + NotFoundError: + allOf: + - $ref: '#/components/schemas/BaseError' + - type: object + properties: + status: + example: 404 + title: + example: Not Found + type: + example: https://httpstatuses.com/404 + instance: + example: kong:trace:1234567890 + detail: + example: Not found + ConflictError: + allOf: + - $ref: '#/components/schemas/BaseError' + - type: object + properties: + status: + example: 409 + title: + example: Conflict + type: + example: https://httpstatuses.com/409 + instance: + example: kong:trace:1234567890 + detail: + example: Conflict responses: BadRequest: description: Bad Request @@ -463,6 +1521,21 @@ components: application/problem+json: schema: $ref: '#/components/schemas/BaseError' + NotFound: + description: Not Found + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundError' + examples: + NotFoundExample: + $ref: '#/components/examples/NotFoundExample' + Conflict: + description: Conflict + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ConflictError' examples: UnauthorizedExample: value: @@ -476,3 +1549,9 @@ components: title: Forbidden instance: kong:trace:2723154947768991354 detail: You do not have permission to perform this action + NotFoundExample: + value: + status: 404 + title: Not Found + instance: kong:trace:6816496025408232265 + detail: Not Found diff --git a/api/v3/request/body.go b/api/v3/request/body.go new file mode 100644 index 000000000..53dd52ffc --- /dev/null +++ b/api/v3/request/body.go @@ -0,0 +1,24 @@ +package request + +import ( + "encoding/json" + "net/http" + + "github.com/openmeterio/openmeter/api/v3/apierrors" +) + +func ParseBody(r *http.Request, payload any) *apierrors.BaseAPIError { + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&payload); err != nil { + return apierrors.NewBadRequestError(r.Context(), err, + apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Reason: "unable to parse body", + Source: apierrors.InvalidParamSourceBody, + }, + }, + ) + } + + return nil +} diff --git a/api/v3/request/sort.go b/api/v3/request/sort.go new file mode 100644 index 000000000..b4e7d99e0 --- /dev/null +++ b/api/v3/request/sort.go @@ -0,0 +1,84 @@ +package request + +import ( + "errors" + "strings" + + "github.com/openmeterio/openmeter/pkg/sortx" +) + +type SortOrder string + +const ( + SortOrderAsc SortOrder = "asc" + SortOrderDesc SortOrder = "desc" +) + +var ( + ErrSortByInvalid = errors.New("invalid sort by") + ErrSortFieldRequired = errors.New("sort field is required") + ErrSortOrderInvalid = errors.New("sort order must be either asc or desc") + defaultOrder = SortOrderAsc +) + +func (s SortOrder) Validate() error { + if s != SortOrderAsc && s != SortOrderDesc { + return ErrSortOrderInvalid + } + + return nil +} + +type SortBy struct { + Field string + Order SortOrder +} + +func ParseSortBy(sort string) (*SortBy, error) { + var s SortBy + err := s.UnmarshalText([]byte(sort)) + if err != nil { + return nil, err + } + + return &s, nil +} + +func (s SortBy) Validate() error { + if s.Field == "" { + return ErrSortFieldRequired + } + + if err := s.Order.Validate(); err != nil { + return err + } + + return nil +} + +func (s *SortBy) UnmarshalText(text []byte) error { + parts := strings.Fields(string(text)) + if len(parts) == 0 { + return ErrSortByInvalid + } + if len(parts) > 2 { + return ErrSortByInvalid + } + + s.Field = parts[0] + if len(parts) == 2 { + s.Order = SortOrder(parts[1]) + } else { + s.Order = defaultOrder + } + + return s.Validate() +} + +func (s SortOrder) ToSortxOrder() sortx.Order { + if s == SortOrderAsc { + return sortx.OrderAsc + } + + return sortx.OrderDesc +} diff --git a/api/v3/response/cursorpagination.go b/api/v3/response/cursorpagination.go new file mode 100644 index 000000000..7b2f83dc5 --- /dev/null +++ b/api/v3/response/cursorpagination.go @@ -0,0 +1,63 @@ +package response + +import ( + "github.com/oapi-codegen/nullable" + "github.com/samber/lo" + + "github.com/openmeterio/openmeter/pkg/pagination/v2" +) + +// CursorMeta Pagination metadata. +type CursorMeta struct { + Page CursorMetaPage `json:"page"` +} + +// CursorMetaPage defines model for CursorMetaPage. +type CursorMetaPage struct { + // First cursor + First *string `json:"first,omitempty"` + + // Last cursor + Last *string `json:"last,omitempty"` + + // Next URI to the next page + Next nullable.Nullable[string] `json:"next"` + + // Previous URI to the previous page + Previous nullable.Nullable[string] `json:"previous"` + + // Size of the requested page + Size int `json:"size"` +} + +// CursorPaginationResponse represents the response structure for cursor-based pagination +type CursorPaginationResponse[T any] struct { + // The data returned + Data []T `json:"data"` + + Meta CursorMeta `json:"meta"` +} + +// NewCursorPaginationResponse creates a new pagination response from an ordered list of items. +// T must implement the Item interface for cursor generation. +func NewCursorPaginationResponse[T pagination.Item](items []T) CursorPaginationResponse[T] { + result := CursorPaginationResponse[T]{ + Data: items, + Meta: CursorMeta{ + Page: CursorMetaPage{ + Next: nullable.NewNullNullable[string](), + Previous: nullable.NewNullNullable[string](), + }, + }, + } + + // Generate first and last cursor from the first and last item if there are any items + if len(items) > 0 { + firstItem := items[0] + lastItem := items[len(items)-1] + result.Meta.Page.First = lo.ToPtr(firstItem.Cursor().Encode()) + result.Meta.Page.Last = lo.ToPtr(lastItem.Cursor().Encode()) + } + + return result +} diff --git a/api/v3/response/pagepagination.go b/api/v3/response/pagepagination.go new file mode 100644 index 000000000..9e684c0cb --- /dev/null +++ b/api/v3/response/pagepagination.go @@ -0,0 +1,25 @@ +package response + +type PagePaginationResponse[T any] struct { + Data []T `json:"data"` + Meta PageMeta `json:"meta"` +} + +type PageMeta struct { + Page PageMetaPage `json:"page"` +} + +type PageMetaPage struct { + Size int `json:"size"` + Number int `json:"number"` + Total *int `json:"total,omitempty"` +} + +func NewPagePaginationResponse[T any](items []T, page PageMetaPage) PagePaginationResponse[T] { + return PagePaginationResponse[T]{ + Data: items, + Meta: PageMeta{ + Page: page, + }, + } +} diff --git a/api/v3/server/routes.go b/api/v3/server/routes.go index 2c2bccc71..ac9048f2d 100644 --- a/api/v3/server/routes.go +++ b/api/v3/server/routes.go @@ -1,9 +1,63 @@ package server import ( + "errors" "net/http" + + api "github.com/openmeterio/openmeter/api/v3" + "github.com/openmeterio/openmeter/api/v3/apierrors" ) +// Meters + +func (s *Server) ListMeters(w http.ResponseWriter, r *http.Request, params api.ListMetersParams) { + apierrors.NewNotImplementedError(r.Context(), errors.New("not implemented")).HandleAPIError(w, r) +} + +func (s *Server) CreateMeter(w http.ResponseWriter, r *http.Request) { + apierrors.NewNotImplementedError(r.Context(), errors.New("not implemented")).HandleAPIError(w, r) +} + +func (s *Server) GetMeter(w http.ResponseWriter, r *http.Request, meterId api.ULID) { + apierrors.NewNotImplementedError(r.Context(), errors.New("not implemented")).HandleAPIError(w, r) +} + +// Events + func (s *Server) IngestMeteringEvents(w http.ResponseWriter, r *http.Request) { s.eventsHandler.IngestEvents().ServeHTTP(w, r) } + +// Customers + +func (s *Server) CreateCustomer(w http.ResponseWriter, r *http.Request) { + s.customersHandler.CreateCustomer().ServeHTTP(w, r) +} + +func (s *Server) GetCustomer(w http.ResponseWriter, r *http.Request, customerId api.ULID) { + s.customersHandler.GetCustomer().With(customerId).ServeHTTP(w, r) +} + +func (s *Server) ListCustomers(w http.ResponseWriter, r *http.Request, params api.ListCustomersParams) { + s.customersHandler.ListCustomers().With(params).ServeHTTP(w, r) +} + +func (s *Server) UpsertCustomer(w http.ResponseWriter, r *http.Request, customerId api.ULID) { + s.customersHandler.UpsertCustomer().With(customerId).ServeHTTP(w, r) +} + +func (s *Server) DeleteCustomer(w http.ResponseWriter, r *http.Request, customerId api.ULID) { + s.customersHandler.DeleteCustomer().With(customerId).ServeHTTP(w, r) +} + +func (s *Server) CheckCustomerFeatureAccess(w http.ResponseWriter, r *http.Request, customerId api.ULID, featureKey api.ResourceKey) { + apierrors.NewNotImplementedError(r.Context(), errors.New("not implemented")).HandleAPIError(w, r) +} + +func (s *Server) CreateCustomerSubscription(w http.ResponseWriter, r *http.Request, customerId api.ULID) { + apierrors.NewNotImplementedError(r.Context(), errors.New("not implemented")).HandleAPIError(w, r) +} + +func (s *Server) ListCustomerSubscriptions(w http.ResponseWriter, r *http.Request, customerId api.ULID, params api.ListCustomerSubscriptionsParams) { + apierrors.NewNotImplementedError(r.Context(), errors.New("not implemented")).HandleAPIError(w, r) +} diff --git a/api/v3/server/server.go b/api/v3/server/server.go index 8ce2e795e..5479630ed 100644 --- a/api/v3/server/server.go +++ b/api/v3/server/server.go @@ -14,9 +14,11 @@ import ( api "github.com/openmeterio/openmeter/api/v3" "github.com/openmeterio/openmeter/api/v3/apierrors" + customershandler "github.com/openmeterio/openmeter/api/v3/handlers/customers" eventshandler "github.com/openmeterio/openmeter/api/v3/handlers/events" "github.com/openmeterio/openmeter/api/v3/oasmiddleware" "github.com/openmeterio/openmeter/api/v3/render" + "github.com/openmeterio/openmeter/openmeter/customer" "github.com/openmeterio/openmeter/openmeter/ingest" "github.com/openmeterio/openmeter/openmeter/namespace/namespacedriver" "github.com/openmeterio/openmeter/pkg/errorsx" @@ -30,7 +32,8 @@ type Config struct { Middlewares []func(http.Handler) http.Handler // services - IngestService ingest.Service + IngestService ingest.Service + CustomerService customer.Service } func (c *Config) Validate() error { @@ -52,6 +55,10 @@ func (c *Config) Validate() error { errs = append(errs, errors.New("ingest service is required")) } + if c.CustomerService == nil { + errs = append(errs, errors.New("customer service is required")) + } + return errors.Join(errs...) } @@ -61,7 +68,8 @@ type Server struct { swagger *openapi3.T // handlers - eventsHandler eventshandler.Handler + eventsHandler eventshandler.Handler + customersHandler customershandler.Handler } // Make sure we conform to ServerInterface @@ -96,11 +104,13 @@ func NewServer(config *Config) (*Server, error) { } eventsHandler := eventshandler.New(resolveNamespace, config.IngestService, httptransport.WithErrorHandler(config.ErrorHandler)) + customersHandler := customershandler.New(resolveNamespace, config.CustomerService, httptransport.WithErrorHandler(config.ErrorHandler)) return &Server{ - Config: config, - swagger: swagger, - eventsHandler: eventsHandler, + Config: config, + swagger: swagger, + eventsHandler: eventsHandler, + customersHandler: customersHandler, }, nil } diff --git a/openmeter/customer/adapter/customer.go b/openmeter/customer/adapter/customer.go index 861f4c0ae..117391d05 100644 --- a/openmeter/customer/adapter/customer.go +++ b/openmeter/customer/adapter/customer.go @@ -9,7 +9,6 @@ import ( "entgo.io/ent/dialect/sql" "github.com/samber/lo" - "github.com/openmeterio/openmeter/api" "github.com/openmeterio/openmeter/openmeter/customer" entdb "github.com/openmeterio/openmeter/openmeter/ent/db" customerdb "github.com/openmeterio/openmeter/openmeter/ent/db/customer" @@ -87,11 +86,11 @@ func (a *adapter) ListCustomers(ctx context.Context, input customer.ListCustomer } switch input.OrderBy { - case api.CustomerOrderById: + case "id": query = query.Order(customerdb.ByID(order...)) - case api.CustomerOrderByCreatedAt: + case "created_at": query = query.Order(customerdb.ByCreatedAt(order...)) - case api.CustomerOrderByName: + case "name": fallthrough default: query = query.Order(customerdb.ByName(order...)) diff --git a/openmeter/customer/customer.go b/openmeter/customer/customer.go index 122b36497..138df1c21 100644 --- a/openmeter/customer/customer.go +++ b/openmeter/customer/customer.go @@ -7,7 +7,6 @@ import ( "github.com/samber/mo" - "github.com/openmeterio/openmeter/api" "github.com/openmeterio/openmeter/openmeter/streaming" "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/currencyx" @@ -301,7 +300,7 @@ type ListCustomersInput struct { IncludeDeleted bool // Order - OrderBy api.CustomerOrderBy + OrderBy string Order sortx.Order // Filters diff --git a/openmeter/customer/httpdriver/customer.go b/openmeter/customer/httpdriver/customer.go index a3b9832af..acdb7d3d2 100644 --- a/openmeter/customer/httpdriver/customer.go +++ b/openmeter/customer/httpdriver/customer.go @@ -48,7 +48,7 @@ func (h *handler) ListCustomers() ListCustomersHandler { }, // Order - OrderBy: defaultx.WithDefault(params.OrderBy, api.CustomerOrderByName), + OrderBy: string(defaultx.WithDefault(params.OrderBy, api.CustomerOrderByName)), Order: sortx.Order(defaultx.WithDefault(params.Order, api.SortOrderASC)), // Filters diff --git a/openmeter/server/server.go b/openmeter/server/server.go index 2555f890c..8063f33f6 100644 --- a/openmeter/server/server.go +++ b/openmeter/server/server.go @@ -93,6 +93,7 @@ func NewServer(config *Config) (*Server, error) { NamespaceDecoder: namespacedriver.StaticNamespaceDecoder(config.RouterConfig.NamespaceManager.GetDefaultNamespace()), ErrorHandler: config.RouterConfig.ErrorHandler, IngestService: config.RouterConfig.IngestService, + CustomerService: config.RouterConfig.Customer, Middlewares: []func(http.Handler) http.Handler{ middleware.RealIP, middleware.RequestID, diff --git a/test/customer/customer.go b/test/customer/customer.go index f4fbc76f4..92174be41 100644 --- a/test/customer/customer.go +++ b/test/customer/customer.go @@ -11,7 +11,6 @@ import ( "github.com/samber/lo" "github.com/stretchr/testify/require" - "github.com/openmeterio/openmeter/api" "github.com/openmeterio/openmeter/openmeter/customer" "github.com/openmeterio/openmeter/openmeter/entitlement" "github.com/openmeterio/openmeter/openmeter/productcatalog" @@ -597,8 +596,8 @@ func (s *CustomerHandlerTestSuite) TestList(ctx context.Context, t *testing.T) { list, err = service.ListCustomers(ctx, customer.ListCustomersInput{ Namespace: s.namespace, Page: page, - OrderBy: api.CustomerOrderByName, - Order: sortx.Order(api.SortOrderDESC), + OrderBy: "name", + Order: sortx.OrderDesc, }) require.NoError(t, err, "Listing customers with order by name must not return error")