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.
@@ -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)