# Wallet Utils Usage Guide

This package provides wallet point transaction utilities, ported from the PHP `WalletPoint` class.

## Installation

From your Django backend folder:

```bash
pip install -e ../packages/wallet_utils
```

## Quick Start

### 1. Add to Django INSTALLED_APPS

Add `wallet_utils` to your `INSTALLED_APPS` in `settings.py`:

```python
INSTALLED_APPS = [
    # ... other apps
    "wallet_utils",
]
```

### 2. Run Migrations

The package includes its own migrations for the `WalletTransaction` model:

```bash
python manage.py migrate wallet_utils
```

### 3. Define Point Types and Use the Service

The repository only needs to define point types and their decimal places:

```python
from decimal import Decimal
from wallet_utils import WalletService, WalletRepository
from wallet_utils.exceptions import InsufficientBalanceError
from your_app.models import User

# Define point types and their decimal places
point_types = {
    "credit_balance": 2,  # 2 decimal places
    "reward_points": 0,   # No decimal places (integer)
    "crypto_balance": 8,  # 8 decimal places
}

# Option 1: Use User model for balances (default)
repo = WalletRepository(
    user_model=User,
    point_types=point_types,
)

# Option 2: Use separate Wallet model for balances
from your_app.models import Wallet

repo = WalletRepository(
    user_model=User,
    wallet_balance_model=Wallet,  # Separate model with user_id field
    point_types=point_types,
)

# Use the service
service = WalletService(repo)

# Add points (system-initiated, uses default iid=-100)
from wallet_utils.transaction_types import WALLET_DEPOSIT

try:
    transaction_id = service.add_point(
        user_id=123,
        point_type="credit_balance",
        amount=Decimal("100.00"),
        remarks="Deposit from payment",
        trans_type=WALLET_DEPOSIT,  # Use transaction type constant
    )
    print(f"Transaction created: {transaction_id}")
except Exception as e:
    print(f"Error: {e}")

# Add points (user-initiated)
from wallet_utils.transaction_types import REFERRAL_BONUS

try:
    transaction_id = service.add_point(
        user_id=123,
        point_type="credit_balance",
        amount=Decimal("50.00"),
        remarks="Reward from referral",
        trans_type=REFERRAL_BONUS,  # Use transaction type constant
        iid=456,  # User 456 initiated this transaction
    )
    print(f"Transaction created: {transaction_id}")
except Exception as e:
    print(f"Error: {e}")

# Deduct points (uses atomic SQL with WHERE clause)
from wallet_utils.transaction_types import WALLET_WITHDRAW

try:
    transaction_id = service.deduct_point(
        user_id=123,
        point_type="credit_balance",
        amount=Decimal("50.00"),
        remarks="Withdrawal request",
        trans_type=WALLET_WITHDRAW,  # Use transaction type constant
        allow_negative=False,
        iid=123,  # User 123 initiated this withdrawal
    )
    print(f"Transaction created: {transaction_id}")
except InsufficientBalanceError as e:
    print(f"Insufficient balance: Available {e.available}, Requested {e.requested}")
except Exception as e:
    print(f"Error: {e}")
```

## Custom Field Names

If your User model field names differ from point type names, use `point_type_field_map`:

```python
repo = WalletRepository(
    user_model=User,
    point_types={
        "credit_balance": 2,
        "reward_points": 0,
    },
    point_type_field_map={
        "credit_balance": "credit_balance",  # Default: same as point_type
        "reward_points": "points",           # Custom: field is "points" not "reward_points"
    },
)
```

## Separate Wallet Balance Model

By default, wallet balances are stored directly on the User model. You can use a separate model for storing balances:

```python
from wallet_utils import WalletRepository
from your_app.models import User, Wallet

# Your Wallet model should have:
# - user_id field (or specify custom field name)
# - Fields for each point type (e.g., credit_balance, reward_points)

class Wallet(models.Model):
    user_id = models.BigIntegerField(unique=True, db_index=True)
    credit_balance = models.DecimalField(max_digits=20, decimal_places=2, default=0)
    reward_points = models.DecimalField(max_digits=20, decimal_places=0, default=0)
    # ... other point type fields

# Use separate wallet model
repo = WalletRepository(
    user_model=User,
    wallet_balance_model=Wallet,
    point_types={
        "credit_balance": 2,
        "reward_points": 0,
    },
    wallet_user_id_field="user_id",  # Optional: defaults to "user_id"
)

# If your wallet model uses a different field name for user reference:
repo = WalletRepository(
    user_model=User,
    wallet_balance_model=Wallet,
    point_types={"credit_balance": 2},
    wallet_user_id_field="owner_id",  # Custom field name
)
```

## Custom Wallet Transaction Model

You can use your own model for storing transactions:

```python
from wallet_utils import WalletRepository
from wallet_utils.models import AbstractWalletTransaction, WalletTransaction
from your_app.models import User

# Option 1: Use default WalletTransaction model
repo = WalletRepository(
    user_model=User,
    point_types={"credit_balance": 2},
)

# Option 2: Extend AbstractWalletTransaction for custom table (only one table created)
class CustomTransaction(AbstractWalletTransaction):
    """Your custom transaction model."""
    class Meta(AbstractWalletTransaction.Meta):
        db_table = "custom_transactions"

repo = WalletRepository(
    user_model=User,
    point_types={"credit_balance": 2},
    wallet_model=CustomTransaction,
)

# Option 3: Extend WalletTransaction for same table with custom fields
class CustomTransactionSameTable(WalletTransaction):
    """Custom transaction model using same table."""
    amount = models.DecimalField(max_digits=20, decimal_places=8)
    balance = models.DecimalField(max_digits=20, decimal_places=8)
    
    class Meta:
        db_table = "wallet_transactions"  # Same table as parent

repo = WalletRepository(
    user_model=User,
    point_types={"credit_balance": 8},
    wallet_model=CustomTransactionSameTable,
)
```

**Important**: 
- Use `AbstractWalletTransaction` if you want a custom table name and only one table created
- Use `WalletTransaction` if you want to use the default table or customize fields on the same table
- See "Customizing Decimal Places" section for more details

## Features

### Multiple Point Types

The service supports multiple wallet/point types. Define them in `point_types` dictionary mapping point type names to their decimal places.

### Transaction Recording

All operations automatically create transaction records in the `WalletTransaction` model. Transactions are always saved to the database.

### Transaction Types

Transaction types are represented as integer constants. Import them from `wallet_utils.transaction_types`:

```python
from wallet_utils.transaction_types import (
    WALLET_DEPOSIT,
    WALLET_WITHDRAW,
    PACKAGE_ACTIVATION,
    COMMISSION_DISTRIBUTION,
    ROI_DISTRIBUTION,
    REFERRAL_BONUS,
    # ... and many more
)

# Use in transactions
service.add_point(
    user_id=123,
    point_type="credit_balance",
    amount=Decimal("100.00"),
    remarks="Deposit",
    trans_type=WALLET_DEPOSIT,  # Integer constant: 1000
)
```

**Available Transaction Type Categories:**

- **Wallet Operations (1000-1999)**: `WALLET_DEPOSIT`, `WALLET_WITHDRAW`, `WALLET_TRANSFER`, etc.
- **Package Operations (2000-2999)**: `PACKAGE_ACTIVATION`, `PACKAGE_UPGRADE`, `PACKAGE_RENEWAL`, etc.
- **Commission & Rewards (3000-3999)**: `COMMISSION_DISTRIBUTION`, `ROI_DISTRIBUTION`, `REFERRAL_BONUS`, etc.
- **Product Operations (4000-4999)**: `PRODUCT_ORDER`, `PRODUCT_REDEMPTION`, etc.
- **System Operations (5000-5999)**: `SYSTEM_ADJUSTMENT`, `SYSTEM_REWARD`, etc.
- **Payment Operations (6000-6999)**: `PAYMENT_RECEIVED`, `PAYMENT_REFUND`, etc.
- **Loan & Credit (7000-7999)**: `LOAN_DISBURSEMENT`, `LOAN_REPAYMENT`, etc.
- **Exchange & Conversion (8000-8999)**: `CURRENCY_EXCHANGE`, `POINT_CONVERSION`, etc.
- **Fee Operations (9000-9999)**: `TRANSACTION_FEE`, `WITHDRAWAL_FEE`, etc.

You can also use helper functions:

```python
from wallet_utils.transaction_types import get_transaction_type, get_transaction_type_name

# Get integer constant from string name
trans_type = get_transaction_type("wallet-deposit")  # Returns 1000

# Get string name from integer constant
name = get_transaction_type_name(1000)  # Returns "wallet-deposit"
```

**Note**: `trans_type` is optional and can be `None` if you don't need to categorize the transaction.

### Extra Data

Pass additional fields via `params["data"]`:

```python
from wallet_utils.transaction_types import ROI_DISTRIBUTION

service.add_point(
    user_id=123,
    point_type="credit_balance",
    amount=Decimal("100.00"),
    remarks="Reward",
    trans_type=ROI_DISTRIBUTION,  # Use transaction type constant
    iid=-100,  # System-initiated
    params={
        "data": {
            "investment_id": 456,
            "reward_type": "daily",
        }
    },
)
```

### Initiator ID (iid)

The `iid` parameter indicates which user performed the transaction:
- Use a positive user ID for user-initiated transactions (e.g., `iid=123`)
- Use `-100` for system-initiated transactions (this is the default)
- The `iid` is stored in the transaction record for audit purposes

### Decimal Precision

Decimal precision is automatically determined from the point type definition. For example, if you define `"crypto_balance": 8`, all operations on that point type will use 8 decimal places.

### Atomic Deduction with Race Condition Prevention

The `deduct_point()` method uses atomic SQL operations with a WHERE clause to prevent race conditions:

```sql
UPDATE users 
SET credit_balance = credit_balance - 50.00
WHERE id = 123 AND credit_balance >= 50.00
```

The method checks if any rows were affected (`rowCount > 0`) to confirm the deduction was successful. This ensures:
- No race conditions (database-level locking)
- Accurate balance checking (no need to check balance separately)
- Atomic operations (all-or-nothing)

## Transaction History Retrieval

The service provides methods to retrieve transaction history with various filters:

```python
from datetime import datetime, timedelta

# Get all transactions for a user
transactions = service.get_transaction_history(user_id=123)

# Get credit transactions for a specific point type
transactions = service.get_transaction_history(
    user_id=123,
    point_type="credit_balance",
    transaction_type="c",  # 'c' for credit, 'd' for debit
)

# Get transactions in date range with pagination
end = datetime.now()
start = end - timedelta(days=30)
transactions = service.get_transaction_history(
    user_id=123,
    start_date=start,
    end_date=end,
    limit=100,
    offset=0,
)

# Get transactions by transaction type code
from wallet_utils.transaction_types import WALLET_DEPOSIT

transactions = service.get_transaction_history(
    user_id=123,
    trans_type=WALLET_DEPOSIT,  # Filter by transaction type integer constant
)

# Get transactions initiated by a specific user
transactions = service.get_transaction_history(
    iid=456,  # User 456 initiated these transactions
)

# Get a single transaction by ID
transaction = service.get_transaction_by_id(transaction_id=789)
if transaction:
    print(f"Amount: {transaction.amount}, Balance: {transaction.balance}")

# Count transactions matching filters
from wallet_utils.transaction_types import WALLET_DEPOSIT

count = service.count_transactions(
    user_id=123,
    point_type="credit_balance",
    transaction_type="c",
    trans_type=WALLET_DEPOSIT,  # Filter by transaction type
    start_date=start,
    end_date=end,
)
print(f"Found {count} transactions")
```

### Available Filters

- `user_id`: Filter by user ID (maps to `uid` field)
- `point_type`: Filter by wallet/point type (maps to `wtype` field)
- `trans_type`: Filter by transaction type code (maps to `trans_type` field) - integer constant (e.g., `WALLET_DEPOSIT`)
- `transaction_type`: Filter by transaction type ('c' for credit, 'd' for debit) (maps to `type` field)
- `iid`: Filter by initiator ID (maps to `iid` field) - user who performed the transaction (-100 for system)
- `start_date`: Filter transactions from this date (inclusive) - datetime or ISO string (maps to `cdate` field)
- `end_date`: Filter transactions until this date (inclusive) - datetime or ISO string (maps to `cdate` field)
- `limit`: Maximum number of records to return
- `offset`: Number of records to skip (for pagination)

Transactions are returned ordered by creation date (newest first).

## Error Handling

The package uses custom exceptions:

- `WalletOperationError`: Base exception for wallet operations
- `InsufficientBalanceError`: Insufficient balance (includes available/requested amounts)
- `InvalidPointTypeError`: Point type doesn't exist
- `InvalidParamsError`: Invalid parameters

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

try:
    service.deduct_point(...)
except InsufficientBalanceError as e:
    # Handle insufficient balance
    print(f"Available: {e.available}, Requested: {e.requested}")
except InvalidPointTypeError as e:
    # Handle invalid point type
    print(f"Invalid point type: {e.point_type}")
except InvalidParamsError as e:
    # Handle invalid parameters
    print(f"Invalid params in {e.method}")
```

## Custom Repository Implementation

If you need custom behavior, you can implement your own repository:

```python
from wallet_utils import WalletRepository, WalletService
from your_app.models import User

class CustomRepository(WalletRepository):
    """Custom repository with additional logic."""
    
    def get_point_types(self) -> dict[str, int]:
        # Load from database, config, etc.
        return {
            "credit_balance": 2,
            "reward_points": 0,
        }
    
    def get_user_model(self):
        return User

# Use custom repository
repo = CustomRepository(user_model=User, point_types={...})
service = WalletService(repo)
```

## Database Schema

### WalletTransaction Models

The package includes two models:

1. **`AbstractWalletTransaction`**: Abstract base model (does not create a table). Extend this if you want a custom table name and only one table created.
2. **`WalletTransaction`**: Concrete model that extends `AbstractWalletTransaction` and creates the `wallet_transactions` table.

Both models have the same fields. The `WalletTransaction` model has the following fields:

#### Fields

- **`id`** (AutoField): Primary key, auto-incrementing integer
- **`wtype`** (CharField, max_length=50, indexed): Wallet/point type (e.g., "credit_balance", "reward_points")
- **`iid`** (BigIntegerField, indexed): Initiator ID - user who performed this transaction (-100 for system-initiated transactions)
- **`uid`** (BigIntegerField, indexed): User ID - owner of the wallet
  - **Note**: This field does NOT use a forced foreign key constraint. This allows flexibility for special cases like system user IDs (e.g., -100) that may not exist in the User table
- **`type`** (CharField, max_length=1, choices): Transaction type
  - `"c"` = Credit/Add
  - `"d"` = Debit/Deduct
- **`amount`** (DecimalField, max_digits=20, decimal_places=2): Transaction amount (default: 2 decimal places)
- **`balance`** (DecimalField, max_digits=20, decimal_places=2): Balance after this transaction (default: 2 decimal places)
- **`trans_type`** (IntegerField, indexed, null=True, blank=True): Transaction type code (integer constant, e.g., 1000 for wallet-deposit)
- **`descr`** (TextField, blank=True, default=""): Remarks/description
- **`cdate`** (DateTimeField, auto_now_add=True, indexed): Creation date/time
- **`extra_data`** (JSONField, default=dict, blank=True): Additional fields stored as JSON

#### Indexes

The model includes the following database indexes for optimal query performance:

1. **Single field indexes** (on fields with `db_index=True`):
   - `wtype` - For filtering by wallet/point type
   - `iid` - For filtering by initiator ID
   - `uid` - For filtering by user ID
   - `trans_type` - For filtering by transaction type code
   - `cdate` - For date range queries and ordering

2. **Composite indexes** (defined in `Meta.indexes`):
   - `(uid, wtype)` - For querying user's transactions by point type
   - `(uid, cdate)` - For querying user's transactions by date
   - `(wtype, trans_type)` - For querying transactions by point type and transaction code
   - `(iid, cdate)` - For querying transactions by initiator and date

#### Model Configuration

- **Table name**: `wallet_transactions` (can be customized via `db_table` in Meta)
- **Default ordering**: `-cdate` (newest first)
- **No foreign key constraints**: The `uid` field does not enforce a foreign key relationship, allowing flexibility for system users and special cases

#### Example Model Definitions

```python
from wallet_utils.models import AbstractWalletTransaction, WalletTransaction

# Abstract base model (does not create a table)
class AbstractWalletTransaction(models.Model):
    # ... all fields defined here ...
    class Meta:
        abstract = True
        # ... indexes and ordering ...

# Concrete model (creates wallet_transactions table)
class WalletTransaction(AbstractWalletTransaction):
    class Meta(AbstractWalletTransaction.Meta):
        db_table = "wallet_transactions"

# Custom model extending abstract (creates only custom_wallet_trans table)
class CustomWalletTransaction(AbstractWalletTransaction):
    amount = models.DecimalField(max_digits=20, decimal_places=8)
    balance = models.DecimalField(max_digits=20, decimal_places=8)
    
    class Meta(AbstractWalletTransaction.Meta):
        db_table = "custom_wallet_trans"  # Only this table will be created
```

#### Customizing Decimal Places and Table Names

The `WalletTransaction` model uses **2 decimal places** as the default for `amount` and `balance` fields. If you need different decimal precision or a custom table name, you have two options:

##### Option 1: Extend AbstractWalletTransaction (Recommended for Custom Tables)

To maintain **only ONE table** when using a custom table name, extend `AbstractWalletTransaction` instead of `WalletTransaction`:

```python
from wallet_utils.models import AbstractWalletTransaction
from django.db import models

class CustomWalletTransaction(AbstractWalletTransaction):
    """Custom transaction model with different decimal places and table name."""
    
    amount = models.DecimalField(max_digits=20, decimal_places=8)  # 8 decimal places
    balance = models.DecimalField(max_digits=20, decimal_places=8)  # 8 decimal places
    
    class Meta(AbstractWalletTransaction.Meta):
        db_table = "custom_wallet_trans"  # Only this table will be created

# Use your custom model
repo = WalletRepository(
    user_model=User,
    point_types={"credit_balance": 8},  # Match decimal places
    wallet_model=CustomWalletTransaction,  # Use custom model
)
```

**Important: Single Table with AbstractWalletTransaction**

When you extend `AbstractWalletTransaction` with a custom `db_table`:
- **Only ONE table will be created** - your custom table (e.g., `custom_wallet_trans`)
- The default `wallet_transactions` table will **NOT** be created (since `AbstractWalletTransaction` is abstract)
- You must create and run migrations for your custom model in your app
- You can still add `wallet_utils` to `INSTALLED_APPS` for the package functionality, but you should **skip running `wallet_utils` migrations** if you don't want the default table

**To skip wallet_utils migrations:**
```python
# In your settings.py
MIGRATION_MODULES = {
    'wallet_utils': None,  # Skip migrations for wallet_utils
}
```

Or simply don't run `python manage.py migrate wallet_utils` - only run migrations for your app.

##### Option 2: Extend WalletTransaction (For Same Table)

If you want to use the same `wallet_transactions` table but customize fields:

```python
from wallet_utils.models import WalletTransaction
from django.db import models

class CustomWalletTransaction(WalletTransaction):
    """Custom transaction model with different decimal places."""
    
    amount = models.DecimalField(max_digits=20, decimal_places=8)  # 8 decimal places
    balance = models.DecimalField(max_digits=20, decimal_places=8)  # 8 decimal places
    
    class Meta:
        db_table = "wallet_transactions"  # Use same table as parent
        # Copy indexes from parent if needed
        indexes = [
            models.Index(fields=["uid", "wtype"]),
            models.Index(fields=["uid", "cdate"]),
            models.Index(fields=["wtype", "trans_type"]),
            models.Index(fields=["iid", "cdate"]),
        ]
        ordering = ["-cdate"]

# Use your custom model
repo = WalletRepository(
    user_model=User,
    point_types={"credit_balance": 8},  # Match decimal places
    wallet_model=CustomWalletTransaction,  # Use custom model
)
```

**Note**: When extending `WalletTransaction` (concrete model):
- If `db_table = "wallet_transactions"` (same as parent): Both models use the same table
- If `db_table = "custom_wallet_trans"` (different name): **TWO tables will be created** - both `wallet_transactions` (from `wallet_utils` migrations) and `custom_wallet_trans` (from your migrations)
- If you omit `db_table`: Multi-table inheritance is created (not recommended)

**Recommendation**: 
- **For custom table names**: Use `AbstractWalletTransaction` (Option 1) to ensure only one table is created
- **For same table with custom fields**: Use `WalletTransaction` with `db_table = "wallet_transactions"` (Option 2)
- **Never omit `db_table`** unless you intentionally want separate tables (not recommended)

**Note**: Make sure to:
1. Create and run migrations for your custom model
2. Set `wallet_model=CustomWalletTransaction` when initializing `WalletRepository`
3. Match the `decimal_places` in your `point_types` definition with the model field's `decimal_places`
4. Copy all indexes from the parent model if you want the same query performance

##### Option 2: Modify After Installation (Not Recommended)

If you installed the package via `pip install`, you can modify the model directly in the installed package location:

```bash
# Find the installed package location
python -c "import wallet_utils; print(wallet_utils.__file__)"

# Edit the models.py file in that location
# Change decimal_places from 2 to your desired value
```

**Warning**: This approach is **not recommended** because:
- Changes will be lost when you reinstall or update the package
- Makes your codebase harder to maintain
- Can cause issues in team environments
- Requires manual migration updates

**Recommendation**: Always use Option 1 (extending the model) for production code. It's cleaner, maintainable, and follows Django best practices.

## Transaction Type Constants

The package includes a comprehensive set of transaction type constants organized by category:

### Wallet Operations (1000-1999)
- `WALLET_DEPOSIT` (1000)
- `WALLET_DEPOSIT_CANCEL` (1001)
- `WALLET_TRANSFER` (1002)
- `WALLET_WITHDRAW` (1003)
- `WALLET_WITHDRAW_REFUND` (1004)
- `WALLET_ADJUST` (1005)
- `WALLET_TOPUP` (1006)
- `WALLET_DEDUCT` (1007)

### Package Operations (2000-2999)
- `PACKAGE_ACTIVATION` (2000)
- `PACKAGE_ACTIVATION_CANCEL` (2001)
- `PACKAGE_UPGRADE` (2002)
- `PACKAGE_UPGRADE_CANCEL` (2003)
- `PACKAGE_RENEWAL` (2004)
- `PACKAGE_RENEWAL_CANCEL` (2005)
- `PACKAGE_EXPIRY_REFUND` (2006)

### Commission & Rewards (3000-3999)
- `COMMISSION_DISTRIBUTION` (3000)
- `COMMISSION_DISTRIBUTION_REVERSE` (3001)
- `ROI_DISTRIBUTION` (3100)
- `ROI_DISTRIBUTION_REVERSE` (3101)
- `REFERRAL_BONUS` (3200)
- `REFERRAL_BONUS_REVERSE` (3201)
- `MATCHING_BONUS` (3300)
- `MATCHING_BONUS_REVERSE` (3301)
- `LEADERSHIP_BONUS` (3400)
- `LEADERSHIP_BONUS_REVERSE` (3401)
- `PROMOTION_BONUS` (3500)
- `PROMOTION_BONUS_REVERSE` (3501)

### Product Operations (4000-4999)
- `PRODUCT_ORDER` (4000)
- `PRODUCT_ORDER_CANCEL` (4001)
- `PRODUCT_ORDER_REFUND` (4002)
- `PRODUCT_REDEMPTION` (4003)
- `PRODUCT_REDEMPTION_CANCEL` (4004)
- `PRODUCT_EXCHANGE` (4005)
- `PRODUCT_EXCHANGE_REVERSE` (4006)

### System Operations (5000-5999)
- `SYSTEM_ADJUSTMENT` (5000)
- `SYSTEM_REWARD` (5001)
- `SYSTEM_PENALTY` (5002)
- `SYSTEM_REVERSAL` (5003)
- `SYSTEM_MIGRATION` (5004)

### Payment Operations (6000-6999)
- `PAYMENT_RECEIVED` (6000)
- `PAYMENT_REFUND` (6001)
- `PAYMENT_FAILED_REFUND` (6002)
- `PAYMENT_CHARGEBACK` (6003)
- `PAYMENT_CHARGEBACK_REVERSAL` (6004)

### Loan & Credit Operations (7000-7999)
- `LOAN_DISBURSEMENT` (7000)
- `LOAN_REPAYMENT` (7001)
- `LOAN_WRITE_OFF` (7002)
- `CREDIT_LIMIT_INCREASE` (7003)
- `CREDIT_LIMIT_DECREASE` (7004)

### Exchange & Conversion (8000-8999)
- `CURRENCY_EXCHANGE` (8000)
- `POINT_CONVERSION` (8001)
- `POINT_CONVERSION_REVERSE` (8002)
- `BONUS_CONVERSION` (8003)
- `BONUS_CONVERSION_REVERSE` (8004)

### Fee Operations (9000-9999)
- `TRANSACTION_FEE` (9000)
- `TRANSACTION_FEE_REFUND` (9001)
- `WITHDRAWAL_FEE` (9002)
- `WITHDRAWAL_FEE_REFUND` (9003)
- `TRANSFER_FEE` (9004)
- `TRANSFER_FEE_REFUND` (9005)
- `MAINTENANCE_FEE` (9006)
- `MAINTENANCE_FEE_REFUND` (9007)

**Usage:**
```python
from wallet_utils import WALLET_DEPOSIT, ROI_DISTRIBUTION, WALLET_WITHDRAW

# Or import from transaction_types module
from wallet_utils.transaction_types import WALLET_DEPOSIT

service.add_point(
    user_id=123,
    point_type="credit_balance",
    amount=Decimal("100.00"),
    remarks="Deposit",
    trans_type=WALLET_DEPOSIT,  # Use constant instead of string
)
```

## Differences from PHP Version

1. **Error Handling**: Uses Python exceptions instead of error codes
2. **Type Safety**: Uses Decimal for financial amounts
3. **Simplified Repository**: Only needs to define point types and decimal places
4. **Built-in Models**: Includes Django models and migrations
5. **Atomic Deduction**: Uses SQL WHERE clause for atomic deduction with race condition prevention
6. **Always Save Records**: Transactions are always saved (no `save_record` option)
7. **Simplified**: Removed PHP-specific features (class codes, translation functions)
8. **Transaction Types**: Uses integer constants instead of string codes for better performance and type safety

## Testing and Development

### Test Suite Location

The comprehensive test suite for this package is located in the `sandbox/` directory at the project root level (same level as `packages/`). This ensures:

- ✅ **Tests are kept with the package** for easy access during development
- ✅ **Tests are NOT included** when installing the package via pip
- ✅ **Clean package distribution** without test files

### Running Tests

To run the test suite:

```bash
# Navigate to sandbox directory
cd sandbox

# Install dependencies
pip install -r requirements.txt

# Install wallet_utils in editable mode
pip install -e ../packages/wallet_utils

# Run migrations
python manage.py migrate

# Run all tests
python manage.py test test_app.tests
```

See `sandbox/README.md` for detailed test documentation and coverage information.

### Package Distribution

When the package is installed via `pip install`, only the following are included:
- Package source code (`src/wallet_utils/`)
- Documentation (`docs/`)
- Migrations (`src/wallet_utils/migrations/`)
- Package metadata (`pyproject.toml`)

Test files in `sandbox/` are **excluded** from the package distribution, ensuring a clean installation for end users.
