spree-paypal_platform 5.0.1

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 (36) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +34 -0
  3. data/LICENSE +10 -0
  4. data/README.md +117 -0
  5. data/Rakefile +27 -0
  6. data/app/models/spree/payment_sessions/paypal_checkout.rb +94 -0
  7. data/app/models/spree/paypal_checkout/base.rb +14 -0
  8. data/app/models/spree/paypal_checkout/gateway/payment_sessions.rb +278 -0
  9. data/app/models/spree/paypal_checkout/gateway.rb +309 -0
  10. data/app/models/spree/paypal_checkout/order.rb +89 -0
  11. data/app/models/spree/paypal_checkout/order_decorator.rb +24 -0
  12. data/app/models/spree/paypal_checkout/payment_method_decorator.rb +29 -0
  13. data/app/models/spree/paypal_checkout/payment_sources/apple_pay.rb +49 -0
  14. data/app/models/spree/paypal_checkout/payment_sources/card.rb +42 -0
  15. data/app/models/spree/paypal_checkout/payment_sources/paypal.rb +37 -0
  16. data/app/models/spree/paypal_checkout/store_decorator.rb +19 -0
  17. data/app/presenters/spree/paypal_checkout/order_presenter.rb +130 -0
  18. data/app/services/spree/paypal_checkout/capture_order.rb +44 -0
  19. data/app/services/spree/paypal_checkout/create_payment.rb +49 -0
  20. data/app/services/spree/paypal_checkout/create_source.rb +109 -0
  21. data/app/views/spree/admin/payment_methods/configuration_guides/_spree_paypal_checkout.html.erb +16 -0
  22. data/app/views/spree/admin/payment_methods/descriptions/_spree_paypal_checkout.html.erb +10 -0
  23. data/app/views/spree/admin/payments/source_forms/_spree_paypal_checkout.html.erb +7 -0
  24. data/app/views/spree/payment_sources/_paypal_checkout.html.erb +17 -0
  25. data/config/initializers/spree.rb +8 -0
  26. data/db/migrate/20250528095719_create_spree_paypal_checkout_orders.rb +22 -0
  27. data/db/migrate/20260817120000_rewrite_spree_paypal_checkout_sti_types.rb +33 -0
  28. data/db/migrate/20260817130000_add_unique_index_on_paypal_checkout_order_paypal_id.rb +7 -0
  29. data/lib/generators/spree/paypal_checkout/install/install_generator.rb +39 -0
  30. data/lib/spree/paypal_checkout/engine.rb +41 -0
  31. data/lib/spree/paypal_checkout/factories.rb +59 -0
  32. data/lib/spree/paypal_checkout/version.rb +8 -0
  33. data/lib/spree/paypal_checkout.rb +45 -0
  34. data/lib/spree-paypal_checkout.rb +7 -0
  35. data/lib/spree-paypal_platform.rb +7 -0
  36. metadata +178 -0
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'paypal_server_sdk'
4
+
5
+ module Spree
6
+ module PaypalCheckout
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
+ #
22
+ def initialize(order)
23
+ @order = order
24
+ end
25
+
26
+ attr_reader :order
27
+
28
+ ##
29
+ # @return [Hash] body ready for `client.orders.create_order`
30
+ #
31
+ def to_json(*_args)
32
+ {
33
+ 'body' => PaypalServerSdk::OrderRequest.new(
34
+ intent: PaypalServerSdk::CheckoutPaymentIntent::CAPTURE,
35
+ purchase_units: [purchase_unit],
36
+ payment_source: payment_source
37
+ )
38
+ }
39
+ end
40
+
41
+ private
42
+
43
+ def purchase_unit
44
+ args = {
45
+ amount: amount_with_breakdown,
46
+ items: items
47
+ }
48
+ args[:shipping] = shipping_details if shipping_details
49
+
50
+ PaypalServerSdk::PurchaseUnitRequest.new(**args)
51
+ end
52
+
53
+ # NOTE: the breakdown only sends additional (non-included) tax — sending
54
+ # the full tax_total double-counts VAT on tax-inclusive markets and
55
+ # trips AMOUNT_MISMATCH. Do not change to order.tax_total.
56
+ def amount_with_breakdown
57
+ PaypalServerSdk::AmountWithBreakdown.new(
58
+ currency_code: order.currency.upcase,
59
+ value: order.total.to_s,
60
+ breakdown: PaypalServerSdk::AmountBreakdown.new(
61
+ item_total: paypal_money(order.item_total),
62
+ shipping: paypal_money(order.ship_total),
63
+ tax_total: paypal_money(order.additional_tax_total),
64
+ discount: paypal_money((order.promo_total || 0).abs)
65
+ )
66
+ )
67
+ end
68
+
69
+ def paypal_money(amount)
70
+ PaypalServerSdk::Money.new(
71
+ currency_code: order.currency.upcase,
72
+ value: amount.to_s
73
+ )
74
+ end
75
+
76
+ def items
77
+ order.line_items.map do |line_item|
78
+ PaypalServerSdk::Item.new(
79
+ name: line_item.name.to_s[0...PAYPAL_ITEM_NAME_MAX_LENGTH],
80
+ unit_amount: paypal_money(line_item.price),
81
+ quantity: line_item.quantity.to_s,
82
+ sku: line_item.sku,
83
+ category: line_item.variant.digital? ? PaypalServerSdk::ItemCategory::DIGITAL_GOODS : PaypalServerSdk::ItemCategory::PHYSICAL_GOODS
84
+ )
85
+ end
86
+ end
87
+
88
+ def shipping_details
89
+ address = order.ship_address
90
+ return nil if address.blank?
91
+
92
+ PaypalServerSdk::ShippingDetails.new(
93
+ name: PaypalServerSdk::ShippingName.new(full_name: address.full_name),
94
+ address: PaypalServerSdk::Address.new(
95
+ country_code: address.country&.iso,
96
+ address_line_1: address.address1,
97
+ address_line_2: address.address2.presence,
98
+ admin_area_2: address.city,
99
+ admin_area_1: region_code(address),
100
+ postal_code: address.zipcode
101
+ )
102
+ )
103
+ end
104
+
105
+ def region_code(address)
106
+ address.state&.abbr.presence || address.state_name.presence
107
+ end
108
+
109
+ def payment_source
110
+ PaypalServerSdk::PaymentSource.new(
111
+ paypal: PaypalServerSdk::PaypalWallet.new(
112
+ experience_context: PaypalServerSdk::PaypalWalletExperienceContext.new(
113
+ brand_name: order.store&.name,
114
+ shipping_preference: shipping_preference,
115
+ user_action: PaypalServerSdk::PaypalExperienceUserAction::PAY_NOW
116
+ )
117
+ )
118
+ )
119
+ end
120
+
121
+ def shipping_preference
122
+ if order.ship_address.present?
123
+ PaypalServerSdk::PaypalWalletContextShippingPreference::SET_PROVIDED_ADDRESS
124
+ else
125
+ PaypalServerSdk::PaypalWalletContextShippingPreference::NO_SHIPPING
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module PaypalCheckout
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::PaypalCheckout::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::PaypalCheckout::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 PaypalCheckout
5
+ ##
6
+ # Creates the Spree::Payment for a captured legacy PayPal order.
7
+ #
8
+ class CreatePayment
9
+ ##
10
+ # @param paypal_order [Spree::PaypalCheckout::Order]
11
+ # @param order [Spree::Order, NilClass]
12
+ # @param gateway [Spree::PaypalCheckout::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,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module PaypalCheckout
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::PaypalCheckout::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[:paypal].present?
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
+ source = PaymentSources::Paypal.find_or_initialize_by(
48
+ payment_method: gateway,
49
+ gateway_payment_profile_id: wallet[:account_id]
50
+ )
51
+ source.update!(
52
+ user: user,
53
+ email: wallet[:email_address],
54
+ name: full_name(wallet[:name]),
55
+ account_id: wallet[:account_id],
56
+ account_status: wallet[:account_status]
57
+ )
58
+ source
59
+ end
60
+
61
+ def create_apple_pay_source
62
+ wallet = paypal_payment_source[:apple_pay]
63
+ card = wallet[:card] || {}
64
+ profile_id = wallet[:id].presence ||
65
+ wallet[:token].presence ||
66
+ ['applepay', card[:brand], card[:last_digits]].compact.join('-')
67
+
68
+ source = PaymentSources::ApplePay.find_or_initialize_by(
69
+ payment_method: gateway,
70
+ gateway_payment_profile_id: profile_id
71
+ )
72
+ source.update!(
73
+ user: user,
74
+ name: full_name(wallet[:name]).presence || wallet[:name],
75
+ email: wallet[:email_address],
76
+ card_brand: card[:brand],
77
+ last_digits: card[:last_digits],
78
+ card_type: card[:type]
79
+ )
80
+ source
81
+ end
82
+
83
+ def create_card_source
84
+ card = paypal_payment_source[:card]
85
+ profile_id = card[:id].presence || ['card', card[:brand], card[:last_digits]].compact.join('-')
86
+
87
+ source = PaymentSources::Card.find_or_initialize_by(
88
+ payment_method: gateway,
89
+ gateway_payment_profile_id: profile_id
90
+ )
91
+ source.update!(
92
+ user: user,
93
+ name: card[:name],
94
+ card_brand: card[:brand],
95
+ last_digits: card[:last_digits],
96
+ card_type: card[:type]
97
+ )
98
+ source
99
+ end
100
+
101
+ def full_name(name)
102
+ return name if name.is_a?(String)
103
+ return '' if name.blank?
104
+
105
+ "#{name[:given_name]} #{name[:surname]}".strip
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,16 @@
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, then set <em>Apple pay domain</em> below. The button only renders in Safari over HTTPS.
15
+ </p>
16
+ </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::PaypalCheckout::Gateway
8
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSpreePaypalCheckoutOrders < ActiveRecord::Migration[7.2]
4
+ def change
5
+ create_table :spree_paypal_checkout_orders do |t|
6
+ t.references :order, null: false
7
+ t.references :payment_method, null: false
8
+ t.string :paypal_id, null: false
9
+ t.decimal :amount, null: false, precision: 10, scale: 2
10
+
11
+ if t.respond_to? :jsonb
12
+ t.jsonb :data
13
+ else
14
+ t.json :data
15
+ end
16
+
17
+ t.timestamps
18
+ end
19
+
20
+ add_index :spree_paypal_checkout_orders, [:order_id, :paypal_id], unique: true
21
+ end
22
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RewriteSpreePaypalCheckoutStiTypes < ActiveRecord::Migration[7.2]
4
+ # Hosts coming from the official / forked `spree_paypal_checkout` gem store
5
+ # `SpreePaypalCheckout::Gateway` (and `…::PaymentSources::Paypal`) in STI
6
+ # type columns. The new gem aliases those constants, so checkout keeps
7
+ # working before this migration runs; rewriting the strings makes the
8
+ # alias optional for the next deploy.
9
+ LEGACY_TO_CURRENT = {
10
+ 'SpreePaypalCheckout::Gateway' => 'Spree::PaypalCheckout::Gateway',
11
+ 'SpreePaypalCheckout::PaymentSources::Paypal' => 'Spree::PaypalCheckout::PaymentSources::Paypal',
12
+ 'SpreePaypalCheckout::Order' => 'Spree::PaypalCheckout::Order'
13
+ }.freeze
14
+
15
+ def up
16
+ rewrite_column(:spree_payment_methods, :type)
17
+ rewrite_column(:spree_payment_sources, :type) if column_exists?(:spree_payment_sources, :type)
18
+ end
19
+
20
+ def down
21
+ rewrite_column(:spree_payment_methods, :type, LEGACY_TO_CURRENT.invert)
22
+ rewrite_column(:spree_payment_sources, :type, LEGACY_TO_CURRENT.invert) if column_exists?(:spree_payment_sources, :type)
23
+ end
24
+
25
+ private
26
+
27
+ def rewrite_column(table, column, mapping = LEGACY_TO_CURRENT)
28
+ mapping.each do |from, to|
29
+ reversible_update = "UPDATE #{table} SET #{column} = #{quote(to)} WHERE #{column} = #{quote(from)}"
30
+ execute reversible_update
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddUniqueIndexOnPaypalCheckoutOrderPaypalId < ActiveRecord::Migration[7.2]
4
+ def change
5
+ add_index :spree_paypal_checkout_orders, :paypal_id, unique: true
6
+ end
7
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module PaypalCheckout
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::PaypalCheckout 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_checkout: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 PaypalCheckout
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_checkout.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_checkout'
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,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :paypal_checkout_gateway, class: 'Spree::PaypalCheckout::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
+ }
14
+ end
15
+ end
16
+
17
+ factory :paypal_checkout_payment_source, class: 'Spree::PaypalCheckout::PaymentSources::Paypal' do
18
+ association :payment_method, factory: :paypal_checkout_gateway
19
+ gateway_payment_profile_id { 'PAY-CUSTOMER-ID' }
20
+ end
21
+
22
+ factory :paypal_checkout_apple_pay_source, class: 'Spree::PaypalCheckout::PaymentSources::ApplePay' do
23
+ association :payment_method, factory: :paypal_checkout_gateway
24
+ gateway_payment_profile_id { 'APPLEPAY-TOKEN-1' }
25
+ card_brand { 'VISA' }
26
+ last_digits { '4242' }
27
+ end
28
+
29
+ factory :paypal_checkout_payment_session, class: 'Spree::PaymentSessions::PaypalCheckout' do
30
+ association :order, factory: :order
31
+ association :payment_method, factory: :paypal_checkout_gateway
32
+ amount { order.total }
33
+ currency { order.currency }
34
+ status { 'pending' }
35
+ external_id { "PAYPAL-ORDER-#{SecureRandom.hex(8).upcase}" }
36
+ external_data do
37
+ JSON.parse(File.read(Spree::PaypalCheckout::Engine.root.join('spec/fixtures/paypal_order.json')))
38
+ end
39
+
40
+ factory :completed_paypal_checkout_payment_session do
41
+ status { 'completed' }
42
+ external_data do
43
+ JSON.parse(File.read(Spree::PaypalCheckout::Engine.root.join('spec/fixtures/captured_paypal_order.json')))
44
+ end
45
+ end
46
+ end
47
+
48
+ factory :paypal_checkout_order, class: 'Spree::PaypalCheckout::Order' do
49
+ paypal_id { 'PAY-ORDER-ID' }
50
+ order { create(:order) }
51
+ payment_method { create(:paypal_checkout_gateway) }
52
+ amount { order.total }
53
+ data { JSON.parse(File.read(Spree::PaypalCheckout::Engine.root.join('spec/fixtures/paypal_order.json'))) }
54
+
55
+ factory :captured_paypal_checkout_order do
56
+ data { JSON.parse(File.read(Spree::PaypalCheckout::Engine.root.join('spec/fixtures/captured_paypal_order.json'))) }
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module PaypalCheckout
5
+ # Major version tracks Spree's major version: 5.x supports Spree 5.x.
6
+ VERSION = '5.0.1'
7
+ end
8
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spree_core'
4
+ require 'paypal_server_sdk'
5
+ require 'spree/paypal_checkout/version'
6
+ require 'spree/paypal_checkout/engine'
7
+
8
+ module Spree
9
+ module PaypalCheckout
10
+ ##
11
+ # Table prefix for models nested under this module.
12
+ #
13
+ # Must live on the module, not on {Base}. ActiveRecord walks
14
+ # `module_parents` for `table_name_prefix` and finds `Spree` first
15
+ # (`"spree_"`) unless this closer parent defines it.
16
+ #
17
+ # @return [String]
18
+ #
19
+ def self.table_name_prefix
20
+ 'spree_paypal_checkout_'
21
+ end
22
+
23
+ ##
24
+ # STI type names that count as this gem's gateway.
25
+ #
26
+ # Includes the historical `SpreePaypalCheckout::Gateway` string from the
27
+ # official / forked gem so existing payment-method rows keep matching
28
+ # after a host switches gems, even before the type-rewrite migration
29
+ # runs.
30
+ #
31
+ # @return [Array<String>]
32
+ #
33
+ def self.gateway_type_names
34
+ (
35
+ [Gateway.name, 'SpreePaypalCheckout::Gateway'] + Gateway.descendants.map(&:name)
36
+ ).uniq
37
+ end
38
+ end
39
+ end
40
+
41
+ # Historical top-level namespace from `spree_paypal_checkout`. Existing
42
+ # `spree_payment_methods.type` / `spree_payment_sources.type` rows store
43
+ # `SpreePaypalCheckout::…`; this alias lets those strings constantize
44
+ # onto the new classes.
45
+ SpreePaypalCheckout = Spree::PaypalCheckout
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Bundler auto-requires a gem by its *name*, so `gem "spree-paypal_checkout"`
4
+ # in a host Gemfile issues `require "spree-paypal_checkout"`. The real entry
5
+ # point is `spree/paypal_checkout` (matching the Spree::PaypalCheckout
6
+ # namespace), so this shim keeps the default `Bundler.require` working.
7
+ require 'spree/paypal_checkout'
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Bundler auto-requires a gem by its *name*, so `gem "spree-paypal_platform"`
4
+ # in a host Gemfile issues `require "spree-paypal_platform"`. The real entry
5
+ # point is `spree/paypal_checkout` (matching the Spree::PaypalCheckout
6
+ # namespace), so this shim keeps the default `Bundler.require` working.
7
+ require 'spree/paypal_checkout'