zai_payment 2.1.0 → 2.2.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,716 @@
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
+ ### Payment Controller
247
+
248
+ ```ruby
249
+ # app/controllers/payments_controller.rb
250
+ class PaymentsController < ApplicationController
251
+ def show
252
+ @transaction = current_user.transactions.find(params[:id])
253
+ @card_accounts = fetch_card_accounts
254
+ end
255
+
256
+ def create
257
+ transaction = current_user.transactions.find(params[:transaction_id])
258
+ client = ZaiPayment::Client.new
259
+
260
+ # Make payment using card account
261
+ response = client.items.make_payment(
262
+ id: transaction.zai_item_id,
263
+ account_id: params[:card_account_id] # The card account ID
264
+ )
265
+
266
+ transaction.update(
267
+ status: 'processing',
268
+ zai_transaction_id: response.data.dig('transactions', 0, 'id')
269
+ )
270
+
271
+ redirect_to transaction_path(transaction),
272
+ notice: 'Payment initiated successfully. You will be notified once completed.'
273
+ rescue ZaiPayment::Errors::ApiError => e
274
+ redirect_to payment_path(transaction), alert: "Payment failed: #{e.message}"
275
+ end
276
+
277
+ private
278
+
279
+ def fetch_card_accounts
280
+ client = ZaiPayment::Client.new
281
+ response = client.card_accounts.list(user_id: current_user.zai_user_id)
282
+ response.data['card_accounts'] || []
283
+ end
284
+ end
285
+ ```
286
+
287
+ ### Payment View
288
+
289
+ ```erb
290
+ <!-- app/views/payments/show.html.erb -->
291
+ <div class="payment-page">
292
+ <h2>Complete Payment</h2>
293
+
294
+ <div class="transaction-details">
295
+ <p><strong>Amount:</strong> <%= number_to_currency(@transaction.amount) %></p>
296
+ <p><strong>Description:</strong> <%= @transaction.description %></p>
297
+ <p><strong>Seller:</strong> <%= @transaction.seller.name %></p>
298
+ </div>
299
+
300
+ <%= form_with url: payments_path, method: :post do |f| %>
301
+ <%= f.hidden_field :transaction_id, value: @transaction.id %>
302
+
303
+ <div class="form-group">
304
+ <%= f.label :card_account_id, "Select Card" %>
305
+ <%= f.select :card_account_id,
306
+ options_for_select(@card_accounts.map { |ca|
307
+ ["#{ca['card']['type']} ending in #{ca['card']['number']}", ca['id']]
308
+ }),
309
+ { include_blank: 'Choose a card' },
310
+ class: 'form-control' %>
311
+ </div>
312
+
313
+ <div class="actions">
314
+ <%= f.submit "Pay #{number_to_currency(@transaction.amount)}",
315
+ class: 'btn btn-primary' %>
316
+ <%= link_to 'Add New Card', new_card_account_path,
317
+ class: 'btn btn-secondary' %>
318
+ </div>
319
+ <% end %>
320
+ </div>
321
+ ```
322
+
323
+ ## Webhook Handling
324
+
325
+ ### Setting Up Webhooks
326
+
327
+ ```ruby
328
+ # One-time setup (can be done in Rails console or a rake task)
329
+ # lib/tasks/zai_setup.rake
330
+
331
+ namespace :zai do
332
+ desc "Setup Zai webhooks"
333
+ task setup_webhooks: :environment do
334
+ client = ZaiPayment::Client.new
335
+
336
+ # Item status changes
337
+ client.webhooks.create(
338
+ object_type: 'items',
339
+ url: "#{ENV['APP_URL']}/webhooks/zai/items"
340
+ )
341
+
342
+ # Transaction events
343
+ client.webhooks.create(
344
+ object_type: 'transactions',
345
+ url: "#{ENV['APP_URL']}/webhooks/zai/transactions"
346
+ )
347
+
348
+ # Batch transaction events (settlement)
349
+ client.webhooks.create(
350
+ object_type: 'batch_transactions',
351
+ url: "#{ENV['APP_URL']}/webhooks/zai/batch_transactions"
352
+ )
353
+
354
+ puts "Webhooks configured successfully!"
355
+ end
356
+ end
357
+ ```
358
+
359
+ ### Webhook Controller
360
+
361
+ ```ruby
362
+ # app/controllers/webhooks/zai_controller.rb
363
+ module Webhooks
364
+ class ZaiController < ApplicationController
365
+ skip_before_action :verify_authenticity_token
366
+ before_action :verify_webhook_signature
367
+
368
+ # Handle item status changes
369
+ def items
370
+ payload = JSON.parse(request.body.read)
371
+
372
+ # Item completed
373
+ if payload['status'] == 'completed'
374
+ item_id = payload['id']
375
+ transaction = Transaction.find_by(zai_item_id: item_id)
376
+
377
+ if transaction
378
+ transaction.update(
379
+ status: 'completed',
380
+ released_amount: payload['released_amount']
381
+ )
382
+
383
+ # Notify user
384
+ PaymentMailer.payment_completed(transaction).deliver_later
385
+ end
386
+ end
387
+
388
+ head :ok
389
+ end
390
+
391
+ # Handle transaction events (card authorized)
392
+ def transactions
393
+ payload = JSON.parse(request.body.read)
394
+
395
+ if payload['type'] == 'payment' && payload['type_method'] == 'credit_card'
396
+ transaction = Transaction.find_by(zai_transaction_id: payload['id'])
397
+
398
+ if transaction
399
+ if payload['status'] == 'successful'
400
+ transaction.update(status: 'authorized')
401
+ # Notify user
402
+ PaymentMailer.payment_authorized(transaction).deliver_later
403
+ elsif payload['status'] == 'failed'
404
+ transaction.update(status: 'failed', failure_reason: payload['failure_reason'])
405
+ PaymentMailer.payment_failed(transaction).deliver_later
406
+ end
407
+ end
408
+ end
409
+
410
+ head :ok
411
+ end
412
+
413
+ # Handle batch transactions (funds settled)
414
+ def batch_transactions
415
+ payload = JSON.parse(request.body.read)
416
+
417
+ if payload['type'] == 'payment_funding' &&
418
+ payload['type_method'] == 'credit_card' &&
419
+ payload['status'] == 'successful'
420
+
421
+ # Find related item and mark as settled
422
+ batch_id = payload['id']
423
+ transaction = Transaction.find_by(zai_batch_transaction_id: batch_id)
424
+
425
+ if transaction
426
+ transaction.update(status: 'settled')
427
+ # Funds are now in seller's wallet
428
+ PaymentMailer.payment_settled(transaction).deliver_later
429
+ end
430
+ end
431
+
432
+ head :ok
433
+ end
434
+
435
+ private
436
+
437
+ def verify_webhook_signature
438
+ # Verify the webhook signature using the gem's helper
439
+ signature = request.headers['X-Webhook-Signature']
440
+
441
+ unless ZaiPayment::Webhook.verify_signature?(
442
+ payload: request.body.read,
443
+ signature: signature,
444
+ secret: ENV['ZAI_WEBHOOK_SECRET']
445
+ )
446
+ head :unauthorized
447
+ end
448
+ end
449
+ end
450
+ end
451
+ ```
452
+
453
+ ### Routes for Webhooks
454
+
455
+ ```ruby
456
+ # config/routes.rb
457
+ Rails.application.routes.draw do
458
+ namespace :webhooks do
459
+ post 'zai/items', to: 'zai#items'
460
+ post 'zai/transactions', to: 'zai#transactions'
461
+ post 'zai/batch_transactions', to: 'zai#batch_transactions'
462
+ end
463
+ end
464
+ ```
465
+
466
+ ## Complete Flow Example
467
+
468
+ ### Service Object for Complete Payment Flow
469
+
470
+ ```ruby
471
+ # app/services/card_payment_flow.rb
472
+ class CardPaymentFlow
473
+ attr_reader :client, :errors
474
+
475
+ def initialize
476
+ @client = ZaiPayment::Client.new
477
+ @errors = []
478
+ end
479
+
480
+ # Complete flow from creating users to payment
481
+ def execute(buyer_params:, seller_params:, payment_params:)
482
+ ActiveRecord::Base.transaction do
483
+ # Step 1: Create or get buyer
484
+ buyer_id = ensure_zai_user(buyer_params)
485
+ return false unless buyer_id
486
+
487
+ # Step 2: Create or get seller
488
+ seller_id = ensure_zai_user(seller_params)
489
+ return false unless seller_id
490
+
491
+ # Step 3: Create item
492
+ item_id = create_item(
493
+ buyer_id: buyer_id,
494
+ seller_id: seller_id,
495
+ amount: payment_params[:amount],
496
+ description: payment_params[:description]
497
+ )
498
+ return false unless item_id
499
+
500
+ # Step 4: Make payment
501
+ make_payment(
502
+ item_id: item_id,
503
+ card_account_id: payment_params[:card_account_id]
504
+ )
505
+ end
506
+ rescue ZaiPayment::Errors::ApiError => e
507
+ @errors << e.message
508
+ false
509
+ end
510
+
511
+ private
512
+
513
+ def ensure_zai_user(user_params)
514
+ # Check if user already exists in Zai
515
+ return user_params[:zai_user_id] if user_params[:zai_user_id].present?
516
+
517
+ # Create new user
518
+ response = @client.users.create(
519
+ id: user_params[:id],
520
+ first_name: user_params[:first_name],
521
+ last_name: user_params[:last_name],
522
+ email: user_params[:email],
523
+ mobile: user_params[:mobile],
524
+ country: 'AUS'
525
+ )
526
+
527
+ response.data['id']
528
+ end
529
+
530
+ def create_item(buyer_id:, seller_id:, amount:, description:)
531
+ response = @client.items.create(
532
+ id: "item_#{SecureRandom.hex(8)}",
533
+ name: description,
534
+ amount: (amount.to_f * 100).to_i,
535
+ payment_type: 1,
536
+ buyer_id: buyer_id,
537
+ seller_id: seller_id,
538
+ description: description
539
+ )
540
+
541
+ response.data['id']
542
+ end
543
+
544
+ def make_payment(item_id:, card_account_id:)
545
+ response = @client.items.make_payment(
546
+ id: item_id,
547
+ account_id: card_account_id
548
+ )
549
+
550
+ response.success?
551
+ end
552
+ end
553
+ ```
554
+
555
+ ### Usage Example
556
+
557
+ ```ruby
558
+ # In a controller or background job
559
+ class ProcessPaymentJob < ApplicationJob
560
+ queue_as :default
561
+
562
+ def perform(transaction_id)
563
+ transaction = Transaction.find(transaction_id)
564
+ flow = CardPaymentFlow.new
565
+
566
+ success = flow.execute(
567
+ buyer_params: {
568
+ id: "buyer_#{transaction.buyer.id}",
569
+ zai_user_id: transaction.buyer.zai_user_id,
570
+ first_name: transaction.buyer.first_name,
571
+ last_name: transaction.buyer.last_name,
572
+ email: transaction.buyer.email,
573
+ mobile: transaction.buyer.phone
574
+ },
575
+ seller_params: {
576
+ id: "seller_#{transaction.seller.id}",
577
+ zai_user_id: transaction.seller.zai_user_id,
578
+ first_name: transaction.seller.first_name,
579
+ last_name: transaction.seller.last_name,
580
+ email: transaction.seller.email,
581
+ mobile: transaction.seller.phone
582
+ },
583
+ payment_params: {
584
+ amount: transaction.amount,
585
+ description: transaction.description,
586
+ card_account_id: transaction.buyer.zai_card_account_id
587
+ }
588
+ )
589
+
590
+ if success
591
+ transaction.update(status: 'processing')
592
+ PaymentMailer.payment_initiated(transaction).deliver_now
593
+ else
594
+ transaction.update(status: 'failed', error_message: flow.errors.join(', '))
595
+ PaymentMailer.payment_failed(transaction).deliver_now
596
+ end
597
+ end
598
+ end
599
+ ```
600
+
601
+ ## Pre-live Testing
602
+
603
+ For testing in the pre-live environment:
604
+
605
+ ```ruby
606
+ # After making a payment, simulate settlement
607
+ class SimulateSettlement
608
+ def self.call(transaction_id)
609
+ transaction = Transaction.find(transaction_id)
610
+ client = ZaiPayment::Client.new
611
+
612
+ # Step 1: Export batch transactions
613
+ response = client.batch_transactions.export
614
+ batch_transactions = response.data['batch_transactions']
615
+
616
+ # Find the relevant batch transaction
617
+ batch_tx = batch_transactions.find do |bt|
618
+ bt['related_items']&.include?(transaction.zai_item_id)
619
+ end
620
+
621
+ return unless batch_tx
622
+
623
+ # Step 2: Move to batched state (12700)
624
+ client.batches.update_transaction_state(
625
+ id: batch_tx['batch_id'],
626
+ exported_ids: [batch_tx['id']],
627
+ state: 12700
628
+ )
629
+
630
+ # Step 3: Move to successful state (12000)
631
+ client.batches.update_transaction_state(
632
+ id: batch_tx['batch_id'],
633
+ exported_ids: [batch_tx['id']],
634
+ state: 12000
635
+ )
636
+
637
+ puts "Settlement simulated for transaction #{transaction_id}"
638
+ end
639
+ end
640
+ ```
641
+
642
+ ## Error Handling
643
+
644
+ ```ruby
645
+ # app/services/payment_error_handler.rb
646
+ class PaymentErrorHandler
647
+ RETRY_ERRORS = [
648
+ ZaiPayment::Errors::TimeoutError,
649
+ ZaiPayment::Errors::ConnectionError,
650
+ ZaiPayment::Errors::ServerError
651
+ ]
652
+
653
+ PERMANENT_ERRORS = [
654
+ ZaiPayment::Errors::ValidationError,
655
+ ZaiPayment::Errors::UnauthorizedError,
656
+ ZaiPayment::Errors::ForbiddenError,
657
+ ZaiPayment::Errors::NotFoundError
658
+ ]
659
+
660
+ def self.handle(error, transaction)
661
+ case error
662
+ when *RETRY_ERRORS
663
+ # Retry the job
664
+ ProcessPaymentJob.set(wait: 5.minutes).perform_later(transaction.id)
665
+ when *PERMANENT_ERRORS
666
+ # Mark as failed permanently
667
+ transaction.update(
668
+ status: 'failed',
669
+ error_message: error.message
670
+ )
671
+ PaymentMailer.payment_error(transaction, error).deliver_now
672
+ when ZaiPayment::Errors::RateLimitError
673
+ # Retry after longer delay
674
+ ProcessPaymentJob.set(wait: 1.hour).perform_later(transaction.id)
675
+ else
676
+ # Log unknown error
677
+ Rails.logger.error("Unknown payment error: #{error.class} - #{error.message}")
678
+ Sentry.capture_exception(error) if defined?(Sentry)
679
+ end
680
+ end
681
+ end
682
+ ```
683
+
684
+ ## Summary
685
+
686
+ This example demonstrates:
687
+
688
+ 1. **User Creation**: Creating buyer and seller users in Zai
689
+ 2. **Card Capture**: Using the hosted form to securely capture card details
690
+ 3. **Item Creation**: Creating a payment item between buyer and seller
691
+ 4. **Payment Processing**: Making a payment using a card account
692
+ 5. **Webhook Handling**: Responding to payment events (authorized, completed, settled)
693
+ 6. **Error Handling**: Properly handling various error scenarios
694
+ 7. **Testing**: Simulating settlements in pre-live environment
695
+
696
+ ### Payment Flow States
697
+
698
+ 1. **Item Created** → Item exists but no payment made
699
+ 2. **Payment Initiated** → `make_payment` called
700
+ 3. **Authorized** → Card transaction successful (webhook: `transactions`)
701
+ 4. **Completed** → Item status changed to completed (webhook: `items`)
702
+ 5. **Settled** → Funds moved to seller's wallet (webhook: `batch_transactions`)
703
+
704
+ ### Important Notes
705
+
706
+ - Always store Zai IDs (`zai_user_id`, `zai_item_id`, `zai_card_account_id`) in your database
707
+ - Use webhooks for asynchronous updates rather than polling
708
+ - Implement proper error handling and retries
709
+ - Use background jobs for payment processing
710
+ - Test thoroughly in pre-live environment before production
711
+ - Verify webhook signatures for security
712
+ - Handle PCI compliance requirements (the hosted form helps with this)
713
+
714
+
715
+ ## Reference
716
+ - [Zai Cards Payin Workflow](https://developer.hellozai.com/docs/cards-payin-workflow)