spree_vpago 0.1.0.pre.beta → 2.0.5.pre.beta

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test_and_publish_gem.yml +2 -0
  3. data/Gemfile.lock +25 -23
  4. data/app/assets/javascripts/vpago/vpago_payments/user_informers/firebase.js +1686 -1426
  5. data/app/controllers/spree/admin/payment_methods_controller_decorator.rb +9 -1
  6. data/app/controllers/spree/vpago_payments_controller.rb +12 -2
  7. data/app/helpers/vpago/admin/base_helper_decorator.rb +1 -0
  8. data/app/javascripts/queue_processor.js +33 -0
  9. data/app/javascripts/queue_processor.test.js +88 -0
  10. data/app/javascripts/vpago/vpago_payments/user_informers/firebase.js +92 -13
  11. data/app/jobs/vpago/payment_capturer_job.rb +11 -0
  12. data/app/jobs/vpago/payment_processor_job.rb +3 -0
  13. data/app/models/spree/gateway/vattanac_mini_app.rb +75 -0
  14. data/app/models/vpago/address_decorator.rb +10 -0
  15. data/app/models/vpago/order_decorator.rb +13 -1
  16. data/app/models/vpago/payment_decorator.rb +23 -1
  17. data/app/models/vpago/payment_method_decorator.rb +7 -1
  18. data/app/overrides/spree/admin/payment_methods/index/payment_methods_tabs.html.erb.deface +7 -1
  19. data/app/overrides/spree/admin/payment_methods/index/tenant_body.html.erb.deface +5 -0
  20. data/app/overrides/spree/admin/payment_methods/index/tenant_header.html.erb.deface +5 -0
  21. data/app/serializers/spree/v2/storefront/payment_serializer_decorator.rb +1 -1
  22. data/app/services/vpago/aes_encrypter.rb +56 -0
  23. data/app/services/vpago/payment_finder.rb +19 -3
  24. data/app/services/vpago/payment_processable.rb +54 -0
  25. data/app/services/vpago/payment_processor.rb +58 -41
  26. data/app/services/vpago/payment_url_constructor.rb +1 -1
  27. data/app/services/vpago/rsa_handler.rb +27 -0
  28. data/app/services/vpago/user_informers/firebase.rb +21 -16
  29. data/app/services/vpago/vattanac_mini_app_data_handler.rb +33 -0
  30. data/app/views/spree/admin/payments/source_views/_vattanac_mini_app.html.erb +6 -0
  31. data/app/views/spree/admin/shared/_payment_methods_tabs.html.erb +7 -0
  32. data/app/views/spree/vpago_payments/forms/spree/gateway/_vattanac_mini_app.html.erb +89 -0
  33. data/app/views/spree/vpago_payments/processing.html.erb +36 -23
  34. data/lib/spree_vpago/engine.rb +2 -1
  35. data/lib/spree_vpago/version.rb +1 -1
  36. data/lib/vpago/payway_v2/base.rb +8 -8
  37. data/lib/vpago/payway_v2/checkout.rb +2 -2
  38. data/lib/vpago/vattanac_mini_app/base.rb +52 -0
  39. data/lib/vpago/vattanac_mini_app/checkout.rb +9 -0
  40. data/lib/vpago/vattanac_mini_app/refund_issuer.rb +54 -0
  41. data/node_modules/.yarn-integrity +93 -2
  42. data/package.json +6 -1
  43. data/yarn.lock +556 -2
  44. metadata +18 -2
@@ -3,7 +3,15 @@ module Spree
3
3
  module PaymentMethodsControllerDecorator
4
4
  def scope
5
5
  scope = current_store.payment_methods_including_vendor.accessible_by(current_ability, :index)
6
- scope = scope.where.not(vendor_id: nil) if params[:tab] == 'vendors'
6
+
7
+ if params[:tab] == 'vendors'
8
+ scope = scope.where.not(vendor_id: nil)
9
+ elsif params[:tab] == 'tenants'
10
+ scope = scope.joins(:vendor)
11
+ .where.not(vendor_id: nil)
12
+ .where.not(spree_vendors: { tenant_id: nil })
13
+ end
14
+
7
15
  scope
8
16
  end
9
17
 
@@ -39,7 +39,8 @@ module Spree
39
39
  def process_payment
40
40
  return render json: { status: :ok }, status: :ok if request.method != 'POST'
41
41
 
42
- @payment = Vpago::PaymentFinder.new(params.permit!.to_h).find_and_verify
42
+ return_params = sanitize_return_params
43
+ @payment = Vpago::PaymentFinder.new(return_params).find_and_verify
43
44
  return render_not_found unless @payment.present?
44
45
 
45
46
  unless @payment.order.paid?
@@ -54,7 +55,16 @@ module Spree
54
55
  render json: { status: :internal_server_error, message: 'Failed to enqueue payment processor job' }, status: :internal_server_error
55
56
  end
56
57
 
57
- def record_not_found
58
+ def sanitize_return_params
59
+ sanitized_params = params.permit!.to_h
60
+
61
+ # In ABA case, it returns params in side return params.
62
+ sanitized_params.merge!(JSON.parse(sanitized_params.delete(:return_params))) if sanitized_params[:return_params].present?
63
+
64
+ sanitized_params
65
+ end
66
+
67
+ def render_not_found
58
68
  respond_to do |format|
59
69
  format.html { render file: Rails.public_path.join('404.html'), status: :not_found, layout: false }
60
70
  format.json { render json: { status: :not_found }, status: :not_found }
@@ -18,6 +18,7 @@ module Vpago
18
18
  cheque
19
19
  payway_cards
20
20
  wingpay
21
+ vattanac_mini_app
21
22
  ]
22
23
  end
23
24
 
@@ -0,0 +1,33 @@
1
+ export default class QueueProcessor {
2
+ constructor() {
3
+ this.queues = [];
4
+ this.processing = false;
5
+ }
6
+
7
+ queueStateChange({ callback, minDelayInMs = 1000 }) {
8
+ this.queues.push({ callback, minDelayInMs });
9
+ if (!this.processing) this.#processQueue();
10
+ }
11
+
12
+ async #processQueue() {
13
+ if (this.queues.length === 0) {
14
+ this.processing = false;
15
+ return;
16
+ }
17
+
18
+ this.processing = true;
19
+ const { callback, minDelayInMs } = this.queues.shift();
20
+ const startTime = Date.now();
21
+
22
+ await callback();
23
+
24
+ const elapsedTime = Date.now() - startTime;
25
+ if (elapsedTime < minDelayInMs) {
26
+ await new Promise((resolve) =>
27
+ setTimeout(resolve, minDelayInMs - elapsedTime)
28
+ );
29
+ }
30
+
31
+ this.#processQueue();
32
+ }
33
+ }
@@ -0,0 +1,88 @@
1
+ import { expect } from "chai";
2
+ import QueueProcessor from "./queue_processor.js";
3
+
4
+ describe("QueueProcessor", () => {
5
+ let queueProcessor;
6
+ let minDelayInMs = 100;
7
+
8
+ beforeEach(() => {
9
+ queueProcessor = new QueueProcessor();
10
+ });
11
+
12
+ it("should initially have an empty queue and not be processing", () => {
13
+ expect(queueProcessor.queues).to.have.lengthOf(0);
14
+ expect(queueProcessor.processing).to.equal(false);
15
+ });
16
+
17
+ it("should run queue 1 by 1 while keep min delay 100", async () => {
18
+ let timeToProcess = 50;
19
+
20
+ const mockCallback = () =>
21
+ new Promise((resolve) => setTimeout(resolve, timeToProcess));
22
+
23
+ queueProcessor.queueStateChange({
24
+ callback: mockCallback,
25
+ minDelayInMs: minDelayInMs,
26
+ });
27
+
28
+ queueProcessor.queueStateChange({
29
+ callback: mockCallback,
30
+ minDelayInMs: minDelayInMs,
31
+ });
32
+
33
+ queueProcessor.queueStateChange({
34
+ callback: mockCallback,
35
+ minDelayInMs: minDelayInMs,
36
+ });
37
+
38
+ expect(queueProcessor.queues).to.have.lengthOf(2);
39
+ expect(queueProcessor.processing).to.equal(true);
40
+
41
+ await new Promise((resolve) => setTimeout(resolve, minDelayInMs * 3 + 4)); // +4ms for buffer
42
+
43
+ expect(queueProcessor.queues).to.have.lengthOf(0);
44
+ expect(queueProcessor.processing).to.equal(false);
45
+ });
46
+
47
+ describe("when process take less than the delay", () => {
48
+ it("should process and wait for remaining delay", async () => {
49
+ let timeToProcess = 50;
50
+
51
+ const mockCallback = () =>
52
+ new Promise((resolve) => setTimeout(resolve, timeToProcess));
53
+
54
+ queueProcessor.queueStateChange({
55
+ callback: mockCallback,
56
+ minDelayInMs: minDelayInMs,
57
+ });
58
+
59
+ expect(queueProcessor.queues).to.have.lengthOf(0);
60
+ expect(queueProcessor.processing).to.equal(true);
61
+
62
+ await new Promise((resolve) => setTimeout(resolve, minDelayInMs + 1)); // +1ms for buffer
63
+
64
+ expect(queueProcessor.queues).to.have.lengthOf(0);
65
+ expect(queueProcessor.processing).to.equal(false);
66
+ });
67
+ });
68
+
69
+ describe("when process take longer than the delay", () => {
70
+ it("should process and not wait for delay", async () => {
71
+ let timeToProcess = 200;
72
+
73
+ const mockCallback = () =>
74
+ new Promise((resolve) => setTimeout(resolve, timeToProcess));
75
+
76
+ queueProcessor.queueStateChange({
77
+ callback: mockCallback,
78
+ minDelayInMs: minDelayInMs,
79
+ });
80
+
81
+ expect(queueProcessor.queues).to.have.lengthOf(0);
82
+ expect(queueProcessor.processing).to.equal(true);
83
+
84
+ await new Promise((resolve) => setTimeout(resolve, timeToProcess + 1)); // +1ms for buffer
85
+ expect(queueProcessor.processing).to.equal(false);
86
+ });
87
+ });
88
+ });
@@ -1,9 +1,10 @@
1
1
  import { initializeApp } from "firebase/app";
2
2
  import { getFirestore, doc, onSnapshot, setDoc } from "firebase/firestore";
3
+ import QueueProcessor from "../../../queue_processor.js";
3
4
 
4
5
  async function listenToProcessingState({
5
6
  firebaseConfigs,
6
- orderNumber,
7
+ documentReferencePath,
7
8
  onPaymentIsProcessing,
8
9
  onOrderIsProcessing,
9
10
  onOrderIsCompleted,
@@ -15,43 +16,121 @@ async function listenToProcessingState({
15
16
  const app = initializeApp(firebaseConfigs);
16
17
  const db = getFirestore(app);
17
18
 
18
- const currentDate = new Date().toISOString().split("T")[0];
19
-
20
- const documentRef = doc(db, "statuses", "cart", currentDate, orderNumber);
19
+ const documentRef = doc(db, documentReferencePath);
21
20
  await setDoc(documentRef, { listening: true }, { merge: true });
22
21
 
22
+ const queueProcessor = new QueueProcessor();
23
+
23
24
  onSnapshot(documentRef, (doc) => {
24
25
  let documentData = doc.data();
25
26
 
27
+ let messageCode = documentData["message_code"];
26
28
  let orderState = documentData["order_state"];
27
29
  let paymentState = documentData["payment_state"];
28
- let messageCode = documentData["message_code"];
29
- let logMessage = documentData["log_message"];
30
+ let processing = documentData["processing"] === true;
31
+ let reasonCode = documentData["reason_code"];
32
+ let reasonMessage = documentData["reason_message"];
30
33
 
31
34
  let orderCompleted = orderState === "complete";
32
35
  if (orderCompleted) {
33
- onCompleted(orderState, paymentState);
36
+ queueProcessor.queueStateChange({
37
+ minDelayInMs: 1500,
38
+ callback: async () => {
39
+ await onCompleted(
40
+ orderState,
41
+ paymentState,
42
+ reasonCode,
43
+ reasonMessage
44
+ );
45
+ },
46
+ });
34
47
  return;
35
48
  }
36
49
 
37
50
  switch (messageCode) {
38
51
  case "payment_is_processing":
39
- onPaymentIsProcessing(orderState, paymentState, logMessage);
52
+ queueProcessor.queueStateChange({
53
+ minDelayInMs: 1500,
54
+ callback: async () => {
55
+ await onPaymentIsProcessing(
56
+ orderState,
57
+ paymentState,
58
+ processing,
59
+ reasonCode,
60
+ reasonMessage
61
+ );
62
+ },
63
+ });
40
64
  break;
41
65
  case "order_is_processing":
42
- onOrderIsProcessing(orderState, paymentState, logMessage);
66
+ queueProcessor.queueStateChange({
67
+ minDelayInMs: 1500,
68
+ callback: async () => {
69
+ await onOrderIsProcessing(
70
+ orderState,
71
+ paymentState,
72
+ processing,
73
+ reasonCode,
74
+ reasonMessage
75
+ );
76
+ },
77
+ });
43
78
  break;
44
79
  case "order_is_completed":
45
- onOrderIsCompleted(orderState, paymentState, logMessage);
80
+ queueProcessor.queueStateChange({
81
+ minDelayInMs: 1500,
82
+ callback: async () => {
83
+ await onOrderIsCompleted(
84
+ orderState,
85
+ paymentState,
86
+ processing,
87
+ reasonCode,
88
+ reasonMessage
89
+ );
90
+ },
91
+ });
46
92
  break;
47
93
  case "order_process_failed":
48
- onOrderProcessFailed(orderState, paymentState, logMessage);
94
+ queueProcessor.queueStateChange({
95
+ minDelayInMs: 1500,
96
+ callback: async () => {
97
+ await onOrderProcessFailed(
98
+ orderState,
99
+ paymentState,
100
+ processing,
101
+ reasonCode,
102
+ reasonMessage
103
+ );
104
+ },
105
+ });
49
106
  break;
50
107
  case "payment_is_refunded":
51
- onPaymentIsRefunded(orderState, paymentState, logMessage);
108
+ queueProcessor.queueStateChange({
109
+ minDelayInMs: 1500,
110
+ callback: async () => {
111
+ await onPaymentIsRefunded(
112
+ orderState,
113
+ paymentState,
114
+ processing,
115
+ reasonCode,
116
+ reasonMessage
117
+ );
118
+ },
119
+ });
52
120
  break;
53
121
  case "payment_process_failed":
54
- onPaymentProcessFailed(orderState, paymentState, logMessage);
122
+ queueProcessor.queueStateChange({
123
+ minDelayInMs: 1500,
124
+ callback: async () => {
125
+ await onPaymentProcessFailed(
126
+ orderState,
127
+ paymentState,
128
+ processing,
129
+ reasonCode,
130
+ reasonMessage
131
+ );
132
+ },
133
+ });
55
134
  break;
56
135
  default:
57
136
  break;
@@ -0,0 +1,11 @@
1
+ # Put :payment_processing at a higher priority in your project: config/sidekiq.yml
2
+ module Vpago
3
+ class PaymentCapturerJob < ::ApplicationUniqueJob
4
+ queue_as :payment_processing
5
+
6
+ def perform(payment_id)
7
+ payment = Spree::Payment.find(payment_id)
8
+ payment.capture! if payment.pending?
9
+ end
10
+ end
11
+ end
@@ -1,5 +1,8 @@
1
+ # Put :payment_processing at a higher priority in your project: config/sidekiq.yml
1
2
  module Vpago
2
3
  class PaymentProcessorJob < ::ApplicationUniqueJob
4
+ queue_as :payment_processing
5
+
3
6
  def perform(options)
4
7
  payment = Spree::Payment.find_by(number: options[:payment_number])
5
8
  Vpago::PaymentProcessor.new(payment: payment).call
@@ -0,0 +1,75 @@
1
+ module Spree
2
+ class Gateway::VattanacMiniApp < PaymentMethod
3
+
4
+ def method_type
5
+ 'vattanac_mini_app'
6
+ end
7
+
8
+ def payment_source_class
9
+ Spree::VpagoPaymentSource
10
+ end
11
+
12
+ # force to purchase instead of authorize
13
+ def auto_capture?
14
+ true
15
+ end
16
+
17
+
18
+ # override
19
+ # purchase is used when pre auth disabled
20
+ def purchase(_amount, _source, gateway_options = {})
21
+ _, payment_number = gateway_options[:order_id].split('-')
22
+ payment = Spree::Payment.find_by(number: payment_number)
23
+
24
+ params = {}
25
+
26
+ params[:payment_response] = payment.transaction_response
27
+
28
+
29
+ if payment.transaction_response["status"] == 'SUCCESS'
30
+ ActiveMerchant::Billing::Response.new(true, 'Payway Gateway: Purchased', params)
31
+ else
32
+ ActiveMerchant::Billing::Response.new(false, 'Payway Gateway: Purchasing Failed', params)
33
+ end
34
+
35
+ end
36
+
37
+
38
+ # override
39
+ def void(_response_code, gateway_options)
40
+ _, payment_number = gateway_options[:order_id].split('-')
41
+ payment = Spree::Payment.find_by(number: payment_number)
42
+
43
+ if payment.vattanac_mini_app_payment?
44
+ params = {}
45
+ success, params[:refund_response] = vattanac_mini_app_refund(payment)
46
+
47
+ if success
48
+ ActiveMerchant::Billing::Response.new(true, 'Payway Gateway: successfully canceled.', params)
49
+ else
50
+ ActiveMerchant::Billing::Response.new(false, 'Payway Gateway: Failed to canceleed', params)
51
+ end
52
+ else
53
+ ActiveMerchant::Billing::Response.new(true, 'Payway Gateway: Payment has been voided.')
54
+ end
55
+ end
56
+
57
+
58
+ def vattanac_mini_app_refund(payment)
59
+
60
+ refund_issuer = Vpago::VattanacMiniApp::RefundIssuer.new(payment, {})
61
+ refund_issuer.call
62
+
63
+ [refund_issuer.success?, refund_issuer.response]
64
+
65
+ end
66
+
67
+ def cancel(_response_code)
68
+ # we can use this to send request to payment gateway api to cancel the payment ( void )
69
+ # currently Payway does not support to cancel the gateway
70
+
71
+ # in our case don't do anything
72
+ ActiveMerchant::Billing::Response.new(true, 'Vattanc order has been cancelled.')
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,10 @@
1
+ module Vpago
2
+ module AddressDecorator
3
+ # override
4
+ def require_phone?
5
+ false
6
+ end
7
+ end
8
+ end
9
+
10
+ Spree::Address.prepend(Vpago::AddressDecorator) unless Spree::Address.included_modules.include?(Vpago::AddressDecorator)
@@ -48,7 +48,9 @@ module Vpago
48
48
 
49
49
  # override
50
50
  def available_payment_methods(store = nil)
51
- payment_methods = if vendor_payment_methods.any?
51
+ payment_methods = if respond_to?(:tenant) && tenant.present?
52
+ tenant_payment_methods
53
+ elsif vendor_payment_methods.any?
52
54
  vendor_payment_methods
53
55
  else
54
56
  collect_payment_methods(store)
@@ -61,6 +63,10 @@ module Vpago
61
63
  end
62
64
  end
63
65
 
66
+ def tenant_payment_methods
67
+ tenant.tenant_payment_methods
68
+ end
69
+
64
70
  def line_items_count
65
71
  line_items.size
66
72
  end
@@ -72,6 +78,12 @@ module Vpago
72
78
  def order_adjustment_total
73
79
  adjustments.eligible.sum(:amount)
74
80
  end
81
+
82
+ # override this method if you want flexibility
83
+ # for example, host per payment method or tenant
84
+ def payment_host
85
+ ENV.fetch('DEFAULT_URL_HOST')
86
+ end
75
87
  end
76
88
  end
77
89
 
@@ -11,13 +11,30 @@ module Vpago
11
11
  to: :url_constructor
12
12
  end
13
13
 
14
+ # override:
15
+ # to give payment another chance to re-process, even if it failed.
14
16
  def process!
15
- # give payment another chance to re-process, even if it failed.
16
17
  update!(state: :checkout) if processing? || send(:has_invalid_state?)
17
18
 
18
19
  super
19
20
  end
20
21
 
22
+ # override:
23
+ # to allow capture faraday connection error. gateway_error method already write rails log for this.
24
+ def protect_from_connection_error
25
+ yield
26
+ rescue ActiveMerchant::ConnectionError => e
27
+ failure!
28
+ gateway_error(e)
29
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
30
+ failure!
31
+ gateway_error(ActiveMerchant::ConnectionError.new(e.message, e))
32
+ end
33
+
34
+ def user_informer
35
+ @user_informer ||= ::Vpago::UserInformers::Firebase.new(order)
36
+ end
37
+
21
38
  def url_constructor
22
39
  @url_constructor ||= Vpago::PaymentUrlConstructor.new(self)
23
40
  end
@@ -38,6 +55,11 @@ module Vpago
38
55
  def pre_auth_cancelled?
39
56
  pre_auth_status == 'CANCELLED'
40
57
  end
58
+
59
+ def vattanac_mini_app_payment?
60
+ payment_method.type_vattanac_mini_app?
61
+ end
62
+
41
63
  end
42
64
  end
43
65
 
@@ -5,6 +5,7 @@ module Vpago
5
5
  TYPE_WINGSDK = 'Spree::Gateway::WingSdk'.freeze
6
6
  TYPE_ACLEDA = 'Spree::Gateway::Acleda'.freeze
7
7
  TYPE_ACLEDA_MOBILE = 'Spree::Gateway::AcledaMobile'.freeze
8
+ TYPE_VATTANAC_MINI_APP = 'Spree::Gateway::VattanacMiniApp'.freeze
8
9
 
9
10
  def self.prepended(base)
10
11
  base.preference :icon_name, :string, default: 'cheque'
@@ -16,7 +17,8 @@ module Vpago
16
17
  Spree::PaymentMethod::TYPE_PAYWAY,
17
18
  Spree::PaymentMethod::TYPE_WINGSDK,
18
19
  Spree::PaymentMethod::TYPE_ACLEDA,
19
- Spree::PaymentMethod::TYPE_ACLEDA_MOBILE
20
+ Spree::PaymentMethod::TYPE_ACLEDA_MOBILE,
21
+ Spree::PaymentMethod::TYPE_VATTANAC_MINI_APP
20
22
  ]
21
23
  end
22
24
  end
@@ -85,6 +87,10 @@ module Vpago
85
87
  def type_wingsdk?
86
88
  type == Spree::PaymentMethod::TYPE_WINGSDK
87
89
  end
90
+
91
+ def type_vattanac_mini_app?
92
+ type == Spree::PaymentMethod::TYPE_VATTANAC_MINI_APP
93
+ end
88
94
  end
89
95
  end
90
96
 
@@ -4,6 +4,12 @@
4
4
 
5
5
  <% if params[:tab] == 'vendors' %>
6
6
  <div class="alert alert-info mb-3">
7
- <%= svg_icon name: "info-circle.svg", classes: 'mr-2', width: '16', height: '16' %> Payment methods for each vendor. Once set, only those payment methods will be displayed to user.
7
+ <%= svg_icon name: "info-circle.svg", classes: 'mr-2', width: '16', height: '16' %>
8
+ Payment methods for each vendor. Once set, only those payment methods will be displayed to users.
9
+ </div>
10
+ <% elsif params[:tab] == 'tenants' %>
11
+ <div class="alert alert-info mb-3">
12
+ <%= svg_icon name: "info-circle.svg", classes: 'mr-2', width: '16', height: '16' %>
13
+ Payment methods for each tenant. Once set, only those payment methods will be displayed to users.
8
14
  </div>
9
15
  <% end %>
@@ -0,0 +1,5 @@
1
+ <!-- insert_before "[data-hook='admin_payment_methods_index_row_actions']" -->
2
+
3
+ <% if params[:tab] == 'tenants' %>
4
+ <td class="text-center"><%= method.vendor&.name || 'N/A' %></td>
5
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <!-- insert_before "[data-hook='admin_payment_methods_index_header_actions']" -->
2
+
3
+ <% if params[:tab] == 'tenants' %>
4
+ <th class="text-center"><%= Spree.t(:tenant) %></th>
5
+ <% end %>
@@ -3,7 +3,7 @@ module Spree
3
3
  module Storefront
4
4
  module PaymentSerializerDecorator
5
5
  def self.prepended(base)
6
- base.attributes :pre_auth_status, :checkout_url
6
+ base.attributes :pre_auth_status, :checkout_url, :process_payment_url
7
7
  end
8
8
  end
9
9
  end
@@ -0,0 +1,56 @@
1
+ require 'base64'
2
+ require 'openssl'
3
+
4
+ module Vpago
5
+ class AesEncrypter
6
+ ALGORITHM = 'aes-256-gcm'.freeze
7
+ KEY_LENGTH = 32
8
+ IV_LENGTH = 12
9
+ TAG_LENGTH = 16
10
+
11
+ def self.encrypt(plaintext, base64_key)
12
+ key = Base64.decode64(base64_key)
13
+ validate_key!(key)
14
+
15
+ cipher = OpenSSL::Cipher.new(ALGORITHM)
16
+ cipher.encrypt
17
+ cipher.key = key[0, KEY_LENGTH]
18
+ iv = cipher.random_iv
19
+ cipher.iv = iv
20
+
21
+ ciphertext = cipher.update(plaintext) + cipher.final
22
+ tag = cipher.auth_tag
23
+
24
+ combined = iv + ciphertext + tag
25
+ Base64.strict_encode64(combined)
26
+ end
27
+
28
+ def self.decrypt(encrypted_text, base64_key)
29
+ key = Base64.decode64(base64_key)
30
+ validate_key!(key)
31
+
32
+ combined = Base64.decode64(encrypted_text)
33
+ iv = combined[0, IV_LENGTH]
34
+ tag = combined[-TAG_LENGTH..]
35
+ ciphertext = combined[IV_LENGTH...-TAG_LENGTH]
36
+
37
+ cipher = OpenSSL::Cipher.new(ALGORITHM)
38
+ cipher.decrypt
39
+ cipher.key = key[0, KEY_LENGTH]
40
+ cipher.iv = iv
41
+ cipher.auth_tag = tag
42
+
43
+ cipher.update(ciphertext) + cipher.final
44
+ rescue OpenSSL::Cipher::CipherError => e
45
+ raise "Decryption failed: #{e.message}"
46
+ end
47
+
48
+ def self.validate_key!(key)
49
+ return if key.is_a?(String) && key.bytesize >= KEY_LENGTH
50
+
51
+ raise ArgumentError, "Key must be a string of at least #{KEY_LENGTH} bytes"
52
+ end
53
+ end
54
+ end
55
+
56
+
@@ -7,6 +7,7 @@ module Vpago
7
7
  end
8
8
 
9
9
  def find_and_verify
10
+
10
11
  find_and_verify!
11
12
  rescue StandardError, ActiveRecord::RecordNotFound => e
12
13
  Rails.logger.error("PaymentJwtVerifier#find_and_verify error: #{e.class} - #{e.message}")
@@ -14,14 +15,29 @@ module Vpago
14
15
  end
15
16
 
16
17
  def find_and_verify!
17
- order = Spree::Order.find_by!(number: params_hash[:order_number])
18
- verify_jwt!(order)
19
18
 
20
- Spree::Payment.find_by!(number: params_hash[:payment_number])
19
+ if vattanac_mini_app_payload?
20
+ payload = Vpago::VattanacMiniAppDataHandler.new.decrypt_data(@params_hash[:data])
21
+ payment = Spree::Payment.find_by!(number: payload['paymentId'])
22
+ payment.update(transaction_response: payload)
23
+ payment
24
+ else
25
+ order = Spree::Order.find_by!(number: params_hash[:order_number])
26
+ verify_jwt!(order)
27
+
28
+ Spree::Payment.find_by!(number: params_hash[:payment_number])
29
+ end
21
30
  end
22
31
 
23
32
  def verify_jwt!(order)
24
33
  JWT.decode(params_hash[:order_jwt_token], order.token, 'HS256')
25
34
  end
35
+
36
+ def vattanac_mini_app_payload?
37
+ params_hash[:data].present?
38
+ end
39
+
26
40
  end
27
41
  end
42
+
43
+