ducky/subscriptions
ducky/subscriptions/api/subscription_handlers.go
Add comments, move ChangingSystemProperties to the api package. Add comments to all our exported types and variables in subscription.go, both to make golint happy and because it's good to have comments. Move the subscriptions.ChangingSystemProperties helper to api.changingSystemProperties, because it returns API-specific strings and there's no real reason it has to be in the subscriptions package--everything it needs to work on is exported.
| 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 } |