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