solidus_braintree 1.4.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +12 -0
- data/.gem_release.yml +5 -0
- data/.github_changelog_generator +2 -0
- data/.gitignore +20 -11
- data/.rspec +1 -1
- data/.rubocop.yml +79 -0
- data/CHANGELOG.md +178 -18
- data/Gemfile +31 -23
- data/LICENSE +26 -0
- data/README.md +387 -26
- data/Rakefile +4 -19
- data/app/assets/config/solidus_braintree_manifest.js +1 -0
- data/app/assets/images/solidus_braintree/venmo/venmo_active_blue_button_280x48.svg +19 -0
- data/app/assets/images/solidus_braintree/venmo/venmo_active_blue_button_320x48.svg +19 -0
- data/app/assets/images/solidus_braintree/venmo/venmo_active_blue_button_375x48.svg +19 -0
- data/app/assets/images/solidus_braintree/venmo/venmo_active_white_button_280x48.svg +19 -0
- data/app/assets/images/solidus_braintree/venmo/venmo_active_white_button_320x48.svg +19 -0
- data/app/assets/images/solidus_braintree/venmo/venmo_active_white_button_375x48.svg +19 -0
- data/app/assets/images/solidus_braintree/venmo/venmo_blue_acceptance_mark.svg +15 -0
- data/app/assets/images/solidus_braintree/venmo/venmo_blue_button_280x48.svg +19 -0
- data/app/assets/images/solidus_braintree/venmo/venmo_blue_button_320x48.svg +19 -0
- data/app/assets/images/solidus_braintree/venmo/venmo_blue_button_375x48.svg +19 -0
- data/app/assets/images/solidus_braintree/venmo/venmo_blue_logo.svg +18 -0
- data/app/assets/images/solidus_braintree/venmo/venmo_white_acceptance_mark.svg +20 -0
- data/app/assets/images/solidus_braintree/venmo/venmo_white_button_280x48.svg +19 -0
- data/app/assets/images/solidus_braintree/venmo/venmo_white_button_320x48.svg +19 -0
- data/app/assets/images/solidus_braintree/venmo/venmo_white_button_375x48.svg +19 -0
- data/app/assets/images/solidus_braintree/venmo/venmo_white_logo.svg +18 -0
- data/app/assets/javascripts/solidus_braintree/apple_pay_button.js +179 -0
- data/app/assets/javascripts/solidus_braintree/checkout.js +108 -0
- data/app/assets/javascripts/solidus_braintree/client.js +239 -0
- data/app/assets/javascripts/solidus_braintree/constants.js +89 -0
- data/app/assets/javascripts/solidus_braintree/frontend.js +14 -0
- data/app/assets/javascripts/solidus_braintree/hosted_form.js +46 -0
- data/app/assets/javascripts/solidus_braintree/paypal_button.js +178 -0
- data/app/assets/javascripts/solidus_braintree/paypal_messaging.js +22 -0
- data/app/assets/javascripts/solidus_braintree/promise.js +20 -0
- data/app/assets/javascripts/solidus_braintree/venmo_button.js +86 -0
- data/app/assets/javascripts/spree/backend/solidus_braintree.js +96 -0
- data/app/assets/javascripts/spree/frontend/paypal_button.js +34 -0
- data/app/assets/javascripts/spree/frontend/solidus_braintree.js +1 -0
- data/app/assets/stylesheets/spree/backend/solidus_braintree.scss +28 -0
- data/app/assets/stylesheets/spree/frontend/solidus_braintree.scss +51 -0
- data/app/decorators/controllers/solidus_braintree/admin_payments_controller_decorator.rb +11 -0
- data/app/decorators/controllers/solidus_braintree/checkout_controller_decorator.rb +11 -0
- data/app/decorators/controllers/solidus_braintree/client_tokens_controller.rb +41 -0
- data/app/decorators/controllers/solidus_braintree/orders_controller_decorator.rb +11 -0
- data/app/decorators/models/solidus_braintree/spree/store_decorator.rb +20 -0
- data/app/decorators/models/solidus_braintree/spree/user_decorator.rb +13 -0
- data/app/helpers/solidus_braintree/braintree_admin_helper.rb +23 -0
- data/app/helpers/solidus_braintree/braintree_checkout_helper.rb +60 -0
- data/app/models/application_record.rb +5 -0
- data/app/models/solidus_braintree/address.rb +64 -0
- data/app/models/solidus_braintree/avs_result.rb +69 -0
- data/app/models/solidus_braintree/configuration.rb +39 -0
- data/app/models/solidus_braintree/customer.rb +8 -0
- data/app/models/solidus_braintree/gateway.rb +433 -0
- data/app/models/solidus_braintree/response.rb +80 -0
- data/app/models/solidus_braintree/source.rb +135 -0
- data/app/models/solidus_braintree/transaction.rb +31 -0
- data/app/models/solidus_braintree/transaction_address.rb +88 -0
- data/app/models/solidus_braintree/transaction_import.rb +98 -0
- data/app/overrides/spree/payments/payment/add_paypal_funding_source_to_payment.rb +9 -0
- data/app/views/spree/api/payments/source_views/_braintree.json.jbuilder +1 -1
- data/app/views/spree/checkout/existing_payment/_braintree.html.erb +10 -0
- data/app/views/spree/shared/_apple_pay_button.html.erb +27 -0
- data/app/views/spree/shared/_braintree_errors.html.erb +16 -0
- data/app/views/spree/shared/_braintree_head_scripts.html.erb +26 -0
- data/app/views/spree/shared/_braintree_hosted_fields.html.erb +43 -0
- data/app/views/spree/shared/_paypal_cart_button.html.erb +38 -0
- data/app/views/spree/shared/_paypal_messaging.html.erb +13 -0
- data/app/views/spree/shared/_venmo_button.html.erb +33 -0
- data/bin/console +4 -1
- data/bin/rails +5 -5
- data/bin/rails-engine +13 -0
- data/bin/rails-sandbox +16 -0
- data/bin/rake +7 -0
- data/bin/sandbox +103 -0
- data/bin/setup +5 -4
- data/config/locales/en.yml +94 -2
- data/config/locales/it.yml +56 -0
- data/config/routes.rb +12 -3
- data/db/migrate/20160830061749_create_solidus_paypal_braintree_sources.rb +16 -0
- data/db/migrate/20160906201711_create_solidus_paypal_braintree_customers.rb +13 -0
- data/db/migrate/20161114231422_create_solidus_paypal_braintree_configurations.rb +11 -0
- data/db/migrate/20161125172005_add_braintree_configuration_to_stores.rb +7 -0
- data/db/migrate/20170203191030_add_credit_card_to_braintree_configuration.rb +6 -0
- data/db/migrate/20170505193712_add_null_constraint_to_sources.rb +38 -0
- data/db/migrate/20170508085402_add_not_null_constraint_to_sources_payment_type.rb +14 -0
- data/db/migrate/20190705115327_add_paypal_button_preferences_to_braintree_configurations.rb +5 -0
- data/db/migrate/20190911141712_add_3d_secure_to_braintree_configuration.rb +5 -0
- data/db/migrate/20211222170950_add_paypal_funding_source_to_solidus_paypal_braintree_sources.rb +5 -0
- data/db/migrate/20220104150301_add_venmo_to_braintree_configuration.rb +5 -0
- data/db/migrate/20230109080950_rename_solidus_paypal_braintree_source_type.rb +31 -0
- data/lib/controllers/backend/solidus_braintree/configurations_controller.rb +48 -0
- data/lib/controllers/frontend/solidus_braintree/checkouts_controller.rb +31 -0
- data/lib/controllers/frontend/solidus_braintree/transactions_controller.rb +67 -0
- data/lib/generators/solidus_braintree/install/install_generator.rb +54 -18
- data/lib/generators/solidus_braintree/install/templates/initializer.rb +6 -0
- data/lib/solidus_braintree/country_mapper.rb +37 -0
- data/lib/solidus_braintree/engine.rb +55 -10
- data/lib/solidus_braintree/extension_configuration.rb +23 -0
- data/lib/solidus_braintree/request_protection.rb +21 -0
- data/lib/solidus_braintree/testing_support/factories.rb +53 -0
- data/lib/solidus_braintree/version.rb +3 -1
- data/lib/solidus_braintree.rb +14 -2
- data/lib/solidus_paypal_braintree.rb +6 -0
- data/lib/views/backend/solidus_braintree/configurations/list.html.erb +63 -0
- data/lib/views/backend/spree/admin/payments/source_forms/_braintree.html.erb +16 -0
- data/lib/views/backend/spree/admin/payments/source_views/_braintree.html.erb +39 -0
- data/lib/views/backend/spree/admin/shared/preference_fields/_preference_select.html.erb +13 -0
- data/lib/views/backend_v1.2/spree/admin/payments/source_forms/_braintree.html.erb +16 -0
- data/lib/views/backend_v2.4/spree/admin/shared/preference_fields/_hash.html.erb +12 -0
- data/lib/views/frontend/solidus_braintree/payments/_payment.html.erb +12 -0
- data/lib/views/frontend/spree/checkout/payment/_braintree.html.erb +23 -0
- data/lib/views/frontend/spree/shared/_paypal_checkout_button.html.erb +32 -0
- data/solidus_braintree.gemspec +39 -38
- data/spec/controllers/solidus_braintree/checkouts_controller_spec.rb +99 -0
- data/spec/controllers/solidus_braintree/client_tokens_controller_spec.rb +55 -0
- data/spec/controllers/solidus_braintree/configurations_controller_spec.rb +73 -0
- data/spec/controllers/solidus_braintree/transactions_controller_spec.rb +183 -0
- data/spec/features/backend/configuration_spec.rb +23 -0
- data/spec/features/backend/new_payment_spec.rb +137 -0
- data/spec/features/frontend/braintree_credit_card_checkout_spec.rb +191 -0
- data/spec/features/frontend/paypal_checkout_spec.rb +166 -0
- data/spec/features/frontend/venmo_checkout_spec.rb +194 -0
- data/spec/fixtures/cassettes/admin/invalid_credit_card.yml +63 -0
- data/spec/fixtures/cassettes/admin/resubmit_credit_card.yml +352 -0
- data/spec/fixtures/cassettes/admin/valid_credit_card.yml +412 -0
- data/spec/fixtures/cassettes/braintree/create_profile.yml +71 -0
- data/spec/fixtures/cassettes/braintree/generate_token.yml +63 -0
- data/spec/fixtures/cassettes/braintree/token.yml +63 -0
- data/spec/fixtures/cassettes/checkout/invalid_credit_card.yml +63 -0
- data/spec/fixtures/cassettes/checkout/resubmit_credit_card.yml +216 -0
- data/spec/fixtures/cassettes/checkout/update.yml +71 -0
- data/spec/fixtures/cassettes/checkout/valid_credit_card.yml +171 -0
- data/spec/fixtures/cassettes/checkout/valid_venmo_transaction.yml +599 -0
- data/spec/fixtures/cassettes/gateway/authorize/credit_card/address.yml +86 -0
- data/spec/fixtures/cassettes/gateway/authorize/merchant_account/EUR.yml +154 -0
- data/spec/fixtures/cassettes/gateway/authorize/paypal/EUR.yml +90 -0
- data/spec/fixtures/cassettes/gateway/authorize/paypal/address.yml +90 -0
- data/spec/fixtures/cassettes/gateway/authorize.yml +86 -0
- data/spec/fixtures/cassettes/gateway/authorized_transaction.yml +73 -0
- data/spec/fixtures/cassettes/gateway/cancel/missing.yml +63 -0
- data/spec/fixtures/cassettes/gateway/cancel/refunds.yml +272 -0
- data/spec/fixtures/cassettes/gateway/cancel/void.yml +201 -0
- data/spec/fixtures/cassettes/gateway/capture.yml +141 -0
- data/spec/fixtures/cassettes/gateway/complete.yml +157 -0
- data/spec/fixtures/cassettes/gateway/credit.yml +208 -0
- data/spec/fixtures/cassettes/gateway/customer.yml +79 -0
- data/spec/fixtures/cassettes/gateway/purchase.yml +87 -0
- data/spec/fixtures/cassettes/gateway/settled_transaction.yml +140 -0
- data/spec/fixtures/cassettes/gateway/void.yml +137 -0
- data/spec/fixtures/cassettes/source/bin.yml +295 -0
- data/spec/fixtures/cassettes/source/card_type.yml +267 -0
- data/spec/fixtures/cassettes/source/last4.yml +267 -0
- data/spec/fixtures/cassettes/transaction/import/valid/capture.yml +224 -0
- data/spec/fixtures/cassettes/transaction/import/valid.yml +71 -0
- data/spec/fixtures/views/spree/orders/edit.html.erb +50 -0
- data/spec/helpers/solidus_braintree/braintree_admin_helper_spec.rb +17 -0
- data/spec/helpers/solidus_braintree/braintree_checkout_helper_spec.rb +70 -0
- data/spec/models/solidus_braintree/address_spec.rb +71 -0
- data/spec/models/solidus_braintree/avs_result_spec.rb +317 -0
- data/spec/models/solidus_braintree/gateway_spec.rb +742 -0
- data/spec/models/solidus_braintree/response_spec.rb +280 -0
- data/spec/models/solidus_braintree/source_spec.rb +539 -0
- data/spec/models/solidus_braintree/transaction_address_spec.rb +235 -0
- data/spec/models/solidus_braintree/transaction_import_spec.rb +302 -0
- data/spec/models/solidus_braintree/transaction_spec.rb +86 -0
- data/spec/models/spree/store_spec.rb +14 -0
- data/spec/requests/spree/api/orders_controller_spec.rb +36 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support/capybara.rb +7 -0
- data/spec/support/gateway_helpers.rb +29 -0
- data/spec/support/order_ready_for_payment.rb +37 -0
- data/spec/support/vcr.rb +42 -0
- data/spec/support/views.rb +1 -0
- metadata +276 -224
- data/LICENSE.txt +0 -21
- data/app/controllers/spree/api/braintree_client_token_controller.rb +0 -13
- data/app/decorators/lib/solidus_braintree/spree/permitted_attributes_decorator.rb +0 -9
- data/app/decorators/models/solidus_braintree/spree/credit_card_decorator.rb +0 -11
- data/app/decorators/models/solidus_braintree/spree/payment_decorator.rb +0 -10
- data/app/helpers/braintree_view_helpers.rb +0 -20
- data/app/models/concerns/solidus_braintree/add_name_validation_concern.rb +0 -8
- data/app/models/concerns/solidus_braintree/inject_device_data_concern.rb +0 -18
- data/app/models/concerns/solidus_braintree/payment_braintree_nonce_concern.rb +0 -8
- data/app/models/concerns/solidus_braintree/permitted_attributes_concern.rb +0 -11
- data/app/models/concerns/solidus_braintree/skip_require_card_numbers_concern.rb +0 -14
- data/app/models/concerns/solidus_braintree/use_data_field_concern.rb +0 -23
- data/app/models/solidus/gateway/braintree_gateway.rb +0 -306
- data/app/overrides/spree/checkout/_confirm/braintree_security.html.erb.deface +0 -9
- data/app/views/spree/admin/payments/source_forms/_braintree.html.erb +0 -38
- data/app/views/spree/admin/payments/source_views/_braintree.html.erb +0 -30
- data/app/views/spree/checkout/payment/_braintree.html.erb +0 -55
- data/app/views/spree/checkout/payment/_braintree_initialization.html.erb +0 -12
- data/config/initializers/braintree.rb +0 -3
- data/db/migrate/20150910170527_add_data_to_credit_card.rb +0 -5
- data/db/migrate/20160426221931_add_braintree_device_data_to_order.rb +0 -5
- data/lib/assets/javascripts/spree/backend/braintree/solidus_braintree.js +0 -59
- data/lib/assets/javascripts/spree/frontend/braintree/solidus_braintree.js +0 -144
- data/lib/assets/javascripts/vendor/braintree.js +0 -8
- data/lib/assets/stylesheets/spree/frontend/solidus_braintree.scss +0 -26
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'braintree'
|
|
4
|
+
require 'solidus_braintree/request_protection'
|
|
5
|
+
|
|
6
|
+
module SolidusBraintree
|
|
7
|
+
class Gateway < ::Spree::PaymentMethod
|
|
8
|
+
include RequestProtection
|
|
9
|
+
|
|
10
|
+
class TokenGenerationDisabledError < StandardError; end
|
|
11
|
+
|
|
12
|
+
# Error message from Braintree that gets returned by a non voidable transaction
|
|
13
|
+
NON_VOIDABLE_STATUS_ERROR_REGEXP = /can only be voided if status is authorized/.freeze
|
|
14
|
+
|
|
15
|
+
TOKEN_GENERATION_DISABLED_MESSAGE = 'Token generation is disabled. ' \
|
|
16
|
+
'To re-enable set the `token_generation_enabled` preference on the ' \
|
|
17
|
+
'gateway to `true`.'
|
|
18
|
+
|
|
19
|
+
ALLOWED_BRAINTREE_OPTIONS = [
|
|
20
|
+
:device_data,
|
|
21
|
+
:device_session_id,
|
|
22
|
+
:merchant_account_id,
|
|
23
|
+
:order_id
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
VOIDABLE_STATUSES = [
|
|
27
|
+
Braintree::Transaction::Status::SubmittedForSettlement,
|
|
28
|
+
Braintree::Transaction::Status::SettlementPending,
|
|
29
|
+
Braintree::Transaction::Status::Authorized
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
# This is useful in feature tests to avoid rate limited requests from
|
|
33
|
+
# Braintree
|
|
34
|
+
preference(:client_sdk_enabled, :boolean, default: true)
|
|
35
|
+
|
|
36
|
+
preference(:token_generation_enabled, :boolean, default: true)
|
|
37
|
+
|
|
38
|
+
# Preferences for configuration of Braintree credentials
|
|
39
|
+
preference(:environment, :string, default: 'sandbox')
|
|
40
|
+
preference(:merchant_id, :string, default: nil)
|
|
41
|
+
preference(:public_key, :string, default: nil)
|
|
42
|
+
preference(:private_key, :string, default: nil)
|
|
43
|
+
preference(:http_open_timeout, :integer, default: 60)
|
|
44
|
+
preference(:http_read_timeout, :integer, default: 60)
|
|
45
|
+
preference(:merchant_currency_map, :hash, default: {})
|
|
46
|
+
preference(:paypal_payee_email_map, :hash, default: {})
|
|
47
|
+
|
|
48
|
+
# Which checkout flow to use (vault/checkout)
|
|
49
|
+
preference(:paypal_flow, :string, default: 'vault')
|
|
50
|
+
|
|
51
|
+
# A hash that gets passed to the `style` key when initializing the credit card fields.
|
|
52
|
+
# See https://developers.braintreepayments.com/guides/hosted-fields/styling/javascript/v3
|
|
53
|
+
preference(:credit_card_fields_style, :hash, default: {})
|
|
54
|
+
|
|
55
|
+
# A hash that gets its keys passed to the associated braintree field placeholder tag.
|
|
56
|
+
# Example: { number: "Enter card number", cvv: "Enter CVV", expirationDate: "mm/yy" }
|
|
57
|
+
preference(:placeholder_text, :hash, default: {})
|
|
58
|
+
|
|
59
|
+
# Wether to use the JS device data collector
|
|
60
|
+
preference(:use_data_collector, :boolean, default: true)
|
|
61
|
+
|
|
62
|
+
# Useful for testing purposes, as PayPal will show funding sources based on the buyer's country;
|
|
63
|
+
# usually retrieved by their ip geolocation. I.e. Venmo will show for US buyers, but not European.
|
|
64
|
+
preference(:force_buyer_country, :string)
|
|
65
|
+
|
|
66
|
+
preference(:enable_venmo_funding, :boolean, default: false)
|
|
67
|
+
|
|
68
|
+
# When on mobile, paying with Venmo, the user may be returned to the same store tab
|
|
69
|
+
# depending on if their browser supports it, otherwise a new tab will be created
|
|
70
|
+
# However, returning to a new tab may break the payment checkout flow for some stores, for example,
|
|
71
|
+
# if they are single-page applications (SPA). Set this to false if this is the case
|
|
72
|
+
preference(:venmo_new_tab_support, :boolean, default: true)
|
|
73
|
+
|
|
74
|
+
def partial_name
|
|
75
|
+
"braintree"
|
|
76
|
+
end
|
|
77
|
+
alias_method :method_type, :partial_name
|
|
78
|
+
|
|
79
|
+
def payment_source_class
|
|
80
|
+
Source
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def braintree
|
|
84
|
+
@braintree ||= Braintree::Gateway.new(gateway_options)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def gateway_options
|
|
88
|
+
{
|
|
89
|
+
environment: preferred_environment.to_sym,
|
|
90
|
+
merchant_id: preferred_merchant_id,
|
|
91
|
+
public_key: preferred_public_key,
|
|
92
|
+
private_key: preferred_private_key,
|
|
93
|
+
http_open_timeout: preferred_http_open_timeout,
|
|
94
|
+
http_read_timeout: preferred_http_read_timeout,
|
|
95
|
+
logger: logger
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Create a payment and submit it for settlement all at once.
|
|
100
|
+
#
|
|
101
|
+
# @api public
|
|
102
|
+
# @param money_cents [Number, String] amount to authorize
|
|
103
|
+
# @param source [Source] payment source
|
|
104
|
+
# @params gateway_options [Hash]
|
|
105
|
+
# extra options to send along. e.g.: device data for fraud prevention
|
|
106
|
+
# @return [Response]
|
|
107
|
+
def purchase(money_cents, source, gateway_options)
|
|
108
|
+
protected_request do
|
|
109
|
+
result = braintree.transaction.sale(
|
|
110
|
+
amount: dollars(money_cents),
|
|
111
|
+
**transaction_options(source, gateway_options, submit_for_settlement: true)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
Response.build(result)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Authorize a payment to be captured later.
|
|
119
|
+
#
|
|
120
|
+
# @api public
|
|
121
|
+
# @param money_cents [Number, String] amount to authorize
|
|
122
|
+
# @param source [Source] payment source
|
|
123
|
+
# @params gateway_options [Hash]
|
|
124
|
+
# extra options to send along. e.g.: device data for fraud prevention
|
|
125
|
+
# @return [Response]
|
|
126
|
+
def authorize(money_cents, source, gateway_options)
|
|
127
|
+
protected_request do
|
|
128
|
+
result = braintree.transaction.sale(
|
|
129
|
+
amount: dollars(money_cents),
|
|
130
|
+
**transaction_options(source, gateway_options)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
Response.build(result)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Collect funds from an authorized payment.
|
|
138
|
+
#
|
|
139
|
+
# @api public
|
|
140
|
+
# @param money_cents [Number, String]
|
|
141
|
+
# amount to capture (partial settlements are supported by the gateway)
|
|
142
|
+
# @param response_code [String] the transaction id of the payment to capture
|
|
143
|
+
# @return [Response]
|
|
144
|
+
def capture(money_cents, response_code, _gateway_options)
|
|
145
|
+
protected_request do
|
|
146
|
+
result = braintree.transaction.submit_for_settlement(
|
|
147
|
+
response_code,
|
|
148
|
+
dollars(money_cents)
|
|
149
|
+
)
|
|
150
|
+
Response.build(result)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Used to refeund a customer for an already settled transaction.
|
|
155
|
+
#
|
|
156
|
+
# @api public
|
|
157
|
+
# @param money_cents [Number, String] amount to refund
|
|
158
|
+
# @param response_code [String] the transaction id of the payment to refund
|
|
159
|
+
# @return [Response]
|
|
160
|
+
def credit(money_cents, _source, response_code, _gateway_options)
|
|
161
|
+
protected_request do
|
|
162
|
+
result = braintree.transaction.refund(
|
|
163
|
+
response_code,
|
|
164
|
+
dollars(money_cents)
|
|
165
|
+
)
|
|
166
|
+
Response.build(result)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Used to cancel a transaction before it is settled.
|
|
171
|
+
#
|
|
172
|
+
# @api public
|
|
173
|
+
# @param response_code [String] the transaction id of the payment to void
|
|
174
|
+
# @return [Response]
|
|
175
|
+
def void(response_code, _source, _gateway_options)
|
|
176
|
+
protected_request do
|
|
177
|
+
result = braintree.transaction.void(response_code)
|
|
178
|
+
Response.build(result)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Will either refund or void the payment depending on its state.
|
|
183
|
+
#
|
|
184
|
+
# If the transaction has not yet been settled, we can void the transaction.
|
|
185
|
+
# Otherwise, we need to issue a refund.
|
|
186
|
+
#
|
|
187
|
+
# @api public
|
|
188
|
+
# @param response_code [String] the transaction id of the payment to void
|
|
189
|
+
# @return [Response]
|
|
190
|
+
def cancel(response_code)
|
|
191
|
+
transaction = protected_request do
|
|
192
|
+
braintree.transaction.find(response_code)
|
|
193
|
+
end
|
|
194
|
+
if VOIDABLE_STATUSES.include?(transaction.status)
|
|
195
|
+
void(response_code, nil, {})
|
|
196
|
+
else
|
|
197
|
+
credit(cents(transaction.amount), nil, response_code, {})
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Will void the payment depending on its state or return false
|
|
202
|
+
#
|
|
203
|
+
# Used by Solidus >= 2.4 instead of +cancel+
|
|
204
|
+
#
|
|
205
|
+
# If the transaction has not yet been settled, we can void the transaction.
|
|
206
|
+
# Otherwise, we return false so Solidus creates a refund instead.
|
|
207
|
+
#
|
|
208
|
+
# @api public
|
|
209
|
+
# @param payment [Spree::Payment] the payment to void
|
|
210
|
+
# @return [Response|FalseClass]
|
|
211
|
+
def try_void(payment)
|
|
212
|
+
transaction = braintree.transaction.find(payment.response_code)
|
|
213
|
+
if transaction.status.in? SolidusBraintree::Gateway::VOIDABLE_STATUSES
|
|
214
|
+
# Sometimes Braintree returns a voidable status although it is not voidable anymore.
|
|
215
|
+
# When we try to void that transaction we receive an error and need to return false
|
|
216
|
+
# so Solidus can create a refund instead.
|
|
217
|
+
begin
|
|
218
|
+
void(payment.response_code, nil, {})
|
|
219
|
+
rescue ActiveMerchant::ConnectionError => e
|
|
220
|
+
e.message.match(NON_VOIDABLE_STATUS_ERROR_REGEXP) ? false : raise(e)
|
|
221
|
+
end
|
|
222
|
+
else
|
|
223
|
+
false
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Creates a new customer profile in Braintree
|
|
228
|
+
#
|
|
229
|
+
# @api public
|
|
230
|
+
# @param payment [Spree::Payment]
|
|
231
|
+
# @return [SolidusBraintree::Customer]
|
|
232
|
+
def create_profile(payment)
|
|
233
|
+
source = payment.source
|
|
234
|
+
|
|
235
|
+
return if source.token.present? || source.customer.present? || source.nonce.nil?
|
|
236
|
+
|
|
237
|
+
result = braintree.customer.create(customer_profile_params(payment))
|
|
238
|
+
fail ::Spree::Core::GatewayError, result.message unless result.success?
|
|
239
|
+
|
|
240
|
+
customer = result.customer
|
|
241
|
+
|
|
242
|
+
source.create_customer!(braintree_customer_id: customer.id).tap do
|
|
243
|
+
if customer.payment_methods.any?
|
|
244
|
+
source.token = customer.payment_methods.last.token
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
source.save!
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# @raise [TokenGenerationDisabledError]
|
|
252
|
+
# If `preferred_token_generation_enabled` is false
|
|
253
|
+
#
|
|
254
|
+
# @return [String]
|
|
255
|
+
# The token that should be used along with the Braintree js-client sdk.
|
|
256
|
+
#
|
|
257
|
+
# @example
|
|
258
|
+
# <script>
|
|
259
|
+
# var token = #{Spree::Braintree::Gateway.first!.generate_token}
|
|
260
|
+
#
|
|
261
|
+
# braintree.client.create(
|
|
262
|
+
# {
|
|
263
|
+
# authorization: token
|
|
264
|
+
# },
|
|
265
|
+
# function(clientError, clientInstance) {
|
|
266
|
+
# ...
|
|
267
|
+
# }
|
|
268
|
+
# );
|
|
269
|
+
# </script>
|
|
270
|
+
def generate_token
|
|
271
|
+
unless preferred_token_generation_enabled
|
|
272
|
+
raise TokenGenerationDisabledError, TOKEN_GENERATION_DISABLED_MESSAGE
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
braintree.client_token.generate
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def payment_profiles_supported?
|
|
279
|
+
true
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def sources_by_order(order)
|
|
283
|
+
source_ids = order.payments.where(payment_method_id: id).pluck(:source_id).uniq
|
|
284
|
+
payment_source_class.where(id: source_ids).with_payment_profile
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def reusable_sources(order)
|
|
288
|
+
if order.completed?
|
|
289
|
+
sources_by_order(order)
|
|
290
|
+
elsif order.user_id
|
|
291
|
+
payment_source_class.where(
|
|
292
|
+
payment_method_id: id,
|
|
293
|
+
user_id: order.user_id
|
|
294
|
+
).with_payment_profile
|
|
295
|
+
else
|
|
296
|
+
[]
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
private
|
|
301
|
+
|
|
302
|
+
# Whether to store this payment method in the PayPal Vault. This only works when the checkout
|
|
303
|
+
# flow is "vault", so make sure to call +super+ if you override it.
|
|
304
|
+
def store_in_vault
|
|
305
|
+
preferred_paypal_flow == 'vault'
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def logger
|
|
309
|
+
Braintree::Configuration.logger.clone.tap do |logger|
|
|
310
|
+
logger.level = Rails.logger.level
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def dollars(cents)
|
|
315
|
+
Money.new(cents).dollars
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def cents(dollars)
|
|
319
|
+
dollars.to_money.cents
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def to_hash(preference_string)
|
|
323
|
+
JSON.parse(preference_string.gsub("=>", ":"))
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def convert_preference_value(value, type, preference_encryptor = nil)
|
|
327
|
+
if type == :hash && value.is_a?(String)
|
|
328
|
+
value = to_hash(value)
|
|
329
|
+
end
|
|
330
|
+
if method(__method__).super_method.arity == 3
|
|
331
|
+
super
|
|
332
|
+
else
|
|
333
|
+
super(value, type)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def transaction_options(source, options, submit_for_settlement: false)
|
|
338
|
+
params = options.select do |key, _|
|
|
339
|
+
ALLOWED_BRAINTREE_OPTIONS.include?(key)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
params[:channel] = "Solidus"
|
|
343
|
+
params[:options] = { store_in_vault_on_success: store_in_vault }
|
|
344
|
+
|
|
345
|
+
if submit_for_settlement
|
|
346
|
+
params[:options][:submit_for_settlement] = true
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
if paypal_email = paypal_payee_email_for(source, options)
|
|
350
|
+
params[:options][:paypal] = { payee_email: paypal_email }
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
if source.venmo? && venmo_business_profile_id
|
|
354
|
+
params[:options][:venmo] = { profile_id: venmo_business_profile_id }
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
if merchant_account_id = merchant_account_for(source, options)
|
|
358
|
+
params[:merchant_account_id] = merchant_account_id
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
if source.token
|
|
362
|
+
params[:payment_method_token] = source.token
|
|
363
|
+
else
|
|
364
|
+
params[:payment_method_nonce] = source.nonce
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
if source.paypal?
|
|
368
|
+
params[:shipping] = braintree_shipping_address(options)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
if source.credit_card?
|
|
372
|
+
params[:billing] = braintree_billing_address(options)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
if source.customer.present?
|
|
376
|
+
params[:customer_id] = source.customer.braintree_customer_id
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
params
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def braintree_shipping_address(options)
|
|
383
|
+
braintree_address_attributes(options[:shipping_address])
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def braintree_billing_address(options)
|
|
387
|
+
braintree_address_attributes(options[:billing_address])
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def braintree_address_attributes(address)
|
|
391
|
+
first, last = address[:name].split(" ", 2)
|
|
392
|
+
{
|
|
393
|
+
first_name: first,
|
|
394
|
+
last_name: last,
|
|
395
|
+
street_address: [address[:address1], address[:address2]].compact.join(" "),
|
|
396
|
+
locality: address[:city],
|
|
397
|
+
postal_code: address[:zip],
|
|
398
|
+
region: address[:state],
|
|
399
|
+
country_code_alpha2: address[:country]
|
|
400
|
+
}
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def merchant_account_for(_source, options)
|
|
404
|
+
return unless options[:currency]
|
|
405
|
+
|
|
406
|
+
preferred_merchant_currency_map[options[:currency]]
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def paypal_payee_email_for(source, options)
|
|
410
|
+
return unless source.paypal?
|
|
411
|
+
|
|
412
|
+
preferred_paypal_payee_email_map[options[:currency]]
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def customer_profile_params(payment)
|
|
416
|
+
params = {}
|
|
417
|
+
|
|
418
|
+
params[:email] = payment&.order&.email
|
|
419
|
+
|
|
420
|
+
if store_in_vault && payment.source.try(:nonce)
|
|
421
|
+
params[:payment_method_nonce] = payment.source.nonce
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
params
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# override with the Venmo business profile that you want to use for transactions,
|
|
428
|
+
# or leave it to be nil if want Braintree to use your default account
|
|
429
|
+
def venmo_business_profile_id
|
|
430
|
+
nil
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_merchant/billing/response'
|
|
4
|
+
require_relative 'avs_result'
|
|
5
|
+
|
|
6
|
+
# Response object that all actions on the gateway should return
|
|
7
|
+
module SolidusBraintree
|
|
8
|
+
class Response < ActiveMerchant::Billing::Response
|
|
9
|
+
# def initialize(success, message, params = {}, options = {})
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
private :new
|
|
13
|
+
|
|
14
|
+
# @param result [Braintree::SuccessfulResult, Braintree::ErrorResult]
|
|
15
|
+
def build(result)
|
|
16
|
+
result.success? ? build_success(result) : build_failure(result)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def build_success(result)
|
|
22
|
+
transaction = result.transaction
|
|
23
|
+
new(true, transaction.status, {}, response_options(transaction))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def build_failure(result)
|
|
27
|
+
transaction = result.transaction
|
|
28
|
+
options = response_options(transaction).update(
|
|
29
|
+
# For error responses we want to have the CVV code
|
|
30
|
+
cvv_result: transaction&.cvv_response_code
|
|
31
|
+
)
|
|
32
|
+
new(false, error_message(result), result.params, options)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def response_options(transaction)
|
|
36
|
+
# Some error responses do not have a transaction
|
|
37
|
+
return {} if transaction.nil?
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
authorization: transaction.id,
|
|
41
|
+
avs_result: SolidusBraintree::AVSResult.build(transaction),
|
|
42
|
+
# As we do not provide the CVV while submitting the transaction (for PCI compliance reasons),
|
|
43
|
+
# we need to ignore the only response we get back (I = not provided).
|
|
44
|
+
# Otherwise Solidus thinks this payment is risky.
|
|
45
|
+
cvv_result: nil
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def error_message(result)
|
|
50
|
+
if result.errors.any?
|
|
51
|
+
result.errors.map { |e| "#{e.message} (#{e.code})" }.join(" ")
|
|
52
|
+
else
|
|
53
|
+
transaction_error_message(result.transaction)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Human readable error message for transaction responses
|
|
58
|
+
def transaction_error_message(transaction)
|
|
59
|
+
case transaction.status
|
|
60
|
+
when 'gateway_rejected'
|
|
61
|
+
I18n.t(transaction.gateway_rejection_reason,
|
|
62
|
+
scope: 'solidus_braintree.gateway_rejection_reasons',
|
|
63
|
+
default: "#{transaction.status.humanize} #{transaction.gateway_rejection_reason.humanize}")
|
|
64
|
+
when 'processor_declined'
|
|
65
|
+
I18n.t(transaction.processor_response_code,
|
|
66
|
+
scope: 'solidus_braintree.processor_response_codes',
|
|
67
|
+
default: "#{transaction.processor_response_text} (#{transaction.processor_response_code})")
|
|
68
|
+
when 'settlement_declined'
|
|
69
|
+
I18n.t(transaction.processor_settlement_response_code,
|
|
70
|
+
scope: 'solidus_braintree.processor_settlement_response_codes',
|
|
71
|
+
default: "#{transaction.processor_settlement_response_text} (#{transaction.processor_settlement_response_code})") # rubocop:disable Layout/LineLength
|
|
72
|
+
else
|
|
73
|
+
I18n.t(transaction.status,
|
|
74
|
+
scope: 'solidus_braintree.transaction_statuses',
|
|
75
|
+
default: transaction.status.humanize)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'solidus_braintree/request_protection'
|
|
4
|
+
|
|
5
|
+
module SolidusBraintree
|
|
6
|
+
class Source < ::Spree::PaymentSource
|
|
7
|
+
include RequestProtection
|
|
8
|
+
|
|
9
|
+
PAYPAL = "PayPalAccount"
|
|
10
|
+
APPLE_PAY = "ApplePayCard"
|
|
11
|
+
VENMO = "VenmoAccount"
|
|
12
|
+
CREDIT_CARD = "CreditCard"
|
|
13
|
+
|
|
14
|
+
enum paypal_funding_source: {
|
|
15
|
+
applepay: 0, bancontact: 1, blik: 2, boleto: 3, card: 4, credit: 5, eps: 6, giropay: 7, ideal: 8,
|
|
16
|
+
itau: 9, maxima: 10, mercadopago: 11, mybank: 12, oxxo: 13, p24: 14, paylater: 15, paypal: 16, payu: 17,
|
|
17
|
+
sepa: 18, sofort: 19, trustly: 20, venmo: 21, verkkopankki: 22, wechatpay: 23, zimpler: 24
|
|
18
|
+
}, _suffix: :funding
|
|
19
|
+
|
|
20
|
+
belongs_to :user, class_name: ::Spree::UserClassHandle.new, optional: true
|
|
21
|
+
belongs_to :payment_method, class_name: 'Spree::PaymentMethod'
|
|
22
|
+
has_many :payments, as: :source, class_name: "Spree::Payment", dependent: :destroy
|
|
23
|
+
|
|
24
|
+
belongs_to :customer, class_name: "SolidusBraintree::Customer", optional: true
|
|
25
|
+
|
|
26
|
+
validates :payment_type, inclusion: [PAYPAL, APPLE_PAY, VENMO, CREDIT_CARD]
|
|
27
|
+
|
|
28
|
+
before_save :clear_paypal_funding_source, unless: :paypal?
|
|
29
|
+
|
|
30
|
+
scope(:with_payment_profile, -> { joins(:customer) })
|
|
31
|
+
scope(:credit_card, -> { where(payment_type: CREDIT_CARD) })
|
|
32
|
+
|
|
33
|
+
delegate :bin, :last_4, :card_type, :expiration_month, :expiration_year, :email,
|
|
34
|
+
:username, :source_description, to: :braintree_payment_method, allow_nil: true
|
|
35
|
+
|
|
36
|
+
# Aliases to match Spree::CreditCard's interface
|
|
37
|
+
alias_method :last_digits, :last_4
|
|
38
|
+
alias_method :month, :expiration_month
|
|
39
|
+
alias_method :year, :expiration_year
|
|
40
|
+
alias_method :cc_type, :card_type
|
|
41
|
+
|
|
42
|
+
# we are not currenctly supporting an "imported" flag
|
|
43
|
+
def imported
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def actions
|
|
48
|
+
%w[capture void credit]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def can_capture?(payment)
|
|
52
|
+
payment.pending? || payment.checkout?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def can_void?(payment)
|
|
56
|
+
return false unless payment.response_code
|
|
57
|
+
|
|
58
|
+
transaction = protected_request do
|
|
59
|
+
braintree_client.transaction.find(payment.response_code)
|
|
60
|
+
end
|
|
61
|
+
Gateway::VOIDABLE_STATUSES.include?(transaction.status)
|
|
62
|
+
rescue ActiveMerchant::ConnectionError
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def can_credit?(payment)
|
|
67
|
+
payment.completed? && payment.credit_allowed > 0
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def friendly_payment_type
|
|
71
|
+
I18n.t(payment_type.underscore, scope: "solidus_braintree.payment_type")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def apple_pay?
|
|
75
|
+
payment_type == APPLE_PAY
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def paypal?
|
|
79
|
+
payment_type == PAYPAL
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def venmo?
|
|
83
|
+
payment_type == VENMO
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def reusable?
|
|
87
|
+
token.present?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def credit_card?
|
|
91
|
+
payment_type == CREDIT_CARD
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def display_number
|
|
95
|
+
if paypal?
|
|
96
|
+
email
|
|
97
|
+
elsif venmo?
|
|
98
|
+
username
|
|
99
|
+
else
|
|
100
|
+
"XXXX-XXXX-XXXX-#{last_digits.to_s.rjust(4, 'X')}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def display_paypal_funding_source
|
|
105
|
+
I18n.t(paypal_funding_source,
|
|
106
|
+
scope: 'solidus_braintree.paypal_funding_sources',
|
|
107
|
+
default: paypal_funding_source)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def display_payment_type
|
|
111
|
+
"#{I18n.t('solidus_braintree.payment_type.label')}: #{friendly_payment_type}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def braintree_payment_method
|
|
117
|
+
return unless braintree_client
|
|
118
|
+
|
|
119
|
+
@braintree_payment_method ||= protected_request do
|
|
120
|
+
braintree_client.payment_method.find(token)
|
|
121
|
+
end
|
|
122
|
+
rescue ActiveMerchant::ConnectionError, ArgumentError => e
|
|
123
|
+
Rails.logger.warn("#{e}: token unknown or missing for #{inspect}")
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def braintree_client
|
|
128
|
+
@braintree_client ||= payment_method.try(:braintree)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def clear_paypal_funding_source
|
|
132
|
+
self.paypal_funding_source = nil
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_model'
|
|
4
|
+
|
|
5
|
+
module SolidusBraintree
|
|
6
|
+
class Transaction
|
|
7
|
+
include ActiveModel::Model
|
|
8
|
+
|
|
9
|
+
attr_accessor :nonce, :payment_method, :payment_type, :paypal_funding_source, :address, :email, :phone
|
|
10
|
+
|
|
11
|
+
validates :nonce, presence: true
|
|
12
|
+
validates :payment_method, presence: true
|
|
13
|
+
validates :payment_type, presence: true
|
|
14
|
+
validates :email, presence: true
|
|
15
|
+
|
|
16
|
+
validate do
|
|
17
|
+
unless payment_method.is_a? SolidusBraintree::Gateway
|
|
18
|
+
errors.add(:payment_method, 'Must be braintree')
|
|
19
|
+
end
|
|
20
|
+
if address&.invalid?
|
|
21
|
+
address.errors.each do |error|
|
|
22
|
+
errors.add(:address, error.full_message)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def address_attributes=(attributes)
|
|
28
|
+
self.address = TransactionAddress.new attributes
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|