diff --git a/Gemfile.lock b/Gemfile.lock index 70913c9..022de5d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - zai_payment (2.6.1) + zai_payment (2.7.0) base64 (~> 0.3.0) faraday (~> 2.0) openssl (~> 3.3) diff --git a/changelog.md b/changelog.md index 9f2d501..7cf1801 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,88 @@ ## [Released] +## [2.7.0] - 2025-11-04 + +### Added +- **Batch Transactions Resource**: Complete batch transaction management for testing in prelive environment ๐Ÿ”„ + - `ZaiPayment.batch_transactions.export_transactions` - Export pending transactions to batched state + - `ZaiPayment.batch_transactions.process_to_bank_processing(batch_id, exported_ids:)` - Move batch to bank_processing state + - `ZaiPayment.batch_transactions.process_to_successful(batch_id, exported_ids:)` - Complete batch processing (triggers webhooks) + - `ZaiPayment.batch_transactions.show_batches` - List all batches + - `ZaiPayment.batch_transactions.show_batch(batch_id)` - Get specific batch details + - `ZaiPayment.batch_transactions.show_transactions(batch_id, limit:, offset:)` - List transactions in a batch with pagination + - Prelive-only endpoints for testing transaction processing workflows + - Full RSpec test suite with 20+ test examples + - Comprehensive documentation in `docs/batch_transactions.md` + - Practical examples in `examples/batch_transactions.md` + +- **Wallet Account Resource**: Complete wallet account management for Australian payments ๐Ÿ’ผ + - `ZaiPayment.wallet_accounts.show(wallet_account_id)` - Get wallet account details including balance + - `ZaiPayment.wallet_accounts.show_user(wallet_account_id)` - Get user associated with wallet account + - `ZaiPayment.wallet_accounts.show_npp_details(wallet_account_id)` - Get NPP (New Payments Platform) details including PayID + - `ZaiPayment.wallet_accounts.show_bpay_details(wallet_account_id)` - Get BPay details including biller code and reference + - `ZaiPayment.wallet_accounts.pay_bill(wallet_account_id, account_id:, amount:, reference_id:)` - Pay bills via BPay from wallet + - Support for checking wallet balances before transactions + - Support for NPP payments with PayID + - Support for BPay bill payments + - Validation for payment amounts and reference IDs + - Full RSpec test suite with 35+ test examples across 5 describe blocks + - Comprehensive documentation in `docs/wallet_accounts.md` + - Practical examples in `examples/wallet_accounts.md` + +### Documentation +- **Batch Transactions Guide** (`docs/batch_transactions.md`): + - Complete guide for all 6 batch transaction endpoints + - Detailed workflow for simulating batch processing + - State transition diagrams and examples + - Error handling patterns for batch operations + - Important notes about prelive-only usage + +- **Batch Transactions Examples** (`examples/batch_transactions.md`): + - Export transactions examples (3 examples) + - Process to bank_processing examples (3 examples) + - Process to successful examples (3 examples) + - Show batches and transactions examples (6 examples) + - Complete workflow patterns + - Rails integration examples + - Webhook simulation workflows + +- **Wallet Accounts Guide** (`docs/wallet_accounts.md`): + - Complete guide for all 5 wallet account endpoints + - Balance checking and payment workflows + - NPP PayID integration examples + - BPay bill payment patterns + - Validation rules and error handling + - Four comprehensive use cases: + - Balance verification before payment + - Multiple bill payments workflow + - User verification for payments + - Disbursement status monitoring + +- **Wallet Accounts Examples** (`examples/wallet_accounts.md`): + - Show wallet account examples (3 examples) + - Show user examples (3 examples) + - NPP details examples + - BPay details examples + - Pay bill examples (3 examples) + - Three common patterns: + - Complete payment workflow + - Wallet payment service class + - Rails controller integration + +### Enhanced +- **Response Class**: Added `disbursements` to `RESPONSE_DATA_KEYS` for automatic data extraction + - `response.data` now properly extracts disbursement objects from pay_bill responses + - Consistent with other resource response handling + +### Updated +- **README.md**: + - Added Wallet Accounts to features section + - Added Batch Transactions documentation links + - Updated roadmap to mark Payments as Done + - Added quick start examples for wallet account operations + +**Full Changelog**: https://github.com/Sentia/zai-payment/compare/v2.6.1...v2.7.0 + ## [2.6.1] - 2025-11-03 ### Changed diff --git a/docs/wallet_accounts.md b/docs/wallet_accounts.md new file mode 100644 index 0000000..6182e17 --- /dev/null +++ b/docs/wallet_accounts.md @@ -0,0 +1,493 @@ +# Wallet Account Management + +The WalletAccount resource provides methods for managing Zai wallet accounts for Australian payments and bill payments. + +## Overview + +Wallet accounts are digital wallets that hold funds and can be used for various payment operations including bill payments via BPay, withdrawals, and other disbursements. Each user in the Zai platform can have a wallet account with a balance that can be topped up and used for payments. + +Once created by Zai, store the returned `:id` and use it for payment operations. The wallet account maintains a balance in the specified currency (typically AUD for Australian marketplaces). + +## References + +- [Wallet Accounts API](https://developer.hellozai.com/reference) +- [Payment Methods Guide](https://developer.hellozai.com/docs/payment-methods) + +## Usage + +### Initialize the WalletAccount Resource + +```ruby +# Using a new instance +wallet_accounts = ZaiPayment::Resources::WalletAccount.new + +# Or use with custom client +client = ZaiPayment::Client.new +wallet_accounts = ZaiPayment::Resources::WalletAccount.new(client: client) +``` + +## Methods + +### Show Wallet Account + +Get details of a specific wallet account by ID. + +#### Parameters + +- `wallet_account_id` (required) - The wallet account ID + +#### Example + +```ruby +# Get wallet account details +response = wallet_accounts.show('5c1c6b10-4c56-0137-8cd7-0242ac110002') + +# Access wallet account details +wallet_account = response.data +puts wallet_account['id'] +puts wallet_account['active'] +puts wallet_account['balance'] +puts wallet_account['currency'] +puts wallet_account['created_at'] +puts wallet_account['updated_at'] + +# Access links +links = wallet_account['links'] +puts links['users'] +puts links['transactions'] +puts links['bpay_details'] +puts links['npp_details'] +``` + +#### Response + +```ruby +{ + "wallet_accounts" => { + "id" => "5c1c6b10-4c56-0137-8cd7-0242ac110002", + "active" => true, + "created_at" => "2019-04-29T02:42:31.536Z", + "updated_at" => "2020-05-03T12:01:02.254Z", + "balance" => 663337, + "currency" => "AUD", + "links" => { + "self" => "/transactions/aed45af0-6f63-0138-901c-0a58a9feac03/wallet_accounts", + "users" => "/wallet_accounts/5c1c6b10-4c56-0137-8cd7-0242ac110002/users", + "batch_transactions" => "/wallet_accounts/5c1c6b10-4c56-0137-8cd7-0242ac110002/batch_transactions", + "transactions" => "/wallet_accounts/5c1c6b10-4c56-0137-8cd7-0242ac110002/transactions", + "bpay_details" => "/wallet_accounts/5c1c6b10-4c56-0137-8cd7-0242ac110002/bpay_details", + "npp_details" => "/wallet_accounts/5c1c6b10-4c56-0137-8cd7-0242ac110002/npp_details", + "virtual_accounts" => "/wallet_accounts/5c1c6b10-4c56-0137-8cd7-0242ac110002/virtual_accounts" + } + } +} +``` + +**Use Cases:** +- Check wallet balance before initiating payments +- Verify account status and activity +- Monitor wallet account details +- Access related resources via links + +### Show Wallet Account User + +Get the User the Wallet Account is associated with using a given wallet_account_id. + +#### Parameters + +- `wallet_account_id` (required) - The wallet account ID + +#### Example + +```ruby +# Get user associated with wallet account +response = wallet_accounts.show_user('5c1c6b10-4c56-0137-8cd7-0242ac110002') + +# Access user details +user = response.data +puts user['id'] +puts user['full_name'] +puts user['email'] +puts user['verification_state'] +puts user['held_state'] +``` + +#### Response + +```ruby +{ + "users" => { + "created_at" => "2020-04-03T07:59:00.379Z", + "updated_at" => "2020-04-03T07:59:00.379Z", + "id" => "Seller_1234", + "full_name" => "Samuel Seller", + "email" => "sam@example.com", + "mobile" => 69543131, + "first_name" => "Samuel", + "last_name" => "Seller", + "custom_descriptor" => "Sam Garden Jobs", + "location" => "AUS", + "verification_state" => "pending", + "held_state" => false, + "roles" => ["customer"], + "dob" => "encrypted", + "government_number" => "encrypted", + "flags" => {}, + "related" => { + "addresses" => "11111111-2222-3333-4444-55555555555," + }, + "links" => { + "self" => "/wallet_accounts/901d8cd0-6af3-0138-967d-0a58a9feac04/users", + "items" => "/users/e6bc0480-57ae-0138-c46e-0a58a9feac03/items", + "wallet_accounts" => "/users/e6bc0480-57ae-0138-c46e-0a58a9feac03/wallet_accounts" + } + } +} +``` + +**Use Cases:** +- Retrieve user information for a wallet account +- Verify user identity before payment +- Check user verification status +- Get user contact details for notifications + +### Pay a Bill + +Pay a bill by withdrawing funds from a Wallet Account to a specified BPay account. + +#### Required Fields + +- `wallet_account_id` (path parameter) - The wallet account ID to withdraw from +- `account_id` - BPay account ID to withdraw to (must be a valid `bpay_account_id`) +- `amount` - Amount in cents to withdraw (must be a positive integer) + +#### Optional Fields + +- `reference_id` - Unique reference information for the payment (cannot contain single quote character) + +#### Example + +```ruby +wallet_accounts = ZaiPayment::Resources::WalletAccount.new + +response = wallet_accounts.pay_bill( + '901d8cd0-6af3-0138-967d-0a58a9feac04', + account_id: 'c1824ad0-73f1-0138-3700-0a58a9feac09', + amount: 173, + reference_id: 'test100' +) + +if response.success? + disbursement = response.data + puts "Disbursement ID: #{disbursement['id']}" + puts "Amount: #{disbursement['amount']}" + puts "State: #{disbursement['state']}" + puts "Reference: #{disbursement['reference_id']}" + puts "To: #{disbursement['to']}" + puts "Account Name: #{disbursement['account_name']}" + puts "Biller Name: #{disbursement['biller_name']}" + puts "Biller Code: #{disbursement['biller_code']}" + puts "CRN: #{disbursement['crn']}" +end +``` + +#### Response + +```ruby +{ + "disbursements" => { + "reference_id" => "test100", + "id" => "8a31ebfa-421b-4cbb-9241-632f71b3778a", + "amount" => 173, + "currency" => "AUD", + "created_at" => "2020-05-09T07:09:03.383Z", + "updated_at" => "2020-05-09T07:09:04.585Z", + "state" => "pending", + "to" => "BPay Account", + "account_name" => "My Water Company", + "biller_name" => "ABC Water", + "biller_code" => 123456, + "crn" => "0987654321", + "links" => { + "transactions" => "/disbursements/8a31ebfa-421b-4cbb-9241-632f71b3778a/transactions", + "wallet_accounts" => "/disbursements/8a31ebfa-421b-4cbb-9241-632f71b3778a/wallet_accounts", + "bank_accounts" => "/disbursements/8a31ebfa-421b-4cbb-9241-632f71b3778a/bank_accounts", + "bpay_accounts" => "/disbursements/8a31ebfa-421b-4cbb-9241-632f71b3778a/bpay_accounts", + "paypal_accounts" => "/disbursements/8a31ebfa-421b-4cbb-9241-632f71b3778a/paypal_accounts", + "items" => "/disbursements/8a31ebfa-421b-4cbb-9241-632f71b3778a/items", + "users" => "/disbursements/8a31ebfa-421b-4cbb-9241-632f71b3778a/users" + } + } +} +``` + +**Important Notes:** +- Amount is in cents (e.g., 173 = $1.73 AUD) +- The wallet must have sufficient balance to cover the payment +- Disbursement state will be "pending" initially, then transitions to "successful" or "failed" +- Reference ID is optional but recommended for tracking purposes + +## Validation Rules + +### Wallet Account ID + +- Required for all methods +- Must not be blank or nil +- Must be a valid UUID format + +### Amount (for pay_bill) + +- Required field +- Must be a positive integer +- Specified in cents (e.g., 100 = $1.00) +- Must not exceed wallet balance + +### Account ID (for pay_bill) + +- Required field +- Must be a valid BPay account ID +- The BPay account must be active and verified + +### Reference ID (for pay_bill) + +- Optional field +- Cannot contain single quote (') character +- Used for tracking and reconciliation +- Should be unique for each payment + +## Error Handling + +The WalletAccount resource raises the following errors: + +### NotFoundError + +Raised when the wallet account does not exist: + +```ruby +begin + wallet_accounts.show('invalid_id') +rescue ZaiPayment::Errors::NotFoundError => e + puts "Wallet account not found: #{e.message}" +end +``` + +### ValidationError + +Raised when required fields are missing or invalid: + +```ruby +begin + wallet_accounts.pay_bill( + '901d8cd0-6af3-0138-967d-0a58a9feac04', + account_id: 'test123' + # Missing required field: amount + ) +rescue ZaiPayment::Errors::ValidationError => e + puts "Validation failed: #{e.message}" + # => "Missing required fields: amount" +end +``` + +### Invalid Amount + +```ruby +begin + wallet_accounts.pay_bill( + '901d8cd0-6af3-0138-967d-0a58a9feac04', + account_id: 'bpay_account_123', + amount: -100 # Negative amount + ) +rescue ZaiPayment::Errors::ValidationError => e + puts e.message + # => "amount must be a positive integer" +end +``` + +### Invalid Reference ID + +```ruby +begin + wallet_accounts.pay_bill( + '901d8cd0-6af3-0138-967d-0a58a9feac04', + account_id: 'bpay_account_123', + amount: 173, + reference_id: "test'100" # Contains single quote + ) +rescue ZaiPayment::Errors::ValidationError => e + puts e.message + # => "reference_id cannot contain single quote (') character" +end +``` + +### Blank Wallet Account ID + +```ruby +begin + wallet_accounts.show('') + # or + wallet_accounts.pay_bill(nil, account_id: 'test', amount: 100) +rescue ZaiPayment::Errors::ValidationError => e + puts "Invalid ID: #{e.message}" + # => "wallet_account_id is required and cannot be blank" +end +``` + +## Use Cases + +### Use Case 1: Check Balance Before Payment + +Check wallet balance before initiating a bill payment: + +```ruby +wallet_accounts = ZaiPayment::Resources::WalletAccount.new + +# Step 1: Check wallet balance +response = wallet_accounts.show('wallet_account_id') + +if response.success? + wallet = response.data + balance = wallet['balance'] # in cents + + # Step 2: Verify sufficient funds + payment_amount = 17300 # $173.00 + + if balance >= payment_amount + puts "Sufficient balance: $#{balance / 100.0}" + + # Step 3: Process payment + payment_response = wallet_accounts.pay_bill( + 'wallet_account_id', + account_id: 'bpay_account_id', + amount: payment_amount, + reference_id: 'bill_#{Time.now.to_i}' + ) + + puts "Payment initiated" if payment_response.success? + else + puts "Insufficient funds: $#{balance / 100.0} < $#{payment_amount / 100.0}" + end +end +``` + +### Use Case 2: Pay Multiple Bills + +Process multiple bill payments from a wallet account: + +```ruby +wallet_accounts = ZaiPayment::Resources::WalletAccount.new +wallet_id = '901d8cd0-6af3-0138-967d-0a58a9feac04' + +bills = [ + { account_id: 'bpay_water', amount: 15000, ref: 'water_202411' }, + { account_id: 'bpay_electricity', amount: 22000, ref: 'elec_202411' }, + { account_id: 'bpay_gas', amount: 8500, ref: 'gas_202411' } +] + +bills.each do |bill| + response = wallet_accounts.pay_bill( + wallet_id, + account_id: bill[:account_id], + amount: bill[:amount], + reference_id: bill[:ref] + ) + + if response.success? + puts "Paid #{bill[:ref]}: $#{bill[:amount] / 100.0}" + else + puts "Failed to pay #{bill[:ref]}" + end +end +``` + +### Use Case 3: Get User Details for Wallet Account + +Retrieve user information for notification purposes: + +```ruby +wallet_accounts = ZaiPayment::Resources::WalletAccount.new + +# Step 1: Get user details +user_response = wallet_accounts.show_user('wallet_account_id') + +if user_response.success? + user = user_response.data + + # Step 2: Verify user eligibility + if user['verification_state'] == 'verified' && !user['held_state'] + puts "User verified: #{user['full_name']}" + puts "Email: #{user['email']}" + + # Step 3: Send notification + # NotificationService.send_payment_confirmation(user['email']) + else + puts "User not eligible for payments" + puts "Verification: #{user['verification_state']}" + puts "On Hold: #{user['held_state']}" + end +end +``` + +### Use Case 4: Monitor Disbursement Status + +Track the status of a bill payment: + +```ruby +wallet_accounts = ZaiPayment::Resources::WalletAccount.new + +# Initiate payment +payment_response = wallet_accounts.pay_bill( + 'wallet_account_id', + account_id: 'bpay_account_id', + amount: 17300, + reference_id: 'bill_123' +) + +if payment_response.success? + disbursement = payment_response.data + disbursement_id = disbursement['id'] + + puts "Payment initiated: #{disbursement_id}" + puts "State: #{disbursement['state']}" + puts "To: #{disbursement['account_name']}" + puts "Biller: #{disbursement['biller_name']}" + + # Monitor using disbursement ID + # Later check status via transactions or webhooks +end +``` + +## Important Notes + +1. **Currency**: Wallet accounts typically use AUD (Australian Dollars) for Australian marketplaces +2. **Balance**: The balance is returned in cents (e.g., 663337 = $6,633.37) +3. **Payment Amount**: Amounts must be specified in cents +4. **BPay Integration**: The `pay_bill` method integrates with BPay for Australian bill payments +5. **Disbursement State**: Payment states include `pending`, `successful`, and `failed` +6. **Reference Tracking**: Use `reference_id` for payment tracking and reconciliation +7. **Sufficient Funds**: Ensure wallet has sufficient balance before initiating payments +8. **Account Status**: Only active wallet accounts can be used for payments + +## Disbursement States + +- **pending**: Payment has been initiated but not yet processed +- **successful**: Payment completed successfully +- **failed**: Payment failed (check error details) + +Monitor payment status through: +- Webhook notifications +- Transaction queries +- Disbursement status checks + +## Related Resources + +- [User Management](users.md) - Creating and managing users +- [BPay Accounts](bpay_accounts.md) - Managing BPay accounts for bill payments +- [Items](items.md) - Creating items for payments + +## Further Reading + +- [Payment Methods Guide](https://developer.hellozai.com/docs/payment-methods) +- [Wallet Account API Reference](https://developer.hellozai.com/reference) +- [BPay Overview](https://developer.hellozai.com/docs/bpay) +- [Verification Process](https://developer.hellozai.com/docs/verification) + diff --git a/examples/wallet_accounts.md b/examples/wallet_accounts.md new file mode 100644 index 0000000..b5ffac2 --- /dev/null +++ b/examples/wallet_accounts.md @@ -0,0 +1,733 @@ +# Wallet Account Management Examples + +This document provides practical examples for managing wallet accounts in Zai Payment. + +## Table of Contents + +- [Setup](#setup) +- [Show Wallet Account Example](#show-wallet-account-example) +- [Show Wallet Account User Example](#show-wallet-account-user-example) +- [Pay a Bill Example](#pay-a-bill-example) +- [Common Patterns](#common-patterns) + +## Setup + +```ruby +require 'zai_payment' + +# Configure ZaiPayment +ZaiPayment.configure do |config| + config.environment = :prelive # or :production + config.client_id = ENV['ZAI_CLIENT_ID'] + config.client_secret = ENV['ZAI_CLIENT_SECRET'] + config.scope = ENV['ZAI_SCOPE'] +end +``` + +## Show Wallet Account Example + +### Example 1: Get Wallet Account Details + +Retrieve details of a specific wallet account. + +```ruby +# Get wallet account details +wallet_accounts = ZaiPayment::Resources::WalletAccount.new + +response = wallet_accounts.show('5c1c6b10-4c56-0137-8cd7-0242ac110002') + +if response.success? + wallet = response.data + puts "Wallet Account ID: #{wallet['id']}" + puts "Active: #{wallet['active']}" + puts "Balance: $#{wallet['balance'] / 100.0}" + puts "Currency: #{wallet['currency']}" + puts "Created At: #{wallet['created_at']}" + puts "Updated At: #{wallet['updated_at']}" + + # Access links + links = wallet['links'] + puts "\nLinks:" + puts " Self: #{links['self']}" + puts " Users: #{links['users']}" + puts " Transactions: #{links['transactions']}" + puts " BPay Details: #{links['bpay_details']}" + puts " NPP Details: #{links['npp_details']}" +else + puts "Failed to retrieve wallet account" + puts "Error: #{response.error}" +end +``` + +### Example 2: Check Balance Before Payment + +Check wallet balance before initiating a payment. + +```ruby +wallet_accounts = ZaiPayment::Resources::WalletAccount.new + +begin + response = wallet_accounts.show('wallet_account_id_here') + + if response.success? + wallet = response.data + balance = wallet['balance'] # in cents + + # Check if account is active + if wallet['active'] + puts "Wallet is active" + puts "Current balance: $#{balance / 100.0}" + + # Check if sufficient funds for payment + payment_amount = 17300 # $173.00 + + if balance >= payment_amount + puts "Sufficient funds for payment of $#{payment_amount / 100.0}" + else + puts "Insufficient funds" + puts " Required: $#{payment_amount / 100.0}" + puts " Available: $#{balance / 100.0}" + puts " Shortfall: $#{(payment_amount - balance) / 100.0}" + end + else + puts "Wallet account is inactive" + end + else + puts "Failed to retrieve wallet account: #{response.error}" + end +rescue ZaiPayment::Errors::NotFoundError => e + puts "Wallet account not found: #{e.message}" +rescue ZaiPayment::Errors::ValidationError => e + puts "Invalid wallet account ID: #{e.message}" +rescue ZaiPayment::Errors::ApiError => e + puts "API error occurred: #{e.message}" +end +``` + +### Example 3: Monitor Wallet Account Status + +Check wallet account status and available resources. + +```ruby +wallet_accounts = ZaiPayment::Resources::WalletAccount.new + +# Step 1: Retrieve wallet account +response = wallet_accounts.show('wallet_account_id') + +if response.success? + wallet = response.data + + # Step 2: Display wallet status + puts "Wallet Account Status:" + puts " ID: #{wallet['id']}" + puts " Active: #{wallet['active']}" + puts " Balance: $#{wallet['balance'] / 100.0}" + puts " Currency: #{wallet['currency']}" + + # Step 3: Check available resources + links = wallet['links'] + + puts "\nAvailable Resources:" + puts " โœ“ Users" if links['users'] + puts " โœ“ Transactions" if links['transactions'] + puts " โœ“ Batch Transactions" if links['batch_transactions'] + puts " โœ“ BPay Details" if links['bpay_details'] + puts " โœ“ NPP Details" if links['npp_details'] + puts " โœ“ Virtual Accounts" if links['virtual_accounts'] +end +``` + +## Show Wallet Account User Example + +### Example 1: Get User Associated with Wallet Account + +Retrieve user details for a wallet account. + +```ruby +# Get user associated with wallet account +wallet_accounts = ZaiPayment::Resources::WalletAccount.new + +response = wallet_accounts.show_user('5c1c6b10-4c56-0137-8cd7-0242ac110002') + +if response.success? + user = response.data + puts "User ID: #{user['id']}" + puts "Full Name: #{user['full_name']}" + puts "Email: #{user['email']}" + puts "First Name: #{user['first_name']}" + puts "Last Name: #{user['last_name']}" + puts "Location: #{user['location']}" + puts "Verification State: #{user['verification_state']}" + puts "Held State: #{user['held_state']}" + puts "Roles: #{user['roles'].join(', ')}" + + # Access links + links = user['links'] + puts "\nLinks:" + puts " Self: #{links['self']}" + puts " Items: #{links['items']}" + puts " Wallet Accounts: #{links['wallet_accounts']}" +else + puts "Failed to retrieve user" + puts "Error: #{response.error}" +end +``` + +### Example 2: Verify User Before Payment + +Check user details before processing a payment from wallet account. + +```ruby +wallet_accounts = ZaiPayment::Resources::WalletAccount.new + +begin + # Step 1: Get user associated with wallet account + user_response = wallet_accounts.show_user('wallet_account_id') + + if user_response.success? + user = user_response.data + + # Step 2: Verify user details + if user['verification_state'] == 'verified' && !user['held_state'] + puts "User verified and not on hold" + puts "Name: #{user['full_name']}" + puts "Email: #{user['email']}" + + # Proceed with payment + puts "โœ“ Ready to process payment" + else + puts "Cannot process payment:" + puts " Verification: #{user['verification_state']}" + puts " On Hold: #{user['held_state']}" + end + end +rescue ZaiPayment::Errors::NotFoundError => e + puts "Wallet account not found: #{e.message}" +rescue ZaiPayment::Errors::ApiError => e + puts "API error: #{e.message}" +end +``` + +### Example 3: Get User Contact Information for Notifications + +Retrieve user contact details for sending payment notifications. + +```ruby +wallet_accounts = ZaiPayment::Resources::WalletAccount.new + +response = wallet_accounts.show_user('wallet_account_id') + +if response.success? + user = response.data + + # Extract contact information + contact_info = { + name: user['full_name'], + email: user['email'], + mobile: user['mobile'], + location: user['location'] + } + + puts "Contact Information:" + puts " Name: #{contact_info[:name]}" + puts " Email: #{contact_info[:email]}" + puts " Mobile: #{contact_info[:mobile]}" + puts " Location: #{contact_info[:location]}" + + # Send notification + # NotificationService.send_payment_confirmation(contact_info) +end +``` + +## Pay a Bill Example + +### Example 1: Basic Bill Payment + +Pay a bill using funds from a wallet account. + +```ruby +# Pay a bill from wallet account +wallet_accounts = ZaiPayment::Resources::WalletAccount.new + +response = wallet_accounts.pay_bill( + '901d8cd0-6af3-0138-967d-0a58a9feac04', + account_id: 'c1824ad0-73f1-0138-3700-0a58a9feac09', + amount: 173, + reference_id: 'test100' +) + +if response.success? + disbursement = response.data + puts "Disbursement ID: #{disbursement['id']}" + puts "Reference: #{disbursement['reference_id']}" + puts "Amount: $#{disbursement['amount'] / 100.0}" + puts "Currency: #{disbursement['currency']}" + puts "State: #{disbursement['state']}" + puts "To: #{disbursement['to']}" + puts "Account Name: #{disbursement['account_name']}" + puts "Biller Name: #{disbursement['biller_name']}" + puts "Biller Code: #{disbursement['biller_code']}" + puts "CRN: #{disbursement['crn']}" + puts "Created At: #{disbursement['created_at']}" + + # Access links + links = disbursement['links'] + puts "\nLinks:" + puts " Transactions: #{links['transactions']}" + puts " Wallet Accounts: #{links['wallet_accounts']}" + puts " BPay Accounts: #{links['bpay_accounts']}" +else + puts "Failed to pay bill" + puts "Error: #{response.error}" +end +``` + +### Example 2: Pay Bill with Balance Check + +Check balance before paying a bill. + +```ruby +wallet_accounts = ZaiPayment::Resources::WalletAccount.new +wallet_id = '901d8cd0-6af3-0138-967d-0a58a9feac04' +payment_amount = 17300 # $173.00 + +begin + # Step 1: Check wallet balance + wallet_response = wallet_accounts.show(wallet_id) + + if wallet_response.success? + wallet = wallet_response.data + balance = wallet['balance'] + + puts "Current balance: $#{balance / 100.0}" + puts "Payment amount: $#{payment_amount / 100.0}" + + # Step 2: Verify sufficient funds + if balance >= payment_amount + puts "Sufficient funds available" + + # Step 3: Process payment + payment_response = wallet_accounts.pay_bill( + wallet_id, + account_id: 'bpay_account_id', + amount: payment_amount, + reference_id: "bill_#{Time.now.to_i}" + ) + + if payment_response.success? + disbursement = payment_response.data + puts "\nโœ“ Bill payment successful" + puts "Disbursement ID: #{disbursement['id']}" + puts "New balance: $#{(balance - payment_amount) / 100.0}" + else + puts "\nโœ— Payment failed: #{payment_response.error}" + end + else + shortfall = payment_amount - balance + puts "\nโœ— Insufficient funds" + puts "Shortfall: $#{shortfall / 100.0}" + end + end +rescue ZaiPayment::Errors::ValidationError => e + puts "Validation error: #{e.message}" +rescue ZaiPayment::Errors::ApiError => e + puts "API error: #{e.message}" +end +``` + +### Example 3: Pay Multiple Bills + +Process multiple bill payments from a wallet account. + +```ruby +wallet_accounts = ZaiPayment::Resources::WalletAccount.new +wallet_id = '901d8cd0-6af3-0138-967d-0a58a9feac04' + +bills = [ + { + account_id: 'bpay_water_account', + amount: 15000, # $150.00 + reference: 'water_bill_nov_2024', + name: 'Water Bill' + }, + { + account_id: 'bpay_electricity_account', + amount: 22000, # $220.00 + reference: 'electricity_bill_nov_2024', + name: 'Electricity Bill' + }, + { + account_id: 'bpay_gas_account', + amount: 8500, # $85.00 + reference: 'gas_bill_nov_2024', + name: 'Gas Bill' + } +] + +# Check total payment amount +total_amount = bills.sum { |bill| bill[:amount] } +puts "Total payment amount: $#{total_amount / 100.0}" + +# Check balance +wallet_response = wallet_accounts.show(wallet_id) +if wallet_response.success? + balance = wallet_response.data['balance'] + puts "Available balance: $#{balance / 100.0}" + + if balance >= total_amount + # Process each bill + bills.each do |bill| + response = wallet_accounts.pay_bill( + wallet_id, + account_id: bill[:account_id], + amount: bill[:amount], + reference_id: bill[:reference] + ) + + if response.success? + disbursement = response.data + puts "\nโœ“ #{bill[:name]} paid: $#{bill[:amount] / 100.0}" + puts " Disbursement ID: #{disbursement['id']}" + puts " State: #{disbursement['state']}" + else + puts "\nโœ— Failed to pay #{bill[:name]}" + end + + # Small delay between payments + sleep(0.5) + end + else + puts "\nโœ— Insufficient funds for all bills" + puts "Shortfall: $#{(total_amount - balance) / 100.0}" + end +end +``` + +## Common Patterns + +### Pattern 1: Complete Payment Workflow + +Full workflow from balance check to payment confirmation. + +```ruby +wallet_accounts = ZaiPayment::Resources::WalletAccount.new +wallet_id = '901d8cd0-6af3-0138-967d-0a58a9feac04' +bpay_account_id = 'c1824ad0-73f1-0138-3700-0a58a9feac09' +payment_amount = 17300 # $173.00 + +begin + # Step 1: Verify user eligibility + user_response = wallet_accounts.show_user(wallet_id) + + if user_response.success? + user = user_response.data + + unless user['verification_state'] == 'verified' && !user['held_state'] + puts "User not eligible for payment" + exit + end + + puts "โœ“ User verified: #{user['full_name']}" + end + + # Step 2: Check wallet balance + wallet_response = wallet_accounts.show(wallet_id) + + if wallet_response.success? + wallet = wallet_response.data + balance = wallet['balance'] + + unless wallet['active'] && balance >= payment_amount + puts "โœ— Wallet not ready for payment" + puts " Active: #{wallet['active']}" + puts " Balance: $#{balance / 100.0}" + exit + end + + puts "โœ“ Sufficient balance: $#{balance / 100.0}" + end + + # Step 3: Process payment + payment_response = wallet_accounts.pay_bill( + wallet_id, + account_id: bpay_account_id, + amount: payment_amount, + reference_id: "bill_#{Time.now.to_i}" + ) + + if payment_response.success? + disbursement = payment_response.data + + puts "\nโœ“ Payment successful" + puts " Disbursement ID: #{disbursement['id']}" + puts " Amount: $#{disbursement['amount'] / 100.0}" + puts " State: #{disbursement['state']}" + puts " To: #{disbursement['account_name']}" + puts " Reference: #{disbursement['reference_id']}" + + # Step 4: Send notification + # NotificationService.send_payment_confirmation(user['email'], disbursement) + else + puts "\nโœ— Payment failed: #{payment_response.error}" + end + +rescue ZaiPayment::Errors::ValidationError => e + puts "Validation error: #{e.message}" +rescue ZaiPayment::Errors::NotFoundError => e + puts "Not found: #{e.message}" +rescue ZaiPayment::Errors::ApiError => e + puts "API error: #{e.message}" +end +``` + +### Pattern 2: Wallet Payment Service + +Implement a payment service class for wallet payments. + +```ruby +class WalletPaymentService + def initialize + @wallet_accounts = ZaiPayment::Resources::WalletAccount.new + end + + def pay_bill(wallet_id, bpay_account_id, amount, reference_id) + # Validate inputs + validate_amount!(amount) + + # Check balance + unless sufficient_balance?(wallet_id, amount) + return { success: false, error: 'Insufficient balance' } + end + + # Process payment + response = @wallet_accounts.pay_bill( + wallet_id, + account_id: bpay_account_id, + amount: amount, + reference_id: reference_id + ) + + if response.success? + disbursement = response.data + + { + success: true, + disbursement_id: disbursement['id'], + amount: disbursement['amount'], + state: disbursement['state'], + reference: disbursement['reference_id'] + } + else + { + success: false, + error: response.error + } + end + rescue ZaiPayment::Errors::ValidationError => e + { success: false, error: "Validation error: #{e.message}" } + rescue ZaiPayment::Errors::ApiError => e + { success: false, error: "API error: #{e.message}" } + end + + def get_balance(wallet_id) + response = @wallet_accounts.show(wallet_id) + + if response.success? + wallet = response.data + { + success: true, + balance: wallet['balance'], + currency: wallet['currency'], + active: wallet['active'] + } + else + { success: false, error: response.error } + end + rescue ZaiPayment::Errors::ApiError => e + { success: false, error: e.message } + end + + private + + def validate_amount!(amount) + raise ArgumentError, 'Amount must be positive' unless amount.positive? + raise ArgumentError, 'Amount must be an integer' unless amount.is_a?(Integer) + end + + def sufficient_balance?(wallet_id, amount) + result = get_balance(wallet_id) + result[:success] && result[:balance] >= amount + end +end + +# Usage +service = WalletPaymentService.new + +# Check balance +balance_result = service.get_balance('wallet_id') +if balance_result[:success] + puts "Balance: $#{balance_result[:balance] / 100.0}" +end + +# Pay bill +payment_result = service.pay_bill( + 'wallet_id', + 'bpay_account_id', + 17300, + 'bill_123' +) + +if payment_result[:success] + puts "Payment successful: #{payment_result[:disbursement_id]}" +else + puts "Payment failed: #{payment_result[:error]}" +end +``` + +### Pattern 3: Rails Controller for Wallet Payments + +Implement wallet payments in a Rails controller. + +```ruby +# In a Rails controller +class WalletPaymentsController < ApplicationController + before_action :authenticate_user! + + def create + wallet_accounts = ZaiPayment::Resources::WalletAccount.new + + begin + # Validate parameters + validate_payment_params! + + # Process payment + response = wallet_accounts.pay_bill( + params[:wallet_account_id], + account_id: params[:bpay_account_id], + amount: params[:amount].to_i, + reference_id: params[:reference_id] + ) + + if response.success? + disbursement = response.data + + # Log payment + Rails.logger.info("Payment successful: #{disbursement['id']}") + + render json: { + success: true, + disbursement_id: disbursement['id'], + amount: disbursement['amount'], + state: disbursement['state'], + message: 'Bill payment successful' + }, status: :created + else + render json: { + success: false, + message: response.error + }, status: :unprocessable_entity + end + rescue ZaiPayment::Errors::ValidationError => e + render json: { + success: false, + message: e.message + }, status: :bad_request + rescue ZaiPayment::Errors::NotFoundError => e + render json: { + success: false, + message: 'Wallet or BPay account not found' + }, status: :not_found + rescue ZaiPayment::Errors::ApiError => e + Rails.logger.error("Payment API error: #{e.message}") + + render json: { + success: false, + message: 'An error occurred while processing the payment' + }, status: :internal_server_error + end + end + + def show_balance + wallet_accounts = ZaiPayment::Resources::WalletAccount.new + + begin + response = wallet_accounts.show(params[:id]) + + if response.success? + wallet = response.data + + render json: { + success: true, + balance: wallet['balance'], + currency: wallet['currency'], + active: wallet['active'] + } + else + render json: { + success: false, + message: response.error + }, status: :unprocessable_entity + end + rescue ZaiPayment::Errors::NotFoundError => e + render json: { + success: false, + message: 'Wallet account not found' + }, status: :not_found + end + end + + private + + def validate_payment_params! + required_params = [:wallet_account_id, :bpay_account_id, :amount] + missing_params = required_params.select { |param| params[param].blank? } + + if missing_params.any? + raise ActionController::ParameterMissing, "Missing parameters: #{missing_params.join(', ')}" + end + + amount = params[:amount].to_i + if amount <= 0 + raise ArgumentError, 'Amount must be positive' + end + end +end +``` + +## Important Notes + +1. **Required Fields**: + - `wallet_account_id` - The wallet account ID + - `account_id` - BPay account ID (for pay_bill) + - `amount` - Payment amount in cents (for pay_bill) + +2. **Amount Validation**: + - Must be a positive integer + - Specified in cents (e.g., 100 = $1.00) + - Cannot exceed wallet balance + +3. **Reference ID**: + - Optional but recommended for tracking + - Cannot contain single quote (') character + - Should be unique for each payment + +4. **Balance Check**: + - Always check balance before payment + - Balance returned in cents + - Verify wallet is active + +5. **Disbursement States**: + - `pending` - Payment initiated + - `successful` - Payment completed + - `failed` - Payment failed + +6. **Error Handling**: + - Always wrap API calls in error handling + - Check for ValidationError, NotFoundError, ApiError + - Log errors for debugging + +7. **User Verification**: + - Verify user state before payment + - Check `verification_state` == 'verified' + - Ensure `held_state` == false + diff --git a/lib/zai_payment.rb b/lib/zai_payment.rb index 4662e3b..964cb8c 100644 --- a/lib/zai_payment.rb +++ b/lib/zai_payment.rb @@ -17,6 +17,7 @@ require_relative 'zai_payment/resources/bank_account' require_relative 'zai_payment/resources/bpay_account' require_relative 'zai_payment/resources/batch_transaction' +require_relative 'zai_payment/resources/wallet_account' module ZaiPayment class << self @@ -75,5 +76,10 @@ def bpay_accounts def batch_transactions @batch_transactions ||= Resources::BatchTransaction.new(client: Client.new(base_endpoint: :core_base)) end + + # @return [ZaiPayment::Resources::WalletAccount] wallet_account resource instance + def wallet_accounts + @wallet_accounts ||= Resources::WalletAccount.new(client: Client.new(base_endpoint: :core_base)) + end end end diff --git a/lib/zai_payment/resources/wallet_account.rb b/lib/zai_payment/resources/wallet_account.rb new file mode 100644 index 0000000..1c502e5 --- /dev/null +++ b/lib/zai_payment/resources/wallet_account.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true + +module ZaiPayment + module Resources + # WalletAccount resource for managing Zai wallet accounts + # + # @see https://developer.hellozai.com/reference + class WalletAccount + attr_reader :client + + # Map of attribute keys to API field names for pay_bill + PAY_BILL_FIELD_MAPPING = { + account_id: :account_id, + amount: :amount, + reference_id: :reference_id + }.freeze + + def initialize(client: nil) + @client = client || Client.new + end + + # Get a specific wallet account by ID + # + # @param wallet_account_id [String] the wallet account ID + # @return [Response] the API response containing wallet account details + # + # @example + # wallet_accounts = ZaiPayment::Resources::WalletAccount.new + # response = wallet_accounts.show("wallet_account_id") + # response.data # => {"id" => "wallet_account_id", "active" => true, ...} + # + # @see https://developer.hellozai.com/reference + def show(wallet_account_id) + validate_id!(wallet_account_id, 'wallet_account_id') + client.get("/wallet_accounts/#{wallet_account_id}") + end + + # Get the user associated with a Wallet Account + # + # Show the User the Wallet Account is associated with using a given wallet_account_id. + # + # @param wallet_account_id [String] the wallet account ID + # @return [Response] the API response containing user details + # + # @example + # wallet_accounts = ZaiPayment::Resources::WalletAccount.new + # response = wallet_accounts.show_user("wallet_account_id") + # response.data # => {"id" => "user_id", "full_name" => "Samuel Seller", ...} + # + # @see https://developer.hellozai.com/reference + def show_user(wallet_account_id) + validate_id!(wallet_account_id, 'wallet_account_id') + client.get("/wallet_accounts/#{wallet_account_id}/users") + end + + # Get NPP details for a Wallet Account + # + # Show NPP details of a specific Wallet Account using a given wallet_account_id. + # NPP (New Payments Platform) details include PayID and payment reference information. + # + # @param wallet_account_id [String] the wallet account ID + # @return [Response] the API response containing NPP details + # + # @example + # wallet_accounts = ZaiPayment::Resources::WalletAccount.new + # response = wallet_accounts.show_npp_details("wallet_account_id") + # response.data # => {"id" => "wallet_account_id", "npp_details" => {...}} + # + # @see https://developer.hellozai.com/reference + def show_npp_details(wallet_account_id) + validate_id!(wallet_account_id, 'wallet_account_id') + client.get("/wallet_accounts/#{wallet_account_id}/npp_details") + end + + # Get BPay details for a Wallet Account + # + # Show BPay details of a specific Wallet Account using a given wallet_account_id. + # BPay details include biller code, reference, and amount information. + # + # @param wallet_account_id [String] the wallet account ID + # @return [Response] the API response containing BPay details + # + # @example + # wallet_accounts = ZaiPayment::Resources::WalletAccount.new + # response = wallet_accounts.show_bpay_details("wallet_account_id") + # response.data # => {"id" => "wallet_account_id", "bpay_details" => {...}} + # + # @see https://developer.hellozai.com/reference + def show_bpay_details(wallet_account_id) + validate_id!(wallet_account_id, 'wallet_account_id') + client.get("/wallet_accounts/#{wallet_account_id}/bpay_details") + end + + # Pay a bill by withdrawing funds from a Wallet Account to a specified BPay account + # + # @param wallet_account_id [String] the wallet account ID + # @param attributes [Hash] bill payment attributes + # @option attributes [String] :account_id (Required) BPay account ID to withdraw to + # @option attributes [Integer] :amount (Required) Amount in cents to withdraw + # @option attributes [String] :reference_id Optional unique reference information + # @return [Response] the API response containing disbursement details + # + # @example Pay a bill + # wallet_accounts = ZaiPayment::Resources::WalletAccount.new + # response = wallet_accounts.pay_bill( + # '901d8cd0-6af3-0138-967d-0a58a9feac04', + # account_id: 'c1824ad0-73f1-0138-3700-0a58a9feac09', + # amount: 173, + # reference_id: 'test100' + # ) + # + # @see https://developer.hellozai.com/reference + def pay_bill(wallet_account_id, **attributes) + validate_id!(wallet_account_id, 'wallet_account_id') + validate_pay_bill_attributes!(attributes) + + body = build_pay_bill_body(attributes) + client.post("/wallet_accounts/#{wallet_account_id}/bill_payment", body: body) + end + + private + + def validate_id!(value, field_name) + return unless value.nil? || value.to_s.strip.empty? + + raise Errors::ValidationError, "#{field_name} is required and cannot be blank" + end + + def validate_pay_bill_attributes!(attributes) + validate_required_pay_bill_attributes!(attributes) + validate_amount!(attributes[:amount]) if attributes[:amount] + validate_reference_id!(attributes[:reference_id]) if attributes[:reference_id] + end + + def validate_required_pay_bill_attributes!(attributes) + required_fields = %i[account_id amount] + + missing_fields = required_fields.select do |field| + attributes[field].nil? || (attributes[field].respond_to?(:to_s) && attributes[field].to_s.strip.empty?) + end + + return if missing_fields.empty? + + raise Errors::ValidationError, + "Missing required fields: #{missing_fields.join(', ')}" + end + + def validate_amount!(amount) + # Amount must be a positive integer + return if amount.is_a?(Integer) && amount.positive? + + raise Errors::ValidationError, 'amount must be a positive integer' + end + + def validate_reference_id!(reference_id) + # Reference ID cannot contain single quote character + return unless reference_id.to_s.include?("'") + + raise Errors::ValidationError, "reference_id cannot contain single quote (') character" + end + + def build_pay_bill_body(attributes) + body = {} + + attributes.each do |key, value| + next if value.nil? || (value.respond_to?(:empty?) && value.empty?) + + api_field = PAY_BILL_FIELD_MAPPING[key] + body[api_field] = value if api_field + end + + body + end + end + end +end diff --git a/lib/zai_payment/response.rb b/lib/zai_payment/response.rb index a8173a6..b809a80 100644 --- a/lib/zai_payment/response.rb +++ b/lib/zai_payment/response.rb @@ -8,7 +8,7 @@ class Response RESPONSE_DATA_KEYS = %w[ webhooks users items fees transactions batch_transactions batches bpay_accounts bank_accounts card_accounts - wallet_accounts routing_number + wallet_accounts routing_number disbursements ].freeze def initialize(faraday_response) diff --git a/lib/zai_payment/version.rb b/lib/zai_payment/version.rb index d9776f7..43aeecd 100644 --- a/lib/zai_payment/version.rb +++ b/lib/zai_payment/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module ZaiPayment - VERSION = '2.6.1' + VERSION = '2.7.0' end diff --git a/readme.md b/readme.md index d487dc0..903f3b4 100644 --- a/readme.md +++ b/readme.md @@ -22,6 +22,8 @@ A lightweight and extensible Ruby client for the **Zai (AssemblyPay)** API โ€” s - ๐Ÿ‘ฅ **User Management** - Create and manage payin (buyers) & payout (sellers) users - ๐Ÿ“ฆ **Item Management** - Full CRUD for transactions/payments between buyers and sellers - ๐Ÿฆ **Bank Account Management** - Complete CRUD + validation for AU/UK bank accounts +- ๐Ÿ’ณ **BPay Account Management** - Manage BPay accounts for Australian bill payments +- ๐Ÿ’ผ **Wallet Account Management** - Show wallet accounts, check balances, and pay bills via BPay - ๐ŸŽซ **Token Auth** - Generate secure tokens for bank and card account data collection - ๐Ÿช **Webhooks** - Full CRUD + secure signature verification (HMAC SHA256) - ๐Ÿงช **Batch Transactions** - Prelive-only endpoints for testing batch transaction flows @@ -116,6 +118,48 @@ Manage bank accounts for Australian and UK users, with routing number validation - ๐Ÿ’ก [Bank Account Examples](examples/bank_accounts.md) - Real-world patterns and integration - ๐Ÿ”— [Zai: Bank Accounts API Reference](https://developer.hellozai.com/reference/showbankaccount) +### BPay Accounts + +Manage BPay accounts for Australian bill payments. + +**๐Ÿ“š Documentation:** +- ๐Ÿ“– [BPay Account Management Guide](docs/bpay_accounts.md) - Complete guide for BPay accounts +- ๐Ÿ’ก [BPay Account Examples](examples/bpay_accounts.md) - Real-world patterns and bill payment workflows +- ๐Ÿ”— [Zai: BPay Accounts API Reference](https://developer.hellozai.com/reference/createbpayaccount) + +### Wallet Accounts + +Manage wallet accounts, check balances, and pay bills via BPay. + +**๐Ÿ“š Documentation:** +- ๐Ÿ“– [Wallet Account Management Guide](docs/wallet_accounts.md) - Complete guide for wallet accounts +- ๐Ÿ’ก [Wallet Account Examples](examples/wallet_accounts.md) - Real-world patterns and payment workflows +- ๐Ÿ”— [Zai: Wallet Accounts API Reference](https://developer.hellozai.com/reference) + +**Quick Example:** +```ruby +wallet_accounts = ZaiPayment::Resources::WalletAccount.new + +# Check wallet balance +response = wallet_accounts.show('wallet_account_id') +balance = response.data['balance'] # in cents +puts "Balance: $#{balance / 100.0}" + +# Pay a bill from wallet to BPay account +payment_response = wallet_accounts.pay_bill( + 'wallet_account_id', + account_id: 'bpay_account_id', + amount: 17300, # $173.00 in cents + reference_id: 'bill_nov_2024' +) + +if payment_response.success? + disbursement = payment_response.data + puts "Payment successful: #{disbursement['id']}" + puts "State: #{disbursement['state']}" +end +``` + ### Token Auth Generate secure tokens for collecting bank and card account information. @@ -197,11 +241,12 @@ end | โœ… Users | Manage PayIn / PayOut users | Done | | โœ… Items | Transactions/payments (CRUD) | Done | | โœ… Bank Accounts | AU/UK bank accounts + validation | Done | +| โœ… BPay Accounts | Manage BPay accounts | Done | +| โœ… Wallet Accounts | Show, check balance, pay bills | Done | | โœ… Token Auth | Generate bank/card tokens | Done | | โœ… Batch Transactions (Prelive) | Simulate batch processing flows | Done | -| ๐Ÿ’ณ Payments | Single and recurring payments | ๐Ÿšง In progress | +| โœ… Payments | Single and recurring payments | Done | | ๐Ÿฆ Virtual Accounts (VA / PIPU) | Manage virtual accounts & PayTo | โณ Planned | -| ๐Ÿ’ผ Wallets | Create and manage wallet accounts | โณ Planned | ## ๐Ÿงช Development @@ -258,6 +303,8 @@ Everyone interacting in the ZaiPayment project's codebases, issue trackers, chat - [**User Management Guide**](docs/users.md) - Managing payin and payout users - [**Item Management Guide**](docs/items.md) - Creating and managing transactions/payments - [**Bank Account Guide**](docs/bank_accounts.md) - Managing bank accounts for AU/UK users +- [**BPay Account Guide**](docs/bpay_accounts.md) - Managing BPay accounts for Australian bill payments +- [**Wallet Account Guide**](docs/wallet_accounts.md) - Managing wallet accounts, checking balances, and paying bills - [**Webhook Examples**](examples/webhooks.md) - Complete webhook usage guide - [**Documentation Index**](docs/readme.md) - Full documentation navigation @@ -265,6 +312,8 @@ Everyone interacting in the ZaiPayment project's codebases, issue trackers, chat - [User Examples](examples/users.md) - Real-world user management patterns - [Item Examples](examples/items.md) - Transaction and payment workflows - [Bank Account Examples](examples/bank_accounts.md) - Bank account integration patterns +- [BPay Account Examples](examples/bpay_accounts.md) - BPay account integration patterns +- [Wallet Account Examples](examples/wallet_accounts.md) - Wallet account and bill payment workflows - [Token Auth Examples](examples/token_auths.md) - Secure token generation and integration - [Webhook Examples](examples/webhooks.md) - Webhook integration patterns - [Batch Transaction Examples](examples/batch_transactions.md) - Testing batch transaction flows (prelive only) diff --git a/spec/zai_payment/resources/wallet_account_spec.rb b/spec/zai_payment/resources/wallet_account_spec.rb new file mode 100644 index 0000000..4a78d1c --- /dev/null +++ b/spec/zai_payment/resources/wallet_account_spec.rb @@ -0,0 +1,408 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ZaiPayment::Resources::WalletAccount do + let(:stubs) { Faraday::Adapter::Test::Stubs.new } + let(:wallet_account_resource) { described_class.new(client: test_client) } + + let(:test_client) do + config = ZaiPayment::Config.new.tap do |c| + c.environment = :prelive + c.client_id = 'test_client_id' + c.client_secret = 'test_client_secret' + c.scope = 'test_scope' + end + + token_provider = instance_double(ZaiPayment::Auth::TokenProvider, bearer_token: 'Bearer test_token') + client = ZaiPayment::Client.new(config: config, token_provider: token_provider, base_endpoint: :core_base) + + test_connection = Faraday.new do |faraday| + faraday.request :json + faraday.response :json, content_type: /\bjson$/ + faraday.adapter :test, stubs + end + + allow(client).to receive(:connection).and_return(test_connection) + client + end + + after do + stubs.verify_stubbed_calls + end + + describe '#show' do + let(:wallet_account_data) do + { + 'wallet_accounts' => { + 'id' => '5c1c6b10-4c56-0137-8cd7-0242ac110002', + 'active' => true, + 'created_at' => '2019-04-29T02:42:31.536Z', + 'updated_at' => '2020-05-03T12:01:02.254Z', + 'balance' => 663_337, + 'currency' => 'AUD', + 'links' => { + 'self' => '/transactions/aed45af0-6f63-0138-901c-0a58a9feac03/wallet_accounts', + 'users' => '/wallet_accounts/5c1c6b10-4c56-0137-8cd7-0242ac110002/users', + 'batch_transactions' => '/wallet_accounts/5c1c6b10-4c56-0137-8cd7-0242ac110002/batch_transactions', + 'transactions' => '/wallet_accounts/5c1c6b10-4c56-0137-8cd7-0242ac110002/transactions', + 'bpay_details' => '/wallet_accounts/5c1c6b10-4c56-0137-8cd7-0242ac110002/bpay_details', + 'npp_details' => '/wallet_accounts/5c1c6b10-4c56-0137-8cd7-0242ac110002/npp_details', + 'virtual_accounts' => '/wallet_accounts/5c1c6b10-4c56-0137-8cd7-0242ac110002/virtual_accounts' + } + } + } + end + + context 'when wallet account exists' do + before do + stubs.get('/wallet_accounts/5c1c6b10-4c56-0137-8cd7-0242ac110002') do + [200, { 'Content-Type' => 'application/json' }, wallet_account_data] + end + end + + it 'returns the correct response type and wallet account details' do + response = wallet_account_resource.show('5c1c6b10-4c56-0137-8cd7-0242ac110002') + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + expect(response.data['id']).to eq('5c1c6b10-4c56-0137-8cd7-0242ac110002') + end + end + + context 'when wallet account does not exist' do + before do + stubs.get('/wallet_accounts/invalid_id') do + [404, { 'Content-Type' => 'application/json' }, { 'errors' => 'Not found' }] + end + end + + it 'raises a NotFoundError' do + expect { wallet_account_resource.show('invalid_id') } + .to raise_error(ZaiPayment::Errors::NotFoundError) + end + end + + context 'when wallet_account_id is blank' do + it 'raises a ValidationError for empty string' do + expect { wallet_account_resource.show('') } + .to raise_error(ZaiPayment::Errors::ValidationError, /wallet_account_id/) + end + + it 'raises a ValidationError for nil' do + expect { wallet_account_resource.show(nil) } + .to raise_error(ZaiPayment::Errors::ValidationError, /wallet_account_id/) + end + end + end + + describe '#show_user' do + let(:user_data) do + { + 'users' => { + 'created_at' => '2020-04-03T07:59:00.379Z', + 'updated_at' => '2020-04-03T07:59:00.379Z', + 'id' => 'Seller_1234', + 'full_name' => 'Samuel Seller', + 'email' => 'sam@example.com', + 'mobile' => 69_543_131, + 'first_name' => 'Samuel', + 'last_name' => 'Seller', + 'custom_descriptor' => 'Sam Garden Jobs', + 'location' => 'AUS', + 'verification_state' => 'pending', + 'held_state' => false, + 'roles' => ['customer'], + 'links' => { + 'self' => '/wallet_accounts/901d8cd0-6af3-0138-967d-0a58a9feac04/users', + 'items' => '/users/e6bc0480-57ae-0138-c46e-0a58a9feac03/items' + } + } + } + end + + context 'when Wallet Account has an associated user' do + before do + stubs.get('/wallet_accounts/901d8cd0-6af3-0138-967d-0a58a9feac04/users') do + [200, { 'Content-Type' => 'application/json' }, user_data] + end + end + + it 'returns the correct response type and user details' do + response = wallet_account_resource.show_user('901d8cd0-6af3-0138-967d-0a58a9feac04') + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + expect(response.data['id']).to eq('Seller_1234') + end + end + + context 'when Wallet Account does not exist' do + before do + stubs.get('/wallet_accounts/invalid_id/users') do + [404, { 'Content-Type' => 'application/json' }, { 'errors' => 'Not found' }] + end + end + + it 'raises a NotFoundError' do + expect { wallet_account_resource.show_user('invalid_id') } + .to raise_error(ZaiPayment::Errors::NotFoundError) + end + end + + context 'when wallet_account_id is blank' do + it 'raises a ValidationError for empty string' do + expect { wallet_account_resource.show_user('') } + .to raise_error(ZaiPayment::Errors::ValidationError, /wallet_account_id/) + end + + it 'raises a ValidationError for nil' do + expect { wallet_account_resource.show_user(nil) } + .to raise_error(ZaiPayment::Errors::ValidationError, /wallet_account_id/) + end + end + end + + describe '#show_npp_details' do + let(:npp_details_data) do + { + 'wallet_accounts' => { + 'id' => '5c1c6b10-4c56-0137-8cd7-0242ac110002', + 'npp_details' => { + 'pay_id' => 'npp@assemblypayments.com', + 'marketplace_pay_ids' => [ + { + 'pay_id' => 'npp@assemblypayments.com', + 'type' => 'emal' + }, + { + 'pay_id' => 'Assembly Payments', + 'type' => 'orgn' + }, + { + 'pay_id' => 96_637_632_645, + 'type' => 'aubn' + } + ], + 'reference' => '100014012148074', + 'amount' => '$0.00', + 'currency' => 'AUD' + } + } + } + end + + context 'when Wallet Account has NPP details' do + before do + stubs.get('/wallet_accounts/5c1c6b10-4c56-0137-8cd7-0242ac110002/npp_details') do + [200, { 'Content-Type' => 'application/json' }, npp_details_data] + end + end + + it 'returns the correct response type and NPP details' do + response = wallet_account_resource.show_npp_details('5c1c6b10-4c56-0137-8cd7-0242ac110002') + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + expect(response.data['id']).to eq('5c1c6b10-4c56-0137-8cd7-0242ac110002') + end + end + + context 'when Wallet Account does not exist' do + before do + stubs.get('/wallet_accounts/invalid_id/npp_details') do + [404, { 'Content-Type' => 'application/json' }, { 'errors' => 'Not found' }] + end + end + + it 'raises a NotFoundError' do + expect { wallet_account_resource.show_npp_details('invalid_id') } + .to raise_error(ZaiPayment::Errors::NotFoundError) + end + end + + context 'when wallet_account_id is blank' do + it 'raises a ValidationError for empty string' do + expect { wallet_account_resource.show_npp_details('') } + .to raise_error(ZaiPayment::Errors::ValidationError, /wallet_account_id/) + end + + it 'raises a ValidationError for nil' do + expect { wallet_account_resource.show_npp_details(nil) } + .to raise_error(ZaiPayment::Errors::ValidationError, /wallet_account_id/) + end + end + end + + describe '#show_bpay_details' do + let(:bpay_details_data) do + { + 'wallet_accounts' => { + 'id' => '5c1c6b10-4c56-0137-8cd7-0242ac110002', + 'bpay_details' => { + 'biller_code' => '230680', + 'reference' => '100014012148074', + 'amount' => '$0.00', + 'currency' => 'AUD' + } + } + } + end + + context 'when Wallet Account has BPay details' do + before do + stubs.get('/wallet_accounts/5c1c6b10-4c56-0137-8cd7-0242ac110002/bpay_details') do + [200, { 'Content-Type' => 'application/json' }, bpay_details_data] + end + end + + it 'returns the correct response type and BPay details' do + response = wallet_account_resource.show_bpay_details('5c1c6b10-4c56-0137-8cd7-0242ac110002') + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + expect(response.data['id']).to eq('5c1c6b10-4c56-0137-8cd7-0242ac110002') + end + end + + context 'when Wallet Account does not exist' do + before do + stubs.get('/wallet_accounts/invalid_id/bpay_details') do + [404, { 'Content-Type' => 'application/json' }, { 'errors' => 'Not found' }] + end + end + + it 'raises a NotFoundError' do + expect { wallet_account_resource.show_bpay_details('invalid_id') } + .to raise_error(ZaiPayment::Errors::NotFoundError) + end + end + + context 'when wallet_account_id is blank' do + it 'raises a ValidationError for empty string' do + expect { wallet_account_resource.show_bpay_details('') } + .to raise_error(ZaiPayment::Errors::ValidationError, /wallet_account_id/) + end + + it 'raises a ValidationError for nil' do + expect { wallet_account_resource.show_bpay_details(nil) } + .to raise_error(ZaiPayment::Errors::ValidationError, /wallet_account_id/) + end + end + end + + describe '#pay_bill' do + let(:disbursement_data) do + { + 'disbursements' => { + 'reference_id' => 'test100', + 'id' => '8a31ebfa-421b-4cbb-9241-632f71b3778a', + 'amount' => 173, + 'currency' => 'AUD', + 'created_at' => '2020-05-09T07:09:03.383Z', + 'updated_at' => '2020-05-09T07:09:04.585Z', + 'state' => 'pending', + 'to' => 'BPay Account', + 'account_name' => 'My Water Company', + 'biller_name' => 'ABC Water', + 'biller_code' => 123_456, + 'crn' => '0987654321', + 'links' => { + 'transactions' => '/disbursements/8a31ebfa-421b-4cbb-9241-632f71b3778a/transactions', + 'wallet_accounts' => '/disbursements/8a31ebfa-421b-4cbb-9241-632f71b3778a/wallet_accounts', + 'bank_accounts' => '/disbursements/8a31ebfa-421b-4cbb-9241-632f71b3778a/bank_accounts', + 'bpay_accounts' => '/disbursements/8a31ebfa-421b-4cbb-9241-632f71b3778a/bpay_accounts', + 'paypal_accounts' => '/disbursements/8a31ebfa-421b-4cbb-9241-632f71b3778a/paypal_accounts', + 'items' => '/disbursements/8a31ebfa-421b-4cbb-9241-632f71b3778a/items', + 'users' => '/disbursements/8a31ebfa-421b-4cbb-9241-632f71b3778a/users' + } + } + } + end + + let(:valid_params) do + { + account_id: 'c1824ad0-73f1-0138-3700-0a58a9feac09', + amount: 173, + reference_id: 'test100' + } + end + + context 'when successful' do + before do + stubs.post('/wallet_accounts/901d8cd0-6af3-0138-967d-0a58a9feac04/bill_payment') do + [201, { 'Content-Type' => 'application/json' }, disbursement_data] + end + end + + it 'returns the correct response type and creates disbursement' do + response = wallet_account_resource.pay_bill('901d8cd0-6af3-0138-967d-0a58a9feac04', **valid_params) + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + expect(response.data['id']).to eq('8a31ebfa-421b-4cbb-9241-632f71b3778a') + end + end + + context 'with optional reference_id' do + before do + stubs.post('/wallet_accounts/901d8cd0-6af3-0138-967d-0a58a9feac04/bill_payment') do + [201, { 'Content-Type' => 'application/json' }, disbursement_data] + end + end + + it 'includes reference_id in the request' do + response = wallet_account_resource.pay_bill('901d8cd0-6af3-0138-967d-0a58a9feac04', **valid_params) + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + expect(response.data['reference_id']).to eq('test100') + end + end + + context 'when validation fails' do + it 'raises error when required fields are missing' do + expect do + wallet_account_resource.pay_bill('901d8cd0-6af3-0138-967d-0a58a9feac04', account_id: 'test123') + end.to raise_error(ZaiPayment::Errors::ValidationError, /Missing required fields/) + end + + it 'raises error for invalid amount' do + params = valid_params.merge(amount: -100) + expect { wallet_account_resource.pay_bill('901d8cd0-6af3-0138-967d-0a58a9feac04', **params) } + .to raise_error(ZaiPayment::Errors::ValidationError, /amount must be a positive integer/) + end + + it 'raises error for reference_id with single quote' do + params = valid_params.merge(reference_id: "test'100") + expect { wallet_account_resource.pay_bill('901d8cd0-6af3-0138-967d-0a58a9feac04', **params) } + .to raise_error(ZaiPayment::Errors::ValidationError, /reference_id cannot contain single quote/) + end + end + + context 'when wallet_account_id is blank' do + it 'raises a ValidationError for empty string' do + expect { wallet_account_resource.pay_bill('', **valid_params) } + .to raise_error(ZaiPayment::Errors::ValidationError, /wallet_account_id/) + end + + it 'raises a ValidationError for nil' do + expect { wallet_account_resource.pay_bill(nil, **valid_params) } + .to raise_error(ZaiPayment::Errors::ValidationError, /wallet_account_id/) + end + end + + context 'when wallet account does not exist' do + before do + stubs.post('/wallet_accounts/invalid_id/bill_payment') do + [404, { 'Content-Type' => 'application/json' }, { 'errors' => 'Not found' }] + end + end + + it 'raises a NotFoundError' do + expect { wallet_account_resource.pay_bill('invalid_id', **valid_params) } + .to raise_error(ZaiPayment::Errors::NotFoundError) + end + end + end +end