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

Commit

Permalink
api: expose current period start and end (#282)
Browse files Browse the repository at this point in the history
  • Loading branch information
bmizerany committed Mar 15, 2023
1 parent d87ce90 commit 4b183c9
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 45 deletions.
4 changes: 3 additions & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,11 +322,12 @@ func (h *Handler) serveWhoAmI(w http.ResponseWriter, r *http.Request) error {

func (h *Handler) servePhase(w http.ResponseWriter, r *http.Request) error {
org := r.FormValue("org")
ps, err := h.c.LookupPhases(r.Context(), org)
s, err := h.c.LookupPhases(r.Context(), org)
if err != nil {
return err
}

ps := s.Phases
h.Logf("lookup phases: %# v", pretty.Formatter(ps))

for i, p := range ps {
Expand All @@ -345,6 +346,7 @@ func (h *Handler) servePhase(w http.ResponseWriter, r *http.Request) error {
Tax: apitypes.Taxation{
Automatic: p.AutomaticTax,
},
Current: apitypes.Period(s.Current),
})
}
}
Expand Down
18 changes: 16 additions & 2 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ func TestAPISubscribe(t *testing.T) {
if got.Effective.IsZero() {
t.Error("unexpected zero effective time")
}
ignore := diff.ZeroFields[apitypes.PhaseResponse]("Effective")
ignore := diff.ZeroFields[apitypes.PhaseResponse]("Effective", "Current")
diff.Test(t, t.Errorf, got, want, ignore)
}

Expand Down Expand Up @@ -472,6 +472,20 @@ func TestPhase(t *testing.T) {
if err != nil {
t.Fatal(err)
}

c := got.Current

if c.Effective.IsZero() {
t.Errorf("got zero effective time, want non-zero")
}
if c.End.IsZero() {
t.Errorf("got zero end time, want non-zero")
}
if !c.End.After(c.Effective) {
t.Errorf("unexpected effective time %s after end time %s", c.Effective, c.End)
}

got.Current = apitypes.Period{}
diff.Test(t, t.Errorf, got, tt.want)
})
}
Expand Down Expand Up @@ -517,7 +531,7 @@ func TestPhaseFragments(t *testing.T) {
if got.Effective.IsZero() {
t.Error("unexpected zero effective time")
}
ignore := diff.ZeroFields[apitypes.PhaseResponse]("Effective")
ignore := diff.ZeroFields[apitypes.PhaseResponse]("Effective", "Current")
diff.Test(t, t.Errorf, got, want, ignore)
}

Expand Down
50 changes: 34 additions & 16 deletions api/apitypes/apitypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ type Taxation struct {
Automatic bool `json:"automatic,omitempty"`
}

type Period struct {
Effective time.Time `json:"effective,omitempty"`
End time.Time `json:"end,omitempty"`
}

func (p *Period) IsZero() bool {
return p.Effective.IsZero() && p.End.IsZero()
}

type PhaseResponse struct {
Effective time.Time `json:"effective,omitempty"`
End time.Time `json:"end,omitempty"`
Expand All @@ -38,26 +47,24 @@ type PhaseResponse struct {
Fragments []refs.FeaturePlan `json:"fragments,omitempty"`
Trial bool `json:"trial,omitempty"`
Tax Taxation `json:"tax,omitempty"`
Current Period `json:"current,omitempty"`
}

func (pr PhaseResponse) MarshalJSON() ([]byte, error) {
type Alias PhaseResponse
if pr.End.IsZero() {
return json.Marshal(&struct {
*Alias
End byte `json:"end,omitempty"`
}{
Alias: (*Alias)(&pr),
})
} else {
return json.Marshal(&struct {
*Alias
End time.Time `json:"end"`
}{
Alias: (*Alias)(&pr),
End: pr.End,
})
}
return json.Marshal(&struct {
*Alias
Effective any `json:"effective,omitempty"`
End any `json:"end,omitempty"`
Current any `json:"current,omitempty"`
Tax any `json:"tax,omitempty"`
}{
Alias: (*Alias)(&pr),
Effective: nilIfZero(pr.Effective),
End: nilIfZero(pr.End),
Current: nilIfZero(pr.Current),
Tax: nilIfZero(pr.Tax),
})
}

type PaymentMethodsResponse struct {
Expand Down Expand Up @@ -166,3 +173,14 @@ type ClockResponse struct {
Present time.Time `json:"present"`
Status string `json:"status"`
}

func nilIfZero[T comparable](v T) any {
if z, ok := any(v).(interface{ IsZero() bool }); ok && z.IsZero() {
return nil
}
var zero T
if v == zero {
return nil
}
return v
}
4 changes: 2 additions & 2 deletions api/apitypes/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ func TestPhaseResponseJSON(t *testing.T) {
}{
{
pr: PhaseResponse{},
want: `{"effective":"0001-01-01T00:00:00Z","tax":{}}`,
want: `{}`,
},
{
pr: PhaseResponse{
End: time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
want: `{"effective":"0001-01-01T00:00:00Z","tax":{},"end":"2018-01-01T00:00:00Z"}`,
want: `{"end":"2018-01-01T00:00:00Z"}`,
},
}

Expand Down
36 changes: 32 additions & 4 deletions control/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ type subscription struct {
EndDate time.Time
CanceledAt time.Time
Features []Feature
AutomaticTax bool
AutomaticTax bool // TODO(bmizerany): cheange to tax.Applied
Current Period
}

func (c *Client) lookupSubscription(ctx context.Context, org, name string) (sub subscription, err error) {
Expand Down Expand Up @@ -153,6 +154,8 @@ func (c *Client) lookupSubscription(ctx context.Context, org, name string) (sub
AutomaticTax struct {
Enabled bool
} `json:"automatic_tax"`
CurrentPeriodStart int64 `json:"current_period_start"`
CurrentPeriodEnd int64 `json:"current_period_end"`
}

// TODO(bmizerany): cache the subscription ID and looked it up
Expand Down Expand Up @@ -190,6 +193,10 @@ func (c *Client) lookupSubscription(ctx context.Context, org, name string) (sub
Status: v.Status,
Features: fs,
AutomaticTax: v.AutomaticTax.Enabled,
Current: Period{
Effective: timeUnix(v.CurrentPeriodStart),
End: timeUnix(v.CurrentPeriodEnd),
},
}
if v.TrialEnd > 0 {
s.TrialEnd = time.Unix(v.TrialEnd, 0)
Expand Down Expand Up @@ -701,16 +708,29 @@ func (c *Client) LookupStatus(ctx context.Context, org string) (string, error) {
return s.Status, nil
}

func (c *Client) LookupPhases(ctx context.Context, org string) (ps []Phase, err error) {
type Schedule struct {
Current Period
Phases []Phase
}

func (c *Client) LookupPhases(ctx context.Context, org string) (ps *Schedule, err error) {
s, err := c.lookupSubscription(ctx, org, defaultScheduleName)
if errors.Is(err, errSubscriptionNotFound) {
return nil, nil
return &Schedule{}, nil
}
if err != nil {
return nil, err
}

_, all, err := c.lookupPhases(ctx, org, s, defaultScheduleName)
return all, err
if err != nil {
return nil, err
}
cs := &Schedule{
Current: s.Current,
Phases: all,
}
return cs, nil
}

// LookupPaymentMethods returns the payment methods for the given org.
Expand Down Expand Up @@ -1046,3 +1066,11 @@ func numFeaturesInPlan(fs []refs.FeaturePlan, plan refs.Plan) (n int) {
}
return n
}

// timeUnix is like time.Unix, but returns the zero time if n is zero.
func timeUnix(n int64) time.Time {
if n == 0 {
return time.Time{}
}
return time.Unix(n, 0)
}
50 changes: 30 additions & 20 deletions control/schedule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ var ignoreProviderIDs = diff.OptionList(
diff.ZeroFields[Org]("ProviderID"),
)

var ignoreScheduleTimes = diff.ZeroFields[Schedule]("Current")

func TestSchedule(t *testing.T) {
ciOnly(t)

Expand Down Expand Up @@ -81,7 +83,8 @@ func TestSchedule(t *testing.T) {
t.Fatal(err)
}
t.Logf("got phases %# v", pretty.Formatter(got))
diff.Test(t, t.Errorf, got, want, ignoreProviderIDs)
w := &Schedule{Phases: want}
diff.Test(t, t.Errorf, got, w, ignoreProviderIDs, ignoreScheduleTimes)
}

s.schedule("org:example", 0, "", planFree...)
Expand Down Expand Up @@ -268,7 +271,8 @@ func (s *scheduleTester) checkPhases(org string, want []Phase) {
s.t.Fatal(err)
}
s.t.Logf("got phases %# v", pretty.Formatter(got))
diff.Test(s.t, s.t.Errorf, got, want, ignoreProviderIDs)
w := &Schedule{Phases: want}
diff.Test(s.t, s.t.Errorf, got, w, ignoreProviderIDs, ignoreScheduleTimes)
}

func (s *scheduleTester) report(org, name string, n int) {
Expand Down Expand Up @@ -589,13 +593,16 @@ func TestScheduleMinMaxItems(t *testing.T) {
t.Fatal(err)
}

want := []Phase{{
want := &Schedule{Phases: []Phase{{
Org: "org:example",
Features: wantFeatures,
Current: true,
Plans: nil, // fragments only
}}
diff.Test(t, t.Errorf, got, want, diff.ZeroFields[Phase]("Effective"))
}}}
diff.Test(t, t.Errorf, got, want,
diff.ZeroFields[Phase]("Effective"),
ignoreScheduleTimes,
)
}

func TestSchedulePaymentMethod(t *testing.T) {
Expand Down Expand Up @@ -768,16 +775,16 @@ func TestLookupPhasesWithTiersRoundTrip(t *testing.T) {
t.Fatal(err)
}

want := []Phase{{
want := &Schedule{Phases: []Phase{{
Org: "org:example",
Effective: t0,
Current: true,
Features: fps,

Plans: plans("plan:test@0"),
}}
}}}

diff.Test(t, t.Errorf, got, want, ignoreProviderIDs)
diff.Test(t, t.Errorf, got, want, ignoreProviderIDs, ignoreScheduleTimes)
}

func TestSubscribeToPlan(t *testing.T) {
Expand Down Expand Up @@ -807,16 +814,16 @@ func TestSubscribeToPlan(t *testing.T) {
if err != nil {
t.Fatal(err)
}
want := []Phase{{
want := &Schedule{Phases: []Phase{{
Org: "org:example",
Current: true,
Effective: t0,
Features: FeaturePlans(fs),

Plans: plans("plan:pro@0"),
}}
}}}

diff.Test(t, t.Errorf, got, want, ignoreProviderIDs)
diff.Test(t, t.Errorf, got, want, ignoreProviderIDs, ignoreScheduleTimes)
}

func TestDedupCustomer(t *testing.T) {
Expand Down Expand Up @@ -870,15 +877,15 @@ func TestLookupPhases(t *testing.T) {
if err != nil {
t.Fatal(err)
}
want := []Phase{{
want := &Schedule{Phases: []Phase{{
Org: "org:example",
Current: true,
Effective: t0,
Features: FeaturePlans(fs0),

Plans: plans("plan:test@0"),
}}
diff.Test(t, t.Errorf, got, want, ignoreProviderIDs)
}}}
diff.Test(t, t.Errorf, got, want, ignoreProviderIDs, ignoreScheduleTimes)

fs1 := []Feature{
{
Expand All @@ -902,22 +909,22 @@ func TestLookupPhases(t *testing.T) {
t.Fatal(err)
}

for i, p := range got {
for i, p := range got.Phases {
p.Features = slices.Clone(p.Features)
refs.SortGroupedByVersion(p.Features)
got[i] = p
got.Phases[i] = p
}

want = []Phase{{
want = &Schedule{Phases: []Phase{{
Org: "org:example",
Current: true,
Effective: t0,
Features: fpsFrag,

Plans: plans("plan:test@0"),
}}
}}}

diff.Test(t, t.Errorf, got, want, ignoreProviderIDs)
diff.Test(t, t.Errorf, got, want, ignoreProviderIDs, ignoreScheduleTimes)
}

func TestLookupPaymentMethods(t *testing.T) {
Expand Down Expand Up @@ -1189,7 +1196,10 @@ func TestLookupPhasesNoSchedule(t *testing.T) {
t.Fatal(err)
}

diff.Test(t, t.Errorf, got, tt.want)
want := &Schedule{
Phases: tt.want,
}
diff.Test(t, t.Errorf, got, want, ignoreScheduleTimes)
})
}
}
Expand Down

0 comments on commit 4b183c9

Please sign in to comment.