parsbank 0.0.5 → 0.0.8

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.
@@ -0,0 +1,134 @@
1
+ module Parsbank
2
+ class TransactionRequest
3
+ def self.create(args = {})
4
+ bank = fetch_bank(args)
5
+ validate_bank(bank)
6
+
7
+ description = args.fetch(:description)
8
+ callback_url = generate_callback_url(bank, description)
9
+
10
+ crypto_amount, fiat_amount, real_amount = fetch_amounts(args)
11
+ validate_amounts(bank, crypto_amount, fiat_amount, real_amount)
12
+
13
+ transaction = initialize_transaction(args, description, fiat_amount, crypto_amount, bank, callback_url)
14
+ process_gateway(bank, transaction, description, callback_url, crypto_amount, fiat_amount, args)
15
+
16
+ { transaction: transaction, html_form: @result }
17
+ end
18
+
19
+ def self.fetch_bank(args)
20
+ args.fetch(:bank, Parsbank.available_gateways_list.keys.sample)
21
+ end
22
+
23
+ def self.validate_bank(bank)
24
+ selected_bank = Parsbank.available_gateways_list[bank]
25
+ raise "Bank not enabled or not exists in #{Parsbank.configuration.secrets_path}: #{bank}" unless selected_bank
26
+ end
27
+
28
+ def self.generate_callback_url(bank, _description)
29
+ selected_bank = Parsbank.available_gateways_list[bank]
30
+ "#{selected_bank['callback_url'] || Parsbank.configuration.callback_url}&bank_name=#{bank}"
31
+ end
32
+
33
+ def self.fetch_amounts(args)
34
+ [
35
+ args.fetch(:crypto_amount, nil),
36
+ args.fetch(:fiat_amount, nil),
37
+ args.fetch(:real_amount, nil)
38
+ ]
39
+ end
40
+
41
+ def self.validate_amounts(bank, crypto_amount, fiat_amount, real_amount)
42
+ raise 'Amount fields are empty: crypto_amount OR fiat_amount OR real_amount' if [crypto_amount, fiat_amount,
43
+ real_amount].all?(&:nil?)
44
+
45
+ tags = Parsbank.supported_psp[bank]['tags']
46
+ if tags.include?('crypto') && crypto_amount.nil? && real_amount.nil?
47
+ raise "#{bank} needs crypto_amount or real_amount"
48
+ end
49
+
50
+ return unless tags.include?('rial') && fiat_amount.nil? && real_amount.nil?
51
+
52
+ raise "#{bank} needs fiat_amount or real_amount"
53
+ end
54
+
55
+ def self.initialize_transaction(args, description, fiat_amount, crypto_amount, bank, callback_url)
56
+ model_class = Parsbank.configuration.model || 'Transaction'
57
+ @transaction= Object.const_get(model_class).new(
58
+ description: description,
59
+ amount: fiat_amount || crypto_amount,
60
+ gateway: bank,
61
+ callback_url: callback_url,
62
+ status: 'start',
63
+ user_id: args.fetch(:user_id, nil),
64
+ cart_id: args.fetch(:cart_id, nil),
65
+ local_id: args.fetch(:local_id, nil),
66
+ ip: args.fetch(:ip, nil)
67
+ )
68
+ @transaction.save
69
+
70
+ @transaction
71
+ end
72
+
73
+ def self.process_gateway(bank, transaction, description, callback_url, crypto_amount, fiat_amount, args)
74
+ case bank
75
+ when 'mellat'
76
+ process_mellat(transaction, description, callback_url, fiat_amount)
77
+ when 'zarinpal'
78
+ process_zarinpal(transaction, description, callback_url, fiat_amount)
79
+ when 'zibal'
80
+ process_zibal(description, callback_url, fiat_amount)
81
+ when 'bscbitcoin'
82
+ process_bscbitcoin(transaction, description, crypto_amount, args)
83
+ else
84
+ raise "Unsupported gateway: #{bank}"
85
+ end
86
+ end
87
+
88
+ def self.process_mellat(transaction, description, callback_url, fiat_amount)
89
+ mellat = Parsbank::Mellat.new(
90
+ amount: fiat_amount,
91
+ additional_data: description,
92
+ callback_url: callback_url,
93
+ orderId: transaction.id
94
+ )
95
+ mellat.call
96
+ transaction.update!(gateway_response: mellat.response, unit: 'irr')
97
+ @result = mellat.redirect_form
98
+ end
99
+
100
+ def self.process_zarinpal(transaction, description, callback_url, fiat_amount)
101
+ zarinpal = Parsbank::Zarinpal.new(
102
+ amount: fiat_amount,
103
+ additional_data: description,
104
+ callback_url: callback_url
105
+ )
106
+ zarinpal.call
107
+ transaction.update!(
108
+ gateway_response: zarinpal.response,
109
+ track_id: zarinpal.ref_id,
110
+ unit: 'irt'
111
+ )
112
+ @result = zarinpal.redirect_form
113
+ end
114
+
115
+ def self.process_zibal(description, callback_url, fiat_amount)
116
+ zibal = Parsbank::Zibal.new(
117
+ amount: fiat_amount,
118
+ additional_data: description,
119
+ callback_url: callback_url
120
+ )
121
+ zibal.call
122
+ @result = zibal.redirect_form
123
+ end
124
+
125
+ def self.process_bscbitcoin(transaction, description, crypto_amount, args)
126
+ bscbitcoin = Parsbank::BscBitcoin.new(
127
+ additional_data: description
128
+ )
129
+ convert_real_amount_to_assets if crypto_amount.nil? && args.key?(:real_amount)
130
+ @result = bscbitcoin.generate_payment_address(amount: crypto_amount)
131
+ transaction.update!(gateway_response: @result, unit: 'bitcoin')
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsbank
4
+ class TransactionVerify
5
+ def self.verify(args = {})
6
+ validate_params(args.params)
7
+
8
+ transaction = initialize_transaction(args, description, fiat_amount, crypto_amount, bank, callback_url)
9
+ process_gateway(bank, transaction, description, callback_url, crypto_amount, fiat_amount, args)
10
+
11
+ { transaction: transaction, html_form: @result }
12
+ end
13
+
14
+ def self.validate_params(params)
15
+ model_class = Parsbank.configuration.model || 'Transaction'
16
+ tack_transaction = Object.const_get(model_class).find(params[:transaction_id].to_i)
17
+ { status: 404, message: I18n.t('model.not_found') } unless tack_transaction.present?
18
+ end
19
+
20
+ def self.fetch_bank(args)
21
+ args.fetch(:bank)
22
+ end
23
+
24
+ def self.validate_bank(bank)
25
+ selected_bank = Parsbank.available_gateways_list[bank]
26
+ raise "Bank not enabled or not exists in #{Parsbank.configuration.secrets_path}: #{bank}" unless selected_bank
27
+ end
28
+
29
+ def self.generate_callback_url(bank, _description)
30
+ selected_bank = Parsbank.available_gateways_list[bank]
31
+ "#{selected_bank['callback_url'] || Parsbank.configuration.callback_url}&bank_name=#{bank}"
32
+ end
33
+
34
+ def self.fetch_amounts(args)
35
+ [
36
+ args.fetch(:crypto_amount, nil),
37
+ args.fetch(:fiat_amount, nil),
38
+ args.fetch(:real_amount, nil)
39
+ ]
40
+ end
41
+
42
+ def self.validate_amounts(bank, crypto_amount, fiat_amount, real_amount)
43
+ raise 'Amount fields are empty: crypto_amount OR fiat_amount OR real_amount' if [crypto_amount, fiat_amount,
44
+ real_amount].all?(&:nil?)
45
+
46
+ tags = $SUPPORTED_PSP[bank]['tags']
47
+ if tags.include?('crypto') && crypto_amount.nil? && real_amount.nil?
48
+ raise "#{bank} needs crypto_amount or real_amount"
49
+ end
50
+
51
+ return unless tags.include?('rial') && fiat_amount.nil? && real_amount.nil?
52
+
53
+ raise "#{bank} needs fiat_amount or real_amount"
54
+ end
55
+
56
+ def self.initialize_transaction(args, description, fiat_amount, crypto_amount, bank, callback_url)
57
+ model_class = Parsbank.configuration.model || 'Transaction'
58
+ @transaction = Object.const_get(model_class).new(
59
+ description: description,
60
+ amount: fiat_amount || crypto_amount,
61
+ gateway: bank,
62
+ callback_url: callback_url,
63
+ status: 'start',
64
+ user_id: args.fetch(:user_id, nil),
65
+ cart_id: args.fetch(:cart_id, nil),
66
+ local_id: args.fetch(:local_id, nil),
67
+ ip: args.fetch(:ip, nil)
68
+ )
69
+ @transaction.save
70
+
71
+ @transaction
72
+ end
73
+
74
+ def self.process_gateway(bank, transaction, description, callback_url, crypto_amount, fiat_amount, args)
75
+ case bank
76
+ when 'mellat'
77
+ process_mellat(transaction, description, callback_url, fiat_amount)
78
+ when 'zarinpal'
79
+ process_zarinpal(transaction, description, callback_url, fiat_amount)
80
+ when 'zibal'
81
+ process_zibal(description, callback_url, fiat_amount)
82
+ when 'bscbitcoin'
83
+ process_bscbitcoin(transaction, description, crypto_amount, args)
84
+ else
85
+ raise "Unsupported gateway: #{bank}"
86
+ end
87
+ end
88
+
89
+ def self.process_mellat(transaction, description, callback_url, fiat_amount)
90
+ mellat = Parsbank::Mellat.new(
91
+ amount: fiat_amount,
92
+ additional_data: description,
93
+ callback_url: callback_url,
94
+ orderId: transaction.id
95
+ )
96
+ mellat.call
97
+ transaction.update!(gateway_response: mellat.response, unit: 'irr')
98
+ @result = mellat.redirect_form
99
+ end
100
+
101
+ def self.process_zarinpal(transaction, description, callback_url, fiat_amount)
102
+ zarinpal = Parsbank::Zarinpal.new(
103
+ amount: fiat_amount,
104
+ additional_data: description,
105
+ callback_url: callback_url
106
+ )
107
+ zarinpal.call
108
+ transaction.update!(
109
+ gateway_response: zarinpal.response,
110
+ track_id: zarinpal.ref_id,
111
+ unit: 'irt'
112
+ )
113
+ @result = zarinpal.redirect_form
114
+ end
115
+
116
+ def self.process_zibal(description, callback_url, fiat_amount)
117
+ zibal = Parsbank::Zibal.new(
118
+ amount: fiat_amount,
119
+ additional_data: description,
120
+ callback_url: callback_url
121
+ )
122
+ zibal.call
123
+ @result = zibal.redirect_form
124
+ end
125
+
126
+ def self.process_bscbitcoin(transaction, description, crypto_amount, args)
127
+ bscbitcoin = Parsbank::BscBitcoin.new(
128
+ additional_data: description
129
+ )
130
+ convert_real_amount_to_assets if crypto_amount.nil? && args.key?(:real_amount)
131
+ @result = bscbitcoin.generate_payment_address(amount: crypto_amount)
132
+ transaction.update!(gateway_response: @result, unit: 'bitcoin')
133
+ end
134
+ end
135
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsbank
4
- VERSION = '0.0.5'
4
+ VERSION = '0.0.8'
5
5
  end
@@ -1,4 +1,4 @@
1
- <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1575 350">
1
+ <svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" viewBox="0 0 1575 350">
2
2
  <defs>
3
3
  <style>
4
4
  .cls-1 {
@@ -1,32 +1,23 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Parsbank
2
- class Zarinpal
3
- attr_accessor :amount, :description, :email, :mobile, :merchant_id
4
+ class Zarinpal < Gates
5
+ attr_accessor :amount, :description, :email, :mobile, :merchant_id, :wsdl
4
6
  attr_reader :response, :status, :status_message, :ref_id, :logo
5
7
 
6
8
  def initialize(args = {})
7
- @mobile = args.fetch(:mobile, nil)
8
- @email = args.fetch(:email, nil)
9
+ @mobile = args.fetch(:mobile, '')
10
+ @email = args.fetch(:email, '')
9
11
  @amount = args.fetch(:amount)
10
12
  @description = args.fetch(:description, ' ')
11
13
  @callback_url = args.fetch(:callback_url,
12
14
  (default_config(:callback_url) || Parsbank.configuration.callback_url))
13
15
  @merchant_id = args.fetch(:merchant_id, default_config(:merchant_id))
14
- @wsdl = create_wsdl_client
16
+ @wsdl = create_rest_client
15
17
  rescue KeyError => e
16
18
  raise ArgumentError, "Missing required argument: #{e.message}"
17
19
  end
18
20
 
19
- def self.logo
20
- file_path = "#{__dir__}/logo.svg"
21
- return [404, { 'Content-Type' => 'text/plain' }, ['File not found']] unless File.exist?(file_path)
22
-
23
- [
24
- 200,
25
- { 'Content-Type' => 'image/svg+xml' },
26
- File.open(file_path, 'r')
27
- ]
28
- end
29
-
30
21
  def validate(response = nil)
31
22
  @response = response[:payment_request_response] || response[:payment_verification_response] || response
32
23
  @ref_id = @response[:authority]
@@ -45,61 +36,99 @@ module Parsbank
45
36
  end
46
37
 
47
38
  def call
48
- response = @wsdl.call(:payment_request, message: build_request_message)
49
- validate(response.body)
50
- rescue Savon::Error => e
51
- raise "SOAP request failed: #{e.message}"
39
+ response = @wsdl.call
40
+
41
+ Rails.logger.info "Received response with status: #{response.status}, body: #{response.body.inspect}"
42
+
43
+ if response.valid?
44
+ validate(response.body)
45
+ else
46
+ @valid = false
47
+ Rails.logger.error "POST request to #{BASE_URL}/#{endpoint} failed with status: #{response.status}, error: #{response.body.inspect}"
48
+ raise "API request failed with status #{response.status}: #{response.body}"
49
+ end
50
+ rescue Faraday::ConnectionFailed => e
51
+ Rails.logger.error "Connection failed: #{e.message}"
52
+ raise "Connection to API failed: #{e.message}"
53
+ rescue Faraday::TimeoutError => e
54
+ Rails.logger.error "Request timed out: #{e.message}"
55
+ raise "API request timed out: #{e.message}"
56
+ rescue StandardError => e
57
+ Rails.logger.error "An error occurred: #{e.message}"
58
+ raise "An unexpected error occurred: #{e.message}"
59
+
60
+ JSON.parse response.body
61
+
62
+ if response[:success]
63
+ validate(response[:body])
64
+ else
65
+ { status: :nok, message: begin
66
+ response[:error]
67
+ rescue StandardError
68
+ response
69
+ end }
70
+ end
52
71
  end
53
72
 
54
- def redirect_form
55
- "
73
+ def redirect_form(ref_id)
74
+ javascript_tag = <<-JS
56
75
  <script type='text/javascript' charset='utf-8'>
57
- function postRefId (refIdValue) {
58
- var form = document.createElement('form');
59
- form.setAttribute('method', 'POST');
60
- form.setAttribute('action', 'https://www.zarinpal.com/pg/StartPay/#{ref_id}');
61
- form.setAttribute('target', '_self');
62
- var hiddenField = document.createElement('input');
63
- hiddenField.setAttribute('name', 'RefId');
64
- hiddenField.setAttribute('value', refIdValue);
65
- form.appendChild(hiddenField);
66
- document.body.appendChild(form);
67
- form.submit();
68
- document.body.removeChild(form);
69
- }
70
-
71
-
72
- postRefId('#{ref_id}') %>')
73
- </script>
74
- "
76
+ function postRefId(refIdValue) {
77
+ var form = document.createElement('form');
78
+ form.setAttribute('method', 'POST');
79
+ form.setAttribute('action', 'https://www.zarinpal.com/pg/StartPay/' + refIdValue);
80
+ form.setAttribute('target', '_self');
81
+ var hiddenField = document.createElement('input');
82
+ hiddenField.setAttribute('name', 'RefId');
83
+ hiddenField.setAttribute('value', refIdValue);
84
+ form.appendChild(hiddenField);
85
+ document.body.appendChild(form);
86
+ form.submit();
87
+ document.body.removeChild(form);
88
+ }
89
+ postRefId('#{ref_id}');
90
+ </script>
91
+ JS
92
+
93
+ redirect_loaders do
94
+ "#{javascript_tag}#{I18n.t('actions.redirect_to_gate')}"
95
+ end
75
96
  end
76
97
 
77
98
  private
78
99
 
79
- def default_config(key)
80
- Parsbank.load_secrets_yaml[self.class.name.split('::').last.downcase][key.to_s]
81
- end
82
-
83
- def create_wsdl_client
84
- Savon.client(
85
- wsdl: default_config(:wsdl) || 'https://de.zarinpal.com/pg/services/WebGate/wsdl',
86
- pretty_print_xml: (Parsbank.configuration.debug ? true : false),
87
- namespace: 'http://interfaces.core.sw.bps.com/',
88
- log: (Parsbank.configuration.debug ? true : false),
89
- logger: Rails.logger,
90
- log_level: (Parsbank.configuration.debug ? :debug : :fatal)
100
+ def create_rest_client
101
+ Parsbank::Restfull.new(
102
+ endpoint: default_config(:endpoint) || 'https://payment.zarinpal.com/pg/v4/payment',
103
+ action: 'request.json',
104
+ headers: {
105
+ 'Content-Type' => 'application/json'
106
+ },
107
+ request_message: build_request_message,
108
+ http_method: :post,
109
+ response_type: :json
91
110
  )
111
+
112
+
92
113
  end
93
114
 
115
+ def deep_clean(hash)
116
+ hash.reject do |_, value|
117
+ (value.is_a?(Hash) && deep_clean(value).empty?) || value.nil? || value == ""
118
+ end.transform_values do |v|
119
+ v.is_a?(Hash) ? deep_clean(v) : v
120
+ end
121
+
122
+ end
123
+
94
124
  def build_request_message
95
- {
96
- 'MerchantID' => @merchant_id,
97
- 'Mobile' => @mobile,
98
- 'Email' => @email,
99
- 'Amount' => @amount,
100
- 'Description' => @description,
101
- 'CallbackURL' => @callback_url
102
- }
125
+ deep_clean({
126
+ 'merchant_id' => @merchant_id,
127
+ 'metadata' => {'mobile' => @mobile, 'email' => @email,'order_id'=> ''},
128
+ 'amount' => @amount,
129
+ 'description' => @description,
130
+ 'callback_url' => @callback_url
131
+ })
103
132
  end
104
133
 
105
134
  def perform_validation
@@ -3,7 +3,7 @@
3
3
  require 'faraday'
4
4
  require 'faraday_middleware'
5
5
  module Parsbank
6
- class Zibal
6
+ class Zibal < Gates
7
7
  attr_accessor :amount, :description, :email, :mobile, :merchant, :callbackUrl, :orderId, :allowedCards, :ledgerId,
8
8
  :nationalCode, :checkMobileWithCard
9
9
 
@@ -25,17 +25,6 @@ module Parsbank
25
25
  raise ArgumentError, "Missing required argument: #{e.message}"
26
26
  end
27
27
 
28
- def self.logo
29
- file_path = "#{__dir__}/logo.svg"
30
- return [404, { 'Content-Type' => 'text/plain' }, ['File not found']] unless File.exist?(file_path)
31
-
32
- [
33
- 200,
34
- { 'Content-Type' => 'image/svg+xml' },
35
- File.open(file_path, 'r')
36
- ]
37
- end
38
-
39
28
  def validate(response = nil)
40
29
  @response = response
41
30
  @ref_id = @response['trackId']
@@ -55,8 +44,6 @@ module Parsbank
55
44
 
56
45
  def call
57
46
  create_rest_client
58
- rescue Savon::Error => e
59
- raise "SOAP request failed: #{e.message}"
60
47
  end
61
48
 
62
49
  def redirect_form
@@ -84,10 +71,6 @@ module Parsbank
84
71
 
85
72
  private
86
73
 
87
- def default_config(key)
88
- Parsbank.load_secrets_yaml[self.class.name.split('::').last.downcase][key.to_s]
89
- end
90
-
91
74
  def create_rest_client
92
75
  connection = Parsbank::Restfull.new(
93
76
  endpoint: default_config(:endpoint) || 'https://gateway.zibal.ir',