workarea-sezzle 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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