ducky/subscriptions
ducky/subscriptions/api/subscription_handlers.go
Create a client for working with subscriptions. We mostly copied our code.secondbit.org/auth.hg/client package to create a simple client library for communicating with our Subscriptions API. Right now, the client only has support for creating a subscription. It remains untested, but it builds.
| 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 } |