Spaces:
Sleeping
Sleeping
File size: 10,241 Bytes
38ac151 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 | # Ticket Management - Critical Design Corrections
## Overview
This document explains the critical corrections made to the ticket deduplication strategy and subcontractor assignment model based on real-world business requirements.
---
## 1. Deduplication Strategy - CORRECTED β
### Previous (WRONG) Design
```python
# dedup_key = MD5(source + source_id + ticket_type)
# Cleared on completion β allows re-creation
```
**Problems:**
- Allowed same sales order to create multiple tickets
- Dedup key was cleared on completion/cancellation
- Not real-world compliant
### Current (CORRECT) Design - WITH REACTIVATION SUPPORT
```python
# dedup_key = MD5(source + source_id + ticket_type + created_at)
# PERMANENT - never cleared
# Tracks creation instance, not lifecycle state
```
**Business Rules:**
1. **One source = One ticket per creation instance**
- A sales order creates ONE ticket when first promoted
- Dedup_key is permanent (tracks creation timestamp)
- Ticket can be reactivated without creating new row
2. **Reactivation (Same Service):**
- Customer cancels β Ticket status = cancelled (dedup_key remains)
- Customer wants service again β REACTIVATE existing ticket
- Update: status cancelledβopen, new dates, add reactivation note
- **Benefits**: Space efficient, payment history preserved, chronic canceller detection
3. **New Service (Different Package):**
- Customer had 10Mbps (cancelled) β Now wants 50Mbps
- Create NEW sales order β Creates NEW ticket
- New dedup_key (different creation timestamp)
- History shows multiple tickets for customer
4. **Why This Design?**
- **Space efficient**: No duplicate rows for reactivations
- **Payment tracking**: If customer paid before cancelling, we know
- **Customer behavior**: Can identify chronic cancellers
- **Audit trail**: Complete history per customer
- **Flexibility**: New package = new ticket, same service = reactivate
### Code Changes
**Model (`src/app/models/ticket.py`):**
```python
# Before
dedup_key = Column(Text, nullable=True, unique=False)
def generate_dedup_key(source, source_id, ticket_type):
dedup_string = f"{source}::{source_id}::{ticket_type}"
return hashlib.md5(dedup_string.encode()).hexdigest()
def mark_as_completed(self):
self.dedup_key = None # WRONG - cleared
# After
dedup_key = Column(Text, nullable=True, unique=True)
def generate_dedup_key(source, source_id, ticket_type, created_at):
timestamp = created_at.isoformat()
dedup_string = f"{source}::{source_id}::{ticket_type}::{timestamp}"
return hashlib.md5(dedup_string.encode()).hexdigest()
def mark_as_completed(self):
# dedup_key NEVER cleared - permanent
```
**Service (`src/app/services/ticket_service.py`):**
```python
# Check for existing ticket (one source = one ticket)
existing_ticket = db.query(Ticket).filter(
Ticket.source == TicketSource.SALES_ORDER.value,
Ticket.source_id == sales_order.id,
Ticket.ticket_type == TicketType.INSTALLATION.value,
Ticket.deleted_at.is_(None)
).first()
if existing_ticket:
raise HTTPException(
status_code=409,
detail="Ticket already exists. One sales order can only create one ticket."
)
# Generate permanent dedup key
created_at = datetime.utcnow()
dedup_key = Ticket.generate_dedup_key(
source=TicketSource.SALES_ORDER.value,
source_id=sales_order.id,
ticket_type=TicketType.INSTALLATION.value,
created_at=created_at
)
```
---
## 2. Subcontractor Assignment - REMOVED β
### Previous Design
- `project_subcontractor_id` column in tickets table
- Direct link to project_subcontractors
### Current (CORRECT) Design
- **REMOVED** `project_subcontractor_id` from tickets
- Subcontractor relationship via: `ticket_assignments β users β project_team β project_subcontractors`
**Why?**
- Tickets use assignments for field agent allocation
- Assignments link to users
- Users have subcontractor relationship via project_team
- No need for direct subcontractor link on ticket
### Migration
```sql
-- File: supabase/migrations/20241116000001_remove_subcontractor_from_tickets.sql
ALTER TABLE tickets
DROP CONSTRAINT IF EXISTS tickets_project_subcontractor_id_fkey;
DROP INDEX IF EXISTS idx_tickets_subcontractor;
ALTER TABLE tickets
DROP COLUMN IF EXISTS project_subcontractor_id;
```
### Model Changes
```python
# Before
project_id = Column(UUID, ForeignKey("projects.id"), nullable=False)
project_subcontractor_id = Column(UUID, ForeignKey("project_subcontractors.id"), nullable=True)
# After
project_id = Column(UUID, ForeignKey("projects.id"), nullable=False)
# Note: Subcontractor via ticket_assignments β users β project_team
```
---
## 3. Task β Ticket Creation - IMPLEMENTED β
### Added Support for Infrastructure Tasks
**Service Method:**
```python
@staticmethod
def create_ticket_from_task(
db: Session,
data: TicketCreateFromTask,
current_user: User
) -> Ticket:
"""
Create infrastructure ticket from task.
Business Rule: One task = One ticket ever
"""
```
**API Endpoint:**
```
POST /api/v1/tickets/from-task
```
**Example:**
```json
{
"task_id": "uuid-here",
"priority": "normal",
"scheduled_date": "2024-03-20",
"scheduled_time_slot": "morning",
"work_description": "Install fiber cable from pole A to B",
"notes": "Requires cherry picker"
}
```
---
## 4. Real-World Business Flow
### Sales Order Scenario (With Reactivation)
```
SCENARIO A: SAME SERVICE REACTIVATION (Space Efficient)
1. Customer orders 10Mbps WiFi
β Sales order created (status=pending)
2. Sales order promoted to ticket
β Ticket created (dedup_key = MD5(...timestamp1...))
β Sales order status = processed
3. Customer cancels before installation
β Ticket status = cancelled
β Dedup key REMAINS (permanent)
β Payment record preserved (if they paid)
4. Customer wants WiFi again (SAME 10Mbps)
β Find existing sales order & cancelled ticket
β REACTIVATE ticket: status cancelledβopen
β Update dates, add reactivation note
β REUSE same dedup_key
β Space efficient! No new row created
5. If customer cancels AGAIN
β Ticket status = cancelled again
β System knows: This customer is a chronic canceller!
SCENARIO B: DIFFERENT SERVICE (New Ticket)
1. Customer had 10Mbps (cancelled)
2. Customer now wants 50Mbps (different package)
β Create NEW sales order (different package)
β Creates NEW ticket (dedup_key = MD5(...timestamp2...))
3. History shows:
- Sales Order 1: 10Mbps (cancelled)
- Sales Order 2: 50Mbps (active)
- Customer has 2 tickets in system
```
### Task Scenario
```
1. Infrastructure project: Install fiber backbone
β Task created: "Install cable segment A-B"
2. Task promoted to ticket
β Ticket created (dedup_key = MD5(...timestamp...))
β Assigned to field agent
3. Work completed
β Ticket status = completed
β Task status = completed
β Dedup key REMAINS
4. Cable damaged, needs replacement
β NEW task created: "Replace cable segment A-B"
β NEW ticket created (different dedup_key)
```
---
## 5. Database Schema Impact
### Unique Constraint
```sql
-- Previous (WRONG)
CREATE UNIQUE INDEX idx_tickets_source_unique
ON tickets (source, source_id, ticket_type)
WHERE deleted_at IS NULL AND source_id IS NOT NULL;
-- This allowed re-creation after completion
```
**Current (CORRECT):**
- Dedup_key column has `unique=True` in model
- Timestamp in dedup_key ensures permanent uniqueness
- One source can only create one ticket ever
---
## 6. Testing Scenarios
### Scenario 1: Duplicate Prevention
```python
# Create ticket from sales order
ticket1 = create_ticket_from_sales_order(sales_order_id="uuid1")
# β
Success
# Try to create again
ticket2 = create_ticket_from_sales_order(sales_order_id="uuid1")
# β 409 Conflict: "Ticket already exists"
```
### Scenario 2: Reactivation (Space Efficient)
```python
# Complete/cancel ticket
ticket.mark_as_cancelled(reason="Customer changed mind")
# dedup_key remains (permanent)
# Customer wants service again
ticket2 = create_ticket_from_sales_order(sales_order_id="uuid1")
# β
Success - REACTIVATES existing ticket
# - Status: cancelled β open
# - Updates dates and notes
# - Reuses same dedup_key
# - No new row created (space efficient!)
# - Payment history preserved
```
### Scenario 3: New Source = New Ticket
```python
# Complete first ticket
ticket1.mark_as_completed()
# Create NEW sales order (new business transaction)
sales_order2 = create_sales_order(customer_id=same_customer)
# Create ticket from new sales order
ticket2 = create_ticket_from_sales_order(sales_order_id=sales_order2.id)
# β
Success (different source_id β different dedup_key)
```
---
## 7. Migration Checklist
- [x] Update Ticket model - permanent dedup_key with timestamp
- [x] Remove project_subcontractor_id from Ticket model
- [x] Update generate_dedup_key() to include timestamp
- [x] Remove dedup_key clearing in mark_as_completed()
- [x] Remove dedup_key clearing in mark_as_cancelled()
- [x] Update ticket service to check source existence (not just dedup_key)
- [x] Add create_ticket_from_task() method
- [x] Add POST /tickets/from-task endpoint
- [x] Create Supabase migration to remove subcontractor column
- [x] Update documentation
---
## 8. Summary
### Key Changes
1. **Dedup key is now PERMANENT** (includes timestamp, never cleared)
2. **One source = One ticket FOREVER** (real-world compliant)
3. **Removed subcontractor_id** (use assignments instead)
4. **Added task β ticket creation** (infrastructure support)
### Business Impact
- β
Prevents accidental duplicate tickets
- β
Clear audit trail (one source = one ticket)
- β
Forces proper business flow (new order = new ticket)
- β
Simplified assignment model (via project_team)
- β
Infrastructure project support (tasks β tickets)
### Files Changed
- `src/app/models/ticket.py` - Model updates
- `src/app/services/ticket_service.py` - Service logic
- `src/app/api/v1/tickets.py` - API endpoint
- `supabase/migrations/20241116000001_remove_subcontractor_from_tickets.sql` - Migration
- `docs/agent/TICKET_MANAGEMENT_CHECKLIST.md` - Documentation
|