Permalink
Browse files

Pass additional info to PayPal and Stripe on checkout

For Stripe:
- Shipping address
- invoice number
- metadata (order id)

For PayPal:
- Items
- Shipping address
  • Loading branch information...
mraerino committed Nov 4, 2018
1 parent 5b70f79 commit 3bc869e4bcd736aa38173ebf33b478b4f2015b03
Showing with 166 additions and 14 deletions.
  1. +3 −2 api/payments.go
  2. +50 −3 api/payments_test.go
  3. +4 −2 glide.lock
  4. +2 −0 glide.yaml
  5. +3 −1 payments/payments.go
  6. +78 −3 payments/paypal/paypal.go
  7. +26 −3 payments/stripe/stripe.go
@@ -111,7 +111,8 @@ func (a *API) PaymentCreate(w http.ResponseWriter, r *http.Request) error {
loader := tx.
Preload("LineItems").
Preload("Downloads").
Preload("BillingAddress")
Preload("BillingAddress").
Preload("ShippingAddress")
if result := loader.First(order, "id = ?", orderID); result.Error != nil {
tx.Rollback()
if result.RecordNotFound() {
@@ -162,7 +163,7 @@ func (a *API) PaymentCreate(w http.ResponseWriter, r *http.Request) error {
}
tr := models.NewTransaction(order)
processorID, err := charge(params.Amount, params.Currency)
processorID, err := charge(params.Amount, params.Currency, order, invoiceNumber)
tr.ProcessorID = processorID
tr.InvoiceNumber = invoiceNumber
@@ -9,6 +9,7 @@ import (
"net/url"
"testing"
"github.com/mitchellh/mapstructure"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -18,6 +19,7 @@ import (
"strings"
paypalsdk "github.com/netlify/PayPal-Go-SDK"
"github.com/netlify/gocommerce/conf"
gcontext "github.com/netlify/gocommerce/context"
"github.com/netlify/gocommerce/models"
@@ -331,6 +333,10 @@ func TestPaymentCreate(t *testing.T) {
rsp := test.DB.Save(test.Data.secondOrder)
require.NoError(t, rsp.Error, "Failed to update order")
addr := test.Data.secondOrder.ShippingAddress
addr.Country = "United States"
assert.NoError(t, test.DB.Save(&addr).Error)
var loginCount, paymentCount int
paymentID := "4CF18861HF410323V"
amtString := fmt.Sprintf("%.2f", float64(test.Data.secondOrder.Total)/100)
@@ -341,6 +347,40 @@ func TestPaymentCreate(t *testing.T) {
fmt.Fprint(w, `{"access_token":"EEwJ6tF9x5WCIZDYzyZGaz6Khbw7raYRIBV_WxVvgmsG","expires_in":100000}`)
loginCount++
case "/v1/payments/payment/" + paymentID:
if r.Method == http.MethodPatch {
payload := []paypalsdk.PaymentPatch{}
assert.NoError(t, json.NewDecoder(r.Body).Decode(&payload))
for _, patch := range payload {
switch patch.Path {
case "/transactions/0/invoice_number":
assert.Equal(t, "1", patch.Value)
case "/transactions/0/item_list":
rawVal, ok := patch.Value.(map[string]interface{})
assert.True(t, ok)
val := paypalsdk.ItemList{}
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Result: &val,
TagName: "json",
})
assert.NoError(t, err)
assert.NoError(t, dec.Decode(&rawVal))
assert.Len(t, val.Items, 2)
for _, item := range val.Items {
switch item.SKU {
case "456-i-rollover-all-things":
assert.Equal(t, test.Data.secondLineItem1.Title, item.Name)
assert.Equal(t, test.Data.secondLineItem1.Description, item.Description)
case "234-fancy-belts":
assert.Equal(t, test.Data.secondLineItem2.Title, item.Name)
assert.Equal(t, test.Data.secondLineItem2.Description, item.Description)
}
}
assert.NotNil(t, val.ShippingAddress)
assert.Equal(t, test.Data.secondOrder.ShippingAddress.Name, val.ShippingAddress.RecipientName)
}
}
}
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"id":"`+paymentID+`","transactions":[{"amount":{"total":"`+amtString+`","currency":"`+test.Data.secondOrder.Currency+`"}}]}`)
paymentCount++
@@ -365,6 +405,7 @@ func TestPaymentCreate(t *testing.T) {
PaypalID: paymentID,
PaypalUserID: "456",
Provider: payments.PayPalProvider,
OrderID: test.Data.secondOrder.ID,
}
body, err := json.Marshal(params)
@@ -377,22 +418,27 @@ func TestPaymentCreate(t *testing.T) {
assert.Equal(t, paymentID, trans.ProcessorID)
assert.Equal(t, models.PaidState, trans.Status)
assert.Equal(t, 1, loginCount, "too many login calls")
assert.Equal(t, 2, paymentCount, "too many payment calls")
assert.Equal(t, 3, paymentCount, "too many payment calls")
})
})
t.Run("Stripe", func(t *testing.T) {
test := NewRouteTest(t)
callCount := 0
stripe.SetBackend(stripe.APIBackend, NewTrackingStripeBackend(func(method, path, key string, params stripe.ParamsContainer, v interface{}) {
switch path {
case "/charges":
payload := params.GetParams()
fmt.Println("meta:", payload.Metadata)
assert.Equal(t, test.Data.firstOrder.ID, payload.Metadata["order_id"])
assert.Equal(t, "1", payload.Metadata["invoice_number"])
callCount++
default:
t.Fatalf("unknown Stripe API call to %s", path)
}
}))
defer stripe.SetBackend(stripe.APIBackend, nil)
test := NewRouteTest(t)
test.Data.firstOrder.PaymentState = models.PendingState
rsp := test.DB.Save(test.Data.firstOrder)
require.NoError(t, rsp.Error, "Failed to update order")
@@ -574,6 +620,7 @@ type paypalPaymentParams struct {
PaypalID string `json:"paypal_payment_id"`
PaypalUserID string `json:"paypal_user_id"`
Provider string `json:"provider"`
OrderID string `json:"order_id"`
}
type paypalPreauthorizeParams struct {
@@ -622,7 +669,7 @@ func (mp *memProvider) NewPreauthorizer(ctx context.Context, r *http.Request) (p
return mp.preauthorize, nil
}
func (mp *memProvider) charge(amount uint64, currency string) (string, error) {
func (mp *memProvider) charge(amount uint64, currency string, order *models.Order, invoiceNumber int64) (string, error) {
return "", errors.New("Shouldn't have called this")
}

Some generated files are not rendered by default. Learn more.

Oops, something went wrong.
@@ -44,6 +44,8 @@ import:
version: v1.3.0
- package: github.com/imdario/mergo
version: 0.2.2
- package: github.com/pariz/gountries
version: 0.1.3
testImport:
- package: github.com/stretchr/testify
version: v1.1.3
@@ -3,6 +3,8 @@ package payments
import (
"context"
"net/http"
"github.com/netlify/gocommerce/models"
)
const (
@@ -22,7 +24,7 @@ type Provider interface {
}
// Charger wraps the Charge method which creates new payments with the provider.
type Charger func(amount uint64, currency string) (string, error)
type Charger func(amount uint64, currency string, order *models.Order, invoiceNumber int64) (string, error)
// Refunder wraps the Refund method which refunds payments with the provider.
type Refunder func(transactionID string, amount uint64, currency string) (string, error)
@@ -6,8 +6,12 @@ import (
"fmt"
"net/http"
"strconv"
"strings"
"sync"
"github.com/netlify/gocommerce/models"
"github.com/pariz/gountries"
paypalsdk "github.com/netlify/PayPal-Go-SDK"
"github.com/netlify/gocommerce/conf"
gcontext "github.com/netlify/gocommerce/context"
@@ -86,12 +90,79 @@ func (p *paypalPaymentProvider) NewCharger(ctx context.Context, r *http.Request)
return nil, errors.New("Payments requires a paypal_payment_id and paypal_user_id pair")
}
return func(amount uint64, currency string) (string, error) {
return p.charge(bp.PaypalID, bp.PaypalUserID, amount, currency)
return func(amount uint64, currency string, order *models.Order, invoiceNumber int64) (string, error) {
return p.charge(bp.PaypalID, bp.PaypalUserID, amount, currency, order, invoiceNumber)
}, nil
}
func (p *paypalPaymentProvider) charge(paymentID string, userID string, amount uint64, currency string) (string, error) {
func prepareItemsFromOrder(order *models.Order) []paypalsdk.Item {
items := []paypalsdk.Item{}
for _, lineItem := range order.LineItems {
item := paypalsdk.Item{
Quantity: int(lineItem.GetQuantity()),
Name: lineItem.Title,
Price: formatAmount(lineItem.PriceInLowestUnit()),
Currency: order.Currency,
SKU: lineItem.ProductSku(),
Description: lineItem.Description,
}
if lineItem.FixedVAT() > 0 {
item.Tax = fmt.Sprintf("%d%%", lineItem.FixedVAT())
}
items = append(items, item)
}
return items
}
func prepareShippingAddress(addr models.Address) *paypalsdk.ShippingAddress {
countryQuery := gountries.New()
country, err := countryQuery.FindCountryByName(strings.ToLower(addr.Country))
if err != nil {
return nil
}
return &paypalsdk.ShippingAddress{
RecipientName: addr.Name,
Line1: addr.Address1,
Line2: addr.Address2,
City: addr.City,
CountryCode: country.Codes.Alpha2,
PostalCode: addr.Zip,
State: addr.State,
}
}
func (p *paypalPaymentProvider) updatePaymentWithOrder(paymentID string, order *models.Order, invoiceNumber int64) error {
invoiceNumPatch := paypalsdk.PaymentPatch{
Operation: "add",
Path: "/transactions/0/invoice_number",
Value: fmt.Sprintf("%d", invoiceNumber),
}
itemList := paypalsdk.ItemList{
Items: prepareItemsFromOrder(order),
}
if a := prepareShippingAddress(order.ShippingAddress); a != nil {
itemList.ShippingAddress = a
}
itemListPatch := paypalsdk.PaymentPatch{
Operation: "add",
Path: "/transactions/0/item_list",
Value: &itemList,
}
_, err := p.client.PatchPayment(paymentID, []paypalsdk.PaymentPatch{invoiceNumPatch, itemListPatch})
if err != nil {
switch e := err.(type) {
case *paypalsdk.ErrorResponse:
fmt.Println(e.Details)
}
}
return err
}
func (p *paypalPaymentProvider) charge(paymentID string, userID string, amount uint64, currency string, order *models.Order, invoiceNumber int64) (string, error) {
payment, err := p.client.GetPayment(paymentID)
if err != nil {
return "", err
@@ -110,6 +181,10 @@ func (p *paypalPaymentProvider) charge(paymentID string, userID string, amount u
return "", fmt.Errorf("The Amount in the transaction doesn't match the amount for the order: %v", payment.Transactions[0].Amount)
}
if err := p.updatePaymentWithOrder(paymentID, order, invoiceNumber); err != nil {
return "", errors.Wrap(err, "Updating the PayPal payment with order details failed")
}
executeResult, err := p.client.ExecuteApprovedPayment(paymentID, userID)
if err != nil {
return "", err
@@ -2,10 +2,12 @@ package stripe
import (
"context"
"fmt"
"net/http"
"encoding/json"
"github.com/netlify/gocommerce/models"
"github.com/netlify/gocommerce/payments"
"github.com/pkg/errors"
stripe "github.com/stripe/stripe-go"
@@ -56,18 +58,39 @@ func (s *stripePaymentProvider) NewCharger(ctx context.Context, r *http.Request)
return nil, errors.New("Stripe requires a stripe_token for creating a payment")
}
return func(amount uint64, currency string) (string, error) {
return s.charge(bp.StripeToken, amount, currency)
return func(amount uint64, currency string, order *models.Order, invoiceNumber int64) (string, error) {
return s.charge(bp.StripeToken, amount, currency, order, invoiceNumber)
}, nil
}
func (s *stripePaymentProvider) charge(token string, amount uint64, currency string) (string, error) {
func prepareShippingAddress(addr models.Address) *stripe.ShippingDetailsParams {
return &stripe.ShippingDetailsParams{
Address: &stripe.AddressParams{
Line1: &addr.Address1,
Line2: &addr.Address2,
City: &addr.City,
State: &addr.State,
PostalCode: &addr.Zip,
Country: &addr.Country,
},
}
}
func (s *stripePaymentProvider) charge(token string, amount uint64, currency string, order *models.Order, invoiceNumber int64) (string, error) {
stripeAmount := int64(amount)
stripeDescription := fmt.Sprintf("Invoice No. %d", invoiceNumber)
ch, err := s.client.Charges.New(&stripe.ChargeParams{
Amount: &stripeAmount,
Source: &stripe.SourceParams{Token: &token},
Currency: &currency,
Description: &stripeDescription,
Shipping: prepareShippingAddress(order.ShippingAddress),
Params: stripe.Params{
Metadata: map[string]string{
"order_id": order.ID,
"invoice_number": fmt.Sprintf("%d", invoiceNumber),
},
},
})
if err != nil {

0 comments on commit 3bc869e

Please sign in to comment.