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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +219 -204
- data/lib/lipwa/auth_strategies/api_key.rb +28 -0
- data/lib/lipwa/auth_strategies/base.rb +4 -0
- data/lib/lipwa/auth_strategies/bearer_token.rb +5 -0
- data/lib/lipwa/auth_strategies/jenga.rb +58 -0
- data/lib/lipwa/auth_strategies.rb +2 -0
- data/lib/lipwa/capabilities/authorize.rb +26 -0
- data/lib/lipwa/capabilities/bank_transfer.rb +66 -0
- data/lib/lipwa/capabilities/c2b.rb +11 -10
- data/lib/lipwa/capabilities/capture.rb +25 -0
- data/lib/lipwa/capabilities/disbursement.rb +16 -17
- data/lib/lipwa/capabilities/lightning_invoice.rb +75 -0
- data/lib/lipwa/capabilities/lightning_payment.rb +71 -0
- data/lib/lipwa/capabilities/purchase.rb +26 -0
- data/lib/lipwa/capabilities/refund.rb +14 -14
- data/lib/lipwa/capabilities/status_query.rb +21 -12
- data/lib/lipwa/capabilities/stk_push.rb +8 -5
- data/lib/lipwa/capabilities/void.rb +25 -0
- data/lib/lipwa/configuration_snapshot.rb +50 -0
- data/lib/lipwa/context.rb +60 -0
- data/lib/lipwa/contracts/bank_transfer_contract.rb +61 -0
- data/lib/lipwa/contracts/card_payment_contract.rb +59 -0
- data/lib/lipwa/contracts/card_transaction_contract.rb +21 -0
- data/lib/lipwa/contracts/lightning_invoice_check_contract.rb +18 -0
- data/lib/lipwa/contracts/lightning_invoice_contract.rb +27 -0
- data/lib/lipwa/contracts/lightning_payment_check_contract.rb +18 -0
- data/lib/lipwa/contracts/lightning_payment_contract.rb +14 -0
- data/lib/lipwa/contracts/status_query_contract.rb +29 -6
- data/lib/lipwa/errors.rb +4 -2
- data/lib/lipwa/gateway.rb +24 -5
- data/lib/lipwa/gateways/coop_bank/auth.rb +68 -0
- data/lib/lipwa/gateways/coop_bank.rb +115 -0
- data/lib/lipwa/gateways/flutterwave.rb +160 -0
- data/lib/lipwa/gateways/jenga/auth.rb +76 -0
- data/lib/lipwa/gateways/jenga.rb +282 -0
- data/lib/lipwa/gateways/lnbits.rb +67 -0
- data/lib/lipwa/gateways/mpesa/auth.rb +18 -2
- data/lib/lipwa/gateways/mpesa.rb +16 -11
- data/lib/lipwa/gateways/paystack.rb +119 -0
- data/lib/lipwa/gateways/pesapal/auth.rb +72 -0
- data/lib/lipwa/gateways/pesapal.rb +123 -0
- data/lib/lipwa/gateways.rb +7 -1
- data/lib/lipwa/http_adapter.rb +56 -17
- data/lib/lipwa/logging.rb +100 -0
- data/lib/lipwa/money.rb +33 -4
- data/lib/lipwa/types.rb +3 -3
- data/lib/lipwa/version.rb +1 -1
- data/lib/lipwa/webhooks/coop_bank.rb +28 -0
- data/lib/lipwa/webhooks/jenga.rb +55 -0
- data/lib/lipwa/webhooks/lnbits.rb +46 -0
- data/lib/lipwa.rb +29 -0
- metadata +32 -4
- data/CODE_OF_CONDUCT.md +0 -10
- data/Rakefile +0 -12
- 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
|
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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:
|
|
69
|
+
InitiatorName: config.initiator_name,
|
|
71
70
|
SecurityCredential: security_credential,
|
|
72
71
|
CommandID: params[:command_id],
|
|
73
|
-
PartyA:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
104
|
-
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.
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
78
|
-
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.
|
|
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
|
|
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
|
-
|
|
31
|
-
|
|
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 =
|
|
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:
|
|
65
|
+
Initiator: config.initiator_name,
|
|
57
66
|
SecurityCredential: security_credential,
|
|
58
67
|
CommandID: "TransactionStatusQuery",
|
|
59
68
|
TransactionID: params[:transaction_id],
|
|
60
|
-
PartyA:
|
|
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
|
-
|
|
77
|
-
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.
|
|
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,
|