Update subscription_creator to use the new strategy.
When creating subscriptions through the client, detect when the returned error
is saying the account already has a subscription, or the subscription already
exists in stripe.
Add an UpdateSubscription function that will update a subscription through the
API.
Update our subscription_creator listener to listen for profile creation and
login verification messages. This mainly involved fixing the constants (the
system, model, and topic) that the listener for profile creation was listening
for. It also meant adding a new updateMessageHandler that listens for login
verification, tries to create a subscription that has the user ID and email of
the login that was verified, and if a subscription already exists, updates it
instead to use the email address that was just verified. This will ensure that
users get their receipts automatically emailed to them by Stripe.
6 "code.secondbit.org/api.hg"
7 "code.secondbit.org/auth.hg"
8 "code.secondbit.org/trout.hg"
9 "code.secondbit.org/uuid.hg"
11 "code.secondbit.org/ducky/subscriptions.hg"
13 "golang.org/x/net/context"
17 ScopeSubscription = auth.Scope{ID: "subscriptions", Name: "Manage Subscriptions", Description: "Read and update your subscription information."}
18 ScopeSubscriptionAdmin = auth.Scope{ID: "subscriptions_admin", Name: "Administer Subscriptions", Description: "Read and update subscription information, bypassing ACL."}
21 func HandleSubscriptions(router *trout.Router, c context.Context) {
22 router.Endpoint("/subscriptions").Methods("POST", "OPTIONS").Handler(
23 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, CreateSubscriptionHandler))))
24 router.Endpoint("/subscriptions/{id}").Methods("GET", "OPTIONS").Handler(
25 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, GetSubscriptionHandler))))
26 router.Endpoint("/subscriptions/{id}").Methods("PATCH", "OPTIONS").Handler(
27 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, PatchSubscriptionHandler))))
30 func CreateSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) {
31 store, err := getSubscriptionStore(c)
33 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
36 stripe, err := getStripeClient(c)
38 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
41 if !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) {
42 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
45 var req subscriptions.SubscriptionChange
46 err = api.Decode(r, &req)
48 api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError})
51 // BUG(paddy): Need to validate the request when creating a subscription
52 sub, err := subscriptions.New(req, stripe, store)
54 var rErr api.RequestError
57 case subscriptions.ErrSubscriptionAlreadyExists:
58 rErr = api.RequestError{Slug: api.RequestErrConflict, Field: "/user_id"}
59 code = http.StatusBadRequest
60 case subscriptions.ErrStripeSubscriptionAlreadyExists:
61 rErr = api.RequestError{Slug: api.RequestErrConflict, Field: "/stripe_token"}
62 code = http.StatusBadRequest
64 rErr = api.RequestError{Slug: api.RequestErrActOfGod}
65 code = http.StatusInternalServerError
67 api.Encode(w, r, code, Response{Errors: []api.RequestError{rErr}})
70 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
71 api.Encode(w, r, http.StatusCreated, resp)
74 func GetSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) {
75 store, err := getSubscriptionStore(c)
77 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
80 vars := trout.RequestVars(r)
81 rawID := vars.Get("id")
83 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrMissing, Param: "id"}}})
86 id, err := uuid.Parse(rawID)
88 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}})
91 if !api.CheckScopes(r, ScopeSubscription.ID) {
92 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
95 userID, err := api.AuthUser(r)
97 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
100 if !id.Equal(userID) && !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) {
101 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
104 subs, err := store.GetSubscriptions([]uuid.ID{id})
106 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
109 sub, ok := subs[id.String()]
111 api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}})
114 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
115 api.Encode(w, r, http.StatusOK, resp)
118 func PatchSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) {
119 store, err := getSubscriptionStore(c)
121 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
124 stripe, err := getStripeClient(c)
126 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
129 if !api.CheckScopes(r, ScopeSubscription.ID) {
130 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
133 userID, err := api.AuthUser(r)
135 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
138 vars := trout.RequestVars(r)
139 rawID := vars.Get("id")
141 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrMissing, Param: "id"}}})
144 id, err := uuid.Parse(rawID)
146 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}})
150 var req subscriptions.SubscriptionChange
151 err = api.Decode(r, &req)
153 api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError})
156 // BUG(paddy): Need to validate the request when updating a subscription
158 // only admin users can update the system-controlled properties
159 changedSysProps := subscriptions.ChangingSystemProperties(req)
160 if len(changedSysProps) > 0 && !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) {
161 errs := make([]api.RequestError, len(changedSysProps))
162 for pos, prop := range changedSysProps {
163 errs[pos] = api.RequestError{Slug: api.RequestErrAccessDenied, Field: prop}
165 api.Encode(w, r, http.StatusBadRequest, Response{Errors: errs})
169 subs, err := store.GetSubscriptions([]uuid.ID{userID})
171 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
174 sub, ok := subs[userID.String()]
176 api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}})
179 stripeSub, err := subscriptions.UpdateStripeSubscription(sub.StripeSubscription, req.Plan, req.StripeSource, stripe)
181 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
184 change := subscriptions.StripeSubscriptionChange(sub, *stripeSub)
185 if change.IsEmpty() {
186 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
187 api.Encode(w, r, http.StatusOK, resp)
190 err = store.UpdateSubscription(id, change)
192 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
195 sub.ApplyChange(change)
196 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
197 api.Encode(w, r, http.StatusOK, resp)