Spaces:
Running
feat(order): Implement order management service with create, retrieve, and list endpoints
Browse files- Add order creation endpoint with JWT authentication and product validation
- Implement order retrieval endpoint to fetch individual order details
- Add advanced order listing endpoint with filtering, projection, and sorting support
- Create Order model with sales_order, sales_order_items, and sales_order_addresses tables
- Define OrderSchema and OrderListSchema for request/response validation
- Add OrderService with business logic for order processing and database operations
- Implement automatic tax calculation and unique order number generation
- Add comprehensive API documentation with quick reference guide and feature README
- Add startup guide for order service configuration and deployment
- Integrate order router into main application with /orders endpoint prefix
- Validate single merchant per order and product availability from catalogue
- Support optional billing address with automatic fallback to shipping address
- ORDER_API_QUICK_REF.md +356 -0
- ORDER_FEATURE_README.md +333 -0
- START_ORDER_SERVICE.md +264 -0
- app/main.py +2 -0
- app/order/__init__.py +4 -0
- app/order/controllers/__init__.py +1 -0
- app/order/controllers/router.py +161 -0
- app/order/models/__init__.py +4 -0
- app/order/models/model.py +151 -0
- app/order/schemas/__init__.py +22 -0
- app/order/schemas/schema.py +140 -0
- app/order/services/__init__.py +4 -0
- app/order/services/service.py +491 -0
- app/sql.py +3 -1
|
@@ -0,0 +1,356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Order API Quick Reference
|
| 2 |
+
|
| 3 |
+
## Endpoints
|
| 4 |
+
|
| 5 |
+
### 1. Create Order
|
| 6 |
+
```
|
| 7 |
+
POST /orders/create
|
| 8 |
+
Authorization: Bearer <customer_token>
|
| 9 |
+
```
|
| 10 |
+
|
| 11 |
+
**Request Body:**
|
| 12 |
+
```json
|
| 13 |
+
{
|
| 14 |
+
"items": [
|
| 15 |
+
{
|
| 16 |
+
"catalogue_id": "string",
|
| 17 |
+
"quantity": 1
|
| 18 |
+
}
|
| 19 |
+
],
|
| 20 |
+
"shipping_address": {
|
| 21 |
+
"address_type": "shipping",
|
| 22 |
+
"line1": "string",
|
| 23 |
+
"city": "string",
|
| 24 |
+
"state": "string",
|
| 25 |
+
"postal_code": "string",
|
| 26 |
+
"country": "India"
|
| 27 |
+
},
|
| 28 |
+
"billing_address": { // Optional, defaults to shipping
|
| 29 |
+
"address_type": "billing",
|
| 30 |
+
"line1": "string",
|
| 31 |
+
"city": "string",
|
| 32 |
+
"state": "string",
|
| 33 |
+
"postal_code": "string",
|
| 34 |
+
"country": "India"
|
| 35 |
+
},
|
| 36 |
+
"customer_name": "string",
|
| 37 |
+
"customer_phone": "string",
|
| 38 |
+
"customer_email": "string",
|
| 39 |
+
"payment_method": "string",
|
| 40 |
+
"notes": "string"
|
| 41 |
+
}
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
**Response (201):**
|
| 45 |
+
```json
|
| 46 |
+
{
|
| 47 |
+
"success": true,
|
| 48 |
+
"message": "Order created successfully",
|
| 49 |
+
"data": {
|
| 50 |
+
"sales_order_id": "uuid",
|
| 51 |
+
"order_number": "ORD-20260207-0001",
|
| 52 |
+
"merchant_id": "uuid",
|
| 53 |
+
"order_date": "2026-02-07T10:30:00",
|
| 54 |
+
"status": "pending",
|
| 55 |
+
"customer_id": "string",
|
| 56 |
+
"subtotal": 1000.00,
|
| 57 |
+
"total_tax": 180.00,
|
| 58 |
+
"grand_total": 1180.00,
|
| 59 |
+
"payment_status": "pending",
|
| 60 |
+
"fulfillment_status": "pending",
|
| 61 |
+
"items": [...],
|
| 62 |
+
"addresses": [...]
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
---
|
| 68 |
+
|
| 69 |
+
### 2. Get Order Details
|
| 70 |
+
```
|
| 71 |
+
GET /orders/{order_id}
|
| 72 |
+
Authorization: Bearer <customer_token>
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
**Response (200):**
|
| 76 |
+
```json
|
| 77 |
+
{
|
| 78 |
+
"success": true,
|
| 79 |
+
"data": {
|
| 80 |
+
"sales_order_id": "uuid",
|
| 81 |
+
"order_number": "ORD-20260207-0001",
|
| 82 |
+
"status": "pending",
|
| 83 |
+
"grand_total": 1180.00,
|
| 84 |
+
"items": [
|
| 85 |
+
{
|
| 86 |
+
"id": "uuid",
|
| 87 |
+
"product_id": "CAT-001",
|
| 88 |
+
"product_name": "Product Name",
|
| 89 |
+
"quantity": 2,
|
| 90 |
+
"unit_price": 500.00,
|
| 91 |
+
"line_total": 1180.00
|
| 92 |
+
}
|
| 93 |
+
],
|
| 94 |
+
"addresses": [...]
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
---
|
| 100 |
+
|
| 101 |
+
### 3. List Orders (Simple)
|
| 102 |
+
```
|
| 103 |
+
GET /orders/?skip=0&limit=10
|
| 104 |
+
Authorization: Bearer <customer_token>
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
**Response (200):**
|
| 108 |
+
```json
|
| 109 |
+
{
|
| 110 |
+
"success": true,
|
| 111 |
+
"data": {
|
| 112 |
+
"total": 25,
|
| 113 |
+
"orders": [...]
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
---
|
| 119 |
+
|
| 120 |
+
### 4. List Orders (Advanced with Projection)
|
| 121 |
+
```
|
| 122 |
+
POST /orders/list
|
| 123 |
+
Authorization: Bearer <customer_token>
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
**Request Body:**
|
| 127 |
+
```json
|
| 128 |
+
{
|
| 129 |
+
"filters": {
|
| 130 |
+
"status": "pending",
|
| 131 |
+
"payment_status": "pending",
|
| 132 |
+
"fulfillment_status": "pending"
|
| 133 |
+
},
|
| 134 |
+
"skip": 0,
|
| 135 |
+
"limit": 10,
|
| 136 |
+
"projection_list": [
|
| 137 |
+
"sales_order_id",
|
| 138 |
+
"order_number",
|
| 139 |
+
"order_date",
|
| 140 |
+
"status",
|
| 141 |
+
"grand_total",
|
| 142 |
+
"payment_status"
|
| 143 |
+
],
|
| 144 |
+
"sort_by": "created_at",
|
| 145 |
+
"sort_order": "desc"
|
| 146 |
+
}
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
**Response (200):**
|
| 150 |
+
```json
|
| 151 |
+
{
|
| 152 |
+
"total": 25,
|
| 153 |
+
"skip": 0,
|
| 154 |
+
"limit": 10,
|
| 155 |
+
"orders": [
|
| 156 |
+
{
|
| 157 |
+
"sales_order_id": "uuid",
|
| 158 |
+
"order_number": "ORD-20260207-0001",
|
| 159 |
+
"order_date": "2026-02-07T10:30:00",
|
| 160 |
+
"status": "pending",
|
| 161 |
+
"grand_total": 1180.00,
|
| 162 |
+
"payment_status": "pending"
|
| 163 |
+
}
|
| 164 |
+
]
|
| 165 |
+
}
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
---
|
| 169 |
+
|
| 170 |
+
## Order Statuses
|
| 171 |
+
|
| 172 |
+
### Order Status
|
| 173 |
+
- `pending` - Order created, awaiting confirmation
|
| 174 |
+
- `confirmed` - Order confirmed by merchant
|
| 175 |
+
- `processing` - Order being prepared
|
| 176 |
+
- `shipped` - Order shipped
|
| 177 |
+
- `delivered` - Order delivered
|
| 178 |
+
- `cancelled` - Order cancelled
|
| 179 |
+
|
| 180 |
+
### Payment Status
|
| 181 |
+
- `pending` - Payment not received
|
| 182 |
+
- `paid` - Payment completed
|
| 183 |
+
- `failed` - Payment failed
|
| 184 |
+
- `refunded` - Payment refunded
|
| 185 |
+
|
| 186 |
+
### Fulfillment Status
|
| 187 |
+
- `pending` - Not yet fulfilled
|
| 188 |
+
- `processing` - Being prepared
|
| 189 |
+
- `shipped` - Shipped to customer
|
| 190 |
+
- `delivered` - Delivered to customer
|
| 191 |
+
- `cancelled` - Fulfillment cancelled
|
| 192 |
+
|
| 193 |
+
---
|
| 194 |
+
|
| 195 |
+
## Error Responses
|
| 196 |
+
|
| 197 |
+
### 400 Bad Request
|
| 198 |
+
```json
|
| 199 |
+
{
|
| 200 |
+
"detail": "Product not found: CAT-001"
|
| 201 |
+
}
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
### 401 Unauthorized
|
| 205 |
+
```json
|
| 206 |
+
{
|
| 207 |
+
"detail": "Could not validate credentials"
|
| 208 |
+
}
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
### 403 Forbidden
|
| 212 |
+
```json
|
| 213 |
+
{
|
| 214 |
+
"detail": "This endpoint requires customer authentication"
|
| 215 |
+
}
|
| 216 |
+
```
|
| 217 |
+
|
| 218 |
+
### 404 Not Found
|
| 219 |
+
```json
|
| 220 |
+
{
|
| 221 |
+
"detail": "Order not found"
|
| 222 |
+
}
|
| 223 |
+
```
|
| 224 |
+
|
| 225 |
+
---
|
| 226 |
+
|
| 227 |
+
## Authentication
|
| 228 |
+
|
| 229 |
+
Get customer token from auth-ms:
|
| 230 |
+
|
| 231 |
+
```bash
|
| 232 |
+
# 1. Request OTP
|
| 233 |
+
curl -X POST "http://localhost:8001/auth/customer/request-otp" \
|
| 234 |
+
-H "Content-Type: application/json" \
|
| 235 |
+
-d '{"phone_number": "+919876543210"}'
|
| 236 |
+
|
| 237 |
+
# 2. Verify OTP and get token
|
| 238 |
+
curl -X POST "http://localhost:8001/auth/customer/verify-otp" \
|
| 239 |
+
-H "Content-Type: application/json" \
|
| 240 |
+
-d '{
|
| 241 |
+
"phone_number": "+919876543210",
|
| 242 |
+
"otp": "123456"
|
| 243 |
+
}'
|
| 244 |
+
|
| 245 |
+
# Response includes access_token
|
| 246 |
+
{
|
| 247 |
+
"access_token": "eyJhbGc...",
|
| 248 |
+
"token_type": "bearer"
|
| 249 |
+
}
|
| 250 |
+
```
|
| 251 |
+
|
| 252 |
+
---
|
| 253 |
+
|
| 254 |
+
## Testing with cURL
|
| 255 |
+
|
| 256 |
+
### Create Order
|
| 257 |
+
```bash
|
| 258 |
+
curl -X POST "http://localhost:8000/orders/create" \
|
| 259 |
+
-H "Authorization: Bearer YOUR_TOKEN" \
|
| 260 |
+
-H "Content-Type: application/json" \
|
| 261 |
+
-d '{
|
| 262 |
+
"items": [{"catalogue_id": "CAT-001", "quantity": 2}],
|
| 263 |
+
"shipping_address": {
|
| 264 |
+
"address_type": "shipping",
|
| 265 |
+
"line1": "123 Main St",
|
| 266 |
+
"city": "Mumbai",
|
| 267 |
+
"state": "Maharashtra",
|
| 268 |
+
"postal_code": "400001",
|
| 269 |
+
"country": "India"
|
| 270 |
+
},
|
| 271 |
+
"customer_name": "John Doe",
|
| 272 |
+
"payment_method": "cod"
|
| 273 |
+
}'
|
| 274 |
+
```
|
| 275 |
+
|
| 276 |
+
### Get Order
|
| 277 |
+
```bash
|
| 278 |
+
curl -X GET "http://localhost:8000/orders/ORDER_ID" \
|
| 279 |
+
-H "Authorization: Bearer YOUR_TOKEN"
|
| 280 |
+
```
|
| 281 |
+
|
| 282 |
+
### List Orders with Projection
|
| 283 |
+
```bash
|
| 284 |
+
curl -X POST "http://localhost:8000/orders/list" \
|
| 285 |
+
-H "Authorization: Bearer YOUR_TOKEN" \
|
| 286 |
+
-H "Content-Type: application/json" \
|
| 287 |
+
-d '{
|
| 288 |
+
"projection_list": ["order_number", "status", "grand_total"],
|
| 289 |
+
"limit": 5
|
| 290 |
+
}'
|
| 291 |
+
```
|
| 292 |
+
|
| 293 |
+
---
|
| 294 |
+
|
| 295 |
+
## Python Example
|
| 296 |
+
|
| 297 |
+
```python
|
| 298 |
+
import requests
|
| 299 |
+
|
| 300 |
+
# Get token
|
| 301 |
+
token_response = requests.post(
|
| 302 |
+
"http://localhost:8001/auth/customer/verify-otp",
|
| 303 |
+
json={"phone_number": "+919876543210", "otp": "123456"}
|
| 304 |
+
)
|
| 305 |
+
token = token_response.json()["access_token"]
|
| 306 |
+
|
| 307 |
+
# Create order
|
| 308 |
+
headers = {"Authorization": f"Bearer {token}"}
|
| 309 |
+
order_data = {
|
| 310 |
+
"items": [{"catalogue_id": "CAT-001", "quantity": 2}],
|
| 311 |
+
"shipping_address": {
|
| 312 |
+
"address_type": "shipping",
|
| 313 |
+
"line1": "123 Main St",
|
| 314 |
+
"city": "Mumbai",
|
| 315 |
+
"state": "Maharashtra",
|
| 316 |
+
"postal_code": "400001",
|
| 317 |
+
"country": "India"
|
| 318 |
+
}
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
response = requests.post(
|
| 322 |
+
"http://localhost:8000/orders/create",
|
| 323 |
+
headers=headers,
|
| 324 |
+
json=order_data
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
order = response.json()["data"]
|
| 328 |
+
print(f"Order created: {order['order_number']}")
|
| 329 |
+
```
|
| 330 |
+
|
| 331 |
+
---
|
| 332 |
+
|
| 333 |
+
## Database Tables
|
| 334 |
+
|
| 335 |
+
### trans.sales_orders
|
| 336 |
+
Main order table with customer, merchant, and financial information.
|
| 337 |
+
|
| 338 |
+
### trans.sales_order_items
|
| 339 |
+
Order line items with product details, quantities, and pricing.
|
| 340 |
+
|
| 341 |
+
### trans.sales_order_addresses
|
| 342 |
+
Shipping and billing addresses for orders.
|
| 343 |
+
|
| 344 |
+
---
|
| 345 |
+
|
| 346 |
+
## Key Features
|
| 347 |
+
|
| 348 |
+
✅ Customer JWT authentication
|
| 349 |
+
✅ Product validation from MongoDB
|
| 350 |
+
✅ Single merchant per order validation
|
| 351 |
+
✅ Automatic tax calculation
|
| 352 |
+
✅ Unique order number generation
|
| 353 |
+
✅ PostgreSQL persistence (trans schema)
|
| 354 |
+
✅ Projection list support (API standard)
|
| 355 |
+
✅ Filtering and sorting
|
| 356 |
+
✅ Comprehensive error handling
|
|
@@ -0,0 +1,333 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Order Feature Implementation
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
Complete order management feature for e-commerce microservice with customer authentication, PostgreSQL persistence, and projection list support.
|
| 6 |
+
|
| 7 |
+
## Architecture
|
| 8 |
+
|
| 9 |
+
### Database Schema
|
| 10 |
+
All tables use the `trans` schema in PostgreSQL:
|
| 11 |
+
|
| 12 |
+
- **trans.sales_orders** - Main order table
|
| 13 |
+
- **trans.sales_order_items** - Order line items
|
| 14 |
+
- **trans.sales_order_addresses** - Shipping and billing addresses
|
| 15 |
+
|
| 16 |
+
### Components
|
| 17 |
+
|
| 18 |
+
```
|
| 19 |
+
app/order/
|
| 20 |
+
├── __init__.py
|
| 21 |
+
├── models/
|
| 22 |
+
│ ├── __init__.py
|
| 23 |
+
│ └── model.py # SQLAlchemy models
|
| 24 |
+
├── schemas/
|
| 25 |
+
│ ├── __init__.py
|
| 26 |
+
│ └── schema.py # Pydantic request/response schemas
|
| 27 |
+
├── services/
|
| 28 |
+
│ ├── __init__.py
|
| 29 |
+
│ └── service.py # Business logic
|
| 30 |
+
└── controllers/
|
| 31 |
+
├── __init__.py
|
| 32 |
+
└── router.py # FastAPI endpoints
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
## Features
|
| 36 |
+
|
| 37 |
+
### 1. Order Creation
|
| 38 |
+
- **Endpoint**: `POST /orders/create`
|
| 39 |
+
- **Authentication**: Required (Customer JWT)
|
| 40 |
+
- **Features**:
|
| 41 |
+
- Validates products from MongoDB catalogue
|
| 42 |
+
- Ensures single merchant per order
|
| 43 |
+
- Calculates subtotal, tax, and grand total
|
| 44 |
+
- Generates unique order numbers (ORD-YYYYMMDD-NNNN)
|
| 45 |
+
- Persists to PostgreSQL trans schema
|
| 46 |
+
- Supports shipping and billing addresses
|
| 47 |
+
|
| 48 |
+
### 2. Get Order Details
|
| 49 |
+
- **Endpoint**: `GET /orders/{order_id}`
|
| 50 |
+
- **Authentication**: Required (Customer JWT)
|
| 51 |
+
- **Features**:
|
| 52 |
+
- Returns complete order with items and addresses
|
| 53 |
+
- Customer can only access their own orders
|
| 54 |
+
|
| 55 |
+
### 3. List Orders (Simple)
|
| 56 |
+
- **Endpoint**: `GET /orders/`
|
| 57 |
+
- **Authentication**: Required (Customer JWT)
|
| 58 |
+
- **Features**:
|
| 59 |
+
- Simple pagination with skip/limit
|
| 60 |
+
- Returns full order data
|
| 61 |
+
|
| 62 |
+
### 4. List Orders (Advanced)
|
| 63 |
+
- **Endpoint**: `POST /orders/list`
|
| 64 |
+
- **Authentication**: Required (Customer JWT)
|
| 65 |
+
- **Features**:
|
| 66 |
+
- Filtering by status, payment_status, fulfillment_status
|
| 67 |
+
- Pagination with skip/limit
|
| 68 |
+
- Sorting by any field (asc/desc)
|
| 69 |
+
- **Projection list support** (API standard compliance)
|
| 70 |
+
- Returns only requested fields for optimized responses
|
| 71 |
+
|
| 72 |
+
## API Examples
|
| 73 |
+
|
| 74 |
+
### Create Order
|
| 75 |
+
|
| 76 |
+
```bash
|
| 77 |
+
curl -X POST "http://localhost:8000/orders/create" \
|
| 78 |
+
-H "Authorization: Bearer <customer_token>" \
|
| 79 |
+
-H "Content-Type: application/json" \
|
| 80 |
+
-d '{
|
| 81 |
+
"items": [
|
| 82 |
+
{
|
| 83 |
+
"catalogue_id": "CAT-001",
|
| 84 |
+
"quantity": 2
|
| 85 |
+
}
|
| 86 |
+
],
|
| 87 |
+
"shipping_address": {
|
| 88 |
+
"address_type": "shipping",
|
| 89 |
+
"line1": "123 Main Street",
|
| 90 |
+
"city": "Mumbai",
|
| 91 |
+
"state": "Maharashtra",
|
| 92 |
+
"postal_code": "400001",
|
| 93 |
+
"country": "India"
|
| 94 |
+
},
|
| 95 |
+
"customer_name": "John Doe",
|
| 96 |
+
"customer_phone": "+919876543210",
|
| 97 |
+
"customer_email": "john@example.com",
|
| 98 |
+
"payment_method": "cod",
|
| 99 |
+
"notes": "Deliver between 10 AM - 2 PM"
|
| 100 |
+
}'
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
### Get Order
|
| 104 |
+
|
| 105 |
+
```bash
|
| 106 |
+
curl -X GET "http://localhost:8000/orders/{order_id}" \
|
| 107 |
+
-H "Authorization: Bearer <customer_token>"
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
### List Orders (Simple)
|
| 111 |
+
|
| 112 |
+
```bash
|
| 113 |
+
curl -X GET "http://localhost:8000/orders/?skip=0&limit=10" \
|
| 114 |
+
-H "Authorization: Bearer <customer_token>"
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
### List Orders with Projection
|
| 118 |
+
|
| 119 |
+
```bash
|
| 120 |
+
curl -X POST "http://localhost:8000/orders/list" \
|
| 121 |
+
-H "Authorization: Bearer <customer_token>" \
|
| 122 |
+
-H "Content-Type: application/json" \
|
| 123 |
+
-d '{
|
| 124 |
+
"filters": {
|
| 125 |
+
"status": "pending"
|
| 126 |
+
},
|
| 127 |
+
"skip": 0,
|
| 128 |
+
"limit": 10,
|
| 129 |
+
"projection_list": [
|
| 130 |
+
"sales_order_id",
|
| 131 |
+
"order_number",
|
| 132 |
+
"order_date",
|
| 133 |
+
"status",
|
| 134 |
+
"grand_total",
|
| 135 |
+
"payment_status"
|
| 136 |
+
],
|
| 137 |
+
"sort_by": "created_at",
|
| 138 |
+
"sort_order": "desc"
|
| 139 |
+
}'
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
## Database Schema Details
|
| 143 |
+
|
| 144 |
+
### sales_orders Table
|
| 145 |
+
|
| 146 |
+
| Column | Type | Description |
|
| 147 |
+
|--------|------|-------------|
|
| 148 |
+
| sales_order_id | UUID | Primary key |
|
| 149 |
+
| order_number | VARCHAR(50) | Unique order number |
|
| 150 |
+
| merchant_id | UUID | Merchant ID (from product) |
|
| 151 |
+
| customer_id | VARCHAR(100) | Customer ID (from JWT) |
|
| 152 |
+
| order_date | TIMESTAMP | Order creation date |
|
| 153 |
+
| status | VARCHAR(50) | Order status (pending, confirmed, etc.) |
|
| 154 |
+
| subtotal | NUMERIC(15,2) | Subtotal before tax |
|
| 155 |
+
| total_tax | NUMERIC(15,2) | Total tax amount |
|
| 156 |
+
| grand_total | NUMERIC(15,2) | Final total |
|
| 157 |
+
| payment_status | VARCHAR(50) | Payment status |
|
| 158 |
+
| fulfillment_status | VARCHAR(50) | Fulfillment status |
|
| 159 |
+
| source | VARCHAR(50) | Order source (ecommerce) |
|
| 160 |
+
| channel | VARCHAR(50) | Order channel (web) |
|
| 161 |
+
|
| 162 |
+
### sales_order_items Table
|
| 163 |
+
|
| 164 |
+
| Column | Type | Description |
|
| 165 |
+
|--------|------|-------------|
|
| 166 |
+
| id | UUID | Primary key |
|
| 167 |
+
| sales_order_id | UUID | Foreign key to sales_orders |
|
| 168 |
+
| product_id | VARCHAR(100) | Product catalogue ID |
|
| 169 |
+
| product_name | VARCHAR(255) | Product name |
|
| 170 |
+
| quantity | NUMERIC(15,3) | Quantity ordered |
|
| 171 |
+
| unit_price | NUMERIC(15,2) | Unit price |
|
| 172 |
+
| tax_percent | NUMERIC(5,2) | Tax percentage |
|
| 173 |
+
| line_total | NUMERIC(15,2) | Line total (price + tax) |
|
| 174 |
+
| uom | VARCHAR(20) | Unit of measure |
|
| 175 |
+
| hsn_code | VARCHAR(20) | HSN code for tax |
|
| 176 |
+
|
| 177 |
+
### sales_order_addresses Table
|
| 178 |
+
|
| 179 |
+
| Column | Type | Description |
|
| 180 |
+
|--------|------|-------------|
|
| 181 |
+
| id | UUID | Primary key |
|
| 182 |
+
| sales_order_id | UUID | Foreign key to sales_orders |
|
| 183 |
+
| address_type | VARCHAR(50) | shipping or billing |
|
| 184 |
+
| line1 | VARCHAR(255) | Address line 1 |
|
| 185 |
+
| line2 | VARCHAR(255) | Address line 2 |
|
| 186 |
+
| city | VARCHAR(100) | City |
|
| 187 |
+
| state | VARCHAR(100) | State |
|
| 188 |
+
| postal_code | VARCHAR(20) | Postal code |
|
| 189 |
+
| country | VARCHAR(100) | Country |
|
| 190 |
+
|
| 191 |
+
## Authentication
|
| 192 |
+
|
| 193 |
+
All order endpoints require customer authentication via JWT token:
|
| 194 |
+
|
| 195 |
+
1. Customer obtains token from auth-ms (`/auth/customer/verify-otp`)
|
| 196 |
+
2. Token contains `customer_id` in the `sub` claim
|
| 197 |
+
3. Token must have `user_type: "customer"`
|
| 198 |
+
4. Token is passed in Authorization header: `Bearer <token>`
|
| 199 |
+
|
| 200 |
+
## Order Flow
|
| 201 |
+
|
| 202 |
+
1. **Customer browses products** → Product Catalogue API
|
| 203 |
+
2. **Customer adds to cart** → Cart API (Redis)
|
| 204 |
+
3. **Customer creates order** → Order API
|
| 205 |
+
- Validates products from MongoDB
|
| 206 |
+
- Calculates totals
|
| 207 |
+
- Persists to PostgreSQL
|
| 208 |
+
- Returns order confirmation
|
| 209 |
+
4. **Customer views orders** → Order List API
|
| 210 |
+
5. **Customer tracks order** → Order Details API
|
| 211 |
+
|
| 212 |
+
## Projection List Support
|
| 213 |
+
|
| 214 |
+
Following the API list endpoint standard, the `/orders/list` endpoint supports projection:
|
| 215 |
+
|
| 216 |
+
```python
|
| 217 |
+
# Request only specific fields
|
| 218 |
+
{
|
| 219 |
+
"projection_list": ["order_number", "status", "grand_total"]
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
# Response contains only requested fields
|
| 223 |
+
{
|
| 224 |
+
"total": 10,
|
| 225 |
+
"orders": [
|
| 226 |
+
{
|
| 227 |
+
"order_number": "ORD-20260207-0001",
|
| 228 |
+
"status": "pending",
|
| 229 |
+
"grand_total": 1500.00
|
| 230 |
+
}
|
| 231 |
+
]
|
| 232 |
+
}
|
| 233 |
+
```
|
| 234 |
+
|
| 235 |
+
**Benefits**:
|
| 236 |
+
- 50-90% reduction in payload size
|
| 237 |
+
- Faster response times
|
| 238 |
+
- Reduced network bandwidth
|
| 239 |
+
- Flexible client-side data requirements
|
| 240 |
+
|
| 241 |
+
## Testing
|
| 242 |
+
|
| 243 |
+
Run the test script:
|
| 244 |
+
|
| 245 |
+
```bash
|
| 246 |
+
python test_order_api.py
|
| 247 |
+
```
|
| 248 |
+
|
| 249 |
+
The test script covers:
|
| 250 |
+
- Customer authentication
|
| 251 |
+
- Order creation
|
| 252 |
+
- Order retrieval
|
| 253 |
+
- Order listing (simple)
|
| 254 |
+
- Order listing with projection
|
| 255 |
+
- Order listing with filters
|
| 256 |
+
|
| 257 |
+
## Error Handling
|
| 258 |
+
|
| 259 |
+
The API returns appropriate HTTP status codes:
|
| 260 |
+
|
| 261 |
+
- **201 Created** - Order created successfully
|
| 262 |
+
- **200 OK** - Order retrieved/listed successfully
|
| 263 |
+
- **400 Bad Request** - Invalid request data or business logic error
|
| 264 |
+
- **401 Unauthorized** - Missing or invalid token
|
| 265 |
+
- **403 Forbidden** - Not a customer token
|
| 266 |
+
- **404 Not Found** - Order not found
|
| 267 |
+
|
| 268 |
+
## Configuration
|
| 269 |
+
|
| 270 |
+
Required environment variables in `.env`:
|
| 271 |
+
|
| 272 |
+
```env
|
| 273 |
+
# PostgreSQL (for order persistence)
|
| 274 |
+
DB_HOST=your-postgres-host
|
| 275 |
+
DB_PORT=5432
|
| 276 |
+
DB_NAME=cuatrolabs
|
| 277 |
+
DB_USER=trans_owner
|
| 278 |
+
DB_PASSWORD=your-password
|
| 279 |
+
DB_SSLMODE=require
|
| 280 |
+
|
| 281 |
+
# MongoDB (for product catalogue)
|
| 282 |
+
MONGODB_URI=mongodb://localhost:27017
|
| 283 |
+
MONGODB_DB_NAME=app_db
|
| 284 |
+
|
| 285 |
+
# JWT (for customer authentication)
|
| 286 |
+
SECRET_KEY=your-secret-key
|
| 287 |
+
ALGORITHM=HS256
|
| 288 |
+
```
|
| 289 |
+
|
| 290 |
+
## Next Steps
|
| 291 |
+
|
| 292 |
+
Potential enhancements:
|
| 293 |
+
|
| 294 |
+
1. **Payment Integration**
|
| 295 |
+
- Payment gateway integration
|
| 296 |
+
- Payment status updates
|
| 297 |
+
- Refund handling
|
| 298 |
+
|
| 299 |
+
2. **Order Status Updates**
|
| 300 |
+
- Status transition workflow
|
| 301 |
+
- Notifications on status change
|
| 302 |
+
- Delivery tracking
|
| 303 |
+
|
| 304 |
+
3. **Inventory Management**
|
| 305 |
+
- Stock reservation on order creation
|
| 306 |
+
- Stock deduction on order confirmation
|
| 307 |
+
- Stock release on order cancellation
|
| 308 |
+
|
| 309 |
+
4. **Order Cancellation**
|
| 310 |
+
- Customer-initiated cancellation
|
| 311 |
+
- Cancellation policies
|
| 312 |
+
- Refund processing
|
| 313 |
+
|
| 314 |
+
5. **Order History**
|
| 315 |
+
- Order analytics
|
| 316 |
+
- Reorder functionality
|
| 317 |
+
- Order export (PDF, CSV)
|
| 318 |
+
|
| 319 |
+
## Compliance
|
| 320 |
+
|
| 321 |
+
✅ **API List Endpoint Standard** - Implements projection_list support
|
| 322 |
+
✅ **TRANS Schema** - All tables use trans schema
|
| 323 |
+
✅ **Customer Authentication** - JWT-based customer auth
|
| 324 |
+
✅ **Error Handling** - Comprehensive error responses
|
| 325 |
+
✅ **Logging** - Structured logging with insightfy_utils
|
| 326 |
+
|
| 327 |
+
## Support
|
| 328 |
+
|
| 329 |
+
For issues or questions:
|
| 330 |
+
- Check logs in application output
|
| 331 |
+
- Review error messages in API responses
|
| 332 |
+
- Verify database connectivity
|
| 333 |
+
- Ensure customer token is valid
|
|
@@ -0,0 +1,264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Quick Start Guide - Order Service
|
| 2 |
+
|
| 3 |
+
## Prerequisites
|
| 4 |
+
|
| 5 |
+
1. PostgreSQL database running and accessible
|
| 6 |
+
2. MongoDB running with product catalogue data
|
| 7 |
+
3. Auth microservice running (for customer tokens)
|
| 8 |
+
4. Redis running (for cart functionality)
|
| 9 |
+
|
| 10 |
+
## Step 1: Verify Environment Variables
|
| 11 |
+
|
| 12 |
+
Check `cuatrolabs-ecomm-ms/.env`:
|
| 13 |
+
|
| 14 |
+
```env
|
| 15 |
+
# PostgreSQL
|
| 16 |
+
DB_HOST=ep-sweet-surf-a1qeduoy.ap-southeast-1.aws.neon.tech
|
| 17 |
+
DB_PORT=5432
|
| 18 |
+
DB_NAME=cuatrolabs
|
| 19 |
+
DB_USER=trans_owner
|
| 20 |
+
DB_PASSWORD=BookMyService7
|
| 21 |
+
DB_SSLMODE=require
|
| 22 |
+
|
| 23 |
+
# MongoDB
|
| 24 |
+
MONGODB_URI=mongodb://your-mongodb-uri
|
| 25 |
+
MONGODB_DB_NAME=app_db
|
| 26 |
+
|
| 27 |
+
# Redis
|
| 28 |
+
REDIS_HOST=localhost
|
| 29 |
+
REDIS_PORT=6379
|
| 30 |
+
|
| 31 |
+
# JWT
|
| 32 |
+
SECRET_KEY=your-secret-key
|
| 33 |
+
ALGORITHM=HS256
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
## Step 2: Start the Service
|
| 37 |
+
|
| 38 |
+
```bash
|
| 39 |
+
cd cuatrolabs-ecomm-ms
|
| 40 |
+
|
| 41 |
+
# Activate virtual environment (if using one)
|
| 42 |
+
source venv/bin/activate # or: source .venv/bin/activate
|
| 43 |
+
|
| 44 |
+
# Install dependencies (if not already installed)
|
| 45 |
+
pip install -r requirements.txt
|
| 46 |
+
|
| 47 |
+
# Start the service
|
| 48 |
+
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
## Step 3: Verify Startup
|
| 52 |
+
|
| 53 |
+
Look for these log messages:
|
| 54 |
+
|
| 55 |
+
```
|
| 56 |
+
[POSTGRES] ✅ Connection successful
|
| 57 |
+
[SCHEMA] ✅ TRANS schema exists
|
| 58 |
+
[SCHEMA] ✅ All App tables correctly use 'trans' schema
|
| 59 |
+
[SCHEMA] ✅ Database tables created successfully in TRANS schema
|
| 60 |
+
✅ TRANS schema enforcement completed successfully
|
| 61 |
+
App Microservice started successfully
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
## Step 4: Check API Documentation
|
| 65 |
+
|
| 66 |
+
Open in browser:
|
| 67 |
+
- Swagger UI: http://localhost:8000/docs
|
| 68 |
+
- ReDoc: http://localhost:8000/redoc
|
| 69 |
+
|
| 70 |
+
Look for the "Orders" section with these endpoints:
|
| 71 |
+
- POST /orders/create
|
| 72 |
+
- GET /orders/{order_id}
|
| 73 |
+
- GET /orders/
|
| 74 |
+
- POST /orders/list
|
| 75 |
+
|
| 76 |
+
## Step 5: Get a Customer Token
|
| 77 |
+
|
| 78 |
+
```bash
|
| 79 |
+
# Request OTP
|
| 80 |
+
curl -X POST "http://localhost:8001/auth/customer/request-otp" \
|
| 81 |
+
-H "Content-Type: application/json" \
|
| 82 |
+
-d '{"phone_number": "+919876543210"}'
|
| 83 |
+
|
| 84 |
+
# Verify OTP (use actual OTP from SMS/logs)
|
| 85 |
+
curl -X POST "http://localhost:8001/auth/customer/verify-otp" \
|
| 86 |
+
-H "Content-Type: application/json" \
|
| 87 |
+
-d '{
|
| 88 |
+
"phone_number": "+919876543210",
|
| 89 |
+
"otp": "123456"
|
| 90 |
+
}'
|
| 91 |
+
|
| 92 |
+
# Save the access_token from response
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
## Step 6: Create Your First Order
|
| 96 |
+
|
| 97 |
+
```bash
|
| 98 |
+
# Replace YOUR_TOKEN with the token from Step 5
|
| 99 |
+
# Replace CAT-001 with an actual catalogue_id from your MongoDB
|
| 100 |
+
|
| 101 |
+
curl -X POST "http://localhost:8000/orders/create" \
|
| 102 |
+
-H "Authorization: Bearer YOUR_TOKEN" \
|
| 103 |
+
-H "Content-Type: application/json" \
|
| 104 |
+
-d '{
|
| 105 |
+
"items": [
|
| 106 |
+
{
|
| 107 |
+
"catalogue_id": "CAT-001",
|
| 108 |
+
"quantity": 2
|
| 109 |
+
}
|
| 110 |
+
],
|
| 111 |
+
"shipping_address": {
|
| 112 |
+
"address_type": "shipping",
|
| 113 |
+
"line1": "123 Main Street",
|
| 114 |
+
"city": "Mumbai",
|
| 115 |
+
"state": "Maharashtra",
|
| 116 |
+
"postal_code": "400001",
|
| 117 |
+
"country": "India"
|
| 118 |
+
},
|
| 119 |
+
"customer_name": "Test Customer",
|
| 120 |
+
"customer_phone": "+919876543210",
|
| 121 |
+
"payment_method": "cod"
|
| 122 |
+
}'
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
## Step 7: Verify Order Creation
|
| 126 |
+
|
| 127 |
+
Check the response for:
|
| 128 |
+
```json
|
| 129 |
+
{
|
| 130 |
+
"success": true,
|
| 131 |
+
"message": "Order created successfully",
|
| 132 |
+
"data": {
|
| 133 |
+
"sales_order_id": "...",
|
| 134 |
+
"order_number": "ORD-20260207-0001",
|
| 135 |
+
"status": "pending",
|
| 136 |
+
"grand_total": 1180.00
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
## Step 8: List Your Orders
|
| 142 |
+
|
| 143 |
+
```bash
|
| 144 |
+
curl -X GET "http://localhost:8000/orders/" \
|
| 145 |
+
-H "Authorization: Bearer YOUR_TOKEN"
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
## Troubleshooting
|
| 149 |
+
|
| 150 |
+
### Database Connection Failed
|
| 151 |
+
- Verify PostgreSQL is running
|
| 152 |
+
- Check DB_HOST, DB_PORT, DB_USER, DB_PASSWORD in .env
|
| 153 |
+
- Test connection: `psql -h HOST -U USER -d DATABASE`
|
| 154 |
+
|
| 155 |
+
### Product Not Found
|
| 156 |
+
- Verify MongoDB is running
|
| 157 |
+
- Check product exists in scm_catalogues collection
|
| 158 |
+
- Ensure product has `allow_ecommerce: true`
|
| 159 |
+
- Verify `catalogue_type: "Product"`
|
| 160 |
+
|
| 161 |
+
### Token Invalid
|
| 162 |
+
- Ensure auth-ms is running
|
| 163 |
+
- Verify SECRET_KEY matches between services
|
| 164 |
+
- Check token hasn't expired
|
| 165 |
+
- Ensure using customer token (not system user)
|
| 166 |
+
|
| 167 |
+
### Schema Errors
|
| 168 |
+
- Check logs for schema validation errors
|
| 169 |
+
- Verify all models use `__table_args__ = {"schema": "trans"}`
|
| 170 |
+
- Run: `python -c "from app.order.models.model import *; print('Models OK')"`
|
| 171 |
+
|
| 172 |
+
### Import Errors
|
| 173 |
+
- Install dependencies: `pip install -r requirements.txt`
|
| 174 |
+
- Check Python version: `python --version` (should be 3.9+)
|
| 175 |
+
- Verify virtual environment is activated
|
| 176 |
+
|
| 177 |
+
## Database Verification
|
| 178 |
+
|
| 179 |
+
Check tables were created:
|
| 180 |
+
|
| 181 |
+
```sql
|
| 182 |
+
-- Connect to PostgreSQL
|
| 183 |
+
psql -h HOST -U USER -d DATABASE
|
| 184 |
+
|
| 185 |
+
-- List tables in trans schema
|
| 186 |
+
\dt trans.*
|
| 187 |
+
|
| 188 |
+
-- Should see:
|
| 189 |
+
-- trans.sales_orders
|
| 190 |
+
-- trans.sales_order_items
|
| 191 |
+
-- trans.sales_order_addresses
|
| 192 |
+
|
| 193 |
+
-- Check order count
|
| 194 |
+
SELECT COUNT(*) FROM trans.sales_orders;
|
| 195 |
+
|
| 196 |
+
-- View recent orders
|
| 197 |
+
SELECT order_number, status, grand_total, created_at
|
| 198 |
+
FROM trans.sales_orders
|
| 199 |
+
ORDER BY created_at DESC
|
| 200 |
+
LIMIT 5;
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
## Testing with Script
|
| 204 |
+
|
| 205 |
+
Run the automated test script:
|
| 206 |
+
|
| 207 |
+
```bash
|
| 208 |
+
python test_order_api.py
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
This will:
|
| 212 |
+
1. Get customer token
|
| 213 |
+
2. Create test order
|
| 214 |
+
3. Retrieve order details
|
| 215 |
+
4. List orders
|
| 216 |
+
5. Test projection support
|
| 217 |
+
|
| 218 |
+
## API Documentation
|
| 219 |
+
|
| 220 |
+
- Full documentation: `ORDER_FEATURE_README.md`
|
| 221 |
+
- Quick reference: `ORDER_API_QUICK_REF.md`
|
| 222 |
+
- Implementation details: `ORDER_IMPLEMENTATION_COMPLETE.md`
|
| 223 |
+
|
| 224 |
+
## Health Check
|
| 225 |
+
|
| 226 |
+
```bash
|
| 227 |
+
curl http://localhost:8000/health
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
Expected response:
|
| 231 |
+
```json
|
| 232 |
+
{
|
| 233 |
+
"status": "healthy",
|
| 234 |
+
"service": "app-microservice",
|
| 235 |
+
"version": "1.0.0"
|
| 236 |
+
}
|
| 237 |
+
```
|
| 238 |
+
|
| 239 |
+
## Next Steps
|
| 240 |
+
|
| 241 |
+
1. ✅ Service running
|
| 242 |
+
2. ✅ Database tables created
|
| 243 |
+
3. ✅ First order created
|
| 244 |
+
4. ✅ Orders listing works
|
| 245 |
+
|
| 246 |
+
Now you can:
|
| 247 |
+
- Integrate with frontend
|
| 248 |
+
- Add more products to catalogue
|
| 249 |
+
- Test different order scenarios
|
| 250 |
+
- Implement payment integration
|
| 251 |
+
- Add order status updates
|
| 252 |
+
|
| 253 |
+
## Support
|
| 254 |
+
|
| 255 |
+
If you encounter issues:
|
| 256 |
+
1. Check service logs for errors
|
| 257 |
+
2. Verify all prerequisites are running
|
| 258 |
+
3. Review environment variables
|
| 259 |
+
4. Check database connectivity
|
| 260 |
+
5. Verify product data in MongoDB
|
| 261 |
+
|
| 262 |
+
## Success! 🎉
|
| 263 |
+
|
| 264 |
+
Your order service is now running and ready to accept customer orders!
|
|
@@ -16,6 +16,7 @@ from app.merchant_discovery.controllers.router import router as merchant_discove
|
|
| 16 |
from app.product_catalogue.controllers.router import router as product_catalogue_router
|
| 17 |
from app.cart.controllers.router import router as cart_router
|
| 18 |
from app.appointments.controllers.router import router as appointments_router
|
|
|
|
| 19 |
# from app.notifications.controllers.router import router as notifications_router
|
| 20 |
|
| 21 |
# Initialize logging first
|
|
@@ -99,6 +100,7 @@ app.include_router(merchant_discovery_router)
|
|
| 99 |
app.include_router(product_catalogue_router)
|
| 100 |
app.include_router(cart_router)
|
| 101 |
app.include_router(appointments_router)
|
|
|
|
| 102 |
# app.include_router(notifications_router)
|
| 103 |
|
| 104 |
|
|
|
|
| 16 |
from app.product_catalogue.controllers.router import router as product_catalogue_router
|
| 17 |
from app.cart.controllers.router import router as cart_router
|
| 18 |
from app.appointments.controllers.router import router as appointments_router
|
| 19 |
+
from app.order.controllers.router import router as order_router
|
| 20 |
# from app.notifications.controllers.router import router as notifications_router
|
| 21 |
|
| 22 |
# Initialize logging first
|
|
|
|
| 100 |
app.include_router(product_catalogue_router)
|
| 101 |
app.include_router(cart_router)
|
| 102 |
app.include_router(appointments_router)
|
| 103 |
+
app.include_router(order_router)
|
| 104 |
# app.include_router(notifications_router)
|
| 105 |
|
| 106 |
|
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Order module for e-commerce microservice.
|
| 3 |
+
Handles customer order creation and management.
|
| 4 |
+
"""
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Order controllers"""
|
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Order API endpoints for customer order management.
|
| 3 |
+
"""
|
| 4 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
| 5 |
+
from insightfy_utils.logging import get_logger
|
| 6 |
+
|
| 7 |
+
from app.dependencies.auth import get_current_customer, CustomerToken
|
| 8 |
+
from app.order.services.service import OrderService
|
| 9 |
+
from app.order.schemas.schema import (
|
| 10 |
+
CreateOrderRequest,
|
| 11 |
+
OrderResponse,
|
| 12 |
+
OrderListRequest,
|
| 13 |
+
OrderListResponse
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
logger = get_logger(__name__)
|
| 17 |
+
|
| 18 |
+
router = APIRouter(
|
| 19 |
+
prefix="/orders",
|
| 20 |
+
tags=["Orders"]
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@router.post(
|
| 25 |
+
"/create",
|
| 26 |
+
response_model=dict,
|
| 27 |
+
status_code=status.HTTP_201_CREATED,
|
| 28 |
+
summary="Create a new order",
|
| 29 |
+
description="Create a new order from cart items. Requires customer authentication."
|
| 30 |
+
)
|
| 31 |
+
async def create_order(
|
| 32 |
+
request: CreateOrderRequest,
|
| 33 |
+
current_customer: CustomerToken = Depends(get_current_customer)
|
| 34 |
+
):
|
| 35 |
+
"""
|
| 36 |
+
Create a new order for the authenticated customer.
|
| 37 |
+
|
| 38 |
+
- Validates all products exist and are available
|
| 39 |
+
- Ensures all items are from the same merchant
|
| 40 |
+
- Calculates totals including tax
|
| 41 |
+
- Persists order to PostgreSQL trans schema
|
| 42 |
+
- Returns complete order details
|
| 43 |
+
"""
|
| 44 |
+
result = await OrderService.create_order(
|
| 45 |
+
customer_id=current_customer.customer_id,
|
| 46 |
+
request=request
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
if not result["success"]:
|
| 50 |
+
raise HTTPException(
|
| 51 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 52 |
+
detail=result["message"]
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
return {
|
| 56 |
+
"success": True,
|
| 57 |
+
"message": result["message"],
|
| 58 |
+
"data": result["order"]
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@router.get(
|
| 63 |
+
"/{order_id}",
|
| 64 |
+
response_model=dict,
|
| 65 |
+
summary="Get order details",
|
| 66 |
+
description="Get detailed information about a specific order"
|
| 67 |
+
)
|
| 68 |
+
async def get_order(
|
| 69 |
+
order_id: str,
|
| 70 |
+
current_customer: CustomerToken = Depends(get_current_customer)
|
| 71 |
+
):
|
| 72 |
+
"""
|
| 73 |
+
Get order details by ID.
|
| 74 |
+
|
| 75 |
+
Only returns orders belonging to the authenticated customer.
|
| 76 |
+
"""
|
| 77 |
+
order = await OrderService.get_order(
|
| 78 |
+
customer_id=current_customer.customer_id,
|
| 79 |
+
order_id=order_id
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
if not order:
|
| 83 |
+
raise HTTPException(
|
| 84 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 85 |
+
detail="Order not found"
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
return {
|
| 89 |
+
"success": True,
|
| 90 |
+
"data": order
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
@router.post(
|
| 95 |
+
"/list",
|
| 96 |
+
response_model=OrderListResponse,
|
| 97 |
+
summary="List customer orders",
|
| 98 |
+
description="List all orders for the authenticated customer with optional filtering and projection"
|
| 99 |
+
)
|
| 100 |
+
async def list_orders(
|
| 101 |
+
request: OrderListRequest,
|
| 102 |
+
current_customer: CustomerToken = Depends(get_current_customer)
|
| 103 |
+
):
|
| 104 |
+
"""
|
| 105 |
+
List orders for the authenticated customer.
|
| 106 |
+
|
| 107 |
+
Supports:
|
| 108 |
+
- Filtering by status, payment_status, fulfillment_status
|
| 109 |
+
- Pagination with skip/limit
|
| 110 |
+
- Sorting by any field
|
| 111 |
+
- Field projection for optimized responses
|
| 112 |
+
|
| 113 |
+
Following API list endpoint standard with projection_list support.
|
| 114 |
+
"""
|
| 115 |
+
result = await OrderService.list_orders(
|
| 116 |
+
customer_id=current_customer.customer_id,
|
| 117 |
+
filters=request.filters,
|
| 118 |
+
skip=request.skip,
|
| 119 |
+
limit=request.limit,
|
| 120 |
+
projection_list=request.projection_list,
|
| 121 |
+
sort_by=request.sort_by,
|
| 122 |
+
sort_order=request.sort_order
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
return OrderListResponse(
|
| 126 |
+
total=result["total"],
|
| 127 |
+
skip=result["skip"],
|
| 128 |
+
limit=result["limit"],
|
| 129 |
+
orders=result["orders"]
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
@router.get(
|
| 134 |
+
"/",
|
| 135 |
+
response_model=dict,
|
| 136 |
+
summary="Get customer orders (simple)",
|
| 137 |
+
description="Simple GET endpoint to list customer orders"
|
| 138 |
+
)
|
| 139 |
+
async def get_orders_simple(
|
| 140 |
+
skip: int = 0,
|
| 141 |
+
limit: int = 100,
|
| 142 |
+
current_customer: CustomerToken = Depends(get_current_customer)
|
| 143 |
+
):
|
| 144 |
+
"""
|
| 145 |
+
Simple GET endpoint to list customer orders.
|
| 146 |
+
|
| 147 |
+
For advanced filtering and projection, use POST /orders/list instead.
|
| 148 |
+
"""
|
| 149 |
+
result = await OrderService.list_orders(
|
| 150 |
+
customer_id=current_customer.customer_id,
|
| 151 |
+
skip=skip,
|
| 152 |
+
limit=limit
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
return {
|
| 156 |
+
"success": True,
|
| 157 |
+
"data": {
|
| 158 |
+
"total": result["total"],
|
| 159 |
+
"orders": result["orders"]
|
| 160 |
+
}
|
| 161 |
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Order models"""
|
| 2 |
+
from app.order.models.model import SalesOrder, SalesOrderItem, SalesOrderAddress
|
| 3 |
+
|
| 4 |
+
__all__ = ["SalesOrder", "SalesOrderItem", "SalesOrderAddress"]
|
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SQLAlchemy models for sales orders.
|
| 3 |
+
Maps to trans.sales_orders, trans.sales_order_items, and trans.sales_order_addresses tables.
|
| 4 |
+
"""
|
| 5 |
+
from sqlalchemy import Column, String, Integer, Numeric, DateTime, Text, ForeignKey, ARRAY
|
| 6 |
+
from sqlalchemy.dialects.postgresql import UUID
|
| 7 |
+
from sqlalchemy.orm import relationship
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
import uuid
|
| 10 |
+
|
| 11 |
+
from app.core.database import Base
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class SalesOrder(Base):
|
| 15 |
+
"""Sales order model - trans.sales_orders table"""
|
| 16 |
+
__tablename__ = "sales_orders"
|
| 17 |
+
__table_args__ = {"schema": "trans"}
|
| 18 |
+
|
| 19 |
+
sales_order_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 20 |
+
order_number = Column(String(50), unique=True, nullable=False, index=True)
|
| 21 |
+
branch_id = Column(UUID(as_uuid=True), nullable=True)
|
| 22 |
+
merchant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
| 23 |
+
order_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
| 24 |
+
status = Column(String(50), nullable=False, default="pending")
|
| 25 |
+
|
| 26 |
+
# Customer information
|
| 27 |
+
customer_id = Column(String(100), nullable=False, index=True)
|
| 28 |
+
customer_name = Column(String(255), nullable=True)
|
| 29 |
+
customer_type = Column(String(50), nullable=True, default="retail")
|
| 30 |
+
customer_phone = Column(String(20), nullable=True)
|
| 31 |
+
customer_email = Column(String(255), nullable=True)
|
| 32 |
+
customer_gstin = Column(String(50), nullable=True)
|
| 33 |
+
|
| 34 |
+
# Financial information
|
| 35 |
+
subtotal = Column(Numeric(15, 2), nullable=False, default=0.0)
|
| 36 |
+
total_discount = Column(Numeric(15, 2), nullable=False, default=0.0)
|
| 37 |
+
total_tax = Column(Numeric(15, 2), nullable=False, default=0.0)
|
| 38 |
+
shipping_charges = Column(Numeric(15, 2), nullable=False, default=0.0)
|
| 39 |
+
grand_total = Column(Numeric(15, 2), nullable=False, default=0.0)
|
| 40 |
+
cgst = Column(Numeric(15, 2), nullable=True, default=0.0)
|
| 41 |
+
sgst = Column(Numeric(15, 2), nullable=True, default=0.0)
|
| 42 |
+
igst = Column(Numeric(15, 2), nullable=True, default=0.0)
|
| 43 |
+
|
| 44 |
+
# Payment information
|
| 45 |
+
payment_type = Column(String(50), nullable=True)
|
| 46 |
+
payment_status = Column(String(50), nullable=False, default="pending")
|
| 47 |
+
payment_method = Column(String(50), nullable=True)
|
| 48 |
+
payment_date = Column(DateTime, nullable=True)
|
| 49 |
+
payment_reference = Column(String(255), nullable=True)
|
| 50 |
+
amount_paid = Column(Numeric(15, 2), nullable=False, default=0.0)
|
| 51 |
+
amount_due = Column(Numeric(15, 2), nullable=False, default=0.0)
|
| 52 |
+
credit_terms = Column(String(100), nullable=True)
|
| 53 |
+
credit_limit = Column(Numeric(15, 2), nullable=True)
|
| 54 |
+
|
| 55 |
+
# Fulfillment information
|
| 56 |
+
fulfillment_status = Column(String(50), nullable=False, default="pending")
|
| 57 |
+
expected_delivery_date = Column(DateTime, nullable=True)
|
| 58 |
+
actual_delivery_date = Column(DateTime, nullable=True)
|
| 59 |
+
|
| 60 |
+
# Invoice information
|
| 61 |
+
invoice_id = Column(UUID(as_uuid=True), nullable=True)
|
| 62 |
+
invoice_number = Column(String(50), nullable=True)
|
| 63 |
+
invoice_date = Column(DateTime, nullable=True)
|
| 64 |
+
invoice_pdf_url = Column(Text, nullable=True)
|
| 65 |
+
|
| 66 |
+
# Additional information
|
| 67 |
+
notes = Column(Text, nullable=True)
|
| 68 |
+
internal_notes = Column(Text, nullable=True)
|
| 69 |
+
|
| 70 |
+
# Audit fields
|
| 71 |
+
created_by = Column(String(100), nullable=True)
|
| 72 |
+
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
| 73 |
+
updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
|
| 74 |
+
approved_by = Column(String(100), nullable=True)
|
| 75 |
+
approved_at = Column(DateTime, nullable=True)
|
| 76 |
+
|
| 77 |
+
# Metadata
|
| 78 |
+
source = Column(String(50), nullable=True, default="ecommerce")
|
| 79 |
+
channel = Column(String(50), nullable=True, default="web")
|
| 80 |
+
tags = Column(ARRAY(String), nullable=True)
|
| 81 |
+
version = Column(Integer, nullable=False, default=1)
|
| 82 |
+
|
| 83 |
+
# Relationships
|
| 84 |
+
items = relationship("SalesOrderItem", back_populates="order", cascade="all, delete-orphan")
|
| 85 |
+
addresses = relationship("SalesOrderAddress", back_populates="order", cascade="all, delete-orphan")
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class SalesOrderItem(Base):
|
| 89 |
+
"""Sales order item model - trans.sales_order_items table"""
|
| 90 |
+
__tablename__ = "sales_order_items"
|
| 91 |
+
__table_args__ = {"schema": "trans"}
|
| 92 |
+
|
| 93 |
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 94 |
+
sales_order_id = Column(UUID(as_uuid=True), ForeignKey("trans.sales_orders.sales_order_id"), nullable=False, index=True)
|
| 95 |
+
|
| 96 |
+
# Product information
|
| 97 |
+
sku = Column(String(100), nullable=True)
|
| 98 |
+
product_id = Column(String(100), nullable=False)
|
| 99 |
+
product_name = Column(String(255), nullable=False)
|
| 100 |
+
item_type = Column(String(50), nullable=True, default="product")
|
| 101 |
+
|
| 102 |
+
# Quantity and pricing
|
| 103 |
+
quantity = Column(Numeric(15, 3), nullable=False)
|
| 104 |
+
unit_price = Column(Numeric(15, 2), nullable=False)
|
| 105 |
+
tax_percent = Column(Numeric(5, 2), nullable=False, default=0.0)
|
| 106 |
+
discount_percent = Column(Numeric(5, 2), nullable=False, default=0.0)
|
| 107 |
+
line_total = Column(Numeric(15, 2), nullable=False)
|
| 108 |
+
|
| 109 |
+
# Additional information
|
| 110 |
+
hsn_code = Column(String(20), nullable=True)
|
| 111 |
+
uom = Column(String(20), nullable=True, default="PCS")
|
| 112 |
+
batch_no = Column(String(100), nullable=True)
|
| 113 |
+
serials = Column(ARRAY(String), nullable=True)
|
| 114 |
+
|
| 115 |
+
# Service-related fields
|
| 116 |
+
staff_id = Column(String(100), nullable=True)
|
| 117 |
+
staff_name = Column(String(255), nullable=True)
|
| 118 |
+
|
| 119 |
+
# Metadata
|
| 120 |
+
remarks = Column(Text, nullable=True)
|
| 121 |
+
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
| 122 |
+
|
| 123 |
+
# Relationships
|
| 124 |
+
order = relationship("SalesOrder", back_populates="items")
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
class SalesOrderAddress(Base):
|
| 128 |
+
"""Sales order address model - trans.sales_order_addresses table"""
|
| 129 |
+
__tablename__ = "sales_order_addresses"
|
| 130 |
+
__table_args__ = {"schema": "trans"}
|
| 131 |
+
|
| 132 |
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 133 |
+
sales_order_id = Column(UUID(as_uuid=True), ForeignKey("trans.sales_orders.sales_order_id"), nullable=False, index=True)
|
| 134 |
+
|
| 135 |
+
# Address type (shipping, billing)
|
| 136 |
+
address_type = Column(String(50), nullable=False)
|
| 137 |
+
|
| 138 |
+
# Address fields
|
| 139 |
+
line1 = Column(String(255), nullable=False)
|
| 140 |
+
line2 = Column(String(255), nullable=True)
|
| 141 |
+
city = Column(String(100), nullable=False)
|
| 142 |
+
state = Column(String(100), nullable=False)
|
| 143 |
+
postal_code = Column(String(20), nullable=False)
|
| 144 |
+
country = Column(String(100), nullable=False, default="India")
|
| 145 |
+
landmark = Column(String(255), nullable=True)
|
| 146 |
+
|
| 147 |
+
# Metadata
|
| 148 |
+
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
| 149 |
+
|
| 150 |
+
# Relationships
|
| 151 |
+
order = relationship("SalesOrder", back_populates="addresses")
|
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Order schemas"""
|
| 2 |
+
from app.order.schemas.schema import (
|
| 3 |
+
CreateOrderRequest,
|
| 4 |
+
OrderItemRequest,
|
| 5 |
+
OrderAddressRequest,
|
| 6 |
+
OrderResponse,
|
| 7 |
+
OrderItemResponse,
|
| 8 |
+
OrderAddressResponse,
|
| 9 |
+
OrderListRequest,
|
| 10 |
+
OrderListResponse
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
__all__ = [
|
| 14 |
+
"CreateOrderRequest",
|
| 15 |
+
"OrderItemRequest",
|
| 16 |
+
"OrderAddressRequest",
|
| 17 |
+
"OrderResponse",
|
| 18 |
+
"OrderItemResponse",
|
| 19 |
+
"OrderAddressResponse",
|
| 20 |
+
"OrderListRequest",
|
| 21 |
+
"OrderListResponse"
|
| 22 |
+
]
|
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Pydantic schemas for order API requests and responses.
|
| 3 |
+
"""
|
| 4 |
+
from pydantic import BaseModel, Field, field_validator
|
| 5 |
+
from typing import Optional, List
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from decimal import Decimal
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class OrderItemRequest(BaseModel):
|
| 11 |
+
"""Request schema for order item"""
|
| 12 |
+
catalogue_id: str = Field(..., description="Product catalogue ID")
|
| 13 |
+
quantity: int = Field(..., gt=0, description="Quantity to order")
|
| 14 |
+
|
| 15 |
+
@field_validator('quantity')
|
| 16 |
+
@classmethod
|
| 17 |
+
def validate_quantity(cls, v):
|
| 18 |
+
if v <= 0:
|
| 19 |
+
raise ValueError('Quantity must be greater than 0')
|
| 20 |
+
return v
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class OrderAddressRequest(BaseModel):
|
| 24 |
+
"""Request schema for order address"""
|
| 25 |
+
address_type: str = Field(..., description="Address type: shipping or billing")
|
| 26 |
+
line1: str = Field(..., min_length=1, max_length=255, description="Address line 1")
|
| 27 |
+
line2: Optional[str] = Field(None, max_length=255, description="Address line 2")
|
| 28 |
+
city: str = Field(..., min_length=1, max_length=100, description="City")
|
| 29 |
+
state: str = Field(..., min_length=1, max_length=100, description="State")
|
| 30 |
+
postal_code: str = Field(..., min_length=1, max_length=20, description="Postal code")
|
| 31 |
+
country: str = Field(default="India", max_length=100, description="Country")
|
| 32 |
+
landmark: Optional[str] = Field(None, max_length=255, description="Landmark")
|
| 33 |
+
|
| 34 |
+
@field_validator('address_type')
|
| 35 |
+
@classmethod
|
| 36 |
+
def validate_address_type(cls, v):
|
| 37 |
+
if v.lower() not in ['shipping', 'billing']:
|
| 38 |
+
raise ValueError('Address type must be either "shipping" or "billing"')
|
| 39 |
+
return v.lower()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class CreateOrderRequest(BaseModel):
|
| 43 |
+
"""Request schema for creating an order"""
|
| 44 |
+
items: List[OrderItemRequest] = Field(..., min_length=1, description="Order items")
|
| 45 |
+
shipping_address: OrderAddressRequest = Field(..., description="Shipping address")
|
| 46 |
+
billing_address: Optional[OrderAddressRequest] = Field(None, description="Billing address (optional, defaults to shipping)")
|
| 47 |
+
customer_name: Optional[str] = Field(None, max_length=255, description="Customer name")
|
| 48 |
+
customer_phone: Optional[str] = Field(None, max_length=20, description="Customer phone")
|
| 49 |
+
customer_email: Optional[str] = Field(None, max_length=255, description="Customer email")
|
| 50 |
+
payment_method: Optional[str] = Field(None, description="Payment method")
|
| 51 |
+
notes: Optional[str] = Field(None, description="Order notes")
|
| 52 |
+
|
| 53 |
+
@field_validator('items')
|
| 54 |
+
@classmethod
|
| 55 |
+
def validate_items(cls, v):
|
| 56 |
+
if not v or len(v) == 0:
|
| 57 |
+
raise ValueError('Order must have at least one item')
|
| 58 |
+
return v
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class OrderItemResponse(BaseModel):
|
| 62 |
+
"""Response schema for order item"""
|
| 63 |
+
id: str
|
| 64 |
+
product_id: str
|
| 65 |
+
product_name: str
|
| 66 |
+
sku: Optional[str] = None
|
| 67 |
+
quantity: float
|
| 68 |
+
unit_price: float
|
| 69 |
+
tax_percent: float
|
| 70 |
+
discount_percent: float
|
| 71 |
+
line_total: float
|
| 72 |
+
uom: Optional[str] = None
|
| 73 |
+
hsn_code: Optional[str] = None
|
| 74 |
+
|
| 75 |
+
class Config:
|
| 76 |
+
from_attributes = True
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class OrderAddressResponse(BaseModel):
|
| 80 |
+
"""Response schema for order address"""
|
| 81 |
+
id: str
|
| 82 |
+
address_type: str
|
| 83 |
+
line1: str
|
| 84 |
+
line2: Optional[str] = None
|
| 85 |
+
city: str
|
| 86 |
+
state: str
|
| 87 |
+
postal_code: str
|
| 88 |
+
country: str
|
| 89 |
+
landmark: Optional[str] = None
|
| 90 |
+
|
| 91 |
+
class Config:
|
| 92 |
+
from_attributes = True
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class OrderResponse(BaseModel):
|
| 96 |
+
"""Response schema for order"""
|
| 97 |
+
sales_order_id: str
|
| 98 |
+
order_number: str
|
| 99 |
+
merchant_id: str
|
| 100 |
+
order_date: datetime
|
| 101 |
+
status: str
|
| 102 |
+
customer_id: str
|
| 103 |
+
customer_name: Optional[str] = None
|
| 104 |
+
customer_phone: Optional[str] = None
|
| 105 |
+
customer_email: Optional[str] = None
|
| 106 |
+
subtotal: float
|
| 107 |
+
total_discount: float
|
| 108 |
+
total_tax: float
|
| 109 |
+
shipping_charges: float
|
| 110 |
+
grand_total: float
|
| 111 |
+
payment_status: str
|
| 112 |
+
payment_method: Optional[str] = None
|
| 113 |
+
fulfillment_status: str
|
| 114 |
+
notes: Optional[str] = None
|
| 115 |
+
source: Optional[str] = None
|
| 116 |
+
channel: Optional[str] = None
|
| 117 |
+
created_at: datetime
|
| 118 |
+
items: List[OrderItemResponse] = []
|
| 119 |
+
addresses: List[OrderAddressResponse] = []
|
| 120 |
+
|
| 121 |
+
class Config:
|
| 122 |
+
from_attributes = True
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
class OrderListRequest(BaseModel):
|
| 126 |
+
"""Request schema for listing orders with projection support"""
|
| 127 |
+
filters: Optional[dict] = Field(default_factory=dict, description="Filter criteria")
|
| 128 |
+
skip: int = Field(default=0, ge=0, description="Number of records to skip")
|
| 129 |
+
limit: int = Field(default=100, ge=1, le=1000, description="Maximum number of records to return")
|
| 130 |
+
projection_list: Optional[List[str]] = Field(None, description="List of fields to include in response")
|
| 131 |
+
sort_by: Optional[str] = Field(default="created_at", description="Field to sort by")
|
| 132 |
+
sort_order: Optional[str] = Field(default="desc", description="Sort order: asc or desc")
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
class OrderListResponse(BaseModel):
|
| 136 |
+
"""Response schema for order list"""
|
| 137 |
+
total: int = Field(..., description="Total number of orders matching filters")
|
| 138 |
+
skip: int = Field(..., description="Number of records skipped")
|
| 139 |
+
limit: int = Field(..., description="Maximum number of records returned")
|
| 140 |
+
orders: List[dict] = Field(..., description="List of orders")
|
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Order services"""
|
| 2 |
+
from app.order.services.service import OrderService
|
| 3 |
+
|
| 4 |
+
__all__ = ["OrderService"]
|
|
@@ -0,0 +1,491 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Order service for creating and managing customer orders.
|
| 3 |
+
Handles order creation, persistence to PostgreSQL, and order retrieval.
|
| 4 |
+
"""
|
| 5 |
+
from typing import Optional, List, Dict, Any
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from decimal import Decimal
|
| 8 |
+
from insightfy_utils.logging import get_logger
|
| 9 |
+
from sqlalchemy import select, func, desc, asc
|
| 10 |
+
from sqlalchemy.orm import selectinload
|
| 11 |
+
|
| 12 |
+
from app.sql import async_session
|
| 13 |
+
from app.nosql import get_database
|
| 14 |
+
from app.order.models.model import SalesOrder, SalesOrderItem, SalesOrderAddress
|
| 15 |
+
from app.order.schemas.schema import (
|
| 16 |
+
CreateOrderRequest,
|
| 17 |
+
OrderResponse,
|
| 18 |
+
OrderItemResponse,
|
| 19 |
+
OrderAddressResponse
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
logger = get_logger(__name__)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class OrderService:
|
| 26 |
+
"""Service for managing customer orders"""
|
| 27 |
+
|
| 28 |
+
@staticmethod
|
| 29 |
+
async def _get_product_details(catalogue_id: str) -> Optional[Dict[str, Any]]:
|
| 30 |
+
"""
|
| 31 |
+
Fetch product details from MongoDB.
|
| 32 |
+
|
| 33 |
+
Args:
|
| 34 |
+
catalogue_id: Product catalogue ID
|
| 35 |
+
|
| 36 |
+
Returns:
|
| 37 |
+
Product details dict or None if not found
|
| 38 |
+
"""
|
| 39 |
+
try:
|
| 40 |
+
db = get_database()
|
| 41 |
+
collection = db["scm_catalogues"]
|
| 42 |
+
|
| 43 |
+
product = await collection.find_one({
|
| 44 |
+
"catalogue_id": catalogue_id,
|
| 45 |
+
"catalogue_type": "Product",
|
| 46 |
+
"meta.status": {"$in": ["active", "Active"]},
|
| 47 |
+
"allow_ecommerce": True
|
| 48 |
+
})
|
| 49 |
+
|
| 50 |
+
if not product:
|
| 51 |
+
logger.warning(f"Product not found: {catalogue_id}")
|
| 52 |
+
return None
|
| 53 |
+
|
| 54 |
+
# Extract pricing and tax information
|
| 55 |
+
pricing = product.get("pricing", {})
|
| 56 |
+
mrp = pricing.get("mrp", 0.0)
|
| 57 |
+
|
| 58 |
+
tax = product.get("tax", {})
|
| 59 |
+
gst_rate = tax.get("gst_rate", 0.0)
|
| 60 |
+
hsn_code = tax.get("hsn_code")
|
| 61 |
+
|
| 62 |
+
# Extract inventory information
|
| 63 |
+
inventory = product.get("inventory", {})
|
| 64 |
+
unit = inventory.get("unit", "PCS")
|
| 65 |
+
|
| 66 |
+
return {
|
| 67 |
+
"merchant_id": product.get("merchant_id"),
|
| 68 |
+
"catalogue_id": product.get("catalogue_id"),
|
| 69 |
+
"catalogue_code": product.get("catalogue_code"),
|
| 70 |
+
"name": product.get("catalogue_name", ""),
|
| 71 |
+
"price": float(mrp) if mrp else 0.0,
|
| 72 |
+
"gst_rate": float(gst_rate) if gst_rate else 0.0,
|
| 73 |
+
"hsn_code": hsn_code,
|
| 74 |
+
"unit": unit
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
except Exception as e:
|
| 78 |
+
logger.error(f"Error fetching product details: {str(e)}", exc_info=e)
|
| 79 |
+
return None
|
| 80 |
+
|
| 81 |
+
@staticmethod
|
| 82 |
+
async def _generate_order_number() -> str:
|
| 83 |
+
"""
|
| 84 |
+
Generate unique order number.
|
| 85 |
+
Format: ORD-YYYYMMDD-NNNN
|
| 86 |
+
"""
|
| 87 |
+
date_str = datetime.utcnow().strftime("%Y%m%d")
|
| 88 |
+
|
| 89 |
+
async with async_session() as session:
|
| 90 |
+
# Get count of orders today
|
| 91 |
+
result = await session.execute(
|
| 92 |
+
select(func.count(SalesOrder.sales_order_id))
|
| 93 |
+
.where(SalesOrder.order_number.like(f"ORD-{date_str}-%"))
|
| 94 |
+
)
|
| 95 |
+
count = result.scalar() or 0
|
| 96 |
+
|
| 97 |
+
order_number = f"ORD-{date_str}-{count + 1:04d}"
|
| 98 |
+
return order_number
|
| 99 |
+
|
| 100 |
+
@staticmethod
|
| 101 |
+
async def create_order(
|
| 102 |
+
customer_id: str,
|
| 103 |
+
request: CreateOrderRequest
|
| 104 |
+
) -> Dict[str, Any]:
|
| 105 |
+
"""
|
| 106 |
+
Create a new order from customer request.
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
customer_id: Customer ID from JWT token
|
| 110 |
+
request: Order creation request
|
| 111 |
+
|
| 112 |
+
Returns:
|
| 113 |
+
Result dict with success status and order data
|
| 114 |
+
"""
|
| 115 |
+
try:
|
| 116 |
+
# Validate and fetch product details for all items
|
| 117 |
+
items_data = []
|
| 118 |
+
merchant_ids = set()
|
| 119 |
+
|
| 120 |
+
for item_req in request.items:
|
| 121 |
+
product = await OrderService._get_product_details(item_req.catalogue_id)
|
| 122 |
+
if not product:
|
| 123 |
+
return {
|
| 124 |
+
"success": False,
|
| 125 |
+
"message": f"Product not found: {item_req.catalogue_id}",
|
| 126 |
+
"order": None
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
merchant_ids.add(product["merchant_id"])
|
| 130 |
+
items_data.append({
|
| 131 |
+
"product": product,
|
| 132 |
+
"quantity": item_req.quantity
|
| 133 |
+
})
|
| 134 |
+
|
| 135 |
+
# Validate single merchant per order
|
| 136 |
+
if len(merchant_ids) > 1:
|
| 137 |
+
return {
|
| 138 |
+
"success": False,
|
| 139 |
+
"message": "All items must be from the same merchant",
|
| 140 |
+
"order": None
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
merchant_id = merchant_ids.pop()
|
| 144 |
+
|
| 145 |
+
# Calculate order totals
|
| 146 |
+
subtotal = Decimal("0.00")
|
| 147 |
+
total_tax = Decimal("0.00")
|
| 148 |
+
|
| 149 |
+
for item_data in items_data:
|
| 150 |
+
product = item_data["product"]
|
| 151 |
+
quantity = Decimal(str(item_data["quantity"]))
|
| 152 |
+
unit_price = Decimal(str(product["price"]))
|
| 153 |
+
gst_rate = Decimal(str(product["gst_rate"]))
|
| 154 |
+
|
| 155 |
+
line_subtotal = unit_price * quantity
|
| 156 |
+
line_tax = line_subtotal * (gst_rate / Decimal("100"))
|
| 157 |
+
|
| 158 |
+
subtotal += line_subtotal
|
| 159 |
+
total_tax += line_tax
|
| 160 |
+
|
| 161 |
+
item_data["unit_price"] = unit_price
|
| 162 |
+
item_data["line_subtotal"] = line_subtotal
|
| 163 |
+
item_data["line_tax"] = line_tax
|
| 164 |
+
item_data["line_total"] = line_subtotal + line_tax
|
| 165 |
+
|
| 166 |
+
grand_total = subtotal + total_tax
|
| 167 |
+
|
| 168 |
+
# Generate order number
|
| 169 |
+
order_number = await OrderService._generate_order_number()
|
| 170 |
+
|
| 171 |
+
# Create order in database
|
| 172 |
+
async with async_session() as session:
|
| 173 |
+
async with session.begin():
|
| 174 |
+
# Create sales order
|
| 175 |
+
sales_order = SalesOrder(
|
| 176 |
+
order_number=order_number,
|
| 177 |
+
merchant_id=merchant_id,
|
| 178 |
+
order_date=datetime.utcnow(),
|
| 179 |
+
status="pending",
|
| 180 |
+
customer_id=customer_id,
|
| 181 |
+
customer_name=request.customer_name,
|
| 182 |
+
customer_phone=request.customer_phone,
|
| 183 |
+
customer_email=request.customer_email,
|
| 184 |
+
customer_type="retail",
|
| 185 |
+
subtotal=float(subtotal),
|
| 186 |
+
total_discount=0.0,
|
| 187 |
+
total_tax=float(total_tax),
|
| 188 |
+
shipping_charges=0.0,
|
| 189 |
+
grand_total=float(grand_total),
|
| 190 |
+
cgst=0.0,
|
| 191 |
+
sgst=0.0,
|
| 192 |
+
igst=float(total_tax), # Simplified: all tax as IGST
|
| 193 |
+
payment_status="pending",
|
| 194 |
+
payment_method=request.payment_method,
|
| 195 |
+
amount_paid=0.0,
|
| 196 |
+
amount_due=float(grand_total),
|
| 197 |
+
fulfillment_status="pending",
|
| 198 |
+
notes=request.notes,
|
| 199 |
+
created_by=customer_id,
|
| 200 |
+
source="ecommerce",
|
| 201 |
+
channel="web"
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
session.add(sales_order)
|
| 205 |
+
await session.flush() # Get sales_order_id
|
| 206 |
+
|
| 207 |
+
# Create order items
|
| 208 |
+
for item_data in items_data:
|
| 209 |
+
product = item_data["product"]
|
| 210 |
+
|
| 211 |
+
order_item = SalesOrderItem(
|
| 212 |
+
sales_order_id=sales_order.sales_order_id,
|
| 213 |
+
sku=product["catalogue_code"],
|
| 214 |
+
product_id=product["catalogue_id"],
|
| 215 |
+
product_name=product["name"],
|
| 216 |
+
item_type="product",
|
| 217 |
+
quantity=float(item_data["quantity"]),
|
| 218 |
+
unit_price=float(item_data["unit_price"]),
|
| 219 |
+
tax_percent=float(product["gst_rate"]),
|
| 220 |
+
discount_percent=0.0,
|
| 221 |
+
line_total=float(item_data["line_total"]),
|
| 222 |
+
hsn_code=product.get("hsn_code"),
|
| 223 |
+
uom=product["unit"]
|
| 224 |
+
)
|
| 225 |
+
session.add(order_item)
|
| 226 |
+
|
| 227 |
+
# Create shipping address
|
| 228 |
+
shipping_addr = SalesOrderAddress(
|
| 229 |
+
sales_order_id=sales_order.sales_order_id,
|
| 230 |
+
address_type="shipping",
|
| 231 |
+
line1=request.shipping_address.line1,
|
| 232 |
+
line2=request.shipping_address.line2,
|
| 233 |
+
city=request.shipping_address.city,
|
| 234 |
+
state=request.shipping_address.state,
|
| 235 |
+
postal_code=request.shipping_address.postal_code,
|
| 236 |
+
country=request.shipping_address.country,
|
| 237 |
+
landmark=request.shipping_address.landmark
|
| 238 |
+
)
|
| 239 |
+
session.add(shipping_addr)
|
| 240 |
+
|
| 241 |
+
# Create billing address (use shipping if not provided)
|
| 242 |
+
billing_req = request.billing_address or request.shipping_address
|
| 243 |
+
billing_addr = SalesOrderAddress(
|
| 244 |
+
sales_order_id=sales_order.sales_order_id,
|
| 245 |
+
address_type="billing",
|
| 246 |
+
line1=billing_req.line1,
|
| 247 |
+
line2=billing_req.line2,
|
| 248 |
+
city=billing_req.city,
|
| 249 |
+
state=billing_req.state,
|
| 250 |
+
postal_code=billing_req.postal_code,
|
| 251 |
+
country=billing_req.country,
|
| 252 |
+
landmark=billing_req.landmark
|
| 253 |
+
)
|
| 254 |
+
session.add(billing_addr)
|
| 255 |
+
|
| 256 |
+
await session.commit()
|
| 257 |
+
|
| 258 |
+
# Refresh to get relationships
|
| 259 |
+
await session.refresh(sales_order)
|
| 260 |
+
|
| 261 |
+
# Load relationships
|
| 262 |
+
result = await session.execute(
|
| 263 |
+
select(SalesOrder)
|
| 264 |
+
.options(
|
| 265 |
+
selectinload(SalesOrder.items),
|
| 266 |
+
selectinload(SalesOrder.addresses)
|
| 267 |
+
)
|
| 268 |
+
.where(SalesOrder.sales_order_id == sales_order.sales_order_id)
|
| 269 |
+
)
|
| 270 |
+
order_with_relations = result.scalar_one()
|
| 271 |
+
|
| 272 |
+
logger.info(
|
| 273 |
+
"Order created successfully",
|
| 274 |
+
extra={
|
| 275 |
+
"customer_id": customer_id,
|
| 276 |
+
"order_number": order_number,
|
| 277 |
+
"merchant_id": str(merchant_id),
|
| 278 |
+
"grand_total": float(grand_total)
|
| 279 |
+
}
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
# Convert to response model
|
| 283 |
+
order_response = OrderService._order_to_response(order_with_relations)
|
| 284 |
+
|
| 285 |
+
return {
|
| 286 |
+
"success": True,
|
| 287 |
+
"message": "Order created successfully",
|
| 288 |
+
"order": order_response
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
except Exception as e:
|
| 292 |
+
logger.error(f"Error creating order: {str(e)}", exc_info=e)
|
| 293 |
+
return {
|
| 294 |
+
"success": False,
|
| 295 |
+
"message": f"Failed to create order: {str(e)}",
|
| 296 |
+
"order": None
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
@staticmethod
|
| 300 |
+
def _order_to_response(order: SalesOrder) -> OrderResponse:
|
| 301 |
+
"""Convert SQLAlchemy model to Pydantic response"""
|
| 302 |
+
return OrderResponse(
|
| 303 |
+
sales_order_id=str(order.sales_order_id),
|
| 304 |
+
order_number=order.order_number,
|
| 305 |
+
merchant_id=str(order.merchant_id),
|
| 306 |
+
order_date=order.order_date,
|
| 307 |
+
status=order.status,
|
| 308 |
+
customer_id=order.customer_id,
|
| 309 |
+
customer_name=order.customer_name,
|
| 310 |
+
customer_phone=order.customer_phone,
|
| 311 |
+
customer_email=order.customer_email,
|
| 312 |
+
subtotal=float(order.subtotal),
|
| 313 |
+
total_discount=float(order.total_discount),
|
| 314 |
+
total_tax=float(order.total_tax),
|
| 315 |
+
shipping_charges=float(order.shipping_charges),
|
| 316 |
+
grand_total=float(order.grand_total),
|
| 317 |
+
payment_status=order.payment_status,
|
| 318 |
+
payment_method=order.payment_method,
|
| 319 |
+
fulfillment_status=order.fulfillment_status,
|
| 320 |
+
notes=order.notes,
|
| 321 |
+
source=order.source,
|
| 322 |
+
channel=order.channel,
|
| 323 |
+
created_at=order.created_at,
|
| 324 |
+
items=[
|
| 325 |
+
OrderItemResponse(
|
| 326 |
+
id=str(item.id),
|
| 327 |
+
product_id=item.product_id,
|
| 328 |
+
product_name=item.product_name,
|
| 329 |
+
sku=item.sku,
|
| 330 |
+
quantity=float(item.quantity),
|
| 331 |
+
unit_price=float(item.unit_price),
|
| 332 |
+
tax_percent=float(item.tax_percent),
|
| 333 |
+
discount_percent=float(item.discount_percent),
|
| 334 |
+
line_total=float(item.line_total),
|
| 335 |
+
uom=item.uom,
|
| 336 |
+
hsn_code=item.hsn_code
|
| 337 |
+
)
|
| 338 |
+
for item in order.items
|
| 339 |
+
],
|
| 340 |
+
addresses=[
|
| 341 |
+
OrderAddressResponse(
|
| 342 |
+
id=str(addr.id),
|
| 343 |
+
address_type=addr.address_type,
|
| 344 |
+
line1=addr.line1,
|
| 345 |
+
line2=addr.line2,
|
| 346 |
+
city=addr.city,
|
| 347 |
+
state=addr.state,
|
| 348 |
+
postal_code=addr.postal_code,
|
| 349 |
+
country=addr.country,
|
| 350 |
+
landmark=addr.landmark
|
| 351 |
+
)
|
| 352 |
+
for addr in order.addresses
|
| 353 |
+
]
|
| 354 |
+
)
|
| 355 |
+
|
| 356 |
+
@staticmethod
|
| 357 |
+
async def get_order(
|
| 358 |
+
customer_id: str,
|
| 359 |
+
order_id: str
|
| 360 |
+
) -> Optional[OrderResponse]:
|
| 361 |
+
"""
|
| 362 |
+
Get order by ID for a specific customer.
|
| 363 |
+
|
| 364 |
+
Args:
|
| 365 |
+
customer_id: Customer ID from JWT token
|
| 366 |
+
order_id: Order ID
|
| 367 |
+
|
| 368 |
+
Returns:
|
| 369 |
+
OrderResponse or None if not found
|
| 370 |
+
"""
|
| 371 |
+
try:
|
| 372 |
+
async with async_session() as session:
|
| 373 |
+
result = await session.execute(
|
| 374 |
+
select(SalesOrder)
|
| 375 |
+
.options(
|
| 376 |
+
selectinload(SalesOrder.items),
|
| 377 |
+
selectinload(SalesOrder.addresses)
|
| 378 |
+
)
|
| 379 |
+
.where(
|
| 380 |
+
SalesOrder.sales_order_id == order_id,
|
| 381 |
+
SalesOrder.customer_id == customer_id
|
| 382 |
+
)
|
| 383 |
+
)
|
| 384 |
+
order = result.scalar_one_or_none()
|
| 385 |
+
|
| 386 |
+
if not order:
|
| 387 |
+
return None
|
| 388 |
+
|
| 389 |
+
return OrderService._order_to_response(order)
|
| 390 |
+
|
| 391 |
+
except Exception as e:
|
| 392 |
+
logger.error(f"Error getting order: {str(e)}", exc_info=e)
|
| 393 |
+
return None
|
| 394 |
+
|
| 395 |
+
@staticmethod
|
| 396 |
+
async def list_orders(
|
| 397 |
+
customer_id: str,
|
| 398 |
+
filters: Optional[Dict[str, Any]] = None,
|
| 399 |
+
skip: int = 0,
|
| 400 |
+
limit: int = 100,
|
| 401 |
+
projection_list: Optional[List[str]] = None,
|
| 402 |
+
sort_by: str = "created_at",
|
| 403 |
+
sort_order: str = "desc"
|
| 404 |
+
) -> Dict[str, Any]:
|
| 405 |
+
"""
|
| 406 |
+
List orders for a customer with optional projection support.
|
| 407 |
+
|
| 408 |
+
Args:
|
| 409 |
+
customer_id: Customer ID from JWT token
|
| 410 |
+
filters: Additional filter criteria
|
| 411 |
+
skip: Number of records to skip
|
| 412 |
+
limit: Maximum number of records to return
|
| 413 |
+
projection_list: List of fields to include in response
|
| 414 |
+
sort_by: Field to sort by
|
| 415 |
+
sort_order: Sort order (asc or desc)
|
| 416 |
+
|
| 417 |
+
Returns:
|
| 418 |
+
Dict with total count and orders list
|
| 419 |
+
"""
|
| 420 |
+
try:
|
| 421 |
+
async with async_session() as session:
|
| 422 |
+
# Build base query
|
| 423 |
+
query = select(SalesOrder).where(SalesOrder.customer_id == customer_id)
|
| 424 |
+
|
| 425 |
+
# Apply additional filters
|
| 426 |
+
if filters:
|
| 427 |
+
if "status" in filters:
|
| 428 |
+
query = query.where(SalesOrder.status == filters["status"])
|
| 429 |
+
if "payment_status" in filters:
|
| 430 |
+
query = query.where(SalesOrder.payment_status == filters["payment_status"])
|
| 431 |
+
if "fulfillment_status" in filters:
|
| 432 |
+
query = query.where(SalesOrder.fulfillment_status == filters["fulfillment_status"])
|
| 433 |
+
|
| 434 |
+
# Get total count
|
| 435 |
+
count_query = select(func.count()).select_from(query.subquery())
|
| 436 |
+
total_result = await session.execute(count_query)
|
| 437 |
+
total = total_result.scalar() or 0
|
| 438 |
+
|
| 439 |
+
# Apply sorting
|
| 440 |
+
sort_column = getattr(SalesOrder, sort_by, SalesOrder.created_at)
|
| 441 |
+
if sort_order.lower() == "asc":
|
| 442 |
+
query = query.order_by(asc(sort_column))
|
| 443 |
+
else:
|
| 444 |
+
query = query.order_by(desc(sort_column))
|
| 445 |
+
|
| 446 |
+
# Apply pagination
|
| 447 |
+
query = query.offset(skip).limit(limit)
|
| 448 |
+
|
| 449 |
+
# Load relationships if not using projection
|
| 450 |
+
if not projection_list:
|
| 451 |
+
query = query.options(
|
| 452 |
+
selectinload(SalesOrder.items),
|
| 453 |
+
selectinload(SalesOrder.addresses)
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
result = await session.execute(query)
|
| 457 |
+
orders = result.scalars().all()
|
| 458 |
+
|
| 459 |
+
# Convert to response format
|
| 460 |
+
if projection_list:
|
| 461 |
+
# Return only requested fields
|
| 462 |
+
orders_list = [
|
| 463 |
+
{
|
| 464 |
+
field: getattr(order, field, None)
|
| 465 |
+
for field in projection_list
|
| 466 |
+
if hasattr(order, field)
|
| 467 |
+
}
|
| 468 |
+
for order in orders
|
| 469 |
+
]
|
| 470 |
+
else:
|
| 471 |
+
# Return full order objects
|
| 472 |
+
orders_list = [
|
| 473 |
+
OrderService._order_to_response(order).model_dump()
|
| 474 |
+
for order in orders
|
| 475 |
+
]
|
| 476 |
+
|
| 477 |
+
return {
|
| 478 |
+
"total": total,
|
| 479 |
+
"skip": skip,
|
| 480 |
+
"limit": limit,
|
| 481 |
+
"orders": orders_list
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
except Exception as e:
|
| 485 |
+
logger.error(f"Error listing orders: {str(e)}", exc_info=e)
|
| 486 |
+
return {
|
| 487 |
+
"total": 0,
|
| 488 |
+
"skip": skip,
|
| 489 |
+
"limit": limit,
|
| 490 |
+
"orders": []
|
| 491 |
+
}
|
|
@@ -253,7 +253,7 @@ async def enforce_trans_schema() -> None:
|
|
| 253 |
from app.core.database import Base
|
| 254 |
|
| 255 |
# Import all models to ensure they're registered with Base
|
| 256 |
-
|
| 257 |
|
| 258 |
# Validate schema compliance
|
| 259 |
non_trans_tables = []
|
|
@@ -283,6 +283,8 @@ async def create_tables() -> None:
|
|
| 283 |
await enforce_trans_schema()
|
| 284 |
|
| 285 |
from app.core.database import Base
|
|
|
|
|
|
|
| 286 |
|
| 287 |
async with async_engine.begin() as conn:
|
| 288 |
# Create all tables (schema already validated)
|
|
|
|
| 253 |
from app.core.database import Base
|
| 254 |
|
| 255 |
# Import all models to ensure they're registered with Base
|
| 256 |
+
from app.order.models.model import SalesOrder, SalesOrderItem, SalesOrderAddress
|
| 257 |
|
| 258 |
# Validate schema compliance
|
| 259 |
non_trans_tables = []
|
|
|
|
| 283 |
await enforce_trans_schema()
|
| 284 |
|
| 285 |
from app.core.database import Base
|
| 286 |
+
# Import all models to ensure they're registered
|
| 287 |
+
from app.order.models.model import SalesOrder, SalesOrderItem, SalesOrderAddress
|
| 288 |
|
| 289 |
async with async_engine.begin() as conn:
|
| 290 |
# Create all tables (schema already validated)
|