wittyflow 0.1.0 → 1.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,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # config/initializers/wittyflow.rb
4
+ #
5
+ # Wittyflow SMS Gem Configuration
6
+ # This initializer sets up the Wittyflow SMS service for your Rails application
7
+
8
+ Wittyflow.configure do |config|
9
+ # Required: Your Wittyflow API credentials
10
+ # Get these from your Wittyflow dashboard
11
+ config.app_id = Rails.application.credentials.wittyflow_app_id || ENV.fetch("WITTYFLOW_APP_ID", nil)
12
+ config.app_secret = Rails.application.credentials.wittyflow_app_secret || ENV.fetch("WITTYFLOW_APP_SECRET", nil)
13
+
14
+ # Optional: API endpoint (default is production endpoint)
15
+ # config.api_endpoint = "https://api.wittyflow.com/v1"
16
+
17
+ # Optional: Request timeout in seconds (default: 30)
18
+ config.timeout = 30
19
+
20
+ # Optional: Number of retries for failed requests (default: 3)
21
+ config.retries = 3
22
+
23
+ # Optional: Delay between retries in seconds (default: 1)
24
+ config.retry_delay = 1
25
+
26
+ # Optional: Default country code for phone number formatting (default: "233" for Ghana)
27
+ config.default_country_code = "233"
28
+
29
+ # Optional: Custom logger (default: Logger to STDOUT)
30
+ # Use Rails logger in Rails applications
31
+ config.logger = Rails.logger
32
+
33
+ # Optional: Set log level for Wittyflow-specific logs
34
+ # config.logger.level = Logger::INFO
35
+ end
36
+
37
+ # Validate configuration in development and production
38
+ if (Rails.env.development? || Rails.env.production?) && !Wittyflow.config.valid?
39
+ Rails.logger.warn "Wittyflow configuration is invalid. SMS functionality will not work."
40
+ Rails.logger.warn "Please ensure WITTYFLOW_APP_ID and WITTYFLOW_APP_SECRET are set."
41
+ end
42
+
43
+ # Example: Test the configuration in development
44
+ if Rails.env.development? && Wittyflow.config.valid?
45
+ Rails.application.configure do
46
+ config.after_initialize do
47
+ Wittyflow::Client.new
48
+ # You can uncomment the line below to test account balance on startup
49
+ # balance = client.account_balance
50
+ # Rails.logger.info "Wittyflow account balance: #{balance}"
51
+ Rails.logger.info "Wittyflow SMS gem initialized successfully"
52
+ rescue Wittyflow::Error => e
53
+ Rails.logger.warn "Wittyflow initialization test failed: #{e.message}"
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "timeout"
5
+
6
+ module Wittyflow
7
+ class Client
8
+ include HTTParty
9
+
10
+ attr_reader :app_id, :app_secret, :config
11
+
12
+ def initialize(app_id: nil, app_secret: nil, **options)
13
+ @app_id = app_id || Wittyflow.config.app_id
14
+ @app_secret = app_secret || Wittyflow.config.app_secret
15
+ @config = build_config(options)
16
+
17
+ validate_credentials!
18
+ setup_httparty
19
+ end
20
+
21
+ # Send regular SMS
22
+ def send_sms(from:, to:, message:)
23
+ send_message(from: from, to: to, message: message, type: :regular)
24
+ end
25
+
26
+ # Send flash SMS (appears directly on screen)
27
+ def send_flash_sms(from:, to:, message:)
28
+ send_message(from: from, to: to, message: message, type: :flash)
29
+ end
30
+
31
+ # Check SMS delivery status
32
+ def message_status(message_id)
33
+ validate_presence!(:message_id, message_id)
34
+
35
+ url = "#{config.api_endpoint}/messages/#{message_id}/retrieve"
36
+ params = auth_params
37
+
38
+ with_error_handling do
39
+ response = self.class.get(url, query: params, timeout: config.timeout)
40
+ parse_response(response)
41
+ end
42
+ end
43
+
44
+ # Get account balance
45
+ def account_balance
46
+ url = "#{config.api_endpoint}/account/balance"
47
+ params = auth_params
48
+
49
+ with_error_handling do
50
+ response = self.class.get(url, query: params, timeout: config.timeout)
51
+ parse_response(response)
52
+ end
53
+ end
54
+
55
+ # Send bulk SMS to multiple recipients
56
+ def send_bulk_sms(from:, to:, message:, type: :regular)
57
+ validate_presence!(:from, from)
58
+ validate_presence!(:to, to)
59
+ validate_presence!(:message, message)
60
+
61
+ recipients = Array(to).map { |phone| format_phone_number(phone) }
62
+
63
+ recipients.map do |recipient|
64
+ send_message(from: from, to: recipient, message: message, type: type)
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def build_config(options)
71
+ config = Configuration.new
72
+ config.api_endpoint = options[:api_endpoint] || Wittyflow.config.api_endpoint
73
+ config.timeout = options[:timeout] || Wittyflow.config.timeout
74
+ config.retries = options[:retries] || Wittyflow.config.retries
75
+ config.retry_delay = options[:retry_delay] || Wittyflow.config.retry_delay
76
+ config.logger = options[:logger] || Wittyflow.config.logger
77
+ config.default_country_code = options[:default_country_code] || Wittyflow.config.default_country_code
78
+ config
79
+ end
80
+
81
+ def setup_httparty
82
+ self.class.base_uri config.api_endpoint
83
+ self.class.headers({
84
+ "Content-Type" => "application/x-www-form-urlencoded",
85
+ "Accept" => "application/json",
86
+ "User-Agent" => "Wittyflow Ruby Gem #{Wittyflow::VERSION}"
87
+ })
88
+ end
89
+
90
+ def validate_credentials!
91
+ raise ConfigurationError, "app_id is required" if app_id.nil? || app_id.empty?
92
+ raise ConfigurationError, "app_secret is required" if app_secret.nil? || app_secret.empty?
93
+ end
94
+
95
+ def validate_presence!(field, value)
96
+ raise ValidationError, "#{field} is required" if value.nil? || value.to_s.empty?
97
+ end
98
+
99
+ def send_message(from:, to:, message:, type:)
100
+ validate_presence!(:from, from)
101
+ validate_presence!(:to, to)
102
+ validate_presence!(:message, message)
103
+
104
+ formatted_to = format_phone_number(to)
105
+
106
+ body = {
107
+ from: from.to_s,
108
+ to: formatted_to,
109
+ message: message.to_s,
110
+ type: type == :flash ? 0 : 1,
111
+ app_id: app_id,
112
+ app_secret: app_secret
113
+ }
114
+
115
+ url = "#{config.api_endpoint}/messages/send"
116
+
117
+ with_error_handling do
118
+ response = self.class.post(url, body: body, timeout: config.timeout)
119
+ parse_response(response)
120
+ end
121
+ end
122
+
123
+ def format_phone_number(phone)
124
+ phone_str = phone.to_s.strip
125
+
126
+ # Remove any non-digit characters except +
127
+ phone_str = phone_str.gsub(/[^\d+]/, "")
128
+
129
+ # Handle different phone number formats
130
+ case phone_str
131
+ when /^\+233/ # International format for Ghana
132
+ phone_str.sub(/^\+/, "")
133
+ when /^233/ # Ghana country code without +
134
+ phone_str
135
+ when /^0/ # Local format starting with 0
136
+ "#{config.default_country_code}#{phone_str[1..]}"
137
+ else # Assume it's a local number without leading 0
138
+ "#{config.default_country_code}#{phone_str}"
139
+ end
140
+ end
141
+
142
+ def auth_params
143
+ {
144
+ app_id: app_id,
145
+ app_secret: app_secret
146
+ }
147
+ end
148
+
149
+ def with_error_handling
150
+ attempts = 0
151
+ begin
152
+ attempts += 1
153
+ yield
154
+ rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Timeout::Error => e
155
+ raise NetworkError, "Network timeout: #{e.message}"
156
+ rescue HTTParty::Error, SocketError => e
157
+ unless attempts < config.retries
158
+ raise NetworkError, "Network error after #{config.retries} attempts: #{e.message}"
159
+ end
160
+
161
+ config.logger&.warn(
162
+ "Request failed (attempt #{attempts}/#{config.retries}), " \
163
+ "retrying in #{config.retry_delay}s: #{e.message}"
164
+ )
165
+ sleep(config.retry_delay)
166
+ retry
167
+ rescue Wittyflow::Error => e
168
+ # Re-raise our custom errors without modification
169
+ raise e
170
+ rescue StandardError => e
171
+ raise APIError, "Unexpected error: #{e.message}"
172
+ end
173
+ end
174
+
175
+ def parse_response(response)
176
+ case response.code
177
+ when 200..299
178
+ response.parsed_response
179
+ when 401
180
+ raise AuthenticationError, "Invalid app_id or app_secret"
181
+ when 400
182
+ error_message = response.parsed_response&.dig("message") || "Bad request"
183
+ raise ValidationError, error_message
184
+ when 429
185
+ raise APIError, "Rate limit exceeded"
186
+ when 500..599
187
+ raise APIError, "Server error (#{response.code})"
188
+ else
189
+ raise APIError, "Unexpected response (#{response.code}): #{response.body}"
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wittyflow
4
+ class Configuration
5
+ attr_accessor :app_id, :app_secret, :api_endpoint, :timeout, :retries, :retry_delay, :logger, :default_country_code
6
+
7
+ def initialize
8
+ @api_endpoint = "https://api.wittyflow.com/v1"
9
+ @timeout = 30
10
+ @retries = 3
11
+ @retry_delay = 1
12
+ @logger = Logger.new($stdout, level: Logger::INFO)
13
+ @default_country_code = "233" # Ghana
14
+ end
15
+
16
+ def valid?
17
+ !app_id.nil? && !app_secret.nil? && !app_id.empty? && !app_secret.empty?
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wittyflow
4
+ # Backward compatibility wrapper for the old Sms class
5
+ # @deprecated Use Wittyflow::Client instead
6
+ class Sms
7
+ def initialize(app_id, app_secret)
8
+ warn "[DEPRECATION] Wittyflow::Sms is deprecated. Use Wittyflow::Client instead."
9
+ @client = Client.new(app_id: app_id, app_secret: app_secret)
10
+ end
11
+
12
+ def send_sms(sender, receiver_phone_number, message_to_send)
13
+ @client.send_sms(from: sender, to: receiver_phone_number, message: message_to_send)
14
+ end
15
+
16
+ def send_flash_sms(sender, receiver_phone_number, message_to_send)
17
+ @client.send_flash_sms(from: sender, to: receiver_phone_number, message: message_to_send)
18
+ end
19
+
20
+ def check_sms_status(sms_id)
21
+ @client.message_status(sms_id)
22
+ end
23
+
24
+ def account_balance
25
+ @client.account_balance
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :client
31
+ end
32
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wittyflow
4
- VERSION = "0.1.0"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/wittyflow.rb CHANGED
@@ -1,76 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "wittyflow/version"
4
- require 'httparty'
3
+ require "zeitwerk"
4
+ require "httparty"
5
+ require "logger"
5
6
 
6
- module Wittyflow
7
-
8
- class Sms
9
- attr_accessor :app_id, :app_secret, :api_endpoint
10
-
11
- def initialize(app_id, app_secret)
12
- @app_id = app_id
13
- @app_secret = app_secret
14
- @api_endpoint = "https://api.wittyflow.com/v1"
15
- end
16
-
17
- def send_sms(sender, receiver_phone_number, message_to_send)
18
- send_general_sms(sender, receiver_phone_number, message_to_send)
19
- end
20
-
21
- def send_flash_sms(sender, receiver_phone_number, message_to_send)
22
- send_general_sms(sender, receiver_phone_number, message_to_send, is_flash=true)
23
- end
24
-
25
- def check_sms_status(sms_id)
26
- make_request(form_sms_status_check_url(sms_id), {}, "get")
27
- end
28
-
29
- def account_balance
30
- make_request(account_balance_url, {}, "get")
31
- end
7
+ loader = Zeitwerk::Loader.for_gem
8
+ loader.setup
32
9
 
33
- private
34
-
35
- def make_request(url, body, type="post")
36
- options = {
37
- body: body
38
- }
39
- requ_hash = {
40
- "post" => -> (url, options) {HTTParty.post(url, options)},
41
- "get" => -> (url, options) {HTTParty.get(url, {})},
42
- }
43
- results = requ_hash[type].call(url, options)
44
- rescue => e
45
- puts "ERROR: #{e}"
46
- end
47
-
48
- def form_send_sms_body(sender, receiver_phone_number, message_to_send, is_flash=false)
49
- res = {
50
- from: "#{sender}",
51
- to: "233#{receiver_phone_number[1..-1]}",
52
- type: 1,
53
- message: "#{message_to_send}",
54
- app_id: "#{@app_id}",
55
- app_secret: "#{@app_secret}"
56
- }
57
- res[:type] = 0 if is_flash
58
- res
59
- end
60
-
61
- def send_general_sms(sender, receiver_phone_number, message_to_send, is_flash=false)
62
- body_to_send = form_send_sms_body(sender, receiver_phone_number, message_to_send, is_flash)
63
- send_sms_endpoint = "#{@api_endpoint}/messages/send"
64
- make_request(send_sms_endpoint, body_to_send)
65
- end
10
+ module Wittyflow
11
+ class Error < StandardError; end
12
+ class ConfigurationError < Error; end
13
+ class APIError < Error; end
14
+ class NetworkError < Error; end
15
+ class AuthenticationError < Error; end
16
+ class ValidationError < Error; end
17
+
18
+ class << self
19
+ attr_accessor :configuration
20
+ end
66
21
 
67
- def account_balance_url
68
- "#{@api_endpoint}/account/balance?app_id=#{@app_id}&app_secret=#{@app_secret}"
69
- end
22
+ def self.configure
23
+ self.configuration ||= Configuration.new
24
+ yield(configuration) if block_given?
25
+ configuration
26
+ end
70
27
 
71
- def form_sms_status_check_url(sms_id)
72
- "#{@api_endpoint}/messages/#{sms_id}/retrieve?app_id=#{@app_id}&app_secret=#{@app_secret}"
73
- end
28
+ def self.config
29
+ configuration || configure
74
30
  end
75
31
 
32
+ # For backward compatibility
33
+ def self.new(app_id, app_secret, options = {})
34
+ Client.new(app_id: app_id, app_secret: app_secret, **options)
35
+ end
76
36
  end
Binary file
data/wittyflow.gemspec CHANGED
@@ -8,28 +8,51 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["charlesagyemang"]
9
9
  spec.email = ["micnkru@gmail.com"]
10
10
 
11
- spec.summary = "Send Sms To Any Number In Ghana Using Wittyflow"
12
- spec.description = "Send Sms To Any Number In Ghana Using Wittyflow"
11
+ spec.summary = "A production-ready Ruby gem for sending SMS via Wittyflow API"
12
+ spec.description = "Wittyflow is a robust, well-tested Ruby gem for integrating with the Wittyflow SMS service. " \
13
+ "Features include SMS sending, delivery tracking, account balance checking, retry logic, " \
14
+ "and comprehensive error handling."
13
15
  spec.homepage = "https://github.com/charlesagyemang/wittyflow_sms_ruby_gem"
14
16
  spec.license = "MIT"
15
- spec.required_ruby_version = ">= 2.0.0"
16
-
17
- # spec.metadata["allowed_push_host"] = "https://github.com/charlesagyemang/wittyflow_sms_ruby_gem"
17
+ spec.required_ruby_version = ">= 3.0.0"
18
18
 
19
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
19
20
  spec.metadata["homepage_uri"] = "https://github.com/charlesagyemang/wittyflow_sms_ruby_gem"
20
21
  spec.metadata["source_code_uri"] = "https://github.com/charlesagyemang/wittyflow_sms_ruby_gem"
21
- spec.metadata["changelog_uri"] = "https://github.com/charlesagyemang/wittyflow_sms_ruby_gem"
22
+ spec.metadata["changelog_uri"] = "https://github.com/charlesagyemang/wittyflow_sms_ruby_gem/blob/main/CHANGELOG.md"
23
+ spec.metadata["bug_tracker_uri"] = "https://github.com/charlesagyemang/wittyflow_sms_ruby_gem/issues"
24
+ spec.metadata["documentation_uri"] = "https://github.com/charlesagyemang/wittyflow_sms_ruby_gem/blob/main/README.md"
25
+ spec.metadata["rubygems_mfa_required"] = "true"
22
26
 
23
27
  # Specify which files should be added to the gem when it is released.
24
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
28
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
29
+ `git ls-files -z`.split("\x0").reject do |f|
30
+ f.match(%r{\A(?:test|spec|features)/}) ||
31
+ f.start_with?(".") ||
32
+ f.match(/\A(Gemfile|Rakefile)/)
33
+ end
27
34
  end
35
+
28
36
  spec.bindir = "exe"
29
37
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
38
  spec.require_paths = ["lib"]
31
39
 
32
- # Uncomment to register a new dependency of your gem
33
- spec.add_dependency "httparty", "~> 0.17.3"
34
-
40
+ # Runtime dependencies (alphabetically ordered)
41
+ spec.add_dependency "csv", "~> 3.2"
42
+ spec.add_dependency "httparty", "~> 0.21.0"
43
+ spec.add_dependency "zeitwerk", "~> 2.6"
44
+
45
+ # Development dependencies
46
+ spec.add_development_dependency "guard", "~> 2.18"
47
+ spec.add_development_dependency "guard-rspec", "~> 4.7"
48
+ spec.add_development_dependency "pry", "~> 0.14"
49
+ spec.add_development_dependency "rake", "~> 13.0"
50
+ spec.add_development_dependency "rspec", "~> 3.12"
51
+ spec.add_development_dependency "rubocop", "~> 1.50"
52
+ spec.add_development_dependency "rubocop-performance", "~> 1.16"
53
+ spec.add_development_dependency "rubocop-rspec", "~> 2.20"
54
+ spec.add_development_dependency "simplecov", "~> 0.22"
55
+ spec.add_development_dependency "vcr", "~> 6.1"
56
+ spec.add_development_dependency "webmock", "~> 3.18"
57
+ spec.add_development_dependency "yard", "~> 0.9"
35
58
  end