ducky/subscriptions
ducky/subscriptions/api/subscription_handlers.go
Add golint comments. Comment on some more of our exported types, functions, and variables, both to make golint happy and because uncommented code never ever ends well.
1 package api
3 import (
4 "net/http"
6 "code.secondbit.org/api.hg"
7 "code.secondbit.org/auth.hg"
8 "code.secondbit.org/trout.hg"
9 "code.secondbit.org/uuid.hg"
11 "code.secondbit.org/ducky/subscriptions.hg"
13 "golang.org/x/net/context"
14 )
16 var (
17 ScopeSubscription = auth.Scope{ID: "subscriptions", Name: "Manage Subscriptions", Description: "Read and update your subscription information."}
18 ScopeSubscriptionAdmin = auth.Scope{ID: "subscriptions_admin", Name: "Administer Subscriptions", Description: "Read and update subscription information, bypassing ACL."}
19 )
21 // changingSystemProperties takes a SubscriptionChange and returns a
22 // slice of JSON pointers to the properties that are changing but are
23 // also designated as "system properties" in the API. If no properties
24 // meet these criteria, an empty slice is returned.
25 func changingSystemProperties(change subscriptions.SubscriptionChange) []string {
26 var changes []string
27 if change.StripeSubscription != nil {
28 changes = append(changes, "/stripe_subscription")
29 }
30 if change.Status != nil {
31 changes = append(changes, "/status")
32 }
33 if change.TrialStart != nil {
34 changes = append(changes, "/trial_start")
35 }
36 if change.TrialEnd != nil {
37 changes = append(changes, "/trial_end")
38 }
39 if change.PeriodStart != nil {
40 changes = append(changes, "/period_start")
41 }
42 if change.PeriodEnd != nil {
43 changes = append(changes, "/period_end")
44 }
45 if change.CanceledAt != nil {
46 changes = append(changes, "/canceled_at")
47 }
48 if change.FailedChargeAttempts != nil {
49 changes = append(changes, "/failed_charge_attempts")
50 }
51 if change.LastFailedCharge != nil {
52 changes = append(changes, "/last_failed_charge")
53 }
54 if change.LastNotified != nil {
55 changes = append(changes, "/last_notified")
56 }
57 return changes
58 }
60 func HandleSubscriptions(router *trout.Router, c context.Context) {
61 router.Endpoint("/subscriptions").Methods("POST", "OPTIONS").Handler(
62 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, CreateSubscriptionHandler))))
63 router.Endpoint("/subscriptions/{id}").Methods("GET", "OPTIONS").Handler(
64 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, GetSubscriptionHandler))))
65 router.Endpoint("/subscriptions/{id}").Methods("PATCH", "OPTIONS").Handler(
66 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, PatchSubscriptionHandler))))
67 }
69 func CreateSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) {
70 store, err := getSubscriptionStore(c)
71 if err != nil {
72 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
73 return
74 }
75 stripe, err := getStripeClient(c)
76 if err != nil {
77 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
78 return
79 }
80 if !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) {
81 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
82 return
83 }
84 var req subscriptions.SubscriptionChange
85 err = api.Decode(r, &req)
86 if err != nil {
87 api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError})
88 return
89 }
90 // BUG(paddy): Need to validate the request when creating a subscription
91 sub, err := subscriptions.New(req, stripe, store)
92 if err != nil {
93 var rErr api.RequestError
94 var code int
95 switch err {
96 case subscriptions.ErrSubscriptionAlreadyExists:
97 rErr = api.RequestError{Slug: api.RequestErrConflict, Field: "/user_id"}
98 code = http.StatusBadRequest
99 case subscriptions.ErrStripeSubscriptionAlreadyExists:
100 rErr = api.RequestError{Slug: api.RequestErrConflict, Field: "/stripe_token"}
101 code = http.StatusBadRequest
102 default:
103 rErr = api.RequestError{Slug: api.RequestErrActOfGod}
104 code = http.StatusInternalServerError
105 }
106 api.Encode(w, r, code, Response{Errors: []api.RequestError{rErr}})
107 return
108 }
109 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
110 api.Encode(w, r, http.StatusCreated, resp)
111 }
113 func GetSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) {
114 store, err := getSubscriptionStore(c)
115 if err != nil {
116 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
117 return
118 }
119 vars := trout.RequestVars(r)
120 rawID := vars.Get("id")
121 if rawID == "" {
122 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrMissing, Param: "id"}}})
123 return
124 }
125 id, err := uuid.Parse(rawID)
126 if err != nil {
127 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}})
128 return
129 }
130 if !api.CheckScopes(r, ScopeSubscription.ID) {
131 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
132 return
133 }
134 userID, err := api.AuthUser(r)
135 if err != nil {
136 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
137 return
138 }
139 if !id.Equal(userID) && !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) {
140 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
141 return
142 }
143 subs, err := store.GetSubscriptions([]uuid.ID{id})
144 if err != nil {
145 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
146 return
147 }
148 sub, ok := subs[id.String()]
149 if !ok {
150 api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}})
151 return
152 }
153 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
154 api.Encode(w, r, http.StatusOK, resp)
155 }
157 func PatchSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) {
158 store, err := getSubscriptionStore(c)
159 if err != nil {
160 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
161 return
162 }
163 stripe, err := getStripeClient(c)
164 if err != nil {
165 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
166 return
167 }
168 if !api.CheckScopes(r, ScopeSubscription.ID) {
169 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
170 return
171 }
172 userID, err := api.AuthUser(r)
173 if err != nil {
174 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
175 return
176 }
177 vars := trout.RequestVars(r)
178 rawID := vars.Get("id")
179 if rawID == "" {
180 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrMissing, Param: "id"}}})
181 return
182 }
183 id, err := uuid.Parse(rawID)
184 if err != nil {
185 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}})
186 return
187 }
189 var req subscriptions.SubscriptionChange
190 err = api.Decode(r, &req)
191 if err != nil {
192 api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError})
193 return
194 }
195 // BUG(paddy): Need to validate the request when updating a subscription
197 // only admin users can update the system-controlled properties
198 changedSysProps := changingSystemProperties(req)
199 if len(changedSysProps) > 0 && !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) {
200 errs := make([]api.RequestError, len(changedSysProps))
201 for pos, prop := range changedSysProps {
202 errs[pos] = api.RequestError{Slug: api.RequestErrAccessDenied, Field: prop}
203 }
204 api.Encode(w, r, http.StatusBadRequest, Response{Errors: errs})
205 return
206 }
208 subs, err := store.GetSubscriptions([]uuid.ID{userID})
209 if err != nil {
210 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
211 return
212 }
213 sub, ok := subs[userID.String()]
214 if !ok {
215 api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}})
216 return
217 }
218 stripeSub, err := subscriptions.UpdateStripeSubscription(sub.StripeSubscription, req.Plan, req.StripeSource, stripe)
219 if err != nil {
220 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
221 return
222 }
223 change := subscriptions.StripeSubscriptionChange(sub, *stripeSub)
224 if change.IsEmpty() {
225 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
226 api.Encode(w, r, http.StatusOK, resp)
227 return
228 }
229 err = store.UpdateSubscription(id, change)
230 if err != nil {
231 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
232 return
233 }
234 sub.ApplyChange(change)
235 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
236 api.Encode(w, r, http.StatusOK, resp)
237 }