workarea-sezzle 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  3. data/.github/ISSUE_TEMPLATE/documentation-request.md +17 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. data/.gitignore +23 -0
  6. data/.rubocop.yml +405 -0
  7. data/.scss-lint.yml +204 -0
  8. data/Gemfile +17 -0
  9. data/LICENSE +7 -0
  10. data/README.md +95 -0
  11. data/Rakefile +56 -0
  12. data/Workarea +0 -0
  13. data/app/assets/images/workarea/admin/sezzle/.keep +0 -0
  14. data/app/assets/images/workarea/sezzle_logo_white.svg +1 -0
  15. data/app/assets/images/workarea/storefront/sezzle/.keep +0 -0
  16. data/app/assets/javascripts/workarea/admin/sezzle/.keep +0 -0
  17. data/app/assets/javascripts/workarea/storefront/sezzle/.keep +0 -0
  18. data/app/assets/stylesheets/workarea/admin/sezzle/.keep +0 -0
  19. data/app/assets/stylesheets/workarea/admin/sezzle/components/_payment_icon.scss +13 -0
  20. data/app/assets/stylesheets/workarea/storefront/sezzle/.keep +0 -0
  21. data/app/assets/stylesheets/workarea/storefront/sezzle/components/_payment_icon.scss +13 -0
  22. data/app/assets/stylesheets/workarea/storefront/sezzle/components/_sezzle.scss +13 -0
  23. data/app/controllers/.keep +0 -0
  24. data/app/controllers/workarea/storefront/checkout/place_order_controller.decorator +11 -0
  25. data/app/controllers/workarea/storefront/sezzle_controller.rb +109 -0
  26. data/app/helpers/.keep +0 -0
  27. data/app/mailers/.keep +0 -0
  28. data/app/models/.keep +0 -0
  29. data/app/models/workarea/payment/authorize/sezzle.rb +32 -0
  30. data/app/models/workarea/payment/capture/sezzle.rb +85 -0
  31. data/app/models/workarea/payment/purchase/sezzle.rb +30 -0
  32. data/app/models/workarea/payment/refund/sezzle.rb +47 -0
  33. data/app/models/workarea/payment/tender/sezzle.rb +15 -0
  34. data/app/models/workarea/paymentl.decorator +32 -0
  35. data/app/services/workarea/sezzle/order_builder.rb +135 -0
  36. data/app/view_models/workarea/storefront/checkout/payment_view_model.decorator +11 -0
  37. data/app/views/.keep +0 -0
  38. data/app/views/workarea/admin/orders/tenders/_sezzle.html.haml +3 -0
  39. data/app/views/workarea/storefront/carts/_sezzle_widget.html.haml +5 -0
  40. data/app/views/workarea/storefront/checkouts/_sezzle_payment.html.haml +8 -0
  41. data/app/views/workarea/storefront/order_mailer/tenders/_sezzle.html.haml +1 -0
  42. data/app/views/workarea/storefront/order_mailer/tenders/_sezzle.text.erb +1 -0
  43. data/app/views/workarea/storefront/orders/tenders/_sezzle.html.haml +4 -0
  44. data/app/views/workarea/storefront/products/_sezzle_widget.html.haml +10 -0
  45. data/app/views/workarea/storefront/sezzle/_script.html.haml +22 -0
  46. data/bin/rails +25 -0
  47. data/config/initializers/appends.rb +30 -0
  48. data/config/initializers/configurations.rb +41 -0
  49. data/config/initializers/workarea.rb +21 -0
  50. data/config/locales/en.yml +43 -0
  51. data/config/routes.rb +5 -0
  52. data/lib/workarea/sezzle.rb +53 -0
  53. data/lib/workarea/sezzle/authentication.rb +49 -0
  54. data/lib/workarea/sezzle/bogus_gateway.rb +128 -0
  55. data/lib/workarea/sezzle/engine.rb +10 -0
  56. data/lib/workarea/sezzle/gateway.rb +90 -0
  57. data/lib/workarea/sezzle/response.rb +27 -0
  58. data/lib/workarea/sezzle/version.rb +5 -0
  59. data/module +0 -0
  60. data/test/dummy/.ruby-version +1 -0
  61. data/test/dummy/Rakefile +6 -0
  62. data/test/dummy/app/assets/config/manifest.js +2 -0
  63. data/test/dummy/app/assets/images/.keep +0 -0
  64. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  65. data/test/dummy/app/controllers/application_controller.rb +2 -0
  66. data/test/dummy/app/controllers/concerns/.keep +0 -0
  67. data/test/dummy/app/helpers/application_helper.rb +2 -0
  68. data/test/dummy/app/javascript/packs/application.js +14 -0
  69. data/test/dummy/app/jobs/application_job.rb +7 -0
  70. data/test/dummy/app/mailers/application_mailer.rb +4 -0
  71. data/test/dummy/app/models/concerns/.keep +0 -0
  72. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  73. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  74. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  75. data/test/dummy/bin/rails +4 -0
  76. data/test/dummy/bin/rake +4 -0
  77. data/test/dummy/bin/setup +25 -0
  78. data/test/dummy/config.ru +5 -0
  79. data/test/dummy/config/application.rb +34 -0
  80. data/test/dummy/config/boot.rb +5 -0
  81. data/test/dummy/config/environment.rb +5 -0
  82. data/test/dummy/config/environments/development.rb +53 -0
  83. data/test/dummy/config/environments/production.rb +101 -0
  84. data/test/dummy/config/environments/test.rb +47 -0
  85. data/test/dummy/config/initializers/application_controller_renderer.rb +8 -0
  86. data/test/dummy/config/initializers/assets.rb +12 -0
  87. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  88. data/test/dummy/config/initializers/content_security_policy.rb +28 -0
  89. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  90. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  91. data/test/dummy/config/initializers/inflections.rb +16 -0
  92. data/test/dummy/config/initializers/mime_types.rb +4 -0
  93. data/test/dummy/config/initializers/workarea.rb +5 -0
  94. data/test/dummy/config/initializers/wrap_parameters.rb +9 -0
  95. data/test/dummy/config/locales/en.yml +33 -0
  96. data/test/dummy/config/puma.rb +38 -0
  97. data/test/dummy/config/routes.rb +5 -0
  98. data/test/dummy/config/spring.rb +6 -0
  99. data/test/dummy/db/seeds.rb +2 -0
  100. data/test/dummy/lib/assets/.keep +0 -0
  101. data/test/dummy/log/.keep +0 -0
  102. data/test/integration/workarea/storefront/sezzle_integration_test.rb +205 -0
  103. data/test/models/workarea/payment/sezzle_payment_integration_test.rb +82 -0
  104. data/test/services/workarea/sezzle/order_builder_test.rb +116 -0
  105. data/test/teaspoon_env.rb +6 -0
  106. data/test/test_helper.rb +10 -0
  107. data/workarea-sezzle.gemspec +20 -0
  108. metadata +163 -0
@@ -0,0 +1,11 @@
1
+ module Workarea
2
+ decorate Storefront::Checkout::PlaceOrderController, with: 'sezzle' do
3
+ def place_order
4
+ if params[:payment] == 'sezzle' && !current_checkout.payment.sezzle?
5
+ redirect_to start_sezzle_path and return
6
+ else
7
+ super
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,109 @@
1
+ module Workarea
2
+ class Storefront::SezzleController < Storefront::ApplicationController
3
+ include Storefront::CurrentCheckout
4
+
5
+ before_action :validate_checkout
6
+
7
+ def start
8
+ self.current_order = current_checkout.order
9
+
10
+ Pricing.perform(current_order, current_shipping)
11
+
12
+ check_inventory || (return)
13
+
14
+ # get a sezzle session for the current order
15
+ order_payload = Sezzle::OrderBuilder.new(current_order).build
16
+ response = gateway.create_session(order_payload)
17
+
18
+ if !response.success?
19
+ flash[:error] = t('workarea.storefront.sezzle.payment_error')
20
+ redirect_to(checkout_payment_path) && (return)
21
+ end
22
+
23
+ payment = Workarea::Payment.find(current_order.id)
24
+
25
+ payment.clear_credit_card
26
+
27
+ tender_attrs = {
28
+ sezzle_id: response.body['order']['uuid'],
29
+ sezzle_response: response.body.to_json,
30
+ intent: Workarea.config.sezzle_payment_action
31
+ }
32
+
33
+ payment.set_sezzle(tender_attrs)
34
+
35
+ redirect_url = response.body['order']['checkout_url']
36
+ redirect_to redirect_url
37
+ end
38
+
39
+ def complete
40
+ self.current_order = Order.find(params[:order_id]) rescue nil
41
+ payment = current_checkout.payment
42
+
43
+ current_order.user_id = current_user.try(:id)
44
+ check_inventory || (return)
45
+
46
+ shipping = current_shipping
47
+
48
+ Pricing.perform(current_order, shipping)
49
+ payment.adjust_tender_amounts(current_order.total_price)
50
+
51
+ # place the order.
52
+ if current_checkout.place_order
53
+ completed_place_order
54
+ else
55
+ incomplete_place_order
56
+ end
57
+ end
58
+
59
+ def cancel
60
+ self.current_order = Order.find(params[:order_id])
61
+
62
+ current_order.user_id = current_user.try(:id)
63
+
64
+ payment = current_checkout.payment
65
+ payment.clear_sezzle
66
+
67
+ flash[:success] = t('workarea.storefront.sezzle.cancel_message')
68
+
69
+ redirect_to checkout_payment_path
70
+ end
71
+
72
+ private
73
+
74
+ def gateway
75
+ Sezzle.gateway
76
+ end
77
+
78
+ def completed_place_order
79
+ Storefront::OrderMailer.confirmation(current_order.id).deliver_later
80
+ self.completed_order = current_order
81
+ clear_current_order
82
+
83
+ flash[:success] = t('workarea.storefront.flash_messages.order_placed')
84
+ redirect_to finished_checkout_destination
85
+ end
86
+
87
+ def incomplete_place_order
88
+ if current_checkout.shipping.try(:errors).present?
89
+ flash[:error] = current_checkout.shipping.errors.to_a.to_sentence
90
+ redirect_to checkout_shipping_path
91
+ else
92
+ flash[:error] = t('workarea.storefront.sezzle.payment_error')
93
+
94
+ payment = current_checkout.payment
95
+ payment.clear_sezzle
96
+
97
+ redirect_to checkout_payment_path
98
+ end
99
+ end
100
+
101
+ def finished_checkout_destination
102
+ if current_admin.present? && current_admin.orders_access?
103
+ admin.order_path(completed_order)
104
+ else
105
+ checkout_confirmation_path
106
+ end
107
+ end
108
+ end
109
+ end
File without changes
File without changes
File without changes
@@ -0,0 +1,32 @@
1
+ module Workarea
2
+ class Payment
3
+ module Authorize
4
+ class Sezzle
5
+ include OperationImplementation
6
+ include CreditCardOperation
7
+
8
+ # sezzle authorizes the funds on their platform. No call is made to the
9
+ # API. Workarea just records the response for posterity.
10
+ def complete!
11
+ transaction.response = ActiveMerchant::Billing::Response.new(
12
+ true,
13
+ I18n.t(
14
+ 'workarea.sezzle.authorize',
15
+ amount: transaction.amount
16
+ ),
17
+ pre_authorized_response_message
18
+ )
19
+ end
20
+
21
+ private
22
+
23
+ def pre_authorized_response_message
24
+ {
25
+ message: I18n.t('workarea.sezzle.pre_authorized_message',
26
+ amount: transaction.amount)
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,85 @@
1
+ module Workarea
2
+ class Payment
3
+ class Capture
4
+ class Sezzle
5
+ include OperationImplementation
6
+ include CreditCardOperation
7
+
8
+ def complete!
9
+ # if the intent of the intial order was CAPTURE then Workarea will just record a message
10
+ # on the transaction and not reach out to the third party. Tenders with an intent of
11
+ # capture will already be captured by sezzle during the users checkout process.
12
+ if tender.intent == 'CAPTURE'
13
+ transaction.response = ActiveMerchant::Billing::Response.new(
14
+ true,
15
+ I18n.t(
16
+ 'workarea.sezzle.capture',
17
+ amount: transaction.amount
18
+ ),
19
+ pre_captured_response_message
20
+ )
21
+ else
22
+ gateway_capture
23
+ end
24
+ end
25
+
26
+ def cancel!
27
+ return unless transaction.success?
28
+
29
+ payment_id = transaction.response.params['id']
30
+ response = gateway.void(payment_id)
31
+
32
+ transaction.cancellation = ActiveMerchant::Billing::Response.new(
33
+ true,
34
+ I18n.t('workarea.sezzle.void'),
35
+ response.body
36
+ )
37
+ end
38
+
39
+ private
40
+
41
+ def pre_captured_response_message
42
+ {
43
+ message: I18n.t('workarea.sezzle.pre_captured_message',
44
+ amount: transaction.amount)
45
+ }
46
+ end
47
+
48
+ def gateway
49
+ Workarea::Sezzle.gateway
50
+ end
51
+
52
+ def transaction_options
53
+ {
54
+ capture_amount: {
55
+ amount_in_cents: tender.amount.cents,
56
+ currency: transaction.amount.currency.iso_code
57
+ },
58
+ partial_capture: transaction.amount < tender.amount
59
+ }
60
+ end
61
+
62
+ def gateway_capture
63
+ response = gateway.capture(tender.sezzle_id, transaction_options)
64
+
65
+ if response.success?
66
+ transaction.response = ActiveMerchant::Billing::Response.new(
67
+ true,
68
+ I18n.t(
69
+ 'workarea.sezzle.capture',
70
+ amount: transaction.amount
71
+ ),
72
+ response.body
73
+ )
74
+ else
75
+ transaction.response = ActiveMerchant::Billing::Response.new(
76
+ false,
77
+ I18n.t('workarea.sezzle.capture_failure'),
78
+ response.body
79
+ )
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,30 @@
1
+ module Workarea
2
+ class Payment
3
+ module Purchase
4
+ class Sezzle
5
+ include OperationImplementation
6
+ include CreditCardOperation
7
+
8
+ def complete!
9
+ transaction.response = ActiveMerchant::Billing::Response.new(
10
+ true,
11
+ I18n.t(
12
+ 'workarea.sezzle.purchase',
13
+ amount: transaction.amount
14
+ ),
15
+ pre_purchased_response_message
16
+ )
17
+ end
18
+
19
+ private
20
+
21
+ def pre_purchased_response_message
22
+ {
23
+ message: I18n.t('workarea.sezzle.pre_purchased_message',
24
+ amount: transaction.amount)
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,47 @@
1
+ module Workarea
2
+ class Payment
3
+ class Refund
4
+ class Sezzle
5
+ include OperationImplementation
6
+ include CreditCardOperation
7
+
8
+ def complete!
9
+ response = gateway.refund(tender.sezzle_id, transaction_options)
10
+ if response.success?
11
+ transaction.response = ActiveMerchant::Billing::Response.new(
12
+ true,
13
+ I18n.t(
14
+ 'workarea.sezzle.refund',
15
+ amount: transaction.amount
16
+ ),
17
+ response.body
18
+ )
19
+ else
20
+ transaction.response = ActiveMerchant::Billing::Response.new(
21
+ false,
22
+ I18n.t('workarea.sezzle.refund'),
23
+ response.body
24
+ )
25
+ end
26
+ end
27
+
28
+ def cancel!
29
+ #no-op - nothing to role back on refund.
30
+ end
31
+
32
+ private
33
+
34
+ def gateway
35
+ Workarea::Sezzle.gateway
36
+ end
37
+
38
+ def transaction_options
39
+ {
40
+ amount_in_cents: transaction.amount.cents,
41
+ currency: transaction.amount.currency.iso_code
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,15 @@
1
+ module Workarea
2
+ class Payment
3
+ class Tender
4
+ class Sezzle < Tender
5
+ field :intent, type: String
6
+ field :sezzle_id, type: String
7
+ field :sezzle_response, type: String
8
+
9
+ def slug
10
+ :sezzle
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,32 @@
1
+ module Workarea
2
+ decorate Payment, with: :sezzle do
3
+ decorated do
4
+ embeds_one :sezzle, class_name: 'Workarea::Payment::Tender::Sezzle'
5
+ end
6
+
7
+ def sezzle?
8
+ sezzle.present?
9
+ end
10
+
11
+ def set_sezzle(attrs)
12
+ build_sezzle unless sezzle
13
+ sezzle.attributes = attrs.slice(
14
+ :sezzle_id,
15
+ :sezzle_response,
16
+ :intent
17
+ )
18
+
19
+ save
20
+ end
21
+
22
+ def clear_sezzle
23
+ self.sezzle = nil
24
+ save
25
+ end
26
+
27
+ def set_credit_card(*)
28
+ self.sezzle = nil
29
+ super
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,135 @@
1
+ module Workarea
2
+ module Sezzle
3
+ class OrderBuilder
4
+ module ProductUrl
5
+ include Workarea::I18n::DefaultUrlOptions
6
+ include Storefront::Engine.routes.url_helpers
7
+ extend self
8
+ end
9
+ attr_reader :order
10
+
11
+ # @param ::Workarea::Order
12
+ def initialize(order)
13
+ @order = Workarea::Storefront::OrderViewModel.new(order)
14
+ end
15
+
16
+ def build
17
+ {
18
+ cancel_url: {
19
+ href: cancel_url,
20
+ method: 'GET'
21
+ },
22
+ complete_url: {
23
+ href: complete_url,
24
+ method: 'GET'
25
+ },
26
+ customer: {
27
+ tokenization: true,
28
+ email: order.email,
29
+ first_name: payment.address.first_name,
30
+ last_name: payment.address.last_name,
31
+ phone: phone_number,
32
+ dob: nil, # workarea does not collect oob,
33
+ billing_address: address(payment.address),
34
+ shipping_address: address(shipping&.address)
35
+ },
36
+ order: {
37
+ intent: Workarea.config.sezzle_payment_action,
38
+ reference_id: order.id,
39
+ description: "Sezzle Order #{order.id}",
40
+ requires_shipping_info: false,
41
+ items: items,
42
+ discounts: discounts,
43
+ shipping_amount: {
44
+ amount_in_cents: order.shipping_total.cents,
45
+ currency: currency_code
46
+ },
47
+ tax_amount: {
48
+ amount_in_cents: order.tax_total.cents,
49
+ currency: currency_code
50
+ },
51
+ order_amount: {
52
+ amount_in_cents: order.order_balance.cents,
53
+ currency: currency_code
54
+ }
55
+ }
56
+ }
57
+ end
58
+
59
+ private
60
+
61
+ def currency_code
62
+ @currency_code = order.total_price.currency.iso_code
63
+ end
64
+
65
+ def shipping
66
+ @shipping = Workarea::Shipping.find_by_order(order.id)
67
+ end
68
+
69
+ def payment
70
+ @payment = Workarea::Payment.find(order.id)
71
+ end
72
+
73
+ def address(address_obj)
74
+ return {} unless address_obj.present?
75
+
76
+ {
77
+ name: "#{address_obj.first_name} #{address_obj.last_name}",
78
+ street: address_obj.street,
79
+ street2: address_obj.street_2,
80
+ city: address_obj.city,
81
+ state: address_obj.region,
82
+ postal_code: address_obj.postal_code,
83
+ country_code: address_obj.country.alpha2,
84
+ phone_number: address_obj.phone_number
85
+ }
86
+ end
87
+
88
+ def items
89
+ order.items.map do |oi|
90
+ product = Workarea::Catalog::Product.find_by_sku(oi.sku)
91
+ {
92
+ name: product.name,
93
+ sku: oi.sku,
94
+ quantity: oi.quantity,
95
+ price: {
96
+ amount_in_cents: oi.total_price.cents,
97
+ currency: currency_code
98
+ }
99
+ }
100
+ end
101
+ end
102
+
103
+ def discounts
104
+ discounts = order.price_adjustments.select { |p| p.discount? }
105
+ discounts.map do |d|
106
+ {
107
+ name: d.description,
108
+ amount: {
109
+ amount_in_cents: d.amount.abs.cents,
110
+ currency: currency_code
111
+ }
112
+ }
113
+ end
114
+ end
115
+
116
+ def complete_url
117
+ Storefront::Engine.routes.url_helpers.complete_sezzle_url(
118
+ host: Workarea.config.host,
119
+ order_id: order.id
120
+ )
121
+ end
122
+
123
+ def cancel_url
124
+ Storefront::Engine.routes.url_helpers.cancel_sezzle_url(
125
+ host: Workarea.config.host,
126
+ order_id: order.id
127
+ )
128
+ end
129
+
130
+ def phone_number
131
+ payment.address.phone_number || shipping.address.phone_number
132
+ end
133
+ end
134
+ end
135
+ end