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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +67 -0
  3. data/LICENSE +10 -0
  4. data/README.md +84 -0
  5. data/Rakefile +27 -0
  6. data/app/models/spree/payment_sessions/paypal_merchant.rb +94 -0
  7. data/app/models/spree/paypal_merchant/base.rb +14 -0
  8. data/app/models/spree/paypal_merchant/gateway/payment_sessions.rb +308 -0
  9. data/app/models/spree/paypal_merchant/gateway.rb +324 -0
  10. data/app/models/spree/paypal_merchant/order.rb +89 -0
  11. data/app/models/spree/paypal_merchant/order_decorator.rb +24 -0
  12. data/app/models/spree/paypal_merchant/payment_method_decorator.rb +29 -0
  13. data/app/models/spree/paypal_merchant/payment_sources/apple_pay.rb +49 -0
  14. data/app/models/spree/paypal_merchant/payment_sources/card.rb +42 -0
  15. data/app/models/spree/paypal_merchant/payment_sources/paypal.rb +37 -0
  16. data/app/models/spree/paypal_merchant/store_decorator.rb +19 -0
  17. data/app/presenters/spree/paypal_merchant/order_presenter.rb +148 -0
  18. data/app/serializers/spree/api/v3/payment_method_serializer_decorator.rb +20 -0
  19. data/app/services/spree/paypal_merchant/capture_order.rb +44 -0
  20. data/app/services/spree/paypal_merchant/create_payment.rb +49 -0
  21. data/app/services/spree/paypal_merchant/create_source.rb +110 -0
  22. data/app/views/spree/admin/payment_methods/configuration_guides/_spree_paypal_merchant.html.erb +17 -0
  23. data/app/views/spree/admin/payment_methods/descriptions/_spree_paypal_merchant.html.erb +10 -0
  24. data/app/views/spree/admin/payments/source_forms/_spree_paypal_merchant.html.erb +7 -0
  25. data/app/views/spree/payment_sources/_paypal_merchant.html.erb +17 -0
  26. data/config/initializers/spree.rb +8 -0
  27. data/db/migrate/20250528095719_create_spree_paypal_merchant_orders.rb +31 -0
  28. data/db/migrate/20261003120000_rename_spree_paypal_platform_to_paypal_merchant.rb +46 -0
  29. data/lib/generators/spree/paypal_merchant/install/install_generator.rb +39 -0
  30. data/lib/spree/paypal_merchant/engine.rb +41 -0
  31. data/lib/spree/paypal_merchant/factories.rb +60 -0
  32. data/lib/spree/paypal_merchant/version.rb +8 -0
  33. data/lib/spree/paypal_merchant.rb +32 -0
  34. data/lib/spree-paypal_merchant.rb +6 -0
  35. 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
@@ -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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module PaypalMerchant
5
+ # Major version tracks Spree's major version: 5.x supports Spree 5.x.
6
+ VERSION = '5.2.0'
7
+ end
8
+ end