spree-paypal_merchant 5.2.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 +7 -0
- data/CHANGELOG.md +67 -0
- data/LICENSE +10 -0
- data/README.md +84 -0
- data/Rakefile +27 -0
- data/app/models/spree/payment_sessions/paypal_merchant.rb +94 -0
- data/app/models/spree/paypal_merchant/base.rb +14 -0
- data/app/models/spree/paypal_merchant/gateway/payment_sessions.rb +308 -0
- data/app/models/spree/paypal_merchant/gateway.rb +324 -0
- data/app/models/spree/paypal_merchant/order.rb +89 -0
- data/app/models/spree/paypal_merchant/order_decorator.rb +24 -0
- data/app/models/spree/paypal_merchant/payment_method_decorator.rb +29 -0
- data/app/models/spree/paypal_merchant/payment_sources/apple_pay.rb +49 -0
- data/app/models/spree/paypal_merchant/payment_sources/card.rb +42 -0
- data/app/models/spree/paypal_merchant/payment_sources/paypal.rb +37 -0
- data/app/models/spree/paypal_merchant/store_decorator.rb +19 -0
- data/app/presenters/spree/paypal_merchant/order_presenter.rb +148 -0
- data/app/serializers/spree/api/v3/payment_method_serializer_decorator.rb +20 -0
- data/app/services/spree/paypal_merchant/capture_order.rb +44 -0
- data/app/services/spree/paypal_merchant/create_payment.rb +49 -0
- data/app/services/spree/paypal_merchant/create_source.rb +110 -0
- data/app/views/spree/admin/payment_methods/configuration_guides/_spree_paypal_merchant.html.erb +17 -0
- data/app/views/spree/admin/payment_methods/descriptions/_spree_paypal_merchant.html.erb +10 -0
- data/app/views/spree/admin/payments/source_forms/_spree_paypal_merchant.html.erb +7 -0
- data/app/views/spree/payment_sources/_paypal_merchant.html.erb +17 -0
- data/config/initializers/spree.rb +8 -0
- data/db/migrate/20250528095719_create_spree_paypal_merchant_orders.rb +31 -0
- data/db/migrate/20261003120000_rename_spree_paypal_platform_to_paypal_merchant.rb +46 -0
- data/lib/generators/spree/paypal_merchant/install/install_generator.rb +39 -0
- data/lib/spree/paypal_merchant/engine.rb +41 -0
- data/lib/spree/paypal_merchant/factories.rb +60 -0
- data/lib/spree/paypal_merchant/version.rb +8 -0
- data/lib/spree/paypal_merchant.rb +32 -0
- data/lib/spree-paypal_merchant.rb +6 -0
- metadata +176 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'paypal_server_sdk'
|
|
4
|
+
|
|
5
|
+
module Spree
|
|
6
|
+
module PaypalMerchant
|
|
7
|
+
##
|
|
8
|
+
# Builds a PayPal Orders API create-order payload from a Spree order.
|
|
9
|
+
#
|
|
10
|
+
# The storefront owns the address: we send `purchase_units[].shipping`
|
|
11
|
+
# from `order.ship_address` and pin it with `SET_PROVIDED_ADDRESS` so
|
|
12
|
+
# PayPal does not re-collect it. The amount breakdown sends only
|
|
13
|
+
# `additional_tax_total` — sending full `tax_total` double-counts
|
|
14
|
+
# included VAT and trips PayPal `AMOUNT_MISMATCH`.
|
|
15
|
+
#
|
|
16
|
+
class OrderPresenter
|
|
17
|
+
PAYPAL_ITEM_NAME_MAX_LENGTH = 127
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# @param order [Spree::Order]
|
|
21
|
+
# @param amount [Numeric, NilClass] what PayPal should charge; defaults to
|
|
22
|
+
# the order total. Less than the total when a gift card or store credit
|
|
23
|
+
# pays part of the order.
|
|
24
|
+
#
|
|
25
|
+
def initialize(order, amount: nil)
|
|
26
|
+
@order = order
|
|
27
|
+
@amount = amount
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
attr_reader :order
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# @return [Hash] body ready for `client.orders.create_order`
|
|
34
|
+
#
|
|
35
|
+
def to_json(*_args)
|
|
36
|
+
{
|
|
37
|
+
'body' => PaypalServerSdk::OrderRequest.new(
|
|
38
|
+
intent: PaypalServerSdk::CheckoutPaymentIntent::CAPTURE,
|
|
39
|
+
purchase_units: [purchase_unit],
|
|
40
|
+
payment_source: payment_source
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def purchase_unit
|
|
48
|
+
# A gift card or store credit pays part of the order, so the breakdown
|
|
49
|
+
# and items (which always sum to the full order total) can't describe
|
|
50
|
+
# what PayPal charges. PayPal accepts an amount on its own.
|
|
51
|
+
args =
|
|
52
|
+
if partial_charge?
|
|
53
|
+
{ amount: PaypalServerSdk::AmountWithBreakdown.new(currency_code: order.currency.upcase,
|
|
54
|
+
value: charge_amount.to_s) }
|
|
55
|
+
else
|
|
56
|
+
{ amount: amount_with_breakdown, items: items }
|
|
57
|
+
end
|
|
58
|
+
args[:shipping] = shipping_details if shipping_details
|
|
59
|
+
|
|
60
|
+
PaypalServerSdk::PurchaseUnitRequest.new(**args)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def charge_amount
|
|
64
|
+
@amount.presence || order.total
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def partial_charge?
|
|
68
|
+
charge_amount.to_d != order.total.to_d
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# NOTE: the breakdown only sends additional (non-included) tax — sending
|
|
72
|
+
# the full tax_total double-counts VAT on tax-inclusive markets and
|
|
73
|
+
# trips AMOUNT_MISMATCH. Do not change to order.tax_total.
|
|
74
|
+
def amount_with_breakdown
|
|
75
|
+
PaypalServerSdk::AmountWithBreakdown.new(
|
|
76
|
+
currency_code: order.currency.upcase,
|
|
77
|
+
value: order.total.to_s,
|
|
78
|
+
breakdown: PaypalServerSdk::AmountBreakdown.new(
|
|
79
|
+
item_total: paypal_money(order.item_total),
|
|
80
|
+
shipping: paypal_money(order.ship_total),
|
|
81
|
+
tax_total: paypal_money(order.additional_tax_total),
|
|
82
|
+
discount: paypal_money((order.promo_total || 0).abs)
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def paypal_money(amount)
|
|
88
|
+
PaypalServerSdk::Money.new(
|
|
89
|
+
currency_code: order.currency.upcase,
|
|
90
|
+
value: amount.to_s
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def items
|
|
95
|
+
order.line_items.map do |line_item|
|
|
96
|
+
PaypalServerSdk::Item.new(
|
|
97
|
+
name: line_item.name.to_s[0...PAYPAL_ITEM_NAME_MAX_LENGTH],
|
|
98
|
+
unit_amount: paypal_money(line_item.price),
|
|
99
|
+
quantity: line_item.quantity.to_s,
|
|
100
|
+
sku: line_item.sku,
|
|
101
|
+
category: line_item.variant.digital? ? PaypalServerSdk::ItemCategory::DIGITAL_GOODS : PaypalServerSdk::ItemCategory::PHYSICAL_GOODS
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def shipping_details
|
|
107
|
+
address = order.ship_address
|
|
108
|
+
return nil if address.blank?
|
|
109
|
+
|
|
110
|
+
PaypalServerSdk::ShippingDetails.new(
|
|
111
|
+
name: PaypalServerSdk::ShippingName.new(full_name: address.full_name),
|
|
112
|
+
address: PaypalServerSdk::Address.new(
|
|
113
|
+
country_code: address.country&.iso,
|
|
114
|
+
address_line_1: address.address1,
|
|
115
|
+
address_line_2: address.address2.presence,
|
|
116
|
+
admin_area_2: address.city,
|
|
117
|
+
admin_area_1: region_code(address),
|
|
118
|
+
postal_code: address.zipcode
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def region_code(address)
|
|
124
|
+
address.state&.abbr.presence || address.state_name.presence
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def payment_source
|
|
128
|
+
PaypalServerSdk::PaymentSource.new(
|
|
129
|
+
paypal: PaypalServerSdk::PaypalWallet.new(
|
|
130
|
+
experience_context: PaypalServerSdk::PaypalWalletExperienceContext.new(
|
|
131
|
+
brand_name: order.store&.name,
|
|
132
|
+
shipping_preference: shipping_preference,
|
|
133
|
+
user_action: PaypalServerSdk::PaypalExperienceUserAction::PAY_NOW
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def shipping_preference
|
|
140
|
+
if order.ship_address.present?
|
|
141
|
+
PaypalServerSdk::PaypalWalletContextShippingPreference::SET_PROVIDED_ADDRESS
|
|
142
|
+
else
|
|
143
|
+
PaypalServerSdk::PaypalWalletContextShippingPreference::NO_SHIPPING
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
module Api
|
|
5
|
+
module V3
|
|
6
|
+
##
|
|
7
|
+
# Exposes non-secret payment-method preferences to the storefront.
|
|
8
|
+
#
|
|
9
|
+
module PaymentMethodSerializerDecorator
|
|
10
|
+
def self.prepended(base)
|
|
11
|
+
base.attribute :public_preferences, &:public_preferences
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
Spree::Api::V3::PaymentMethodSerializer.prepend(
|
|
19
|
+
Spree::Api::V3::PaymentMethodSerializerDecorator
|
|
20
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
module PaypalMerchant
|
|
5
|
+
##
|
|
6
|
+
# Captures a legacy {Order} via the PayPal Orders API and completes
|
|
7
|
+
# the Spree checkout.
|
|
8
|
+
#
|
|
9
|
+
class CaptureOrder
|
|
10
|
+
##
|
|
11
|
+
# @param paypal_order [Spree::PaypalMerchant::Order]
|
|
12
|
+
#
|
|
13
|
+
def initialize(paypal_order:)
|
|
14
|
+
@paypal_order = paypal_order
|
|
15
|
+
@order = paypal_order.order
|
|
16
|
+
@gateway = paypal_order.gateway
|
|
17
|
+
@amount = paypal_order.amount
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :paypal_order, :order, :gateway, :amount
|
|
21
|
+
|
|
22
|
+
##
|
|
23
|
+
# @return [Spree::PaypalMerchant::Order]
|
|
24
|
+
#
|
|
25
|
+
def call
|
|
26
|
+
return paypal_order if order.completed? || order.canceled?
|
|
27
|
+
|
|
28
|
+
gateway_response = gateway.capture(
|
|
29
|
+
(amount.to_d * 100).to_i,
|
|
30
|
+
paypal_order.paypal_id,
|
|
31
|
+
{ order_id: order.number }
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
order.with_lock do
|
|
35
|
+
paypal_order.update!(data: gateway_response.params)
|
|
36
|
+
paypal_order.create_payment!
|
|
37
|
+
Spree::Dependencies.checkout_complete_service.constantize.call(order: order)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
paypal_order
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
module PaypalMerchant
|
|
5
|
+
##
|
|
6
|
+
# Creates the Spree::Payment for a captured legacy PayPal order.
|
|
7
|
+
#
|
|
8
|
+
class CreatePayment
|
|
9
|
+
##
|
|
10
|
+
# @param paypal_order [Spree::PaypalMerchant::Order]
|
|
11
|
+
# @param order [Spree::Order, NilClass]
|
|
12
|
+
# @param gateway [Spree::PaypalMerchant::Gateway, NilClass]
|
|
13
|
+
# @param amount [Numeric, NilClass]
|
|
14
|
+
#
|
|
15
|
+
def initialize(paypal_order:, order: nil, gateway: nil, amount: nil)
|
|
16
|
+
@paypal_order = paypal_order
|
|
17
|
+
@order = order || paypal_order.order
|
|
18
|
+
@gateway = gateway || paypal_order.gateway
|
|
19
|
+
@amount = amount || paypal_order.amount
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
##
|
|
23
|
+
# @return [Spree::Payment]
|
|
24
|
+
#
|
|
25
|
+
def call
|
|
26
|
+
source = CreateSource.new(
|
|
27
|
+
paypal_payment_source: paypal_order.payment_source,
|
|
28
|
+
gateway: gateway,
|
|
29
|
+
order: order
|
|
30
|
+
).call
|
|
31
|
+
|
|
32
|
+
payment = order.payments.find_or_initialize_by(
|
|
33
|
+
payment_method_id: gateway.id,
|
|
34
|
+
response_code: paypal_order.paypal_payment_id,
|
|
35
|
+
amount: amount
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
payment.source = source if source.present?
|
|
39
|
+
payment.state = 'completed'
|
|
40
|
+
payment.save!
|
|
41
|
+
payment
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
attr_reader :order, :gateway, :paypal_order, :amount
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
module PaypalMerchant
|
|
5
|
+
##
|
|
6
|
+
# Builds a {PaymentSources} record from a PayPal Orders `payment_source`.
|
|
7
|
+
#
|
|
8
|
+
# PayPal returns one of `paypal`, `apple_pay`, or `card` (Card Fields).
|
|
9
|
+
# Unknown keys raise so a new wallet is not silently dropped.
|
|
10
|
+
#
|
|
11
|
+
class CreateSource
|
|
12
|
+
##
|
|
13
|
+
# @param paypal_payment_source [Hash]
|
|
14
|
+
# @param gateway [Spree::PaypalMerchant::Gateway]
|
|
15
|
+
# @param order [Spree::Order, NilClass]
|
|
16
|
+
# @param user [Spree::User, NilClass]
|
|
17
|
+
#
|
|
18
|
+
def initialize(paypal_payment_source:, gateway:, order: nil, user: nil)
|
|
19
|
+
@paypal_payment_source = paypal_payment_source.with_indifferent_access
|
|
20
|
+
@gateway = gateway
|
|
21
|
+
@user = user || order&.user
|
|
22
|
+
@order = order
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# @return [Spree::PaymentSource]
|
|
27
|
+
# @raise [ArgumentError] when the payload has no known wallet key
|
|
28
|
+
#
|
|
29
|
+
def call
|
|
30
|
+
if paypal_payment_source.key?(:paypal)
|
|
31
|
+
create_paypal_source
|
|
32
|
+
elsif paypal_payment_source[:apple_pay].present?
|
|
33
|
+
create_apple_pay_source
|
|
34
|
+
elsif paypal_payment_source[:card].present?
|
|
35
|
+
create_card_source
|
|
36
|
+
else
|
|
37
|
+
raise ArgumentError, "Unsupported PayPal payment source: #{paypal_payment_source.keys.join(', ')}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
attr_reader :gateway, :user, :paypal_payment_source, :order
|
|
44
|
+
|
|
45
|
+
def create_paypal_source
|
|
46
|
+
wallet = paypal_payment_source[:paypal] || {}
|
|
47
|
+
profile_id = wallet[:account_id].presence || "paypal-#{order&.number || 'guest'}"
|
|
48
|
+
source = PaymentSources::Paypal.find_or_initialize_by(
|
|
49
|
+
payment_method: gateway,
|
|
50
|
+
gateway_payment_profile_id: profile_id
|
|
51
|
+
)
|
|
52
|
+
source.update!(
|
|
53
|
+
user: user,
|
|
54
|
+
email: wallet[:email_address],
|
|
55
|
+
name: full_name(wallet[:name]),
|
|
56
|
+
account_id: wallet[:account_id],
|
|
57
|
+
account_status: wallet[:account_status]
|
|
58
|
+
)
|
|
59
|
+
source
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def create_apple_pay_source
|
|
63
|
+
wallet = paypal_payment_source[:apple_pay]
|
|
64
|
+
card = wallet[:card] || {}
|
|
65
|
+
profile_id = wallet[:id].presence ||
|
|
66
|
+
wallet[:token].presence ||
|
|
67
|
+
['applepay', card[:brand], card[:last_digits]].compact.join('-')
|
|
68
|
+
|
|
69
|
+
source = PaymentSources::ApplePay.find_or_initialize_by(
|
|
70
|
+
payment_method: gateway,
|
|
71
|
+
gateway_payment_profile_id: profile_id
|
|
72
|
+
)
|
|
73
|
+
source.update!(
|
|
74
|
+
user: user,
|
|
75
|
+
name: full_name(wallet[:name]).presence || wallet[:name],
|
|
76
|
+
email: wallet[:email_address],
|
|
77
|
+
card_brand: card[:brand],
|
|
78
|
+
last_digits: card[:last_digits],
|
|
79
|
+
card_type: card[:type]
|
|
80
|
+
)
|
|
81
|
+
source
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def create_card_source
|
|
85
|
+
card = paypal_payment_source[:card]
|
|
86
|
+
profile_id = card[:id].presence || ['card', card[:brand], card[:last_digits]].compact.join('-')
|
|
87
|
+
|
|
88
|
+
source = PaymentSources::Card.find_or_initialize_by(
|
|
89
|
+
payment_method: gateway,
|
|
90
|
+
gateway_payment_profile_id: profile_id
|
|
91
|
+
)
|
|
92
|
+
source.update!(
|
|
93
|
+
user: user,
|
|
94
|
+
name: card[:name],
|
|
95
|
+
card_brand: card[:brand],
|
|
96
|
+
last_digits: card[:last_digits],
|
|
97
|
+
card_type: card[:type]
|
|
98
|
+
)
|
|
99
|
+
source
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def full_name(name)
|
|
103
|
+
return name if name.is_a?(String)
|
|
104
|
+
return '' if name.blank?
|
|
105
|
+
|
|
106
|
+
"#{name[:given_name]} #{name[:surname]}".strip
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
data/app/views/spree/admin/payment_methods/configuration_guides/_spree_paypal_merchant.html.erb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<div class="alert alert-info">
|
|
2
|
+
<p class="mb-0">
|
|
3
|
+
To find your <strong>Client ID</strong> and <strong>Client Secret</strong>, go to the
|
|
4
|
+
<%= external_link_to 'PayPal Developer Dashboard', 'https://developer.paypal.com/dashboard/', class: 'alert-link' %>.
|
|
5
|
+
For more information, see the <%= external_link_to 'PayPal REST API documentation', 'https://developer.paypal.com/api/rest/', class: 'alert-link' %>.
|
|
6
|
+
</p>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="alert alert-info mt-3">
|
|
10
|
+
<p class="mb-0">
|
|
11
|
+
<strong>Apple Pay</strong> is a funding source of this same PayPal method, not a second payment method.
|
|
12
|
+
Register every storefront domain that will show the Apple Pay button in the
|
|
13
|
+
<%= external_link_to 'PayPal Apple Pay domain registration', 'https://developer.paypal.com/docs/checkout/apm/apple-pay/', class: 'alert-link' %>
|
|
14
|
+
dashboard. Use the checkboxes below to offer Card Fields and Apple Pay
|
|
15
|
+
on the storefront. The Apple Pay button only renders in Safari over HTTPS.
|
|
16
|
+
</p>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<p class="mb-1">
|
|
2
|
+
PayPal Checkout: wallet, Pay Later, Apple Pay, and Card Fields on one payment method.
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<div class="d-flex align-items-center">
|
|
6
|
+
<%= payment_method_icon_tag 'paypal', class: 'm-1' %>
|
|
7
|
+
<%= payment_method_icon_tag 'visa', class: 'm-1' %>
|
|
8
|
+
<%= payment_method_icon_tag 'master', class: 'm-1' %>
|
|
9
|
+
<%= payment_method_icon_tag 'apple_pay', class: 'm-1' %>
|
|
10
|
+
</div>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<% if previous_cards.any? %>
|
|
2
|
+
<%= render 'spree/admin/payments/source_forms/previous_cards', previous_cards: previous_cards, f: f %>
|
|
3
|
+
<% else %>
|
|
4
|
+
<span class="text-gray-600">
|
|
5
|
+
<%= Spree.t(:no_payment_sources_available, default: 'No saved PayPal accounts available.') %>
|
|
6
|
+
</span>
|
|
7
|
+
<% end %>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<p class="mb-0">
|
|
2
|
+
<%= source.try(:display_payment_info) || source.class.try(:display_name) %>
|
|
3
|
+
</p>
|
|
4
|
+
<% if source.respond_to?(:email) && source.email.present? %>
|
|
5
|
+
<p class="mb-0">
|
|
6
|
+
<%= source.email %>
|
|
7
|
+
</p>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% if source.respond_to?(:account_status) && source.account_status.present? %>
|
|
10
|
+
<p class="mb-0">
|
|
11
|
+
<% if source.account_status == 'VERIFIED' %>
|
|
12
|
+
<code class="text-success"><%= source.account_status %></code>
|
|
13
|
+
<% else %>
|
|
14
|
+
<code class="text-danger"><%= source.account_status %></code>
|
|
15
|
+
<% end %>
|
|
16
|
+
</p>
|
|
17
|
+
<% end %>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Must be after_initialize: spree_core assigns (does not append to)
|
|
4
|
+
# config.spree.payment_methods in its own after_initialize, and a
|
|
5
|
+
# file-scope registration here would be silently clobbered.
|
|
6
|
+
Rails.application.config.after_initialize do
|
|
7
|
+
Rails.application.config.spree.payment_methods << Spree::PaypalMerchant::Gateway
|
|
8
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateSpreePaypalMerchantOrders < ActiveRecord::Migration[7.2]
|
|
4
|
+
def up
|
|
5
|
+
# A host upgrading from spree-paypal_platform already has the table under
|
|
6
|
+
# its old name; RenameSpreePaypalPlatformToPaypalMerchant moves it.
|
|
7
|
+
return if table_exists?(:spree_paypal_platform_orders) || table_exists?(:spree_paypal_merchant_orders)
|
|
8
|
+
|
|
9
|
+
create_table :spree_paypal_merchant_orders do |t|
|
|
10
|
+
t.references :order, null: false
|
|
11
|
+
t.references :payment_method, null: false
|
|
12
|
+
t.string :paypal_id, null: false
|
|
13
|
+
t.decimal :amount, null: false, precision: 10, scale: 2
|
|
14
|
+
|
|
15
|
+
if t.respond_to? :jsonb
|
|
16
|
+
t.jsonb :data
|
|
17
|
+
else
|
|
18
|
+
t.json :data
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
t.timestamps
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
add_index :spree_paypal_merchant_orders, %i[order_id paypal_id], unique: true
|
|
25
|
+
add_index :spree_paypal_merchant_orders, :paypal_id, unique: true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def down
|
|
29
|
+
drop_table :spree_paypal_merchant_orders, if_exists: true
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Upgrades a host from spree-paypal_platform. Renames the orders table (and
|
|
4
|
+
# its default-named indexes) and rewrites stored STI class names, otherwise
|
|
5
|
+
# PaymentMethod.all raises SubclassNotFound. A no-op on a fresh install.
|
|
6
|
+
class RenameSpreePaypalPlatformToPaypalMerchant < ActiveRecord::Migration[7.2]
|
|
7
|
+
TYPES = {
|
|
8
|
+
spree_payment_methods: {
|
|
9
|
+
'Spree::PaypalPlatform::Gateway' => 'Spree::PaypalMerchant::Gateway'
|
|
10
|
+
},
|
|
11
|
+
spree_payment_sources: {
|
|
12
|
+
'Spree::PaypalPlatform::PaymentSources::Paypal' => 'Spree::PaypalMerchant::PaymentSources::Paypal',
|
|
13
|
+
'Spree::PaypalPlatform::PaymentSources::ApplePay' => 'Spree::PaypalMerchant::PaymentSources::ApplePay',
|
|
14
|
+
'Spree::PaypalPlatform::PaymentSources::Card' => 'Spree::PaypalMerchant::PaymentSources::Card'
|
|
15
|
+
},
|
|
16
|
+
spree_payment_sessions: {
|
|
17
|
+
'Spree::PaymentSessions::PaypalPlatform' => 'Spree::PaymentSessions::PaypalMerchant'
|
|
18
|
+
}
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def up
|
|
22
|
+
if table_exists?(:spree_paypal_platform_orders) && !table_exists?(:spree_paypal_merchant_orders)
|
|
23
|
+
rename_table :spree_paypal_platform_orders, :spree_paypal_merchant_orders
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
TYPES.each { |table, mapping| rewrite(table, mapping) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def down
|
|
30
|
+
if table_exists?(:spree_paypal_merchant_orders) && !table_exists?(:spree_paypal_platform_orders)
|
|
31
|
+
rename_table :spree_paypal_merchant_orders, :spree_paypal_platform_orders
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
TYPES.each { |table, mapping| rewrite(table, mapping.invert) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def rewrite(table, mapping)
|
|
40
|
+
return unless table_exists?(table) && column_exists?(table, :type)
|
|
41
|
+
|
|
42
|
+
mapping.each do |from, to|
|
|
43
|
+
execute "UPDATE #{table} SET type = #{quote(to)} WHERE type = #{quote(from)}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
module PaypalMerchant
|
|
5
|
+
module Generators
|
|
6
|
+
##
|
|
7
|
+
# Copies this gem's migrations into the host app and optionally runs them.
|
|
8
|
+
#
|
|
9
|
+
class InstallGenerator < Rails::Generators::Base
|
|
10
|
+
desc 'Copies Spree::PaypalMerchant migrations into the host application and runs them.'
|
|
11
|
+
|
|
12
|
+
class_option :auto_run_migrations,
|
|
13
|
+
type: :boolean,
|
|
14
|
+
default: false,
|
|
15
|
+
desc: 'Run the copied migrations immediately instead of asking'
|
|
16
|
+
|
|
17
|
+
def copy_migrations
|
|
18
|
+
rake 'spree_paypal_merchant:install:migrations'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run_migrations
|
|
22
|
+
if run_migrations?
|
|
23
|
+
rake 'db:migrate'
|
|
24
|
+
else
|
|
25
|
+
say_status :skip, 'db:migrate — remember to run it before using the gem', :yellow
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def run_migrations?
|
|
32
|
+
return true if options[:auto_run_migrations]
|
|
33
|
+
|
|
34
|
+
ask('Run the migrations now? [Yn]').to_s.strip.casecmp('n') != 0
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
module PaypalMerchant
|
|
5
|
+
##
|
|
6
|
+
# Rails engine for the PayPal Checkout payment method.
|
|
7
|
+
#
|
|
8
|
+
class Engine < ::Rails::Engine
|
|
9
|
+
require 'spree/core'
|
|
10
|
+
isolate_namespace Spree
|
|
11
|
+
|
|
12
|
+
# Decorators reopen existing constants, so Zeitwerk must not autoload them.
|
|
13
|
+
initializer 'spree_paypal_merchant.ignore_decorators_from_zeitwerk', before: :setup_main_autoloader do
|
|
14
|
+
Rails.autoloaders.main.ignore(
|
|
15
|
+
File.join(File.dirname(__FILE__), '../../../app/**/*_decorator*.rb')
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# engine_name generates route-helper prefixes and must be a valid Ruby
|
|
20
|
+
# identifier, so it cannot contain a dash.
|
|
21
|
+
engine_name 'spree_paypal_merchant'
|
|
22
|
+
|
|
23
|
+
config.generators do |g|
|
|
24
|
+
g.test_framework :rspec
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
# Loads the gem's decorators. Called on every reload in development.
|
|
29
|
+
#
|
|
30
|
+
# @return [void]
|
|
31
|
+
#
|
|
32
|
+
def self.activate
|
|
33
|
+
Dir.glob(File.join(File.dirname(__FILE__), '../../../app/**/*_decorator*.rb')).each do |c|
|
|
34
|
+
Rails.configuration.cache_classes ? require(c) : load(c)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
config.to_prepare(&method(:activate).to_proc)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
FactoryBot.define do
|
|
4
|
+
factory :paypal_merchant_gateway, class: 'Spree::PaypalMerchant::Gateway' do
|
|
5
|
+
name { 'PayPal Checkout' }
|
|
6
|
+
association :store, factory: :store
|
|
7
|
+
preferences do
|
|
8
|
+
{
|
|
9
|
+
client_id: ENV.fetch('PAYPAL_CLIENT_ID', 'client_id_test'),
|
|
10
|
+
client_secret: ENV.fetch('PAYPAL_CLIENT_SECRET', 'client_secret_test'),
|
|
11
|
+
test_mode: true,
|
|
12
|
+
enable_apple_pay: true,
|
|
13
|
+
enable_card_fields: true
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
factory :paypal_merchant_payment_source, class: 'Spree::PaypalMerchant::PaymentSources::Paypal' do
|
|
19
|
+
association :payment_method, factory: :paypal_merchant_gateway
|
|
20
|
+
gateway_payment_profile_id { 'PAY-CUSTOMER-ID' }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
factory :paypal_merchant_apple_pay_source, class: 'Spree::PaypalMerchant::PaymentSources::ApplePay' do
|
|
24
|
+
association :payment_method, factory: :paypal_merchant_gateway
|
|
25
|
+
gateway_payment_profile_id { 'APPLEPAY-TOKEN-1' }
|
|
26
|
+
card_brand { 'VISA' }
|
|
27
|
+
last_digits { '4242' }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
factory :paypal_merchant_payment_session, class: 'Spree::PaymentSessions::PaypalMerchant' do
|
|
31
|
+
association :order, factory: :order
|
|
32
|
+
association :payment_method, factory: :paypal_merchant_gateway
|
|
33
|
+
amount { order.total }
|
|
34
|
+
currency { order.currency }
|
|
35
|
+
status { 'pending' }
|
|
36
|
+
external_id { "PAYPAL-ORDER-#{SecureRandom.hex(8).upcase}" }
|
|
37
|
+
external_data do
|
|
38
|
+
JSON.parse(File.read(Spree::PaypalMerchant::Engine.root.join('spec/fixtures/paypal_order.json')))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
factory :completed_paypal_merchant_payment_session do
|
|
42
|
+
status { 'completed' }
|
|
43
|
+
external_data do
|
|
44
|
+
JSON.parse(File.read(Spree::PaypalMerchant::Engine.root.join('spec/fixtures/captured_paypal_order.json')))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
factory :paypal_merchant_order, class: 'Spree::PaypalMerchant::Order' do
|
|
50
|
+
paypal_id { 'PAY-ORDER-ID' }
|
|
51
|
+
order { create(:order) }
|
|
52
|
+
payment_method { create(:paypal_merchant_gateway) }
|
|
53
|
+
amount { order.total }
|
|
54
|
+
data { JSON.parse(File.read(Spree::PaypalMerchant::Engine.root.join('spec/fixtures/paypal_order.json'))) }
|
|
55
|
+
|
|
56
|
+
factory :captured_paypal_merchant_order do
|
|
57
|
+
data { JSON.parse(File.read(Spree::PaypalMerchant::Engine.root.join('spec/fixtures/captured_paypal_order.json'))) }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|