mpesa_stk 1.3 → 3.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.
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mpesa_stk/client'
4
+
5
+ module MpesaStk
6
+ # Register and query pull transaction callbacks.
7
+ class PullTransactions < Client
8
+ class << self
9
+ def register(**options)
10
+ new(**options).register_url
11
+ end
12
+
13
+ def call(start_date, end_date, **options)
14
+ new(**options).query_transactions(start_date, end_date)
15
+ end
16
+ end
17
+
18
+ def register_url
19
+ post(
20
+ 'pull_transactions_register_url',
21
+ {
22
+ ShortCode: option('business_short_code', :short_code),
23
+ RequestType: @options.fetch(:request_type, ''),
24
+ NominatedNumber: @options.fetch(:nominated_number, ''),
25
+ CallBackURL: option('callback_url')
26
+ },
27
+ error_message: 'Failed to register pull transactions URL'
28
+ )
29
+ end
30
+
31
+ def query_transactions(start_date, end_date)
32
+ post(
33
+ 'pull_transactions_query_url',
34
+ {
35
+ ShortCode: option('business_short_code', :short_code),
36
+ StartDate: start_date,
37
+ EndDate: end_date,
38
+ OffSetValue: @options.fetch(:offset_value, '0')
39
+ },
40
+ error_message: 'Failed to query pull transactions'
41
+ )
42
+ end
43
+ end
44
+ end
@@ -1,131 +1,69 @@
1
- require "mpesa_stk/access_token"
1
+ # frozen_string_literal: true
2
+
3
+ require 'mpesa_stk/client'
2
4
 
3
5
  module MpesaStk
4
- class Push
5
- class << self
6
- def pay_bill(amount, phone_number, hash = {})
7
- new(amount, phone_number, "CustomerPayBillOnline", nil, hash["business_short_code"], hash["callback_url"], hash["business_passkey"], hash["key"], hash["secret"]).push_payment
8
- end
6
+ # STK Push (Lipa na M-Pesa): PayBill and Buy Goods.
7
+ #
8
+ # MpesaStk::Push.call(amount, phone) # PayBill, ENV only
9
+ # MpesaStk::Push.call(amount, phone, type: :buy_goods) # Buy Goods
10
+ # MpesaStk::Push.call(amount, phone, key: '...', ...) # per-request overrides
11
+ class Push < Client
12
+ TRANSACTION_TYPES = {
13
+ pay_bill: 'CustomerPayBillOnline',
14
+ buy_goods: 'CustomerBuyGoodsOnline'
15
+ }.freeze
9
16
 
10
- def buy_goods(amount, phone_number, hash = {})
11
- new(amount, phone_number, "CustomerBuyGoodsOnline", hash["till_number"], hash["business_short_code"], hash["callback_url"], hash["business_passkey"], hash["key"], hash["secret"]).push_payment
17
+ class << self
18
+ def call(amount, phone_number, type: :pay_bill, **options)
19
+ new(amount, phone_number, type: type, **options).push_payment
12
20
  end
13
21
  end
14
22
 
15
- attr_reader :token, :amount, :phone_number, :till_number, :business_short_code, :callback_url, :business_passkey, :transaction_type
23
+ attr_reader :amount, :phone_number, :transaction_type
16
24
 
17
- def initialize(amount, phone_number, transaction_type, till_number = nil, business_short_code = nil, callback_url = nil, business_passkey = nil, key = nil, secret = nil)
18
- @token = MpesaStk::AccessToken.call(key, secret)
19
- @transaction_type = transaction_type
20
- @till_number = till_number
21
- @business_short_code = business_short_code
22
- @callback_url = callback_url
23
- @business_passkey = business_passkey
25
+ def initialize(amount, phone_number, type: :pay_bill, **options)
26
+ @transaction_type = TRANSACTION_TYPES.fetch(type) do
27
+ raise ArgumentError, "Unknown STK type: #{type}. Use :pay_bill or :buy_goods"
28
+ end
29
+
30
+ super(**options)
24
31
  @amount = amount
25
32
  @phone_number = phone_number
26
33
  end
27
34
 
28
35
  def push_payment
29
- response = HTTParty.post(url, headers: headers, body: body)
30
- JSON.parse(response.body)
36
+ post('process_request_url', stk_push_payload, error_message: 'Failed to push payment')
31
37
  end
32
38
 
33
39
  private
34
40
 
35
- def url
36
- "#{ENV['base_url']}#{ENV['process_request_url']}"
37
- end
41
+ def stk_push_payload
42
+ short_code, passkey, timestamp = stk_credentials
38
43
 
39
- def headers
40
44
  {
41
- "Authorization" => "Bearer #{token}",
42
- "Content-Type" => "application/json"
43
- }
44
- end
45
-
46
- def body
47
- {
48
- BusinessShortCode: get_business_short_code,
49
- Password: generate_password,
50
- Timestamp: "#{timestamp}",
45
+ BusinessShortCode: short_code,
46
+ Password: stk_password(short_code, passkey, timestamp),
47
+ Timestamp: timestamp.to_s,
51
48
  TransactionType: transaction_type,
52
- Amount: "#{amount}",
53
- PartyA: "#{phone_number}",
54
- PartyB: get_till_number,
55
- PhoneNumber: "#{phone_number}",
56
- CallBackURL: get_callback_url,
57
- AccountReference: generate_bill_reference_number(5),
58
- TransactionDesc: generate_bill_reference_number(5)
59
- }.to_json
60
- end
61
-
62
- def generate_bill_reference_number(number)
63
- charset = Array('A'..'Z') + Array('a'..'z')
64
- Array.new(number) { charset.sample }.join
65
- end
66
-
67
- def timestamp
68
- DateTime.now.strftime("%Y%m%d%H%M%S").to_i
69
- end
70
-
71
- # shortcode
72
- # passkey
73
- # timestamp
74
- def generate_password
75
- key = "#{get_business_short_code}#{get_business_passkey}#{timestamp}"
76
- Base64.encode64(key).split("\n").join
77
- end
78
-
79
- def get_business_short_code
80
- if business_short_code.nil? || business_short_code.eql?("")
81
- if ENV['business_short_code'].nil? || ENV['business_short_code'].eql?("")
82
- raise Exception.new "Business Short Code is not defined"
83
- else
84
- ENV['business_short_code']
85
- end
86
- else
87
- business_short_code
88
- end
49
+ Amount: amount.to_s,
50
+ PartyA: phone_number.to_s,
51
+ PartyB: party_b(short_code),
52
+ PhoneNumber: phone_number.to_s,
53
+ CallBackURL: option('callback_url'),
54
+ AccountReference: random_reference,
55
+ TransactionDesc: random_reference
56
+ }
89
57
  end
90
58
 
91
- def get_business_passkey
92
- if business_passkey.nil? || business_passkey.eql?("")
93
- if ENV['business_passkey'].nil? || ENV['business_passkey'].eql?("")
94
- raise Exception.new "Business Passkey is not defined"
95
- else
96
- ENV['business_passkey']
97
- end
98
- else
99
- business_passkey
100
- end
59
+ def stk_credentials
60
+ [option('business_short_code'), option('business_passkey'), stk_timestamp]
101
61
  end
102
62
 
103
- def get_callback_url
104
- if callback_url.nil? || callback_url.eql?("")
105
- if ENV['callback_url'].nil? || ENV['callback_url'].eql?("")
106
- raise Exception.new "Callback URL is not defined"
107
- else
108
- ENV['callback_url']
109
- end
110
- else
111
- callback_url
112
- end
113
- end
63
+ def party_b(short_code)
64
+ return short_code if transaction_type == 'CustomerPayBillOnline'
114
65
 
115
- def get_till_number
116
- if transaction_type.eql?("CustomerPayBillOnline")
117
- get_business_short_code
118
- else
119
- if till_number.nil?
120
- if ENV['till_number'].nil? || ENV['till_number'].eql?("")
121
- raise Exception.new "Till number is not defined"
122
- else
123
- ENV['till_number']
124
- end
125
- else
126
- till_number
127
- end
128
- end
66
+ option('till_number')
129
67
  end
130
68
  end
131
- end
69
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mpesa_stk/client'
4
+
5
+ module MpesaStk
6
+ # Create M-Pesa Ratiba standing orders (recurring payments).
7
+ class Ratiba < Client
8
+ class << self
9
+ def call(amount:, party_a:, start_date:, end_date:, **options)
10
+ new(**options, amount: amount, party_a: party_a, start_date: start_date, end_date: end_date).create
11
+ end
12
+ end
13
+
14
+ def create
15
+ post('ratiba_url', ratiba_payload, error_message: 'Failed to create standing order')
16
+ end
17
+
18
+ private
19
+
20
+ def ratiba_payload
21
+ {
22
+ StandingOrderName: @options.fetch(:standing_order_name, 'Standing Order'),
23
+ BusinessShortCode: option('business_short_code'),
24
+ TransactionType: @options.fetch(:transaction_type, 'Standing Order Customer Pay Bill'),
25
+ Amount: @options.fetch(:amount).to_s,
26
+ PartyA: @options.fetch(:party_a),
27
+ ReceiverPartyIdentifierType: @options.fetch(:receiver_party_identifier_type, '4'),
28
+ CallBackURL: option('callback_url'),
29
+ AccountReference: @options.fetch(:account_reference, ''),
30
+ TransactionDesc: @options.fetch(:transaction_desc, ''),
31
+ Frequency: @options.fetch(:frequency, '3'),
32
+ StartDate: @options.fetch(:start_date),
33
+ EndDate: @options.fetch(:end_date)
34
+ }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mpesa_stk/client'
4
+
5
+ module MpesaStk
6
+ # Reverse a completed M-Pesa transaction.
7
+ class Reversal < Client
8
+ class << self
9
+ def call(transaction_id, amount, **options)
10
+ new(transaction_id, amount, **options).reverse_transaction
11
+ end
12
+ end
13
+
14
+ attr_reader :transaction_id, :amount
15
+
16
+ def initialize(transaction_id, amount, **options)
17
+ super(**options)
18
+ @transaction_id = transaction_id
19
+ @amount = amount
20
+ end
21
+
22
+ def reverse_transaction
23
+ post(
24
+ 'reversal_url',
25
+ {
26
+ Initiator: option('initiator'),
27
+ SecurityCredential: option('security_credential'),
28
+ CommandID: 'TransactionReversal',
29
+ TransactionID: transaction_id,
30
+ Amount: amount.to_s,
31
+ ReceiverParty: option('business_short_code', :receiver_party),
32
+ RecieverIdentifierType: @options.fetch(:receiver_identifier_type, '4'),
33
+ ResultURL: option('result_url'),
34
+ QueueTimeOutURL: option('queue_timeout_url')
35
+ },
36
+ error_message: 'Failed to reverse transaction'
37
+ )
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mpesa_stk/client'
4
+
5
+ module MpesaStk
6
+ # Query the status of an STK Push checkout request.
7
+ class StkPushQuery < Client
8
+ class << self
9
+ def call(checkout_request_id, **options)
10
+ new(checkout_request_id, **options).query_status
11
+ end
12
+ end
13
+
14
+ attr_reader :checkout_request_id
15
+
16
+ def initialize(checkout_request_id, **options)
17
+ super(**options)
18
+ @checkout_request_id = checkout_request_id
19
+ end
20
+
21
+ def query_status
22
+ short_code = option('business_short_code')
23
+ passkey = option('business_passkey')
24
+ timestamp = stk_timestamp
25
+
26
+ post(
27
+ 'stk_push_query_url',
28
+ {
29
+ BusinessShortCode: short_code,
30
+ Password: stk_password(short_code, passkey, timestamp),
31
+ Timestamp: timestamp.to_s,
32
+ CheckoutRequestID: checkout_request_id
33
+ },
34
+ error_message: 'Failed to query STK push status'
35
+ )
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mpesa_stk/client'
4
+
5
+ module MpesaStk
6
+ # Query the status of any M-Pesa transaction by ID.
7
+ class TransactionStatus < Client
8
+ class << self
9
+ def call(transaction_id, **options)
10
+ new(transaction_id, **options).query_status
11
+ end
12
+ end
13
+
14
+ attr_reader :transaction_id
15
+
16
+ def initialize(transaction_id, **options)
17
+ super(**options)
18
+ @transaction_id = transaction_id
19
+ end
20
+
21
+ def query_status
22
+ post(
23
+ 'transaction_status_url',
24
+ {
25
+ Initiator: option('initiator'),
26
+ SecurityCredential: option('security_credential'),
27
+ CommandID: 'TransactionStatusQuery',
28
+ TransactionID: transaction_id,
29
+ PartyA: option('business_short_code', :party_a),
30
+ IdentifierType: @options.fetch(:identifier_type, '4'),
31
+ ResultURL: option('result_url'),
32
+ QueueTimeOutURL: option('queue_timeout_url')
33
+ },
34
+ error_message: 'Failed to query transaction status'
35
+ )
36
+ end
37
+ end
38
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MpesaStk
2
- VERSION = "1.3"
4
+ VERSION = '3.0.0'
3
5
  end
data/lib/mpesa_stk.rb CHANGED
@@ -1,5 +1,29 @@
1
- require "mpesa_stk/version"
2
- require 'mpesa_stk/push_payment'
1
+ # frozen_string_literal: true
2
+
3
+ require 'mpesa_stk/version'
4
+ require 'mpesa_stk/config'
5
+ require 'mpesa_stk/client'
6
+ require 'mpesa_stk/access_token'
3
7
  require 'mpesa_stk/push'
4
- require 'dotenv/load'
8
+ require 'mpesa_stk/transaction_status'
9
+ require 'mpesa_stk/stk_push_query'
10
+ require 'mpesa_stk/b2c'
11
+ require 'mpesa_stk/b2b'
12
+ require 'mpesa_stk/c2b'
13
+ require 'mpesa_stk/account_balance'
14
+ require 'mpesa_stk/reversal'
15
+ require 'mpesa_stk/ratiba'
16
+ require 'mpesa_stk/iot'
17
+ require 'mpesa_stk/imsi'
18
+ require 'mpesa_stk/pull_transactions'
19
+
20
+ require 'dotenv/load' unless ENV['MPESA_STK_SKIP_DOTENV'] == 'true'
5
21
  require 'httparty'
22
+ require 'securerandom'
23
+
24
+ # Ruby client for Safaricom Daraja M-Pesa APIs (STK Push, B2C, B2B, C2B, and related services).
25
+ module MpesaStk
26
+ def self.configure(&block)
27
+ Config.configure(&block)
28
+ end
29
+ end
data/mpesa_stk.gemspec CHANGED
@@ -1,36 +1,49 @@
1
+ # frozen_string_literal: true
1
2
 
2
- lib = File.expand_path("../lib", __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "mpesa_stk/version"
5
+ require 'mpesa_stk/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = "mpesa_stk"
8
+ spec.name = 'mpesa_stk'
8
9
  spec.version = MpesaStk::VERSION
9
- spec.authors = ["mboya", "cess"]
10
- spec.email = ["mboyaberry@gmail.com", "cessmbuguar@gmail.com"]
10
+ spec.authors = %w[mboya cess]
11
+ spec.email = ['mboyaberry@gmail.com', 'cessmbuguar@gmail.com']
11
12
 
12
- spec.summary = %q{Lipa na M-Pesa Online Payment.}
13
- spec.description = %q{initiate a M-Pesa transaction on behalf of a customer using STK Push.}
14
- spec.homepage = "https://github.com/mboya/mpesa_stk"
15
- spec.license = "MIT"
13
+ spec.summary = 'Lipa na M-Pesa Online Payment.'
14
+ spec.description = 'initiate a M-Pesa transaction on behalf of a customer using STK Push.'
15
+ spec.homepage = 'https://github.com/mboya/mpesa_stk'
16
+ spec.license = 'MIT'
16
17
 
17
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ spec.required_ruby_version = '>= 2.6.0'
19
+
20
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
21
  f.match(%r{^(test|spec|features)/})
19
22
  end
20
- spec.bindir = "exe"
23
+ spec.bindir = 'exe'
21
24
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
- spec.require_paths = ["lib"]
25
+ spec.require_paths = ['lib']
23
26
 
24
- spec.add_dependency 'httparty', '>= 0.15.6', '< 0.19.0'
25
- spec.add_dependency 'redis-rack', '~> 2.0', '>= 2.0.2'
27
+ spec.metadata['homepage_uri'] = spec.homepage
28
+ spec.metadata['source_code_uri'] = "#{spec.homepage}.git"
29
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/master/CHANGELOG.md"
30
+ spec.metadata['rubygems_mfa_required'] = 'true'
31
+
32
+ spec.add_dependency 'base64', '>= 0.1.0'
33
+ spec.add_dependency 'csv', '>= 3.0.0'
34
+ spec.add_dependency 'httparty', '>= 0.15.6', '< 0.25.0'
35
+ spec.add_dependency 'redis', '>= 4.0'
26
36
  spec.add_dependency 'redis-namespace', '~> 1.5', '>= 1.5.3'
37
+ spec.add_dependency 'redis-rack', '~> 2.0', '>= 2.0.2'
27
38
 
28
- spec.add_development_dependency "bundler"
29
- spec.add_development_dependency "rake", ">= 12.3.3"
30
- spec.add_development_dependency "minitest", "~> 5.0"
39
+ spec.add_development_dependency 'bundler', '~> 2.0'
40
+ spec.add_development_dependency 'minitest', '~> 5.20'
41
+ spec.add_development_dependency 'rake', '>= 12.3.3'
31
42
 
32
- spec.add_development_dependency 'pry', '~> 0.10.4'
33
- spec.add_development_dependency 'pry-nav', '~> 0.2.4'
34
- spec.add_development_dependency 'webmock', '~> 3.0', '>= 3.0.1'
35
- spec.add_development_dependency "dotenv", "2.7.5"
43
+ spec.add_development_dependency 'dotenv', '~> 3.0'
44
+ spec.add_development_dependency 'pry', '~> 0.14'
45
+ spec.add_development_dependency 'pry-nav', '~> 1.0'
46
+ spec.add_development_dependency 'rubocop', '~> 1.0'
47
+ spec.add_development_dependency 'simplecov', '~> 0.22'
48
+ spec.add_development_dependency 'webmock', '~> 3.18'
36
49
  end