Skip to content

Commit dc4e109

Browse files
committed
feat(plan): update divisible periods to allow hours and validate plan billing cadence to be at least 1 month
1 parent 5483c81 commit dc4e109

File tree

4 files changed

+312
-12
lines changed

4 files changed

+312
-12
lines changed

openmeter/productcatalog/errors.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,15 @@ var ErrIDEmpty = models.NewValidationIssue(
389389

390390
// Plan errors
391391

392+
const ErrCodePlanBillingCadenceInvalid models.ErrorCode = "plan_billing_cadence_invalid"
393+
394+
var ErrPlanBillingCadenceInvalid = models.NewValidationIssue(
395+
ErrCodePlanBillingCadenceInvalid,
396+
"billing cadence must be at least 1 month",
397+
models.WithFieldString("billingCadence"),
398+
models.WithCriticalSeverity(),
399+
)
400+
392401
const ErrCodePlanPhaseWithNegativeDuration models.ErrorCode = "plan_phase_with_negative_duration"
393402

394403
var ErrPlanPhaseWithNegativeDuration = models.NewValidationIssue(

openmeter/productcatalog/plan.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,20 @@ func ValidatePlanPhases() models.ValidatorFunc[Plan] {
103103
}
104104
}
105105

106+
// ValidatePlanMinimumBillingCadence validates that the billing cadence of the plan is at least a month.
107+
func ValidatePlanMinimumBillingCadence() models.ValidatorFunc[Plan] {
108+
return func(p Plan) error {
109+
var errs []error
110+
111+
// Billing Cadence has to be at least 1 month
112+
if p.BillingCadence.Compare(isodate.NewPeriod(0, 1, 0, 0, 0, 0, 0)) < 0 {
113+
errs = append(errs, ErrPlanBillingCadenceInvalid)
114+
}
115+
116+
return errors.Join(errs...)
117+
}
118+
}
119+
106120
// ValidatePlanHasAlignedBillingCadences validates that the billing cadence of the plan is aligned with the billing cadence of the rate cards.
107121
func ValidatePlanHasAlignedBillingCadences() models.ValidatorFunc[Plan] {
108122
return func(p Plan) error {
@@ -162,6 +176,7 @@ func (p Plan) Validate() error {
162176
return p.ValidateWith(
163177
ValidatePlanMeta(),
164178
ValidatePlanPhases(),
179+
ValidatePlanMinimumBillingCadence(),
165180
ValidatePlanHasAlignedBillingCadences(),
166181
)
167182
}

pkg/isodate/date.go

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package isodate
44

55
import (
6+
"fmt"
67
"testing"
78
"time"
89

@@ -58,17 +59,21 @@ func (p Period) Simplify(exact bool) Period {
5859
return Period{p.Period.Simplify(exact)}
5960
}
6061

61-
// InDays returns the value of the period in days
62-
func (p Period) InDays(daysInMonth int) (decimal.Decimal, error) {
63-
years, err := p.Period.YearsDecimal().Mul(decimal.MustNew(int64(daysInMonth*12), 0))
62+
// InHours returns the value of the period in hours
63+
func (p Period) InHours(daysInMonth int) (decimal.Decimal, error) {
64+
years, err := p.Period.YearsDecimal().Mul(decimal.MustNew(int64(daysInMonth*12*24), 0))
6465
if err != nil {
6566
return decimal.Zero, err
6667
}
67-
months, err := p.Period.MonthsDecimal().Mul(decimal.MustNew(int64(daysInMonth), 0))
68+
months, err := p.Period.MonthsDecimal().Mul(decimal.MustNew(int64(daysInMonth*24), 0))
6869
if err != nil {
6970
return decimal.Zero, err
7071
}
71-
weeks, err := p.Period.WeeksDecimal().Mul(decimal.MustNew(7, 0))
72+
weeks, err := p.Period.WeeksDecimal().Mul(decimal.MustNew(7*24, 0))
73+
if err != nil {
74+
return decimal.Zero, err
75+
}
76+
days, err := p.Period.DaysDecimal().Mul(decimal.MustNew(24, 0))
7277
if err != nil {
7378
return decimal.Zero, err
7479
}
@@ -81,7 +86,11 @@ func (p Period) InDays(daysInMonth int) (decimal.Decimal, error) {
8186
if err != nil {
8287
return decimal.Zero, err
8388
}
84-
v, err = v.Add(p.Period.DaysDecimal())
89+
v, err = v.Add(days)
90+
if err != nil {
91+
return decimal.Zero, err
92+
}
93+
v, err = v.Add(p.Period.HoursDecimal())
8594
if err != nil {
8695
return decimal.Zero, err
8796
}
@@ -124,18 +133,22 @@ func (larger Period) DivisibleBy(smaller Period) (bool, error) {
124133
return false, nil
125134
}
126135

136+
if l.Minutes() != 0 || l.Seconds() != 0 || s.Minutes() != 0 || s.Seconds() != 0 {
137+
return false, fmt.Errorf("divisible periods must be whole numbers of hours")
138+
}
139+
127140
testDaysInMonth := []int{28, 29, 30, 31}
128141
for _, daysInMonth := range testDaysInMonth {
129-
// replace months with days
130-
ldays, err := l.InDays(daysInMonth)
142+
// get periods in hours
143+
lh, err := l.InHours(daysInMonth)
131144
if err != nil {
132145
return false, err
133146
}
134-
sdays, err := s.InDays(daysInMonth)
147+
sh, err := s.InHours(daysInMonth)
135148
if err != nil {
136149
return false, err
137150
}
138-
if _, r, err := ldays.QuoRem(sdays); err != nil || !r.IsZero() {
151+
if _, r, err := lh.QuoRem(sh); err != nil || !r.IsZero() {
139152
return false, err
140153
}
141154
}

0 commit comments

Comments
 (0)