zai_payment 2.6.1 → 2.8.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,302 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZaiPayment
4
+ module Resources
5
+ # BatchTransaction resource for managing Zai batch transactions (Prelive only)
6
+ #
7
+ # @note These endpoints are only available in the prelive environment
8
+ class BatchTransaction
9
+ attr_reader :client, :config
10
+
11
+ # Valid transaction types
12
+ TRANSACTION_TYPES = %w[payment refund disbursement fee deposit withdrawal].freeze
13
+
14
+ # Valid transaction type methods
15
+ TRANSACTION_TYPE_METHODS = %w[credit_card npp bpay wallet_account_transfer wire_transfer misc].freeze
16
+
17
+ # Valid directions
18
+ DIRECTIONS = %w[debit credit].freeze
19
+
20
+ def initialize(client: nil, config: nil)
21
+ @client = client || Client.new(base_endpoint: :core_base)
22
+ @config = config || ZaiPayment.config
23
+ end
24
+
25
+ # List batch transactions
26
+ #
27
+ # Retrieve an ordered and paginated list of existing batch transactions.
28
+ # The list can be filtered by account, batch ID, item, and transaction type.
29
+ #
30
+ # @param options [Hash] optional filters
31
+ # @option options [Integer] :limit number of records to return (default: 10, max: 200)
32
+ # @option options [Integer] :offset number of records to skip (default: 0)
33
+ # @option options [String] :account_id Bank, Card or Wallet Account ID
34
+ # @option options [String] :batch_id Batch ID
35
+ # @option options [String] :item_id Item ID
36
+ # @option options [String] :transaction_type transaction type
37
+ # (payment, refund, disbursement, fee, deposit, withdrawal)
38
+ # @option options [String] :transaction_type_method transaction method (credit_card, npp, bpay, etc.)
39
+ # @option options [String] :direction direction (debit, credit)
40
+ # @option options [String] :created_before ISO 8601 date/time to filter transactions created before
41
+ # @option options [String] :created_after ISO 8601 date/time to filter transactions created after
42
+ # @option options [String] :disbursement_bank the bank used for disbursing the payment
43
+ # @option options [String] :processing_bank the bank used for processing the payment
44
+ # @return [Response] the API response containing batch_transactions array
45
+ #
46
+ # @example List all batch transactions
47
+ # batch_transactions = ZaiPayment.batch_transactions
48
+ # response = batch_transactions.list
49
+ # response.data # => [{"id" => 12484, "status" => 12200, ...}]
50
+ #
51
+ # @example List with filters
52
+ # response = batch_transactions.list(
53
+ # transaction_type: 'disbursement',
54
+ # direction: 'credit',
55
+ # limit: 50
56
+ # )
57
+ #
58
+ # @see https://developer.hellozai.com/reference/listbatchtransactions
59
+ def list(**options)
60
+ validate_list_options(options)
61
+
62
+ params = build_list_params(options)
63
+
64
+ client.get('/batch_transactions', params: params)
65
+ end
66
+
67
+ # Show a batch transaction
68
+ #
69
+ # Get a batch transaction using its ID (UUID or numeric ID).
70
+ #
71
+ # @param id [String] the batch transaction ID
72
+ # @return [Response] the API response containing batch_transactions object
73
+ #
74
+ # @example Get a batch transaction by UUID
75
+ # batch_transactions = ZaiPayment.batch_transactions
76
+ # response = batch_transactions.show('90c1418b-f4f4-413e-a4ba-f29c334e7f55')
77
+ # response.data # => {"id" => 13143, "uuid" => "90c1418b-f4f4-413e-a4ba-f29c334e7f55", ...}
78
+ #
79
+ # @example Get a batch transaction by numeric ID
80
+ # response = batch_transactions.show('13143')
81
+ # response.data['state'] # => "successful"
82
+ #
83
+ # @raise [Errors::ValidationError] if id is blank
84
+ #
85
+ # @see https://developer.hellozai.com/reference/showbatchtransaction
86
+ def show(id)
87
+ validate_id!(id, 'id')
88
+
89
+ client.get("/batch_transactions/#{id}")
90
+ end
91
+
92
+ # Export batch transactions (Prelive only)
93
+ #
94
+ # Calls the GET /batch_transactions/export_transactions API which moves all pending
95
+ # batch_transactions into batched state. As a result, this API will return all the
96
+ # batch_transactions that have moved from pending to batched. Please store the id
97
+ # in order to progress it in Pre-live.
98
+ #
99
+ # @return [Response] the API response containing transactions array with batch_id
100
+ #
101
+ # @example Export transactions
102
+ # batch_transactions = ZaiPayment.batch_transactions
103
+ # response = batch_transactions.export_transactions
104
+ # response.data # => {"transactions" => [{"id" => "...", "batch_id" => "...", "status" => "batched", ...}]}
105
+ #
106
+ # @raise [Errors::ConfigurationError] if not in prelive environment
107
+ #
108
+ # @see https://developer.hellozai.com/reference (Prelive endpoints)
109
+ def export_transactions
110
+ ensure_prelive_environment!
111
+
112
+ client.get('/batch_transactions/export_transactions')
113
+ end
114
+
115
+ # Update batch transaction states (Prelive only)
116
+ #
117
+ # Calls the PATCH /batches/:id/transaction_states API which moves one or more
118
+ # batch_transactions into a specific state. You will need to pass in the batch_id
119
+ # from the export_transactions response.
120
+ #
121
+ # State codes:
122
+ # - 12700: bank_processing state
123
+ # - 12000: successful state (final state, triggers webhook)
124
+ #
125
+ # @param batch_id [String] the batch ID from export_transactions response
126
+ # @param exported_ids [Array<String>] array of transaction IDs to update
127
+ # @param state [Integer] the target state code (12700 or 12000)
128
+ # @return [Response] the API response containing job information
129
+ #
130
+ # @example Move transactions to bank_processing state
131
+ # batch_transactions = ZaiPayment.batch_transactions
132
+ # response = batch_transactions.update_transaction_states(
133
+ # "batch_id",
134
+ # exported_ids: ["439970a2-e0a1-418e-aecf-6b519c115c55"],
135
+ # state: 12700
136
+ # )
137
+ # response.body # => {
138
+ # "aggregated_jobs_uuid" => "c1cbc502-9754-42fd-9731-2330ddd7a41f",
139
+ # "msg" => "1 jobs have been sent to the queue.",
140
+ # "errors" => []
141
+ # }
142
+ #
143
+ # @example Move transactions to successful state
144
+ # response = batch_transactions.update_transaction_states(
145
+ # "batch_id",
146
+ # exported_ids: ["439970a2-e0a1-418e-aecf-6b519c115c55"],
147
+ # state: 12000
148
+ # )
149
+ # response.body # => {
150
+ # "aggregated_jobs_uuid" => "...",
151
+ # "msg" => "1 jobs have been sent to the queue.",
152
+ # "errors" => []
153
+ # }
154
+ #
155
+ # @raise [Errors::ConfigurationError] if not in prelive environment
156
+ # @raise [Errors::ValidationError] if parameters are invalid
157
+ #
158
+ # @see https://developer.hellozai.com/reference (Prelive endpoints)
159
+ def update_transaction_states(batch_id, exported_ids:, state:)
160
+ ensure_prelive_environment!
161
+ validate_id!(batch_id, 'batch_id')
162
+ validate_exported_ids!(exported_ids)
163
+ validate_state!(state)
164
+
165
+ body = {
166
+ exported_ids: exported_ids,
167
+ state: state
168
+ }
169
+
170
+ client.patch("/batches/#{batch_id}/transaction_states", body: body)
171
+ end
172
+
173
+ # Move transactions to bank_processing state (Prelive only)
174
+ #
175
+ # Convenience method that calls update_transaction_states with state 12700.
176
+ # This simulates the step where transactions are moved to bank_processing state.
177
+ #
178
+ # @param batch_id [String] the batch ID from export_transactions response
179
+ # @param exported_ids [Array<String>] array of transaction IDs to update
180
+ # @return [Response] the API response with aggregated_jobs_uuid, msg, and errors
181
+ #
182
+ # @example
183
+ # batch_transactions = ZaiPayment.batch_transactions
184
+ # response = batch_transactions.process_to_bank_processing(
185
+ # "batch_id",
186
+ # exported_ids: ["439970a2-e0a1-418e-aecf-6b519c115c55"]
187
+ # )
188
+ # response.body["msg"] # => "1 jobs have been sent to the queue."
189
+ #
190
+ # @raise [Errors::ConfigurationError] if not in prelive environment
191
+ # @raise [Errors::ValidationError] if parameters are invalid
192
+ def process_to_bank_processing(batch_id, exported_ids:)
193
+ update_transaction_states(batch_id, exported_ids: exported_ids, state: 12_700)
194
+ end
195
+
196
+ # Move transactions to successful state (Prelive only)
197
+ #
198
+ # Convenience method that calls update_transaction_states with state 12000.
199
+ # This simulates the final step where transactions are marked as successful
200
+ # and triggers the batch_transactions webhook.
201
+ #
202
+ # @param batch_id [String] the batch ID from export_transactions response
203
+ # @param exported_ids [Array<String>] array of transaction IDs to update
204
+ # @return [Response] the API response with aggregated_jobs_uuid, msg, and errors
205
+ #
206
+ # @example
207
+ # batch_transactions = ZaiPayment.batch_transactions
208
+ # response = batch_transactions.process_to_successful(
209
+ # "batch_id",
210
+ # exported_ids: ["439970a2-e0a1-418e-aecf-6b519c115c55"]
211
+ # )
212
+ # response.body["msg"] # => "1 jobs have been sent to the queue."
213
+ #
214
+ # @raise [Errors::ConfigurationError] if not in prelive environment
215
+ # @raise [Errors::ValidationError] if parameters are invalid
216
+ def process_to_successful(batch_id, exported_ids:)
217
+ update_transaction_states(batch_id, exported_ids: exported_ids, state: 12_000)
218
+ end
219
+
220
+ private
221
+
222
+ def ensure_prelive_environment!
223
+ return if config.environment.to_sym == :prelive
224
+
225
+ raise Errors::ConfigurationError,
226
+ 'Batch transaction endpoints are only available in prelive environment. ' \
227
+ "Current environment: #{config.environment}"
228
+ end
229
+
230
+ def validate_id!(value, field_name)
231
+ return unless value.nil? || value.to_s.strip.empty?
232
+
233
+ raise Errors::ValidationError, "#{field_name} is required and cannot be blank"
234
+ end
235
+
236
+ def validate_exported_ids!(exported_ids)
237
+ if exported_ids.nil? || !exported_ids.is_a?(Array) || exported_ids.empty?
238
+ raise Errors::ValidationError,
239
+ 'exported_ids is required and must be a non-empty array'
240
+ end
241
+
242
+ return unless exported_ids.any? { |id| id.nil? || id.to_s.strip.empty? }
243
+
244
+ raise Errors::ValidationError,
245
+ 'exported_ids cannot contain nil or empty values'
246
+ end
247
+
248
+ def validate_state!(state)
249
+ valid_states = [12_700, 12_000]
250
+
251
+ return if valid_states.include?(state)
252
+
253
+ raise Errors::ValidationError,
254
+ "state must be 12700 (bank_processing) or 12000 (successful), got: #{state}"
255
+ end
256
+
257
+ def validate_transaction_type!(transaction_type)
258
+ return if TRANSACTION_TYPES.include?(transaction_type.to_s)
259
+
260
+ raise Errors::ValidationError,
261
+ "transaction_type must be one of: #{TRANSACTION_TYPES.join(', ')}"
262
+ end
263
+
264
+ def validate_transaction_type_method!(transaction_type_method)
265
+ return if TRANSACTION_TYPE_METHODS.include?(transaction_type_method.to_s)
266
+
267
+ raise Errors::ValidationError,
268
+ "transaction_type_method must be one of: #{TRANSACTION_TYPE_METHODS.join(', ')}"
269
+ end
270
+
271
+ def validate_direction!(direction)
272
+ return if DIRECTIONS.include?(direction.to_s)
273
+
274
+ raise Errors::ValidationError,
275
+ "direction must be one of: #{DIRECTIONS.join(', ')}"
276
+ end
277
+
278
+ def validate_list_options(options)
279
+ validate_transaction_type!(options[:transaction_type]) if options[:transaction_type]
280
+ validate_transaction_type_method!(options[:transaction_type_method]) if options[:transaction_type_method]
281
+ validate_direction!(options[:direction]) if options[:direction]
282
+ end
283
+
284
+ def build_list_params(options)
285
+ {
286
+ limit: options.fetch(:limit, 10),
287
+ offset: options.fetch(:offset, 0),
288
+ account_id: options[:account_id],
289
+ batch_id: options[:batch_id],
290
+ item_id: options[:item_id],
291
+ transaction_type: options[:transaction_type],
292
+ transaction_type_method: options[:transaction_type_method],
293
+ direction: options[:direction],
294
+ created_before: options[:created_before],
295
+ created_after: options[:created_after],
296
+ disbursement_bank: options[:disbursement_bank],
297
+ processing_bank: options[:processing_bank]
298
+ }.compact
299
+ end
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZaiPayment
4
+ module Resources
5
+ # PayID resource for managing Zai PayID registrations
6
+ #
7
+ # @see https://developer.hellozai.com/reference/registerpayid
8
+ class PayId
9
+ attr_reader :client
10
+
11
+ # Map of attribute keys to API field names for create
12
+ CREATE_FIELD_MAPPING = {
13
+ pay_id: :pay_id,
14
+ type: :type,
15
+ details: :details
16
+ }.freeze
17
+
18
+ # Valid PayID types
19
+ VALID_TYPES = %w[EMAIL].freeze
20
+
21
+ # Valid PayID statuses for update
22
+ VALID_STATUSES = %w[deregistered].freeze
23
+
24
+ def initialize(client: nil)
25
+ @client = client || Client.new(base_endpoint: :va_base)
26
+ end
27
+
28
+ # Register a PayID for a given Virtual Account
29
+ #
30
+ # @param virtual_account_id [String] the virtual account ID
31
+ # @param attributes [Hash] PayID attributes
32
+ # @option attributes [String] :pay_id (Required) The PayID being registered (max 256 chars)
33
+ # @option attributes [String] :type (Required) The type of PayID ('EMAIL')
34
+ # @option attributes [Hash] :details (Required) Additional details
35
+ # @option details [String] :pay_id_name Name to identify the entity (1-140 chars)
36
+ # @option details [String] :owner_legal_name Full legal account name (1-140 chars)
37
+ # @return [Response] the API response containing PayID details
38
+ #
39
+ # @example Register an EMAIL PayID
40
+ # pay_ids = ZaiPayment::Resources::PayId.new
41
+ # response = pay_ids.create(
42
+ # '46deb476-c1a6-41eb-8eb7-26a695bbe5bc',
43
+ # pay_id: 'jsmith@mydomain.com',
44
+ # type: 'EMAIL',
45
+ # details: {
46
+ # pay_id_name: 'J Smith',
47
+ # owner_legal_name: 'Mr John Smith'
48
+ # }
49
+ # )
50
+ # response.data # => {"id" => "46deb476-c1a6-41eb-8eb7-26a695bbe5bc", ...}
51
+ #
52
+ # @see https://developer.hellozai.com/reference/registerpayid
53
+ def create(virtual_account_id, **attributes)
54
+ validate_id!(virtual_account_id, 'virtual_account_id')
55
+ validate_create_attributes!(attributes)
56
+
57
+ body = build_create_body(attributes)
58
+ client.post("/virtual_accounts/#{virtual_account_id}/pay_ids", body: body)
59
+ end
60
+
61
+ # Show a specific PayID
62
+ #
63
+ # @param pay_id_id [String] the PayID ID
64
+ # @return [Response] the API response containing PayID details
65
+ #
66
+ # @example Get PayID details
67
+ # pay_ids = ZaiPayment::Resources::PayId.new
68
+ # response = pay_ids.show('46deb476-c1a6-41eb-8eb7-26a695bbe5bc')
69
+ # response.data # => {"id" => "46deb476-c1a6-41eb-8eb7-26a695bbe5bc", ...}
70
+ #
71
+ # @see https://developer.hellozai.com/reference/retrieveapayid
72
+ def show(pay_id_id)
73
+ validate_id!(pay_id_id, 'pay_id_id')
74
+ client.get("/pay_ids/#{pay_id_id}")
75
+ end
76
+
77
+ # Update Status for a PayID
78
+ #
79
+ # Update the status of a PayID. Currently, this endpoint only supports deregistering
80
+ # PayIDs by setting the status to 'deregistered'. This is an asynchronous operation
81
+ # that returns a 202 Accepted response.
82
+ #
83
+ # @param pay_id_id [String] the PayID ID
84
+ # @param status [String] the new status (must be 'deregistered')
85
+ # @return [Response] the API response containing the operation status
86
+ #
87
+ # @example Deregister a PayID
88
+ # pay_ids = ZaiPayment::Resources::PayId.new
89
+ # response = pay_ids.update_status(
90
+ # '46deb476-c1a6-41eb-8eb7-26a695bbe5bc',
91
+ # 'deregistered'
92
+ # )
93
+ # response.data # => {"id" => "46deb476-c1a6-41eb-8eb7-26a695bbe5bc", "message" => "...", ...}
94
+ #
95
+ # @see https://developer.hellozai.com/reference/updatepayidstatus
96
+ def update_status(pay_id_id, status)
97
+ validate_id!(pay_id_id, 'pay_id_id')
98
+ validate_status!(status)
99
+
100
+ body = { status: status }
101
+ client.patch("/pay_ids/#{pay_id_id}/status", body: body)
102
+ end
103
+
104
+ private
105
+
106
+ def validate_id!(value, field_name)
107
+ return unless value.nil? || value.to_s.strip.empty?
108
+
109
+ raise Errors::ValidationError, "#{field_name} is required and cannot be blank"
110
+ end
111
+
112
+ def validate_create_attributes!(attributes)
113
+ validate_pay_id!(attributes[:pay_id])
114
+ validate_type!(attributes[:type])
115
+ validate_details!(attributes[:details])
116
+ end
117
+
118
+ def validate_pay_id!(pay_id)
119
+ if pay_id.nil? || pay_id.to_s.strip.empty?
120
+ raise Errors::ValidationError, 'pay_id is required and cannot be blank'
121
+ end
122
+
123
+ return unless pay_id.to_s.length > 256
124
+
125
+ raise Errors::ValidationError, 'pay_id must be 256 characters or less'
126
+ end
127
+
128
+ def validate_type!(type)
129
+ raise Errors::ValidationError, 'type is required and cannot be blank' if type.nil? || type.to_s.strip.empty?
130
+
131
+ return if VALID_TYPES.include?(type.to_s.upcase)
132
+
133
+ raise Errors::ValidationError,
134
+ "type must be one of: #{VALID_TYPES.join(', ')}, got '#{type}'"
135
+ end
136
+
137
+ def validate_details!(details)
138
+ raise Errors::ValidationError, 'details is required and must be a hash' if details.nil? || !details.is_a?(Hash)
139
+
140
+ validate_pay_id_name!(details[:pay_id_name])
141
+ validate_owner_legal_name!(details[:owner_legal_name])
142
+ end
143
+
144
+ def validate_pay_id_name!(pay_id_name)
145
+ return unless pay_id_name
146
+
147
+ raise Errors::ValidationError, 'pay_id_name cannot be empty when provided' if pay_id_name.to_s.empty?
148
+
149
+ return unless pay_id_name.to_s.length > 140
150
+
151
+ raise Errors::ValidationError, 'pay_id_name must be between 1 and 140 characters'
152
+ end
153
+
154
+ def validate_owner_legal_name!(owner_legal_name)
155
+ return unless owner_legal_name
156
+
157
+ raise Errors::ValidationError, 'owner_legal_name cannot be empty when provided' if owner_legal_name.to_s.empty?
158
+
159
+ return unless owner_legal_name.to_s.length > 140
160
+
161
+ raise Errors::ValidationError, 'owner_legal_name must be between 1 and 140 characters'
162
+ end
163
+
164
+ def validate_status!(status)
165
+ raise Errors::ValidationError, 'status cannot be blank' if status.nil? || status.to_s.strip.empty?
166
+
167
+ return if VALID_STATUSES.include?(status.to_s)
168
+
169
+ raise Errors::ValidationError,
170
+ "status must be 'deregistered', got '#{status}'"
171
+ end
172
+
173
+ # rubocop:disable Metrics/AbcSize
174
+ def build_create_body(attributes)
175
+ body = {}
176
+
177
+ # Add pay_id
178
+ body[:pay_id] = attributes[:pay_id] if attributes[:pay_id]
179
+
180
+ # Add type (convert to uppercase to match API expectations)
181
+ body[:type] = attributes[:type].to_s.upcase if attributes[:type]
182
+
183
+ # Add details
184
+ if attributes[:details].is_a?(Hash)
185
+ body[:details] = {}
186
+ details = attributes[:details]
187
+
188
+ body[:details][:pay_id_name] = details[:pay_id_name] if details[:pay_id_name]
189
+ body[:details][:owner_legal_name] = details[:owner_legal_name] if details[:owner_legal_name]
190
+ end
191
+
192
+ body
193
+ end
194
+ # rubocop:enable Metrics/AbcSize
195
+ end
196
+ end
197
+ end