Skip to content
This repository has been archived by the owner on Jan 2, 2024. It is now read-only.

control: support division transform #268

Merged
merged 1 commit into from
Mar 2, 2023
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
7 changes: 6 additions & 1 deletion api/apitypes/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,18 @@ func (t *Tier) UnmarshalJSON(data []byte) error {
return nil
}

type Divide struct {
By int `json:"by,omitempty"`
Rounding string `json:"rounding,omitempty"`
}

type Feature struct {
Title string `json:"title,omitempty"`
Base float64 `json:"base,omitempty"`
Mode string `json:"mode,omitempty"`
Aggregate string `json:"aggregate,omitempty"`
Tiers []Tier `json:"tiers,omitempty"`
PermLink string `json:"permLink,omitempty"`
Divide *Divide `json:"divide,omitempty"`
}

type Plan struct {
Expand Down
18 changes: 17 additions & 1 deletion api/materialize/views.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ func FromPricingHuJSON(data []byte) (fs []control.Feature, err error) {
for plan, p := range m.Plans {
for feature, f := range p.Features {
fn := feature.WithPlan(plan)

divide := values.Coalesce(f.Divide, &apitypes.Divide{})
ff := control.Feature{
FeaturePlan: fn,

Expand All @@ -52,6 +54,9 @@ func FromPricingHuJSON(data []byte) (fs []control.Feature, err error) {

Mode: values.Coalesce(f.Mode, "graduated"),
Aggregate: values.Coalesce(f.Aggregate, "sum"),

TransformDenominator: divide.By,
TransformRoundUp: divide.Rounding == "up",
}

if len(f.Tiers) > 0 {
Expand Down Expand Up @@ -99,13 +104,24 @@ func ToPricingJSON(fs []control.Feature) ([]byte, error) {
}
}

p.Features[f.FeaturePlan.Name()] = apitypes.Feature{
af := apitypes.Feature{
Title: values.ZeroIf(f.Title, f.FeaturePlan.String()),
Base: f.Base,
Mode: values.ZeroIf(f.Mode, "graduated"),
Aggregate: values.ZeroIf(f.Aggregate, "sum"),
Tiers: tiers,
}
if f.TransformDenominator != 0 {
var round string
if f.TransformRoundUp {
round = "up"
}
af.Divide = &apitypes.Divide{
By: f.TransformDenominator,
Rounding: round,
}
}
p.Features[f.FeaturePlan.Name()] = af
m.Plans[f.Plan()] = p
}
return json.MarshalIndent(m, "", " ")
Expand Down
17 changes: 17 additions & 0 deletions api/materialize/views_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ func TestPricingHuJSON(t *testing.T) {
"feature:base": {
"base": 100,
},
"feature:xform": {
"divide": {"by": 100, "rounding": "up"},
}
},
},
}
Expand Down Expand Up @@ -74,6 +77,17 @@ func TestPricingHuJSON(t *testing.T) {
{Upto: tier.Inf, Price: 50, Base: 0},
},
},
{
PlanTitle: "Just an example plan to show off features part duex",
Title: "feature:xform@plan:example@2",
FeaturePlan: refs.MustParseFeaturePlan("feature:xform@plan:example@2"),
Currency: "usd",
Interval: "@monthly",
Mode: "graduated", // defaults
Aggregate: "sum",
TransformDenominator: 100,
TransformRoundUp: true,
},
}

diff.Test(t, t.Errorf, got, want)
Expand Down Expand Up @@ -104,6 +118,9 @@ func TestPricingHuJSON(t *testing.T) {
"features": {
"feature:base": {
"base": 100,
},
"feature:xform": {
"divide": {"by": 100, "rounding": "up"},
}
}
}
Expand Down
36 changes: 27 additions & 9 deletions control/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ type Feature struct {

// ReportID is the ID for reporting usage to the billing provider.
ReportID string

TransformDenominator int // the denominator for transforming usage
TransformRoundUp bool // whether to round up transformed usage; otherwise round down
}

// TODO(bmizerany): remove FQN and replace with simply adding the version to
Expand Down Expand Up @@ -276,6 +279,15 @@ func (c *Client) pushFeature(ctx context.Context, f Feature) (providerID string,
data.Set("recurring", "interval", interval)
data.Set("recurring", "interval_count", 1) // TODO: support user-defined interval count

if f.TransformDenominator != 0 {
round := "down"
if f.TransformRoundUp {
round = "up"
}
data.Set("transform_quantity", "divide_by", f.TransformDenominator)
data.Set("transform_quantity", "round", round)
}

numTiers := len(f.Tiers)
switch {
case numTiers == 0:
Expand Down Expand Up @@ -357,19 +369,25 @@ type stripePrice struct {
PriceDecimal float64 `json:"unit_amount_decimal,string"`
Base int `json:"flat_amount"`
}
Currency string
Currency string
TransformQuantity struct {
DivideBy int `json:"divide_by"`
Round string `json:"round"`
} `json:"transform_quantity"`
}

func stripePriceToFeature(p stripePrice) Feature {
f := Feature{
ProviderID: p.ProviderID(),
PlanTitle: p.Metadata.PlanTitle,
FeaturePlan: p.Metadata.Feature,
Title: p.Metadata.Title,
Currency: p.Currency,
Interval: intervalFromStripe[p.Recurring.Interval],
Mode: p.TiersMode,
Aggregate: aggregateFromStripe[p.Recurring.AggregateUsage],
ProviderID: p.ProviderID(),
PlanTitle: p.Metadata.PlanTitle,
FeaturePlan: p.Metadata.Feature,
Title: p.Metadata.Title,
Currency: p.Currency,
Interval: intervalFromStripe[p.Recurring.Interval],
Mode: p.TiersMode,
Aggregate: aggregateFromStripe[p.Recurring.AggregateUsage],
TransformDenominator: p.TransformQuantity.DivideBy,
TransformRoundUp: p.TransformQuantity.Round == "up",
}

if len(p.Tiers) == 0 && p.Recurring.UsageType == "metered" {
Expand Down
3 changes: 3 additions & 0 deletions control/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ func TestRoundTrip(t *testing.T) {
// tiers_mode isn't needed or even set
Mode: "",

TransformDenominator: 100,
TransformRoundUp: true,

Aggregate: "perpetual",
Tiers: []Tier{
{Upto: 1, Price: 100, Base: 0},
Expand Down
44 changes: 41 additions & 3 deletions control/schedule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,12 @@ func (s *scheduleTester) advanceTo(t time.Time) {
}
}

func (s *scheduleTester) advanceToNextPeriod() {
func (s *scheduleTester) advanceToNextPeriod(numPeriods int) {
// TODO(bmizerany): make Phase aware so that it jumps based on the
// start of the next phase if the current phase ends sooner than than 1
// interval of the current phase.
now := s.clock.Present()
eop := time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, time.UTC)
eop := time.Date(now.Year(), now.Month()+time.Month(numPeriods), 1, 0, 0, 0, 0, time.UTC)
s.t.Logf("advancing to next period %s", eop)
s.advanceTo(eop)
}
Expand Down Expand Up @@ -398,7 +398,7 @@ func TestScheduleCancel(t *testing.T) {
s.report("org:paid", "feature:x", 99)
s.advance(10)
s.cancel("org:paid")
s.advanceToNextPeriod()
s.advanceToNextPeriod(1)

// check usage is billed
s.checkInvoices("org:paid", []Invoice{{
Expand Down Expand Up @@ -427,6 +427,44 @@ func TestScheduleCancel(t *testing.T) {
}})
}

func TestScheduleTransforms(t *testing.T) {
featureUp := mpf("feature:up@0")

s := newScheduleTester(t)
s.push([]Feature{{
FeaturePlan: featureUp,
Interval: "@monthly",
Currency: "usd",
Mode: "graduated",
Aggregate: "sum",
Tiers: []Tier{{Upto: Inf, Price: 2}},
TransformDenominator: 3,
TransformRoundUp: true,
}})

s.setPaymentMethod("org:paid", "pm_card_us")
s.schedule("org:paid", 0, "", featureUp)
s.report("org:paid", "feature:up", 100)
s.advanceToNextPeriod(2)

const wantAmount = 68 // 100 / 3 = 33.3333, rounded up to 34, 34 * 2 = 68

// check usage is billed
s.checkInvoices("org:paid", []Invoice{{
Lines: []InvoiceLineItem{
{Feature: featureUp, Quantity: 100, Amount: wantAmount}, // 100 / 3 = 33.3333, rounded up to 34, 34 * 2 = 68 },
},
SubtotalPreTax: wantAmount,
Subtotal: wantAmount,
TotalPreTax: wantAmount,
Total: wantAmount,
}, {
Lines: []InvoiceLineItem{
{Feature: featureUp},
},
}})
}

func TestScheduleCancelNothing(t *testing.T) {
s := newScheduleTester(t)
s.cancel("org:paid")
Expand Down