solidus_signifyd 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +21 -0
  3. data/Gemfile +10 -3
  4. data/README.md +58 -1
  5. data/app/controllers/spree/api/spree_signifyd/orders_controller.rb +1 -1
  6. data/app/models/spree/signifyd_configuration.rb +1 -0
  7. data/app/models/spree_signifyd/order_concerns.rb +18 -1
  8. data/app/models/spree_signifyd/shipment_decorator.rb +1 -1
  9. data/app/serializers/spree_signifyd/credit_card_serializer.rb +9 -9
  10. data/app/serializers/spree_signifyd/order_serializer.rb +22 -10
  11. data/app/serializers/spree_signifyd/user_serializer.rb +13 -2
  12. data/lib/spree_signifyd.rb +2 -3
  13. data/lib/spree_signifyd/create_signifyd_case.rb +3 -4
  14. data/lib/spree_signifyd/engine.rb +1 -0
  15. data/solidus_signifyd.gemspec +4 -4
  16. data/spec/controllers/spree/api/spree_signifyd/orders_controller_spec.rb +6 -10
  17. data/spec/lib/spree_signifyd/create_signifyd_case_spec.rb +4 -4
  18. data/spec/lib/spree_signifyd_spec.rb +14 -3
  19. data/spec/models/spree/order_spec.rb +38 -11
  20. data/spec/models/spree/shipment_spec.rb +33 -35
  21. data/spec/serializers/spree_signifyd/billing_address_serializer.rb +1 -1
  22. data/spec/serializers/spree_signifyd/credit_card_serializer_spec.rb +2 -2
  23. data/spec/serializers/spree_signifyd/delivery_address_serializer_spec.rb +2 -2
  24. data/spec/serializers/spree_signifyd/order_serializer_spec.rb +58 -17
  25. data/spec/serializers/spree_signifyd/user_serializer_spec.rb +9 -1
  26. data/spec/spec_helper.rb +7 -0
  27. data/spec/support/api_schema_matcher.rb +9 -0
  28. data/spec/support/schemas/v2/case.json +305 -0
  29. metadata +46 -21
@@ -13,7 +13,7 @@ describe Spree::Order, :type => :model do
13
13
  subject { order.is_risky? }
14
14
 
15
15
  context "no signifyd_score" do
16
- it { should eq false }
16
+ it { is_expected.to eq false }
17
17
  end
18
18
 
19
19
  context "signifyd_score present" do
@@ -21,31 +21,58 @@ describe Spree::Order, :type => :model do
21
21
 
22
22
  context "approved" do
23
23
  before { SpreeSignifyd.approve(order: order) }
24
- it { should eq false }
24
+ it { is_expected.to eq false }
25
25
  end
26
26
 
27
27
  context "not approved" do
28
- it { should eq true }
28
+ it { is_expected.to eq true }
29
29
  end
30
30
  end
31
31
  end
32
32
 
33
33
  describe "transition to complete" do
34
34
  let(:order) { create(:order_with_line_items, state: 'confirm') }
35
- let!(:payment) { create(:payment, amount: order.total, order: order ) }
36
35
 
37
- it "calls #create_signifyd_case" do
38
- expect(SpreeSignifyd).to receive(:create_case).with(order_number: order.number)
39
- order.complete!
36
+ shared_examples "an order we send to signifyd" do
37
+ it "creates a new SIGNIFYD case" do
38
+ expect(SpreeSignifyd).to receive(:create_case).with(order_number: order.number)
39
+ order.complete!
40
+ end
40
41
  end
41
42
 
42
- context "the order is already approved" do # e.g. unreturned exchanges are automatically approved
43
- it "does not create a case" do
44
- order.contents.approve(user: Spree.user_class.first)
43
+ shared_examples "an order we DO NOT send to signifyd" do
44
+ it "does not create a new SIGNIFYD case" do
45
45
  expect(SpreeSignifyd).not_to receive(:create_case)
46
46
  order.complete!
47
47
  end
48
48
  end
49
- end
50
49
 
50
+ context "paid with store credit only" do
51
+ let!(:payment) { create(:store_credit_payment, amount: order.total, order: order ) }
52
+
53
+ it_behaves_like "an order we send to signifyd"
54
+
55
+ context "don't send store credit orders to SIGNIFYD" do
56
+ before { SpreeSignifyd::Config[:exclude_store_credit_orders] = true }
57
+
58
+ it_behaves_like "an order we DO NOT send to signifyd"
59
+
60
+ it "is immediately approved" do
61
+ expect{ order.complete! }.to change{ order.approved? }.from(false).to(true)
62
+ end
63
+ end
64
+ end
65
+
66
+ context "paid with cash" do
67
+ let!(:payment) { create(:payment, amount: order.total, order: order ) }
68
+
69
+ it_behaves_like "an order we send to signifyd"
70
+
71
+ context "the order is already approved" do # e.g. unreturned exchanges are automatically approved
72
+ before { order.contents.approve(user: Spree.user_class.first) }
73
+
74
+ it_behaves_like "an order we DO NOT send to signifyd"
75
+ end
76
+ end
77
+ end
51
78
  end
@@ -5,52 +5,50 @@ describe Spree::Shipment, :type => :model do
5
5
  let(:shipment) { create(:shipment) }
6
6
  subject { shipment.determine_state(shipment.order) }
7
7
 
8
- describe "#determine_state_with_signifyd" do
9
-
10
- context "with a risky order" do
11
- before { shipment.order.stub(:is_risky?).and_return(true) }
12
-
13
- context "the order is not approved" do
14
- it "returns pending" do
15
- shipment.order.stub(:approved?).and_return(false)
16
- subject.should eq "pending"
17
- end
8
+ describe "#determine_state" do
9
+ context "with a canceled order" do
10
+ before do
11
+ shipment.order.update(state: 'canceled')
12
+ shipment.update(state: 'canceled')
18
13
  end
19
14
 
20
- context "the order is approved" do
21
- it "defaults to existing behavior" do
22
- shipment.order.stub(:approved?).and_return(true)
23
- shipment.should_receive(:determine_state).with(shipment.order)
24
- subject
25
- end
15
+ it "canceled shipments remain canceled" do
16
+ expect(subject).to eq "canceled"
26
17
  end
27
18
  end
28
19
 
29
- context "without a risky order" do
30
- before { shipment.order.stub(:is_risky?).and_return(false) }
20
+ context "with an approved order" do
21
+ before { shipment.order.contents.approve(name: 'test approver') }
31
22
 
32
- it "defaults to existing behavior" do
33
- shipment.should_receive(:determine_state).with(shipment.order)
34
- subject
23
+ it "pending shipments remain pending" do
24
+ expect(subject).to eq "pending"
35
25
  end
36
- end
37
26
 
38
- context "shipment state" do
39
- [:shipped, :ready].each do |state|
40
- context "the shipment is #{state}" do
41
- before { shipment.update_columns(state: state) }
42
- it "defaults to existing behavior" do
43
- shipment.should_receive(:determine_state).with(shipment.order)
44
- subject
27
+ describe "regular Solidus behaviour" do
28
+ context "order cannot ship" do
29
+ before { allow(shipment.order).to receive_messages can_ship?: false }
30
+
31
+ it 'returns pending' do
32
+ expect(subject).to eq 'pending'
45
33
  end
46
34
  end
47
- end
48
35
 
49
- [:pending, :canceled].each do |state|
50
- context "the shipment is #{state}" do
51
- before { shipment.update_columns(state: state) }
52
- it "is pending" do
53
- subject.should eq "pending"
36
+ context "order can ship" do
37
+ before { allow(shipment.order).to receive_messages can_ship?: true }
38
+
39
+ it 'returns shipped when already shipped' do
40
+ allow(shipment).to receive_messages state: 'shipped'
41
+ expect(subject).to eq 'shipped'
42
+ end
43
+
44
+ it 'returns pending when unpaid' do
45
+ allow(shipment.order).to receive_messages paid?: false
46
+ expect(subject).to eq 'pending'
47
+ end
48
+
49
+ it 'returns ready when paid' do
50
+ allow(shipment.order).to receive_messages paid?: true
51
+ expect(subject).to eq 'ready'
54
52
  end
55
53
  end
56
54
  end
@@ -6,7 +6,7 @@ module SpreeSignifyd
6
6
  let(:serialized_address) { JSON.parse(BillingAddressSerializer.new(bill_address).to_json) }
7
7
 
8
8
  context "node values" do
9
- it { serialized_address.should include 'billingAddress' }
9
+ it { expect(serialized_address).to include 'billingAddress' }
10
10
  end
11
11
  end
12
12
  end
@@ -15,11 +15,11 @@ module SpreeSignifyd
15
15
  end
16
16
 
17
17
  it "expiryMonth" do
18
- expect(serialized_credit_card['expiryMonth']).to eq credit_card.month
18
+ expect(serialized_credit_card['expiryMonth']).to eq credit_card.month.to_i
19
19
  end
20
20
 
21
21
  it "expiryYear" do
22
- expect(serialized_credit_card['expiryYear']).to eq credit_card.year
22
+ expect(serialized_credit_card['expiryYear']).to eq credit_card.year.to_i
23
23
  end
24
24
  end
25
25
  end
@@ -6,8 +6,8 @@ module SpreeSignifyd
6
6
  let(:serialized_address) { JSON.parse(DeliveryAddressSerializer.new(delivery_address).to_json) }
7
7
 
8
8
  context "node values" do
9
- it { serialized_address.should include 'deliveryAddress' }
10
- it { serialized_address['fullName'].should eq delivery_address.full_name }
9
+ it { expect(serialized_address).to include 'deliveryAddress' }
10
+ it { expect(serialized_address['fullName']).to eq delivery_address.full_name }
11
11
  end
12
12
  end
13
13
  end
@@ -2,54 +2,95 @@ require 'spec_helper'
2
2
 
3
3
  module SpreeSignifyd
4
4
  describe OrderSerializer do
5
- let(:order) { create(:shipped_order, line_items_count: 1) }
5
+ let(:order) { create :shipped_order,
6
+ line_items_count: 1,
7
+ last_ip_address: "127.0.0.1"
8
+ }
6
9
  let(:line_item) { order.line_items.first }
7
10
  let(:serialized_order) { JSON.parse(OrderSerializer.new(order).to_json) }
8
11
 
12
+ describe 'document format' do
13
+ before do
14
+ # we can't pass payments into the :shipped_order factory
15
+ order.payments.last.update(avs_response: "M", cvv_response_code: "M")
16
+ end
17
+
18
+ it 'matches the SIGNIFYD V2 api' do
19
+ expect(serialized_order).to match_schema('v2/case.json')
20
+ end
21
+ end
22
+
9
23
  describe "node values" do
10
24
  context "purchase" do
11
25
 
12
26
  let(:purchase) { serialized_order['purchase'] }
13
27
 
14
- it { purchase['browserIpAddress'].should eq order.last_ip_address }
15
- it { purchase['orderId'].should eq order.number }
16
- it { purchase['createdAt'].should eq order.completed_at.utc.iso8601 }
17
- it { purchase['currency'].should eq order.currency }
18
- it { purchase['totalPrice'].should eq order.total.to_s }
28
+ it { expect(purchase['browserIpAddress']).to eq order.last_ip_address }
29
+ it { expect(purchase['orderId']).to eq order.number }
30
+ it { expect(purchase['createdAt']).to eq order.completed_at.utc.iso8601 }
31
+ it { expect(purchase['currency']).to eq order.currency }
32
+ it { expect(purchase['totalPrice']).to eq order.total }
19
33
 
20
34
  context "with a payment" do
21
- it { purchase['avsResponseCode'].should eq order.payments.last.avs_response }
22
- it { purchase['cvvResponseCode'].should eq order.payments.last.cvv_response_code }
35
+ it { expect(purchase['avsResponseCode']).to eq order.payments.last.avs_response.to_s }
36
+ it { expect(purchase['cvvResponseCode']).to eq order.payments.last.cvv_response_code.to_s }
37
+
38
+ context "when the payment is a paypal payment" do
39
+ before do
40
+ order.payments.first.source.update({
41
+ cc_type: "paypal"
42
+ })
43
+ end
44
+
45
+ it "includes a paymentGateway specification for signifyd" do
46
+ expect(purchase['paymentGateway']).to eql("paypal_account")
47
+ end
48
+ end
49
+
50
+ context "when the payment is not a paypal payment" do
51
+ it "does not include a paymentGateway key" do
52
+ expect(purchase['paymentGateway']).to eql(nil)
53
+ end
54
+ end
55
+ end
56
+
57
+ context "paid with store credit" do
58
+ before do
59
+ create(:store_credit_payment, amount: order.total, order: order)
60
+ order.payments.reload
61
+ end
62
+
63
+ it { expect(serialized_order["card"]).to eq({}) }
23
64
  end
24
65
 
25
66
  context "without a payment" do
26
67
  let(:order) { create(:completed_order_with_totals) }
27
68
 
28
- it { purchase['avsResponseCode'].should be nil }
29
- it { purchase['cvvResponseCode'].should be nil }
69
+ it { expect(purchase['avsResponseCode']).to eq "" }
70
+ it { expect(purchase['cvvResponseCode']).to eq "" }
30
71
  end
31
72
 
32
73
  it "contains a products node" do
33
- purchase['products'].should eq [ JSON.parse(SpreeSignifyd::LineItemSerializer.new(line_item).to_json) ]
74
+ expect(purchase['products']).to eq [ JSON.parse(SpreeSignifyd::LineItemSerializer.new(line_item).to_json) ]
34
75
  end
35
76
  end
36
77
 
37
78
  context "userAccount" do
38
- it { serialized_order.should include 'userAccount' }
79
+ it { expect(serialized_order).to include 'userAccount' }
39
80
  end
40
81
 
41
82
  context "recipient" do
42
- it { serialized_order.should include 'recipient' }
43
- it { serialized_order["recipient"]["confirmationEmail"].should eq order.email }
83
+ it { expect(serialized_order).to include 'recipient' }
84
+ it { expect(serialized_order["recipient"]["confirmationEmail"]).to eq order.email }
44
85
  end
45
86
 
46
87
  context "card" do
47
- it { serialized_order.should include 'card' }
88
+ it { expect(serialized_order).to include 'card' }
48
89
 
49
90
  context "credit card payment" do
50
91
  let!(:payment) { create(:payment, order: order) }
51
92
 
52
- it { serialized_order["card"].should include 'billingAddress'}
93
+ it { expect(serialized_order["card"]).to include 'billingAddress'}
53
94
  end
54
95
 
55
96
  context "no payment source" do
@@ -62,7 +103,7 @@ module SpreeSignifyd
62
103
 
63
104
  context "non credit card payment" do
64
105
  it "contains no data" do
65
- Spree::CreditCard.any_instance.stub(:instance_of?).and_return(false)
106
+ allow_any_instance_of(Spree::CreditCard).to receive(:instance_of?).and_return(false)
66
107
  expect(serialized_order["card"]).to eq({})
67
108
  end
68
109
  end
@@ -9,6 +9,10 @@ module SpreeSignifyd
9
9
 
10
10
  let(:serialized_user) { JSON.parse(UserSerializer.new(user).to_json) }
11
11
 
12
+ before do
13
+ old_complete_order.update_attributes(completed_at: 30.days.ago)
14
+ end
15
+
12
16
  context "node values" do
13
17
  it "emailAddress" do
14
18
  expect(serialized_user['emailAddress']).to eq user.email
@@ -18,6 +22,10 @@ module SpreeSignifyd
18
22
  expect(serialized_user['username']).to eq user.email
19
23
  end
20
24
 
25
+ it "phone" do
26
+ expect(serialized_user['phone']).to eq new_complete_order.ship_address.phone
27
+ end
28
+
21
29
  it "createdDate" do
22
30
  expect(serialized_user['createdDate']).to eq user.created_at.utc.iso8601
23
31
  end
@@ -46,7 +54,7 @@ module SpreeSignifyd
46
54
  end
47
55
 
48
56
  it "aggregateOrderDollars" do
49
- expect(serialized_user['aggregateOrderDollars']).to eq (old_complete_order.total + new_complete_order.total).to_s
57
+ expect(serialized_user['aggregateOrderDollars']).to eq (old_complete_order.total + new_complete_order.total)
50
58
  end
51
59
  end
52
60
  end
@@ -38,6 +38,7 @@ RSpec.configure do |config|
38
38
  config.run_all_when_everything_filtered = true
39
39
  config.use_transactional_fixtures = false
40
40
 
41
+ config.include RSpec::Rails::Matchers
41
42
  config.include FactoryGirl::Syntax::Methods
42
43
 
43
44
  # Ensure Suite is set to use transactions for speed.
@@ -46,10 +47,16 @@ RSpec.configure do |config|
46
47
  DatabaseCleaner.clean_with :truncation
47
48
  end
48
49
 
50
+ # allow us to test various preference settings without cross contamination
51
+ config.before :each do
52
+ SpreeSignifyd::Config.reset
53
+ end
54
+
49
55
  # Before each spec check if it is a Javascript test and switch between using database transactions or not where necessary.
50
56
  config.before :each do
51
57
  DatabaseCleaner.strategy = example.metadata[:js] ? :truncation : :transaction
52
58
  DatabaseCleaner.start
59
+ ActiveJob::Base.queue_adapter = :test
53
60
 
54
61
  allow(Signifyd::Case).to receive(:create).and_return(
55
62
  { code: 201, body: { investigationId: 123 } }
@@ -0,0 +1,9 @@
1
+ require "json-schema"
2
+
3
+ RSpec::Matchers.define :match_schema do |schema|
4
+ match do |candidate|
5
+ schema_directory = File.expand_path("../schemas", __FILE__)
6
+ schema_path = File.join(schema_directory, schema)
7
+ JSON::Validator.validate!(schema_path, candidate)
8
+ end
9
+ end
@@ -0,0 +1,305 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-04/schema#",
3
+ "type": "object",
4
+ "properties": {
5
+ "purchase": {
6
+ "type": "object",
7
+ "description": "The purchase.",
8
+ "properties": {
9
+ "browserIpAddress": {
10
+ "type": "string",
11
+ "description": "The IP Address of the browser that was used to make the This is the IP Address that was used to connect to your site and make the "
12
+ },
13
+ "shipments": {
14
+ "type": "array",
15
+ "description": "The shipments association with this purchase"
16
+ },
17
+ "products": {
18
+ "type": "array",
19
+ "description": "The products purchased in the transaction."
20
+ },
21
+ "orderId": {
22
+ "type": "string",
23
+ "description": "A string uniquely identifying this order."
24
+ },
25
+ "createdAt": {
26
+ "type": "string",
27
+ "description": "`yyyy-MM-dd'T'HH:mm:ssZ` The date and time when the order was placed, shown on the signifyd console. See the Dates section of these docs for more information about date formats."
28
+ },
29
+ "paymentGateway": {
30
+ "type": "string",
31
+ "description": "The gateway that processed the transaction. For paypal orders use paypal_account."
32
+ },
33
+ "paymentMethod": {
34
+ "type": "object",
35
+ "description": "The method the user used to complete the "
36
+ },
37
+ "transactionId": {
38
+ "type": "string",
39
+ "description": "The unique identifier provided by the payment gateway for this order. If you have provided us with credentials for your payment gateway we can obtain additional details about the order, like AVS and CVV status, from your payment provider."
40
+ },
41
+ "currency": {
42
+ "type": "string",
43
+ "description": "The currency type of the order, in 3 letter ISO 4217 format. Defaults to USD."
44
+ },
45
+ "avsResponseCode": {
46
+ "type": "string",
47
+ "description": "The response code from the address verification system (AVS). Accepted codes: http://www.emsecommerce.net/avs_cvv2_response_codes.htm"
48
+ },
49
+ "cvvResponseCode": {
50
+ "type": "string",
51
+ "description": "The response code from the card verification value (CVV) check. Accepted codes listed on above link."
52
+ },
53
+ "orderChannel": {
54
+ "type": "string",
55
+ "description": "The method used by the buyer to place the order. Either WEB or PHONE."
56
+ },
57
+ "receivedBy": {
58
+ "type": "string",
59
+ "description": "If the order was was taken by a customer service or sales agent, his or her name."
60
+ },
61
+ "totalPrice": {
62
+ "type": "number",
63
+ "description": "The total price of the order, including shipping price and taxes."
64
+ }
65
+ },
66
+ "required": [
67
+ "browserIpAddress",
68
+ "orderId",
69
+ "createdAt",
70
+ "avsResponseCode",
71
+ "cvvResponseCode",
72
+ "totalPrice"
73
+ ]
74
+ },
75
+ "recipient": {
76
+ "type": "object",
77
+ "description": "The person receiving the goods.",
78
+ "properties": {
79
+ "fullName": {
80
+ "type": "string",
81
+ "description": "The full name of the person receiving the goods. If this item is being shipped, then this field is the person it is being shipping to. Don't assume this name is the same as card.cardHolderName. Only put a value here if the name will actually appear on the shipping label. If this item is digital, then this field will likely be blank."
82
+ },
83
+ "confirmationEmail": {
84
+ "type": "string",
85
+ "description": "When this purchase was completed, you likely sent a confirmation email or you will be sending a confirmation email to someone once you approve the order. This is the email address to which that confirmation email will be sent."
86
+ },
87
+ "confirmationPhone": {
88
+ "type": "string",
89
+ "description": "The phone number that you would call if there was something wrong with this order or the phone number that was supplied with the shipping information."
90
+ },
91
+ "organization": {
92
+ "type": "string",
93
+ "description": "If provided by the buyer, the name of the recipient's company or organization."
94
+ },
95
+ "deliveryAddress": {
96
+ "type": "object",
97
+ "description": "The address to which the order will be delivered.",
98
+ "properties": {
99
+ "streetAddress": {
100
+ "type": "string",
101
+ "description": "The street number and street name."
102
+ },
103
+ "unit": {
104
+ "type": "string",
105
+ "description": "The unit or apartment number."
106
+ },
107
+ "city": {
108
+ "type": "string",
109
+ "description": "The city name."
110
+ },
111
+ "provinceCode": {
112
+ "type": "string",
113
+ "description": "The code or abbreviation for the province."
114
+ },
115
+ "postalCode string": {
116
+ "type": "string",
117
+ "description": "The postal code."
118
+ },
119
+ "countryCode": {
120
+ "type": "string",
121
+ "description": "The two-letter ISO-3166 country code. If left blank, we will assume US."
122
+ },
123
+ "latitude": {
124
+ "type": "number",
125
+ "description": "The latitude of the address. Used when address details are not provided. Ignored otherwise."
126
+ },
127
+ "longitude": {
128
+ "type": "number",
129
+ "description": "The longitude of the address. Used when address details are not provided. Ignored otherwise."
130
+ }
131
+ }
132
+ }
133
+ },
134
+ "required": [
135
+ "fullName",
136
+ "confirmationEmail",
137
+ "deliveryAddress"
138
+ ]
139
+ },
140
+ "card": {
141
+ "type": "object",
142
+ "properties": {
143
+ "cardHolderName": {
144
+ "type": "string",
145
+ "description": "The full name on the credit card that was charged."
146
+ },
147
+ "bin": {
148
+ "type": "number",
149
+ "description": "The first six digits of the credit card, the bank identification number, which uniquely identifies the issuer."
150
+ },
151
+ "last4": {
152
+ "type": "string",
153
+ "description": "The last four digits of the card number."
154
+ },
155
+ "expiryMonth": {
156
+ "type": "number",
157
+ "description": "MM representation of the expiration month of the card."
158
+ },
159
+ "expiryYear": {
160
+ "type": "number",
161
+ "description": "yyyy representation of the expiration year of the card."
162
+ },
163
+ "billingAddress": {
164
+ "type": "object",
165
+ "properties": {
166
+ "streetAddress": {
167
+ "type": "string",
168
+ "description": "The street number and street name."
169
+ },
170
+ "unit": {
171
+ "type": "string",
172
+ "description": "The unit or apartment number."
173
+ },
174
+ "city": {
175
+ "type": "string",
176
+ "description": "The city name."
177
+ },
178
+ "provinceCode": {
179
+ "type": "string",
180
+ "description": "The code or abbreviation for the province."
181
+ },
182
+ "postalCode string": {
183
+ "type": "string",
184
+ "description": "The postal code."
185
+ },
186
+ "countryCode": {
187
+ "type": "string",
188
+ "description": "The two-letter ISO-3166 country code. If left blank, we will assume US."
189
+ },
190
+ "latitude": {
191
+ "type": "number",
192
+ "description": "The latitude of the address. Used when address details are not provided. Ignored otherwise."
193
+ },
194
+ "longitude": {
195
+ "type": "number",
196
+ "description": "The longitude of the address. Used when address details are not provided. Ignored otherwise."
197
+ }
198
+ },
199
+ "description": "The billing address for the card."
200
+ }
201
+ },
202
+ "required": [
203
+ "cardHolderName",
204
+ "billingAddress"
205
+ ]
206
+ },
207
+ "userAccount": {
208
+ "type": "object",
209
+ "properties": {
210
+ "email": {
211
+ "type": "string",
212
+ "description": "The primary email address associated with the account."
213
+ },
214
+ "username": {
215
+ "type": "string",
216
+ "description": "The username associated with the account. Please supply this even if it is the same as the email address."
217
+ },
218
+ "phone": {
219
+ "type": "string",
220
+ "description": "The phone number association with the account."
221
+ },
222
+ "createdDate": {
223
+ "type": "string",
224
+ "description": "`yyyy-MM-dd'T'HH:mm:ssZ` The date when the account was created. See the Dates section of these docs for more information about date formats."
225
+ },
226
+ "accountNumber": {
227
+ "type": "string",
228
+ "description": "Your unique identifier for the account."
229
+ },
230
+ "lastOrderId": {
231
+ "type": "string",
232
+ "description": "The unique identifier for the last order placed by this account, prior to the current order."
233
+ },
234
+ "aggregateOrderCount": {
235
+ "type": "number",
236
+ "description": "The total count of orders placed by this account since it was created, including the current order."
237
+ },
238
+ "aggregateOrderDollars": {
239
+ "type": "number",
240
+ "description": "The total amount spent by this account since it was created, including the current order."
241
+ },
242
+ "lastUpdateDate": {
243
+ "type": "string",
244
+ "description": "`yyyy-MM-dd'T'HH:mm:ssZ` The last time a change was made to this account other than an order being placed. Examples include changing email addresses or adding a new credit card. See the Dates section of these docs for more information about date formats."
245
+ }
246
+ }
247
+ },
248
+ "seller": {
249
+ "type": "object",
250
+ "properties": {
251
+ "name": {
252
+ "type": "string",
253
+ "description": "The business name of the seller."
254
+ },
255
+ "domain": {
256
+ "type": "string",
257
+ "description": "The domain of the seller."
258
+ },
259
+ "shipFromAddress": {
260
+ "type": "object",
261
+ "properties": {
262
+ "streetAddress": {
263
+ "type": "string",
264
+ "description": "The street number and street name."
265
+ },
266
+ "unit": {
267
+ "type": "string",
268
+ "description": "The unit or apartment number."
269
+ },
270
+ "city": {
271
+ "type": "string",
272
+ "description": "The city name."
273
+ },
274
+ "provinceCode": {
275
+ "type": "string",
276
+ "description": "The code or abbreviation for the province."
277
+ },
278
+ "postalCode string": {
279
+ "type": "string",
280
+ "description": "The postal code."
281
+ },
282
+ "countryCode": {
283
+ "type": "string",
284
+ "description": "The two-letter ISO-3166 country code. If left blank, we will assume US."
285
+ },
286
+ "latitude": {
287
+ "type": "number",
288
+ "description": "The latitude of the address. Used when address details are not provided. Ignored otherwise."
289
+ },
290
+ "longitude": {
291
+ "type": "number",
292
+ "description": "The longitude of the address. Used when address details are not provided. Ignored otherwise."
293
+ }
294
+ },
295
+ "description": "The location from which the seller shipped the order."
296
+ }
297
+ }
298
+ }
299
+ },
300
+ "required": [
301
+ "purchase",
302
+ "recipient",
303
+ "card"
304
+ ]
305
+ }