zai_payment 2.3.1 → 2.4.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.
- checksums.yaml +4 -4
- data/badges/coverage.json +1 -1
- data/changelog.md +54 -0
- data/docs/items.md +575 -0
- data/examples/items.md +2115 -0
- data/examples/rails_card_payment.md +550 -14
- data/lib/zai_payment/client.rb +13 -2
- data/lib/zai_payment/config.rb +4 -3
- data/lib/zai_payment/resources/item.rb +257 -0
- data/lib/zai_payment/version.rb +1 -1
- data/readme.md +5 -0
- metadata +1 -2
- data/token_auth_implementation_summary.md +0 -249
|
@@ -243,43 +243,300 @@ end
|
|
|
243
243
|
|
|
244
244
|
## Making a Payment
|
|
245
245
|
|
|
246
|
-
|
|
246
|
+
Once you have an item created and the buyer has a card account, you can process the payment.
|
|
247
|
+
|
|
248
|
+
### Basic Payment
|
|
247
249
|
|
|
248
250
|
```ruby
|
|
249
251
|
# app/controllers/payments_controller.rb
|
|
250
252
|
class PaymentsController < ApplicationController
|
|
251
|
-
def show
|
|
252
|
-
@transaction = current_user.transactions.find(params[:id])
|
|
253
|
-
@card_accounts = fetch_card_accounts
|
|
254
|
-
end
|
|
255
|
-
|
|
256
253
|
def create
|
|
257
254
|
transaction = current_user.transactions.find(params[:transaction_id])
|
|
258
255
|
client = ZaiPayment::Client.new
|
|
259
256
|
|
|
260
257
|
# Make payment using card account
|
|
261
258
|
response = client.items.make_payment(
|
|
262
|
-
|
|
263
|
-
account_id: params[:card_account_id]
|
|
259
|
+
transaction.zai_item_id,
|
|
260
|
+
account_id: params[:card_account_id] # Required
|
|
264
261
|
)
|
|
265
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?
|
|
266
302
|
transaction.update(
|
|
267
303
|
status: 'processing',
|
|
268
|
-
|
|
304
|
+
payment_state: response.data['payment_state'],
|
|
305
|
+
zai_state: response.data['state']
|
|
269
306
|
)
|
|
270
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')
|
|
271
363
|
redirect_to transaction_path(transaction),
|
|
272
|
-
notice: 'Payment
|
|
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
|
|
273
424
|
rescue ZaiPayment::Errors::ApiError => e
|
|
274
|
-
|
|
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
|
|
275
518
|
end
|
|
276
519
|
|
|
277
520
|
private
|
|
278
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
|
+
|
|
279
528
|
def fetch_card_accounts
|
|
280
529
|
client = ZaiPayment::Client.new
|
|
281
530
|
response = client.card_accounts.list(user_id: current_user.zai_user_id)
|
|
282
|
-
|
|
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
|
+
[]
|
|
283
540
|
end
|
|
284
541
|
end
|
|
285
542
|
```
|
|
@@ -543,8 +800,8 @@ class CardPaymentFlow
|
|
|
543
800
|
|
|
544
801
|
def make_payment(item_id:, card_account_id:)
|
|
545
802
|
response = @client.items.make_payment(
|
|
546
|
-
|
|
547
|
-
account_id: card_account_id
|
|
803
|
+
item_id,
|
|
804
|
+
account_id: card_account_id # Required
|
|
548
805
|
)
|
|
549
806
|
|
|
550
807
|
response.success?
|
|
@@ -598,6 +855,285 @@ class ProcessPaymentJob < ApplicationJob
|
|
|
598
855
|
end
|
|
599
856
|
```
|
|
600
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
|
+
|
|
601
1137
|
## Pre-live Testing
|
|
602
1138
|
|
|
603
1139
|
For testing in the pre-live environment:
|
data/lib/zai_payment/client.rb
CHANGED
|
@@ -61,6 +61,8 @@ module ZaiPayment
|
|
|
61
61
|
Response.new(response)
|
|
62
62
|
rescue Faraday::Error => e
|
|
63
63
|
handle_faraday_error(e)
|
|
64
|
+
rescue Net::ReadTimeout, Net::OpenTimeout => e
|
|
65
|
+
handle_net_timeout_error(e)
|
|
64
66
|
end
|
|
65
67
|
|
|
66
68
|
def connection
|
|
@@ -93,8 +95,13 @@ module ZaiPayment
|
|
|
93
95
|
end
|
|
94
96
|
|
|
95
97
|
def apply_timeouts(faraday)
|
|
96
|
-
faraday
|
|
97
|
-
faraday
|
|
98
|
+
set_timeout_option(faraday, :timeout, config.timeout)
|
|
99
|
+
set_timeout_option(faraday, :open_timeout, config.open_timeout)
|
|
100
|
+
set_timeout_option(faraday, :read_timeout, config.read_timeout)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def set_timeout_option(faraday, option, value)
|
|
104
|
+
faraday.options.public_send("#{option}=", value) if value
|
|
98
105
|
end
|
|
99
106
|
|
|
100
107
|
def base_url
|
|
@@ -120,5 +127,9 @@ module ZaiPayment
|
|
|
120
127
|
raise Errors::ApiError, "Request failed: #{error.message}"
|
|
121
128
|
end
|
|
122
129
|
end
|
|
130
|
+
|
|
131
|
+
def handle_net_timeout_error(error)
|
|
132
|
+
raise Errors::TimeoutError, "Request timed out: #{error.class.name} with #{error.message}"
|
|
133
|
+
end
|
|
123
134
|
end
|
|
124
135
|
end
|
data/lib/zai_payment/config.rb
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
module ZaiPayment
|
|
4
4
|
class Config
|
|
5
5
|
attr_accessor :environment, :client_id, :client_secret, :scope,
|
|
6
|
-
:timeout, :open_timeout
|
|
6
|
+
:timeout, :open_timeout, :read_timeout
|
|
7
7
|
|
|
8
8
|
def initialize
|
|
9
9
|
@environment = :prelive # or :production
|
|
10
10
|
@client_id = nil
|
|
11
11
|
@client_secret = nil
|
|
12
12
|
@scope = nil
|
|
13
|
-
@timeout = 10
|
|
14
|
-
@open_timeout = 10
|
|
13
|
+
@timeout = 30 # General timeout - increased from 10 to 30 seconds
|
|
14
|
+
@open_timeout = 10 # Connection open timeout
|
|
15
|
+
@read_timeout = 30 # Read timeout - new separate configuration
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def validate!
|