bayarcash 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE +21 -0
- data/README.md +517 -0
- data/bayarcash.gemspec +32 -0
- data/lib/bayarcash/callback_verifications.rb +177 -0
- data/lib/bayarcash/checksum_generator.rb +110 -0
- data/lib/bayarcash/client.rb +293 -0
- data/lib/bayarcash/duit_now/dobw.rb +33 -0
- data/lib/bayarcash/errors.rb +49 -0
- data/lib/bayarcash/fpx.rb +26 -0
- data/lib/bayarcash/fpx_direct_debit.rb +75 -0
- data/lib/bayarcash/fpx_direct_debit_payment_intent.rb +70 -0
- data/lib/bayarcash/makes_http_requests.rb +143 -0
- data/lib/bayarcash/manual_bank_transfer.rb +224 -0
- data/lib/bayarcash/resources/fpx_bank_resource.rb +11 -0
- data/lib/bayarcash/resources/fpx_direct_debit_application_resource.rb +12 -0
- data/lib/bayarcash/resources/fpx_direct_debit_resource.rb +15 -0
- data/lib/bayarcash/resources/payment_intent_resource.rb +13 -0
- data/lib/bayarcash/resources/portal_resource.rb +14 -0
- data/lib/bayarcash/resources/resource.rb +90 -0
- data/lib/bayarcash/resources/transaction_resource.rb +14 -0
- data/lib/bayarcash/security_utils.rb +37 -0
- data/lib/bayarcash/util.rb +92 -0
- data/lib/bayarcash/version.rb +5 -0
- data/lib/bayarcash.rb +41 -0
- metadata +152 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bayarcash
|
|
4
|
+
# FPX Direct Debit constants: application types, payer id types, frequency modes,
|
|
5
|
+
# and mandate status codes, together with their human-readable labels.
|
|
6
|
+
class FpxDirectDebit
|
|
7
|
+
# Application type
|
|
8
|
+
ENROLMENT = "01"
|
|
9
|
+
MAINTENANCE = "02"
|
|
10
|
+
TERMINATION = "03"
|
|
11
|
+
|
|
12
|
+
# Buyer / payer ID type
|
|
13
|
+
NRIC = 1
|
|
14
|
+
OLD_IC = 2
|
|
15
|
+
PASSPORT = 3
|
|
16
|
+
BUSINESS_REGISTRATION = 4
|
|
17
|
+
OTHERS = 5
|
|
18
|
+
|
|
19
|
+
# Frequency mode
|
|
20
|
+
MODE_DAILY = "DL"
|
|
21
|
+
MODE_WEEKLY = "WK"
|
|
22
|
+
MODE_MONTHLY = "MT"
|
|
23
|
+
MODE_YEARLY = "YR"
|
|
24
|
+
|
|
25
|
+
# Status code
|
|
26
|
+
STATUS_NEW = 0
|
|
27
|
+
STATUS_WAITING_APPROVAL = 1
|
|
28
|
+
STATUS_FAILED_BANK_VERIFICATION = 2
|
|
29
|
+
STATUS_ACTIVE = 3
|
|
30
|
+
STATUS_TERMINATED = 4
|
|
31
|
+
STATUS_APPROVED = 5
|
|
32
|
+
STATUS_REJECTED = 6
|
|
33
|
+
STATUS_CANCELLED = 7
|
|
34
|
+
STATUS_ERROR = 8
|
|
35
|
+
|
|
36
|
+
STATUS_LABELS = {
|
|
37
|
+
STATUS_NEW => "New",
|
|
38
|
+
STATUS_WAITING_APPROVAL => "Waiting Approval",
|
|
39
|
+
STATUS_FAILED_BANK_VERIFICATION => "Bank Verification Failed",
|
|
40
|
+
STATUS_APPROVED => "Approved",
|
|
41
|
+
STATUS_REJECTED => "Rejected",
|
|
42
|
+
STATUS_CANCELLED => "Cancelled",
|
|
43
|
+
STATUS_ERROR => "Error",
|
|
44
|
+
STATUS_ACTIVE => "Active",
|
|
45
|
+
STATUS_TERMINATED => "Terminated"
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
# @param status_code [Integer]
|
|
49
|
+
# @return [String] label for the status, or "UNKNOWN STATUS"
|
|
50
|
+
def self.get_status_text(status_code)
|
|
51
|
+
STATUS_LABELS.fetch(status_code.to_i, "UNKNOWN STATUS")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param application_type [String] one of {ENROLMENT}, {MAINTENANCE}, {TERMINATION}
|
|
55
|
+
# @return [String, nil]
|
|
56
|
+
def self.get_application_type_text(application_type)
|
|
57
|
+
case application_type
|
|
58
|
+
when ENROLMENT then "Enrollment"
|
|
59
|
+
when MAINTENANCE then "Maintenance"
|
|
60
|
+
when TERMINATION then "Termination"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param frequency_mode_code [String] one of {MODE_DAILY}, {MODE_WEEKLY}, {MODE_MONTHLY}, {MODE_YEARLY}
|
|
65
|
+
# @return [String, nil]
|
|
66
|
+
def self.get_frequency_mode_text(frequency_mode_code)
|
|
67
|
+
case frequency_mode_code
|
|
68
|
+
when MODE_DAILY then "Daily"
|
|
69
|
+
when MODE_WEEKLY then "Weekly"
|
|
70
|
+
when MODE_MONTHLY then "Monthly"
|
|
71
|
+
when MODE_YEARLY then "Yearly"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bayarcash
|
|
4
|
+
# FPX Direct Debit mandate operations (enrolment, maintenance, termination) and
|
|
5
|
+
# mandate/transaction retrieval.
|
|
6
|
+
module FpxDirectDebitPaymentIntent
|
|
7
|
+
# Enrol a new direct-debit mandate.
|
|
8
|
+
#
|
|
9
|
+
# @param data [Hash]
|
|
10
|
+
# @return [Bayarcash::Resources::FpxDirectDebitApplicationResource]
|
|
11
|
+
def create_fpx_direct_debit_enrollment(data)
|
|
12
|
+
Bayarcash::Resources::FpxDirectDebitApplicationResource.new(
|
|
13
|
+
post("mandates", data),
|
|
14
|
+
self
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Maintain (update) an existing mandate.
|
|
19
|
+
#
|
|
20
|
+
# @param mandate_id [String]
|
|
21
|
+
# @param data [Hash]
|
|
22
|
+
# @return [Bayarcash::Resources::FpxDirectDebitApplicationResource]
|
|
23
|
+
def create_fpx_direct_debit_maintenance(mandate_id, data)
|
|
24
|
+
Bayarcash::Resources::FpxDirectDebitApplicationResource.new(
|
|
25
|
+
put("mandates/#{mandate_id}", data),
|
|
26
|
+
self
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Terminate an existing mandate.
|
|
31
|
+
#
|
|
32
|
+
# @param mandate_id [String]
|
|
33
|
+
# @param data [Hash]
|
|
34
|
+
# @return [Bayarcash::Resources::FpxDirectDebitApplicationResource]
|
|
35
|
+
def create_fpx_direct_debit_termination(mandate_id, data)
|
|
36
|
+
Bayarcash::Resources::FpxDirectDebitApplicationResource.new(
|
|
37
|
+
delete("mandates/#{mandate_id}", data),
|
|
38
|
+
self
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Retrieve a direct-debit mandate transaction.
|
|
43
|
+
#
|
|
44
|
+
# @param id [String]
|
|
45
|
+
# @return [Bayarcash::Resources::TransactionResource]
|
|
46
|
+
def get_fpx_direct_debit_transaction(id)
|
|
47
|
+
Bayarcash::Resources::TransactionResource.new(
|
|
48
|
+
get("mandates/transactions/#{id}"),
|
|
49
|
+
self
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Deprecated misspelled alias, kept for backward compatibility.
|
|
54
|
+
#
|
|
55
|
+
# @deprecated Use {#get_fpx_direct_debit_transaction} instead.
|
|
56
|
+
def getfpx_direct_debitransaction(id)
|
|
57
|
+
get_fpx_direct_debit_transaction(id)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Retrieve a direct-debit mandate.
|
|
61
|
+
#
|
|
62
|
+
# @param id [String]
|
|
63
|
+
# @return [Bayarcash::Resources::FpxDirectDebitResource]
|
|
64
|
+
def get_fpx_direct_debit(id)
|
|
65
|
+
Bayarcash::Resources::FpxDirectDebitResource.new(
|
|
66
|
+
get("mandates/#{id}")
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bayarcash
|
|
4
|
+
# HTTP transport for the Bayarcash API.
|
|
5
|
+
#
|
|
6
|
+
# Requests default to form-urlencoded bodies (matching the PHP SDK's Guzzle
|
|
7
|
+
# `form_params` behaviour). To send a JSON body, pass a payload shaped like
|
|
8
|
+
# `{ json: {...} }`. Non-2xx responses are mapped to typed errors.
|
|
9
|
+
module MakesHttpRequests
|
|
10
|
+
# @return [Faraday::Connection]
|
|
11
|
+
attr_reader :connection
|
|
12
|
+
|
|
13
|
+
# @param uri [String]
|
|
14
|
+
# @return [Object] decoded response
|
|
15
|
+
def get(uri)
|
|
16
|
+
request(:get, uri)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param uri [String]
|
|
20
|
+
# @param payload [Hash]
|
|
21
|
+
# @return [Object] decoded response
|
|
22
|
+
def post(uri, payload = {})
|
|
23
|
+
request(:post, uri, payload)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param uri [String]
|
|
27
|
+
# @param payload [Hash]
|
|
28
|
+
# @return [Object] decoded response
|
|
29
|
+
def put(uri, payload = {})
|
|
30
|
+
request(:put, uri, payload)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param uri [String]
|
|
34
|
+
# @param payload [Hash]
|
|
35
|
+
# @return [Object] decoded response
|
|
36
|
+
def delete(uri, payload = {})
|
|
37
|
+
request(:delete, uri, payload)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Retry a block until it returns a truthy value or the timeout elapses.
|
|
41
|
+
#
|
|
42
|
+
# @param timeout [Integer] seconds
|
|
43
|
+
# @param sleep_seconds [Integer]
|
|
44
|
+
# @yield the operation to retry
|
|
45
|
+
# @return [Object] the truthy result
|
|
46
|
+
# @raise [Bayarcash::TimeoutError] when the timeout elapses
|
|
47
|
+
def retry_until(timeout, sleep_seconds = 5)
|
|
48
|
+
start = Time.now.to_i
|
|
49
|
+
|
|
50
|
+
loop do
|
|
51
|
+
output = yield
|
|
52
|
+
return output if output
|
|
53
|
+
|
|
54
|
+
if Time.now.to_i - start < timeout
|
|
55
|
+
sleep(sleep_seconds)
|
|
56
|
+
else
|
|
57
|
+
output = [] if output.nil? || output == false
|
|
58
|
+
output = [output] unless output.is_a?(Array)
|
|
59
|
+
raise Bayarcash::TimeoutError.new(output)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
protected
|
|
65
|
+
|
|
66
|
+
# Build (or rebuild) the Faraday connection for the current base URI/token/timeout.
|
|
67
|
+
#
|
|
68
|
+
# @return [Faraday::Connection]
|
|
69
|
+
def build_connection
|
|
70
|
+
@connection = Faraday.new(url: base_uri) do |faraday|
|
|
71
|
+
faraday.headers["Authorization"] = "Bearer #{@token}"
|
|
72
|
+
faraday.headers["Accept"] = "application/json"
|
|
73
|
+
faraday.options.timeout = @timeout if @timeout
|
|
74
|
+
faraday.adapter Faraday.default_adapter
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Perform a request and decode the response, or map the error.
|
|
79
|
+
def request(verb, uri, payload = {})
|
|
80
|
+
payload ||= {}
|
|
81
|
+
|
|
82
|
+
response = connection.run_request(verb.to_s.downcase.to_sym, uri, nil, nil) do |req|
|
|
83
|
+
if json_payload?(payload)
|
|
84
|
+
req.headers["Content-Type"] = "application/json"
|
|
85
|
+
req.body = JSON.generate(payload[:json] || payload["json"])
|
|
86
|
+
elsif !payload.empty?
|
|
87
|
+
req.headers["Content-Type"] = "application/x-www-form-urlencoded"
|
|
88
|
+
req.body = Bayarcash::Util.build_query(payload)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
status = response.status
|
|
93
|
+
return handle_request_error(response) if status < 200 || status > 299
|
|
94
|
+
|
|
95
|
+
parse_body(response.body.to_s)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Map a non-2xx response to a typed error.
|
|
99
|
+
def handle_request_error(response)
|
|
100
|
+
body = response.body.to_s
|
|
101
|
+
|
|
102
|
+
case response.status
|
|
103
|
+
when 422
|
|
104
|
+
parsed = Bayarcash::Util.safe_json(body)
|
|
105
|
+
raise Bayarcash::ValidationError.new(parsed.is_a?(Hash) || parsed.is_a?(Array) ? parsed : {})
|
|
106
|
+
when 404
|
|
107
|
+
raise Bayarcash::NotFoundError.new
|
|
108
|
+
when 400
|
|
109
|
+
raise Bayarcash::FailedActionError, failed_action_message(body)
|
|
110
|
+
when 429
|
|
111
|
+
reset = response.headers["x-ratelimit-reset"]
|
|
112
|
+
raise Bayarcash::RateLimitExceededError.new(reset ? reset.to_i : nil)
|
|
113
|
+
else
|
|
114
|
+
raise Bayarcash::Error, body
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def json_payload?(payload)
|
|
121
|
+
payload.is_a?(Hash) && (payload.key?(:json) || payload.key?("json"))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def parse_body(body)
|
|
125
|
+
return body if body.nil? || body.empty?
|
|
126
|
+
|
|
127
|
+
parsed = Bayarcash::Util.safe_json(body)
|
|
128
|
+
return body if parsed.nil?
|
|
129
|
+
return body if Bayarcash::Util.php_falsy?(parsed)
|
|
130
|
+
|
|
131
|
+
parsed
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def failed_action_message(body)
|
|
135
|
+
decoded = Bayarcash::Util.safe_json(body)
|
|
136
|
+
return body unless decoded.is_a?(Hash)
|
|
137
|
+
|
|
138
|
+
message = decoded["message"] || decoded["error"] || body
|
|
139
|
+
message = JSON.generate(message) if message.is_a?(Hash) || message.is_a?(Array)
|
|
140
|
+
message
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bayarcash
|
|
4
|
+
# Manual (offline) bank transfer operations.
|
|
5
|
+
#
|
|
6
|
+
# These calls hit a different base URL than the versioned API (no /v2 or /v3
|
|
7
|
+
# segment) and support a multipart `proof_of_payment` file upload.
|
|
8
|
+
module ManualBankTransfer
|
|
9
|
+
REQUIRED_MANUAL_TRANSFER_FIELDS = %w[
|
|
10
|
+
portal_key buyer_name buyer_email order_amount order_no payment_gateway
|
|
11
|
+
merchant_bank_name merchant_bank_account merchant_bank_account_holder
|
|
12
|
+
bank_transfer_type bank_transfer_notes
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
FILE_CONTENT_TYPES = {
|
|
16
|
+
"jpg" => "image/jpeg",
|
|
17
|
+
"jpeg" => "image/jpeg",
|
|
18
|
+
"png" => "image/png",
|
|
19
|
+
"gif" => "image/gif",
|
|
20
|
+
"pdf" => "application/pdf"
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
# Create a manual bank transfer payment.
|
|
24
|
+
#
|
|
25
|
+
# @param data [Hash] payment and customer details (see README for the field list)
|
|
26
|
+
# @param allow_redirect [Boolean] whether to auto-follow HTTP redirects
|
|
27
|
+
# @return [Hash, String] response hash or raw string depending on the API response
|
|
28
|
+
# @raise [ArgumentError] when required fields are missing or invalid
|
|
29
|
+
# @raise [Bayarcash::Error] when the API request fails
|
|
30
|
+
def create_manual_bank_transfer(data, allow_redirect = false)
|
|
31
|
+
data = stringify_keys(data)
|
|
32
|
+
|
|
33
|
+
validate_manual_transfer_data(data)
|
|
34
|
+
|
|
35
|
+
data["bank_transfer_date"] ||= Date.today.strftime("%Y-%m-%d")
|
|
36
|
+
|
|
37
|
+
post_fields = prepare_manual_transfer_post_fields(data)
|
|
38
|
+
|
|
39
|
+
response = execute_manual_transfer_request(post_fields, allow_redirect)
|
|
40
|
+
|
|
41
|
+
process_manual_transfer_response(response[:body], response[:http_code], allow_redirect)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Update the status of an existing manual bank transfer.
|
|
45
|
+
#
|
|
46
|
+
# @param ref_no [String] transaction reference number
|
|
47
|
+
# @param status [String] new status code
|
|
48
|
+
# @param amount [String] transaction amount
|
|
49
|
+
# @return [Object] decoded API response
|
|
50
|
+
# @raise [Bayarcash::Error] when the update fails
|
|
51
|
+
def update_manual_bank_transfer_status(ref_no, status, amount)
|
|
52
|
+
data = { "ref_no" => ref_no, "status" => status, "amount" => amount }
|
|
53
|
+
|
|
54
|
+
conn = Faraday.new do |faraday|
|
|
55
|
+
faraday.request :url_encoded
|
|
56
|
+
faraday.adapter Faraday.default_adapter
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
url = "#{manual_transfer_base_url}/manual-bank-transfer/update-status"
|
|
60
|
+
|
|
61
|
+
begin
|
|
62
|
+
response = conn.post(url) do |req|
|
|
63
|
+
req.headers["Accept"] = "application/json"
|
|
64
|
+
req.headers["Authorization"] = "Bearer #{@token}"
|
|
65
|
+
req.headers["Content-Type"] = "application/x-www-form-urlencoded"
|
|
66
|
+
req.body = data
|
|
67
|
+
end
|
|
68
|
+
rescue Faraday::Error => e
|
|
69
|
+
raise Bayarcash::Error, "Connection failed: #{e.message}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
handle_api_response(response.body.to_s, response.status)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Extract structured data from an HTML form response.
|
|
76
|
+
#
|
|
77
|
+
# @param html_response [String]
|
|
78
|
+
# @return [Hash{String => String}]
|
|
79
|
+
def parse_manual_bank_transfer_response(html_response)
|
|
80
|
+
data = {}
|
|
81
|
+
|
|
82
|
+
if (form_id = html_response[/id="([^"]+)"/, 1])
|
|
83
|
+
data["form_id"] = form_id
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if (return_url = html_response[/action="([^"]+)"/, 1])
|
|
87
|
+
data["return_url"] = return_url
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
html_response.scan(/<input name="([^"]+)" type="hidden" value="([^"]*)">/).each do |name, value|
|
|
91
|
+
data[name] = value
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
data
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
protected
|
|
98
|
+
|
|
99
|
+
# The manual-transfer base URL (note: no /v2 or /v3 segment).
|
|
100
|
+
#
|
|
101
|
+
# @return [String]
|
|
102
|
+
def manual_transfer_base_url
|
|
103
|
+
@sandbox ? "https://console.bayarcash-sandbox.com/api" : "https://console.bayar.cash/api"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def validate_manual_transfer_data(data)
|
|
109
|
+
REQUIRED_MANUAL_TRANSFER_FIELDS.each do |field|
|
|
110
|
+
if data[field].nil?
|
|
111
|
+
raise ArgumentError, "Required field '#{field}' is missing"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
if data["payment_gateway"].nil? || data["payment_gateway"].to_s != "2"
|
|
116
|
+
raise ArgumentError, "Invalid payment gateway. Value must be 2 for manual bank transfers."
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
if data["proof_of_payment"] && !File.exist?(data["proof_of_payment"])
|
|
120
|
+
raise ArgumentError, "Proof of payment file does not exist"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def prepare_manual_transfer_post_fields(data)
|
|
125
|
+
post_fields = {}
|
|
126
|
+
|
|
127
|
+
data.each do |key, value|
|
|
128
|
+
post_fields[key] = value unless key == "proof_of_payment"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if data["proof_of_payment"] && File.exist?(data["proof_of_payment"])
|
|
132
|
+
path = data["proof_of_payment"]
|
|
133
|
+
post_fields["proof_of_payment"] = Faraday::Multipart::FilePart.new(
|
|
134
|
+
path,
|
|
135
|
+
file_content_type(path),
|
|
136
|
+
File.basename(path)
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
post_fields
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def execute_manual_transfer_request(post_fields, allow_redirect)
|
|
144
|
+
conn = Faraday.new do |faraday|
|
|
145
|
+
faraday.request :multipart
|
|
146
|
+
faraday.request :url_encoded
|
|
147
|
+
faraday.adapter Faraday.default_adapter
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
url = "#{manual_transfer_base_url}/manual-bank-transfer"
|
|
151
|
+
|
|
152
|
+
begin
|
|
153
|
+
response = conn.post(url) do |req|
|
|
154
|
+
req.headers["Accept"] = "application/json"
|
|
155
|
+
req.headers["Authorization"] = "Bearer #{@token}"
|
|
156
|
+
req.body = post_fields
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
redirects = 0
|
|
160
|
+
while allow_redirect && response.status >= 300 && response.status < 400 &&
|
|
161
|
+
response.headers["location"] && redirects < 10
|
|
162
|
+
response = Faraday.get(response.headers["location"])
|
|
163
|
+
redirects += 1
|
|
164
|
+
end
|
|
165
|
+
rescue Faraday::Error => e
|
|
166
|
+
raise Bayarcash::Error, "cURL Error: #{e.message}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
{ body: response.body.to_s, http_code: response.status }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def process_manual_transfer_response(response, http_code, allow_redirect)
|
|
173
|
+
if http_code >= 200 && http_code < 300
|
|
174
|
+
if response.include?("<form")
|
|
175
|
+
parsed = parse_manual_bank_transfer_response(response)
|
|
176
|
+
|
|
177
|
+
{
|
|
178
|
+
success: true,
|
|
179
|
+
html_form: response,
|
|
180
|
+
form_data: parsed,
|
|
181
|
+
return_url: parsed["return_url"]
|
|
182
|
+
}
|
|
183
|
+
else
|
|
184
|
+
decoded = Bayarcash::Util.safe_json(response)
|
|
185
|
+
decoded && !Bayarcash::Util.php_falsy?(decoded) ? decoded : response
|
|
186
|
+
end
|
|
187
|
+
elsif http_code >= 300 && http_code < 400 && !allow_redirect
|
|
188
|
+
{ redirect_url: response }
|
|
189
|
+
else
|
|
190
|
+
handle_api_error(response, http_code)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def handle_api_error(response, http_code)
|
|
195
|
+
decoded = Bayarcash::Util.safe_json(response)
|
|
196
|
+
|
|
197
|
+
if decoded.is_a?(Hash) && decoded.key?("message")
|
|
198
|
+
raise Bayarcash::Error, decoded["message"]
|
|
199
|
+
else
|
|
200
|
+
raise Bayarcash::Error, "API Error (HTTP #{http_code}): #{response[0, 200]}"
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def handle_api_response(response, http_code)
|
|
205
|
+
if http_code >= 200 && http_code < 300
|
|
206
|
+
decoded = Bayarcash::Util.safe_json(response)
|
|
207
|
+
decoded && !Bayarcash::Util.php_falsy?(decoded) ? decoded : response
|
|
208
|
+
else
|
|
209
|
+
handle_api_error(response, http_code)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def file_content_type(file_path)
|
|
214
|
+
extension = File.extname(file_path).delete(".").downcase
|
|
215
|
+
FILE_CONTENT_TYPES.fetch(extension, "application/octet-stream")
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def stringify_keys(data)
|
|
219
|
+
(data || {}).each_with_object({}) do |(key, value), memo|
|
|
220
|
+
memo[key.to_s] = value
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bayarcash
|
|
4
|
+
module Resources
|
|
5
|
+
# Returned by {Bayarcash::Client#fpx_banks_list}.
|
|
6
|
+
class FpxBankResource < Resource
|
|
7
|
+
attributes :bank_name, :bank_display_name, :bank_code, :bank_code_hashed,
|
|
8
|
+
:bank_availability
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bayarcash
|
|
4
|
+
module Resources
|
|
5
|
+
# Returned by direct-debit enrolment, maintenance and termination calls.
|
|
6
|
+
class FpxDirectDebitApplicationResource < Resource
|
|
7
|
+
attributes :payer_name, :payer_id_type, :payer_id, :payer_email,
|
|
8
|
+
:payer_telephone_number, :order_number, :amount, :application_type,
|
|
9
|
+
:application_reason, :frequency_mode, :effective_date, :expiry_date, :url
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bayarcash
|
|
4
|
+
module Resources
|
|
5
|
+
# Returned by {Bayarcash::Client#get_fpx_direct_debit} (a mandate).
|
|
6
|
+
class FpxDirectDebitResource < Resource
|
|
7
|
+
attributes :id, :updated_at, :mandate_reference_number, :order_number,
|
|
8
|
+
:application_reason, :frequency_mode, :frequency_mode_label,
|
|
9
|
+
:effective_date, :expiry_date, :currency, :amount, :payer_name,
|
|
10
|
+
:payer_id, :payer_id_type, :payer_bank_account_number, :payer_email,
|
|
11
|
+
:payer_telephone_number, :status, :status_description, :return_url,
|
|
12
|
+
:metadata, :portal, :merchant
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bayarcash
|
|
4
|
+
module Resources
|
|
5
|
+
# Returned by {Bayarcash::Client#create_payment_intent} and
|
|
6
|
+
# {Bayarcash::Client#get_payment_intent}.
|
|
7
|
+
class PaymentIntentResource < Resource
|
|
8
|
+
attributes :payer_name, :payer_email, :payer_telephone_number, :order_number,
|
|
9
|
+
:amount, :url, :type, :id, :status, :last_attempt, :paid_at,
|
|
10
|
+
:currency, :attempts
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bayarcash
|
|
4
|
+
module Resources
|
|
5
|
+
# Returned by {Bayarcash::Client#get_portals}.
|
|
6
|
+
class PortalResource < Resource
|
|
7
|
+
attributes :id, :created_at, :portal_key, :portal_name, :website_url,
|
|
8
|
+
:transaction_notification_email, :secondary_transaction_notification_email,
|
|
9
|
+
:custom_payment_button_text, :enabled_sms_on_successful_transaction,
|
|
10
|
+
:split_payment_enabled, :split_payment_merchants, :payment_channels,
|
|
11
|
+
:merchant, :url, :merchant_id
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bayarcash
|
|
4
|
+
module Resources
|
|
5
|
+
# Base data-transfer object for API responses.
|
|
6
|
+
#
|
|
7
|
+
# Attributes are populated from the (snake_case) keys of the API payload and
|
|
8
|
+
# exposed as reader methods. Declared attributes always respond (returning nil
|
|
9
|
+
# when the API omits them); any unknown field the API returns is captured too,
|
|
10
|
+
# so the DTOs are tolerant of new or missing fields.
|
|
11
|
+
class Resource
|
|
12
|
+
# Declare known attributes for a resource subclass. Each becomes a reader that
|
|
13
|
+
# returns nil when absent.
|
|
14
|
+
#
|
|
15
|
+
# @param names [Array<Symbol, String>]
|
|
16
|
+
def self.attributes(*names)
|
|
17
|
+
@declared_attributes ||= []
|
|
18
|
+
names.each do |name|
|
|
19
|
+
key = name.to_s
|
|
20
|
+
@declared_attributes << key unless @declared_attributes.include?(key)
|
|
21
|
+
define_method(key) { @attributes[key] }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [Array<String>] declared attribute names, including inherited ones
|
|
26
|
+
def self.declared_attributes
|
|
27
|
+
inherited = superclass.respond_to?(:declared_attributes) ? superclass.declared_attributes : []
|
|
28
|
+
inherited + (@declared_attributes || [])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param attributes [Hash] the API payload
|
|
32
|
+
# @param bayarcash [Bayarcash::Client, nil] owning client (never serialized)
|
|
33
|
+
def initialize(attributes = {}, bayarcash = nil)
|
|
34
|
+
@bayarcash = bayarcash
|
|
35
|
+
@attributes = {}
|
|
36
|
+
fill(attributes)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Read a raw attribute by key (string or symbol).
|
|
40
|
+
#
|
|
41
|
+
# @param key [String, Symbol]
|
|
42
|
+
# @return [Object, nil]
|
|
43
|
+
def [](key)
|
|
44
|
+
@attributes[key.to_s]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [Hash{String => Object}] the attributes, with nested resources converted
|
|
48
|
+
def to_h
|
|
49
|
+
@attributes.each_with_object({}) do |(key, value), memo|
|
|
50
|
+
memo[key] = deep_to_h(value)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
alias to_array to_h
|
|
54
|
+
alias to_hash to_h
|
|
55
|
+
|
|
56
|
+
def respond_to_missing?(name, include_private = false)
|
|
57
|
+
key = name.to_s
|
|
58
|
+
@attributes.key?(key) || self.class.declared_attributes.include?(key) || super
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def method_missing(name, *args)
|
|
62
|
+
key = name.to_s
|
|
63
|
+
if @attributes.key?(key) || self.class.declared_attributes.include?(key)
|
|
64
|
+
@attributes[key]
|
|
65
|
+
else
|
|
66
|
+
super
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def fill(attributes)
|
|
73
|
+
(attributes || {}).each do |key, value|
|
|
74
|
+
@attributes[key.to_s] = value
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def deep_to_h(value)
|
|
79
|
+
case value
|
|
80
|
+
when Resource
|
|
81
|
+
value.to_h
|
|
82
|
+
when Array
|
|
83
|
+
value.map { |item| item.is_a?(Resource) ? item.to_h : item }
|
|
84
|
+
else
|
|
85
|
+
value
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bayarcash
|
|
4
|
+
module Resources
|
|
5
|
+
# Returned by transaction lookups and queries.
|
|
6
|
+
class TransactionResource < Resource
|
|
7
|
+
attributes :id, :updated_at, :created_at, :datetime, :payer_name, :payer_email,
|
|
8
|
+
:payer_telephone_number, :order_number, :currency, :amount,
|
|
9
|
+
:exchange_reference_number, :exchange_transaction_id, :payer_bank_name,
|
|
10
|
+
:status, :status_description, :return_url, :metadata, :payout,
|
|
11
|
+
:payment_gateway, :portal, :merchant, :mandate
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|