Migrating notification system, message queues, letters, and webhook configurations for automated communications.
Migration of notifications and letters from DTX 1.0 to DTX 2.0. This is Phase 4 β depends on Areas 2, 3 (Customers/Accounts, Vendors/Records).
DTX 1.0's notification system was built around a settings + template + sent-history architecture: notification stored per-customer timing schedules, notification_webhook mapped events to email recipient lists (PHP serialized), letter stored email templates with __PLACEHOLDER__ macros, messagequeue stored sent emails with full HTML bodies, and envelope tracked broker responses.
DTX 2.0 has a fully rebuilt notification system in the docutrax_utils app with a queue-based architecture: NotificationSchedule (account-scoped, per-type/per-sequence rows with cumulative timing), NotificationQueue (pending items processed by Celery tasks), EmailLog (delivery tracking), and EmailTemplate (reusable templates with {PLACEHOLDER} variables). Additionally, InAppNotification provides bell-icon notifications β a feature with no DTX 1.0 equivalent.
All target models exist. No missing models (unlike Area 9).
| DTX 1.0 Table | Rows (sample) | Type | DTX 2.0 Target |
|---|---|---|---|
notification |
100 | Configuration | NotificationSchedule (docutrax_utils) |
notification_webhook |
100 | Configuration | NotificationSchedule.email_recipients |
notification_lookup |
100 | Denormalized join | β (replaced by Account FK) |
notification_audit |
100 | Audit trail | β (historical) |
letter |
100 | Templates | EmailTemplate (docutrax_utils) |
messagequeue |
100 | Sent history | EmailLog (docutrax_utils) |
envelope |
100 | Response tracking | No target model |
| DTX 2.0 Model | App | File | Lines |
|---|---|---|---|
NotificationSchedule |
docutrax_utils |
docutrax_utils/models.py |
307β455 |
EmailLog |
docutrax_utils |
docutrax_utils/models.py |
457β597 |
NotificationQueue |
docutrax_utils |
docutrax_utils/models.py |
641β743 |
EmailTemplate |
docutrax_utils |
docutrax_utils/models.py |
745β837 |
InAppNotification |
docutrax_utils |
docutrax_utils/models.py |
1170β1369 |
CronSetting |
docutrax_utils |
docutrax_utils/models.py |
599β639 |
| Component | File | Purpose |
|---|---|---|
EmailNotificationService |
docutrax_utils/services/email_notification_service.py |
Template rendering, recipient resolution, send logic |
NotificationQueueService |
docutrax_utils/services/notification_queue_service.py |
Queue building, timing calculations, guard checks |
email_service.py |
docutrax_utils/services/email_service.py |
Low-level email send with Celery fallback |
build_notification_queue |
docutrax_utils/tasks.py |
Celery task: hourly queue building |
process_notification_queue |
docutrax_utils/tasks.py |
Celery task: daily send at 8 AM |
ConditionalEmailBackend |
docutrax_utils/email_backends.py |
Safety filter for test/staging |
| Email templates | docutrax_utils/templates/emails/ |
HTML templates (base, compliance, expiration, etc.) |
| Depends On | What's Needed |
|---|---|
| Area 2: Customers/Accounts | notification.customer_id β Account mapping; AccountEmail for recipients |
| Area 3: Vendors/Records | messagequeue.vendor_id β Record mapping; recipient addresses |
| Area 4: Certificates/Documents | Expiration events trigger notifications |
| Depended On By | What It Provides |
|---|---|
| Area 10: Reporting | Notification metrics for reports |
This is the most complex migration in this area. DTX 1.0 stores notification settings as wide rows (one row per customer/type with ~20 timing columns). DTX 2.0 uses narrow rows (one row per account/type/sequence with a single days_after column).
DTX 1.0 structure: Each customer has 2 notification rows (type=COI and type=DOCUMENT). Each row contains columns for multiple reminder stages:
- CR* = Certificate Request settings (first remind, second remind, non-response alert, restart)
- CE* = Certificate Expiration settings (start-to-expire, first remind, expired, second remind, alert, restart) with Before/After timing
- CNC* = Non-Compliance settings (first, second, alert, restart)
- DR* = Document Request settings (first remind, second remind, non-response alert)
DTX 2.0 structure: Each account has multiple NotificationSchedule rows, one per (type, sequence) combination:
- type: coi_request, coi_non_compliance, coi_expiration, document_request, document_non_compliance, document_expiration, coi_review, document_review
- sequence: initial, reminder, final, alert, restart_notices, soon_to_expire, expired
- days_after: cumulative days from T0
- email_recipients: JSON array (populated from notification_webhook)
Column mapping β COI notification row:
| DTX 1.0 Column | DTX 2.0 type | DTX 2.0 sequence | DTX 2.0 days_after |
|---|---|---|---|
| (implicit) | coi_request |
initial |
0 |
CRFRRSetting |
coi_request |
reminder |
CRFRRSetting (cumulative) |
CRSRRSetting |
coi_request |
final |
CRFRR + CRSRR (cumulative) |
CRNRAlertSetting |
coi_request |
alert |
CRFRR + CRSRR + CRNRAlert (cumulative) |
CRRestartSetting |
coi_request |
restart_notices |
CRFRR + CRSRR + CRNRAlert + CRRestart (cumulative) |
CESTESetting / CEFERTiming=Before |
coi_expiration |
soon_to_expire |
CESTESetting, timing_type=before |
CEFERSetting / CEFERTiming |
coi_expiration |
reminder |
CEFERSetting, timing_type per CEFERTiming |
CEEXPSetting / CEEXPTiming |
coi_expiration |
expired |
CEEXPSetting, timing_type per CEEXPTiming |
CESERSetting / CESERTiming |
coi_expiration |
final |
CESERSetting, timing_type per CESERTiming |
CEEXPAlertSetting / CEEXPAlertTiming |
coi_expiration |
alert |
CEEXPAlertSetting, timing_type per CEEXPAlertTiming |
CERestartSetting |
coi_expiration |
restart_notices |
CERestartSetting, timing_type=after |
CNCFSetting |
coi_non_compliance |
reminder |
CNCFSetting (cumulative) |
CNCSSetting |
coi_non_compliance |
final |
CNCF + CNCS (cumulative) |
CNCAlertSetting |
coi_non_compliance |
alert |
CNCF + CNCS + CNCAlert (cumulative) |
CNCRestartSetting |
coi_non_compliance |
restart_notices |
CNCF + CNCS + CNCAlert + CNCRestart (cumulative) |
Column mapping β DOCUMENT notification row:
| DTX 1.0 Column | DTX 2.0 type | DTX 2.0 sequence | DTX 2.0 days_after |
|---|---|---|---|
| (implicit) | document_request |
initial |
0 |
DRFRRSetting |
document_request |
reminder |
DRFRRSetting (cumulative) |
DRSRRSetting |
document_request |
final |
DRFRR + DRSRR (cumulative) |
DRNRAlertSetting |
document_request |
alert |
DRFRR + DRSRR + DRNRAlert (cumulative) |
Email recipients from notification_webhook:
For each (customer_id, type, selector) in notification_webhook:
1. Deserialize PHP serialized webhook field β extract email array
2. Map selector to (type, sequence):
| DTX 1.0 selector | DTX 2.0 type | DTX 2.0 sequence |
|---|---|---|
NONCOMP |
coi_non_compliance / document_non_compliance |
initial |
NONCOMP_REMIND_1 |
*_non_compliance |
reminder |
NONCOMP_REMIND_2 |
*_non_compliance |
final |
EXPIRE_SOON |
*_expiration |
soon_to_expire |
EXPIRE_REMIND_1 |
*_expiration |
reminder |
EXPIRE_REMIND_2 |
*_expiration |
final |
EXPIRED |
*_expiration |
expired |
REQUEST_REMIND_2 |
*_request |
final |
NotificationSchedule.email_recipientsYield: Each DTX 1.0 customer produces up to ~30 NotificationSchedule rows (COI: 5 request + 6 expiration + 5 non-compliance = 16; DOCUMENT: 4 request + similar = ~14).
Additional fields:
- notification.send_hold β NotificationSchedule.is_active (SEND=True, HOLD=False)
- notification.user_id β store in ownership fields if needed
- notification.create_time β use as creation timestamp
- notification.customer_id β remap to NotificationSchedule.account FK via Area 2 mapping
The notification_lookup table (12 columns) is a denormalized join table mapping customers to their notification configurations via coi_notification_id and doc_notification_id FKs, plus a broker_id reference.
Drop this table. DTX 2.0 links NotificationSchedule directly to Account via FK. The customerβnotification relationship is implicit. The broker_id reference is irrelevant (Area 9 found no Broker model in DTX 2.0).
Data preserved: The coi_send_hold / doc_send_hold overrides and coi_dow_bitmap / doc_dow_bitmap day-of-week settings from this table are partially captured by Decision #1 (send_hold β is_active). The dow_bitmap values have no DTX 2.0 equivalent (see Decision #7).
The notification_audit table (25 columns) records BEFORE/AFTER snapshots of notification setting changes. It has 3 fewer columns than the live notification table (missing CRRestartSetting, CERestartSetting, CNCRestartSetting), indicating schema drift β columns were added to notification without updating the audit table.
Don't migrate. This is historical audit data for a settings table that is being completely restructured (Decision #1). The old BEFORE/AFTER snapshots of DTX 1.0-format settings have no value in DTX 2.0's new architecture. DTX 2.0 tracks notification activity via EmailLog and NotificationQueue rather than settings-change auditing.
Keep CSV as archive for reference.
The letter table (9 columns) stores customer-specific email templates with HTML bodies containing __PLACEHOLDER__ macros (e.g., __DATE__, __SO_NAME__, __PH_NAME__, __EXPIRING_POLICIES__).
Column mapping:
| DTX 1.0 | DTX 2.0 | Action |
|---|---|---|
customer_id |
entity_type + entity_id |
Set entity_type='account', entity_id = remapped Account ID |
type |
(part of name) |
Incorporate into template name (e.g., "COI Expire Remind 2") |
selector |
(part of name) |
Incorporate into template name |
subject |
subject |
Direct, with placeholder remapping |
letter_body |
body_html |
Direct, with placeholder remapping |
edit_time |
modified |
Use as modification timestamp |
name |
β | Drop (always NULL in sample) |
addl_email |
β | Drop (always NULL) |
internal_reference_name |
β | Drop (always NULL) |
| β | body_text |
New β leave NULL (plain text fallback) |
| β | from_email |
New β leave NULL (use system default) |
| β | from_name |
New β leave NULL (use system default) |
| β | variables |
New β populate from detected placeholders in body_html |
| β | is_active |
New β set True |
| β | is_default |
New β set False |
| β | language |
New β set 'en' (default); set 'es' for Spanish templates |
Placeholder macro remapping:
| DTX 1.0 | DTX 2.0 | Notes |
|---|---|---|
__DATE__ |
{DATE} |
Current date |
__SO_NAME__ |
{SO_NAME} |
Account/service owner name |
__SO_LOGO__ |
{SO_LOGO} |
Account logo URL |
__SO_ADDRESS1__ |
{SO_ADDRESS1} |
Account address |
__SO_CITY__, __SO_STATE__, __SO_ZIPCODE__ |
{SO_CITY}, {SO_STATE}, {SO_ZIPCODE} |
Address components |
__PH_NAME__ |
{PH_NAME} |
Policyholder/record name |
__PH_ADDRESS1__, etc. |
{PH_ADDRESS1}, etc. |
Record address components |
__PH_CONTACT_EMAIL__ |
{PH_CONTACT_EMAIL} |
Record email |
__CH_NAME__ |
{CH_NAME} |
Certificate holder name |
__SUBJECT__ |
{SUBJECT} |
Dynamic subject line |
__EXPIRING_POLICIES__ |
{EXPIRING_POLICIES} |
Expiring policy list HTML |
__DOCUMENTS_EXPIRING__ |
{DOCUMENTS_EXPIRING} |
Expiring documents list HTML |
__REQUIREMENTS__ |
{REQUIREMENTS} |
Requirements list HTML |
__DOCUMENT_REQUIREMENTS__ |
{DOCUMENT_REQUIREMENTS} |
Document requirements list |
__CUSTOM_MESSAGE_1__ |
{CUSTOM_MESSAGE_1} |
Custom per-customer message |
__SITE_URL__ |
{SITE_URL} |
Upload portal URL |
__DOCUMENTS_URL__ |
{DOCUMENTS_URL} |
Document upload URL |
Approach:
- Regex-replace all __([A-Z0-9_]+)__ patterns with {$1} in both subject and body_html.
- Detect unique placeholders from body_html and populate the variables JSON list.
- Set name = "{type} {selector}" (e.g., "COI EXPIRE_REMIND_2").
- Some templates contain Spanish text β set language='es' for those, or create bilingual variants.
Pre-migration action: Build a complete list of all __PLACEHOLDER__ macros used across all letter rows and verify each maps to a DTX 2.0 template variable supported by EmailNotificationService.get_record_context().
The messagequeue table (15 columns) stores complete sent email records including full HTML bodies (letter_body), recipient addresses, send/read timestamps, and GUID-based tracking pixels.
Don't migrate. Rationale:
- Structural mismatch: DTX 2.0's EmailLog tracks delivery metadata (schedule FK, delivery_status, context_data) β it does not store full HTML email bodies. The monolithic letter_body field (averaging ~57 lines of HTML per row) has no target column.
- Historical data: These are records of emails already sent. They have no operational value in the new system.
- Tracking infrastructure change: DTX 1.0 used GUID-based tracking pixels (risktoolbox.com/letters/messageread/{GUID}) and read_time tracking. DTX 2.0 uses delivery_status from the email provider (Mailgun).
- Volume concern: The full messagequeue table likely contains thousands of rows, each with multi-KB HTML bodies.
Keep CSV as archive. If historical email audit is needed, it can be queried from the archived data.
The envelope table (9 columns) tracks broker/vendor responses to notification emails. When a vendor receives a compliance request email with a portal link, the envelope records their interaction: whether they accessed it, how many clicks, and the outcome (Completed, Declined, Voided). The vendor_data column contains PHP serialized contact information (doubly-serialized β the email field within is itself a PHP serialized array).
DTX 2.0 does not have an Envelope model. The DTX 2.0 system uses magic links (cryptographically signed URLs) for vendor document upload, tracked through the standard Document upload flow rather than a dedicated response-tracking table.
The team must decide: 1. Archive and discard β The envelope tracking was tightly coupled to the old notification system. DTX 2.0's magic link + Document upload flow replaces this functionality differently. 2. Create a ResponseTracking model β If historical response rates (Completed vs Declined vs Voided) are valuable for analytics or to benchmark the new system's vendor engagement rates. 3. Extract aggregate metrics only β Summarize response rates per customer/vendor and store as reference data without migrating individual envelope records.
Notable data in envelope:
- event distribution: Voided (60%), Completed (36%), Declined (3%), NULL (1%) β shows most compliance requests expire without vendor action.
- vendor_data: Contains vendor contact snapshots (name, address, email, phone). These are point-in-time copies, not canonical records.
DTX 1.0 has two notification control settings that map imperfectly to DTX 2.0:
1. send_hold (SEND/HOLD):
- notification.send_hold: Global toggle per customer/type
- notification_lookup.coi_send_hold / doc_send_hold: Per-customer overrides
- DTX 2.0 mapping: NotificationSchedule.is_active (True/False). Partial mapping β DTX 2.0 is per-schedule-row, not per-customer-type. A customer with send_hold=HOLD for COI would set is_active=False on all their COI-type schedules.
2. dow_bitmap (day-of-week):
- notification.dow_bitmap: Bitmask for which days notifications can be sent (e.g., 32 = Friday only, 62 = Mon-Fri, 127 = every day)
- DTX 2.0 has no equivalent. The Celery task process_notification_queue runs daily at 8 AM regardless of day-of-week.
- Impact: Customers who restricted notifications to specific days will now receive them any day.
- Options:
- A. Accept the gap β process notifications daily (simplification)
- B. Add a dow_bitmap or send_days field to NotificationSchedule to preserve per-customer day restrictions
- C. Store in metadata/notes for manual configuration later
| Source Area | Deferred Item | Resolution |
|---|---|---|
| Area 1: Users & Auth | alert_subscribers table (user_id + customer_id alert preferences, coi_dow_bitmap) |
Resolved β dropped per Area 1 Decision #5. DTX 2.0 NotificationSchedule replaces user-level alert toggles with account-level schedules. The coi_dow_bitmap gap is covered in Decision #7. |
| Area 1: Users & Auth | bounced_email table |
Resolved β not migrated per Area 1 Decision #11. DTX 2.0 tracks bounces via Mailgun delivery webhooks and EmailLog.delivery_status. |
| Area 3: Vendors/Records | Vendor ping fields (ping_date, ping_to, ping_cc, ping_subject, ping_message, ping_note) | Resolved β all dropped per Area 3. Replaced by NotificationQueue + EmailLog tracking. |
| Area 3: Vendors/Records | additional_language, custom_message_2 |
Resolved β stored as reference data per Area 3. Can be used to populate {CUSTOM_MESSAGE_1} in EmailTemplate if needed. |
| Area 9: Brokers/Programs | program.envelope_id FK |
Resolved β depends on Decision #6. Program table not migrated (Area 9). If envelope is not migrated, this FK is moot. |
| # | Issue | Impact | Mitigation |
|---|---|---|---|
| 1 | PHP serialized email arrays in notification_webhook.webhook and envelope.vendor_data |
Requires deserialization to extract email addresses. envelope.vendor_data is doubly-serialized (email field is itself a serialized array). |
Use a PHP unserialize library (Python: phpserialize package) during migration ETL. |
| 2 | Cumulative vs incremental timing ambiguity in DTX 1.0 | CR/CE/CNC/DR settings may be incremental intervals or cumulative offsets from T0. The sample shows all-identical values (14/14/5/30), making it impossible to distinguish from sample data alone. | Pre-migration action: examine DTX 1.0 PHP code to confirm whether settings are incremental or cumulative. |
| 3 | dow_bitmap encoding | Bitmask values (0, 2, 32, 36, 42, 64, 127) need interpretation. Likely Mon=1, Tue=2, Wed=4, Thu=8, Fri=16, Sat=32, Sun=64, but the exact bit assignment is unconfirmed. | Pre-migration action: verify bit-to-day mapping from DTX 1.0 source code. |
| 4 | Schema drift between notification (28 cols) and notification_audit (25 cols) | 3 "restart" columns missing from audit table β added after the audit table was created. | No impact (audit not migrated β Decision #3). |
| 5 | letter table has no surrogate PK | Composite key (customer_id, type, selector). DTX 2.0 EmailTemplate uses auto-increment PK. | Generate new PKs during migration. Use unique constraint (entity_type, entity_id, name, language) for deduplication. |
| 6 | __DATE__ placeholder un-replaced in some messagequeue bodies |
Indicates the template engine sometimes stored pre-rendered HTML with placeholder artifacts. | Only relevant if messagequeue is migrated (Decision #5 says don't). |
| 7 | Homogeneous sample data | notification sample: all send_hold=HOLD, all dow_bitmap=32. messagequeue: single customer, single event type. letter: single selector. |
Full dataset likely has more variety. Pre-migration: query distinct values from full DTX 1.0 database. |
| 8 | Bilingual templates | Some letter templates contain Spanish text ("Favor de enviarle esta solicitud..."). | Set language='es' for Spanish templates, or create separate en/es EmailTemplate rows. Detect via regex on letter_body. |
| 9 | Envelope doubly-serialized vendor_data | Email field within the serialized array is itself a PHP serialized array. Requires recursive deserialization. | Only relevant if envelope data is preserved (Decision #6). |
| # | Action | Blocking? |
|---|---|---|
| 1 | Confirm timing semantics: Examine DTX 1.0 PHP source code to determine whether CR/CE/CNC/DR setting values are incremental intervals or cumulative offsets from T0. | Yes β needed for correct days_after calculation in Decision #1. |
| 2 | Verify dow_bitmap encoding: Confirm which bit corresponds to which day of the week (Mon=1 or Sun=1?). | Yes β needed if team chooses to preserve dow_bitmap per Decision #7 Option B. |
| 3 | Inventory all PLACEHOLDER macros: Query full letter table for all distinct placeholder patterns used. Verify each maps to a DTX 2.0 template variable in EmailNotificationService.get_record_context(). |
Yes β needed for Decision #4 placeholder remapping. |
| 4 | Query full notification variety: Get distinct send_hold, dow_bitmap, and type distributions from full DTX 1.0 database to understand actual configuration diversity. |
No β but helps gauge migration scope. |
| 5 | Assess notification_webhook selector coverage: Get full list of distinct selectors (beyond the 8 in sample) to confirm the selector β (type, sequence) mapping is complete. | Yes β needed for Decision #1 email recipient mapping. |
Step 1: Migrate letter β EmailTemplate (templates needed before schedules reference them)
Step 2: Migrate notification + notification_webhook β NotificationSchedule (config with email recipients)
Step 3: Drop notification_lookup (no migration needed)
Step 4: Drop notification_audit (no migration needed)
Step 5: Archive messagequeue (no migration, keep CSV)
Step 6: Resolve envelope per team decision
Step 7: Bootstrap NotificationQueue and EmailLog (empty β populated by DTX 2.0 operations going forward)
| Area | Reference | Status |
|---|---|---|
| Area 1: Users & Auth | alert_subscribers dropped; bounced_email not migrated |
Resolved |
| Area 2: Customers/Accounts | notification.customer_id β Account remap; AccountEmail for recipients |
Resolved β use Area 2 mapping |
| Area 3: Vendors/Records | Vendor ping fields dropped; additional_language, custom_message_2 stored as ref |
Resolved |
| Area 9: Brokers/Programs | notification_lookup.broker_id (dropped with table); program.envelope_id |
Resolved β both tables dropped/dependent on Decision #6 |
| Area 11: File Repository | No direct references | N/A |
| # | Decision | Status | Approach |
|---|---|---|---|
| 1 | notification + notification_webhook β NotificationSchedule | Documented | See decision details below |
| 2 | notification_lookup β Drop | Documented | See decision details below |
| 3 | notification_audit β Don't migrate | Documented | See decision details below |
| 4 | letter β EmailTemplate | Documented | See decision details below |
| 5 | messagequeue β Don't migrate (archive) | Documented | See decision details below |
| 6 | envelope β No target model | Documented | See decision details below |
| 7 | send_hold and dow_bitmap capability gap | Documented | See decision details below |
| 8 | Deferred references from other areas | Documented | See decision details below |