zai_payment 2.9.0 → 2.9.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.
- checksums.yaml +4 -4
- data/lib/zai_payment/version.rb +1 -1
- metadata +6 -43
- data/.yardopts +0 -3
- data/IMPLEMENTATION_SUMMARY.md +0 -183
- data/RESPONSE_FORMAT_CORRECTION.md +0 -75
- data/Rakefile +0 -12
- data/badges/.gitkeep +0 -2
- data/badges/coverage.json +0 -1
- data/changelog.md +0 -750
- data/code_of_conduct.md +0 -132
- data/contributing.md +0 -383
- data/docs/architecture.md +0 -232
- data/docs/authentication.md +0 -647
- data/docs/bank_accounts.md +0 -496
- data/docs/batch_transactions.md +0 -340
- data/docs/bpay_accounts.md +0 -519
- data/docs/direct_api_usage.md +0 -489
- data/docs/items.md +0 -1241
- data/docs/pay_ids.md +0 -777
- data/docs/readme.md +0 -111
- data/docs/token_auths.md +0 -523
- data/docs/user_id_field.md +0 -284
- data/docs/user_quick_reference.md +0 -230
- data/docs/users.md +0 -750
- data/docs/virtual_accounts.md +0 -916
- data/docs/wallet_accounts.md +0 -493
- data/docs/webhook_security_quickstart.md +0 -136
- data/docs/webhook_signature.md +0 -244
- data/docs/webhooks.md +0 -417
- data/examples/bank_accounts.md +0 -637
- data/examples/batch_transactions.md +0 -450
- data/examples/bpay_accounts.md +0 -642
- data/examples/items.md +0 -2713
- data/examples/pay_ids.md +0 -871
- data/examples/rails_card_payment.md +0 -1252
- data/examples/token_auths.md +0 -687
- data/examples/users.md +0 -765
- data/examples/virtual_accounts.md +0 -1530
- data/examples/wallet_accounts.md +0 -733
- data/examples/webhooks.md +0 -635
- data/readme.md +0 -357
- data/sig/zai_payment.rbs +0 -4
|
@@ -1,1252 +0,0 @@
|
|
|
1
|
-
# Rails Card Payment Example
|
|
2
|
-
|
|
3
|
-
This guide demonstrates how to implement a complete card payment workflow in a Rails application using the `zai_payment` gem.
|
|
4
|
-
|
|
5
|
-
## Table of Contents
|
|
6
|
-
|
|
7
|
-
1. [Setup](#setup)
|
|
8
|
-
2. [Configuration](#configuration)
|
|
9
|
-
3. [User Management](#user-management)
|
|
10
|
-
4. [Card Account Setup](#card-account-setup)
|
|
11
|
-
5. [Creating an Item](#creating-an-item)
|
|
12
|
-
6. [Making a Payment](#making-a-payment)
|
|
13
|
-
7. [Webhook Handling](#webhook-handling)
|
|
14
|
-
8. [Complete Flow Example](#complete-flow-example)
|
|
15
|
-
9. [Reference](#reference)
|
|
16
|
-
|
|
17
|
-
## Setup
|
|
18
|
-
|
|
19
|
-
Add the gem to your Gemfile:
|
|
20
|
-
|
|
21
|
-
```ruby
|
|
22
|
-
gem 'zai_payment'
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Then run:
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
bundle install
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Configuration
|
|
32
|
-
|
|
33
|
-
Configure the gem in an initializer (`config/initializers/zai_payment.rb`):
|
|
34
|
-
|
|
35
|
-
```ruby
|
|
36
|
-
ZaiPayment.configure do |config|
|
|
37
|
-
config.environment = Rails.env.production? ? 'production' : 'prelive'
|
|
38
|
-
config.client_id = ENV.fetch("ZAI_CLIENT_ID")
|
|
39
|
-
config.client_secret = ENV.fetch("ZAI_CLIENT_SECRET")
|
|
40
|
-
config.scope = ENV.fetch("ZAI_OAUTH_SCOPE")
|
|
41
|
-
end
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## User Management
|
|
45
|
-
|
|
46
|
-
### Creating Users
|
|
47
|
-
|
|
48
|
-
```ruby
|
|
49
|
-
# In your controller or service object
|
|
50
|
-
class PaymentService
|
|
51
|
-
def initialize
|
|
52
|
-
@client = ZaiPayment::Client.new
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Create a buyer
|
|
56
|
-
def create_buyer(email:, first_name:, last_name:, mobile:)
|
|
57
|
-
response = @client.users.create(
|
|
58
|
-
id: "buyer_#{SecureRandom.hex(8)}", # Your internal user ID
|
|
59
|
-
first_name: first_name,
|
|
60
|
-
last_name: last_name,
|
|
61
|
-
email: email,
|
|
62
|
-
mobile: mobile,
|
|
63
|
-
country: 'AUS'
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
# Store zai_user_id in your database
|
|
67
|
-
response.data['id']
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Create a seller
|
|
71
|
-
def create_seller(email:, first_name:, last_name:, mobile:)
|
|
72
|
-
response = @client.users.create(
|
|
73
|
-
id: "seller_#{SecureRandom.hex(8)}",
|
|
74
|
-
first_name: first_name,
|
|
75
|
-
last_name: last_name,
|
|
76
|
-
email: email,
|
|
77
|
-
mobile: mobile,
|
|
78
|
-
country: 'AUS'
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
response.data['id']
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### Example Controller
|
|
87
|
-
|
|
88
|
-
```ruby
|
|
89
|
-
# app/controllers/users_controller.rb
|
|
90
|
-
class UsersController < ApplicationController
|
|
91
|
-
def create_zai_user
|
|
92
|
-
service = PaymentService.new
|
|
93
|
-
|
|
94
|
-
zai_user_id = service.create_buyer(
|
|
95
|
-
email: current_user.email,
|
|
96
|
-
first_name: current_user.first_name,
|
|
97
|
-
last_name: current_user.last_name,
|
|
98
|
-
mobile: current_user.phone
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
# Update your user record
|
|
102
|
-
current_user.update(zai_user_id: zai_user_id)
|
|
103
|
-
|
|
104
|
-
redirect_to dashboard_path, notice: 'User created successfully'
|
|
105
|
-
rescue ZaiPayment::Errors::ApiError => e
|
|
106
|
-
redirect_to dashboard_path, alert: "Error: #{e.message}"
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
## Card Account Setup
|
|
112
|
-
|
|
113
|
-
### Generating a Card Auth Token
|
|
114
|
-
|
|
115
|
-
```ruby
|
|
116
|
-
# app/controllers/card_accounts_controller.rb
|
|
117
|
-
class CardAccountsController < ApplicationController
|
|
118
|
-
def new
|
|
119
|
-
# Generate a card auth token for the hosted form
|
|
120
|
-
client = ZaiPayment::Client.new
|
|
121
|
-
|
|
122
|
-
response = client.token_auths.create(
|
|
123
|
-
token_type: 'card',
|
|
124
|
-
user_id: current_user.zai_user_id
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
@card_token = response.data['token']
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### View with Hosted Form
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
```erb
|
|
136
|
-
<!-- app/views/card_accounts/new.html.erb -->
|
|
137
|
-
<div class="card-form-container">
|
|
138
|
-
<h2>Add Your Card</h2>
|
|
139
|
-
|
|
140
|
-
<div id="card-container"></div>
|
|
141
|
-
|
|
142
|
-
<script src="https://hosted.assemblypay.com/assembly.js"></script>
|
|
143
|
-
<script>
|
|
144
|
-
let dropinHandler = DropIn.create({
|
|
145
|
-
cardTokenAuth: '<%= @card_token %>',
|
|
146
|
-
environment: '<%= Rails.env.production? ? 'production' : 'prelive' %>',
|
|
147
|
-
targetElementId: '#card-container',
|
|
148
|
-
cardAccountCreationCallback: function(cardAccountResult) {
|
|
149
|
-
// Send the card account ID to your server
|
|
150
|
-
fetch('/card_accounts', {
|
|
151
|
-
method: 'POST',
|
|
152
|
-
headers: {
|
|
153
|
-
'Content-Type': 'application/json',
|
|
154
|
-
'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
|
|
155
|
-
},
|
|
156
|
-
body: JSON.stringify({
|
|
157
|
-
card_account_id: cardAccountResult.id
|
|
158
|
-
})
|
|
159
|
-
})
|
|
160
|
-
.then(response => response.json())
|
|
161
|
-
.then(data => {
|
|
162
|
-
alert("Card saved successfully!");
|
|
163
|
-
window.location.href = '/payments/new';
|
|
164
|
-
})
|
|
165
|
-
.catch(error => {
|
|
166
|
-
alert("Error saving card: " + error);
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
}, function (error, instance) {
|
|
170
|
-
if(error) {
|
|
171
|
-
alert("Error: " + error);
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
</script>
|
|
175
|
-
</div>
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
> **Note:** For more details on the Hosted Form integration, including PCI compliance, supported browsers, and error handling, see the [Zai Hosted Form Documentation](https://developer.hellozai.com/docs/integrating-drop-in-ui-for-capturing-a-credit-card).
|
|
179
|
-
|
|
180
|
-
### Storing Card Account ID
|
|
181
|
-
|
|
182
|
-
```ruby
|
|
183
|
-
# app/controllers/card_accounts_controller.rb
|
|
184
|
-
class CardAccountsController < ApplicationController
|
|
185
|
-
def create
|
|
186
|
-
# Store the card account ID returned from the hosted form
|
|
187
|
-
current_user.update(
|
|
188
|
-
zai_card_account_id: params[:card_account_id]
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
render json: { success: true }
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
# List user's card accounts
|
|
195
|
-
def index
|
|
196
|
-
client = ZaiPayment::Client.new
|
|
197
|
-
|
|
198
|
-
response = client.card_accounts.list(
|
|
199
|
-
user_id: current_user.zai_user_id
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
@card_accounts = response.data['card_accounts']
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
## Creating an Item
|
|
208
|
-
|
|
209
|
-
```ruby
|
|
210
|
-
# app/controllers/items_controller.rb
|
|
211
|
-
class ItemsController < ApplicationController
|
|
212
|
-
def create
|
|
213
|
-
client = ZaiPayment::Client.new
|
|
214
|
-
|
|
215
|
-
# Assuming you have a transaction/order model
|
|
216
|
-
transaction = current_user.transactions.create!(
|
|
217
|
-
amount: params[:amount],
|
|
218
|
-
seller_id: params[:seller_id],
|
|
219
|
-
description: params[:description]
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
# Create item in Zai
|
|
223
|
-
response = client.items.create(
|
|
224
|
-
id: "order_#{transaction.id}", # Your internal order ID
|
|
225
|
-
name: params[:description],
|
|
226
|
-
amount: (params[:amount].to_f * 100).to_i, # Convert to cents
|
|
227
|
-
payment_type: 1, # 1 = Payin
|
|
228
|
-
buyer_id: current_user.zai_user_id,
|
|
229
|
-
seller_id: User.find(params[:seller_id]).zai_user_id,
|
|
230
|
-
fee_ids: '', # Optional: platform fees
|
|
231
|
-
description: params[:description]
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
zai_item_id = response.data['id']
|
|
235
|
-
transaction.update(zai_item_id: zai_item_id)
|
|
236
|
-
|
|
237
|
-
redirect_to payment_path(transaction), notice: 'Ready to make payment'
|
|
238
|
-
rescue ZaiPayment::Errors::ApiError => e
|
|
239
|
-
redirect_to new_item_path, alert: "Error: #{e.message}"
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
## Making a Payment
|
|
245
|
-
|
|
246
|
-
Once you have an item created and the buyer has a card account, you can process the payment.
|
|
247
|
-
|
|
248
|
-
### Basic Payment
|
|
249
|
-
|
|
250
|
-
```ruby
|
|
251
|
-
# app/controllers/payments_controller.rb
|
|
252
|
-
class PaymentsController < ApplicationController
|
|
253
|
-
def create
|
|
254
|
-
transaction = current_user.transactions.find(params[:transaction_id])
|
|
255
|
-
client = ZaiPayment::Client.new
|
|
256
|
-
|
|
257
|
-
# Make payment using card account
|
|
258
|
-
response = client.items.make_payment(
|
|
259
|
-
transaction.zai_item_id,
|
|
260
|
-
account_id: params[:card_account_id] # Required
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
if response.success?
|
|
264
|
-
transaction.update(
|
|
265
|
-
status: 'processing',
|
|
266
|
-
payment_state: response.data['payment_state']
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
redirect_to transaction_path(transaction),
|
|
270
|
-
notice: 'Payment initiated successfully!'
|
|
271
|
-
else
|
|
272
|
-
redirect_to payment_path(transaction),
|
|
273
|
-
alert: "Payment failed: #{response.error_message}"
|
|
274
|
-
end
|
|
275
|
-
rescue ZaiPayment::Errors::ValidationError => e
|
|
276
|
-
# Handles missing account_id or other validation errors
|
|
277
|
-
redirect_to payment_path(transaction), alert: "Validation error: #{e.message}"
|
|
278
|
-
rescue ZaiPayment::Errors::ApiError => e
|
|
279
|
-
redirect_to payment_path(transaction), alert: "Error: #{e.message}"
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### Payment with Fraud Protection
|
|
285
|
-
|
|
286
|
-
Include device information and IP address for enhanced fraud protection:
|
|
287
|
-
|
|
288
|
-
```ruby
|
|
289
|
-
def create
|
|
290
|
-
transaction = current_user.transactions.find(params[:transaction_id])
|
|
291
|
-
client = ZaiPayment::Client.new
|
|
292
|
-
|
|
293
|
-
response = client.items.make_payment(
|
|
294
|
-
transaction.zai_item_id,
|
|
295
|
-
account_id: params[:card_account_id], # Required
|
|
296
|
-
device_id: session[:device_id], # Track device
|
|
297
|
-
ip_address: request.remote_ip, # Client IP address
|
|
298
|
-
merchant_phone: current_user.phone # Merchant contact
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
if response.success?
|
|
302
|
-
transaction.update(
|
|
303
|
-
status: 'processing',
|
|
304
|
-
payment_state: response.data['payment_state'],
|
|
305
|
-
zai_state: response.data['state']
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
# Log the payment for tracking
|
|
309
|
-
Rails.logger.info "Payment initiated: Item #{transaction.zai_item_id}, IP: #{request.remote_ip}"
|
|
310
|
-
|
|
311
|
-
flash[:notice] = 'Payment is being processed. You will receive confirmation shortly.'
|
|
312
|
-
redirect_to transaction_path(transaction)
|
|
313
|
-
else
|
|
314
|
-
handle_payment_error(transaction, response)
|
|
315
|
-
end
|
|
316
|
-
rescue ZaiPayment::Errors::ApiError => e
|
|
317
|
-
handle_payment_exception(transaction, e)
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
private
|
|
321
|
-
|
|
322
|
-
def handle_payment_error(transaction, response)
|
|
323
|
-
case response.status
|
|
324
|
-
when 422
|
|
325
|
-
# Validation error - likely card declined or insufficient funds
|
|
326
|
-
flash[:alert] = "Payment declined: #{response.error_message}"
|
|
327
|
-
when 404
|
|
328
|
-
flash[:alert] = "Item or card account not found. Please try again."
|
|
329
|
-
else
|
|
330
|
-
flash[:alert] = "Payment error: #{response.error_message}"
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
transaction.update(status: 'failed', error_message: response.error_message)
|
|
334
|
-
redirect_to payment_path(transaction)
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
def handle_payment_exception(transaction, error)
|
|
338
|
-
Rails.logger.error "Payment exception: #{error.class} - #{error.message}"
|
|
339
|
-
|
|
340
|
-
transaction.update(status: 'error', error_message: error.message)
|
|
341
|
-
redirect_to payment_path(transaction), alert: "An error occurred: #{error.message}"
|
|
342
|
-
end
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
### Payment with CVV Verification
|
|
346
|
-
|
|
347
|
-
For additional security, collect and pass CVV:
|
|
348
|
-
|
|
349
|
-
```ruby
|
|
350
|
-
def create
|
|
351
|
-
transaction = current_user.transactions.find(params[:transaction_id])
|
|
352
|
-
client = ZaiPayment::Client.new
|
|
353
|
-
|
|
354
|
-
response = client.items.make_payment(
|
|
355
|
-
transaction.zai_item_id,
|
|
356
|
-
account_id: params[:card_account_id], # Required
|
|
357
|
-
cvv: params[:cvv], # From secure form input
|
|
358
|
-
ip_address: request.remote_ip
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
if response.success?
|
|
362
|
-
transaction.update(status: 'processing')
|
|
363
|
-
redirect_to transaction_path(transaction),
|
|
364
|
-
notice: 'Payment processed with CVV verification.'
|
|
365
|
-
else
|
|
366
|
-
redirect_to payment_path(transaction),
|
|
367
|
-
alert: "CVV verification failed: #{response.error_message}"
|
|
368
|
-
end
|
|
369
|
-
end
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
### Complete Payment Service
|
|
373
|
-
|
|
374
|
-
A comprehensive service object for handling payments:
|
|
375
|
-
|
|
376
|
-
```ruby
|
|
377
|
-
# app/services/payment_processor.rb
|
|
378
|
-
class PaymentProcessor
|
|
379
|
-
attr_reader :transaction, :errors
|
|
380
|
-
|
|
381
|
-
def initialize(transaction)
|
|
382
|
-
@transaction = transaction
|
|
383
|
-
@client = ZaiPayment::Client.new
|
|
384
|
-
@errors = []
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
def process(card_account_id:, ip_address:, device_id: nil, cvv: nil)
|
|
388
|
-
validate_payment_readiness
|
|
389
|
-
return false if @errors.any?
|
|
390
|
-
|
|
391
|
-
make_payment(card_account_id, ip_address, device_id, cvv)
|
|
392
|
-
end
|
|
393
|
-
|
|
394
|
-
private
|
|
395
|
-
|
|
396
|
-
def validate_payment_readiness
|
|
397
|
-
@errors << "Transaction already processed" if transaction.paid?
|
|
398
|
-
@errors << "Item ID missing" unless transaction.zai_item_id.present?
|
|
399
|
-
@errors << "Buyer missing" unless transaction.buyer.zai_user_id.present?
|
|
400
|
-
end
|
|
401
|
-
|
|
402
|
-
def make_payment(card_account_id, ip_address, device_id, cvv)
|
|
403
|
-
payment_params = {
|
|
404
|
-
account_id: card_account_id, # Required
|
|
405
|
-
ip_address: ip_address
|
|
406
|
-
}
|
|
407
|
-
payment_params[:device_id] = device_id if device_id.present?
|
|
408
|
-
payment_params[:cvv] = cvv if cvv.present?
|
|
409
|
-
|
|
410
|
-
response = @client.items.make_payment(
|
|
411
|
-
transaction.zai_item_id,
|
|
412
|
-
**payment_params
|
|
413
|
-
)
|
|
414
|
-
|
|
415
|
-
if response.success?
|
|
416
|
-
update_transaction_success(response)
|
|
417
|
-
notify_success
|
|
418
|
-
true
|
|
419
|
-
else
|
|
420
|
-
update_transaction_failure(response)
|
|
421
|
-
@errors << response.error_message
|
|
422
|
-
false
|
|
423
|
-
end
|
|
424
|
-
rescue ZaiPayment::Errors::ApiError => e
|
|
425
|
-
handle_api_error(e)
|
|
426
|
-
false
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
def update_transaction_success(response)
|
|
430
|
-
transaction.update!(
|
|
431
|
-
status: 'processing',
|
|
432
|
-
payment_state: response.data['payment_state'],
|
|
433
|
-
zai_state: response.data['state'],
|
|
434
|
-
paid_at: Time.current
|
|
435
|
-
)
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
def update_transaction_failure(response)
|
|
439
|
-
transaction.update!(
|
|
440
|
-
status: 'failed',
|
|
441
|
-
error_message: response.error_message,
|
|
442
|
-
failed_at: Time.current
|
|
443
|
-
)
|
|
444
|
-
end
|
|
445
|
-
|
|
446
|
-
def handle_api_error(error)
|
|
447
|
-
transaction.update!(
|
|
448
|
-
status: 'error',
|
|
449
|
-
error_message: error.message
|
|
450
|
-
)
|
|
451
|
-
@errors << error.message
|
|
452
|
-
|
|
453
|
-
# Log for monitoring
|
|
454
|
-
Rails.logger.error "Payment API Error: #{error.class} - #{error.message}"
|
|
455
|
-
|
|
456
|
-
# Send to error tracking (e.g., Sentry)
|
|
457
|
-
Sentry.capture_exception(error) if defined?(Sentry)
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
def notify_success
|
|
461
|
-
# Send success notification
|
|
462
|
-
PaymentMailer.payment_initiated(transaction).deliver_later
|
|
463
|
-
end
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
# Usage in controller:
|
|
467
|
-
def create
|
|
468
|
-
transaction = current_user.transactions.find(params[:transaction_id])
|
|
469
|
-
processor = PaymentProcessor.new(transaction)
|
|
470
|
-
|
|
471
|
-
if processor.process(
|
|
472
|
-
card_account_id: params[:card_account_id],
|
|
473
|
-
ip_address: request.remote_ip,
|
|
474
|
-
device_id: session[:device_id],
|
|
475
|
-
cvv: params[:cvv]
|
|
476
|
-
)
|
|
477
|
-
redirect_to transaction_path(transaction), notice: 'Payment processing!'
|
|
478
|
-
else
|
|
479
|
-
flash[:alert] = processor.errors.join(', ')
|
|
480
|
-
redirect_to payment_path(transaction)
|
|
481
|
-
end
|
|
482
|
-
end
|
|
483
|
-
```
|
|
484
|
-
|
|
485
|
-
### Payment Controller (Complete)
|
|
486
|
-
|
|
487
|
-
```ruby
|
|
488
|
-
# app/controllers/payments_controller.rb
|
|
489
|
-
class PaymentsController < ApplicationController
|
|
490
|
-
before_action :authenticate_user!
|
|
491
|
-
before_action :set_transaction, only: [:show, :create]
|
|
492
|
-
|
|
493
|
-
def show
|
|
494
|
-
@card_accounts = fetch_card_accounts
|
|
495
|
-
|
|
496
|
-
unless @card_accounts.any?
|
|
497
|
-
redirect_to new_card_account_path,
|
|
498
|
-
alert: 'Please add a payment method first.'
|
|
499
|
-
end
|
|
500
|
-
end
|
|
501
|
-
|
|
502
|
-
def create
|
|
503
|
-
processor = PaymentProcessor.new(@transaction)
|
|
504
|
-
|
|
505
|
-
if processor.process(
|
|
506
|
-
card_account_id: params[:card_account_id],
|
|
507
|
-
ip_address: request.remote_ip,
|
|
508
|
-
device_id: session[:device_id],
|
|
509
|
-
cvv: params[:cvv]
|
|
510
|
-
)
|
|
511
|
-
flash[:success] = 'Payment initiated! Check your email for confirmation.'
|
|
512
|
-
redirect_to transaction_path(@transaction)
|
|
513
|
-
else
|
|
514
|
-
flash.now[:alert] = processor.errors.join(', ')
|
|
515
|
-
@card_accounts = fetch_card_accounts
|
|
516
|
-
render :show
|
|
517
|
-
end
|
|
518
|
-
end
|
|
519
|
-
|
|
520
|
-
private
|
|
521
|
-
|
|
522
|
-
def set_transaction
|
|
523
|
-
@transaction = current_user.transactions.find(params[:id] || params[:transaction_id])
|
|
524
|
-
rescue ActiveRecord::RecordNotFound
|
|
525
|
-
redirect_to transactions_path, alert: 'Transaction not found.'
|
|
526
|
-
end
|
|
527
|
-
|
|
528
|
-
def fetch_card_accounts
|
|
529
|
-
client = ZaiPayment::Client.new
|
|
530
|
-
response = client.card_accounts.list(user_id: current_user.zai_user_id)
|
|
531
|
-
|
|
532
|
-
if response.success?
|
|
533
|
-
response.data['card_accounts'] || []
|
|
534
|
-
else
|
|
535
|
-
[]
|
|
536
|
-
end
|
|
537
|
-
rescue ZaiPayment::Errors::ApiError => e
|
|
538
|
-
Rails.logger.error "Failed to fetch card accounts: #{e.message}"
|
|
539
|
-
[]
|
|
540
|
-
end
|
|
541
|
-
end
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
### Payment View
|
|
545
|
-
|
|
546
|
-
```erb
|
|
547
|
-
<!-- app/views/payments/show.html.erb -->
|
|
548
|
-
<div class="payment-page">
|
|
549
|
-
<h2>Complete Payment</h2>
|
|
550
|
-
|
|
551
|
-
<div class="transaction-details">
|
|
552
|
-
<p><strong>Amount:</strong> <%= number_to_currency(@transaction.amount) %></p>
|
|
553
|
-
<p><strong>Description:</strong> <%= @transaction.description %></p>
|
|
554
|
-
<p><strong>Seller:</strong> <%= @transaction.seller.name %></p>
|
|
555
|
-
</div>
|
|
556
|
-
|
|
557
|
-
<%= form_with url: payments_path, method: :post do |f| %>
|
|
558
|
-
<%= f.hidden_field :transaction_id, value: @transaction.id %>
|
|
559
|
-
|
|
560
|
-
<div class="form-group">
|
|
561
|
-
<%= f.label :card_account_id, "Select Card" %>
|
|
562
|
-
<%= f.select :card_account_id,
|
|
563
|
-
options_for_select(@card_accounts.map { |ca|
|
|
564
|
-
["#{ca['card']['type']} ending in #{ca['card']['number']}", ca['id']]
|
|
565
|
-
}),
|
|
566
|
-
{ include_blank: 'Choose a card' },
|
|
567
|
-
class: 'form-control' %>
|
|
568
|
-
</div>
|
|
569
|
-
|
|
570
|
-
<div class="actions">
|
|
571
|
-
<%= f.submit "Pay #{number_to_currency(@transaction.amount)}",
|
|
572
|
-
class: 'btn btn-primary' %>
|
|
573
|
-
<%= link_to 'Add New Card', new_card_account_path,
|
|
574
|
-
class: 'btn btn-secondary' %>
|
|
575
|
-
</div>
|
|
576
|
-
<% end %>
|
|
577
|
-
</div>
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
## Webhook Handling
|
|
581
|
-
|
|
582
|
-
### Setting Up Webhooks
|
|
583
|
-
|
|
584
|
-
```ruby
|
|
585
|
-
# One-time setup (can be done in Rails console or a rake task)
|
|
586
|
-
# lib/tasks/zai_setup.rake
|
|
587
|
-
|
|
588
|
-
namespace :zai do
|
|
589
|
-
desc "Setup Zai webhooks"
|
|
590
|
-
task setup_webhooks: :environment do
|
|
591
|
-
client = ZaiPayment::Client.new
|
|
592
|
-
|
|
593
|
-
# Item status changes
|
|
594
|
-
client.webhooks.create(
|
|
595
|
-
object_type: 'items',
|
|
596
|
-
url: "#{ENV['APP_URL']}/webhooks/zai/items"
|
|
597
|
-
)
|
|
598
|
-
|
|
599
|
-
# Transaction events
|
|
600
|
-
client.webhooks.create(
|
|
601
|
-
object_type: 'transactions',
|
|
602
|
-
url: "#{ENV['APP_URL']}/webhooks/zai/transactions"
|
|
603
|
-
)
|
|
604
|
-
|
|
605
|
-
# Batch transaction events (settlement)
|
|
606
|
-
client.webhooks.create(
|
|
607
|
-
object_type: 'batch_transactions',
|
|
608
|
-
url: "#{ENV['APP_URL']}/webhooks/zai/batch_transactions"
|
|
609
|
-
)
|
|
610
|
-
|
|
611
|
-
puts "Webhooks configured successfully!"
|
|
612
|
-
end
|
|
613
|
-
end
|
|
614
|
-
```
|
|
615
|
-
|
|
616
|
-
### Webhook Controller
|
|
617
|
-
|
|
618
|
-
```ruby
|
|
619
|
-
# app/controllers/webhooks/zai_controller.rb
|
|
620
|
-
module Webhooks
|
|
621
|
-
class ZaiController < ApplicationController
|
|
622
|
-
skip_before_action :verify_authenticity_token
|
|
623
|
-
before_action :verify_webhook_signature
|
|
624
|
-
|
|
625
|
-
# Handle item status changes
|
|
626
|
-
def items
|
|
627
|
-
payload = JSON.parse(request.body.read)
|
|
628
|
-
|
|
629
|
-
# Item completed
|
|
630
|
-
if payload['status'] == 'completed'
|
|
631
|
-
item_id = payload['id']
|
|
632
|
-
transaction = Transaction.find_by(zai_item_id: item_id)
|
|
633
|
-
|
|
634
|
-
if transaction
|
|
635
|
-
transaction.update(
|
|
636
|
-
status: 'completed',
|
|
637
|
-
released_amount: payload['released_amount']
|
|
638
|
-
)
|
|
639
|
-
|
|
640
|
-
# Notify user
|
|
641
|
-
PaymentMailer.payment_completed(transaction).deliver_later
|
|
642
|
-
end
|
|
643
|
-
end
|
|
644
|
-
|
|
645
|
-
head :ok
|
|
646
|
-
end
|
|
647
|
-
|
|
648
|
-
# Handle transaction events (card authorized)
|
|
649
|
-
def transactions
|
|
650
|
-
payload = JSON.parse(request.body.read)
|
|
651
|
-
|
|
652
|
-
if payload['type'] == 'payment' && payload['type_method'] == 'credit_card'
|
|
653
|
-
transaction = Transaction.find_by(zai_transaction_id: payload['id'])
|
|
654
|
-
|
|
655
|
-
if transaction
|
|
656
|
-
if payload['status'] == 'successful'
|
|
657
|
-
transaction.update(status: 'authorized')
|
|
658
|
-
# Notify user
|
|
659
|
-
PaymentMailer.payment_authorized(transaction).deliver_later
|
|
660
|
-
elsif payload['status'] == 'failed'
|
|
661
|
-
transaction.update(status: 'failed', failure_reason: payload['failure_reason'])
|
|
662
|
-
PaymentMailer.payment_failed(transaction).deliver_later
|
|
663
|
-
end
|
|
664
|
-
end
|
|
665
|
-
end
|
|
666
|
-
|
|
667
|
-
head :ok
|
|
668
|
-
end
|
|
669
|
-
|
|
670
|
-
# Handle batch transactions (funds settled)
|
|
671
|
-
def batch_transactions
|
|
672
|
-
payload = JSON.parse(request.body.read)
|
|
673
|
-
|
|
674
|
-
if payload['type'] == 'payment_funding' &&
|
|
675
|
-
payload['type_method'] == 'credit_card' &&
|
|
676
|
-
payload['status'] == 'successful'
|
|
677
|
-
|
|
678
|
-
# Find related item and mark as settled
|
|
679
|
-
batch_id = payload['id']
|
|
680
|
-
transaction = Transaction.find_by(zai_batch_transaction_id: batch_id)
|
|
681
|
-
|
|
682
|
-
if transaction
|
|
683
|
-
transaction.update(status: 'settled')
|
|
684
|
-
# Funds are now in seller's wallet
|
|
685
|
-
PaymentMailer.payment_settled(transaction).deliver_later
|
|
686
|
-
end
|
|
687
|
-
end
|
|
688
|
-
|
|
689
|
-
head :ok
|
|
690
|
-
end
|
|
691
|
-
|
|
692
|
-
private
|
|
693
|
-
|
|
694
|
-
def verify_webhook_signature
|
|
695
|
-
# Verify the webhook signature using the gem's helper
|
|
696
|
-
signature = request.headers['X-Webhook-Signature']
|
|
697
|
-
|
|
698
|
-
unless ZaiPayment::Webhook.verify_signature?(
|
|
699
|
-
payload: request.body.read,
|
|
700
|
-
signature: signature,
|
|
701
|
-
secret: ENV['ZAI_WEBHOOK_SECRET']
|
|
702
|
-
)
|
|
703
|
-
head :unauthorized
|
|
704
|
-
end
|
|
705
|
-
end
|
|
706
|
-
end
|
|
707
|
-
end
|
|
708
|
-
```
|
|
709
|
-
|
|
710
|
-
### Routes for Webhooks
|
|
711
|
-
|
|
712
|
-
```ruby
|
|
713
|
-
# config/routes.rb
|
|
714
|
-
Rails.application.routes.draw do
|
|
715
|
-
namespace :webhooks do
|
|
716
|
-
post 'zai/items', to: 'zai#items'
|
|
717
|
-
post 'zai/transactions', to: 'zai#transactions'
|
|
718
|
-
post 'zai/batch_transactions', to: 'zai#batch_transactions'
|
|
719
|
-
end
|
|
720
|
-
end
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
## Complete Flow Example
|
|
724
|
-
|
|
725
|
-
### Service Object for Complete Payment Flow
|
|
726
|
-
|
|
727
|
-
```ruby
|
|
728
|
-
# app/services/card_payment_flow.rb
|
|
729
|
-
class CardPaymentFlow
|
|
730
|
-
attr_reader :client, :errors
|
|
731
|
-
|
|
732
|
-
def initialize
|
|
733
|
-
@client = ZaiPayment::Client.new
|
|
734
|
-
@errors = []
|
|
735
|
-
end
|
|
736
|
-
|
|
737
|
-
# Complete flow from creating users to payment
|
|
738
|
-
def execute(buyer_params:, seller_params:, payment_params:)
|
|
739
|
-
ActiveRecord::Base.transaction do
|
|
740
|
-
# Step 1: Create or get buyer
|
|
741
|
-
buyer_id = ensure_zai_user(buyer_params)
|
|
742
|
-
return false unless buyer_id
|
|
743
|
-
|
|
744
|
-
# Step 2: Create or get seller
|
|
745
|
-
seller_id = ensure_zai_user(seller_params)
|
|
746
|
-
return false unless seller_id
|
|
747
|
-
|
|
748
|
-
# Step 3: Create item
|
|
749
|
-
item_id = create_item(
|
|
750
|
-
buyer_id: buyer_id,
|
|
751
|
-
seller_id: seller_id,
|
|
752
|
-
amount: payment_params[:amount],
|
|
753
|
-
description: payment_params[:description]
|
|
754
|
-
)
|
|
755
|
-
return false unless item_id
|
|
756
|
-
|
|
757
|
-
# Step 4: Make payment
|
|
758
|
-
make_payment(
|
|
759
|
-
item_id: item_id,
|
|
760
|
-
card_account_id: payment_params[:card_account_id]
|
|
761
|
-
)
|
|
762
|
-
end
|
|
763
|
-
rescue ZaiPayment::Errors::ApiError => e
|
|
764
|
-
@errors << e.message
|
|
765
|
-
false
|
|
766
|
-
end
|
|
767
|
-
|
|
768
|
-
private
|
|
769
|
-
|
|
770
|
-
def ensure_zai_user(user_params)
|
|
771
|
-
# Check if user already exists in Zai
|
|
772
|
-
return user_params[:zai_user_id] if user_params[:zai_user_id].present?
|
|
773
|
-
|
|
774
|
-
# Create new user
|
|
775
|
-
response = @client.users.create(
|
|
776
|
-
id: user_params[:id],
|
|
777
|
-
first_name: user_params[:first_name],
|
|
778
|
-
last_name: user_params[:last_name],
|
|
779
|
-
email: user_params[:email],
|
|
780
|
-
mobile: user_params[:mobile],
|
|
781
|
-
country: 'AUS'
|
|
782
|
-
)
|
|
783
|
-
|
|
784
|
-
response.data['id']
|
|
785
|
-
end
|
|
786
|
-
|
|
787
|
-
def create_item(buyer_id:, seller_id:, amount:, description:)
|
|
788
|
-
response = @client.items.create(
|
|
789
|
-
id: "item_#{SecureRandom.hex(8)}",
|
|
790
|
-
name: description,
|
|
791
|
-
amount: (amount.to_f * 100).to_i,
|
|
792
|
-
payment_type: 1,
|
|
793
|
-
buyer_id: buyer_id,
|
|
794
|
-
seller_id: seller_id,
|
|
795
|
-
description: description
|
|
796
|
-
)
|
|
797
|
-
|
|
798
|
-
response.data['id']
|
|
799
|
-
end
|
|
800
|
-
|
|
801
|
-
def make_payment(item_id:, card_account_id:)
|
|
802
|
-
response = @client.items.make_payment(
|
|
803
|
-
item_id,
|
|
804
|
-
account_id: card_account_id # Required
|
|
805
|
-
)
|
|
806
|
-
|
|
807
|
-
response.success?
|
|
808
|
-
end
|
|
809
|
-
end
|
|
810
|
-
```
|
|
811
|
-
|
|
812
|
-
### Usage Example
|
|
813
|
-
|
|
814
|
-
```ruby
|
|
815
|
-
# In a controller or background job
|
|
816
|
-
class ProcessPaymentJob < ApplicationJob
|
|
817
|
-
queue_as :default
|
|
818
|
-
|
|
819
|
-
def perform(transaction_id)
|
|
820
|
-
transaction = Transaction.find(transaction_id)
|
|
821
|
-
flow = CardPaymentFlow.new
|
|
822
|
-
|
|
823
|
-
success = flow.execute(
|
|
824
|
-
buyer_params: {
|
|
825
|
-
id: "buyer_#{transaction.buyer.id}",
|
|
826
|
-
zai_user_id: transaction.buyer.zai_user_id,
|
|
827
|
-
first_name: transaction.buyer.first_name,
|
|
828
|
-
last_name: transaction.buyer.last_name,
|
|
829
|
-
email: transaction.buyer.email,
|
|
830
|
-
mobile: transaction.buyer.phone
|
|
831
|
-
},
|
|
832
|
-
seller_params: {
|
|
833
|
-
id: "seller_#{transaction.seller.id}",
|
|
834
|
-
zai_user_id: transaction.seller.zai_user_id,
|
|
835
|
-
first_name: transaction.seller.first_name,
|
|
836
|
-
last_name: transaction.seller.last_name,
|
|
837
|
-
email: transaction.seller.email,
|
|
838
|
-
mobile: transaction.seller.phone
|
|
839
|
-
},
|
|
840
|
-
payment_params: {
|
|
841
|
-
amount: transaction.amount,
|
|
842
|
-
description: transaction.description,
|
|
843
|
-
card_account_id: transaction.buyer.zai_card_account_id
|
|
844
|
-
}
|
|
845
|
-
)
|
|
846
|
-
|
|
847
|
-
if success
|
|
848
|
-
transaction.update(status: 'processing')
|
|
849
|
-
PaymentMailer.payment_initiated(transaction).deliver_now
|
|
850
|
-
else
|
|
851
|
-
transaction.update(status: 'failed', error_message: flow.errors.join(', '))
|
|
852
|
-
PaymentMailer.payment_failed(transaction).deliver_now
|
|
853
|
-
end
|
|
854
|
-
end
|
|
855
|
-
end
|
|
856
|
-
```
|
|
857
|
-
|
|
858
|
-
## Authorize Payment
|
|
859
|
-
|
|
860
|
-
Authorize a payment without immediately capturing funds. This is useful for scenarios like hotel bookings or rental deposits where you want to verify the card and hold funds before completing the transaction.
|
|
861
|
-
|
|
862
|
-
### Basic Authorization
|
|
863
|
-
|
|
864
|
-
```ruby
|
|
865
|
-
# app/controllers/authorizations_controller.rb
|
|
866
|
-
class AuthorizationsController < ApplicationController
|
|
867
|
-
def create
|
|
868
|
-
transaction = current_user.transactions.find(params[:transaction_id])
|
|
869
|
-
client = ZaiPayment::Client.new
|
|
870
|
-
|
|
871
|
-
# Authorize payment (hold funds without capturing)
|
|
872
|
-
response = client.items.authorize_payment(
|
|
873
|
-
transaction.zai_item_id,
|
|
874
|
-
account_id: params[:card_account_id] # Required
|
|
875
|
-
)
|
|
876
|
-
|
|
877
|
-
if response.success?
|
|
878
|
-
transaction.update(
|
|
879
|
-
status: 'authorized',
|
|
880
|
-
payment_state: response.data['payment_state']
|
|
881
|
-
)
|
|
882
|
-
|
|
883
|
-
redirect_to transaction_path(transaction),
|
|
884
|
-
notice: 'Payment authorized successfully! Funds are on hold.'
|
|
885
|
-
else
|
|
886
|
-
redirect_to payment_path(transaction),
|
|
887
|
-
alert: "Authorization failed: #{response.error_message}"
|
|
888
|
-
end
|
|
889
|
-
rescue ZaiPayment::Errors::ValidationError => e
|
|
890
|
-
redirect_to payment_path(transaction), alert: "Validation error: #{e.message}"
|
|
891
|
-
rescue ZaiPayment::Errors::ApiError => e
|
|
892
|
-
redirect_to payment_path(transaction), alert: "Error: #{e.message}"
|
|
893
|
-
end
|
|
894
|
-
end
|
|
895
|
-
```
|
|
896
|
-
|
|
897
|
-
### Authorization with CVV
|
|
898
|
-
|
|
899
|
-
For additional security, include CVV verification:
|
|
900
|
-
|
|
901
|
-
```ruby
|
|
902
|
-
def create
|
|
903
|
-
transaction = current_user.transactions.find(params[:transaction_id])
|
|
904
|
-
client = ZaiPayment::Client.new
|
|
905
|
-
|
|
906
|
-
response = client.items.authorize_payment(
|
|
907
|
-
transaction.zai_item_id,
|
|
908
|
-
account_id: params[:card_account_id], # Required
|
|
909
|
-
cvv: params[:cvv], # From secure form input
|
|
910
|
-
merchant_phone: current_user.phone
|
|
911
|
-
)
|
|
912
|
-
|
|
913
|
-
if response.success?
|
|
914
|
-
transaction.update(
|
|
915
|
-
status: 'authorized',
|
|
916
|
-
payment_state: response.data['payment_state'],
|
|
917
|
-
zai_state: response.data['state']
|
|
918
|
-
)
|
|
919
|
-
|
|
920
|
-
# Log the authorization
|
|
921
|
-
Rails.logger.info "Payment authorized: Item #{transaction.zai_item_id}"
|
|
922
|
-
|
|
923
|
-
flash[:notice] = 'Payment authorized. Funds are on hold for 7 days.'
|
|
924
|
-
redirect_to transaction_path(transaction)
|
|
925
|
-
else
|
|
926
|
-
handle_authorization_error(transaction, response)
|
|
927
|
-
end
|
|
928
|
-
rescue ZaiPayment::Errors::ApiError => e
|
|
929
|
-
handle_authorization_exception(transaction, e)
|
|
930
|
-
end
|
|
931
|
-
|
|
932
|
-
private
|
|
933
|
-
|
|
934
|
-
def handle_authorization_error(transaction, response)
|
|
935
|
-
case response.status
|
|
936
|
-
when 422
|
|
937
|
-
flash[:alert] = "Authorization declined: #{response.error_message}"
|
|
938
|
-
when 404
|
|
939
|
-
flash[:alert] = "Item or card account not found. Please try again."
|
|
940
|
-
else
|
|
941
|
-
flash[:alert] = "Authorization error: #{response.error_message}"
|
|
942
|
-
end
|
|
943
|
-
|
|
944
|
-
transaction.update(status: 'authorization_failed', error_message: response.error_message)
|
|
945
|
-
redirect_to payment_path(transaction)
|
|
946
|
-
end
|
|
947
|
-
|
|
948
|
-
def handle_authorization_exception(transaction, error)
|
|
949
|
-
Rails.logger.error "Authorization exception: #{error.class} - #{error.message}"
|
|
950
|
-
|
|
951
|
-
transaction.update(status: 'error', error_message: error.message)
|
|
952
|
-
redirect_to payment_path(transaction), alert: "An error occurred: #{error.message}"
|
|
953
|
-
end
|
|
954
|
-
```
|
|
955
|
-
|
|
956
|
-
### Complete Authorization Service
|
|
957
|
-
|
|
958
|
-
A comprehensive service object for handling payment authorizations:
|
|
959
|
-
|
|
960
|
-
```ruby
|
|
961
|
-
# app/services/payment_authorizer.rb
|
|
962
|
-
class PaymentAuthorizer
|
|
963
|
-
attr_reader :transaction, :errors
|
|
964
|
-
|
|
965
|
-
def initialize(transaction)
|
|
966
|
-
@transaction = transaction
|
|
967
|
-
@client = ZaiPayment::Client.new
|
|
968
|
-
@errors = []
|
|
969
|
-
end
|
|
970
|
-
|
|
971
|
-
def authorize(card_account_id:, cvv: nil, merchant_phone: nil)
|
|
972
|
-
validate_authorization_readiness
|
|
973
|
-
return false if @errors.any?
|
|
974
|
-
|
|
975
|
-
perform_authorization(card_account_id, cvv, merchant_phone)
|
|
976
|
-
end
|
|
977
|
-
|
|
978
|
-
private
|
|
979
|
-
|
|
980
|
-
def validate_authorization_readiness
|
|
981
|
-
@errors << "Transaction already authorized" if transaction.authorized?
|
|
982
|
-
@errors << "Item ID missing" unless transaction.zai_item_id.present?
|
|
983
|
-
@errors << "Buyer missing" unless transaction.buyer.zai_user_id.present?
|
|
984
|
-
end
|
|
985
|
-
|
|
986
|
-
def perform_authorization(card_account_id, cvv, merchant_phone)
|
|
987
|
-
auth_params = {
|
|
988
|
-
account_id: card_account_id # Required
|
|
989
|
-
}
|
|
990
|
-
auth_params[:cvv] = cvv if cvv.present?
|
|
991
|
-
auth_params[:merchant_phone] = merchant_phone if merchant_phone.present?
|
|
992
|
-
|
|
993
|
-
response = @client.items.authorize_payment(
|
|
994
|
-
transaction.zai_item_id,
|
|
995
|
-
**auth_params
|
|
996
|
-
)
|
|
997
|
-
|
|
998
|
-
if response.success?
|
|
999
|
-
update_transaction_success(response)
|
|
1000
|
-
notify_success
|
|
1001
|
-
true
|
|
1002
|
-
else
|
|
1003
|
-
update_transaction_failure(response)
|
|
1004
|
-
@errors << response.error_message
|
|
1005
|
-
false
|
|
1006
|
-
end
|
|
1007
|
-
rescue ZaiPayment::Errors::ApiError => e
|
|
1008
|
-
handle_api_error(e)
|
|
1009
|
-
false
|
|
1010
|
-
end
|
|
1011
|
-
|
|
1012
|
-
def update_transaction_success(response)
|
|
1013
|
-
transaction.update!(
|
|
1014
|
-
status: 'authorized',
|
|
1015
|
-
payment_state: response.data['payment_state'],
|
|
1016
|
-
zai_state: response.data['state'],
|
|
1017
|
-
authorized_at: Time.current,
|
|
1018
|
-
authorization_expires_at: 7.days.from_now # Typical hold period
|
|
1019
|
-
)
|
|
1020
|
-
end
|
|
1021
|
-
|
|
1022
|
-
def update_transaction_failure(response)
|
|
1023
|
-
transaction.update!(
|
|
1024
|
-
status: 'authorization_failed',
|
|
1025
|
-
error_message: response.error_message,
|
|
1026
|
-
failed_at: Time.current
|
|
1027
|
-
)
|
|
1028
|
-
end
|
|
1029
|
-
|
|
1030
|
-
def handle_api_error(error)
|
|
1031
|
-
transaction.update!(
|
|
1032
|
-
status: 'error',
|
|
1033
|
-
error_message: error.message
|
|
1034
|
-
)
|
|
1035
|
-
@errors << error.message
|
|
1036
|
-
|
|
1037
|
-
Rails.logger.error "Authorization API Error: #{error.class} - #{error.message}"
|
|
1038
|
-
Sentry.capture_exception(error) if defined?(Sentry)
|
|
1039
|
-
end
|
|
1040
|
-
|
|
1041
|
-
def notify_success
|
|
1042
|
-
PaymentMailer.payment_authorized(transaction).deliver_later
|
|
1043
|
-
end
|
|
1044
|
-
end
|
|
1045
|
-
|
|
1046
|
-
# Usage in controller:
|
|
1047
|
-
def create
|
|
1048
|
-
transaction = current_user.transactions.find(params[:transaction_id])
|
|
1049
|
-
authorizer = PaymentAuthorizer.new(transaction)
|
|
1050
|
-
|
|
1051
|
-
if authorizer.authorize(
|
|
1052
|
-
card_account_id: params[:card_account_id],
|
|
1053
|
-
cvv: params[:cvv],
|
|
1054
|
-
merchant_phone: current_user.phone
|
|
1055
|
-
)
|
|
1056
|
-
redirect_to transaction_path(transaction),
|
|
1057
|
-
notice: 'Payment authorized! Funds are on hold.'
|
|
1058
|
-
else
|
|
1059
|
-
flash[:alert] = authorizer.errors.join(', ')
|
|
1060
|
-
redirect_to payment_path(transaction)
|
|
1061
|
-
end
|
|
1062
|
-
end
|
|
1063
|
-
```
|
|
1064
|
-
|
|
1065
|
-
### Authorization Flow States
|
|
1066
|
-
|
|
1067
|
-
After calling `authorize_payment`, track these states:
|
|
1068
|
-
|
|
1069
|
-
| State | Description | Next Action |
|
|
1070
|
-
|-------|-------------|-------------|
|
|
1071
|
-
| `authorized` | Payment authorized, funds on hold | Capture or cancel |
|
|
1072
|
-
| `payment_held` | Authorized but held for review | Wait for review |
|
|
1073
|
-
| `authorization_failed` | Authorization failed | Retry or cancel |
|
|
1074
|
-
|
|
1075
|
-
### Capturing an Authorized Payment
|
|
1076
|
-
|
|
1077
|
-
After authorization, you can capture the payment:
|
|
1078
|
-
|
|
1079
|
-
```ruby
|
|
1080
|
-
# When ready to complete the transaction (e.g., after service delivery)
|
|
1081
|
-
def capture_payment
|
|
1082
|
-
transaction = Transaction.find(params[:id])
|
|
1083
|
-
|
|
1084
|
-
# Check if authorization is still valid
|
|
1085
|
-
if transaction.authorized? && transaction.authorization_expires_at > Time.current
|
|
1086
|
-
# Use make_payment to capture or complete the item
|
|
1087
|
-
client = ZaiPayment::Client.new
|
|
1088
|
-
response = client.items.make_payment(
|
|
1089
|
-
transaction.zai_item_id,
|
|
1090
|
-
account_id: transaction.card_account_id
|
|
1091
|
-
)
|
|
1092
|
-
|
|
1093
|
-
if response.success?
|
|
1094
|
-
transaction.update(
|
|
1095
|
-
status: 'captured',
|
|
1096
|
-
captured_at: Time.current
|
|
1097
|
-
)
|
|
1098
|
-
flash[:notice] = 'Payment captured successfully!'
|
|
1099
|
-
else
|
|
1100
|
-
flash[:alert] = "Capture failed: #{response.error_message}"
|
|
1101
|
-
end
|
|
1102
|
-
else
|
|
1103
|
-
flash[:alert] = 'Authorization expired or invalid'
|
|
1104
|
-
end
|
|
1105
|
-
|
|
1106
|
-
redirect_to transaction_path(transaction)
|
|
1107
|
-
end
|
|
1108
|
-
```
|
|
1109
|
-
|
|
1110
|
-
### Canceling an Authorization
|
|
1111
|
-
|
|
1112
|
-
To release held funds:
|
|
1113
|
-
|
|
1114
|
-
```ruby
|
|
1115
|
-
def cancel_authorization
|
|
1116
|
-
transaction = Transaction.find(params[:id])
|
|
1117
|
-
|
|
1118
|
-
if transaction.authorized?
|
|
1119
|
-
client = ZaiPayment::Client.new
|
|
1120
|
-
response = client.items.cancel(transaction.zai_item_id)
|
|
1121
|
-
|
|
1122
|
-
if response.success?
|
|
1123
|
-
transaction.update(
|
|
1124
|
-
status: 'authorization_cancelled',
|
|
1125
|
-
cancelled_at: Time.current
|
|
1126
|
-
)
|
|
1127
|
-
flash[:notice] = 'Authorization cancelled. Funds released.'
|
|
1128
|
-
else
|
|
1129
|
-
flash[:alert] = "Cancellation failed: #{response.error_message}"
|
|
1130
|
-
end
|
|
1131
|
-
end
|
|
1132
|
-
|
|
1133
|
-
redirect_to transaction_path(transaction)
|
|
1134
|
-
end
|
|
1135
|
-
```
|
|
1136
|
-
|
|
1137
|
-
## Pre-live Testing
|
|
1138
|
-
|
|
1139
|
-
For testing in the pre-live environment:
|
|
1140
|
-
|
|
1141
|
-
```ruby
|
|
1142
|
-
# After making a payment, simulate settlement
|
|
1143
|
-
class SimulateSettlement
|
|
1144
|
-
def self.call(transaction_id)
|
|
1145
|
-
transaction = Transaction.find(transaction_id)
|
|
1146
|
-
client = ZaiPayment::Client.new
|
|
1147
|
-
|
|
1148
|
-
# Step 1: Export batch transactions
|
|
1149
|
-
response = client.batch_transactions.export
|
|
1150
|
-
batch_transactions = response.data['batch_transactions']
|
|
1151
|
-
|
|
1152
|
-
# Find the relevant batch transaction
|
|
1153
|
-
batch_tx = batch_transactions.find do |bt|
|
|
1154
|
-
bt['related_items']&.include?(transaction.zai_item_id)
|
|
1155
|
-
end
|
|
1156
|
-
|
|
1157
|
-
return unless batch_tx
|
|
1158
|
-
|
|
1159
|
-
# Step 2: Move to batched state (12700)
|
|
1160
|
-
client.batches.update_transaction_state(
|
|
1161
|
-
id: batch_tx['batch_id'],
|
|
1162
|
-
exported_ids: [batch_tx['id']],
|
|
1163
|
-
state: 12700
|
|
1164
|
-
)
|
|
1165
|
-
|
|
1166
|
-
# Step 3: Move to successful state (12000)
|
|
1167
|
-
client.batches.update_transaction_state(
|
|
1168
|
-
id: batch_tx['batch_id'],
|
|
1169
|
-
exported_ids: [batch_tx['id']],
|
|
1170
|
-
state: 12000
|
|
1171
|
-
)
|
|
1172
|
-
|
|
1173
|
-
puts "Settlement simulated for transaction #{transaction_id}"
|
|
1174
|
-
end
|
|
1175
|
-
end
|
|
1176
|
-
```
|
|
1177
|
-
|
|
1178
|
-
## Error Handling
|
|
1179
|
-
|
|
1180
|
-
```ruby
|
|
1181
|
-
# app/services/payment_error_handler.rb
|
|
1182
|
-
class PaymentErrorHandler
|
|
1183
|
-
RETRY_ERRORS = [
|
|
1184
|
-
ZaiPayment::Errors::TimeoutError,
|
|
1185
|
-
ZaiPayment::Errors::ConnectionError,
|
|
1186
|
-
ZaiPayment::Errors::ServerError
|
|
1187
|
-
]
|
|
1188
|
-
|
|
1189
|
-
PERMANENT_ERRORS = [
|
|
1190
|
-
ZaiPayment::Errors::ValidationError,
|
|
1191
|
-
ZaiPayment::Errors::UnauthorizedError,
|
|
1192
|
-
ZaiPayment::Errors::ForbiddenError,
|
|
1193
|
-
ZaiPayment::Errors::NotFoundError
|
|
1194
|
-
]
|
|
1195
|
-
|
|
1196
|
-
def self.handle(error, transaction)
|
|
1197
|
-
case error
|
|
1198
|
-
when *RETRY_ERRORS
|
|
1199
|
-
# Retry the job
|
|
1200
|
-
ProcessPaymentJob.set(wait: 5.minutes).perform_later(transaction.id)
|
|
1201
|
-
when *PERMANENT_ERRORS
|
|
1202
|
-
# Mark as failed permanently
|
|
1203
|
-
transaction.update(
|
|
1204
|
-
status: 'failed',
|
|
1205
|
-
error_message: error.message
|
|
1206
|
-
)
|
|
1207
|
-
PaymentMailer.payment_error(transaction, error).deliver_now
|
|
1208
|
-
when ZaiPayment::Errors::RateLimitError
|
|
1209
|
-
# Retry after longer delay
|
|
1210
|
-
ProcessPaymentJob.set(wait: 1.hour).perform_later(transaction.id)
|
|
1211
|
-
else
|
|
1212
|
-
# Log unknown error
|
|
1213
|
-
Rails.logger.error("Unknown payment error: #{error.class} - #{error.message}")
|
|
1214
|
-
Sentry.capture_exception(error) if defined?(Sentry)
|
|
1215
|
-
end
|
|
1216
|
-
end
|
|
1217
|
-
end
|
|
1218
|
-
```
|
|
1219
|
-
|
|
1220
|
-
## Summary
|
|
1221
|
-
|
|
1222
|
-
This example demonstrates:
|
|
1223
|
-
|
|
1224
|
-
1. **User Creation**: Creating buyer and seller users in Zai
|
|
1225
|
-
2. **Card Capture**: Using the hosted form to securely capture card details
|
|
1226
|
-
3. **Item Creation**: Creating a payment item between buyer and seller
|
|
1227
|
-
4. **Payment Processing**: Making a payment using a card account
|
|
1228
|
-
5. **Webhook Handling**: Responding to payment events (authorized, completed, settled)
|
|
1229
|
-
6. **Error Handling**: Properly handling various error scenarios
|
|
1230
|
-
7. **Testing**: Simulating settlements in pre-live environment
|
|
1231
|
-
|
|
1232
|
-
### Payment Flow States
|
|
1233
|
-
|
|
1234
|
-
1. **Item Created** → Item exists but no payment made
|
|
1235
|
-
2. **Payment Initiated** → `make_payment` called
|
|
1236
|
-
3. **Authorized** → Card transaction successful (webhook: `transactions`)
|
|
1237
|
-
4. **Completed** → Item status changed to completed (webhook: `items`)
|
|
1238
|
-
5. **Settled** → Funds moved to seller's wallet (webhook: `batch_transactions`)
|
|
1239
|
-
|
|
1240
|
-
### Important Notes
|
|
1241
|
-
|
|
1242
|
-
- Always store Zai IDs (`zai_user_id`, `zai_item_id`, `zai_card_account_id`) in your database
|
|
1243
|
-
- Use webhooks for asynchronous updates rather than polling
|
|
1244
|
-
- Implement proper error handling and retries
|
|
1245
|
-
- Use background jobs for payment processing
|
|
1246
|
-
- Test thoroughly in pre-live environment before production
|
|
1247
|
-
- Verify webhook signatures for security
|
|
1248
|
-
- Handle PCI compliance requirements (the hosted form helps with this)
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
## Reference
|
|
1252
|
-
- [Zai Cards Payin Workflow](https://developer.hellozai.com/docs/cards-payin-workflow)
|