ducky/subscriptions
ducky/subscriptions/api/subscription_handlers.go
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.
| paddy@4 | 1 package api |
| paddy@4 | 2 |
| paddy@4 | 3 import ( |
| paddy@4 | 4 "net/http" |
| paddy@4 | 5 |
| paddy@4 | 6 "code.secondbit.org/api.hg" |
| paddy@6 | 7 "code.secondbit.org/auth.hg" |
| paddy@4 | 8 "code.secondbit.org/trout.hg" |
| paddy@4 | 9 "code.secondbit.org/uuid.hg" |
| paddy@4 | 10 |
| paddy@6 | 11 "code.secondbit.org/ducky/subscriptions.hg" |
| paddy@6 | 12 |
| paddy@4 | 13 "golang.org/x/net/context" |
| paddy@4 | 14 ) |
| paddy@4 | 15 |
| paddy@6 | 16 var ( |
| paddy@6 | 17 ScopeSubscription = auth.Scope{ID: "subscriptions", Name: "Manage Subscriptions", Description: "Read and update your subscription information."} |
| paddy@6 | 18 ScopeSubscriptionAdmin = auth.Scope{ID: "subscriptions_admin", Name: "Administer Subscriptions", Description: "Read and update subscription information, bypassing ACL."} |
| paddy@4 | 19 ) |
| paddy@4 | 20 |
| paddy@4 | 21 func HandleSubscriptions(router *trout.Router, c context.Context) { |
| paddy@4 | 22 router.Endpoint("/subscriptions").Methods("POST", "OPTIONS").Handler( |
| paddy@4 | 23 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, CreateSubscriptionHandler)))) |
| paddy@4 | 24 router.Endpoint("/subscriptions/{id}").Methods("GET", "OPTIONS").Handler( |
| paddy@4 | 25 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, GetSubscriptionHandler)))) |
| paddy@6 | 26 router.Endpoint("/subscriptions/{id}").Methods("PATCH", "OPTIONS").Handler( |
| paddy@6 | 27 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, PatchSubscriptionHandler)))) |
| paddy@4 | 28 } |
| paddy@4 | 29 |
| paddy@4 | 30 func CreateSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) { |
| paddy@4 | 31 store, err := getSubscriptionStore(c) |
| paddy@4 | 32 if err != nil { |
| paddy@4 | 33 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) |
| paddy@4 | 34 return |
| paddy@4 | 35 } |
| paddy@4 | 36 stripe, err := getStripeClient(c) |
| paddy@4 | 37 if err != nil { |
| paddy@4 | 38 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) |
| paddy@4 | 39 return |
| paddy@4 | 40 } |
| paddy@6 | 41 if !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) { |
| paddy@4 | 42 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}}) |
| paddy@4 | 43 return |
| paddy@4 | 44 } |
| paddy@6 | 45 var req subscriptions.SubscriptionChange |
| paddy@4 | 46 err = api.Decode(r, &req) |
| paddy@4 | 47 if err != nil { |
| paddy@4 | 48 api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError}) |
| paddy@4 | 49 return |
| paddy@4 | 50 } |
| paddy@6 | 51 // BUG(paddy): Need to validate the request when creating a subscription |
| paddy@4 | 52 sub, err := subscriptions.New(req, stripe, store) |
| paddy@4 | 53 if err != nil { |
| paddy@4 | 54 var rErr api.RequestError |
| paddy@4 | 55 var code int |
| paddy@4 | 56 switch err { |
| paddy@4 | 57 case subscriptions.ErrSubscriptionAlreadyExists: |
| paddy@4 | 58 rErr = api.RequestError{Slug: api.RequestErrConflict, Field: "/user_id"} |
| paddy@4 | 59 code = http.StatusBadRequest |
| paddy@4 | 60 case subscriptions.ErrStripeSubscriptionAlreadyExists: |
| paddy@4 | 61 rErr = api.RequestError{Slug: api.RequestErrConflict, Field: "/stripe_token"} |
| paddy@4 | 62 code = http.StatusBadRequest |
| paddy@4 | 63 default: |
| paddy@4 | 64 rErr = api.RequestError{Slug: api.RequestErrActOfGod} |
| paddy@4 | 65 code = http.StatusInternalServerError |
| paddy@4 | 66 } |
| paddy@4 | 67 api.Encode(w, r, code, Response{Errors: []api.RequestError{rErr}}) |
| paddy@4 | 68 return |
| paddy@4 | 69 } |
| paddy@4 | 70 resp := Response{Subscriptions: []subscriptions.Subscription{sub}} |
| paddy@4 | 71 api.Encode(w, r, http.StatusCreated, resp) |
| paddy@4 | 72 } |
| paddy@4 | 73 |
| paddy@4 | 74 func GetSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) { |
| paddy@4 | 75 store, err := getSubscriptionStore(c) |
| paddy@4 | 76 if err != nil { |
| paddy@4 | 77 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) |
| paddy@4 | 78 return |
| paddy@4 | 79 } |
| paddy@4 | 80 vars := trout.RequestVars(r) |
| paddy@4 | 81 rawID := vars.Get("id") |
| paddy@4 | 82 if rawID == "" { |
| paddy@4 | 83 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrMissing, Param: "id"}}}) |
| paddy@4 | 84 return |
| paddy@4 | 85 } |
| paddy@4 | 86 id, err := uuid.Parse(rawID) |
| paddy@4 | 87 if err != nil { |
| paddy@4 | 88 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}}) |
| paddy@4 | 89 return |
| paddy@4 | 90 } |
| paddy@6 | 91 if !api.CheckScopes(r, ScopeSubscription.ID) { |
| paddy@4 | 92 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}}) |
| paddy@4 | 93 return |
| paddy@4 | 94 } |
| paddy@4 | 95 userID, err := api.AuthUser(r) |
| paddy@4 | 96 if err != nil { |
| paddy@4 | 97 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}}) |
| paddy@4 | 98 return |
| paddy@4 | 99 } |
| paddy@6 | 100 if !id.Equal(userID) && !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) { |
| paddy@4 | 101 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}}) |
| paddy@4 | 102 return |
| paddy@4 | 103 } |
| paddy@4 | 104 subs, err := store.GetSubscriptions([]uuid.ID{id}) |
| paddy@4 | 105 if err != nil { |
| paddy@4 | 106 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) |
| paddy@4 | 107 return |
| paddy@4 | 108 } |
| paddy@4 | 109 sub, ok := subs[id.String()] |
| paddy@4 | 110 if !ok { |
| paddy@4 | 111 api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}}) |
| paddy@4 | 112 return |
| paddy@4 | 113 } |
| paddy@4 | 114 resp := Response{Subscriptions: []subscriptions.Subscription{sub}} |
| paddy@4 | 115 api.Encode(w, r, http.StatusOK, resp) |
| paddy@4 | 116 } |
| paddy@6 | 117 |
| paddy@6 | 118 func PatchSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) { |
| paddy@6 | 119 store, err := getSubscriptionStore(c) |
| paddy@6 | 120 if err != nil { |
| paddy@6 | 121 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) |
| paddy@6 | 122 return |
| paddy@6 | 123 } |
| paddy@6 | 124 stripe, err := getStripeClient(c) |
| paddy@6 | 125 if err != nil { |
| paddy@6 | 126 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) |
| paddy@6 | 127 return |
| paddy@6 | 128 } |
| paddy@6 | 129 if !api.CheckScopes(r, ScopeSubscription.ID) { |
| paddy@6 | 130 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}}) |
| paddy@6 | 131 return |
| paddy@6 | 132 } |
| paddy@6 | 133 userID, err := api.AuthUser(r) |
| paddy@6 | 134 if err != nil { |
| paddy@6 | 135 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}}) |
| paddy@6 | 136 return |
| paddy@6 | 137 } |
| paddy@6 | 138 vars := trout.RequestVars(r) |
| paddy@6 | 139 rawID := vars.Get("id") |
| paddy@6 | 140 if rawID == "" { |
| paddy@6 | 141 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrMissing, Param: "id"}}}) |
| paddy@6 | 142 return |
| paddy@6 | 143 } |
| paddy@6 | 144 id, err := uuid.Parse(rawID) |
| paddy@6 | 145 if err != nil { |
| paddy@6 | 146 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}}) |
| paddy@6 | 147 return |
| paddy@6 | 148 } |
| paddy@6 | 149 |
| paddy@6 | 150 var req subscriptions.SubscriptionChange |
| paddy@6 | 151 err = api.Decode(r, &req) |
| paddy@6 | 152 if err != nil { |
| paddy@6 | 153 api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError}) |
| paddy@6 | 154 return |
| paddy@6 | 155 } |
| paddy@6 | 156 // BUG(paddy): Need to validate the request when updating a subscription |
| paddy@6 | 157 |
| paddy@6 | 158 // only admin users can update the system-controlled properties |
| paddy@6 | 159 changedSysProps := subscriptions.ChangingSystemProperties(req) |
| paddy@6 | 160 if len(changedSysProps) > 0 && !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) { |
| paddy@6 | 161 errs := make([]api.RequestError, len(changedSysProps)) |
| paddy@6 | 162 for pos, prop := range changedSysProps { |
| paddy@6 | 163 errs[pos] = api.RequestError{Slug: api.RequestErrAccessDenied, Field: prop} |
| paddy@6 | 164 } |
| paddy@6 | 165 api.Encode(w, r, http.StatusBadRequest, Response{Errors: errs}) |
| paddy@6 | 166 return |
| paddy@6 | 167 } |
| paddy@6 | 168 |
| paddy@6 | 169 subs, err := store.GetSubscriptions([]uuid.ID{userID}) |
| paddy@6 | 170 if err != nil { |
| paddy@6 | 171 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) |
| paddy@6 | 172 return |
| paddy@6 | 173 } |
| paddy@6 | 174 sub, ok := subs[userID.String()] |
| paddy@6 | 175 if !ok { |
| paddy@6 | 176 api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}}) |
| paddy@6 | 177 return |
| paddy@6 | 178 } |
| paddy@6 | 179 stripeSub, err := subscriptions.UpdateStripeSubscription(sub.StripeSubscription, req.Plan, req.StripeSource, stripe) |
| paddy@6 | 180 if err != nil { |
| paddy@6 | 181 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) |
| paddy@6 | 182 return |
| paddy@6 | 183 } |
| paddy@6 | 184 change := subscriptions.StripeSubscriptionChange(sub, *stripeSub) |
| paddy@6 | 185 if change.IsEmpty() { |
| paddy@6 | 186 resp := Response{Subscriptions: []subscriptions.Subscription{sub}} |
| paddy@6 | 187 api.Encode(w, r, http.StatusOK, resp) |
| paddy@6 | 188 return |
| paddy@6 | 189 } |
| paddy@6 | 190 err = store.UpdateSubscription(id, change) |
| paddy@6 | 191 if err != nil { |
| paddy@6 | 192 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) |
| paddy@6 | 193 return |
| paddy@6 | 194 } |
| paddy@6 | 195 sub.ApplyChange(change) |
| paddy@6 | 196 resp := Response{Subscriptions: []subscriptions.Subscription{sub}} |
| paddy@6 | 197 api.Encode(w, r, http.StatusOK, resp) |
| paddy@6 | 198 } |