lipwa 0.1.2 → 0.1.4

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +219 -204
  4. data/lib/lipwa/auth_strategies/api_key.rb +28 -0
  5. data/lib/lipwa/auth_strategies/base.rb +4 -0
  6. data/lib/lipwa/auth_strategies/bearer_token.rb +5 -0
  7. data/lib/lipwa/auth_strategies/jenga.rb +58 -0
  8. data/lib/lipwa/auth_strategies.rb +2 -0
  9. data/lib/lipwa/capabilities/authorize.rb +26 -0
  10. data/lib/lipwa/capabilities/bank_transfer.rb +66 -0
  11. data/lib/lipwa/capabilities/c2b.rb +11 -10
  12. data/lib/lipwa/capabilities/capture.rb +25 -0
  13. data/lib/lipwa/capabilities/disbursement.rb +16 -17
  14. data/lib/lipwa/capabilities/lightning_invoice.rb +75 -0
  15. data/lib/lipwa/capabilities/lightning_payment.rb +71 -0
  16. data/lib/lipwa/capabilities/purchase.rb +26 -0
  17. data/lib/lipwa/capabilities/refund.rb +14 -14
  18. data/lib/lipwa/capabilities/status_query.rb +21 -12
  19. data/lib/lipwa/capabilities/stk_push.rb +8 -5
  20. data/lib/lipwa/capabilities/void.rb +25 -0
  21. data/lib/lipwa/configuration_snapshot.rb +50 -0
  22. data/lib/lipwa/context.rb +60 -0
  23. data/lib/lipwa/contracts/bank_transfer_contract.rb +61 -0
  24. data/lib/lipwa/contracts/card_payment_contract.rb +59 -0
  25. data/lib/lipwa/contracts/card_transaction_contract.rb +21 -0
  26. data/lib/lipwa/contracts/lightning_invoice_check_contract.rb +18 -0
  27. data/lib/lipwa/contracts/lightning_invoice_contract.rb +27 -0
  28. data/lib/lipwa/contracts/lightning_payment_check_contract.rb +18 -0
  29. data/lib/lipwa/contracts/lightning_payment_contract.rb +14 -0
  30. data/lib/lipwa/contracts/status_query_contract.rb +29 -6
  31. data/lib/lipwa/errors.rb +4 -2
  32. data/lib/lipwa/gateway.rb +24 -5
  33. data/lib/lipwa/gateways/coop_bank/auth.rb +68 -0
  34. data/lib/lipwa/gateways/coop_bank.rb +115 -0
  35. data/lib/lipwa/gateways/flutterwave.rb +160 -0
  36. data/lib/lipwa/gateways/jenga/auth.rb +76 -0
  37. data/lib/lipwa/gateways/jenga.rb +282 -0
  38. data/lib/lipwa/gateways/lnbits.rb +67 -0
  39. data/lib/lipwa/gateways/mpesa/auth.rb +18 -2
  40. data/lib/lipwa/gateways/mpesa.rb +16 -11
  41. data/lib/lipwa/gateways/paystack.rb +119 -0
  42. data/lib/lipwa/gateways/pesapal/auth.rb +72 -0
  43. data/lib/lipwa/gateways/pesapal.rb +123 -0
  44. data/lib/lipwa/gateways.rb +7 -1
  45. data/lib/lipwa/http_adapter.rb +56 -17
  46. data/lib/lipwa/logging.rb +100 -0
  47. data/lib/lipwa/money.rb +33 -4
  48. data/lib/lipwa/types.rb +3 -3
  49. data/lib/lipwa/version.rb +1 -1
  50. data/lib/lipwa/webhooks/coop_bank.rb +28 -0
  51. data/lib/lipwa/webhooks/jenga.rb +55 -0
  52. data/lib/lipwa/webhooks/lnbits.rb +46 -0
  53. data/lib/lipwa.rb +29 -0
  54. metadata +32 -4
  55. data/CODE_OF_CONDUCT.md +0 -10
  56. data/Rakefile +0 -12
  57. data/sig/lipwa.rbs +0 -4
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "openssl"
5
+ require_relative "base"
6
+
7
+ module Lipwa
8
+ module AuthStrategies
9
+ # Adds Jenga HQ's OAuth2 bearer token and RSA-SHA256 request signature.
10
+ #
11
+ # Jenga defines a different signature formula for each endpoint. The
12
+ # signature_payload callable receives the Faraday::Env for the outgoing
13
+ # request and must return the exact concatenated String required by that
14
+ # endpoint (without separators). It is evaluated for every request.
15
+ class Jenga < Base
16
+ def initialize(token_provider:, private_key:, signature_payload:)
17
+ super()
18
+ validate_callable!(token_provider, :token_provider)
19
+ validate_callable!(signature_payload, :signature_payload)
20
+
21
+ @token_provider = token_provider
22
+ @private_key = parse_private_key(private_key)
23
+ @signature_payload = signature_payload
24
+ end
25
+
26
+ def initialize_copy(source)
27
+ super
28
+ @token_provider = @token_provider.dup
29
+ end
30
+
31
+ def apply(env)
32
+ payload = @signature_payload.call(env)
33
+ raise ArgumentError, "signature_payload must return a String" unless payload.is_a?(String)
34
+
35
+ signature = @private_key.sign(OpenSSL::Digest.new("SHA256"), payload)
36
+ env.request_headers["Authorization"] = "Bearer #{@token_provider.call}"
37
+ env.request_headers["Signature"] = Base64.strict_encode64(signature)
38
+ end
39
+
40
+ private
41
+
42
+ def validate_callable!(value, name)
43
+ raise ArgumentError, "#{name} must respond to #call" unless value.respond_to?(:call)
44
+ end
45
+
46
+ def parse_private_key(private_key)
47
+ key = private_key.is_a?(OpenSSL::PKey::RSA) ? private_key : OpenSSL::PKey::RSA.new(private_key)
48
+ raise ArgumentError, "private_key must contain an RSA private key" unless key.private?
49
+
50
+ key
51
+ rescue OpenSSL::PKey::PKeyError, TypeError
52
+ raise ArgumentError, "private_key must be an RSA private key or PEM encoded RSA private key"
53
+ end
54
+ end
55
+
56
+ JengaHQ = Jenga
57
+ end
58
+ end
@@ -3,3 +3,5 @@
3
3
  require_relative "auth_strategies/base"
4
4
  require_relative "auth_strategies/none"
5
5
  require_relative "auth_strategies/bearer_token"
6
+ require_relative "auth_strategies/api_key"
7
+ require_relative "auth_strategies/jenga"
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads"
4
+ require_relative "../capability"
5
+ require_relative "../contracts/card_payment_contract"
6
+
7
+ module Lipwa
8
+ module Capabilities
9
+ # Creates a provider authorization flow without exposing card data.
10
+ module Authorize
11
+ extend Lipwa::Capability
12
+ include Dry::Monads[:result]
13
+ self.capability_name = :authorize
14
+
15
+ CONTRACT = Lipwa::Contracts::CardPaymentContract.new
16
+
17
+ def authorize(**params)
18
+ idempotency_key = params.delete(:idempotency_key)
19
+ validation = CONTRACT.call(params)
20
+ return Failure(Lipwa::ValidationError.new(validation)) if validation.failure?
21
+
22
+ perform_authorize(validation.to_h, idempotency_key)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads"
4
+ require_relative "../capability"
5
+ require_relative "../contracts/bank_transfer_contract"
6
+ module Lipwa
7
+ module Capabilities
8
+ module BankTransfer
9
+ extend Lipwa::Capability
10
+ include Dry::Monads[:result]
11
+ self.capability_name = :bank_transfer
12
+ TRANSFER_CONTRACT = Lipwa::Contracts::BankTransferContract.new
13
+ ACCOUNT_CONTRACT = Lipwa::Contracts::BankAccountContract.new
14
+ STATEMENT_CONTRACT = Lipwa::Contracts::BankStatementContract.new
15
+
16
+ def transfer(rail:, amount:, source_account:, destination_account:, reference:, narration: nil,
17
+ callback_url: nil, destination_bank_code: nil, destination_name: nil, biller_code: nil,
18
+ idempotency_key: nil)
19
+ args = { rail: rail, amount: amount, source_account: source_account,
20
+ destination_account: destination_account, reference: reference, narration: narration,
21
+ callback_url: callback_url, destination_bank_code: destination_bank_code,
22
+ destination_name: destination_name, biller_code: biller_code }
23
+ invoke_bank_operation(TRANSFER_CONTRACT, args, idempotency_key) do |params, key|
24
+ bank_transfer_request(params, key)
25
+ end
26
+ end
27
+
28
+ def balance(account_number:, message_reference: nil, idempotency_key: nil)
29
+ invoke_bank_operation(ACCOUNT_CONTRACT,
30
+ { account_number: account_number, message_reference: message_reference },
31
+ idempotency_key) do |params, key|
32
+ bank_balance_request(params, key)
33
+ end
34
+ end
35
+
36
+ def statement(account_number:, from_date: nil, to_date: nil, message_reference: nil,
37
+ idempotency_key: nil)
38
+ args = { account_number: account_number, from_date: from_date, to_date: to_date,
39
+ message_reference: message_reference }
40
+ invoke_bank_operation(STATEMENT_CONTRACT, args, idempotency_key) do |params, key|
41
+ bank_statement_request(params, key)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def invoke_bank_operation(contract, args, idempotency_key)
48
+ validation = contract.call(args)
49
+ return Failure(Lipwa::ValidationError.new(validation)) if validation.failure?
50
+
51
+ build_bank_response(yield(validation.to_h, idempotency_key).body)
52
+ rescue Lipwa::GatewayError => e
53
+ Failure(e)
54
+ end
55
+
56
+ def build_bank_response(body)
57
+ code = body["ResponseCode"] || body["responseCode"] || body["Code"] || body["code"]
58
+ reference = body["MessageReference"] || body["messageReference"] || body["TransactionReference"] || body["transactionReference"]
59
+ message = body["ResponseMessage"] || body["responseMessage"] || body["Message"] || body["message"]
60
+ success = code.nil? || %w[0 00 000 200 Success SUCCESS].include?(code.to_s)
61
+ Success(Lipwa::Response.new(success: success, provider_reference: reference&.to_s,
62
+ message: message&.to_s, code: code&.to_s, raw: body))
63
+ end
64
+ end
65
+ end
66
+ end
@@ -24,7 +24,7 @@ module Lipwa
24
24
  REGISTER_URLS_CONTRACT = Lipwa::Contracts::C2bRegisterUrlsContract.new
25
25
  SIMULATE_CONTRACT = Lipwa::Contracts::C2bSimulateContract.new
26
26
 
27
- def register_urls(validation_url:, confirmation_url:, response_type: "Completed")
27
+ def register_urls(validation_url:, confirmation_url:, response_type: "Completed", idempotency_key: nil)
28
28
  validation = REGISTER_URLS_CONTRACT.call(
29
29
  validation_url: validation_url,
30
30
  confirmation_url: confirmation_url,
@@ -32,13 +32,14 @@ module Lipwa
32
32
  )
33
33
  return Failure(Lipwa::ValidationError.new(validation)) if validation.failure?
34
34
 
35
- perform_register_urls(validation.to_h)
35
+ perform_register_urls(validation.to_h, idempotency_key)
36
36
  end
37
37
 
38
38
  # Sandbox-only: triggers a simulated C2B payment so the
39
39
  # registered validation/confirmation URLs can be exercised
40
40
  # without a real customer transaction.
41
- def simulate(amount:, phone_number:, bill_ref_number:, command_id: "CustomerPayBillOnline")
41
+ def simulate(amount:, phone_number:, bill_ref_number:, command_id: "CustomerPayBillOnline",
42
+ idempotency_key: nil)
42
43
  validation = SIMULATE_CONTRACT.call(
43
44
  amount: amount,
44
45
  phone_number: phone_number,
@@ -47,13 +48,13 @@ module Lipwa
47
48
  )
48
49
  return Failure(Lipwa::ValidationError.new(validation)) if validation.failure?
49
50
 
50
- perform_simulate(validation.to_h)
51
+ perform_simulate(validation.to_h, idempotency_key)
51
52
  end
52
53
 
53
54
  private
54
55
 
55
- def perform_register_urls(params)
56
- response = http.post(REGISTER_URLS_PATH, body: register_urls_body(params))
56
+ def perform_register_urls(params, idempotency_key)
57
+ response = http.post(REGISTER_URLS_PATH, body: register_urls_body(params), idempotency_key: idempotency_key)
57
58
 
58
59
  build_c2b_response(response.body)
59
60
  rescue Lipwa::GatewayError => e
@@ -62,15 +63,15 @@ module Lipwa
62
63
 
63
64
  def register_urls_body(params)
64
65
  {
65
- ShortCode: self.class.config.shortcode,
66
+ ShortCode: config.shortcode,
66
67
  ResponseType: params[:response_type],
67
68
  ConfirmationURL: params[:confirmation_url],
68
69
  ValidationURL: params[:validation_url]
69
70
  }
70
71
  end
71
72
 
72
- def perform_simulate(params)
73
- response = http.post(SIMULATE_PATH, body: simulate_body(params))
73
+ def perform_simulate(params, idempotency_key)
74
+ response = http.post(SIMULATE_PATH, body: simulate_body(params), idempotency_key: idempotency_key)
74
75
 
75
76
  build_c2b_response(response.body)
76
77
  rescue Lipwa::GatewayError => e
@@ -79,7 +80,7 @@ module Lipwa
79
80
 
80
81
  def simulate_body(params)
81
82
  {
82
- ShortCode: self.class.config.shortcode,
83
+ ShortCode: config.shortcode,
83
84
  CommandID: params[:command_id],
84
85
  Amount: params[:amount].amount,
85
86
  Msisdn: params[:phone_number],
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads"
4
+ require_relative "../capability"
5
+ require_relative "../contracts/card_transaction_contract"
6
+
7
+ module Lipwa
8
+ module Capabilities
9
+ # Confirms a provider authorization has completed successfully.
10
+ module Capture
11
+ extend Lipwa::Capability
12
+ include Dry::Monads[:result]
13
+ self.capability_name = :capture
14
+
15
+ CONTRACT = Lipwa::Contracts::CardTransactionContract.new
16
+
17
+ def capture(authorization:, amount: nil, idempotency_key: nil)
18
+ validation = CONTRACT.call({ authorization: authorization, amount: amount }.compact)
19
+ return Failure(Lipwa::ValidationError.new(validation)) if validation.failure?
20
+
21
+ perform_capture(validation.to_h, idempotency_key)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -28,29 +28,28 @@ module Lipwa
28
28
 
29
29
  # rubocop:disable Metrics/ParameterLists
30
30
  def disburse(command_id:, amount:, party_b:, remarks:, result_url:, queue_timeout_url:, occasion: nil,
31
- account_reference: nil)
32
- validate_and_disburse(
33
- command_id: command_id, amount: amount, party_b: party_b, remarks: remarks,
34
- result_url: result_url, queue_timeout_url: queue_timeout_url,
35
- occasion: occasion, account_reference: account_reference
36
- )
31
+ account_reference: nil, idempotency_key: nil)
32
+ args = { command_id: command_id, amount: amount, party_b: party_b, remarks: remarks,
33
+ result_url: result_url, queue_timeout_url: queue_timeout_url,
34
+ occasion: occasion, account_reference: account_reference }
35
+ validate_and_disburse(args, idempotency_key)
37
36
  end
38
37
  # rubocop:enable Metrics/ParameterLists
39
38
 
40
39
  private
41
40
 
42
- def validate_and_disburse(args)
41
+ def validate_and_disburse(args, idempotency_key)
43
42
  validation = CONTRACT.call(args)
44
43
  return Failure(Lipwa::ValidationError.new(validation)) if validation.failure?
45
44
 
46
- perform_disburse(validation.to_h)
45
+ perform_disburse(validation.to_h, idempotency_key)
47
46
  end
48
47
 
49
- def perform_disburse(params)
48
+ def perform_disburse(params, idempotency_key)
50
49
  ensure_disbursement_config_present!
51
50
  path = B2C_COMMAND_IDS.include?(params[:command_id]) ? B2C_PATH : B2B_PATH
52
51
 
53
- response = http.post(path, body: disbursement_body(params))
52
+ response = http.post(path, body: disbursement_body(params), idempotency_key: idempotency_key)
54
53
 
55
54
  build_disbursement_response(response.body)
56
55
  rescue Lipwa::GatewayError => e
@@ -67,10 +66,10 @@ module Lipwa
67
66
 
68
67
  def b2c_body(params)
69
68
  {
70
- InitiatorName: self.class.config.initiator_name,
69
+ InitiatorName: config.initiator_name,
71
70
  SecurityCredential: security_credential,
72
71
  CommandID: params[:command_id],
73
- PartyA: self.class.config.shortcode,
72
+ PartyA: config.shortcode,
74
73
  PartyB: params[:party_b],
75
74
  Occasion: params[:occasion]
76
75
  }.merge(shared_disbursement_fields(params))
@@ -78,12 +77,12 @@ module Lipwa
78
77
 
79
78
  def b2b_body(params)
80
79
  {
81
- Initiator: self.class.config.initiator_name,
80
+ Initiator: config.initiator_name,
82
81
  SecurityCredential: security_credential,
83
82
  CommandID: params[:command_id],
84
83
  SenderIdentifierType: "4",
85
84
  RecieverIdentifierType: "4",
86
- PartyA: self.class.config.shortcode,
85
+ PartyA: config.shortcode,
87
86
  PartyB: params[:party_b],
88
87
  AccountReference: params[:account_reference]
89
88
  }.merge(shared_disbursement_fields(params))
@@ -100,8 +99,8 @@ module Lipwa
100
99
 
101
100
  def security_credential
102
101
  Lipwa::Gateways::Mpesa::SecurityCredential.encrypt(
103
- self.class.config.initiator_password,
104
- cert: self.class.config.security_credential_cert
102
+ config.initiator_password,
103
+ cert: config.security_credential_cert
105
104
  )
106
105
  end
107
106
 
@@ -117,7 +116,7 @@ module Lipwa
117
116
  end
118
117
 
119
118
  def ensure_disbursement_config_present!
120
- config = self.class.config
119
+ config = self.config
121
120
  return if config.initiator_name && config.initiator_password && config.security_credential_cert
122
121
 
123
122
  raise Lipwa::ConfigurationError,
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads"
4
+ require_relative "../capability"
5
+ require_relative "../contracts/lightning_invoice_contract"
6
+ require_relative "../contracts/lightning_invoice_check_contract"
7
+
8
+ module Lipwa
9
+ module Capabilities
10
+ # Creates and checks LNbits Lightning invoices. Amounts passed to
11
+ # #create_invoice are integer satoshis; they intentionally do not use
12
+ # Lipwa::Money because BTC/sats are outside the ISO-4217 money model.
13
+ module LightningInvoice
14
+ extend Lipwa::Capability
15
+ include Dry::Monads[:result]
16
+ self.capability_name = :lightning_invoice
17
+
18
+ PATH = "/api/v1/payments"
19
+ CREATE_CONTRACT = Lipwa::Contracts::LightningInvoiceContract.new
20
+ CHECK_CONTRACT = Lipwa::Contracts::LightningInvoiceCheckContract.new
21
+
22
+ def create_invoice(amount_sats:, memo: nil, expiry: nil, webhook_url: nil)
23
+ validation = CREATE_CONTRACT.call(
24
+ amount_sats: amount_sats, memo: memo, expiry: expiry, webhook_url: webhook_url
25
+ )
26
+ return Failure(Lipwa::ValidationError.new(validation)) if validation.failure?
27
+
28
+ perform_create_invoice(validation.to_h)
29
+ end
30
+
31
+ def check_invoice(payment_hash:)
32
+ validation = CHECK_CONTRACT.call(payment_hash: payment_hash)
33
+ return Failure(Lipwa::ValidationError.new(validation)) if validation.failure?
34
+
35
+ perform_check_invoice(validation.to_h)
36
+ end
37
+
38
+ private
39
+
40
+ def perform_create_invoice(params)
41
+ response = http.post(PATH, body: lightning_invoice_body(params))
42
+
43
+ Success(Lipwa::Response.new(
44
+ success: true,
45
+ provider_reference: response.body["payment_hash"],
46
+ raw: response.body
47
+ ))
48
+ rescue Lipwa::GatewayError => e
49
+ Failure(e)
50
+ end
51
+
52
+ def perform_check_invoice(params)
53
+ response = http.get("#{PATH}/#{params[:payment_hash]}")
54
+
55
+ Success(Lipwa::Response.new(
56
+ success: response.body["paid"] == true,
57
+ provider_reference: params[:payment_hash],
58
+ raw: response.body
59
+ ))
60
+ rescue Lipwa::GatewayError => e
61
+ Failure(e)
62
+ end
63
+
64
+ def lightning_invoice_body(params)
65
+ {
66
+ out: false,
67
+ amount: params[:amount_sats],
68
+ memo: params[:memo],
69
+ expiry: params[:expiry],
70
+ webhook: params[:webhook_url]
71
+ }.compact
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads"
4
+ require_relative "../capability"
5
+ require_relative "../contracts/lightning_payment_contract"
6
+ require_relative "../contracts/lightning_payment_check_contract"
7
+
8
+ module Lipwa
9
+ module Capabilities
10
+ # Sends and reconciles outbound Lightning payments. This capability is
11
+ # deliberately separate from LightningInvoice because spending requires a
12
+ # materially more privileged credential than receiving.
13
+ module LightningPayment
14
+ extend Lipwa::Capability
15
+ include Dry::Monads[:result]
16
+ self.capability_name = :lightning_payment
17
+
18
+ PATH = "/api/v1/payments"
19
+ PAY_CONTRACT = Lipwa::Contracts::LightningPaymentContract.new
20
+ CHECK_CONTRACT = Lipwa::Contracts::LightningPaymentCheckContract.new
21
+
22
+ def pay_invoice(bolt11:)
23
+ validation = PAY_CONTRACT.call(bolt11: bolt11)
24
+ return Failure(Lipwa::ValidationError.new(validation)) if validation.failure?
25
+
26
+ perform_pay_invoice(validation.to_h)
27
+ end
28
+
29
+ def check_payment(payment_hash:)
30
+ validation = CHECK_CONTRACT.call(payment_hash: payment_hash)
31
+ return Failure(Lipwa::ValidationError.new(validation)) if validation.failure?
32
+
33
+ perform_check_payment(validation.to_h)
34
+ end
35
+
36
+ private
37
+
38
+ def perform_pay_invoice(params)
39
+ response = lightning_payment_http.post(PATH, body: { out: true, bolt11: params[:bolt11] })
40
+ Success(payment_response(response.body))
41
+ rescue Lipwa::GatewayError => e
42
+ Failure(e)
43
+ end
44
+
45
+ def perform_check_payment(params)
46
+ response = lightning_payment_http.get("#{PATH}/#{params[:payment_hash]}")
47
+ Success(payment_response(response.body, payment_hash: params[:payment_hash]))
48
+ rescue Lipwa::GatewayError => e
49
+ Failure(e)
50
+ end
51
+
52
+ def payment_response(payload, payment_hash: nil)
53
+ Lipwa::Response.new(
54
+ success: payment_successful?(payload),
55
+ provider_reference: payment_hash || payload["payment_hash"],
56
+ message: payload["detail"] || payload["message"],
57
+ raw: payload
58
+ )
59
+ end
60
+
61
+ def payment_successful?(payload)
62
+ payload["paid"] == true || payload["status"].to_s.casecmp?("success")
63
+ end
64
+
65
+ # Gateways with distinct spending credentials override this method.
66
+ def lightning_payment_http
67
+ http
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads"
4
+ require_relative "../capability"
5
+ require_relative "../contracts/card_payment_contract"
6
+
7
+ module Lipwa
8
+ module Capabilities
9
+ # Creates a payment which is collected in the provider checkout flow.
10
+ module Purchase
11
+ extend Lipwa::Capability
12
+ include Dry::Monads[:result]
13
+ self.capability_name = :purchase
14
+
15
+ CONTRACT = Lipwa::Contracts::CardPaymentContract.new
16
+
17
+ def purchase(**params)
18
+ idempotency_key = params.delete(:idempotency_key)
19
+ validation = CONTRACT.call(params)
20
+ return Failure(Lipwa::ValidationError.new(validation)) if validation.failure?
21
+
22
+ perform_purchase(validation.to_h, idempotency_key)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -24,27 +24,27 @@ module Lipwa
24
24
  CONTRACT = Lipwa::Contracts::RefundContract.new
25
25
 
26
26
  # rubocop:disable Metrics/ParameterLists
27
- def refund(transaction_id:, amount:, remarks:, result_url:, queue_timeout_url:, occasion: nil)
28
- validate_and_refund(
29
- transaction_id: transaction_id, amount: amount, remarks: remarks,
30
- result_url: result_url, queue_timeout_url: queue_timeout_url, occasion: occasion
31
- )
27
+ def refund(transaction_id:, amount:, remarks:, result_url:, queue_timeout_url:, occasion: nil,
28
+ idempotency_key: nil)
29
+ args = { transaction_id: transaction_id, amount: amount, remarks: remarks,
30
+ result_url: result_url, queue_timeout_url: queue_timeout_url, occasion: occasion }
31
+ validate_and_refund(args, idempotency_key)
32
32
  end
33
33
  # rubocop:enable Metrics/ParameterLists
34
34
 
35
35
  private
36
36
 
37
- def validate_and_refund(args)
37
+ def validate_and_refund(args, idempotency_key)
38
38
  validation = CONTRACT.call(args)
39
39
  return Failure(Lipwa::ValidationError.new(validation)) if validation.failure?
40
40
 
41
- perform_refund(validation.to_h)
41
+ perform_refund(validation.to_h, idempotency_key)
42
42
  end
43
43
 
44
- def perform_refund(params)
44
+ def perform_refund(params, idempotency_key)
45
45
  ensure_refund_config_present!
46
46
 
47
- response = http.post(PATH, body: refund_body(params))
47
+ response = http.post(PATH, body: refund_body(params), idempotency_key: idempotency_key)
48
48
 
49
49
  build_refund_response(response.body)
50
50
  rescue Lipwa::GatewayError => e
@@ -53,12 +53,12 @@ module Lipwa
53
53
 
54
54
  def refund_body(params)
55
55
  {
56
- Initiator: self.class.config.initiator_name,
56
+ Initiator: config.initiator_name,
57
57
  SecurityCredential: security_credential,
58
58
  CommandID: "TransactionReversal",
59
59
  TransactionID: params[:transaction_id],
60
60
  Amount: params[:amount].amount,
61
- ReceiverParty: self.class.config.shortcode,
61
+ ReceiverParty: config.shortcode,
62
62
  RecieverIdentifierType: RECEIVER_IDENTIFIER_TYPE
63
63
  }.merge(shared_refund_fields(params))
64
64
  end
@@ -74,8 +74,8 @@ module Lipwa
74
74
 
75
75
  def security_credential
76
76
  Lipwa::Gateways::Mpesa::SecurityCredential.encrypt(
77
- self.class.config.initiator_password,
78
- cert: self.class.config.security_credential_cert
77
+ config.initiator_password,
78
+ cert: config.security_credential_cert
79
79
  )
80
80
  end
81
81
 
@@ -90,7 +90,7 @@ module Lipwa
90
90
  end
91
91
 
92
92
  def ensure_refund_config_present!
93
- config = self.class.config
93
+ config = self.config
94
94
  return if config.initiator_name && config.initiator_password && config.security_credential_cert
95
95
 
96
96
  raise Lipwa::ConfigurationError,
@@ -25,39 +25,48 @@ module Lipwa
25
25
 
26
26
  CONTRACT = Lipwa::Contracts::StatusQueryContract.new
27
27
 
28
- def status(transaction_id:, remarks:, result_url:, queue_timeout_url:, occasion: nil)
28
+ def status(transaction_id: nil, message_reference: nil, remarks: nil, result_url: nil, # rubocop:disable Metrics/ParameterLists
29
+ queue_timeout_url: nil, occasion: nil, idempotency_key: nil)
29
30
  validate_and_query(
30
- transaction_id: transaction_id, remarks: remarks, result_url: result_url,
31
- queue_timeout_url: queue_timeout_url, occasion: occasion
31
+ {
32
+ transaction_id: transaction_id, message_reference: message_reference,
33
+ remarks: remarks, result_url: result_url,
34
+ queue_timeout_url: queue_timeout_url, occasion: occasion
35
+ },
36
+ idempotency_key
32
37
  )
33
38
  end
34
39
 
35
40
  private
36
41
 
37
- def validate_and_query(args)
42
+ def validate_and_query(args, idempotency_key)
38
43
  validation = CONTRACT.call(args)
39
44
  return Failure(Lipwa::ValidationError.new(validation)) if validation.failure?
40
45
 
41
- perform_status_query(validation.to_h)
46
+ perform_status_query(validation.to_h, idempotency_key)
42
47
  end
43
48
 
44
- def perform_status_query(params)
49
+ def perform_status_query(params, idempotency_key)
45
50
  ensure_status_query_config_present!
46
51
 
47
- response = http.post(PATH, body: status_query_body(params))
52
+ response = status_query_request(params, idempotency_key)
48
53
 
49
54
  build_status_query_response(response.body)
50
55
  rescue Lipwa::GatewayError => e
51
56
  Failure(e)
52
57
  end
53
58
 
59
+ def status_query_request(params, idempotency_key)
60
+ http.post(PATH, body: status_query_body(params), idempotency_key: idempotency_key)
61
+ end
62
+
54
63
  def status_query_body(params)
55
64
  {
56
- Initiator: self.class.config.initiator_name,
65
+ Initiator: config.initiator_name,
57
66
  SecurityCredential: security_credential,
58
67
  CommandID: "TransactionStatusQuery",
59
68
  TransactionID: params[:transaction_id],
60
- PartyA: self.class.config.shortcode,
69
+ PartyA: config.shortcode,
61
70
  IdentifierType: IDENTIFIER_TYPE
62
71
  }.merge(shared_status_query_fields(params))
63
72
  end
@@ -73,8 +82,8 @@ module Lipwa
73
82
 
74
83
  def security_credential
75
84
  Lipwa::Gateways::Mpesa::SecurityCredential.encrypt(
76
- self.class.config.initiator_password,
77
- cert: self.class.config.security_credential_cert
85
+ config.initiator_password,
86
+ cert: config.security_credential_cert
78
87
  )
79
88
  end
80
89
 
@@ -89,7 +98,7 @@ module Lipwa
89
98
  end
90
99
 
91
100
  def ensure_status_query_config_present!
92
- config = self.class.config
101
+ config = self.config
93
102
  return if config.initiator_name && config.initiator_password && config.security_credential_cert
94
103
 
95
104
  raise Lipwa::ConfigurationError,