This document provides comprehensive documentation for all database models in the Marketing App. The application follows a B2B (Business-to-Business) multi-tenant architecture where organizations manage their marketing campaigns and contacts.
- Create and activate venv (Windows PowerShell)
python -m venv venv
./venv/Scripts/Activate.ps1
pip install -U pip
pip install django djangorestframework drf-spectacular djangorestframework-simplejwt Pillow- Apply migrations and run
python manage.py migrate
python manage.py runserver- API schema and docs (drf-spectacular)
- OpenAPI JSON:
/api/schema/ - Swagger UI:
/api/docs/ - ReDoc:
/api/redoc/
The conversations and SMS features require Twilio credentials. If you see "Twilio credentials not configured" errors, follow these steps:
- Quick Setup (run this in the marketing_app directory):
python setup_twilio.py-
Manual Setup:
- Create a
.envfile in themarketing_appdirectory - Add your Twilio credentials (see
TWILIO_SETUP.mdfor details) - Get credentials from https://console.twilio.com/
- Create a
-
Required Environment Variables:
TWILIO_ACCOUNT_SID=your_account_sid_here
TWILIO_AUTH_TOKEN=your_auth_token_here
TWILIO_CONVERSATIONS_SERVICE_SID=your_service_sid_here
TWILIO_PHONE_NUMBER=your_twilio_phone_number📖 For detailed setup instructions, see TWILIO_SETUP.md
Purpose: Central tenant model for multi-tenancy. Each organization is a separate business entity with its own data, users, and settings.
Key Features:
- Multi-tenant isolation
- Subscription management integration
- Customizable contact types per organization
- Timezone and branding support
class Organization(models.Model):
name = models.CharField(max_length=200) # Organization name
domain = models.CharField(max_length=100, unique=True) # Custom domain (optional)
logo = models.ImageField(upload_to='organizations/logos/') # Brand logo
timezone = models.CharField(max_length=50, default='UTC') # Organization timezone
# Organization profile
organization_type = models.ForeignKey(OrganizationType) # Church, E-commerce, etc.
auto_seed_contact_types = models.BooleanField(default=True) # Auto-create contact types
# Legacy subscription fields (deprecated - use payment app)
subscription_plan = models.CharField(max_length=50) # DEPRECATED
subscription_expires_at = models.DateTimeField() # DEPRECATEDRelationships:
users→ OneToMany with Usercontacts→ OneToMany with Contactcontact_lists→ OneToMany with ContactListcontact_types→ OneToMany with ContactTypesubscription→ OneToOne with OrganizationSubscription
Convenience Properties:
# Subscription info (reads from payment app)
org.current_subscription # OrganizationSubscription instance
org.current_plan # SubscriptionPlan instance
org.subscription_status # 'active', 'trialing', 'past_due', etc.
org.has_active_subscription # Boolean
org.subscription_expires_at # DateTimeBusiness Logic:
- Auto-creates contact types on creation (if
auto_seed_contact_types=True) - Uses
OrganizationSetupServicefor initialization - Supports custom domains for white-labeling
Purpose: Application users who can log in and manage marketing campaigns. Each user belongs to an organization.
Key Features:
- Extended Django AbstractUser
- Role-based permissions
- Organization-scoped access
- Profile management
class User(AbstractUser):
organization = models.ForeignKey(Organization) # Tenant isolation
role = models.CharField(choices=[ # Permission level
('admin', 'Admin'), # Full access
('manager', 'Manager'), # Manage campaigns, users
('marketer', 'Marketer'), # Create campaigns, manage contacts
('viewer', 'Viewer'), # Read-only access
])
phone = models.CharField(max_length=20) # Contact number
avatar = models.ImageField(upload_to='users/avatars/') # Profile picture
is_verified = models.BooleanField(default=False) # Email verification
last_login_ip = models.GenericIPAddressField() # Security trackingRoles & Permissions:
- Admin: Full system access, user management, billing
- Manager: Campaign management, team oversight, reporting
- Marketer: Create campaigns, manage contacts, send messages
- Viewer: Read-only access to reports and data
Security Features:
- IP tracking for security audits
- Email verification system
- Organization-scoped data access
Purpose: Marketing audience/recipients. These are the people organizations communicate with through campaigns.
Key Features:
- Comprehensive contact information
- Flexible contact type system
- Marketing preferences
- Subscription management
- Custom fields support
class Contact(models.Model):
organization = models.ForeignKey(Organization) # Tenant isolation
# Basic Information
email = models.EmailField() # Primary identifier
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
phone = models.CharField(max_length=20, validators=[...]) # Validated phone format
# Demographics
date_of_birth = models.DateField()
gender = models.CharField(choices=[('male', 'Male'), ('female', 'Female')])
location = models.CharField(max_length=200)
country = models.CharField(max_length=100)
timezone = models.CharField(max_length=50)
# Marketing Preferences
contact_type = models.ForeignKey(ContactType) # Flexible type system
is_subscribed = models.BooleanField(default=True) # Opt-in status
subscription_source = models.CharField(max_length=100) # How they subscribed
tags = models.JSONField(default=list) # Custom tags
custom_fields = models.JSONField(default=dict) # Flexible attributes
# Status & Tracking
status = models.CharField(choices=ContactStatus.choices) # Active, Unsubscribed, etc.
created_at = models.DateTimeField(auto_now_add=True)
last_contacted_at = models.DateTimeField()Contact Statuses:
active- Can receive communicationsunsubscribed- Opted out of communicationsbounced- Email address invalidcomplained- Marked as spamblocked- Manually blocked
Custom Fields Example:
contact.custom_fields = {
'company': 'Acme Corp',
'industry': 'Technology',
'lead_score': 85,
'preferred_contact_time': 'morning'
}Business Logic:
- Auto-assigns default contact type on creation
- Unique email per organization
- Comprehensive indexing for performance
Purpose: Organize contacts into groups for targeted campaigns.
Key Features:
- Static and dynamic lists
- JSON-based filtering criteria
- Multi-list membership support
class ContactList(models.Model):
organization = models.ForeignKey(Organization)
name = models.CharField(max_length=200)
description = models.TextField()
contacts = models.ManyToManyField(Contact, related_name='lists')
# Dynamic List Features
is_dynamic = models.BooleanField(default=False) # Auto-updating list
filter_criteria = models.JSONField(default=dict) # Filter rules
created_by = models.ForeignKey(User)
created_at = models.DateTimeField(auto_now_add=True)Dynamic List Example:
# Auto-include all contacts from California with 'premium' tag
list.filter_criteria = {
'location__icontains': 'California',
'tags__contains': 'premium',
'status': 'active'
}Purpose: Predefined organization categories with associated contact type templates.
class OrganizationType(models.Model):
name = models.CharField(max_length=100) # 'Church', 'E-commerce'
slug = models.SlugField(max_length=50) # 'church', 'ecommerce'
description = models.TextField()
icon = models.CharField(max_length=50) # UI icon class
color = models.CharField(max_length=7) # Brand color
is_active = models.BooleanField(default=True)
sort_order = models.PositiveIntegerField(default=0)Predefined Types:
- Church: New Member, Visitor, Member, Volunteer, Donor
- E-commerce: Lead, Prospect, Customer, VIP Customer, Churned
- SaaS: Trial User, Subscriber, Enterprise, Churned
- Non-profit: Supporter, Volunteer, Donor, Board Member
Purpose: Template contact types for each organization type.
class ContactTypeTemplate(models.Model):
organization_type = models.ForeignKey(OrganizationType)
name = models.CharField(max_length=50) # 'new_member'
display_name = models.CharField(max_length=100) # 'New Member'
description = models.TextField()
color = models.CharField(max_length=7) # UI color
is_default = models.BooleanField(default=False) # Default for new contacts
sort_order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)Purpose: Organization-specific contact types created from templates.
class ContactType(models.Model):
organization = models.ForeignKey(Organization)
name = models.CharField(max_length=50) # Inherited from template
display_name = models.CharField(max_length=100) # Can be customized
description = models.TextField()
color = models.CharField(max_length=7)
is_default = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
sort_order = models.PositiveIntegerField(default=0)Seeding Process:
- Organization created with
organization_type OrganizationSetupService.seed_contact_types()called- Templates copied to organization-specific ContactTypes
- Can be customized after creation
Purpose: Catalog of available subscription plans with pricing and features.
class SubscriptionPlan(models.Model):
name = models.CharField(max_length=100) # 'Basic', 'Premium'
description = models.TextField()
# Pricing
price_monthly = models.DecimalField(max_digits=10, decimal_places=2)
price_yearly = models.DecimalField(max_digits=10, decimal_places=2)
currency = models.CharField(max_length=3, default='USD')
# Limits
max_contacts = models.IntegerField() # null = unlimited
max_emails_per_month = models.IntegerField()
max_sms_per_month = models.IntegerField()
max_whatsapp_per_month = models.IntegerField()
max_campaigns = models.IntegerField()
# Features
features = models.JSONField(default=list) # ['analytics', 'automation']
is_active = models.BooleanField(default=True)
is_popular = models.BooleanField(default=False) # Highlight in UIExample Plans:
# Free Plan
{
'name': 'Free',
'price_monthly': 0,
'max_contacts': 1000,
'max_emails_per_month': 1000,
'features': ['basic_analytics']
}
# Premium Plan
{
'name': 'Premium',
'price_monthly': 99.00,
'max_contacts': None, # Unlimited
'max_emails_per_month': 50000,
'features': ['advanced_analytics', 'automation', 'a_b_testing']
}Purpose: Active subscription for each organization.
class OrganizationSubscription(models.Model):
organization = models.OneToOneField(Organization)
plan = models.ForeignKey(SubscriptionPlan)
# Status
status = models.CharField(choices=SubscriptionStatus.choices)
billing_cycle = models.CharField(choices=BillingCycle.choices)
# Periods
started_at = models.DateTimeField(auto_now_add=True)
current_period_start = models.DateTimeField()
current_period_end = models.DateTimeField()
cancelled_at = models.DateTimeField(null=True)
trial_end = models.DateTimeField(null=True)
# Payment
amount = models.DecimalField(max_digits=10, decimal_places=2)
currency = models.CharField(max_length=3, default='USD')
# External Integration
stripe_subscription_id = models.CharField(max_length=200)
stripe_customer_id = models.CharField(max_length=200)Subscription Statuses:
active- Currently active and paidtrialing- In trial periodpast_due- Payment failed, grace periodcancelled- Cancelled but may still have accessunpaid- Payment failed, no access
Purpose: Individual payment records.
class Payment(models.Model):
organization = models.ForeignKey(Organization)
subscription = models.ForeignKey(OrganizationSubscription)
amount = models.DecimalField(max_digits=10, decimal_places=2)
currency = models.CharField(max_length=3, default='USD')
status = models.CharField(choices=PaymentStatus.choices)
payment_method = models.CharField(choices=PaymentMethod.choices)
created_at = models.DateTimeField(auto_now_add=True)
paid_at = models.DateTimeField(null=True)
# External References
stripe_payment_intent_id = models.CharField(max_length=200)
stripe_charge_id = models.CharField(max_length=200)
description = models.TextField()
metadata = models.JSONField(default=dict)Purpose: Billing invoices for payments.
class Invoice(models.Model):
organization = models.ForeignKey(Organization)
subscription = models.ForeignKey(OrganizationSubscription)
payment = models.OneToOneField(Payment, null=True)
invoice_number = models.CharField(max_length=100, unique=True)
amount = models.DecimalField(max_digits=10, decimal_places=2)
tax_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
status = models.CharField(choices=InvoiceStatus.choices)
created_at = models.DateTimeField(auto_now_add=True)
due_date = models.DateTimeField()
paid_at = models.DateTimeField(null=True)
stripe_invoice_id = models.CharField(max_length=200)
description = models.TextField()
line_items = models.JSONField(default=list)Purpose: Track usage for billing and limits.
class UsageRecord(models.Model):
organization = models.ForeignKey(Organization)
service_type = models.CharField(choices=ServiceType.choices) # email, sms, whatsapp
count = models.IntegerField() # Usage count
period_start = models.DateTimeField() # Billing period
period_end = models.DateTimeField()
unit_cost = models.DecimalField(max_digits=8, decimal_places=4)
total_cost = models.DecimalField(max_digits=10, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)Usage Tracking Example:
# Track email usage for billing
UsageRecord.objects.create(
organization=org,
service_type='email',
count=1500,
period_start=datetime(2024, 1, 1),
period_end=datetime(2024, 1, 31),
unit_cost=0.01,
total_cost=15.00
)Purpose: Billing information for organizations.
class BillingAddress(models.Model):
organization = models.OneToOneField(Organization)
# Address
company_name = models.CharField(max_length=200)
address_line_1 = models.CharField(max_length=200)
address_line_2 = models.CharField(max_length=200)
city = models.CharField(max_length=100)
state = models.CharField(max_length=100)
postal_code = models.CharField(max_length=20)
country = models.CharField(max_length=100)
# Contact
contact_name = models.CharField(max_length=200)
contact_email = models.EmailField()
contact_phone = models.CharField(max_length=20)
# Tax
tax_id = models.CharField(max_length=100)
vat_number = models.CharField(max_length=100)Organization (1) ←→ (M) User
Organization (1) ←→ (M) Contact
Organization (1) ←→ (M) ContactList
Organization (1) ←→ (M) ContactType
Organization (1) ←→ (1) OrganizationSubscription
Organization (1) ←→ (1) BillingAddress
OrganizationType (1) ←→ (M) Organization
OrganizationType (1) ←→ (M) ContactTypeTemplate
ContactTypeTemplate (1) ←→ (M) ContactType (via seeding)
ContactList (M) ←→ (M) Contact
OrganizationSubscription (1) ←→ (M) Payment
OrganizationSubscription (1) ←→ (M) Invoice
OrganizationSubscription (1) ←→ (1) SubscriptionPlan
Organization (1) ←→ (M) UsageRecord
- Multi-tenancy: All data scoped to
Organization - User Management: Users belong to one organization
- Contact Management: Contacts belong to one organization
- Subscription: One subscription per organization
- Flexible Types: Organization types define contact type templates
-
Organization Creation:
org = Organization.objects.create( name="Acme Church", organization_type=church_type, auto_seed_contact_types=True )
-
Automatic Setup:
OrganizationSetupService.setup_new_organization(org)called- Contact types seeded from templates
- Default settings applied
-
User Onboarding:
user = User.objects.create_user( username="pastor@acmechurch.com", organization=org, role="admin" )
-
Template-based Seeding:
# Church gets: New Member, Visitor, Member, Volunteer, Donor church_templates = ContactTypeTemplate.objects.filter( organization_type__slug='church' )
-
Custom Modifications:
# Customize after creation new_member_type = org.contact_types.get(name='new_member') new_member_type.display_name = "New Believer" new_member_type.color = "#28a745" new_member_type.save()
-
Plan Selection:
plan = SubscriptionPlan.objects.get(name='Premium') subscription = OrganizationSubscription.objects.create( organization=org, plan=plan, status='active', billing_cycle='monthly', current_period_start=timezone.now(), current_period_end=timezone.now() + timedelta(days=30) )
-
Usage Tracking:
# Track email sends UsageRecord.objects.create( organization=org, service_type='email', count=campaign.emails_sent, period_start=period_start, period_end=period_end )
# 1. Get church organization type
church_type = OrganizationType.objects.get(slug='church')
# 2. Create organization
org = Organization.objects.create(
name="Grace Community Church",
organization_type=church_type,
domain="gracechurch.com",
timezone="America/New_York"
)
# 3. Contact types automatically seeded:
# - New Member (default)
# - Visitor
# - Member
# - Volunteer
# - Donor
# - Inactive
# 4. Create admin user
admin = User.objects.create_user(
username="pastor@gracechurch.com",
email="pastor@gracechurch.com",
password="secure_password",
organization=org,
role="admin",
first_name="John",
last_name="Smith"
)# Add new church member
contact = Contact.objects.create(
organization=org,
email="member@example.com",
first_name="Jane",
last_name="Doe",
phone="+1234567890",
contact_type=org.contact_types.get(name='new_member'),
tags=['baptism_class', 'small_group'],
custom_fields={
'baptism_date': '2024-01-15',
'small_group': 'Young Adults',
'volunteer_interest': 'Children\'s Ministry'
}
)
# Create contact list for small group
small_group_list = ContactList.objects.create(
organization=org,
name="Young Adults Small Group",
description="Members of the young adults ministry",
created_by=admin,
filter_criteria={
'tags__contains': 'young_adults',
'status': 'active'
},
is_dynamic=True
)# Get premium plan
premium_plan = SubscriptionPlan.objects.get(name='Premium')
# Create subscription
subscription = OrganizationSubscription.objects.create(
organization=org,
plan=premium_plan,
status='active',
billing_cycle='monthly',
amount=99.00,
current_period_start=timezone.now(),
current_period_end=timezone.now() + timedelta(days=30)
)
# Check subscription status
if org.has_active_subscription:
print(f"Plan: {org.current_plan.name}")
print(f"Status: {org.subscription_status}")
print(f"Expires: {org.subscription_expires_at}")# Track email campaign usage
campaign = EmailCampaign.objects.get(id=1)
UsageRecord.objects.create(
organization=org,
service_type='email',
count=campaign.recipients.count(),
period_start=timezone.now().replace(day=1),
period_end=timezone.now().replace(day=1) + timedelta(days=30),
unit_cost=0.01,
total_cost=campaign.recipients.count() * 0.01
)Contact Model:
(organization, email)- Unique constraint(organization, status)- Status filtering(created_at)- Time-based queries
UsageRecord Model:
(organization, service_type)- Usage queries(period_start, period_end)- Period filtering
# Efficient contact queries
contacts = Contact.objects.filter(
organization=org,
status='active'
).select_related('contact_type').prefetch_related('lists')
# Efficient subscription queries
subscription = OrganizationSubscription.objects.select_related(
'plan'
).get(organization=org)# Cache organization subscription info
@cached_property
def subscription_info(self):
return {
'plan': self.current_plan.name,
'status': self.subscription_status,
'expires': self.subscription_expires_at
}- All queries must filter by
organization - No cross-tenant data access
- User permissions scoped to organization
- Contact data encrypted at rest
- GDPR compliance features (unsubscribe, data export)
- Audit logging for sensitive operations
- Organization-scoped API endpoints
- Role-based permission checks
- Rate limiting per organization
- Organization Types: Create predefined types
- Contact Type Templates: Seed templates for each type
- Subscription Migration: Map legacy
subscription_plantoSubscriptionPlan - Data Cleanup: Remove deprecated fields
- Run migrations for new models
- Seed organization types and templates
- Migrate existing subscription data
- Update application code to use new models
- Remove legacy fields
This documentation provides a comprehensive overview of the Marketing App's database architecture. The flexible contact type system allows organizations to customize their contact management while maintaining a consistent data structure for the application.