diff --git a/Gemfile.lock b/Gemfile.lock index b394542..3e25c11 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - zai_payment (2.4.0) + zai_payment (2.5.0) base64 (~> 0.3.0) faraday (~> 2.0) openssl (~> 3.3) diff --git a/changelog.md b/changelog.md index 70ed1cf..1efd4ae 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,45 @@ ## [Released] + +## [2.5.0] - 2025-11-03 + +### Added +- **Bank Account Resource**: Complete CRUD operations plus validation for Australian and UK bank accounts 🏦 + - `ZaiPayment.bank_accounts.show(bank_account_id, include_decrypted_fields:)` - Get bank account details with optional decrypted fields + - `ZaiPayment.bank_accounts.create_au(user_id:, bank_name:, account_name:, routing_number:, account_number:, account_type:, holder_type:, country:)` - Create Australian bank account + - `ZaiPayment.bank_accounts.create_uk(user_id:, bank_name:, account_name:, routing_number:, account_number:, account_type:, holder_type:, country:, iban:, swift_code:)` - Create UK bank account with IBAN and SWIFT code + - `ZaiPayment.bank_accounts.redact(bank_account_id)` - Redact (deactivate) a bank account + - `ZaiPayment.bank_accounts.validate_routing_number(routing_number)` - Validate US bank routing numbers and get bank information + - Support for retrieving decrypted sensitive fields (full account numbers) + - Support for savings and checking account types + - Support for personal and business holder types + - Comprehensive validation for required fields and formats + - Full RSpec test suite with 25 test examples + - Comprehensive documentation in `docs/bank_accounts.md` + - Practical examples in `examples/bank_accounts.md` + +### Documentation +- **Bank Accounts Guide** (`docs/bank_accounts.md`): + - Complete guide for showing, creating, validating, and redacting bank accounts + - Documentation for decrypted fields parameter + - Validation endpoint documentation for US routing numbers + - Redaction endpoint documentation with warnings + - Validation rules for account types, holder types, and country codes + - Response structure documentation + - Error handling examples + - Use cases for disbursement accounts, multi-currency setups, and routing number validation +- **Bank Accounts Examples** (`examples/bank_accounts.md`): + - Examples for retrieving bank account details (masked and decrypted) + - Examples for redacting bank accounts with error handling (Examples 9-10) + - Examples for validating US routing numbers (Examples 11-13) + - Real-world scenarios for Australian bank accounts + - UK bank account creation with IBAN/SWIFT + - Business account setup examples + - Multi-region account management patterns + - Routing number validation integration in forms + - Security best practices for handling decrypted data + +**Full Changelog**: https://github.com/Sentia/zai-payment/compare/v2.4.0...v2.5.0 + ## [2.4.0] - 2025-11-02 ### Added - **Item Payment Actions API**: Advanced payment operations for managing item transactions 💳 diff --git a/docs/bank_accounts.md b/docs/bank_accounts.md new file mode 100644 index 0000000..f355f72 --- /dev/null +++ b/docs/bank_accounts.md @@ -0,0 +1,494 @@ +# Bank Account Management + +The BankAccount resource provides methods for managing Zai bank accounts for both Australian and UK regions. + +## Overview + +Bank accounts are used as either a funding source or a disbursement destination. Once created, store the returned `:id` and use it for a `make_payment` Item Action call. The `:id` is also referred to as a `:token` when invoking Bank Accounts. + +For platforms operating in the UK, `iban` and `swift_code` are extra required fields in addition to the standard Australian fields. + +## References + +- [Create Bank Account API](https://developer.hellozai.com/reference/createbankaccount) +- [Bank Account Formats by Country](https://developer.hellozai.com/docs/bank-account-formats) + +## Usage + +### Initialize the BankAccount Resource + +```ruby +# Using a new instance +bank_accounts = ZaiPayment::Resources::BankAccount.new + +# Or use with custom client +client = ZaiPayment::Client.new +bank_accounts = ZaiPayment::Resources::BankAccount.new(client: client) +``` + +## Methods + +### Show Bank Account + +Get details of a specific bank account by ID. + +#### Parameters + +- `bank_account_id` (required) - The bank account ID +- `include_decrypted_fields` (optional) - Boolean. If true, the API will decrypt and return sensitive bank account fields (for example, the full account number). Defaults to false. + +#### Example + +```ruby +# Basic usage (returns masked account numbers) +response = bank_accounts.show('bank_account_id') + +# Access bank account details +bank_account = response.data +puts bank_account['id'] +puts bank_account['active'] +puts bank_account['verification_status'] +puts bank_account['currency'] + +# Access bank details (account numbers are masked) +bank = bank_account['bank'] +puts bank['bank_name'] +puts bank['account_name'] +puts bank['account_type'] +puts bank['account_number'] # => "XXX234" (masked) +``` + +#### Example with Decrypted Fields + +```ruby +# Request with decrypted fields +response = bank_accounts.show('bank_account_id', include_decrypted_fields: true) + +# Access bank details (account numbers are decrypted) +bank = response.data['bank'] +puts bank['account_number'] # => "12345678" (full number) +puts bank['routing_number'] # => "111123" (full number) +``` + +### Redact Bank Account + +Redact a bank account using the given bank_account_id. Redacted bank accounts can no longer be used as a funding source or a disbursement destination. + +#### Parameters + +- `bank_account_id` (required) - The bank account ID + +#### Example + +```ruby +response = bank_accounts.redact('bank_account_id') + +if response.success? + puts "Bank account successfully redacted" +else + puts "Failed to redact bank account" +end +``` + +#### Response + +```ruby +{ + "bank_account" => "Successfully redacted" +} +``` + +**Important Notes:** +- Once redacted, the bank account cannot be used for payments or disbursements +- This action cannot be undone +- Use with caution + +### Validate Routing Number + +Validate a US bank routing number before creating an account. This can be used to provide on-demand verification and further information of the bank information a user is providing. + +#### Parameters + +- `routing_number` (required) - The US bank routing number (9 digits) + +#### Example + +```ruby +response = bank_accounts.validate_routing_number('122235821') + +if response.success? + routing_info = response.data + puts "Routing Number: #{routing_info['routing_number']}" + puts "Bank Name: #{routing_info['customer_name']}" + puts "City: #{routing_info['city']}" + puts "State: #{routing_info['state_code']}" + puts "ZIP: #{routing_info['zip']}" + puts "Phone: #{routing_info['phone_area_code']}-#{routing_info['phone_prefix']}-#{routing_info['phone_suffix']}" +else + puts "Invalid routing number" +end +``` + +#### Response + +```ruby +{ + "routing_number" => "122235821", + "customer_name" => "US BANK NA", + "address" => "EP-MN-WN1A", + "city" => "ST. PAUL", + "state_code" => "MN", + "zip" => "55107", + "zip_extension" => "1419", + "phone_area_code" => "800", + "phone_prefix" => "937", + "phone_suffix" => "631" +} +``` + +**Use Cases:** +- Validate routing numbers before bank account creation +- Display bank information to users for confirmation +- Provide real-time feedback during form entry +- Reduce errors in bank account setup + +### Create Australian Bank Account + +Create a new bank account for an Australian user. + +#### Required Fields + +- `user_id` - User ID (defaults to aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee) +- `bank_name` - Bank name (defaults to Bank of Australia) +- `account_name` - Account name (defaults to Samuel Seller) +- `routing_number` - Routing number / BSB number (defaults to 111123). See [Bank account formats by country](https://developer.hellozai.com/docs/bank-account-formats). +- `account_number` - Account number (defaults to 111234). See [Bank account formats by country](https://developer.hellozai.com/docs/bank-account-formats). +- `account_type` - Bank account type ('savings' or 'checking', defaults to checking) +- `holder_type` - Holder type ('personal' or 'business', defaults to personal) +- `country` - ISO 3166-1 alpha-3 country code (length ≤ 3, defaults to AUS) + +#### Optional Fields + +- `payout_currency` - ISO 4217 alpha-3 currency code for payouts +- `currency` - ISO 4217 alpha-3 currency code. This is an optional field and if not provided, the item will be created with the default currency of the marketplace. + +#### Example + +```ruby +response = bank_accounts.create_au( + user_id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + bank_name: 'Bank of Australia', + account_name: 'Samuel Seller', + routing_number: '111123', + account_number: '111234', + account_type: 'checking', + holder_type: 'personal', + country: 'AUS', + payout_currency: 'AUD', + currency: 'AUD' +) + +if response.success? + bank_account = response.data + puts "Bank Account ID: #{bank_account['id']}" + puts "Active: #{bank_account['active']}" + puts "Verification Status: #{bank_account['verification_status']}" + puts "Currency: #{bank_account['currency']}" + + # Access bank details + bank = bank_account['bank'] + puts "Bank Name: #{bank['bank_name']}" + puts "Account Name: #{bank['account_name']}" + puts "Account Type: #{bank['account_type']}" + puts "Holder Type: #{bank['holder_type']}" + puts "Routing Number: #{bank['routing_number']}" + puts "Direct Debit Status: #{bank['direct_debit_authority_status']}" + + # Access links + links = bank_account['links'] + puts "Self Link: #{links['self']}" + puts "Users Link: #{links['users']}" +end +``` + +### Create UK Bank Account + +Create a new bank account for a UK user. UK bank accounts require additional fields: `iban` and `swift_code`. + +#### Required Fields + +All fields from Australian bank accounts plus: + +- `iban` - International Bank Account Number (required for UK) +- `swift_code` - SWIFT Code / BIC (required for UK) + +#### Example + +```ruby +response = bank_accounts.create_uk( + user_id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + bank_name: 'Bank of UK', + account_name: 'Samuel Seller', + routing_number: '111123', + account_number: '111234', + account_type: 'checking', + holder_type: 'personal', + country: 'GBR', + iban: 'GB25QHWM02498765432109', + swift_code: 'BUKBGB22', + payout_currency: 'GBP', + currency: 'GBP' +) + +if response.success? + bank_account = response.data + puts "Bank Account ID: #{bank_account['id']}" + + bank = bank_account['bank'] + puts "IBAN: #{bank['iban']}" + puts "SWIFT Code: #{bank['swift_code']}" +end +``` + +## Response Structure + +Both methods return a `ZaiPayment::Response` object with the following structure: + +```ruby +{ + "bank_accounts" => { + "id" => "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "active" => true, + "verification_status" => "not_verified", + "currency" => "AUD", + "bank" => { + "bank_name" => "Bank of Australia", + "country" => "AUS", + "account_name" => "Samuel Seller", + "routing_number" => "XXXXX3", # Masked for security + "account_number" => "XXX234", # Masked for security + "iban" => "null,", # Or actual IBAN for UK + "swift_code" => "null,", # Or actual SWIFT for UK + "holder_type" => "personal", + "account_type" => "checking", + "direct_debit_authority_status" => "approved" + }, + "links" => { + "self" => "/bank_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "users" => "/bank_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/users", + "direct_debit_authorities" => "/bank_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/direct_debit_authorities" + } + } +} +``` + +## Validation Rules + +### Account Type + +Must be one of: +- `savings` - Savings account +- `checking` - Checking/current account + +### Holder Type + +Must be one of: +- `personal` - Personal/individual account +- `business` - Business/company account + +### Country Code + +Must be a valid ISO 3166-1 alpha-3 code (3 letters): +- Australia: `AUS` +- United Kingdom: `GBR` + +### Currency Code + +When provided, must be a valid ISO 4217 alpha-3 code: +- Australian Dollar: `AUD` +- British Pound: `GBP` +- US Dollar: `USD` + +## Error Handling + +The BankAccount resource raises the following errors: + +### ValidationError + +Raised when required fields are missing or invalid: + +```ruby +begin + bank_accounts.create_au( + user_id: 'user_123', + bank_name: 'Test Bank' + # Missing required fields + ) +rescue ZaiPayment::Errors::ValidationError => e + puts "Validation failed: #{e.message}" + # => "Missing required fields: account_name, routing_number, account_number, account_type, holder_type, country" +end +``` + +### Invalid Account Type + +```ruby +begin + bank_accounts.create_au( + user_id: 'user_123', + bank_name: 'Test Bank', + account_name: 'Test', + routing_number: '111123', + account_number: '111234', + account_type: 'invalid', + holder_type: 'personal', + country: 'AUS' + ) +rescue ZaiPayment::Errors::ValidationError => e + puts e.message + # => "account_type must be one of: savings, checking" +end +``` + +### Invalid Holder Type + +```ruby +begin + bank_accounts.create_au( + user_id: 'user_123', + bank_name: 'Test Bank', + account_name: 'Test', + routing_number: '111123', + account_number: '111234', + account_type: 'checking', + holder_type: 'invalid', + country: 'AUS' + ) +rescue ZaiPayment::Errors::ValidationError => e + puts e.message + # => "holder_type must be one of: personal, business" +end +``` + +### Invalid Country Code + +```ruby +begin + bank_accounts.create_au( + user_id: 'user_123', + bank_name: 'Test Bank', + account_name: 'Test', + routing_number: '111123', + account_number: '111234', + account_type: 'checking', + holder_type: 'personal', + country: 'INVALID' + ) +rescue ZaiPayment::Errors::ValidationError => e + puts e.message + # => "country must be a valid ISO 3166-1 alpha-3 code (e.g., AUS, GBR)" +end +``` + +## Use Cases + +### Use Case 1: Disbursement Account Setup + +After creating a payout user (seller), create a bank account for receiving payments: + +```ruby +# Step 1: Create payout user +users = ZaiPayment::Resources::User.new +user_response = users.create( + user_type: 'payout', + email: 'seller@example.com', + first_name: 'Jane', + last_name: 'Seller', + country: 'AUS', + dob: '01/01/1990', + address_line1: '123 Main St', + city: 'Sydney', + state: 'NSW', + zip: '2000' +) + +user_id = user_response.data['id'] + +# Step 2: Create bank account +bank_accounts = ZaiPayment::Resources::BankAccount.new +bank_response = bank_accounts.create_au( + user_id: user_id, + bank_name: 'Commonwealth Bank', + account_name: 'Jane Seller', + routing_number: '062000', + account_number: '12345678', + account_type: 'savings', + holder_type: 'personal', + country: 'AUS', + payout_currency: 'AUD' +) + +account_id = bank_response.data['id'] + +# Step 3: Set as disbursement account +users.set_disbursement_account(user_id, account_id) +``` + +### Use Case 2: Multi-Currency Business Account + +Create business bank accounts for different currencies: + +```ruby +bank_accounts = ZaiPayment::Resources::BankAccount.new + +# Australian business account +au_response = bank_accounts.create_au( + user_id: 'business_user_123', + bank_name: 'Westpac', + account_name: 'ABC Pty Ltd', + routing_number: '032000', + account_number: '87654321', + account_type: 'checking', + holder_type: 'business', + country: 'AUS', + payout_currency: 'AUD' +) + +# UK business account +uk_response = bank_accounts.create_uk( + user_id: 'business_user_123', + bank_name: 'Barclays', + account_name: 'ABC Limited', + routing_number: '200000', + account_number: '55779911', + account_type: 'checking', + holder_type: 'business', + country: 'GBR', + iban: 'GB33BUKB20000055779911', + swift_code: 'BARCGB22', + payout_currency: 'GBP' +) +``` + +## Important Notes + +1. **Security**: Account numbers and routing numbers are masked in API responses for security +2. **Verification**: New bank accounts typically have `verification_status: "not_verified"` until verified by Zai +3. **Direct Debit**: The `direct_debit_authority_status` indicates if direct debit is available for the account +4. **Token Usage**: The returned `id` can be used as a token for payment operations +5. **Region Differences**: + - Australia: Only requires standard banking details + - UK: Additionally requires IBAN and SWIFT code + +## Related Resources + +- [User Management](users.md) - Creating and managing users +- [Item Management](items.md) - Creating transactions/payments +- [Disbursement Accounts](users.md#set-disbursement-account) - Setting default payout accounts + +## Further Reading + +- [Bank Account Formats by Country](https://developer.hellozai.com/docs/bank-account-formats) +- [Payment Methods Guide](https://developer.hellozai.com/docs/payment-methods) +- [Verification Process](https://developer.hellozai.com/docs/verification) + diff --git a/examples/bank_accounts.md b/examples/bank_accounts.md new file mode 100644 index 0000000..1a4b230 --- /dev/null +++ b/examples/bank_accounts.md @@ -0,0 +1,637 @@ +# Bank Account Management Examples + +This document provides practical examples for managing bank accounts in Zai Payment. + +## Table of Contents + +- [Setup](#setup) +- [Show Bank Account Example](#show-bank-account-example) +- [Redact Bank Account Example](#redact-bank-account-example) +- [Validate Routing Number Example](#validate-routing-number-example) +- [Australian Bank Account Examples](#australian-bank-account-examples) +- [UK Bank Account Examples](#uk-bank-account-examples) +- [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 Bank Account Example + +### Example 1: Get Bank Account Details + +Retrieve details of a specific bank account. + +```ruby +# Get bank account details +bank_accounts = ZaiPayment::Resources::BankAccount.new + +response = bank_accounts.show('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee') + +if response.success? + bank_account = response.data + puts "Bank Account ID: #{bank_account['id']}" + puts "Active: #{bank_account['active']}" + puts "Verification Status: #{bank_account['verification_status']}" + puts "Currency: #{bank_account['currency']}" + + # Access bank details (numbers are masked by default) + bank = bank_account['bank'] + puts "\nBank Details:" + puts " Bank Name: #{bank['bank_name']}" + puts " Country: #{bank['country']}" + puts " Account Name: #{bank['account_name']}" + puts " Account Type: #{bank['account_type']}" + puts " Holder Type: #{bank['holder_type']}" + puts " Account Number: #{bank['account_number']}" # => "XXX234" (masked) + puts " Routing Number: #{bank['routing_number']}" # => "XXXXX3" (masked) + puts " Direct Debit Status: #{bank['direct_debit_authority_status']}" + + # Access links + links = bank_account['links'] + puts "\nLinks:" + puts " Self: #{links['self']}" + puts " Users: #{links['users']}" +else + puts "Failed to retrieve bank account" + puts "Error: #{response.error}" +end +``` + +### Example 2: Get Bank Account with Decrypted Fields + +Retrieve full, unmasked bank account details for secure operations. + +```ruby +# Get bank account details with decrypted fields +bank_accounts = ZaiPayment::Resources::BankAccount.new + +response = bank_accounts.show('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + include_decrypted_fields: true) + +if response.success? + bank_account = response.data + bank = bank_account['bank'] + + puts "Bank Account Details (Decrypted):" + puts " Account Name: #{bank['account_name']}" + puts " Full Account Number: #{bank['account_number']}" # => "12345678" (full number) + puts " Full Routing Number: #{bank['routing_number']}" # => "111123" (full number) + + # Important: Handle decrypted data securely + # - Don't log in production + # - Don't store in logs + # - Use for immediate processing only +else + puts "Failed to retrieve bank account" +end +``` + +## Redact Bank Account Example + +### Example 9: Redact a Bank Account + +Redact (deactivate) a bank account so it can no longer be used. + +```ruby +# Redact a bank account +bank_accounts = ZaiPayment::Resources::BankAccount.new + +response = bank_accounts.redact('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee') + +if response.success? + puts "Bank account successfully redacted" + puts "Response: #{response.data}" + # => {"bank_account"=>"Successfully redacted"} + + # The bank account can no longer be used for: + # - Funding source for payments + # - Disbursement destination +else + puts "Failed to redact bank account" + puts "Error: #{response.error}" +end +``` + +### Example 10: Redact with Error Handling + +Handle edge cases when redacting a bank account. + +```ruby +bank_accounts = ZaiPayment::Resources::BankAccount.new + +begin + # Attempt to redact the bank account + response = bank_accounts.redact('bank_account_id_here') + + if response.success? + puts "Bank account redacted successfully" + + # Log the action for audit purposes + Rails.logger.info("Bank account #{bank_account_id} was redacted at #{Time.now}") + else + puts "Redaction failed: #{response.error}" + end +rescue ZaiPayment::Errors::NotFoundError => e + puts "Bank account not found: #{e.message}" +rescue ZaiPayment::Errors::ValidationError => e + puts "Invalid bank account ID: #{e.message}" +rescue ZaiPayment::Errors::ApiError => e + puts "API error occurred: #{e.message}" +end +``` + +## Validate Routing Number Example + +### Example 11: Validate US Bank Routing Number + +Validate a US bank routing number to get bank information. + +```ruby +# Validate a routing number +bank_accounts = ZaiPayment::Resources::BankAccount.new + +response = bank_accounts.validate_routing_number('122235821') + +if response.success? + routing_info = response.data + + puts "Routing Number Validation Results:" + puts " Routing Number: #{routing_info['routing_number']}" + puts " Bank Name: #{routing_info['customer_name']}" + puts " Address: #{routing_info['address']}" + puts " City: #{routing_info['city']}" + puts " State: #{routing_info['state_code']}" + puts " ZIP: #{routing_info['zip']}-#{routing_info['zip_extension']}" + + # Format phone number + phone = "#{routing_info['phone_area_code']}-#{routing_info['phone_prefix']}-#{routing_info['phone_suffix']}" + puts " Phone: #{phone}" +else + puts "Invalid routing number" + puts "Error: #{response.error}" +end +``` + +### Example 12: Validate Routing Number Before Account Creation + +Use routing number validation as part of the account creation flow. + +```ruby +bank_accounts = ZaiPayment::Resources::BankAccount.new + +# Step 1: Validate the routing number +routing_number = '122235821' + +begin + validation_response = bank_accounts.validate_routing_number(routing_number) + + if validation_response.success? + bank_info = validation_response.data + + # Show user the bank information for confirmation + puts "You are creating an account with:" + puts " Bank: #{bank_info['customer_name']}" + puts " Location: #{bank_info['city']}, #{bank_info['state_code']}" + + # Step 2: Create the bank account if validation passes + account_response = bank_accounts.create_au( + user_id: 'user_123', + bank_name: bank_info['customer_name'], + account_name: 'John Doe', + routing_number: routing_number, + account_number: '12345678', + account_type: 'checking', + holder_type: 'personal', + country: 'USA' + ) + + if account_response.success? + puts "Bank account created successfully!" + puts "Account ID: #{account_response.data['id']}" + end + else + puts "Invalid routing number. Please check and try again." + end +rescue ZaiPayment::Errors::NotFoundError + puts "Routing number not found. Please verify the number." +rescue ZaiPayment::Errors::ValidationError => e + puts "Validation error: #{e.message}" +end +``` + +### Example 13: Real-time Routing Number Validation in Forms + +Implement real-time validation for user input. + +```ruby +# In a Rails controller or form handler +class BankAccountsController < ApplicationController + def validate_routing_number + routing_number = params[:routing_number] + + bank_accounts = ZaiPayment::Resources::BankAccount.new + + begin + response = bank_accounts.validate_routing_number(routing_number) + + if response.success? + bank_info = response.data + + # Return bank info to display in the form + render json: { + valid: true, + bank_name: bank_info['customer_name'], + city: bank_info['city'], + state: bank_info['state_code'], + message: "Valid routing number for #{bank_info['customer_name']}" + } + else + render json: { + valid: false, + message: 'Invalid routing number' + }, status: :unprocessable_entity + end + rescue ZaiPayment::Errors::NotFoundError + render json: { + valid: false, + message: 'Routing number not found' + }, status: :not_found + rescue ZaiPayment::Errors::ValidationError => e + render json: { + valid: false, + message: e.message + }, status: :bad_request + end + end +end +``` + +## Australian Bank Account Examples + +### Example 1: Create Basic Australian Bank Account + +Create a bank account for an Australian user with personal account details. + +```ruby +# Create an Australian bank account +bank_accounts = ZaiPayment::Resources::BankAccount.new + +response = bank_accounts.create_au( + user_id: 'user_abc123', + bank_name: 'Bank of Australia', + account_name: 'Samuel Seller', + routing_number: '111123', # BSB number + account_number: '111234', + account_type: 'checking', + holder_type: 'personal', + country: 'AUS' +) + +if response.success? + bank_account = response.data + puts "Bank account created successfully!" + puts "Account ID: #{bank_account['id']}" + puts "Verification Status: #{bank_account['verification_status']}" + puts "Account Name: #{bank_account['bank']['account_name']}" +else + puts "Failed to create bank account" + puts "Error: #{response.error}" +end +``` + +### Example 4: Australian Business Bank Account + +Create a business bank account for an Australian company. + +```ruby +bank_accounts = ZaiPayment::Resources::BankAccount.new + +response = bank_accounts.create_au( + user_id: 'user_company456', + bank_name: 'Commonwealth Bank', + account_name: 'ABC Company Pty Ltd', + routing_number: '062000', # BSB for Commonwealth Bank + account_number: '12345678', + account_type: 'checking', + holder_type: 'business', # Business account + country: 'AUS', + payout_currency: 'AUD' +) + +if response.success? + bank_account = response.data + puts "Business bank account created: #{bank_account['id']}" + puts "Holder Type: #{bank_account['bank']['holder_type']}" + puts "Direct Debit Status: #{bank_account['bank']['direct_debit_authority_status']}" +end +``` + +### Example 5: Australian Savings Account + +Create a savings account for disbursements. + +```ruby +bank_accounts = ZaiPayment::Resources::BankAccount.new + +response = bank_accounts.create_au( + user_id: 'user_saver789', + bank_name: 'National Australia Bank', + account_name: 'John Savings', + routing_number: '083000', # NAB BSB + account_number: '87654321', + account_type: 'savings', # Savings account + holder_type: 'personal', + country: 'AUS', + payout_currency: 'AUD', + currency: 'AUD' +) + +if response.success? + bank_account = response.data + puts "Savings account created: #{bank_account['id']}" + puts "Currency: #{bank_account['currency']}" +end +``` + +## UK Bank Account Examples + +### Example 6: Create Basic UK Bank Account + +Create a bank account for a UK user with IBAN and SWIFT code. + +```ruby +# Create a UK bank account +bank_accounts = ZaiPayment::Resources::BankAccount.new + +response = bank_accounts.create_uk( + user_id: 'user_uk123', + bank_name: 'Bank of UK', + account_name: 'Samuel Seller', + routing_number: '111123', # Sort code + account_number: '111234', + account_type: 'checking', + holder_type: 'personal', + country: 'GBR', + iban: 'GB25QHWM02498765432109', # Required for UK + swift_code: 'BUKBGB22' # Required for UK +) + +if response.success? + bank_account = response.data + puts "UK bank account created successfully!" + puts "Account ID: #{bank_account['id']}" + puts "IBAN: #{bank_account['bank']['iban']}" + puts "SWIFT: #{bank_account['bank']['swift_code']}" +else + puts "Failed to create UK bank account" + puts "Error: #{response.error}" +end +``` + +### Example 7: UK Business Bank Account + +Create a business bank account for a UK company with full details. + +```ruby +bank_accounts = ZaiPayment::Resources::BankAccount.new + +response = bank_accounts.create_uk( + user_id: 'user_uk_company456', + bank_name: 'Barclays Bank', + account_name: 'XYZ Limited', + routing_number: '200000', # Barclays sort code + account_number: '55779911', + account_type: 'checking', + holder_type: 'business', + country: 'GBR', + iban: 'GB33BUKB20000055779911', + swift_code: 'BARCGB22', + payout_currency: 'GBP', + currency: 'GBP' +) + +if response.success? + bank_account = response.data + puts "UK business account created: #{bank_account['id']}" + puts "Bank Name: #{bank_account['bank']['bank_name']}" + puts "Currency: #{bank_account['currency']}" +end +``` + +### Example 8: UK Savings Account with Full Details + +Create a UK savings account with all available information. + +```ruby +bank_accounts = ZaiPayment::Resources::BankAccount.new + +response = bank_accounts.create_uk( + user_id: 'user_uk_saver789', + bank_name: 'HSBC UK Bank', + account_name: 'Jane Smith', + routing_number: '400000', + account_number: '12345678', + account_type: 'savings', + holder_type: 'personal', + country: 'GBR', + iban: 'GB82HBUK40000012345678', + swift_code: 'HBUKGB4B', + payout_currency: 'GBP', + currency: 'GBP' +) + +if response.success? + bank_account = response.data + puts "UK savings account created: #{bank_account['id']}" + puts "Active: #{bank_account['active']}" + puts "Verification Status: #{bank_account['verification_status']}" +end +``` + +## Common Patterns + +### Pattern 1: Creating Bank Account After User Registration + +Typical workflow when onboarding a seller. + +```ruby +# Step 1: Create a payout user (seller) +users = ZaiPayment::Resources::User.new + +user_response = users.create( + user_type: 'payout', + email: 'seller@example.com', + first_name: 'Sarah', + last_name: 'Seller', + country: 'AUS', + dob: '15/01/1990', + address_line1: '123 Market St', + city: 'Sydney', + state: 'NSW', + zip: '2000', + mobile: '+61412345678' +) + +user_id = user_response.data['id'] +puts "User created: #{user_id}" + +# Step 2: Create bank account for the seller +bank_accounts = ZaiPayment::Resources::BankAccount.new + +bank_response = bank_accounts.create_au( + user_id: user_id, + bank_name: 'Bank of Australia', + account_name: 'Sarah Seller', + routing_number: '111123', + account_number: '111234', + account_type: 'checking', + holder_type: 'personal', + country: 'AUS', + payout_currency: 'AUD' +) + +if bank_response.success? + bank_account_id = bank_response.data['id'] + puts "Bank account created: #{bank_account_id}" + + # Step 3: Set as disbursement account + users.set_disbursement_account(user_id, bank_account_id) + puts "Disbursement account set successfully" +end +``` + +### Pattern 2: Error Handling + +Handle validation errors when creating bank accounts. + +```ruby +bank_accounts = ZaiPayment::Resources::BankAccount.new + +begin + response = bank_accounts.create_au( + user_id: 'user_123', + bank_name: 'Test Bank', + account_name: 'Test User', + routing_number: '111123', + account_number: '111234', + account_type: 'invalid_type', # This will cause an error + holder_type: 'personal', + country: 'AUS' + ) +rescue ZaiPayment::Errors::ValidationError => e + puts "Validation error: #{e.message}" +rescue ZaiPayment::Errors::ApiError => e + puts "API error: #{e.message}" + puts "Status code: #{e.status_code}" +end +``` + +### Pattern 3: Multi-Region Setup + +Create bank accounts for users in different regions. + +```ruby +bank_accounts = ZaiPayment::Resources::BankAccount.new + +# Helper method to create region-specific bank account +def create_bank_account_for_region(bank_accounts, user_id, region, account_details) + case region + when :australia + bank_accounts.create_au( + user_id: user_id, + **account_details + ) + when :uk + bank_accounts.create_uk( + user_id: user_id, + **account_details + ) + else + raise "Unsupported region: #{region}" + end +end + +# Australian user +aus_response = create_bank_account_for_region( + bank_accounts, + 'user_aus_123', + :australia, + { + bank_name: 'Bank of Australia', + account_name: 'AU User', + routing_number: '111123', + account_number: '111234', + account_type: 'checking', + holder_type: 'personal', + country: 'AUS', + payout_currency: 'AUD' + } +) + +puts "Australian account: #{aus_response.data['id']}" if aus_response.success? + +# UK user +uk_response = create_bank_account_for_region( + bank_accounts, + 'user_uk_456', + :uk, + { + bank_name: 'UK Bank', + account_name: 'UK User', + routing_number: '111123', + account_number: '111234', + account_type: 'checking', + holder_type: 'personal', + country: 'GBR', + iban: 'GB25QHWM02498765432109', + swift_code: 'BUKBGB22', + payout_currency: 'GBP' + } +) + +puts "UK account: #{uk_response.data['id']}" if uk_response.success? +``` + +## Important Notes + +1. **Required Fields for Australia**: + - `user_id`, `bank_name`, `account_name`, `routing_number` (BSB), `account_number`, `account_type`, `holder_type`, `country` + +2. **Required Fields for UK** (includes all AU fields plus): + - `iban` - International Bank Account Number + - `swift_code` - SWIFT/BIC code + +3. **Account Types**: + - `checking` - Current/checking account + - `savings` - Savings account + +4. **Holder Types**: + - `personal` - Personal/individual account + - `business` - Business/company account + +5. **Country Codes**: + - Use ISO 3166-1 alpha-3 codes (3 letters) + - Australia: `AUS` + - United Kingdom: `GBR` + +6. **Currency Codes**: + - Use ISO 4217 alpha-3 codes + - Australian Dollar: `AUD` + - British Pound: `GBP` + +7. **Bank Account Usage**: + - Store the returned `:id` for future use in disbursements + - Use `set_disbursement_account` to set the default payout account + - The `:id` is also referred to as a `:token` when invoking bank accounts + diff --git a/lib/zai_payment.rb b/lib/zai_payment.rb index d0b4734..1cb8d2f 100644 --- a/lib/zai_payment.rb +++ b/lib/zai_payment.rb @@ -14,6 +14,7 @@ require_relative 'zai_payment/resources/user' require_relative 'zai_payment/resources/item' require_relative 'zai_payment/resources/token_auth' +require_relative 'zai_payment/resources/bank_account' module ZaiPayment class << self @@ -57,5 +58,10 @@ def items def token_auths @token_auths ||= Resources::TokenAuth.new(client: Client.new(base_endpoint: :core_base)) end + + # @return [ZaiPayment::Resources::BankAccount] bank_account resource instance + def bank_accounts + @bank_accounts ||= Resources::BankAccount.new(client: Client.new(base_endpoint: :core_base)) + end end end diff --git a/lib/zai_payment/resources/bank_account.rb b/lib/zai_payment/resources/bank_account.rb new file mode 100644 index 0000000..63ba61f --- /dev/null +++ b/lib/zai_payment/resources/bank_account.rb @@ -0,0 +1,295 @@ +# frozen_string_literal: true + +module ZaiPayment + module Resources + # BankAccount resource for managing Zai bank accounts + # + # @see https://developer.hellozai.com/reference/createbankaccount + class BankAccount + attr_reader :client + + # Map of attribute keys to API field names + FIELD_MAPPING = { + user_id: :user_id, + bank_name: :bank_name, + account_name: :account_name, + routing_number: :routing_number, + account_number: :account_number, + account_type: :account_type, + holder_type: :holder_type, + country: :country, + payout_currency: :payout_currency, + currency: :currency + }.freeze + + # Map of UK-specific attribute keys to API field names + UK_FIELD_MAPPING = { + user_id: :user_id, + bank_name: :bank_name, + account_name: :account_name, + routing_number: :routing_number, + account_number: :account_number, + account_type: :account_type, + holder_type: :holder_type, + country: :country, + payout_currency: :payout_currency, + currency: :currency, + iban: :iban, + swift_code: :swift_code + }.freeze + + # Valid account types + VALID_ACCOUNT_TYPES = %w[savings checking].freeze + + # Valid holder types + VALID_HOLDER_TYPES = %w[personal business].freeze + + def initialize(client: nil) + @client = client || Client.new + end + + # Get a specific bank account by ID + # + # @param bank_account_id [String] the bank account ID + # @param include_decrypted_fields [Boolean] if true, the API will decrypt and return + # sensitive bank account fields (for example, the full account number). Defaults to false + # @return [Response] the API response containing bank account details + # + # @example + # bank_accounts = ZaiPayment::Resources::BankAccount.new + # response = bank_accounts.show("bank_account_id") + # response.data # => {"id" => "bank_account_id", "active" => true, ...} + # + # @example with decrypted fields + # response = bank_accounts.show("bank_account_id", include_decrypted_fields: true) + # # Returns full account number instead of masked version + # + # @see https://developer.hellozai.com/reference/showbankaccount + def show(bank_account_id, include_decrypted_fields: false) + validate_id!(bank_account_id, 'bank_account_id') + + params = {} + params[:include_decrypted_fields] = include_decrypted_fields if include_decrypted_fields + + client.get("/bank_accounts/#{bank_account_id}", params: params) + end + + # Create a new bank account for Australia + # + # @param attributes [Hash] bank account attributes + # @option attributes [String] :user_id (Required) User ID + # @option attributes [String] :bank_name (Required) Bank name (defaults to Bank of Australia) + # @option attributes [String] :account_name (Required) Account name (defaults to Samuel Seller) + # @option attributes [String] :routing_number (Required) Routing number / BSB number + # (defaults to 111123) + # @option attributes [String] :account_number (Required) Account number + # (defaults to 111234) + # @option attributes [String] :account_type (Required) Account type + # ('savings' or 'checking', defaults to checking) + # @option attributes [String] :holder_type (Required) Holder type ('personal' or 'business', defaults to personal) + # @option attributes [String] :country (Required) Country code (ISO 3166-1 alpha-3, max 3 chars, defaults to AUS) + # @option attributes [String] :payout_currency Optional currency code for payouts (ISO 4217 alpha-3) + # @option attributes [String] :currency Optional currency code (ISO 4217 alpha-3) + # @return [Response] the API response containing created bank account + # + # @example Create an Australian bank account + # bank_accounts = ZaiPayment::Resources::BankAccount.new + # response = bank_accounts.create_au( + # user_id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + # bank_name: 'Bank of Australia', + # account_name: 'Samuel Seller', + # routing_number: '111123', + # account_number: '111234', + # account_type: 'checking', + # holder_type: 'personal', + # country: 'AUS', + # payout_currency: 'AUD', + # currency: 'AUD' + # ) + # + # @see https://developer.hellozai.com/reference/createbankaccount + def create_au(**attributes) + validate_create_au_attributes!(attributes) + + body = build_bank_account_body(attributes, :au) + client.post('/bank_accounts', body: body) + end + + # Create a new bank account for UK + # + # @param attributes [Hash] bank account attributes + # @option attributes [String] :user_id (Required) User ID + # @option attributes [String] :bank_name (Required) Bank name (defaults to Bank of UK) + # @option attributes [String] :account_name (Required) Account name (defaults to Samuel Seller) + # @option attributes [String] :routing_number (Required) Routing number / Sort Code / BSB + # number (defaults to 111123) + # @option attributes [String] :account_number (Required) Account number + # (defaults to 111234) + # @option attributes [String] :account_type (Required) Account type + # ('savings' or 'checking', defaults to checking) + # @option attributes [String] :holder_type (Required) Holder type ('personal' or 'business', defaults to personal) + # @option attributes [String] :country (Required) Country code (ISO 3166-1 alpha-3, max 3 chars, defaults to GBR) + # @option attributes [String] :payout_currency Optional currency code for payouts (ISO 4217 alpha-3) + # @option attributes [String] :currency Optional currency code (ISO 4217 alpha-3) + # @option attributes [String] :iban (Required for UK) IBAN number + # @option attributes [String] :swift_code (Required for UK) SWIFT Code / BIC + # @return [Response] the API response containing created bank account + # + # @example Create a UK bank account + # bank_accounts = ZaiPayment::Resources::BankAccount.new + # response = bank_accounts.create_uk( + # user_id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + # bank_name: 'Bank of UK', + # account_name: 'Samuel Seller', + # routing_number: '111123', + # account_number: '111234', + # account_type: 'checking', + # holder_type: 'personal', + # country: 'GBR', + # payout_currency: 'GBP', + # currency: 'GBP', + # iban: 'GB25QHWM02498765432109', + # swift_code: 'BUKBGB22' + # ) + # + # @see https://developer.hellozai.com/reference/createbankaccount + def create_uk(**attributes) + validate_create_uk_attributes!(attributes) + + body = build_bank_account_body(attributes, :uk) + client.post('/bank_accounts', body: body) + end + + # Redact a bank account + # + # Redacts a bank account using the given bank_account_id. Redacted bank accounts + # can no longer be used as a funding source or a disbursement destination. + # + # @param bank_account_id [String] the bank account ID + # @return [Response] the API response + # + # @example + # bank_accounts = ZaiPayment::Resources::BankAccount.new + # response = bank_accounts.redact("bank_account_id") + # + # @see https://developer.hellozai.com/reference/redactbankaccount + def redact(bank_account_id) + validate_id!(bank_account_id, 'bank_account_id') + client.delete("/bank_accounts/#{bank_account_id}") + end + + # Validate a US bank routing number + # + # Validates a US bank routing number before creating an account. This can be used to + # provide on-demand verification and further information of the bank information a user + # is providing. + # + # @param routing_number [String] the US bank routing number + # @return [Response] the API response containing routing number details + # + # @example + # bank_accounts = ZaiPayment::Resources::BankAccount.new + # response = bank_accounts.validate_routing_number("122235821") + # response.data # => {"routing_number" => "122235821", "customer_name" => "US BANK NA", ...} + # + # @see https://developer.hellozai.com/reference/validateroutingnumber + def validate_routing_number(routing_number) + validate_presence!(routing_number, 'routing_number') + + params = { routing_number: routing_number } + client.get('/tools/routing_number', params: params) + 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_presence!(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_create_au_attributes!(attributes) + validate_required_au_attributes!(attributes) + validate_account_type!(attributes[:account_type]) if attributes[:account_type] + validate_holder_type!(attributes[:holder_type]) if attributes[:holder_type] + validate_country!(attributes[:country]) if attributes[:country] + end + + def validate_create_uk_attributes!(attributes) + validate_required_uk_attributes!(attributes) + validate_account_type!(attributes[:account_type]) if attributes[:account_type] + validate_holder_type!(attributes[:holder_type]) if attributes[:holder_type] + validate_country!(attributes[:country]) if attributes[:country] + end + + def validate_required_au_attributes!(attributes) + required_fields = %i[user_id bank_name account_name routing_number account_number + account_type holder_type country] + + missing_fields = required_fields.select do |field| + attributes[field].nil? || attributes[field].to_s.strip.empty? + end + + return if missing_fields.empty? + + raise Errors::ValidationError, + "Missing required fields: #{missing_fields.join(', ')}" + end + + def validate_required_uk_attributes!(attributes) + required_fields = %i[user_id bank_name account_name routing_number account_number + account_type holder_type country iban swift_code] + + missing_fields = required_fields.select do |field| + attributes[field].nil? || attributes[field].to_s.strip.empty? + end + + return if missing_fields.empty? + + raise Errors::ValidationError, + "Missing required fields: #{missing_fields.join(', ')}" + end + + def validate_account_type!(account_type) + return if VALID_ACCOUNT_TYPES.include?(account_type.to_s.downcase) + + raise Errors::ValidationError, + "account_type must be one of: #{VALID_ACCOUNT_TYPES.join(', ')}" + end + + def validate_holder_type!(holder_type) + return if VALID_HOLDER_TYPES.include?(holder_type.to_s.downcase) + + raise Errors::ValidationError, + "holder_type must be one of: #{VALID_HOLDER_TYPES.join(', ')}" + end + + def validate_country!(country) + # Country should be ISO 3166-1 alpha-3 code (3 letters) + return if country.to_s.match?(/\A[A-Z]{3}\z/i) + + raise Errors::ValidationError, 'country must be a valid ISO 3166-1 alpha-3 code (e.g., AUS, GBR)' + end + + def build_bank_account_body(attributes, region) + body = {} + field_mapping = region == :uk ? UK_FIELD_MAPPING : FIELD_MAPPING + + attributes.each do |key, value| + next if value.nil? || (value.respond_to?(:empty?) && value.empty?) + + api_field = 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 f66fac4..88a7413 100644 --- a/lib/zai_payment/response.rb +++ b/lib/zai_payment/response.rb @@ -8,6 +8,7 @@ class Response RESPONSE_DATA_KEYS = %w[ webhooks users items fees transactions batch_transactions bpay_accounts bank_accounts card_accounts + routing_number ].freeze def initialize(faraday_response) diff --git a/lib/zai_payment/version.rb b/lib/zai_payment/version.rb index 4cfae98..2148c81 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.4.0' + VERSION = '2.5.0' end diff --git a/readme.md b/readme.md index 294bae1..6430110 100644 --- a/readme.md +++ b/readme.md @@ -21,12 +21,13 @@ A lightweight and extensible Ruby client for the **Zai (AssemblyPay)** API — s - 🧠 **Smart Token Caching** - Auto-refresh before expiration, thread-safe storage - 👥 **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 - 🎫 **Token Auth** - Generate secure tokens for bank and card account data collection - 🪝 **Webhooks** - Full CRUD + secure signature verification (HMAC SHA256) - ⚙️ **Environment-Aware** - Seamless Pre-live / Production switching - 🧱 **Modular & Extensible** - Clean resource-based architecture - 🧰 **Zero Heavy Dependencies** - Lightweight, fast, and reliable -- 📦 **Production Ready** - 88%+ test coverage, RuboCop compliant +- 📦 **Production Ready** - 97%+ test coverage, RuboCop compliant --- @@ -88,82 +89,7 @@ The gem handles OAuth2 Client Credentials flow automatically - tokens are cached ### Users -Manage payin (buyer) and payout (seller/merchant) users: - -```ruby -# Create a payin user (buyer) -response = ZaiPayment.users.create( - user_type: 'payin', - email: 'buyer@example.com', - first_name: 'John', - last_name: 'Doe', - country: 'USA', - mobile: '+1234567890' -) - -# Create a payout user (seller/merchant) -response = ZaiPayment.users.create( - user_type: 'payout', - email: 'seller@example.com', - first_name: 'Jane', - last_name: 'Smith', - country: 'AUS', - dob: '01/01/1990', - address_line1: '456 Market St', - city: 'Sydney', - state: 'NSW', - zip: '2000' -) - -# Create a business user with company details -response = ZaiPayment.users.create( - user_type: 'payout', - email: 'director@company.com', - first_name: 'John', - last_name: 'Director', - country: 'AUS', - mobile: '+61412345678', - authorized_signer_title: 'Director', - company: { - name: 'My Company', - legal_name: 'My Company Pty Ltd', - tax_number: '123456789', - business_email: 'admin@company.com', - country: 'AUS', - charge_tax: true - } -) - -# List users -response = ZaiPayment.users.list(limit: 10, offset: 0) - -# Get user details -response = ZaiPayment.users.show('user_id') - -# Update user -response = ZaiPayment.users.update('user_id', mobile: '+9876543210') - -# Show user wallet account -response = ZaiPayment.users.wallet_account('user_id') - -# List user items with pagination -response = ZaiPayment.users.items('user_id', limit: 50, offset: 10) - -# Set user disbursement account -response = ZaiPayment.users.set_disbursement_account('user_id', 'bank_account_id') - -# Show user bank account -response = ZaiPayment.users.bank_account('user_id') - -# Verify user (prelive only) -response = ZaiPayment.users.verify('user_id') - -# Show user card account -response = ZaiPayment.users.card_account('user_id') - -# List user's BPay accounts -response = ZaiPayment.users.bpay_accounts('user_id') -``` +Manage payin (buyer) and payout (seller/merchant) users. **📚 Documentation:** - 📖 [User Management Guide](docs/users.md) - Complete guide for payin and payout users @@ -173,69 +99,25 @@ response = ZaiPayment.users.bpay_accounts('user_id') ### Items -Manage transactions/payments between buyers and sellers: - -```ruby -# Create an item -response = ZaiPayment.items.create( - name: "Product Purchase", - amount: 10000, # Amount in cents ($100.00) - payment_type: 2, # Credit card - buyer_id: "buyer-123", - seller_id: "seller-456", - description: "Purchase of premium product", - currency: "AUD", - tax_invoice: true -) - -# List items -response = ZaiPayment.items.list(limit: 20, offset: 0) - -# Get item details -response = ZaiPayment.items.show('item_id') - -# Update item -response = ZaiPayment.items.update('item_id', name: 'Updated Name') - -# Get item status -response = ZaiPayment.items.show_status('item_id') - -# Get buyer/seller details -response = ZaiPayment.items.show_buyer('item_id') -response = ZaiPayment.items.show_seller('item_id') - -# List transactions -response = ZaiPayment.items.list_transactions('item_id') -``` +Manage transactions/payments between buyers and sellers. **📚 Documentation:** - 📖 [Item Management Guide](docs/items.md) - Complete guide for creating and managing items - 💡 [Item Examples](examples/items.md) - Real-world usage patterns and complete workflows - 🔗 [Zai: Items API Reference](https://developer.hellozai.com/reference/listitems) -### Token Auth +### Bank Accounts -Generate secure tokens for collecting bank and card account information: +Manage bank accounts for Australian and UK users, with routing number validation. -```ruby -# Generate a bank token (for collecting bank account details) -response = ZaiPayment.token_auths.generate( - user_id: "seller-68611249", - token_type: "bank" -) - -token = response.data['token_auth']['token'] -# Use this token with PromisePay.js on the frontend - -# Generate a card token (for collecting credit card details) -response = ZaiPayment.token_auths.generate( - user_id: "buyer-12345", - token_type: "card" -) - -token = response.data['token_auth']['token'] -# Use this token with PromisePay.js on the frontend -``` +**📚 Documentation:** +- 📖 [Bank Account Management Guide](docs/bank_accounts.md) - Complete guide for bank accounts +- 💡 [Bank Account Examples](examples/bank_accounts.md) - Real-world patterns and integration +- 🔗 [Zai: Bank Accounts API Reference](https://developer.hellozai.com/reference/showbankaccount) + +### Token Auth + +Generate secure tokens for collecting bank and card account information. **📚 Documentation:** - 💡 [Token Auth Examples](examples/token_auths.md) - Complete integration guide with PromisePay.js @@ -243,24 +125,7 @@ token = response.data['token_auth']['token'] ### Webhooks -Manage webhook endpoints: - -```ruby -# List webhooks -response = ZaiPayment.webhooks.list -webhooks = response.data - -# Create a webhook -response = ZaiPayment.webhooks.create( - url: 'https://example.com/webhooks/zai', - object_type: 'transactions', - enabled: true -) - -# Secure your webhooks with signature verification -secret_key = SecureRandom.alphanumeric(32) -ZaiPayment.webhooks.create_secret_key(secret_key: secret_key) -``` +Manage webhook endpoints with secure signature verification. **📚 Documentation:** - 📖 [Webhook Examples & Complete Guide](examples/webhooks.md) - Full CRUD operations and patterns @@ -301,6 +166,7 @@ end | ✅ Webhooks | CRUD for webhook endpoints | Done | | ✅ Users | Manage PayIn / PayOut users | Done | | ✅ Items | Transactions/payments (CRUD) | Done | +| ✅ Bank Accounts | AU/UK bank accounts + validation | Done | | ✅ Token Auth | Generate bank/card tokens | Done | | 💳 Payments | Single and recurring payments | 🚧 In progress | | 🏦 Virtual Accounts (VA / PIPU) | Manage virtual accounts & PayTo | ⏳ Planned | @@ -360,12 +226,14 @@ Everyone interacting in the ZaiPayment project's codebases, issue trackers, chat - [**Authentication Guide**](docs/authentication.md) - Two approaches to getting tokens, automatic management - [**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 - [**Webhook Examples**](examples/webhooks.md) - Complete webhook usage guide - [**Documentation Index**](docs/readme.md) - Full documentation navigation ### Examples & Patterns - [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 - [Token Auth Examples](examples/token_auths.md) - Secure token generation and integration - [Webhook Examples](examples/webhooks.md) - Webhook integration patterns diff --git a/spec/zai_payment/resources/bank_account_spec.rb b/spec/zai_payment/resources/bank_account_spec.rb new file mode 100644 index 0000000..1170fce --- /dev/null +++ b/spec/zai_payment/resources/bank_account_spec.rb @@ -0,0 +1,437 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ZaiPayment::Resources::BankAccount do + let(:stubs) { Faraday::Adapter::Test::Stubs.new } + let(:bank_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(:bank_account_data) do + { + 'bank_accounts' => { + 'id' => 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'active' => true, + 'created_at' => '2020-04-27T20:28:22.378Z', + 'updated_at' => '2020-04-27T20:28:22.378Z', + 'verification_status' => 'not_verified', + 'currency' => 'AUD', + 'bank' => { + 'bank_name' => 'Bank of Australia', + 'country' => 'AUS', + 'account_name' => 'Samuel Seller', + 'routing_number' => 'XXXXX3', + 'account_number' => 'XXX234', + 'holder_type' => 'personal', + 'account_type' => 'checking', + 'direct_debit_authority_status' => 'approved' + }, + 'links' => { + 'self' => '/bank_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'users' => '/bank_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/users', + 'direct_debit_authorities' => '/bank_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/direct_debit_authorities' + } + } + } + end + + context 'when bank account exists' do + before do + stubs.get('/bank_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee') do + [200, { 'Content-Type' => 'application/json' }, bank_account_data] + end + end + + it 'returns the correct response type and bank account details' do + response = bank_account_resource.show('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee') + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + expect(response.data['id']).to eq('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee') + end + end + + context 'with include_decrypted_fields parameter' do + before do + stubs.get('/bank_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee') do |env| + if env.params['include_decrypted_fields'] == 'true' + decrypted_data = bank_account_data.dup + decrypted_data['bank_accounts']['bank']['account_number'] = '12345678' + [200, { 'Content-Type' => 'application/json' }, decrypted_data] + else + [200, { 'Content-Type' => 'application/json' }, bank_account_data] + end + end + end + + it 'includes decrypted fields when true' do + response = bank_account_resource.show( + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + include_decrypted_fields: true + ) + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + + it 'excludes decrypted fields when false' do + response = bank_account_resource.show( + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + include_decrypted_fields: false + ) + + expect(response).to be_a(ZaiPayment::Response) + expect(response.data['bank']['account_number']).to eq('XXX234') + end + end + + context 'when bank account does not exist' do + before do + stubs.get('/bank_accounts/invalid_id') do + [404, { 'Content-Type' => 'application/json' }, { 'errors' => 'Not found' }] + end + end + + it 'raises a NotFoundError' do + expect { bank_account_resource.show('invalid_id') } + .to raise_error(ZaiPayment::Errors::NotFoundError) + end + end + + context 'when bank_account_id is blank' do + it 'raises a ValidationError for empty string' do + expect { bank_account_resource.show('') } + .to raise_error(ZaiPayment::Errors::ValidationError, /bank_account_id/) + end + + it 'raises a ValidationError for nil' do + expect { bank_account_resource.show(nil) } + .to raise_error(ZaiPayment::Errors::ValidationError, /bank_account_id/) + end + end + end + + describe '#redact' do + context 'when successful' do + before do + stubs.delete('/bank_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee') do + [200, { 'Content-Type' => 'application/json' }, { 'bank_account' => 'Successfully redacted' }] + end + end + + it 'returns the correct response type' do + response = bank_account_resource.redact('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee') + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + end + + context 'when bank_account_id is blank' do + it 'raises a ValidationError for empty string' do + expect { bank_account_resource.redact('') } + .to raise_error(ZaiPayment::Errors::ValidationError, /bank_account_id/) + end + + it 'raises a ValidationError for nil' do + expect { bank_account_resource.redact(nil) } + .to raise_error(ZaiPayment::Errors::ValidationError, /bank_account_id/) + end + end + + context 'when bank account does not exist' do + before do + stubs.delete('/bank_accounts/invalid_id') do + [404, { 'Content-Type' => 'application/json' }, { 'errors' => 'Not found' }] + end + end + + it 'raises a NotFoundError' do + expect { bank_account_resource.redact('invalid_id') } + .to raise_error(ZaiPayment::Errors::NotFoundError) + end + end + end + + describe '#validate_routing_number' do + let(:routing_number_data) do + { + 'routing_number' => { + 'routing_number' => '122235821', + 'customer_name' => 'US BANK NA', + 'address' => 'EP-MN-WN1A', + 'city' => 'ST. PAUL', + 'state_code' => 'MN', + 'zip' => '55107', + 'zip_extension' => '1419', + 'phone_area_code' => '800', + 'phone_prefix' => '937', + 'phone_suffix' => '631' + } + } + end + + context 'when routing number is valid' do + before do + stubs.get('/tools/routing_number') do |env| + if env.params['routing_number'] == '122235821' + [200, { 'Content-Type' => 'application/json' }, routing_number_data] + else + [404, { 'Content-Type' => 'application/json' }, { 'errors' => 'Invalid routing number' }] + end + end + end + + it 'returns the correct response type' do + response = bank_account_resource.validate_routing_number('122235821') + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + + it 'returns routing number details' do + response = bank_account_resource.validate_routing_number('122235821') + + expect(response.data['routing_number']).to eq('122235821') + expect(response.data['customer_name']).to eq('US BANK NA') + end + end + + context 'when routing number is invalid' do + before do + stubs.get('/tools/routing_number') do + [404, { 'Content-Type' => 'application/json' }, { 'errors' => 'Invalid routing number' }] + end + end + + it 'raises a NotFoundError' do + expect { bank_account_resource.validate_routing_number('invalid') } + .to raise_error(ZaiPayment::Errors::NotFoundError) + end + end + + context 'when routing_number is blank' do + it 'raises a ValidationError for empty string' do + expect { bank_account_resource.validate_routing_number('') } + .to raise_error(ZaiPayment::Errors::ValidationError, /routing_number/) + end + + it 'raises a ValidationError for nil' do + expect { bank_account_resource.validate_routing_number(nil) } + .to raise_error(ZaiPayment::Errors::ValidationError, /routing_number/) + end + end + end + + describe '#create_au' do + let(:bank_account_data) do + { + 'bank_accounts' => { + 'id' => 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'active' => true, + 'verification_status' => 'not_verified', + 'currency' => 'AUD', + 'bank' => { + 'bank_name' => 'Bank of Australia', + 'country' => 'AUS', + 'account_name' => 'Samuel Seller', + 'routing_number' => 'XXXXX3', + 'account_number' => 'XXX234', + 'iban' => 'null,', + 'swift_code' => 'null,', + 'holder_type' => 'personal', + 'account_type' => 'checking', + 'direct_debit_authority_status' => 'approved' + }, + 'links' => { + 'self' => '/bank_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'users' => '/bank_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/users', + 'direct_debit_authorities' => '/bank_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/direct_debit_authorities' + } + } + } + end + + let(:valid_au_params) do + { + user_id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + bank_name: 'Bank of Australia', + account_name: 'Samuel Seller', + routing_number: '111123', + account_number: '111234', + account_type: 'checking', + holder_type: 'personal', + country: 'AUS' + } + end + + context 'when successful' do + before do + stubs.post('/bank_accounts') do + [201, { 'Content-Type' => 'application/json' }, bank_account_data] + end + end + + it 'returns the correct response type and creates bank account' do + response = bank_account_resource.create_au(**valid_au_params) + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + end + + context 'with optional fields' do + before do + stubs.post('/bank_accounts') do + [201, { 'Content-Type' => 'application/json' }, bank_account_data] + end + end + + it 'accepts optional payout_currency and currency' do + response = bank_account_resource.create_au(**valid_au_params, payout_currency: 'AUD', currency: 'AUD') + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + end + + context 'when validation fails' do + it 'raises error when required fields are missing' do + expect do + bank_account_resource.create_au(user_id: 'user_123', bank_name: 'Bank of Australia') + end.to raise_error(ZaiPayment::Errors::ValidationError, /Missing required fields/) + end + + it 'raises error for invalid account_type' do + params = valid_au_params.merge(account_type: 'invalid') + expect { bank_account_resource.create_au(**params) } + .to raise_error(ZaiPayment::Errors::ValidationError) + end + + it 'raises error for invalid holder_type' do + params = valid_au_params.merge(holder_type: 'invalid') + expect { bank_account_resource.create_au(**params) } + .to raise_error(ZaiPayment::Errors::ValidationError) + end + end + end + + describe '#create_uk' do + let(:bank_account_data) do + { + 'bank_accounts' => { + 'id' => 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'active' => true, + 'verification_status' => 'not_verified', + 'currency' => 'GBP', + 'bank' => { + 'bank_name' => 'Bank of UK', + 'country' => 'GBR', + 'account_name' => 'Samuel Seller', + 'routing_number' => 'XXXXX3', + 'account_number' => 'XXX234', + 'iban' => 'GB25QHWM02498765432109', + 'swift_code' => 'BUKBGB22', + 'holder_type' => 'personal', + 'account_type' => 'checking', + 'direct_debit_authority_status' => 'approved' + }, + 'links' => { + 'self' => '/bank_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'users' => '/bank_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/users', + 'direct_debit_authorities' => '/bank_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/direct_debit_authorities' + } + } + } + end + + let(:valid_uk_params) do + { + user_id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + bank_name: 'Bank of UK', + account_name: 'Samuel Seller', + routing_number: '111123', + account_number: '111234', + account_type: 'checking', + holder_type: 'personal', + country: 'GBR', + iban: 'GB25QHWM02498765432109', + swift_code: 'BUKBGB22' + } + end + + context 'when successful' do + before do + stubs.post('/bank_accounts') do + [201, { 'Content-Type' => 'application/json' }, bank_account_data] + end + end + + it 'returns the correct response type and creates bank account' do + response = bank_account_resource.create_uk(**valid_uk_params) + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + end + + context 'with optional fields' do + before do + stubs.post('/bank_accounts') do + [201, { 'Content-Type' => 'application/json' }, bank_account_data] + end + end + + it 'accepts optional payout_currency and currency' do + response = bank_account_resource.create_uk(**valid_uk_params, payout_currency: 'GBP', currency: 'GBP') + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + end + + context 'when validation fails' do + it 'raises error when required fields are missing' do + expect { bank_account_resource.create_uk(user_id: 'user_123', bank_name: 'Bank of UK') } + .to raise_error(ZaiPayment::Errors::ValidationError, /Missing required fields/) + end + + it 'raises error when UK-specific fields are missing' do + params = valid_uk_params.except(:iban, :swift_code) + expect { bank_account_resource.create_uk(**params) } + .to raise_error(ZaiPayment::Errors::ValidationError) + end + + it 'raises error for invalid country format' do + params = valid_uk_params.merge(country: 'INVALID') + expect { bank_account_resource.create_uk(**params) } + .to raise_error(ZaiPayment::Errors::ValidationError) + end + end + end +end