Skip to content

Commit

Permalink
Merge pull request #538 from stripe/alexander/flexible-billing
Browse files Browse the repository at this point in the history
Flexible billing primitives
  • Loading branch information
brandur-stripe authored Apr 2, 2018
2 parents 59e79a3 + 4fdc1ee commit c817432
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ cache:

env:
global:
- STRIPE_MOCK_VERSION=0.8.0
- STRIPE_MOCK_VERSION=0.11.0

go:
- "1.7"
Expand Down
2 changes: 1 addition & 1 deletion form/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ func buildArrayOrSliceEncoder(t reflect.Type) encoderFunc {
elemF(values, indexV, arrNames, indexV.Kind() == reflect.Ptr, nil)

if isAppender(indexV.Type()) && !indexV.IsNil() {
v.Interface().(Appender).AppendTo(values, arrNames)
indexV.Interface().(Appender).AppendTo(values, arrNames)
}
}
}
Expand Down
124 changes: 101 additions & 23 deletions plan.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,74 @@
package stripe

import (
"strconv"

"github.com/stripe/stripe-go/form"
)

// PlanInterval is the list of allowed values for a plan's interval.
// Allowed values are "day", "week", "month", "year".
type PlanInterval string

const (
// PlanBillingSchemeTiered indicates that the price per single unit is tiered
// and can change with the total number of units.
PlanBillingSchemeTiered string = "tiered"
// PlanBillingSchemePerUnit indicates that each unit is billed at a fixed
// price.
PlanBillingSchemePerUnit string = "per_unit"
)

const (
// PlanUsageTypeLicensed indicates that the set Quantity on a subscription item
// will be used to bill for a subscription.
PlanUsageTypeLicensed string = "licensed"
// PlanUsageTypeMetered indicates that usage records must be reported instead
// of setting a Quantity on a subscription item.
PlanUsageTypeMetered string = "metered"
)

const (
// PlanTransformUsageModeRoundDown represents a bucket billing configuration: a partially
// filled bucket will count as an empty bucket.
PlanTransformUsageModeRoundDown string = "round_down"
// PlanTransformUsageModeRoundUp represents a bucket billing configuration: a partially
// filled bucket will count as a full bucket.
PlanTransformUsageModeRoundUp string = "round_up"
)

// PlanTransformUsage represents the bucket billing configuration.
type PlanTransformUsage struct {
DivideBy int64 `json:"bucket_size"`
Round string `json:"round"`
}

// PlanTier configures tiered pricing
type PlanTier struct {
Amount uint64 `json:"amount"`
UpTo uint64 `json:"up_to"`
}

// Plan is the resource representing a Stripe plan.
// For more details see https://stripe.com/docs/api#plans.
type Plan struct {
Amount uint64 `json:"amount"`
Created int64 `json:"created"`
Currency Currency `json:"currency"`
Deleted bool `json:"deleted"`
ID string `json:"id"`
Interval PlanInterval `json:"interval"`
IntervalCount uint64 `json:"interval_count"`
Live bool `json:"livemode"`
Meta map[string]string `json:"metadata"`
Nickname string `json:"nickname"`
Product string `json:"product"`
TrialPeriod uint64 `json:"trial_period_days"`
Amount uint64 `json:"amount"`
BillingScheme string `json:"billing_scheme"`
Created int64 `json:"created"`
Currency Currency `json:"currency"`
Deleted bool `json:"deleted"`
ID string `json:"id"`
Interval PlanInterval `json:"interval"`
IntervalCount uint64 `json:"interval_count"`
Live bool `json:"livemode"`
Meta map[string]string `json:"metadata"`
Nickname string `json:"nickname"`
Product string `json:"product"`
Tiers []*PlanTier `json:"tiers"`
TiersMode string `json:"tiers_mode"`
TransformUsage *PlanTransformUsage `json:"transform_usage"`
TrialPeriod uint64 `json:"trial_period_days"`
UsageType string `json:"usage_type"`
}

// PlanList is a list of plans as returned from a list endpoint.
Expand All @@ -38,17 +88,45 @@ type PlanListParams struct {
// PlanParams is the set of parameters that can be used when creating or updating a plan.
// For more details see https://stripe.com/docs/api#create_plan and https://stripe.com/docs/api#update_plan.
type PlanParams struct {
Params `form:"*"`
Amount uint64 `form:"amount"`
AmountZero bool `form:"amount,zero"`
Currency Currency `form:"currency"`
ID string `form:"id"`
Interval PlanInterval `form:"interval"`
IntervalCount uint64 `form:"interval_count"`
Nickname string `form:"nickname"`
Product *PlanProductParams `form:"product"`
ProductID *string `form:"product"`
TrialPeriod uint64 `form:"trial_period_days"`
Params `form:"*"`
Amount uint64 `form:"amount"`
AmountZero bool `form:"amount,zero"`
BillingScheme string `form:"billing_scheme"`
Currency Currency `form:"currency"`
ID string `form:"id"`
Interval PlanInterval `form:"interval"`
IntervalCount uint64 `form:"interval_count"`
Nickname string `form:"nickname"`
Product *PlanProductParams `form:"product"`
ProductID *string `form:"product"`
Tiers []*PlanTierParams `form:"tiers,indexed"`
TiersMode string `form:"tiers_mode"`
TransformUsage *PlanTransformUsageParams `form:"transform_usage"`
TrialPeriod uint64 `form:"trial_period_days"`
UsageType string `form:"usage_type"`
}

// PlanTransformUsageParams represents the bucket billing configuration.
type PlanTransformUsageParams struct {
DivideBy int64 `form:"bucket_size"`
Round string `form:"round"`
}

// PlanTierParams configures tiered pricing
type PlanTierParams struct {
Params `form:"*"`
Amount uint64 `form:"amount"`
UpTo uint64 `form:"-"` // handled in custom AppendTo
UpToInf bool `form:"-"` // handled in custom AppendTo
}

// AppendTo implements custom up_to serialisation logic for tiers configuration
func (config *PlanTierParams) AppendTo(body *form.Values, keyParts []string) {
if config.UpToInf {
body.Add(form.FormatKey(append(keyParts, "up_to")), "inf")
} else {
body.Add(form.FormatKey(append(keyParts, "up_to")), strconv.FormatUint(config.UpTo, 10))
}
}

// PlanProductParams is the set of parameters that can be used when creating a product inside a plan
Expand Down
18 changes: 15 additions & 3 deletions plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,34 @@ func TestPlanParams_AppendTo(t *testing.T) {
Name: "Sapphire Elite",
StatementDescriptor: "SAPPHIRE",
}
productId := "prod_123abc"
productID := "prod_123abc"
tiers := []*PlanTierParams{{Amount: 123, UpTo: 321}, {Amount: 123, UpToInf: true}}
testCases := []struct {
field string
params *PlanParams
want interface{}
}{
{"amount", &PlanParams{}, ""},
{"amount", &PlanParams{Amount: 0, AmountZero: false}, ""},
{"amount", &PlanParams{Amount: 0, AmountZero: true}, strconv.FormatUint(0, 10)},
{"amount", &PlanParams{Amount: 123}, strconv.FormatUint(123, 10)},
{"currency", &PlanParams{Currency: "USD"}, "USD"},
{"id", &PlanParams{ID: "sapphire-elite"}, "sapphire-elite"},
{"interval", &PlanParams{Interval: "month"}, "month"},
{"interval_count", &PlanParams{IntervalCount: 3}, strconv.FormatUint(3, 10)},
{"interval", &PlanParams{Interval: "month"}, "month"},
{"product", &PlanParams{ProductID: &productID}, "prod_123abc"},
{"product[id]", &PlanParams{Product: &productParams}, "ID"},
{"product[name]", &PlanParams{Product: &productParams}, "Sapphire Elite"},
{"product[statement_descriptor]", &PlanParams{Product: &productParams}, "SAPPHIRE"},
{"product", &PlanParams{ProductID: &productId}, "prod_123abc"},
{"tiers_mode", &PlanParams{TiersMode: "volume"}, "volume"},
{"tiers[0][amount]", &PlanParams{Tiers: tiers}, strconv.FormatUint(123, 10)},
{"tiers[0][up_to]", &PlanParams{Tiers: tiers}, strconv.FormatUint(321, 10)},
{"tiers[1][amount]", &PlanParams{Tiers: tiers}, strconv.FormatUint(123, 10)},
{"tiers[1][up_to]", &PlanParams{Tiers: tiers}, "inf"},
{"transform_usage[bucket_size]", &PlanParams{TransformUsage: &PlanTransformUsageParams{DivideBy: 123, Round: "round_up"}}, strconv.FormatUint(123, 10)},
{"transform_usage[round]", &PlanParams{TransformUsage: &PlanTransformUsageParams{DivideBy: 123, Round: "round_up"}}, "round_up"},
{"trial_period_days", &PlanParams{TrialPeriod: 123}, strconv.FormatUint(123, 10)},
{"usage_type", &PlanParams{UsageType: "metered"}, "metered"},
}
for _, tc := range testCases {
t.Run(tc.field, func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion scripts/check_gofmt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ find_files() {

bad_files=$(find_files | xargs gofmt -s -l)
if [[ -n "${bad_files}" ]]; then
echo "!!! gofmt needs to be run on the following files: "
echo "!!! gofmt -s needs to be run on the following files: "
echo "${bad_files}"
exit 1
fi
33 changes: 33 additions & 0 deletions usage_record.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package stripe

const (
// UsageRecordParamsActionIncrement indicates that if two usage records conflict
// (i.E. are reported a the same timestamp), their Quantity will be summed
UsageRecordParamsActionIncrement string = "increment"

// UsageRecordParamsActionSet indicates that if two usage records conflict
// (i.E. are reported a the same timestamp), the Quantity of the most recent
// usage record will overwrite any other quantity.
UsageRecordParamsActionSet string = "set"
)

// UsageRecord represents a usage record.
// See https://stripe.com/docs/api#usage_records
type UsageRecord struct {
ID string `json:"id"`
Live bool `json:"livemode"`
Quantity uint64 `json:"quantity"`
SubscriptionItem string `json:"subscription_item"`
Timestamp uint64 `json:"timestamp"`
}

// UsageRecordParams create a usage record for a specified subscription item
// and date, and fills it with a quantity.
type UsageRecordParams struct {
Params `form:"*"`
Action string `form:"action"`
Quantity uint64 `form:"quantity"`
QuantityZero bool `form:"quantity,zero"`
SubscriptionItem string `form:"-"` // passed in the URL
Timestamp uint64 `form:"timestamp"`
}
30 changes: 30 additions & 0 deletions usage_record_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package stripe

import (
"strconv"
"testing"

assert "github.com/stretchr/testify/require"
"github.com/stripe/stripe-go/form"
)

func TestUsageRecordParams_AppendTo(t *testing.T) {
testCases := []struct {
field string
params *UsageRecordParams
want interface{}
}{
{"quantity", &UsageRecordParams{Quantity: 2000}, strconv.FormatUint(2000, 10)},
{"quantity", &UsageRecordParams{QuantityZero: true}, strconv.FormatUint(0, 10)},
{"timestamp", &UsageRecordParams{Timestamp: 123123123}, strconv.FormatUint(123123123, 10)},
{"action", &UsageRecordParams{Action: "increment"}, "increment"},
}
for _, tc := range testCases {
t.Run(tc.field, func(t *testing.T) {
body := &form.Values{}
form.AppendTo(body, tc.params)
values := body.ToValues()
assert.Equal(t, tc.want, values.Get(tc.field))
})
}
}
38 changes: 38 additions & 0 deletions usagerecord/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Package usage_record provides the /subscription_items/{SUBSCRIPTION_ITEM_ID}/usage_records APIs
package usagerecord

import (
"fmt"
"net/url"

stripe "github.com/stripe/stripe-go"
"github.com/stripe/stripe-go/form"
)

// Client is used to invoke /plans APIs.
type Client struct {
B stripe.Backend
Key string
}

// New creates a new usage record.
// For more details see https://stripe.com/docs/api#usage_records
func New(params *stripe.UsageRecordParams) (*stripe.UsageRecord, error) {
return getC().New(params)
}

// New internal implementation to create a new usage record.
func (c Client) New(params *stripe.UsageRecordParams) (*stripe.UsageRecord, error) {
body := &form.Values{}
form.AppendTo(body, params)

url := fmt.Sprintf("/subscription_items/%s/usage_records", url.QueryEscape(params.SubscriptionItem))
record := &stripe.UsageRecord{}
err := c.B.Call("POST", url, c.Key, body, &params.Params, record)

return record, err
}

func getC() Client {
return Client{stripe.GetBackend(stripe.APIBackend), stripe.Key}
}
22 changes: 22 additions & 0 deletions usagerecord/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package usagerecord

import (
"testing"
"time"

assert "github.com/stretchr/testify/require"
stripe "github.com/stripe/stripe-go"
_ "github.com/stripe/stripe-go/testing"
)

func TestUsageRecordNew(t *testing.T) {
now := uint64(time.Now().Unix())
usageRecord, err := New(&stripe.UsageRecordParams{
Quantity: 123,
Timestamp: now,
Action: stripe.UsageRecordParamsActionIncrement,
SubscriptionItem: "si_123",
})
assert.Nil(t, err)
assert.NotNil(t, usageRecord)
}

0 comments on commit c817432

Please sign in to comment.