diff --git a/Gemfile.lock b/Gemfile.lock index 3e25c11..79ad990 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - zai_payment (2.5.0) + zai_payment (2.6.0) base64 (~> 0.3.0) faraday (~> 2.0) openssl (~> 3.3) diff --git a/changelog.md b/changelog.md index 1efd4ae..ca9c9a4 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,65 @@ ## [Released] +## [2.6.0] - 2025-11-03 + +### Added +- **BPay Account Resource**: Complete BPay account management for Australian bill payments šŸ’° + - `ZaiPayment.bpay_accounts.show(bpay_account_id)` - Get BPay account details including biller information + - `ZaiPayment.bpay_accounts.create(user_id:, account_name:, biller_code:, bpay_crn:)` - Create BPay account for disbursement destinations + - `ZaiPayment.bpay_accounts.redact(bpay_account_id)` - Redact (deactivate) a BPay account + - `ZaiPayment.bpay_accounts.show_user(bpay_account_id)` - Get user associated with a BPay account + - Validation for biller code (3-10 digits) + - Validation for BPay CRN (Customer Reference Number, 2-20 digits) + - Support for Australian bill payment disbursements + - Full RSpec test suite with 12 test examples across 4 describe blocks + - Comprehensive documentation in `docs/bpay_accounts.md` + - Practical examples in `examples/bpay_accounts.md` + +### Documentation +- **BPay Accounts Guide** (`docs/bpay_accounts.md`): + - Complete guide for showing, creating, redacting BPay accounts, and retrieving associated users + - Detailed API reference for all four endpoints + - Response structure documentation with example payloads + - Validation rules for biller codes and BPay CRN formats + - Error handling documentation (ValidationError, NotFoundError) + - Four comprehensive use cases: + - Disbursement account setup for bill payments + - User verification before disbursement + - Deactivating old BPay accounts + - Managing multiple utility BPay accounts + - Important notes about BPay account usage and restrictions +- **BPay Accounts Examples** (`examples/bpay_accounts.md`): + - Show BPay account examples (3 examples): + - Basic account retrieval with full details + - Error handling and status verification + - Pre-disbursement verification workflow + - Show BPay account user examples (3 examples): + - User information retrieval + - User verification before disbursement + - Contact information extraction for notifications + - Redact BPay account examples (3 examples): + - Basic redaction with response handling + - Comprehensive error handling + - Verification before redacting workflow + - Create BPay account examples (3 examples): + - Basic account creation + - Error handling patterns + - Multiple utility account setup + - Four common patterns: + - Retrieve and verify before disbursement + - Complete onboarding workflow with user creation + - BPay account form handler for Rails applications + - BPay details validation helper + +### Features +- BPay accounts work as disbursement destinations for Australian bill payments +- Supports all major Australian billers (utilities, telecommunications, financial services, government) +- Automatic biller name lookup based on biller code +- Full lifecycle management (create, show, show_user, redact) +- Secure validation of BPay-specific formats + +**Full Changelog**: https://github.com/Sentia/zai-payment/compare/v2.5.0...v2.6.0 + ## [2.5.0] - 2025-11-03 ### Added diff --git a/docs/bpay_accounts.md b/docs/bpay_accounts.md new file mode 100644 index 0000000..db1f122 --- /dev/null +++ b/docs/bpay_accounts.md @@ -0,0 +1,519 @@ +# BPay Account Management + +The BpayAccount resource provides methods for managing Zai BPay accounts for Australian bill payments. + +## Overview + +BPay accounts are used as disbursement destinations for Australian bill payments. BPay is a popular Australian electronic bill payment system that allows payments to be made through the customer's internet or telephone banking facility. + +Once created, store the returned `:id` and use it for disbursement operations. The `:id` is also referred to as a `:token` when invoking BPay Accounts. + +## References + +- [Create BPay Account API](https://developer.hellozai.com/reference/createbpayaccount) +- [BPay Overview](https://developer.hellozai.com/docs/bpay) + +## Usage + +### Initialize the BpayAccount Resource + +```ruby +# Using a new instance +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +# Or use with custom client +client = ZaiPayment::Client.new +bpay_accounts = ZaiPayment::Resources::BpayAccount.new(client: client) +``` + +## Methods + +### Show BPay Account + +Get details of a specific BPay account by ID. + +#### Parameters + +- `bpay_account_id` (required) - The BPay account ID + +#### Example + +```ruby +# Get BPay account details +response = bpay_accounts.show('bpay_account_id') + +# Access BPay account details +bpay_account = response.data +puts bpay_account['id'] +puts bpay_account['active'] +puts bpay_account['verification_status'] +puts bpay_account['currency'] + +# Access BPay details +bpay_details = bpay_account['bpay_details'] +puts bpay_details['account_name'] +puts bpay_details['biller_code'] +puts bpay_details['biller_name'] +puts bpay_details['crn'] +``` + +#### Response + +```ruby +{ + "bpay_accounts" => { + "id" => "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "active" => true, + "created_at" => "2020-04-03T07:59:00.379Z", + "updated_at" => "2020-04-03T07:59:00.379Z", + "verification_status" => "not_verified", + "currency" => "AUD", + "bpay_details" => { + "account_name" => "My Water Bill Company", + "biller_code" => 123456, + "biller_name" => "ABC Water", + "crn" => 987654321 + }, + "links" => { + "self" => "/bpay_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "users" => "/bpay_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/users" + } + } +} +``` + +**Use Cases:** +- Retrieve BPay account details before disbursement +- Verify account status and verification +- Check biller information +- Validate account is active + +### Show BPay Account User + +Get the User the BPay Account is associated with using a given bpay_account_id. + +#### Parameters + +- `bpay_account_id` (required) - The BPay account ID + +#### Example + +```ruby +# Get user associated with BPay account +response = bpay_accounts.show_user('bpay_account_id') + +# 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" => "/bpay_accounts/901d8cd0-6af3-0138-967d-0a58a9feac04/users", + "items" => "/users/e6bc0480-57ae-0138-c46e-0a58a9feac03/items", + "card_accounts" => "/users/e6bc0480-57ae-0138-c46e-0a58a9feac03/card_accounts", + "bpay_accounts" => "/users/e6bc0480-57ae-0138-c46e-0a58a9feac03/bpay_accounts", + "wallet_accounts" => "/users/e6bc0480-57ae-0138-c46e-0a58a9feac03/wallet_accounts" + } + } +} +``` + +**Use Cases:** +- Retrieve user information for a BPay account +- Verify user identity before disbursement +- Check user verification status +- Get user contact details for notifications + +### Redact BPay Account + +Redact a BPay account using the given bpay_account_id. Redacted BPay accounts can no longer be used as a disbursement destination. + +**Note**: This is marked as a "Future Feature" in the Zai API documentation but is implemented for forward compatibility. + +#### Parameters + +- `bpay_account_id` (required) - The BPay account ID + +#### Example + +```ruby +response = bpay_accounts.redact('bpay_account_id') + +if response.success? + puts "BPay account successfully redacted" +else + puts "Failed to redact BPay account" +end +``` + +#### Response + +```ruby +{ + "bpay_account" => "Successfully redacted" +} +``` + +**Important Notes:** +- Once redacted, the BPay account cannot be used for disbursements +- This action cannot be undone +- Use with caution + +### Create BPay Account + +Create a BPay Account to be used as a Disbursement destination. + +#### Required Fields + +- `user_id` - User ID (UUID format) +- `account_name` - Name assigned by the platform/marketplace to identify the account (similar to a nickname). Defaults to "My Water Bill Company" +- `biller_code` - The Biller Code for the biller that will receive the payment. Must be a numeric value with 3 to 10 digits. +- `bpay_crn` - Customer reference number (crn) to be used for this BPay account. The CRN must contain between 2 and 20 digits. Defaults to "987654321" + +#### Example + +```ruby +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +response = bpay_accounts.create( + user_id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + account_name: 'My Water Bill Company', + biller_code: 123456, + bpay_crn: '987654321' +) + +if response.success? + bpay_account = response.data + puts "BPay Account ID: #{bpay_account['id']}" + puts "Active: #{bpay_account['active']}" + puts "Verification Status: #{bpay_account['verification_status']}" + puts "Currency: #{bpay_account['currency']}" + + # Access BPay details + bpay_details = bpay_account['bpay_details'] + puts "Account Name: #{bpay_details['account_name']}" + puts "Biller Code: #{bpay_details['biller_code']}" + puts "Biller Name: #{bpay_details['biller_name']}" + puts "CRN: #{bpay_details['crn']}" + + # Access links + links = bpay_account['links'] + puts "Self Link: #{links['self']}" + puts "Users Link: #{links['users']}" +end +``` + +## Response Structure + +The methods return a `ZaiPayment::Response` object with the following structure: + +```ruby +{ + "bpay_accounts" => { + "id" => "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "created_at" => "2020-04-03T07:59:00.379Z", + "updated_at" => "2020-04-03T07:59:00.379Z", + "active" => true, + "verification_status" => "not_verified", + "currency" => "AUD", + "bpay_details" => { + "account_name" => "My Water Bill Company", + "biller_code" => 123456, + "biller_name" => "ABC Water", + "crn" => 987654321 + }, + "links" => { + "self" => "/bpay_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "users" => "/bpay_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/users" + } + } +} +``` + +## Validation Rules + +### Biller Code + +- Must be a numeric value +- Must contain between 3 and 10 digits +- Examples: `123`, `123456`, `1234567890` + +### BPay CRN (Customer Reference Number) + +- Must be numeric +- Must contain between 2 and 20 digits +- Examples: `12`, `987654321`, `12345678901234567890` + +### Account Name + +- Required field +- Used to identify the account (similar to a nickname) +- Should be descriptive (e.g., "Water Bill", "Electricity Account") + +### User ID + +- Required field +- Must be a valid UUID +- Must reference an existing user in the system + +## Error Handling + +The BpayAccount resource raises the following errors: + +### NotFoundError + +Raised when the BPay account does not exist: + +```ruby +begin + bpay_accounts.show('invalid_id') +rescue ZaiPayment::Errors::NotFoundError => e + puts "BPay account not found: #{e.message}" +end +``` + +### ValidationError + +Raised when required fields are missing or invalid: + +```ruby +begin + bpay_accounts.create( + user_id: 'user_123', + account_name: 'Test Account' + # Missing required fields + ) +rescue ZaiPayment::Errors::ValidationError => e + puts "Validation failed: #{e.message}" + # => "Missing required fields: biller_code, bpay_crn" +end +``` + +### Invalid Biller Code + +```ruby +begin + bpay_accounts.create( + user_id: 'user_123', + account_name: 'Test Account', + biller_code: 12, # Only 2 digits (requires 3-10) + bpay_crn: '987654321' + ) +rescue ZaiPayment::Errors::ValidationError => e + puts e.message + # => "biller_code must be a numeric value with 3 to 10 digits" +end +``` + +### Invalid BPay CRN + +```ruby +begin + bpay_accounts.create( + user_id: 'user_123', + account_name: 'Test Account', + biller_code: 123456, + bpay_crn: '1' # Only 1 digit (requires 2-20) + ) +rescue ZaiPayment::Errors::ValidationError => e + puts e.message + # => "bpay_crn must contain between 2 and 20 digits" +end +``` + +### Blank BPay Account ID + +Raised when trying to show or redact a BPay account with blank/nil ID: + +```ruby +begin + bpay_accounts.show('') + # or + bpay_accounts.redact(nil) +rescue ZaiPayment::Errors::ValidationError => e + puts "Invalid ID: #{e.message}" + # => "bpay_account_id is required and cannot be blank" +end +``` + +## Use Cases + +### Use Case 1: Disbursement Account for Bill Payments + +After creating a payout user, create a BPay account for receiving bill payments: + +```ruby +# Step 1: Create payout user +users = ZaiPayment::Resources::User.new +user_response = users.create( + user_type: 'payout', + email: 'biller@example.com', + first_name: 'Water', + last_name: 'Company', + 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 BPay account +bpay_accounts = ZaiPayment::Resources::BpayAccount.new +bpay_response = bpay_accounts.create( + user_id: user_id, + account_name: 'Water Bill Payment', + biller_code: 123456, + bpay_crn: '987654321' +) + +account_id = bpay_response.data['id'] + +# Step 3: Set as disbursement account +users.set_disbursement_account(user_id, account_id) +``` + +### Use Case 2: Get User Details for BPay Account + +Retrieve user information associated with a BPay account: + +```ruby +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +# Step 1: Get user details +user_response = bpay_accounts.show_user('bpay_account_id') + +if user_response.success? + user = user_response.data + + # Step 2: Verify user is eligible for disbursement + if user['verification_state'] == 'verified' && !user['held_state'] + puts "User eligible for disbursement" + puts "Name: #{user['full_name']}" + puts "Email: #{user['email']}" + + # Proceed with disbursement + else + puts "User not eligible" + puts "Verification: #{user['verification_state']}" + puts "Held: #{user['held_state']}" + end +end +``` + +### Use Case 3: Deactivate Old BPay Account + +Redact an old BPay account that is no longer needed: + +```ruby +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +# Step 1: Verify the account before redacting +response = bpay_accounts.show('old_bpay_account_id') + +if response.success? + bpay_account = response.data + puts "Redacting: #{bpay_account['bpay_details']['account_name']}" + + # Step 2: Redact the account + redact_response = bpay_accounts.redact('old_bpay_account_id') + + if redact_response.success? + puts "BPay account successfully redacted" + end +end +``` + +### Use Case 4: Multiple Utility BPay Accounts + +Create multiple BPay accounts for different utility bills: + +```ruby +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +# Water bill +water_response = bpay_accounts.create( + user_id: 'user_123', + account_name: 'Sydney Water Bill', + biller_code: 12345, + bpay_crn: '1122334455' +) + +# Electricity bill +electricity_response = bpay_accounts.create( + user_id: 'user_123', + account_name: 'Energy Australia Bill', + biller_code: 67890, + bpay_crn: '9988776655' +) + +# Gas bill +gas_response = bpay_accounts.create( + user_id: 'user_123', + account_name: 'AGL Gas Bill', + biller_code: 54321, + bpay_crn: '5566778899' +) +``` + +## Important Notes + +1. **Australian Only**: BPay is an Australian payment system and typically uses AUD currency +2. **Disbursement Use**: BPay accounts are used as disbursement destinations, not funding sources +3. **Verification**: New BPay accounts typically have `verification_status: "not_verified"` until verified by Zai +4. **Biller Information**: The API returns `biller_name` which is retrieved based on the `biller_code` +5. **Token Usage**: The returned `id` can be used as a token for disbursement operations +6. **CRN Format**: The CRN (Customer Reference Number) format may vary by biller - check with the specific biller for their format requirements + +## Common BPay Billers + +BPay biller codes are assigned by BPAY. Some common categories include: + +- **Utilities**: Water, electricity, gas +- **Telecommunications**: Phone, internet, mobile +- **Financial Services**: Credit cards, loans, insurance +- **Government**: Council rates, fines, taxes + +**Note**: Always verify the biller code with the actual biller before creating a BPay account. + +## Related Resources + +- [User Management](users.md) - Creating and managing users +- [Disbursement Accounts](users.md#set-disbursement-account) - Setting default payout accounts + +## Further Reading + +- [BPay Overview](https://www.bpay.com.au/) +- [Payment Methods Guide](https://developer.hellozai.com/docs/payment-methods) +- [Verification Process](https://developer.hellozai.com/docs/verification) + diff --git a/examples/bpay_accounts.md b/examples/bpay_accounts.md new file mode 100644 index 0000000..96b8db9 --- /dev/null +++ b/examples/bpay_accounts.md @@ -0,0 +1,642 @@ +# BPay Account Management Examples + +This document provides practical examples for managing BPay accounts in Zai Payment. + +## Table of Contents + +- [Setup](#setup) +- [Show BPay Account Example](#show-bpay-account-example) +- [Show BPay Account User Example](#show-bpay-account-user-example) +- [Redact BPay Account Example](#redact-bpay-account-example) +- [Create BPay Account Example](#create-bpay-account-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 BPay Account Example + +### Example 1: Get BPay Account Details + +Retrieve details of a specific BPay account. + +```ruby +# Get BPay account details +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +response = bpay_accounts.show('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee') + +if response.success? + bpay_account = response.data + puts "BPay Account ID: #{bpay_account['id']}" + puts "Active: #{bpay_account['active']}" + puts "Verification Status: #{bpay_account['verification_status']}" + puts "Currency: #{bpay_account['currency']}" + puts "Created At: #{bpay_account['created_at']}" + puts "Updated At: #{bpay_account['updated_at']}" + + # Access BPay details + bpay_details = bpay_account['bpay_details'] + puts "\nBPay Details:" + puts " Account Name: #{bpay_details['account_name']}" + puts " Biller Code: #{bpay_details['biller_code']}" + puts " Biller Name: #{bpay_details['biller_name']}" + puts " CRN: #{bpay_details['crn']}" + + # Access links + links = bpay_account['links'] + puts "\nLinks:" + puts " Self: #{links['self']}" + puts " Users: #{links['users']}" +else + puts "Failed to retrieve BPay account" + puts "Error: #{response.error}" +end +``` + +### Example 2: Show BPay Account with Error Handling + +Handle edge cases when retrieving BPay account details. + +```ruby +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +begin + response = bpay_accounts.show('bpay_account_id_here') + + if response.success? + bpay_account = response.data + + # Check if account is active + if bpay_account['active'] + puts "BPay account is active" + puts "Biller: #{bpay_account['bpay_details']['biller_name']}" + puts "CRN: #{bpay_account['bpay_details']['crn']}" + else + puts "BPay account is inactive" + end + + # Check verification status + case bpay_account['verification_status'] + when 'verified' + puts "Account is verified and ready to use" + when 'not_verified' + puts "Account pending verification" + when 'pending_verification' + puts "Account verification in progress" + end + else + puts "Failed to retrieve BPay account: #{response.error}" + end +rescue ZaiPayment::Errors::NotFoundError => e + puts "BPay account not found: #{e.message}" +rescue ZaiPayment::Errors::ValidationError => e + puts "Invalid BPay account ID: #{e.message}" +rescue ZaiPayment::Errors::ApiError => e + puts "API error occurred: #{e.message}" +end +``` + +### Example 3: Verify BPay Account Details Before Processing + +Check BPay account details before initiating a disbursement. + +```ruby +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +# Step 1: Retrieve BPay account +response = bpay_accounts.show('bpay_account_id') + +if response.success? + bpay_account = response.data + + # Step 2: Verify account is ready for disbursement + if bpay_account['active'] && bpay_account['verification_status'] == 'verified' + bpay_details = bpay_account['bpay_details'] + + puts "Ready to disburse to:" + puts " Biller: #{bpay_details['biller_name']}" + puts " Account: #{bpay_details['account_name']}" + puts " CRN: #{bpay_details['crn']}" + + # Proceed with disbursement + # items.make_payment(...) + else + puts "Cannot disburse: Account not ready" + puts " Active: #{bpay_account['active']}" + puts " Status: #{bpay_account['verification_status']}" + end +end +``` + +## Show BPay Account User Example + +### Example 1: Get User Associated with BPay Account + +Retrieve user details for a BPay account. + +```ruby +# Get user associated with BPay account +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +response = bpay_accounts.show_user('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee') + +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 Disbursement + +Check user details before processing a disbursement to a BPay account. + +```ruby +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +begin + # Step 1: Get user associated with BPay account + user_response = bpay_accounts.show_user('bpay_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 disbursement + puts "āœ“ Ready to process disbursement" + else + puts "Cannot disburse:" + puts " Verification: #{user['verification_state']}" + puts " On Hold: #{user['held_state']}" + end + end +rescue ZaiPayment::Errors::NotFoundError => e + puts "BPay account not found: #{e.message}" +rescue ZaiPayment::Errors::ApiError => e + puts "API error: #{e.message}" +end +``` + +### Example 3: Get User Contact Information + +Retrieve user contact details for notifications. + +```ruby +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +response = bpay_accounts.show_user('bpay_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_disbursement_notice(contact_info) +end +``` + +## Redact BPay Account Example + +### Example 1: Redact a BPay Account + +Redact (deactivate) a BPay account so it can no longer be used. + +```ruby +# Redact a BPay account +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +response = bpay_accounts.redact('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee') + +if response.success? + puts "BPay account successfully redacted" + puts "Response: #{response.data}" + # => {"bpay_account"=>"Successfully redacted"} + + # The BPay account can no longer be used for: + # - Disbursement destination +else + puts "Failed to redact BPay account" + puts "Error: #{response.error}" +end +``` + +### Example 2: Redact with Error Handling + +Handle edge cases when redacting a BPay account. + +```ruby +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +begin + # Attempt to redact the BPay account + response = bpay_accounts.redact('bpay_account_id_here') + + if response.success? + puts "BPay account redacted successfully" + + # Log the action for audit purposes + Rails.logger.info("BPay account #{bpay_account_id} was redacted at #{Time.now}") + else + puts "Redaction failed: #{response.error}" + end +rescue ZaiPayment::Errors::NotFoundError => e + puts "BPay account not found: #{e.message}" +rescue ZaiPayment::Errors::ValidationError => e + puts "Invalid BPay account ID: #{e.message}" +rescue ZaiPayment::Errors::ApiError => e + puts "API error occurred: #{e.message}" +end +``` + +### Example 3: Verify Before Redacting + +Verify BPay account details before redacting. + +```ruby +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +# Step 1: Retrieve BPay account to verify +response = bpay_accounts.show('bpay_account_id') + +if response.success? + bpay_account = response.data + + # Step 2: Confirm details before redacting + puts "About to redact:" + puts " Account: #{bpay_account['bpay_details']['account_name']}" + puts " Biller: #{bpay_account['bpay_details']['biller_name']}" + puts " Active: #{bpay_account['active']}" + + # Step 3: Redact the account + redact_response = bpay_accounts.redact('bpay_account_id') + + if redact_response.success? + puts "\nāœ“ BPay account redacted successfully" + else + puts "\nāœ— Failed to redact BPay account" + end +end +``` + +## Create BPay Account Example + +### Example 1: Create Basic BPay Account + +Create a BPay account for disbursement destination. + +```ruby +# Create a BPay account +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +response = bpay_accounts.create( + user_id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + account_name: 'My Water Bill Company', + biller_code: 123456, + bpay_crn: '987654321' +) + +if response.success? + bpay_account = response.data + puts "BPay Account ID: #{bpay_account['id']}" + puts "Active: #{bpay_account['active']}" + puts "Verification Status: #{bpay_account['verification_status']}" + puts "Currency: #{bpay_account['currency']}" + + # Access BPay details + bpay_details = bpay_account['bpay_details'] + puts "\nBPay Details:" + puts " Account Name: #{bpay_details['account_name']}" + puts " Biller Code: #{bpay_details['biller_code']}" + puts " Biller Name: #{bpay_details['biller_name']}" + puts " CRN: #{bpay_details['crn']}" + + # Access links + links = bpay_account['links'] + puts "\nLinks:" + puts " Self: #{links['self']}" + puts " Users: #{links['users']}" +else + puts "Failed to create BPay account" + puts "Error: #{response.error}" +end +``` + +### Example 2: Create BPay Account with Error Handling + +Handle validation errors when creating BPay accounts. + +```ruby +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +begin + response = bpay_accounts.create( + user_id: 'user_123', + account_name: 'My Electricity Bill', + biller_code: 456789, + bpay_crn: '123456789' + ) + + if response.success? + puts "BPay account created successfully!" + puts "Account ID: #{response.data['id']}" + else + puts "Failed to create BPay account: #{response.error}" + end +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 +``` + +### Example 3: Create BPay Account for Utility Payment + +Create a BPay account for utility bill payments. + +```ruby +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +# Water bill +response = bpay_accounts.create( + user_id: 'user_456', + account_name: 'Sydney Water Bill', + biller_code: 12345, + bpay_crn: '1122334455' +) + +if response.success? + puts "Water bill BPay account created: #{response.data['id']}" +end + +# Electricity bill +response = bpay_accounts.create( + user_id: 'user_456', + account_name: 'Energy Australia Bill', + biller_code: 67890, + bpay_crn: '9988776655' +) + +if response.success? + puts "Electricity BPay account created: #{response.data['id']}" +end +``` + +## Common Patterns + +### Pattern 1: Retrieve and Verify BPay Account Before Disbursement + +Check BPay account status before processing a payment. + +```ruby +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +# Step 1: Retrieve BPay account +response = bpay_accounts.show('bpay_account_id') + +if response.success? + bpay_account = response.data + + # Step 2: Verify account is ready + if bpay_account['active'] && bpay_account['verification_status'] == 'verified' + puts "BPay account is ready for disbursement" + + # Step 3: Proceed with payment + items = ZaiPayment::Resources::Item.new + payment_response = items.make_payment( + item_id: 'item_123', + account_id: bpay_account['id'] + ) + + puts "Payment initiated" if payment_response.success? + else + puts "Account not ready: #{bpay_account['verification_status']}" + end +end +``` + +### Pattern 2: Creating BPay Account After User Registration + +Typical workflow when setting up disbursement accounts. + +```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 BPay account for the seller +bpay_accounts = ZaiPayment::Resources::BpayAccount.new + +bpay_response = bpay_accounts.create( + user_id: user_id, + account_name: 'My Service Bill', + biller_code: 123456, + bpay_crn: '987654321' +) + +if bpay_response.success? + bpay_account_id = bpay_response.data['id'] + puts "BPay account created: #{bpay_account_id}" + + # Step 3: Set as disbursement account + users.set_disbursement_account(user_id, bpay_account_id) + puts "Disbursement account set successfully" +end +``` + +### Pattern 2: BPay Account Form Handler + +Implement BPay account creation in a Rails controller. + +```ruby +# In a Rails controller +class BpayAccountsController < ApplicationController + def create + bpay_accounts = ZaiPayment::Resources::BpayAccount.new + + begin + response = bpay_accounts.create( + user_id: params[:user_id], + account_name: params[:account_name], + biller_code: params[:biller_code].to_i, + bpay_crn: params[:bpay_crn] + ) + + if response.success? + bpay_account = response.data + + render json: { + success: true, + bpay_account_id: bpay_account['id'], + message: 'BPay account created successfully' + }, 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::ApiError => e + render json: { + success: false, + message: 'An error occurred while creating the BPay account' + }, status: :internal_server_error + end + end +end +``` + +### Pattern 3: Validate BPay Details Before Creating Account + +Validate biller code and CRN format before API call. + +```ruby +class BpayAccountValidator + def self.validate_biller_code(biller_code) + biller_code_str = biller_code.to_s + + unless biller_code_str.match?(/\A\d{3,10}\z/) + return { valid: false, message: 'Biller code must be 3 to 10 digits' } + end + + { valid: true } + end + + def self.validate_bpay_crn(bpay_crn) + bpay_crn_str = bpay_crn.to_s + + unless bpay_crn_str.match?(/\A\d{2,20}\z/) + return { valid: false, message: 'CRN must be between 2 and 20 digits' } + end + + { valid: true } + end + + def self.validate_all(biller_code, bpay_crn) + biller_validation = validate_biller_code(biller_code) + return biller_validation unless biller_validation[:valid] + + crn_validation = validate_bpay_crn(bpay_crn) + return crn_validation unless crn_validation[:valid] + + { valid: true } + end +end + +# Usage +validation = BpayAccountValidator.validate_all(123456, '987654321') + +if validation[:valid] + bpay_accounts = ZaiPayment::Resources::BpayAccount.new + response = bpay_accounts.create( + user_id: 'user_123', + account_name: 'My Bill', + biller_code: 123456, + bpay_crn: '987654321' + ) + + puts "BPay account created: #{response.data['id']}" if response.success? +else + puts "Validation failed: #{validation[:message]}" +end +``` + +## Important Notes + +1. **Required Fields**: + - `user_id` - The user ID associated with the BPay account + - `account_name` - Nickname for the BPay account + - `biller_code` - 3 to 10 digits + - `bpay_crn` - Customer Reference Number, 2 to 20 digits + +2. **Biller Code Validation**: + - Must be numeric + - Must be between 3 and 10 digits + - Example: 123456 + +3. **BPay CRN Validation**: + - Must be numeric + - Must be between 2 and 20 digits + - Example: 987654321 + +4. **BPay Account Usage**: + - Used as a disbursement destination + - Store the returned `:id` for future use + - Can be set as default disbursement account + +5. **Currency**: + - BPay accounts are typically in AUD (Australian Dollars) + - Currency is set automatically based on the marketplace configuration + +6. **Verification Status**: + - New BPay accounts typically have `verification_status: "not_verified"` + - Verification is handled by Zai + diff --git a/implementation.md b/implementation.md deleted file mode 100644 index 8809c2c..0000000 --- a/implementation.md +++ /dev/null @@ -1,304 +0,0 @@ -# User Management Implementation Summary - -## Overview - -This document summarizes the implementation of the User Management feature for the Zai Payment Ruby library. - -## Implementation Date - -October 23, 2025 - -## What Was Implemented - -### 1. User Resource Class (`lib/zai_payment/resources/user.rb`) - -A comprehensive User resource that provides CRUD operations for managing both payin (buyer) and payout (seller/merchant) users. - -**Key Features:** -- āœ… List users with pagination -- āœ… Show user details by ID -- āœ… Create payin users (buyers) -- āœ… Create payout users (sellers/merchants) -- āœ… Update user information -- āœ… Comprehensive validation for all user types -- āœ… Support for all Zai API user fields - -**Supported Fields:** -- Email, first name, last name (required) -- Country (ISO 3166-1 alpha-3 code, required) -- Address details (line1, line2, city, state, zip) -- Contact information (mobile, phone) -- Date of birth (DD/MM/YYYY format) -- Government ID number -- Device ID and IP address (for fraud prevention) -- User type designation (payin/payout) - -**Validation:** -- Required field validation -- Email format validation -- Country code validation (3-letter ISO codes) -- Date of birth format validation (DD/MM/YYYY) -- User type validation (payin/payout) - -### 2. Client Updates (`lib/zai_payment/client.rb`) - -**Changes:** -- Added `base_endpoint` parameter to constructor -- Updated `base_url` method to support multiple API endpoints -- Users API uses `core_base` endpoint -- Webhooks API uses `va_base` endpoint - -### 3. Response Updates (`lib/zai_payment/response.rb`) - -**Changes:** -- Updated `data` method to handle both `webhooks` and `users` response formats -- Maintains backward compatibility with existing webhook code - -### 4. Main Module Integration (`lib/zai_payment.rb`) - -**Changes:** -- Added `require` for User resource -- Added `users` accessor method -- Properly configured User resource to use `core_base` endpoint - -### 5. Comprehensive Test Suite (`spec/zai_payment/resources/user_spec.rb`) - -**Test Coverage:** -- List users with pagination -- Show user details -- Create payin users with various configurations -- Create payout users with required fields -- Validation error handling -- API error handling -- Update operations -- User type validation -- Integration with main module - -**Test Statistics:** -- 40+ test cases -- Covers all CRUD operations -- Tests both success and error scenarios -- Validates all field types -- Tests integration points - -### 6. Documentation - -#### User Guide (`docs/users.md`) -Comprehensive guide covering: -- Overview of payin vs payout users -- Required fields for each user type -- Complete API reference -- Field reference table -- Error handling patterns -- Best practices -- Response structures -- Complete examples - -#### Usage Examples (`examples/users.md`) -Practical examples including: -- Basic payin user creation -- Complete payin user profiles -- Progressive profile building -- Individual payout users -- International users (AU, UK, US) -- List and pagination -- Update operations -- Error handling patterns -- Rails integration example -- Batch operations -- User profile validation helper -- RSpec integration tests -- Common patterns with retry logic - -#### readme Updates (`readme.md`) -- Added Users section with quick examples -- Updated roadmap to mark Users as "Done" -- Added documentation links -- Updated Getting Started section - -## API Endpoints - -The implementation works with the following Zai API endpoints: - -- `GET /users` - List users -- `GET /users/:id` - Show user -- `POST /users` - Create user -- `PATCH /users/:id` - Update user - -## Usage Examples - -### Create a Payin User (Buyer) - -```ruby -response = ZaiPayment.users.create( - email: 'buyer@example.com', - first_name: 'John', - last_name: 'Doe', - country: 'USA', - mobile: '+1234567890' -) - -user_id = response.data['id'] -``` - -### Create a Payout User (Seller/Merchant) - -```ruby -response = ZaiPayment.users.create( - 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', - mobile: '+61412345678' -) - -seller_id = response.data['id'] -``` - -### List Users - -```ruby -response = ZaiPayment.users.list(limit: 10, offset: 0) -users = response.data -``` - -### Show User - -```ruby -response = ZaiPayment.users.show('user_id') -user = response.data -``` - -### Update User - -```ruby -response = ZaiPayment.users.update( - 'user_id', - mobile: '+9876543210', - address_line1: '789 New St' -) -``` - -## Key Differences: Payin vs Payout Users - -### Payin User (Buyer) Requirements -**Required:** -- Email, first name, last name, country -- Device ID and IP address (when charging) - -**Recommended:** -- Address, city, state, zip -- Mobile, DOB - -### Payout User (Seller/Merchant) Requirements -**Required:** -- Email, first name, last name, country -- Address, city, state, zip -- Date of birth (DD/MM/YYYY format) - -**Recommended:** -- Mobile, government number - -## Validation Rules - -1. **Email**: Must be valid email format -2. **Country**: Must be 3-letter ISO 3166-1 alpha-3 code (e.g., USA, AUS, GBR) -3. **Date of Birth**: Must be DD/MM/YYYY format (e.g., 01/01/1990) -4. **User Type**: Must be 'payin' or 'payout' (optional field) - -## Error Handling - -The implementation provides proper error handling for: -- `ValidationError` - Missing or invalid fields -- `UnauthorizedError` - Authentication failures -- `NotFoundError` - User not found -- `ApiError` - General API errors -- `ConnectionError` - Network issues -- `TimeoutError` - Request timeouts - -## Best Practices Implemented - -1. **Progressive Profile Building**: Create users with minimal info, update later -2. **Proper Validation**: Validate data before API calls -3. **Error Recovery**: Handle errors gracefully with proper error classes -4. **Type Safety**: Validate user types and field formats -5. **Documentation**: Comprehensive guides and examples -6. **Testing**: Extensive test coverage for all scenarios - -## Files Created/Modified - -### Created Files: -1. `/lib/zai_payment/resources/user.rb` - User resource class -2. `/spec/zai_payment/resources/user_spec.rb` - Test suite -3. `/docs/users.md` - User management guide -4. `/examples/users.md` - Usage examples - -### Modified Files: -1. `/lib/zai_payment/client.rb` - Added endpoint support -2. `/lib/zai_payment/response.rb` - Added users data handling -3. `/lib/zai_payment.rb` - Integrated User resource -4. `/readme.md` - Added Users section and updated roadmap - -## Code Quality - -- āœ… No linter errors -- āœ… Follows existing code patterns -- āœ… Comprehensive test coverage -- āœ… Well-documented with YARD comments -- āœ… Follows Ruby best practices -- āœ… Consistent with webhook implementation - -## Architecture Decisions - -1. **Endpoint Routing**: Users use `core_base`, webhooks use `va_base` -2. **Validation Strategy**: Client-side validation before API calls -3. **Field Mapping**: Direct 1:1 mapping with Zai API fields -4. **Error Handling**: Leverage existing error class hierarchy -5. **Testing Approach**: Match webhook test patterns - -## Integration Points - -The User resource integrates seamlessly with: -- Authentication system (OAuth2 tokens) -- Error handling framework -- Response wrapper -- Configuration management -- Testing infrastructure - -## Next Steps - -The implementation is complete and ready for use. Recommended next steps: - -1. āœ… Run the full test suite -2. āœ… Review documentation -3. āœ… Try examples in development environment -4. Consider adding: - - Company user support (for payout users) - - User verification status checking - - Bank account associations - - Payment method attachments - -## References - -- [Zai: Onboarding a Payin User](https://developer.hellozai.com/docs/onboarding-a-pay-in-user) -- [Zai: Onboarding a Payout User](https://developer.hellozai.com/docs/onboarding-a-pay-out-user) -- [Zai API Reference](https://developer.hellozai.com/reference) - -## Support - -For questions or issues: -1. Check the documentation in `/docs/users.md` -2. Review examples in `/examples/users.md` -3. Run tests: `bundle exec rspec spec/zai_payment/resources/user_spec.rb` -4. Refer to Zai Developer Portal: https://developer.hellozai.com/ - ---- - -**Implementation completed successfully! šŸŽ‰** - -All CRUD operations for User management are now available in the ZaiPayment gem, following best practices and maintaining consistency with the existing codebase. diff --git a/implementation_summary.md b/implementation_summary.md deleted file mode 100644 index 5766f34..0000000 --- a/implementation_summary.md +++ /dev/null @@ -1,195 +0,0 @@ -# New User Parameters Implementation Summary - -This document summarizes the new body parameters added to the User resource for creating users in the Zai Payment gem. - -## Added Parameters - -### Individual User Parameters - -The following new parameters have been added for individual users: - -1. **`drivers_license_number`** (String) - - Driving license number of the user - - Optional field for enhanced verification - -2. **`drivers_license_state`** (String) - - State section of the user's driving license - - Optional field for enhanced verification - -3. **`logo_url`** (String) - - URL link to the logo - - Optional field for merchant branding - -4. **`color_1`** (String) - - Primary color code (e.g., #FF5733) - - Optional field for merchant branding - -5. **`color_2`** (String) - - Secondary color code (e.g., #C70039) - - Optional field for merchant branding - -6. **`custom_descriptor`** (String) - - Custom text that appears on bank statements - - Optional field for merchant branding - - Shows on bundle direct debit statements, wire payouts, and PayPal statements - -7. **`authorized_signer_title`** (String) - - Job title of the authorized signer (e.g., "Director", "General Manager") - - Required for AMEX merchants - - Refers to the role/job title for the individual user who is authorized to sign contracts - -### Company Object - -A new **`company`** parameter has been added to support business users. When provided, it creates a company for the user. - -#### Required Company Fields - -- **`name`** (String) - Company name -- **`legal_name`** (String) - Legal business name (e.g., "ABC Pty Ltd") -- **`tax_number`** (String) - ABN/TFN/Tax number -- **`business_email`** (String) - Business email address -- **`country`** (String) - Country code (ISO 3166-1 alpha-3) - -#### Optional Company Fields - -- **`charge_tax`** (Boolean) - Whether to charge GST/tax (true/false) -- **`address_line1`** (String) - Business address line 1 -- **`address_line2`** (String) - Business address line 2 -- **`city`** (String) - Business city -- **`state`** (String) - Business state/province -- **`zip`** (String) - Business postal code -- **`phone`** (String) - Business phone number - -## Implementation Details - -### Code Changes - -1. **Field Mappings** - Added new fields to `FIELD_MAPPING` constant -2. **Company Field Mapping** - Added new `COMPANY_FIELD_MAPPING` constant -3. **Validation** - Added `validate_company!` method to validate company required fields -4. **Body Building** - Updated `build_user_body` to handle company object separately -5. **Company Body Builder** - Added `build_company_body` method to construct company payload -6. **Documentation** - Updated all method documentation with new parameters - -### Special Handling - -- **Company Object**: Handled separately in `build_user_body` method -- **Boolean Values**: Special handling for `charge_tax` to preserve `false` values -- **Nested Structure**: Company fields are properly nested in the API payload - -## Usage Examples - -### Enhanced Verification with Driver's License - -```ruby -ZaiPayment.users.create( - email: 'user@example.com', - first_name: 'John', - last_name: 'Doe', - country: 'USA', - drivers_license_number: 'D1234567', - drivers_license_state: 'CA', - government_number: '123-45-6789' -) -``` - -### Merchant with Custom Branding - -```ruby -ZaiPayment.users.create( - email: 'merchant@example.com', - first_name: 'Jane', - last_name: 'Smith', - country: 'AUS', - logo_url: 'https://example.com/logo.png', - color_1: '#FF5733', - color_2: '#C70039', - custom_descriptor: 'MY STORE' -) -``` - -### Business User with Company - -```ruby -ZaiPayment.users.create( - email: 'director@company.com', - first_name: 'John', - last_name: 'Director', - country: 'AUS', - mobile: '+61412345678', - authorized_signer_title: 'Director', - company: { - name: 'ABC Company', - legal_name: 'ABC Company Pty Ltd', - tax_number: '123456789', - business_email: 'admin@abc.com', - country: 'AUS', - charge_tax: true, - address_line1: '123 Business St', - city: 'Melbourne', - state: 'VIC', - zip: '3000' - } -) -``` - -### AMEX Merchant Setup - -```ruby -ZaiPayment.users.create( - email: 'amex.merchant@example.com', - first_name: 'Michael', - last_name: 'Manager', - country: 'AUS', - authorized_signer_title: 'Managing Director', # Required for AMEX - company: { - name: 'AMEX Shop', - legal_name: 'AMEX Shop Pty Limited', - tax_number: '51824753556', - business_email: 'finance@amexshop.com', - country: 'AUS', - charge_tax: true - } -) -``` - -## Validation - -The following validations are in place: - -1. **Company Validation**: When `company` is provided, all required company fields must be present -2. **Email Format**: Must be a valid email address -3. **Country Code**: Must be ISO 3166-1 alpha-3 format (3 letters) -4. **DOB Format**: Must be in DD/MM/YYYY format -5. **User ID**: Cannot contain '.' character - -## Documentation Updates - -The following documentation files have been updated: - -1. **`lib/zai_payment/resources/user.rb`** - Implementation -2. **`examples/users.md`** - Usage examples and patterns -3. **`docs/users.md`** - Field reference and comprehensive guide -4. **`readme.md`** - Quick start example - -## Testing - -All new parameters have been tested and verified: -- āœ“ Drivers license parameters -- āœ“ Branding parameters (logo_url, colors, custom_descriptor) -- āœ“ Authorized signer title -- āœ“ Company object with all fields -- āœ“ Company validation (required fields) -- āœ“ Boolean handling (charge_tax false preservation) - -## API Compatibility - -These changes are **backward compatible**. All new parameters are optional and existing code will continue to work without modifications. - -## Related Files - -- `/lib/zai_payment/resources/user.rb` - Main implementation -- `/examples/users.md` - Usage examples -- `/docs/users.md` - Field reference -- `/readme.md` - Quick start guide - diff --git a/lib/zai_payment.rb b/lib/zai_payment.rb index 1cb8d2f..1c31ce2 100644 --- a/lib/zai_payment.rb +++ b/lib/zai_payment.rb @@ -15,6 +15,7 @@ require_relative 'zai_payment/resources/item' require_relative 'zai_payment/resources/token_auth' require_relative 'zai_payment/resources/bank_account' +require_relative 'zai_payment/resources/bpay_account' module ZaiPayment class << self @@ -63,5 +64,10 @@ def token_auths def bank_accounts @bank_accounts ||= Resources::BankAccount.new(client: Client.new(base_endpoint: :core_base)) end + + # @return [ZaiPayment::Resources::BpayAccount] bpay_account resource instance + def bpay_accounts + @bpay_accounts ||= Resources::BpayAccount.new(client: Client.new(base_endpoint: :core_base)) + end end end diff --git a/lib/zai_payment/resources/bpay_account.rb b/lib/zai_payment/resources/bpay_account.rb new file mode 100644 index 0000000..782d672 --- /dev/null +++ b/lib/zai_payment/resources/bpay_account.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +module ZaiPayment + module Resources + # BpayAccount resource for managing Zai BPay accounts + # + # @see https://developer.hellozai.com/reference/createbpayaccount + class BpayAccount + attr_reader :client + + # Map of attribute keys to API field names + FIELD_MAPPING = { + user_id: :user_id, + account_name: :account_name, + biller_code: :biller_code, + bpay_crn: :bpay_crn + }.freeze + + def initialize(client: nil) + @client = client || Client.new + end + + # Get a specific BPay account by ID + # + # @param bpay_account_id [String] the BPay account ID + # @return [Response] the API response containing BPay account details + # + # @example + # bpay_accounts = ZaiPayment::Resources::BpayAccount.new + # response = bpay_accounts.show("bpay_account_id") + # response.data # => {"id" => "bpay_account_id", "active" => true, ...} + # + # @see https://developer.hellozai.com/reference/showbpayaccount + def show(bpay_account_id) + validate_id!(bpay_account_id, 'bpay_account_id') + client.get("/bpay_accounts/#{bpay_account_id}") + end + + # Redact a BPay account + # + # Redacts a BPay account using the given bpay_account_id. Redacted BPay accounts + # can no longer be used as a disbursement destination. + # + # @param bpay_account_id [String] the BPay account ID + # @return [Response] the API response + # + # @example + # bpay_accounts = ZaiPayment::Resources::BpayAccount.new + # response = bpay_accounts.redact("bpay_account_id") + # + # @see https://developer.hellozai.com/reference/redactbpayaccount + def redact(bpay_account_id) + validate_id!(bpay_account_id, 'bpay_account_id') + client.delete("/bpay_accounts/#{bpay_account_id}") + end + + # Get the user associated with a BPay account + # + # Show the User the BPay Account is associated with using a given bpay_account_id. + # + # @param bpay_account_id [String] the BPay account ID + # @return [Response] the API response containing user details + # + # @example + # bpay_accounts = ZaiPayment::Resources::BpayAccount.new + # response = bpay_accounts.show_user("bpay_account_id") + # response.data # => {"id" => "user_id", "full_name" => "Samuel Seller", ...} + # + # @see https://developer.hellozai.com/reference/showbpayaccountuser + def show_user(bpay_account_id) + validate_id!(bpay_account_id, 'bpay_account_id') + client.get("/bpay_accounts/#{bpay_account_id}/users") + end + + # Create a new BPay account + # + # Create a BPay Account to be used as a Disbursement destination. + # + # @param attributes [Hash] BPay account attributes + # @option attributes [String] :user_id (Required) User ID + # @option attributes [String] :account_name (Required) Name assigned by the platform/marketplace + # to identify the account (similar to a nickname). Defaults to "My Water Bill Company" + # @option attributes [Integer] :biller_code (Required) The Biller Code for the biller that will + # receive the payment. The Biller Code must be a numeric value with 3 to 10 digits. + # @option attributes [String] :bpay_crn (Required) Customer reference number (crn) to be used for + # this bpay account. The CRN must contain between 2 and 20 digits. Defaults to "987654321" + # @return [Response] the API response containing created BPay account + # + # @example Create a BPay account + # bpay_accounts = ZaiPayment::Resources::BpayAccount.new + # response = bpay_accounts.create( + # user_id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + # account_name: 'My Water Bill Company', + # biller_code: 123456, + # bpay_crn: '987654321' + # ) + # + # @see https://developer.hellozai.com/reference/createbpayaccount + def create(**attributes) + validate_create_attributes!(attributes) + + body = build_bpay_account_body(attributes) + client.post('/bpay_accounts', 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_create_attributes!(attributes) + validate_required_attributes!(attributes) + validate_biller_code!(attributes[:biller_code]) if attributes[:biller_code] + validate_bpay_crn!(attributes[:bpay_crn]) if attributes[:bpay_crn] + end + + def validate_required_attributes!(attributes) + required_fields = %i[user_id account_name biller_code bpay_crn] + + 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_biller_code!(biller_code) + # Biller code must be a numeric value with 3 to 10 digits + biller_code_str = biller_code.to_s + + return if biller_code_str.match?(/\A\d{3,10}\z/) + + raise Errors::ValidationError, + 'biller_code must be a numeric value with 3 to 10 digits' + end + + def validate_bpay_crn!(bpay_crn) + # CRN must contain between 2 and 20 digits + bpay_crn_str = bpay_crn.to_s + + return if bpay_crn_str.match?(/\A\d{2,20}\z/) + + raise Errors::ValidationError, + 'bpay_crn must contain between 2 and 20 digits' + end + + def build_bpay_account_body(attributes) + body = {} + + 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/version.rb b/lib/zai_payment/version.rb index 2148c81..43ac7ce 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.5.0' + VERSION = '2.6.0' end diff --git a/spec/zai_payment/resources/bpay_account_spec.rb b/spec/zai_payment/resources/bpay_account_spec.rb new file mode 100644 index 0000000..2fe5b30 --- /dev/null +++ b/spec/zai_payment/resources/bpay_account_spec.rb @@ -0,0 +1,279 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ZaiPayment::Resources::BpayAccount do + let(:stubs) { Faraday::Adapter::Test::Stubs.new } + let(:bpay_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(:bpay_account_data) do + { + 'bpay_accounts' => { + 'id' => 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'active' => true, + 'created_at' => '2020-04-03T07:59:00.379Z', + 'updated_at' => '2020-04-03T07:59:00.379Z', + 'verification_status' => 'not_verified', + 'currency' => 'AUD', + 'bpay_details' => { + 'account_name' => 'My Water Bill Company', + 'biller_code' => 123_456, + 'biller_name' => 'ABC Water', + 'crn' => 987_654_321 + }, + 'links' => { + 'self' => '/bpay_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'users' => '/bpay_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/users' + } + } + } + end + + context 'when BPay account exists' do + before do + stubs.get('/bpay_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee') do + [200, { 'Content-Type' => 'application/json' }, bpay_account_data] + end + end + + it 'returns the correct response type and BPay account details' do + response = bpay_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 'when BPay account does not exist' do + before do + stubs.get('/bpay_accounts/invalid_id') do + [404, { 'Content-Type' => 'application/json' }, { 'errors' => 'Not found' }] + end + end + + it 'raises a NotFoundError' do + expect { bpay_account_resource.show('invalid_id') } + .to raise_error(ZaiPayment::Errors::NotFoundError) + end + end + + context 'when bpay_account_id is blank' do + it 'raises a ValidationError for empty string' do + expect { bpay_account_resource.show('') } + .to raise_error(ZaiPayment::Errors::ValidationError, /bpay_account_id/) + end + + it 'raises a ValidationError for nil' do + expect { bpay_account_resource.show(nil) } + .to raise_error(ZaiPayment::Errors::ValidationError, /bpay_account_id/) + end + end + end + + describe '#redact' do + context 'when successful' do + before do + stubs.delete('/bpay_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee') do + [200, { 'Content-Type' => 'application/json' }, { 'bpay_account' => 'Successfully redacted' }] + end + end + + it 'returns the correct response type' do + response = bpay_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 bpay_account_id is blank' do + it 'raises a ValidationError for empty string' do + expect { bpay_account_resource.redact('') } + .to raise_error(ZaiPayment::Errors::ValidationError, /bpay_account_id/) + end + + it 'raises a ValidationError for nil' do + expect { bpay_account_resource.redact(nil) } + .to raise_error(ZaiPayment::Errors::ValidationError, /bpay_account_id/) + end + end + + context 'when BPay account does not exist' do + before do + stubs.delete('/bpay_accounts/invalid_id') do + [404, { 'Content-Type' => 'application/json' }, { 'errors' => 'Not found' }] + end + end + + it 'raises a NotFoundError' do + expect { bpay_account_resource.redact('invalid_id') } + .to raise_error(ZaiPayment::Errors::NotFoundError) + 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' => '/bpay_accounts/901d8cd0-6af3-0138-967d-0a58a9feac04/users', + 'items' => '/users/e6bc0480-57ae-0138-c46e-0a58a9feac03/items' + } + } + } + end + + context 'when BPay account has an associated user' do + before do + stubs.get('/bpay_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/users') do + [200, { 'Content-Type' => 'application/json' }, user_data] + end + end + + it 'returns the correct response type and user details' do + response = bpay_account_resource.show_user('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee') + + 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 BPay account does not exist' do + before do + stubs.get('/bpay_accounts/invalid_id/users') do + [404, { 'Content-Type' => 'application/json' }, { 'errors' => 'Not found' }] + end + end + + it 'raises a NotFoundError' do + expect { bpay_account_resource.show_user('invalid_id') } + .to raise_error(ZaiPayment::Errors::NotFoundError) + end + end + + context 'when bpay_account_id is blank' do + it 'raises a ValidationError for empty string' do + expect { bpay_account_resource.show_user('') } + .to raise_error(ZaiPayment::Errors::ValidationError, /bpay_account_id/) + end + + it 'raises a ValidationError for nil' do + expect { bpay_account_resource.show_user(nil) } + .to raise_error(ZaiPayment::Errors::ValidationError, /bpay_account_id/) + end + end + end + + describe '#create' do + let(:bpay_account_data) do + { + 'bpay_accounts' => { + 'id' => 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'created_at' => '2020-04-03T07:59:00.379Z', + 'updated_at' => '2020-04-03T07:59:00.379Z', + 'active' => true, + 'verification_status' => 'not_verified', + 'currency' => 'AUD', + 'bpay_details' => { + 'account_name' => 'My Water Bill Company', + 'biller_code' => 123_456, + 'biller_name' => 'ABC Water', + 'crn' => 987_654_321 + }, + 'links' => { + 'self' => '/bpay_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'users' => '/bpay_accounts/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/users' + } + } + } + end + + let(:valid_params) do + { + user_id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + account_name: 'My Water Bill Company', + biller_code: 123_456, + bpay_crn: '987654321' + } + end + + context 'when successful' do + before do + stubs.post('/bpay_accounts') do + [201, { 'Content-Type' => 'application/json' }, bpay_account_data] + end + end + + it 'returns the correct response type and creates BPay account' do + response = bpay_account_resource.create(**valid_params) + + 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 'when validation fails' do + it 'raises error when required fields are missing' do + expect do + bpay_account_resource.create(user_id: 'user_123', account_name: 'Test') + end.to raise_error(ZaiPayment::Errors::ValidationError, /Missing required fields/) + end + + it 'raises error for invalid biller_code length' do + params = valid_params.merge(biller_code: 12) + expect { bpay_account_resource.create(**params) } + .to raise_error(ZaiPayment::Errors::ValidationError, + /biller_code must be a numeric value with 3 to 10 digits/) + end + + it 'raises error for invalid bpay_crn length' do + params = valid_params.merge(bpay_crn: '1') + expect { bpay_account_resource.create(**params) } + .to raise_error(ZaiPayment::Errors::ValidationError, /bpay_crn must contain between 2 and 20 digits/) + end + end + end +end