Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flexible billing primitives #538

Merged
merged 1 commit into from
Apr 2, 2018
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
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)
}