ducky/subscriptions
ducky/subscriptions/api/subscription_handlers.go
Return errors from responses in client. When the client makes a request, non-200 responses _are not_ considered errors. So we need to check the response.Errors property, and if it has errors, _then_ we consider the request to have an error. To make this happen, we created an httpErrors type that fulfills the error interface and just wraps the response Errors property. Then callers can type-cast it and interrogate it.
| 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 } |