Skip to content

Commit a93ddb7

Browse files
committed
feat(api): added ratecard mapping helpers
1 parent 6833de8 commit a93ddb7

2 files changed

Lines changed: 623 additions & 2 deletions

File tree

api/v3/handlers/plans/convert.go

Lines changed: 236 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
api "github.com/openmeterio/openmeter/api/v3"
99
"github.com/openmeterio/openmeter/openmeter/productcatalog"
1010
"github.com/openmeterio/openmeter/openmeter/productcatalog/plan"
11+
"github.com/openmeterio/openmeter/openmeter/taxcode"
1112
"github.com/openmeterio/openmeter/pkg/models"
1213
)
1314

@@ -66,13 +67,246 @@ func fromPlanPhase(p plan.Phase) (api.BillingPlanPhase, error) {
6667
Duration: (*api.ISO8601Duration)(p.Duration.ISOStringPtrOrNil()),
6768
Key: p.Key,
6869
Name: p.Name,
69-
// TODO: convert rate cards to BillingRateCard
70-
RateCards: make([]api.BillingRateCard, 0, len(p.RateCards)),
70+
RateCards: make([]api.BillingRateCard, 0, len(p.RateCards)),
71+
}
72+
73+
for _, rc := range p.RateCards {
74+
billingRC, err := fromRateCard(rc)
75+
if err != nil {
76+
return phase, fmt.Errorf("failed to convert rate card %q: %w", rc.Key(), err)
77+
}
78+
79+
phase.RateCards = append(phase.RateCards, billingRC)
7180
}
7281

7382
return phase, nil
7483
}
7584

85+
func fromRateCard(rc productcatalog.RateCard) (api.BillingRateCard, error) {
86+
meta := rc.AsMeta()
87+
88+
result := api.BillingRateCard{
89+
Key: meta.Key,
90+
Name: meta.Name,
91+
Description: meta.Description,
92+
Discounts: fromBillingDiscounts(meta.Discounts),
93+
TaxConfig: fromBillingTaxConfig(meta.TaxConfig, meta.TaxCode),
94+
}
95+
96+
if meta.FeatureID != nil {
97+
result.Feature = &api.FeatureReferenceItem{
98+
Id: *meta.FeatureID,
99+
}
100+
}
101+
102+
switch rc.Type() {
103+
case productcatalog.FlatFeeRateCardType:
104+
if bc := rc.GetBillingCadence(); bc != nil {
105+
result.BillingCadence = lo.ToPtr(api.ISO8601Duration(bc.ISOString().String()))
106+
}
107+
108+
if meta.Price != nil {
109+
flatPrice, err := meta.Price.AsFlat()
110+
if err != nil {
111+
return result, fmt.Errorf("failed to read flat price: %w", err)
112+
}
113+
114+
result.PaymentTerm = lo.ToPtr(api.BillingPricePaymentTerm(flatPrice.PaymentTerm))
115+
}
116+
117+
case productcatalog.UsageBasedRateCardType:
118+
bc := rc.GetBillingCadence()
119+
if bc == nil {
120+
return result, fmt.Errorf("usage-based rate card %q missing billing cadence", meta.Key)
121+
}
122+
123+
result.BillingCadence = lo.ToPtr(api.ISO8601Duration(bc.ISOString().String()))
124+
125+
if meta.Price != nil {
126+
result.Commitments = fromBillingCommitments(meta.Price.GetCommitments())
127+
}
128+
129+
default:
130+
return result, fmt.Errorf("unknown rate card type: %s", rc.Type())
131+
}
132+
133+
price, err := fromBillingPrice(meta.Price)
134+
if err != nil {
135+
return result, fmt.Errorf("failed to convert price: %w", err)
136+
}
137+
138+
result.Price = price
139+
140+
return result, nil
141+
}
142+
143+
func fromBillingPrice(p *productcatalog.Price) (api.BillingPrice, error) {
144+
var result api.BillingPrice
145+
146+
if p == nil {
147+
if err := result.FromBillingPriceFree(api.BillingPriceFree{
148+
Type: api.BillingPriceFreeType("free"),
149+
}); err != nil {
150+
return result, fmt.Errorf("failed to set free price: %w", err)
151+
}
152+
153+
return result, nil
154+
}
155+
156+
switch p.Type() {
157+
case productcatalog.FlatPriceType:
158+
flat, err := p.AsFlat()
159+
if err != nil {
160+
return result, fmt.Errorf("failed to read flat price: %w", err)
161+
}
162+
163+
if err = result.FromBillingPriceFlat(api.BillingPriceFlat{
164+
Amount: api.Numeric(flat.Amount.String()),
165+
Type: api.BillingPriceFlatType("flat"),
166+
}); err != nil {
167+
return result, fmt.Errorf("failed to set flat price: %w", err)
168+
}
169+
170+
case productcatalog.UnitPriceType:
171+
unit, err := p.AsUnit()
172+
if err != nil {
173+
return result, fmt.Errorf("failed to read unit price: %w", err)
174+
}
175+
176+
if err = result.FromBillingPriceUnit(api.BillingPriceUnit{
177+
Amount: api.Numeric(unit.Amount.String()),
178+
Type: api.BillingPriceUnitType("unit"),
179+
}); err != nil {
180+
return result, fmt.Errorf("failed to set unit price: %w", err)
181+
}
182+
183+
case productcatalog.TieredPriceType:
184+
tiered, err := p.AsTiered()
185+
if err != nil {
186+
return result, fmt.Errorf("failed to read tiered price: %w", err)
187+
}
188+
189+
tiers := fromBillingPriceTiers(tiered.Tiers)
190+
191+
switch tiered.Mode {
192+
case productcatalog.GraduatedTieredPrice:
193+
if err = result.FromBillingPriceGraduated(api.BillingPriceGraduated{
194+
Tiers: tiers,
195+
Type: api.BillingPriceGraduatedType("graduated"),
196+
}); err != nil {
197+
return result, fmt.Errorf("failed to set graduated price: %w", err)
198+
}
199+
200+
case productcatalog.VolumeTieredPrice:
201+
if err = result.FromBillingPriceVolume(api.BillingPriceVolume{
202+
Tiers: tiers,
203+
Type: api.BillingPriceVolumeType("volume"),
204+
}); err != nil {
205+
return result, fmt.Errorf("failed to set volume price: %w", err)
206+
}
207+
208+
default:
209+
return result, fmt.Errorf("unknown tiered price mode: %s", tiered.Mode)
210+
}
211+
212+
case productcatalog.DynamicPriceType:
213+
return result, fmt.Errorf("dynamic price is not supported in v3 API")
214+
215+
case productcatalog.PackagePriceType:
216+
return result, fmt.Errorf("package price is not supported in v3 API")
217+
218+
default:
219+
return result, fmt.Errorf("unknown price type: %s", p.Type())
220+
}
221+
222+
return result, nil
223+
}
224+
225+
func fromBillingPriceTiers(tiers []productcatalog.PriceTier) []api.BillingPriceTier {
226+
result := make([]api.BillingPriceTier, 0, len(tiers))
227+
228+
for _, t := range tiers {
229+
tier := api.BillingPriceTier{}
230+
231+
if t.UpToAmount != nil {
232+
tier.UpToAmount = lo.ToPtr(api.Numeric(t.UpToAmount.String()))
233+
}
234+
235+
if t.FlatPrice != nil {
236+
tier.FlatPrice = &api.BillingPriceFlat{
237+
Amount: api.Numeric(t.FlatPrice.Amount.String()),
238+
Type: api.BillingPriceFlatType("flat"),
239+
}
240+
}
241+
242+
if t.UnitPrice != nil {
243+
tier.UnitPrice = &api.BillingPriceUnit{
244+
Amount: api.Numeric(t.UnitPrice.Amount.String()),
245+
Type: api.BillingPriceUnitType("unit"),
246+
}
247+
}
248+
249+
result = append(result, tier)
250+
}
251+
252+
return result
253+
}
254+
255+
func fromBillingTaxConfig(c *productcatalog.TaxConfig, tc *taxcode.TaxCode) *api.BillingRateCardTaxConfig {
256+
if c == nil || tc == nil {
257+
return nil
258+
}
259+
260+
result := &api.BillingRateCardTaxConfig{
261+
Code: api.TaxCodeReferenceItem{
262+
Id: tc.ID,
263+
},
264+
}
265+
266+
if c.Behavior != nil {
267+
result.Behavior = lo.ToPtr(api.BillingTaxBehavior(*c.Behavior))
268+
}
269+
270+
return result
271+
}
272+
273+
func fromBillingDiscounts(d productcatalog.Discounts) *api.BillingRateCardDiscounts {
274+
if d.Percentage == nil && d.Usage == nil {
275+
return nil
276+
}
277+
278+
result := &api.BillingRateCardDiscounts{}
279+
280+
if d.Percentage != nil {
281+
pct := float32(d.Percentage.Percentage.InexactFloat64())
282+
result.Percentage = &pct
283+
}
284+
285+
if d.Usage != nil {
286+
result.Usage = lo.ToPtr(api.Numeric(d.Usage.Quantity.String()))
287+
}
288+
289+
return result
290+
}
291+
292+
func fromBillingCommitments(c productcatalog.Commitments) *api.BillingSpendCommitments {
293+
if c.MinimumAmount == nil && c.MaximumAmount == nil {
294+
return nil
295+
}
296+
297+
result := &api.BillingSpendCommitments{}
298+
299+
if c.MinimumAmount != nil {
300+
result.MinimumAmount = lo.ToPtr(api.Numeric(c.MinimumAmount.String()))
301+
}
302+
303+
if c.MaximumAmount != nil {
304+
result.MaximumAmount = lo.ToPtr(api.Numeric(c.MaximumAmount.String()))
305+
}
306+
307+
return result
308+
}
309+
76310
func fromValidationErrors(issues models.ValidationIssues) *[]api.ProductCatalogValidationError {
77311
if len(issues) == 0 {
78312
return nil

0 commit comments

Comments
 (0)