ducky/subscriptions

Paddy 2015-10-04 Parent:b063bc0a6e84

17:7eef47ecc01c Go to Latest

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.

History
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 }