ruby-daraja 0.1.0.pre.alpha

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 54d32eb80ef95e66f06eb67cafd137afb492c5075838214b4b84a623132234b2
4
+ data.tar.gz: 6c91f7aa3e0574c532a93edbec21c65c6873c42fbf5377e5c7d3f4fc150a1252
5
+ SHA512:
6
+ metadata.gz: ccb350036f1e564249f1a95892c08837c5bb6fe55fe1f55f78b53c725c0dab06dca18a93fd5465a33ce3c36f6ed0ea751feebebf58356e37382c1bab5932c8b2
7
+ data.tar.gz: 488ed6272db7dcb62fc1e4a9486d3cbbcb261d3fabe326e535d73210c8f53d642d6c1d1beff022c7f5d3ed157761ae31b070429ffd44d1ba820bd6dbe1c147ba
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,90 @@
1
+ # require other spec gems
2
+ require: rubocop-rspec
3
+
4
+ # AllCops configuration
5
+ AllCops:
6
+ NewCops: enable
7
+ # Exclude the vendor directory and any generated files
8
+ Exclude:
9
+ - 'vendor/**/*'
10
+ - '**/*.rdoc'
11
+ - '**/*.md'
12
+ - '**/Gemfile'
13
+ - '**/Rakefile'
14
+ - '**/Guardfile'
15
+ - '**/config.ru'
16
+
17
+ # Style configuration
18
+ Style/Documentation:
19
+ Enabled: true
20
+ Exclude:
21
+ - 'lib/daraja_api_wrapper/version.rb'
22
+
23
+ # Lint configuration
24
+ Lint/AmbiguousRegexpLiteral:
25
+ Enabled: true
26
+ Lint/BlockAlignment:
27
+ Enabled: true
28
+ Lint/DeprecatedClassMethods:
29
+ Enabled: true
30
+ Lint/RedundantCopDisableDirective:
31
+ Enabled: true
32
+ Lint/ShadowingOuterLocalVariable:
33
+ Enabled: true
34
+
35
+ # Metrics configuration
36
+ Metrics/BlockLength:
37
+ Enabled: true
38
+ Max: 20
39
+ Metrics/CyclomaticComplexity:
40
+ Enabled: true
41
+ Max: 10
42
+ Metrics/LineLength:
43
+ Enabled: true
44
+ Max: 120
45
+ Metrics/MethodLength:
46
+ Enabled: true
47
+ Max: 20
48
+ Metrics/PerceivedComplexity:
49
+ Enabled: true
50
+ Max: 10
51
+ Metrics/ParameterLists:
52
+ Enabled: true
53
+ Max: 10
54
+
55
+ # Naming configuration
56
+ Naming/AccessorMethodName:
57
+ Enabled: true
58
+ Naming/MethodName:
59
+ Enabled: true
60
+ Naming/PredicateName:
61
+ Enabled: true
62
+
63
+
64
+ # RSpec configuration
65
+ RSpec/ExampleLength:
66
+ Enabled: true
67
+ Max: 10
68
+ RSpec/BeforeAfterAll:
69
+ Enabled: true
70
+ RSpec/DescribeClass:
71
+ Enabled: true
72
+ RSpec/ExpectActual:
73
+ Enabled: true
74
+ RSpec/ImplicitExpect:
75
+ Enabled: true
76
+ RSpec/LetBeforeExamples:
77
+ Enabled: true
78
+ RSpec/MultipleExpectations:
79
+ Enabled: true
80
+ Max: 3
81
+ RSpec/NestedGroups:
82
+ Enabled: true
83
+ RSpec/RepeatedDescription:
84
+ Enabled: true
85
+ RSpec/SingleArgumentMessageChain:
86
+ Enabled: true
87
+ RSpec/SubjectStub:
88
+ Enabled: true
89
+ RSpec/VerifiedDoubles:
90
+ Enabled: true
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2023-04-18
4
+
5
+ - Initial release
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at okumu.otsembo@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in ruby-daraja.gemspec
6
+ gemspec
7
+
8
+ gem "faraday"
9
+ gem "rake", "~> 13.0"
10
+ gem "rspec", "~> 3.0"
11
+
12
+ gem 'rubocop-rspec', require: false
13
+ gem "rubocop", "~> 1.21", require: false
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 otsembo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # Ruby::Daraja
2
+
3
+ `Ruby::Daraja` is a Ruby wrapper for the [Safaricom Daraja API](https://developer.safaricom.co.ke). It provides a simple interface to the API endpoints and allows for smooth setup in `Ruby on Rails` applications or any other Ruby application.
4
+
5
+ ## Installation
6
+
7
+ **IMPORTANT**: This gem is not yet released to [rubygems.org](https://rubygems.org). The instructions below are for when the gem is released.
8
+
9
+ Install the gem and add to the application's Gemfile by executing:
10
+
11
+ $ bundle add ruby-daraja
12
+
13
+ If bundler is not being used to manage dependencies, install the gem by executing:
14
+
15
+ $ gem install ruby-daraja
16
+
17
+ # Usage
18
+ With `ruby-daraja` you can easily make requests to the Safaricom Daraja API. The DSL is simple and easy to use. The DSL is divided into two parts:
19
+
20
+ 1. [Configuration](#configuration)
21
+ 2. [Requests](#requests)
22
+ 1. [Register C2B URLs](#register-c2b-urls)
23
+ 2. [MPESA PayBill](#mpesa-paybill)
24
+ 3. [MPESA Buy Goods And Services](#mpesa-buy-goods-and-services)
25
+ ## Configuration
26
+ In order to get started with the DSL, you need to configure the gem with your credentials.
27
+
28
+ There are two ways to get started with `ruby-daraja`:
29
+ 1. [Development](#development)
30
+ 2. [Production](#production)
31
+
32
+
33
+ ### Development
34
+ This is a quick-start mode for the gem. It is meant to get you up and running quickly. It is not recommended for `production` applications. To get started with the gem in `development` mode, follow the steps below:
35
+
36
+ 1. Setup environment variables needed for your application. The environment variables needed are:
37
+ ```shell
38
+ # mandatory variables
39
+ CONSUMER_KEY='daraja consumer key here'
40
+ CONSUMER_SECRET='daraja consumer secret here'
41
+
42
+ # optional variables
43
+ VALIDATION_URL='C2B Validation URL'
44
+ CONFIRMATION_URL='C2B Confirmation URL'
45
+ IS_SANDBOX='boolean indicating whether you are in sandbox or production'
46
+ SHORT_CODE='daraja (paybill /till number) short code here'
47
+ B2C_RESULT_URL='B2C Result URL'
48
+ INITIATOR_NAME='B2C initiator name'
49
+ INITIATOR_PASSWORD='B2C initiator password'
50
+ SSL_CERTIFICATE='path to SSL certificate'
51
+ BALANCE_RESULT_URL='Balance Result URL'
52
+ ```
53
+ 2. You will need a ```Daraja::AppConfig``` instance. This instance is used to configure the application. It is created as follows:
54
+ ```ruby
55
+ require 'ruby-daraja'
56
+
57
+ app_config = Daraja::Default.new._app_config
58
+ ```
59
+
60
+ ### Production
61
+ `TODO: Add instructions for production mode`
62
+
63
+ ## Requests
64
+
65
+ ### Register C2B URLs
66
+
67
+ 1. Register C2B URLs - This needs to be done only once for each set of URLs.
68
+ ```ruby
69
+ require 'ruby-daraja'
70
+
71
+ app_config = `Daraja::AppConfig instance here`
72
+ responses = app_config.register_urls # array of JSON responses from Safaricom Daraja API
73
+ ```
74
+
75
+ ### MPESA PayBill
76
+
77
+ 1. Create a new `Daraja::PayBill` instance.
78
+ ```ruby
79
+ require 'ruby-daraja'
80
+
81
+ app_config = `Daraja::AppConfig instance here`
82
+ paybill_client = Daraja::PayBill.new(config: app_config, pass_key: 'pass key here')
83
+ ```
84
+ 2. Initiate Payment Request (STK Push) for `MPESA PayBill`.
85
+ ```ruby
86
+ response = paybill_client.initiate_stk_push(
87
+ amount: 1,
88
+ phone_number: '2547xxxxxxxx',
89
+ account_reference: 'account reference',
90
+ transaction_description: 'transaction description'
91
+ ) # JSON response from Safaricom Daraja API
92
+ ```
93
+
94
+ 3. Initiate B2C Request for `MPESA PayBill`
95
+ ```ruby
96
+ response = paybill_client.initiate_b2c(
97
+ amount: 1,
98
+ phone_number: '2547xxxxxxxx',
99
+ type: 0, # 0 for business, 1 for salary, 2 for promotion
100
+ remarks: 'remarks',
101
+ occasion: 'occasion'
102
+ ) # JSON response from Safaricom Daraja API
103
+ ```
104
+
105
+ 4. Check balance for `MPESA PayBill`
106
+ ```ruby
107
+ response = paybill_client.initiate_balance_request(
108
+ remarks: 'remarks'
109
+ ) # JSON response from Safaricom Daraja API
110
+ ```
111
+
112
+ ### MPESA Buy Goods And Services
113
+
114
+ 1. Create a new `Daraja::BuyGoods` instance.
115
+ ```ruby
116
+ require 'ruby-daraja'
117
+
118
+ app_config = `Daraja::AppConfig instance here`
119
+ till_client = Daraja::BuyGoods.new(config: app_config, pass_key: 'pass key here')
120
+ ```
121
+
122
+ 2. Initiate Payment Request (STK Push) for `MPESA Buy Goods And Services`.
123
+ ```ruby
124
+ response = till_client.initiate_stk_push(
125
+ amount: 1,
126
+ phone_number: '2547xxxxxxxx',
127
+ account_reference: 'account reference',
128
+ transaction_description: 'transaction description'
129
+ ) # JSON response from Safaricom Daraja API
130
+ ```
131
+
132
+ 3. Initiate B2C Request for `MPESA Buy Goods And Services`
133
+ ```ruby
134
+ response = till_client.initiate_b2c(
135
+ amount: 1,
136
+ phone_number: '2547xxxxxxxx',
137
+ type: 0, # 0 for business, 1 for salary, 2 for promotion
138
+ remarks: 'remarks',
139
+ occasion: 'occasion'
140
+ ) # JSON response from Safaricom Daraja API
141
+ ```
142
+
143
+ 4. Check balance for `MPESA Buy Goods And Services`
144
+ ```ruby
145
+ response = till_client.initiate_balance_request(
146
+ remarks: 'remarks'
147
+ ) # JSON response from Safaricom Daraja API
148
+ ```
149
+
150
+ ## Development
151
+
152
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
153
+
154
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
155
+
156
+ ## Contributing
157
+
158
+ Bug reports and pull requests are welcome on GitHub at https://github.com/otsembo/ruby-daraja. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/ruby-daraja/blob/master/CODE_OF_CONDUCT.md).
159
+
160
+ ## License
161
+
162
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
163
+
164
+ ## Code of Conduct
165
+
166
+ Everyone interacting in the Ruby::Daraja project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/ruby-daraja/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Application Utilities
4
+ module AppUtils
5
+ # base configuration class
6
+ class BaseConfig
7
+ attr_accessor :connection
8
+ attr_writer :is_sandbox
9
+
10
+ def initialize(is_sandbox: false)
11
+ @is_sandbox = is_sandbox
12
+ @connection = Faraday.new(url: toggle_base_url, headers: { 'Content-Type' => 'application/json' }) do |f|
13
+ f.request :url_encoded
14
+ f.adapter Faraday.default_adapter
15
+ end
16
+ end
17
+
18
+ # toggle between sandbox and production daraja API urls
19
+ # @return [String] url
20
+ def toggle_base_url
21
+ if @is_sandbox
22
+ 'https://sandbox.safaricom.co.ke/'
23
+ else
24
+ 'https://api.safaricom.co.ke/'
25
+ end
26
+ end
27
+ end
28
+
29
+ # Default utility class
30
+ # @return [Default] default
31
+ class Default
32
+ # create Faraday connection object
33
+ # @return [Faraday::Connection] conn
34
+ def _connection
35
+ Faraday.new(url: _toggle_base_url, headers: { 'Content-Type' => 'application/json' }) do |f|
36
+ f.request :url_encoded
37
+ f.adapter Faraday.default_adapter
38
+ end
39
+ end
40
+
41
+ # toggle between sandbox and production daraja API urls
42
+ # @return [String] url
43
+ def _toggle_base_url
44
+ if _is_sandbox
45
+ 'https://sandbox.safaricom.co.ke/'
46
+ else
47
+ 'https://api.safaricom.co.ke/'
48
+ end
49
+ end
50
+
51
+ # APP OBJECTS
52
+ # create a new instance of DarajaAuthProvider class in sandbox mode
53
+ def _auth_provider
54
+ DarajaAuthProvider.create(
55
+ key: _consumer_key,
56
+ secret: _consumer_secret,
57
+ is_sandbox: _is_sandbox
58
+ )
59
+ end
60
+
61
+ # create a new instance of AppConfig class in sandbox mode
62
+ # @return [AppConfig] app_config
63
+ def _app_config
64
+ Pay::AppConfig.setup(
65
+ provider: _auth_provider,
66
+ short_code: _short_code,
67
+ validation_url: _validation_url,
68
+ confirmation_url: _confirmation_url,
69
+ b2c_result_url: _b2c_result_url,
70
+ initiator_name: _initiator_name,
71
+ initiator_password: _initiator_password,
72
+ ssl_certificate: _ssl_certificate,
73
+ balance_result_url: _balance_result_url
74
+ )
75
+ end
76
+
77
+ ## FETCH ENVIRONMENT VARIABLES
78
+ # fetch consumer key from environment variables
79
+ # @return [String] consumer_key
80
+ # @raise [RuntimeError] if consumer key is not set
81
+ def _consumer_key
82
+ ENV.fetch('CONSUMER_KEY') do
83
+ raise 'Consumer Key not set'
84
+ end
85
+ end
86
+
87
+ # fetch consumer secret from environment variables
88
+ # @return [String] consumer_secret
89
+ # @raise [RuntimeError] if consumer secret is not set
90
+ def _consumer_secret
91
+ ENV.fetch('CONSUMER_SECRET') do
92
+ raise 'Consumer Secret not set'
93
+ end
94
+ end
95
+
96
+ # fetch Validation URL from environment variables
97
+ # @return [String] validation_url
98
+ # @raise [RuntimeError] if validation url is not set
99
+ def _validation_url
100
+ ENV.fetch('VALIDATION_URL') do
101
+ raise 'Validation URL not set'
102
+ end
103
+ end
104
+
105
+ # fetch Confirmation URL from environment variables
106
+ # @return [String] confirmation_url
107
+ # @raise [RuntimeError] if confirmation url is not set
108
+ def _confirmation_url
109
+ ENV.fetch('CONFIRMATION_URL') do
110
+ raise 'Confirmation URL not set'
111
+ end
112
+ end
113
+
114
+ # fetch B2C Result URL from environment variables
115
+ # @return [String] b2c_result_url
116
+ # @return [nil] if b2c result url is not set
117
+ def _b2c_result_url
118
+ ENV.fetch('B2C_RESULT_URL', nil)
119
+ end
120
+
121
+ # fetch Balance Result URL from environment variables
122
+ # @return [String] balance_result_url
123
+ # @return [nil] if balance result url is not set
124
+ def _balance_result_url
125
+ ENV.fetch('BALANCE_RESULT_URL', nil)
126
+ end
127
+
128
+ # fetch sandbox mode from environment variables
129
+ # @return [Boolean] is_sandbox
130
+ # default: true
131
+ def _is_sandbox
132
+ ENV.fetch('IS_SANDBOX', true)
133
+ end
134
+
135
+ # fetch short code from environment variables
136
+ # @return [String] short_code
137
+ # @raise [RuntimeError] if short code is not set
138
+ def _short_code
139
+ ENV.fetch('SHORT_CODE') do
140
+ raise 'Short Code not set'
141
+ end
142
+ end
143
+
144
+ # fetch Initiator Name from environment variables
145
+ # @return [String] initiator_name
146
+ # @raise [RuntimeError] if initiator name is not set
147
+ def _initiator_name
148
+ ENV.fetch('INITIATOR_NAME') do
149
+ raise 'Initiator Name not set'
150
+ end
151
+ end
152
+
153
+ # fetch Initiator Password from environment variables
154
+ # @return [String] initiator_password
155
+ # @return [nil] if initiator password is not set
156
+ def _initiator_password
157
+ ENV.fetch('INITIATOR_PASSWORD', nil)
158
+ end
159
+
160
+ # fetch SSL Certificate from environment variables
161
+ # @return [String] ssl_certificate
162
+ # @return [nil] if ssl certificate is not set
163
+ def _ssl_certificate
164
+ ENV.fetch('SSL_CERTIFICATE', nil)
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Daraja
5
+ VERSION = '0.1.0-alpha'
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'daraja/version'
4
+ require_relative 'daraja_auth_provider'
5
+ require_relative 'pay'
6
+
7
+ # Main App module that has the Daraja Wrapper.
8
+ module Daraja
9
+ include AppUtils
10
+ include Pay
11
+ class Error < StandardError; end
12
+
13
+ # import all externally defined classes
14
+ class AuthProvider < DarajaAuthProvider; end
15
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'faraday'
5
+ require_relative 'app_utils'
6
+ # Create Access Token for Daraja Requests
7
+ class DarajaAuthProvider < AppUtils::BaseConfig
8
+ attr_accessor :url, :token, :consumer_key, :consumer_secret
9
+ attr_reader :request_token, :is_sandbox
10
+
11
+ def initialize(consumer_key: nil, consumer_secret: nil, is_sandbox: false)
12
+ super(is_sandbox: is_sandbox)
13
+ @consumer_key = consumer_key
14
+ @consumer_secret = consumer_secret
15
+ @request_token = create_auth_token
16
+ @token = fetch_token
17
+ end
18
+
19
+ # create a new instance of DarajaAuthProvider class in sandbox mode (default)
20
+ # @return [DarajaAuthProvider] auth_provider
21
+ def self.create(key: nil, secret: nil, is_sandbox: true)
22
+ DarajaAuthProvider.new(consumer_key: key, consumer_secret: secret, is_sandbox: is_sandbox)
23
+ end
24
+
25
+ private
26
+
27
+ # create an authorization token from consumer key and secret
28
+ # @return [String] token
29
+ def create_auth_token
30
+ key = @consumer_key
31
+ secret = @consumer_secret
32
+ Base64.strict_encode64("#{key}:#{secret}")
33
+ end
34
+
35
+ # fetch authorization token from daraja API
36
+ # @return [String] token
37
+ def fetch_token
38
+ response = @connection.get('/oauth/v1/generate?grant_type=client_credentials') do |req|
39
+ req.headers['Authorization'] = "Basic #{@request_token}"
40
+ end
41
+ begin
42
+ @token = JSON.parse(response.body)['access_token']
43
+ rescue JSON::ParserError => _e
44
+ return nil
45
+ end
46
+ @token.to_str
47
+ end
48
+ end
data/lib/ruby/pay.rb ADDED
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require_relative 'payment'
5
+
6
+ # Main module for processing requests
7
+ module Pay
8
+ # AppConfig class
9
+ class AppConfig < AppUtils::BaseConfig
10
+ attr_accessor :short_code,
11
+ :confirmation_url,
12
+ :validation_url,
13
+ :b2c_result_url,
14
+ :initiator_name,
15
+ :initiator_password,
16
+ :ssl_certificate,
17
+ :balance_result_url
18
+ attr_reader :provider
19
+
20
+ def initialize(
21
+ provider: DarajaAuthProvider.create,
22
+ short_code: nil,
23
+ confirmation_url: nil,
24
+ validation_url: nil,
25
+ b2c_result_url: nil,
26
+ initiator_name: nil,
27
+ initiator_password: nil,
28
+ ssl_certificate: nil,
29
+ balance_result_url: nil
30
+ )
31
+ super(is_sandbox: provider.is_sandbox)
32
+ @short_code = short_code
33
+ @confirmation_url = confirmation_url
34
+ @validation_url = validation_url
35
+ @provider = provider
36
+ @b2c_result_url = b2c_result_url
37
+ @initiator_name = initiator_name
38
+ @initiator_password = initiator_password
39
+ @ssl_certificate = ssl_certificate
40
+ @balance_result_url = balance_result_url
41
+ end
42
+
43
+ # receive all inputs for payment setup
44
+ def self.setup(
45
+ provider: DarajaAuthProvider.create,
46
+ short_code: nil,
47
+ confirmation_url: nil,
48
+ validation_url: nil,
49
+ b2c_result_url: nil,
50
+ initiator_name: nil,
51
+ initiator_password: nil,
52
+ ssl_certificate: nil,
53
+ balance_result_url: nil
54
+ )
55
+ AppConfig.new(provider: provider,
56
+ short_code: short_code,
57
+ confirmation_url: confirmation_url,
58
+ validation_url: validation_url,
59
+ b2c_result_url: b2c_result_url,
60
+ initiator_name: initiator_name,
61
+ initiator_password: initiator_password,
62
+ ssl_certificate: ssl_certificate,
63
+ balance_result_url: balance_result_url)
64
+ end
65
+
66
+ # register all request types (fail / success) urls
67
+ # @return [Array] response
68
+ def register_urls
69
+ %w[Completed Cancelled].map { |status| setup_status(status) }
70
+ end
71
+
72
+ private
73
+
74
+ # register url
75
+ # @param [String] status
76
+ # @return [Hash] response
77
+ def setup_status(status)
78
+ response = @connection.post('/mpesa/c2b/v1/registerurl') do |req|
79
+ req.headers['Authorization'] = "Basic #{@provider.token}"
80
+ req.body = {
81
+ ShortCode: @short_code,
82
+ ResponseType: status,
83
+ ConfirmationURL: @confirmation_url,
84
+ ValidationURL: @validation_url
85
+ }.to_json
86
+ end
87
+ JSON.parse(response.body)
88
+ end
89
+ end
90
+
91
+ # MPESA Pay Bill Online (C2B / B2C)
92
+ class PayBill < Payment
93
+ def initialize(config: AppConfig.new, pass_key: nil)
94
+ super(config: config, pass_key: pass_key, push_type: 0)
95
+ end
96
+ end
97
+
98
+ # MPESA Buy Goods Online (C2B / B2C)
99
+ class BuyGoods < Payment
100
+ def initialize(config: AppConfig.new, pass_key: nil)
101
+ super(config: config, pass_key: pass_key, push_type: 1)
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ # initialize Payment class
4
+ class Payment
5
+ attr_reader :config, :payment_credentials, :push_type
6
+ attr_accessor :pass_key
7
+
8
+ C2B = %w[CustomerPayBillOnline CustomerBuyGoodsOnline].freeze
9
+ B2C = %w[BusinessPayment SalaryPayment PromotionPayment].freeze
10
+ def initialize(config: AppConfig.new, pass_key: nil, push_type: 0)
11
+ @config = config
12
+ @pass_key = pass_key
13
+ @push_type = push_type
14
+ @payment_credentials = encode_password
15
+ end
16
+
17
+ # send STK Push request
18
+ # @param [String] phone_number
19
+ # @param [String] amount
20
+ # @param [String] reference
21
+ # @param [String] description
22
+ # @return [Hash] response
23
+ def initiate_stk_push(phone_number:, amount:, reference:, description:)
24
+ response = @connection.post('/mpesa/stkpush/v1/processrequest') do |req|
25
+ req.headers['Authorization'] = "Basic #{@config.provider.token}"
26
+ req.body = {
27
+ BusinessShortCode: @config.short_code,
28
+ Password: @payment_credentials.password,
29
+ Timestamp: @payment_credentials.timestamp,
30
+ TransactionType: C2B[@push_type],
31
+ Amount: amount,
32
+ PartyA: phone_number,
33
+ PartyB: @config.short_code,
34
+ PhoneNumber: phone_number,
35
+ CallBackURL: @config.confirmation_url,
36
+ AccountReference: reference,
37
+ TransactionDesc: description
38
+ }.to_json
39
+ end
40
+ JSON.parse(response.body)
41
+ end
42
+
43
+ # send B2C request
44
+ # @param [String] phone_number
45
+ # @param [String] amount
46
+ # @param [String] remarks
47
+ # @param [int] type
48
+ # @param [String|nil] occasion
49
+ # @return [Hash<String, String|nil>] response
50
+ def initiate_b2c(phone_number:, amount:, remarks:, type: 0, occasion: nil)
51
+ response = @connection.post('/mpesa/b2c/v1/paymentrequest') do |req|
52
+ req.headers['Authorization'] = "Basic #{@config.provider.token}"
53
+ req.body = {
54
+ InitiatorName: @config.initiator_name,
55
+ SecurityCredential: security_credential,
56
+ CommandID: B2C[type],
57
+ Amount: amount,
58
+ PartyA: @config.short_code,
59
+ PartyB: phone_number,
60
+ Remarks: remarks,
61
+ QueueTimeOutURL: @config.b2c_result_url,
62
+ ResultURL: @config.b2c_result_url,
63
+ Occasion: occasion
64
+ }.to_json
65
+ end
66
+ JSON.parse(response)
67
+ end
68
+
69
+ # initiate balance request
70
+ # @param [String?] remarks
71
+ # @return [Hash<String, String|nil>] response
72
+ def initiate_balance_request(remarks: nil)
73
+ response = @connection.post('/mpesa/accountbalance/v1/query') do |req|
74
+ req.headers['Authorization'] = "Basic #{@config.provider.token}"
75
+ req.body = {
76
+ Initiator: @config.initiator_name,
77
+ SecurityCredential: security_credential,
78
+ CommandID: 'AccountBalance',
79
+ PartyA: @config.short_code,
80
+ IdentifierType: 4,
81
+ Remarks: remarks,
82
+ QueueTimeOutURL: @config.balance_result_url,
83
+ ResultURL: @config.balance_result_url
84
+ }.to_json
85
+ end
86
+ JSON.parse(response)
87
+ end
88
+
89
+ private
90
+
91
+ def encode_password
92
+ timestamp = Time.now.strftime('%Y%m%d%H%M%S').to_s.to_i
93
+ {
94
+ timestamp: timestamp,
95
+ password: Base64.strict_encode64("#{@config.short_code}#{@pass_key}#{timestamp}")
96
+ }
97
+ end
98
+
99
+ def security_credential
100
+ Base64.strict_encode64(OpenSSL::HMAC.hexdigest('sha256', @config.initiator_password, @config.ssl_certificate))
101
+ end
102
+ end
@@ -0,0 +1,3 @@
1
+ module AppUtils
2
+
3
+ end
data/sig/app_utils.rbs ADDED
@@ -0,0 +1,26 @@
1
+ module AppUtils
2
+ class BaseConfig
3
+ attr_accessor connection: untyped
4
+ attr_writer is_sandbox: bool
5
+
6
+ def toggle_base_url: -> string
7
+ end
8
+
9
+ class Default
10
+ def _balance_result_url: -> string
11
+ def _toggle_base_url : () -> void
12
+ def _connection: -> untyped
13
+ def _short_code: -> string
14
+ def _ssl_certificate: -> string
15
+ def _auth_provider: -> DarajaAuthProvider
16
+ def _app_config: -> BaseConfig
17
+ def _consumer_key: -> string
18
+ def _consumer_secret: -> string
19
+ def _validation_url: -> string
20
+ def _confirmation_url: -> string
21
+ def _b2c_result_url: -> string
22
+ def _initiator_name: -> string
23
+ def _initiator_password: -> string
24
+ def _is_sandbox: -> bool
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ class DarajaAuthProvider
2
+ attr_accessor connection: untyped
3
+ attr_reader is_sandbox: bool
4
+ attr_accessor url: string | nil
5
+ attr_accessor token: string
6
+ attr_writer is_sandbox: bool
7
+ attr_accessor consumer_key: string | nil
8
+ attr_accessor consumer_secret: string | nil
9
+ attr_reader request_token: string | nil
10
+
11
+ def fetch_token : () -> string
12
+ def create_auth_token : () -> string?
13
+ def toggle_base_url : () -> void
14
+ def self.create : (key: string?, secret: string?, is_sandbox: bool?) -> DarajaAuthProvider
15
+
16
+ end
@@ -0,0 +1,22 @@
1
+ module Pay
2
+ class AppConfig
3
+ attr_reader provider: DarajaAuthProvider
4
+ attr_accessor short_code: int|nil
5
+ attr_accessor confirmation_url: string|nil
6
+ attr_accessor ssl_certificate: string|nil
7
+ attr_accessor validation_url: string|nil
8
+ attr_accessor b2c_result_url: string|nil
9
+ attr_accessor balance_result_url: string|nil
10
+ attr_accessor initiator_name: string|nil
11
+ attr_accessor initiator_password: string|nil
12
+
13
+ def self.setup : () -> Pay::AppConfig
14
+ def register_urls: () -> Array[url_registration_response]
15
+ def setup_status: () -> url_registration_response
16
+ type url_registration_response = {
17
+ ConversationId: string,
18
+ OriginatorCoversationID: string,
19
+ ResponseDescription: string,
20
+ }
21
+ end
22
+ end
data/sig/payment.rbs ADDED
@@ -0,0 +1,20 @@
1
+ class Payment
2
+ include AppUtils
3
+
4
+ C2B: Array[string]
5
+ B2C: Array[string]
6
+
7
+ attr_reader config: AppUtils::BaseConfig
8
+ attr_accessor pass_key: string|nil
9
+ attr_reader payment_credentials: { timestamp: string, password: string }
10
+ attr_reader push_type: int
11
+
12
+ def initiate_balance_request: -> Hash[String, String|int]
13
+ def initiate_stk_push: -> Hash[String, String|int]
14
+ def initiate_b2c: -> Hash[String, String|int]
15
+
16
+ private
17
+
18
+ def encode_password: -> { timestamp: string, password: string }
19
+ def security_credential: -> string
20
+ end
@@ -0,0 +1,6 @@
1
+ module Ruby
2
+ module Daraja
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-daraja
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre.alpha
5
+ platform: ruby
6
+ authors:
7
+ - otsembo
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-05-23 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |-
14
+ The gem abstracts away the low-level details of the API, providing a simple and intuitive
15
+ interface for sending payment requests, checking payment status, and managing payment callbacks
16
+ email:
17
+ - okumu.otsembo@gmail.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - ".rspec"
23
+ - ".rubocop.yml"
24
+ - CHANGELOG.md
25
+ - CODE_OF_CONDUCT.md
26
+ - Gemfile
27
+ - LICENSE.txt
28
+ - README.md
29
+ - Rakefile
30
+ - lib/ruby/app_utils.rb
31
+ - lib/ruby/daraja.rb
32
+ - lib/ruby/daraja/version.rb
33
+ - lib/ruby/daraja_auth_provider.rb
34
+ - lib/ruby/pay.rb
35
+ - lib/ruby/payment.rb
36
+ - sig/app_utils.rbs
37
+ - sig/app_utils/base_config.rbs
38
+ - sig/daraja_auth_provider.rbs
39
+ - sig/pay/app_config.rbs
40
+ - sig/payment.rbs
41
+ - sig/ruby/daraja.rbs
42
+ homepage: https://github.com/otsembo/ruby-daraja
43
+ licenses:
44
+ - MIT
45
+ metadata:
46
+ rubygems_mfa_required: 'true'
47
+ homepage_uri: https://github.com/otsembo/ruby-daraja
48
+ source_code_uri: https://github.com/otsembo/ruby-daraja
49
+ changelog_uri: https://github.com/otsembo/ruby-daraja/blob/main/CHANGELOG.md
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 2.7.0
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">"
62
+ - !ruby/object:Gem::Version
63
+ version: 1.3.1
64
+ requirements: []
65
+ rubygems_version: 3.3.5
66
+ signing_key:
67
+ specification_version: 4
68
+ summary: A simple gem that allows implements smooth
69
+ test_files: []