diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/pull_request_template.md similarity index 100% rename from .github/PULL_REQUEST_TEMPLATE.md rename to .github/pull_request_template.md diff --git a/Gemfile.lock b/Gemfile.lock index 4614245..39196f2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - zai_payment (1.3.2) + zai_payment (2.0.0) base64 (~> 0.3.0) faraday (~> 2.0) openssl (~> 3.3) diff --git a/CHANGELOG.md b/changelog.md similarity index 72% rename from CHANGELOG.md rename to changelog.md index 97cc593..b09a453 100644 --- a/CHANGELOG.md +++ b/changelog.md @@ -1,5 +1,32 @@ ## [Released] +## [2.0.0] - 2025-10-24 +### Added +- **Items Management API**: Full CRUD operations for managing Zai items (transactions/payments) ๐Ÿ›’ + - `ZaiPayment.items.list(limit:, offset:)` - List all items with pagination + - `ZaiPayment.items.show(item_id)` - Get item details by ID + - `ZaiPayment.items.create(**attributes)` - Create new item/transaction + - `ZaiPayment.items.update(item_id, **attributes)` - Update item information + - `ZaiPayment.items.delete(item_id)` - Delete an item + - `ZaiPayment.items.show_seller(item_id)` - Get seller details for an item + - `ZaiPayment.items.show_buyer(item_id)` - Get buyer details for an item + - `ZaiPayment.items.show_fees(item_id)` - Get fees associated with an item + - `ZaiPayment.items.show_wire_details(item_id)` - Get wire transfer details for an item + - `ZaiPayment.items.list_transactions(item_id, limit:, offset:)` - List transactions for an item + - `ZaiPayment.items.list_batch_transactions(item_id, limit:, offset:)` - List batch transactions for an item + - `ZaiPayment.items.show_status(item_id)` - Get current status of an item +- Comprehensive validation for item attributes (name, amount, payment_type, buyer_id, seller_id) +- Support for optional item fields (description, currency, fee_ids, custom_descriptor, deposit_reference, etc.) +- Full RSpec test suite for Items resource with 100% coverage +- Comprehensive examples documentation in `examples/items.md` + +### Documentation +- Added detailed Items API examples with complete workflow demonstrations +- Payment types documentation (1-7: Direct Debit, Credit Card, Bank Transfer, Wallet, BPay, PayPal, Other) +- Error handling examples for Items operations + +**Full Changelog**: https://github.com/Sentia/zai-payment/compare/v1.3.2...v2.0.0 + ## [1.3.2] - 2025-10-23 ### Added - YARD documentation generation support with `.yardopts` configuration @@ -32,7 +59,7 @@ - Comprehensive validation for all user types - Email format validation - Country code validation (ISO 3166-1 alpha-3) - - Date of birth format validation (YYYYMMDD) + - Date of birth format validation (DD/MM/YYYY) - User type validation (payin/payout) - Progressive profile building support @@ -44,7 +71,7 @@ - **Main Module**: Added `users` accessor for convenient access to User resource ### Documentation -- **NEW**: [User Management Guide](docs/USERS.md) - Comprehensive guide covering: +- **NEW**: [User Management Guide](docs/users.md) - Comprehensive guide covering: - Overview of payin vs payout users - Required fields for each user type - Complete API reference with examples @@ -64,10 +91,10 @@ - User profile validation helper - RSpec integration tests - Common patterns with retry logic -- **NEW**: [User Quick Reference](docs/USER_QUICK_REFERENCE.md) - Quick lookup for common operations +- **NEW**: [User Quick Reference](docs/user_quick_reference.md) - Quick lookup for common operations - **NEW**: [User Demo Script](examples/user_demo.rb) - Interactive demo of all user operations -- **NEW**: [Implementation Summary](IMPLEMENTATION.md) - Detailed summary of the implementation -- **Updated**: README.md - Added Users section with quick examples and updated roadmap +- **NEW**: [Implementation Summary](implementation.md) - Detailed summary of the implementation +- **Updated**: readme.md - Added Users section with quick examples and updated roadmap ### Testing - 40+ new test cases for User resource @@ -97,24 +124,24 @@ - Support for multiple signatures (key rotation scenarios) ### Documentation -- **NEW**: [Authentication Guide](docs/AUTHENTICATION.md) - Comprehensive guide covering: +- **NEW**: [Authentication Guide](docs/authentication.md) - Comprehensive guide covering: - Short way: `ZaiPayment.token` (one-liner approach) - Long way: `TokenProvider.new(config:).bearer_token` (advanced control) - Token lifecycle and automatic management - Multiple configurations, testing, error handling - Best practices and troubleshooting -- **NEW**: [Webhook Security Quick Start](docs/WEBHOOK_SECURITY_QUICKSTART.md) - 5-minute setup guide -- **NEW**: [Webhook Signature Implementation](docs/WEBHOOK_SIGNATURE.md) - Technical details -- **NEW**: [Documentation Index](docs/README.md) - Central navigation for all docs +- **NEW**: [Webhook Security Quick Start](docs/webhook_security_quickstart.md) - 5-minute setup guide +- **NEW**: [Webhook Signature Implementation](docs/webhook_signature.md) - Technical details +- **NEW**: [Documentation Index](docs/readme.md) - Central navigation for all docs - **Enhanced**: [Webhook Examples](examples/webhooks.md) - Added 400+ lines of examples: - Complete Rails controller implementation - Sinatra example - Rack middleware example - Background job processing pattern - Idempotency pattern -- **Enhanced**: [Webhook Technical Guide](docs/WEBHOOKS.md) - Added 170+ lines on security +- **Enhanced**: [Webhook Technical Guide](docs/webhooks.md) - Added 170+ lines on security - **Reorganized**: All documentation moved to `docs/` folder for better organization -- **Updated**: README.md - Now concise with clear links to detailed documentation +- **Updated**: readme.md - Now concise with clear links to detailed documentation ### Testing - 56 new test cases for webhook signature verification diff --git a/CODE_OF_CONDUCT.md b/code_of_conduct.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to code_of_conduct.md diff --git a/CONTRIBUTING.md b/contributing.md similarity index 98% rename from CONTRIBUTING.md rename to contributing.md index 5eeb47c..7f20c79 100644 --- a/CONTRIBUTING.md +++ b/contributing.md @@ -23,7 +23,7 @@ First off, thank you for considering contributing to Zai Payment! ๐ŸŽ‰ It's peop ## Code of Conduct -This project and everyone participating in it is governed by our [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [contact@sentia.com.au](mailto:contact@sentia.com.au). +This project and everyone participating in it is governed by our [Code of Conduct](code_of_conduct.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [contact@sentia.com.au](mailto:contact@sentia.com.au). --- @@ -86,7 +86,7 @@ Please follow these steps to have your contribution considered by the maintainer **Pull Request Guidelines:** - Keep changes focused - one feature/fix per PR - Link any relevant issues in the PR description -- Update CHANGELOG.md if appropriate +- Update changelog.md if appropriate - Maintain backward compatibility when possible - Include tests for new functionality - Follow the existing code style @@ -268,9 +268,9 @@ bundle exec rubocop -a - **Public APIs must be documented** using YARD syntax - **Include examples** in documentation when helpful -- **Update README.md** when adding new features +- **Update readme.md** when adding new features - **Update relevant docs/** files for architectural changes -- **Keep CHANGELOG.md** updated with notable changes +- **Keep changelog.md** updated with notable changes **YARD Documentation Example:** ```ruby diff --git a/docs/ARCHITECTURE.md b/docs/architecture.md similarity index 100% rename from docs/ARCHITECTURE.md rename to docs/architecture.md diff --git a/docs/AUTHENTICATION.md b/docs/authentication.md similarity index 99% rename from docs/AUTHENTICATION.md rename to docs/authentication.md index 94fc433..608c4a1 100644 --- a/docs/AUTHENTICATION.md +++ b/docs/authentication.md @@ -641,7 +641,7 @@ Gets or refreshes the bearer token. ## Next Steps - โœ… Authentication configured and working -- ๐Ÿ“– Read [Webhook Guide](WEBHOOKS.md) to start using webhooks +- ๐Ÿ“– Read [Webhook Guide](webhooks.md) to start using webhooks - ๐Ÿ’ก Check [Examples](../examples/webhooks.md) for complete code samples -- ๐Ÿ”’ Set up [Webhook Security](WEBHOOK_SECURITY_QUICKSTART.md) +- ๐Ÿ”’ Set up [Webhook Security](webhook_security_quickstart.md) diff --git a/docs/items.md b/docs/items.md new file mode 100644 index 0000000..c838b4f --- /dev/null +++ b/docs/items.md @@ -0,0 +1,666 @@ +# Item Management + +The Item resource provides methods for managing Zai items, which represent transactions or payments between buyers and sellers on your platform. + +## Overview + +Items are the core transactional objects in Zai that represent a payment agreement between a buyer (payin user) and a seller (payout user). Each item tracks: +- Transaction amount +- Payment type +- Buyer and seller information +- Fees and charges +- Transaction state and history +- Wire transfer details (if applicable) + +## References + +- [Zai Items API](https://developer.hellozai.com/reference/listitems) +- [Create Item](https://developer.hellozai.com/reference/createitem) +- [Update Item](https://developer.hellozai.com/reference/updateitem) + +## Usage + +### Initialize the Item Resource + +```ruby +# Using the singleton instance (recommended) +items = ZaiPayment.items + +# Or create a new instance +items = ZaiPayment::Resources::Item.new +``` + +## Methods + +### Create Item + +Create a new item (transaction/payment) between a buyer and a seller. + +#### Required Fields + +- `name` - Name/description of the item +- `amount` - Amount in cents (e.g., 10000 = $100.00) +- `payment_type` - Payment type (1-7, default: 2 for credit card) +- `buyer_id` - Buyer user ID +- `seller_id` - Seller user ID + +#### Optional Fields + +- `id` - Custom unique ID for the item +- `fee_ids` - Array of fee IDs to apply +- `description` - Detailed description +- `currency` - Currency code (e.g., 'AUD', 'USD') +- `custom_descriptor` - Custom text for bank statements +- `buyer_url` - URL for buyer to access transaction +- `seller_url` - URL for seller to access transaction +- `tax_invoice` - Whether to generate a tax invoice (boolean) + +#### Example + +```ruby +response = ZaiPayment.items.create( + name: "Product Purchase", + amount: 10000, # $100.00 in cents + payment_type: 2, # Credit card + buyer_id: "buyer-123", + seller_id: "seller-456", + description: "Purchase of premium product XYZ", + currency: "AUD", + buyer_url: "https://buyer.example.com", + seller_url: "https://seller.example.com", + tax_invoice: true +) + +if response.success? + item = response.data + puts "Item created: #{item['id']}" + puts "Amount: #{item['amount']}" + puts "State: #{item['state']}" +end +``` + +#### Create Item with Custom ID + +```ruby +response = ZaiPayment.items.create( + id: "my-custom-item-#{Time.now.to_i}", + name: "Custom ID Product", + amount: 15000, + payment_type: 2, + buyer_id: "buyer-123", + seller_id: "seller-456" +) +``` + +### List Items + +Retrieve a list of all items with pagination and optional filtering. + +```ruby +# List items with default pagination (10 items) +response = ZaiPayment.items.list + +if response.success? + items_list = response.data + items_list.each do |item| + puts "Item: #{item['name']} - $#{item['amount'] / 100.0}" + end + + # Access pagination metadata + meta = response.meta + puts "Total items: #{meta['total']}" +end +``` + +#### List with Pagination + +```ruby +# Get 20 items starting from offset 40 +response = ZaiPayment.items.list(limit: 20, offset: 40) +``` + +#### Search Items + +```ruby +# Search items by description +response = ZaiPayment.items.list(search: "product") + +# Filter by creation date +response = ZaiPayment.items.list( + created_after: "2024-01-01T00:00:00Z", + created_before: "2024-12-31T23:59:59Z" +) + +# Combine filters +response = ZaiPayment.items.list( + limit: 50, + search: "premium", + created_after: "2024-01-01T00:00:00Z" +) +``` + +### Show Item + +Get details of a specific item by ID. + +```ruby +response = ZaiPayment.items.show("item-123") + +if response.success? + item = response.data + puts "Item ID: #{item['id']}" + puts "Name: #{item['name']}" + puts "Amount: #{item['amount']}" + puts "Payment Type: #{item['payment_type']}" + puts "State: #{item['state']}" + puts "Buyer ID: #{item['buyer_id']}" + puts "Seller ID: #{item['seller_id']}" +end +``` + +### Update Item + +Update an existing item's details. + +```ruby +response = ZaiPayment.items.update( + "item-123", + name: "Updated Product Name", + description: "Updated product description", + amount: 12000, + buyer_url: "https://new-buyer.example.com", + tax_invoice: false +) + +if response.success? + item = response.data + puts "Item updated: #{item['id']}" +end +``` + +### Delete Item + +Delete an item by ID. + +```ruby +response = ZaiPayment.items.delete("item-123") + +if response.success? + puts "Item deleted successfully" +end +``` + +### Show Item Seller + +Get the seller (user) details for a specific item. + +```ruby +response = ZaiPayment.items.show_seller("item-123") + +if response.success? + seller = response.data + puts "Seller: #{seller['first_name']} #{seller['last_name']}" + puts "Email: #{seller['email']}" + puts "Country: #{seller['country']}" +end +``` + +### Show Item Buyer + +Get the buyer (user) details for a specific item. + +```ruby +response = ZaiPayment.items.show_buyer("item-123") + +if response.success? + buyer = response.data + puts "Buyer: #{buyer['first_name']} #{buyer['last_name']}" + puts "Email: #{buyer['email']}" +end +``` + +### Show Item Fees + +Get the fees associated with an item. + +```ruby +response = ZaiPayment.items.show_fees("item-123") + +if response.success? + fees = response.data + + if fees && fees.any? + puts "Item has #{fees.length} fee(s):" + fees.each do |fee| + puts " #{fee['name']}: $#{fee['amount'] / 100.0}" + end + end +end +``` + +### Show Item Wire Details + +Get wire transfer details for an item (useful for bank transfers). + +```ruby +response = ZaiPayment.items.show_wire_details("item-123") + +if response.success? + wire_details = response.data['wire_details'] + + if wire_details + puts "Account Number: #{wire_details['account_number']}" + puts "Routing Number: #{wire_details['routing_number']}" + puts "Bank Name: #{wire_details['bank_name']}" + end +end +``` + +### List Item Transactions + +Get all transactions associated with an item. + +```ruby +# List transactions with default pagination +response = ZaiPayment.items.list_transactions("item-123") + +if response.success? + transactions = response.data + + transactions.each do |transaction| + puts "Transaction #{transaction['id']}: #{transaction['state']}" + puts " Amount: $#{transaction['amount'] / 100.0}" + puts " Type: #{transaction['type']}" + end +end + +# List with custom pagination +response = ZaiPayment.items.list_transactions("item-123", limit: 50, offset: 100) +``` + +### List Item Batch Transactions + +Get all batch transactions associated with an item. + +```ruby +response = ZaiPayment.items.list_batch_transactions("item-123") + +if response.success? + batch_transactions = response.data + + batch_transactions.each do |batch| + puts "Batch #{batch['id']}: #{batch['state']}" + puts " Amount: $#{batch['amount'] / 100.0}" + end +end + +# List with pagination +response = ZaiPayment.items.list_batch_transactions("item-123", limit: 25, offset: 50) +``` + +### Show Item Status + +Get the current status of an item. + +```ruby +response = ZaiPayment.items.show_status("item-123") + +if response.success? + status = response.data + puts "State: #{status['state']}" + puts "Payment State: #{status['payment_state']}" + puts "Disbursement State: #{status['disbursement_state']}" if status['disbursement_state'] +end +``` + +## Field Reference + +### Item Fields + +| Field | Type | Description | Required | +|-------|------|-------------|----------| +| `id` | String | Unique item ID (auto-generated if not provided) | Optional | +| `name` | String | Name/title of the item | โœ“ | +| `amount` | Integer | Amount in cents (e.g., 10000 = $100.00) | โœ“ | +| `payment_type` | Integer | Payment type (1-7, default: 2) | โœ“ | +| `buyer_id` | String | Buyer user ID | โœ“ | +| `seller_id` | String | Seller user ID | โœ“ | +| `fee_ids` | Array | Array of fee IDs to apply | Optional | +| `description` | String | Detailed description | Optional | +| `currency` | String | Currency code (e.g., 'AUD', 'USD') | Optional | +| `custom_descriptor` | String | Custom text for bank statements | Optional | +| `buyer_url` | String | URL for buyer to access transaction | Optional | +| `seller_url` | String | URL for seller to access transaction | Optional | +| `tax_invoice` | Boolean | Whether to generate a tax invoice | Optional | + +### Payment Types + +When creating items, you can specify different payment types: + +| Type | Description | +|------|-------------| +| 1 | Direct Debit | +| 2 | Credit Card (default) | +| 3 | Bank Transfer | +| 4 | Wallet | +| 5 | BPay | +| 6 | PayPal | +| 7 | Other | + +Example: + +```ruby +# Create item with bank transfer payment type +response = ZaiPayment.items.create( + name: "Bank Transfer Payment", + amount: 30000, + payment_type: 3, # Bank Transfer + buyer_id: "buyer-123", + seller_id: "seller-456" +) +``` + +## Item States + +Items go through various states during their lifecycle: + +- `pending` - Item created but not yet paid +- `payment_pending` - Payment in progress +- `payment_held` - Payment held (e.g., for review) +- `payment_deposited` - Payment received and deposited +- `work_completed` - Work/delivery completed +- `completed` - Transaction completed successfully +- `refunded` - Transaction refunded +- `cancelled` - Transaction cancelled + +Check the item status using `show_status` method to track these state changes. + +## Error Handling + +The Item resource will raise validation errors for: + +- Missing required fields +- Invalid amount (must be positive integer in cents) +- Invalid payment type (must be 1-7) +- Invalid item ID format +- Item not found + +```ruby +begin + response = ZaiPayment.items.create( + name: "Product", + amount: -100, # Invalid: negative amount + payment_type: 2, + buyer_id: "buyer-123", + seller_id: "seller-456" + ) +rescue ZaiPayment::Errors::ValidationError => e + puts "Validation error: #{e.message}" +rescue ZaiPayment::Errors::NotFoundError => e + puts "Item not found: #{e.message}" +rescue ZaiPayment::Errors::ApiError => e + puts "API error: #{e.message}" +end +``` + +## Best Practices + +### 1. Use Meaningful Names and Descriptions + +Always provide clear, descriptive names and descriptions for items. This helps with: +- Transaction tracking +- Customer support +- Financial reconciliation + +```ruby +response = ZaiPayment.items.create( + name: "Order #12345 - Premium Widget", + description: "Premium widget with extended warranty - Customer: John Doe", + amount: 29900, + # ... other fields +) +``` + +### 2. Track Custom IDs + +If you have your own order/transaction IDs, use the optional `id` field: + +```ruby +response = ZaiPayment.items.create( + id: "order-#{your_order_id}", + name: "Order #{your_order_id}", + # ... other fields +) +``` + +### 3. Set Buyer and Seller URLs + +Provide URLs so users can access transaction details on your platform: + +```ruby +response = ZaiPayment.items.create( + buyer_url: "https://myapp.com/orders/#{order_id}", + seller_url: "https://myapp.com/sales/#{order_id}", + # ... other fields +) +``` + +### 4. Monitor Transaction Status + +Regularly check item status to track payment and disbursement: + +```ruby +status_response = ZaiPayment.items.show_status(item_id) +puts "Payment: #{status_response.data['payment_state']}" +puts "Disbursement: #{status_response.data['disbursement_state']}" +``` + +### 5. Handle Fees Properly + +If applying platform fees, create fee objects first and reference them: + +```ruby +# Assume you've created fees with IDs: "fee-platform", "fee-service" +response = ZaiPayment.items.create( + name: "Product Purchase", + amount: 50000, + fee_ids: ["fee-platform", "fee-service"], + # ... other fields +) +``` + +## Response Structure + +### Successful Response + +```ruby +response.success? # => true +response.status # => 200 or 201 +response.data # => Item object hash +response.meta # => Pagination metadata (for list) +``` + +### Item Object Example + +```ruby +{ + "id" => "item-abc123", + "name" => "Product Purchase", + "amount" => 10000, + "payment_type" => 2, + "buyer_id" => "buyer-123", + "seller_id" => "seller-456", + "description" => "Purchase of product XYZ", + "currency" => "AUD", + "state" => "pending", + "payment_state" => "pending", + "buyer_url" => "https://buyer.example.com", + "seller_url" => "https://seller.example.com", + "tax_invoice" => true, + "created_at" => "2025-01-01T00:00:00Z", + "updated_at" => "2025-01-01T00:00:00Z" +} +``` + +## Complete Workflow Example + +Here's a complete example of creating an item and tracking it through its lifecycle: + +```ruby +require 'zai_payment' + +# Configure +ZaiPayment.configure do |config| + config.client_id = ENV['ZAI_CLIENT_ID'] + config.client_secret = ENV['ZAI_CLIENT_SECRET'] + config.scope = ENV['ZAI_SCOPE'] + config.environment = :prelive +end + +items = ZaiPayment.items + +# 1. Create an item +create_response = items.create( + name: "E-commerce Purchase - Order #12345", + amount: 50000, # $500.00 + payment_type: 2, # Credit card + buyer_id: "buyer-abc123", + seller_id: "seller-xyz789", + description: "Online store purchase - Premium product bundle", + currency: "AUD", + buyer_url: "https://store.example.com/orders/12345", + seller_url: "https://seller.example.com/sales/12345", + tax_invoice: true +) + +if create_response.success? + item_id = create_response.data['id'] + puts "โœ“ Item created: #{item_id}" + + # 2. Get item details + show_response = items.show(item_id) + puts "โœ“ Item: #{show_response.data['name']}" + + # 3. Get seller details + seller_response = items.show_seller(item_id) + puts "โœ“ Seller: #{seller_response.data['email']}" + + # 4. Get buyer details + buyer_response = items.show_buyer(item_id) + puts "โœ“ Buyer: #{buyer_response.data['email']}" + + # 5. Check fees + fees_response = items.show_fees(item_id) + if fees_response.success? && fees_response.data&.any? + total_fees = fees_response.data.sum { |f| f['amount'] } + puts "โœ“ Total fees: $#{total_fees / 100.0}" + end + + # 6. Monitor status + status_response = items.show_status(item_id) + puts "โœ“ Payment state: #{status_response.data['payment_state']}" + + # 7. List transactions + txn_response = items.list_transactions(item_id) + puts "โœ“ Transactions: #{txn_response.data&.length || 0}" + + # 8. Update if needed + update_response = items.update( + item_id, + description: "Updated: Order #12345 - Payment confirmed" + ) + puts "โœ“ Item updated" if update_response.success? +else + puts "โœ— Error: #{create_response.error_message}" +end +``` + +## Integration with Rails + +### In a Controller + +```ruby +class OrdersController < ApplicationController + def create + # Create users first (buyer and seller) + # ... user creation code ... + + # Create item + response = ZaiPayment.items.create( + name: "Order ##{@order.id}", + amount: (@order.total * 100).to_i, # Convert to cents + payment_type: 2, + buyer_id: @buyer_zai_id, + seller_id: @seller_zai_id, + description: @order.description, + buyer_url: order_url(@order), + seller_url: seller_order_url(@order), + tax_invoice: @order.requires_tax_invoice? + ) + + if response.success? + @order.update(zai_item_id: response.data['id']) + redirect_to @order, notice: 'Order created successfully' + else + flash[:error] = "Payment error: #{response.error_message}" + render :new + end + end + + def show + # Get item status from Zai + if @order.zai_item_id + response = ZaiPayment.items.show_status(@order.zai_item_id) + @payment_status = response.data if response.success? + end + end +end +``` + +### In a Background Job + +```ruby +class CheckItemStatusJob < ApplicationJob + def perform(order_id) + order = Order.find(order_id) + + response = ZaiPayment.items.show_status(order.zai_item_id) + + if response.success? + status = response.data + + order.update( + payment_state: status['payment_state'], + disbursement_state: status['disbursement_state'] + ) + + # Send notifications based on state + if status['payment_state'] == 'completed' + OrderMailer.payment_completed(order).deliver_later + end + end + end +end +``` + +## Testing + +The Item resource includes comprehensive test coverage. Run the tests with: + +```bash +bundle exec rspec spec/zai_payment/resources/item_spec.rb +``` + +## See Also + +- [User Management Guide](users.md) - Managing buyers and sellers +- [Webhook Documentation](webhooks.md) - Receiving item status updates +- [Authentication Documentation](authentication.md) - OAuth2 setup +- [Item Examples](../examples/items.md) - More code examples + +## External Resources + +- [Zai Items API Reference](https://developer.hellozai.com/reference/listitems) +- [Zai Developer Portal](https://developer.hellozai.com/) +- [Payment Types Documentation](https://developer.hellozai.com/docs/payment-types) + diff --git a/docs/README.md b/docs/readme.md similarity index 100% rename from docs/README.md rename to docs/readme.md diff --git a/docs/USER_ID_FIELD.md b/docs/user_id_field.md similarity index 100% rename from docs/USER_ID_FIELD.md rename to docs/user_id_field.md diff --git a/docs/USER_QUICK_REFERENCE.md b/docs/user_quick_reference.md similarity index 95% rename from docs/USER_QUICK_REFERENCE.md rename to docs/user_quick_reference.md index 89589b5..7a2cf5c 100644 --- a/docs/USER_QUICK_REFERENCE.md +++ b/docs/user_quick_reference.md @@ -47,7 +47,7 @@ response = ZaiPayment.users.create( first_name: 'Jane', last_name: 'Smith', country: 'AUS', - dob: '19900101', + dob: '01/01/1990', address_line1: '123 Main St', city: 'Sydney', state: 'NSW', @@ -83,7 +83,7 @@ response = ZaiPayment.users.update( | first_name | String | โœ“ | | last_name | String | โœ“ | | country | String (ISO 3166-1 alpha-3) | โœ“ | -| dob | String (YYYYMMDD) | โœ“ | +| dob | String (DD/MM/YYYY) | โœ“ | | address_line1 | String | โœ“ | | city | String | โœ“ | | state | String | โœ“ | @@ -106,9 +106,9 @@ country: 'GBR' # United Kingdom country: 'CAN' # Canada ``` -### Date of Birth (YYYYMMDD) +### Date of Birth (DD/MM/YYYY) ```ruby -dob: '19900101' # January 1, 1990 +dob: '01/01/1990' # January 1, 1990 ``` ## Error Handling @@ -216,7 +216,7 @@ ruby examples/user_demo.rb ## Documentation Links -- [Full User Guide](USERS.md) +- [Full User Guide](users.md) - [Usage Examples](../examples/users.md) - [Zai: Payin User](https://developer.hellozai.com/docs/onboarding-a-pay-in-user) - [Zai: Payout User](https://developer.hellozai.com/docs/onboarding-a-pay-out-user) @@ -224,7 +224,7 @@ ruby examples/user_demo.rb ## Support For issues or questions: -1. Check the [User Management Guide](USERS.md) +1. Check the [User Management Guide](users.md) 2. Review [Examples](../examples/users.md) 3. Visit [Zai Developer Portal](https://developer.hellozai.com/) diff --git a/docs/USERS.md b/docs/users.md similarity index 100% rename from docs/USERS.md rename to docs/users.md diff --git a/docs/WEBHOOK_SECURITY_QUICKSTART.md b/docs/webhook_security_quickstart.md similarity index 98% rename from docs/WEBHOOK_SECURITY_QUICKSTART.md rename to docs/webhook_security_quickstart.md index 3cf5464..d91d4d8 100644 --- a/docs/WEBHOOK_SECURITY_QUICKSTART.md +++ b/docs/webhook_security_quickstart.md @@ -123,7 +123,7 @@ end ## ๐Ÿ“š Full Documentation -- [Complete Setup Guide](WEBHOOKS.md#webhook-security-signature-verification) +- [Complete Setup Guide](webhooks.md#webhook-security-signature-verification) - [More Examples](../examples/webhooks.md#webhook-security-complete-setup-guide) - [Zai Official Docs](https://developer.hellozai.com/docs/verify-webhook-signatures) diff --git a/docs/WEBHOOK_SIGNATURE.md b/docs/webhook_signature.md similarity index 100% rename from docs/WEBHOOK_SIGNATURE.md rename to docs/webhook_signature.md diff --git a/docs/WEBHOOKS.md b/docs/webhooks.md similarity index 100% rename from docs/WEBHOOKS.md rename to docs/webhooks.md diff --git a/examples/items.md b/examples/items.md new file mode 100644 index 0000000..97ad6fc --- /dev/null +++ b/examples/items.md @@ -0,0 +1,598 @@ +# Zai Payment - Items Examples + +This document provides examples of how to use the Items resource in the Zai Payment gem. + +## Table of Contents + +- [Setup](#setup) +- [Create Item](#create-item) +- [List Items](#list-items) +- [Show Item](#show-item) +- [Update Item](#update-item) +- [Delete Item](#delete-item) +- [Show Item Seller](#show-item-seller) +- [Show Item Buyer](#show-item-buyer) +- [Show Item Fees](#show-item-fees) +- [Show Item Wire Details](#show-item-wire-details) +- [List Item Transactions](#list-item-transactions) +- [List Item Batch Transactions](#list-item-batch-transactions) +- [Show Item Status](#show-item-status) + +## Setup + +```ruby +require 'zai_payment' + +# Configure the gem +ZaiPayment.configure do |config| + config.client_id = 'your_client_id' + config.client_secret = 'your_client_secret' + config.scope = 'your_scope' + config.environment = :prelive # or :production +end + +# Access the items resource +items = ZaiPayment.items +``` + +## Create Item + +Create a new item (transaction/payment) between a buyer and a seller. + +```ruby +# Create a basic item +response = items.create( + name: "Product Purchase", + amount: 10000, # Amount in cents (100.00) + payment_type: 2, # Payment type (1-7, 2 is default) + buyer_id: "buyer-123", + seller_id: "seller-456" +) + +if response.success? + item = response.data + puts "Item created: #{item['id']}" + puts "Name: #{item['name']}" + puts "Amount: #{item['amount']}" +else + puts "Error: #{response.error_message}" +end +``` + +### Create Item with Optional Fields + +```ruby +response = items.create( + name: "Premium Product", + amount: 25000, + payment_type: 2, + buyer_id: "buyer-123", + seller_id: "seller-456", + description: "Purchase of premium product XYZ", + currency: "AUD", + fee_ids: ["fee-1", "fee-2"], + custom_descriptor: "MY STORE PURCHASE", + buyer_url: "https://buyer.example.com", + seller_url: "https://seller.example.com", + tax_invoice: true +) + +if response.success? + item = response.data + puts "Item created with ID: #{item['id']}" + puts "Description: #{item['description']}" + puts "Tax Invoice: #{item['tax_invoice']}" +end +``` + +### Create Item with Custom ID + +```ruby +response = items.create( + id: "my-custom-item-#{Time.now.to_i}", + name: "Custom ID Product", + amount: 15000, + payment_type: 2, + buyer_id: "buyer-123", + seller_id: "seller-456" +) + +if response.success? + item = response.data + puts "Item created with custom ID: #{item['id']}" +end +``` + +## List Items + +Retrieve a list of all items with pagination and optional search/filtering. + +```ruby +# List items with default pagination (10 items) +response = items.list + +if response.success? + items_list = response.data + items_list.each do |item| + puts "Item ID: #{item['id']}, Name: #{item['name']}, Amount: #{item['amount']}" + end + + # Access metadata + meta = response.meta + puts "Total items: #{meta['total']}" + puts "Limit: #{meta['limit']}" + puts "Offset: #{meta['offset']}" +end +``` + +### List Items with Custom Pagination + +```ruby +# List 20 items starting from offset 40 +response = items.list(limit: 20, offset: 40) + +if response.success? + items_list = response.data + puts "Retrieved #{items_list.length} items" +end +``` + +### Search Items by Description + +```ruby +# Search for items with "product" in the description +response = items.list(search: "product") + +if response.success? + items_list = response.data + puts "Found #{items_list.length} items matching 'product'" + items_list.each do |item| + puts " - #{item['name']}: #{item['description']}" + end +end +``` + +### Filter Items by Creation Date + +```ruby +# Get items created in a specific date range +response = items.list( + created_after: "2024-01-01T00:00:00Z", + created_before: "2024-12-31T23:59:59Z" +) + +if response.success? + items_list = response.data + puts "Found #{items_list.length} items created in 2024" +end +``` + +### Combine Search and Filters + +```ruby +# Search with pagination and date filters +response = items.list( + limit: 50, + offset: 0, + search: "premium", + created_after: "2024-01-01T00:00:00Z" +) + +if response.success? + items_list = response.data + puts "Found #{items_list.length} premium items created after Jan 1, 2024" +end +``` + +## Show Item + +Get details of a specific item by ID. + +```ruby +response = items.show("item-123") + +if response.success? + item = response.data + puts "Item ID: #{item['id']}" + puts "Name: #{item['name']}" + puts "Amount: #{item['amount']}" + puts "Payment Type: #{item['payment_type']}" + puts "Buyer ID: #{item['buyer_id']}" + puts "Seller ID: #{item['seller_id']}" + puts "Description: #{item['description']}" + puts "State: #{item['state']}" + puts "Buyer URL: #{item['buyer_url']}" if item['buyer_url'] + puts "Seller URL: #{item['seller_url']}" if item['seller_url'] + puts "Tax Invoice: #{item['tax_invoice']}" unless item['tax_invoice'].nil? +else + puts "Error: #{response.error_message}" +end +``` + +## Update Item + +Update an existing item's details. + +```ruby +response = items.update( + "item-123", + name: "Updated Product Name", + description: "Updated product description", + amount: 12000, + buyer_url: "https://new-buyer.example.com", + tax_invoice: false +) + +if response.success? + item = response.data + puts "Item updated: #{item['id']}" + puts "New name: #{item['name']}" + puts "New amount: #{item['amount']}" + puts "Tax Invoice: #{item['tax_invoice']}" +else + puts "Error: #{response.error_message}" +end +``` + +### Update Item Seller or Buyer + +```ruby +response = items.update( + "item-123", + seller_id: "new-seller-789", + buyer_id: "new-buyer-012" +) + +if response.success? + puts "Item updated with new buyer and seller" +end +``` + +## Delete Item + +Delete an item by ID. + +```ruby +response = items.delete("item-123") + +if response.success? + puts "Item deleted successfully" +else + puts "Error: #{response.error_message}" +end +``` + +## Show Item Seller + +Get the seller (user) details for a specific item. + +```ruby +response = items.show_seller("item-123") + +if response.success? + seller = response.data + puts "Seller ID: #{seller['id']}" + puts "Seller Email: #{seller['email']}" + puts "Seller Name: #{seller['first_name']} #{seller['last_name']}" + puts "Country: #{seller['country']}" +else + puts "Error: #{response.error_message}" +end +``` + +## Show Item Buyer + +Get the buyer (user) details for a specific item. + +```ruby +response = items.show_buyer("item-123") + +if response.success? + buyer = response.data + puts "Buyer ID: #{buyer['id']}" + puts "Buyer Email: #{buyer['email']}" + puts "Buyer Name: #{buyer['first_name']} #{buyer['last_name']}" + puts "Country: #{buyer['country']}" +else + puts "Error: #{response.error_message}" +end +``` + +## Show Item Fees + +Get the fees associated with an item. + +```ruby +response = items.show_fees("item-123") + +if response.success? + fees = response.data + + if fees && fees.any? + puts "Item has #{fees.length} fee(s):" + fees.each do |fee| + puts " Fee ID: #{fee['id']}" + puts " Name: #{fee['name']}" + puts " Amount: #{fee['amount']}" + puts " Fee Type: #{fee['fee_type']}" + end + else + puts "No fees associated with this item" + end +else + puts "Error: #{response.error_message}" +end +``` + +## Show Item Wire Details + +Get wire transfer details for an item. + +```ruby +response = items.show_wire_details("item-123") + +if response.success? + wire_details = response.data['wire_details'] + + if wire_details + puts "Wire Transfer Details:" + puts " Account Number: #{wire_details['account_number']}" + puts " Routing Number: #{wire_details['routing_number']}" + puts " Bank Name: #{wire_details['bank_name']}" + puts " Swift Code: #{wire_details['swift_code']}" + else + puts "No wire details available for this item" + end +else + puts "Error: #{response.error_message}" +end +``` + +## List Item Transactions + +Get all transactions associated with an item. + +```ruby +# List transactions with default pagination +response = items.list_transactions("item-123") + +if response.success? + transactions = response.data + + if transactions && transactions.any? + puts "Item has #{transactions.length} transaction(s):" + transactions.each do |transaction| + puts " Transaction ID: #{transaction['id']}" + puts " Amount: #{transaction['amount']}" + puts " State: #{transaction['state']}" + puts " Type: #{transaction['type']}" + puts " Created At: #{transaction['created_at']}" + end + else + puts "No transactions found for this item" + end +else + puts "Error: #{response.error_message}" +end +``` + +### List Item Transactions with Pagination + +```ruby +# List 50 transactions starting from offset 100 +response = items.list_transactions("item-123", limit: 50, offset: 100) + +if response.success? + transactions = response.data + puts "Retrieved #{transactions.length} transactions" +end +``` + +## List Item Batch Transactions + +Get all batch transactions associated with an item. + +```ruby +response = items.list_batch_transactions("item-123") + +if response.success? + batch_transactions = response.data + + if batch_transactions && batch_transactions.any? + puts "Item has #{batch_transactions.length} batch transaction(s):" + batch_transactions.each do |batch| + puts " Batch ID: #{batch['id']}" + puts " Amount: #{batch['amount']}" + puts " State: #{batch['state']}" + puts " Created At: #{batch['created_at']}" + end + else + puts "No batch transactions found for this item" + end +else + puts "Error: #{response.error_message}" +end +``` + +### List Item Batch Transactions with Pagination + +```ruby +# List 25 batch transactions starting from offset 50 +response = items.list_batch_transactions("item-123", limit: 25, offset: 50) + +if response.success? + batch_transactions = response.data + puts "Retrieved #{batch_transactions.length} batch transactions" +end +``` + +## Show Item Status + +Get the current status of an item. + +```ruby +response = items.show_status("item-123") + +if response.success? + item_status = response.data + puts "Item ID: #{item_status['id']}" + puts "State: #{item_status['state']}" + puts "Payment State: #{item_status['payment_state']}" + puts "Disbursement State: #{item_status['disbursement_state']}" if item_status['disbursement_state'] + puts "Status Description: #{item_status['status_description']}" if item_status['status_description'] +else + puts "Error: #{response.error_message}" +end +``` + +## Complete Workflow Example + +Here's a complete example of creating an item and performing various operations on it: + +```ruby +require 'zai_payment' + +# Configure +ZaiPayment.configure do |config| + config.client_id = ENV['ZAI_CLIENT_ID'] + config.client_secret = ENV['ZAI_CLIENT_SECRET'] + config.scope = ENV['ZAI_SCOPE'] + config.environment = :prelive +end + +items = ZaiPayment.items + +# 1. Create an item +create_response = items.create( + name: "E-commerce Purchase", + amount: 50000, # $500.00 + payment_type: 2, + buyer_id: "buyer-abc123", + seller_id: "seller-xyz789", + description: "Online store purchase - Order #12345", + currency: "AUD", + buyer_url: "https://buyer-portal.example.com", + seller_url: "https://seller-portal.example.com", + tax_invoice: true +) + +if create_response.success? + item_id = create_response.data['id'] + puts "โœ“ Item created: #{item_id}" + + # 2. Get item details + show_response = items.show(item_id) + if show_response.success? + puts "โœ“ Item retrieved: #{show_response.data['name']}" + end + + # 3. Get seller details + seller_response = items.show_seller(item_id) + if seller_response.success? + seller = seller_response.data + puts "โœ“ Seller: #{seller['email']}" + end + + # 4. Get buyer details + buyer_response = items.show_buyer(item_id) + if buyer_response.success? + buyer = buyer_response.data + puts "โœ“ Buyer: #{buyer['email']}" + end + + # 5. Check item status + status_response = items.show_status(item_id) + if status_response.success? + status = status_response.data + puts "โœ“ Item status: #{status['state']}" + end + + # 6. List transactions + transactions_response = items.list_transactions(item_id) + if transactions_response.success? + txn_count = transactions_response.data&.length || 0 + puts "โœ“ Item has #{txn_count} transaction(s)" + end + + # 7. Update item if needed + update_response = items.update( + item_id, + description: "Updated: Online store purchase - Order #12345 (Confirmed)", + tax_invoice: true + ) + if update_response.success? + puts "โœ“ Item updated successfully" + end +else + puts "โœ— Error creating item: #{create_response.error_message}" +end +``` + +## Error Handling + +Always check the response status and handle errors appropriately: + +```ruby +response = items.show("item-123") + +if response.success? + # Handle successful response + item = response.data + puts "Item: #{item['name']}" +else + # Handle error + case response.status + when 404 + puts "Item not found" + when 401 + puts "Authentication failed - check your credentials" + when 422 + puts "Validation error: #{response.error_message}" + else + puts "Error: #{response.error_message}" + end +end +``` + +## Payment Types + +When creating items, you can specify different payment types: + +- **1**: Direct Debit +- **2**: Credit Card (default) +- **3**: Bank Transfer +- **4**: Wallet +- **5**: BPay +- **6**: PayPal +- **7**: Other + +Example: + +```ruby +# Create item with bank transfer payment type +response = items.create( + name: "Bank Transfer Payment", + amount: 30000, + payment_type: 3, # Bank Transfer + buyer_id: "buyer-123", + seller_id: "seller-456" +) +``` + +## API Reference + +For more information about the Zai Items API, see: + +- [Create Item](https://developer.hellozai.com/reference/createitem) +- [List Items](https://developer.hellozai.com/reference/listitems) +- [Show Item](https://developer.hellozai.com/reference/showitem) +- [Update Item](https://developer.hellozai.com/reference/updateitem) +- [Delete Item](https://developer.hellozai.com/reference/deleteitem) +- [Show Item Seller](https://developer.hellozai.com/reference/showitemseller) +- [Show Item Buyer](https://developer.hellozai.com/reference/showitembuyer) +- [Show Item Fees](https://developer.hellozai.com/reference/showitemfees) +- [Show Item Wire Details](https://developer.hellozai.com/reference/showitemwiredetails) +- [List Item Transactions](https://developer.hellozai.com/reference/listitemtransactions) +- [List Item Batch Transactions](https://developer.hellozai.com/reference/listitembatchtransactions) +- [Show Item Status](https://developer.hellozai.com/reference/showitemstatus) + diff --git a/examples/users.md b/examples/users.md index 517e24c..9409097 100644 --- a/examples/users.md +++ b/examples/users.md @@ -32,6 +32,7 @@ Create a buyer with minimal required information. ```ruby # Create a basic payin user response = ZaiPayment.users.create( + user_type: 'payin', email: 'buyer@example.com', first_name: 'John', last_name: 'Doe', @@ -43,6 +44,9 @@ if response.success? puts "Payin user created successfully!" puts "User ID: #{user['id']}" puts "Email: #{user['email']}" + + # Note: device_id and ip_address will be required later + # when creating an item and charging a card else puts "Failed to create user" end @@ -55,6 +59,7 @@ Create a buyer with all recommended information for better fraud prevention. ```ruby response = ZaiPayment.users.create( # Required fields + user_type: 'payin', email: 'john.buyer@example.com', first_name: 'John', last_name: 'Doe', @@ -67,15 +72,14 @@ response = ZaiPayment.users.create( state: 'NY', zip: '10001', mobile: '+1234567890', - dob: '15/01/1990', - - # For fraud prevention (required when charging) - device_id: 'device_abc123xyz', - ip_address: '192.168.1.1' + dob: '15/01/1990' ) user = response.data puts "Complete payin user profile created: #{user['id']}" + +# Note: device_id and ip_address can be stored separately +# and will be required when creating an item and charging a card ``` ### Example 3: Progressive Profile Building @@ -85,6 +89,7 @@ Create a user quickly, then update with additional information later. ```ruby # Step 1: Quick user creation during signup response = ZaiPayment.users.create( + user_type: 'payin', email: 'quicksignup@example.com', first_name: 'Jane', last_name: 'Smith', @@ -106,14 +111,9 @@ ZaiPayment.users.update( puts "User profile updated with shipping address" -# Step 3: Add device info before payment -ZaiPayment.users.update( - user_id, - device_id: 'device_xyz789', - ip_address: '203.0.113.42' -) - -puts "Device information added for payment" +# Note: device_id and ip_address should be captured during +# payment flow and will be required when creating an item +# They are not stored in the user profile, but used at transaction time ``` ## Payout User Examples @@ -124,6 +124,9 @@ Create a seller who will receive payments. All required fields must be provided. ```ruby response = ZaiPayment.users.create( + # User type + user_type: 'payout', + # Required for payout users email: 'seller@example.com', first_name: 'Alice', @@ -137,8 +140,7 @@ response = ZaiPayment.users.create( # Recommended mobile: '+14155551234', - government_number: '123456789', # SSN or Tax ID - user_type: 'payout' + government_number: '123456789' # SSN or Tax ID ) seller = response.data @@ -152,6 +154,7 @@ Create an Australian seller with appropriate details. ```ruby response = ZaiPayment.users.create( + user_type: 'payout', email: 'aussie.seller@example.com', first_name: 'Bruce', last_name: 'Williams', @@ -165,8 +168,7 @@ response = ZaiPayment.users.create( zip: '2000', mobile: '+61298765432', - government_number: '123456789', # TFN (Tax File Number) - user_type: 'payout' + government_number: '123456789' # TFN (Tax File Number) ) if response.success? @@ -182,6 +184,7 @@ Create a UK-based seller. ```ruby response = ZaiPayment.users.create( + user_type: 'payout', email: 'uk.merchant@example.com', first_name: 'Oliver', last_name: 'Brown', @@ -195,8 +198,7 @@ response = ZaiPayment.users.create( zip: 'SW1A 2AA', mobile: '+447700900123', - government_number: 'AB123456C', # National Insurance Number - user_type: 'payout' + government_number: 'AB123456C' # National Insurance Number ) merchant = response.data @@ -316,18 +318,21 @@ Create multiple users efficiently. ```ruby users_to_create = [ { + user_type: 'payin', email: 'buyer1@example.com', first_name: 'Alice', last_name: 'Anderson', country: 'USA' }, { + user_type: 'payin', email: 'buyer2@example.com', first_name: 'Bob', last_name: 'Brown', country: 'USA' }, { + user_type: 'payout', email: 'seller1@example.com', first_name: 'Charlie', last_name: 'Chen', @@ -336,8 +341,7 @@ users_to_create = [ address_line1: '123 Test St', city: 'Sydney', state: 'NSW', - zip: '2000', - user_type: 'payout' + zip: '2000' } ] @@ -549,6 +553,7 @@ end # Step 1: Create user during signup (minimal info) def create_initial_user(email:, name_parts:) ZaiPayment.users.create( + user_type: 'payin', # or 'payout' based on your use case email: email, first_name: name_parts[:first], last_name: name_parts[:last], @@ -592,32 +597,40 @@ Create a user representing a business entity with full company details. ```ruby # Example: Create a merchant user with company information response = ZaiPayment.users.create( - # Personal details + # User type + user_type: 'payout', + + # Personal details (required for payout users) email: 'john.director@example.com', first_name: 'John', last_name: 'Smith', country: 'AUS', + dob: '15/06/1985', + address_line1: '789 Business Ave', + city: 'Melbourne', + state: 'VIC', + zip: '3000', mobile: '+61412345678', # Business role authorized_signer_title: 'Director', - # Company details + # Company details (required fields for payout companies) company: { name: 'Smith Trading Co', legal_name: 'Smith Trading Company Pty Ltd', tax_number: '53004085616', # ABN for Australian companies business_email: 'accounts@smithtrading.com', country: 'AUS', - charge_tax: true, # GST registered - - # Business address address_line1: '123 Business Street', - address_line2: 'Suite 5', city: 'Melbourne', state: 'VIC', zip: '3000', - phone: '+61398765432' + phone: '+61398765432', + + # Optional fields + address_line2: 'Suite 5', + charge_tax: true # GST registered } ) @@ -630,27 +643,24 @@ end ### Pattern 4: Enhanced Fraud Prevention -Create users with additional verification data for enhanced security. +Capture device information for payin users during payment flow. ```ruby -# Example: Payin user with driver's license and IP tracking +# Example: Payin user creation with recommended fields response = ZaiPayment.users.create( # Required fields + user_type: 'payin', email: 'secure.buyer@example.com', first_name: 'Sarah', last_name: 'Johnson', country: 'USA', - # Enhanced verification + # Recommended verification fields dob: '15/01/1990', government_number: '123-45-6789', # SSN for US drivers_license_number: 'D1234567', drivers_license_state: 'CA', - # Fraud prevention - ip_address: '192.168.1.100', - device_id: 'device_abc123xyz', - # Contact and address mobile: '+14155551234', address_line1: '456 Market Street', @@ -660,16 +670,22 @@ response = ZaiPayment.users.create( zip: '94103' ) -puts "Secure user created with enhanced verification" +user_id = response.data['id'] +puts "User created with enhanced profile" + +# Note: device_id and ip_address should be captured during payment +# and will be required when creating an item and charging a card. +# They are typically obtained from your payment form or checkout page. ``` ### Pattern 5: Custom Branding for Merchants -Create a merchant user with custom branding for statements and payment pages. +Create a payout merchant user with custom branding for statements and payment pages. ```ruby # Example: Merchant with custom branding response = ZaiPayment.users.create( + user_type: 'payout', email: 'merchant@brandedstore.com', first_name: 'Alex', last_name: 'Merchant', @@ -677,17 +693,17 @@ response = ZaiPayment.users.create( mobile: '+61411222333', dob: '10/05/1985', + # Required address for payout users + address_line1: '789 Retail Plaza', + city: 'Brisbane', + state: 'QLD', + zip: '4000', + # Branding logo_url: 'https://example.com/logo.png', color_1: '#FF5733', # Primary brand color color_2: '#C70039', # Secondary brand color - custom_descriptor: 'BRANDED STORE', # Shows on bank statements - - # Address - address_line1: '789 Retail Plaza', - city: 'Brisbane', - state: 'QLD', - zip: '4000' + custom_descriptor: 'BRANDED STORE' # Shows on bank statements ) merchant = response.data @@ -697,11 +713,12 @@ puts "Custom descriptor: #{merchant['custom_descriptor']}" ### Pattern 6: AMEX Merchant Setup -Create a merchant specifically configured for American Express transactions. +Create a payout merchant specifically configured for American Express transactions. ```ruby # Example: AMEX merchant with required fields response = ZaiPayment.users.create( + user_type: 'payout', email: 'director@amexshop.com', first_name: 'Michael', last_name: 'Director', @@ -712,25 +729,27 @@ response = ZaiPayment.users.create( # AMEX requirement: Must specify authorized signer title authorized_signer_title: 'Managing Director', - # Business details + # Required address for payout users address_line1: '100 Corporate Drive', city: 'Sydney', state: 'NSW', zip: '2000', - # Company for AMEX merchants + # Company for AMEX merchants (required fields for payout companies) company: { name: 'AMEX Shop', legal_name: 'AMEX Shop Pty Limited', tax_number: '51824753556', business_email: 'finance@amexshop.com', country: 'AUS', - charge_tax: true, address_line1: '100 Corporate Drive', city: 'Sydney', state: 'NSW', zip: '2000', - phone: '+61299887766' + phone: '+61299887766', + + # Optional + charge_tax: true } ) @@ -739,7 +758,7 @@ puts "AMEX-ready merchant created: #{response.data['id']}" ## See Also -- [User Management Documentation](../docs/USERS.md) +- [User Management Documentation](../docs/users.md) - [Webhook Examples](webhooks.md) - [Zai API Reference](https://developer.hellozai.com/reference) diff --git a/IMPLEMENTATION.md b/implementation.md similarity index 95% rename from IMPLEMENTATION.md rename to implementation.md index 56161b8..75a2ef6 100644 --- a/IMPLEMENTATION.md +++ b/implementation.md @@ -28,7 +28,7 @@ A comprehensive User resource that provides CRUD operations for managing both pa - Country (ISO 3166-1 alpha-3 code, required) - Address details (line1, line2, city, state, zip) - Contact information (mobile, phone) -- Date of birth (YYYYMMDD format) +- Date of birth (DD/MM/YYYY format) - Government ID number - Device ID and IP address (for fraud prevention) - User type designation (payin/payout) @@ -37,7 +37,7 @@ A comprehensive User resource that provides CRUD operations for managing both pa - Required field validation - Email format validation - Country code validation (3-letter ISO codes) -- Date of birth format validation (YYYYMMDD) +- Date of birth format validation (DD/MM/YYYY) - User type validation (payin/payout) ### 2. Client Updates (`lib/zai_payment/client.rb`) @@ -83,7 +83,7 @@ A comprehensive User resource that provides CRUD operations for managing both pa ### 6. Documentation -#### User Guide (`docs/USERS.md`) +#### User Guide (`docs/users.md`) Comprehensive guide covering: - Overview of payin vs payout users - Required fields for each user type @@ -110,7 +110,7 @@ Practical examples including: - RSpec integration tests - Common patterns with retry logic -#### README Updates (`README.md`) +#### README Updates (`readme.md`) - Added Users section with quick examples - Updated roadmap to mark Users as "Done" - Added documentation links @@ -199,7 +199,7 @@ response = ZaiPayment.users.update( **Required:** - Email, first name, last name, country - Address, city, state, zip -- Date of birth (YYYYMMDD format) +- Date of birth (DD/MM/YYYY format) **Recommended:** - Mobile, government number @@ -208,7 +208,7 @@ response = ZaiPayment.users.update( 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 YYYYMMDD format (e.g., 19900101) +3. **Date of Birth**: Must be DD/MM/YYYY format (e.g., 19900101) 4. **User Type**: Must be 'payin' or 'payout' (optional field) ## Error Handling @@ -235,14 +235,14 @@ The implementation provides proper error handling for: ### 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 +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 +4. `/readme.md` - Added Users section and updated roadmap ## Code Quality @@ -292,7 +292,7 @@ The implementation is complete and ready for use. Recommended next steps: ## Support For questions or issues: -1. Check the documentation in `/docs/USERS.md` +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/ diff --git a/IMPLEMENTATION_SUMMARY.md b/implementation_summary.md similarity index 100% rename from IMPLEMENTATION_SUMMARY.md rename to implementation_summary.md diff --git a/lib/zai_payment.rb b/lib/zai_payment.rb index f7894e5..2ff151e 100644 --- a/lib/zai_payment.rb +++ b/lib/zai_payment.rb @@ -12,6 +12,7 @@ require_relative 'zai_payment/response' require_relative 'zai_payment/resources/webhook' require_relative 'zai_payment/resources/user' +require_relative 'zai_payment/resources/item' module ZaiPayment class << self @@ -45,5 +46,10 @@ def webhooks def users @users ||= Resources::User.new(client: Client.new(base_endpoint: :core_base)) end + + # @return [ZaiPayment::Resources::Item] item resource instance + def items + @items ||= Resources::Item.new + end end end diff --git a/lib/zai_payment/resources/item.rb b/lib/zai_payment/resources/item.rb new file mode 100644 index 0000000..e412969 --- /dev/null +++ b/lib/zai_payment/resources/item.rb @@ -0,0 +1,363 @@ +# frozen_string_literal: true + +module ZaiPayment + module Resources + # Item resource for managing Zai items (transactions/payments) + # + # @see https://developer.hellozai.com/reference/listitems + class Item + attr_reader :client + + # Map of attribute keys to API field names + FIELD_MAPPING = { + id: :id, + name: :name, + amount: :amount, + payment_type: :payment_type, + buyer_id: :buyer_id, + seller_id: :seller_id, + fee_ids: :fee_ids, + description: :description, + currency: :currency, + custom_descriptor: :custom_descriptor, + buyer_url: :buyer_url, + seller_url: :seller_url, + tax_invoice: :tax_invoice + }.freeze + + def initialize(client: nil) + @client = client || Client.new + end + + # List all items + # + # @param limit [Integer] number of records to return (default: 10, max: 200) + # @param offset [Integer] number of records to skip (default: 0) + # @param search [String] optional text value to search within item description + # @param created_before [String] optional ISO 8601 date/time to filter items created before + # (e.g. '2020-02-27T23:54:59Z') + # @param created_after [String] optional ISO 8601 date/time to filter items created after + # (e.g. '2020-02-27T23:54:59Z') + # @return [Response] the API response containing items array + # + # @example List all items + # items = ZaiPayment::Resources::Item.new + # response = items.list + # response.data # => [{"id" => "...", "name" => "..."}, ...] + # + # @example List items with search + # response = items.list(search: "product") + # + # @example List items created within a date range + # response = items.list( + # created_after: "2024-01-01T00:00:00Z", + # created_before: "2024-12-31T23:59:59Z" + # ) + # + # @see https://developer.hellozai.com/reference/listitems + def list(limit: 10, offset: 0, search: nil, created_before: nil, created_after: nil) + params = { + limit: limit, + offset: offset + } + + params[:search] = search if search + params[:created_before] = created_before if created_before + params[:created_after] = created_after if created_after + + client.get('/items', params: params) + end + + # Get a specific item by ID + # + # @param item_id [String] the item ID + # @return [Response] the API response containing item details + # + # @example + # items = ZaiPayment::Resources::Item.new + # response = items.show("item_id") + # response.data # => {"items" => {"id" => "item_id", "name" => "...", ...}} + # + # @see https://developer.hellozai.com/reference/showitem + def show(item_id) + validate_id!(item_id, 'item_id') + client.get("/items/#{item_id}") + end + + # Create a new item + # + # @param attributes [Hash] item attributes + # @option attributes [String] :id Optional unique ID for the item + # @option attributes [String] :name (Required) Name of the item + # @option attributes [Integer] :amount (Required) Amount in cents + # @option attributes [String] :payment_type (Required) Payment type (1-7, default: 2) + # @option attributes [String] :buyer_id (Required) Buyer user ID + # @option attributes [String] :seller_id (Required) Seller user ID + # @option attributes [Array] :fee_ids Optional array of fee IDs + # @option attributes [String] :description Optional description + # @option attributes [String] :currency Optional currency code (e.g., 'AUD') + # @option attributes [String] :custom_descriptor Optional custom descriptor + # @option attributes [String] :buyer_url Optional buyer URL + # @option attributes [String] :seller_url Optional seller URL + # @option attributes [Boolean] :tax_invoice Optional tax invoice flag + # @return [Response] the API response containing created item + # + # @example + # items = ZaiPayment::Resources::Item.new + # response = items.create( + # name: "Product Purchase", + # amount: 10000, + # payment_type: 2, + # buyer_id: "buyer-123", + # seller_id: "seller-456", + # description: "Purchase of product XYZ" + # ) + # + # @see https://developer.hellozai.com/reference/createitem + def create(**attributes) + validate_create_attributes!(attributes) + + body = build_item_body(attributes) + client.post('/items', body: body) + end + + # Update an existing item + # + # @param item_id [String] the item ID + # @param attributes [Hash] item attributes to update + # @option attributes [String] :name Name of the item + # @option attributes [Integer] :amount Amount in cents + # @option attributes [String] :description Description + # @option attributes [String] :buyer_id Buyer user ID + # @option attributes [String] :seller_id Seller user ID + # @option attributes [Array] :fee_ids Array of fee IDs + # @option attributes [String] :custom_descriptor Custom descriptor + # @option attributes [String] :buyer_url Buyer URL + # @option attributes [String] :seller_url Seller URL + # @option attributes [Boolean] :tax_invoice Tax invoice flag + # @return [Response] the API response containing updated item + # + # @example + # items = ZaiPayment::Resources::Item.new + # response = items.update( + # "item_id", + # name: "Updated Product Name", + # description: "Updated description" + # ) + # + # @see https://developer.hellozai.com/reference/updateitem + def update(item_id, **attributes) + validate_id!(item_id, 'item_id') + + body = build_item_body(attributes) + + raise Errors::ValidationError, 'At least one attribute must be provided for update' if body.empty? + + client.patch("/items/#{item_id}", body: body) + end + + # Delete an item + # + # @param item_id [String] the item ID + # @return [Response] the API response + # + # @example + # items = ZaiPayment::Resources::Item.new + # response = items.delete("item_id") + # + # @see https://developer.hellozai.com/reference/deleteitem + def delete(item_id) + validate_id!(item_id, 'item_id') + client.delete("/items/#{item_id}") + end + + # Show item seller + # + # @param item_id [String] the item ID + # @return [Response] the API response containing seller details + # + # @example + # items = ZaiPayment::Resources::Item.new + # response = items.show_seller("item_id") + # response.data # => {"users" => {"id" => "...", "email" => "...", ...}} + # + # @see https://developer.hellozai.com/reference/showitemseller + def show_seller(item_id) + validate_id!(item_id, 'item_id') + client.get("/items/#{item_id}/sellers") + end + + # Show item buyer + # + # @param item_id [String] the item ID + # @return [Response] the API response containing buyer details + # + # @example + # items = ZaiPayment::Resources::Item.new + # response = items.show_buyer("item_id") + # response.data # => {"users" => {"id" => "...", "email" => "...", ...}} + # + # @see https://developer.hellozai.com/reference/showitembuyer + def show_buyer(item_id) + validate_id!(item_id, 'item_id') + client.get("/items/#{item_id}/buyers") + end + + # Show item fees + # + # @param item_id [String] the item ID + # @return [Response] the API response containing fees details + # + # @example + # items = ZaiPayment::Resources::Item.new + # response = items.show_fees("item_id") + # response.data # => {"fees" => [{"id" => "...", "amount" => "...", ...}]} + # + # @see https://developer.hellozai.com/reference/showitemfees + def show_fees(item_id) + validate_id!(item_id, 'item_id') + client.get("/items/#{item_id}/fees") + end + + # Show item wire details + # + # @param item_id [String] the item ID + # @return [Response] the API response containing wire transfer details + # + # @example + # items = ZaiPayment::Resources::Item.new + # response = items.show_wire_details("item_id") + # response.data # => {"items" => {"wire_details" => {...}}} + # + # @see https://developer.hellozai.com/reference/showitemwiredetails + def show_wire_details(item_id) + validate_id!(item_id, 'item_id') + client.get("/items/#{item_id}/wire_details") + end + + # List item transactions + # + # @param item_id [String] the item ID + # @param limit [Integer] number of records to return (default: 10) + # @param offset [Integer] number of records to skip (default: 0) + # @return [Response] the API response containing transactions array + # + # @example + # items = ZaiPayment::Resources::Item.new + # response = items.list_transactions("item_id") + # response.data # => {"transactions" => [{"id" => "...", "amount" => "...", ...}]} + # + # @see https://developer.hellozai.com/reference/listitemtransactions + def list_transactions(item_id, limit: 10, offset: 0) + validate_id!(item_id, 'item_id') + + params = { + limit: limit, + offset: offset + } + + client.get("/items/#{item_id}/transactions", params: params) + end + + # List item batch transactions + # + # @param item_id [String] the item ID + # @param limit [Integer] number of records to return (default: 10) + # @param offset [Integer] number of records to skip (default: 0) + # @return [Response] the API response containing batch transactions array + # + # @example + # items = ZaiPayment::Resources::Item.new + # response = items.list_batch_transactions("item_id") + # response.data # => {"batch_transactions" => [{"id" => "...", ...}]} + # + # @see https://developer.hellozai.com/reference/listitembatchtransactions + def list_batch_transactions(item_id, limit: 10, offset: 0) + validate_id!(item_id, 'item_id') + + params = { + limit: limit, + offset: offset + } + + client.get("/items/#{item_id}/batch_transactions", params: params) + end + + # Show item status + # + # @param item_id [String] the item ID + # @return [Response] the API response containing status details + # + # @example + # items = ZaiPayment::Resources::Item.new + # response = items.show_status("item_id") + # response.data # => {"items" => {"state" => "...", ...}} + # + # @see https://developer.hellozai.com/reference/showitemstatus + def show_status(item_id) + validate_id!(item_id, 'item_id') + client.get("/items/#{item_id}/status") + end + + private + + def validate_id!(value, field_name) + return unless value.nil? || value.to_s.strip.empty? + + raise Errors::ValidationError, "#{field_name} is required and cannot be blank" + end + + def validate_presence!(value, field_name) + return unless value.nil? || value.to_s.strip.empty? + + raise Errors::ValidationError, "#{field_name} is required and cannot be blank" + end + + def validate_create_attributes!(attributes) + validate_required_attributes!(attributes) + validate_amount!(attributes[:amount]) if attributes[:amount] + validate_payment_type!(attributes[:payment_type]) if attributes[:payment_type] + end + + def validate_required_attributes!(attributes) + required_fields = %i[name amount payment_type buyer_id seller_id] + + missing_fields = required_fields.select do |field| + attributes[field].nil? || (attributes[field].respond_to?(:empty?) && attributes[field].to_s.strip.empty?) + end + + return if missing_fields.empty? + + raise Errors::ValidationError, + "Missing required fields: #{missing_fields.join(', ')}" + end + + def validate_amount!(amount) + return if amount.is_a?(Integer) && amount.positive? + + raise Errors::ValidationError, 'amount must be a positive integer (in cents)' + end + + def validate_payment_type!(payment_type) + # Payment types: 1-7 (2 is default) + valid_types = %w[1 2 3 4 5 6 7] + return if valid_types.include?(payment_type.to_s) + + raise Errors::ValidationError, 'payment_type must be between 1 and 7' + end + + def build_item_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/resources/user.rb b/lib/zai_payment/resources/user.rb index 6297d95..7db5c40 100644 --- a/lib/zai_payment/resources/user.rb +++ b/lib/zai_payment/resources/user.rb @@ -107,19 +107,22 @@ def show(user_id) # @option attributes [String] :id Optional unique ID for the user. If not provided, # Zai will generate one automatically. Cannot contain '.' character. # Useful for mapping to your existing system's user IDs. + # @option attributes [String] :user_type User type ('payin' or 'payout'). + # This determines which fields are required. # @option attributes [String] :email (Required) user's email address # @option attributes [String] :first_name (Required) user's first name # @option attributes [String] :last_name (Required) user's last name # @option attributes [String] :country (Required) user's country code (ISO 3166-1 alpha-3) - # @option attributes [String] :user_type Optional user type ('payin' or 'payout') + # @option attributes [String] :address_line1 (Required for payout) user's address line 1 + # @option attributes [String] :city (Required for payout) user's city + # @option attributes [String] :state (Required for payout) user's state + # @option attributes [String] :zip (Required for payout) user's postal/zip code + # @option attributes [String] :dob (Required for payout) user's date of birth (DD/MM/YYYY) + # @option attributes [String] :device_id device ID for fraud prevention (required when charging card) + # @option attributes [String] :ip_address IP address for fraud prevention (required when charging card) + # @option attributes [String] :address_line2 user's address line 2 # @option attributes [String] :mobile user's mobile phone number (international format with '+') # @option attributes [String] :phone user's phone number - # @option attributes [String] :address_line1 user's address line 1 - # @option attributes [String] :address_line2 user's address line 2 - # @option attributes [String] :city user's city - # @option attributes [String] :state user's state - # @option attributes [String] :zip user's postal/zip code - # @option attributes [String] :dob user's date of birth (DD/MM/YYYY) # @option attributes [String] :government_number user's government ID number (SSN, TFN, etc.) # @option attributes [String] :drivers_license_number driving license number # @option attributes [String] :drivers_license_state state section of the user's driving license @@ -129,13 +132,12 @@ def show(user_id) # @option attributes [String] :custom_descriptor custom descriptor for bundle direct debit statements # @option attributes [String] :authorized_signer_title job title for AMEX merchants (e.g., Director) # @option attributes [Hash] :company company details (creates a company for the user) - # @option attributes [String] :device_id device ID for fraud prevention - # @option attributes [String] :ip_address IP address for fraud prevention # @return [Response] the API response containing created user # # @example Create a payin user (buyer) with auto-generated ID # users = ZaiPayment::Resources::User.new # response = users.create( + # user_type: "payin", # email: "buyer@example.com", # first_name: "John", # last_name: "Doe", @@ -146,25 +148,29 @@ def show(user_id) # state: "NY", # zip: "10001" # ) + # # Note: device_id and ip_address are not required at user creation, + # # but will be required when creating an item and charging a card # # @example Create a payin user with custom ID # users = ZaiPayment::Resources::User.new # response = users.create( # id: "buyer-#{your_user_id}", + # user_type: "payin", # email: "buyer@example.com", # first_name: "John", # last_name: "Doe", # country: "USA" # ) # - # @example Create a payout user (seller/merchant) + # @example Create a payout user (seller/merchant) - individual # users = ZaiPayment::Resources::User.new # response = users.create( + # user_type: "payout", # email: "seller@example.com", # first_name: "Jane", # last_name: "Smith", # country: "AUS", - # dob: "19900101", + # dob: "01/01/1990", # address_line1: "456 Market St", # city: "Sydney", # state: "NSW", @@ -172,13 +178,19 @@ def show(user_id) # mobile: "+61412345678" # ) # - # @example Create a user with company details + # @example Create a payout user with company details # users = ZaiPayment::Resources::User.new # response = users.create( + # user_type: "payout", # email: "business@example.com", # first_name: "John", # last_name: "Doe", # country: "AUS", + # dob: "15/06/1985", + # address_line1: "789 Business Ave", + # city: "Melbourne", + # state: "VIC", + # zip: "3000", # mobile: "+61412345678", # authorized_signer_title: "Director", # company: { @@ -186,13 +198,12 @@ def show(user_id) # legal_name: "ABC 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", - # phone: "+61398765432" + # phone: "+61398765432", + # country: "AUS" # } # ) # @@ -274,12 +285,23 @@ def validate_create_attributes!(attributes) # rubocop:disable Metrics/AbcSize validate_country!(attributes[:country]) validate_dob!(attributes[:dob]) if attributes[:dob] validate_user_id!(attributes[:id]) if attributes[:id] - validate_company!(attributes[:company]) if attributes[:company] + validate_company!(attributes[:company], attributes[:user_type]) if attributes[:company] end def validate_required_attributes!(attributes) + # Base required fields for all users required_fields = %i[email first_name last_name country] + # Additional required fields for payout users + user_type = attributes[:user_type]&.to_s&.downcase + if user_type == USER_TYPE_PAYOUT + # For payout users, these fields become required + required_fields += %i[address_line1 city state zip dob] + end + + # NOTE: device_id and ip_address are NOT required at user creation for payin users. + # They are only required later when an item is created and a card is charged. + missing_fields = required_fields.select do |field| attributes[field].nil? || attributes[field].to_s.strip.empty? end @@ -329,15 +351,11 @@ def validate_user_id!(user_id) raise Errors::ValidationError, 'id cannot be blank if provided' end - def validate_company!(company) + def validate_company!(company, user_type = nil) return unless company.is_a?(Hash) - # Required company fields - required_company_fields = %i[name legal_name tax_number business_email country] - - missing_fields = required_company_fields.select do |field| - company[field].nil? || company[field].to_s.strip.empty? - end + required_fields = required_company_fields(user_type) + missing_fields = find_missing_company_fields(company, required_fields) return if missing_fields.empty? @@ -345,6 +363,26 @@ def validate_company!(company) "Company is missing required fields: #{missing_fields.join(', ')}" end + def required_company_fields(user_type) + base_fields = %i[name legal_name tax_number business_email] + additional_fields = payout_company?(user_type) ? payout_company_fields : %i[country] + base_fields + additional_fields + end + + def payout_company?(user_type) + user_type&.to_s&.downcase == USER_TYPE_PAYOUT + end + + def payout_company_fields + %i[address_line1 city state zip phone country] + end + + def find_missing_company_fields(company, required_fields) + required_fields.select do |field| + company[field].nil? || company[field].to_s.strip.empty? + end + end + def build_user_body(attributes) # rubocop:disable Metrics/CyclomaticComplexity body = {} diff --git a/lib/zai_payment/response.rb b/lib/zai_payment/response.rb index 7ff228d..0bdd650 100644 --- a/lib/zai_payment/response.rb +++ b/lib/zai_payment/response.rb @@ -30,9 +30,14 @@ def server_error? end # Get the data from the response body + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity def data - body.is_a?(Hash) ? body['webhooks'] || body['users'] || body : body + return body unless body.is_a?(Hash) + + body['webhooks'] || body['users'] || body['items'] || body['fees'] || + body['transactions'] || body['batch_transactions'] || body end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity # Get pagination or metadata info def meta diff --git a/lib/zai_payment/version.rb b/lib/zai_payment/version.rb index ae7e706..e7722e2 100644 --- a/lib/zai_payment/version.rb +++ b/lib/zai_payment/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module ZaiPayment - VERSION = '1.3.2' + VERSION = '2.0.0' end diff --git a/README.md b/readme.md similarity index 77% rename from README.md rename to readme.md index e92f194..0d1d5d2 100644 --- a/README.md +++ b/readme.md @@ -1,7 +1,7 @@ # Zai Payment Ruby Library ![GitHub License](https://img.shields.io/github/license/Sentia/zai-payment) -[![Code of Conduct](https://img.shields.io/badge/code%20of%20conduct-MIT-blue.svg)](./CODE_OF_CONDUCT.md) +[![Code of Conduct](https://img.shields.io/badge/code%20of%20conduct-MIT-blue.svg)](./code_of_conduct.md) [![Gem Version](https://img.shields.io/gem/v/zai_payment.svg)](https://rubygems.org/gems/zai_payment) [![GitHub release](https://img.shields.io/github/release/Sentia/zai-payment.svg)](https://github.com/Sentia/zai-payment/releases) [![Gem](https://img.shields.io/gem/dt/zai_payment.svg)](https://rubygems.org/gems/zai_payment) @@ -9,7 +9,7 @@ ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FSentia%2Fzai-payment%2Fmain%2Fbadges%2Fcoverage.json) ![GitHub top language](https://img.shields.io/github/languages/top/Sentia/zai-payment) [![Documentation](https://img.shields.io/badge/docs-rubydoc.info-blue.svg)](https://rubydoc.info/gems/zai_payment?refresh=true) -[![Contributing](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](./CONTRIBUTING.md) +[![Contributing](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](./contributing.md) A lightweight and extensible Ruby client for the **Zai (AssemblyPay)** API โ€” starting with secure OAuth2 authentication, and ready for Payments, Virtual Accounts, Webhooks, and more. @@ -20,6 +20,7 @@ A lightweight and extensible Ruby client for the **Zai (AssemblyPay)** API โ€” s - ๐Ÿ” **OAuth2 Authentication** - Client Credentials flow with automatic token management - ๐Ÿง  **Smart Token Caching** - Auto-refresh before expiration, thread-safe storage - ๐Ÿ‘ฅ **User Management** - Create and manage payin (buyers) & payout (sellers) users +- ๐Ÿ“ฆ **Item Management** - Full CRUD for transactions/payments between buyers and sellers - ๐Ÿช **Webhooks** - Full CRUD + secure signature verification (HMAC SHA256) - โš™๏ธ **Environment-Aware** - Seamless Pre-live / Production switching - ๐Ÿงฑ **Modular & Extensible** - Clean resource-based architecture @@ -77,7 +78,7 @@ token = token_provider.bearer_token The gem handles OAuth2 Client Credentials flow automatically - tokens are cached and refreshed before expiration. -๐Ÿ“– **Complete Authentication Guide** - Two approaches, examples, and best practices +๐Ÿ“– **Complete Authentication Guide** - Two approaches, examples, and best practices ### Users @@ -99,7 +100,7 @@ response = ZaiPayment.users.create( first_name: 'Jane', last_name: 'Smith', country: 'AUS', - dob: '19900101', + dob: '01/01/1990', address_line1: '456 Market St', city: 'Sydney', state: 'NSW', @@ -135,11 +136,53 @@ response = ZaiPayment.users.update('user_id', mobile: '+9876543210') ``` **๐Ÿ“š Documentation:** -- ๐Ÿ“– [User Management Guide](docs/USERS.md) - Complete guide for payin and payout users +- ๐Ÿ“– [User Management Guide](docs/users.md) - Complete guide for payin and payout users - ๐Ÿ’ก [User Examples](examples/users.md) - Real-world usage patterns and Rails integration - ๐Ÿ”— [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) +### Items + +Manage transactions/payments between buyers and sellers: + +```ruby +# Create an item +response = ZaiPayment.items.create( + name: "Product Purchase", + amount: 10000, # Amount in cents ($100.00) + payment_type: 2, # Credit card + buyer_id: "buyer-123", + seller_id: "seller-456", + description: "Purchase of premium product", + currency: "AUD", + tax_invoice: true +) + +# List items +response = ZaiPayment.items.list(limit: 20, offset: 0) + +# Get item details +response = ZaiPayment.items.show('item_id') + +# Update item +response = ZaiPayment.items.update('item_id', name: 'Updated Name') + +# Get item status +response = ZaiPayment.items.show_status('item_id') + +# Get buyer/seller details +response = ZaiPayment.items.show_buyer('item_id') +response = ZaiPayment.items.show_seller('item_id') + +# List transactions +response = ZaiPayment.items.list_transactions('item_id') +``` + +**๐Ÿ“š Documentation:** +- ๐Ÿ“– [Item Management Guide](docs/items.md) - Complete guide for creating and managing items +- ๐Ÿ’ก [Item Examples](examples/items.md) - Real-world usage patterns and complete workflows +- ๐Ÿ”— [Zai: Items API Reference](https://developer.hellozai.com/reference/listitems) + ### Webhooks Manage webhook endpoints: @@ -163,9 +206,9 @@ ZaiPayment.webhooks.create_secret_key(secret_key: secret_key) **๐Ÿ“š Documentation:** - ๐Ÿ“– [Webhook Examples & Complete Guide](examples/webhooks.md) - Full CRUD operations and patterns -- ๐Ÿ”’ [Security Quick Start](docs/WEBHOOK_SECURITY_QUICKSTART.md) - 5-minute webhook security setup -- ๐Ÿ—๏ธ [Architecture & Implementation](docs/WEBHOOKS.md) - Detailed technical documentation -- ๐Ÿ” [Signature Verification Details](docs/WEBHOOK_SIGNATURE.md) - Security implementation specs +- ๐Ÿ”’ [Security Quick Start](docs/webhook_security_quickstart.md) - 5-minute webhook security setup +- ๐Ÿ—๏ธ [Architecture & Implementation](docs/webhooks.md) - Detailed technical documentation +- ๐Ÿ” [Signature Verification Details](docs/webhook_signature.md) - Security implementation specs ### Error Handling @@ -199,6 +242,7 @@ end | โœ… Authentication | OAuth2 Client Credentials flow | Done | | โœ… Webhooks | CRUD for webhook endpoints | Done | | โœ… Users | Manage PayIn / PayOut users | Done | +| โœ… Items | Transactions/payments (CRUD) | Done | | ๐Ÿ’ณ Payments | Single and recurring payments | ๐Ÿšง In progress | | ๐Ÿฆ Virtual Accounts (VA / PIPU) | Manage virtual accounts & PayTo | โณ Planned | | ๐Ÿ’ผ Wallets | Create and manage wallet accounts | โณ Planned | @@ -236,12 +280,12 @@ This will load the gem and all its dependencies, allowing you to experiment with ## ๐Ÿงพ Versioning This gem follows [Semantic Versioning](https://semver.org). -See [CHANGELOG.md](./CHANGELOG.md) for release history. +See [changelog.md](./changelog.md) for release history. ## ๐Ÿค Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/Sentia/zai-payment. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/Sentia/zai-payment/blob/main/CODE_OF_CONDUCT.md). +Bug reports and pull requests are welcome on GitHub at https://github.com/Sentia/zai-payment. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/Sentia/zai-payment/blob/main/code_of_conduct.md). ## ๐Ÿชช License @@ -249,27 +293,29 @@ The gem is available as open source under the terms of the [MIT License](https:/ ## Code of Conduct -Everyone interacting in the ZaiPayment project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/Sentia/zai-payment/blob/main/CODE_OF_CONDUCT.md). +Everyone interacting in the ZaiPayment project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/Sentia/zai-payment/blob/main/code_of_conduct.md). ## ๐Ÿ“š Documentation ### Getting Started -- [**Authentication Guide**](docs/AUTHENTICATION.md) - Two approaches to getting tokens, automatic management -- [**User Management Guide**](docs/USERS.md) - Managing payin and payout users +- [**Authentication Guide**](docs/authentication.md) - Two approaches to getting tokens, automatic management +- [**User Management Guide**](docs/users.md) - Managing payin and payout users +- [**Item Management Guide**](docs/items.md) - Creating and managing transactions/payments - [**Webhook Examples**](examples/webhooks.md) - Complete webhook usage guide -- [**Documentation Index**](docs/README.md) - Full documentation navigation +- [**Documentation Index**](docs/readme.md) - Full documentation navigation ### Examples & Patterns - [User Examples](examples/users.md) - Real-world user management patterns +- [Item Examples](examples/items.md) - Transaction and payment workflows - [Webhook Examples](examples/webhooks.md) - Webhook integration patterns ### Technical Guides -- [Webhook Architecture](docs/WEBHOOKS.md) - Technical implementation details -- [Architecture Overview](docs/ARCHITECTURE.md) - System architecture and design +- [Webhook Architecture](docs/webhooks.md) - Technical implementation details +- [Architecture Overview](docs/architecture.md) - System architecture and design ### Security -- [Webhook Security Quick Start](docs/WEBHOOK_SECURITY_QUICKSTART.md) - 5-minute setup guide -- [Signature Verification](docs/WEBHOOK_SIGNATURE.md) - Implementation details +- [Webhook Security Quick Start](docs/webhook_security_quickstart.md) - 5-minute setup guide +- [Signature Verification](docs/webhook_signature.md) - Implementation details ### External Resources - [Zai Developer Portal](https://developer.hellozai.com/) diff --git a/spec/zai_payment/resources/item_spec.rb b/spec/zai_payment/resources/item_spec.rb new file mode 100644 index 0000000..4e1d16a --- /dev/null +++ b/spec/zai_payment/resources/item_spec.rb @@ -0,0 +1,670 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ZaiPayment::Resources::Item do + let(:stubs) { Faraday::Adapter::Test::Stubs.new } + let(:item_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) + + 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 '#list' do + context 'when successful' do + before do + stubs.get('/items') do |env| + [200, { 'Content-Type' => 'application/json' }, item_list_data] if env.params['limit'] == '10' + end + end + + let(:item_list_data) do + { + 'items' => [ + { + 'id' => 'item_1', + 'name' => 'Product A', + 'amount' => 10_000, + 'payment_type' => 2, + 'buyer_id' => 'buyer_123', + 'seller_id' => 'seller_456' + }, + { + 'id' => 'item_2', + 'name' => 'Product B', + 'amount' => 20_000, + 'payment_type' => 2, + 'buyer_id' => 'buyer_789', + 'seller_id' => 'seller_012' + } + ], + 'meta' => { + 'total' => 2, + 'limit' => 10, + 'offset' => 0 + } + } + end + + it 'returns the correct response type' do + response = item_resource.list + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + + it 'returns the item data' do + response = item_resource.list + expect(response.data).to eq(item_list_data['items']) + end + + it 'returns the metadata' do + response = item_resource.list + expect(response.meta).to eq(item_list_data['meta']) + end + end + + context 'with custom pagination' do + before do + stubs.get('/items') do |env| + [200, { 'Content-Type' => 'application/json' }, item_list_data] if env.params['limit'] == '20' + end + end + + let(:item_list_data) do + { + 'items' => [], + 'meta' => { 'total' => 0, 'limit' => 20, 'offset' => 10 } + } + end + + it 'accepts custom limit and offset' do + response = item_resource.list(limit: 20, offset: 10) + expect(response.success?).to be true + end + end + + context 'with search parameter' do + before do + stubs.get('/items') do |env| + [200, { 'Content-Type' => 'application/json' }, search_results] if env.params['search'] == 'product' + end + end + + let(:search_results) do + { + 'items' => [ + { + 'id' => 'item_1', + 'name' => 'Product A', + 'description' => 'Premium product', + 'amount' => 10_000 + } + ], + 'meta' => { 'total' => 1, 'limit' => 10, 'offset' => 0 } + } + end + + it 'accepts search parameter' do + response = item_resource.list(search: 'product') + expect(response.success?).to be true + expect(response.data.length).to eq(1) + end + end + + context 'with date filters' do + before do + stubs.get('/items') do |env| + if env.params['created_after'] && env.params['created_before'] + [200, { 'Content-Type' => 'application/json' }, filtered_results] + end + end + end + + let(:filtered_results) do + { + 'items' => [ + { + 'id' => 'item_1', + 'name' => 'Recent Item', + 'amount' => 10_000, + 'created_at' => '2024-06-15T10:00:00Z' + } + ], + 'meta' => { 'total' => 1, 'limit' => 10, 'offset' => 0 } + } + end + + it 'accepts created_after and created_before parameters' do + response = item_resource.list( + created_after: '2024-01-01T00:00:00Z', + created_before: '2024-12-31T23:59:59Z' + ) + expect(response.success?).to be true + expect(response.data.length).to eq(1) + end + end + end + + describe '#show' do + context 'when item exists' do + before do + stubs.get('/items/item_123') do + [200, { 'Content-Type' => 'application/json' }, item_detail] + end + end + + let(:item_detail) do + { + 'items' => { + 'id' => 'item_123', + 'name' => 'Test Product', + 'amount' => 15_000, + 'payment_type' => 2, + 'buyer_id' => 'buyer_123', + 'seller_id' => 'seller_456', + 'description' => 'Test description' + } + } + end + + it 'returns the correct response type' do + response = item_resource.show('item_123') + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + + it 'returns the item details' do + response = item_resource.show('item_123') + expect(response.data['id']).to eq('item_123') + expect(response.data['name']).to eq('Test Product') + end + end + + context 'when item does not exist' do + before do + stubs.get('/items/item_123') do + [404, { 'Content-Type' => 'application/json' }, { 'error' => 'Item not found' }] + end + end + + it 'raises a NotFoundError' do + expect { item_resource.show('item_123') }.to raise_error(ZaiPayment::Errors::NotFoundError) + end + end + + context 'when item_id is blank' do + it 'raises a ValidationError for empty string' do + expect { item_resource.show('') }.to raise_error(ZaiPayment::Errors::ValidationError, /item_id/) + end + + it 'raises a ValidationError for nil' do + expect { item_resource.show(nil) }.to raise_error(ZaiPayment::Errors::ValidationError, /item_id/) + end + end + end + + describe '#create' do + let(:base_params) do + { + name: 'Test Product', + amount: 10_000, + payment_type: 2, + buyer_id: 'buyer_123', + seller_id: 'seller_456' + } + end + + context 'when creating an item successfully' do + let(:created_response) do + { + 'items' => base_params.transform_keys(&:to_s).merge('id' => 'item_new') + } + end + + before do + stubs.post('/items') do |env| + body = JSON.parse(env.body) + [201, { 'Content-Type' => 'application/json' }, created_response] if body['name'] == 'Test Product' + end + end + + it 'returns the correct response type' do + response = item_resource.create(**base_params) + expect(response).to be_a(ZaiPayment::Response) + end + + it 'returns the created item with correct data' do + response = item_resource.create(**base_params) + expect(response.data['id']).to eq('item_new') + expect(response.data['name']).to eq(base_params[:name]) + expect(response.data['amount']).to eq(base_params[:amount]) + end + end + + context 'when required fields are missing' do + it 'raises a ValidationError for missing name' do + params = base_params.except(:name) + expect { item_resource.create(**params) }.to raise_error( + ZaiPayment::Errors::ValidationError, /Missing required fields:.*name/ + ) + end + + it 'raises a ValidationError for missing amount' do + params = base_params.except(:amount) + expect { item_resource.create(**params) }.to raise_error( + ZaiPayment::Errors::ValidationError, /Missing required fields:.*amount/ + ) + end + + it 'raises a ValidationError for missing payment_type' do + params = base_params.except(:payment_type) + expect { item_resource.create(**params) }.to raise_error( + ZaiPayment::Errors::ValidationError, /Missing required fields:.*payment_type/ + ) + end + + it 'raises a ValidationError for missing buyer_id' do + params = base_params.except(:buyer_id) + expect { item_resource.create(**params) }.to raise_error( + ZaiPayment::Errors::ValidationError, /Missing required fields:.*buyer_id/ + ) + end + + it 'raises a ValidationError for missing seller_id' do + params = base_params.except(:seller_id) + expect { item_resource.create(**params) }.to raise_error( + ZaiPayment::Errors::ValidationError, /Missing required fields:.*seller_id/ + ) + end + end + + context 'when amount is invalid' do + it 'raises a ValidationError for negative amount' do + params = base_params.merge(amount: -100) + expect { item_resource.create(**params) } + .to raise_error(ZaiPayment::Errors::ValidationError, /amount must be a positive integer/) + end + + it 'raises a ValidationError for zero amount' do + params = base_params.merge(amount: 0) + expect { item_resource.create(**params) } + .to raise_error(ZaiPayment::Errors::ValidationError, /amount must be a positive integer/) + end + + it 'raises a ValidationError for non-integer amount' do + params = base_params.merge(amount: 'not_a_number') + expect { item_resource.create(**params) } + .to raise_error(ZaiPayment::Errors::ValidationError, /amount must be a positive integer/) + end + end + + context 'when payment_type is invalid' do + it 'raises a ValidationError for invalid payment_type' do + params = base_params.merge(payment_type: 10) + expect { item_resource.create(**params) } + .to raise_error(ZaiPayment::Errors::ValidationError, /payment_type must be between 1 and 7/) + end + end + end + + describe '#update' do + context 'when successful' do + before do + stubs.patch('/items/item_123') do |env| + body = JSON.parse(env.body) + [200, { 'Content-Type' => 'application/json' }, updated_response] if body['name'] == 'Updated Product' + end + end + + let(:updated_response) do + { + 'items' => { + 'id' => 'item_123', + 'name' => 'Updated Product', + 'amount' => 12_000, + 'description' => 'Updated description' + } + } + end + + it 'returns the correct response type' do + response = item_resource.update('item_123', name: 'Updated Product', amount: 12_000) + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + + it 'returns the updated item data' do + response = item_resource.update('item_123', name: 'Updated Product', amount: 12_000) + expect(response.data['name']).to eq('Updated Product') + expect(response.data['amount']).to eq(12_000) + end + end + + context 'when item_id is blank' do + it 'raises a ValidationError' do + expect do + item_resource.update('', name: 'Updated Product') + end.to raise_error(ZaiPayment::Errors::ValidationError, /item_id/) + end + end + + context 'when no update parameters provided' do + it 'raises a ValidationError' do + expect do + item_resource.update('item_123') + end.to raise_error(ZaiPayment::Errors::ValidationError, /At least one attribute/) + end + end + end + + describe '#delete' do + context 'when successful' do + before do + stubs.delete('/items/item_123') do + [204, { 'Content-Type' => 'application/json' }, {}] + end + end + + it 'returns the correct response type' do + response = item_resource.delete('item_123') + expect(response).to be_a(ZaiPayment::Response) + end + end + + context 'when item_id is blank' do + it 'raises a ValidationError' do + expect { item_resource.delete('') }.to raise_error(ZaiPayment::Errors::ValidationError, /item_id/) + end + end + end + + describe '#show_seller' do + context 'when successful' do + before do + stubs.get('/items/item_123/sellers') do + [200, { 'Content-Type' => 'application/json' }, seller_data] + end + end + + let(:seller_data) do + { + 'users' => { + 'id' => 'seller_456', + 'email' => 'seller@example.com', + 'first_name' => 'Jane', + 'last_name' => 'Smith' + } + } + end + + it 'returns the correct response type' do + response = item_resource.show_seller('item_123') + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + + it 'returns the seller details' do + response = item_resource.show_seller('item_123') + expect(response.data['id']).to eq('seller_456') + end + end + + context 'when item_id is blank' do + it 'raises a ValidationError' do + expect { item_resource.show_seller('') }.to raise_error(ZaiPayment::Errors::ValidationError, /item_id/) + end + end + end + + describe '#show_buyer' do + context 'when successful' do + before do + stubs.get('/items/item_123/buyers') do + [200, { 'Content-Type' => 'application/json' }, buyer_data] + end + end + + let(:buyer_data) do + { + 'users' => { + 'id' => 'buyer_123', + 'email' => 'buyer@example.com', + 'first_name' => 'John', + 'last_name' => 'Doe' + } + } + end + + it 'returns the correct response type' do + response = item_resource.show_buyer('item_123') + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + + it 'returns the buyer details' do + response = item_resource.show_buyer('item_123') + expect(response.data['id']).to eq('buyer_123') + end + end + + context 'when item_id is blank' do + it 'raises a ValidationError' do + expect { item_resource.show_buyer('') }.to raise_error(ZaiPayment::Errors::ValidationError, /item_id/) + end + end + end + + describe '#show_fees' do + context 'when successful' do + before do + stubs.get('/items/item_123/fees') do + [200, { 'Content-Type' => 'application/json' }, fees_data] + end + end + + let(:fees_data) do + { + 'fees' => [ + { 'id' => 'fee_1', 'amount' => 500, 'name' => 'Processing Fee' }, + { 'id' => 'fee_2', 'amount' => 300, 'name' => 'Service Fee' } + ] + } + end + + it 'returns the correct response type' do + response = item_resource.show_fees('item_123') + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + + it 'returns the fees details' do + response = item_resource.show_fees('item_123') + expect(response.data.length).to eq(2) + end + end + + context 'when item_id is blank' do + it 'raises a ValidationError' do + expect { item_resource.show_fees('') }.to raise_error(ZaiPayment::Errors::ValidationError, /item_id/) + end + end + end + + describe '#show_wire_details' do + context 'when successful' do + before do + stubs.get('/items/item_123/wire_details') do + [200, { 'Content-Type' => 'application/json' }, wire_data] + end + end + + let(:wire_data) do + { + 'items' => { + 'wire_details' => { + 'account_number' => '12345678', + 'routing_number' => '987654321', + 'bank_name' => 'Test Bank' + } + } + } + end + + it 'returns the correct response type' do + response = item_resource.show_wire_details('item_123') + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + + it 'returns the wire details' do + response = item_resource.show_wire_details('item_123') + expect(response.data['wire_details']).to be_a(Hash) + end + end + + context 'when item_id is blank' do + it 'raises a ValidationError' do + expect { item_resource.show_wire_details('') }.to raise_error(ZaiPayment::Errors::ValidationError, /item_id/) + end + end + end + + describe '#list_transactions' do + context 'when successful' do + before do + stubs.get('/items/item_123/transactions') do |env| + [200, { 'Content-Type' => 'application/json' }, transactions_data] if env.params['limit'] == '10' + end + end + + let(:transactions_data) do + { + 'transactions' => [ + { 'id' => 'txn_1', 'amount' => 10_000, 'state' => 'completed' }, + { 'id' => 'txn_2', 'amount' => 5000, 'state' => 'pending' } + ] + } + end + + it 'returns the correct response type' do + response = item_resource.list_transactions('item_123') + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + + it 'returns the transactions data' do + response = item_resource.list_transactions('item_123') + expect(response.data.length).to eq(2) + end + end + + context 'when item_id is blank' do + it 'raises a ValidationError' do + expect do + item_resource.list_transactions('') + end.to raise_error(ZaiPayment::Errors::ValidationError, /item_id/) + end + end + end + + describe '#list_batch_transactions' do + context 'when successful' do + before do + stubs.get('/items/item_123/batch_transactions') do |env| + [200, { 'Content-Type' => 'application/json' }, batch_data] if env.params['limit'] == '10' + end + end + + let(:batch_data) do + { + 'batch_transactions' => [ + { 'id' => 'batch_1', 'amount' => 100_000, 'state' => 'completed' } + ] + } + end + + it 'returns the correct response type' do + response = item_resource.list_batch_transactions('item_123') + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + + it 'returns the batch transactions data' do + response = item_resource.list_batch_transactions('item_123') + expect(response.data.length).to eq(1) + end + end + + context 'when item_id is blank' do + it 'raises a ValidationError' do + expect do + item_resource.list_batch_transactions('') + end.to raise_error(ZaiPayment::Errors::ValidationError, /item_id/) + end + end + end + + describe '#show_status' do + context 'when successful' do + before do + stubs.get('/items/item_123/status') do + [200, { 'Content-Type' => 'application/json' }, status_data] + end + end + + let(:status_data) do + { + 'items' => { + 'id' => 'item_123', + 'state' => 'completed', + 'payment_state' => 'paid' + } + } + end + + it 'returns the correct response type' do + response = item_resource.show_status('item_123') + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + + it 'returns the status details' do + response = item_resource.show_status('item_123') + expect(response.data['state']).to eq('completed') + end + end + + context 'when item_id is blank' do + it 'raises a ValidationError' do + expect { item_resource.show_status('') }.to raise_error(ZaiPayment::Errors::ValidationError, /item_id/) + end + end + end + + describe 'integration with ZaiPayment module' do + it 'is accessible through ZaiPayment.items' do + expect(ZaiPayment.items).to be_a(described_class) + end + end +end diff --git a/spec/zai_payment/resources/user_spec.rb b/spec/zai_payment/resources/user_spec.rb index 4a21142..ac6264d 100644 --- a/spec/zai_payment/resources/user_spec.rb +++ b/spec/zai_payment/resources/user_spec.rb @@ -384,6 +384,43 @@ end end + context 'when user_type is not present' do + before do + stubs.post('/users') do |env| + body = JSON.parse(env.body) + # API will handle default user_type (likely 'payin') + [201, { 'Content-Type' => 'application/json' }, created_response] if body['user_type'].nil? + end + end + + let(:created_response) do + base_params.transform_keys(&:to_s).merge( + 'id' => 'user-123', + 'user_type' => 'payin' # API default + ) + end + + it 'allows user creation without user_type' do + response = user_resource.create(**base_params) + expect(response).to be_a(ZaiPayment::Response) + expect(response.success?).to be true + end + + it 'only validates base required fields' do + # Should NOT require payout fields like address_line1, city, state, zip, dob + params = base_params # Only has: email, first_name, last_name, country + expect { user_resource.create(**params) }.not_to raise_error + end + + it 'does not call validate_user_type! when user_type is nil' do + # This validates that the conditional check works + params = base_params + allow(user_resource).to receive(:validate_user_type!).and_call_original + user_resource.create(**params) + expect(user_resource).not_to have_received(:validate_user_type!) + end + end + context 'when custom id is provided' do before do stubs.post('/users') do |env| @@ -542,8 +579,11 @@ end it 'accepts payout user type' do - params = base_params.merge(user_type: 'payout') - expect { user_resource.create(**params) }.not_to raise_error + payout_params = base_params.merge( + user_type: 'payout', address_line1: '123 Main St', city: 'Sydney', + state: 'NSW', zip: '2000', dob: '01/01/1990' + ) + expect { user_resource.create(**payout_params) }.not_to raise_error end it 'accepts uppercase user type' do