|
8 | 8 | api "github.com/openmeterio/openmeter/api/v3" |
9 | 9 | "github.com/openmeterio/openmeter/openmeter/productcatalog" |
10 | 10 | "github.com/openmeterio/openmeter/openmeter/productcatalog/plan" |
| 11 | + "github.com/openmeterio/openmeter/openmeter/taxcode" |
11 | 12 | "github.com/openmeterio/openmeter/pkg/models" |
12 | 13 | ) |
13 | 14 |
|
@@ -66,13 +67,246 @@ func fromPlanPhase(p plan.Phase) (api.BillingPlanPhase, error) { |
66 | 67 | Duration: (*api.ISO8601Duration)(p.Duration.ISOStringPtrOrNil()), |
67 | 68 | Key: p.Key, |
68 | 69 | 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) |
71 | 80 | } |
72 | 81 |
|
73 | 82 | return phase, nil |
74 | 83 | } |
75 | 84 |
|
| 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 | + |
76 | 310 | func fromValidationErrors(issues models.ValidationIssues) *[]api.ProductCatalogValidationError { |
77 | 311 | if len(issues) == 0 { |
78 | 312 | return nil |
|
0 commit comments