ducky/subscriptions
ducky/subscriptions/api/subscription_handlers.go
Update with comments for all exported functions. We now have golint-approved comments for all the exported functions in the subscriptions package. Next challenge: all the sub-packages!
| 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@11 | 21 // changingSystemProperties takes a SubscriptionChange and returns a |
| paddy@11 | 22 // slice of JSON pointers to the properties that are changing but are |
| paddy@11 | 23 // also designated as "system properties" in the API. If no properties |
| paddy@11 | 24 // meet these criteria, an empty slice is returned. |
| paddy@11 | 25 func changingSystemProperties(change subscriptions.SubscriptionChange) []string { |
| paddy@11 | 26 var changes []string |
| paddy@11 | 27 if change.StripeSubscription != nil { |
| paddy@11 | 28 changes = append(changes, "/stripe_subscription") |
| paddy@11 | 29 } |
| paddy@11 | 30 if change.Status != nil { |
| paddy@11 | 31 changes = append(changes, "/status") |
| paddy@11 | 32 } |
| paddy@11 | 33 if change.TrialStart != nil { |
| paddy@11 | 34 changes = append(changes, "/trial_start") |
| paddy@11 | 35 } |
| paddy@11 | 36 if change.TrialEnd != nil { |
| paddy@11 | 37 changes = append(changes, "/trial_end") |
| paddy@11 | 38 } |
| paddy@11 | 39 if change.PeriodStart != nil { |
| paddy@11 | 40 changes = append(changes, "/period_start") |
| paddy@11 | 41 } |
| paddy@11 | 42 if change.PeriodEnd != nil { |
| paddy@11 | 43 changes = append(changes, "/period_end") |
| paddy@11 | 44 } |
| paddy@11 | 45 if change.CanceledAt != nil { |
| paddy@11 | 46 changes = append(changes, "/canceled_at") |
| paddy@11 | 47 } |
| paddy@11 | 48 if change.FailedChargeAttempts != nil { |
| paddy@11 | 49 changes = append(changes, "/failed_charge_attempts") |
| paddy@11 | 50 } |
| paddy@11 | 51 if change.LastFailedCharge != nil { |
| paddy@11 | 52 changes = append(changes, "/last_failed_charge") |
| paddy@11 | 53 } |
| paddy@11 | 54 if change.LastNotified != nil { |
| paddy@11 | 55 changes = append(changes, "/last_notified") |
| paddy@11 | 56 } |
| paddy@11 | 57 return changes |
| paddy@11 | 58 } |
| paddy@11 | 59 |
| paddy@4 | 60 func HandleSubscriptions(router *trout.Router, c context.Context) { |
| paddy@4 | 61 router.Endpoint("/subscriptions").Methods("POST", "OPTIONS").Handler( |
| paddy@4 | 62 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, CreateSubscriptionHandler)))) |
| paddy@4 | 63 router.Endpoint("/subscriptions/{id}").Methods("GET", "OPTIONS").Handler( |
| paddy@4 | 64 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, GetSubscriptionHandler)))) |
| paddy@6 | 65 router.Endpoint("/subscriptions/{id}").Methods("PATCH", "OPTIONS").Handler( |
| paddy@6 | 66 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, PatchSubscriptionHandler)))) |
| paddy@4 | 67 } |
| paddy@4 | 68 |
| paddy@4 | 69 func CreateSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) { |
| paddy@4 | 70 store, err := getSubscriptionStore(c) |
| paddy@4 | 71 if err != nil { |
| paddy@4 | 72 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) |
| paddy@4 | 73 return |
| paddy@4 | 74 } |
| paddy@4 | 75 stripe, err := getStripeClient(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@6 | 80 if !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) { |
| paddy@4 | 81 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}}) |
| paddy@4 | 82 return |
| paddy@4 | 83 } |
| paddy@6 | 84 var req subscriptions.SubscriptionChange |
| paddy@4 | 85 err = api.Decode(r, &req) |
| paddy@4 | 86 if err != nil { |
| paddy@4 | 87 api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError}) |
| paddy@4 | 88 return |
| paddy@4 | 89 } |
| paddy@6 | 90 // BUG(paddy): Need to validate the request when creating a subscription |
| paddy@4 | 91 sub, err := subscriptions.New(req, stripe, store) |
| paddy@4 | 92 if err != nil { |
| paddy@4 | 93 var rErr api.RequestError |
| paddy@4 | 94 var code int |
| paddy@4 | 95 switch err { |
| paddy@4 | 96 case subscriptions.ErrSubscriptionAlreadyExists: |
| paddy@4 | 97 rErr = api.RequestError{Slug: api.RequestErrConflict, Field: "/user_id"} |
| paddy@4 | 98 code = http.StatusBadRequest |
| paddy@4 | 99 case subscriptions.ErrStripeSubscriptionAlreadyExists: |
| paddy@4 | 100 rErr = api.RequestError{Slug: api.RequestErrConflict, Field: "/stripe_token"} |
| paddy@4 | 101 code = http.StatusBadRequest |
| paddy@4 | 102 default: |
| paddy@4 | 103 rErr = api.RequestError{Slug: api.RequestErrActOfGod} |
| paddy@4 | 104 code = http.StatusInternalServerError |
| paddy@4 | 105 } |
| paddy@4 | 106 api.Encode(w, r, code, Response{Errors: []api.RequestError{rErr}}) |
| paddy@4 | 107 return |
| paddy@4 | 108 } |
| paddy@4 | 109 resp := Response{Subscriptions: []subscriptions.Subscription{sub}} |
| paddy@4 | 110 api.Encode(w, r, http.StatusCreated, resp) |
| paddy@4 | 111 } |
| paddy@4 | 112 |
| paddy@4 | 113 func GetSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) { |
| paddy@4 | 114 store, err := getSubscriptionStore(c) |
| paddy@4 | 115 if err != nil { |
| paddy@4 | 116 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) |
| paddy@4 | 117 return |
| paddy@4 | 118 } |
| paddy@4 | 119 vars := trout.RequestVars(r) |
| paddy@4 | 120 rawID := vars.Get("id") |
| paddy@4 | 121 if rawID == "" { |
| paddy@4 | 122 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrMissing, Param: "id"}}}) |
| paddy@4 | 123 return |
| paddy@4 | 124 } |
| paddy@4 | 125 id, err := uuid.Parse(rawID) |
| paddy@4 | 126 if err != nil { |
| paddy@4 | 127 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}}) |
| paddy@4 | 128 return |
| paddy@4 | 129 } |
| paddy@6 | 130 if !api.CheckScopes(r, ScopeSubscription.ID) { |
| paddy@4 | 131 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}}) |
| paddy@4 | 132 return |
| paddy@4 | 133 } |
| paddy@4 | 134 userID, err := api.AuthUser(r) |
| paddy@4 | 135 if err != nil { |
| paddy@4 | 136 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}}) |
| paddy@4 | 137 return |
| paddy@4 | 138 } |
| paddy@6 | 139 if !id.Equal(userID) && !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) { |
| paddy@4 | 140 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}}) |
| paddy@4 | 141 return |
| paddy@4 | 142 } |
| paddy@4 | 143 subs, err := store.GetSubscriptions([]uuid.ID{id}) |
| paddy@4 | 144 if err != nil { |
| paddy@4 | 145 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) |
| paddy@4 | 146 return |
| paddy@4 | 147 } |
| paddy@4 | 148 sub, ok := subs[id.String()] |
| paddy@4 | 149 if !ok { |
| paddy@4 | 150 api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}}) |
| paddy@4 | 151 return |
| paddy@4 | 152 } |
| paddy@4 | 153 resp := Response{Subscriptions: []subscriptions.Subscription{sub}} |
| paddy@4 | 154 api.Encode(w, r, http.StatusOK, resp) |
| paddy@4 | 155 } |
| paddy@6 | 156 |
| paddy@6 | 157 func PatchSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) { |
| paddy@6 | 158 store, err := getSubscriptionStore(c) |
| paddy@6 | 159 if err != nil { |
| paddy@6 | 160 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) |
| paddy@6 | 161 return |
| paddy@6 | 162 } |
| paddy@6 | 163 stripe, err := getStripeClient(c) |
| paddy@6 | 164 if err != nil { |
| paddy@6 | 165 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) |
| paddy@6 | 166 return |
| paddy@6 | 167 } |
| paddy@6 | 168 if !api.CheckScopes(r, ScopeSubscription.ID) { |
| paddy@6 | 169 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}}) |
| paddy@6 | 170 return |
| paddy@6 | 171 } |
| paddy@6 | 172 userID, err := api.AuthUser(r) |
| paddy@6 | 173 if err != nil { |
| paddy@6 | 174 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}}) |
| paddy@6 | 175 return |
| paddy@6 | 176 } |
| paddy@6 | 177 vars := trout.RequestVars(r) |
| paddy@6 | 178 rawID := vars.Get("id") |
| paddy@6 | 179 if rawID == "" { |
| paddy@6 | 180 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrMissing, Param: "id"}}}) |
| paddy@6 | 181 return |
| paddy@6 | 182 } |
| paddy@6 | 183 id, err := uuid.Parse(rawID) |
| paddy@6 | 184 if err != nil { |
| paddy@6 | 185 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}}) |
| paddy@6 | 186 return |
| paddy@6 | 187 } |
| paddy@6 | 188 |
| paddy@6 | 189 var req subscriptions.SubscriptionChange |
| paddy@6 | 190 err = api.Decode(r, &req) |
| paddy@6 | 191 if err != nil { |
| paddy@6 | 192 api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError}) |
| paddy@6 | 193 return |
| paddy@6 | 194 } |
| paddy@6 | 195 // BUG(paddy): Need to validate the request when updating a subscription |
| paddy@6 | 196 |
| paddy@6 | 197 // only admin users can update the system-controlled properties |
| paddy@11 | 198 changedSysProps := changingSystemProperties(req) |
| paddy@6 | 199 if len(changedSysProps) > 0 && !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) { |
| paddy@6 | 200 errs := make([]api.RequestError, len(changedSysProps)) |
| paddy@6 | 201 for pos, prop := range changedSysProps { |
| paddy@6 | 202 errs[pos] = api.RequestError{Slug: api.RequestErrAccessDenied, Field: prop} |
| paddy@6 | 203 } |
| paddy@6 | 204 api.Encode(w, r, http.StatusBadRequest, Response{Errors: errs}) |
| paddy@6 | 205 return |
| paddy@6 | 206 } |
| paddy@6 | 207 |
| paddy@6 | 208 subs, err := store.GetSubscriptions([]uuid.ID{userID}) |
| paddy@6 | 209 if err != nil { |
| paddy@6 | 210 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) |
| paddy@6 | 211 return |
| paddy@6 | 212 } |
| paddy@6 | 213 sub, ok := subs[userID.String()] |
| paddy@6 | 214 if !ok { |
| paddy@6 | 215 api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}}) |
| paddy@6 | 216 return |
| paddy@6 | 217 } |
| paddy@6 | 218 stripeSub, err := subscriptions.UpdateStripeSubscription(sub.StripeSubscription, req.Plan, req.StripeSource, stripe) |
| paddy@6 | 219 if err != nil { |
| paddy@6 | 220 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) |
| paddy@6 | 221 return |
| paddy@6 | 222 } |
| paddy@6 | 223 change := subscriptions.StripeSubscriptionChange(sub, *stripeSub) |
| paddy@6 | 224 if change.IsEmpty() { |
| paddy@6 | 225 resp := Response{Subscriptions: []subscriptions.Subscription{sub}} |
| paddy@6 | 226 api.Encode(w, r, http.StatusOK, resp) |
| paddy@6 | 227 return |
| paddy@6 | 228 } |
| paddy@6 | 229 err = store.UpdateSubscription(id, change) |
| paddy@6 | 230 if err != nil { |
| paddy@6 | 231 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) |
| paddy@6 | 232 return |
| paddy@6 | 233 } |
| paddy@6 | 234 sub.ApplyChange(change) |
| paddy@6 | 235 resp := Response{Subscriptions: []subscriptions.Subscription{sub}} |
| paddy@6 | 236 api.Encode(w, r, http.StatusOK, resp) |
| paddy@6 | 237 } |