sadad 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 54fb49202126ef2f736a0b0453132aacd8ec0c38ffdce64eb77b66e3cff580d7
4
+ data.tar.gz: 4bceae03cebf3de09de544649eb603c5e20eda8371dd28004a09768fa90a104c
5
+ SHA512:
6
+ metadata.gz: c19a0cba2a92d66c03741d4183167c18659d56c7f07a1ebd1852485fc2561b024fab8273cde210f4740d8a24903bc9bf15da1c75d1b7d9853f95a140d1a7776b
7
+ data.tar.gz: db66ce797a97366442ce423da19b6ac28cc16a69a503f23b4d6bbd573fd80b249af51ab3ea1a9830841fa0d348f392e257a2d1dae6e7c766dd7ef5e35ca32789
data/README.md ADDED
@@ -0,0 +1,209 @@
1
+ # Sadad Payments Gem
2
+
3
+ Ruby gem for integrating with Sadad payment gateway. This gem provides complete functionality for creating and managing bills through Sadad's API.
4
+
5
+ ## Table of Contents
6
+ 1. [Installation](#installation)
7
+ 2. [Configuration](#configuration)
8
+ 3. [API Usage](#api-usage)
9
+ 1. [Single Bill Operations](#single-bill-operations)
10
+ 1. [Create Bill](#create-bill)
11
+ 2. [Update Bill](#update-bill)
12
+ 3. [Inquiry Bill](#inquiry-bill)
13
+ 4. [Expire Bill](#expire-bill)
14
+ 2. [Multiple Bills Operations](#multiple-bills-operations)
15
+ 1. [Create Bills](#create-bills)
16
+ 2. [Deactivate Bill](#deactivate-bill)
17
+ 4. [Error Handling](#error-handling)
18
+ 5. [Contributing](#contributing)
19
+ 6. [License](#license)
20
+
21
+
22
+ ## Installation
23
+
24
+ Install the gem and add to the application's Gemfile by executing:
25
+
26
+ $ bundle add sadad
27
+
28
+ If bundler is not being used to manage dependencies, install the gem by executing:
29
+
30
+ $ gem install sadad
31
+
32
+
33
+ Next, you need to run the generator:
34
+
35
+ $ rails generate sadad:install
36
+
37
+ **Note:** This will create a `config/initializers/sadad.rb` file with the default configuration.
38
+
39
+
40
+ ## Configuration
41
+
42
+ Configure the gem with your configuration
43
+
44
+ ```ruby
45
+ Sadad.configure do |config|
46
+ config.merchant_id = "merchant_id"
47
+ config.program_id = "program_id"
48
+ config.client_certificate_pem_file = "client_certificate_pem_file"
49
+ end
50
+ ```
51
+
52
+ For `merchant_id`, `program_id`, and `client_certificate_pem_file`, you can set its value in 3 ways
53
+ - Global across whole application (through initializer and configuration)
54
+ ```ruby
55
+ Sadad.configure do |config|
56
+ config.merchant_id = "merchant_id"
57
+ config.program_id = "program_id"
58
+ config.client_certificate_pem_file = "client_certificate_pem_file"
59
+ end
60
+ ```
61
+
62
+ - Per method or block (through the application variables). All consecutive requests will use that token until it is set to a different value
63
+ ```ruby
64
+ Sadad.merchant_id = "merchant_id"
65
+ Sadad.program_id = "program_id"
66
+ Sadad.client_certificate_pem_file = "client_certificate_pem_file"
67
+ ```
68
+
69
+ - Per request (through opts parameter)
70
+ ```ruby
71
+ Sadad::Bills.create(opts: { merchant_id: "merchant_id", program_id: "program_id", client_certificate_pem_file: "client_certificate_pem_file" })
72
+ ```
73
+
74
+
75
+ ## API Usage
76
+
77
+ Any API call will return an object with following methods:
78
+
79
+ ```ruby
80
+ result = Sadad.doSomething
81
+ result.success?
82
+ result.failure?
83
+ result.payload
84
+ result.error
85
+ ```
86
+
87
+ ### Single Bill Operations
88
+
89
+ #### Create Bill
90
+
91
+ ```ruby
92
+ Sadad::SingleBill.create(
93
+ user_national_id: "1133331101",
94
+ user_first_name: "Khaled",
95
+ user_last_name: "Ahmed",
96
+ invoice_id: "1998654321",
97
+ bill_type: "OneTime",
98
+ display_info: "Details of the invoice issued",
99
+ amount_due: 275.25,
100
+ invoice_created_date: "2025-02-23T10:46:37",
101
+ expiry_date: "2025-03-23T10:46:37"
102
+ opts: {}
103
+ )
104
+ ```
105
+
106
+ #### Update Bill
107
+
108
+ ```ruby
109
+ Sadad::SingleBill.update(
110
+ user_national_id: "1133331101",
111
+ user_first_name: "Khaled",
112
+ user_last_name: "Ahmed",
113
+ invoice_id: "1998654321",
114
+ bill_type: "OneTime",
115
+ display_info: "Details of the invoice issued",
116
+ amount_due: 275.25,
117
+ invoice_created_date: "2025-02-23T10:46:37",
118
+ expiry_date: "2025-03-23T10:46:37",
119
+ opts: {}
120
+ )
121
+ ```
122
+
123
+ #### Expire Bill
124
+
125
+ ```ruby
126
+ Sadad::SingleBill.expire(
127
+ user_national_id: "1133331101",
128
+ user_first_name: "Khaled",
129
+ user_last_name: "Ahmed",
130
+ invoice_id: "1998654321",
131
+ bill_type: "OneTime",
132
+ display_info: "Details of the invoice issued",
133
+ amount_due: 275.25,
134
+ invoice_created_date: "2025-02-23T10:46:37",
135
+ expiry_date: "2025-03-23T10:46:37",
136
+ opts: {}
137
+ )
138
+ ```
139
+
140
+ #### Inquiry Bill
141
+
142
+ ```ruby
143
+ Sadad::SingleBill.inquiry(
144
+ sadad_number: "1998654321",
145
+ opts: {}
146
+ )
147
+ ```
148
+
149
+
150
+ ### Multiple Bills Operations
151
+
152
+ #### Create Bills
153
+
154
+ ```ruby
155
+ invoices = [
156
+ {
157
+ user_national_id: "1133331101",
158
+ user_first_name: "Khaled",
159
+ user_last_name: "Ahmed",
160
+ invoice_id: "1998054111",
161
+ invoice_status: "BillNew",
162
+ bill_type: "OneTime",
163
+ display_info: "Details of the invoice issued",
164
+ amount_due: 275.25,
165
+ invoice_created_date: "2025-02-23T10:46:37",
166
+ expiry_date: "2025-03-23T10:46:37"
167
+ },
168
+ {
169
+ user_national_id: "1983331102",
170
+ user_first_name: "Ahmed",
171
+ user_last_name: "Ali",
172
+ invoice_id: "1998254321",
173
+ invoice_status: "BillNew",
174
+ bill_type: "Recurring",
175
+ display_info: "Monthly subscription payment",
176
+ amount_due: 5000.00,
177
+ invoice_created_date: "2025-02-23T10:46:37",
178
+ expiry_date: "2025-03-23T10:46:37",
179
+ minimum_partial_amount: 500
180
+ }
181
+ ]
182
+
183
+ Sadad::MultipleBills.create(invoices, opts: {})
184
+ ```
185
+
186
+ #### Deactivate Bill
187
+
188
+ ```ruby
189
+ Sadad::MultipleBills.deactivate(sadad_numbers: ["1998654321", "1998654322"], opts: {})
190
+ ```
191
+
192
+ ## Errors:
193
+ Errors could be one of the following:
194
+
195
+ ```ruby
196
+ Sadad::ForbiddenError
197
+ Sadad::APIConnectionError
198
+ Sadad::APIError
199
+ Sadad::InvalidRequestError (With `param` attribute)
200
+ ```
201
+
202
+ ## Contributing
203
+
204
+ Bug reports and pull requests are welcome.
205
+
206
+ ## License
207
+
208
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
209
+
@@ -0,0 +1,15 @@
1
+ require "rails/generators"
2
+
3
+ module Sadad
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ desc "Creates a Sadad initializers file."
9
+
10
+ def copy_initializer
11
+ copy_file "sadad.rb", "config/initializers/sadad.rb"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sadad.configure do |config|
4
+ # config.merchant_id = ""
5
+ # config.program_id = ""
6
+ # config.client_certificate_pem_file = ""
7
+ end
@@ -0,0 +1,54 @@
1
+ module Sadad
2
+ class ApiBase
3
+ include Sadad::Request
4
+ include Sadad::JsonSchemas::Validator
5
+
6
+ Response = Struct.new(:success?, :payload, :error) do
7
+ def failure?
8
+ !success?
9
+ end
10
+ end
11
+
12
+ def initialize(**params)
13
+ @params = params
14
+ @opts = filtered_opts(params[:opts])
15
+ end
16
+
17
+ def call
18
+ validate_params!
19
+ @response = call_api
20
+ handle_response_error
21
+ handle_sadad_response
22
+ rescue StandardError => e
23
+ failure(e)
24
+ end
25
+
26
+ private
27
+
28
+ def filtered_opts(opts)
29
+ opts&.reject { |_k, v| v&.empty? } || {}
30
+ end
31
+
32
+ def call_api
33
+ ::Faraday
34
+ .new(Sadad.base_uri, ssl: ssl_client_certificate)
35
+ .post(uri_path, api_params, headers)
36
+ end
37
+
38
+ def uri_path
39
+ raise NotImplementedError, "Subclasses must implement uri_path"
40
+ end
41
+
42
+ def request_body
43
+ raise NotImplementedError, "Subclasses must implement request_body"
44
+ end
45
+
46
+ def success(payload = nil)
47
+ Response.new(true, payload)
48
+ end
49
+
50
+ def failure(exception)
51
+ Response.new(false, nil, exception)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ module Sadad
2
+ module Api
3
+ class Bill
4
+ def self.request_body(params)
5
+ sanity_checks!(params)
6
+
7
+ {
8
+ Beneficiary: {
9
+ Id: params[:user_national_id],
10
+ FirstName: params[:user_first_name],
11
+ LastName: params[:user_last_name]
12
+ },
13
+ InvoiceId: params[:invoice_id],
14
+ InvoiceStatus: params[:invoice_status],
15
+ BillType: params[:bill_type],
16
+ DisplayInfo: params[:display_info] || "Details of invoice number: #{params[:invoice_id]}",
17
+ AmountDue: params[:amount_due],
18
+ CreateDate: format_time(params[:invoice_created_date]),
19
+ ExpiryDate: format_time(params[:expiry_date]),
20
+ PaymentRange: payment_range(params)
21
+ }.compact
22
+ end
23
+
24
+ def self.format_time(time_string)
25
+ Time.parse(time_string).strftime("%Y-%m-%dT%H:%M:%S") if time_string
26
+ end
27
+
28
+ def self.payment_range(params)
29
+ { MinPartialAmount: params[:minimum_partial_amount] } if params[:minimum_partial_amount]
30
+ end
31
+
32
+ def self.sanity_checks!(params)
33
+ raise Sadad::InvalidRequestError.new("params cannot be empty", params) if params.empty?
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ module Sadad
2
+ module MultipleBills
3
+ class Create < ApiBase
4
+ include Sadad::JsonSchemas::MultipleBills
5
+
6
+ def self.call(invoices:, opts: {})
7
+ new(invoices: invoices, opts: opts).call
8
+ end
9
+
10
+ def uri_path
11
+ "#{BILL_API_PREFIX}/ProcessMultiBills"
12
+ end
13
+
14
+ private
15
+
16
+ def request_body
17
+ {
18
+ Invoices: @params[:invoices].map do |invoice|
19
+ Sadad::Api::Bill.request_body(invoice)
20
+ end
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ module Sadad
2
+ module MultipleBills
3
+ class Deactivate < ApiBase
4
+ include Sadad::JsonSchemas::DeactivateBill
5
+
6
+ def self.call(sadad_numbers:, opts: {})
7
+ new(sadad_numbers: sadad_numbers, opts: opts).call
8
+ end
9
+
10
+ def uri_path
11
+ "#{BILL_API_PREFIX}/DeactivateBill"
12
+ end
13
+
14
+ private
15
+
16
+ def request_body
17
+ {
18
+ Invoices: @params[:sadad_numbers].map do |sadad_number|
19
+ {
20
+ SADADNumber: sadad_number,
21
+ InvoiceStatus: BILL_DEACTIVATED
22
+ }
23
+ end
24
+ }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,11 @@
1
+ module Sadad
2
+ module MultipleBills
3
+ def self.create(invoices:, opts: {})
4
+ Sadad::MultipleBills::Create.call(invoices: invoices, opts: opts)
5
+ end
6
+
7
+ def self.deactivate(sadad_numbers:, opts: {})
8
+ Sadad::MultipleBills::Deactivate.call(sadad_numbers: sadad_numbers, opts: opts)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,152 @@
1
+ module Sadad
2
+ module Request
3
+ def headers
4
+ @headers = {
5
+ "Content-Type": "application/json"
6
+ }
7
+ end
8
+
9
+ def ssl_client_certificate
10
+ @ssl_client_certificate = {
11
+ client_cert: OpenSSL::X509::Certificate.new(File.read(client_certificate_pem_file)),
12
+ client_key: OpenSSL::PKey::RSA.new(File.read(client_certificate_pem_file))
13
+ }
14
+ rescue StandardError
15
+ {}
16
+ end
17
+
18
+ def api_params
19
+ merchant_api_params.merge(request_body).to_json
20
+ end
21
+
22
+ def merchant_api_params
23
+ {
24
+ UUID: SecureRandom.uuid,
25
+ Timestamp: Time.now.strftime("%Y-%m-%dT%H:%M:%S"), ## formats to "2018-08-18T17:46:37"
26
+ MerchantId: merchant_id,
27
+ ProgramId: program_id
28
+ }
29
+ end
30
+
31
+ def program_id
32
+ @opts[:program_id] || Sadad.program_id || Sadad.configuration.program_id
33
+ end
34
+
35
+ def merchant_id
36
+ @opts[:merchant_id] || Sadad.merchant_id || Sadad.configuration.merchant_id
37
+ end
38
+
39
+ def client_certificate_pem_file
40
+ @opts[:client_certificate_pem_file] ||
41
+ Sadad.client_certificate_pem_file ||
42
+ Sadad.configuration.client_certificate_pem_file
43
+ end
44
+
45
+ def parsed_response
46
+ @parsed_response = begin
47
+ JSON.parse(@response.body)
48
+ rescue StandardError
49
+ {}
50
+ end
51
+ end
52
+
53
+ def error_message
54
+ parsed_response.is_a?(Array) ? array_error_message : single_error_message
55
+ end
56
+
57
+ def error_code
58
+ if parsed_response.is_a?(Array)
59
+ error_item = parsed_response.select { |item| item["Status"] && item["Status"]["Code"] != 0 }
60
+ error_item&.map { |item| item["Status"]["Code"] }
61
+ else
62
+ parsed_response.dig("errors", 0, "error_code") || parsed_response.dig("Status", "Code")
63
+ end
64
+ end
65
+
66
+ def handle_response_error
67
+ case response_status
68
+ when 400
69
+ raise_invalid_request_error
70
+ when 403
71
+ raise_forbidden_error
72
+ when 404
73
+ raise_not_found_error
74
+ when (400..599)
75
+ raise_api_error
76
+ end
77
+ end
78
+
79
+ def handle_sadad_response
80
+ internal_error? ? failure(build_internal_error) : success(parsed_response)
81
+ end
82
+
83
+ private
84
+
85
+ def response_status
86
+ @response_status = @response.status
87
+ end
88
+
89
+ def response_body
90
+ @response_body = @response.body
91
+ end
92
+
93
+ def array_error_message
94
+ error_items = parsed_response.select { |item| item["Status"] && item["Status"]["Code"] != 0 }
95
+ error_items&.map { |item| item["Status"]["Description"] }&.join(", ")
96
+ end
97
+
98
+ def single_error_message
99
+ parsed_response["message"] || parsed_response.dig("Status", "Description")
100
+ end
101
+
102
+ def internal_error?
103
+ return false unless parsed_response.is_a?(Hash)
104
+
105
+ status = parsed_response["Status"] || {}
106
+ status["Code"].to_i != 0 || status["Severity"]&.downcase == "error"
107
+ end
108
+
109
+ def build_internal_error
110
+ status = parsed_response["Status"] || {}
111
+ APIError.new(
112
+ status["Description"],
113
+ http_status: response_status,
114
+ http_body: response_body,
115
+ error_code: status["Code"]
116
+ )
117
+ end
118
+
119
+ def raise_invalid_request_error
120
+ raise InvalidRequestError.new(
121
+ error_message,
122
+ error_code,
123
+ http_status: response_status,
124
+ http_body: response_body
125
+ )
126
+ end
127
+
128
+ def raise_forbidden_error
129
+ raise ForbiddenError.new(
130
+ "Invalid Credentials",
131
+ http_status: response_status,
132
+ http_body: response_body
133
+ )
134
+ end
135
+
136
+ def raise_not_found_error
137
+ raise APIError.new(
138
+ "Resource not found",
139
+ http_status: response_status,
140
+ http_body: response_body
141
+ )
142
+ end
143
+
144
+ def raise_api_error
145
+ raise APIError.new(
146
+ error_message,
147
+ http_status: response_status,
148
+ http_body: response_body
149
+ )
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,37 @@
1
+ module Sadad
2
+ module SingleBill
3
+ class Create < ApiBase
4
+ include Sadad::JsonSchemas::SingleBill
5
+
6
+ def self.call(user_national_id:, user_first_name:, user_last_name:,
7
+ invoice_id:, invoice_status:, amount_due:, bill_type:,
8
+ invoice_created_date:, expiry_date:, display_info: nil,
9
+ minimum_partial_amount: nil, opts: {})
10
+ new(
11
+ user_national_id: user_national_id,
12
+ user_first_name: user_first_name,
13
+ user_last_name: user_last_name,
14
+ invoice_id: invoice_id,
15
+ invoice_status: invoice_status,
16
+ display_info: display_info,
17
+ amount_due: amount_due,
18
+ bill_type: bill_type,
19
+ invoice_created_date: invoice_created_date,
20
+ expiry_date: expiry_date,
21
+ minimum_partial_amount: minimum_partial_amount,
22
+ opts: opts
23
+ ).call
24
+ end
25
+
26
+ def uri_path
27
+ "#{BILL_API_PREFIX}/CreateSingleBill"
28
+ end
29
+
30
+ def request_body
31
+ {
32
+ Invoice: Sadad::Api::Bill.request_body(@params)
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,23 @@
1
+ module Sadad
2
+ module SingleBill
3
+ class Inquire < ApiBase
4
+ include Sadad::JsonSchemas::InquireBill
5
+
6
+ def self.call(sadad_number:, opts: {})
7
+ new(sadad_number: sadad_number, opts: opts).call
8
+ end
9
+
10
+ def uri_path
11
+ "#{BILL_API_PREFIX}/InquireBill"
12
+ end
13
+
14
+ private
15
+
16
+ def request_body
17
+ {
18
+ SADADNumber: @params[:sadad_number]
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ module Sadad
2
+ module SingleBill
3
+ class << self
4
+ def create(params)
5
+ create_with_status(params, BILL_NEW)
6
+ end
7
+
8
+ def update(params)
9
+ create_with_status(params, BILL_UPDATED)
10
+ end
11
+
12
+ def expire(params)
13
+ create_with_status(params, BILL_EXPIRED)
14
+ end
15
+
16
+ def inquire(sadad_number:, opts: {})
17
+ Sadad::SingleBill::Inquire.call(sadad_number: sadad_number, opts: opts)
18
+ end
19
+
20
+ private
21
+
22
+ def create_with_status(params, invoice_status)
23
+ Sadad::SingleBill::Create.call(
24
+ **params, invoice_status: invoice_status
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ module Sadad
2
+ class Configuration
3
+ attr_accessor :merchant_id, :program_id, :client_certificate_pem_file
4
+ end
5
+ end
@@ -0,0 +1,71 @@
1
+ module Sadad
2
+ # SadadError is the base error from which all other more specific Sadad
3
+ # errors derive.
4
+ class SadadError < StandardError
5
+ attr_reader :message, :error, :http_body, :http_headers, :http_status, :json_body
6
+
7
+ # Initializes a SadadError.
8
+ def initialize(message = nil, http_status: nil, http_body: nil)
9
+ @message = message || http_body
10
+ @http_status = http_status
11
+ @http_body = http_body
12
+ begin
13
+ @json_body = JSON.parse(http_body)
14
+ rescue StandardError
15
+ @json_body = nil
16
+ end
17
+ end
18
+
19
+ def to_s
20
+ status_string = @http_status.nil? ? "" : "(Status #{@http_status}) "
21
+ "#{status_string}#{@message}"
22
+ end
23
+ end
24
+
25
+ # ForbiddenError is raised when invalid merchant credentials are used to connect
26
+ # to Sadad's servers. Invalid merchant credentials includes:
27
+ # - Invalid Merchant ID
28
+ # - Invalid Program ID
29
+ # - Invalid Client Certificate file
30
+ class ForbiddenError < SadadError
31
+ end
32
+
33
+ # APIConnectionError is raised in the event that the SDK can't connect to
34
+ # Sadad's servers. That can be for a variety of different reasons from a
35
+ # downed network to a bad TLS certificate.
36
+ class APIConnectionError < SadadError
37
+ end
38
+
39
+ # APIError is raised when Sadad's API returns an error response. These errors
40
+ # can be caused by various issues like invalid parameters, authentication failures,
41
+ # rate limiting, or internal server errors. The error includes:
42
+ # - message: Human readable error description
43
+ # - http_status: HTTP status code (e.g. 400, 401, 429, 500)
44
+ # - http_body: Raw response body from the API
45
+ # - error_code: Sadad-specific error code for more granular error handling
46
+ class APIError < SadadError
47
+ attr_reader :error_code
48
+
49
+ def initialize(message = nil, http_status: nil, http_body: nil, error_code: nil)
50
+ super(message, http_status: http_status, http_body: http_body)
51
+ @error_code = error_code
52
+ end
53
+
54
+ def to_s
55
+ error_code_string = @error_code ? "(Error Code #{@error_code}) " : ""
56
+ status_string = @http_status ? "(Status #{@http_status}) " : ""
57
+ "#{status_string}#{error_code_string}#{@message}"
58
+ end
59
+ end
60
+
61
+ # InvalidRequestError is raised when a request is initiated with invalid
62
+ # parameters.
63
+ class InvalidRequestError < SadadError
64
+ attr_accessor :param
65
+
66
+ def initialize(message, param, http_status: nil, http_body: nil)
67
+ super(message, http_status: http_status, http_body: http_body)
68
+ @param = param
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,20 @@
1
+ module Sadad
2
+ module JsonSchemas
3
+ module Beneficiary
4
+ include Validator
5
+
6
+ def self.schema
7
+ {
8
+ "$schema": "http://json-schema.org/draft-06/schema",
9
+ type: "object",
10
+ properties: {
11
+ Id: Types::Identifier.schema(min_length: 10, max_length: 10),
12
+ FirstName: Types::String.schema,
13
+ LastName: Types::String.schema
14
+ },
15
+ required: %w[Id FirstName LastName]
16
+ }
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,32 @@
1
+ module Sadad
2
+ module JsonSchemas
3
+ module DeactivateBill
4
+ include Validator
5
+
6
+ private
7
+
8
+ def schema
9
+ {
10
+ "$schema": "http://json-schema.org/draft-06/schema",
11
+ type: "object",
12
+ properties: {
13
+ UUID: Types::Uuid.schema,
14
+ Timestamp: Types::Timestamp.schema,
15
+ MerchantId: Types::Identifier.schema,
16
+ ProgramId: Types::Identifier.schema,
17
+ Invoices: {
18
+ type: "array",
19
+ items: DeactivateInvoice.schema,
20
+ minItems: 1
21
+ }
22
+ },
23
+ required: required_fields
24
+ }
25
+ end
26
+
27
+ def required_fields
28
+ %w[UUID Timestamp MerchantId ProgramId Invoices]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,19 @@
1
+ module Sadad
2
+ module JsonSchemas
3
+ module DeactivateInvoice
4
+ include Validator
5
+
6
+ def self.schema
7
+ {
8
+ "$schema": "http://json-schema.org/draft-06/schema",
9
+ type: "object",
10
+ properties: {
11
+ SADADNumber: Types::Identifier.schema,
12
+ InvoiceStatus: Types::Enum.schema(values: [BILL_DEACTIVATED])
13
+ },
14
+ required: %w[SADADNumber InvoiceStatus]
15
+ }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ module Sadad
2
+ module JsonSchemas
3
+ module InquireBill
4
+ include Validator
5
+
6
+ private
7
+
8
+ def schema
9
+ {
10
+ "$schema": "http://json-schema.org/draft-06/schema",
11
+ type: "object",
12
+ properties: {
13
+ UUID: Types::Uuid.schema,
14
+ Timestamp: Types::Timestamp.schema,
15
+ MerchantId: Types::Identifier.schema,
16
+ ProgramId: Types::Identifier.schema,
17
+ SADADNumber: Types::Identifier.schema
18
+ },
19
+ required: %w[UUID Timestamp MerchantId ProgramId SADADNumber]
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ module Sadad
2
+ module JsonSchemas
3
+ module Invoice
4
+ include Validator
5
+
6
+ def self.schema
7
+ {
8
+ "$schema": "http://json-schema.org/draft-06/schema",
9
+ type: "object",
10
+ properties: {
11
+ Beneficiary: Beneficiary.schema,
12
+ InvoiceId: Types::Identifier.schema,
13
+ InvoiceStatus: Types::Enum.schema(values: INVOICE_STATUSES),
14
+ BillType: Types::Enum.schema(values: BILL_TYPES),
15
+ DisplayInfo: Types::String.schema(allows_null: true),
16
+ AmountDue: Types::Decimal.schema,
17
+ CreateDate: Types::Timestamp.schema,
18
+ ExpiryDate: Types::Timestamp.schema(allows_null: true),
19
+ PaymentRange: PaymentRange.schema(allows_null: true)
20
+ },
21
+ required: %w[Beneficiary InvoiceId InvoiceStatus BillType AmountDue CreateDate]
22
+ }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ module Sadad
2
+ module JsonSchemas
3
+ module MultipleBills
4
+ include Validator
5
+
6
+ private
7
+
8
+ def schema
9
+ {
10
+ "$schema": "http://json-schema.org/draft-06/schema",
11
+ type: "object",
12
+ properties: {
13
+ UUID: Types::Uuid.schema,
14
+ Timestamp: Types::Timestamp.schema,
15
+ MerchantId: Types::Identifier.schema,
16
+ ProgramId: Types::Identifier.schema,
17
+ Invoices: {
18
+ type: "array",
19
+ items: Invoice.schema,
20
+ minItems: 1
21
+ }
22
+ },
23
+ required: %w[UUID Timestamp MerchantId ProgramId Invoices]
24
+ }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ module Sadad
2
+ module JsonSchemas
3
+ module PaymentRange
4
+ include Validator
5
+
6
+ def self.schema(allows_null: false)
7
+ {
8
+ "$schema": "http://json-schema.org/draft-06/schema",
9
+ type: ["object", (allows_null ? "null" : nil)].compact,
10
+ properties: {
11
+ MinPartialAmount: Types::Decimal.schema(allows_null: allows_null)
12
+ },
13
+ required: ["MinPartialAmount"]
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,32 @@
1
+ module Sadad
2
+ module JsonSchemas
3
+ module SingleBill
4
+ include Validator
5
+
6
+ private
7
+
8
+ def schema
9
+ {
10
+ "$schema": "http://json-schema.org/draft-06/schema",
11
+ type: "object",
12
+ properties: bill_properties,
13
+ required: required_fields
14
+ }
15
+ end
16
+
17
+ def bill_properties
18
+ {
19
+ UUID: Types::Uuid.schema,
20
+ Timestamp: Types::Timestamp.schema,
21
+ MerchantId: Types::Identifier.schema,
22
+ ProgramId: Types::Identifier.schema,
23
+ Invoice: Invoice.schema
24
+ }
25
+ end
26
+
27
+ def required_fields
28
+ %w[UUID Timestamp MerchantId ProgramId Invoice]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ module Sadad
2
+ module JsonSchemas
3
+ module Types
4
+ module Decimal
5
+ def self.schema(allows_null: false, min: 0, max: nil)
6
+ {
7
+ "$schema": "http://json-schema.org/draft-06/schema",
8
+ type: ["number", (allows_null ? "null" : nil)].compact,
9
+ minimum: min,
10
+ maximum: max,
11
+ multipleOf: 0.01
12
+ }.compact
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ module Sadad
2
+ module JsonSchemas
3
+ module Types
4
+ module Enum
5
+ def self.schema(values:, default: nil, allows_null: false)
6
+ {
7
+ "$schema": "http://json-schema.org/draft-06/schema",
8
+ enum: allows_null ? [*values, nil] : values,
9
+ default: default
10
+ }.compact
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ module Sadad
2
+ module JsonSchemas
3
+ module Types
4
+ module Identifier
5
+ def self.schema(min_length: 1, max_length: nil)
6
+ {
7
+ "$schema": "http://json-schema.org/draft-06/schema",
8
+ type: "string",
9
+ pattern: "^\\d+$",
10
+ minLength: min_length,
11
+ maxLength: max_length
12
+ }.compact
13
+ end
14
+
15
+ def schema
16
+ Sadad::JsonSchemas::Types::Identifier.schema
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module Sadad
2
+ module JsonSchemas
3
+ module Types
4
+ module String
5
+ def self.schema(allows_null: false, max_length: nil, default: nil)
6
+ {
7
+ "$schema": "http://json-schema.org/draft-06/schema",
8
+ type: ["string", (allows_null ? "null" : nil)].compact,
9
+ minLength: allows_null ? 0 : 1,
10
+ maxLength: max_length,
11
+ default: default
12
+ }.compact
13
+ end
14
+
15
+ def schema
16
+ Sadad::JsonSchemas::Types::String.schema
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ module Sadad
2
+ module JsonSchemas
3
+ module Types
4
+ module Timestamp
5
+ def self.schema(allows_null: false)
6
+ {
7
+ "$schema": "http://json-schema.org/draft-06/schema",
8
+ type: ["string", (allows_null ? "null" : nil)].compact,
9
+ pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$"
10
+ }
11
+ end
12
+
13
+ def schema
14
+ Sadad::JsonSchemas::Types::Timestamp.schema
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module Sadad
2
+ module JsonSchemas
3
+ module Types
4
+ module Uuid
5
+ def self.schema(allows_null: false)
6
+ {
7
+ "$schema": "http://json-schema.org/draft-06/schema",
8
+ type: ["string", (allows_null ? "null" : nil)].compact,
9
+ pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
10
+ }
11
+ end
12
+
13
+ def schema
14
+ Sadad::JsonSchemas::Types::Uuid.schema
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,63 @@
1
+ module Sadad
2
+ module JsonSchemas
3
+ module Validator
4
+ ERROR_MESSAGES = {
5
+ Pattern: "%<property>s has invalid format",
6
+ Type: "%<property>s must be a %<type>s",
7
+ Required: "%<property>s is required",
8
+ Minimum: "%<property>s is too small",
9
+ Maximum: "%<property>s is too large",
10
+ Format: "%<property>s has invalid format",
11
+ Enum: "%<property>s must be one of: %<allowed_values>s"
12
+ }.freeze
13
+
14
+ def validate_params!
15
+ JSON::Validator.validate!(schema, api_params)
16
+ rescue JSON::Schema::ValidationError => e
17
+ handle_validation_error(e)
18
+ end
19
+
20
+ private
21
+
22
+ def handle_validation_error(error)
23
+ error_key = normalize_error_key(error)
24
+ property = extract_property_name(error)
25
+ error_params = extract_error_params(error)
26
+
27
+ raise InvalidRequestError.new(
28
+ format_error_message(error_key, property, error_params),
29
+ error.message
30
+ )
31
+ end
32
+
33
+ def normalize_error_key(error)
34
+ error.failed_attribute.to_s
35
+ .split("::")
36
+ .last
37
+ .delete_suffix("Attribute")
38
+ end
39
+
40
+ def extract_property_name(error)
41
+ path = error.message[/'#\/([^']+)'/i, 1]
42
+ path&.humanize || "Field"
43
+ end
44
+
45
+ def extract_error_params(error)
46
+ schema_data = error.schema.instance_variable_get(:@schema)
47
+ {
48
+ type: schema_data["type"],
49
+ pattern: schema_data["pattern"],
50
+ allowed_values: Array(schema_data["enum"])&.join(", ")
51
+ }.compact
52
+ end
53
+
54
+ def format_error_message(error_key, property, error_params)
55
+ format(
56
+ ERROR_MESSAGES.fetch(error_key.to_sym, "%<property>s is invalid"),
57
+ property: property,
58
+ **error_params
59
+ )
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,3 @@
1
+ module Sadad
2
+ VERSION = "0.1.0".freeze
3
+ end
data/lib/sadad.rb ADDED
@@ -0,0 +1,75 @@
1
+ require "logger"
2
+ require "active_support"
3
+ require "active_support/core_ext/module"
4
+ require "active_support/core_ext/numeric/time"
5
+ require "faraday"
6
+ require "json-schema"
7
+ require "securerandom"
8
+ require "openssl"
9
+ require "json"
10
+
11
+ require_relative "sadad/version"
12
+ require_relative "sadad/errors"
13
+
14
+ # JSON Schemas
15
+ require "sadad/json_schemas/validator"
16
+ require "sadad/json_schemas/types/identifier"
17
+ require "sadad/json_schemas/types/uuid"
18
+ require "sadad/json_schemas/types/timestamp"
19
+ require "sadad/json_schemas/types/string"
20
+ require "sadad/json_schemas/types/decimal"
21
+ require "sadad/json_schemas/types/enum"
22
+ require "sadad/json_schemas/beneficiary"
23
+ require "sadad/json_schemas/invoice"
24
+ require "sadad/json_schemas/single_bill"
25
+ require "sadad/json_schemas/payment_range"
26
+ require "sadad/json_schemas/multiple_bills"
27
+ require "sadad/json_schemas/deactivate_bill"
28
+ require "sadad/json_schemas/deactivate_invoice"
29
+ require "sadad/json_schemas/inquire_bill"
30
+
31
+ # API
32
+ require "sadad/api/request"
33
+ require "sadad/configuration"
34
+ require "sadad/api/api_base"
35
+ require "sadad/api/bill"
36
+ require "sadad/api/single_bill/create"
37
+ require "sadad/api/single_bill/inquire"
38
+ require "sadad/api/single_bill"
39
+ require "sadad/api/multiple_bills/create"
40
+ require "sadad/api/multiple_bills/deactivate"
41
+ require "sadad/api/multiple_bills"
42
+
43
+ module Sadad
44
+ SANDBOX_URI = "https://rosomtest.brightware.com.sa/RosomAPI/".freeze
45
+ PRODUCTION_URI = "".freeze ## TODO: add production URI
46
+
47
+ BILL_API_PREFIX = "api/Bill".freeze
48
+
49
+ INVOICE_STATUSES = %w[BillNew BillUpdated BillExpired].freeze
50
+ BILL_TYPES = %w[OneTime Recurring].freeze
51
+ BILL_DEACTIVATED = "BillDeactivated".freeze
52
+ BILL_NEW = "BillNew".freeze
53
+ BILL_UPDATED = "BillUpdated".freeze
54
+ BILL_EXPIRED = "BillExpired".freeze
55
+
56
+ class << self
57
+ attr_accessor :merchant_id, :program_id, :client_certificate_pem_file
58
+
59
+ def base_uri
60
+ if defined?(Rails) && Rails.respond_to?(:env) && Rails.env.production?
61
+ PRODUCTION_URI
62
+ else
63
+ SANDBOX_URI
64
+ end
65
+ end
66
+
67
+ def configure
68
+ yield configuration
69
+ end
70
+
71
+ def configuration
72
+ @configuration ||= Configuration.new
73
+ end
74
+ end
75
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sadad
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Shimaa Marzouk Ali
8
+ - Elaraby Elaidy
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2025-04-22 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '5.2'
21
+ - - "<"
22
+ - !ruby/object:Gem::Version
23
+ version: '7.0'
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ version: '5.2'
31
+ - - "<"
32
+ - !ruby/object:Gem::Version
33
+ version: '7.0'
34
+ - !ruby/object:Gem::Dependency
35
+ name: faraday
36
+ requirement: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.10.3
41
+ type: :runtime
42
+ prerelease: false
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 1.10.3
48
+ - !ruby/object:Gem::Dependency
49
+ name: json-schema
50
+ requirement: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 4.3.1
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 4.3.1
62
+ description: Integration with Sadad API! All required resources are included to make
63
+ your payment with Sadad
64
+ email:
65
+ - eng.shimaa1985@gmail.com
66
+ - elarabyelaidy@gmail.com
67
+ executables: []
68
+ extensions: []
69
+ extra_rdoc_files: []
70
+ files:
71
+ - README.md
72
+ - lib/generators/sadad/install_generator.rb
73
+ - lib/generators/sadad/templates/sadad.rb
74
+ - lib/sadad.rb
75
+ - lib/sadad/api/api_base.rb
76
+ - lib/sadad/api/bill.rb
77
+ - lib/sadad/api/multiple_bills.rb
78
+ - lib/sadad/api/multiple_bills/create.rb
79
+ - lib/sadad/api/multiple_bills/deactivate.rb
80
+ - lib/sadad/api/request.rb
81
+ - lib/sadad/api/single_bill.rb
82
+ - lib/sadad/api/single_bill/create.rb
83
+ - lib/sadad/api/single_bill/inquire.rb
84
+ - lib/sadad/configuration.rb
85
+ - lib/sadad/errors.rb
86
+ - lib/sadad/json_schemas/beneficiary.rb
87
+ - lib/sadad/json_schemas/deactivate_bill.rb
88
+ - lib/sadad/json_schemas/deactivate_invoice.rb
89
+ - lib/sadad/json_schemas/inquire_bill.rb
90
+ - lib/sadad/json_schemas/invoice.rb
91
+ - lib/sadad/json_schemas/multiple_bills.rb
92
+ - lib/sadad/json_schemas/payment_range.rb
93
+ - lib/sadad/json_schemas/single_bill.rb
94
+ - lib/sadad/json_schemas/types/decimal.rb
95
+ - lib/sadad/json_schemas/types/enum.rb
96
+ - lib/sadad/json_schemas/types/identifier.rb
97
+ - lib/sadad/json_schemas/types/string.rb
98
+ - lib/sadad/json_schemas/types/timestamp.rb
99
+ - lib/sadad/json_schemas/types/uuid.rb
100
+ - lib/sadad/json_schemas/validator.rb
101
+ - lib/sadad/version.rb
102
+ homepage: https://gitlab.com/autocloud/sadad_rails
103
+ licenses:
104
+ - MIT
105
+ metadata:
106
+ rubygems_mfa_required: 'true'
107
+ allowed_push_host: https://rubygems.org
108
+ post_install_message:
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 2.6.1
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubygems_version: 3.2.3
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: Integration with Sadad API
127
+ test_files: []