Commit Β·
20ffa57
1
Parent(s): de24997
feat: Implement Gift Card Template API with CRUD operations
Browse files- Added gift_card_router.py for handling API routes related to gift card templates.
- Created gift_card_schema.py to define Pydantic models for gift card templates, including validation rules.
- Developed gift_card_service.py to encapsulate business logic for creating, retrieving, updating, and deleting gift card templates.
- Introduced test_gift_card_implementation.py to validate schema functionality and ensure proper imports.
- Implemented error handling and logging throughout the service and router for better traceability.
- GIFT_CARD_IMPLEMENTATION.md +190 -0
- app/app.py +7 -0
- app/models/gift_card_models.py +271 -0
- app/repositories/gift_card_repository.py +267 -0
- app/routers/gift_card_router.py +376 -0
- app/schemas/gift_card_schema.py +224 -0
- app/services/gift_card_service.py +406 -0
- test_gift_card_implementation.py +193 -0
GIFT_CARD_IMPLEMENTATION.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# π― Gift Card Template API Implementation
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
A complete end-to-end implementation of Gift Card Template management for the EMS (Entity Management System) microservice. This implementation provides full CRUD operations for gift card templates with comprehensive validation and business logic.
|
| 5 |
+
|
| 6 |
+
## π Files Created
|
| 7 |
+
|
| 8 |
+
### 1. Models Layer
|
| 9 |
+
- **`app/models/gift_card_models.py`**
|
| 10 |
+
- `GiftCardTemplateModel` class with MongoDB operations
|
| 11 |
+
- Full CRUD operations (Create, Read, Update, Delete)
|
| 12 |
+
- Stock management for physical cards
|
| 13 |
+
- Automatic timestamp handling
|
| 14 |
+
|
| 15 |
+
### 2. Schemas Layer
|
| 16 |
+
- **`app/schemas/gift_card_schema.py`**
|
| 17 |
+
- Pydantic models for request/response validation
|
| 18 |
+
- `GiftCardTemplateCreate` - Creation schema
|
| 19 |
+
- `GiftCardTemplateUpdate` - Update schema
|
| 20 |
+
- `GiftCardTemplateResponse` - Response schema
|
| 21 |
+
- `GiftCardTemplateFilter` - Filtering schema
|
| 22 |
+
- Comprehensive validation rules
|
| 23 |
+
|
| 24 |
+
### 3. Repository Layer
|
| 25 |
+
- **`app/repositories/gift_card_repository.py`**
|
| 26 |
+
- `GiftCardRepository` class
|
| 27 |
+
- Clean interface between services and models
|
| 28 |
+
- Helper methods for common operations
|
| 29 |
+
|
| 30 |
+
### 4. Service Layer
|
| 31 |
+
- **`app/services/gift_card_service.py`**
|
| 32 |
+
- `GiftCardService` class with business logic
|
| 33 |
+
- Validation of business rules
|
| 34 |
+
- Error handling and logging
|
| 35 |
+
|
| 36 |
+
### 5. Router Layer
|
| 37 |
+
- **`app/routers/gift_card_router.py`**
|
| 38 |
+
- FastAPI router with 7 endpoints
|
| 39 |
+
- Comprehensive API documentation
|
| 40 |
+
- Permission-based access control
|
| 41 |
+
|
| 42 |
+
### 6. App Registration
|
| 43 |
+
- **`app/app.py`** - Updated to include gift card router
|
| 44 |
+
|
| 45 |
+
## π API Endpoints
|
| 46 |
+
|
| 47 |
+
### Core CRUD Operations
|
| 48 |
+
1. **`POST /api/v1/giftcard/`** - Create gift card template
|
| 49 |
+
2. **`GET /api/v1/giftcard/`** - List templates with filtering/pagination
|
| 50 |
+
3. **`GET /api/v1/giftcard/{id}`** - Get specific template
|
| 51 |
+
4. **`PUT /api/v1/giftcard/{id}`** - Update template
|
| 52 |
+
5. **`DELETE /api/v1/giftcard/{id}`** - Delete template
|
| 53 |
+
|
| 54 |
+
### Utility Endpoints
|
| 55 |
+
6. **`GET /api/v1/giftcard/{id}/stock`** - Check stock availability
|
| 56 |
+
7. **`GET /api/v1/giftcard/active/list`** - Get active templates
|
| 57 |
+
|
| 58 |
+
## π Database Schema
|
| 59 |
+
|
| 60 |
+
### Collection: `gift_card_templates`
|
| 61 |
+
|
| 62 |
+
| Field | Type | Required | Description |
|
| 63 |
+
|-------|------|----------|-------------|
|
| 64 |
+
| `_id` | string | β
| Unique template ID (tpl_xxxxxxxx) |
|
| 65 |
+
| `name` | string | β
| Template name |
|
| 66 |
+
| `description` | string | β | Template description |
|
| 67 |
+
| `delivery_type` | enum | β
| `digital` or `physical` |
|
| 68 |
+
| `currency` | string | β
| ISO 4217 code (INR, USD, etc.) |
|
| 69 |
+
| `validity_days` | int/null | β | Days valid from issuance |
|
| 70 |
+
| `allow_custom_amount` | boolean | β
| Allow custom amounts |
|
| 71 |
+
| `predefined_amounts` | number[] | β | Fixed denominations |
|
| 72 |
+
| `reloadable` | boolean | β
| Can be topped up |
|
| 73 |
+
| `allow_partial_redemption` | boolean | β
| Multiple redemptions allowed |
|
| 74 |
+
| `requires_pin` | boolean | β
| PIN protection required |
|
| 75 |
+
| `requires_otp` | boolean | β
| OTP required for redemption |
|
| 76 |
+
| `status` | enum | β
| `active`, `inactive`, `archived` |
|
| 77 |
+
| `max_issues` | int/null | β | Max cards (physical only) |
|
| 78 |
+
| `issued_count` | int | β
| Auto-incremented counter |
|
| 79 |
+
| `design_template` | object | β | Design information |
|
| 80 |
+
| `branch_ids` | string[] | β | Branch restrictions |
|
| 81 |
+
| `campaign_id` | string | β | Campaign linkage |
|
| 82 |
+
| `metadata` | object | β | Additional data |
|
| 83 |
+
| `merchant_id` | string | β
| Merchant identifier |
|
| 84 |
+
| `created_by` | string | β
| Creator user ID |
|
| 85 |
+
| `created_at` | datetime | β
| Creation timestamp |
|
| 86 |
+
| `updated_at` | datetime | β | Update timestamp |
|
| 87 |
+
|
| 88 |
+
## π Security Features
|
| 89 |
+
|
| 90 |
+
### Authentication & Authorization
|
| 91 |
+
- JWT-based authentication via `get_current_user`
|
| 92 |
+
- Permission-based access control
|
| 93 |
+
- Merchant-level data isolation
|
| 94 |
+
|
| 95 |
+
### Data Validation
|
| 96 |
+
- Comprehensive Pydantic validation
|
| 97 |
+
- Business rule enforcement
|
| 98 |
+
- Input sanitization
|
| 99 |
+
|
| 100 |
+
## π― Business Rules Implemented
|
| 101 |
+
|
| 102 |
+
### Template Creation Rules
|
| 103 |
+
1. **Physical cards** must have `max_issues` specified
|
| 104 |
+
2. **Physical cards** cannot be `reloadable`
|
| 105 |
+
3. Either `allow_custom_amount` or `predefined_amounts` required
|
| 106 |
+
4. Template names must be non-empty
|
| 107 |
+
5. Predefined amounts must be positive and unique
|
| 108 |
+
|
| 109 |
+
### Template Update Rules
|
| 110 |
+
1. Cannot change `delivery_type` of existing template
|
| 111 |
+
2. Cannot make physical cards reloadable
|
| 112 |
+
3. Cannot reduce `max_issues` below `issued_count`
|
| 113 |
+
4. Physical cards must always have `max_issues`
|
| 114 |
+
|
| 115 |
+
### Template Deletion Rules
|
| 116 |
+
1. Cannot delete templates with `issued_count > 0`
|
| 117 |
+
2. Recommend deactivation instead of deletion
|
| 118 |
+
|
| 119 |
+
## π Example Usage
|
| 120 |
+
|
| 121 |
+
### Create Digital Template
|
| 122 |
+
```json
|
| 123 |
+
POST /api/v1/giftcard/
|
| 124 |
+
{
|
| 125 |
+
"name": "Birthday Card",
|
| 126 |
+
"description": "Special card for birthdays",
|
| 127 |
+
"delivery_type": "digital",
|
| 128 |
+
"currency": "INR",
|
| 129 |
+
"validity_days": 365,
|
| 130 |
+
"allow_custom_amount": true,
|
| 131 |
+
"predefined_amounts": [500, 1000, 2000],
|
| 132 |
+
"reloadable": false,
|
| 133 |
+
"allow_partial_redemption": true,
|
| 134 |
+
"requires_pin": true,
|
| 135 |
+
"requires_otp": false,
|
| 136 |
+
"status": "active",
|
| 137 |
+
"design_template": {
|
| 138 |
+
"theme": "blue",
|
| 139 |
+
"image_url": "https://..."
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
### List Templates with Filtering
|
| 145 |
+
```
|
| 146 |
+
GET /api/v1/giftcard/?status=active&delivery_type=digital&limit=50
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
### Update Template Status
|
| 150 |
+
```json
|
| 151 |
+
PUT /api/v1/giftcard/tpl_12345678
|
| 152 |
+
{
|
| 153 |
+
"status": "inactive",
|
| 154 |
+
"predefined_amounts": [1000, 2000]
|
| 155 |
+
}
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
## π§ͺ Testing
|
| 159 |
+
|
| 160 |
+
A comprehensive test script (`test_gift_card_implementation.py`) is provided that validates:
|
| 161 |
+
- Schema validation
|
| 162 |
+
- Business rule enforcement
|
| 163 |
+
- Module imports
|
| 164 |
+
- Error handling
|
| 165 |
+
|
| 166 |
+
## π Features Summary
|
| 167 |
+
|
| 168 |
+
β
**Complete CRUD Operations**
|
| 169 |
+
β
**MongoDB Integration**
|
| 170 |
+
β
**Pydantic Validation**
|
| 171 |
+
β
**Business Logic Layer**
|
| 172 |
+
β
**REST API Endpoints**
|
| 173 |
+
β
**Comprehensive Documentation**
|
| 174 |
+
β
**Error Handling**
|
| 175 |
+
β
**Permission System**
|
| 176 |
+
β
**Filtering & Pagination**
|
| 177 |
+
β
**Stock Management**
|
| 178 |
+
β
**Timestamp Tracking**
|
| 179 |
+
|
| 180 |
+
## π Next Steps
|
| 181 |
+
|
| 182 |
+
1. **Database Setup** - Ensure MongoDB connection is configured
|
| 183 |
+
2. **Dependencies** - Install required Python packages (FastAPI, Pydantic, Motor)
|
| 184 |
+
3. **Testing** - Set up unit and integration tests
|
| 185 |
+
4. **Deployment** - Deploy to staging/production environment
|
| 186 |
+
5. **Integration** - Connect with gift card issuance system
|
| 187 |
+
|
| 188 |
+
---
|
| 189 |
+
|
| 190 |
+
*Implementation completed according to the specified API endpoint specs and MongoDB schema requirements.*
|
app/app.py
CHANGED
|
@@ -4,6 +4,7 @@ from app.routers.catalogue_router import router as catalogue_router
|
|
| 4 |
from app.routers.supplier_route import router as supplier_router
|
| 5 |
from app.routers.taxonomy_route import router as taxonomy_router
|
| 6 |
from app.routers.promotion_router import router as promotion_router
|
|
|
|
| 7 |
|
| 8 |
# Initialize FastAPI application
|
| 9 |
app = FastAPI(
|
|
@@ -48,6 +49,12 @@ app.include_router(
|
|
| 48 |
tags=["Taxonomy"]
|
| 49 |
)
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
# Optional root endpoint
|
| 53 |
@app.get("/", tags=["Health"])
|
|
|
|
| 4 |
from app.routers.supplier_route import router as supplier_router
|
| 5 |
from app.routers.taxonomy_route import router as taxonomy_router
|
| 6 |
from app.routers.promotion_router import router as promotion_router
|
| 7 |
+
from app.routers.gift_card_router import router as gift_card_router
|
| 8 |
|
| 9 |
# Initialize FastAPI application
|
| 10 |
app = FastAPI(
|
|
|
|
| 49 |
tags=["Taxonomy"]
|
| 50 |
)
|
| 51 |
|
| 52 |
+
app.include_router(
|
| 53 |
+
gift_card_router,
|
| 54 |
+
prefix="/api/v1/giftcard",
|
| 55 |
+
tags=["Gift Card"]
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
|
| 59 |
# Optional root endpoint
|
| 60 |
@app.get("/", tags=["Health"])
|
app/models/gift_card_models.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import uuid
|
| 3 |
+
from typing import Any, Dict, List, Optional
|
| 4 |
+
from datetime import datetime, timezone
|
| 5 |
+
from bson import ObjectId
|
| 6 |
+
from fastapi import HTTPException
|
| 7 |
+
from app.repositories.db import db
|
| 8 |
+
|
| 9 |
+
# Configure logging for this module
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
# Collection name
|
| 13 |
+
GIFT_CARD_TEMPLATES_COLLECTION = "gift_card_templates"
|
| 14 |
+
|
| 15 |
+
class GiftCardTemplateModel:
|
| 16 |
+
"""
|
| 17 |
+
MongoDB model for Gift Card Templates.
|
| 18 |
+
Handles CRUD operations for gift card templates in MongoDB.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
@staticmethod
|
| 22 |
+
async def create_template(data: Dict[str, Any]) -> str:
|
| 23 |
+
"""
|
| 24 |
+
Create a new gift card template.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
data (Dict[str, Any]): Template data to be inserted
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
str: The inserted template ID
|
| 31 |
+
|
| 32 |
+
Raises:
|
| 33 |
+
RuntimeError: If the creation fails
|
| 34 |
+
"""
|
| 35 |
+
try:
|
| 36 |
+
# Generate unique template ID
|
| 37 |
+
template_id = f"tpl_{str(uuid.uuid4())[:8]}"
|
| 38 |
+
data['_id'] = template_id
|
| 39 |
+
data['issued_count'] = 0 # Initialize issued count
|
| 40 |
+
data['created_at'] = datetime.now(timezone.utc)
|
| 41 |
+
data['updated_at'] = None
|
| 42 |
+
|
| 43 |
+
# Insert data into MongoDB collection
|
| 44 |
+
await db[GIFT_CARD_TEMPLATES_COLLECTION].insert_one(data)
|
| 45 |
+
|
| 46 |
+
# Log success for traceability
|
| 47 |
+
logger.info(f"Gift card template created successfully with ID: {template_id}")
|
| 48 |
+
|
| 49 |
+
return template_id
|
| 50 |
+
except Exception as e:
|
| 51 |
+
# Log the error with stack trace for debugging
|
| 52 |
+
logger.error(f"Error creating gift card template: {e}", exc_info=True)
|
| 53 |
+
|
| 54 |
+
# Raise a more specific error with a custom message
|
| 55 |
+
raise RuntimeError("Failed to create gift card template") from e
|
| 56 |
+
|
| 57 |
+
@staticmethod
|
| 58 |
+
async def get_templates(
|
| 59 |
+
filter_criteria: Dict[str, Any],
|
| 60 |
+
offset: int = 0,
|
| 61 |
+
limit: int = 100
|
| 62 |
+
) -> Dict[str, Any]:
|
| 63 |
+
"""
|
| 64 |
+
Get gift card templates with pagination and filtering.
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
filter_criteria (Dict[str, Any]): MongoDB filter criteria
|
| 68 |
+
offset (int): Number of documents to skip
|
| 69 |
+
limit (int): Maximum number of documents to return
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
Dict[str, Any]: Dictionary containing templates and total count
|
| 73 |
+
"""
|
| 74 |
+
try:
|
| 75 |
+
# Get total count
|
| 76 |
+
total = await db[GIFT_CARD_TEMPLATES_COLLECTION].count_documents(filter_criteria)
|
| 77 |
+
|
| 78 |
+
# Get paginated documents
|
| 79 |
+
cursor = db[GIFT_CARD_TEMPLATES_COLLECTION].find(filter_criteria)
|
| 80 |
+
cursor = cursor.skip(offset).limit(limit).sort("created_at", -1)
|
| 81 |
+
|
| 82 |
+
templates = await cursor.to_list(length=limit)
|
| 83 |
+
|
| 84 |
+
# Convert ObjectId to string for JSON serialization
|
| 85 |
+
for template in templates:
|
| 86 |
+
if isinstance(template.get('_id'), ObjectId):
|
| 87 |
+
template['_id'] = str(template['_id'])
|
| 88 |
+
|
| 89 |
+
logger.info(f"Retrieved {len(templates)} gift card templates")
|
| 90 |
+
|
| 91 |
+
return {
|
| 92 |
+
"templates": templates,
|
| 93 |
+
"total": total,
|
| 94 |
+
"offset": offset,
|
| 95 |
+
"limit": limit
|
| 96 |
+
}
|
| 97 |
+
except Exception as e:
|
| 98 |
+
logger.error(f"Error retrieving gift card templates: {e}", exc_info=True)
|
| 99 |
+
raise RuntimeError("Failed to retrieve gift card templates") from e
|
| 100 |
+
|
| 101 |
+
@staticmethod
|
| 102 |
+
async def get_template_by_id(template_id: str) -> Optional[Dict[str, Any]]:
|
| 103 |
+
"""
|
| 104 |
+
Get a specific gift card template by ID.
|
| 105 |
+
|
| 106 |
+
Args:
|
| 107 |
+
template_id (str): The template ID to search for
|
| 108 |
+
|
| 109 |
+
Returns:
|
| 110 |
+
Optional[Dict[str, Any]]: Template data or None if not found
|
| 111 |
+
"""
|
| 112 |
+
try:
|
| 113 |
+
template = await db[GIFT_CARD_TEMPLATES_COLLECTION].find_one({"_id": template_id})
|
| 114 |
+
|
| 115 |
+
if template:
|
| 116 |
+
# Convert ObjectId to string for JSON serialization
|
| 117 |
+
if isinstance(template.get('_id'), ObjectId):
|
| 118 |
+
template['_id'] = str(template['_id'])
|
| 119 |
+
|
| 120 |
+
logger.info(f"Gift card template {template_id} retrieved successfully")
|
| 121 |
+
else:
|
| 122 |
+
logger.info(f"Gift card template {template_id} not found")
|
| 123 |
+
|
| 124 |
+
return template
|
| 125 |
+
except Exception as e:
|
| 126 |
+
logger.error(f"Error retrieving gift card template {template_id}: {e}", exc_info=True)
|
| 127 |
+
raise RuntimeError("Failed to retrieve gift card template") from e
|
| 128 |
+
|
| 129 |
+
@staticmethod
|
| 130 |
+
async def update_template(template_id: str, update_data: Dict[str, Any]) -> bool:
|
| 131 |
+
"""
|
| 132 |
+
Update a gift card template.
|
| 133 |
+
|
| 134 |
+
Args:
|
| 135 |
+
template_id (str): The template ID to update
|
| 136 |
+
update_data (Dict[str, Any]): Fields to update
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
bool: True if the update was successful, False otherwise
|
| 140 |
+
"""
|
| 141 |
+
try:
|
| 142 |
+
# Check if template exists
|
| 143 |
+
existing_template = await db[GIFT_CARD_TEMPLATES_COLLECTION].find_one({"_id": template_id})
|
| 144 |
+
if not existing_template:
|
| 145 |
+
raise HTTPException(status_code=404, detail="Gift card template not found")
|
| 146 |
+
|
| 147 |
+
# Add updated_at timestamp
|
| 148 |
+
update_data['updated_at'] = datetime.now(timezone.utc)
|
| 149 |
+
|
| 150 |
+
# Perform update
|
| 151 |
+
result = await db[GIFT_CARD_TEMPLATES_COLLECTION].update_one(
|
| 152 |
+
{"_id": template_id},
|
| 153 |
+
{"$set": update_data}
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
success = result.modified_count > 0
|
| 157 |
+
|
| 158 |
+
if success:
|
| 159 |
+
logger.info(f"Gift card template {template_id} updated successfully")
|
| 160 |
+
else:
|
| 161 |
+
logger.warning(f"No changes made to gift card template {template_id}")
|
| 162 |
+
|
| 163 |
+
return success
|
| 164 |
+
except HTTPException:
|
| 165 |
+
raise
|
| 166 |
+
except Exception as e:
|
| 167 |
+
logger.error(f"Error updating gift card template {template_id}: {e}", exc_info=True)
|
| 168 |
+
raise RuntimeError("Failed to update gift card template") from e
|
| 169 |
+
|
| 170 |
+
@staticmethod
|
| 171 |
+
async def delete_template(template_id: str) -> bool:
|
| 172 |
+
"""
|
| 173 |
+
Delete a gift card template.
|
| 174 |
+
|
| 175 |
+
Args:
|
| 176 |
+
template_id (str): The template ID to delete
|
| 177 |
+
|
| 178 |
+
Returns:
|
| 179 |
+
bool: True if the deletion was successful, False otherwise
|
| 180 |
+
"""
|
| 181 |
+
try:
|
| 182 |
+
result = await db[GIFT_CARD_TEMPLATES_COLLECTION].delete_one({"_id": template_id})
|
| 183 |
+
|
| 184 |
+
success = result.deleted_count > 0
|
| 185 |
+
|
| 186 |
+
if success:
|
| 187 |
+
logger.info(f"Gift card template {template_id} deleted successfully")
|
| 188 |
+
else:
|
| 189 |
+
logger.warning(f"Gift card template {template_id} not found for deletion")
|
| 190 |
+
|
| 191 |
+
return success
|
| 192 |
+
except Exception as e:
|
| 193 |
+
logger.error(f"Error deleting gift card template {template_id}: {e}", exc_info=True)
|
| 194 |
+
raise RuntimeError("Failed to delete gift card template") from e
|
| 195 |
+
|
| 196 |
+
@staticmethod
|
| 197 |
+
async def increment_issued_count(template_id: str) -> bool:
|
| 198 |
+
"""
|
| 199 |
+
Increment the issued count for a template when a new gift card is issued.
|
| 200 |
+
|
| 201 |
+
Args:
|
| 202 |
+
template_id (str): The template ID to increment count for
|
| 203 |
+
|
| 204 |
+
Returns:
|
| 205 |
+
bool: True if the increment was successful, False otherwise
|
| 206 |
+
"""
|
| 207 |
+
try:
|
| 208 |
+
result = await db[GIFT_CARD_TEMPLATES_COLLECTION].update_one(
|
| 209 |
+
{"_id": template_id},
|
| 210 |
+
{
|
| 211 |
+
"$inc": {"issued_count": 1},
|
| 212 |
+
"$set": {"updated_at": datetime.now(timezone.utc)}
|
| 213 |
+
}
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
success = result.modified_count > 0
|
| 217 |
+
|
| 218 |
+
if success:
|
| 219 |
+
logger.info(f"Issued count incremented for template {template_id}")
|
| 220 |
+
else:
|
| 221 |
+
logger.warning(f"Failed to increment issued count for template {template_id}")
|
| 222 |
+
|
| 223 |
+
return success
|
| 224 |
+
except Exception as e:
|
| 225 |
+
logger.error(f"Error incrementing issued count for template {template_id}: {e}", exc_info=True)
|
| 226 |
+
raise RuntimeError("Failed to increment issued count") from e
|
| 227 |
+
|
| 228 |
+
@staticmethod
|
| 229 |
+
async def check_stock_availability(template_id: str) -> Dict[str, Any]:
|
| 230 |
+
"""
|
| 231 |
+
Check if a template has available stock (for physical cards with max_issues).
|
| 232 |
+
|
| 233 |
+
Args:
|
| 234 |
+
template_id (str): The template ID to check
|
| 235 |
+
|
| 236 |
+
Returns:
|
| 237 |
+
Dict[str, Any]: Stock availability information
|
| 238 |
+
"""
|
| 239 |
+
try:
|
| 240 |
+
template = await db[GIFT_CARD_TEMPLATES_COLLECTION].find_one({"_id": template_id})
|
| 241 |
+
|
| 242 |
+
if not template:
|
| 243 |
+
raise HTTPException(status_code=404, detail="Gift card template not found")
|
| 244 |
+
|
| 245 |
+
max_issues = template.get("max_issues")
|
| 246 |
+
issued_count = template.get("issued_count", 0)
|
| 247 |
+
|
| 248 |
+
# For digital cards or unlimited templates
|
| 249 |
+
if max_issues is None:
|
| 250 |
+
return {
|
| 251 |
+
"available": True,
|
| 252 |
+
"unlimited": True,
|
| 253 |
+
"remaining": None
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
# For physical cards with limited stock
|
| 257 |
+
remaining = max_issues - issued_count
|
| 258 |
+
available = remaining > 0
|
| 259 |
+
|
| 260 |
+
return {
|
| 261 |
+
"available": available,
|
| 262 |
+
"unlimited": False,
|
| 263 |
+
"remaining": remaining,
|
| 264 |
+
"max_issues": max_issues,
|
| 265 |
+
"issued_count": issued_count
|
| 266 |
+
}
|
| 267 |
+
except HTTPException:
|
| 268 |
+
raise
|
| 269 |
+
except Exception as e:
|
| 270 |
+
logger.error(f"Error checking stock availability for template {template_id}: {e}", exc_info=True)
|
| 271 |
+
raise RuntimeError("Failed to check stock availability") from e
|
app/repositories/gift_card_repository.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import Any, Dict, List, Optional
|
| 3 |
+
from app.models.gift_card_models import GiftCardTemplateModel
|
| 4 |
+
from app.schemas.gift_card_schema import GiftCardTemplateFilter
|
| 5 |
+
|
| 6 |
+
# Configure logging for this module
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
class GiftCardRepository:
|
| 10 |
+
"""
|
| 11 |
+
Repository layer for Gift Card operations.
|
| 12 |
+
Provides a clean interface between services and the data model.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
@staticmethod
|
| 16 |
+
async def create_template(template_data: Dict[str, Any]) -> str:
|
| 17 |
+
"""
|
| 18 |
+
Create a new gift card template.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
template_data (Dict[str, Any]): Template data to create
|
| 22 |
+
|
| 23 |
+
Returns:
|
| 24 |
+
str: Created template ID
|
| 25 |
+
"""
|
| 26 |
+
try:
|
| 27 |
+
template_id = await GiftCardTemplateModel.create_template(template_data)
|
| 28 |
+
logger.info(f"Template created with ID: {template_id}")
|
| 29 |
+
return template_id
|
| 30 |
+
except Exception as e:
|
| 31 |
+
logger.error(f"Repository error creating template: {e}")
|
| 32 |
+
raise
|
| 33 |
+
|
| 34 |
+
@staticmethod
|
| 35 |
+
async def get_templates(
|
| 36 |
+
filter_criteria: Dict[str, Any],
|
| 37 |
+
offset: int = 0,
|
| 38 |
+
limit: int = 100
|
| 39 |
+
) -> Dict[str, Any]:
|
| 40 |
+
"""
|
| 41 |
+
Get gift card templates with pagination and filtering.
|
| 42 |
+
|
| 43 |
+
Args:
|
| 44 |
+
filter_criteria (Dict[str, Any]): MongoDB filter criteria
|
| 45 |
+
offset (int): Number of documents to skip
|
| 46 |
+
limit (int): Maximum number of documents to return
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
Dict[str, Any]: Templates with pagination info
|
| 50 |
+
"""
|
| 51 |
+
try:
|
| 52 |
+
result = await GiftCardTemplateModel.get_templates(filter_criteria, offset, limit)
|
| 53 |
+
logger.info(f"Retrieved {len(result['templates'])} templates")
|
| 54 |
+
return result
|
| 55 |
+
except Exception as e:
|
| 56 |
+
logger.error(f"Repository error getting templates: {e}")
|
| 57 |
+
raise
|
| 58 |
+
|
| 59 |
+
@staticmethod
|
| 60 |
+
async def get_template_by_id(template_id: str) -> Optional[Dict[str, Any]]:
|
| 61 |
+
"""
|
| 62 |
+
Get a specific gift card template by ID.
|
| 63 |
+
|
| 64 |
+
Args:
|
| 65 |
+
template_id (str): Template ID to retrieve
|
| 66 |
+
|
| 67 |
+
Returns:
|
| 68 |
+
Optional[Dict[str, Any]]: Template data or None if not found
|
| 69 |
+
"""
|
| 70 |
+
try:
|
| 71 |
+
template = await GiftCardTemplateModel.get_template_by_id(template_id)
|
| 72 |
+
if template:
|
| 73 |
+
logger.info(f"Retrieved template: {template_id}")
|
| 74 |
+
else:
|
| 75 |
+
logger.info(f"Template not found: {template_id}")
|
| 76 |
+
return template
|
| 77 |
+
except Exception as e:
|
| 78 |
+
logger.error(f"Repository error getting template {template_id}: {e}")
|
| 79 |
+
raise
|
| 80 |
+
|
| 81 |
+
@staticmethod
|
| 82 |
+
async def update_template(template_id: str, update_data: Dict[str, Any]) -> bool:
|
| 83 |
+
"""
|
| 84 |
+
Update a gift card template.
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
template_id (str): Template ID to update
|
| 88 |
+
update_data (Dict[str, Any]): Fields to update
|
| 89 |
+
|
| 90 |
+
Returns:
|
| 91 |
+
bool: True if update was successful
|
| 92 |
+
"""
|
| 93 |
+
try:
|
| 94 |
+
success = await GiftCardTemplateModel.update_template(template_id, update_data)
|
| 95 |
+
if success:
|
| 96 |
+
logger.info(f"Template updated: {template_id}")
|
| 97 |
+
else:
|
| 98 |
+
logger.warning(f"No changes made to template: {template_id}")
|
| 99 |
+
return success
|
| 100 |
+
except Exception as e:
|
| 101 |
+
logger.error(f"Repository error updating template {template_id}: {e}")
|
| 102 |
+
raise
|
| 103 |
+
|
| 104 |
+
@staticmethod
|
| 105 |
+
async def delete_template(template_id: str) -> bool:
|
| 106 |
+
"""
|
| 107 |
+
Delete a gift card template.
|
| 108 |
+
|
| 109 |
+
Args:
|
| 110 |
+
template_id (str): Template ID to delete
|
| 111 |
+
|
| 112 |
+
Returns:
|
| 113 |
+
bool: True if deletion was successful
|
| 114 |
+
"""
|
| 115 |
+
try:
|
| 116 |
+
success = await GiftCardTemplateModel.delete_template(template_id)
|
| 117 |
+
if success:
|
| 118 |
+
logger.info(f"Template deleted: {template_id}")
|
| 119 |
+
else:
|
| 120 |
+
logger.warning(f"Template not found for deletion: {template_id}")
|
| 121 |
+
return success
|
| 122 |
+
except Exception as e:
|
| 123 |
+
logger.error(f"Repository error deleting template {template_id}: {e}")
|
| 124 |
+
raise
|
| 125 |
+
|
| 126 |
+
@staticmethod
|
| 127 |
+
async def check_template_exists(template_id: str) -> bool:
|
| 128 |
+
"""
|
| 129 |
+
Check if a template exists.
|
| 130 |
+
|
| 131 |
+
Args:
|
| 132 |
+
template_id (str): Template ID to check
|
| 133 |
+
|
| 134 |
+
Returns:
|
| 135 |
+
bool: True if template exists
|
| 136 |
+
"""
|
| 137 |
+
try:
|
| 138 |
+
template = await GiftCardTemplateModel.get_template_by_id(template_id)
|
| 139 |
+
return template is not None
|
| 140 |
+
except Exception as e:
|
| 141 |
+
logger.error(f"Repository error checking template existence {template_id}: {e}")
|
| 142 |
+
raise
|
| 143 |
+
|
| 144 |
+
@staticmethod
|
| 145 |
+
async def increment_issued_count(template_id: str) -> bool:
|
| 146 |
+
"""
|
| 147 |
+
Increment the issued count for a template.
|
| 148 |
+
|
| 149 |
+
Args:
|
| 150 |
+
template_id (str): Template ID to increment count for
|
| 151 |
+
|
| 152 |
+
Returns:
|
| 153 |
+
bool: True if increment was successful
|
| 154 |
+
"""
|
| 155 |
+
try:
|
| 156 |
+
success = await GiftCardTemplateModel.increment_issued_count(template_id)
|
| 157 |
+
if success:
|
| 158 |
+
logger.info(f"Issued count incremented for template: {template_id}")
|
| 159 |
+
return success
|
| 160 |
+
except Exception as e:
|
| 161 |
+
logger.error(f"Repository error incrementing issued count {template_id}: {e}")
|
| 162 |
+
raise
|
| 163 |
+
|
| 164 |
+
@staticmethod
|
| 165 |
+
async def check_stock_availability(template_id: str) -> Dict[str, Any]:
|
| 166 |
+
"""
|
| 167 |
+
Check stock availability for a template.
|
| 168 |
+
|
| 169 |
+
Args:
|
| 170 |
+
template_id (str): Template ID to check
|
| 171 |
+
|
| 172 |
+
Returns:
|
| 173 |
+
Dict[str, Any]: Stock availability information
|
| 174 |
+
"""
|
| 175 |
+
try:
|
| 176 |
+
stock_info = await GiftCardTemplateModel.check_stock_availability(template_id)
|
| 177 |
+
logger.info(f"Stock check for template {template_id}: available={stock_info['available']}")
|
| 178 |
+
return stock_info
|
| 179 |
+
except Exception as e:
|
| 180 |
+
logger.error(f"Repository error checking stock for template {template_id}: {e}")
|
| 181 |
+
raise
|
| 182 |
+
|
| 183 |
+
@staticmethod
|
| 184 |
+
def build_filter_criteria(
|
| 185 |
+
merchant_id: str,
|
| 186 |
+
filters: Optional[GiftCardTemplateFilter] = None
|
| 187 |
+
) -> Dict[str, Any]:
|
| 188 |
+
"""
|
| 189 |
+
Build MongoDB filter criteria from filter parameters.
|
| 190 |
+
|
| 191 |
+
Args:
|
| 192 |
+
merchant_id (str): Merchant ID to filter by
|
| 193 |
+
filters (Optional[GiftCardTemplateFilter]): Filter parameters
|
| 194 |
+
|
| 195 |
+
Returns:
|
| 196 |
+
Dict[str, Any]: MongoDB filter criteria
|
| 197 |
+
"""
|
| 198 |
+
if filters:
|
| 199 |
+
return filters.to_mongo_filter(merchant_id)
|
| 200 |
+
else:
|
| 201 |
+
return {"merchant_id": merchant_id}
|
| 202 |
+
|
| 203 |
+
@staticmethod
|
| 204 |
+
async def get_templates_by_status(
|
| 205 |
+
merchant_id: str,
|
| 206 |
+
status: str,
|
| 207 |
+
limit: int = 100
|
| 208 |
+
) -> List[Dict[str, Any]]:
|
| 209 |
+
"""
|
| 210 |
+
Get templates by status for a specific merchant.
|
| 211 |
+
|
| 212 |
+
Args:
|
| 213 |
+
merchant_id (str): Merchant ID
|
| 214 |
+
status (str): Template status
|
| 215 |
+
limit (int): Maximum number of templates to return
|
| 216 |
+
|
| 217 |
+
Returns:
|
| 218 |
+
List[Dict[str, Any]]: List of templates
|
| 219 |
+
"""
|
| 220 |
+
try:
|
| 221 |
+
filter_criteria = {"merchant_id": merchant_id, "status": status}
|
| 222 |
+
result = await GiftCardTemplateModel.get_templates(filter_criteria, 0, limit)
|
| 223 |
+
logger.info(f"Retrieved {len(result['templates'])} templates with status {status}")
|
| 224 |
+
return result["templates"]
|
| 225 |
+
except Exception as e:
|
| 226 |
+
logger.error(f"Repository error getting templates by status {status}: {e}")
|
| 227 |
+
raise
|
| 228 |
+
|
| 229 |
+
@staticmethod
|
| 230 |
+
async def xxget_active_templates(merchant_id: str, limit: int = 100) -> List[Dict[str, Any]]:
|
| 231 |
+
"""
|
| 232 |
+
Get active templates for a specific merchant.
|
| 233 |
+
|
| 234 |
+
Args:
|
| 235 |
+
merchant_id (str): Merchant ID
|
| 236 |
+
limit (int): Maximum number of templates to return
|
| 237 |
+
|
| 238 |
+
Returns:
|
| 239 |
+
List[Dict[str, Any]]: List of active templates
|
| 240 |
+
"""
|
| 241 |
+
return await GiftCardRepository.get_templates_by_status(merchant_id, "active", limit)
|
| 242 |
+
|
| 243 |
+
@staticmethod
|
| 244 |
+
async def get_templates_by_delivery_type(
|
| 245 |
+
merchant_id: str,
|
| 246 |
+
delivery_type: str,
|
| 247 |
+
limit: int = 100
|
| 248 |
+
) -> List[Dict[str, Any]]:
|
| 249 |
+
"""
|
| 250 |
+
Get templates by delivery type for a specific merchant.
|
| 251 |
+
|
| 252 |
+
Args:
|
| 253 |
+
merchant_id (str): Merchant ID
|
| 254 |
+
delivery_type (str): Delivery type (digital/physical)
|
| 255 |
+
limit (int): Maximum number of templates to return
|
| 256 |
+
|
| 257 |
+
Returns:
|
| 258 |
+
List[Dict[str, Any]]: List of templates
|
| 259 |
+
"""
|
| 260 |
+
try:
|
| 261 |
+
filter_criteria = {"merchant_id": merchant_id, "delivery_type": delivery_type}
|
| 262 |
+
result = await GiftCardTemplateModel.get_templates(filter_criteria, 0, limit)
|
| 263 |
+
logger.info(f"Retrieved {len(result['templates'])} {delivery_type} templates")
|
| 264 |
+
return result["templates"]
|
| 265 |
+
except Exception as e:
|
| 266 |
+
logger.error(f"Repository error getting templates by delivery type {delivery_type}: {e}")
|
| 267 |
+
raise
|
app/routers/gift_card_router.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from fastapi import APIRouter, Depends, HTTPException, Query, Path
|
| 4 |
+
|
| 5 |
+
from app.services.gift_card_service import GiftCardService
|
| 6 |
+
from app.schemas.gift_card_schema import (
|
| 7 |
+
GiftCardTemplateCreate,
|
| 8 |
+
GiftCardTemplateUpdate,
|
| 9 |
+
GiftCardTemplateResponse,
|
| 10 |
+
GiftCardTemplateListResponse,
|
| 11 |
+
GiftCardTemplateCreateResponse,
|
| 12 |
+
GiftCardTemplateFilter,
|
| 13 |
+
GiftCardStockResponse,
|
| 14 |
+
DeliveryType,
|
| 15 |
+
TemplateStatus,
|
| 16 |
+
Currency
|
| 17 |
+
)
|
| 18 |
+
from app.dependencies.auth import get_current_user, require_permission, AccessID
|
| 19 |
+
|
| 20 |
+
# Router Initialization
|
| 21 |
+
router = APIRouter()
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
# Constants
|
| 25 |
+
INTERNAL_SERVER_ERROR = "Internal server error"
|
| 26 |
+
MERCHANT_ID_REQUIRED = "Merchant ID is required"
|
| 27 |
+
USER_ID_REQUIRED = "User ID is required"
|
| 28 |
+
TEMPLATE_ID_DESCRIPTION = "Gift card template ID"
|
| 29 |
+
|
| 30 |
+
# Permission Dependencies (assuming similar pattern to catalogue router)
|
| 31 |
+
async def require_create_giftcard_permission(current_user: dict = Depends(get_current_user)):
|
| 32 |
+
return await require_permission(AccessID.CREATE_CATALOGUE.value, current_user) # Using catalogue permission for now
|
| 33 |
+
|
| 34 |
+
async def require_update_giftcard_permission(current_user: dict = Depends(get_current_user)):
|
| 35 |
+
return await require_permission(AccessID.UPDATE_CATALOGUE.value, current_user)
|
| 36 |
+
|
| 37 |
+
async def require_view_giftcard_permission(current_user: dict = Depends(get_current_user)):
|
| 38 |
+
return await require_permission(AccessID.VIEW_CATALOGUE.value, current_user)
|
| 39 |
+
|
| 40 |
+
async def require_delete_giftcard_permission(current_user: dict = Depends(get_current_user)):
|
| 41 |
+
return await require_permission(AccessID.DELETE_CATALOGUE.value, current_user)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# Route Handlers
|
| 45 |
+
|
| 46 |
+
@router.post("/", status_code=201, response_model=GiftCardTemplateCreateResponse)
|
| 47 |
+
async def create_gift_card_template(
|
| 48 |
+
template_data: GiftCardTemplateCreate,
|
| 49 |
+
current_user: dict = Depends(require_create_giftcard_permission)
|
| 50 |
+
) -> GiftCardTemplateCreateResponse:
|
| 51 |
+
"""
|
| 52 |
+
Create a new gift card template (blueprint).
|
| 53 |
+
|
| 54 |
+
This endpoint allows merchants to create gift card templates that define
|
| 55 |
+
the characteristics and rules for gift cards that can be issued later.
|
| 56 |
+
|
| 57 |
+
**Business Rules:**
|
| 58 |
+
- Physical cards must have max_issues specified
|
| 59 |
+
- Physical cards cannot be reloadable
|
| 60 |
+
- Either allow_custom_amount must be True or predefined_amounts must be provided
|
| 61 |
+
- Template names must be unique within a merchant
|
| 62 |
+
|
| 63 |
+
**Example Request:**
|
| 64 |
+
```json
|
| 65 |
+
{
|
| 66 |
+
"name": "Birthday Card",
|
| 67 |
+
"description": "Special card for birthdays",
|
| 68 |
+
"delivery_type": "digital",
|
| 69 |
+
"currency": "INR",
|
| 70 |
+
"validity_days": 365,
|
| 71 |
+
"allow_custom_amount": true,
|
| 72 |
+
"predefined_amounts": [500, 1000, 2000],
|
| 73 |
+
"reloadable": false,
|
| 74 |
+
"allow_partial_redemption": true,
|
| 75 |
+
"requires_pin": true,
|
| 76 |
+
"requires_otp": false,
|
| 77 |
+
"status": "active",
|
| 78 |
+
"design_template": {
|
| 79 |
+
"theme": "blue",
|
| 80 |
+
"image_url": "https://..."
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
```
|
| 84 |
+
"""
|
| 85 |
+
try:
|
| 86 |
+
merchant_id = current_user.get("merchant_id")
|
| 87 |
+
user_id = current_user.get("user_id")
|
| 88 |
+
|
| 89 |
+
if not merchant_id:
|
| 90 |
+
raise HTTPException(status_code=400, detail=MERCHANT_ID_REQUIRED)
|
| 91 |
+
if not user_id:
|
| 92 |
+
raise HTTPException(status_code=400, detail=USER_ID_REQUIRED)
|
| 93 |
+
|
| 94 |
+
logger.info(f"Creating gift card template for merchant {merchant_id} by user {user_id}")
|
| 95 |
+
|
| 96 |
+
result = await GiftCardService.create_template(template_data, merchant_id, user_id)
|
| 97 |
+
|
| 98 |
+
logger.info(f"Gift card template created successfully: {result.id}")
|
| 99 |
+
return result
|
| 100 |
+
|
| 101 |
+
except HTTPException:
|
| 102 |
+
raise
|
| 103 |
+
except Exception as e:
|
| 104 |
+
logger.error(f"Unexpected error creating gift card template: {e}", exc_info=True)
|
| 105 |
+
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
@router.get("/", response_model=GiftCardTemplateListResponse)
|
| 109 |
+
async def get_gift_card_templates(
|
| 110 |
+
status: Optional[TemplateStatus] = Query(None, description="Filter by template status"),
|
| 111 |
+
delivery_type: Optional[DeliveryType] = Query(None, description="Filter by delivery type"),
|
| 112 |
+
currency: Optional[Currency] = Query(None, description="Filter by currency"),
|
| 113 |
+
branch_id: Optional[str] = Query(None, description="Filter by branch ID"),
|
| 114 |
+
campaign_id: Optional[str] = Query(None, description="Filter by campaign ID"),
|
| 115 |
+
created_by: Optional[str] = Query(None, description="Filter by creator user ID"),
|
| 116 |
+
offset: int = Query(0, ge=0, description="Number of items to skip for pagination"),
|
| 117 |
+
limit: int = Query(100, ge=1, le=1000, description="Maximum number of items to return"),
|
| 118 |
+
current_user: dict = Depends(require_view_giftcard_permission)
|
| 119 |
+
) -> GiftCardTemplateListResponse:
|
| 120 |
+
"""
|
| 121 |
+
List all gift card templates with optional filtering.
|
| 122 |
+
|
| 123 |
+
This endpoint returns a paginated list of gift card templates for the merchant
|
| 124 |
+
with optional filtering by various criteria.
|
| 125 |
+
|
| 126 |
+
**Query Parameters:**
|
| 127 |
+
- **status**: Filter by template status (active, inactive, archived)
|
| 128 |
+
- **delivery_type**: Filter by delivery type (digital, physical)
|
| 129 |
+
- **currency**: Filter by currency (INR, USD, EUR, GBP)
|
| 130 |
+
- **branch_id**: Filter templates available for specific branch
|
| 131 |
+
- **campaign_id**: Filter templates linked to specific campaign
|
| 132 |
+
- **created_by**: Filter by creator user ID
|
| 133 |
+
- **offset**: Number of items to skip (for pagination)
|
| 134 |
+
- **limit**: Maximum items to return (1-1000)
|
| 135 |
+
|
| 136 |
+
**Response includes:**
|
| 137 |
+
- List of templates matching the criteria
|
| 138 |
+
- Total count of matching templates
|
| 139 |
+
- Pagination information
|
| 140 |
+
"""
|
| 141 |
+
try:
|
| 142 |
+
merchant_id = current_user.get("merchant_id")
|
| 143 |
+
|
| 144 |
+
if not merchant_id:
|
| 145 |
+
raise HTTPException(status_code=400, detail=MERCHANT_ID_REQUIRED)
|
| 146 |
+
|
| 147 |
+
# Build filter object
|
| 148 |
+
filters = GiftCardTemplateFilter(
|
| 149 |
+
status=status,
|
| 150 |
+
delivery_type=delivery_type,
|
| 151 |
+
currency=currency,
|
| 152 |
+
branch_id=branch_id,
|
| 153 |
+
campaign_id=campaign_id,
|
| 154 |
+
created_by=created_by
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
logger.info(f"Fetching gift card templates for merchant {merchant_id}")
|
| 158 |
+
|
| 159 |
+
result = await GiftCardService.get_templates(merchant_id, filters, offset, limit)
|
| 160 |
+
|
| 161 |
+
logger.info(f"Retrieved {len(result.templates)} gift card templates")
|
| 162 |
+
return result
|
| 163 |
+
|
| 164 |
+
except HTTPException:
|
| 165 |
+
raise
|
| 166 |
+
except Exception as e:
|
| 167 |
+
logger.error(f"Unexpected error fetching gift card templates: {e}", exc_info=True)
|
| 168 |
+
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
@router.get("/{template_id}", response_model=GiftCardTemplateResponse)
|
| 172 |
+
async def get_gift_card_template(
|
| 173 |
+
template_id: str = Path(..., description=TEMPLATE_ID_DESCRIPTION),
|
| 174 |
+
current_user: dict = Depends(require_view_giftcard_permission)
|
| 175 |
+
) -> GiftCardTemplateResponse:
|
| 176 |
+
"""
|
| 177 |
+
Get a specific gift card template by ID.
|
| 178 |
+
|
| 179 |
+
This endpoint returns detailed information about a specific gift card template.
|
| 180 |
+
|
| 181 |
+
**Path Parameters:**
|
| 182 |
+
- **template_id**: The unique identifier of the gift card template
|
| 183 |
+
|
| 184 |
+
**Security:**
|
| 185 |
+
- Only returns templates that belong to the authenticated merchant
|
| 186 |
+
- Returns 404 if template doesn't exist or doesn't belong to merchant
|
| 187 |
+
"""
|
| 188 |
+
try:
|
| 189 |
+
merchant_id = current_user.get("merchant_id")
|
| 190 |
+
|
| 191 |
+
if not merchant_id:
|
| 192 |
+
raise HTTPException(status_code=400, detail=MERCHANT_ID_REQUIRED)
|
| 193 |
+
|
| 194 |
+
logger.info(f"Fetching gift card template {template_id} for merchant {merchant_id}")
|
| 195 |
+
|
| 196 |
+
result = await GiftCardService.get_template_by_id(template_id, merchant_id)
|
| 197 |
+
|
| 198 |
+
logger.info(f"Retrieved gift card template: {template_id}")
|
| 199 |
+
return result
|
| 200 |
+
|
| 201 |
+
except HTTPException:
|
| 202 |
+
raise
|
| 203 |
+
except Exception as e:
|
| 204 |
+
logger.error(f"Unexpected error fetching gift card template {template_id}: {e}", exc_info=True)
|
| 205 |
+
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
@router.put("/{template_id}", response_model=GiftCardTemplateResponse)
|
| 209 |
+
async def update_gift_card_template(
|
| 210 |
+
template_id: str = Path(..., description=TEMPLATE_ID_DESCRIPTION),
|
| 211 |
+
update_data: GiftCardTemplateUpdate = ...,
|
| 212 |
+
current_user: dict = Depends(require_update_giftcard_permission)
|
| 213 |
+
) -> GiftCardTemplateResponse:
|
| 214 |
+
"""
|
| 215 |
+
Update an existing gift card template.
|
| 216 |
+
|
| 217 |
+
This endpoint allows updating specific fields of a gift card template.
|
| 218 |
+
Some fields have restrictions based on business rules.
|
| 219 |
+
|
| 220 |
+
**Path Parameters:**
|
| 221 |
+
- **template_id**: The unique identifier of the gift card template
|
| 222 |
+
|
| 223 |
+
**Business Rules:**
|
| 224 |
+
- Cannot change delivery_type of existing template
|
| 225 |
+
- Physical cards cannot be made reloadable
|
| 226 |
+
- Cannot reduce max_issues below current issued_count
|
| 227 |
+
- Physical cards must always have max_issues specified
|
| 228 |
+
|
| 229 |
+
**Common Use Cases:**
|
| 230 |
+
- Deactivate template: `{"status": "inactive"}`
|
| 231 |
+
- Update predefined amounts: `{"predefined_amounts": [1000, 2000]}`
|
| 232 |
+
- Change design: `{"design_template": {"theme": "gold", "image_url": "..."}}`
|
| 233 |
+
|
| 234 |
+
**Example Request:**
|
| 235 |
+
```json
|
| 236 |
+
{
|
| 237 |
+
"status": "inactive",
|
| 238 |
+
"predefined_amounts": [1000, 2000]
|
| 239 |
+
}
|
| 240 |
+
```
|
| 241 |
+
"""
|
| 242 |
+
try:
|
| 243 |
+
merchant_id = current_user.get("merchant_id")
|
| 244 |
+
|
| 245 |
+
if not merchant_id:
|
| 246 |
+
raise HTTPException(status_code=400, detail=MERCHANT_ID_REQUIRED)
|
| 247 |
+
|
| 248 |
+
logger.info(f"Updating gift card template {template_id} for merchant {merchant_id}")
|
| 249 |
+
|
| 250 |
+
result = await GiftCardService.update_template(template_id, update_data, merchant_id)
|
| 251 |
+
|
| 252 |
+
logger.info(f"Gift card template updated successfully: {template_id}")
|
| 253 |
+
return result
|
| 254 |
+
|
| 255 |
+
except HTTPException:
|
| 256 |
+
raise
|
| 257 |
+
except Exception as e:
|
| 258 |
+
logger.error(f"Unexpected error updating gift card template {template_id}: {e}", exc_info=True)
|
| 259 |
+
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR)
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
@router.delete("/{template_id}")
|
| 263 |
+
async def delete_gift_card_template(
|
| 264 |
+
template_id: str = Path(..., description=TEMPLATE_ID_DESCRIPTION),
|
| 265 |
+
current_user: dict = Depends(require_delete_giftcard_permission)
|
| 266 |
+
) -> dict:
|
| 267 |
+
"""
|
| 268 |
+
Delete a gift card template.
|
| 269 |
+
|
| 270 |
+
This endpoint deletes a gift card template. Templates can only be deleted
|
| 271 |
+
if no gift cards have been issued from them.
|
| 272 |
+
|
| 273 |
+
**Path Parameters:**
|
| 274 |
+
- **template_id**: The unique identifier of the gift card template
|
| 275 |
+
|
| 276 |
+
**Business Rules:**
|
| 277 |
+
- Cannot delete templates that have issued_count > 0
|
| 278 |
+
- Consider deactivating instead of deleting for templates with issued cards
|
| 279 |
+
|
| 280 |
+
**Security:**
|
| 281 |
+
- Only allows deletion of templates that belong to the authenticated merchant
|
| 282 |
+
"""
|
| 283 |
+
try:
|
| 284 |
+
merchant_id = current_user.get("merchant_id")
|
| 285 |
+
|
| 286 |
+
if not merchant_id:
|
| 287 |
+
raise HTTPException(status_code=400, detail=MERCHANT_ID_REQUIRED)
|
| 288 |
+
|
| 289 |
+
logger.info(f"Deleting gift card template {template_id} for merchant {merchant_id}")
|
| 290 |
+
|
| 291 |
+
result = await GiftCardService.delete_template(template_id, merchant_id)
|
| 292 |
+
|
| 293 |
+
logger.info(f"Gift card template deleted successfully: {template_id}")
|
| 294 |
+
return result
|
| 295 |
+
|
| 296 |
+
except HTTPException:
|
| 297 |
+
raise
|
| 298 |
+
except Exception as e:
|
| 299 |
+
logger.error(f"Unexpected error deleting gift card template {template_id}: {e}", exc_info=True)
|
| 300 |
+
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR)
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
# Additional utility endpoints
|
| 304 |
+
|
| 305 |
+
@router.get("/{template_id}/stock", response_model=GiftCardStockResponse)
|
| 306 |
+
async def check_gift_card_template_stock(
|
| 307 |
+
template_id: str = Path(..., description=TEMPLATE_ID_DESCRIPTION),
|
| 308 |
+
current_user: dict = Depends(require_view_giftcard_permission)
|
| 309 |
+
) -> GiftCardStockResponse:
|
| 310 |
+
"""
|
| 311 |
+
Check stock availability for a gift card template.
|
| 312 |
+
|
| 313 |
+
This endpoint returns information about whether a template has available
|
| 314 |
+
stock for issuing new gift cards.
|
| 315 |
+
|
| 316 |
+
**Path Parameters:**
|
| 317 |
+
- **template_id**: The unique identifier of the gift card template
|
| 318 |
+
|
| 319 |
+
**Response:**
|
| 320 |
+
- **available**: Whether cards can be issued from this template
|
| 321 |
+
- **unlimited**: Whether the template has unlimited stock (digital cards)
|
| 322 |
+
- **remaining**: Number of cards remaining (null for unlimited)
|
| 323 |
+
- **max_issues**: Maximum number of cards that can be issued
|
| 324 |
+
- **issued_count**: Number of cards already issued
|
| 325 |
+
"""
|
| 326 |
+
try:
|
| 327 |
+
merchant_id = current_user.get("merchant_id")
|
| 328 |
+
|
| 329 |
+
if not merchant_id:
|
| 330 |
+
raise HTTPException(status_code=400, detail=MERCHANT_ID_REQUIRED)
|
| 331 |
+
|
| 332 |
+
logger.info(f"Checking stock for gift card template {template_id}")
|
| 333 |
+
|
| 334 |
+
result = await GiftCardService.check_stock_availability(template_id, merchant_id)
|
| 335 |
+
|
| 336 |
+
return result
|
| 337 |
+
|
| 338 |
+
except HTTPException:
|
| 339 |
+
raise
|
| 340 |
+
except Exception as e:
|
| 341 |
+
logger.error(f"Unexpected error checking stock for template {template_id}: {e}", exc_info=True)
|
| 342 |
+
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR)
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
@router.get("/active/list", response_model=list[GiftCardTemplateResponse])
|
| 346 |
+
async def get_active_gift_card_templates(
|
| 347 |
+
current_user: dict = Depends(require_view_giftcard_permission)
|
| 348 |
+
) -> list[GiftCardTemplateResponse]:
|
| 349 |
+
"""
|
| 350 |
+
Get all active gift card templates for the merchant.
|
| 351 |
+
|
| 352 |
+
This is a convenience endpoint that returns only active templates,
|
| 353 |
+
commonly used for displaying available templates to customers.
|
| 354 |
+
|
| 355 |
+
**Returns:**
|
| 356 |
+
- List of all active gift card templates for the merchant
|
| 357 |
+
- Templates are sorted by creation date (newest first)
|
| 358 |
+
"""
|
| 359 |
+
try:
|
| 360 |
+
merchant_id = current_user.get("merchant_id")
|
| 361 |
+
|
| 362 |
+
if not merchant_id:
|
| 363 |
+
raise HTTPException(status_code=400, detail=MERCHANT_ID_REQUIRED)
|
| 364 |
+
|
| 365 |
+
logger.info(f"Fetching active gift card templates for merchant {merchant_id}")
|
| 366 |
+
|
| 367 |
+
result = await GiftCardService.get_active_templates(merchant_id)
|
| 368 |
+
|
| 369 |
+
logger.info(f"Retrieved {len(result)} active gift card templates")
|
| 370 |
+
return result
|
| 371 |
+
|
| 372 |
+
except HTTPException:
|
| 373 |
+
raise
|
| 374 |
+
except Exception as e:
|
| 375 |
+
logger.error(f"Unexpected error fetching active gift card templates: {e}", exc_info=True)
|
| 376 |
+
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR)
|
app/schemas/gift_card_schema.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
from typing import Any, Dict, List, Literal, Optional, Union
|
| 3 |
+
from pydantic import BaseModel, Field, model_validator, field_validator
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
# Enums for gift card template fields
|
| 7 |
+
DeliveryType = Literal["digital", "physical"]
|
| 8 |
+
TemplateStatus = Literal["active", "inactive", "archived"]
|
| 9 |
+
Currency = Literal["INR", "USD", "EUR", "GBP"] # Add more currencies as needed
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# Design template schema
|
| 13 |
+
class DesignTemplate(BaseModel):
|
| 14 |
+
"""Design template configuration for gift cards."""
|
| 15 |
+
theme: str = Field(..., description="Theme name (e.g., 'blue', 'gold')")
|
| 16 |
+
image_url: Optional[str] = Field(None, description="URL to the template image")
|
| 17 |
+
|
| 18 |
+
@field_validator('theme')
|
| 19 |
+
@classmethod
|
| 20 |
+
def validate_theme(cls, v):
|
| 21 |
+
if not v.strip():
|
| 22 |
+
raise ValueError("Theme cannot be empty")
|
| 23 |
+
return v.strip()
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# Base gift card template schema
|
| 27 |
+
class GiftCardTemplateBase(BaseModel):
|
| 28 |
+
"""Base schema for gift card templates."""
|
| 29 |
+
name: str = Field(..., description="Template name (e.g., 'Birthday Gift Card')", min_length=1, max_length=100)
|
| 30 |
+
description: Optional[str] = Field(None, description="Longer description for admins/marketing", max_length=500)
|
| 31 |
+
delivery_type: DeliveryType = Field(..., description="Delivery type: digital or physical")
|
| 32 |
+
currency: Currency = Field(..., description="ISO 4217 currency code")
|
| 33 |
+
validity_days: Optional[int] = Field(None, description="Days card is valid from issuance; null = no expiry", ge=1)
|
| 34 |
+
allow_custom_amount: bool = Field(..., description="Allow purchaser to enter custom value")
|
| 35 |
+
predefined_amounts: Optional[List[float]] = Field(None, description="Array of fixed denominations")
|
| 36 |
+
reloadable: bool = Field(..., description="Can the card be topped up after issue?")
|
| 37 |
+
allow_partial_redemption: bool = Field(..., description="Can recipient use the card multiple times until balance is zero?")
|
| 38 |
+
requires_pin: bool = Field(..., description="If true, issued cards must have PIN protection")
|
| 39 |
+
requires_otp: bool = Field(..., description="If true, OTP required at redemption")
|
| 40 |
+
status: TemplateStatus = Field(..., description="Template status")
|
| 41 |
+
max_issues: Optional[int] = Field(None, description="For physical cards: max number available; For digital: null = unlimited", ge=1)
|
| 42 |
+
design_template: Optional[DesignTemplate] = Field(None, description="Branding information")
|
| 43 |
+
branch_ids: Optional[List[str]] = Field(None, description="Restrict usage to specific branches (empty = all)")
|
| 44 |
+
campaign_id: Optional[str] = Field(None, description="Link to campaign (if promotional)")
|
| 45 |
+
metadata: Optional[Dict[str, Any]] = Field(None, description="Freeform extra data")
|
| 46 |
+
created_by: str = Field(..., description="User ID who created template")
|
| 47 |
+
|
| 48 |
+
@field_validator('name')
|
| 49 |
+
@classmethod
|
| 50 |
+
def validate_name(cls, v):
|
| 51 |
+
if not v.strip():
|
| 52 |
+
raise ValueError("Name cannot be empty")
|
| 53 |
+
return v.strip()
|
| 54 |
+
|
| 55 |
+
@field_validator('predefined_amounts')
|
| 56 |
+
@classmethod
|
| 57 |
+
def validate_predefined_amounts(cls, v):
|
| 58 |
+
if v is not None:
|
| 59 |
+
if not v: # Empty list
|
| 60 |
+
raise ValueError("Predefined amounts cannot be an empty list; use null instead")
|
| 61 |
+
if any(amount <= 0 for amount in v):
|
| 62 |
+
raise ValueError("All predefined amounts must be positive")
|
| 63 |
+
if len(set(v)) != len(v):
|
| 64 |
+
raise ValueError("Predefined amounts must be unique")
|
| 65 |
+
return v
|
| 66 |
+
|
| 67 |
+
@field_validator('branch_ids')
|
| 68 |
+
@classmethod
|
| 69 |
+
def validate_branch_ids(cls, v):
|
| 70 |
+
if v is not None and not v: # Empty list
|
| 71 |
+
return None # Convert empty list to None
|
| 72 |
+
return v
|
| 73 |
+
|
| 74 |
+
@model_validator(mode='after')
|
| 75 |
+
def validate_amounts_configuration(self):
|
| 76 |
+
"""Validate that either custom amounts or predefined amounts are allowed."""
|
| 77 |
+
if not self.allow_custom_amount and not self.predefined_amounts:
|
| 78 |
+
raise ValueError("Either allow_custom_amount must be True or predefined_amounts must be provided")
|
| 79 |
+
return self
|
| 80 |
+
|
| 81 |
+
@model_validator(mode='after')
|
| 82 |
+
def validate_physical_card_constraints(self):
|
| 83 |
+
"""Validate constraints specific to physical cards."""
|
| 84 |
+
if self.delivery_type == "physical":
|
| 85 |
+
if self.max_issues is None:
|
| 86 |
+
raise ValueError("Physical cards must have max_issues specified")
|
| 87 |
+
if self.reloadable:
|
| 88 |
+
raise ValueError("Physical cards cannot be reloadable")
|
| 89 |
+
return self
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
# Create gift card template request schema
|
| 93 |
+
class GiftCardTemplateCreate(GiftCardTemplateBase):
|
| 94 |
+
"""Schema for creating a new gift card template."""
|
| 95 |
+
pass
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
# Update gift card template request schema
|
| 99 |
+
class GiftCardTemplateUpdate(BaseModel):
|
| 100 |
+
"""Schema for updating an existing gift card template."""
|
| 101 |
+
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
| 102 |
+
description: Optional[str] = Field(None, max_length=500)
|
| 103 |
+
validity_days: Optional[int] = Field(None, ge=1)
|
| 104 |
+
allow_custom_amount: Optional[bool] = None
|
| 105 |
+
predefined_amounts: Optional[List[float]] = None
|
| 106 |
+
reloadable: Optional[bool] = None
|
| 107 |
+
allow_partial_redemption: Optional[bool] = None
|
| 108 |
+
requires_pin: Optional[bool] = None
|
| 109 |
+
requires_otp: Optional[bool] = None
|
| 110 |
+
status: Optional[TemplateStatus] = None
|
| 111 |
+
max_issues: Optional[int] = Field(None, ge=1)
|
| 112 |
+
design_template: Optional[DesignTemplate] = None
|
| 113 |
+
branch_ids: Optional[List[str]] = None
|
| 114 |
+
campaign_id: Optional[str] = None
|
| 115 |
+
metadata: Optional[Dict[str, Any]] = None
|
| 116 |
+
|
| 117 |
+
@field_validator('name')
|
| 118 |
+
@classmethod
|
| 119 |
+
def validate_name(cls, v):
|
| 120 |
+
if v is not None and not v.strip():
|
| 121 |
+
raise ValueError("Name cannot be empty")
|
| 122 |
+
return v.strip() if v else v
|
| 123 |
+
|
| 124 |
+
@field_validator('predefined_amounts')
|
| 125 |
+
@classmethod
|
| 126 |
+
def validate_predefined_amounts(cls, v):
|
| 127 |
+
if v is not None:
|
| 128 |
+
if not v: # Empty list
|
| 129 |
+
raise ValueError("Predefined amounts cannot be an empty list; use null instead")
|
| 130 |
+
if any(amount <= 0 for amount in v):
|
| 131 |
+
raise ValueError("All predefined amounts must be positive")
|
| 132 |
+
if len(set(v)) != len(v):
|
| 133 |
+
raise ValueError("Predefined amounts must be unique")
|
| 134 |
+
return v
|
| 135 |
+
|
| 136 |
+
@field_validator('branch_ids')
|
| 137 |
+
@classmethod
|
| 138 |
+
def validate_branch_ids(cls, v):
|
| 139 |
+
if v is not None and not v: # Empty list
|
| 140 |
+
return None # Convert empty list to None
|
| 141 |
+
return v
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
# Gift card template response schema
|
| 145 |
+
class GiftCardTemplateResponse(GiftCardTemplateBase):
|
| 146 |
+
"""Schema for gift card template responses."""
|
| 147 |
+
id: str = Field(..., description="Unique template ID", alias="_id")
|
| 148 |
+
issued_count: int = Field(..., description="Number of cards issued from this template")
|
| 149 |
+
created_at: datetime = Field(..., description="Creation timestamp")
|
| 150 |
+
updated_at: Optional[datetime] = Field(None, description="Last update timestamp")
|
| 151 |
+
|
| 152 |
+
class Config:
|
| 153 |
+
populate_by_name = True
|
| 154 |
+
json_encoders = {
|
| 155 |
+
datetime: lambda v: v.isoformat() if v else None
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
# Gift card template list response schema
|
| 160 |
+
class GiftCardTemplateListResponse(BaseModel):
|
| 161 |
+
"""Schema for paginated gift card template list responses."""
|
| 162 |
+
templates: List[GiftCardTemplateResponse] = Field(..., description="List of gift card templates")
|
| 163 |
+
total: int = Field(..., description="Total number of templates matching the filter")
|
| 164 |
+
offset: int = Field(..., description="Number of items skipped")
|
| 165 |
+
limit: int = Field(..., description="Maximum number of items returned")
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
# Simple create response schema
|
| 169 |
+
class GiftCardTemplateCreateResponse(BaseModel):
|
| 170 |
+
"""Schema for gift card template creation response."""
|
| 171 |
+
id: str = Field(..., description="Unique template ID")
|
| 172 |
+
name: str = Field(..., description="Template name")
|
| 173 |
+
status: TemplateStatus = Field(..., description="Template status")
|
| 174 |
+
created_at: datetime = Field(..., description="Creation timestamp")
|
| 175 |
+
|
| 176 |
+
class Config:
|
| 177 |
+
json_encoders = {
|
| 178 |
+
datetime: lambda v: v.isoformat() if v else None
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
# Stock availability response schema
|
| 183 |
+
class GiftCardStockResponse(BaseModel):
|
| 184 |
+
"""Schema for gift card stock availability response."""
|
| 185 |
+
available: bool = Field(..., description="Whether the template has available stock")
|
| 186 |
+
unlimited: bool = Field(..., description="Whether the template has unlimited stock")
|
| 187 |
+
remaining: Optional[int] = Field(None, description="Number of cards remaining (null for unlimited)")
|
| 188 |
+
max_issues: Optional[int] = Field(None, description="Maximum number of cards that can be issued")
|
| 189 |
+
issued_count: Optional[int] = Field(None, description="Number of cards already issued")
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
# Filter schema for listing templates
|
| 193 |
+
class GiftCardTemplateFilter(BaseModel):
|
| 194 |
+
"""Schema for filtering gift card templates."""
|
| 195 |
+
status: Optional[TemplateStatus] = Field(None, description="Filter by template status")
|
| 196 |
+
delivery_type: Optional[DeliveryType] = Field(None, description="Filter by delivery type")
|
| 197 |
+
currency: Optional[Currency] = Field(None, description="Filter by currency")
|
| 198 |
+
branch_id: Optional[str] = Field(None, description="Filter by branch ID")
|
| 199 |
+
campaign_id: Optional[str] = Field(None, description="Filter by campaign ID")
|
| 200 |
+
created_by: Optional[str] = Field(None, description="Filter by creator user ID")
|
| 201 |
+
|
| 202 |
+
def to_mongo_filter(self, merchant_id: str) -> Dict[str, Any]:
|
| 203 |
+
"""Convert filter to MongoDB query format."""
|
| 204 |
+
mongo_filter = {"merchant_id": merchant_id} # Always filter by merchant
|
| 205 |
+
|
| 206 |
+
if self.status:
|
| 207 |
+
mongo_filter["status"] = self.status
|
| 208 |
+
if self.delivery_type:
|
| 209 |
+
mongo_filter["delivery_type"] = self.delivery_type
|
| 210 |
+
if self.currency:
|
| 211 |
+
mongo_filter["currency"] = self.currency
|
| 212 |
+
if self.branch_id:
|
| 213 |
+
mongo_filter["$or"] = [
|
| 214 |
+
{"branch_ids": {"$in": [self.branch_id]}},
|
| 215 |
+
{"branch_ids": {"$exists": False}},
|
| 216 |
+
{"branch_ids": None},
|
| 217 |
+
{"branch_ids": []}
|
| 218 |
+
]
|
| 219 |
+
if self.campaign_id:
|
| 220 |
+
mongo_filter["campaign_id"] = self.campaign_id
|
| 221 |
+
if self.created_by:
|
| 222 |
+
mongo_filter["created_by"] = self.created_by
|
| 223 |
+
|
| 224 |
+
return mongo_filter
|
app/services/gift_card_service.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import Any, Dict, List, Optional
|
| 3 |
+
from fastapi import HTTPException
|
| 4 |
+
from pydantic import ValidationError
|
| 5 |
+
|
| 6 |
+
from app.repositories.gift_card_repository import GiftCardRepository
|
| 7 |
+
from app.schemas.gift_card_schema import (
|
| 8 |
+
GiftCardTemplateCreate,
|
| 9 |
+
GiftCardTemplateUpdate,
|
| 10 |
+
GiftCardTemplateResponse,
|
| 11 |
+
GiftCardTemplateListResponse,
|
| 12 |
+
GiftCardTemplateCreateResponse,
|
| 13 |
+
GiftCardTemplateFilter,
|
| 14 |
+
GiftCardStockResponse
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
# Configure logging for this module
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
# Constants for error messages
|
| 21 |
+
INTERNAL_SERVER_ERROR = "Internal server error"
|
| 22 |
+
TEMPLATE_NOT_FOUND = "Gift card template not found"
|
| 23 |
+
TEMPLATE_CREATION_FAILED = "Failed to create gift card template"
|
| 24 |
+
TEMPLATE_UPDATE_FAILED = "Failed to update gift card template"
|
| 25 |
+
TEMPLATE_DELETE_FAILED = "Failed to delete gift card template"
|
| 26 |
+
|
| 27 |
+
class GiftCardService:
|
| 28 |
+
"""
|
| 29 |
+
Service layer for Gift Card operations.
|
| 30 |
+
Contains business logic and validation for gift card templates.
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
@staticmethod
|
| 34 |
+
async def create_template(
|
| 35 |
+
template_data: GiftCardTemplateCreate,
|
| 36 |
+
merchant_id: str,
|
| 37 |
+
created_by: str
|
| 38 |
+
) -> GiftCardTemplateCreateResponse:
|
| 39 |
+
"""
|
| 40 |
+
Create a new gift card template.
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
template_data (GiftCardTemplateCreate): Template data to create
|
| 44 |
+
merchant_id (str): Merchant ID
|
| 45 |
+
created_by (str): User ID who is creating the template
|
| 46 |
+
|
| 47 |
+
Returns:
|
| 48 |
+
GiftCardTemplateCreateResponse: Created template info
|
| 49 |
+
|
| 50 |
+
Raises:
|
| 51 |
+
HTTPException: If creation fails
|
| 52 |
+
"""
|
| 53 |
+
try:
|
| 54 |
+
# Convert Pydantic model to dict and add merchant info
|
| 55 |
+
data = template_data.model_dump(by_alias=True, exclude_none=True)
|
| 56 |
+
data['merchant_id'] = merchant_id
|
| 57 |
+
data['created_by'] = created_by
|
| 58 |
+
|
| 59 |
+
logger.info(f"Creating gift card template for merchant {merchant_id}")
|
| 60 |
+
|
| 61 |
+
# Create template via repository
|
| 62 |
+
template_id = await GiftCardRepository.create_template(data)
|
| 63 |
+
|
| 64 |
+
# Get the created template to return full info
|
| 65 |
+
created_template = await GiftCardRepository.get_template_by_id(template_id)
|
| 66 |
+
if not created_template:
|
| 67 |
+
raise HTTPException(status_code=500, detail="Template created but could not be retrieved")
|
| 68 |
+
|
| 69 |
+
return GiftCardTemplateCreateResponse(
|
| 70 |
+
id=created_template['_id'],
|
| 71 |
+
name=created_template['name'],
|
| 72 |
+
status=created_template['status'],
|
| 73 |
+
created_at=created_template['created_at']
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
except ValidationError as ve:
|
| 77 |
+
logger.error(f"Validation error creating template: {ve}")
|
| 78 |
+
raise HTTPException(status_code=400, detail=f"Validation error: {ve}")
|
| 79 |
+
except HTTPException:
|
| 80 |
+
raise
|
| 81 |
+
except Exception as e:
|
| 82 |
+
logger.error(f"Service error creating template: {e}", exc_info=True)
|
| 83 |
+
raise HTTPException(status_code=500, detail=TEMPLATE_CREATION_FAILED)
|
| 84 |
+
|
| 85 |
+
@staticmethod
|
| 86 |
+
async def get_templates(
|
| 87 |
+
merchant_id: str,
|
| 88 |
+
filters: Optional[GiftCardTemplateFilter] = None,
|
| 89 |
+
offset: int = 0,
|
| 90 |
+
limit: int = 100
|
| 91 |
+
) -> GiftCardTemplateListResponse:
|
| 92 |
+
"""
|
| 93 |
+
Get gift card templates with pagination and filtering.
|
| 94 |
+
|
| 95 |
+
Args:
|
| 96 |
+
merchant_id (str): Merchant ID
|
| 97 |
+
filters (Optional[GiftCardTemplateFilter]): Filter criteria
|
| 98 |
+
offset (int): Number of items to skip
|
| 99 |
+
limit (int): Maximum number of items to return
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
GiftCardTemplateListResponse: Paginated list of templates
|
| 103 |
+
|
| 104 |
+
Raises:
|
| 105 |
+
HTTPException: If retrieval fails
|
| 106 |
+
"""
|
| 107 |
+
try:
|
| 108 |
+
# Build filter criteria
|
| 109 |
+
filter_criteria = GiftCardRepository.build_filter_criteria(merchant_id, filters)
|
| 110 |
+
|
| 111 |
+
logger.info(f"Fetching templates for merchant {merchant_id} with filters: {filter_criteria}")
|
| 112 |
+
|
| 113 |
+
# Get templates from repository
|
| 114 |
+
result = await GiftCardRepository.get_templates(filter_criteria, offset, limit)
|
| 115 |
+
|
| 116 |
+
# Convert to response models
|
| 117 |
+
templates = []
|
| 118 |
+
for template_data in result['templates']:
|
| 119 |
+
try:
|
| 120 |
+
templates.append(GiftCardTemplateResponse(**template_data))
|
| 121 |
+
except ValidationError as ve:
|
| 122 |
+
logger.warning(f"Failed to parse template: {template_data}, error: {ve}")
|
| 123 |
+
continue
|
| 124 |
+
|
| 125 |
+
return GiftCardTemplateListResponse(
|
| 126 |
+
templates=templates,
|
| 127 |
+
total=result['total'],
|
| 128 |
+
offset=offset,
|
| 129 |
+
limit=limit
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
except HTTPException:
|
| 133 |
+
raise
|
| 134 |
+
except Exception as e:
|
| 135 |
+
logger.error(f"Service error getting templates: {e}", exc_info=True)
|
| 136 |
+
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR)
|
| 137 |
+
|
| 138 |
+
@staticmethod
|
| 139 |
+
async def get_template_by_id(
|
| 140 |
+
template_id: str,
|
| 141 |
+
merchant_id: str
|
| 142 |
+
) -> GiftCardTemplateResponse:
|
| 143 |
+
"""
|
| 144 |
+
Get a specific gift card template by ID.
|
| 145 |
+
|
| 146 |
+
Args:
|
| 147 |
+
template_id (str): Template ID to retrieve
|
| 148 |
+
merchant_id (str): Merchant ID for security
|
| 149 |
+
|
| 150 |
+
Returns:
|
| 151 |
+
GiftCardTemplateResponse: Template data
|
| 152 |
+
|
| 153 |
+
Raises:
|
| 154 |
+
HTTPException: If template not found or access denied
|
| 155 |
+
"""
|
| 156 |
+
try:
|
| 157 |
+
logger.info(f"Fetching template {template_id} for merchant {merchant_id}")
|
| 158 |
+
|
| 159 |
+
template = await GiftCardRepository.get_template_by_id(template_id)
|
| 160 |
+
|
| 161 |
+
if not template:
|
| 162 |
+
raise HTTPException(status_code=404, detail=TEMPLATE_NOT_FOUND)
|
| 163 |
+
|
| 164 |
+
# Check if template belongs to the merchant
|
| 165 |
+
if template.get('merchant_id') != merchant_id:
|
| 166 |
+
raise HTTPException(status_code=404, detail=TEMPLATE_NOT_FOUND)
|
| 167 |
+
|
| 168 |
+
return GiftCardTemplateResponse(**template)
|
| 169 |
+
|
| 170 |
+
except HTTPException:
|
| 171 |
+
raise
|
| 172 |
+
except ValidationError as ve:
|
| 173 |
+
logger.error(f"Validation error parsing template {template_id}: {ve}")
|
| 174 |
+
raise HTTPException(status_code=500, detail="Template data validation failed")
|
| 175 |
+
except Exception as e:
|
| 176 |
+
logger.error(f"Service error getting template {template_id}: {e}", exc_info=True)
|
| 177 |
+
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR)
|
| 178 |
+
|
| 179 |
+
@staticmethod
|
| 180 |
+
async def update_template(
|
| 181 |
+
template_id: str,
|
| 182 |
+
update_data: GiftCardTemplateUpdate,
|
| 183 |
+
merchant_id: str
|
| 184 |
+
) -> GiftCardTemplateResponse:
|
| 185 |
+
"""
|
| 186 |
+
Update a gift card template.
|
| 187 |
+
|
| 188 |
+
Args:
|
| 189 |
+
template_id (str): Template ID to update
|
| 190 |
+
update_data (GiftCardTemplateUpdate): Update data
|
| 191 |
+
merchant_id (str): Merchant ID for security
|
| 192 |
+
|
| 193 |
+
Returns:
|
| 194 |
+
GiftCardTemplateResponse: Updated template data
|
| 195 |
+
|
| 196 |
+
Raises:
|
| 197 |
+
HTTPException: If update fails or template not found
|
| 198 |
+
"""
|
| 199 |
+
try:
|
| 200 |
+
logger.info(f"Updating template {template_id} for merchant {merchant_id}")
|
| 201 |
+
|
| 202 |
+
# First verify the template exists and belongs to the merchant
|
| 203 |
+
existing_template = await GiftCardRepository.get_template_by_id(template_id)
|
| 204 |
+
if not existing_template:
|
| 205 |
+
raise HTTPException(status_code=404, detail=TEMPLATE_NOT_FOUND)
|
| 206 |
+
|
| 207 |
+
if existing_template.get('merchant_id') != merchant_id:
|
| 208 |
+
raise HTTPException(status_code=404, detail=TEMPLATE_NOT_FOUND)
|
| 209 |
+
|
| 210 |
+
# Convert update data to dict, excluding None values
|
| 211 |
+
update_dict = update_data.model_dump(by_alias=True, exclude_none=True)
|
| 212 |
+
|
| 213 |
+
if not update_dict:
|
| 214 |
+
raise HTTPException(status_code=400, detail="No update data provided")
|
| 215 |
+
|
| 216 |
+
# Validate business rules for updates
|
| 217 |
+
await GiftCardService._validate_template_update(existing_template, update_dict)
|
| 218 |
+
|
| 219 |
+
# Perform update
|
| 220 |
+
success = await GiftCardRepository.update_template(template_id, update_dict)
|
| 221 |
+
|
| 222 |
+
if not success:
|
| 223 |
+
raise HTTPException(status_code=500, detail=TEMPLATE_UPDATE_FAILED)
|
| 224 |
+
|
| 225 |
+
# Return updated template
|
| 226 |
+
updated_template = await GiftCardRepository.get_template_by_id(template_id)
|
| 227 |
+
return GiftCardTemplateResponse(**updated_template)
|
| 228 |
+
|
| 229 |
+
except HTTPException:
|
| 230 |
+
raise
|
| 231 |
+
except ValidationError as ve:
|
| 232 |
+
logger.error(f"Validation error updating template {template_id}: {ve}")
|
| 233 |
+
raise HTTPException(status_code=400, detail=f"Validation error: {ve}")
|
| 234 |
+
except Exception as e:
|
| 235 |
+
logger.error(f"Service error updating template {template_id}: {e}", exc_info=True)
|
| 236 |
+
raise HTTPException(status_code=500, detail=TEMPLATE_UPDATE_FAILED)
|
| 237 |
+
|
| 238 |
+
@staticmethod
|
| 239 |
+
async def delete_template(
|
| 240 |
+
template_id: str,
|
| 241 |
+
merchant_id: str
|
| 242 |
+
) -> Dict[str, str]:
|
| 243 |
+
"""
|
| 244 |
+
Delete a gift card template.
|
| 245 |
+
|
| 246 |
+
Args:
|
| 247 |
+
template_id (str): Template ID to delete
|
| 248 |
+
merchant_id (str): Merchant ID for security
|
| 249 |
+
|
| 250 |
+
Returns:
|
| 251 |
+
Dict[str, str]: Success message
|
| 252 |
+
|
| 253 |
+
Raises:
|
| 254 |
+
HTTPException: If deletion fails or template not found
|
| 255 |
+
"""
|
| 256 |
+
try:
|
| 257 |
+
logger.info(f"Deleting template {template_id} for merchant {merchant_id}")
|
| 258 |
+
|
| 259 |
+
# First verify the template exists and belongs to the merchant
|
| 260 |
+
existing_template = await GiftCardRepository.get_template_by_id(template_id)
|
| 261 |
+
if not existing_template:
|
| 262 |
+
raise HTTPException(status_code=404, detail=TEMPLATE_NOT_FOUND)
|
| 263 |
+
|
| 264 |
+
if existing_template.get('merchant_id') != merchant_id:
|
| 265 |
+
raise HTTPException(status_code=404, detail=TEMPLATE_NOT_FOUND)
|
| 266 |
+
|
| 267 |
+
# Check if template can be deleted (business rule: no issued cards)
|
| 268 |
+
issued_count = existing_template.get('issued_count', 0)
|
| 269 |
+
if issued_count > 0:
|
| 270 |
+
raise HTTPException(
|
| 271 |
+
status_code=400,
|
| 272 |
+
detail=f"Cannot delete template with {issued_count} issued cards. Consider deactivating instead."
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
# Perform deletion
|
| 276 |
+
success = await GiftCardRepository.delete_template(template_id)
|
| 277 |
+
|
| 278 |
+
if not success:
|
| 279 |
+
raise HTTPException(status_code=500, detail=TEMPLATE_DELETE_FAILED)
|
| 280 |
+
|
| 281 |
+
return {"message": f"Template {template_id} deleted successfully"}
|
| 282 |
+
|
| 283 |
+
except HTTPException:
|
| 284 |
+
raise
|
| 285 |
+
except Exception as e:
|
| 286 |
+
logger.error(f"Service error deleting template {template_id}: {e}", exc_info=True)
|
| 287 |
+
raise HTTPException(status_code=500, detail=TEMPLATE_DELETE_FAILED)
|
| 288 |
+
|
| 289 |
+
@staticmethod
|
| 290 |
+
async def check_stock_availability(
|
| 291 |
+
template_id: str,
|
| 292 |
+
merchant_id: str
|
| 293 |
+
) -> GiftCardStockResponse:
|
| 294 |
+
"""
|
| 295 |
+
Check stock availability for a template.
|
| 296 |
+
|
| 297 |
+
Args:
|
| 298 |
+
template_id (str): Template ID to check
|
| 299 |
+
merchant_id (str): Merchant ID for security
|
| 300 |
+
|
| 301 |
+
Returns:
|
| 302 |
+
GiftCardStockResponse: Stock availability info
|
| 303 |
+
|
| 304 |
+
Raises:
|
| 305 |
+
HTTPException: If template not found or access denied
|
| 306 |
+
"""
|
| 307 |
+
try:
|
| 308 |
+
logger.info(f"Checking stock for template {template_id}")
|
| 309 |
+
|
| 310 |
+
# First verify the template exists and belongs to the merchant
|
| 311 |
+
existing_template = await GiftCardRepository.get_template_by_id(template_id)
|
| 312 |
+
if not existing_template:
|
| 313 |
+
raise HTTPException(status_code=404, detail=TEMPLATE_NOT_FOUND)
|
| 314 |
+
|
| 315 |
+
if existing_template.get('merchant_id') != merchant_id:
|
| 316 |
+
raise HTTPException(status_code=404, detail=TEMPLATE_NOT_FOUND)
|
| 317 |
+
|
| 318 |
+
# Get stock information
|
| 319 |
+
stock_info = await GiftCardRepository.check_stock_availability(template_id)
|
| 320 |
+
|
| 321 |
+
return GiftCardStockResponse(**stock_info)
|
| 322 |
+
|
| 323 |
+
except HTTPException:
|
| 324 |
+
raise
|
| 325 |
+
except Exception as e:
|
| 326 |
+
logger.error(f"Service error checking stock for template {template_id}: {e}", exc_info=True)
|
| 327 |
+
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR)
|
| 328 |
+
|
| 329 |
+
@staticmethod
|
| 330 |
+
async def get_active_templates(merchant_id: str) -> List[GiftCardTemplateResponse]:
|
| 331 |
+
"""
|
| 332 |
+
Get active templates for a merchant.
|
| 333 |
+
|
| 334 |
+
Args:
|
| 335 |
+
merchant_id (str): Merchant ID
|
| 336 |
+
|
| 337 |
+
Returns:
|
| 338 |
+
List[GiftCardTemplateResponse]: List of active templates
|
| 339 |
+
"""
|
| 340 |
+
try:
|
| 341 |
+
templates_data = await GiftCardRepository.get_active_templates(merchant_id)
|
| 342 |
+
|
| 343 |
+
templates = []
|
| 344 |
+
for template_data in templates_data:
|
| 345 |
+
try:
|
| 346 |
+
templates.append(GiftCardTemplateResponse(**template_data))
|
| 347 |
+
except ValidationError as ve:
|
| 348 |
+
logger.warning(f"Failed to parse active template: {template_data}, error: {ve}")
|
| 349 |
+
continue
|
| 350 |
+
|
| 351 |
+
return templates
|
| 352 |
+
|
| 353 |
+
except Exception as e:
|
| 354 |
+
logger.error(f"Service error getting active templates: {e}", exc_info=True)
|
| 355 |
+
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR)
|
| 356 |
+
|
| 357 |
+
@staticmethod
|
| 358 |
+
async def _validate_template_update(
|
| 359 |
+
existing_template: Dict[str, Any],
|
| 360 |
+
update_data: Dict[str, Any]
|
| 361 |
+
) -> None:
|
| 362 |
+
"""
|
| 363 |
+
Validate business rules for template updates.
|
| 364 |
+
|
| 365 |
+
Args:
|
| 366 |
+
existing_template (Dict[str, Any]): Current template data
|
| 367 |
+
update_data (Dict[str, Any]): Proposed updates
|
| 368 |
+
|
| 369 |
+
Raises:
|
| 370 |
+
HTTPException: If validation fails
|
| 371 |
+
"""
|
| 372 |
+
# Check if trying to change delivery_type
|
| 373 |
+
if 'delivery_type' in update_data:
|
| 374 |
+
current_delivery_type = existing_template.get('delivery_type')
|
| 375 |
+
new_delivery_type = update_data['delivery_type']
|
| 376 |
+
|
| 377 |
+
if current_delivery_type != new_delivery_type:
|
| 378 |
+
raise HTTPException(
|
| 379 |
+
status_code=400,
|
| 380 |
+
detail="Cannot change delivery_type of an existing template"
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
# Check physical card constraints
|
| 384 |
+
if existing_template.get('delivery_type') == 'physical':
|
| 385 |
+
if 'reloadable' in update_data and update_data['reloadable']:
|
| 386 |
+
raise HTTPException(
|
| 387 |
+
status_code=400,
|
| 388 |
+
detail="Physical cards cannot be made reloadable"
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
if 'max_issues' in update_data and update_data['max_issues'] is None:
|
| 392 |
+
raise HTTPException(
|
| 393 |
+
status_code=400,
|
| 394 |
+
detail="Physical cards must have max_issues specified"
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
# Check if trying to reduce max_issues below issued_count
|
| 398 |
+
if 'max_issues' in update_data:
|
| 399 |
+
issued_count = existing_template.get('issued_count', 0)
|
| 400 |
+
new_max_issues = update_data['max_issues']
|
| 401 |
+
|
| 402 |
+
if new_max_issues is not None and new_max_issues < issued_count:
|
| 403 |
+
raise HTTPException(
|
| 404 |
+
status_code=400,
|
| 405 |
+
detail=f"Cannot set max_issues ({new_max_issues}) below issued_count ({issued_count})"
|
| 406 |
+
)
|
test_gift_card_implementation.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Test script for Gift Card Template API endpoints.
|
| 4 |
+
This script demonstrates the usage of the gift card template endpoints.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import asyncio
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
# Add the app directory to Python path
|
| 12 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
| 13 |
+
|
| 14 |
+
from app.schemas.gift_card_schema import (
|
| 15 |
+
GiftCardTemplateCreate,
|
| 16 |
+
GiftCardTemplateUpdate,
|
| 17 |
+
DesignTemplate
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
def test_gift_card_schemas():
|
| 21 |
+
"""Test gift card schema validation."""
|
| 22 |
+
print("Testing Gift Card Template Schemas...")
|
| 23 |
+
|
| 24 |
+
# Test creating a digital gift card template
|
| 25 |
+
try:
|
| 26 |
+
digital_template = GiftCardTemplateCreate(
|
| 27 |
+
name="Digital Birthday Card",
|
| 28 |
+
description="Special digital card for birthdays",
|
| 29 |
+
delivery_type="digital",
|
| 30 |
+
currency="INR",
|
| 31 |
+
validity_days=365,
|
| 32 |
+
allow_custom_amount=True,
|
| 33 |
+
predefined_amounts=[500, 1000, 2000],
|
| 34 |
+
reloadable=False,
|
| 35 |
+
allow_partial_redemption=True,
|
| 36 |
+
requires_pin=True,
|
| 37 |
+
requires_otp=False,
|
| 38 |
+
status="active",
|
| 39 |
+
design_template=DesignTemplate(
|
| 40 |
+
theme="blue",
|
| 41 |
+
image_url="https://example.com/birthday.png"
|
| 42 |
+
),
|
| 43 |
+
created_by="admin_123"
|
| 44 |
+
)
|
| 45 |
+
print("β Digital template schema validation passed")
|
| 46 |
+
print(f" Template name: {digital_template.name}")
|
| 47 |
+
print(f" Delivery type: {digital_template.delivery_type}")
|
| 48 |
+
print(f" Currency: {digital_template.currency}")
|
| 49 |
+
except Exception as e:
|
| 50 |
+
print(f"β Digital template schema validation failed: {e}")
|
| 51 |
+
return False
|
| 52 |
+
|
| 53 |
+
# Test creating a physical gift card template
|
| 54 |
+
try:
|
| 55 |
+
physical_template = GiftCardTemplateCreate(
|
| 56 |
+
name="Physical Premium βΉ1000",
|
| 57 |
+
description="Premium physical plastic card with fixed denomination",
|
| 58 |
+
delivery_type="physical",
|
| 59 |
+
currency="INR",
|
| 60 |
+
validity_days=730,
|
| 61 |
+
allow_custom_amount=False,
|
| 62 |
+
predefined_amounts=[1000],
|
| 63 |
+
reloadable=False,
|
| 64 |
+
allow_partial_redemption=True,
|
| 65 |
+
requires_pin=True,
|
| 66 |
+
requires_otp=False,
|
| 67 |
+
status="active",
|
| 68 |
+
max_issues=500,
|
| 69 |
+
design_template=DesignTemplate(
|
| 70 |
+
theme="gold",
|
| 71 |
+
image_url="https://example.com/physical.png"
|
| 72 |
+
),
|
| 73 |
+
branch_ids=["branch_1", "branch_2"],
|
| 74 |
+
campaign_id="campaign_holiday_2025",
|
| 75 |
+
metadata={"batch": "2025_holiday"},
|
| 76 |
+
created_by="marketing_mgr"
|
| 77 |
+
)
|
| 78 |
+
print("β Physical template schema validation passed")
|
| 79 |
+
print(f" Template name: {physical_template.name}")
|
| 80 |
+
print(f" Max issues: {physical_template.max_issues}")
|
| 81 |
+
print(f" Branch IDs: {physical_template.branch_ids}")
|
| 82 |
+
except Exception as e:
|
| 83 |
+
print(f"β Physical template schema validation failed: {e}")
|
| 84 |
+
return False
|
| 85 |
+
|
| 86 |
+
# Test update schema
|
| 87 |
+
try:
|
| 88 |
+
update_template = GiftCardTemplateUpdate(
|
| 89 |
+
status="inactive",
|
| 90 |
+
predefined_amounts=[1000, 2000]
|
| 91 |
+
)
|
| 92 |
+
print("β Update template schema validation passed")
|
| 93 |
+
print(f" Status update: {update_template.status}")
|
| 94 |
+
print(f" Amounts update: {update_template.predefined_amounts}")
|
| 95 |
+
except Exception as e:
|
| 96 |
+
print(f"β Update template schema validation failed: {e}")
|
| 97 |
+
return False
|
| 98 |
+
|
| 99 |
+
# Test validation errors
|
| 100 |
+
try:
|
| 101 |
+
# This should fail - physical card without max_issues
|
| 102 |
+
GiftCardTemplateCreate(
|
| 103 |
+
name="Invalid Physical Card",
|
| 104 |
+
delivery_type="physical",
|
| 105 |
+
currency="INR",
|
| 106 |
+
allow_custom_amount=True,
|
| 107 |
+
reloadable=False,
|
| 108 |
+
allow_partial_redemption=True,
|
| 109 |
+
requires_pin=True,
|
| 110 |
+
requires_otp=False,
|
| 111 |
+
status="active",
|
| 112 |
+
created_by="admin_123"
|
| 113 |
+
)
|
| 114 |
+
print("β Validation error test failed - should have caught missing max_issues")
|
| 115 |
+
return False
|
| 116 |
+
except ValueError as e:
|
| 117 |
+
print("β Validation error correctly caught:", str(e))
|
| 118 |
+
|
| 119 |
+
try:
|
| 120 |
+
# This should fail - no amounts configuration
|
| 121 |
+
GiftCardTemplateCreate(
|
| 122 |
+
name="Invalid Amounts Card",
|
| 123 |
+
delivery_type="digital",
|
| 124 |
+
currency="INR",
|
| 125 |
+
allow_custom_amount=False,
|
| 126 |
+
reloadable=False,
|
| 127 |
+
allow_partial_redemption=True,
|
| 128 |
+
requires_pin=True,
|
| 129 |
+
requires_otp=False,
|
| 130 |
+
status="active",
|
| 131 |
+
created_by="admin_123"
|
| 132 |
+
)
|
| 133 |
+
print("β Validation error test failed - should have caught missing amounts config")
|
| 134 |
+
return False
|
| 135 |
+
except ValueError as e:
|
| 136 |
+
print("β Validation error correctly caught:", str(e))
|
| 137 |
+
|
| 138 |
+
return True
|
| 139 |
+
|
| 140 |
+
def test_models_import():
|
| 141 |
+
"""Test that all models can be imported successfully."""
|
| 142 |
+
try:
|
| 143 |
+
from app.models.gift_card_models import GiftCardTemplateModel
|
| 144 |
+
from app.repositories.gift_card_repository import GiftCardRepository
|
| 145 |
+
from app.services.gift_card_service import GiftCardService
|
| 146 |
+
from app.routers.gift_card_router import router
|
| 147 |
+
|
| 148 |
+
print("β All modules imported successfully")
|
| 149 |
+
print(f" Model: {GiftCardTemplateModel.__name__}")
|
| 150 |
+
print(f" Repository: {GiftCardRepository.__name__}")
|
| 151 |
+
print(f" Service: {GiftCardService.__name__}")
|
| 152 |
+
print(f" Router: {router.__class__.__name__}")
|
| 153 |
+
return True
|
| 154 |
+
except ImportError as e:
|
| 155 |
+
print(f"β Import error: {e}")
|
| 156 |
+
return False
|
| 157 |
+
|
| 158 |
+
def main():
|
| 159 |
+
"""Run all tests."""
|
| 160 |
+
print("π― Gift Card Template Implementation Test\n")
|
| 161 |
+
|
| 162 |
+
# Test imports
|
| 163 |
+
print("1. Testing module imports...")
|
| 164 |
+
import_success = test_models_import()
|
| 165 |
+
print()
|
| 166 |
+
|
| 167 |
+
# Test schemas
|
| 168 |
+
print("2. Testing schema validation...")
|
| 169 |
+
schema_success = test_gift_card_schemas()
|
| 170 |
+
print()
|
| 171 |
+
|
| 172 |
+
# Summary
|
| 173 |
+
print("π Test Summary:")
|
| 174 |
+
print(f" Module imports: {'β PASS' if import_success else 'β FAIL'}")
|
| 175 |
+
print(f" Schema validation: {'β PASS' if schema_success else 'β FAIL'}")
|
| 176 |
+
|
| 177 |
+
if import_success and schema_success:
|
| 178 |
+
print("\nπ All tests passed! Gift Card Template implementation is working correctly.")
|
| 179 |
+
print("\nπ Available endpoints:")
|
| 180 |
+
print(" POST /api/v1/giftcard/ - Create gift card template")
|
| 181 |
+
print(" GET /api/v1/giftcard/ - List gift card templates")
|
| 182 |
+
print(" GET /api/v1/giftcard/{id} - Get specific template")
|
| 183 |
+
print(" PUT /api/v1/giftcard/{id} - Update template")
|
| 184 |
+
print(" DELETE /api/v1/giftcard/{id} - Delete template")
|
| 185 |
+
print(" GET /api/v1/giftcard/{id}/stock - Check stock availability")
|
| 186 |
+
print(" GET /api/v1/giftcard/active/list - Get active templates")
|
| 187 |
+
return True
|
| 188 |
+
else:
|
| 189 |
+
print("\nβ Some tests failed. Please check the implementation.")
|
| 190 |
+
return False
|
| 191 |
+
|
| 192 |
+
if __name__ == "__main__":
|
| 193 |
+
main()
|