16 KiB
OnDeck MVP — Policy Renewal Workflow System
Minimum Viable Product scope for the OnDeck policy renewal management platform. This document defines the feature set required for initial production deployment.
Table of Contents
- User Authentication
- Logging
- AMS360 Data Sync
- Task Management & Renewal Workflow
- Renewal Grouping & Policy Exclusion
1. User Authentication
1.1 Authentication Providers
| Provider | Purpose | Status |
|---|---|---|
| Local credentials | Development and testing (3 built-in accounts: admin, manager, ae) | Existing |
| Microsoft Entra ID (Azure AD) | Production SSO | Existing |
1.2 Account Linking
When a user signs in via Microsoft Entra ID and the email matches an existing local account, the accounts are linked automatically by populating the entraOid field on the existing user record.
- First lookup: find user by email where
entraOidis null (pre-created local account) - If found: update the record with the Entra Object ID to link the accounts
- Fallback: standard upsert by
entraOidfor net-new users
Reference:
src/lib/auth.ts:110-144
1.3 Role-Based Access Control
Four roles with granular permissions:
| Role | Key Permissions |
|---|---|
| Admin | Full access — users, clients, tasks, designations, templates, sync, reports, audit |
| Manager | Read users; read/write clients, tasks, templates; assign tasks; view reports |
| Account Executive | Read/write clients and tasks; read designations |
| Claims | Read clients; read/write tasks |
Permission categories: User Management, Client Management, Task Management, Designation Management, Template Management, Sync Operations, Reports, Audit.
Role resolution combines two sources:
- Entra Group mappings —
EntraGroupRoleMappingtable maps Azure AD groups to application roles - Direct assignments —
UserRolejoin table for explicit per-user role grants
Reference:
src/lib/auth/roles.ts,src/lib/auth/permissions.ts
1.4 Session Strategy
- JWT-based sessions (
strategy: 'jwt') - Session callback enriches the token with:
id,entraOid,roles[], mergedpermissionsobject (union of all assigned role permissions)
2. Logging
2.1 Authentication Logging
| Event | Details |
|---|---|
| Sign-in success | Provider, user ID, timestamp |
| Sign-in failure | Provider, email attempted, failure reason |
| Account linking | User ID, linked Entra OID |
2.2 Action Logging (Audit)
All user-initiated mutations on entities are recorded in the AuditLog model:
| Field | Description |
|---|---|
userId |
Actor who performed the action |
action |
Operation type (create, update, delete) |
entityType |
Target entity (Client, Policy, Task, etc.) |
entityId |
Target record ID |
oldValues |
JSON snapshot of previous state |
newValues |
JSON snapshot of new state |
ipAddress |
Request origin |
userAgent |
Client identifier |
createdAt |
Timestamp |
Reference:
AuditLogmodel inprisma/schema.prisma
2.3 Error Logging
- API route errors with request context
- Sync failures with stream identification and row-level detail
- Unhandled exceptions with stack traces
2.4 Info Logging
- Sync progress: rows fetched, processed, inserted, updated per stream
- Scheduled job execution: start, completion, next scheduled run
- System events: application startup, configuration changes
2.5 Alerts & Notifications
Alerts are delivered through the existing Notification model:
| Alert Type | Trigger |
|---|---|
| Overdue tasks | Task dueDate has passed with status not in COMPLETED/NA/CANCELLED |
| Sync failures | Any sync stream fails after retry exhaustion |
| System health | Scheduler down, database connectivity issues |
User notification preferences are stored in NotificationPreference with per-type toggles for: in-app, email, and Teams delivery channels.
Reference:
NotificationandNotificationPreferencemodels inprisma/schema.prisma
3. AMS360 Data Sync
3.1 Overview
Sync data from the AFW SQL Server database (AMS360 source of truth) into the local PostgreSQL database. The sync engine connects to AFW via a managed connection pool (max 10 connections, 60s request timeout).
Reference:
src/lib/sync/afw-connection.ts
3.2 Department Scope
| Item | Current State | MVP Target |
|---|---|---|
| Departments synced | 5 departments (Dept1, Dept8, Dept9, Dept10, Dept12) | All 13 departments |
MVP change: Remove the hardcoded ShortName IN (...) filter in the policy sync query so all AFW departments are included.
Reference:
src/lib/sync/afw-queries.ts:221— current 5-department WHERE clause
3.3 Sync Streams
Three parallel streams execute concurrently via Promise.all():
| Stream | Source (AFW) | Target (Local) | Key Mapping |
|---|---|---|---|
| Employees → Users | AFW_Employee (Status = 'A') |
User |
EmpCode, Name, Email, DefaultGLDeptCode → department |
| Customers → Clients | AFW_Customer (Active = 'Y') |
Client |
CustId → amsCustomerId, contact fields, producer code |
| Policies → Policies | AFW_BasicPolInfo + 20 joined lookup tables |
Policy |
Complex CTE with company, department, personnel lookups; up to 2 additional reps and 2 additional execs per policy |
Reference:
src/lib/sync/sync-engine.ts,src/lib/sync/afw-queries.ts,src/lib/sync/mappers.ts
3.4 Sync Modes
| Mode | Behavior |
|---|---|
| Incremental (default) | Syncs records changed since last successful sync, using ChangedDate filtering where supported |
| Full | Re-syncs all records regardless of last sync timestamp |
Signature: runSync(triggeredBy?: string, isIncremental: boolean = true)
3.5 Scheduling & Triggers
| Mechanism | Details |
|---|---|
| Scheduled | Configurable cron expression stored in SyncConfig table (default: 0 2 * * * — daily at 2 AM) |
| Manual | POST /api/sync with optional date range selection; requires sync.trigger permission |
Configuration keys in SyncConfig:
sync_enabled— master togglesync_schedule_cron— cron expressionpolicy_start_date/policy_end_date— date range for policy sync window
Reference:
src/lib/sync/scheduler.ts
3.6 Retry Logic
Each sync stream independently retries on failure:
- Max retries: 3
- Backoff: Exponential — 2s, 4s, 8s
3.7 Sync Logging
Every sync execution is recorded in the SyncLog model:
| Field | Description |
|---|---|
syncType |
Which stream or overall sync |
startedAt / completedAt |
Timing |
status |
Success or failure |
rowsProcessed |
Total rows fetched from AFW |
rowsInserted |
New records created |
rowsUpdated |
Existing records updated |
errorMessage |
Failure details (if any) |
triggeredBy |
User ID or "scheduler" |
3.8 API Endpoints
| Method | Path | Purpose | Permission |
|---|---|---|---|
POST |
/api/sync |
Trigger manual sync | sync.trigger |
GET |
/api/sync |
List recent sync logs | sync.trigger |
GET |
/api/sync/status |
Scheduler status | sync.trigger |
4. Task Management & Renewal Workflow
4.1 Task Templates
Templates define repeatable task patterns for renewal workflows:
| Field | Description |
|---|---|
name |
Template display name |
description |
Detailed instructions |
department |
PERSONAL_LINES, COMMERCIAL_LINES, CLAIMS, BENEFITS, OTHER |
timing |
PRE_RENEWAL or POST_RENEWAL |
daysOffset |
Signed integer — number of days before (negative) or after (positive) policy expiration |
defaultPriority |
LOW, MEDIUM, HIGH, or URGENT |
designationId |
Optional — restrict template to clients with a specific designation |
displayOrder |
Presentation ordering |
isActive |
Soft-disable toggle |
Designation-based rules: When a template has a designationId, it only generates tasks for clients whose primary or secondary designation matches.
Reference:
TaskTemplatemodel inprisma/schema.prisma
4.2 Task Auto-Generation
Tasks are generated from templates based on policy expiration dates:
- Identify policies approaching expiration within the configured window
- Match applicable templates by department and (optionally) client designation
- Calculate due date:
policy.expirationDate + template.daysOffset - Create task records linked to the client, policy, and source template
4.3 Task Statuses
| Status | Description |
|---|---|
NOT_STARTED |
Default state on creation |
IN_PROGRESS |
Work underway |
COMPLETED |
Finished — captures completedAt timestamp and completedBy user |
BLOCKED |
Cannot proceed — requires intervention |
NA |
Not applicable — requires naReason documentation |
CANCELLED |
Voided — requires cancelledReason documentation |
4.4 Priority Levels
| Priority | Usage |
|---|---|
LOW |
Routine, no urgency |
MEDIUM |
Standard renewal tasks |
HIGH |
Approaching deadline or important client |
URGENT |
Immediate attention required |
4.5 Timing & Day Offsets
Tasks are classified as either:
- PRE_RENEWAL — due before the policy expiration date (negative
daysOffset) - POST_RENEWAL — due after the policy expiration date (positive
daysOffset)
The daysOffset value is configurable per template, allowing precise scheduling of each step in the renewal workflow.
4.6 Task Assignment
- Tasks support multi-user assignment via the
TaskAssignmentjoin table - Workload visibility:
GET /api/dashboard/workloadprovides manager-facing metrics on task distribution across team members - Dashboard displays per-user statistics: overdue, due today, upcoming
4.7 N/A Marking
When a task is marked as NA, the naReason field is required. This ensures accountability and auditability for skipped tasks. Similarly, CANCELLED status requires cancelledReason.
4.8 API Endpoints
| Method | Path | Purpose | Permission |
|---|---|---|---|
GET |
/api/tasks |
List tasks with filtering (status, clientId, assignedToMe, pagination) | tasks.read |
POST |
/api/tasks |
Create task | tasks.write |
Query parameters: status, clientId, assignedToMe, page, limit (default 50).
5. Renewal Grouping & Policy Exclusion
Status: NEW — not yet implemented. This section defines net-new functionality for the MVP.
5.1 Problem Statement
A single client may have multiple policies with the same expiration date. Currently, there is no mechanism to control how renewal tasks are generated across those policies — whether they share a single task set or need separate ones. Additionally, some policies (being non-renewed, rewritten, or cancelled) should be excluded from task generation entirely.
5.2 Default Behavior: Client-Level Task Sets
When no explicit grouping exists, the system generates one set of renewal tasks per client based on the client's policies and their expiration dates. This preserves backward compatibility with the existing workflow.
5.3 Renewal Groups
A new RenewalGroup entity enables manual control over how policies are grouped for task generation:
RenewalGroup model (new):
| Field | Type | Description |
|---|---|---|
id |
UUID | Primary key |
name |
String | User-provided label (e.g., "Main BOP + Auto bundle") |
clientId |
FK → Client | Owning client |
createdBy |
FK → User | Who created the group |
createdAt |
DateTime | Timestamp |
updatedAt |
DateTime | Timestamp |
RenewalGroupPolicy join table (new):
| Field | Type | Description |
|---|---|---|
renewalGroupId |
FK → RenewalGroup | Group membership |
policyId |
FK → Policy | Included policy |
5.4 Manual Grouping
Users can create explicit renewal groups by selecting which policies should share a single task set:
- Select a client
- Choose one or more policies from that client
- Name the group
- Task generation uses the group rather than the full client policy set
Use case: A client has 5 policies but 3 of them are bundled and should be renewed together. Create one group with those 3 policies — they get one shared task set. The remaining 2 policies can be in separate groups or fall through to default behavior.
5.5 Splitting
When a client's policies need separate renewal workflows, users can split them into distinct renewal groups:
- Each group gets its own independent set of renewal tasks
- Groups can contain one or many policies
- Ungrouped policies fall back to default client-level behavior
5.6 Policy Exclusion
Individual policies can be marked as excluded from task generation:
Policy model addition (new field):
| Field | Type | Description |
|---|---|---|
isExcludedFromRenewal |
Boolean (default: false) | Excludes policy from task generation |
exclusionReason |
String (nullable) | Required when excluded — documents why |
Use cases for exclusion:
- Policy is being non-renewed
- Policy is being rewritten under a new policy number
- Policy is being moved to a different carrier outside the normal renewal flow
- Client has requested cancellation
Excluded policies are skipped during task auto-generation but remain visible in the client's policy list with a visual indicator.
5.7 Task Generation Logic (Updated)
With renewal grouping and exclusion in place, task auto-generation follows this precedence:
- Exclude any policy where
isExcludedFromRenewal = true - Grouped policies: For each
RenewalGroup, generate one task set using the group's policies - Ungrouped policies: For remaining policies not in any group, generate one task set per client (default behavior)
Appendix
A. Technology Stack
| Layer | Technology |
|---|---|
| Frontend | React, TypeScript, Next.js 16 (App Router), Tailwind CSS, shadcn/ui |
| Backend | Next.js API Routes, NextAuth.js 4.x (JWT) |
| Database | PostgreSQL via Prisma ORM 7.x |
| Source DB | SQL Server via mssql (AFW/AMS360) |
| Scheduling | node-cron |
| Deployment | Docker |
B. Data Model Index
| Model | Purpose | Status |
|---|---|---|
| User | Application users synced from AFW employees | Existing |
| Client | Customers synced from AFW | Existing |
| Policy | Insurance policies synced from AFW | Existing |
| Task | Renewal workflow tasks | Existing |
| TaskTemplate | Reusable task definitions | Existing |
| TaskAssignment | Multi-user task assignment | Existing |
| Designation | Client shape/classification | Existing |
| AuditLog | Action audit trail | Existing |
| Notification | User-facing alerts | Existing |
| NotificationPreference | Per-user delivery prefs | Existing |
| SyncLog | Sync execution history | Existing |
| SyncConfig | Sync configuration key-value store | Existing |
| EntraGroupRoleMapping | Azure AD group → role mapping | Existing |
| UserRole | Direct user → role assignment | Existing |
| RenewalGroup | Policy grouping for shared task sets | New |
| RenewalGroupPolicy | Group ↔ Policy join table | New |