This document provides examples of how to use the Items resource in the Zai Payment gem.
- Setup
- Create Item
- List Items
- Show Item
- Update Item
- Delete Item
- Show Item Seller
- Show Item Buyer
- Show Item Fees
- Show Item Wire Details
- List Item Transactions
- List Item Batch Transactions
- Show Item Status
- Make Payment
- Make Async Payment
- Cancel Item
- Refund Item
require 'zai_payment'
# Configure the gem
ZaiPayment.configure do |config|
config.client_id = 'your_client_id'
config.client_secret = 'your_client_secret'
config.scope = 'your_scope'
config.environment = :prelive # or :production
end
# Access the items resource
items = ZaiPayment.itemsCreate a new item (transaction/payment) between a buyer and a seller.
# Create a basic item
response = items.create(
name: "Product Purchase",
amount: 10000, # Amount in cents (100.00)
payment_type: 2, # Payment type (1-7, 2 is default)
buyer_id: "buyer-123",
seller_id: "seller-456"
)
if response.success?
item = response.data
puts "Item created: #{item['id']}"
puts "Name: #{item['name']}"
puts "Amount: #{item['amount']}"
else
puts "Error: #{response.error_message}"
endresponse = items.create(
name: "Premium Product",
amount: 25000,
payment_type: 2,
buyer_id: "buyer-123",
seller_id: "seller-456",
description: "Purchase of premium product XYZ",
currency: "AUD",
fee_ids: ["fee-1", "fee-2"],
custom_descriptor: "MY STORE PURCHASE",
buyer_url: "https://buyer.example.com",
seller_url: "https://seller.example.com",
tax_invoice: true
)
if response.success?
item = response.data
puts "Item created with ID: #{item['id']}"
puts "Description: #{item['description']}"
puts "Tax Invoice: #{item['tax_invoice']}"
endresponse = items.create(
id: "my-custom-item-#{Time.now.to_i}",
name: "Custom ID Product",
amount: 15000,
payment_type: 2,
buyer_id: "buyer-123",
seller_id: "seller-456"
)
if response.success?
item = response.data
puts "Item created with custom ID: #{item['id']}"
endRetrieve a list of all items with pagination and optional search/filtering.
# List items with default pagination (10 items)
response = items.list
if response.success?
items_list = response.data
items_list.each do |item|
puts "Item ID: #{item['id']}, Name: #{item['name']}, Amount: #{item['amount']}"
end
# Access metadata
meta = response.meta
puts "Total items: #{meta['total']}"
puts "Limit: #{meta['limit']}"
puts "Offset: #{meta['offset']}"
end# List 20 items starting from offset 40
response = items.list(limit: 20, offset: 40)
if response.success?
items_list = response.data
puts "Retrieved #{items_list.length} items"
end# Search for items with "product" in the description
response = items.list(search: "product")
if response.success?
items_list = response.data
puts "Found #{items_list.length} items matching 'product'"
items_list.each do |item|
puts " - #{item['name']}: #{item['description']}"
end
end# Get items created in a specific date range
response = items.list(
created_after: "2024-01-01T00:00:00Z",
created_before: "2024-12-31T23:59:59Z"
)
if response.success?
items_list = response.data
puts "Found #{items_list.length} items created in 2024"
end# Search with pagination and date filters
response = items.list(
limit: 50,
offset: 0,
search: "premium",
created_after: "2024-01-01T00:00:00Z"
)
if response.success?
items_list = response.data
puts "Found #{items_list.length} premium items created after Jan 1, 2024"
endGet details of a specific item by ID.
response = items.show("item-123")
if response.success?
item = response.data
puts "Item ID: #{item['id']}"
puts "Name: #{item['name']}"
puts "Amount: #{item['amount']}"
puts "Payment Type: #{item['payment_type']}"
puts "Buyer ID: #{item['buyer_id']}"
puts "Seller ID: #{item['seller_id']}"
puts "Description: #{item['description']}"
puts "State: #{item['state']}"
puts "Buyer URL: #{item['buyer_url']}" if item['buyer_url']
puts "Seller URL: #{item['seller_url']}" if item['seller_url']
puts "Tax Invoice: #{item['tax_invoice']}" unless item['tax_invoice'].nil?
else
puts "Error: #{response.error_message}"
endUpdate an existing item's details.
response = items.update(
"item-123",
name: "Updated Product Name",
description: "Updated product description",
amount: 12000,
buyer_url: "https://new-buyer.example.com",
tax_invoice: false
)
if response.success?
item = response.data
puts "Item updated: #{item['id']}"
puts "New name: #{item['name']}"
puts "New amount: #{item['amount']}"
puts "Tax Invoice: #{item['tax_invoice']}"
else
puts "Error: #{response.error_message}"
endresponse = items.update(
"item-123",
seller_id: "new-seller-789",
buyer_id: "new-buyer-012"
)
if response.success?
puts "Item updated with new buyer and seller"
endDelete an item by ID.
response = items.delete("item-123")
if response.success?
puts "Item deleted successfully"
else
puts "Error: #{response.error_message}"
endGet the seller (user) details for a specific item.
response = items.show_seller("item-123")
if response.success?
seller = response.data
puts "Seller ID: #{seller['id']}"
puts "Seller Email: #{seller['email']}"
puts "Seller Name: #{seller['first_name']} #{seller['last_name']}"
puts "Country: #{seller['country']}"
else
puts "Error: #{response.error_message}"
endGet the buyer (user) details for a specific item.
response = items.show_buyer("item-123")
if response.success?
buyer = response.data
puts "Buyer ID: #{buyer['id']}"
puts "Buyer Email: #{buyer['email']}"
puts "Buyer Name: #{buyer['first_name']} #{buyer['last_name']}"
puts "Country: #{buyer['country']}"
else
puts "Error: #{response.error_message}"
endGet the fees associated with an item.
response = items.show_fees("item-123")
if response.success?
fees = response.data
if fees && fees.any?
puts "Item has #{fees.length} fee(s):"
fees.each do |fee|
puts " Fee ID: #{fee['id']}"
puts " Name: #{fee['name']}"
puts " Amount: #{fee['amount']}"
puts " Fee Type: #{fee['fee_type']}"
end
else
puts "No fees associated with this item"
end
else
puts "Error: #{response.error_message}"
endGet wire transfer details for an item.
response = items.show_wire_details("item-123")
if response.success?
wire_details = response.data['wire_details']
if wire_details
puts "Wire Transfer Details:"
puts " Account Number: #{wire_details['account_number']}"
puts " Routing Number: #{wire_details['routing_number']}"
puts " Bank Name: #{wire_details['bank_name']}"
puts " Swift Code: #{wire_details['swift_code']}"
else
puts "No wire details available for this item"
end
else
puts "Error: #{response.error_message}"
endGet all transactions associated with an item.
# List transactions with default pagination
response = items.list_transactions("item-123")
if response.success?
transactions = response.data
if transactions && transactions.any?
puts "Item has #{transactions.length} transaction(s):"
transactions.each do |transaction|
puts " Transaction ID: #{transaction['id']}"
puts " Amount: #{transaction['amount']}"
puts " State: #{transaction['state']}"
puts " Type: #{transaction['type']}"
puts " Created At: #{transaction['created_at']}"
end
else
puts "No transactions found for this item"
end
else
puts "Error: #{response.error_message}"
end# List 50 transactions starting from offset 100
response = items.list_transactions("item-123", limit: 50, offset: 100)
if response.success?
transactions = response.data
puts "Retrieved #{transactions.length} transactions"
endGet all batch transactions associated with an item.
response = items.list_batch_transactions("item-123")
if response.success?
batch_transactions = response.data
if batch_transactions && batch_transactions.any?
puts "Item has #{batch_transactions.length} batch transaction(s):"
batch_transactions.each do |batch|
puts " Batch ID: #{batch['id']}"
puts " Amount: #{batch['amount']}"
puts " State: #{batch['state']}"
puts " Created At: #{batch['created_at']}"
end
else
puts "No batch transactions found for this item"
end
else
puts "Error: #{response.error_message}"
end# List 25 batch transactions starting from offset 50
response = items.list_batch_transactions("item-123", limit: 25, offset: 50)
if response.success?
batch_transactions = response.data
puts "Retrieved #{batch_transactions.length} batch transactions"
endGet the current status of an item.
response = items.show_status("item-123")
if response.success?
item_status = response.data
puts "Item ID: #{item_status['id']}"
puts "State: #{item_status['state']}"
puts "Payment State: #{item_status['payment_state']}"
puts "Disbursement State: #{item_status['disbursement_state']}" if item_status['disbursement_state']
puts "Status Description: #{item_status['status_description']}" if item_status['status_description']
else
puts "Error: #{response.error_message}"
endProcess a payment for an item using a card account. This method charges the buyer's card and initiates the payment flow.
# Make a payment with just the required parameters
response = items.make_payment(
"item-123",
account_id: "card_account-456" # Required
)
if response.success?
item = response.data
puts "Payment initiated for item: #{item['id']}"
puts "State: #{item['state']}"
puts "Payment State: #{item['payment_state']}"
else
puts "Payment failed: #{response.error_message}"
endFor enhanced fraud protection, include device and IP address information:
response = items.make_payment(
"item-123",
account_id: "card_account-456", # Required
device_id: "device_789",
ip_address: request.remote_ip # In a Rails controller
)
if response.success?
puts "Payment processed with device tracking"
endSome card payments may require CVV verification:
response = items.make_payment(
"item-123",
account_id: "card_account-456", # Required
cvv: "123" # CVV from secure form
)
if response.success?
puts "Payment processed with CVV verification"
endMaximum fraud protection with all available parameters:
response = items.make_payment(
"item-123",
account_id: "card_account-456", # Required
device_id: "device_789",
ip_address: "192.168.1.1",
cvv: "123",
merchant_phone: "+61412345678"
)
if response.success?
item = response.data
puts "Payment initiated successfully"
puts "Item State: #{item['state']}"
puts "Payment State: #{item['payment_state']}"
puts "Amount: #{item['amount']}"
else
puts "Payment failed: #{response.error_message}"
endbegin
response = items.make_payment(
"item-123",
account_id: "card_account-456"
)
if response.success?
puts "Payment successful"
else
# Handle API errors
case response.status
when 422
puts "Validation error: #{response.error_message}"
# Common: Insufficient funds, card declined, etc.
when 404
puts "Item or card account not found"
when 401
puts "Authentication failed"
else
puts "Payment error: #{response.error_message}"
end
end
rescue ZaiPayment::Errors::ValidationError => e
puts "Validation error: #{e.message}"
# Example: "account_id is required and cannot be blank"
rescue ZaiPayment::Errors::NotFoundError => e
puts "Resource not found: #{e.message}"
rescue ZaiPayment::Errors::ApiError => e
puts "API error: #{e.message}"
endComplete example showing item creation through payment:
require 'zai_payment'
# Configure
ZaiPayment.configure do |config|
config.client_id = ENV['ZAI_CLIENT_ID']
config.client_secret = ENV['ZAI_CLIENT_SECRET']
config.scope = ENV['ZAI_SCOPE']
config.environment = :prelive
end
items = ZaiPayment.items
# Step 1: Create an item
create_response = items.create(
name: "Product Purchase",
amount: 10000, # $100.00
payment_type: 2,
buyer_id: "buyer-123",
seller_id: "seller-456",
description: "Purchase of premium widget"
)
if create_response.success?
item_id = create_response.data['id']
puts "✓ Item created: #{item_id}"
# Step 2: Make the payment
payment_response = items.make_payment(
item_id,
account_id: "card_account-789", # Buyer's card account (Required)
ip_address: "192.168.1.1",
device_id: "device_abc123"
)
if payment_response.success?
puts "✓ Payment initiated"
puts " State: #{payment_response.data['state']}"
puts " Payment State: #{payment_response.data['payment_state']}"
# Step 3: Check payment status
sleep 2 # Wait for processing
status_response = items.show_status(item_id)
if status_response.success?
status = status_response.data
puts "✓ Current status:"
puts " State: #{status['state']}"
puts " Payment State: #{status['payment_state']}"
end
else
puts "✗ Payment failed: #{payment_response.error_message}"
end
else
puts "✗ Item creation failed: #{create_response.error_message}"
endAfter calling make_payment, the item will go through several states:
| State | Description |
|---|---|
payment_pending |
Payment has been initiated |
payment_processing |
Card is being charged |
completed |
Payment successful, funds held in escrow |
payment_held |
Payment succeeded but held for review |
payment_failed |
Payment failed (card declined, insufficient funds, etc.) |
After making a payment, listen for webhook events to track the payment status:
# In your webhook handler
def handle_transaction_webhook(payload)
if payload['type'] == 'payment' && payload['status'] == 'successful'
item_id = payload['related_items'].first
puts "Payment successful for item: #{item_id}"
# Update your database
Order.find_by(zai_item_id: item_id).update(status: 'paid')
elsif payload['status'] == 'failed'
puts "Payment failed: #{payload['failure_reason']}"
end
endAuthorize 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.
# Authorize a payment with required parameters
response = items.authorize_payment(
"item-123",
account_id: "card_account-456" # Required
)
if response.success?
item = response.data
puts "Payment authorized for item: #{item['id']}"
puts "State: #{item['state']}"
puts "Payment State: #{item['payment_state']}"
else
puts "Authorization failed: #{response.error_message}"
endFor additional security, include CVV verification:
response = items.authorize_payment(
"item-123",
account_id: "card_account-456", # Required
cvv: "123" # CVV from secure form
)
if response.success?
puts "Payment authorized with CVV verification"
endresponse = items.authorize_payment(
"item-123",
account_id: "card_account-456", # Required
cvv: "123",
merchant_phone: "+61412345678"
)
if response.success?
item = response.data
puts "Payment authorized successfully"
puts "Item State: #{item['state']}"
puts "Payment State: #{item['payment_state']}"
puts "Amount: #{item['amount']}"
else
puts "Authorization failed: #{response.error_message}"
endbegin
response = items.authorize_payment(
"item-123",
account_id: "card_account-456"
)
if response.success?
puts "Authorization successful"
else
# Handle API errors
case response.status
when 422
puts "Validation error: #{response.error_message}"
# Common: Invalid card, card declined, etc.
when 404
puts "Item or card account not found"
when 401
puts "Authentication failed"
else
puts "Authorization error: #{response.error_message}"
end
end
rescue ZaiPayment::Errors::ValidationError => e
puts "Validation error: #{e.message}"
# Example: "account_id is required and cannot be blank"
rescue ZaiPayment::Errors::NotFoundError => e
puts "Resource not found: #{e.message}"
rescue ZaiPayment::Errors::ApiError => e
puts "API error: #{e.message}"
endComplete example showing item creation through authorization:
require 'zai_payment'
# Configure
ZaiPayment.configure do |config|
config.client_id = ENV['ZAI_CLIENT_ID']
config.client_secret = ENV['ZAI_CLIENT_SECRET']
config.scope = ENV['ZAI_SCOPE']
config.environment = :prelive
end
items = ZaiPayment.items
# Step 1: Create an item
create_response = items.create(
name: "Hotel Reservation",
amount: 50000, # $500.00
payment_type: 2,
buyer_id: "buyer-123",
seller_id: "seller-456",
description: "Hotel booking - Hold authorization"
)
if create_response.success?
item_id = create_response.data['id']
puts "✓ Item created: #{item_id}"
# Step 2: Authorize the payment (hold funds without capturing)
auth_response = items.authorize_payment(
item_id,
account_id: "card_account-789",
cvv: "123",
merchant_phone: "+61412345678"
)
if auth_response.success?
puts "✓ Payment authorized (funds on hold)"
puts " State: #{auth_response.data['state']}"
puts " Payment State: #{auth_response.data['payment_state']}"
# Step 3: Check authorization status
status_response = items.show_status(item_id)
if status_response.success?
status = status_response.data
puts "✓ Current status:"
puts " State: #{status['state']}"
puts " Payment State: #{status['payment_state']}"
end
# Note: Funds are now held. You would later either:
# - Capture the payment (via make_payment or complete the item)
# - Cancel the authorization (via cancel)
else
puts "✗ Authorization failed: #{auth_response.error_message}"
end
else
puts "✗ Item creation failed: #{create_response.error_message}"
endAfter calling authorize_payment, the item will go through several states:
| State | Description |
|---|---|
payment_authorized |
Payment has been authorized, funds are on hold |
payment_held |
Payment authorized but held for review |
authorization_failed |
Authorization failed (card declined, insufficient funds, etc.) |
Important Notes:
- Authorized funds are typically held for 7 days before being automatically released
- To complete the transaction, you need to capture the payment separately
- You can cancel an authorization to release the held funds immediately
- Not all payment processors support separate authorization and capture
After authorizing a payment, listen for webhook events:
# In your webhook handler
def handle_authorization_webhook(payload)
if payload['type'] == 'authorization' && payload['status'] == 'successful'
item_id = payload['related_items'].first
puts "Payment authorized for item: #{item_id}"
# Update your database
Order.find_by(zai_item_id: item_id).update(status: 'authorized')
elsif payload['status'] == 'failed'
puts "Authorization failed: #{payload['failure_reason']}"
end
endHere's a complete example of creating an item and performing various operations on it:
require 'zai_payment'
# Configure
ZaiPayment.configure do |config|
config.client_id = ENV['ZAI_CLIENT_ID']
config.client_secret = ENV['ZAI_CLIENT_SECRET']
config.scope = ENV['ZAI_SCOPE']
config.environment = :prelive
end
items = ZaiPayment.items
# 1. Create an item
create_response = items.create(
name: "E-commerce Purchase",
amount: 50000, # $500.00
payment_type: 2,
buyer_id: "buyer-abc123",
seller_id: "seller-xyz789",
description: "Online store purchase - Order #12345",
currency: "AUD",
buyer_url: "https://buyer-portal.example.com",
seller_url: "https://seller-portal.example.com",
tax_invoice: true
)
if create_response.success?
item_id = create_response.data['id']
puts "✓ Item created: #{item_id}"
# 2. Get item details
show_response = items.show(item_id)
if show_response.success?
puts "✓ Item retrieved: #{show_response.data['name']}"
end
# 3. Get seller details
seller_response = items.show_seller(item_id)
if seller_response.success?
seller = seller_response.data
puts "✓ Seller: #{seller['email']}"
end
# 4. Get buyer details
buyer_response = items.show_buyer(item_id)
if buyer_response.success?
buyer = buyer_response.data
puts "✓ Buyer: #{buyer['email']}"
end
# 5. Check item status
status_response = items.show_status(item_id)
if status_response.success?
status = status_response.data
puts "✓ Item status: #{status['state']}"
end
# 6. List transactions
transactions_response = items.list_transactions(item_id)
if transactions_response.success?
txn_count = transactions_response.data&.length || 0
puts "✓ Item has #{txn_count} transaction(s)"
end
# 7. Update item if needed
update_response = items.update(
item_id,
description: "Updated: Online store purchase - Order #12345 (Confirmed)",
tax_invoice: true
)
if update_response.success?
puts "✓ Item updated successfully"
end
else
puts "✗ Error creating item: #{create_response.error_message}"
endAlways check the response status and handle errors appropriately:
response = items.show("item-123")
if response.success?
# Handle successful response
item = response.data
puts "Item: #{item['name']}"
else
# Handle error
case response.status
when 404
puts "Item not found"
when 401
puts "Authentication failed - check your credentials"
when 422
puts "Validation error: #{response.error_message}"
else
puts "Error: #{response.error_message}"
end
endCapture 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.
Capture the full authorized amount:
response = items.capture_payment("item-123")
if response.success?
item = response.data
puts "Payment captured successfully"
puts "Item ID: #{item['id']}"
puts "Amount: $#{item['amount'] / 100.0}"
puts "State: #{item['state']}"
puts "Payment State: #{item['payment_state']}"
else
puts "Capture failed: #{response.error_message}"
endCapture only a portion of the authorized amount:
# Capture $50 of a $100 authorization
response = items.capture_payment("item-123", amount: 5000)
if response.success?
item = response.data
puts "Partial payment captured: $#{item['amount'] / 100.0}"
puts "State: #{item['state']}"
puts "Payment State: #{item['payment_state']}"
else
puts "Capture failed: #{response.error_message}"
endbegin
response = items.capture_payment("item-123", amount: 10_000)
if response.success?
item = response.data
puts "✓ Payment captured: #{item['id']}"
puts " Amount: $#{item['amount'] / 100.0}"
puts " State: #{item['state']}"
puts " Payment State: #{item['payment_state']}"
else
# Handle API errors
case response.status
when 422
puts "Cannot capture: #{response.error_message}"
# Common: Payment not authorized, authorization expired, or invalid amount
when 404
puts "Item not found"
when 401
puts "Authentication failed"
else
puts "Capture error: #{response.error_message}"
end
end
rescue ZaiPayment::Errors::ValidationError => e
puts "Validation error: #{e.message}"
rescue ZaiPayment::Errors::NotFoundError => e
puts "Item not found: #{e.message}"
rescue ZaiPayment::Errors::ApiError => e
puts "API error: #{e.message}"
endCheck authorization status before attempting to capture:
# Check current status
status_response = items.show_status("item-123")
if status_response.success?
status = status_response.data
payment_state = status['payment_state']
puts "Current payment state: #{payment_state}"
# Only capture if payment is authorized
if payment_state == 'authorized' || payment_state == 'payment_authorized'
capture_response = items.capture_payment("item-123")
if capture_response.success?
puts "✓ Payment captured successfully"
else
puts "✗ Capture failed: #{capture_response.error_message}"
end
else
puts "Payment cannot be captured - current state: #{payment_state}"
end
endFull example demonstrating the two-step payment process:
# Step 1: Authorize the payment
puts "Step 1: Authorizing payment..."
auth_response = items.authorize_payment(
"item-123",
account_id: "card_account-456",
cvv: "123"
)
if auth_response.success?
puts "✓ Payment authorized"
auth_data = auth_response.data
puts " Item ID: #{auth_data['id']}"
puts " Amount: $#{auth_data['amount'] / 100.0}"
puts " State: #{auth_data['state']}"
# Step 2: Verify authorization status
puts "\nStep 2: Verifying authorization..."
status_response = items.show_status("item-123")
if status_response.success?
status = status_response.data
puts "✓ Status verified"
puts " Payment State: #{status['payment_state']}"
# Step 3: Capture the payment (can be done immediately or later)
puts "\nStep 3: Capturing payment..."
# Wait a moment (optional - simulate real-world delay)
sleep 1
capture_response = items.capture_payment("item-123")
if capture_response.success?
capture_data = capture_response.data
puts "✓ Payment captured successfully"
puts " Item ID: #{capture_data['id']}"
puts " Final State: #{capture_data['state']}"
puts " Final Payment State: #{capture_data['payment_state']}"
else
puts "✗ Capture failed: #{capture_response.error_message}"
end
else
puts "✗ Status check failed: #{status_response.error_message}"
end
else
puts "✗ Authorization failed: #{auth_response.error_message}"
endImplement retry logic for transient errors:
def capture_payment_with_retry(item_id, amount: nil, max_retries: 3)
retries = 0
begin
response = items.capture_payment(item_id, amount: amount)
if response.success?
puts "✓ Payment captured successfully"
return response
elsif response.status == 422
# Validation error - don't retry
puts "✗ Capture failed: #{response.error_message}"
return response
else
# Other errors - maybe retry
raise "Capture error: #{response.error_message}"
end
rescue => e
retries += 1
if retries < max_retries
puts "⚠ Capture attempt #{retries} failed: #{e.message}"
puts " Retrying in #{retries * 2} seconds..."
sleep(retries * 2)
retry
else
puts "✗ Capture failed after #{max_retries} attempts"
raise e
end
end
end
# Usage
begin
response = capture_payment_with_retry("item-123", amount: 10_000)
puts "Final state: #{response.data['payment_state']}" if response.success?
rescue => e
puts "Capture ultimately failed: #{e.message}"
endA complete example showing how to handle the authorize and capture workflow in a real application:
class PaymentProcessor
def initialize
@items = ZaiPayment.items
end
def process_two_step_payment(order)
# Step 1: Authorize
puts "Processing order ##{order[:id]} - Amount: $#{order[:amount] / 100.0}"
auth_response = authorize_payment(order)
return { success: false, error: "Authorization failed" } unless auth_response
# Step 2: Perform additional checks (inventory, fraud, etc.)
return { success: false, error: "Fraud check failed" } unless verify_order(order)
# Step 3: Capture
capture_response = capture_payment(order[:item_id], order[:capture_amount])
return { success: false, error: "Capture failed" } unless capture_response
# Step 4: Update order and notify
finalize_order(order, capture_response)
{ success: true, data: capture_response.data }
end
private
def authorize_payment(order)
puts "→ Authorizing payment..."
response = @items.authorize_payment(
order[:item_id],
account_id: order[:account_id],
cvv: order[:cvv]
)
if response.success?
puts "✓ Payment authorized: #{order[:item_id]}"
response
else
puts "✗ Authorization failed: #{response.error_message}"
nil
end
end
def verify_order(order)
puts "→ Verifying order..."
# Check inventory
return false unless check_inventory(order[:items])
# Check fraud score
return false unless check_fraud_score(order[:buyer_id])
puts "✓ Order verified"
true
end
def capture_payment(item_id, amount = nil)
puts "→ Capturing payment..."
response = @items.capture_payment(item_id, amount: amount)
if response.success?
puts "✓ Payment captured: #{item_id}"
response
else
puts "✗ Capture failed: #{response.error_message}"
nil
end
end
def finalize_order(order, capture_response)
puts "→ Finalizing order..."
# Update order status in database
# update_order_status(order[:id], 'paid')
# Send confirmation email
# send_confirmation_email(order[:buyer_email])
# Update inventory
# reduce_inventory(order[:items])
puts "✓ Order finalized: ##{order[:id]}"
end
def check_inventory(items)
# Implement inventory check
true
end
def check_fraud_score(buyer_id)
# Implement fraud check
true
end
end
# Usage
processor = PaymentProcessor.new
order = {
id: 12345,
item_id: "item-abc123",
account_id: "card_account-456",
cvv: "123",
amount: 10_000,
capture_amount: 10_000, # Can be less for partial capture
buyer_id: "buyer-789",
buyer_email: "customer@example.com",
items: ["product-1", "product-2"]
}
result = processor.process_two_step_payment(order)
if result[:success]
puts "\n✓ Payment completed successfully!"
puts " Item: #{result[:data]['id']}"
puts " State: #{result[:data]['payment_state']}"
else
puts "\n✗ Payment failed: #{result[:error]}"
endPayments can be captured when in these states:
| State | Can Capture? | Description |
|---|---|---|
authorized |
✓ Yes | Payment authorized and ready to capture |
payment_authorized |
✓ Yes | Payment authorized and ready to capture |
pending |
✗ No | Payment not authorized yet |
payment_pending |
✗ No | Payment processing, not authorized |
completed |
✗ No | Already captured |
payment_deposited |
✗ No | Already captured and deposited |
cancelled |
✗ No | Authorization cancelled |
refunded |
✗ No | Payment refunded |
- Timely Captures: Capture authorized payments within 7 days (typical authorization expiration)
- Status Verification: Always check payment state before attempting capture
- Partial Captures: Use for order adjustments or split fulfillment
- Error Handling: Implement robust error handling and retry logic
- Logging: Log all authorization and capture attempts for audit trails
- Notifications: Notify customers when payment is captured
- Timeouts: Set appropriate timeout values for capture requests
| Error | Cause | Solution |
|---|---|---|
| "Payment not authorized" | Trying to capture non-authorized payment | Authorize first, then capture |
| "Authorization expired" | Authorization older than 7 days | Create new item and authorize again |
| "Amount exceeds authorized amount" | Capture amount > authorized amount | Reduce capture amount or re-authorize |
| "Item not found" | Invalid item ID | Verify item ID is correct |
| "Payment already captured" | Duplicate capture attempt | Check payment state before capture |
class PaymentsController < ApplicationController
def capture
@order = Order.find(params[:order_id])
# Verify order can be captured
unless @order.payment_state == 'authorized'
flash[:error] = "Payment not authorized"
redirect_to @order and return
end
# Capture the payment
response = ZaiPayment.items.capture_payment(
@order.zai_item_id,
amount: params[:amount] # Optional for partial capture
)
if response.success?
@order.update(
payment_state: response.data['payment_state'],
captured_at: Time.current
)
flash[:success] = "Payment captured successfully"
redirect_to @order
else
flash[:error] = "Capture failed: #{response.error_message}"
render :show
end
end
endclass CapturePaymentJob < ApplicationJob
queue_as :payments
def perform(order_id)
order = Order.find(order_id)
# Check if order is ready to capture
return unless order.ready_to_capture?
# Capture the payment
response = ZaiPayment.items.capture_payment(order.zai_item_id)
if response.success?
order.update(
payment_state: 'captured',
captured_at: Time.current
)
# Send confirmation email
OrderMailer.payment_captured(order).deliver_later
# Update inventory
order.reduce_inventory!
else
# Log error and retry or alert
Rails.logger.error("Capture failed for order #{order_id}: #{response.error_message}")
# Retry if appropriate
raise "Capture failed" if response.status >= 500
end
end
endInitiate 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.
This method is specifically designed for payments that require 3D Secure verification, providing enhanced security for card transactions.
# Make an async payment with just the required parameters
response = items.make_payment_async(
"item-123",
account_id: "card_account-456" # Required
)
if response.success?
payment_id = response.data['payment_id']
payment_token = response.data['payment_token']
item = response.data['items']
puts "Payment initiated: #{payment_id}"
puts "Payment token for 3DS2: #{payment_token}"
puts "Item state: #{item['state']}"
puts "Amount: $#{item['amount'] / 100.0}"
# Use the payment_token to initialise the 3DS2 web component
# on the client side (JavaScript)
else
puts "Payment failed: #{response.error_message}"
endTo explicitly request a 3D Secure challenge:
response = items.make_payment_async(
"item-123",
account_id: "card_account-456",
request_three_d_secure: "challenge"
)
if response.success?
payment_token = response.data['payment_token']
payment_id = response.data['payment_id']
puts "Payment initiated with 3DS challenge: #{payment_id}"
puts "Payment token: #{payment_token}"
# Send the payment_token to the client to initialise 3DS2 component
# The component will display the 3DS challenge to the user
else
puts "Payment initiation failed: #{response.error_message}"
endWhen using the default 'automatic' mode, the system determines whether 3DS is required:
response = items.make_payment_async(
"item-123",
account_id: "card_account-456",
request_three_d_secure: "automatic" # This is the default
)
if response.success?
item = response.data['items']
payment_token = response.data['payment_token']
puts "3DS handled automatically"
puts "Item state: #{item['state']}"
# The payment_token will be provided if 3DS is required
if payment_token && !payment_token.empty?
puts "3DS verification required - use token: #{payment_token}"
# Send token to client for 3DS2 component initialisation
else
puts "3DS verification not required - payment processed"
end
endComplete example showing how to implement async payment in a Rails application:
# app/controllers/payments_controller.rb
class PaymentsController < ApplicationController
def create_async_payment
items = ZaiPayment.items
# Step 1: Initiate async payment
response = items.make_payment_async(
params[:item_id],
account_id: params[:account_id],
request_three_d_secure: "automatic"
)
if response.success?
payment_id = response.data['payment_id']
payment_token = response.data['payment_token']
item_data = response.data['items']
# Store payment_id for tracking
@payment = Payment.create!(
zai_payment_id: payment_id,
zai_item_id: item_data['id'],
amount: item_data['amount'],
state: item_data['state'],
payment_token: payment_token
)
# Return payment_token to client for 3DS2 initialisation
render json: {
success: true,
payment_id: payment_id,
payment_token: payment_token,
requires_3ds: payment_token.present?
}
else
render json: {
success: false,
error: response.error_message
}, status: :unprocessable_entity
end
end
end// app/javascript/payments/three_d_secure.js
// Client-side 3DS2 component initialisation
async function initiateAsyncPayment(itemId, accountId) {
try {
// Call your Rails backend to initiate the payment
const response = await fetch('/payments/create_async', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
},
body: JSON.stringify({
item_id: itemId,
account_id: accountId
})
});
const data = await response.json();
if (data.success && data.requires_3ds) {
// Initialize 3DS2 component with the payment_token
await initialize3DS2Component(data.payment_token);
} else if (data.success) {
// Payment completed without 3DS
window.location.href = '/payments/success';
} else {
alert('Payment failed: ' + data.error);
}
} catch (error) {
console.error('Payment error:', error);
alert('Payment failed. Please try again.');
}
}
async function initialize3DS2Component(paymentToken) {
// Use Zai's 3DS2 SDK to initialize the component
// This is a simplified example - refer to Zai documentation for actual implementation
const threeDSComponent = new ZaiThreeDSecure({
paymentToken: paymentToken,
onSuccess: function(result) {
console.log('3DS verification successful', result);
window.location.href = '/payments/success';
},
onError: function(error) {
console.error('3DS verification failed', error);
alert('Payment verification failed. Please try again.');
}
});
threeDSComponent.mount('#three-ds-container');
}begin
response = items.make_payment_async(
"item-123",
account_id: "card_account-456"
)
if response.success?
payment_id = response.data['payment_id']
payment_token = response.data['payment_token']
puts "✓ Payment initiated: #{payment_id}"
if payment_token
puts " 3DS verification required"
puts " Token: #{payment_token}"
else
puts " 3DS not required, payment processing"
end
else
# Handle API errors
case response.status
when 422
puts "Validation error: #{response.error_message}"
# Common: Invalid account, insufficient funds
when 404
puts "Item or account not found"
when 401
puts "Authentication failed"
else
puts "Payment error: #{response.error_message}"
end
end
rescue ZaiPayment::Errors::ValidationError => e
puts "Validation error: #{e.message}"
# Example: "account_id is required and cannot be blank"
rescue ZaiPayment::Errors::NotFoundError => e
puts "Resource not found: #{e.message}"
rescue ZaiPayment::Errors::BadRequestError => e
puts "Bad request: #{e.message}"
rescue ZaiPayment::Errors::ApiError => e
puts "API error: #{e.message}"
endComplete example showing item creation through async payment with 3DS:
require 'zai_payment'
# Configure
ZaiPayment.configure do |config|
config.client_id = ENV['ZAI_CLIENT_ID']
config.client_secret = ENV['ZAI_CLIENT_SECRET']
config.scope = ENV['ZAI_SCOPE']
config.environment = :prelive
end
items = ZaiPayment.items
# Step 1: Create an item
create_response = items.create(
name: "Premium Product with 3DS",
amount: 25000, # $250.00
payment_type: 2,
buyer_id: "buyer-123",
seller_id: "seller-456",
description: "High-value product requiring 3DS verification"
)
if create_response.success?
item_id = create_response.data['id']
puts "✓ Item created: #{item_id}"
# Step 2: Initiate async payment with 3DS
payment_response = items.make_payment_async(
item_id,
account_id: "card_account-789",
request_three_d_secure: "automatic"
)
if payment_response.success?
payment_id = payment_response.data['payment_id']
payment_token = payment_response.data['payment_token']
item_data = payment_response.data['items']
puts "✓ Async payment initiated: #{payment_id}"
puts " Item state: #{item_data['state']}"
puts " Amount: $#{item_data['amount'] / 100.0}"
if payment_token && !payment_token.empty?
puts " 3DS verification required"
puts " Payment token: #{payment_token}"
puts " → Send this token to client for 3DS2 component"
# In a real application:
# 1. Send payment_token to the client
# 2. Client initializes 3DS2 component
# 3. User completes 3DS challenge
# 4. Listen for webhook to confirm payment status
else
puts " 3DS not required - payment processing automatically"
# Step 3: Monitor payment via webhook or polling
# In production, use webhooks for real-time updates
end
else
puts "✗ Payment failed: #{payment_response.error_message}"
end
else
puts "✗ Item creation failed: #{create_response.error_message}"
endThe make_payment_async response includes several important fields:
response = items.make_payment_async("item-123", account_id: "account-456")
if response.success?
# Top-level payment information
payment_id = response.data['payment_id'] # Unique payment identifier
account_id = response.data['account_id'] # Account used for payment
payment_token = response.data['payment_token'] # Token for 3DS2 initialization
# Item details
item = response.data['items']
item_id = item['id'] # Item/transaction ID
state = item['state'] # Current state (e.g., 'pending')
amount = item['amount'] # Amount in cents
currency = item['currency'] # Currency code (e.g., 'AUD')
payment_method = item['payment_method'] # Payment method used
# Related resources
related = item['related']
buyer_id = related['buyers'] # Buyer user ID
seller_id = related['sellers'] # Seller user ID
# Useful links
links = item['links']
status_url = links['status'] # URL to check status
transactions_url = links['transactions'] # URL for transactions
endThe request_three_d_secure parameter accepts three values:
| Value | Description | Use Case |
|---|---|---|
'automatic' |
System determines if 3DS is required (default) | Recommended for most cases - balances security and UX |
'challenge' |
Always request 3DS challenge | High-value transactions, compliance requirements |
'any' |
Request 3DS regardless of challenge flow | Maximum security, regulatory requirements |
# Automatic (recommended)
items.make_payment_async("item-123",
account_id: "account-456",
request_three_d_secure: "automatic"
)
# Force challenge for high-value transactions
items.make_payment_async("item-123",
account_id: "account-456",
request_three_d_secure: "challenge"
)
# Maximum security
items.make_payment_async("item-123",
account_id: "account-456",
request_three_d_secure: "any"
)- Payment Token: The
payment_tokenmust be sent to the client to initialize the 3DS2 web component - Webhooks: After 3DS authentication completes, listen for webhooks to get the final payment status
- Timeout: 3DS challenges typically expire after 10-15 minutes of inactivity
- Failed Authentication: If 3DS verification fails, the payment will be automatically cancelled
- Testing: Use Zai test cards to simulate different 3DS scenarios in prelive environment
After initiating an async payment, monitor webhooks for status updates:
# In your webhook handler (e.g., Rails controller)
def handle_payment_webhook
# Verify webhook signature first
webhooks = ZaiPayment.webhooks
begin
if webhooks.verify_signature(request.body.read, request.headers['X-Signature'])
payload = JSON.parse(request.body.read)
case payload['type']
when 'transaction'
if payload['status'] == 'successful'
payment_id = payload['payment_id']
item_id = payload['item_id']
# Update your database
payment = Payment.find_by(zai_payment_id: payment_id)
payment.update!(status: 'completed', completed_at: Time.current)
# Send confirmation email, fulfill order, etc.
OrderFulfillmentJob.perform_later(payment.order_id)
puts "✓ Payment completed: #{payment_id}"
elsif payload['status'] == 'failed'
# Handle failed payment
payment = Payment.find_by(zai_payment_id: payload['payment_id'])
payment.update!(status: 'failed', failure_reason: payload['message'])
# Notify customer
PaymentFailureMailer.notify(payment.user).deliver_later
end
end
head :ok
else
head :unauthorized
end
rescue => e
Rails.logger.error "Webhook error: #{e.message}"
head :internal_server_error
end
endCancel an existing item/payment. This operation is typically used to cancel a pending payment before it has been processed or completed.
response = items.cancel("item-123")
if response.success?
item = response.data
puts "Item cancelled successfully"
puts "Item ID: #{item['id']}"
puts "State: #{item['state']}"
puts "Payment State: #{item['payment_state']}"
else
puts "Cancel failed: #{response.error_message}"
endbegin
response = items.cancel("item-123")
if response.success?
item = response.data
puts "✓ Item cancelled: #{item['id']}"
puts " State: #{item['state']}"
puts " Payment State: #{item['payment_state']}"
else
# Handle API errors
case response.status
when 422
puts "Cannot cancel: #{response.error_message}"
# Common: Item already completed or in a state that can't be cancelled
when 404
puts "Item not found"
when 401
puts "Authentication failed"
else
puts "Cancellation error: #{response.error_message}"
end
end
rescue ZaiPayment::Errors::ValidationError => e
puts "Validation error: #{e.message}"
rescue ZaiPayment::Errors::NotFoundError => e
puts "Item not found: #{e.message}"
rescue ZaiPayment::Errors::ApiError => e
puts "API error: #{e.message}"
endCheck item status before attempting to cancel:
# Check current status
status_response = items.show_status("item-123")
if status_response.success?
status = status_response.data
current_state = status['state']
payment_state = status['payment_state']
puts "Current state: #{current_state}"
puts "Payment state: #{payment_state}"
# Only cancel if in a cancellable state
if ['pending', 'payment_pending'].include?(current_state)
cancel_response = items.cancel("item-123")
if cancel_response.success?
puts "✓ Item cancelled successfully"
else
puts "✗ Cancel failed: #{cancel_response.error_message}"
end
else
puts "Item cannot be cancelled - current state: #{current_state}"
end
endComplete example showing item creation, payment, and cancellation:
require 'zai_payment'
# Configure
ZaiPayment.configure do |config|
config.client_id = ENV['ZAI_CLIENT_ID']
config.client_secret = ENV['ZAI_CLIENT_SECRET']
config.scope = ENV['ZAI_SCOPE']
config.environment = :prelive
end
items = ZaiPayment.items
# Step 1: Create an item
create_response = items.create(
name: "Product Purchase",
amount: 10000, # $100.00
payment_type: 2,
buyer_id: "buyer-123",
seller_id: "seller-456",
description: "Purchase of premium widget"
)
if create_response.success?
item_id = create_response.data['id']
puts "✓ Item created: #{item_id}"
# Step 2: Customer decides to cancel before payment
puts "\nCustomer requested cancellation..."
# Step 3: Check if item can be cancelled
status_response = items.show_status(item_id)
if status_response.success?
current_state = status_response.data['state']
puts "Current item state: #{current_state}"
# Step 4: Cancel the item
if ['pending', 'payment_pending'].include?(current_state)
cancel_response = items.cancel(item_id)
if cancel_response.success?
cancelled_item = cancel_response.data
puts "✓ Item cancelled successfully"
puts " Final state: #{cancelled_item['state']}"
puts " Payment state: #{cancelled_item['payment_state']}"
# Notify customer
# CustomerMailer.order_cancelled(customer_email, item_id).deliver_later
else
puts "✗ Cancellation failed: #{cancel_response.error_message}"
end
else
puts "✗ Item cannot be cancelled - current state: #{current_state}"
end
end
else
puts "✗ Item creation failed: #{create_response.error_message}"
endItems can typically be cancelled when in these states:
| State | Can Cancel? | Description |
|---|---|---|
pending |
✓ Yes | Item created but no payment initiated |
payment_pending |
✓ Yes | Payment initiated but not yet processed |
payment_processing |
Maybe | Depends on payment processor |
completed |
✗ No | Payment completed, must refund instead |
payment_held |
Maybe | May require admin approval |
cancelled |
✗ No | Already cancelled |
refunded |
✗ No | Already refunded |
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.
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.
response = items.refund("item-123")
if response.success?
item = response.data
puts "Item refunded successfully"
puts "Item ID: #{item['id']}"
puts "State: #{item['state']}"
puts "Payment State: #{item['payment_state']}"
else
puts "Refund failed: #{response.error_message}"
endProcess a partial refund for a specific amount:
# Refund $50.00 out of the original $100.00 transaction
response = items.refund(
"item-123",
refund_amount: 5000 # Amount in cents
)
if response.success?
item = response.data
puts "Partial refund processed"
puts "Refund Amount: $50.00"
puts "State: #{item['state']}"
else
puts "Partial refund failed: #{response.error_message}"
endProvide a reason for the refund:
response = items.refund(
"item-123",
refund_message: "Customer returned defective product"
)
if response.success?
puts "Refund processed with message"
else
puts "Refund failed: #{response.error_message}"
endSpecify which account to refund to:
response = items.refund(
"item-123",
account_id: "account_789"
)
if response.success?
puts "Refund sent to specified account"
else
puts "Refund failed: #{response.error_message}"
endProcess a partial refund with a message and specific account:
response = items.refund(
"item-123",
refund_amount: 5000,
refund_message: "Partial refund for shipping damage",
account_id: "account_789"
)
if response.success?
item = response.data
puts "✓ Partial refund processed"
puts " Amount: $50.00"
puts " Message: Partial refund for shipping damage"
puts " State: #{item['state']}"
puts " Payment State: #{item['payment_state']}"
else
puts "✗ Refund failed: #{response.error_message}"
endbegin
response = items.refund("item-123")
if response.success?
item = response.data
puts "✓ Item refunded: #{item['id']}"
puts " State: #{item['state']}"
puts " Payment State: #{item['payment_state']}"
else
# Handle API errors
case response.status
when 422
puts "Cannot refund: #{response.error_message}"
# Common: Item already refunded, not in refundable state, etc.
when 404
puts "Item not found"
when 401
puts "Authentication failed"
else
puts "Refund error: #{response.error_message}"
end
end
rescue ZaiPayment::Errors::ValidationError => e
puts "Validation error: #{e.message}"
rescue ZaiPayment::Errors::NotFoundError => e
puts "Item not found: #{e.message}"
rescue ZaiPayment::Errors::ApiError => e
puts "API error: #{e.message}"
endCheck item status before attempting to refund:
# Check current status
status_response = items.show_status("item-123")
if status_response.success?
status = status_response.data
current_state = status['state']
payment_state = status['payment_state']
puts "Current state: #{current_state}"
puts "Payment state: #{payment_state}"
# Only refund if in a refundable state
if ['completed', 'payment_deposited', 'work_completed'].include?(payment_state)
refund_response = items.refund("item-123")
if refund_response.success?
puts "✓ Item refunded successfully"
else
puts "✗ Refund failed: #{refund_response.error_message}"
end
else
puts "Item cannot be refunded - payment state: #{payment_state}"
end
endComplete example showing item creation, payment, and refund:
require 'zai_payment'
# Configure
ZaiPayment.configure do |config|
config.client_id = ENV['ZAI_CLIENT_ID']
config.client_secret = ENV['ZAI_CLIENT_SECRET']
config.scope = ENV['ZAI_SCOPE']
config.environment = :prelive
end
items = ZaiPayment.items
# Step 1: Create an item
create_response = items.create(
name: "Product Purchase",
amount: 10000, # $100.00
payment_type: 2,
buyer_id: "buyer-123",
seller_id: "seller-456",
description: "Purchase of premium widget"
)
if create_response.success?
item_id = create_response.data['id']
puts "✓ Item created: #{item_id}"
# Step 2: Make the payment
payment_response = items.make_payment(
item_id,
account_id: "card_account-789"
)
if payment_response.success?
puts "✓ Payment processed"
# Step 3: Customer requests refund
puts "\nCustomer requested refund..."
# Wait for payment to complete
sleep 2
# Step 4: Check if item can be refunded
status_response = items.show_status(item_id)
if status_response.success?
payment_state = status_response.data['payment_state']
puts "Current payment state: #{payment_state}"
# Step 5: Process the refund
if ['completed', 'payment_deposited'].include?(payment_state)
refund_response = items.refund(
item_id,
refund_message: "Customer return - changed mind"
)
if refund_response.success?
refunded_item = refund_response.data
puts "✓ Refund processed successfully"
puts " Final state: #{refunded_item['state']}"
puts " Payment state: #{refunded_item['payment_state']}"
# Notify customer
# CustomerMailer.refund_processed(customer_email, item_id).deliver_later
else
puts "✗ Refund failed: #{refund_response.error_message}"
end
else
puts "✗ Item cannot be refunded - payment state: #{payment_state}"
end
end
else
puts "✗ Payment failed: #{payment_response.error_message}"
end
else
puts "✗ Item creation failed: #{create_response.error_message}"
endItems can typically be refunded when in these states:
| State | Can Refund? | Description |
|---|---|---|
pending |
✗ No | Item not yet paid, cancel instead |
payment_pending |
✗ No | Payment not completed, cancel instead |
completed |
✓ Yes | Payment completed successfully |
payment_deposited |
✓ Yes | Payment received and deposited |
work_completed |
✓ Yes | Work completed, funds can be refunded |
cancelled |
✗ No | Already cancelled |
refunded |
✗ No | Already refunded |
payment_held |
Maybe | May require admin approval |
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.
class RefundsController < ApplicationController
def create
@order = Order.find(params[:order_id])
# Ensure order belongs to current user or is admin
unless @order.user == current_user || current_user.admin?
redirect_to root_path, alert: 'Unauthorized'
return
end
# Process refund in Zai
response = ZaiPayment.items.refund(
@order.zai_item_id,
refund_amount: params[:refund_amount]&.to_i,
refund_message: params[:refund_message]
)
if response.success?
@order.update(
status: 'refunded',
refunded_at: Time.current,
refund_amount: params[:refund_amount]&.to_i,
refund_reason: params[:refund_message]
)
redirect_to @order, notice: 'Refund processed successfully'
else
flash[:error] = "Cannot process refund: #{response.error_message}"
redirect_to @order
end
rescue ZaiPayment::Errors::ValidationError => e
flash[:error] = "Refund error: #{e.message}"
redirect_to @order
end
endclass RefundService
def initialize(order, refund_params = {})
@order = order
@refund_amount = refund_params[:amount]
@refund_message = refund_params[:message]
@account_id = refund_params[:account_id]
end
def process_refund
# Validate order is refundable
unless refundable?
return { success: false, error: 'Order cannot be refunded' }
end
# Validate refund amount
if @refund_amount && @refund_amount > @order.total_amount
return { success: false, error: 'Refund amount exceeds order total' }
end
# Process refund in Zai
response = ZaiPayment.items.refund(
@order.zai_item_id,
refund_amount: @refund_amount,
refund_message: @refund_message,
account_id: @account_id
)
if response.success?
# Update local database
@order.update(
status: @refund_amount == @order.total_amount ? 'refunded' : 'partially_refunded',
zai_state: response.data['state'],
zai_payment_state: response.data['payment_state'],
refunded_at: Time.current,
refund_amount: @refund_amount || @order.total_amount,
refund_reason: @refund_message
)
# Send notification
OrderMailer.refund_processed(@order).deliver_later
# Log the refund
@order.refund_logs.create(
amount: @refund_amount || @order.total_amount,
reason: @refund_message,
processed_at: Time.current
)
{ success: true, order: @order }
else
{ success: false, error: response.error_message }
end
rescue ZaiPayment::Errors::ApiError => e
{ success: false, error: e.message }
end
private
def refundable?
# Check local status
return false unless @order.status.in?(['completed', 'paid'])
# Check Zai status
status_response = ZaiPayment.items.show_status(@order.zai_item_id)
return false unless status_response.success?
payment_state = status_response.data['payment_state']
payment_state.in?(['completed', 'payment_deposited', 'work_completed'])
rescue
false
end
end
# Usage:
# service = RefundService.new(order, amount: 5000, message: 'Customer return')
# result = service.process_refund
# if result[:success]
# # Handle success
# else
# # Handle error: result[:error]
# endAfter processing a refund, you may receive webhook notifications:
# In your webhook handler
def handle_refund_webhook(payload)
if payload['type'] == 'refund' && payload['status'] == 'successful'
item_id = payload['related_items'].first
puts "Refund successful for item: #{item_id}"
# Update your database
Order.find_by(zai_item_id: item_id)&.update(
status: 'refunded',
zai_state: payload['state'],
refunded_at: Time.current,
refund_amount: payload['amount']
)
# Notify customer
order = Order.find_by(zai_item_id: item_id)
OrderMailer.refund_confirmed(order).deliver_later if order
elsif payload['status'] == 'failed'
puts "Refund failed: #{payload['failure_reason']}"
end
end# spec/services/refund_service_spec.rb
RSpec.describe RefundService do
let(:order) { create(:order, status: 'completed', zai_item_id: 'item-123', total_amount: 10000) }
let(:service) { described_class.new(order, amount: 5000, message: 'Customer return') }
describe '#process_refund' do
context 'when order can be refunded' do
before do
allow(ZaiPayment.items).to receive(:show_status).and_return(
double(success?: true, data: { 'payment_state' => 'completed' })
)
allow(ZaiPayment.items).to receive(:refund).and_return(
double(
success?: true,
data: { 'id' => 'item-123', 'state' => 'refunded', 'payment_state' => 'refunded' }
)
)
end
it 'successfully processes the refund' do
result = service.process_refund
expect(result[:success]).to be true
expect(order.reload.status).to eq('partially_refunded')
expect(order.refund_amount).to eq(5000)
expect(order.refunded_at).to be_present
end
it 'marks as fully refunded when refund amount equals total' do
service = described_class.new(order, amount: 10000)
result = service.process_refund
expect(order.reload.status).to eq('refunded')
end
end
context 'when order cannot be refunded' do
before do
order.update(status: 'pending')
end
it 'returns error' do
result = service.process_refund
expect(result[:success]).to be false
expect(result[:error]).to include('cannot be refunded')
end
end
context 'when refund amount exceeds order total' do
let(:service) { described_class.new(order, amount: 20000) }
it 'returns error' do
result = service.process_refund
expect(result[:success]).to be false
expect(result[:error]).to include('exceeds order total')
end
end
end
endclass OrdersController < ApplicationController
def cancel
@order = Order.find(params[:id])
# Ensure order belongs to current user
unless @order.user == current_user
redirect_to root_path, alert: 'Unauthorized'
return
end
# Cancel in Zai
response = ZaiPayment.items.cancel(@order.zai_item_id)
if response.success?
@order.update(
status: 'cancelled',
cancelled_at: Time.current
)
redirect_to @order, notice: 'Order cancelled successfully'
else
flash[:error] = "Cannot cancel order: #{response.error_message}"
redirect_to @order
end
rescue ZaiPayment::Errors::ValidationError => e
flash[:error] = "Cancellation error: #{e.message}"
redirect_to @order
end
endclass OrderCancellationService
def initialize(order)
@order = order
end
def cancel
# Check if order can be cancelled
unless cancellable?
return { success: false, error: 'Order cannot be cancelled' }
end
# Cancel in Zai
response = ZaiPayment.items.cancel(@order.zai_item_id)
if response.success?
# Update local database
@order.update(
status: 'cancelled',
zai_state: response.data['state'],
zai_payment_state: response.data['payment_state'],
cancelled_at: Time.current,
cancelled_by: @order.user_id
)
# Send notification
OrderMailer.order_cancelled(@order).deliver_later
# Refund any processing fees if applicable
process_fee_refund if @order.processing_fee.present?
{ success: true, order: @order }
else
{ success: false, error: response.error_message }
end
rescue ZaiPayment::Errors::ApiError => e
{ success: false, error: e.message }
end
private
def cancellable?
# Check local status
return false unless @order.status.in?(['pending', 'payment_pending'])
# Check Zai status
status_response = ZaiPayment.items.show_status(@order.zai_item_id)
return false unless status_response.success?
status_response.data['state'].in?(['pending', 'payment_pending'])
rescue
false
end
def process_fee_refund
# Custom logic for refunding processing fees
# ...
end
end
# Usage:
# service = OrderCancellationService.new(order)
# result = service.cancel
# if result[:success]
# # Handle success
# else
# # Handle error: result[:error]
# endAfter cancelling an item, you may receive webhook notifications:
# In your webhook handler
def handle_item_webhook(payload)
if payload['type'] == 'item' && payload['status'] == 'cancelled'
item_id = payload['id']
puts "Item cancelled: #{item_id}"
# Update your database
Order.find_by(zai_item_id: item_id)&.update(
status: 'cancelled',
zai_state: payload['state'],
cancelled_at: Time.current
)
# Notify customer
order = Order.find_by(zai_item_id: item_id)
OrderMailer.cancellation_confirmed(order).deliver_later if order
end
end# spec/services/order_cancellation_service_spec.rb
RSpec.describe OrderCancellationService do
let(:order) { create(:order, status: 'pending', zai_item_id: 'item-123') }
let(:service) { described_class.new(order) }
describe '#cancel' do
context 'when order can be cancelled' do
before do
allow(ZaiPayment.items).to receive(:show_status).and_return(
double(success?: true, data: { 'state' => 'pending' })
)
allow(ZaiPayment.items).to receive(:cancel).and_return(
double(
success?: true,
data: { 'id' => 'item-123', 'state' => 'cancelled', 'payment_state' => 'cancelled' }
)
)
end
it 'successfully cancels the order' do
result = service.cancel
expect(result[:success]).to be true
expect(order.reload.status).to eq('cancelled')
expect(order.cancelled_at).to be_present
end
end
context 'when order cannot be cancelled' do
before do
order.update(status: 'completed')
end
it 'returns error' do
result = service.cancel
expect(result[:success]).to be false
expect(result[:error]).to include('cannot be cancelled')
end
end
end
endWhen creating items, you can specify different payment types:
- 1: Direct Debit
- 2: Credit Card (default)
- 3: Bank Transfer
- 4: Wallet
- 5: BPay
- 6: PayPal
- 7: Other
Example:
# Create item with bank transfer payment type
response = items.create(
name: "Bank Transfer Payment",
amount: 30000,
payment_type: 3, # Bank Transfer
buyer_id: "buyer-123",
seller_id: "seller-456"
)For more information about the Zai Items API, see: