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