Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/skills/e2e/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ You are helping the user write OpenMeter end-to-end tests that run against a liv

This is the black-box layer. Unlike the `/test` skill (which covers in-process unit/integration/service tests using `testutils.TestEnv` + `testutils.InitPostgresDB`), e2e tests hit the wire format: JSON in, JSON out, status codes, problem+json error bodies. Use this skill when the value of the test comes from exercising the HTTP contract, the OpenAPI binder, or cross-service behavior.

General test style from `AGENTS.md` and the `/test` skill still applies. Keep this skill as e2e-specific guidance, not a parallel set of test conventions.

## Two styles, same package

Both live in `e2e/` and share the build tag, environment, and skip-when-unset convention. Pick by what the endpoint under test offers:
Expand Down Expand Up @@ -48,6 +50,10 @@ Notes:

## Shared conventions (both styles)

### Helper functions

Prefer a single explicit test body over single-use setup wrappers. Do not add helper functions for e2e setup, conversion, or assertions unless the helper is used by at least two tests in the same package or its name captures non-obvious domain semantics that would otherwise be easy to miss.

### Unique fixture keys

The docker-compose DB is shared across re-runs and parallel tests. Fixed keys collide. Always generate keys with a suffix:
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ In tests, prefer `t.Context()` when a `testing.T` or `testing.TB` is available i

Prefer one consistent test harness style over mixed ad hoc structures. Use production-backed paths, such as rating-backed or service-backed fixtures, when the real path can express the scenario; keep hand-assembled fixtures for cases that cannot be produced realistically. If a behavior is a suite-wide rule, hardcode it into the shared harness instead of exposing it as per-test knobs.

Avoid redundant test helpers and duplicate setup paths. Prefer parameterizing one helper over maintaining near-identical helpers, use literal helper names that state exactly what they do, and inline two-line single-use helpers that do not need `defer`. Clean up dead test helpers immediately after refactors.
Avoid redundant test helpers and duplicate setup paths. Prefer parameterizing one helper over maintaining near-identical helpers, use literal helper names that state exactly what they do, and inline single-use helpers that only wrap setup, conversion, or assertions even when the test becomes longer. Add a test helper only when it is used by at least two tests in the same package or when the helper name captures non-obvious domain semantics that would otherwise be easy to miss. Clean up dead test helpers immediately after refactors.

For service and lifecycle subtests, start each subtest body with concise intent comments when the scenario is non-trivial:

Expand Down
1,497 changes: 749 additions & 748 deletions api/api.gen.go

Large diffs are not rendered by default.

1,471 changes: 736 additions & 735 deletions api/client/go/client.gen.go

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion api/client/javascript/src/client/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7725,7 +7725,10 @@ export interface components {
* When null, the feature or service is free.
*/
price: components['schemas']['RateCardUsageBasedPrice'] | null
/** @description The discounts that are applied to the line. */
/**
* @deprecated
* @description The discounts that are applied to the line.
*/
discounts?: components['schemas']['BillingDiscounts']
}
/** @description InvoiceWorkflowInvoicingSettingsReplaceUpdate represents the update model for the invoicing settings of an invoice workflow. */
Expand Down
1 change: 1 addition & 0 deletions api/openapi.cloud.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19773,6 +19773,7 @@ components:
allOf:
- $ref: '#/components/schemas/BillingDiscounts'
description: The discounts that are applied to the line.
deprecated: true
description: InvoiceUsageBasedRateCard represents the rate card (intent) for an usage-based line.
InvoiceWorkflowInvoicingSettingsReplaceUpdate:
type: object
Expand Down
1 change: 1 addition & 0 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20471,6 +20471,7 @@ components:
allOf:
- $ref: '#/components/schemas/BillingDiscounts'
description: The discounts that are applied to the line.
deprecated: true
description: InvoiceUsageBasedRateCard represents the rate card (intent) for an usage-based line.
InvoiceWorkflowInvoicingSettingsReplaceUpdate:
type: object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,7 @@ model InvoiceUsageBasedRateCard {
/**
* The discounts that are applied to the line.
*/
#deprecated "V1 invoice responses do not expose persisted rate card discounts"
@visibility(Lifecycle.Read, Lifecycle.Create, Lifecycle.Update)
discounts?: BillingDiscounts;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Expand Down
31 changes: 20 additions & 11 deletions api/v3/handlers/customers/charges/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ func toAPIBillingPriceTier(t productcatalog.PriceTier, _ int) api.BillingPriceTi
}

// convertFlatFeeDiscounts maps the optional percentage discount to the anonymous API struct.
func convertFlatFeeDiscounts(pd *productcatalog.PercentageDiscount) *api.BillingChargeFlatFeeDiscounts {
func convertFlatFeeDiscounts(pd *billing.PercentageDiscount) *api.BillingChargeFlatFeeDiscounts {
if pd == nil {
return nil
}
Expand All @@ -307,7 +307,7 @@ func convertFlatFeeDiscounts(pd *productcatalog.PercentageDiscount) *api.Billing
}

// convertUsageBasedDiscounts maps usage-based discounts to the API type.
func convertUsageBasedDiscounts(d productcatalog.Discounts) *api.BillingRateCardDiscounts {
func convertUsageBasedDiscounts(d billing.Discounts) *api.BillingRateCardDiscounts {
if d.Percentage == nil && d.Usage == nil {
return nil
}
Expand Down Expand Up @@ -476,11 +476,15 @@ func convertFlatFeeChargeAPIToIntent(customerID string, flatFee api.CreateCharge
metadata = models.Metadata(*flatFee.Labels)
}

var discount *productcatalog.PercentageDiscount
var discount *billing.PercentageDiscount
if flatFee.Discounts != nil && flatFee.Discounts.Percentage != nil {
discount = &productcatalog.PercentageDiscount{
Percentage: models.NewPercentage(float64(lo.FromPtr(flatFee.Discounts.Percentage))),
}
discount = billing.Discounts{
Percentage: &billing.PercentageDiscount{
PercentageDiscount: productcatalog.PercentageDiscount{
Percentage: models.NewPercentage(float64(lo.FromPtr(flatFee.Discounts.Percentage))),
},
},
}.UpsertCorrelationIDs().Percentage
}

var proRating productcatalog.ProRatingConfig
Expand Down Expand Up @@ -537,22 +541,27 @@ func convertUsageBaseChargeAPIToIntent(customerID string, usageBasedFee api.Crea
metadata = models.Metadata(*usageBasedFee.Labels)
}

var discounts productcatalog.Discounts
var discounts billing.Discounts
if usageBasedFee.Discounts != nil {
if usageBasedFee.Discounts.Percentage != nil {
discounts.Percentage = &productcatalog.PercentageDiscount{
Percentage: models.NewPercentage(float64(lo.FromPtr(usageBasedFee.Discounts.Percentage))),
discounts.Percentage = &billing.PercentageDiscount{
PercentageDiscount: productcatalog.PercentageDiscount{
Percentage: models.NewPercentage(float64(lo.FromPtr(usageBasedFee.Discounts.Percentage))),
},
}
}
if usageBasedFee.Discounts.Usage != nil {
quantity, err := alpacadecimal.NewFromString(lo.FromPtr(usageBasedFee.Discounts.Usage))
if err != nil {
return zero, fmt.Errorf("invalid usage discount quantity: %w", err)
}
discounts.Usage = &productcatalog.UsageDiscount{
Quantity: quantity,
discounts.Usage = &billing.UsageDiscount{
UsageDiscount: productcatalog.UsageDiscount{
Quantity: quantity,
},
}
}
discounts = discounts.UpsertCorrelationIDs()
}

price, err := plans.FromAPIBillingPrice(usageBasedFee.Price, lo.ToPtr(api.BillingPricePaymentTermInArrears))
Expand Down
Loading
Loading