Skip to content

Commit d10745e

Browse files
authored
Fix charge-backed discount invoice edits (#4603)
1 parent eaeb4d5 commit d10745e

64 files changed

Lines changed: 2850 additions & 1827 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/e2e/SKILL.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ You are helping the user write OpenMeter end-to-end tests that run against a liv
1212

1313
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.
1414

15+
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.
16+
1517
## Two styles, same package
1618

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

4951
## Shared conventions (both styles)
5052

53+
### Helper functions
54+
55+
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.
56+
5157
### Unique fixture keys
5258

5359
The docker-compose DB is shared across re-runs and parallel tests. Fixed keys collide. Always generate keys with a suffix:

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ In tests, prefer `t.Context()` when a `testing.T` or `testing.TB` is available i
147147

148148
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.
149149

150-
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.
150+
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.
151151

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

api/api.gen.go

Lines changed: 749 additions & 748 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/client/go/client.gen.go

Lines changed: 736 additions & 735 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/client/javascript/src/client/schemas.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7725,7 +7725,10 @@ export interface components {
77257725
* When null, the feature or service is free.
77267726
*/
77277727
price: components['schemas']['RateCardUsageBasedPrice'] | null
7728-
/** @description The discounts that are applied to the line. */
7728+
/**
7729+
* @deprecated
7730+
* @description The discounts that are applied to the line.
7731+
*/
77297732
discounts?: components['schemas']['BillingDiscounts']
77307733
}
77317734
/** @description InvoiceWorkflowInvoicingSettingsReplaceUpdate represents the update model for the invoicing settings of an invoice workflow. */

api/openapi.cloud.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19773,6 +19773,7 @@ components:
1977319773
allOf:
1977419774
- $ref: '#/components/schemas/BillingDiscounts'
1977519775
description: The discounts that are applied to the line.
19776+
deprecated: true
1977619777
description: InvoiceUsageBasedRateCard represents the rate card (intent) for an usage-based line.
1977719778
InvoiceWorkflowInvoicingSettingsReplaceUpdate:
1977819779
type: object

api/openapi.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20471,6 +20471,7 @@ components:
2047120471
allOf:
2047220472
- $ref: '#/components/schemas/BillingDiscounts'
2047320473
description: The discounts that are applied to the line.
20474+
deprecated: true
2047420475
description: InvoiceUsageBasedRateCard represents the rate card (intent) for an usage-based line.
2047520476
InvoiceWorkflowInvoicingSettingsReplaceUpdate:
2047620477
type: object

api/spec/packages/legacy/src/billing/invoices/invoice.tsp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,7 @@ model InvoiceUsageBasedRateCard {
656656
/**
657657
* The discounts that are applied to the line.
658658
*/
659+
#deprecated "V1 invoice responses do not expose persisted rate card discounts"
659660
@visibility(Lifecycle.Read, Lifecycle.Create, Lifecycle.Update)
660661
discounts?: BillingDiscounts;
661662
}

api/v3/handlers/customers/charges/convert.go

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ func toAPIBillingPriceTier(t productcatalog.PriceTier, _ int) api.BillingPriceTi
298298
}
299299

300300
// convertFlatFeeDiscounts maps the optional percentage discount to the anonymous API struct.
301-
func convertFlatFeeDiscounts(pd *productcatalog.PercentageDiscount) *api.BillingChargeFlatFeeDiscounts {
301+
func convertFlatFeeDiscounts(pd *billing.PercentageDiscount) *api.BillingChargeFlatFeeDiscounts {
302302
if pd == nil {
303303
return nil
304304
}
@@ -307,7 +307,7 @@ func convertFlatFeeDiscounts(pd *productcatalog.PercentageDiscount) *api.Billing
307307
}
308308

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

479-
var discount *productcatalog.PercentageDiscount
479+
var discount *billing.PercentageDiscount
480480
if flatFee.Discounts != nil && flatFee.Discounts.Percentage != nil {
481-
discount = &productcatalog.PercentageDiscount{
482-
Percentage: models.NewPercentage(float64(lo.FromPtr(flatFee.Discounts.Percentage))),
483-
}
481+
discount = billing.Discounts{
482+
Percentage: &billing.PercentageDiscount{
483+
PercentageDiscount: productcatalog.PercentageDiscount{
484+
Percentage: models.NewPercentage(float64(lo.FromPtr(flatFee.Discounts.Percentage))),
485+
},
486+
},
487+
}.UpsertCorrelationIDs().Percentage
484488
}
485489

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

540-
var discounts productcatalog.Discounts
544+
var discounts billing.Discounts
541545
if usageBasedFee.Discounts != nil {
542546
if usageBasedFee.Discounts.Percentage != nil {
543-
discounts.Percentage = &productcatalog.PercentageDiscount{
544-
Percentage: models.NewPercentage(float64(lo.FromPtr(usageBasedFee.Discounts.Percentage))),
547+
discounts.Percentage = &billing.PercentageDiscount{
548+
PercentageDiscount: productcatalog.PercentageDiscount{
549+
Percentage: models.NewPercentage(float64(lo.FromPtr(usageBasedFee.Discounts.Percentage))),
550+
},
545551
}
546552
}
547553
if usageBasedFee.Discounts.Usage != nil {
548554
quantity, err := alpacadecimal.NewFromString(lo.FromPtr(usageBasedFee.Discounts.Usage))
549555
if err != nil {
550556
return zero, fmt.Errorf("invalid usage discount quantity: %w", err)
551557
}
552-
discounts.Usage = &productcatalog.UsageDiscount{
553-
Quantity: quantity,
558+
discounts.Usage = &billing.UsageDiscount{
559+
UsageDiscount: productcatalog.UsageDiscount{
560+
Quantity: quantity,
561+
},
554562
}
555563
}
564+
discounts = discounts.UpsertCorrelationIDs()
556565
}
557566

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

0 commit comments

Comments
 (0)