# Transfer Feature Design

## Overview

The transfer feature enables users to transfer points/wallet balance from one user to another using atomic database transactions. This ensures data consistency and prevents race conditions.

## Problem Statement

When transferring points between users, two separate operations must occur:
1. Deduct points from the sender's balance
2. Add points to the recipient's balance

Without atomicity, the following issues can occur:
- Both operations succeed but one fails (partial transfer)
- Race conditions if multiple transfers happen simultaneously
- Inconsistent balances across users

## Solution Architecture

### Atomic Transfer Using Database Transactions

The transfer feature will use Django's `transaction.atomic()` decorator to ensure both operations complete together or rollback completely.

```
START TRANSACTION
├─ Deduct from sender (atomic SQL with balance check)
├─ Add to recipient (atomic SQL)
└─ CREATE transaction records for both (sender and recipient)
COMMIT or ROLLBACK
```

## API Design

### Method Signature

```python
def transfer_point(
    from_user_id: int,
    to_user_id: int,
    from_point_type: str,
    to_point_type: str,
    amount: Decimal,
    remarks: str,
    trans_type: int | None = None,
    params: Optional[Dict[str, Any]] = None,
    iid: int = -100,
) -> Dict[str, int]:
    """
    Transfer points from one user to another atomically.
    Supports transferring between different point types (e.g., cash points to credit points).
    
    Args:
        from_user_id: User ID of the sender
        to_user_id: User ID of the recipient (can be same as from_user_id)
        from_point_type: Type of wallet/points being deducted (e.g., "cash_balance")
        to_point_type: Type of wallet/points being added (e.g., "credit_balance")
        amount: Amount to transfer (must be positive)
        remarks: Description/remarks for the transaction
        trans_type: Transaction type code (optional, default: WALLET_TRANSFER)
        params: Extra parameters dict with optional 'data' key
        iid: Initiator ID - user who initiated the transfer (-100 for system)
    
    Returns:
        Dict with keys:
        - 'from_transaction_id': Transaction ID for the deduction
        - 'to_transaction_id': Transaction ID for the addition
    
    Raises:
        InvalidPointTypeError: If point type doesn't exist
        InvalidParamsError: If parameters are invalid
        InsufficientBalanceError: If sender has insufficient balance
        UserNotFoundError: If recipient user doesn't exist
        WalletOperationError: If the transfer fails
    """
```

### Response Structure

```python
{
    'from_transaction_id': 12345,  # Deduction transaction
    'to_transaction_id': 12346,    # Addition transaction
}
```

## Implementation Details

### Database-Level Atomicity

1. **Atomic Deduction**: Use SQL WHERE clause to prevent race conditions
   ```sql
   UPDATE user_wallets 
   SET balance = balance - amount
   WHERE user_id = ? AND point_type = ? AND balance >= amount
   ```

2. **Atomic Addition**: Standard update (can never fail balance check)
   ```sql
   UPDATE user_wallets
   SET balance = balance + amount
   WHERE user_id = ? AND point_type = ?
   ```

3. **Transaction Wrapping**: Both operations wrapped in `transaction.atomic()`

### Transaction Records

Both deduction and addition will create separate transaction records:

**Sender's Record (Deduction)**:
- `uid`: sender's user ID
- `type`: 'd' (debit)
- `trans_type`: WALLET_TRANSFER (1002)
- `wtype`: from_point_type (the point type being deducted)
- `extra_data`: Includes recipient user ID and transfer metadata

**Recipient's Record (Addition)**:
- `uid`: recipient's user ID
- `type`: 'c' (credit)
- `trans_type`: WALLET_TRANSFER (1002)
- `wtype`: to_point_type (the point type being added)
- `extra_data`: Includes sender user ID and transfer metadata

### Linking Transfers

Optional: Store reference in `extra_data` to link related transactions:

```python
from_extra = {'transfer_recipient': to_user_id, 'transfer_ref': 'ABC123'}
to_extra = {'transfer_sender': from_user_id, 'transfer_ref': 'ABC123'}
```

## Design Decisions

1. **Circular Transfers**: No prevention enforced
   - Allow circular transfers (A→B→C→A) as they're valid scenarios

2. **Transfer Limits**: No global limits enforced
   - Applications can implement their own limits at the business logic layer

3. **Recipient Validation**: Yes, validate recipient exists
   - Raise `UserNotFoundError` if recipient user doesn't exist

4. **Transfer Reversal**: No reversal support
   - Transfers are final. Create new transfer in opposite direction if needed

5. **Fee Handling**: No fees
   - Transfers have no built-in fee mechanism
   - Applications can implement fees separately using additional transactions

6. **Notification**: Yes, via Django signals
   - Trigger `wallet_transfer_completed` signal after successful transfer
   - Developers register signal handlers in their app

7. **Self-Transfers**: Allow same user transfers
   - Valid for auditing and point type conversions
   - Both records created for audit trail

8. **Decimal Precision**: Not a concern
   - Point type defines precision globally; both users automatically use same precision

9. **Cross-Type Transfers**: Support transferring between different point types
   - User A can transfer 100 cash points → User B receives 100 credit points
   - Enables flexible point operations (e.g., conversion, exchange)

## Validation & Exceptions

### Required Validations

1. **Amount Validation**
   - Must be positive (> 0)
   - Must not exceed max_digits=20, decimal_places=2

2. **Point Type Validation**
   - Both `from_point_type` and `to_point_type` must exist
   - Raise `InvalidPointTypeError` if invalid

3. **Recipient Validation**
   - Recipient user must exist in repository
   - Raise `UserNotFoundError` if doesn't exist

4. **Balance Validation**
   - Sender must have sufficient balance in `from_point_type`
   - Raise `InsufficientBalanceError` if insufficient

5. **Parameters Validation**
   - `params` dict must be valid
   - Raise `InvalidParamsError` if invalid

### Exception Hierarchy

```python
# New exception for transfer feature
class UserNotFoundError(WalletOperationError):
    """Raised when recipient user is not found."""
    def __init__(self, user_id: int):
        self.user_id = user_id
        super().__init__(f"User {user_id} not found")
```

## Edge Cases to Handle

1. **Same User Transfer**: `from_user_id == to_user_id`
   - Allow for point type conversion (e.g., cash → credit)
   - Both records created for audit trail

2. **Cross-Type Transfer**: `from_point_type != to_point_type`
   - Example: Deduct cash points, add credit points
   - Useful for conversions or exchanges
   - Point type definitions handle their own decimal precision

3. **Zero Amount**: `amount == 0`
   - Reject with validation error

4. **Negative Amount**: `amount < 0`
   - Reject with validation error

5. **Non-existent Users**: Either user doesn't exist
   - Raise `UserNotFoundError` for recipient
   - Sender's balance check will fail if doesn't exist

6. **Concurrent Transfers**: Multiple transfers to/from same users
   - Atomic operations ensure data consistency
   - Slowest transfer might fail if balance depleted
   - Expected behavior, not an error

7. **Large Amounts**: Transfer > 99999999.99
   - Validate against `max_digits=20, decimal_places=2`
   - Should work fine with current schema

## Implementation Steps

1. Add `UserNotFoundError` exception to `WalletOperationError`
2. Create `wallet_transfer_completed` signal in signals module
3. Add `transfer_point()` method to `WalletService`
4. Add `recipient_exists()` validation method to `WalletRepositoryProtocol`
5. Add comprehensive validation logic for:
   - Amount validation
   - Point type validation
   - Recipient validation
   - Balance validation
6. Wrap in `transaction.atomic()` from `django.db import transaction`
7. Emit signal after successful transfer
8. Add comprehensive tests:
   - Normal transfer (same point type)
   - Cross-type transfer (different point types)
   - Self-transfer
   - Insufficient balance
   - Invalid point type
   - Invalid users/recipients
   - Signal emission
   - Rollback on failure
9. Update documentation with examples

## Example Usage

### Basic Transfer

```python
from decimal import Decimal
from wallet_utils import WalletService, WALLET_TRANSFER

service = WalletService(repository)

result = service.transfer_point(
    from_user_id=100,
    to_user_id=200,
    from_point_type='cash_balance',
    to_point_type='cash_balance',
    amount=Decimal('50.00'),
    remarks='Peer-to-peer transfer',
    trans_type=WALLET_TRANSFER,
)

print(f"Deduction transaction: {result['from_transaction_id']}")
print(f"Addition transaction: {result['to_transaction_id']}")
```

### Transfer Between Different Point Types

```python
# User A transfers 100 cash points to User B as credit points
result = service.transfer_point(
    from_user_id=100,
    to_user_id=200,
    from_point_type='cash_balance',
    to_point_type='credit_balance',
    amount=Decimal('100.00'),
    remarks='Convert cash to credit points',
    trans_type=WALLET_TRANSFER,
)
```

### Point Type Conversion (Self-Transfer)

```python
# User converts their own cash points to credit points
result = service.transfer_point(
    from_user_id=100,
    to_user_id=100,  # Same user
    from_point_type='cash_balance',
    to_point_type='credit_balance',
    amount=Decimal('50.00'),
    remarks='Self conversion: cash to credit',
)
```

### Error Handling with Notifications

```python
from wallet_utils.exceptions import (
    InsufficientBalanceError,
    InvalidPointTypeError,
    UserNotFoundError,
)

try:
    result = service.transfer_point(
        from_user_id=100,
        to_user_id=200,
        from_point_type='cash_balance',
        to_point_type='credit_balance',
        amount=Decimal('50.00'),
        remarks='Transfer',
    )
except InsufficientBalanceError as e:
    print(f"Insufficient balance: {e.available} available")
except InvalidPointTypeError as e:
    print(f"Invalid point type: {e.point_type}")
except UserNotFoundError as e:
    print(f"User not found: {e.user_id}")
```

### Listening to Transfer Signals

```python
# myapp/handlers.py
from django.dispatch import receiver
from wallet_utils.signals import wallet_transfer_completed

@receiver(wallet_transfer_completed)
def notify_recipient(sender, **kwargs):
    """Send notification to recipient."""
    from django.core.mail import send_mail
    
    to_user_id = kwargs['to_user_id']
    amount = kwargs['amount']
    to_user = User.objects.get(id=to_user_id)
    
    send_mail(
        subject='You received points',
        message=f'You received {amount} points',
        from_email='noreply@example.com',
        recipient_list=[to_user.email],
    )
```

## Testing Strategy

### Unit Tests

1. **Happy Path**
   - Transfer with sufficient balance
   - Verify both records created
   - Verify balances updated correctly

2. **Insufficient Balance**
   - Transfer exceeds sender's balance
   - Verify `InsufficientBalanceError` raised
   - Verify no records created
   - Verify balances unchanged

3. **Invalid Inputs**
   - Invalid point type
   - Negative/zero amount
   - Invalid user IDs

4. **Atomicity**
   - Mock repository failure on recipient add
   - Verify rollback (deduction not recorded)
   - Verify balances unchanged

5. **Edge Cases**
   - Self-transfer
   - Large amounts
   - Decimal precision

### Integration Tests

- Multi-user concurrent transfers
- Verify final balances correct
- Transaction record ordering

## Signals & Notifications

The transfer feature triggers a **Django signal** after successful transfer, allowing applications to implement notifications or additional processing without coupling to the wallet service.

### Signal Definition

```python
# src/wallet_utils/signals.py
from django.dispatch import Signal

wallet_transfer_completed = Signal()
```

### Signal Parameters

```python
{
    'from_user_id': int,          # Sender user ID
    'to_user_id': int,            # Recipient user ID
    'from_point_type': str,       # Deducted point type
    'to_point_type': str,         # Added point type
    'amount': Decimal,            # Transfer amount
    'from_transaction_id': int,   # Deduction transaction ID
    'to_transaction_id': int,     # Addition transaction ID
    'remarks': str,               # Transfer remarks
    'trans_type': int,            # Transaction type code
    'iid': int,                   # Initiator ID
    'timestamp': datetime,        # Transfer completion time
}
```

### Registering Signal Handlers

Developers register signal handlers in their Django app's `apps.py`:

```python
# myapp/apps.py
from django.apps import AppConfig

class MyAppConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'myapp'

    def ready(self):
        from wallet_utils.signals import wallet_transfer_completed
        from .handlers import on_transfer_completed
        
        wallet_transfer_completed.connect(on_transfer_completed)
```

### Example Signal Handler

```python
# myapp/handlers.py
from django.dispatch import receiver
from wallet_utils.signals import wallet_transfer_completed

@receiver(wallet_transfer_completed)
def on_transfer_completed(sender, **kwargs):
    """Handle wallet transfer completion."""
    from_user_id = kwargs['from_user_id']
    to_user_id = kwargs['to_user_id']
    amount = kwargs['amount']
    
    # Example: Send notification to recipient
    send_notification(
        user_id=to_user_id,
        message=f"Received {amount} points from user {from_user_id}"
    )
    
    # Example: Log transfer to audit system
    AuditLog.objects.create(
        event_type='wallet_transfer',
        from_user=from_user_id,
        to_user=to_user_id,
        amount=amount,
        transaction_ids=[
            kwargs['from_transaction_id'],
            kwargs['to_transaction_id']
        ]
    )
    
    # Example: Trigger additional business logic
    process_loyalty_rewards(to_user_id, amount)
```

### Multiple Handlers

Multiple handlers can listen to the same signal:

```python
# myapp/apps.py
def ready(self):
    from wallet_utils.signals import wallet_transfer_completed
    
    # Register multiple handlers
    wallet_transfer_completed.connect(send_notification)
    wallet_transfer_completed.connect(log_audit_trail)
    wallet_transfer_completed.connect(update_user_tier)
```

## Backward Compatibility

- No breaking changes to existing API
- New method is addition, not modification
- Existing transaction records unchanged
- All existing types remain valid

## Performance Considerations

- **Atomicity**: Uses single transaction wrapper for both operations
- **Database Queries**: 4 total (2 updates + 2 transaction creates)
- **Indexes**: Existing indexes on `uid`, `wtype`, `cdate` sufficient
- **Lock Duration**: Minimal (atomic updates are fast)

## Related Discussion Points

1. Should we support batch transfers (multiple recipients in single transaction)?
2. Should we add transfer scheduling or delayed transfers?
3. Should we add transfer policies (e.g., whitelist recipients)?
4. Should we track transfer chains/lineage across multiple transfers?

---

**Document Version**: 2.0  
**Status**: Ready for Implementation  
**Last Updated**: 2025-12-21
**Changes**: Added cross-type transfers, signals for notifications, recipient validation, self-transfers enabled
