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

Commit

Permalink
control: report Plans for subscriptions without schedules (#283)
Browse files Browse the repository at this point in the history
Previous to this commit, the /v1/phase endpoint did not report Plans for
subscriptions without schedules. This is immediately noticeable is using
Checkout, which does not create a schedule.
  • Loading branch information
bmizerany committed Mar 21, 2023
1 parent 4b183c9 commit c3fbf7b
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 35 deletions.
103 changes: 70 additions & 33 deletions control/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ import (
"time"

"golang.org/x/exp/slices"
"golang.org/x/sync/errgroup"
"kr.dev/errorfmt"
"tier.run/refs"
"tier.run/stripe"
"tier.run/types/payment"
"tier.run/values"
)

type clockKey struct{}
Expand Down Expand Up @@ -274,44 +272,23 @@ type stripeSubSchedule struct {
func (c *Client) lookupPhases(ctx context.Context, org string, s subscription, name string) (current Phase, all []Phase, err error) {
defer errorfmt.Handlef("lookupPhases: %w", &err)

if s.ScheduleID == "" {
ps := subscriptionToPhases(org, s)
return ps[0], ps, nil
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()

g, ctx := errgroup.WithContext(ctx)

var ss stripeSubSchedule
g.Go(func() error {
var f stripe.Form
f.Add("expand[]", "phases.items.price")
return c.Stripe.Do(ctx, "GET", "/v1/subscription_schedules/"+s.ScheduleID, f, &ss)
})

var m []refs.FeaturePlan
featureByProviderID := make(map[string]refs.FeaturePlan)
g.Go(func() (err error) {
fut := futureGo(func() ([]Feature, error) {
fs, err := c.Pull(ctx, 0)
if err != nil {
return err
return nil, err
}
m = values.MapFunc(fs, func(f Feature) refs.FeaturePlan {
featureByProviderID[f.ProviderID] = f.FeaturePlan
return f.FeaturePlan
})
return err
return fs, nil
})

if err := g.Wait(); err != nil {
return Phase{}, nil, err
}

for _, p := range ss.Phases {
fs := make([]refs.FeaturePlan, 0, len(p.Items))
for _, pi := range p.Items {
fs = append(fs, featureByProviderID[pi.Price.ProviderID()])
wholePlans := func(fs []refs.FeaturePlan) ([]refs.Plan, error) {
mfs, err := fut.get()
if err != nil {
return nil, err
}

m := FeaturePlans(mfs)
var plans []refs.Plan
for _, f := range fs {
if slices.Contains(plans, f.Plan()) {
Expand All @@ -322,7 +299,47 @@ func (c *Client) lookupPhases(ctx context.Context, org string, s subscription, n
if inModel == inPhase {
plans = append(plans, f.Plan())
}
}
return plans, nil
}

if s.ScheduleID == "" {
ps := subscriptionToPhases(org, s)
for i, p := range ps {
plans, err := wholePlans(p.Features)
if err != nil {
return Phase{}, nil, err
}
ps[i].Plans = plans
}
return ps[0], ps, nil
}

var ss stripeSubSchedule
var f stripe.Form
f.Add("expand[]", "phases.items.price")
if err := c.Stripe.Do(ctx, "GET", "/v1/subscription_schedules/"+s.ScheduleID, f, &ss); err != nil {
return Phase{}, nil, err
}

m, err := fut.get()
if err != nil {
return Phase{}, nil, err
}
featureByProviderID := make(map[string]refs.FeaturePlan)
for _, f := range m {
featureByProviderID[f.ProviderID] = f.FeaturePlan
}

for _, p := range ss.Phases {
fs := make([]refs.FeaturePlan, 0, len(p.Items))
for _, pi := range p.Items {
fs = append(fs, featureByProviderID[pi.Price.ProviderID()])
}

plans, err := wholePlans(fs)
if err != nil {
return Phase{}, nil, err
}

p := Phase{
Expand Down Expand Up @@ -1074,3 +1091,23 @@ func timeUnix(n int64) time.Time {
}
return time.Unix(n, 0)
}

type future[T any] struct {
ch chan struct{}
v T
err error
}

func futureGo[T any](fn func() (T, error)) *future[T] {
f := &future[T]{ch: make(chan struct{})}
go func() {
f.v, f.err = fn()
close(f.ch)
}()
return f
}

func (f *future[T]) get() (T, error) {
<-f.ch
return f.v, f.err
}
22 changes: 20 additions & 2 deletions control/schedule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1039,9 +1039,19 @@ func TestLookupPhasesNoSchedule(t *testing.T) {
// TODO(bmizerany): This tests assumptions, but we need an integration
// test provin fields "like" trial actually fall off / go to zero when
// the trial is over. This needs a test clock with stripe.

newHandler := func(s string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case they.Want(r, "GET", "/v1/prices"):
writeHuJSON(w, `
{"data": [
{
"id": "price_test",
"metadata": {"tier.feature": "feature:x@plan:test@0"},
},
]}
`)
case they.Want(r, "GET", "/v1/customers"):
writeHuJSON(w, `
{"data": [
Expand All @@ -1060,7 +1070,7 @@ func TestLookupPhasesNoSchedule(t *testing.T) {
"tier.subscription": "default",
},
"items": {"data": [{"price": {
"metadata": {"tier.feature": "feature:x@0"},
"metadata": {"tier.feature": "feature:x@plan:test@0"},
}}]},
},
]}
Expand All @@ -1072,7 +1082,7 @@ func TestLookupPhasesNoSchedule(t *testing.T) {
}

fs := []refs.FeaturePlan{
mpf("feature:x@0"),
mpf("feature:x@plan:test@0"),
}

cases := []struct {
Expand All @@ -1089,6 +1099,7 @@ func TestLookupPhasesNoSchedule(t *testing.T) {
Current: true,
Trial: false,
Features: fs,
Plans: []refs.Plan{mpp("plan:test@0")},
}},
},
{
Expand All @@ -1101,6 +1112,7 @@ func TestLookupPhasesNoSchedule(t *testing.T) {
Effective: time.Unix(123123123, 0),
Current: true,
Features: fs,
Plans: []refs.Plan{mpp("plan:test@0")},
}, {
Org: "org:test",
Effective: time.Unix(223123123, 0),
Expand All @@ -1119,12 +1131,14 @@ func TestLookupPhasesNoSchedule(t *testing.T) {
Current: false,
Trial: true,
Features: fs,
Plans: []refs.Plan{mpp("plan:test@0")},
}, {
Org: "org:test",
Effective: time.Unix(200000000, 0),
Features: fs,
Current: true, // <---- current
Trial: false,
Plans: []refs.Plan{mpp("plan:test@0")},
}, {
Org: "org:test",
Effective: time.Unix(400000000, 0),
Expand All @@ -1145,10 +1159,12 @@ func TestLookupPhasesNoSchedule(t *testing.T) {
Current: true,
Trial: true,
Features: fs,
Plans: []refs.Plan{mpp("plan:test@0")},
}, {
Org: "org:test",
Effective: time.Unix(200000000, 0),
Features: fs,
Plans: []refs.Plan{mpp("plan:test@0")},
}},
},
{
Expand All @@ -1165,10 +1181,12 @@ func TestLookupPhasesNoSchedule(t *testing.T) {
Current: false,
Trial: true,
Features: fs,
Plans: []refs.Plan{mpp("plan:test@0")},
}, {
Org: "org:test",
Effective: time.Unix(200000000, 0),
Features: fs,
Plans: []refs.Plan{mpp("plan:test@0")},
}, {
Org: "org:test",
Effective: time.Unix(400000000, 0),
Expand Down

0 comments on commit c3fbf7b

Please sign in to comment.