diff --git a/app/models/solidus_paypal_braintree/customer.rb b/app/models/solidus_paypal_braintree/customer.rb new file mode 100644 index 00000000..3a41402a --- /dev/null +++ b/app/models/solidus_paypal_braintree/customer.rb @@ -0,0 +1,4 @@ +class SolidusPaypalBraintree::Customer < ApplicationRecord + belongs_to :user, class_name: "Spree::User" + has_many :sources, class_name: "SolidusPaypalBraintree::Source", inverse_of: :customer +end diff --git a/app/models/solidus_paypal_braintree/gateway.rb b/app/models/solidus_paypal_braintree/gateway.rb index bfb66ccb..65054c73 100644 --- a/app/models/solidus_paypal_braintree/gateway.rb +++ b/app/models/solidus_paypal_braintree/gateway.rb @@ -131,7 +131,20 @@ def cancel(response_code) end end - def create_profile(_payment) + # Creates a new customer profile in Braintree + # + # @api public + # @param payment [Spree::Payment] + # @return [SolidusPaypalBraintree::Customer] + def create_profile(payment) + source = payment.source + + result = Braintree::Customer.create + customer_id = result.customer.id + + source.create_customer!(braintree_customer_id: customer_id).tap do + source.save! + end end # @return [String] diff --git a/app/models/solidus_paypal_braintree/source.rb b/app/models/solidus_paypal_braintree/source.rb index 72ca5c26..48c96e9a 100644 --- a/app/models/solidus_paypal_braintree/source.rb +++ b/app/models/solidus_paypal_braintree/source.rb @@ -1,6 +1,9 @@ class SolidusPaypalBraintree::Source < ApplicationRecord + belongs_to :user, class_name: "Spree::User" belongs_to :payment_method, class_name: 'Spree::PaymentMethod' + belongs_to :customer, class_name: "SolidusPaypalBraintree::Customer" + # we are not currenctly supporting an "imported" flag def imported false diff --git a/db/migrate/20160830061749_create_solidus_paypal_braintree_sources.rb b/db/migrate/20160830061749_create_solidus_paypal_braintree_sources.rb index 235ea389..6cca4d5a 100644 --- a/db/migrate/20160830061749_create_solidus_paypal_braintree_sources.rb +++ b/db/migrate/20160830061749_create_solidus_paypal_braintree_sources.rb @@ -4,6 +4,7 @@ def change t.string :nonce t.string :payment_type t.integer :user_id, index: true + t.references :customer, index: true t.references :payment_method, foreign_key: { to_table: :spree_payment_method }, index: { name: 'index_braintree_source_payment_method' } t.timestamps null: false diff --git a/db/migrate/20160906201711_create_solidus_paypal_braintree_customers.rb b/db/migrate/20160906201711_create_solidus_paypal_braintree_customers.rb new file mode 100644 index 00000000..2f168714 --- /dev/null +++ b/db/migrate/20160906201711_create_solidus_paypal_braintree_customers.rb @@ -0,0 +1,11 @@ +class CreateSolidusPaypalBraintreeCustomers < ActiveRecord::Migration + def change + create_table :solidus_paypal_braintree_customers do |t| + t.references :user + t.string :braintree_customer_id + end + + add_index :solidus_paypal_braintree_customers, :user_id, unique: true, name: "index_braintree_customers_on_user_id" + add_index :solidus_paypal_braintree_customers, :braintree_customer_id, unique: true, name: "index_braintree_customers_on_braintree_customer_id" + end +end diff --git a/spec/controllers/solidus_paypal_braintree/checkouts_controller_spec.rb b/spec/controllers/solidus_paypal_braintree/checkouts_controller_spec.rb index ac7dfa29..babacda3 100644 --- a/spec/controllers/solidus_paypal_braintree/checkouts_controller_spec.rb +++ b/spec/controllers/solidus_paypal_braintree/checkouts_controller_spec.rb @@ -4,7 +4,8 @@ RSpec.describe SolidusPaypalBraintree::CheckoutsController, type: :controller do include_context 'order ready for payment' - describe 'PATCH update' do + cassette_options = { cassette_name: "checkouts_controller/update" } + describe 'PATCH update', vcr: cassette_options do subject(:patch_update) { patch :update, params } let(:params) do diff --git a/spec/fixtures/cassettes/braintree/create_profile.yml b/spec/fixtures/cassettes/braintree/create_profile.yml new file mode 100644 index 00000000..6b49ad20 --- /dev/null +++ b/spec/fixtures/cassettes/braintree/create_profile.yml @@ -0,0 +1,141 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.sandbox.braintreegateway.com/merchants/7rdg92j7bm7fk5h3/customers + body: + encoding: UTF-8 + string: | + + + + headers: + Accept-Encoding: + - gzip + Accept: + - application/xml + User-Agent: + - Braintree Ruby Gem 2.66.0 + X-Apiversion: + - '4' + Authorization: + - Basic bXdqa2t4d2NwMzJja2huZjphOTI5OGY0M2IzMGM2OTlkYjMwNzJjYzRhMDBmN2Y0OQ== + Content-Type: + - application/xml + response: + status: + code: 201 + message: Created + headers: + Date: + - Tue, 06 Sep 2016 21:41:43 GMT + Content-Type: + - application/xml; charset=utf-8 + Transfer-Encoding: + - chunked + X-Frame-Options: + - SAMEORIGIN + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Authentication: + - basic_auth + X-User: + - 3v249hqtptsg744y + Vary: + - Accept-Encoding + Content-Encoding: + - gzip + Etag: + - W/"7e7cc6ad3329ffec365cc2fdfb82b56a" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 2bb50e11-9bd6-4011-a30d-27e90247ab3b + X-Runtime: + - '0.122484' + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + H4sIABc4z1cAA5SRzW6DMBCE73kKy3eXnxQIlSG3PkF66W3DLsEtNsg2bXj7 + OjRKKtEeKvky83l2R7bcn3XPPsg6NZiKJw8xZ2SaAZU5Vfzl8Cx2fF9vZDM5 + P2iy9YYxqbDOs7zMdlksoyAuXmBNB8aLoAuLpzJ9K466aN+zbiujn/Ryu1XW + eWFAEzOqr7i3E/FoQT38RZpBj2DmlU8aVL9yx24w6xktnFfeJx2d8r/sswSe + UIBnfh6p4hikV5p4ncZJLuJSxPkhTZ4ew9m+yugeWPLTiP/L3wPf+5c3F62i + Ht2tEiovGrDorkPBWpivjQHRknO0YqHb7QO/AAAA//8DAHz1uH/zAQAA + http_version: + recorded_at: Tue, 06 Sep 2016 21:42:27 GMT +- request: + method: post + uri: https://api.sandbox.braintreegateway.com/merchants/7rdg92j7bm7fk5h3/customers + body: + encoding: UTF-8 + string: | + + + + headers: + Accept-Encoding: + - gzip + Accept: + - application/xml + User-Agent: + - Braintree Ruby Gem 2.66.0 + X-Apiversion: + - '4' + Authorization: + - Basic bXdqa2t4d2NwMzJja2huZjphOTI5OGY0M2IzMGM2OTlkYjMwNzJjYzRhMDBmN2Y0OQ== + Content-Type: + - application/xml + response: + status: + code: 201 + message: Created + headers: + Date: + - Tue, 06 Sep 2016 21:41:44 GMT + Content-Type: + - application/xml; charset=utf-8 + Transfer-Encoding: + - chunked + X-Frame-Options: + - SAMEORIGIN + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Authentication: + - basic_auth + X-User: + - 3v249hqtptsg744y + Vary: + - Accept-Encoding + Content-Encoding: + - gzip + Etag: + - W/"a9b265882228cffd7ddca2afbbeaa10f" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - a3a2daaf-83d2-4c38-bb92-c2e4d2ecb310 + X-Runtime: + - '0.222559' + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + H4sIABg4z1cAA5RRy07DMBC89yss300eTRuKnPTGF5QLt2120xhsJ7IdaP4e + N1QtUuCA5MvM7OzOruX+bDT7IOdVbyuePaSckW16VPZU8ZfDs3jk+3olm9GH + 3pCrV4xJhXW2Los8zTYyieDCRa3pwAYRcenwtMvfyqMp2/dNt5bJT/VS3Srn + g7BgiFmlKx7cSDyZJQ1/KU1vBrDTgicDSi/YoevtskcL5wX3SUevwi/zHEEg + FBBYmAaqOEYYlCFex9W3It2JdHvIs6civuJVJnfD7B8H/J//bvieP99ctIo0 + +lskVEE04NBfm4JzMF0TA6Ij72mhxWy3D/wCAAD//wMAISGoAvMBAAA= + http_version: + recorded_at: Tue, 06 Sep 2016 21:42:27 GMT +recorded_with: VCR 3.0.3 diff --git a/spec/fixtures/cassettes/braintree/payment.yml b/spec/fixtures/cassettes/braintree/payment.yml index 0bcb7af6..26e235c4 100644 --- a/spec/fixtures/cassettes/braintree/payment.yml +++ b/spec/fixtures/cassettes/braintree/payment.yml @@ -107,4 +107,280 @@ http_interactions: sEg1b6xnzn/1DwAAAP//AwAU76XzrxcAAA== http_version: recorded_at: Tue, 06 Sep 2016 23:37:41 GMT +- request: + method: post + uri: https://api.sandbox.braintreegateway.com/merchants/7rdg92j7bm7fk5h3/customers + body: + encoding: UTF-8 + string: | + + + + headers: + Accept-Encoding: + - gzip + Accept: + - application/xml + User-Agent: + - Braintree Ruby Gem 2.66.0 + X-Apiversion: + - '4' + Authorization: + - Basic bXdqa2t4d2NwMzJja2huZjphOTI5OGY0M2IzMGM2OTlkYjMwNzJjYzRhMDBmN2Y0OQ== + Content-Type: + - application/xml + response: + status: + code: 201 + message: Created + headers: + Date: + - Wed, 07 Sep 2016 16:06:02 GMT + Content-Type: + - application/xml; charset=utf-8 + Transfer-Encoding: + - chunked + X-Frame-Options: + - SAMEORIGIN + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Authentication: + - basic_auth + X-User: + - 3v249hqtptsg744y + Vary: + - Accept-Encoding + Content-Encoding: + - gzip + Etag: + - W/"7660eb59912968fdff337835b6ea5942" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 0346b4a4-030a-467c-88e8-4acf70d1c1fe + X-Runtime: + - '0.146704' + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + H4sIAOo60FcAA5SRzU7DMBCE730Ky3eTH2h+kJPeeIJy4bbNbhqX2IlsB5q3 + xw1VixQ4IPky83l2R7bcnXXPPsg6NZiKJw8xZ2SaAZU5Vvx1/yIKvqs3spmc + HzTZesOYVFgX5TbLn4pCRkFcvMCaDowXQecWj2V6yg86b9+33aOMftLL7VZZ + 54UBTcyovuLeTsSjBfXwF2kGPYKZVz5pUP3KHbvBrGe0cF55n3Rwyv+yzxJ4 + QgGe+XmkimOQXmnidRonmYhLEef7JHuOw0nfZHQPLPlpxP/l74Hv/cubi1ZR + j+5WCZUXDVh016FgLczXxoBoyTlasdDt9oFfAAAA//8DAN7ctoPzAQAA + http_version: + recorded_at: Wed, 07 Sep 2016 16:06:47 GMT +- request: + method: post + uri: https://api.sandbox.braintreegateway.com/merchants/7rdg92j7bm7fk5h3/customers + body: + encoding: UTF-8 + string: | + + + + headers: + Accept-Encoding: + - gzip + Accept: + - application/xml + User-Agent: + - Braintree Ruby Gem 2.66.0 + X-Apiversion: + - '4' + Authorization: + - Basic bXdqa2t4d2NwMzJja2huZjphOTI5OGY0M2IzMGM2OTlkYjMwNzJjYzRhMDBmN2Y0OQ== + Content-Type: + - application/xml + response: + status: + code: 201 + message: Created + headers: + Date: + - Wed, 07 Sep 2016 16:06:02 GMT + Content-Type: + - application/xml; charset=utf-8 + Transfer-Encoding: + - chunked + X-Frame-Options: + - SAMEORIGIN + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Authentication: + - basic_auth + X-User: + - 3v249hqtptsg744y + Vary: + - Accept-Encoding + Content-Encoding: + - gzip + Etag: + - W/"96954c0df2790855a4108f8f0c4fe3f1" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - e22e5be4-0a97-4b62-91ad-8104d424d96e + X-Runtime: + - '0.134923' + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + H4sIAOo60FcAA5SRwW6DMBBE7/kKy3cXQxtIKkNu/YL00tuGXYJTbJBt2vD3 + dWiUVKI9VPJl5nl2R7banU3HPsh53duSpw+SM7J1j9oeS/66fxEbvqtWqh59 + 6A25asWY0lhlG/m0yfO1SqK4eJHVLdggoi4cHrfZqTiYonlft48q+Ukvtxvt + fBAWDDGru5IHNxJPZtTBX6TuzQB2WvhkQHcLd2h7u5zRwHnhfdLB6/DLPkcQ + CAUEFqaBSo5RBm2IV5lMcyG3Qhb7NH+W8WRvKrkH5vw44P/y98D3/vnNRaOp + Q3+rhDqIGhz661BwDqZrY0B05D0tWOx2+8AvAAAA//8DADWVWw7zAQAA + http_version: + recorded_at: Wed, 07 Sep 2016 16:06:47 GMT +- request: + method: post + uri: https://api.sandbox.braintreegateway.com/merchants/7rdg92j7bm7fk5h3/customers + body: + encoding: UTF-8 + string: | + + + + headers: + Accept-Encoding: + - gzip + Accept: + - application/xml + User-Agent: + - Braintree Ruby Gem 2.66.0 + X-Apiversion: + - '4' + Authorization: + - Basic bXdqa2t4d2NwMzJja2huZjphOTI5OGY0M2IzMGM2OTlkYjMwNzJjYzRhMDBmN2Y0OQ== + Content-Type: + - application/xml + response: + status: + code: 201 + message: Created + headers: + Date: + - Wed, 07 Sep 2016 16:06:03 GMT + Content-Type: + - application/xml; charset=utf-8 + Transfer-Encoding: + - chunked + X-Frame-Options: + - SAMEORIGIN + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Authentication: + - basic_auth + X-User: + - 3v249hqtptsg744y + Vary: + - Accept-Encoding + Content-Encoding: + - gzip + Etag: + - W/"720eeb1439a2e15376b63d72298f0002" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - b1cc5374-cb81-454b-920a-9b0133648eb1 + X-Runtime: + - '0.135676' + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + H4sIAOs60FcAA5SRwW6DMBBE7/kKy3cXA2ookSG3fkF66W3DLsEtNsg2bfj7 + OjRKKtEeKvky8zy7I1vtz6ZnH+S8HmzF0wfJGdlmQG1PFX85PIsnvq83qpl8 + GAy5esOY0ljnZV6UeSZVEsXFi6zpwAYRdeHwVGZvxdEU7ftjl6vkJ73cbrXz + QVgwxKzuKx7cRDxZUA9/kWYwI9h55ZMB3a/csRvsekYL55X3SUevwy/7HEEg + FBBYmEeqOEYZtCFeZzLdClkKWRzS7U7Gk7+q5B5Y8tOI/8vfA9/7lzcXraYe + /a0S6iAacOivQ8E5mK+NAdGR97RisdvtA78AAAD//wMA04mUsvMBAAA= + http_version: + recorded_at: Wed, 07 Sep 2016 16:06:47 GMT +- request: + method: post + uri: https://api.sandbox.braintreegateway.com/merchants/7rdg92j7bm7fk5h3/customers + body: + encoding: UTF-8 + string: | + + + + headers: + Accept-Encoding: + - gzip + Accept: + - application/xml + User-Agent: + - Braintree Ruby Gem 2.66.0 + X-Apiversion: + - '4' + Authorization: + - Basic bXdqa2t4d2NwMzJja2huZjphOTI5OGY0M2IzMGM2OTlkYjMwNzJjYzRhMDBmN2Y0OQ== + Content-Type: + - application/xml + response: + status: + code: 201 + message: Created + headers: + Date: + - Wed, 07 Sep 2016 16:06:03 GMT + Content-Type: + - application/xml; charset=utf-8 + Transfer-Encoding: + - chunked + X-Frame-Options: + - SAMEORIGIN + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Authentication: + - basic_auth + X-User: + - 3v249hqtptsg744y + Vary: + - Accept-Encoding + Content-Encoding: + - gzip + Etag: + - W/"c62a72c8d735c4362d50a1b650a5a3d5" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 42a7efa0-81cb-4918-b1c3-8592eb40b66d + X-Runtime: + - '0.112177' + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + H4sIAOs60FcAA5SRzU7DMBCE730Ky3eTn0LcIie98QTlwm2b3TQusRPZDjRv + jxuqFilwQPJl5vPsjmy1O5uOfZDzurclzx5SzsjWPWp7LPnr/kVs+K5aqXr0 + oTfkqhVjSmO1KTZ5Jh+lSqK4eJHVLdggopYOj9v8JA9GNu9P7VolP+nldqOd + D8KCIWZ1V/LgRuLJjDr4i9S9GcBOC58M6G7hDm1vlzMaOC+8Tzp4HX7Z5wgC + oYDAwjRQyTHKoA3xKk+zQqRbkcp9Vjyn8azfVHIPzPlxwP/l74Hv/fObi0ZT + h/5WCXUQNTj016HgHEzXxoDoyHtasNjt9oFfAAAA//8DAMD/2rPzAQAA + http_version: + recorded_at: Wed, 07 Sep 2016 16:06:48 GMT recorded_with: VCR 3.0.3 diff --git a/spec/fixtures/cassettes/checkouts_controller/update.yml b/spec/fixtures/cassettes/checkouts_controller/update.yml new file mode 100644 index 00000000..12bdcb78 --- /dev/null +++ b/spec/fixtures/cassettes/checkouts_controller/update.yml @@ -0,0 +1,72 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.sandbox.braintreegateway.com/merchants/7rdg92j7bm7fk5h3/customers + body: + encoding: UTF-8 + string: | + + + + headers: + Accept-Encoding: + - gzip + Accept: + - application/xml + User-Agent: + - Braintree Ruby Gem 2.66.0 + X-Apiversion: + - '4' + Authorization: + - Basic bXdqa2t4d2NwMzJja2huZjphOTI5OGY0M2IzMGM2OTlkYjMwNzJjYzRhMDBmN2Y0OQ== + Content-Type: + - application/xml + response: + status: + code: 201 + message: Created + headers: + Date: + - Tue, 06 Sep 2016 22:26:30 GMT + Content-Type: + - application/xml; charset=utf-8 + Transfer-Encoding: + - chunked + X-Frame-Options: + - SAMEORIGIN + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Authentication: + - basic_auth + X-User: + - 3v249hqtptsg744y + Vary: + - Accept-Encoding + Content-Encoding: + - gzip + Etag: + - W/"431145f5e51c78e5b27309a6202f5af4" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - a04f1687-4ae7-4e9b-a086-f9cb44961da4 + X-Runtime: + - '0.146319' + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + H4sIAJZCz1cAA5SRwW6DMBBE7/kKy3cXYhRoIkNu/YL00tuGXYJTbJBt2vD3 + dWiUVKI9VD7NPM/uyFb7i+nYBzmve1vy9VPKGdm6R21PJX89vIhnvq9Wqh59 + 6A25asWY0ljJLJ4i26gkiqsXWd2CDSLqwuFpK8/F0RTN+6bNVPKTXm832vkg + LBhiVnclD24knsyog79I3ZsB7LTwyYDuFu7Q9nY5o4HLwvuko9fhl32OIBAK + CCxMA5UcowzaEK9kus5FuhVpfpByJ/Ndlr6p5BGY8+OA/8s/At/75zcXjaYO + /b0S6iBqcOhvQ8E5mG6NAdGR97Rgsdv9A78AAAD//wMABrZt9/MBAAA= + http_version: + recorded_at: Tue, 06 Sep 2016 22:27:14 GMT +recorded_with: VCR 3.0.3 diff --git a/spec/models/solidus_paypal_braintree/gateway_spec.rb b/spec/models/solidus_paypal_braintree/gateway_spec.rb index aa4aa27f..e1abb807 100644 --- a/spec/models/solidus_paypal_braintree/gateway_spec.rb +++ b/spec/models/solidus_paypal_braintree/gateway_spec.rb @@ -7,9 +7,12 @@ described_class.new end + let(:user) { create :user } + let(:source) do SolidusPaypalBraintree::Source.new( - nonce: 'fake-paypal-future-nonce' + nonce: 'fake-paypal-future-nonce', + user: user ) end @@ -170,6 +173,24 @@ end end end + + cassette_options = { cassette_name: "braintree/create_profile" } + describe "#create_profile", vcr: cassette_options do + let(:payment) do + create(:payment, { + payment_method: gateway, + source: source + }) + end + + subject(:profile) { gateway.create_profile(payment) } + + it 'creates and returns a new customer profile', aggregate_failures: true do + expect(profile).to be_a SolidusPaypalBraintree::Customer + expect(profile.sources).to eq [source] + expect(profile.braintree_customer_id).to be_present + end + end end cassette_options = { cassette_name: "braintree/token" }