Skip to content

Commit 1fcc5aa

Browse files
committed
Support 2-step payments in Stripe
Enable via payment.stripe.use_payment_intents
1 parent 9b7badd commit 1fcc5aa

File tree

8 files changed

+234
-27
lines changed

8 files changed

+234
-27
lines changed

api/api.go

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

9494
r.Route("/payments", func(r *router) {
95-
r.Use(adminRequired)
96-
97-
r.Get("/", api.PaymentList)
95+
r.With(adminRequired).Get("/", api.PaymentList)
9896
r.Route("/{payment_id}", func(r *router) {
99-
r.Get("/", api.PaymentView)
100-
r.With(addGetBody).Post("/refund", api.PaymentRefund)
97+
r.With(adminRequired).Get("/", api.PaymentView)
98+
r.With(adminRequired).With(addGetBody).Post("/refund", api.PaymentRefund)
99+
r.Post("/confirm", api.PaymentConfirm)
101100
})
102101
})
103102

api/payments.go

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func (a *API) PaymentCreate(w http.ResponseWriter, r *http.Request) error {
100100
if provider == nil {
101101
return badRequestError("Payment provider '%s' not configured", params.ProviderType)
102102
}
103-
charge, err := provider.NewCharger(ctx, r)
103+
charge, err := provider.NewCharger(ctx, r, log.WithField("component", "payment_provider"))
104104
if err != nil {
105105
return badRequestError("Error creating payment provider: %v", err)
106106
}
@@ -167,6 +167,14 @@ func (a *API) PaymentCreate(w http.ResponseWriter, r *http.Request) error {
167167
tr.ProcessorID = processorID
168168
tr.InvoiceNumber = invoiceNumber
169169

170+
if pendingErr, ok := err.(*payments.PaymentPendingError); ok {
171+
tr.Status = models.PendingState
172+
tr.ProviderMetadata = pendingErr.Metadata()
173+
tx.Create(tr)
174+
tx.Commit()
175+
return sendJSON(w, 200, tr)
176+
}
177+
170178
if err != nil {
171179
tr.FailureCode = strconv.FormatInt(http.StatusInternalServerError, 10)
172180
tr.FailureDescription = err.Error()
@@ -206,6 +214,75 @@ func (a *API) PaymentCreate(w http.ResponseWriter, r *http.Request) error {
206214
return sendJSON(w, http.StatusOK, tr)
207215
}
208216

217+
// PaymentConfirm allows client to confirm if a pending transaction has been completed. Updates transaction and order
218+
func (a *API) PaymentConfirm(w http.ResponseWriter, r *http.Request) error {
219+
ctx := r.Context()
220+
221+
payID := chi.URLParam(r, "payment_id")
222+
trans, httpErr := a.getTransaction(payID)
223+
if httpErr != nil {
224+
return httpErr
225+
}
226+
227+
if trans.UserID != "" {
228+
token := gcontext.GetToken(ctx)
229+
if token == nil {
230+
return unauthorizedError("You must be logged in to confirm this payment")
231+
}
232+
claims := token.Claims.(*claims.JWTClaims)
233+
if trans.UserID != claims.Subject {
234+
return unauthorizedError("You must be logged in to confirm this payment")
235+
}
236+
}
237+
238+
log := getLogEntry(r)
239+
order, httpErr := queryForOrder(a.db, trans.OrderID, log)
240+
if httpErr != nil {
241+
return httpErr
242+
}
243+
if order.PaymentProcessor == "" {
244+
return badRequestError("Order does not specify a payment provider")
245+
}
246+
247+
provider := gcontext.GetPaymentProviders(ctx)[order.PaymentProcessor]
248+
if provider == nil {
249+
return badRequestError("Payment provider '%s' not configured", order.PaymentProcessor)
250+
}
251+
confirm, err := provider.NewConfirmer(ctx, r, log.WithField("component", "payment_provider"))
252+
if err != nil {
253+
return badRequestError("Error creating payment provider: %v", err)
254+
}
255+
256+
if err := confirm(trans.ProcessorID); err != nil {
257+
if confirmFail, ok := err.(*payments.PaymentConfirmFailError); ok {
258+
return badRequestError("Error confirming payment: %s", confirmFail.Error())
259+
}
260+
return internalServerError("Error on provider while trying to confirm: %v. Try again later.", err)
261+
}
262+
263+
tx := a.db.Begin()
264+
265+
if trans.InvoiceNumber == 0 {
266+
invoiceNumber, err := models.NextInvoiceNumber(tx, order.InstanceID)
267+
if err != nil {
268+
tx.Rollback()
269+
return internalServerError("We failed to generate a valid invoice ID, please try again later: %v", err)
270+
}
271+
trans.InvoiceNumber = invoiceNumber
272+
}
273+
274+
trans.Status = models.PaidState
275+
tx.Save(trans)
276+
order.State = models.PaidState
277+
order.InvoiceNumber = trans.InvoiceNumber
278+
tx.Save(order)
279+
if err := tx.Commit().Error; err != nil {
280+
return internalServerError("Saving payment failed").WithInternalError(err)
281+
}
282+
283+
return sendJSON(w, 200, trans)
284+
}
285+
209286
// PaymentList will list all the payments that meet the criteria. It is only available to admins.
210287
func (a *API) PaymentList(w http.ResponseWriter, r *http.Request) error {
211288
log := getLogEntry(r)
@@ -280,7 +357,7 @@ func (a *API) PaymentRefund(w http.ResponseWriter, r *http.Request) error {
280357
if provider == nil {
281358
return badRequestError("Payment provider '%s' not configured", order.PaymentProcessor)
282359
}
283-
refund, err := provider.NewRefunder(ctx, r)
360+
refund, err := provider.NewRefunder(ctx, r, log.WithField("component", "payment_provider"))
284361
if err != nil {
285362
return badRequestError("Error creating payment provider: %v", err)
286363
}
@@ -328,6 +405,7 @@ func (a *API) PaymentRefund(w http.ResponseWriter, r *http.Request) error {
328405
// PreauthorizePayment creates a new payment that can be authorized in the browser
329406
func (a *API) PreauthorizePayment(w http.ResponseWriter, r *http.Request) error {
330407
ctx := r.Context()
408+
log := getLogEntry(r)
331409
params := PaymentParams{}
332410
ct := r.Header.Get("Content-Type")
333411
mediaType, _, err := mime.ParseMediaType(ct)
@@ -362,7 +440,7 @@ func (a *API) PreauthorizePayment(w http.ResponseWriter, r *http.Request) error
362440
if provider == nil {
363441
return badRequestError("Payment provider '%s' not configured", providerType)
364442
}
365-
preauthorize, err := provider.NewPreauthorizer(ctx, r)
443+
preauthorize, err := provider.NewPreauthorizer(ctx, r, log.WithField("component", "payment_provider"))
366444
if err != nil {
367445
return badRequestError("Error creating payment provider: %v", err)
368446
}
@@ -426,7 +504,8 @@ func createPaymentProviders(c *conf.Configuration) (map[string]payments.Provider
426504
provs := map[string]payments.Provider{}
427505
if c.Payment.Stripe.Enabled {
428506
p, err := stripe.NewPaymentProvider(stripe.Config{
429-
SecretKey: c.Payment.Stripe.SecretKey,
507+
SecretKey: c.Payment.Stripe.SecretKey,
508+
UsePaymentIntents: c.Payment.Stripe.UsePaymentIntents,
430509
})
431510
if err != nil {
432511
return nil, err

api/payments_test.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"testing"
1111

1212
"github.com/mitchellh/mapstructure"
13+
"github.com/sirupsen/logrus"
1314
"github.com/stretchr/testify/assert"
1415
"github.com/stretchr/testify/require"
1516

@@ -659,15 +660,18 @@ type refundCall struct {
659660
func (mp *memProvider) Name() string {
660661
return mp.name
661662
}
662-
func (mp *memProvider) NewCharger(ctx context.Context, r *http.Request) (payments.Charger, error) {
663+
func (mp *memProvider) NewCharger(ctx context.Context, r *http.Request, log logrus.FieldLogger) (payments.Charger, error) {
663664
return mp.charge, nil
664665
}
665-
func (mp *memProvider) NewRefunder(ctx context.Context, r *http.Request) (payments.Refunder, error) {
666+
func (mp *memProvider) NewRefunder(ctx context.Context, r *http.Request, log logrus.FieldLogger) (payments.Refunder, error) {
666667
return mp.refund, nil
667668
}
668-
func (mp *memProvider) NewPreauthorizer(ctx context.Context, r *http.Request) (payments.Preauthorizer, error) {
669+
func (mp *memProvider) NewPreauthorizer(ctx context.Context, r *http.Request, log logrus.FieldLogger) (payments.Preauthorizer, error) {
669670
return mp.preauthorize, nil
670671
}
672+
func (mp *memProvider) NewConfirmer(ctx context.Context, r *http.Request, log logrus.FieldLogger) (payments.Confirmer, error) {
673+
return mp.confirm, nil
674+
}
671675

672676
func (mp *memProvider) charge(amount uint64, currency string, order *models.Order, invoiceNumber int64) (string, error) {
673677
return "", errors.New("Shouldn't have called this")
@@ -690,6 +694,10 @@ func (mp *memProvider) preauthorize(amount uint64, currency string, description
690694
return nil, nil
691695
}
692696

697+
func (mp *memProvider) confirm(paymentID string) error {
698+
return nil
699+
}
700+
693701
type stripeCallFunc func(method, path, key string, params stripe.ParamsContainer, v interface{})
694702

695703
func NewTrackingStripeBackend(fn stripeCallFunc) stripe.Backend {

conf/configuration.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ type Configuration struct {
6868
Enabled bool `json:"enabled"`
6969
PublicKey string `json:"public_key" split_words:"true"`
7070
SecretKey string `json:"secret_key" split_words:"true"`
71+
72+
UsePaymentIntents bool `json:"use_payment_intents" split_words:"true"`
7173
} `json:"stripe"`
7274
PayPal struct {
7375
Enabled bool `json:"enabled"`

models/transaction.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ type Transaction struct {
3737

3838
CreatedAt time.Time `json:"created_at"`
3939
DeletedAt *time.Time `json:"-"`
40+
41+
ProviderMetadata map[string]interface{} `json:"provider_metadata,omitempty" sql:"-"`
4042
}
4143

4244
// TableName returns the database table name for the Transaction model.

payments/payments.go

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net/http"
66

77
"github.com/netlify/gocommerce/models"
8+
"github.com/sirupsen/logrus"
89
)
910

1011
const (
@@ -18,9 +19,10 @@ const (
1819
// preauthorize payments.
1920
type Provider interface {
2021
Name() string
21-
NewCharger(ctx context.Context, r *http.Request) (Charger, error)
22-
NewRefunder(ctx context.Context, r *http.Request) (Refunder, error)
23-
NewPreauthorizer(ctx context.Context, r *http.Request) (Preauthorizer, error)
22+
NewCharger(ctx context.Context, r *http.Request, log logrus.FieldLogger) (Charger, error)
23+
NewRefunder(ctx context.Context, r *http.Request, log logrus.FieldLogger) (Refunder, error)
24+
NewPreauthorizer(ctx context.Context, r *http.Request, log logrus.FieldLogger) (Preauthorizer, error)
25+
NewConfirmer(ctx context.Context, r *http.Request, log logrus.FieldLogger) (Confirmer, error)
2426
}
2527

2628
// Charger wraps the Charge method which creates new payments with the provider.
@@ -37,3 +39,41 @@ type Preauthorizer func(amount uint64, currency string, description string) (*Pr
3739
type PreauthorizationResult struct {
3840
ID string `json:"id"`
3941
}
42+
43+
// Confirmer wraps a confirm method used for checking two-step payments in a synchronous flow
44+
type Confirmer func(paymentID string) error
45+
46+
// PaymentPendingError is returned when the payment provider requests additional action
47+
// e.g. 2-step authorization through 3D secure
48+
type PaymentPendingError struct {
49+
metadata map[string]interface{}
50+
}
51+
52+
// NewPaymentPendingError creates an error for a pending action on a payment
53+
func NewPaymentPendingError(metadata map[string]interface{}) error {
54+
return &PaymentPendingError{metadata}
55+
}
56+
57+
func (p *PaymentPendingError) Error() string {
58+
return "The payment provider requested additional actions on the transaction."
59+
}
60+
61+
// Metadata returns fields that should be passed to the client
62+
// for use in additional actions
63+
func (p *PaymentPendingError) Metadata() map[string]interface{} {
64+
return p.metadata
65+
}
66+
67+
// PaymentConfirmFailError is returned when the confirmation request got a negative response
68+
type PaymentConfirmFailError struct {
69+
message string
70+
}
71+
72+
// NewPaymentConfirmFailError creates an error to use when a payment confirmation fails
73+
func NewPaymentConfirmFailError(msg string) error {
74+
return &PaymentConfirmFailError{message: msg}
75+
}
76+
77+
func (p *PaymentConfirmFailError) Error() string {
78+
return p.message
79+
}

payments/paypal/paypal.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/netlify/gocommerce/models"
1313
"github.com/pariz/gountries"
14+
"github.com/sirupsen/logrus"
1415

1516
paypalsdk "github.com/netlify/PayPal-Go-SDK"
1617
"github.com/netlify/gocommerce/conf"
@@ -76,7 +77,7 @@ func (p *paypalPaymentProvider) Name() string {
7677
return payments.PayPalProvider
7778
}
7879

79-
func (p *paypalPaymentProvider) NewCharger(ctx context.Context, r *http.Request) (payments.Charger, error) {
80+
func (p *paypalPaymentProvider) NewCharger(ctx context.Context, r *http.Request, log logrus.FieldLogger) (payments.Charger, error) {
8081
var bp paypalBodyParams
8182
bod, err := r.GetBody()
8283
if err != nil {
@@ -193,7 +194,7 @@ func (p *paypalPaymentProvider) charge(paymentID string, userID string, amount u
193194
return executeResult.ID, nil
194195
}
195196

196-
func (p *paypalPaymentProvider) NewRefunder(ctx context.Context, r *http.Request) (payments.Refunder, error) {
197+
func (p *paypalPaymentProvider) NewRefunder(ctx context.Context, r *http.Request, log logrus.FieldLogger) (payments.Refunder, error) {
197198
return p.refund, nil
198199
}
199200

@@ -209,7 +210,7 @@ func (p *paypalPaymentProvider) refund(transactionID string, amount uint64, curr
209210
return ref.ID, nil
210211
}
211212

212-
func (p *paypalPaymentProvider) NewPreauthorizer(ctx context.Context, r *http.Request) (payments.Preauthorizer, error) {
213+
func (p *paypalPaymentProvider) NewPreauthorizer(ctx context.Context, r *http.Request, log logrus.FieldLogger) (payments.Preauthorizer, error) {
213214
config := gcontext.GetConfig(ctx)
214215
return func(amount uint64, currency string, description string) (*payments.PreauthorizationResult, error) {
215216
return p.preauthorize(config, amount, currency, description)
@@ -278,3 +279,7 @@ func (p *paypalPaymentProvider) getExperience() (*paypalsdk.WebProfile, error) {
278279
func formatAmount(amount uint64) string {
279280
return strconv.FormatFloat(float64(amount)/100, 'f', 2, 64)
280281
}
282+
283+
func (p *paypalPaymentProvider) NewConfirmer(ctx context.Context, r *http.Request, log logrus.FieldLogger) (payments.Confirmer, error) {
284+
return nil, errors.New("Paypal does not provide manual 2-step confirmation")
285+
}

0 commit comments

Comments
 (0)