diff --git a/docs/pay_ids.md b/docs/pay_ids.md new file mode 100644 index 0000000..40acd64 --- /dev/null +++ b/docs/pay_ids.md @@ -0,0 +1,777 @@ +# PayID Management + +The PayId resource provides methods for registering and managing Zai PayIDs for virtual accounts. + +## Overview + +PayIDs are easy-to-remember identifiers (like email addresses) that can be used instead of BSB and account numbers for receiving payments in Australia's New Payments Platform (NPP). They provide a more user-friendly way for customers to make payments. + +PayIDs are particularly useful for: +- Simplifying payment collection with memorable identifiers +- Enabling fast payments through the New Payments Platform +- Providing customers with an alternative to BSB and account numbers +- Enhancing user experience with familiar email-based identifiers + +## Key Features + +- **Email-based PayIDs**: Register email addresses as payment identifiers +- **Linked to Virtual Accounts**: Each PayID is associated with a virtual account +- **Status Tracking**: Monitor PayID status (pending_activation, active, etc.) +- **Secure Registration**: Validated registration process with proper error handling + +## References + +- [PayID API](https://developer.hellozai.com/reference/registerpayid) +- [Zai API Documentation](https://developer.hellozai.com/docs) + +## Usage + +### Initialize the PayId Resource + +```ruby +# Using a new instance +pay_ids = ZaiPayment::Resources::PayId.new + +# Or use with custom client +client = ZaiPayment::Client.new(base_endpoint: :va_base) +pay_ids = ZaiPayment::Resources::PayId.new(client: client) +``` + +## Methods + +### Register PayID + +Register a PayID for a given Virtual Account. This creates a PayID that customers can use to send payments to the virtual account. + +#### Parameters + +- `virtual_account_id` (required) - The virtual account ID +- `pay_id` (required) - The PayID being registered (max 256 characters) +- `type` (required) - The type of PayID (currently only 'EMAIL' is supported) +- `details` (required) - Hash containing additional details: + - `pay_id_name` (optional) - Name to identify the entity (1-140 characters) + - `owner_legal_name` (optional) - Full legal account name (1-140 characters) + +#### Example + +```ruby +# Register an EMAIL PayID +pay_ids = ZaiPayment::Resources::PayId.new + +response = pay_ids.create( + '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + pay_id: 'jsmith@mydomain.com', + type: 'EMAIL', + details: { + pay_id_name: 'J Smith', + owner_legal_name: 'Mr John Smith' + } +) + +# Access PayID details +if response.success? + pay_id = response.data + + puts "PayID: #{pay_id['pay_id']}" + puts "Type: #{pay_id['type']}" + puts "Status: #{pay_id['status']}" + puts "PayID Name: #{pay_id['details']['pay_id_name']}" + puts "Owner Legal Name: #{pay_id['details']['owner_legal_name']}" +end +``` + +#### Response + +```ruby +{ + "pay_ids" => { + "id" => "46deb476-c1a6-41eb-8eb7-26a695bbe5bc", + "pay_id" => "jsmith@mydomain.com", + "type" => "EMAIL", + "status" => "pending_activation", + "created_at" => "2020-04-27T20:28:22.378Z", + "updated_at" => "2020-04-27T20:28:22.378Z", + "details" => { + "pay_id_name" => "J Smith", + "owner_legal_name" => "Mr John Smith" + }, + "links" => { + "self" => "/pay_ids/46deb476-c1a6-41eb-8eb7-26a695bbe5bc", + "virtual_accounts" => "/virtual_accounts/46deb476-c1a6-41eb-8eb7-26a695bbe5bc" + } + } +} +``` + +**Response Fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `id` | String | Unique identifier for the PayID | +| `pay_id` | String | The registered PayID (email address) | +| `type` | String | Type of PayID (e.g., "EMAIL") | +| `status` | String | PayID status (pending_activation, active, etc.) | +| `created_at` | String | ISO 8601 timestamp of creation | +| `updated_at` | String | ISO 8601 timestamp of last update | +| `details` | Hash | Additional PayID details | +| `details.pay_id_name` | String | Name to identify the entity | +| `details.owner_legal_name` | String | Full legal account name | +| `links` | Hash | Related resource links | +| `links.self` | String | URL to the PayID resource | +| `links.virtual_accounts` | String | URL to the associated virtual account | + +**Use Cases:** + +- Register PayIDs for easy customer payments +- Enable NPP fast payments +- Provide user-friendly payment identifiers +- Link email addresses to virtual accounts for payment collection +- Set up payment collection for businesses and individuals + +### Show PayID + +Show details of a specific PayID using the given PayID ID. + +#### Parameters + +- `pay_id_id` (required) - The PayID ID + +#### Example + +```ruby +# Get specific PayID details +pay_ids = ZaiPayment::Resources::PayId.new +response = pay_ids.show('46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + +# Access PayID details +if response.success? + pay_id = response.data + + puts "PayID: #{pay_id['pay_id']}" + puts "Type: #{pay_id['type']}" + puts "Status: #{pay_id['status']}" + puts "PayID Name: #{pay_id['details']['pay_id_name']}" + puts "Owner Legal Name: #{pay_id['details']['owner_legal_name']}" +end +``` + +#### Response + +```ruby +{ + "pay_ids" => { + "id" => "46deb476-c1a6-41eb-8eb7-26a695bbe5bc", + "pay_id" => "jsmith@mydomain.com", + "type" => "EMAIL", + "status" => "active", + "created_at" => "2020-04-27T20:28:22.378Z", + "updated_at" => "2020-04-27T20:28:22.378Z", + "details" => { + "pay_id_name" => "J Smith", + "owner_legal_name" => "Mr John Smith" + }, + "links" => { + "self" => "/pay_ids/46deb476-c1a6-41eb-8eb7-26a695bbe5bc", + "virtual_accounts" => "/virtual_accounts/46deb476-c1a6-41eb-8eb7-26a695bbe5bc" + } + } +} +``` + +**Response Fields:** + +The response contains a single PayID object with the same fields as described in the Register PayID section. + +**Use Cases:** + +- Verify PayID details before using +- Check PayID status +- Retrieve PayID information for display to users +- Audit PayID configurations +- Validate PayID information +- Monitor PayID updates + +### Update PayID Status + +Update the status of a PayID. Currently, this endpoint only supports deregistering PayIDs by setting the status to 'deregistered'. This is an asynchronous operation that returns a 202 Accepted response. + +**Important:** Once a PayID is deregistered, it cannot be re-registered to the same virtual account. + +#### Parameters + +- `pay_id_id` (required) - The PayID ID +- `status` (required) - The new status (must be 'deregistered') + +#### Example + +```ruby +# Deregister a PayID +pay_ids = ZaiPayment::Resources::PayId.new + +response = pay_ids.update_status( + '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + 'deregistered' +) + +# Check the response +if response.success? + puts "PayID deregistration initiated" + puts "ID: #{response.data['id']}" + puts "Message: #{response.data['message']}" + puts "Link: #{response.data['links']['self']}" +end +``` + +#### Response + +```ruby +{ + "id" => "46deb476-c1a6-41eb-8eb7-26a695bbe5bc", + "message" => "PayID deregistration has been accepted for processing", + "links" => { + "self" => "/pay_ids/46deb476-c1a6-41eb-8eb7-26a695bbe5bc" + } +} +``` + +**Response Fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `id` | String | The PayID ID | +| `message` | String | Confirmation message about the status update | +| `links` | Hash | Related resource links | +| `links.self` | String | URL to the PayID resource | + +**Use Cases:** + +- Deregister PayIDs when no longer needed +- Remove PayID from a virtual account +- Clean up unused payment identifiers +- Comply with customer requests to remove their email from the system +- Deactivate PayIDs as part of account closure + +**Important Notes:** + +- This operation returns 202 Accepted because it's processed asynchronously +- The actual status change may take a few moments to complete +- Only 'deregistered' is a valid status value; other values will raise a ValidationError +- Deregistered PayIDs cannot be re-registered + +## Validation Rules + +### virtual_account_id + +- **Required**: Yes +- **Type**: String (UUID) +- **Description**: The ID of the virtual account that this PayID will be linked to. The virtual account must exist before registering a PayID. + +### pay_id + +- **Required**: Yes +- **Type**: String +- **Max Length**: 256 characters +- **Description**: The PayID being registered. For EMAIL type, this should be a valid email address. + +### type + +- **Required**: Yes +- **Type**: String (Enum) +- **Allowed Values**: EMAIL +- **Description**: The type of PayID being registered. Currently, only EMAIL type is supported. + +### details + +- **Required**: Yes +- **Type**: Hash +- **Description**: Additional details about the PayID registration. + +#### details.pay_id_name + +- **Required**: No +- **Type**: String +- **Length**: 1-140 characters (when provided) +- **Description**: A name that can be used to identify the entity registering the PayID. This helps customers confirm they're sending to the right recipient. + +#### details.owner_legal_name + +- **Required**: No +- **Type**: String +- **Length**: 1-140 characters (when provided) +- **Description**: The full legal account name. This is displayed to customers during payment confirmation. + +## Error Handling + +The PayId methods can raise the following errors: + +### ZaiPayment::Errors::ValidationError + +Raised when input parameters fail validation: + +```ruby +begin + response = pay_ids.create( + '', # Empty virtual_account_id + pay_id: 'test@example.com', + type: 'EMAIL', + details: {} + ) +rescue ZaiPayment::Errors::ValidationError => e + puts "Validation failed: #{e.message}" + # Output: "virtual_account_id is required and cannot be blank" +end +``` + +**Common validation errors:** +- `virtual_account_id is required and cannot be blank` +- `pay_id is required and cannot be blank` +- `pay_id must be 256 characters or less` +- `type is required and cannot be blank` +- `type must be one of: EMAIL` +- `details is required and must be a hash` +- `pay_id_name must be between 1 and 140 characters` +- `owner_legal_name must be between 1 and 140 characters` + +### ZaiPayment::Errors::NotFoundError + +Raised when the virtual account doesn't exist: + +```ruby +begin + response = pay_ids.create( + 'invalid-virtual-account-id', + pay_id: 'test@example.com', + type: 'EMAIL', + details: {} + ) +rescue ZaiPayment::Errors::NotFoundError => e + puts "Not found: #{e.message}" +end +``` + +### ZaiPayment::Errors::UnauthorizedError + +Raised when authentication fails: + +```ruby +begin + response = pay_ids.create( + virtual_account_id, + pay_id: 'test@example.com', + type: 'EMAIL', + details: {} + ) +rescue ZaiPayment::Errors::UnauthorizedError => e + puts "Authentication failed: #{e.message}" + # Check your API credentials +end +``` + +### ZaiPayment::Errors::BadRequestError + +Raised when the request is malformed or contains invalid data (e.g., PayID already registered): + +```ruby +begin + response = pay_ids.create( + virtual_account_id, + pay_id: 'test@example.com', + type: 'EMAIL', + details: {} + ) +rescue ZaiPayment::Errors::BadRequestError => e + puts "Bad request: #{e.message}" + # May indicate PayID is already registered +end +``` + +## Complete Example + +Here's a complete workflow showing how to create a virtual account and register a PayID with proper error handling: + +```ruby +require 'zai_payment' + +# Configure ZaiPayment +ZaiPayment.configure do |config| + config.environment = :prelive + config.client_id = ENV['ZAI_CLIENT_ID'] + config.client_secret = ENV['ZAI_CLIENT_SECRET'] + config.scope = ENV['ZAI_SCOPE'] +end + +# Initialize resources +virtual_accounts = ZaiPayment::Resources::VirtualAccount.new +pay_ids = ZaiPayment::Resources::PayId.new +wallet_account_id = 'ae07556e-22ef-11eb-adc1-0242ac120002' + +begin + # Step 1: Create a Virtual Account + puts "Creating virtual account..." + + va_response = virtual_accounts.create( + wallet_account_id, + account_name: 'Customer Payment Account', + aka_names: ['Customer Account'] + ) + + if va_response.success? + virtual_account = va_response.data + virtual_account_id = virtual_account['id'] + + puts "✓ Virtual Account Created" + puts " ID: #{virtual_account_id}" + puts " BSB: #{virtual_account['routing_number']}" + puts " Account: #{virtual_account['account_number']}" + puts " Status: #{virtual_account['status']}" + + # Step 2: Register PayID + puts "\nRegistering PayID..." + + payid_response = pay_ids.create( + virtual_account_id, + pay_id: 'customer@mybusiness.com', + type: 'EMAIL', + details: { + pay_id_name: 'My Business', + owner_legal_name: 'My Business Pty Ltd' + } + ) + + if payid_response.success? + pay_id = payid_response.data + + puts "✓ PayID Registered Successfully!" + puts "─" * 60 + puts "PayID Details:" + puts " ID: #{pay_id['id']}" + puts " PayID: #{pay_id['pay_id']}" + puts " Type: #{pay_id['type']}" + puts " Status: #{pay_id['status']}" + puts "" + puts "Payment Information:" + puts " PayID Name: #{pay_id['details']['pay_id_name']}" + puts " Owner Legal Name: #{pay_id['details']['owner_legal_name']}" + puts "" + puts "Customers can now send payments using:" + puts " PayID: #{pay_id['pay_id']}" + puts " OR" + puts " BSB: #{virtual_account['routing_number']}" + puts " Account: #{virtual_account['account_number']}" + puts "─" * 60 + + # Store the details in your database for future reference + # YourDatabase.store_pay_id(pay_id) + else + puts "Failed to register PayID" + end + else + puts "Failed to create virtual account" + end + +rescue ZaiPayment::Errors::ValidationError => e + puts "Validation Error: #{e.message}" + puts "Please check your input parameters" + +rescue ZaiPayment::Errors::NotFoundError => e + puts "Resource Not Found: #{e.message}" + puts "Please verify the wallet account or virtual account exists" + +rescue ZaiPayment::Errors::UnauthorizedError => e + puts "Authentication Failed: #{e.message}" + puts "Please check your API credentials" + +rescue ZaiPayment::Errors::BadRequestError => e + puts "Bad Request: #{e.message}" + puts "The PayID may already be registered or request is invalid" + +rescue ZaiPayment::Errors::ApiError => e + puts "API Error: #{e.message}" +end +``` + +## Configuration + +PayIDs use the `va_base` endpoint, which is automatically configured based on your environment: + +### Prelive Environment + +```ruby +ZaiPayment.configure do |config| + config.environment = :prelive + # Uses: https://sandbox.au-0000.api.assemblypay.com +end +``` + +### Production Environment + +```ruby +ZaiPayment.configure do |config| + config.environment = :production + # Uses: https://secure.api.promisepay.com +end +``` + +The PayId resource automatically uses the correct endpoint based on your configuration. + +## Best Practices + +### 1. Use Meaningful Names + +Use descriptive names in the details to help customers confirm the recipient: + +```ruby +# Good +details: { + pay_id_name: 'Acme Corporation', + owner_legal_name: 'Acme Corporation Pty Ltd' +} + +# Avoid +details: { + pay_id_name: 'Acc1', + owner_legal_name: 'A' +} +``` + +### 2. Validate Email Format + +Pre-validate the email format before registration: + +```ruby +def valid_email?(email) + email =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i +end + +pay_id = 'customer@example.com' + +if valid_email?(pay_id) + # Proceed with registration +else + puts "Invalid email format" +end +``` + +### 3. Store PayID Details + +Always store the PayID details in your database: + +```ruby +response = pay_ids.create(virtual_account_id, pay_id: email, type: 'EMAIL', details: details) + +if response.success? + pay_id = response.data + + # Store in database + PayIdRecord.create!( + external_id: pay_id['id'], + virtual_account_id: virtual_account_id, + pay_id: pay_id['pay_id'], + pay_id_type: pay_id['type'], + pay_id_name: pay_id['details']['pay_id_name'], + owner_legal_name: pay_id['details']['owner_legal_name'], + status: pay_id['status'] + ) +end +``` + +### 4. Handle Errors Gracefully + +Always implement proper error handling: + +```ruby +def register_pay_id_safely(virtual_account_id, pay_id_email, details) + pay_ids = ZaiPayment::Resources::PayId.new + + begin + response = pay_ids.create( + virtual_account_id, + pay_id: pay_id_email, + type: 'EMAIL', + details: details + ) + { success: true, data: response.data } + rescue ZaiPayment::Errors::ValidationError => e + { success: false, error: 'validation', message: e.message } + rescue ZaiPayment::Errors::NotFoundError => e + { success: false, error: 'not_found', message: e.message } + rescue ZaiPayment::Errors::BadRequestError => e + { success: false, error: 'bad_request', message: e.message } + rescue ZaiPayment::Errors::ApiError => e + { success: false, error: 'api_error', message: e.message } + end +end +``` + +### 5. Verify Virtual Account Exists + +Check that the virtual account exists before registering a PayID: + +```ruby +virtual_accounts = ZaiPayment::Resources::VirtualAccount.new + +begin + # Verify virtual account exists + va_response = virtual_accounts.show(virtual_account_id) + + if va_response.success? && va_response.data['status'] == 'active' + # Proceed with PayID registration + pay_ids.create(virtual_account_id, pay_id: email, type: 'EMAIL', details: details) + else + puts "Virtual account is not active yet" + end +rescue ZaiPayment::Errors::NotFoundError + puts "Virtual account does not exist" +end +``` + +### 6. Monitor PayID Status + +After registration, monitor the PayID status: + +```ruby +pay_id = response.data + +case pay_id['status'] +when 'pending_activation' + puts "PayID registered, awaiting activation" +when 'active' + puts "PayID is active and ready to receive payments" +when 'inactive' + puts "PayID is inactive" +else + puts "Unknown status: #{pay_id['status']}" +end +``` + +### 7. Secure PayID Information + +Treat PayID details as sensitive payment information: + +```ruby +# Don't log sensitive details in production +if Rails.env.production? + logger.info "PayID registered: #{pay_id['id']}" +else + logger.debug "PayID details: #{pay_id.inspect}" +end + +# Use HTTPS for all communications +# Store securely in your database +# Limit access to authorized personnel only +``` + +## Testing + +For testing in prelive environment: + +```ruby +# spec/services/pay_id_service_spec.rb +require 'spec_helper' + +RSpec.describe PayIdService do + let(:virtual_account_id) { 'test-virtual-account-id' } + + describe '#register_pay_id' do + it 'registers a PayID successfully' do + VCR.use_cassette('pay_id_register') do + service = PayIdService.new + result = service.register_pay_id( + virtual_account_id, + pay_id: 'test@example.com', + details: { + pay_id_name: 'Test User', + owner_legal_name: 'Test User Full Name' + } + ) + + expect(result[:success]).to be true + expect(result[:pay_id]['id']).to be_present + expect(result[:pay_id]['pay_id']).to eq('test@example.com') + expect(result[:pay_id]['type']).to eq('EMAIL') + end + end + end +end +``` + +## Troubleshooting + +### Issue: ValidationError - "virtual_account_id is required" + +**Solution**: Ensure you're passing a valid virtual account ID: + +```ruby +# Wrong +pay_ids.create('', pay_id: 'test@example.com', type: 'EMAIL', details: {}) + +# Correct +pay_ids.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', pay_id: 'test@example.com', type: 'EMAIL', details: {}) +``` + +### Issue: NotFoundError - "Virtual account not found" + +**Solution**: Verify the virtual account exists before registering a PayID: + +```ruby +# Check virtual account exists first +virtual_accounts = ZaiPayment::Resources::VirtualAccount.new +begin + va_response = virtual_accounts.show(virtual_account_id) + if va_response.success? + # Virtual account exists, proceed with PayID registration + pay_ids.create(virtual_account_id, pay_id: 'test@example.com', type: 'EMAIL', details: {}) + end +rescue ZaiPayment::Errors::NotFoundError + puts "Virtual account does not exist" +end +``` + +### Issue: ValidationError - "pay_id must be 256 characters or less" + +**Solution**: Ensure the PayID (email) is not too long: + +```ruby +pay_id_email = 'verylongemail@example.com' + +if pay_id_email.length <= 256 + pay_ids.create(virtual_account_id, pay_id: pay_id_email, type: 'EMAIL', details: {}) +else + puts "PayID is too long" +end +``` + +### Issue: ValidationError - "type must be one of: EMAIL" + +**Solution**: Ensure you're using a valid type: + +```ruby +# Wrong +pay_ids.create(virtual_account_id, pay_id: 'test@example.com', type: 'PHONE', details: {}) + +# Correct +pay_ids.create(virtual_account_id, pay_id: 'test@example.com', type: 'EMAIL', details: {}) +``` + +### Issue: BadRequestError - PayID already registered + +**Solution**: PayIDs must be unique. Check if the PayID is already registered: + +```ruby +begin + pay_ids.create(virtual_account_id, pay_id: 'test@example.com', type: 'EMAIL', details: {}) +rescue ZaiPayment::Errors::BadRequestError => e + if e.message.include?('already registered') + puts "This PayID is already registered to another account" + else + puts "Bad request: #{e.message}" + end +end +``` + +## See Also + +- [Virtual Accounts Documentation](virtual_accounts.md) +- [Examples](../examples/pay_ids.md) +- [Zai API Documentation](https://developer.hellozai.com/docs) + diff --git a/examples/pay_ids.md b/examples/pay_ids.md new file mode 100644 index 0000000..62f1077 --- /dev/null +++ b/examples/pay_ids.md @@ -0,0 +1,871 @@ +# PayID Management Examples + +This document provides practical examples for managing PayIDs in Zai Payment. + +## Table of Contents + +- [Setup](#setup) +- [Register PayID Example](#register-payid-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 +``` + +## Register PayID Example + +### Example 1: Register an EMAIL PayID + +Register a PayID for a given virtual account. + +```ruby +# Register PayID +pay_ids = ZaiPayment::Resources::PayId.new + +response = pay_ids.create( + '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', # virtual_account_id + pay_id: 'jsmith@mydomain.com', + type: 'EMAIL', + details: { + pay_id_name: 'J Smith', + owner_legal_name: 'Mr John Smith' + } +) + +if response.success? + pay_id = response.data + puts "PayID Registered!" + puts "ID: #{pay_id['id']}" + puts "PayID: #{pay_id['pay_id']}" + puts "Type: #{pay_id['type']}" + puts "Status: #{pay_id['status']}" + puts "PayID Name: #{pay_id['details']['pay_id_name']}" + puts "Owner Legal Name: #{pay_id['details']['owner_legal_name']}" + puts "Created At: #{pay_id['created_at']}" +else + puts "Failed to register PayID" + puts "Error: #{response.error}" +end +``` + +### Example 2: Register PayID with Error Handling + +Register a PayID with comprehensive error handling. + +```ruby +pay_ids = ZaiPayment::Resources::PayId.new + +begin + response = pay_ids.create( + '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + pay_id: 'jsmith@mydomain.com', + type: 'EMAIL', + details: { + pay_id_name: 'J Smith', + owner_legal_name: 'Mr John Smith' + } + ) + + if response.success? + pay_id = response.data + + puts "✓ PayID Registered Successfully!" + puts "─" * 50 + puts "PayID ID: #{pay_id['id']}" + puts "PayID: #{pay_id['pay_id']}" + puts "Type: #{pay_id['type']}" + puts "Status: #{pay_id['status']}" + puts "" + puts "Details:" + puts " PayID Name: #{pay_id['details']['pay_id_name']}" + puts " Owner Legal Name: #{pay_id['details']['owner_legal_name']}" + puts "" + puts "Links:" + puts " Self: #{pay_id['links']['self']}" + puts " Virtual Account: #{pay_id['links']['virtual_accounts']}" + puts "─" * 50 + end + +rescue ZaiPayment::Errors::ValidationError => e + puts "Validation Error: #{e.message}" + puts "Please check your input parameters" +rescue ZaiPayment::Errors::NotFoundError => e + puts "Not Found: #{e.message}" + puts "The virtual account may not exist" +rescue ZaiPayment::Errors::UnauthorizedError => e + puts "Unauthorized: #{e.message}" + puts "Please check your API credentials" +rescue ZaiPayment::Errors::ApiError => e + puts "API Error: #{e.message}" +end +``` + +### Example 3: Register PayID for Business Email + +Register a PayID using a business email address. + +```ruby +pay_ids = ZaiPayment::Resources::PayId.new + +response = pay_ids.create( + '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + pay_id: 'payments@mybusiness.com.au', + type: 'EMAIL', + details: { + pay_id_name: 'MyBusiness Pty Ltd', + owner_legal_name: 'MyBusiness Pty Ltd' + } +) + +if response.success? + puts "Business PayID registered successfully" + puts "PayID: #{response.data['pay_id']}" + puts "Status: #{response.data['status']}" + puts "Use this PayID for receiving payments from customers" +end +``` + +### Example 4: Register PayID After Creating Virtual Account + +Complete workflow showing virtual account creation followed by PayID registration. + +```ruby +begin + # Step 1: Create a Virtual Account + virtual_accounts = ZaiPayment::Resources::VirtualAccount.new + wallet_account_id = 'ae07556e-22ef-11eb-adc1-0242ac120002' + + va_response = virtual_accounts.create( + wallet_account_id, + account_name: 'Real Estate Trust Account', + aka_names: ['RE Trust Account'] + ) + + if va_response.success? + virtual_account = va_response.data + virtual_account_id = virtual_account['id'] + + puts "✓ Virtual Account Created" + puts " ID: #{virtual_account_id}" + puts " BSB: #{virtual_account['routing_number']}" + puts " Account: #{virtual_account['account_number']}" + + # Step 2: Register PayID for the Virtual Account + pay_ids = ZaiPayment::Resources::PayId.new + + payid_response = pay_ids.create( + virtual_account_id, + pay_id: 'trust@realestate.com.au', + type: 'EMAIL', + details: { + pay_id_name: 'RE Trust', + owner_legal_name: 'Real Estate Trust Account' + } + ) + + if payid_response.success? + pay_id = payid_response.data + + puts "\n✓ PayID Registered" + puts " PayID: #{pay_id['pay_id']}" + puts " Type: #{pay_id['type']}" + puts " Status: #{pay_id['status']}" + puts "" + puts "Customers can now send payments to:" + puts " PayID: #{pay_id['pay_id']}" + puts " OR" + puts " BSB: #{virtual_account['routing_number']}" + puts " Account: #{virtual_account['account_number']}" + end + end + +rescue ZaiPayment::Errors::ApiError => e + puts "Error: #{e.message}" +end +``` + +### Example 5: Validate Before Registering + +Pre-validate PayID data before making the API call. + +```ruby +def validate_pay_id_params(pay_id, type, details) + errors = [] + + # Validate pay_id + if pay_id.nil? || pay_id.strip.empty? + errors << 'PayID is required' + elsif pay_id.length > 256 + errors << 'PayID must be 256 characters or less' + elsif !pay_id.include?('@') + errors << 'Email PayID must contain @ symbol' + end + + # Validate type + if type.nil? || type.strip.empty? + errors << 'Type is required' + elsif type.upcase != 'EMAIL' + errors << 'Type must be EMAIL' + end + + # Validate details + if details.nil? || !details.is_a?(Hash) + errors << 'Details must be a hash' + else + if details[:pay_id_name] && details[:pay_id_name].length > 140 + errors << 'PayID name must be 140 characters or less' + end + + if details[:owner_legal_name] && details[:owner_legal_name].length > 140 + errors << 'Owner legal name must be 140 characters or less' + end + end + + errors +end + +# Usage +virtual_account_id = '46deb476-c1a6-41eb-8eb7-26a695bbe5bc' +pay_id = 'customer@example.com' +type = 'EMAIL' +details = { + pay_id_name: 'Customer Name', + owner_legal_name: 'Customer Full Legal Name' +} + +errors = validate_pay_id_params(pay_id, type, details) + +if errors.empty? + pay_ids = ZaiPayment::Resources::PayId.new + response = pay_ids.create( + virtual_account_id, + pay_id: pay_id, + type: type, + details: details + ) + + puts "✓ PayID registered" if response.success? +else + puts "✗ Validation errors:" + errors.each { |error| puts " - #{error}" } +end +``` + +### Example 6: Register Multiple PayIDs + +Register PayIDs for multiple virtual accounts. + +```ruby +pay_ids = ZaiPayment::Resources::PayId.new + +registrations = [ + { + virtual_account_id: '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + pay_id: 'property1@realestate.com', + pay_id_name: 'Property 1 Trust', + owner_legal_name: 'Property 1 Trust Account' + }, + { + virtual_account_id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + pay_id: 'property2@realestate.com', + pay_id_name: 'Property 2 Trust', + owner_legal_name: 'Property 2 Trust Account' + } +] + +results = [] + +puts "Registering #{registrations.length} PayIDs..." +puts "─" * 60 + +registrations.each_with_index do |reg, index| + begin + response = pay_ids.create( + reg[:virtual_account_id], + pay_id: reg[:pay_id], + type: 'EMAIL', + details: { + pay_id_name: reg[:pay_id_name], + owner_legal_name: reg[:owner_legal_name] + } + ) + + if response.success? + results << { + virtual_account_id: reg[:virtual_account_id], + pay_id: reg[:pay_id], + success: true, + status: response.data['status'] + } + puts "✓ PayID #{index + 1}: #{reg[:pay_id]} - Registered" + end + + rescue ZaiPayment::Errors::NotFoundError => e + results << { virtual_account_id: reg[:virtual_account_id], success: false, error: 'Not found' } + puts "✗ PayID #{index + 1}: #{reg[:pay_id]} - Virtual account not found" + rescue ZaiPayment::Errors::ApiError => e + results << { virtual_account_id: reg[:virtual_account_id], success: false, error: e.message } + puts "✗ PayID #{index + 1}: #{reg[:pay_id]} - #{e.message}" + end + + # Be nice to the API - small delay between requests + sleep(0.5) if index < registrations.length - 1 +end + +puts "─" * 60 +successes = results.count { |r| r[:success] } +failures = results.count { |r| !r[:success] } + +puts "\nResults:" +puts " Successful registrations: #{successes}" +puts " Failed registrations: #{failures}" +``` + +## Show PayID Examples + +### Example 1: Get PayID Details + +Retrieve details of a specific PayID by its ID. + +```ruby +# Get PayID details +pay_ids = ZaiPayment::Resources::PayId.new + +response = pay_ids.show('46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + +if response.success? + pay_id = response.data + + puts "PayID Details:" + puts "─" * 60 + puts "ID: #{pay_id['id']}" + puts "PayID: #{pay_id['pay_id']}" + puts "Type: #{pay_id['type']}" + puts "Status: #{pay_id['status']}" + puts "" + puts "Details:" + puts " PayID Name: #{pay_id['details']['pay_id_name']}" + puts " Owner Legal Name: #{pay_id['details']['owner_legal_name']}" + puts "" + puts "Timestamps:" + puts " Created: #{pay_id['created_at']}" + puts " Updated: #{pay_id['updated_at']}" + puts "─" * 60 +else + puts "Failed to retrieve PayID" + puts "Error: #{response.error}" +end +``` + +### Example 2: Check PayID Status + +Check if a PayID is active before proceeding with operations. + +```ruby +pay_ids = ZaiPayment::Resources::PayId.new + +begin + response = pay_ids.show('46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + + if response.success? + pay_id = response.data + + case pay_id['status'] + when 'active' + puts "✓ PayID is active and ready to receive payments" + puts " PayID: #{pay_id['pay_id']}" + puts " Type: #{pay_id['type']}" + when 'pending_activation' + puts "⏳ PayID is pending activation" + puts " Please wait for activation to complete" + when 'deregistered' + puts "✗ PayID has been deregistered" + puts " Cannot receive payments" + else + puts "⚠ Unknown status: #{pay_id['status']}" + end + end + +rescue ZaiPayment::Errors::NotFoundError => e + puts "PayID not found: #{e.message}" +rescue ZaiPayment::Errors::ApiError => e + puts "API Error: #{e.message}" +end +``` + +### Example 3: Display PayID Information to Users + +Generate payment instructions for customers based on PayID details. + +```ruby +pay_ids = ZaiPayment::Resources::PayId.new + +response = pay_ids.show('46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + +if response.success? + pay_id = response.data + + if pay_id['status'] == 'active' + puts "Payment Information" + puts "=" * 60 + puts "" + puts "You can send payments to:" + puts "" + puts " PayID: #{pay_id['pay_id']}" + puts " Type: #{pay_id['type']}" + puts " Name: #{pay_id['details']['pay_id_name']}" + puts "" + puts "This PayID is registered to:" + puts " #{pay_id['details']['owner_legal_name']}" + puts "" + puts "=" * 60 + else + puts "This PayID is not active yet." + puts "Status: #{pay_id['status']}" + end +end +``` + +### Example 4: Validate PayID Before Payment + +Validate PayID details before initiating a payment. + +```ruby +def validate_pay_id(pay_id_id) + pay_ids = ZaiPayment::Resources::PayId.new + + begin + response = pay_ids.show(pay_id_id) + + if response.success? + pay_id = response.data + + # Validation checks + errors = [] + errors << "PayID is not active" unless pay_id['status'] == 'active' + errors << "Invalid PayID type" unless pay_id['type'] == 'EMAIL' + + if errors.empty? + { + valid: true, + pay_id: pay_id, + payment_info: { + pay_id: pay_id['pay_id'], + type: pay_id['type'], + name: pay_id['details']['pay_id_name'] + } + } + else + { + valid: false, + errors: errors, + pay_id: pay_id + } + end + else + { + valid: false, + errors: ['Failed to retrieve PayID'] + } + end + + rescue ZaiPayment::Errors::NotFoundError + { + valid: false, + errors: ['PayID not found'] + } + rescue ZaiPayment::Errors::ApiError => e + { + valid: false, + errors: ["API Error: #{e.message}"] + } + end +end + +# Usage +result = validate_pay_id('46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + +if result[:valid] + puts "✓ PayID is valid" + puts "Payment Info:" + puts " PayID: #{result[:payment_info][:pay_id]}" + puts " Type: #{result[:payment_info][:type]}" + puts " Name: #{result[:payment_info][:name]}" +else + puts "✗ PayID validation failed:" + result[:errors].each { |error| puts " - #{error}" } +end +``` + +### Example 5: Compare Multiple PayIDs + +Retrieve and compare multiple PayIDs. + +```ruby +pay_ids = ZaiPayment::Resources::PayId.new + +pay_id_ids = [ + '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' +] + +puts "PayID Comparison" +puts "=" * 80 + +pay_id_ids.each do |pay_id_id| + begin + response = pay_ids.show(pay_id_id) + + if response.success? + pay_id = response.data + puts "\n#{pay_id['pay_id']}" + puts " ID: #{pay_id_id[0..7]}..." + puts " Status: #{pay_id['status']}" + puts " Type: #{pay_id['type']}" + puts " Created: #{Date.parse(pay_id['created_at']).strftime('%Y-%m-%d')}" + end + rescue ZaiPayment::Errors::NotFoundError + puts "\n#{pay_id_id[0..7]}..." + puts " Status: Not Found" + end +end + +puts "\n#{'=' * 80}" +``` + +## Update PayID Status Examples + +### Example 1: Deregister a PayID + +Deregister a PayID by setting its status to 'deregistered'. + +```ruby +pay_ids = ZaiPayment::Resources::PayId.new + +begin + response = pay_ids.update_status( + '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + 'deregistered' + ) + + if response.success? + puts "PayID deregistration initiated" + puts "ID: #{response.data['id']}" + puts "Message: #{response.data['message']}" + puts "\nNote: The status update is being processed asynchronously." + end + +rescue ZaiPayment::Errors::NotFoundError => e + puts "PayID not found: #{e.message}" +rescue ZaiPayment::Errors::ValidationError => e + puts "Validation error: #{e.message}" +rescue ZaiPayment::Errors::ApiError => e + puts "API Error: #{e.message}" +end +``` + +### Example 2: Deregister Multiple PayIDs + +Deregister multiple PayIDs in batch. + +```ruby +pay_ids = ZaiPayment::Resources::PayId.new + +pay_id_ids = [ + '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' +] + +results = [] + +pay_id_ids.each_with_index do |pay_id_id, index| + begin + response = pay_ids.update_status(pay_id_id, 'deregistered') + + if response.success? + results << { id: pay_id_id, success: true } + puts "✓ PayID #{index + 1}: Deregistered" + end + + rescue ZaiPayment::Errors::ApiError => e + results << { id: pay_id_id, success: false, error: e.message } + puts "✗ PayID #{index + 1}: #{e.message}" + end +end + +puts "\nDeregistered #{results.count { |r| r[:success] }} out of #{pay_id_ids.length} PayIDs" +``` + +## Common Patterns + +### Pattern 1: Safe PayID Registration with Retry + +```ruby +def register_pay_id_safely(virtual_account_id, pay_id, details, max_retries = 3) + pay_ids = ZaiPayment::Resources::PayId.new + retries = 0 + + begin + response = pay_ids.create( + virtual_account_id, + pay_id: pay_id, + type: 'EMAIL', + details: details + ) + + { + success: true, + pay_id: response.data, + message: 'PayID registered successfully' + } + rescue ZaiPayment::Errors::ValidationError => e + { + success: false, + error: 'validation_error', + message: e.message + } + rescue ZaiPayment::Errors::NotFoundError => e + { + success: false, + error: 'not_found', + message: 'Virtual account not found' + } + rescue ZaiPayment::Errors::TimeoutError => e + retries += 1 + if retries < max_retries + sleep(2**retries) # Exponential backoff + retry + else + { + success: false, + error: 'timeout', + message: 'Request timed out after retries' + } + end + rescue ZaiPayment::Errors::ApiError => e + { + success: false, + error: 'api_error', + message: e.message + } + end +end + +# Usage +result = register_pay_id_safely( + '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + 'customer@example.com', + { + pay_id_name: 'Customer Name', + owner_legal_name: 'Customer Full Name' + } +) + +if result[:success] + puts "✓ Success! PayID: #{result[:pay_id]['pay_id']}" +else + puts "✗ Failed (#{result[:error]}): #{result[:message]}" +end +``` + +### Pattern 2: Store PayID Details in Database + +```ruby +class PayIdManager + attr_reader :pay_ids + + def initialize + @pay_ids = ZaiPayment::Resources::PayId.new + end + + def register_and_store(virtual_account_id, pay_id_email, pay_id_name, owner_legal_name) + response = pay_ids.create( + virtual_account_id, + pay_id: pay_id_email, + type: 'EMAIL', + details: { + pay_id_name: pay_id_name, + owner_legal_name: owner_legal_name + } + ) + + return nil unless response.success? + + pay_id = response.data + + # Store in your database + store_in_database(pay_id) + + pay_id + end + + private + + def store_in_database(pay_id) + # Example: Store in your application database + # PayIdRecord.create!( + # external_id: pay_id['id'], + # virtual_account_id: pay_id['links']['virtual_accounts'].split('/').last, + # pay_id: pay_id['pay_id'], + # pay_id_type: pay_id['type'], + # pay_id_name: pay_id['details']['pay_id_name'], + # owner_legal_name: pay_id['details']['owner_legal_name'], + # status: pay_id['status'] + # ) + puts "Storing PayID #{pay_id['id']} in database..." + end +end + +# Usage +manager = PayIdManager.new +pay_id = manager.register_and_store( + '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + 'customer@example.com', + 'Customer Name', + 'Customer Full Legal Name' +) + +puts "Registered and stored: #{pay_id['pay_id']}" if pay_id +``` + +### Pattern 3: Handle Different Response Scenarios + +```ruby +def register_pay_id_with_handling(virtual_account_id, pay_id, details) + pay_ids = ZaiPayment::Resources::PayId.new + + begin + response = pay_ids.create( + virtual_account_id, + pay_id: pay_id, + type: 'EMAIL', + details: details + ) + + { + success: true, + pay_id: response.data, + message: 'PayID registered successfully' + } + rescue ZaiPayment::Errors::ValidationError => e + { + success: false, + error: 'validation_error', + message: e.message, + user_message: 'Please check the PayID and details provided' + } + rescue ZaiPayment::Errors::NotFoundError => e + { + success: false, + error: 'not_found', + message: 'Virtual account not found', + user_message: 'The virtual account does not exist' + } + rescue ZaiPayment::Errors::BadRequestError => e + { + success: false, + error: 'bad_request', + message: e.message, + user_message: 'Invalid request. Please check your data' + } + rescue ZaiPayment::Errors::ApiError => e + { + success: false, + error: 'api_error', + message: e.message, + user_message: 'An error occurred. Please try again later' + } + end +end + +# Usage +result = register_pay_id_with_handling( + '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + 'test@example.com', + { + pay_id_name: 'Test User', + owner_legal_name: 'Test User Full Name' + } +) + +if result[:success] + puts "Success! PayID: #{result[:pay_id]['pay_id']}" + puts "Status: #{result[:pay_id]['status']}" +else + puts "Error: #{result[:user_message]}" + puts "Details: #{result[:message]}" +end +``` + +## Error Handling + +### Common Errors and Solutions + +```ruby +begin + response = ZaiPayment::Resources::PayId.new.create( + virtual_account_id, + pay_id: 'user@example.com', + type: 'EMAIL', + details: { + pay_id_name: 'User Name', + owner_legal_name: 'User Full Name' + } + ) +rescue ZaiPayment::Errors::ValidationError => e + # Handle validation errors + # - virtual_account_id is blank + # - pay_id is blank or too long + # - type is invalid + # - details is missing or invalid + puts "Validation Error: #{e.message}" +rescue ZaiPayment::Errors::NotFoundError => e + # Handle not found errors + # - virtual account does not exist + puts "Not Found: #{e.message}" +rescue ZaiPayment::Errors::UnauthorizedError => e + # Handle authentication errors + # - Invalid credentials + # - Expired token + puts "Unauthorized: #{e.message}" +rescue ZaiPayment::Errors::ForbiddenError => e + # Handle authorization errors + # - Insufficient permissions + puts "Forbidden: #{e.message}" +rescue ZaiPayment::Errors::BadRequestError => e + # Handle bad request errors + # - Invalid request format + # - PayID already registered + puts "Bad Request: #{e.message}" +rescue ZaiPayment::Errors::TimeoutError => e + # Handle timeout errors + puts "Timeout: #{e.message}" +rescue ZaiPayment::Errors::ApiError => e + # Handle general API errors + puts "API Error: #{e.message}" +end +``` + +## Best Practices + +1. **Always validate input** before making API calls +2. **Handle errors gracefully** with proper error messages +3. **Store PayID details** in your database for reference +4. **Use meaningful names** in pay_id_name and owner_legal_name +5. **Monitor PayID status** after registration (should be `pending_activation`) +6. **Keep PayID secure** - treat like sensitive payment information +7. **Use environment variables** for sensitive configuration +8. **Test in prelive environment** before using in production +9. **Implement proper logging** for audit trails +10. **Verify virtual account exists** before registering PayID + diff --git a/lib/zai_payment.rb b/lib/zai_payment.rb index 9d00c7a..3d1ea67 100644 --- a/lib/zai_payment.rb +++ b/lib/zai_payment.rb @@ -19,6 +19,7 @@ require_relative 'zai_payment/resources/batch_transaction' require_relative 'zai_payment/resources/wallet_account' require_relative 'zai_payment/resources/virtual_account' +require_relative 'zai_payment/resources/pay_id' module ZaiPayment class << self @@ -87,5 +88,10 @@ def wallet_accounts def virtual_accounts @virtual_accounts ||= Resources::VirtualAccount.new end + + # @return [ZaiPayment::Resources::PayId] pay_id resource instance + def pay_ids + @pay_ids ||= Resources::PayId.new + end end end diff --git a/lib/zai_payment/resources/pay_id.rb b/lib/zai_payment/resources/pay_id.rb new file mode 100644 index 0000000..b6b0f70 --- /dev/null +++ b/lib/zai_payment/resources/pay_id.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: true + +module ZaiPayment + module Resources + # PayID resource for managing Zai PayID registrations + # + # @see https://developer.hellozai.com/reference/registerpayid + class PayId + attr_reader :client + + # Map of attribute keys to API field names for create + CREATE_FIELD_MAPPING = { + pay_id: :pay_id, + type: :type, + details: :details + }.freeze + + # Valid PayID types + VALID_TYPES = %w[EMAIL].freeze + + # Valid PayID statuses for update + VALID_STATUSES = %w[deregistered].freeze + + def initialize(client: nil) + @client = client || Client.new(base_endpoint: :va_base) + end + + # Register a PayID for a given Virtual Account + # + # @param virtual_account_id [String] the virtual account ID + # @param attributes [Hash] PayID attributes + # @option attributes [String] :pay_id (Required) The PayID being registered (max 256 chars) + # @option attributes [String] :type (Required) The type of PayID ('EMAIL') + # @option attributes [Hash] :details (Required) Additional details + # @option details [String] :pay_id_name Name to identify the entity (1-140 chars) + # @option details [String] :owner_legal_name Full legal account name (1-140 chars) + # @return [Response] the API response containing PayID details + # + # @example Register an EMAIL PayID + # pay_ids = ZaiPayment::Resources::PayId.new + # response = pay_ids.create( + # '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + # pay_id: 'jsmith@mydomain.com', + # type: 'EMAIL', + # details: { + # pay_id_name: 'J Smith', + # owner_legal_name: 'Mr John Smith' + # } + # ) + # response.data # => {"id" => "46deb476-c1a6-41eb-8eb7-26a695bbe5bc", ...} + # + # @see https://developer.hellozai.com/reference/registerpayid + def create(virtual_account_id, **attributes) + validate_id!(virtual_account_id, 'virtual_account_id') + validate_create_attributes!(attributes) + + body = build_create_body(attributes) + client.post("/virtual_accounts/#{virtual_account_id}/pay_ids", body: body) + end + + # Show a specific PayID + # + # @param pay_id_id [String] the PayID ID + # @return [Response] the API response containing PayID details + # + # @example Get PayID details + # pay_ids = ZaiPayment::Resources::PayId.new + # response = pay_ids.show('46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + # response.data # => {"id" => "46deb476-c1a6-41eb-8eb7-26a695bbe5bc", ...} + # + # @see https://developer.hellozai.com/reference/retrieveapayid + def show(pay_id_id) + validate_id!(pay_id_id, 'pay_id_id') + client.get("/pay_ids/#{pay_id_id}") + end + + # Update Status for a PayID + # + # Update the status of a PayID. Currently, this endpoint only supports deregistering + # PayIDs by setting the status to 'deregistered'. This is an asynchronous operation + # that returns a 202 Accepted response. + # + # @param pay_id_id [String] the PayID ID + # @param status [String] the new status (must be 'deregistered') + # @return [Response] the API response containing the operation status + # + # @example Deregister a PayID + # pay_ids = ZaiPayment::Resources::PayId.new + # response = pay_ids.update_status( + # '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + # 'deregistered' + # ) + # response.data # => {"id" => "46deb476-c1a6-41eb-8eb7-26a695bbe5bc", "message" => "...", ...} + # + # @see https://developer.hellozai.com/reference/updatepayidstatus + def update_status(pay_id_id, status) + validate_id!(pay_id_id, 'pay_id_id') + validate_status!(status) + + body = { status: status } + client.patch("/pay_ids/#{pay_id_id}/status", 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_pay_id!(attributes[:pay_id]) + validate_type!(attributes[:type]) + validate_details!(attributes[:details]) + end + + def validate_pay_id!(pay_id) + if pay_id.nil? || pay_id.to_s.strip.empty? + raise Errors::ValidationError, 'pay_id is required and cannot be blank' + end + + return unless pay_id.to_s.length > 256 + + raise Errors::ValidationError, 'pay_id must be 256 characters or less' + end + + def validate_type!(type) + raise Errors::ValidationError, 'type is required and cannot be blank' if type.nil? || type.to_s.strip.empty? + + return if VALID_TYPES.include?(type.to_s.upcase) + + raise Errors::ValidationError, + "type must be one of: #{VALID_TYPES.join(', ')}, got '#{type}'" + end + + def validate_details!(details) + raise Errors::ValidationError, 'details is required and must be a hash' if details.nil? || !details.is_a?(Hash) + + validate_pay_id_name!(details[:pay_id_name]) + validate_owner_legal_name!(details[:owner_legal_name]) + end + + def validate_pay_id_name!(pay_id_name) + return unless pay_id_name + + raise Errors::ValidationError, 'pay_id_name cannot be empty when provided' if pay_id_name.to_s.empty? + + return unless pay_id_name.to_s.length > 140 + + raise Errors::ValidationError, 'pay_id_name must be between 1 and 140 characters' + end + + def validate_owner_legal_name!(owner_legal_name) + return unless owner_legal_name + + raise Errors::ValidationError, 'owner_legal_name cannot be empty when provided' if owner_legal_name.to_s.empty? + + return unless owner_legal_name.to_s.length > 140 + + raise Errors::ValidationError, 'owner_legal_name must be between 1 and 140 characters' + end + + def validate_status!(status) + raise Errors::ValidationError, 'status cannot be blank' if status.nil? || status.to_s.strip.empty? + + return if VALID_STATUSES.include?(status.to_s) + + raise Errors::ValidationError, + "status must be 'deregistered', got '#{status}'" + end + + # rubocop:disable Metrics/AbcSize + def build_create_body(attributes) + body = {} + + # Add pay_id + body[:pay_id] = attributes[:pay_id] if attributes[:pay_id] + + # Add type (convert to uppercase to match API expectations) + body[:type] = attributes[:type].to_s.upcase if attributes[:type] + + # Add details + if attributes[:details].is_a?(Hash) + body[:details] = {} + details = attributes[:details] + + body[:details][:pay_id_name] = details[:pay_id_name] if details[:pay_id_name] + body[:details][:owner_legal_name] = details[:owner_legal_name] if details[:owner_legal_name] + end + + body + end + # rubocop:enable Metrics/AbcSize + end + end +end diff --git a/lib/zai_payment/response.rb b/lib/zai_payment/response.rb index d0a40fb..a2fd19f 100644 --- a/lib/zai_payment/response.rb +++ b/lib/zai_payment/response.rb @@ -8,7 +8,7 @@ class Response RESPONSE_DATA_KEYS = %w[ webhooks users items fees transactions batch_transactions batches bpay_accounts bank_accounts card_accounts - wallet_accounts virtual_accounts routing_number disbursements + wallet_accounts virtual_accounts routing_number disbursements pay_ids ].freeze def initialize(faraday_response) diff --git a/spec/zai_payment/resources/pay_id_spec.rb b/spec/zai_payment/resources/pay_id_spec.rb new file mode 100644 index 0000000..8e9271e --- /dev/null +++ b/spec/zai_payment/resources/pay_id_spec.rb @@ -0,0 +1,649 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ZaiPayment::Resources::PayId do + let(:stubs) { Faraday::Adapter::Test::Stubs.new } + let(:pay_id_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: :va_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 '#create' do + let(:pay_id_data) do + { + 'pay_ids' => { + 'id' => '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + 'pay_id' => 'jsmith@mydomain.com', + 'type' => 'EMAIL', + 'status' => 'pending_activation', + 'created_at' => '2020-04-27T20:28:22.378Z', + 'updated_at' => '2020-04-27T20:28:22.378Z', + 'details' => { + 'pay_id_name' => 'J Smith', + 'owner_legal_name' => 'Mr John Smith' + }, + 'links' => { + 'self' => '/pay_ids/46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + 'virtual_accounts' => '/virtual_accounts/46deb476-c1a6-41eb-8eb7-26a695bbe5bc' + } + } + } + end + + let(:valid_params) do + { + pay_id: 'jsmith@mydomain.com', + type: 'EMAIL', + details: { + pay_id_name: 'J Smith', + owner_legal_name: 'Mr John Smith' + } + } + end + + context 'when successful' do + before do + stubs.post('/virtual_accounts/46deb476-c1a6-41eb-8eb7-26a695bbe5bc/pay_ids') do + [202, { 'Content-Type' => 'application/json' }, pay_id_data] + end + end + + it 'returns the correct response type and creates PayID' do + response = pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **valid_params) + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + expect(response.data['id']).to eq('46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + end + + it 'includes pay_id and type in response' do + response = pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **valid_params) + + expect(response.data['pay_id']).to eq('jsmith@mydomain.com') + expect(response.data['type']).to eq('EMAIL') + end + + it 'includes details in response' do + response = pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **valid_params) + + expect(response.data['details']['pay_id_name']).to eq('J Smith') + expect(response.data['details']['owner_legal_name']).to eq('Mr John Smith') + end + + it 'includes status and timestamps' do + response = pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **valid_params) + + expect(response.data['status']).to eq('pending_activation') + expect(response.data['created_at']).to eq('2020-04-27T20:28:22.378Z') + expect(response.data['updated_at']).to eq('2020-04-27T20:28:22.378Z') + end + + it 'includes links in response' do + response = pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **valid_params) + + expect(response.data['links']).to be_a(Hash) + expect(response.data['links']['self']).to eq('/pay_ids/46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + expect(response.data['links']['virtual_accounts']) + .to eq('/virtual_accounts/46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + end + end + + context 'with lowercase type' do + before do + stubs.post('/virtual_accounts/46deb476-c1a6-41eb-8eb7-26a695bbe5bc/pay_ids') do + [202, { 'Content-Type' => 'application/json' }, pay_id_data] + end + end + + it 'converts type to uppercase' do + params = valid_params.merge(type: 'email') + response = pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **params) + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + end + + context 'with minimal details' do + before do + stubs.post('/virtual_accounts/46deb476-c1a6-41eb-8eb7-26a695bbe5bc/pay_ids') do + [202, { 'Content-Type' => 'application/json' }, pay_id_data] + end + end + + it 'accepts empty details hash' do + params = valid_params.merge(details: {}) + response = pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **params) + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + end + + context 'with maximum length pay_id' do + before do + stubs.post('/virtual_accounts/46deb476-c1a6-41eb-8eb7-26a695bbe5bc/pay_ids') do + [202, { 'Content-Type' => 'application/json' }, pay_id_data] + end + end + + it 'accepts pay_id with 256 characters' do + max_length_pay_id = 'a' * 256 + params = valid_params.merge(pay_id: max_length_pay_id) + response = pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **params) + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + end + + context 'with maximum length detail fields' do + before do + stubs.post('/virtual_accounts/46deb476-c1a6-41eb-8eb7-26a695bbe5bc/pay_ids') do + [202, { 'Content-Type' => 'application/json' }, pay_id_data] + end + end + + it 'accepts detail fields with 140 characters' do + long_details = { + pay_id_name: 'A' * 140, + owner_legal_name: 'B' * 140 + } + params = valid_params.merge(details: long_details) + response = pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **params) + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + end + + context 'when virtual_account_id is blank' do + it 'raises a ValidationError for empty string' do + expect { pay_id_resource.create('', **valid_params) } + .to raise_error(ZaiPayment::Errors::ValidationError, /virtual_account_id/) + end + + it 'raises a ValidationError for nil' do + expect { pay_id_resource.create(nil, **valid_params) } + .to raise_error(ZaiPayment::Errors::ValidationError, /virtual_account_id/) + end + + it 'raises a ValidationError for whitespace only' do + expect { pay_id_resource.create(' ', **valid_params) } + .to raise_error(ZaiPayment::Errors::ValidationError, /virtual_account_id/) + end + end + + context 'when pay_id validation fails' do + it 'raises error for blank pay_id' do + params = valid_params.merge(pay_id: '') + expect do + pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **params) + end.to raise_error(ZaiPayment::Errors::ValidationError, /pay_id is required/) + end + + it 'raises error for nil pay_id' do + params = valid_params.merge(pay_id: nil) + expect do + pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **params) + end.to raise_error(ZaiPayment::Errors::ValidationError, /pay_id is required/) + end + + it 'raises error for whitespace only pay_id' do + params = valid_params.merge(pay_id: ' ') + expect do + pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **params) + end.to raise_error(ZaiPayment::Errors::ValidationError, /pay_id is required/) + end + + it 'raises error for pay_id longer than 256 characters' do + long_pay_id = 'a' * 257 + params = valid_params.merge(pay_id: long_pay_id) + expect do + pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **params) + end.to raise_error(ZaiPayment::Errors::ValidationError, /pay_id must be 256 characters or less/) + end + end + + context 'when type validation fails' do + it 'raises error for blank type' do + params = valid_params.merge(type: '') + expect do + pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **params) + end.to raise_error(ZaiPayment::Errors::ValidationError, /type is required/) + end + + it 'raises error for nil type' do + params = valid_params.merge(type: nil) + expect do + pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **params) + end.to raise_error(ZaiPayment::Errors::ValidationError, /type is required/) + end + + it 'raises error for whitespace only type' do + params = valid_params.merge(type: ' ') + expect do + pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **params) + end.to raise_error(ZaiPayment::Errors::ValidationError, /type is required/) + end + + it 'raises error for invalid type' do + params = valid_params.merge(type: 'PHONE') + expect do + pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **params) + end.to raise_error(ZaiPayment::Errors::ValidationError, /type must be one of: EMAIL/) + end + + it 'includes the invalid type in error message' do + params = valid_params.merge(type: 'INVALID') + expect do + pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **params) + end.to raise_error(ZaiPayment::Errors::ValidationError, /got 'INVALID'/) + end + end + + context 'when details validation fails' do + it 'raises error for nil details' do + params = valid_params.merge(details: nil) + expect do + pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **params) + end.to raise_error(ZaiPayment::Errors::ValidationError, /details is required and must be a hash/) + end + + it 'raises error for non-hash details' do + params = valid_params.merge(details: 'not a hash') + expect do + pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **params) + end.to raise_error(ZaiPayment::Errors::ValidationError, /details is required and must be a hash/) + end + + it 'raises error for pay_id_name longer than 140 characters' do + long_details = { + pay_id_name: 'A' * 141, + owner_legal_name: 'John Smith' + } + params = valid_params.merge(details: long_details) + + expect { pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **params) } + .to raise_error(ZaiPayment::Errors::ValidationError, /pay_id_name must be between 1 and 140 characters/) + end + + it 'raises error for owner_legal_name longer than 140 characters' do + long_details = { + pay_id_name: 'J Smith', + owner_legal_name: 'B' * 141 + } + params = valid_params.merge(details: long_details) + + expect { pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **params) } + .to raise_error(ZaiPayment::Errors::ValidationError, /owner_legal_name must be between 1 and 140 characters/) + end + end + + context 'when virtual account does not exist' do + before do + stubs.post('/virtual_accounts/invalid_id/pay_ids') do + [404, { 'Content-Type' => 'application/json' }, { 'errors' => 'Not found' }] + end + end + + it 'raises a NotFoundError' do + expect { pay_id_resource.create('invalid_id', **valid_params) } + .to raise_error(ZaiPayment::Errors::NotFoundError) + end + end + + context 'when API returns bad request' do + before do + stubs.post('/virtual_accounts/46deb476-c1a6-41eb-8eb7-26a695bbe5bc/pay_ids') do + [400, { 'Content-Type' => 'application/json' }, { 'errors' => 'Bad request' }] + end + end + + it 'raises a BadRequestError' do + expect { pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **valid_params) } + .to raise_error(ZaiPayment::Errors::BadRequestError) + end + end + + context 'when API returns unauthorized' do + before do + stubs.post('/virtual_accounts/46deb476-c1a6-41eb-8eb7-26a695bbe5bc/pay_ids') do + [401, { 'Content-Type' => 'application/json' }, { 'errors' => 'Unauthorized' }] + end + end + + it 'raises an UnauthorizedError' do + expect { pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **valid_params) } + .to raise_error(ZaiPayment::Errors::UnauthorizedError) + end + end + + context 'when API returns forbidden' do + before do + stubs.post('/virtual_accounts/46deb476-c1a6-41eb-8eb7-26a695bbe5bc/pay_ids') do + [403, { 'Content-Type' => 'application/json' }, { 'errors' => 'Forbidden' }] + end + end + + it 'raises a ForbiddenError' do + expect { pay_id_resource.create('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', **valid_params) } + .to raise_error(ZaiPayment::Errors::ForbiddenError) + end + end + end + + describe '#show' do + let(:pay_id_data) do + { + 'pay_ids' => { + 'id' => '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + 'pay_id' => 'jsmith@mydomain.com', + 'type' => 'EMAIL', + 'status' => 'active', + 'created_at' => '2020-04-27T20:28:22.378Z', + 'updated_at' => '2020-04-27T20:28:22.378Z', + 'details' => { + 'pay_id_name' => 'J Smith', + 'owner_legal_name' => 'Mr John Smith' + }, + 'links' => { + 'self' => '/pay_ids/46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + 'virtual_accounts' => '/virtual_accounts/46deb476-c1a6-41eb-8eb7-26a695bbe5bc' + } + } + } + end + + context 'when PayID exists' do + before do + stubs.get('/pay_ids/46deb476-c1a6-41eb-8eb7-26a695bbe5bc') do + [200, { 'Content-Type' => 'application/json' }, pay_id_data] + end + end + + it 'returns the correct response type and PayID details' do + response = pay_id_resource.show('46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + expect(response.data['id']).to eq('46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + end + + it 'returns PayID with correct details' do + response = pay_id_resource.show('46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + + expect(response.data['pay_id']).to eq('jsmith@mydomain.com') + expect(response.data['type']).to eq('EMAIL') + expect(response.data['status']).to eq('active') + end + + it 'includes details hash' do + response = pay_id_resource.show('46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + + expect(response.data['details']).to be_a(Hash) + expect(response.data['details']['pay_id_name']).to eq('J Smith') + expect(response.data['details']['owner_legal_name']).to eq('Mr John Smith') + end + + it 'includes timestamps' do + response = pay_id_resource.show('46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + + expect(response.data['created_at']).to eq('2020-04-27T20:28:22.378Z') + expect(response.data['updated_at']).to eq('2020-04-27T20:28:22.378Z') + end + + it 'includes links' do + response = pay_id_resource.show('46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + + expect(response.data['links']).to be_a(Hash) + expect(response.data['links']['self']).to eq('/pay_ids/46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + expect(response.data['links']['virtual_accounts']) + .to eq('/virtual_accounts/46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + end + end + + context 'when pay_id_id is blank' do + it 'raises a ValidationError for empty string' do + expect { pay_id_resource.show('') } + .to raise_error(ZaiPayment::Errors::ValidationError, /pay_id_id/) + end + + it 'raises a ValidationError for nil' do + expect { pay_id_resource.show(nil) } + .to raise_error(ZaiPayment::Errors::ValidationError, /pay_id_id/) + end + + it 'raises a ValidationError for whitespace only' do + expect { pay_id_resource.show(' ') } + .to raise_error(ZaiPayment::Errors::ValidationError, /pay_id_id/) + end + end + + context 'when PayID does not exist' do + before do + stubs.get('/pay_ids/invalid_id') do + [404, { 'Content-Type' => 'application/json' }, { 'errors' => 'Not found' }] + end + end + + it 'raises a NotFoundError' do + expect { pay_id_resource.show('invalid_id') } + .to raise_error(ZaiPayment::Errors::NotFoundError) + end + end + + context 'when API returns bad request' do + before do + stubs.get('/pay_ids/46deb476-c1a6-41eb-8eb7-26a695bbe5bc') do + [400, { 'Content-Type' => 'application/json' }, { 'errors' => 'Bad request' }] + end + end + + it 'raises a BadRequestError' do + expect { pay_id_resource.show('46deb476-c1a6-41eb-8eb7-26a695bbe5bc') } + .to raise_error(ZaiPayment::Errors::BadRequestError) + end + end + + context 'when API returns unauthorized' do + before do + stubs.get('/pay_ids/46deb476-c1a6-41eb-8eb7-26a695bbe5bc') do + [401, { 'Content-Type' => 'application/json' }, { 'errors' => 'Unauthorized' }] + end + end + + it 'raises an UnauthorizedError' do + expect { pay_id_resource.show('46deb476-c1a6-41eb-8eb7-26a695bbe5bc') } + .to raise_error(ZaiPayment::Errors::UnauthorizedError) + end + end + + context 'when API returns forbidden' do + before do + stubs.get('/pay_ids/46deb476-c1a6-41eb-8eb7-26a695bbe5bc') do + [403, { 'Content-Type' => 'application/json' }, { 'errors' => 'Forbidden' }] + end + end + + it 'raises a ForbiddenError' do + expect { pay_id_resource.show('46deb476-c1a6-41eb-8eb7-26a695bbe5bc') } + .to raise_error(ZaiPayment::Errors::ForbiddenError) + end + end + end + + describe '#update_status' do + let(:status_update_response_data) do + { + 'id' => '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + 'message' => 'PayID deregistration has been accepted for processing', + 'links' => { + 'self' => '/pay_ids/46deb476-c1a6-41eb-8eb7-26a695bbe5bc' + } + } + end + + context 'when successful' do + before do + stubs.patch('/pay_ids/46deb476-c1a6-41eb-8eb7-26a695bbe5bc/status') do + [202, { 'Content-Type' => 'application/json' }, status_update_response_data] + end + end + + it 'returns the correct response type' do + response = pay_id_resource.update_status( + '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + 'deregistered' + ) + + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + + it 'returns the status update acceptance message' do + response = pay_id_resource.update_status( + '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + 'deregistered' + ) + + expect(response.data['id']).to eq('46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + expect(response.data['message']).to eq('PayID deregistration has been accepted for processing') + end + + it 'includes links in response' do + response = pay_id_resource.update_status( + '46deb476-c1a6-41eb-8eb7-26a695bbe5bc', + 'deregistered' + ) + + expect(response.data['links']).to be_a(Hash) + expect(response.data['links']['self']).to eq('/pay_ids/46deb476-c1a6-41eb-8eb7-26a695bbe5bc') + end + end + + context 'when pay_id_id is blank' do + it 'raises a ValidationError for empty string' do + expect { pay_id_resource.update_status('', 'deregistered') } + .to raise_error(ZaiPayment::Errors::ValidationError, /pay_id_id/) + end + + it 'raises a ValidationError for nil' do + expect { pay_id_resource.update_status(nil, 'deregistered') } + .to raise_error(ZaiPayment::Errors::ValidationError, /pay_id_id/) + end + + it 'raises a ValidationError for whitespace only' do + expect { pay_id_resource.update_status(' ', 'deregistered') } + .to raise_error(ZaiPayment::Errors::ValidationError, /pay_id_id/) + end + end + + context 'when status validation fails' do + it 'raises error for blank status' do + expect do + pay_id_resource.update_status('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', '') + end.to raise_error(ZaiPayment::Errors::ValidationError, /status cannot be blank/) + end + + it 'raises error for nil status' do + expect do + pay_id_resource.update_status('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', nil) + end.to raise_error(ZaiPayment::Errors::ValidationError, /status cannot be blank/) + end + + it 'raises error for whitespace only status' do + expect do + pay_id_resource.update_status('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', ' ') + end.to raise_error(ZaiPayment::Errors::ValidationError, /status cannot be blank/) + end + + it 'raises error for invalid status value' do + expect do + pay_id_resource.update_status('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', 'active') + end.to raise_error(ZaiPayment::Errors::ValidationError, /status must be 'deregistered'/) + end + + it 'includes the invalid status in error message' do + expect do + pay_id_resource.update_status('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', 'pending') + end.to raise_error(ZaiPayment::Errors::ValidationError, /got 'pending'/) + end + end + + context 'when PayID does not exist' do + before do + stubs.patch('/pay_ids/invalid_id/status') do + [404, { 'Content-Type' => 'application/json' }, { 'errors' => 'Not found' }] + end + end + + it 'raises a NotFoundError' do + expect { pay_id_resource.update_status('invalid_id', 'deregistered') } + .to raise_error(ZaiPayment::Errors::NotFoundError) + end + end + + context 'when API returns bad request' do + before do + stubs.patch('/pay_ids/46deb476-c1a6-41eb-8eb7-26a695bbe5bc/status') do + [400, { 'Content-Type' => 'application/json' }, { 'errors' => 'Bad request' }] + end + end + + it 'raises a BadRequestError' do + expect { pay_id_resource.update_status('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', 'deregistered') } + .to raise_error(ZaiPayment::Errors::BadRequestError) + end + end + + context 'when API returns unauthorized' do + before do + stubs.patch('/pay_ids/46deb476-c1a6-41eb-8eb7-26a695bbe5bc/status') do + [401, { 'Content-Type' => 'application/json' }, { 'errors' => 'Unauthorized' }] + end + end + + it 'raises an UnauthorizedError' do + expect { pay_id_resource.update_status('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', 'deregistered') } + .to raise_error(ZaiPayment::Errors::UnauthorizedError) + end + end + + context 'when API returns forbidden' do + before do + stubs.patch('/pay_ids/46deb476-c1a6-41eb-8eb7-26a695bbe5bc/status') do + [403, { 'Content-Type' => 'application/json' }, { 'errors' => 'Forbidden' }] + end + end + + it 'raises a ForbiddenError' do + expect { pay_id_resource.update_status('46deb476-c1a6-41eb-8eb7-26a695bbe5bc', 'deregistered') } + .to raise_error(ZaiPayment::Errors::ForbiddenError) + end + end + end +end