ducky/subscriptions

Paddy 2015-06-22 Parent:61c4ce5850da Child:c4cfceb2f2fb

3:b240b6123548 Go to Latest

ducky/subscriptions/subscription.go

Export all subscriptionStore methods. We're not going to wrap all our subscriptionStore interactions in a Context type, so we need to expose all the functions so other packages can call them. Also, it's now SubscriptionStore. Also creates a SubscriptionRequest type that is used to create a new Subscription instance, along with a Validate method on it, to detect errors when creating a SubscriptionRequest. Update our CreateSubscription function to be New, and have it take a SubscriptionRequest, a Stripe instance, and a SubscriptionStore as arguments. It'll create the Subscription in the SubscriptionStore, create a customer in Stripe, and create the subscription in Stripe, then associate the Stripe subscription with the created Subscription. Also, fix our StripeSubscriptionChange function to correctly equate a missing/zero Unix timestamp from Stripe with a missing/zero time.Time.

History
     1.1 --- a/subscription.go	Tue Jun 16 23:09:59 2015 -0400
     1.2 +++ b/subscription.go	Mon Jun 22 18:34:07 2015 -0400
     1.3 @@ -4,6 +4,7 @@
     1.4  	"errors"
     1.5  	"time"
     1.6  
     1.7 +	"code.secondbit.org/api.hg"
     1.8  	"code.secondbit.org/uuid.hg"
     1.9  )
    1.10  
    1.11 @@ -25,6 +26,10 @@
    1.12  	// ErrNoSubscriptionID is returned when one or more Subscription IDs
    1.13  	// are required, but none are provided.
    1.14  	ErrNoSubscriptionID = errors.New("no Subscription ID provided")
    1.15 +
    1.16 +	planOptions = []string{"basic_monthly", "basic_yearly", "supporter_monthly", "supporter_yearly"}
    1.17 +
    1.18 +	Version string
    1.19  )
    1.20  
    1.21  // Subscription represents the state of a user's payments. It holds
    1.22 @@ -32,20 +37,20 @@
    1.23  // should be charged, how to charge a user and how much to charge
    1.24  // the user.
    1.25  type Subscription struct {
    1.26 -	UserID               uuid.ID
    1.27 -	StripeSubscription   string
    1.28 -	Plan                 string
    1.29 -	Status               string
    1.30 -	Canceling            bool
    1.31 -	Created              time.Time
    1.32 -	TrialStart           time.Time
    1.33 -	TrialEnd             time.Time
    1.34 -	PeriodStart          time.Time
    1.35 -	PeriodEnd            time.Time
    1.36 -	CanceledAt           time.Time
    1.37 -	FailedChargeAttempts int
    1.38 -	LastFailedCharge     time.Time
    1.39 -	LastNotified         time.Time
    1.40 +	UserID               uuid.ID   `json:"user_id"`
    1.41 +	StripeSubscription   string    `json:"stripe_subscription"`
    1.42 +	Plan                 string    `json:"plan"`
    1.43 +	Status               string    `json:"status"`
    1.44 +	Canceling            bool      `json:"canceling"`
    1.45 +	Created              time.Time `json:"created"`
    1.46 +	TrialStart           time.Time `json:"trial_start,omitempty"`
    1.47 +	TrialEnd             time.Time `json:"trial_end,omitempty"`
    1.48 +	PeriodStart          time.Time `json:"period_start,omitempty"`
    1.49 +	PeriodEnd            time.Time `json:"period_end,omitempty"`
    1.50 +	CanceledAt           time.Time `json:"canceled_at,omitempty"`
    1.51 +	FailedChargeAttempts int       `json:"failed_charge_attempts"`
    1.52 +	LastFailedCharge     time.Time `json:"last_failed_charge,omitempty"`
    1.53 +	LastNotified         time.Time `json:"last_notified,omitempty"`
    1.54  }
    1.55  
    1.56  // SubscriptionChange represents desired changes to a Subscription
    1.57 @@ -148,6 +153,53 @@
    1.58  	}
    1.59  }
    1.60  
    1.61 +type SubscriptionRequest struct {
    1.62 +	UserID      uuid.ID `json:"user_id"`
    1.63 +	Email       string  `json:"email"`
    1.64 +	StripeToken string  `json:"stripe_token"`
    1.65 +	Plan        string  `json:"plan"`
    1.66 +}
    1.67 +
    1.68 +func (req SubscriptionRequest) Validate(user uuid.ID, admin bool) []api.RequestError {
    1.69 +	var errors []api.RequestError
    1.70 +	if req.UserID == nil {
    1.71 +		errors = append(errors, api.RequestError{Slug: api.RequestErrMissing, Field: "/user_id"})
    1.72 +	} else if !req.UserID.Equal(user) && !admin {
    1.73 +		errors = append(errors, api.RequestError{Slug: api.RequestErrAccessDenied, Field: "/user_id"})
    1.74 +	}
    1.75 +	if req.Email == "" {
    1.76 +		errors = append(errors, api.RequestError{Slug: api.RequestErrMissing, Field: "/email"})
    1.77 +	}
    1.78 +	if req.StripeToken == "" {
    1.79 +		errors = append(errors, api.RequestError{Slug: api.RequestErrMissing, Field: "/stripe_token"})
    1.80 +	}
    1.81 +	if req.Plan == "" {
    1.82 +		errors = append(errors, api.RequestError{Slug: api.RequestErrMissing, Field: "/plan"})
    1.83 +	} else {
    1.84 +		var acceptablePlan bool
    1.85 +		for _, p := range planOptions {
    1.86 +			if req.Plan == p {
    1.87 +				acceptablePlan = true
    1.88 +				break
    1.89 +			}
    1.90 +		}
    1.91 +		if !acceptablePlan {
    1.92 +			errors = append(errors, api.RequestError{Slug: api.RequestErrInvalidValue, Field: "/plan"})
    1.93 +		}
    1.94 +	}
    1.95 +	return errors
    1.96 +}
    1.97 +
    1.98 +func SubscriptionFromRequest(req SubscriptionRequest) Subscription {
    1.99 +	return Subscription{
   1.100 +		UserID:             req.UserID,
   1.101 +		StripeSubscription: req.StripeToken,
   1.102 +		Plan:               req.Plan,
   1.103 +		Created:            time.Now(),
   1.104 +		TrialStart:         time.Now(),
   1.105 +	}
   1.106 +}
   1.107 +
   1.108  // SubscriptionStats represents a set of statistics about our Subscription
   1.109  // data that will be exposed to the Prometheus scraper.
   1.110  type SubscriptionStats struct {
   1.111 @@ -170,11 +222,11 @@
   1.112  	// was omitted."
   1.113  }
   1.114  
   1.115 -type subscriptionStore interface {
   1.116 -	reset() error
   1.117 -	createSubscription(sub Subscription) error
   1.118 -	updateSubscription(id uuid.ID, change SubscriptionChange) error
   1.119 -	deleteSubscription(id uuid.ID) error
   1.120 -	getSubscriptions(ids []uuid.ID) (map[string]Subscription, error)
   1.121 -	getSubscriptionStats() (SubscriptionStats, error)
   1.122 +type SubscriptionStore interface {
   1.123 +	Reset() error
   1.124 +	CreateSubscription(sub Subscription) error
   1.125 +	UpdateSubscription(id uuid.ID, change SubscriptionChange) error
   1.126 +	DeleteSubscription(id uuid.ID) error
   1.127 +	GetSubscriptions(ids []uuid.ID) (map[string]Subscription, error)
   1.128 +	GetSubscriptionStats() (SubscriptionStats, error)
   1.129  }