Spaces:
Sleeping
Sleeping
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
# 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
# dedup_key = MD5(source + source_id + ticket_type + created_at)
# PERMANENT - never cleared
# Tracks creation instance, not lifecycle state
Business Rules:
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
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
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
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):
# 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):
# 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_idcolumn in tickets table- Direct link to project_subcontractors
Current (CORRECT) Design
- REMOVED
project_subcontractor_idfrom 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
-- 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
# 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:
@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:
{
"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
-- 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=Truein model - Timestamp in dedup_key ensures permanent uniqueness
- One source can only create one ticket ever
6. Testing Scenarios
Scenario 1: Duplicate Prevention
# 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)
# 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
# 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
- Update Ticket model - permanent dedup_key with timestamp
- Remove project_subcontractor_id from Ticket model
- Update generate_dedup_key() to include timestamp
- Remove dedup_key clearing in mark_as_completed()
- Remove dedup_key clearing in mark_as_cancelled()
- Update ticket service to check source existence (not just dedup_key)
- Add create_ticket_from_task() method
- Add POST /tickets/from-task endpoint
- Create Supabase migration to remove subcontractor column
- Update documentation
8. Summary
Key Changes
- Dedup key is now PERMANENT (includes timestamp, never cleared)
- One source = One ticket FOREVER (real-world compliant)
- Removed subcontractor_id (use assignments instead)
- 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 updatessrc/app/services/ticket_service.py- Service logicsrc/app/api/v1/tickets.py- API endpointsupabase/migrations/20241116000001_remove_subcontractor_from_tickets.sql- Migrationdocs/agent/TICKET_MANAGEMENT_CHECKLIST.md- Documentation