Skip to content

Commit 23b821f

Browse files
authored
Merge pull request #182 from netlify/feature/stripe-2step
Support Strong Customer Authentication using Stripe with PaymentIntent
2 parents 6cf6d08 + 388e235 commit 23b821f

File tree

8 files changed

+477
-90
lines changed

8 files changed

+477
-90
lines changed

api/api.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,11 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
116116
})
117117

118118
r.Route("/payments", func(r *router) {
119-
r.Use(adminRequired)
120-
121-
r.Get("/", api.PaymentList)
119+
r.With(adminRequired).Get("/", api.PaymentList)
122120
r.Route("/{payment_id}", func(r *router) {
123-
r.Get("/", api.PaymentView)
124-
r.With(addGetBody).Post("/refund", api.PaymentRefund)
121+
r.With(adminRequired).Get("/", api.PaymentView)
122+
r.With(adminRequired).With(addGetBody).Post("/refund", api.PaymentRefund)
123+
r.Post("/confirm", api.PaymentConfirm)
125124
})
126125
})
127126

api/payments.go

Lines changed: 133 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,45 @@ func (a *API) PaymentListForOrder(w http.ResponseWriter, r *http.Request) error
8080
return sendJSON(w, http.StatusOK, order.Transactions)
8181
}
8282

83-
// PaymentCreate is the endpoint for creating a payment for an order
84-
func (a *API) PaymentCreate(w http.ResponseWriter, r *http.Request) error {
83+
func paymentComplete(r *http.Request, tx *gorm.DB, tr *models.Transaction, order *models.Order) {
8584
ctx := r.Context()
8685
log := getLogEntry(r)
8786
config := gcontext.GetConfig(ctx)
87+
88+
tr.Status = models.PaidState
89+
if tx.NewRecord(tr) {
90+
tx.Create(tr)
91+
} else {
92+
tx.Save(tr)
93+
}
94+
order.PaymentState = models.PaidState
95+
tx.Save(order)
96+
97+
if config.Webhooks.Payment != "" {
98+
hook, err := models.NewHook("payment", config.SiteURL, config.Webhooks.Payment, order.UserID, config.Webhooks.Secret, order)
99+
if err != nil {
100+
log.WithError(err).Error("Failed to process webhook")
101+
}
102+
tx.Save(hook)
103+
}
104+
}
105+
106+
func sendOrderConfirmation(ctx context.Context, log logrus.FieldLogger, tr *models.Transaction) {
88107
mailer := gcontext.GetMailer(ctx)
89108

109+
err1 := mailer.OrderConfirmationMail(tr)
110+
err2 := mailer.OrderReceivedMail(tr)
111+
112+
if err1 != nil || err2 != nil {
113+
log.Errorf("Error sending order confirmation mails: %v %v", err1, err2)
114+
}
115+
}
116+
117+
// PaymentCreate is the endpoint for creating a payment for an order
118+
func (a *API) PaymentCreate(w http.ResponseWriter, r *http.Request) error {
119+
ctx := r.Context()
120+
log := getLogEntry(r)
121+
90122
params := PaymentParams{Currency: "USD"}
91123
err := json.NewDecoder(r.Body).Decode(&params)
92124
if err != nil {
@@ -100,7 +132,7 @@ func (a *API) PaymentCreate(w http.ResponseWriter, r *http.Request) error {
100132
if provider == nil {
101133
return badRequestError("Payment provider '%s' not configured", params.ProviderType)
102134
}
103-
charge, err := provider.NewCharger(ctx, r)
135+
charge, err := provider.NewCharger(ctx, r, log.WithField("component", "payment_provider"))
104136
if err != nil {
105137
return badRequestError("Error creating payment provider: %v", err)
106138
}
@@ -156,18 +188,33 @@ func (a *API) PaymentCreate(w http.ResponseWriter, r *http.Request) error {
156188
return internalServerError("We failed to authorize the amount for this order: %v", err)
157189
}
158190

159-
invoiceNumber, err := models.NextInvoiceNumber(tx, order.InstanceID)
160-
if err != nil {
161-
tx.Rollback()
162-
return internalServerError("We failed to generate a valid invoice ID, please try again later: %v", err)
191+
invoiceNumber := order.InvoiceNumber
192+
if invoiceNumber == 0 {
193+
var err error
194+
invoiceNumber, err = models.NextInvoiceNumber(tx, order.InstanceID)
195+
if err != nil {
196+
tx.Rollback()
197+
return internalServerError("We failed to generate a valid invoice ID, please try again later: %v", err)
198+
}
199+
order.InvoiceNumber = invoiceNumber
163200
}
164201

165202
tr := models.NewTransaction(order)
166203
processorID, err := charge(params.Amount, params.Currency, order, invoiceNumber)
167204
tr.ProcessorID = processorID
168205
tr.InvoiceNumber = invoiceNumber
206+
order.PaymentProcessor = provider.Name()
169207

170208
if err != nil {
209+
if pendingErr, ok := err.(*payments.PaymentPendingError); ok {
210+
tr.Status = models.PendingState
211+
tr.ProviderMetadata = pendingErr.Metadata()
212+
tx.Create(tr)
213+
tx.Save(order)
214+
tx.Commit()
215+
return sendJSON(w, 200, tr)
216+
}
217+
171218
tr.FailureCode = strconv.FormatInt(http.StatusInternalServerError, 10)
172219
tr.FailureDescription = err.Error()
173220
tr.Status = models.FailedState
@@ -176,34 +223,88 @@ func (a *API) PaymentCreate(w http.ResponseWriter, r *http.Request) error {
176223
return internalServerError("There was an error charging your card: %v", err).WithInternalError(err)
177224
}
178225

179-
// mark order and transaction as paid
180-
tr.Status = models.PaidState
181-
tx.Create(tr)
182-
order.PaymentProcessor = provider.Name()
183-
order.PaymentState = models.PaidState
184-
order.InvoiceNumber = invoiceNumber
185-
tx.Save(order)
226+
paymentComplete(r, tx, tr, order)
227+
if err := tx.Commit().Error; err != nil {
228+
return internalServerError("Saving payment failed").WithInternalError(err)
229+
}
186230

187-
if config.Webhooks.Payment != "" {
188-
hook, err := models.NewHook("payment", config.SiteURL, config.Webhooks.Payment, order.UserID, config.Webhooks.Secret, order)
189-
if err != nil {
190-
log.WithError(err).Error("Failed to process webhook")
231+
go sendOrderConfirmation(ctx, log, tr)
232+
233+
return sendJSON(w, http.StatusOK, tr)
234+
}
235+
236+
// PaymentConfirm allows client to confirm if a pending transaction has been completed. Updates transaction and order
237+
func (a *API) PaymentConfirm(w http.ResponseWriter, r *http.Request) error {
238+
ctx := r.Context()
239+
log := getLogEntry(r)
240+
241+
payID := chi.URLParam(r, "payment_id")
242+
trans, httpErr := a.getTransaction(payID)
243+
if httpErr != nil {
244+
return httpErr
245+
}
246+
247+
if trans.UserID != "" {
248+
token := gcontext.GetToken(ctx)
249+
if token == nil {
250+
return unauthorizedError("You must be logged in to confirm this payment")
251+
}
252+
claims := token.Claims.(*claims.JWTClaims)
253+
if trans.UserID != claims.Subject {
254+
return unauthorizedError("You must be logged in to confirm this payment")
191255
}
192-
tx.Save(hook)
193256
}
194257

195-
tx.Commit()
258+
if trans.Status == models.PaidState {
259+
return sendJSON(w, http.StatusOK, trans)
260+
}
196261

197-
go func() {
198-
err1 := mailer.OrderConfirmationMail(tr)
199-
err2 := mailer.OrderReceivedMail(tr)
262+
order := &models.Order{}
263+
if rsp := a.db.Find(order, "id = ?", trans.OrderID); rsp.Error != nil {
264+
if rsp.RecordNotFound() {
265+
return notFoundError("Order not found")
266+
}
267+
return internalServerError("Error while querying for order").WithInternalError(rsp.Error)
268+
}
269+
if order.PaymentProcessor == "" {
270+
return badRequestError("Order does not specify a payment provider")
271+
}
272+
273+
provider := gcontext.GetPaymentProviders(ctx)[order.PaymentProcessor]
274+
if provider == nil {
275+
return badRequestError("Payment provider '%s' not configured", order.PaymentProcessor)
276+
}
277+
confirm, err := provider.NewConfirmer(ctx, r, log.WithField("component", "payment_provider"))
278+
if err != nil {
279+
return badRequestError("Error creating payment provider: %v", err)
280+
}
200281

201-
if err1 != nil || err2 != nil {
202-
log.Errorf("Error sending order confirmation mails: %v %v", err1, err2)
282+
if err := confirm(trans.ProcessorID); err != nil {
283+
if confirmFail, ok := err.(*payments.PaymentConfirmFailError); ok {
284+
return badRequestError("Error confirming payment: %s", confirmFail.Error())
203285
}
204-
}()
286+
return internalServerError("Error on provider while trying to confirm: %v. Try again later.", err)
287+
}
205288

206-
return sendJSON(w, http.StatusOK, tr)
289+
tx := a.db.Begin()
290+
291+
if trans.InvoiceNumber == 0 {
292+
invoiceNumber, err := models.NextInvoiceNumber(tx, order.InstanceID)
293+
if err != nil {
294+
tx.Rollback()
295+
return internalServerError("We failed to generate a valid invoice ID, please try again later: %v", err)
296+
}
297+
trans.InvoiceNumber = invoiceNumber
298+
}
299+
300+
paymentComplete(r, tx, trans, order)
301+
if err := tx.Commit().Error; err != nil {
302+
return internalServerError("Saving payment failed").WithInternalError(err)
303+
}
304+
305+
go sendOrderConfirmation(ctx, log, trans)
306+
307+
return sendJSON(w, http.StatusOK, trans)
207308
}
208309

209310
// PaymentList will list all the payments that meet the criteria. It is only available to admins.
@@ -280,7 +381,7 @@ func (a *API) PaymentRefund(w http.ResponseWriter, r *http.Request) error {
280381
if provider == nil {
281382
return badRequestError("Payment provider '%s' not configured", order.PaymentProcessor)
282383
}
283-
refund, err := provider.NewRefunder(ctx, r)
384+
refund, err := provider.NewRefunder(ctx, r, log.WithField("component", "payment_provider"))
284385
if err != nil {
285386
return badRequestError("Error creating payment provider: %v", err)
286387
}
@@ -328,6 +429,7 @@ func (a *API) PaymentRefund(w http.ResponseWriter, r *http.Request) error {
328429
// PreauthorizePayment creates a new payment that can be authorized in the browser
329430
func (a *API) PreauthorizePayment(w http.ResponseWriter, r *http.Request) error {
330431
ctx := r.Context()
432+
log := getLogEntry(r)
331433
params := PaymentParams{}
332434
ct := r.Header.Get("Content-Type")
333435
mediaType, _, err := mime.ParseMediaType(ct)
@@ -362,7 +464,7 @@ func (a *API) PreauthorizePayment(w http.ResponseWriter, r *http.Request) error
362464
if provider == nil {
363465
return badRequestError("Payment provider '%s' not configured", providerType)
364466
}
365-
preauthorize, err := provider.NewPreauthorizer(ctx, r)
467+
preauthorize, err := provider.NewPreauthorizer(ctx, r, log.WithField("component", "payment_provider"))
366468
if err != nil {
367469
return badRequestError("Error creating payment provider: %v", err)
368470
}
@@ -426,7 +528,8 @@ func createPaymentProviders(c *conf.Configuration) (map[string]payments.Provider
426528
provs := map[string]payments.Provider{}
427529
if c.Payment.Stripe.Enabled {
428530
p, err := stripe.NewPaymentProvider(stripe.Config{
429-
SecretKey: c.Payment.Stripe.SecretKey,
531+
SecretKey: c.Payment.Stripe.SecretKey,
532+
UsePaymentIntents: c.Payment.Stripe.UsePaymentIntents,
430533
})
431534
if err != nil {
432535
return nil, err

0 commit comments

Comments
 (0)