zai_payment 2.3.2 → 2.5.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.
data/examples/items.md CHANGED
@@ -17,6 +17,10 @@ This document provides examples of how to use the Items resource in the Zai Paym
17
17
  - [List Item Transactions](#list-item-transactions)
18
18
  - [List Item Batch Transactions](#list-item-batch-transactions)
19
19
  - [Show Item Status](#show-item-status)
20
+ - [Make Payment](#make-payment)
21
+ - [Make Async Payment](#make-async-payment)
22
+ - [Cancel Item](#cancel-item)
23
+ - [Refund Item](#refund-item)
20
24
 
21
25
  ## Setup
22
26
 
@@ -445,6 +449,409 @@ else
445
449
  end
446
450
  ```
447
451
 
452
+ ## Make Payment
453
+
454
+ Process a payment for an item using a card account. This method charges the buyer's card and initiates the payment flow.
455
+
456
+ ### Basic Payment
457
+
458
+ ```ruby
459
+ # Make a payment with just the required parameters
460
+ response = items.make_payment(
461
+ "item-123",
462
+ account_id: "card_account-456" # Required
463
+ )
464
+
465
+ if response.success?
466
+ item = response.data
467
+ puts "Payment initiated for item: #{item['id']}"
468
+ puts "State: #{item['state']}"
469
+ puts "Payment State: #{item['payment_state']}"
470
+ else
471
+ puts "Payment failed: #{response.error_message}"
472
+ end
473
+ ```
474
+
475
+ ### Payment with Device Information
476
+
477
+ For enhanced fraud protection, include device and IP address information:
478
+
479
+ ```ruby
480
+ response = items.make_payment(
481
+ "item-123",
482
+ account_id: "card_account-456", # Required
483
+ device_id: "device_789",
484
+ ip_address: request.remote_ip # In a Rails controller
485
+ )
486
+
487
+ if response.success?
488
+ puts "Payment processed with device tracking"
489
+ end
490
+ ```
491
+
492
+ ### Payment with CVV
493
+
494
+ Some card payments may require CVV verification:
495
+
496
+ ```ruby
497
+ response = items.make_payment(
498
+ "item-123",
499
+ account_id: "card_account-456", # Required
500
+ cvv: "123" # CVV from secure form
501
+ )
502
+
503
+ if response.success?
504
+ puts "Payment processed with CVV verification"
505
+ end
506
+ ```
507
+
508
+ ### Payment with All Optional Parameters
509
+
510
+ Maximum fraud protection with all available parameters:
511
+
512
+ ```ruby
513
+ response = items.make_payment(
514
+ "item-123",
515
+ account_id: "card_account-456", # Required
516
+ device_id: "device_789",
517
+ ip_address: "192.168.1.1",
518
+ cvv: "123",
519
+ merchant_phone: "+61412345678"
520
+ )
521
+
522
+ if response.success?
523
+ item = response.data
524
+ puts "Payment initiated successfully"
525
+ puts "Item State: #{item['state']}"
526
+ puts "Payment State: #{item['payment_state']}"
527
+ puts "Amount: #{item['amount']}"
528
+ else
529
+ puts "Payment failed: #{response.error_message}"
530
+ end
531
+ ```
532
+
533
+ ### Error Handling for Payments
534
+
535
+ ```ruby
536
+ begin
537
+ response = items.make_payment(
538
+ "item-123",
539
+ account_id: "card_account-456"
540
+ )
541
+
542
+ if response.success?
543
+ puts "Payment successful"
544
+ else
545
+ # Handle API errors
546
+ case response.status
547
+ when 422
548
+ puts "Validation error: #{response.error_message}"
549
+ # Common: Insufficient funds, card declined, etc.
550
+ when 404
551
+ puts "Item or card account not found"
552
+ when 401
553
+ puts "Authentication failed"
554
+ else
555
+ puts "Payment error: #{response.error_message}"
556
+ end
557
+ end
558
+ rescue ZaiPayment::Errors::ValidationError => e
559
+ puts "Validation error: #{e.message}"
560
+ # Example: "account_id is required and cannot be blank"
561
+ rescue ZaiPayment::Errors::NotFoundError => e
562
+ puts "Resource not found: #{e.message}"
563
+ rescue ZaiPayment::Errors::ApiError => e
564
+ puts "API error: #{e.message}"
565
+ end
566
+ ```
567
+
568
+ ### Real-World Payment Flow Example
569
+
570
+ Complete example showing item creation through payment:
571
+
572
+ ```ruby
573
+ require 'zai_payment'
574
+
575
+ # Configure
576
+ ZaiPayment.configure do |config|
577
+ config.client_id = ENV['ZAI_CLIENT_ID']
578
+ config.client_secret = ENV['ZAI_CLIENT_SECRET']
579
+ config.scope = ENV['ZAI_SCOPE']
580
+ config.environment = :prelive
581
+ end
582
+
583
+ items = ZaiPayment.items
584
+
585
+ # Step 1: Create an item
586
+ create_response = items.create(
587
+ name: "Product Purchase",
588
+ amount: 10000, # $100.00
589
+ payment_type: 2,
590
+ buyer_id: "buyer-123",
591
+ seller_id: "seller-456",
592
+ description: "Purchase of premium widget"
593
+ )
594
+
595
+ if create_response.success?
596
+ item_id = create_response.data['id']
597
+ puts "✓ Item created: #{item_id}"
598
+
599
+ # Step 2: Make the payment
600
+ payment_response = items.make_payment(
601
+ item_id,
602
+ account_id: "card_account-789", # Buyer's card account (Required)
603
+ ip_address: "192.168.1.1",
604
+ device_id: "device_abc123"
605
+ )
606
+
607
+ if payment_response.success?
608
+ puts "✓ Payment initiated"
609
+ puts " State: #{payment_response.data['state']}"
610
+ puts " Payment State: #{payment_response.data['payment_state']}"
611
+
612
+ # Step 3: Check payment status
613
+ sleep 2 # Wait for processing
614
+
615
+ status_response = items.show_status(item_id)
616
+ if status_response.success?
617
+ status = status_response.data
618
+ puts "✓ Current status:"
619
+ puts " State: #{status['state']}"
620
+ puts " Payment State: #{status['payment_state']}"
621
+ end
622
+ else
623
+ puts "✗ Payment failed: #{payment_response.error_message}"
624
+ end
625
+ else
626
+ puts "✗ Item creation failed: #{create_response.error_message}"
627
+ end
628
+ ```
629
+
630
+ ### Payment States
631
+
632
+ After calling `make_payment`, the item will go through several states:
633
+
634
+ | State | Description |
635
+ |-------|-------------|
636
+ | `payment_pending` | Payment has been initiated |
637
+ | `payment_processing` | Card is being charged |
638
+ | `completed` | Payment successful, funds held in escrow |
639
+ | `payment_held` | Payment succeeded but held for review |
640
+ | `payment_failed` | Payment failed (card declined, insufficient funds, etc.) |
641
+
642
+ ### Webhook Integration
643
+
644
+ After making a payment, listen for webhook events to track the payment status:
645
+
646
+ ```ruby
647
+ # In your webhook handler
648
+ def handle_transaction_webhook(payload)
649
+ if payload['type'] == 'payment' && payload['status'] == 'successful'
650
+ item_id = payload['related_items'].first
651
+ puts "Payment successful for item: #{item_id}"
652
+
653
+ # Update your database
654
+ Order.find_by(zai_item_id: item_id).update(status: 'paid')
655
+ elsif payload['status'] == 'failed'
656
+ puts "Payment failed: #{payload['failure_reason']}"
657
+ end
658
+ end
659
+ ```
660
+
661
+ ## Authorize Payment
662
+
663
+ Authorize a payment without immediately capturing funds. This is useful for pre-authorization scenarios where you want to verify the card and hold funds before completing the transaction.
664
+
665
+ ### Basic Authorization
666
+
667
+ ```ruby
668
+ # Authorize a payment with required parameters
669
+ response = items.authorize_payment(
670
+ "item-123",
671
+ account_id: "card_account-456" # Required
672
+ )
673
+
674
+ if response.success?
675
+ item = response.data
676
+ puts "Payment authorized for item: #{item['id']}"
677
+ puts "State: #{item['state']}"
678
+ puts "Payment State: #{item['payment_state']}"
679
+ else
680
+ puts "Authorization failed: #{response.error_message}"
681
+ end
682
+ ```
683
+
684
+ ### Authorization with CVV
685
+
686
+ For additional security, include CVV verification:
687
+
688
+ ```ruby
689
+ response = items.authorize_payment(
690
+ "item-123",
691
+ account_id: "card_account-456", # Required
692
+ cvv: "123" # CVV from secure form
693
+ )
694
+
695
+ if response.success?
696
+ puts "Payment authorized with CVV verification"
697
+ end
698
+ ```
699
+
700
+ ### Authorization with All Optional Parameters
701
+
702
+ ```ruby
703
+ response = items.authorize_payment(
704
+ "item-123",
705
+ account_id: "card_account-456", # Required
706
+ cvv: "123",
707
+ merchant_phone: "+61412345678"
708
+ )
709
+
710
+ if response.success?
711
+ item = response.data
712
+ puts "Payment authorized successfully"
713
+ puts "Item State: #{item['state']}"
714
+ puts "Payment State: #{item['payment_state']}"
715
+ puts "Amount: #{item['amount']}"
716
+ else
717
+ puts "Authorization failed: #{response.error_message}"
718
+ end
719
+ ```
720
+
721
+ ### Error Handling for Authorization
722
+
723
+ ```ruby
724
+ begin
725
+ response = items.authorize_payment(
726
+ "item-123",
727
+ account_id: "card_account-456"
728
+ )
729
+
730
+ if response.success?
731
+ puts "Authorization successful"
732
+ else
733
+ # Handle API errors
734
+ case response.status
735
+ when 422
736
+ puts "Validation error: #{response.error_message}"
737
+ # Common: Invalid card, card declined, etc.
738
+ when 404
739
+ puts "Item or card account not found"
740
+ when 401
741
+ puts "Authentication failed"
742
+ else
743
+ puts "Authorization error: #{response.error_message}"
744
+ end
745
+ end
746
+ rescue ZaiPayment::Errors::ValidationError => e
747
+ puts "Validation error: #{e.message}"
748
+ # Example: "account_id is required and cannot be blank"
749
+ rescue ZaiPayment::Errors::NotFoundError => e
750
+ puts "Resource not found: #{e.message}"
751
+ rescue ZaiPayment::Errors::ApiError => e
752
+ puts "API error: #{e.message}"
753
+ end
754
+ ```
755
+
756
+ ### Real-World Authorization Flow Example
757
+
758
+ Complete example showing item creation through authorization:
759
+
760
+ ```ruby
761
+ require 'zai_payment'
762
+
763
+ # Configure
764
+ ZaiPayment.configure do |config|
765
+ config.client_id = ENV['ZAI_CLIENT_ID']
766
+ config.client_secret = ENV['ZAI_CLIENT_SECRET']
767
+ config.scope = ENV['ZAI_SCOPE']
768
+ config.environment = :prelive
769
+ end
770
+
771
+ items = ZaiPayment.items
772
+
773
+ # Step 1: Create an item
774
+ create_response = items.create(
775
+ name: "Hotel Reservation",
776
+ amount: 50000, # $500.00
777
+ payment_type: 2,
778
+ buyer_id: "buyer-123",
779
+ seller_id: "seller-456",
780
+ description: "Hotel booking - Hold authorization"
781
+ )
782
+
783
+ if create_response.success?
784
+ item_id = create_response.data['id']
785
+ puts "✓ Item created: #{item_id}"
786
+
787
+ # Step 2: Authorize the payment (hold funds without capturing)
788
+ auth_response = items.authorize_payment(
789
+ item_id,
790
+ account_id: "card_account-789",
791
+ cvv: "123",
792
+ merchant_phone: "+61412345678"
793
+ )
794
+
795
+ if auth_response.success?
796
+ puts "✓ Payment authorized (funds on hold)"
797
+ puts " State: #{auth_response.data['state']}"
798
+ puts " Payment State: #{auth_response.data['payment_state']}"
799
+
800
+ # Step 3: Check authorization status
801
+ status_response = items.show_status(item_id)
802
+ if status_response.success?
803
+ status = status_response.data
804
+ puts "✓ Current status:"
805
+ puts " State: #{status['state']}"
806
+ puts " Payment State: #{status['payment_state']}"
807
+ end
808
+
809
+ # Note: Funds are now held. You would later either:
810
+ # - Capture the payment (via make_payment or complete the item)
811
+ # - Cancel the authorization (via cancel)
812
+ else
813
+ puts "✗ Authorization failed: #{auth_response.error_message}"
814
+ end
815
+ else
816
+ puts "✗ Item creation failed: #{create_response.error_message}"
817
+ end
818
+ ```
819
+
820
+ ### Authorization States
821
+
822
+ After calling `authorize_payment`, the item will go through several states:
823
+
824
+ | State | Description |
825
+ |-------|-------------|
826
+ | `payment_authorized` | Payment has been authorized, funds are on hold |
827
+ | `payment_held` | Payment authorized but held for review |
828
+ | `authorization_failed` | Authorization failed (card declined, insufficient funds, etc.) |
829
+
830
+ **Important Notes:**
831
+ - Authorized funds are typically held for 7 days before being automatically released
832
+ - To complete the transaction, you need to capture the payment separately
833
+ - You can cancel an authorization to release the held funds immediately
834
+ - Not all payment processors support separate authorization and capture
835
+
836
+ ### Webhook Integration
837
+
838
+ After authorizing a payment, listen for webhook events:
839
+
840
+ ```ruby
841
+ # In your webhook handler
842
+ def handle_authorization_webhook(payload)
843
+ if payload['type'] == 'authorization' && payload['status'] == 'successful'
844
+ item_id = payload['related_items'].first
845
+ puts "Payment authorized for item: #{item_id}"
846
+
847
+ # Update your database
848
+ Order.find_by(zai_item_id: item_id).update(status: 'authorized')
849
+ elsif payload['status'] == 'failed'
850
+ puts "Authorization failed: #{payload['failure_reason']}"
851
+ end
852
+ end
853
+ ```
854
+
448
855
  ## Complete Workflow Example
449
856
 
450
857
  Here's a complete example of creating an item and performing various operations on it:
@@ -554,6 +961,1711 @@ else
554
961
  end
555
962
  ```
556
963
 
964
+ ## Capture Payment
965
+
966
+ Capture a previously authorized payment to complete the transaction. This is the second step in the authorize → capture workflow, allowing you to finalize a payment after authorization.
967
+
968
+ ### Basic Capture (Full Amount)
969
+
970
+ Capture the full authorized amount:
971
+
972
+ ```ruby
973
+ response = items.capture_payment("item-123")
974
+
975
+ if response.success?
976
+ item = response.data
977
+ puts "Payment captured successfully"
978
+ puts "Item ID: #{item['id']}"
979
+ puts "Amount: $#{item['amount'] / 100.0}"
980
+ puts "State: #{item['state']}"
981
+ puts "Payment State: #{item['payment_state']}"
982
+ else
983
+ puts "Capture failed: #{response.error_message}"
984
+ end
985
+ ```
986
+
987
+ ### Partial Capture
988
+
989
+ Capture only a portion of the authorized amount:
990
+
991
+ ```ruby
992
+ # Capture $50 of a $100 authorization
993
+ response = items.capture_payment("item-123", amount: 5000)
994
+
995
+ if response.success?
996
+ item = response.data
997
+ puts "Partial payment captured: $#{item['amount'] / 100.0}"
998
+ puts "State: #{item['state']}"
999
+ puts "Payment State: #{item['payment_state']}"
1000
+ else
1001
+ puts "Capture failed: #{response.error_message}"
1002
+ end
1003
+ ```
1004
+
1005
+ ### Capture with Error Handling
1006
+
1007
+ ```ruby
1008
+ begin
1009
+ response = items.capture_payment("item-123", amount: 10_000)
1010
+
1011
+ if response.success?
1012
+ item = response.data
1013
+ puts "✓ Payment captured: #{item['id']}"
1014
+ puts " Amount: $#{item['amount'] / 100.0}"
1015
+ puts " State: #{item['state']}"
1016
+ puts " Payment State: #{item['payment_state']}"
1017
+ else
1018
+ # Handle API errors
1019
+ case response.status
1020
+ when 422
1021
+ puts "Cannot capture: #{response.error_message}"
1022
+ # Common: Payment not authorized, authorization expired, or invalid amount
1023
+ when 404
1024
+ puts "Item not found"
1025
+ when 401
1026
+ puts "Authentication failed"
1027
+ else
1028
+ puts "Capture error: #{response.error_message}"
1029
+ end
1030
+ end
1031
+ rescue ZaiPayment::Errors::ValidationError => e
1032
+ puts "Validation error: #{e.message}"
1033
+ rescue ZaiPayment::Errors::NotFoundError => e
1034
+ puts "Item not found: #{e.message}"
1035
+ rescue ZaiPayment::Errors::ApiError => e
1036
+ puts "API error: #{e.message}"
1037
+ end
1038
+ ```
1039
+
1040
+ ### Capture with Status Check
1041
+
1042
+ Check authorization status before attempting to capture:
1043
+
1044
+ ```ruby
1045
+ # Check current status
1046
+ status_response = items.show_status("item-123")
1047
+
1048
+ if status_response.success?
1049
+ status = status_response.data
1050
+ payment_state = status['payment_state']
1051
+
1052
+ puts "Current payment state: #{payment_state}"
1053
+
1054
+ # Only capture if payment is authorized
1055
+ if payment_state == 'authorized' || payment_state == 'payment_authorized'
1056
+ capture_response = items.capture_payment("item-123")
1057
+
1058
+ if capture_response.success?
1059
+ puts "✓ Payment captured successfully"
1060
+ else
1061
+ puts "✗ Capture failed: #{capture_response.error_message}"
1062
+ end
1063
+ else
1064
+ puts "Payment cannot be captured - current state: #{payment_state}"
1065
+ end
1066
+ end
1067
+ ```
1068
+
1069
+ ### Complete Authorization and Capture Workflow
1070
+
1071
+ Full example demonstrating the two-step payment process:
1072
+
1073
+ ```ruby
1074
+ # Step 1: Authorize the payment
1075
+ puts "Step 1: Authorizing payment..."
1076
+ auth_response = items.authorize_payment(
1077
+ "item-123",
1078
+ account_id: "card_account-456",
1079
+ cvv: "123"
1080
+ )
1081
+
1082
+ if auth_response.success?
1083
+ puts "✓ Payment authorized"
1084
+ auth_data = auth_response.data
1085
+ puts " Item ID: #{auth_data['id']}"
1086
+ puts " Amount: $#{auth_data['amount'] / 100.0}"
1087
+ puts " State: #{auth_data['state']}"
1088
+
1089
+ # Step 2: Verify authorization status
1090
+ puts "\nStep 2: Verifying authorization..."
1091
+ status_response = items.show_status("item-123")
1092
+
1093
+ if status_response.success?
1094
+ status = status_response.data
1095
+ puts "✓ Status verified"
1096
+ puts " Payment State: #{status['payment_state']}"
1097
+
1098
+ # Step 3: Capture the payment (can be done immediately or later)
1099
+ puts "\nStep 3: Capturing payment..."
1100
+
1101
+ # Wait a moment (optional - simulate real-world delay)
1102
+ sleep 1
1103
+
1104
+ capture_response = items.capture_payment("item-123")
1105
+
1106
+ if capture_response.success?
1107
+ capture_data = capture_response.data
1108
+ puts "✓ Payment captured successfully"
1109
+ puts " Item ID: #{capture_data['id']}"
1110
+ puts " Final State: #{capture_data['state']}"
1111
+ puts " Final Payment State: #{capture_data['payment_state']}"
1112
+ else
1113
+ puts "✗ Capture failed: #{capture_response.error_message}"
1114
+ end
1115
+ else
1116
+ puts "✗ Status check failed: #{status_response.error_message}"
1117
+ end
1118
+ else
1119
+ puts "✗ Authorization failed: #{auth_response.error_message}"
1120
+ end
1121
+ ```
1122
+
1123
+ ### Capture with Retry Logic
1124
+
1125
+ Implement retry logic for transient errors:
1126
+
1127
+ ```ruby
1128
+ def capture_payment_with_retry(item_id, amount: nil, max_retries: 3)
1129
+ retries = 0
1130
+
1131
+ begin
1132
+ response = items.capture_payment(item_id, amount: amount)
1133
+
1134
+ if response.success?
1135
+ puts "✓ Payment captured successfully"
1136
+ return response
1137
+ elsif response.status == 422
1138
+ # Validation error - don't retry
1139
+ puts "✗ Capture failed: #{response.error_message}"
1140
+ return response
1141
+ else
1142
+ # Other errors - maybe retry
1143
+ raise "Capture error: #{response.error_message}"
1144
+ end
1145
+
1146
+ rescue => e
1147
+ retries += 1
1148
+
1149
+ if retries < max_retries
1150
+ puts "⚠ Capture attempt #{retries} failed: #{e.message}"
1151
+ puts " Retrying in #{retries * 2} seconds..."
1152
+ sleep(retries * 2)
1153
+ retry
1154
+ else
1155
+ puts "✗ Capture failed after #{max_retries} attempts"
1156
+ raise e
1157
+ end
1158
+ end
1159
+ end
1160
+
1161
+ # Usage
1162
+ begin
1163
+ response = capture_payment_with_retry("item-123", amount: 10_000)
1164
+ puts "Final state: #{response.data['payment_state']}" if response.success?
1165
+ rescue => e
1166
+ puts "Capture ultimately failed: #{e.message}"
1167
+ end
1168
+ ```
1169
+
1170
+ ### Real-World Capture Flow Example
1171
+
1172
+ A complete example showing how to handle the authorize and capture workflow in a real application:
1173
+
1174
+ ```ruby
1175
+ class PaymentProcessor
1176
+ def initialize
1177
+ @items = ZaiPayment.items
1178
+ end
1179
+
1180
+ def process_two_step_payment(order)
1181
+ # Step 1: Authorize
1182
+ puts "Processing order ##{order[:id]} - Amount: $#{order[:amount] / 100.0}"
1183
+
1184
+ auth_response = authorize_payment(order)
1185
+ return { success: false, error: "Authorization failed" } unless auth_response
1186
+
1187
+ # Step 2: Perform additional checks (inventory, fraud, etc.)
1188
+ return { success: false, error: "Fraud check failed" } unless verify_order(order)
1189
+
1190
+ # Step 3: Capture
1191
+ capture_response = capture_payment(order[:item_id], order[:capture_amount])
1192
+ return { success: false, error: "Capture failed" } unless capture_response
1193
+
1194
+ # Step 4: Update order and notify
1195
+ finalize_order(order, capture_response)
1196
+
1197
+ { success: true, data: capture_response.data }
1198
+ end
1199
+
1200
+ private
1201
+
1202
+ def authorize_payment(order)
1203
+ puts "→ Authorizing payment..."
1204
+
1205
+ response = @items.authorize_payment(
1206
+ order[:item_id],
1207
+ account_id: order[:account_id],
1208
+ cvv: order[:cvv]
1209
+ )
1210
+
1211
+ if response.success?
1212
+ puts "✓ Payment authorized: #{order[:item_id]}"
1213
+ response
1214
+ else
1215
+ puts "✗ Authorization failed: #{response.error_message}"
1216
+ nil
1217
+ end
1218
+ end
1219
+
1220
+ def verify_order(order)
1221
+ puts "→ Verifying order..."
1222
+
1223
+ # Check inventory
1224
+ return false unless check_inventory(order[:items])
1225
+
1226
+ # Check fraud score
1227
+ return false unless check_fraud_score(order[:buyer_id])
1228
+
1229
+ puts "✓ Order verified"
1230
+ true
1231
+ end
1232
+
1233
+ def capture_payment(item_id, amount = nil)
1234
+ puts "→ Capturing payment..."
1235
+
1236
+ response = @items.capture_payment(item_id, amount: amount)
1237
+
1238
+ if response.success?
1239
+ puts "✓ Payment captured: #{item_id}"
1240
+ response
1241
+ else
1242
+ puts "✗ Capture failed: #{response.error_message}"
1243
+ nil
1244
+ end
1245
+ end
1246
+
1247
+ def finalize_order(order, capture_response)
1248
+ puts "→ Finalizing order..."
1249
+
1250
+ # Update order status in database
1251
+ # update_order_status(order[:id], 'paid')
1252
+
1253
+ # Send confirmation email
1254
+ # send_confirmation_email(order[:buyer_email])
1255
+
1256
+ # Update inventory
1257
+ # reduce_inventory(order[:items])
1258
+
1259
+ puts "✓ Order finalized: ##{order[:id]}"
1260
+ end
1261
+
1262
+ def check_inventory(items)
1263
+ # Implement inventory check
1264
+ true
1265
+ end
1266
+
1267
+ def check_fraud_score(buyer_id)
1268
+ # Implement fraud check
1269
+ true
1270
+ end
1271
+ end
1272
+
1273
+ # Usage
1274
+ processor = PaymentProcessor.new
1275
+
1276
+ order = {
1277
+ id: 12345,
1278
+ item_id: "item-abc123",
1279
+ account_id: "card_account-456",
1280
+ cvv: "123",
1281
+ amount: 10_000,
1282
+ capture_amount: 10_000, # Can be less for partial capture
1283
+ buyer_id: "buyer-789",
1284
+ buyer_email: "customer@example.com",
1285
+ items: ["product-1", "product-2"]
1286
+ }
1287
+
1288
+ result = processor.process_two_step_payment(order)
1289
+
1290
+ if result[:success]
1291
+ puts "\n✓ Payment completed successfully!"
1292
+ puts " Item: #{result[:data]['id']}"
1293
+ puts " State: #{result[:data]['payment_state']}"
1294
+ else
1295
+ puts "\n✗ Payment failed: #{result[:error]}"
1296
+ end
1297
+ ```
1298
+
1299
+ ### Capture States and Conditions
1300
+
1301
+ Payments can be captured when in these states:
1302
+
1303
+ | State | Can Capture? | Description |
1304
+ |-------|-------------|-------------|
1305
+ | `authorized` | ✓ Yes | Payment authorized and ready to capture |
1306
+ | `payment_authorized` | ✓ Yes | Payment authorized and ready to capture |
1307
+ | `pending` | ✗ No | Payment not authorized yet |
1308
+ | `payment_pending` | ✗ No | Payment processing, not authorized |
1309
+ | `completed` | ✗ No | Already captured |
1310
+ | `payment_deposited` | ✗ No | Already captured and deposited |
1311
+ | `cancelled` | ✗ No | Authorization cancelled |
1312
+ | `refunded` | ✗ No | Payment refunded |
1313
+
1314
+ ### Best Practices for Capture
1315
+
1316
+ 1. **Timely Captures**: Capture authorized payments within 7 days (typical authorization expiration)
1317
+ 2. **Status Verification**: Always check payment state before attempting capture
1318
+ 3. **Partial Captures**: Use for order adjustments or split fulfillment
1319
+ 4. **Error Handling**: Implement robust error handling and retry logic
1320
+ 5. **Logging**: Log all authorization and capture attempts for audit trails
1321
+ 6. **Notifications**: Notify customers when payment is captured
1322
+ 7. **Timeouts**: Set appropriate timeout values for capture requests
1323
+
1324
+ ### Common Capture Errors
1325
+
1326
+ | Error | Cause | Solution |
1327
+ |-------|-------|----------|
1328
+ | "Payment not authorized" | Trying to capture non-authorized payment | Authorize first, then capture |
1329
+ | "Authorization expired" | Authorization older than 7 days | Create new item and authorize again |
1330
+ | "Amount exceeds authorized amount" | Capture amount > authorized amount | Reduce capture amount or re-authorize |
1331
+ | "Item not found" | Invalid item ID | Verify item ID is correct |
1332
+ | "Payment already captured" | Duplicate capture attempt | Check payment state before capture |
1333
+
1334
+ ### Capture Integration with Rails
1335
+
1336
+ #### In a Controller
1337
+
1338
+ ```ruby
1339
+ class PaymentsController < ApplicationController
1340
+ def capture
1341
+ @order = Order.find(params[:order_id])
1342
+
1343
+ # Verify order can be captured
1344
+ unless @order.payment_state == 'authorized'
1345
+ flash[:error] = "Payment not authorized"
1346
+ redirect_to @order and return
1347
+ end
1348
+
1349
+ # Capture the payment
1350
+ response = ZaiPayment.items.capture_payment(
1351
+ @order.zai_item_id,
1352
+ amount: params[:amount] # Optional for partial capture
1353
+ )
1354
+
1355
+ if response.success?
1356
+ @order.update(
1357
+ payment_state: response.data['payment_state'],
1358
+ captured_at: Time.current
1359
+ )
1360
+
1361
+ flash[:success] = "Payment captured successfully"
1362
+ redirect_to @order
1363
+ else
1364
+ flash[:error] = "Capture failed: #{response.error_message}"
1365
+ render :show
1366
+ end
1367
+ end
1368
+ end
1369
+ ```
1370
+
1371
+ #### In a Background Job
1372
+
1373
+ ```ruby
1374
+ class CapturePaymentJob < ApplicationJob
1375
+ queue_as :payments
1376
+
1377
+ def perform(order_id)
1378
+ order = Order.find(order_id)
1379
+
1380
+ # Check if order is ready to capture
1381
+ return unless order.ready_to_capture?
1382
+
1383
+ # Capture the payment
1384
+ response = ZaiPayment.items.capture_payment(order.zai_item_id)
1385
+
1386
+ if response.success?
1387
+ order.update(
1388
+ payment_state: 'captured',
1389
+ captured_at: Time.current
1390
+ )
1391
+
1392
+ # Send confirmation email
1393
+ OrderMailer.payment_captured(order).deliver_later
1394
+
1395
+ # Update inventory
1396
+ order.reduce_inventory!
1397
+ else
1398
+ # Log error and retry or alert
1399
+ Rails.logger.error("Capture failed for order #{order_id}: #{response.error_message}")
1400
+
1401
+ # Retry if appropriate
1402
+ raise "Capture failed" if response.status >= 500
1403
+ end
1404
+ end
1405
+ end
1406
+ ```
1407
+
1408
+ ## Make Async Payment
1409
+
1410
+ Initiate a card payment with 3D Secure 2.0 (3DS2) authentication support. This endpoint initiates the payment process and returns a `payment_token` that is required for initialising the 3DS2 web component on the client side.
1411
+
1412
+ This method is specifically designed for payments that require 3D Secure verification, providing enhanced security for card transactions.
1413
+
1414
+ ### Basic Async Payment
1415
+
1416
+ ```ruby
1417
+ # Make an async payment with just the required parameters
1418
+ response = items.make_payment_async(
1419
+ "item-123",
1420
+ account_id: "card_account-456" # Required
1421
+ )
1422
+
1423
+ if response.success?
1424
+ payment_id = response.data['payment_id']
1425
+ payment_token = response.data['payment_token']
1426
+ item = response.data['items']
1427
+
1428
+ puts "Payment initiated: #{payment_id}"
1429
+ puts "Payment token for 3DS2: #{payment_token}"
1430
+ puts "Item state: #{item['state']}"
1431
+ puts "Amount: $#{item['amount'] / 100.0}"
1432
+
1433
+ # Use the payment_token to initialise the 3DS2 web component
1434
+ # on the client side (JavaScript)
1435
+ else
1436
+ puts "Payment failed: #{response.error_message}"
1437
+ end
1438
+ ```
1439
+
1440
+ ### Async Payment with 3DS Challenge
1441
+
1442
+ To explicitly request a 3D Secure challenge:
1443
+
1444
+ ```ruby
1445
+ response = items.make_payment_async(
1446
+ "item-123",
1447
+ account_id: "card_account-456",
1448
+ request_three_d_secure: "challenge"
1449
+ )
1450
+
1451
+ if response.success?
1452
+ payment_token = response.data['payment_token']
1453
+ payment_id = response.data['payment_id']
1454
+
1455
+ puts "Payment initiated with 3DS challenge: #{payment_id}"
1456
+ puts "Payment token: #{payment_token}"
1457
+
1458
+ # Send the payment_token to the client to initialise 3DS2 component
1459
+ # The component will display the 3DS challenge to the user
1460
+ else
1461
+ puts "Payment initiation failed: #{response.error_message}"
1462
+ end
1463
+ ```
1464
+
1465
+ ### Automatic 3DS Determination
1466
+
1467
+ When using the default 'automatic' mode, the system determines whether 3DS is required:
1468
+
1469
+ ```ruby
1470
+ response = items.make_payment_async(
1471
+ "item-123",
1472
+ account_id: "card_account-456",
1473
+ request_three_d_secure: "automatic" # This is the default
1474
+ )
1475
+
1476
+ if response.success?
1477
+ item = response.data['items']
1478
+ payment_token = response.data['payment_token']
1479
+
1480
+ puts "3DS handled automatically"
1481
+ puts "Item state: #{item['state']}"
1482
+
1483
+ # The payment_token will be provided if 3DS is required
1484
+ if payment_token && !payment_token.empty?
1485
+ puts "3DS verification required - use token: #{payment_token}"
1486
+ # Send token to client for 3DS2 component initialisation
1487
+ else
1488
+ puts "3DS verification not required - payment processed"
1489
+ end
1490
+ end
1491
+ ```
1492
+
1493
+ ### Complete Rails Example with 3DS2
1494
+
1495
+ Complete example showing how to implement async payment in a Rails application:
1496
+
1497
+ ```ruby
1498
+ # app/controllers/payments_controller.rb
1499
+ class PaymentsController < ApplicationController
1500
+ def create_async_payment
1501
+ items = ZaiPayment.items
1502
+
1503
+ # Step 1: Initiate async payment
1504
+ response = items.make_payment_async(
1505
+ params[:item_id],
1506
+ account_id: params[:account_id],
1507
+ request_three_d_secure: "automatic"
1508
+ )
1509
+
1510
+ if response.success?
1511
+ payment_id = response.data['payment_id']
1512
+ payment_token = response.data['payment_token']
1513
+ item_data = response.data['items']
1514
+
1515
+ # Store payment_id for tracking
1516
+ @payment = Payment.create!(
1517
+ zai_payment_id: payment_id,
1518
+ zai_item_id: item_data['id'],
1519
+ amount: item_data['amount'],
1520
+ state: item_data['state'],
1521
+ payment_token: payment_token
1522
+ )
1523
+
1524
+ # Return payment_token to client for 3DS2 initialisation
1525
+ render json: {
1526
+ success: true,
1527
+ payment_id: payment_id,
1528
+ payment_token: payment_token,
1529
+ requires_3ds: payment_token.present?
1530
+ }
1531
+ else
1532
+ render json: {
1533
+ success: false,
1534
+ error: response.error_message
1535
+ }, status: :unprocessable_entity
1536
+ end
1537
+ end
1538
+ end
1539
+ ```
1540
+
1541
+ ```javascript
1542
+ // app/javascript/payments/three_d_secure.js
1543
+ // Client-side 3DS2 component initialisation
1544
+
1545
+ async function initiateAsyncPayment(itemId, accountId) {
1546
+ try {
1547
+ // Call your Rails backend to initiate the payment
1548
+ const response = await fetch('/payments/create_async', {
1549
+ method: 'POST',
1550
+ headers: {
1551
+ 'Content-Type': 'application/json',
1552
+ 'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
1553
+ },
1554
+ body: JSON.stringify({
1555
+ item_id: itemId,
1556
+ account_id: accountId
1557
+ })
1558
+ });
1559
+
1560
+ const data = await response.json();
1561
+
1562
+ if (data.success && data.requires_3ds) {
1563
+ // Initialize 3DS2 component with the payment_token
1564
+ await initialize3DS2Component(data.payment_token);
1565
+ } else if (data.success) {
1566
+ // Payment completed without 3DS
1567
+ window.location.href = '/payments/success';
1568
+ } else {
1569
+ alert('Payment failed: ' + data.error);
1570
+ }
1571
+ } catch (error) {
1572
+ console.error('Payment error:', error);
1573
+ alert('Payment failed. Please try again.');
1574
+ }
1575
+ }
1576
+
1577
+ async function initialize3DS2Component(paymentToken) {
1578
+ // Use Zai's 3DS2 SDK to initialize the component
1579
+ // This is a simplified example - refer to Zai documentation for actual implementation
1580
+
1581
+ const threeDSComponent = new ZaiThreeDSecure({
1582
+ paymentToken: paymentToken,
1583
+ onSuccess: function(result) {
1584
+ console.log('3DS verification successful', result);
1585
+ window.location.href = '/payments/success';
1586
+ },
1587
+ onError: function(error) {
1588
+ console.error('3DS verification failed', error);
1589
+ alert('Payment verification failed. Please try again.');
1590
+ }
1591
+ });
1592
+
1593
+ threeDSComponent.mount('#three-ds-container');
1594
+ }
1595
+ ```
1596
+
1597
+ ### Error Handling for Async Payments
1598
+
1599
+ ```ruby
1600
+ begin
1601
+ response = items.make_payment_async(
1602
+ "item-123",
1603
+ account_id: "card_account-456"
1604
+ )
1605
+
1606
+ if response.success?
1607
+ payment_id = response.data['payment_id']
1608
+ payment_token = response.data['payment_token']
1609
+
1610
+ puts "✓ Payment initiated: #{payment_id}"
1611
+
1612
+ if payment_token
1613
+ puts " 3DS verification required"
1614
+ puts " Token: #{payment_token}"
1615
+ else
1616
+ puts " 3DS not required, payment processing"
1617
+ end
1618
+ else
1619
+ # Handle API errors
1620
+ case response.status
1621
+ when 422
1622
+ puts "Validation error: #{response.error_message}"
1623
+ # Common: Invalid account, insufficient funds
1624
+ when 404
1625
+ puts "Item or account not found"
1626
+ when 401
1627
+ puts "Authentication failed"
1628
+ else
1629
+ puts "Payment error: #{response.error_message}"
1630
+ end
1631
+ end
1632
+ rescue ZaiPayment::Errors::ValidationError => e
1633
+ puts "Validation error: #{e.message}"
1634
+ # Example: "account_id is required and cannot be blank"
1635
+ rescue ZaiPayment::Errors::NotFoundError => e
1636
+ puts "Resource not found: #{e.message}"
1637
+ rescue ZaiPayment::Errors::BadRequestError => e
1638
+ puts "Bad request: #{e.message}"
1639
+ rescue ZaiPayment::Errors::ApiError => e
1640
+ puts "API error: #{e.message}"
1641
+ end
1642
+ ```
1643
+
1644
+ ### Full E-commerce Flow with Async Payment
1645
+
1646
+ Complete example showing item creation through async payment with 3DS:
1647
+
1648
+ ```ruby
1649
+ require 'zai_payment'
1650
+
1651
+ # Configure
1652
+ ZaiPayment.configure do |config|
1653
+ config.client_id = ENV['ZAI_CLIENT_ID']
1654
+ config.client_secret = ENV['ZAI_CLIENT_SECRET']
1655
+ config.scope = ENV['ZAI_SCOPE']
1656
+ config.environment = :prelive
1657
+ end
1658
+
1659
+ items = ZaiPayment.items
1660
+
1661
+ # Step 1: Create an item
1662
+ create_response = items.create(
1663
+ name: "Premium Product with 3DS",
1664
+ amount: 25000, # $250.00
1665
+ payment_type: 2,
1666
+ buyer_id: "buyer-123",
1667
+ seller_id: "seller-456",
1668
+ description: "High-value product requiring 3DS verification"
1669
+ )
1670
+
1671
+ if create_response.success?
1672
+ item_id = create_response.data['id']
1673
+ puts "✓ Item created: #{item_id}"
1674
+
1675
+ # Step 2: Initiate async payment with 3DS
1676
+ payment_response = items.make_payment_async(
1677
+ item_id,
1678
+ account_id: "card_account-789",
1679
+ request_three_d_secure: "automatic"
1680
+ )
1681
+
1682
+ if payment_response.success?
1683
+ payment_id = payment_response.data['payment_id']
1684
+ payment_token = payment_response.data['payment_token']
1685
+ item_data = payment_response.data['items']
1686
+
1687
+ puts "✓ Async payment initiated: #{payment_id}"
1688
+ puts " Item state: #{item_data['state']}"
1689
+ puts " Amount: $#{item_data['amount'] / 100.0}"
1690
+
1691
+ if payment_token && !payment_token.empty?
1692
+ puts " 3DS verification required"
1693
+ puts " Payment token: #{payment_token}"
1694
+ puts " → Send this token to client for 3DS2 component"
1695
+
1696
+ # In a real application:
1697
+ # 1. Send payment_token to the client
1698
+ # 2. Client initializes 3DS2 component
1699
+ # 3. User completes 3DS challenge
1700
+ # 4. Listen for webhook to confirm payment status
1701
+ else
1702
+ puts " 3DS not required - payment processing automatically"
1703
+
1704
+ # Step 3: Monitor payment via webhook or polling
1705
+ # In production, use webhooks for real-time updates
1706
+ end
1707
+ else
1708
+ puts "✗ Payment failed: #{payment_response.error_message}"
1709
+ end
1710
+ else
1711
+ puts "✗ Item creation failed: #{create_response.error_message}"
1712
+ end
1713
+ ```
1714
+
1715
+ ### Understanding the Response
1716
+
1717
+ The `make_payment_async` response includes several important fields:
1718
+
1719
+ ```ruby
1720
+ response = items.make_payment_async("item-123", account_id: "account-456")
1721
+
1722
+ if response.success?
1723
+ # Top-level payment information
1724
+ payment_id = response.data['payment_id'] # Unique payment identifier
1725
+ account_id = response.data['account_id'] # Account used for payment
1726
+ payment_token = response.data['payment_token'] # Token for 3DS2 initialization
1727
+
1728
+ # Item details
1729
+ item = response.data['items']
1730
+ item_id = item['id'] # Item/transaction ID
1731
+ state = item['state'] # Current state (e.g., 'pending')
1732
+ amount = item['amount'] # Amount in cents
1733
+ currency = item['currency'] # Currency code (e.g., 'AUD')
1734
+ payment_method = item['payment_method'] # Payment method used
1735
+
1736
+ # Related resources
1737
+ related = item['related']
1738
+ buyer_id = related['buyers'] # Buyer user ID
1739
+ seller_id = related['sellers'] # Seller user ID
1740
+
1741
+ # Useful links
1742
+ links = item['links']
1743
+ status_url = links['status'] # URL to check status
1744
+ transactions_url = links['transactions'] # URL for transactions
1745
+ end
1746
+ ```
1747
+
1748
+ ### 3DS Preference Options
1749
+
1750
+ The `request_three_d_secure` parameter accepts three values:
1751
+
1752
+ | Value | Description | Use Case |
1753
+ |-------|-------------|----------|
1754
+ | `'automatic'` | System determines if 3DS is required (default) | Recommended for most cases - balances security and UX |
1755
+ | `'challenge'` | Always request 3DS challenge | High-value transactions, compliance requirements |
1756
+ | `'any'` | Request 3DS regardless of challenge flow | Maximum security, regulatory requirements |
1757
+
1758
+ ```ruby
1759
+ # Automatic (recommended)
1760
+ items.make_payment_async("item-123",
1761
+ account_id: "account-456",
1762
+ request_three_d_secure: "automatic"
1763
+ )
1764
+
1765
+ # Force challenge for high-value transactions
1766
+ items.make_payment_async("item-123",
1767
+ account_id: "account-456",
1768
+ request_three_d_secure: "challenge"
1769
+ )
1770
+
1771
+ # Maximum security
1772
+ items.make_payment_async("item-123",
1773
+ account_id: "account-456",
1774
+ request_three_d_secure: "any"
1775
+ )
1776
+ ```
1777
+
1778
+ ### Important Notes
1779
+
1780
+ 1. **Payment Token**: The `payment_token` must be sent to the client to initialize the 3DS2 web component
1781
+ 2. **Webhooks**: After 3DS authentication completes, listen for webhooks to get the final payment status
1782
+ 3. **Timeout**: 3DS challenges typically expire after 10-15 minutes of inactivity
1783
+ 4. **Failed Authentication**: If 3DS verification fails, the payment will be automatically cancelled
1784
+ 5. **Testing**: Use Zai test cards to simulate different 3DS scenarios in prelive environment
1785
+
1786
+ ### Webhook Integration
1787
+
1788
+ After initiating an async payment, monitor webhooks for status updates:
1789
+
1790
+ ```ruby
1791
+ # In your webhook handler (e.g., Rails controller)
1792
+ def handle_payment_webhook
1793
+ # Verify webhook signature first
1794
+ webhooks = ZaiPayment.webhooks
1795
+
1796
+ begin
1797
+ if webhooks.verify_signature(request.body.read, request.headers['X-Signature'])
1798
+ payload = JSON.parse(request.body.read)
1799
+
1800
+ case payload['type']
1801
+ when 'transaction'
1802
+ if payload['status'] == 'successful'
1803
+ payment_id = payload['payment_id']
1804
+ item_id = payload['item_id']
1805
+
1806
+ # Update your database
1807
+ payment = Payment.find_by(zai_payment_id: payment_id)
1808
+ payment.update!(status: 'completed', completed_at: Time.current)
1809
+
1810
+ # Send confirmation email, fulfill order, etc.
1811
+ OrderFulfillmentJob.perform_later(payment.order_id)
1812
+
1813
+ puts "✓ Payment completed: #{payment_id}"
1814
+ elsif payload['status'] == 'failed'
1815
+ # Handle failed payment
1816
+ payment = Payment.find_by(zai_payment_id: payload['payment_id'])
1817
+ payment.update!(status: 'failed', failure_reason: payload['message'])
1818
+
1819
+ # Notify customer
1820
+ PaymentFailureMailer.notify(payment.user).deliver_later
1821
+ end
1822
+ end
1823
+
1824
+ head :ok
1825
+ else
1826
+ head :unauthorized
1827
+ end
1828
+ rescue => e
1829
+ Rails.logger.error "Webhook error: #{e.message}"
1830
+ head :internal_server_error
1831
+ end
1832
+ end
1833
+ ```
1834
+
1835
+ ## Cancel Item
1836
+
1837
+ Cancel an existing item/payment. This operation is typically used to cancel a pending payment before it has been processed or completed.
1838
+
1839
+ ### Basic Cancel
1840
+
1841
+ ```ruby
1842
+ response = items.cancel("item-123")
1843
+
1844
+ if response.success?
1845
+ item = response.data
1846
+ puts "Item cancelled successfully"
1847
+ puts "Item ID: #{item['id']}"
1848
+ puts "State: #{item['state']}"
1849
+ puts "Payment State: #{item['payment_state']}"
1850
+ else
1851
+ puts "Cancel failed: #{response.error_message}"
1852
+ end
1853
+ ```
1854
+
1855
+ ### Cancel with Error Handling
1856
+
1857
+ ```ruby
1858
+ begin
1859
+ response = items.cancel("item-123")
1860
+
1861
+ if response.success?
1862
+ item = response.data
1863
+ puts "✓ Item cancelled: #{item['id']}"
1864
+ puts " State: #{item['state']}"
1865
+ puts " Payment State: #{item['payment_state']}"
1866
+ else
1867
+ # Handle API errors
1868
+ case response.status
1869
+ when 422
1870
+ puts "Cannot cancel: #{response.error_message}"
1871
+ # Common: Item already completed or in a state that can't be cancelled
1872
+ when 404
1873
+ puts "Item not found"
1874
+ when 401
1875
+ puts "Authentication failed"
1876
+ else
1877
+ puts "Cancellation error: #{response.error_message}"
1878
+ end
1879
+ end
1880
+ rescue ZaiPayment::Errors::ValidationError => e
1881
+ puts "Validation error: #{e.message}"
1882
+ rescue ZaiPayment::Errors::NotFoundError => e
1883
+ puts "Item not found: #{e.message}"
1884
+ rescue ZaiPayment::Errors::ApiError => e
1885
+ puts "API error: #{e.message}"
1886
+ end
1887
+ ```
1888
+
1889
+ ### Cancel with Status Check
1890
+
1891
+ Check item status before attempting to cancel:
1892
+
1893
+ ```ruby
1894
+ # Check current status
1895
+ status_response = items.show_status("item-123")
1896
+
1897
+ if status_response.success?
1898
+ status = status_response.data
1899
+ current_state = status['state']
1900
+ payment_state = status['payment_state']
1901
+
1902
+ puts "Current state: #{current_state}"
1903
+ puts "Payment state: #{payment_state}"
1904
+
1905
+ # Only cancel if in a cancellable state
1906
+ if ['pending', 'payment_pending'].include?(current_state)
1907
+ cancel_response = items.cancel("item-123")
1908
+
1909
+ if cancel_response.success?
1910
+ puts "✓ Item cancelled successfully"
1911
+ else
1912
+ puts "✗ Cancel failed: #{cancel_response.error_message}"
1913
+ end
1914
+ else
1915
+ puts "Item cannot be cancelled - current state: #{current_state}"
1916
+ end
1917
+ end
1918
+ ```
1919
+
1920
+ ### Real-World Cancel Flow Example
1921
+
1922
+ Complete example showing item creation, payment, and cancellation:
1923
+
1924
+ ```ruby
1925
+ require 'zai_payment'
1926
+
1927
+ # Configure
1928
+ ZaiPayment.configure do |config|
1929
+ config.client_id = ENV['ZAI_CLIENT_ID']
1930
+ config.client_secret = ENV['ZAI_CLIENT_SECRET']
1931
+ config.scope = ENV['ZAI_SCOPE']
1932
+ config.environment = :prelive
1933
+ end
1934
+
1935
+ items = ZaiPayment.items
1936
+
1937
+ # Step 1: Create an item
1938
+ create_response = items.create(
1939
+ name: "Product Purchase",
1940
+ amount: 10000, # $100.00
1941
+ payment_type: 2,
1942
+ buyer_id: "buyer-123",
1943
+ seller_id: "seller-456",
1944
+ description: "Purchase of premium widget"
1945
+ )
1946
+
1947
+ if create_response.success?
1948
+ item_id = create_response.data['id']
1949
+ puts "✓ Item created: #{item_id}"
1950
+
1951
+ # Step 2: Customer decides to cancel before payment
1952
+ puts "\nCustomer requested cancellation..."
1953
+
1954
+ # Step 3: Check if item can be cancelled
1955
+ status_response = items.show_status(item_id)
1956
+
1957
+ if status_response.success?
1958
+ current_state = status_response.data['state']
1959
+ puts "Current item state: #{current_state}"
1960
+
1961
+ # Step 4: Cancel the item
1962
+ if ['pending', 'payment_pending'].include?(current_state)
1963
+ cancel_response = items.cancel(item_id)
1964
+
1965
+ if cancel_response.success?
1966
+ cancelled_item = cancel_response.data
1967
+ puts "✓ Item cancelled successfully"
1968
+ puts " Final state: #{cancelled_item['state']}"
1969
+ puts " Payment state: #{cancelled_item['payment_state']}"
1970
+
1971
+ # Notify customer
1972
+ # CustomerMailer.order_cancelled(customer_email, item_id).deliver_later
1973
+ else
1974
+ puts "✗ Cancellation failed: #{cancel_response.error_message}"
1975
+ end
1976
+ else
1977
+ puts "✗ Item cannot be cancelled - current state: #{current_state}"
1978
+ end
1979
+ end
1980
+ else
1981
+ puts "✗ Item creation failed: #{create_response.error_message}"
1982
+ end
1983
+ ```
1984
+
1985
+ ### Cancel States and Conditions
1986
+
1987
+ Items can typically be cancelled when in these states:
1988
+
1989
+ | State | Can Cancel? | Description |
1990
+ |-------|-------------|-------------|
1991
+ | `pending` | ✓ Yes | Item created but no payment initiated |
1992
+ | `payment_pending` | ✓ Yes | Payment initiated but not yet processed |
1993
+ | `payment_processing` | Maybe | Depends on payment processor |
1994
+ | `completed` | ✗ No | Payment completed, must refund instead |
1995
+ | `payment_held` | Maybe | May require admin approval |
1996
+ | `cancelled` | ✗ No | Already cancelled |
1997
+ | `refunded` | ✗ No | Already refunded |
1998
+
1999
+ **Note:** If an item is already completed or funds have been disbursed, you cannot cancel it. In those cases, you may need to process a refund instead.
2000
+
2001
+ ## Refund Item
2002
+
2003
+ Process a refund for a completed payment. This operation returns funds to the buyer and is typically used for customer returns, disputes, or service issues.
2004
+
2005
+ ### Basic Refund
2006
+
2007
+ ```ruby
2008
+ response = items.refund("item-123")
2009
+
2010
+ if response.success?
2011
+ item = response.data
2012
+ puts "Item refunded successfully"
2013
+ puts "Item ID: #{item['id']}"
2014
+ puts "State: #{item['state']}"
2015
+ puts "Payment State: #{item['payment_state']}"
2016
+ else
2017
+ puts "Refund failed: #{response.error_message}"
2018
+ end
2019
+ ```
2020
+
2021
+ ### Partial Refund
2022
+
2023
+ Process a partial refund for a specific amount:
2024
+
2025
+ ```ruby
2026
+ # Refund $50.00 out of the original $100.00 transaction
2027
+ response = items.refund(
2028
+ "item-123",
2029
+ refund_amount: 5000 # Amount in cents
2030
+ )
2031
+
2032
+ if response.success?
2033
+ item = response.data
2034
+ puts "Partial refund processed"
2035
+ puts "Refund Amount: $50.00"
2036
+ puts "State: #{item['state']}"
2037
+ else
2038
+ puts "Partial refund failed: #{response.error_message}"
2039
+ end
2040
+ ```
2041
+
2042
+ ### Refund with Message
2043
+
2044
+ Provide a reason for the refund:
2045
+
2046
+ ```ruby
2047
+ response = items.refund(
2048
+ "item-123",
2049
+ refund_message: "Customer returned defective product"
2050
+ )
2051
+
2052
+ if response.success?
2053
+ puts "Refund processed with message"
2054
+ else
2055
+ puts "Refund failed: #{response.error_message}"
2056
+ end
2057
+ ```
2058
+
2059
+ ### Refund to Specific Account
2060
+
2061
+ Specify which account to refund to:
2062
+
2063
+ ```ruby
2064
+ response = items.refund(
2065
+ "item-123",
2066
+ account_id: "account_789"
2067
+ )
2068
+
2069
+ if response.success?
2070
+ puts "Refund sent to specified account"
2071
+ else
2072
+ puts "Refund failed: #{response.error_message}"
2073
+ end
2074
+ ```
2075
+
2076
+ ### Refund with All Parameters
2077
+
2078
+ Process a partial refund with a message and specific account:
2079
+
2080
+ ```ruby
2081
+ response = items.refund(
2082
+ "item-123",
2083
+ refund_amount: 5000,
2084
+ refund_message: "Partial refund for shipping damage",
2085
+ account_id: "account_789"
2086
+ )
2087
+
2088
+ if response.success?
2089
+ item = response.data
2090
+ puts "✓ Partial refund processed"
2091
+ puts " Amount: $50.00"
2092
+ puts " Message: Partial refund for shipping damage"
2093
+ puts " State: #{item['state']}"
2094
+ puts " Payment State: #{item['payment_state']}"
2095
+ else
2096
+ puts "✗ Refund failed: #{response.error_message}"
2097
+ end
2098
+ ```
2099
+
2100
+ ### Refund with Error Handling
2101
+
2102
+ ```ruby
2103
+ begin
2104
+ response = items.refund("item-123")
2105
+
2106
+ if response.success?
2107
+ item = response.data
2108
+ puts "✓ Item refunded: #{item['id']}"
2109
+ puts " State: #{item['state']}"
2110
+ puts " Payment State: #{item['payment_state']}"
2111
+ else
2112
+ # Handle API errors
2113
+ case response.status
2114
+ when 422
2115
+ puts "Cannot refund: #{response.error_message}"
2116
+ # Common: Item already refunded, not in refundable state, etc.
2117
+ when 404
2118
+ puts "Item not found"
2119
+ when 401
2120
+ puts "Authentication failed"
2121
+ else
2122
+ puts "Refund error: #{response.error_message}"
2123
+ end
2124
+ end
2125
+ rescue ZaiPayment::Errors::ValidationError => e
2126
+ puts "Validation error: #{e.message}"
2127
+ rescue ZaiPayment::Errors::NotFoundError => e
2128
+ puts "Item not found: #{e.message}"
2129
+ rescue ZaiPayment::Errors::ApiError => e
2130
+ puts "API error: #{e.message}"
2131
+ end
2132
+ ```
2133
+
2134
+ ### Refund with Status Check
2135
+
2136
+ Check item status before attempting to refund:
2137
+
2138
+ ```ruby
2139
+ # Check current status
2140
+ status_response = items.show_status("item-123")
2141
+
2142
+ if status_response.success?
2143
+ status = status_response.data
2144
+ current_state = status['state']
2145
+ payment_state = status['payment_state']
2146
+
2147
+ puts "Current state: #{current_state}"
2148
+ puts "Payment state: #{payment_state}"
2149
+
2150
+ # Only refund if in a refundable state
2151
+ if ['completed', 'payment_deposited', 'work_completed'].include?(payment_state)
2152
+ refund_response = items.refund("item-123")
2153
+
2154
+ if refund_response.success?
2155
+ puts "✓ Item refunded successfully"
2156
+ else
2157
+ puts "✗ Refund failed: #{refund_response.error_message}"
2158
+ end
2159
+ else
2160
+ puts "Item cannot be refunded - payment state: #{payment_state}"
2161
+ end
2162
+ end
2163
+ ```
2164
+
2165
+ ### Real-World Refund Flow Example
2166
+
2167
+ Complete example showing item creation, payment, and refund:
2168
+
2169
+ ```ruby
2170
+ require 'zai_payment'
2171
+
2172
+ # Configure
2173
+ ZaiPayment.configure do |config|
2174
+ config.client_id = ENV['ZAI_CLIENT_ID']
2175
+ config.client_secret = ENV['ZAI_CLIENT_SECRET']
2176
+ config.scope = ENV['ZAI_SCOPE']
2177
+ config.environment = :prelive
2178
+ end
2179
+
2180
+ items = ZaiPayment.items
2181
+
2182
+ # Step 1: Create an item
2183
+ create_response = items.create(
2184
+ name: "Product Purchase",
2185
+ amount: 10000, # $100.00
2186
+ payment_type: 2,
2187
+ buyer_id: "buyer-123",
2188
+ seller_id: "seller-456",
2189
+ description: "Purchase of premium widget"
2190
+ )
2191
+
2192
+ if create_response.success?
2193
+ item_id = create_response.data['id']
2194
+ puts "✓ Item created: #{item_id}"
2195
+
2196
+ # Step 2: Make the payment
2197
+ payment_response = items.make_payment(
2198
+ item_id,
2199
+ account_id: "card_account-789"
2200
+ )
2201
+
2202
+ if payment_response.success?
2203
+ puts "✓ Payment processed"
2204
+
2205
+ # Step 3: Customer requests refund
2206
+ puts "\nCustomer requested refund..."
2207
+
2208
+ # Wait for payment to complete
2209
+ sleep 2
2210
+
2211
+ # Step 4: Check if item can be refunded
2212
+ status_response = items.show_status(item_id)
2213
+
2214
+ if status_response.success?
2215
+ payment_state = status_response.data['payment_state']
2216
+ puts "Current payment state: #{payment_state}"
2217
+
2218
+ # Step 5: Process the refund
2219
+ if ['completed', 'payment_deposited'].include?(payment_state)
2220
+ refund_response = items.refund(
2221
+ item_id,
2222
+ refund_message: "Customer return - changed mind"
2223
+ )
2224
+
2225
+ if refund_response.success?
2226
+ refunded_item = refund_response.data
2227
+ puts "✓ Refund processed successfully"
2228
+ puts " Final state: #{refunded_item['state']}"
2229
+ puts " Payment state: #{refunded_item['payment_state']}"
2230
+
2231
+ # Notify customer
2232
+ # CustomerMailer.refund_processed(customer_email, item_id).deliver_later
2233
+ else
2234
+ puts "✗ Refund failed: #{refund_response.error_message}"
2235
+ end
2236
+ else
2237
+ puts "✗ Item cannot be refunded - payment state: #{payment_state}"
2238
+ end
2239
+ end
2240
+ else
2241
+ puts "✗ Payment failed: #{payment_response.error_message}"
2242
+ end
2243
+ else
2244
+ puts "✗ Item creation failed: #{create_response.error_message}"
2245
+ end
2246
+ ```
2247
+
2248
+ ### Refund States and Conditions
2249
+
2250
+ Items can typically be refunded when in these states:
2251
+
2252
+ | State | Can Refund? | Description |
2253
+ |-------|-------------|-------------|
2254
+ | `pending` | ✗ No | Item not yet paid, cancel instead |
2255
+ | `payment_pending` | ✗ No | Payment not completed, cancel instead |
2256
+ | `completed` | ✓ Yes | Payment completed successfully |
2257
+ | `payment_deposited` | ✓ Yes | Payment received and deposited |
2258
+ | `work_completed` | ✓ Yes | Work completed, funds can be refunded |
2259
+ | `cancelled` | ✗ No | Already cancelled |
2260
+ | `refunded` | ✗ No | Already refunded |
2261
+ | `payment_held` | Maybe | May require admin approval |
2262
+
2263
+ **Note:** Full refunds return the entire item amount. Partial refunds return a specified amount less than the total. Multiple partial refunds may be possible depending on your Zai configuration.
2264
+
2265
+ ### Integration with Rails - Refunds
2266
+
2267
+ #### In a Controller
2268
+
2269
+ ```ruby
2270
+ class RefundsController < ApplicationController
2271
+ def create
2272
+ @order = Order.find(params[:order_id])
2273
+
2274
+ # Ensure order belongs to current user or is admin
2275
+ unless @order.user == current_user || current_user.admin?
2276
+ redirect_to root_path, alert: 'Unauthorized'
2277
+ return
2278
+ end
2279
+
2280
+ # Process refund in Zai
2281
+ response = ZaiPayment.items.refund(
2282
+ @order.zai_item_id,
2283
+ refund_amount: params[:refund_amount]&.to_i,
2284
+ refund_message: params[:refund_message]
2285
+ )
2286
+
2287
+ if response.success?
2288
+ @order.update(
2289
+ status: 'refunded',
2290
+ refunded_at: Time.current,
2291
+ refund_amount: params[:refund_amount]&.to_i,
2292
+ refund_reason: params[:refund_message]
2293
+ )
2294
+
2295
+ redirect_to @order, notice: 'Refund processed successfully'
2296
+ else
2297
+ flash[:error] = "Cannot process refund: #{response.error_message}"
2298
+ redirect_to @order
2299
+ end
2300
+ rescue ZaiPayment::Errors::ValidationError => e
2301
+ flash[:error] = "Refund error: #{e.message}"
2302
+ redirect_to @order
2303
+ end
2304
+ end
2305
+ ```
2306
+
2307
+ #### In a Service Object
2308
+
2309
+ ```ruby
2310
+ class RefundService
2311
+ def initialize(order, refund_params = {})
2312
+ @order = order
2313
+ @refund_amount = refund_params[:amount]
2314
+ @refund_message = refund_params[:message]
2315
+ @account_id = refund_params[:account_id]
2316
+ end
2317
+
2318
+ def process_refund
2319
+ # Validate order is refundable
2320
+ unless refundable?
2321
+ return { success: false, error: 'Order cannot be refunded' }
2322
+ end
2323
+
2324
+ # Validate refund amount
2325
+ if @refund_amount && @refund_amount > @order.total_amount
2326
+ return { success: false, error: 'Refund amount exceeds order total' }
2327
+ end
2328
+
2329
+ # Process refund in Zai
2330
+ response = ZaiPayment.items.refund(
2331
+ @order.zai_item_id,
2332
+ refund_amount: @refund_amount,
2333
+ refund_message: @refund_message,
2334
+ account_id: @account_id
2335
+ )
2336
+
2337
+ if response.success?
2338
+ # Update local database
2339
+ @order.update(
2340
+ status: @refund_amount == @order.total_amount ? 'refunded' : 'partially_refunded',
2341
+ zai_state: response.data['state'],
2342
+ zai_payment_state: response.data['payment_state'],
2343
+ refunded_at: Time.current,
2344
+ refund_amount: @refund_amount || @order.total_amount,
2345
+ refund_reason: @refund_message
2346
+ )
2347
+
2348
+ # Send notification
2349
+ OrderMailer.refund_processed(@order).deliver_later
2350
+
2351
+ # Log the refund
2352
+ @order.refund_logs.create(
2353
+ amount: @refund_amount || @order.total_amount,
2354
+ reason: @refund_message,
2355
+ processed_at: Time.current
2356
+ )
2357
+
2358
+ { success: true, order: @order }
2359
+ else
2360
+ { success: false, error: response.error_message }
2361
+ end
2362
+ rescue ZaiPayment::Errors::ApiError => e
2363
+ { success: false, error: e.message }
2364
+ end
2365
+
2366
+ private
2367
+
2368
+ def refundable?
2369
+ # Check local status
2370
+ return false unless @order.status.in?(['completed', 'paid'])
2371
+
2372
+ # Check Zai status
2373
+ status_response = ZaiPayment.items.show_status(@order.zai_item_id)
2374
+ return false unless status_response.success?
2375
+
2376
+ payment_state = status_response.data['payment_state']
2377
+ payment_state.in?(['completed', 'payment_deposited', 'work_completed'])
2378
+ rescue
2379
+ false
2380
+ end
2381
+ end
2382
+
2383
+ # Usage:
2384
+ # service = RefundService.new(order, amount: 5000, message: 'Customer return')
2385
+ # result = service.process_refund
2386
+ # if result[:success]
2387
+ # # Handle success
2388
+ # else
2389
+ # # Handle error: result[:error]
2390
+ # end
2391
+ ```
2392
+
2393
+ ### Webhook Integration for Refunds
2394
+
2395
+ After processing a refund, you may receive webhook notifications:
2396
+
2397
+ ```ruby
2398
+ # In your webhook handler
2399
+ def handle_refund_webhook(payload)
2400
+ if payload['type'] == 'refund' && payload['status'] == 'successful'
2401
+ item_id = payload['related_items'].first
2402
+ puts "Refund successful for item: #{item_id}"
2403
+
2404
+ # Update your database
2405
+ Order.find_by(zai_item_id: item_id)&.update(
2406
+ status: 'refunded',
2407
+ zai_state: payload['state'],
2408
+ refunded_at: Time.current,
2409
+ refund_amount: payload['amount']
2410
+ )
2411
+
2412
+ # Notify customer
2413
+ order = Order.find_by(zai_item_id: item_id)
2414
+ OrderMailer.refund_confirmed(order).deliver_later if order
2415
+ elsif payload['status'] == 'failed'
2416
+ puts "Refund failed: #{payload['failure_reason']}"
2417
+ end
2418
+ end
2419
+ ```
2420
+
2421
+ ### Testing Refund Functionality
2422
+
2423
+ ```ruby
2424
+ # spec/services/refund_service_spec.rb
2425
+ RSpec.describe RefundService do
2426
+ let(:order) { create(:order, status: 'completed', zai_item_id: 'item-123', total_amount: 10000) }
2427
+ let(:service) { described_class.new(order, amount: 5000, message: 'Customer return') }
2428
+
2429
+ describe '#process_refund' do
2430
+ context 'when order can be refunded' do
2431
+ before do
2432
+ allow(ZaiPayment.items).to receive(:show_status).and_return(
2433
+ double(success?: true, data: { 'payment_state' => 'completed' })
2434
+ )
2435
+
2436
+ allow(ZaiPayment.items).to receive(:refund).and_return(
2437
+ double(
2438
+ success?: true,
2439
+ data: { 'id' => 'item-123', 'state' => 'refunded', 'payment_state' => 'refunded' }
2440
+ )
2441
+ )
2442
+ end
2443
+
2444
+ it 'successfully processes the refund' do
2445
+ result = service.process_refund
2446
+
2447
+ expect(result[:success]).to be true
2448
+ expect(order.reload.status).to eq('partially_refunded')
2449
+ expect(order.refund_amount).to eq(5000)
2450
+ expect(order.refunded_at).to be_present
2451
+ end
2452
+
2453
+ it 'marks as fully refunded when refund amount equals total' do
2454
+ service = described_class.new(order, amount: 10000)
2455
+ result = service.process_refund
2456
+
2457
+ expect(order.reload.status).to eq('refunded')
2458
+ end
2459
+ end
2460
+
2461
+ context 'when order cannot be refunded' do
2462
+ before do
2463
+ order.update(status: 'pending')
2464
+ end
2465
+
2466
+ it 'returns error' do
2467
+ result = service.process_refund
2468
+
2469
+ expect(result[:success]).to be false
2470
+ expect(result[:error]).to include('cannot be refunded')
2471
+ end
2472
+ end
2473
+
2474
+ context 'when refund amount exceeds order total' do
2475
+ let(:service) { described_class.new(order, amount: 20000) }
2476
+
2477
+ it 'returns error' do
2478
+ result = service.process_refund
2479
+
2480
+ expect(result[:success]).to be false
2481
+ expect(result[:error]).to include('exceeds order total')
2482
+ end
2483
+ end
2484
+ end
2485
+ end
2486
+ ```
2487
+
2488
+ ### Cancel Integration with Rails
2489
+
2490
+ #### In a Controller
2491
+
2492
+ ```ruby
2493
+ class OrdersController < ApplicationController
2494
+ def cancel
2495
+ @order = Order.find(params[:id])
2496
+
2497
+ # Ensure order belongs to current user
2498
+ unless @order.user == current_user
2499
+ redirect_to root_path, alert: 'Unauthorized'
2500
+ return
2501
+ end
2502
+
2503
+ # Cancel in Zai
2504
+ response = ZaiPayment.items.cancel(@order.zai_item_id)
2505
+
2506
+ if response.success?
2507
+ @order.update(
2508
+ status: 'cancelled',
2509
+ cancelled_at: Time.current
2510
+ )
2511
+
2512
+ redirect_to @order, notice: 'Order cancelled successfully'
2513
+ else
2514
+ flash[:error] = "Cannot cancel order: #{response.error_message}"
2515
+ redirect_to @order
2516
+ end
2517
+ rescue ZaiPayment::Errors::ValidationError => e
2518
+ flash[:error] = "Cancellation error: #{e.message}"
2519
+ redirect_to @order
2520
+ end
2521
+ end
2522
+ ```
2523
+
2524
+ #### In a Service Object
2525
+
2526
+ ```ruby
2527
+ class OrderCancellationService
2528
+ def initialize(order)
2529
+ @order = order
2530
+ end
2531
+
2532
+ def cancel
2533
+ # Check if order can be cancelled
2534
+ unless cancellable?
2535
+ return { success: false, error: 'Order cannot be cancelled' }
2536
+ end
2537
+
2538
+ # Cancel in Zai
2539
+ response = ZaiPayment.items.cancel(@order.zai_item_id)
2540
+
2541
+ if response.success?
2542
+ # Update local database
2543
+ @order.update(
2544
+ status: 'cancelled',
2545
+ zai_state: response.data['state'],
2546
+ zai_payment_state: response.data['payment_state'],
2547
+ cancelled_at: Time.current,
2548
+ cancelled_by: @order.user_id
2549
+ )
2550
+
2551
+ # Send notification
2552
+ OrderMailer.order_cancelled(@order).deliver_later
2553
+
2554
+ # Refund any processing fees if applicable
2555
+ process_fee_refund if @order.processing_fee.present?
2556
+
2557
+ { success: true, order: @order }
2558
+ else
2559
+ { success: false, error: response.error_message }
2560
+ end
2561
+ rescue ZaiPayment::Errors::ApiError => e
2562
+ { success: false, error: e.message }
2563
+ end
2564
+
2565
+ private
2566
+
2567
+ def cancellable?
2568
+ # Check local status
2569
+ return false unless @order.status.in?(['pending', 'payment_pending'])
2570
+
2571
+ # Check Zai status
2572
+ status_response = ZaiPayment.items.show_status(@order.zai_item_id)
2573
+ return false unless status_response.success?
2574
+
2575
+ status_response.data['state'].in?(['pending', 'payment_pending'])
2576
+ rescue
2577
+ false
2578
+ end
2579
+
2580
+ def process_fee_refund
2581
+ # Custom logic for refunding processing fees
2582
+ # ...
2583
+ end
2584
+ end
2585
+
2586
+ # Usage:
2587
+ # service = OrderCancellationService.new(order)
2588
+ # result = service.cancel
2589
+ # if result[:success]
2590
+ # # Handle success
2591
+ # else
2592
+ # # Handle error: result[:error]
2593
+ # end
2594
+ ```
2595
+
2596
+ ### Webhook Integration
2597
+
2598
+ After cancelling an item, you may receive webhook notifications:
2599
+
2600
+ ```ruby
2601
+ # In your webhook handler
2602
+ def handle_item_webhook(payload)
2603
+ if payload['type'] == 'item' && payload['status'] == 'cancelled'
2604
+ item_id = payload['id']
2605
+ puts "Item cancelled: #{item_id}"
2606
+
2607
+ # Update your database
2608
+ Order.find_by(zai_item_id: item_id)&.update(
2609
+ status: 'cancelled',
2610
+ zai_state: payload['state'],
2611
+ cancelled_at: Time.current
2612
+ )
2613
+
2614
+ # Notify customer
2615
+ order = Order.find_by(zai_item_id: item_id)
2616
+ OrderMailer.cancellation_confirmed(order).deliver_later if order
2617
+ end
2618
+ end
2619
+ ```
2620
+
2621
+ ### Testing Cancel Functionality
2622
+
2623
+ ```ruby
2624
+ # spec/services/order_cancellation_service_spec.rb
2625
+ RSpec.describe OrderCancellationService do
2626
+ let(:order) { create(:order, status: 'pending', zai_item_id: 'item-123') }
2627
+ let(:service) { described_class.new(order) }
2628
+
2629
+ describe '#cancel' do
2630
+ context 'when order can be cancelled' do
2631
+ before do
2632
+ allow(ZaiPayment.items).to receive(:show_status).and_return(
2633
+ double(success?: true, data: { 'state' => 'pending' })
2634
+ )
2635
+
2636
+ allow(ZaiPayment.items).to receive(:cancel).and_return(
2637
+ double(
2638
+ success?: true,
2639
+ data: { 'id' => 'item-123', 'state' => 'cancelled', 'payment_state' => 'cancelled' }
2640
+ )
2641
+ )
2642
+ end
2643
+
2644
+ it 'successfully cancels the order' do
2645
+ result = service.cancel
2646
+
2647
+ expect(result[:success]).to be true
2648
+ expect(order.reload.status).to eq('cancelled')
2649
+ expect(order.cancelled_at).to be_present
2650
+ end
2651
+ end
2652
+
2653
+ context 'when order cannot be cancelled' do
2654
+ before do
2655
+ order.update(status: 'completed')
2656
+ end
2657
+
2658
+ it 'returns error' do
2659
+ result = service.cancel
2660
+
2661
+ expect(result[:success]).to be false
2662
+ expect(result[:error]).to include('cannot be cancelled')
2663
+ end
2664
+ end
2665
+ end
2666
+ end
2667
+ ```
2668
+
557
2669
  ## Payment Types
558
2670
 
559
2671
  When creating items, you can specify different payment types:
@@ -595,4 +2707,7 @@ For more information about the Zai Items API, see:
595
2707
  - [List Item Transactions](https://developer.hellozai.com/reference/listitemtransactions)
596
2708
  - [List Item Batch Transactions](https://developer.hellozai.com/reference/listitembatchtransactions)
597
2709
  - [Show Item Status](https://developer.hellozai.com/reference/showitemstatus)
2710
+ - [Make Payment](https://developer.hellozai.com/reference/makepayment)
2711
+ - [Cancel Item](https://developer.hellozai.com/reference/cancelitem)
2712
+ - [Refund Item](https://developer.hellozai.com/reference/refund)
598
2713