ducky/subscriptions

Paddy 2015-07-13 Parent:c4cfceb2f2fb Child:0ae1ff0ee306

7:9e138933e4ce Go to Latest

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.

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