diff --git a/.githooks/prepare-commit-msg b/.githooks/prepare-commit-msg new file mode 100755 index 00000000..21afe8a4 --- /dev/null +++ b/.githooks/prepare-commit-msg @@ -0,0 +1,35 @@ +#!/bin/sh + +# This hook adds the JIRA ticket ID from branch name to every commit. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" + +case "$COMMIT_SOURCE,$SHA1" in + ,|template,) + /usr/bin/perl -i.bak -pe ' + print "\n" . `git diff --cached --name-status -r` + if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; + *) ;; +esac + +# get current branch +branchName=`git rev-parse --abbrev-ref HEAD` +branchName_uc=$(echo $branchName | tr '[:lower:]' '[:upper:]') + +# search JIRA issue id in a pattern such a "feature/ABC-123-description" +jiraId=$(echo $branchName_uc | /usr/bin/perl -ne '/[^\/]*\/([a-zA-Z]+-[0-9]+)/ && print $1') + +# only prepare commit message if pattern matched and jiraId was found +if [ -n "$jiraId" ]; then + matches=$(head -n1 "$COMMIT_MSG_FILE" | grep -c "\[$jiraId\]") + + # only add the jira ticket, if it is not already there. + if [ "$matches" = "0" ]; then # textual match, because numeric "error string" -eq 0 is true + + sed -i.bak -e "1s/^/\[$jiraId\] /" "$COMMIT_MSG_FILE" + fi +fi diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..d912dc1b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,14 @@ +### Ticket + + +### Description + + +### Checklist + +* [ ] I've read the [contribution guidelines](CONTRIBUTING.md) +* [ ] I've added Unit Tests if necessary. +* [ ] I've checked whether additional documentation is needed. +* [ ] I've ensured any personally identifying information is handled with the necessary care. +* [ ] I've checked if my implementation doesn't break other features. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..2f050a39 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# How to Contribute + +## Testing + +The project is covered by a suite of unit tests. + +## Submitting Changes + +Please open a GitHub PR with a clear list of what you have done. +Please follow golang coding conventions. + +Always write a clear description for the PR and mind the quality of your commits (small, descriptive and ordered commits). + +#### Flow of a PR + +- Contributors open a PR +- PR is reviewed, and maintainers give their +1 +- Contributors merge the PR + +**Big PRs will be rejected** Having multiple changes in the PR's description is a high indication that it should be broken into smaller PRs. (e.g This PR does X and Y -> PR for X and PR for Y). + +## Deploying Your Changes + +- **Do not** merge without our code review. +- Make sure your changes do not cause any new errors. + diff --git a/Makefile b/Makefile index 2c1cb6f2..0f010ace 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,13 @@ +.PHONY: hooks unit + SHELL:=/bin/bash +hooks: .git/hooks/prepare-commit-msg + +.git/hooks/prepare-commit-msg: .githooks/prepare-commit-msg + mkdir -p .git/hooks + cp $< $@ + test: go test -parallel 15 -tags='unit integration' ./... diff --git a/address.go b/address.go index 1ea78b1b..17acb718 100644 --- a/address.go +++ b/address.go @@ -5,6 +5,7 @@ import ( "time" ) +// Address represents the address used on transaction requests/responses type Address struct { XMLName xml.Name Id string `xml:"id,omitempty"` @@ -25,20 +26,21 @@ type Address struct { UpdatedAt *time.Time `xml:"updated-at,omitempty"` } +// AddressRequest represents the address used for creating payment methods. type AddressRequest struct { - XMLName xml.Name `xml:"address"` - FirstName string `xml:"first-name,omitempty"` - LastName string `xml:"last-name,omitempty"` - Company string `xml:"company,omitempty"` - StreetAddress string `xml:"street-address,omitempty"` - ExtendedAddress string `xml:"extended-address,omitempty"` - Locality string `xml:"locality,omitempty"` - Region string `xml:"region,omitempty"` - PostalCode string `xml:"postal-code,omitempty"` - CountryCodeAlpha2 string `xml:"country-code-alpha2,omitempty"` - CountryCodeAlpha3 string `xml:"country-code-alpha3,omitempty"` - CountryCodeNumeric string `xml:"country-code-numeric,omitempty"` - CountryName string `xml:"country-name,omitempty"` + XMLName xml.Name + FirstName string `xml:"first-name,omitempty"` + LastName string `xml:"last-name,omitempty"` + Company string `xml:"company,omitempty"` + StreetAddress string `xml:"street-address,omitempty"` + ExtendedAddress string `xml:"extended-address,omitempty"` + Locality string `xml:"locality,omitempty"` + Region string `xml:"region,omitempty"` + PostalCode string `xml:"postal-code,omitempty"` + CountryCodeAlpha2 string `xml:"country-code-alpha2,omitempty"` + CountryCodeAlpha3 string `xml:"country-code-alpha3,omitempty"` + CountryCodeNumeric string `xml:"country-code-numeric,omitempty"` + CountryName string `xml:"country-name,omitempty"` } type Addresses struct { diff --git a/address_gateway.go b/address_gateway.go index e0b7f73a..8db254b0 100644 --- a/address_gateway.go +++ b/address_gateway.go @@ -10,6 +10,7 @@ type AddressGateway struct { // Create creates a new address for the specified customer id. func (g *AddressGateway) Create(ctx context.Context, customerID string, a *AddressRequest) (*Address, error) { + a.XMLName.Local = "address" resp, err := g.execute(ctx, "POST", "customers/"+customerID+"/addresses", &a) if err != nil { return nil, err @@ -36,6 +37,7 @@ func (g *AddressGateway) Delete(ctx context.Context, customerId, addrId string) // Update updates an address for the address id and customer id. func (g *AddressGateway) Update(ctx context.Context, customerID, addrID string, a *AddressRequest) (*Address, error) { + a.XMLName.Local = "address" resp, err := g.execute(ctx, "PUT", "customers/"+customerID+"/addresses/"+addrID, a) if err != nil { return nil, err diff --git a/decimal.go b/decimal.go index 13f6cefa..8a8a4250 100644 --- a/decimal.go +++ b/decimal.go @@ -2,6 +2,7 @@ package braintree import ( "bytes" + "errors" "strconv" "strings" ) @@ -22,6 +23,10 @@ func NewDecimal(unscaled int64, scale int) *Decimal { // MarshalText outputs a decimal representation of the scaled number func (d *Decimal) MarshalText() (text []byte, err error) { + if d == nil { + return nil, errors.New("decimal is nil") + } + b := new(bytes.Buffer) if d.Scale <= 0 { b.WriteString(strconv.FormatInt(d.Unscaled, 10)) diff --git a/decimal_test.go b/decimal_test.go index 50ed7c39..e911f1a7 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -49,36 +49,46 @@ func TestDecimalMarshalText(t *testing.T) { t.Parallel() tests := []struct { - in *Decimal - out []byte + in *Decimal + out []byte + shouldError bool }{ - {NewDecimal(250, -2), []byte("25000")}, - {NewDecimal(2, 0), []byte("2")}, - {NewDecimal(23, 0), []byte("23")}, - {NewDecimal(234, 0), []byte("234")}, - {NewDecimal(0, 1), []byte("0.0")}, - {NewDecimal(1, 1), []byte("0.1")}, - {NewDecimal(12, 1), []byte("1.2")}, - {NewDecimal(0, 2), []byte("0.00")}, - {NewDecimal(5, 2), []byte("0.05")}, - {NewDecimal(55, 2), []byte("0.55")}, - {NewDecimal(250, 2), []byte("2.50")}, - {NewDecimal(4586, 2), []byte("45.86")}, - {NewDecimal(-5504, 2), []byte("-55.04")}, - {NewDecimal(0, 3), []byte("0.000")}, - {NewDecimal(5, 3), []byte("0.005")}, - {NewDecimal(55, 3), []byte("0.055")}, - {NewDecimal(250, 3), []byte("0.250")}, - {NewDecimal(4586, 3), []byte("4.586")}, - {NewDecimal(45867, 3), []byte("45.867")}, - {NewDecimal(-55043, 3), []byte("-55.043")}, + {NewDecimal(250, -2), []byte("25000"), false}, + {NewDecimal(2, 0), []byte("2"), false}, + {NewDecimal(23, 0), []byte("23"), false}, + {NewDecimal(234, 0), []byte("234"), false}, + {NewDecimal(0, 1), []byte("0.0"), false}, + {NewDecimal(1, 1), []byte("0.1"), false}, + {NewDecimal(12, 1), []byte("1.2"), false}, + {NewDecimal(0, 2), []byte("0.00"), false}, + {NewDecimal(5, 2), []byte("0.05"), false}, + {NewDecimal(55, 2), []byte("0.55"), false}, + {NewDecimal(250, 2), []byte("2.50"), false}, + {NewDecimal(4586, 2), []byte("45.86"), false}, + {NewDecimal(-5504, 2), []byte("-55.04"), false}, + {NewDecimal(0, 3), []byte("0.000"), false}, + {NewDecimal(5, 3), []byte("0.005"), false}, + {NewDecimal(55, 3), []byte("0.055"), false}, + {NewDecimal(250, 3), []byte("0.250"), false}, + {NewDecimal(4586, 3), []byte("4.586"), false}, + {NewDecimal(45867, 3), []byte("45.867"), false}, + {NewDecimal(-55043, 3), []byte("-55.043"), false}, + {nil, nil, true}, } for _, tt := range tests { b, err := tt.in.MarshalText() - if err != nil { - t.Errorf("expected %+v.MarshalText() => to not error, but it did with %s", tt.in, err) + + if tt.shouldError { + if err == nil { + t.Errorf("expected %+v.MarshalText() => to error, but it did not", tt.in) + } + } else { + if err != nil { + t.Errorf("expected %+v.MarshalText() => to not error, but it did with %s", tt.in, err) + } } + if string(tt.out) != string(b) { t.Errorf("%+v.MarshalText() => %s, want %s", tt.in, b, tt.out) } diff --git a/go.mod b/go.mod index 70523f5e..e99a6561 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,3 @@ module github.com/braintree-go/braintree-go + +go 1.14 diff --git a/init_test.go b/init_test.go index 4912ee3e..b39c78e0 100644 --- a/init_test.go +++ b/init_test.go @@ -78,7 +78,6 @@ func testSubMerchantAccount() string { func init() { logEnabled := flag.Bool("log", false, "enables logging") - flag.Parse() if *logEnabled { testGateway.Logger = log.New(os.Stderr, "", 0) diff --git a/payment_method_gateway.go b/payment_method_gateway.go index 0518d7bf..8cf282e6 100644 --- a/payment_method_gateway.go +++ b/payment_method_gateway.go @@ -15,6 +15,7 @@ type PaymentMethodRequest struct { Token string `xml:"token,omitempty"` PaymentMethodNonce string `xml:"payment-method-nonce,omitempty"` Options *PaymentMethodRequestOptions `xml:"options,omitempty"` + BillingAddress *AddressRequest `xml:"billing-address,omitempty"` } type PaymentMethodRequestOptions struct { diff --git a/payment_method_integration_test.go b/payment_method_integration_test.go index a208de05..d1148512 100644 --- a/payment_method_integration_test.go +++ b/payment_method_integration_test.go @@ -25,9 +25,25 @@ func TestPaymentMethod(t *testing.T) { g := testGateway.PaymentMethod() // Create using credit card + addr := &AddressRequest{ + FirstName: "Robert", + LastName: "Smith", + Company: "The Cure", + StreetAddress: "39 Acacia Avenue", + ExtendedAddress: "SAV Studios", + Locality: "North End", + Region: "London", + PostalCode: "SW1A 0AA", + CountryCodeAlpha2: "GB", + CountryCodeAlpha3: "GBR", + CountryCodeNumeric: "826", + CountryName: "United Kingdom", + } + paymentMethod, err := g.Create(ctx, &PaymentMethodRequest{ CustomerId: cust.Id, PaymentMethodNonce: FakeNonceTransactableVisa, + BillingAddress: addr, }) if err != nil { t.Fatal(err) @@ -40,9 +56,52 @@ func TestPaymentMethod(t *testing.T) { t.Errorf("Got paymentMethod token %#v, want a value", paymentMethod.GetToken()) } + if card, ok := paymentMethod.(*CreditCard); ok { + ba := card.BillingAddress + if ba.FirstName != addr.FirstName { + t.Errorf("Got paymentMethod billing adress first name %#v, want %#v", ba.FirstName, addr.FirstName) + } + if ba.LastName != addr.LastName { + t.Errorf("Got paymentMethod billing adress last name %#v, want %#v", ba.LastName, addr.LastName) + } + if ba.Company != addr.Company { + t.Errorf("Got paymentMethod billing adress company %#v, want %#v", ba.Company, addr.Company) + } + if ba.StreetAddress != addr.StreetAddress { + t.Errorf("Got paymentMethod billing adress street address %#v, want %#v", ba.StreetAddress, addr.StreetAddress) + } + if ba.ExtendedAddress != addr.ExtendedAddress { + t.Errorf("Got paymentMethod billing adress extended address %#v, want %#v", ba.ExtendedAddress, addr.ExtendedAddress) + } + if ba.Locality != addr.Locality { + t.Errorf("Got paymentMethod billing adress locality %#v, want %#v", ba.Locality, addr.Locality) + } + if ba.Region != addr.Region { + t.Errorf("Got paymentMethod billing adress region %#v, want %#v", ba.Region, addr.Region) + } + if ba.PostalCode != addr.PostalCode { + t.Errorf("Got paymentMethod billing adress postal code %#v, want %#v", ba.PostalCode, addr.PostalCode) + } + if ba.CountryCodeAlpha2 != addr.CountryCodeAlpha2 { + t.Errorf("Got paymentMethod billing adress country alpha2 %#v, want %#v", ba.CountryCodeAlpha2, addr.CountryCodeAlpha2) + } + if ba.CountryCodeAlpha3 != addr.CountryCodeAlpha3 { + t.Errorf("Got paymentMethod billing adress country alpha3 %#v, want %#v", ba.CountryCodeAlpha3, addr.CountryCodeAlpha3) + } + if ba.CountryCodeNumeric != addr.CountryCodeNumeric { + t.Errorf("Got paymentMethod billing adress country numeric %#v, want %#v", ba.CountryCodeNumeric, addr.CountryCodeNumeric) + } + if ba.CountryName != addr.CountryName { + t.Errorf("Got paymentMethod billing adress country name %#v, want %#v", ba.CountryName, addr.CountryName) + } + } else { + t.Error("paymentMethod should have been a credit card") + } + // Update using different credit card rand.Seed(time.Now().UTC().UnixNano()) token := fmt.Sprintf("btgo_test_token_%d", rand.Int()+1) + t.Fatal(token) paymentMethod, err = g.Update(ctx, paymentMethod.GetToken(), &PaymentMethodRequest{ PaymentMethodNonce: FakeNonceTransactableMasterCard, Token: token, diff --git a/transaction.go b/transaction.go index 28029ca2..bcccbc38 100644 --- a/transaction.go +++ b/transaction.go @@ -114,8 +114,8 @@ type TransactionRequest struct { PlanId string `xml:"plan-id,omitempty"` CreditCard *CreditCard `xml:"credit-card,omitempty"` Customer *CustomerRequest `xml:"customer,omitempty"` - BillingAddress *Address `xml:"billing,omitempty"` - ShippingAddress *Address `xml:"shipping,omitempty"` + BillingAddress *AddressRequest `xml:"billing,omitempty"` + ShippingAddress *AddressRequest `xml:"shipping,omitempty"` TaxAmount *Decimal `xml:"tax-amount,omitempty"` TaxExempt bool `xml:"tax-exempt,omitempty"` DeviceData string `xml:"device-data,omitempty"` diff --git a/transaction_integration_test.go b/transaction_integration_test.go index a4b8dd1e..0ab0572d 100644 --- a/transaction_integration_test.go +++ b/transaction_integration_test.go @@ -517,7 +517,7 @@ func TestTransactionCreatedWhenAVSBankDoesNotSupport(t *testing.T) { ExpirationDate: "05/14", CVV: "100", }, - BillingAddress: &Address{ + BillingAddress: &AddressRequest{ StreetAddress: "1 E Main St", Locality: "Chicago", Region: "IL", @@ -562,7 +562,7 @@ func TestTransactionCreatedWhenAVSPostalDoesNotMatch(t *testing.T) { ExpirationDate: "05/14", CVV: "100", }, - BillingAddress: &Address{ + BillingAddress: &AddressRequest{ StreetAddress: "1 E Main St", Locality: "Chicago", Region: "IL", @@ -607,7 +607,7 @@ func TestTransactionCreatedWhenAVStreetAddressDoesNotMatch(t *testing.T) { ExpirationDate: "05/14", CVV: "100", }, - BillingAddress: &Address{ + BillingAddress: &AddressRequest{ StreetAddress: "201 E Main St", // Should cause AVS street address not verified response. Locality: "Chicago", Region: "IL", @@ -875,13 +875,13 @@ func TestAllTransactionFields(t *testing.T) { Customer: &CustomerRequest{ FirstName: "Lionel", }, - BillingAddress: &Address{ + BillingAddress: &AddressRequest{ StreetAddress: "1 E Main St", Locality: "Chicago", Region: "IL", PostalCode: "60637", }, - ShippingAddress: &Address{ + ShippingAddress: &AddressRequest{ StreetAddress: "1 E Main St", Locality: "Chicago", Region: "IL",