zai_payment 1.2.0 → 1.3.1

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.
@@ -5,11 +5,12 @@ require 'faraday'
5
5
  module ZaiPayment
6
6
  # Base API client that handles HTTP requests to Zai API
7
7
  class Client
8
- attr_reader :config, :token_provider
8
+ attr_reader :config, :token_provider, :base_endpoint
9
9
 
10
- def initialize(config: nil, token_provider: nil)
10
+ def initialize(config: nil, token_provider: nil, base_endpoint: nil)
11
11
  @config = config || ZaiPayment.config
12
12
  @token_provider = token_provider || ZaiPayment.auth
13
+ @base_endpoint = base_endpoint
13
14
  end
14
15
 
15
16
  # Perform a GET request
@@ -96,8 +97,14 @@ module ZaiPayment
96
97
  end
97
98
 
98
99
  def base_url
100
+ # Use specified base_endpoint or default to va_base
101
+ # Users API uses core_base endpoint
99
102
  # Webhooks API uses va_base endpoint
100
- config.endpoints[:va_base]
103
+ if base_endpoint
104
+ config.endpoints[base_endpoint]
105
+ else
106
+ config.endpoints[:va_base]
107
+ end
101
108
  end
102
109
 
103
110
  def handle_faraday_error(error)
@@ -0,0 +1,383 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZaiPayment
4
+ module Resources
5
+ # User resource for managing Zai users (payin and payout)
6
+ #
7
+ # @see https://developer.hellozai.com/docs/onboarding-a-pay-in-user
8
+ # @see https://developer.hellozai.com/docs/onboarding-a-pay-out-user
9
+ class User
10
+ attr_reader :client
11
+
12
+ # User types
13
+ USER_TYPE_PAYIN = 'payin'
14
+ USER_TYPE_PAYOUT = 'payout'
15
+
16
+ # Valid user types
17
+ VALID_USER_TYPES = [USER_TYPE_PAYIN, USER_TYPE_PAYOUT].freeze
18
+
19
+ # Map of attribute keys to API field names
20
+ FIELD_MAPPING = {
21
+ id: :id,
22
+ email: :email,
23
+ first_name: :first_name,
24
+ last_name: :last_name,
25
+ mobile: :mobile,
26
+ phone: :phone,
27
+ address_line1: :address_line1,
28
+ address_line2: :address_line2,
29
+ city: :city,
30
+ state: :state,
31
+ zip: :zip,
32
+ country: :country,
33
+ dob: :dob,
34
+ government_number: :government_number,
35
+ drivers_license_number: :drivers_license_number,
36
+ drivers_license_state: :drivers_license_state,
37
+ logo_url: :logo_url,
38
+ color_1: :color_1,
39
+ color_2: :color_2,
40
+ custom_descriptor: :custom_descriptor,
41
+ authorized_signer_title: :authorized_signer_title,
42
+ user_type: :user_type,
43
+ device_id: :device_id,
44
+ ip_address: :ip_address
45
+ }.freeze
46
+
47
+ # Map of company attribute keys to API field names
48
+ COMPANY_FIELD_MAPPING = {
49
+ name: :name,
50
+ legal_name: :legal_name,
51
+ tax_number: :tax_number,
52
+ business_email: :business_email,
53
+ charge_tax: :charge_tax,
54
+ address_line1: :address_line1,
55
+ address_line2: :address_line2,
56
+ city: :city,
57
+ state: :state,
58
+ zip: :zip,
59
+ country: :country,
60
+ phone: :phone
61
+ }.freeze
62
+
63
+ def initialize(client: nil)
64
+ @client = client || Client.new
65
+ end
66
+
67
+ # List all users
68
+ #
69
+ # @param limit [Integer] number of records to return (default: 10)
70
+ # @param offset [Integer] number of records to skip (default: 0)
71
+ # @return [Response] the API response containing users array
72
+ #
73
+ # @example
74
+ # users = ZaiPayment::Resources::User.new
75
+ # response = users.list
76
+ # response.data # => [{"id" => "...", "email" => "..."}, ...]
77
+ #
78
+ # @see https://developer.hellozai.com/reference/getallusers
79
+ def list(limit: 10, offset: 0)
80
+ params = {
81
+ limit: limit,
82
+ offset: offset
83
+ }
84
+
85
+ client.get('/users', params: params)
86
+ end
87
+
88
+ # Get a specific user by ID
89
+ #
90
+ # @param user_id [String] the user ID
91
+ # @return [Response] the API response containing user details
92
+ #
93
+ # @example
94
+ # users = ZaiPayment::Resources::User.new
95
+ # response = users.show("user_id")
96
+ # response.data # => {"id" => "user_id", "email" => "...", ...}
97
+ #
98
+ # @see https://developer.hellozai.com/reference/getuserbyid
99
+ def show(user_id)
100
+ validate_id!(user_id, 'user_id')
101
+ client.get("/users/#{user_id}")
102
+ end
103
+
104
+ # Create a new user (payin or payout)
105
+ #
106
+ # @param attributes [Hash] user attributes
107
+ # @option attributes [String] :id Optional unique ID for the user. If not provided,
108
+ # Zai will generate one automatically. Cannot contain '.' character.
109
+ # Useful for mapping to your existing system's user IDs.
110
+ # @option attributes [String] :email (Required) user's email address
111
+ # @option attributes [String] :first_name (Required) user's first name
112
+ # @option attributes [String] :last_name (Required) user's last name
113
+ # @option attributes [String] :country (Required) user's country code (ISO 3166-1 alpha-3)
114
+ # @option attributes [String] :user_type Optional user type ('payin' or 'payout')
115
+ # @option attributes [String] :mobile user's mobile phone number (international format with '+')
116
+ # @option attributes [String] :phone user's phone number
117
+ # @option attributes [String] :address_line1 user's address line 1
118
+ # @option attributes [String] :address_line2 user's address line 2
119
+ # @option attributes [String] :city user's city
120
+ # @option attributes [String] :state user's state
121
+ # @option attributes [String] :zip user's postal/zip code
122
+ # @option attributes [String] :dob user's date of birth (DD/MM/YYYY)
123
+ # @option attributes [String] :government_number user's government ID number (SSN, TFN, etc.)
124
+ # @option attributes [String] :drivers_license_number driving license number
125
+ # @option attributes [String] :drivers_license_state state section of the user's driving license
126
+ # @option attributes [String] :logo_url URL link to the logo
127
+ # @option attributes [String] :color_1 color code number 1
128
+ # @option attributes [String] :color_2 color code number 2
129
+ # @option attributes [String] :custom_descriptor custom descriptor for bundle direct debit statements
130
+ # @option attributes [String] :authorized_signer_title job title for AMEX merchants (e.g., Director)
131
+ # @option attributes [Hash] :company company details (creates a company for the user)
132
+ # @option attributes [String] :device_id device ID for fraud prevention
133
+ # @option attributes [String] :ip_address IP address for fraud prevention
134
+ # @return [Response] the API response containing created user
135
+ #
136
+ # @example Create a payin user (buyer) with auto-generated ID
137
+ # users = ZaiPayment::Resources::User.new
138
+ # response = users.create(
139
+ # email: "buyer@example.com",
140
+ # first_name: "John",
141
+ # last_name: "Doe",
142
+ # country: "USA",
143
+ # mobile: "+1234567890",
144
+ # address_line1: "123 Main St",
145
+ # city: "New York",
146
+ # state: "NY",
147
+ # zip: "10001"
148
+ # )
149
+ #
150
+ # @example Create a payin user with custom ID
151
+ # users = ZaiPayment::Resources::User.new
152
+ # response = users.create(
153
+ # id: "buyer-#{your_user_id}",
154
+ # email: "buyer@example.com",
155
+ # first_name: "John",
156
+ # last_name: "Doe",
157
+ # country: "USA"
158
+ # )
159
+ #
160
+ # @example Create a payout user (seller/merchant)
161
+ # users = ZaiPayment::Resources::User.new
162
+ # response = users.create(
163
+ # email: "seller@example.com",
164
+ # first_name: "Jane",
165
+ # last_name: "Smith",
166
+ # country: "AUS",
167
+ # dob: "19900101",
168
+ # address_line1: "456 Market St",
169
+ # city: "Sydney",
170
+ # state: "NSW",
171
+ # zip: "2000",
172
+ # mobile: "+61412345678"
173
+ # )
174
+ #
175
+ # @example Create a user with company details
176
+ # users = ZaiPayment::Resources::User.new
177
+ # response = users.create(
178
+ # email: "business@example.com",
179
+ # first_name: "John",
180
+ # last_name: "Doe",
181
+ # country: "AUS",
182
+ # mobile: "+61412345678",
183
+ # authorized_signer_title: "Director",
184
+ # company: {
185
+ # name: "ABC Company",
186
+ # legal_name: "ABC Pty Ltd",
187
+ # tax_number: "123456789",
188
+ # business_email: "admin@abc.com",
189
+ # country: "AUS",
190
+ # charge_tax: true,
191
+ # address_line1: "123 Business St",
192
+ # city: "Melbourne",
193
+ # state: "VIC",
194
+ # zip: "3000",
195
+ # phone: "+61398765432"
196
+ # }
197
+ # )
198
+ #
199
+ # @see https://developer.hellozai.com/reference/createuser
200
+ # @see https://developer.hellozai.com/docs/onboarding-a-pay-in-user
201
+ # @see https://developer.hellozai.com/docs/onboarding-a-pay-out-user
202
+ def create(**attributes)
203
+ validate_create_attributes!(attributes)
204
+
205
+ body = build_user_body(attributes)
206
+ client.post('/users', body: body)
207
+ end
208
+
209
+ # Update an existing user
210
+ #
211
+ # @param user_id [String] the user ID
212
+ # @param attributes [Hash] user attributes to update
213
+ # @option attributes [String] :email user's email address
214
+ # @option attributes [String] :first_name user's first name
215
+ # @option attributes [String] :last_name user's last name
216
+ # @option attributes [String] :mobile user's mobile phone number (international format with '+')
217
+ # @option attributes [String] :phone user's phone number
218
+ # @option attributes [String] :address_line1 user's address line 1
219
+ # @option attributes [String] :address_line2 user's address line 2
220
+ # @option attributes [String] :city user's city
221
+ # @option attributes [String] :state user's state
222
+ # @option attributes [String] :zip user's postal/zip code
223
+ # @option attributes [String] :dob user's date of birth (DD/MM/YYYY)
224
+ # @option attributes [String] :government_number user's government ID number (SSN, TFN, etc.)
225
+ # @option attributes [String] :drivers_license_number driving license number
226
+ # @option attributes [String] :drivers_license_state state section of the user's driving license
227
+ # @option attributes [String] :logo_url URL link to the logo
228
+ # @option attributes [String] :color_1 color code number 1
229
+ # @option attributes [String] :color_2 color code number 2
230
+ # @option attributes [String] :custom_descriptor custom descriptor for bundle direct debit statements
231
+ # @option attributes [String] :authorized_signer_title job title for AMEX merchants (e.g., Director)
232
+ # @return [Response] the API response containing updated user
233
+ #
234
+ # @example
235
+ # users = ZaiPayment::Resources::User.new
236
+ # response = users.update(
237
+ # "user_id",
238
+ # mobile: "+1234567890",
239
+ # address_line1: "789 New St"
240
+ # )
241
+ #
242
+ # @see https://developer.hellozai.com/reference/updateuser
243
+ def update(user_id, **attributes)
244
+ validate_id!(user_id, 'user_id')
245
+
246
+ body = build_user_body(attributes)
247
+
248
+ validate_email!(attributes[:email]) if attributes[:email]
249
+ validate_dob!(attributes[:dob]) if attributes[:dob]
250
+
251
+ raise Errors::ValidationError, 'At least one attribute must be provided for update' if body.empty?
252
+
253
+ client.patch("/users/#{user_id}", body: body)
254
+ end
255
+
256
+ private
257
+
258
+ def validate_id!(value, field_name)
259
+ return unless value.nil? || value.to_s.strip.empty?
260
+
261
+ raise Errors::ValidationError, "#{field_name} is required and cannot be blank"
262
+ end
263
+
264
+ def validate_presence!(value, field_name)
265
+ return unless value.nil? || value.to_s.strip.empty?
266
+
267
+ raise Errors::ValidationError, "#{field_name} is required and cannot be blank"
268
+ end
269
+
270
+ def validate_create_attributes!(attributes) # rubocop:disable Metrics/AbcSize
271
+ validate_required_attributes!(attributes)
272
+ validate_user_type!(attributes[:user_type]) if attributes[:user_type]
273
+ validate_email!(attributes[:email])
274
+ validate_country!(attributes[:country])
275
+ validate_dob!(attributes[:dob]) if attributes[:dob]
276
+ validate_user_id!(attributes[:id]) if attributes[:id]
277
+ validate_company!(attributes[:company]) if attributes[:company]
278
+ end
279
+
280
+ def validate_required_attributes!(attributes)
281
+ required_fields = %i[email first_name last_name country]
282
+
283
+ missing_fields = required_fields.select do |field|
284
+ attributes[field].nil? || attributes[field].to_s.strip.empty?
285
+ end
286
+
287
+ return if missing_fields.empty?
288
+
289
+ raise Errors::ValidationError,
290
+ "Missing required fields: #{missing_fields.join(', ')}"
291
+ end
292
+
293
+ def validate_user_type!(user_type)
294
+ return if VALID_USER_TYPES.include?(user_type.to_s.downcase)
295
+
296
+ raise Errors::ValidationError,
297
+ "user_type must be one of: #{VALID_USER_TYPES.join(', ')}"
298
+ end
299
+
300
+ def validate_email!(email)
301
+ # Basic email format validation
302
+ email_regex = /\A[^@\s]+@[^@\s]+\.[^@\s]+\z/
303
+ return if email&.match?(email_regex)
304
+
305
+ raise Errors::ValidationError, 'email must be a valid email address'
306
+ end
307
+
308
+ def validate_country!(country)
309
+ # Country should be ISO 3166-1 alpha-3 code (3 letters)
310
+ return if country.to_s.match?(/\A[A-Z]{3}\z/i)
311
+
312
+ raise Errors::ValidationError, 'country must be a valid ISO 3166-1 alpha-3 code (e.g., USA, AUS, GBR)'
313
+ end
314
+
315
+ def validate_dob!(dob)
316
+ # Date of birth should be in DD/MM/YYYY format
317
+ return if dob.to_s.match?(%r{\A\d{2}/\d{2}/\d{4}\z})
318
+
319
+ raise Errors::ValidationError, 'dob must be in DD/MM/YYYY format (e.g., 15/01/1990)'
320
+ end
321
+
322
+ def validate_user_id!(user_id)
323
+ # User ID cannot contain '.' character
324
+ raise Errors::ValidationError, "id cannot contain '.' character" if user_id.to_s.include?('.')
325
+
326
+ # Check if empty
327
+ return unless user_id.nil? || user_id.to_s.strip.empty?
328
+
329
+ raise Errors::ValidationError, 'id cannot be blank if provided'
330
+ end
331
+
332
+ def validate_company!(company)
333
+ return unless company.is_a?(Hash)
334
+
335
+ # Required company fields
336
+ required_company_fields = %i[name legal_name tax_number business_email country]
337
+
338
+ missing_fields = required_company_fields.select do |field|
339
+ company[field].nil? || company[field].to_s.strip.empty?
340
+ end
341
+
342
+ return if missing_fields.empty?
343
+
344
+ raise Errors::ValidationError,
345
+ "Company is missing required fields: #{missing_fields.join(', ')}"
346
+ end
347
+
348
+ def build_user_body(attributes) # rubocop:disable Metrics/CyclomaticComplexity
349
+ body = {}
350
+
351
+ attributes.each do |key, value|
352
+ next if value.nil? || (value.respond_to?(:empty?) && value.empty?)
353
+
354
+ # Handle company object separately
355
+ if key == :company
356
+ body[:company] = build_company_body(value) if value.is_a?(Hash)
357
+ next
358
+ end
359
+
360
+ api_field = FIELD_MAPPING[key]
361
+ body[api_field] = value if api_field
362
+ end
363
+
364
+ body
365
+ end
366
+
367
+ def build_company_body(company_attributes)
368
+ company = {}
369
+
370
+ company_attributes.each do |key, value|
371
+ # Don't skip false values for charge_tax
372
+ next if value.nil?
373
+ next if key != :charge_tax && value.respond_to?(:empty?) && value.empty?
374
+
375
+ api_field = COMPANY_FIELD_MAPPING[key]
376
+ company[api_field] = value if api_field
377
+ end
378
+
379
+ company
380
+ end
381
+ end
382
+ end
383
+ end
@@ -31,7 +31,7 @@ module ZaiPayment
31
31
 
32
32
  # Get the data from the response body
33
33
  def data
34
- body.is_a?(Hash) ? body['webhooks'] || body : body
34
+ body.is_a?(Hash) ? body['webhooks'] || body['users'] || body : body
35
35
  end
36
36
 
37
37
  # Get pagination or metadata info
@@ -68,10 +68,23 @@ module ZaiPayment
68
68
 
69
69
  def extract_error_message
70
70
  if body.is_a?(Hash)
71
- body['error'] || body['message'] || body['errors']&.join(', ') || "HTTP #{status}"
71
+ body['error'] || body['message'] || format_errors(body['errors']) || "HTTP #{status}"
72
72
  else
73
73
  "HTTP #{status}: #{body}"
74
74
  end
75
75
  end
76
+
77
+ def format_errors(errors)
78
+ return nil if errors.nil?
79
+
80
+ case errors
81
+ when Array
82
+ errors.join(', ')
83
+ when Hash
84
+ errors.map { |key, value| "#{key}: #{value}" }.join(', ')
85
+ else
86
+ errors.to_s
87
+ end
88
+ end
76
89
  end
77
90
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ZaiPayment
4
- VERSION = '1.2.0'
4
+ VERSION = '1.3.1'
5
5
  end
data/lib/zai_payment.rb CHANGED
@@ -11,6 +11,7 @@ require_relative 'zai_payment/auth/token_stores/memory_store'
11
11
  require_relative 'zai_payment/client'
12
12
  require_relative 'zai_payment/response'
13
13
  require_relative 'zai_payment/resources/webhook'
14
+ require_relative 'zai_payment/resources/user'
14
15
 
15
16
  module ZaiPayment
16
17
  class << self
@@ -39,5 +40,10 @@ module ZaiPayment
39
40
  def webhooks
40
41
  @webhooks ||= Resources::Webhook.new
41
42
  end
43
+
44
+ # @return [ZaiPayment::Resources::User] user resource instance
45
+ def users
46
+ @users ||= Resources::User.new(client: Client.new(base_endpoint: :core_base))
47
+ end
42
48
  end
43
49
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zai_payment
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eddy Jaga
@@ -60,16 +60,24 @@ extra_rdoc_files: []
60
60
  files:
61
61
  - CHANGELOG.md
62
62
  - CODE_OF_CONDUCT.md
63
+ - CONTRIBUTING.md
63
64
  - IMPLEMENTATION.md
65
+ - IMPLEMENTATION_SUMMARY.md
64
66
  - LICENSE.txt
65
67
  - README.md
66
68
  - Rakefile
69
+ - badges/.gitkeep
70
+ - badges/coverage.json
67
71
  - docs/ARCHITECTURE.md
68
72
  - docs/AUTHENTICATION.md
69
73
  - docs/README.md
74
+ - docs/USERS.md
75
+ - docs/USER_ID_FIELD.md
76
+ - docs/USER_QUICK_REFERENCE.md
70
77
  - docs/WEBHOOKS.md
71
78
  - docs/WEBHOOK_SECURITY_QUICKSTART.md
72
79
  - docs/WEBHOOK_SIGNATURE.md
80
+ - examples/users.md
73
81
  - examples/webhooks.md
74
82
  - lib/zai_payment.rb
75
83
  - lib/zai_payment/auth/token_provider.rb
@@ -78,6 +86,7 @@ files:
78
86
  - lib/zai_payment/client.rb
79
87
  - lib/zai_payment/config.rb
80
88
  - lib/zai_payment/errors.rb
89
+ - lib/zai_payment/resources/user.rb
81
90
  - lib/zai_payment/resources/webhook.rb
82
91
  - lib/zai_payment/response.rb
83
92
  - lib/zai_payment/version.rb