zai_payment 2.3.2 → 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.
@@ -243,43 +243,300 @@ end
243
243
 
244
244
  ## Making a Payment
245
245
 
246
- ### Payment Controller
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
- id: transaction.zai_item_id,
263
- account_id: params[:card_account_id] # The 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
- zai_transaction_id: response.data.dig('transactions', 0, 'id')
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 initiated successfully. You will be notified once completed.'
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
- redirect_to payment_path(transaction), alert: "Payment failed: #{e.message}"
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
- response.data['card_accounts'] || []
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
- id: item_id,
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: