dpay 0.1.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 (137) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +29 -0
  3. data/LICENSE +201 -0
  4. data/README.md +191 -0
  5. data/lib/dpay/bank/bank.rb +28 -0
  6. data/lib/dpay/bank/bank_service.rb +28 -0
  7. data/lib/dpay/blik/blik_alias.rb +27 -0
  8. data/lib/dpay/blik/blik_alias_registration.rb +21 -0
  9. data/lib/dpay/blik/blik_alias_type.rb +18 -0
  10. data/lib/dpay/blik/blik_app.rb +17 -0
  11. data/lib/dpay/blik/blik_recurring_registration.rb +74 -0
  12. data/lib/dpay/blik/blik_recurring_registration_info.rb +26 -0
  13. data/lib/dpay/blik/blik_recurring_status.rb +28 -0
  14. data/lib/dpay/blik/blik_service.rb +51 -0
  15. data/lib/dpay/card/apple_pay_request.rb +38 -0
  16. data/lib/dpay/card/card_data.rb +30 -0
  17. data/lib/dpay/card/card_encryptor.rb +27 -0
  18. data/lib/dpay/card/card_payment_request.rb +59 -0
  19. data/lib/dpay/card/card_payment_result.rb +58 -0
  20. data/lib/dpay/card/card_recurring_frequency.rb +23 -0
  21. data/lib/dpay/card/card_recurring_operation.rb +19 -0
  22. data/lib/dpay/card/card_recurring_registration.rb +69 -0
  23. data/lib/dpay/card/card_service.rb +47 -0
  24. data/lib/dpay/card/dcc_decision.rb +18 -0
  25. data/lib/dpay/card/dcc_markup.rb +18 -0
  26. data/lib/dpay/card/dcc_offer.rb +36 -0
  27. data/lib/dpay/card/google_pay_request.rb +35 -0
  28. data/lib/dpay/card/redirect_type.rb +20 -0
  29. data/lib/dpay/client.rb +26 -0
  30. data/lib/dpay/config.rb +39 -0
  31. data/lib/dpay/currency.rb +19 -0
  32. data/lib/dpay/errors.rb +77 -0
  33. data/lib/dpay/http/client.rb +11 -0
  34. data/lib/dpay/http/net_http_client.rb +52 -0
  35. data/lib/dpay/http/request.rb +17 -0
  36. data/lib/dpay/http/response.rb +29 -0
  37. data/lib/dpay/internal/api_requestor.rb +65 -0
  38. data/lib/dpay/internal/base_urls.rb +38 -0
  39. data/lib/dpay/internal/checksum_calculator.rb +26 -0
  40. data/lib/dpay/internal/coerce.rb +46 -0
  41. data/lib/dpay/internal/error_mapper.rb +90 -0
  42. data/lib/dpay/internal/php.rb +71 -0
  43. data/lib/dpay/ipn/ipn_event.rb +84 -0
  44. data/lib/dpay/ipn/ipn_type.rb +19 -0
  45. data/lib/dpay/ipn/ipn_verifier.rb +57 -0
  46. data/lib/dpay/money.rb +85 -0
  47. data/lib/dpay/payment/device_info.rb +42 -0
  48. data/lib/dpay/payment/invoice_details.rb +48 -0
  49. data/lib/dpay/payment/payer.rb +26 -0
  50. data/lib/dpay/payment/payment_service.rb +35 -0
  51. data/lib/dpay/payment/payout_fee_mode.rb +18 -0
  52. data/lib/dpay/payment/payout_instruction.rb +26 -0
  53. data/lib/dpay/payment/payout_position.rb +23 -0
  54. data/lib/dpay/payment/register_payment_request.rb +246 -0
  55. data/lib/dpay/payment/registered_payment.rb +42 -0
  56. data/lib/dpay/payment/return_urls.rb +20 -0
  57. data/lib/dpay/payment/transaction.rb +54 -0
  58. data/lib/dpay/payment/transaction_refund.rb +21 -0
  59. data/lib/dpay/payment/transaction_status.rb +21 -0
  60. data/lib/dpay/payment/transaction_type.rb +23 -0
  61. data/lib/dpay/payout/payout_details.rb +50 -0
  62. data/lib/dpay/payout/payout_receiver.rb +22 -0
  63. data/lib/dpay/payout/payout_service.rb +23 -0
  64. data/lib/dpay/refund/refund.rb +22 -0
  65. data/lib/dpay/refund/refund_availability.rb +23 -0
  66. data/lib/dpay/refund/refund_service.rb +46 -0
  67. data/lib/dpay/testing/mock_http_client.rb +51 -0
  68. data/lib/dpay/testing.rb +4 -0
  69. data/lib/dpay/version.rb +5 -0
  70. data/lib/dpay.rb +68 -0
  71. data/sig/dpay/bank/bank.rbs +17 -0
  72. data/sig/dpay/bank/bank_service.rbs +13 -0
  73. data/sig/dpay/blik/blik_alias.rbs +17 -0
  74. data/sig/dpay/blik/blik_alias_registration.rbs +6 -0
  75. data/sig/dpay/blik/blik_alias_type.rbs +11 -0
  76. data/sig/dpay/blik/blik_app.rbs +10 -0
  77. data/sig/dpay/blik/blik_recurring_registration.rbs +24 -0
  78. data/sig/dpay/blik/blik_recurring_registration_info.rbs +17 -0
  79. data/sig/dpay/blik/blik_recurring_status.rbs +17 -0
  80. data/sig/dpay/blik/blik_service.rbs +13 -0
  81. data/sig/dpay/card/apple_pay_request.rbs +13 -0
  82. data/sig/dpay/card/card_data.rbs +15 -0
  83. data/sig/dpay/card/card_encryptor.rbs +9 -0
  84. data/sig/dpay/card/card_payment_request.rbs +17 -0
  85. data/sig/dpay/card/card_payment_result.rbs +22 -0
  86. data/sig/dpay/card/card_recurring_frequency.rbs +16 -0
  87. data/sig/dpay/card/card_recurring_operation.rbs +12 -0
  88. data/sig/dpay/card/card_recurring_registration.rbs +22 -0
  89. data/sig/dpay/card/card_service.rbs +16 -0
  90. data/sig/dpay/card/dcc_decision.rbs +11 -0
  91. data/sig/dpay/card/dcc_markup.rbs +10 -0
  92. data/sig/dpay/card/dcc_offer.rbs +21 -0
  93. data/sig/dpay/card/google_pay_request.rbs +12 -0
  94. data/sig/dpay/card/redirect_type.rbs +13 -0
  95. data/sig/dpay/client.rbs +21 -0
  96. data/sig/dpay/config.rbs +25 -0
  97. data/sig/dpay/currency.rbs +11 -0
  98. data/sig/dpay/errors.rbs +86 -0
  99. data/sig/dpay/http/client.rbs +7 -0
  100. data/sig/dpay/http/net_http_client.rbs +15 -0
  101. data/sig/dpay/http/request.rbs +12 -0
  102. data/sig/dpay/http/response.rbs +15 -0
  103. data/sig/dpay/internal/api_requestor.rbs +23 -0
  104. data/sig/dpay/internal/base_urls.rbs +13 -0
  105. data/sig/dpay/internal/checksum_calculator.rbs +9 -0
  106. data/sig/dpay/internal/coerce.rbs +12 -0
  107. data/sig/dpay/internal/error_mapper.rbs +16 -0
  108. data/sig/dpay/internal/php.rbs +16 -0
  109. data/sig/dpay/ipn/ipn_event.rbs +28 -0
  110. data/sig/dpay/ipn/ipn_type.rbs +12 -0
  111. data/sig/dpay/ipn/ipn_verifier.rbs +11 -0
  112. data/sig/dpay/money.rbs +25 -0
  113. data/sig/dpay/payment/device_info.rbs +25 -0
  114. data/sig/dpay/payment/invoice_details.rbs +17 -0
  115. data/sig/dpay/payment/payer.rbs +14 -0
  116. data/sig/dpay/payment/payment_service.rbs +11 -0
  117. data/sig/dpay/payment/payout_fee_mode.rbs +11 -0
  118. data/sig/dpay/payment/payout_instruction.rbs +8 -0
  119. data/sig/dpay/payment/payout_position.rbs +10 -0
  120. data/sig/dpay/payment/register_payment_request.rbs +56 -0
  121. data/sig/dpay/payment/registered_payment.rbs +20 -0
  122. data/sig/dpay/payment/return_urls.rbs +11 -0
  123. data/sig/dpay/payment/transaction.rbs +25 -0
  124. data/sig/dpay/payment/transaction_refund.rbs +14 -0
  125. data/sig/dpay/payment/transaction_status.rbs +14 -0
  126. data/sig/dpay/payment/transaction_type.rbs +16 -0
  127. data/sig/dpay/payout/payout_details.rbs +24 -0
  128. data/sig/dpay/payout/payout_receiver.rbs +15 -0
  129. data/sig/dpay/payout/payout_service.rbs +6 -0
  130. data/sig/dpay/refund/refund.rbs +11 -0
  131. data/sig/dpay/refund/refund_availability.rbs +12 -0
  132. data/sig/dpay/refund/refund_service.rbs +14 -0
  133. data/sig/dpay/testing/mock_http_client.rbs +16 -0
  134. data/sig/dpay/testing.rbs +4 -0
  135. data/sig/dpay/version.rbs +3 -0
  136. data/sig/dpay.rbs +2 -0
  137. metadata +180 -0
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DPay
4
+ class BlikRecurringStatus
5
+ ACTIVE = "ACTIVE"
6
+
7
+ attr_reader :alias_value, :alias_type, :status, :expiration_date, :registration, :raw
8
+
9
+ def self.from_api(data)
10
+ new(data)
11
+ end
12
+
13
+ def initialize(data)
14
+ @raw = data
15
+ @alias_value = Internal::Coerce.string(data["alias_value"]) || ""
16
+ @alias_type = Internal::Coerce.string(data["alias_type"]) || BlikAliasType::PAYID
17
+ @status = data["status"].is_a?(String) ? data["status"] : nil
18
+ @expiration_date = data["expiration_date"].is_a?(String) ? data["expiration_date"] : nil
19
+ registration = data["registration"]
20
+ @registration = registration.is_a?(Hash) ? BlikRecurringRegistrationInfo.from_api(registration) : nil
21
+ freeze
22
+ end
23
+
24
+ def active?
25
+ @status == ACTIVE
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DPay
4
+ class BlikService
5
+ def initialize(api)
6
+ @api = api
7
+ end
8
+
9
+ def alias(alias_value, alias_type = BlikAliasType::UID)
10
+ data = @api.post_json(
11
+ Internal::BaseUrls::API_PAYMENTS,
12
+ "/api/v1_0/payments/blik/aliases",
13
+ alias_body(alias_value, alias_type)
14
+ )
15
+
16
+ BlikAlias.from_api(payload(data))
17
+ end
18
+
19
+ def unregister_alias(alias_value, alias_type = BlikAliasType::UID, reason = nil)
20
+ body = alias_body(alias_value, alias_type, reason)
21
+ @api.post_json(Internal::BaseUrls::API_PAYMENTS, "/api/v1_0/payments/blik/aliases/unregister", body)
22
+
23
+ nil
24
+ end
25
+
26
+ def recurring_status(alias_value)
27
+ body = { "service" => @api.service, "alias_value" => alias_value }
28
+ body["checksum"] = @api.checksum.secret_second(@api.service, [alias_value])
29
+
30
+ data = @api.post_json(Internal::BaseUrls::API_PAYMENTS, "/api/v1_0/payments/blik/recurring/status", body)
31
+
32
+ BlikRecurringStatus.from_api(payload(data))
33
+ end
34
+
35
+ private
36
+
37
+ def alias_body(alias_value, alias_type, reason = nil)
38
+ BlikAliasType.assert_valid(alias_type)
39
+
40
+ body = { "service" => @api.service, "alias_value" => alias_value, "alias_type" => alias_type }
41
+ body["reason"] = reason unless reason.nil?
42
+ body["checksum"] = @api.checksum.secret_second(@api.service, [alias_value])
43
+
44
+ body
45
+ end
46
+
47
+ def payload(data)
48
+ data["data"].is_a?(Hash) ? data["data"] : {}
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DPay
4
+ class ApplePayRequest
5
+ INIT = "APPLE_PAY_INIT"
6
+ PAY = "APPLE_PAY"
7
+
8
+ def self.init(device_info)
9
+ new(INIT, nil, device_info)
10
+ end
11
+
12
+ def self.pay(token, device_info)
13
+ new(PAY, token, device_info)
14
+ end
15
+
16
+ def initialize(x_pay_type, token, device_info)
17
+ @x_pay_type = x_pay_type
18
+ @token = token
19
+ @device_info = device_info
20
+ @channel_id = nil
21
+ end
22
+
23
+ def with_channel_id(channel_id)
24
+ @channel_id = channel_id
25
+ self
26
+ end
27
+
28
+ def to_body
29
+ body = {}
30
+ body["channelId"] = @channel_id unless @channel_id.nil?
31
+ body["xPayType"] = @x_pay_type
32
+ body["xPayToken"] = @token unless @token.nil?
33
+ body["deviceInfo"] = @device_info.to_h
34
+
35
+ body
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DPay
4
+ class CardData
5
+ PAN = /\A\d{12,19}\z/
6
+ CVV = /\A\d{3,4}\z/
7
+ EXPIRY = %r{\A(0[1-9]|1[0-2])/\d{2}\z}
8
+
9
+ attr_reader :pan, :cvv, :expiry
10
+
11
+ def initialize(pan, cvv, expiry)
12
+ normalized = pan.to_s.delete(" ")
13
+
14
+ raise InvalidArgumentError, "Card number must be 12-19 digits" unless PAN.match?(normalized)
15
+ raise InvalidArgumentError, "CVV must be 3-4 digits" unless CVV.match?(cvv.to_s)
16
+ raise InvalidArgumentError, "Expiry must be in MM/YY format" unless EXPIRY.match?(expiry.to_s)
17
+
18
+ @pan = normalized
19
+ @cvv = cvv
20
+ @expiry = expiry
21
+ freeze
22
+ end
23
+
24
+ def inspect
25
+ "#<DPay::CardData pan=[FILTERED] cvv=[FILTERED] expiry=[FILTERED]>"
26
+ end
27
+
28
+ alias to_s inspect
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module DPay
6
+ class CardEncryptor
7
+ def encrypt(card, transaction_id, public_key_pem, timestamp: Time.now.to_i)
8
+ key = load_key(public_key_pem)
9
+ payload = Internal::PHP.json_encode(
10
+ { "PN" => card.pan, "SC" => card.cvv, "DT" => card.expiry, "ID" => transaction_id, "TX" => timestamp },
11
+ escape_slashes: true
12
+ )
13
+
14
+ [key.public_encrypt(payload, OpenSSL::PKey::RSA::PKCS1_PADDING)].pack("m0")
15
+ rescue OpenSSL::PKey::PKeyError => e
16
+ raise CardEncryptionError, "Card data encryption failed: #{e.message}"
17
+ end
18
+
19
+ private
20
+
21
+ def load_key(public_key_pem)
22
+ OpenSSL::PKey::RSA.new(public_key_pem.to_s)
23
+ rescue OpenSSL::PKey::PKeyError, ArgumentError, TypeError
24
+ raise CardEncryptionError, "Invalid RSA public key"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DPay
4
+ class CardPaymentRequest
5
+ def self.create(device_info)
6
+ new(device_info)
7
+ end
8
+
9
+ def initialize(device_info)
10
+ @device_info = device_info
11
+ @leading = {}
12
+ @trailing = {}
13
+ end
14
+
15
+ def with_email(email)
16
+ @leading["email"] = email
17
+ self
18
+ end
19
+
20
+ def with_channel_id(channel_id)
21
+ @leading["channelId"] = channel_id
22
+ self
23
+ end
24
+
25
+ def with_card_holder(first_name, last_name)
26
+ @leading["cardHolderFirstName"] = first_name
27
+ @leading["cardHolderLastName"] = last_name
28
+ self
29
+ end
30
+
31
+ def with_encrypted_card_data(encrypted_card_data)
32
+ @leading["encryptedCardData"] = encrypted_card_data
33
+ self
34
+ end
35
+
36
+ def with_three_ds_confirmed(confirmed)
37
+ @trailing["threeDsConfirmed"] = confirmed
38
+ self
39
+ end
40
+
41
+ def with_dcc_decision(decision)
42
+ DccDecision.assert_valid(decision)
43
+ @trailing["dccDecision"] = decision
44
+ self
45
+ end
46
+
47
+ def to_body
48
+ body = {}
49
+
50
+ %w[email channelId cardHolderFirstName cardHolderLastName encryptedCardData].each do |key|
51
+ body[key] = @leading[key] if @leading.key?(key)
52
+ end
53
+ body["deviceInfo"] = @device_info.to_h
54
+ %w[threeDsConfirmed dccDecision].each { |key| body[key] = @trailing[key] if @trailing.key?(key) }
55
+
56
+ body
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DPay
4
+ class CardPaymentResult
5
+ attr_reader :redirect_type, :redirect_text, :dcc_offer, :raw
6
+
7
+ def self.from_api(data)
8
+ new(data)
9
+ end
10
+
11
+ def initialize(data)
12
+ @raw = data
13
+ message = data["message"].is_a?(Hash) ? data["message"] : {}
14
+
15
+ @redirect_type = message["redirectType"].is_a?(String) ? message["redirectType"] : nil
16
+ text = message["redirectText"]
17
+ @redirect_text = text.is_a?(String) && !text.empty? ? text : nil
18
+ @dcc_offer = message["dccOffer"].is_a?(Hash) ? DccOffer.from_api(message["dccOffer"]) : nil
19
+ freeze
20
+ end
21
+
22
+ def success?
23
+ @redirect_type == RedirectType::SUCCESS
24
+ end
25
+
26
+ def three_ds_form?
27
+ @redirect_type == RedirectType::FORM
28
+ end
29
+
30
+ def requires_redirect?
31
+ @redirect_type == RedirectType::URL
32
+ end
33
+
34
+ def dcc_offer?
35
+ @redirect_type == RedirectType::DCC_OFFER
36
+ end
37
+
38
+ def three_ds_form_html
39
+ three_ds_form? ? decode : nil
40
+ end
41
+
42
+ def redirect_url
43
+ requires_redirect? ? decode : nil
44
+ end
45
+
46
+ private
47
+
48
+ def decode
49
+ text = @redirect_text
50
+ return nil if text.nil?
51
+
52
+ decoded = text.unpack1("m0")
53
+ decoded.is_a?(String) ? decoded : nil
54
+ rescue ArgumentError
55
+ nil
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DPay
4
+ module CardRecurringFrequency
5
+ DAILY = "DAILY"
6
+ WEEKLY = "WEEKLY"
7
+ BIWEEKLY = "BIWEEKLY"
8
+ MONTHLY = "MONTHLY"
9
+ QUARTERLY = "QUARTERLY"
10
+ SEMIANNUAL = "SEMIANNUAL"
11
+ ANNUAL = "ANNUAL"
12
+
13
+ ALL = [DAILY, WEEKLY, BIWEEKLY, MONTHLY, QUARTERLY, SEMIANNUAL, ANNUAL].freeze
14
+
15
+ def self.valid?(value)
16
+ ALL.include?(value)
17
+ end
18
+
19
+ def self.assert_valid(value)
20
+ raise InvalidArgumentError, %(Invalid card recurring frequency "#{value}") unless valid?(value)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DPay
4
+ module CardRecurringOperation
5
+ ADD_CARD = "add_card"
6
+ COF_INITIAL = "cof_initial"
7
+ CHARGE = "charge"
8
+
9
+ ALL = [ADD_CARD, COF_INITIAL, CHARGE].freeze
10
+
11
+ def self.valid?(value)
12
+ ALL.include?(value)
13
+ end
14
+
15
+ def self.assert_valid(value)
16
+ raise InvalidArgumentError, %(Invalid card recurring operation "#{value}") unless valid?(value)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DPay
4
+ class CardRecurringRegistration
5
+ DATE = /\A\d{4}-\d{2}-\d{2}\z/
6
+
7
+ def self.create(label)
8
+ new(label)
9
+ end
10
+
11
+ def initialize(label)
12
+ unless label.is_a?(String) && !label.empty? && label.length <= 50
13
+ raise InvalidArgumentError, "Mandate label must be 1-50 characters"
14
+ end
15
+
16
+ @label = label
17
+ @optional = {}
18
+ end
19
+
20
+ def with_frequency(frequency)
21
+ CardRecurringFrequency.assert_valid(frequency)
22
+ @optional["frequency"] = frequency
23
+ self
24
+ end
25
+
26
+ def with_limit_amt(limit_amt)
27
+ @optional["limit_amt"] = limit_amt.minor
28
+ self
29
+ end
30
+
31
+ def with_tot_limit_amt(tot_limit_amt)
32
+ @optional["tot_limit_amt"] = tot_limit_amt.minor
33
+ self
34
+ end
35
+
36
+ def with_limit_amt_fixed(fixed)
37
+ @optional["is_limit_amt_fixed"] = fixed
38
+ self
39
+ end
40
+
41
+ def with_expiration_date(expiration_date)
42
+ @optional["expiration_date"] = assert_date(expiration_date)
43
+ self
44
+ end
45
+
46
+ def with_init_date(init_date)
47
+ @optional["init_date"] = assert_date(init_date)
48
+ self
49
+ end
50
+
51
+ def to_h
52
+ base = { "label" => @label }
53
+
54
+ %w[frequency limit_amt tot_limit_amt is_limit_amt_fixed expiration_date init_date].each do |key|
55
+ base[key] = @optional[key] if @optional.key?(key)
56
+ end
57
+
58
+ base
59
+ end
60
+
61
+ private
62
+
63
+ def assert_date(date)
64
+ raise InvalidArgumentError, %(Date "#{date}" must be in YYYY-MM-DD format) unless DATE.match?(date.to_s)
65
+
66
+ date
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DPay
4
+ class CardService
5
+ def initialize(api)
6
+ @api = api
7
+ end
8
+
9
+ def public_key
10
+ @api.get_text(Internal::BaseUrls::API_PAYMENTS, "/api/v1_0/cards/public-key").strip
11
+ end
12
+
13
+ def pay_otp(transaction_id, request)
14
+ post(transaction_id, "/pay/card-otp", request.to_body)
15
+ end
16
+
17
+ def pre_auth(transaction_id, request)
18
+ post(transaction_id, "/pay/card-pre-auth", request.to_body)
19
+ end
20
+
21
+ def capture(transaction_id, amount)
22
+ post(transaction_id, "/capture", { "amount" => amount.to_decimal.to_f })
23
+ end
24
+
25
+ def cancel(transaction_id, amount = nil)
26
+ post(transaction_id, "/cancellation", amount.nil? ? {} : { "amount" => amount.to_decimal.to_f })
27
+ end
28
+
29
+ def google_pay(transaction_id, request)
30
+ post(transaction_id, "/pay/google-pay", request.to_body)
31
+ end
32
+
33
+ def apple_pay(transaction_id, request)
34
+ post(transaction_id, "/pay/apple-pay", request.to_body)
35
+ end
36
+
37
+ private
38
+
39
+ def post(transaction_id, suffix, body)
40
+ path = "/api/v1_0/cards/payment/#{Internal::PHP.raw_url_encode(transaction_id)}#{suffix}"
41
+ data = @api.post_json(Internal::BaseUrls::API_PAYMENTS, path, body)
42
+ raise CardPaymentError.from_api(data) unless data["success"] == true
43
+
44
+ CardPaymentResult.from_api(data)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DPay
4
+ module DccDecision
5
+ ACCEPT = "accept"
6
+ REJECT = "reject"
7
+
8
+ ALL = [ACCEPT, REJECT].freeze
9
+
10
+ def self.valid?(value)
11
+ ALL.include?(value)
12
+ end
13
+
14
+ def self.assert_valid(value)
15
+ raise InvalidArgumentError, %(Invalid DCC decision "#{value}") unless valid?(value)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DPay
4
+ class DccMarkup
5
+ attr_reader :rate, :additional_info
6
+
7
+ def self.from_api(data)
8
+ new(data)
9
+ end
10
+
11
+ def initialize(data)
12
+ rate = data["rate"]
13
+ @rate = rate.is_a?(Integer) || rate.is_a?(Float) ? rate.to_f : 0.0
14
+ @additional_info = data["additionalInfo"].is_a?(String) ? data["additionalInfo"] : nil
15
+ freeze
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DPay
4
+ class DccOffer
5
+ attr_reader :currency_conversion_id, :original_amount, :converted_amount, :exchange_rate,
6
+ :valid_until, :declaration_text, :markup, :raw
7
+
8
+ def self.from_api(data)
9
+ new(data)
10
+ end
11
+
12
+ def initialize(data)
13
+ @raw = data
14
+ @currency_conversion_id = Internal::Coerce.string(data["currencyConversionId"]) || ""
15
+ @original_amount = Internal::Coerce.money(data["originalAmount"], currency(data["originalCurrency"]))
16
+ @converted_amount = Internal::Coerce.money(data["convertedAmount"], currency(data["convertedCurrency"]))
17
+ exchange_rate = data["exchangeRate"]
18
+ @exchange_rate = exchange_rate.is_a?(Integer) || exchange_rate.is_a?(Float) ? exchange_rate.to_f : 0.0
19
+ @valid_until = Internal::Coerce.string(data["validUntil"]) || ""
20
+ @declaration_text = Internal::Coerce.string(data["declarationText"]) || ""
21
+ @european_economic_area = Internal::Coerce.boolean(data["europeanEconomicArea"])
22
+ @markup = Internal::Coerce.list(data["markup"]).map { |markup| DccMarkup.from_api(markup) }
23
+ freeze
24
+ end
25
+
26
+ def european_economic_area?
27
+ @european_economic_area
28
+ end
29
+
30
+ private
31
+
32
+ def currency(value)
33
+ Currency.valid?(value) ? value : Currency::PLN
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DPay
4
+ class GooglePayRequest
5
+ def self.create(token, device_info)
6
+ new(token, device_info)
7
+ end
8
+
9
+ def initialize(token, device_info)
10
+ @token = token
11
+ @device_info = device_info
12
+ @leading = {}
13
+ end
14
+
15
+ def with_email(email)
16
+ @leading["email"] = email
17
+ self
18
+ end
19
+
20
+ def with_channel_id(channel_id)
21
+ @leading["channelId"] = channel_id
22
+ self
23
+ end
24
+
25
+ def to_body
26
+ body = {}
27
+ %w[email channelId].each { |key| body[key] = @leading[key] if @leading.key?(key) }
28
+ body["xPayType"] = "GOOGLE_PAY"
29
+ body["xPayToken"] = @token
30
+ body["deviceInfo"] = @device_info.to_h
31
+
32
+ body
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DPay
4
+ module RedirectType
5
+ SUCCESS = "SUCCESS"
6
+ FORM = "FORM"
7
+ URL = "URL"
8
+ DCC_OFFER = "DCC_OFFER"
9
+
10
+ ALL = [SUCCESS, FORM, URL, DCC_OFFER].freeze
11
+
12
+ def self.valid?(value)
13
+ ALL.include?(value)
14
+ end
15
+
16
+ def self.assert_valid(value)
17
+ raise InvalidArgumentError, %(Invalid redirect type "#{value}") unless valid?(value)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DPay
4
+ class Client
5
+ VERSION = DPay::VERSION
6
+
7
+ attr_reader :config, :payments, :refunds, :banks, :blik, :cards, :payouts
8
+
9
+ def initialize(service:, secret_hash:, timeout: Config::DEFAULT_TIMEOUT, http_client: nil, base_urls: nil)
10
+ @config = Config.new(
11
+ service: service, secret_hash: secret_hash, timeout: timeout,
12
+ http_client: http_client, base_urls: base_urls
13
+ )
14
+
15
+ api = Internal::ApiRequestor.new(@config, @config.http_client || HTTP::NetHTTPClient.new(@config.timeout))
16
+
17
+ @payments = PaymentService.new(api)
18
+ @refunds = RefundService.new(api)
19
+ @banks = BankService.new(api)
20
+ @blik = BlikService.new(api)
21
+ @cards = CardService.new(api)
22
+ @payouts = PayoutService.new(api)
23
+ freeze
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DPay
4
+ class Config
5
+ DEFAULT_TIMEOUT = 30
6
+
7
+ attr_reader :service, :secret_hash, :timeout, :http_client, :base_urls
8
+
9
+ def initialize(service:, secret_hash:, timeout: DEFAULT_TIMEOUT, http_client: nil, base_urls: nil)
10
+ @service = non_empty_string(service, "service")
11
+ @secret_hash = non_empty_string(secret_hash, "secret_hash")
12
+ @timeout = positive_integer(timeout)
13
+ @http_client = transport(http_client)
14
+ @base_urls = Internal::BaseUrls.new(base_urls || {})
15
+ freeze
16
+ end
17
+
18
+ private
19
+
20
+ def non_empty_string(value, name)
21
+ raise InvalidArgumentError, "#{name} must be a non-empty String" unless value.is_a?(String) && !value.empty?
22
+
23
+ value
24
+ end
25
+
26
+ def positive_integer(value)
27
+ raise InvalidArgumentError, "timeout must be an Integer >= 1" unless value.is_a?(Integer) && value >= 1
28
+
29
+ value
30
+ end
31
+
32
+ def transport(value)
33
+ return nil if value.nil?
34
+ raise InvalidArgumentError, "http_client must respond to #request" unless value.respond_to?(:request)
35
+
36
+ value
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DPay
4
+ module Currency
5
+ PLN = "PLN"
6
+ EUR = "EUR"
7
+ CZK = "CZK"
8
+
9
+ CODE = /\A[A-Z]{3}\z/
10
+
11
+ def self.valid?(code)
12
+ code.is_a?(String) && CODE.match?(code)
13
+ end
14
+
15
+ def self.assert_valid(code)
16
+ raise InvalidArgumentError, %(Invalid currency code "#{code}") unless valid?(code)
17
+ end
18
+ end
19
+ end