Skip to main content

data-model-specification.md

CODITECT Multi-Tenant SaaS Platform - Complete Data Model Specification

Generated: 2025-12-30 Purpose: Comprehensive schema specification with recommended complete data model Status: Production + Recommended Additions for Pilot Launch


Table of Contents

  1. Overview
  2. Entity Relationship Diagram (ERD)
  3. Complete Schema by Domain
  4. Business Rules and Constraints
  5. Multi-Tenant Architecture
  6. Frontend Type Requirements
  7. Database Indexes Strategy
  8. Implementation Roadmap

Overview

Current State (Existing Models)

  • Total Models: 40
  • Total Tables: 31
  • Apps: 10 (with models) + 1 (core, no models)
  • Domain Coverage: Partial (User, Tenant, Commerce, Subscriptions, Permissions, Context, Licenses, Workstations, Repositories)
  • Total Models: 75 (+35 new)
  • Total Tables: 66 (+35 new)
  • Apps: 16 (+6 new apps)
  • Domain Coverage: Complete (adds PM, DMS, Workflows, Audit, Contacts)
  1. pm - Project Management (Task, Sprint, Milestone, TaskComment, TaskAttachment, TaskActivity, Board)
  2. dms - Document Management System (Document, DocumentVersion, Folder, DocumentPermission, DocumentActivity, DocumentShare)
  3. workflows - Workflow Analysis (Workflow, WorkflowExecution, WorkflowStep, WorkflowApproval)
  4. audit - Audit & Compliance (AuditLog, AuditReview, ComplianceReport)
  5. billing - Enhanced Billing (Payment, Refund, TaxRate)
  6. contacts - Contact Management (Address, PhoneNumber, EmailAddress)

Entity Relationship Diagram (ERD)

Core Multi-Tenant Hierarchy

Project Management Domain ERD

DMS Domain ERD

Workflow Analysis Domain ERD

Commerce & Billing ERD


Complete Schema by Domain

Domain 1: User & Tenant Management

TENANT (Existing - Root Multi-Tenant Model)

class Tenant(TenantModel):
id = UUIDField(primary_key=True)
name = CharField(255) # Organization name
slug = SlugField(100, unique=True) # URL-safe identifier
email = EmailField() # Primary contact
license_tier = CharField(20) # trial, starter, professional, enterprise
seats_total = PositiveIntegerField(default=1)
is_active = BooleanField(default=True)
created_at = DateTimeField(auto_now_add=True)
updated_at = DateTimeField(auto_now=True)
stripe_customer_id = CharField(255, null=True)
stripe_subscription_id = CharField(255, null=True)

# RECOMMENDED ADDITIONS:
billing_email = EmailField(null=True) # Separate billing contact
tax_id = CharField(100, null=True) # VAT/EIN for invoicing
phone_number = CharField(50, null=True)
website = URLField(null=True)
industry = CharField(100, null=True)
company_size = CharField(20, null=True) # small, medium, large, enterprise

Indexes: slug, email, is_active Constraints: slug unique


USER (Existing - Custom User Model)

class User(AbstractUser):  # Note: Does NOT use TenantModel (MRO conflicts)
id = UUIDField(primary_key=True)
tenant = ForeignKey(Tenant, on_delete=CASCADE) # Multi-tenant isolation via FK
email = EmailField(unique=True)
role = CharField(20) # owner, admin, member, guest
oauth_provider = CharField(50, null=True) # google, github
oauth_sub = CharField(255, null=True)
updated_at = DateTimeField(auto_now=True)

# RECOMMENDED ADDITIONS:
phone_number = CharField(50, null=True)
avatar_url = URLField(null=True)
timezone = CharField(50, default='UTC')
language = CharField(10, default='en')
notification_preferences = JSONField(default=dict)

Indexes: (tenant, email), (oauth_provider, oauth_sub), role Constraints: (tenant, email) unique


Domain 2: Organization Structure

MEMBERSHIP (Existing)

class Membership(TenantModel):
id = UUIDField(primary_key=True)
tenant = ForeignKey(Tenant, on_delete=CASCADE)
user = TenantForeignKey(User, on_delete=CASCADE)
role = CharField(20) # owner, admin, member, viewer
joined_at = DateTimeField(default=now)
revoked_at = DateTimeField(null=True)

Constraints: (user, tenant) unique


TEAM (Existing)

class Team(TenantModel):
id = UUIDField(primary_key=True)
tenant = ForeignKey(Tenant, on_delete=CASCADE)
name = CharField(255)
description = TextField()
created_at = DateTimeField(auto_now_add=True)
updated_at = DateTimeField(auto_now=True)

Constraints: (tenant, name) unique


PROJECT (Existing)

class Project(TenantModel):
id = UUIDField(primary_key=True)
tenant = ForeignKey(Tenant, on_delete=CASCADE)
name = CharField(255)
description = TextField()
metadata = JSONField(default=dict)
created_at = DateTimeField(auto_now_add=True)
updated_at = DateTimeField(auto_now=True)
deleted_at = DateTimeField(null=True) # Soft delete

Indexes: (tenant, -created_at) WHERE deleted_at IS NULL Constraints: (tenant, name) unique WHERE deleted_at IS NULL


Domain 3: Project Management (NEW)

TASK (NEW - CRITICAL)

class Task(TenantModel):
id = UUIDField(primary_key=True)
tenant = ForeignKey(Tenant, on_delete=CASCADE)
project = TenantForeignKey(Project, on_delete=CASCADE)
title = CharField(500)
description = TextField()
task_type = CharField(20) # task, bug, feature, epic
status = CharField(20) # backlog, todo, in_progress, in_review, done, cancelled
priority = CharField(20) # low, medium, high, urgent
assignee = TenantForeignKey(User, on_delete=SET_NULL, null=True, related_name='assigned_tasks')
reporter = TenantForeignKey(User, on_delete=SET_NULL, null=True, related_name='reported_tasks')
sprint = TenantForeignKey(Sprint, on_delete=SET_NULL, null=True)
milestone = TenantForeignKey(Milestone, on_delete=SET_NULL, null=True)
parent_task = ForeignKey(self, on_delete=SET_NULL, null=True) # Subtasks
story_points = IntegerField(null=True)
due_date = DateTimeField(null=True)
started_at = DateTimeField(null=True)
completed_at = DateTimeField(null=True)
tags = JSONField(default=list)
custom_fields = JSONField(default=dict)
created_at = DateTimeField(auto_now_add=True)
updated_at = DateTimeField(auto_now=True)
deleted_at = DateTimeField(null=True)

Indexes: (tenant, project, status), assignee, sprint, due_date


SPRINT (NEW - HIGH)

class Sprint(TenantModel):
id = UUIDField(primary_key=True)
tenant = ForeignKey(Tenant, on_delete=CASCADE)
project = TenantForeignKey(Project, on_delete=CASCADE)
name = CharField(255)
goal = TextField()
status = CharField(20) # planned, active, completed, cancelled
start_date = DateField()
end_date = DateField()
created_at = DateTimeField(auto_now_add=True)
updated_at = DateTimeField(auto_now=True)
completed_at = DateTimeField(null=True)

Indexes: (tenant, project, status), (start_date, end_date) Constraints: (tenant, project, name) unique


MILESTONE (NEW - MEDIUM)

class Milestone(TenantModel):
id = UUIDField(primary_key=True)
tenant = ForeignKey(Tenant, on_delete=CASCADE)
project = TenantForeignKey(Project, on_delete=CASCADE)
title = CharField(255)
description = TextField()
status = CharField(20) # open, in_progress, completed, cancelled
due_date = DateField(null=True)
progress_percent = IntegerField(default=0) # Calculated
created_at = DateTimeField(auto_now_add=True)
updated_at = DateTimeField(auto_now=True)
completed_at = DateTimeField(null=True)

Domain 4: Document Management System (NEW)

DOCUMENT (NEW - CRITICAL)

class Document(TenantModel):
id = UUIDField(primary_key=True)
tenant = ForeignKey(Tenant, on_delete=CASCADE)
project = TenantForeignKey(Project, on_delete=SET_NULL, null=True)
folder = TenantForeignKey(Folder, on_delete=SET_NULL, null=True)
title = CharField(500)
description = TextField()
file_path = CharField(1000) # GCS path
file_name = CharField(255)
file_size_bytes = BigIntegerField()
mime_type = CharField(100)
checksum = CharField(64) # SHA256 (deduplication)
version = IntegerField(default=1)
status = CharField(20) # draft, published, archived, deleted
owner = TenantForeignKey(User, on_delete=SET_NULL, null=True, related_name='owned_documents')
created_by = TenantForeignKey(User, on_delete=SET_NULL, null=True, related_name='created_documents')
updated_by = TenantForeignKey(User, on_delete=SET_NULL, null=True, related_name='updated_documents')
tags = JSONField(default=list)
metadata = JSONField(default=dict)
created_at = DateTimeField(auto_now_add=True)
updated_at = DateTimeField(auto_now=True)
deleted_at = DateTimeField(null=True)

Indexes: (tenant, folder), (tenant, owner), checksum, (tenant, status, -updated_at)


DOCUMENT_VERSION (NEW - CRITICAL)

class DocumentVersion(TenantModel):
id = UUIDField(primary_key=True)
tenant = ForeignKey(Tenant, on_delete=CASCADE)
document = TenantForeignKey(Document, on_delete=CASCADE)
version_number = IntegerField()
file_path = CharField(1000)
file_size_bytes = BigIntegerField()
checksum = CharField(64)
change_summary = TextField()
created_by = TenantForeignKey(User, on_delete=SET_NULL, null=True)
created_at = DateTimeField(auto_now_add=True)

Indexes: (document, -version_number) Constraints: (document, version_number) unique


Domain 5: Workflow Analysis (NEW)

WORKFLOW (NEW - CRITICAL)

class Workflow(TenantModel):
id = UUIDField(primary_key=True)
tenant = ForeignKey(Tenant, on_delete=CASCADE)
project = TenantForeignKey(Project, on_delete=SET_NULL, null=True)
name = CharField(255)
description = TextField()
workflow_type = CharField(50) # approval, automation, review, deployment, custom
version = IntegerField(default=1)
status = CharField(20) # draft, active, paused, archived
definition = JSONField() # State machine (nodes, edges, conditions)
triggers = JSONField(default=list)
created_by = TenantForeignKey(User, on_delete=SET_NULL, null=True)
created_at = DateTimeField(auto_now_add=True)
updated_at = DateTimeField(auto_now=True)
published_at = DateTimeField(null=True)

Indexes: (tenant, status), (tenant, workflow_type)


WORKFLOW_EXECUTION (NEW - CRITICAL)

class WorkflowExecution(TenantModel):
id = UUIDField(primary_key=True)
tenant = ForeignKey(Tenant, on_delete=CASCADE)
workflow = TenantForeignKey(Workflow, on_delete=CASCADE)
triggered_by = TenantForeignKey(User, on_delete=SET_NULL, null=True)
trigger_type = CharField(50) # manual, scheduled, webhook, event
status = CharField(20) # pending, running, paused, completed, failed, cancelled
current_step = CharField(100, null=True)
started_at = DateTimeField(auto_now_add=True)
completed_at = DateTimeField(null=True)
error_message = TextField(null=True)
context_data = JSONField(default=dict)
created_at = DateTimeField(auto_now_add=True)

Indexes: (workflow, -started_at), (tenant, status)


Domain 6: Audit & Compliance (NEW)

AUDIT_LOG (NEW - HIGH)

class AuditLog(models.Model):  # Global model (not tenant-scoped for immutability)
id = UUIDField(primary_key=True)
tenant = ForeignKey(Tenant, on_delete=CASCADE)
user = ForeignKey(User, on_delete=SET_NULL, null=True)
action = CharField(100) # 'user.login', 'document.delete', etc.
resource_type = CharField(100) # 'User', 'Document', 'Project'
resource_id = CharField(255, null=True)
ip_address = GenericIPAddressField(null=True)
user_agent = CharField(500)
request_method = CharField(10)
request_path = CharField(1000)
changes = JSONField(default=dict) # Before/after values
metadata = JSONField(default=dict)
severity = CharField(20) # info, warning, error, critical
created_at = DateTimeField(auto_now_add=True)

Indexes: (tenant, -created_at), (user, -created_at), (resource_type, resource_id), action, severity Business Rules: Immutable (no updates/deletes), 1 year retention


Domain 7: Commerce & Billing

ORDER (Existing - Enhanced)

class Order(TenantModel):
id = UUIDField(primary_key=True)
tenant = ForeignKey(Tenant, on_delete=SET_NULL, null=True)
user = ForeignKey(User, on_delete=RESTRICT)
stripe_payment_intent_id = CharField(255, unique=True, null=True)
stripe_checkout_session_id = CharField(255, unique=True, null=True)
payment_method = CharField(50)
total_cents = IntegerField()
currency = CharField(3, default='usd')
status = CharField(20)
line_items = JSONField(default=list)
billing_details = JSONField(null=True)
paid_at = DateTimeField(null=True)
created_at = DateTimeField(auto_now_add=True)
updated_at = DateTimeField(auto_now=True)

# RECOMMENDED ADDITIONS:
tax_cents = IntegerField(default=0)
discount_cents = IntegerField(default=0)
discount_code = CharField(50, null=True)
shipping_address = JSONField(null=True)

PAYMENT (NEW - HIGH)

class Payment(TenantModel):
id = UUIDField(primary_key=True)
tenant = ForeignKey(Tenant, on_delete=CASCADE)
order = TenantForeignKey(Order, on_delete=SET_NULL, null=True)
invoice = TenantForeignKey(Invoice, on_delete=SET_NULL, null=True)
stripe_payment_intent_id = CharField(255, unique=True)
stripe_charge_id = CharField(255, null=True)
amount_cents = IntegerField()
currency = CharField(3, default='usd')
status = CharField(20) # pending, processing, succeeded, failed, refunded, cancelled
payment_method = CharField(50) # card, bank_account, google_pay, apple_pay
card_last4 = CharField(4, null=True)
card_brand = CharField(20, null=True)
failure_code = CharField(50, null=True)
failure_message = CharField(500, null=True)
created_at = DateTimeField(auto_now_add=True)
succeeded_at = DateTimeField(null=True)
refunded_at = DateTimeField(null=True)

Indexes: (tenant, -created_at), stripe_payment_intent_id, status


Domain 8: Contact Management (NEW)

ADDRESS (NEW - HIGH)

class Address(TenantModel):
id = UUIDField(primary_key=True)
tenant = ForeignKey(Tenant, on_delete=CASCADE)
entity_type = CharField(50) # 'tenant', 'user', 'order'
entity_id = UUIDField() # GenericForeignKey pattern
address_type = CharField(20) # billing, shipping, office, home
label = CharField(100, null=True)
full_name = CharField(255, null=True)
company_name = CharField(255, null=True)
address_line1 = CharField(255)
address_line2 = CharField(255, null=True)
city = CharField(100)
state = CharField(100, null=True)
postal_code = CharField(20)
country_code = CharField(2) # ISO 3166-1 alpha-2
latitude = DecimalField(9, 6, null=True)
longitude = DecimalField(9, 6, null=True)
is_default = BooleanField(default=False)
is_verified = BooleanField(default=False)
created_at = DateTimeField(auto_now_add=True)
updated_at = DateTimeField(auto_now=True)

Indexes: (tenant, entity_type, entity_id), (tenant, is_default)


Business Rules and Constraints

Global Business Rules

  1. Multi-Tenant Isolation

    • ALL tenant-scoped models MUST inherit from TenantModel OR have ForeignKey(Tenant)
    • Query filtering by tenant is AUTOMATIC via django-multitenant middleware
    • Exception: Global catalog models (Product, Permission, WorkstationConfig, TaxRate)
  2. Soft Deletes

    • Models with user-facing data use deleted_at timestamp (no hard deletes)
    • Applies to: Project, Task, Document, Folder
    • Unique constraints MUST include WHERE deleted_at IS NULL
  3. Audit Trail

    • All state changes MUST be logged to AuditLog
    • Includes: who, what, when, IP, user agent, before/after values
    • AuditLog is IMMUTABLE (no updates, no deletes)
  4. UUID Primary Keys

    • ALL models use UUID primary keys (distributed system requirement)
    • Enables sharding, prevents ID enumeration attacks
  5. Timestamps

    • ALL models have created_at (auto_now_add) and updated_at (auto_now)
    • Exceptions: Immutable models (AuditLog has created_at only)

Domain-Specific Business Rules

Project Management

  1. Task Hierarchy: Tasks can have parent tasks (subtasks), max depth = 3
  2. Sprint Dates: end_date > start_date, no overlapping sprints per project
  3. Task Status Transitions: Enforce state machine (backlog → todo → in_progress → in_review → done)
  4. Story Points: Optional, positive integers only, typically Fibonacci (1, 2, 3, 5, 8, 13)

Document Management

  1. Versioning: DocumentVersion.version_number is auto-increment per document
  2. Checksums: Deduplication via SHA256, same file uploaded twice creates version, not duplicate
  3. Permissions: Inherited from folder if no document-specific permission
  4. Folder Paths: Auto-calculated, max depth = 10 levels
  5. File Size Limits: max_file_size_mb from subscription tier (Starter: 10MB, Pro: 100MB, Enterprise: 1GB)

Workflow Analysis

  1. Workflow Definitions: JSON schema validation required (state machine with nodes, edges, conditions)
  2. Execution State: WorkflowExecution.current_step must be valid step_id from workflow.definition
  3. Approvals: WorkflowApproval expires after 7 days default, configurable per workflow

Commerce & Billing

  1. Cart Expiration: Carts expire 24 hours after last update
  2. Order Immutability: Once status='paid', order cannot be modified (refund creates new Refund record)
  3. Entitlement Creation: Automatic on Order.status='paid' OR Subscription.status='active'
  4. Product Dependencies: Cart validation ensures all requires[] dependencies met before checkout
  5. Tax Calculation: Applied via TaxRate based on billing_address.country_code + state_code

Licenses

  1. Concurrent Seats: License.seats_total limits active sessions (Session.status='active')
  2. Heartbeat TTL: Sessions without heartbeat for 6 minutes → status='zombie' (auto cleanup)
  3. Hardware Fingerprinting: One active session per (license, hardware_id) - prevents seat sharing

Multi-Tenant Architecture

Tenant Isolation Strategy

Pattern 1: TenantModel (Automatic Filtering)

from django_multitenant.models import TenantModel
from django_multitenant.fields import TenantForeignKey

class MyModel(TenantModel):
tenant_id = 'tenant_id' # Tell django-multitenant which field

tenant = models.ForeignKey('tenants.Tenant', on_delete=CASCADE, db_column='tenant_id')
related = TenantForeignKey(RelatedModel, on_delete=CASCADE) # Use TenantForeignKey for tenant-scoped models

Pros: Automatic query filtering, enforced tenant isolation Use For: Most tenant-scoped models (organizations, commerce, subscriptions, context)


Pattern 2: ForeignKey(Tenant) Without TenantModel

class MyModel(models.Model):
tenant = models.ForeignKey('tenants.Tenant', on_delete=CASCADE)

Pros: Simpler, still has FK constraint Cons: Manual filtering required Use For: User (AbstractUser MRO conflict), licenses, sessions


Pattern 3: CharField(tenant_id) Without FK

class MyModel(models.Model):
tenant_id = models.CharField(max_length=63)

Pros: None (legacy pattern) Cons: No FK constraint, manual filtering, prone to errors RECOMMENDATION: Migrate workstations and repositories apps to Pattern 2


Middleware Setup

# settings.py
MIDDLEWARE = [
# ...
'django_multitenant.middleware.MultiTenantMiddleware', # Add after session middleware
# ...
]

Usage Example

from django_multitenant.utils import set_current_tenant

# In API view (automatic via middleware)
def my_view(request):
tenant = request.user.tenant
set_current_tenant(tenant)

# All subsequent TenantModel queries auto-filtered by tenant
projects = Project.objects.all() # Only returns current tenant's projects

Frontend Type Requirements

TypeScript Interfaces for All Models

CRITICAL: Every Django model MUST have corresponding TypeScript interface for frontend type safety.

Location: frontend/src/types/models/

Pattern:

// frontend/src/types/models/project.ts

export interface Project {
id: string; // UUID
tenant: string; // UUID (FK reference)
name: string;
description: string;
metadata: Record<string, any>;
created_at: string; // ISO 8601 timestamp
updated_at: string;
deleted_at: string | null;
}

export interface ProjectListResponse {
count: number;
next: string | null;
previous: string | null;
results: Project[];
}

export interface ProjectDetailResponse extends Project {
tasks_count?: number;
documents_count?: number;
}

Naming Conventions

Django ModelTypeScript InterfaceAPI Endpoint
TaskTask/api/v1/tasks/
TaskCommentTaskComment/api/v1/tasks/:id/comments/
DocumentDocument/api/v1/documents/
DocumentVersionDocumentVersion/api/v1/documents/:id/versions/

Field Type Mapping

Django FieldTypeScript Type
UUIDFieldstring
CharFieldstring
TextFieldstring
IntegerFieldnumber
BigIntegerFieldnumber
BooleanFieldboolean
DateTimeFieldstring (ISO 8601)
DateFieldstring (YYYY-MM-DD)
JSONFieldRecord<string, any> OR specific interface
ForeignKeystring (UUID) OR full object
EmailFieldstring
URLFieldstring

Database Indexes Strategy

Index Types

1. B-Tree Indexes (Default)

  • Used for equality and range queries
  • Applies to: Primary keys, foreign keys, unique constraints

2. Partial Indexes (Filtered)

  • Only index rows matching condition
  • Smaller, faster, less disk space
  • Example: WHERE deleted_at IS NULL, WHERE status='active'

3. Composite Indexes

  • Multiple columns in single index
  • Order matters: most selective column first
  • Example: (tenant_id, status, -created_at)

4. GIN Indexes (for JSONField)

  • Generalized Inverted Index
  • Fast lookups in JSON data
  • Example: CREATE INDEX ON tasks USING GIN (tags)

High-Traffic Models (Query Hotspots)

Task:

CREATE INDEX idx_tasks_tenant_project_status ON tasks (tenant_id, project_id, status) WHERE deleted_at IS NULL;
CREATE INDEX idx_tasks_assignee ON tasks (assignee_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_tasks_sprint ON tasks (sprint_id);
CREATE INDEX idx_tasks_due_date ON tasks (due_date) WHERE status != 'done' AND deleted_at IS NULL;
CREATE INDEX idx_tasks_tags ON tasks USING GIN (tags);

Document:

CREATE INDEX idx_documents_tenant_folder ON documents (tenant_id, folder_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_documents_checksum ON documents (checksum); -- Deduplication
CREATE INDEX idx_documents_owner ON documents (owner_id);
CREATE INDEX idx_documents_status_updated ON documents (tenant_id, status, updated_at DESC);

AuditLog:

CREATE INDEX idx_audit_tenant_created ON audit_logs (tenant_id, created_at DESC);
CREATE INDEX idx_audit_user ON audit_logs (user_id, created_at DESC);
CREATE INDEX idx_audit_resource ON audit_logs (resource_type, resource_id);
CREATE INDEX idx_audit_action ON audit_logs (action);
CREATE INDEX idx_audit_severity ON audit_logs (severity) WHERE severity IN ('error', 'critical');

Implementation Roadmap

Phase 1: CRITICAL Models (12-16 hours - BEFORE PILOT)

Target: December 22-23, 2025

ModelAppPriorityEstimated Time
TaskpmCRITICAL2-3 hours
DocumentdmsCRITICAL2-3 hours
DocumentVersiondmsCRITICAL1-2 hours
WorkflowworkflowsCRITICAL2-3 hours
WorkflowExecutionworkflowsCRITICAL2-3 hours

Steps:

  1. Create Django apps: pm, dms, workflows
  2. Define models with TenantModel
  3. Create migrations
  4. Run migrations
  5. Create admin interfaces
  6. Create serializers (DRF)
  7. Create TypeScript interfaces
  8. Write unit tests

Phase 2: HIGH Models (15-23 hours - WEEK 1 POST-PILOT)

Target: December 26-30, 2025

ModelAppPriorityEstimated Time
SprintpmHIGH1-2 hours
TaskCommentpmHIGH1 hour
FolderdmsHIGH1-2 hours
DocumentPermissiondmsHIGH2-3 hours
WorkflowStepworkflowsHIGH1-2 hours
AuditLogauditHIGH2-3 hours
PaymentbillingHIGH2-3 hours
BillingAddresscontactsHIGH1-2 hours
AddresscontactsHIGH2-3 hours

Phase 3: MEDIUM Models (12-20 hours - WEEK 2-3 POST-PILOT)

Target: January 2026

ModelAppPriorityEstimated Time
MilestonepmMEDIUM1-2 hours
TaskAttachmentpmMEDIUM1-2 hours
TaskActivitypmMEDIUM1-2 hours
DocumentActivitydmsMEDIUM1-2 hours
DocumentSharedmsMEDIUM2-3 hours
WorkflowApprovalworkflowsMEDIUM1-2 hours
AuditReviewauditMEDIUM1-2 hours
RefundbillingMEDIUM1-2 hours
TaxRatebillingMEDIUM1-2 hours
PhoneNumbercontactsMEDIUM1-2 hours

Phase 4: Field Enhancements (2-3 hours)

Target: January 2026

Add recommended fields to existing models:

  • Tenant: billing_email, tax_id, phone_number, website
  • User: phone_number, avatar_url, timezone, language
  • Order: tax_cents, discount_cents, discount_code
  • Cart: discount_code, tax_cents
  • Subscription: trial_start, trial_end

Phase 5: Multi-Tenant Compliance Fix (2-3 hours)

Target: January 2026

Migrate CharField(tenant_id) to ForeignKey(Tenant):

  • workstations app (4 models)
  • repositories app (GiteaOrganization)

Total Effort Summary

PhasePriorityModelsHoursTarget Date
Phase 1CRITICAL812-16Dec 22-23
Phase 2HIGH1515-23Dec 26-30
Phase 3MEDIUM912-20Jan 2026
Phase 4Field GapsN/A2-3Jan 2026
Phase 5Compliance52-3Jan 2026
TOTAL35+43-654-6 weeks

RECOMMENDATION FOR PILOT LAUNCH:

  1. Implement Phase 1 IMMEDIATELY (CRITICAL models) - 12-16 hours
  2. Consider DELAY if needed - DMS and Workflow Analyzer products CANNOT launch without their core models
  3. Alternative: REDUCE SCOPE - Launch with Core product only, delay DMS/Workflow addons to Week 2

End of data-model-specification.md