Pharmagin Speaker Platform - Complete Redesign Specification
Version: 2.0 | Date: 2026-02-21 Based on: 22 analysis documents across 15 business domains
Table of Contents
- Executive Summary
- Current System Problems
- Target Architecture Overview
- Domain Redesign
- 4.1 Program Lifecycle
- 4.2 Speaker Management
- 4.3 Attendee & Registration
- 4.4 Budget & Financial
- 4.5 Compliance & Regulatory
- 4.6 User & Permission
- 4.7 Communication & Notification
- 4.8 Geographic & Territory
- 4.9 Reporting & Analytics
- 4.10 Configuration & Feature Management
- 4.11 Content & Document
- 4.12 Survey & Feedback
- 4.13 Expense Management
- 4.14 Virtual Program
- 4.15 Integration
- Unified Data Model
- API Architecture
- Frontend Architecture
- Security Architecture
- Infrastructure & DevOps
- Migration Strategy
- Implementation Roadmap
1. Executive Summary
1.1 Platform Purpose
Pharmagin is a pharmaceutical speaker program management platform enabling:
- Agency Planners to configure, manage, and close out speaker programs
- Sales Representatives to submit program requests and manage speaker interactions
- Speakers (HCPs) to manage profiles, contracts, training, and expenses
1.2 Scale of Current System
| Metric | Count |
|---|---|
| Backend modules | 37 |
| Database entities | 138+ |
| Frontend applications | 3 (Plannerview, Salesview, Speakerview) |
| External integrations | 12 |
| Feature flags | 165+ |
| API endpoints | ~200+ |
| Business rules | 123+ |
| Identified issues | 332 |
1.3 Why Rewrite
The current system suffers from:
- 18 critical security vulnerabilities (SQL injection, RCE, credential exposure, missing auth)
- Architectural debt: 12 god classes (600-2100+ lines), 98-field entities, scattered state machines
- Scalability blockers: In-memory auth tokens, local filesystem storage, thread-unsafe singletons
- Inconsistent data model: 7 soft-delete patterns, 10+ untyped JSONB fields, missing PKs
- Technology debt: React 15-16.x, Ant Design 3.x, Spring Boot 2.1.7 (EOL)
- No formal state machine: All status transitions hardcoded as if-else in service layer
1.4 Rewrite Goals
- Security First: Eliminate all 18 critical vulnerabilities; RBAC on every endpoint
- Domain-Driven Design: Decompose god entities into bounded contexts
- Lifecycle State Machine: Formal state machine with progressive locking
- Brand-First Budget: Budget allocation driven by brand (drug), not geography alone
- Unified Organization Hierarchy: Replace hardcoded 4-level geography with flexible OrgNode tree
- Modern Stack: Spring Boot 3.x, React 18+, TypeScript, Vite
- Horizontal Scalability: Stateless JWT, distributed cache, object storage
2. Current System Problems
2.1 Critical Security Issues (P0)
| ID | Issue | Location | Risk |
|---|---|---|---|
| S-01 | SQL Injection | SiteController, FileService, ExpenseMapper, Salesforce SOQL | Data breach |
| S-02 | Remote Code Execution | Custom Reports ProcessBuilder | Full server compromise |
| S-03 | API Key Exposure | /v1/public/configuration returns SendGrid/SFTP/API keys | Credential theft |
| S-04 | Unfiltered Data Extract | Raw SQL execution bypassing permission filters | Data exfiltration |
| S-05 | SSO Cookie Insecure | Missing HttpOnly/Secure/SameSite flags | Session hijacking |
| S-06 | Legacy Plaintext Password | Old password comparison still active | Account compromise |
| S-07 | JWT Claim Not Validated | External-Authorization lacks audience/expiry checks | Token spoofing |
| S-08 | 8 Controllers Unprotected | User, Role, Geography, SalesForce, Team, Content, File, Compliance | Unauthorized access |
2.2 Architectural Anti-Patterns
| Pattern | Instances | Impact |
|---|---|---|
| God Classes (>600 LOC) | 12 | AttendeeService 2100+, ProgramService 1577, ProductReportService 1430 |
| God Entities (>70 fields) | 4 | MeetingRequest 98, Speaker 88, Meeting 77, Attendee 70+ |
| Naming Confusion | 6+ | MeetingRequest=Program, Meeting=RegistrationSite, SalesTeam=SalesForce |
| JSONB Abuse (untyped) | 10+ | approvals, dynamicFields, virtualProgramInfo, content, config |
| Hardcoded Values | 15+ | productId==24, categoryId 9/4, FiscalPeriod 2025, DST 2017-2030 |
| Missing @Id | 5 | MeetingChangeLog, FiscalYear, AttendeeSurveyResponse, UserProductRole, SiteTemplate |
| Soft-Delete Inconsistency | 7 patterns | Boolean, Integer, String, status field, del_flag, deleted, hardcoded |
| Dual Entry Points | 7 | /meetings + /programs, /site + /registration, etc. |
2.3 Scalability Blockers
- In-memory Guava Cache for auth tokens (no horizontal scaling)
- Local filesystem for file storage (no multi-instance)
- Thread-unsafe singletons (FieldMappingFactory, SfdcFacade, ZoomAccessTokenManager)
- SimpleDateFormat instance variables (race conditions)
- Raw Thread() for document generation (no pooling)
- No message queue (email sends are fire-and-forget @Async)
- No distributed cache (sessions lost on restart)
- No API gateway (auth/rate-limiting per service)
2.4 Data Integrity Risks
- Missing transactions: District/Territory reassign, Attendee Response save, Agg Spend config, Budget allocation
- Hard deletes: Speaker, Expense, MeetingAlert (with empty Example deletes ALL)
- Orphan data: t_user_auth_token unbounded, temp files leak, history tables accumulate
- 25% attendee unreconciliation rate blocking project closures
- 85% NULL sign_in_status (never initialized on creation)
3. Target Architecture Overview
3.1 Architecture Principles
- Bounded Contexts: Each domain owns its entities, services, and APIs
- Event-Driven: Domain events for cross-domain communication
- State Machine First: All lifecycle entities use formal state machines
- Progressive Locking: Permissions narrow as lifecycle advances
- Brand-First Budget: Budget hierarchy starts from Brand, then geography
- Configuration as Data: Feature flags in database with runtime toggle
- Security by Default: @PreAuthorize on all endpoints, parameterized queries
3.2 Technology Stack
| Layer | Current | Target |
|---|---|---|
| Java | 8 | 17+ |
| Spring Boot | 2.1.7 | 3.2+ |
| Spring Security | OAuth2/JWT (custom) | Spring Security 6 + OAuth2 Resource Server |
| ORM | MyBatis + tk.mybatis | MyBatis-Plus or jOOQ |
| Database | PostgreSQL | PostgreSQL 15+ |
| Cache | Guava (in-memory) | Redis |
| Message Queue | None | RabbitMQ or SQS |
| File Storage | Local filesystem | S3/MinIO |
| Config | Spring Cloud Config (YAML) | Database + Spring Cloud Config (fallback) |
| React | 15-16.x | 18+ |
| UI Framework | Ant Design 3.x | Ant Design 5.x |
| Router | React Router 3-4.x | React Router 6+ |
| State | Redux (manual) | Redux Toolkit + RTK Query |
| Build | CRACO | Vite |
| Language | JavaScript | TypeScript |
| Micro-frontend | Single-SPA (SystemJS) | Module Federation or Single-SPA (ES modules) |
3.3 Service Architecture
┌─────────────────┐
│ API Gateway │ (Auth, Rate Limiting, Routing)
│ (Spring Cloud │
│ Gateway) │
└────────┬────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌────────▼───┐ ┌──────▼──────┐ ┌───▼────────┐
│ pharmagin │ │ pharmagin │ │ pharmagin │
│ -web │ │ -auth │ │ -worker │
│ (REST API) │ │ (SSO+JWT) │ │ (Async) │
└─────┬──────┘ └─────────────┘ └─────┬──────┘
│ │
┌─────▼──────────────────────────────────▼─────┐
│ PostgreSQL │
│ schemas: ooto, public, target │
└──────────────┬─────────────────────────────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌──▼──┐ ┌─────▼──┐ ┌─────▼────┐
│Redis │ │S3/MinIO│ │RabbitMQ │
└──────┘ └────────┘ └──────────┘Service Responsibilities:
- pharmagin-web: Main REST API (all 15 domains)
- pharmagin-auth: Unified SSO (merge pharmagin-sso + pharmagin-login) + JWT issuance
- pharmagin-worker: Async tasks (email, reports, integrations, cron jobs)
- pharmagin-config: Spring Cloud Config Server (retained for YAML fallback)
3.4 Domain Map (Bounded Contexts)
┌─────────────────────────────────────────────────────────────────────┐
│ CORE BUSINESS DOMAINS │
│ │
│ ┌──────────────┐ ┌───────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Program │ │ Speaker │ │ Attendee │ │ Budget │ │
│ │ Lifecycle │ │ Mgmt │ │ & Reg │ │ & Finance │ │
│ └──────┬───────┘ └─────┬─────┘ └──────┬─────┘ └──────┬─────┘ │
│ │ │ │ │ │
│ ┌──────▼───────┐ ┌─────▼─────┐ │
│ │ Compliance │ │ Expense │ │
│ │ & Regulatory │ │ Mgmt │ │
│ └──────────────┘ └───────────┘ │
│ │
├─────────────────────────────────────────────────────────────────────┤
│ SUPPORTING DOMAINS │
│ │
│ ┌──────────────┐ ┌───────────┐ ┌────────────┐ ┌────────────┐ │
│ │ User & │ │ Communi- │ │ Geographic│ │ Config & │ │
│ │ Permission │ │ cation │ │ & Territory│ │ Feature │ │
│ └──────────────┘ └───────────┘ └────────────┘ └────────────┘ │
│ │
│ ┌──────────────┐ ┌───────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Content & │ │ Survey │ │ Virtual │ │ Reporting │ │
│ │ Document │ │ Feedback │ │ Program │ │ & Analytics│ │
│ └──────────────┘ └───────────┘ └────────────┘ └────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────────┤
│ INTEGRATION LAYER │
│ │
│ ┌──────────────┐ ┌───────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Salesforce │ │ Zoom │ │ SendGrid │ │ SFTP/NPI │ │
│ └──────────────┘ └───────────┘ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────────────┘4. Domain Redesign
4.1 Program Lifecycle Domain
4.1.1 Entity Rename
| Current Name | New Name | Rationale |
|---|---|---|
| MeetingRequest | Program | Reflects actual business concept |
| Meeting | RegistrationSite | Clarifies registration infrastructure role |
| MeetingProgramType | ProgramType | Simplifies naming |
| ProgramServiceType | ServiceType | Simplifies naming |
| MeetingChangeLog | ProgramChangeLog | Consistent naming |
| MeetingProjectTask | ProgramTask | Consistent naming |
4.1.2 Entity Decomposition
Current: MeetingRequest (98 fields) - god entity
Target: Decompose into focused entities
Program (core - ~25 fields)
├── program_id (PK, BIGINT)
├── product_id, program_type_id, service_type_id
├── program_name (auto-generated)
├── status (ENUM: see state machine below)
├── approval_status (ENUM: PENDING, APPROVED, DENIED)
├── topic_id, brand_id, team_id
├── planner_id, requestor_user_id
├── meeting_start_time, meeting_end_time, timezone_id
├── single_day_event, date_on_calendar
├── attendee_number, actual_attendee_number
├── registration_site_id (FK)
├── created_at, updated_at, created_by, updated_by
├── deleted (BOOLEAN, default false)
└── old_status (saved before approval)
ProgramLocation (~12 fields)
├── program_id (FK, unique)
├── venue, city, state, postal_code, country_id
├── lat, lng, radius, gps_status
├── venue_notes, reason_for_venue_chosen
└── lowest_price_venue_chosen (BOOLEAN)
ProgramRequestor (~6 fields)
├── program_id (FK, unique)
├── first_name, last_name, job_title
├── phone_number, email
└── user_id (FK to UnifiedUser)
ProgramSpeaker (1:N - replaces hardcoded 3 slots)
├── id (PK)
├── program_id (FK)
├── speaker_id (FK)
├── role (ENUM: PRIMARY, SECONDARY, TERTIARY)
├── honoraria_amount
└── sequence (ordering)
ProgramFinancial (~15 fields)
├── program_id (FK, unique)
├── estimated_budget, meeting_cost, total_cost
├── speaker_honoraria
├── commissionable_rate, commission_rate
├── estimated_commission_amount
├── commission_collected_date, commission_collected_amount
├── cancellation_deadline_1/2/3
├── cancellation_amount_1/2/3
└── canceled_reason
ProgramApproval (1:N - replaces JSONB approvals)
├── id (PK)
├── program_id (FK)
├── approval_level (INT)
├── approver_role (ENUM: DM, RM, UM, RBD, MARKETING, LEGAL)
├── approver_user_id (FK, nullable)
├── status (ENUM: PENDING, APPROVED, DENIED, AUTO_APPROVED)
├── threshold_amount
├── decided_at, comment
└── created_at
ProgramCustomField (1:N - replaces meetingRequestInfo with @-@ delimiters)
├── id (PK)
├── program_id (FK)
├── field_key, field_value
└── field_type (TEXT, NUMBER, DATE, BOOLEAN)4.1.3 Program Status State Machine (14 States)
Based on the Meeting Lifecycle Redesign document, implement a formal 14-state progressive lifecycle:
Code Status Budget Ver Phase Editable Scope
──── ────────────────── ────────── ───────────── ─────────────────
0 DRAFT SOW Preparation Everything
1 WAITLISTED SOW Preparation Everything
2 PENDING_APPROVAL SOW Preparation Read-only
3 DENIED N/A Terminal Read-only
4 PLANNING EST Planning Program + Budget + RegSite
5 REGISTRATION_OPEN EST Registration Budget + Attendees
6 REGISTRATION_CLOSED BILL Execution Budget + Attendance
7 EVENT_COMPLETE BILL Reconciliation Budget + Reconciliation
8 RECONCILED ACT Settlement TOV + Expense
9 CLOSED ACT Archive Nothing
10 CANCELLED BILL Terminal Nothing
11 POSTPONED CXL Terminal Nothing
12 VOID N/A Terminal Nothing
13 REOPENED ACT Special TOV + Expense (limited)State Transitions (Legal Paths):
DRAFT ──────────────→ PLANNING
──────────────→ PENDING_APPROVAL
──────────────→ CANCELLED / VOID
WAITLISTED ─────────→ PLANNING
─────────→ PENDING_APPROVAL
─────────→ CANCELLED / VOID
PENDING_APPROVAL ───→ DRAFT (approved, restore old_status)
───→ DENIED (rejected)
DENIED ─────────────→ DRAFT (resubmit)
PLANNING ───────────→ REGISTRATION_OPEN
───────────→ CANCELLED / POSTPONED / VOID
REGISTRATION_OPEN ──→ REGISTRATION_CLOSED (manual or auto-deadline)
──→ CANCELLED / POSTPONED
REGISTRATION_CLOSED → EVENT_COMPLETE (auto: event end date passed)
→ CANCELLED
EVENT_COMPLETE ─────→ RECONCILED (all attendees reconciled)
─────→ CANCELLED
RECONCILED ─────────→ CLOSED (TOV complete, budget confirmed)
CLOSED ─────────────→ REOPENED (admin only)
REOPENED ───────────→ CLOSED
CANCELLED ──────────→ (terminal, + closedOut flag)
POSTPONED ──────────→ (terminal, + closedOut flag)
VOID ───────────────→ (terminal)Budget Version Derivation (eliminate stored field):
public enum ProgramStatus {
DRAFT(0), WAITLISTED(1), PENDING_APPROVAL(2), DENIED(3),
PLANNING(4), REGISTRATION_OPEN(5), REGISTRATION_CLOSED(6),
EVENT_COMPLETE(7), RECONCILED(8), CLOSED(9),
CANCELLED(10), POSTPONED(11), VOID(12), REOPENED(13);
public BudgetVersion deriveBudgetVersion() {
return switch (this) {
case DRAFT, WAITLISTED, PENDING_APPROVAL -> BudgetVersion.SOW;
case PLANNING, REGISTRATION_OPEN -> BudgetVersion.EST;
case REGISTRATION_CLOSED, EVENT_COMPLETE, CANCELLED -> BudgetVersion.BILL;
case RECONCILED, CLOSED, REOPENED -> BudgetVersion.ACT;
case POSTPONED -> BudgetVersion.CXL;
case DENIED, VOID -> null;
};
}
}4.1.4 Progressive Permission Matrix (MeetingPhaseGuard)
Core enforcement component - every write operation checks:
Operation DRAFT PLAN REG_OPEN REG_CLOSED EVENT RECON CLOSED REOPENED
────────────────────── ───── ───── ──────── ────────── ───── ───── ────── ────────
Edit Program Info Y Y Y Y N N N N
Edit Budget Y Y Y Y Y Y N Y(limited)
Configure Reg Site Y Y Y N N N N N
HCP Self-Register N N Y N N N N N
Add Attendee (manual) Y Y Y Y N N N N
Edit Attendee Info Y Y Y Y Y N N N
Delete Attendee Y Y Y Y N N N N
Edit Sign-In Status N N N Y Y N N N
HCP Reconciliation N N Y Y Y N N N
TOV Calculation N N N N N Y N Y
Assign Speaker Y Y Y Y N N N N
Send Invitations N N Y Y N N N N
Record Expense N N N Y Y Y N Y
Close Program N N N N N Y N YImplementation:
@Component
public class ProgramPhaseGuard {
public enum Operation {
EDIT_PROGRAM_INFO, EDIT_BUDGET, CONFIGURE_REG_SITE,
ACCEPT_REGISTRATION, ADD_ATTENDEE, EDIT_ATTENDEE, DELETE_ATTENDEE,
EDIT_SIGN_IN, RECONCILE_HCP, CALCULATE_TOV,
ASSIGN_SPEAKER, SEND_INVITATION, RECORD_EXPENSE, CLOSE_PROGRAM
}
private static final Map<ProgramStatus, Set<Operation>> ALLOWED = Map.ofEntries(
entry(DRAFT, Set.of(EDIT_PROGRAM_INFO, EDIT_BUDGET, CONFIGURE_REG_SITE,
ADD_ATTENDEE, EDIT_ATTENDEE, DELETE_ATTENDEE, ASSIGN_SPEAKER)),
// ... complete matrix
);
public void enforce(Long programId, Operation op) {
Program program = programRepository.findById(programId)
.orElseThrow(() -> new NotFoundException("Program not found"));
ProgramStatus status = program.getStatus();
if (!ALLOWED.getOrDefault(status, Set.of()).contains(op)) {
throw new PhaseGuardException(
"Operation %s not allowed in status %s".formatted(op, status));
}
}
}4.1.5 Auto-Transitions
Time-Based (Cron Job, hourly):
- REGISTRATION_OPEN → REGISTRATION_CLOSED: When
policy_registration_deadlinereached - REGISTRATION_CLOSED → EVENT_COMPLETE: When
meeting_end_timepassed
Condition-Based (Notifications):
- EVENT_COMPLETE → Notify Planner: "All attendees reconciled. Ready for Settlement?"
- RECONCILED → Notify Planner: "TOV complete, budget confirmed. Ready to close?"
Registration Closure Options (Configurable per Product):
- Manual: Planner clicks "Close Registration"
- Scheduled: Auto-close at
policy_registration_deadline - Pre-Event: Auto-close N hours before event start
- Capacity: Auto-close when
actual_attendee_number>=expected_attendees
4.1.6 Eliminated Status Fields
| Old Field | Replacement |
|---|---|
budget_version_id | Derived from Program Status (automatic) |
budget_status | Derived from Program Status (automatic) |
reg_site_status | Merged into Program Status (PLANNING/REG_OPEN/REG_CLOSED) |
attendee_list_status | Merged into Program Status (EVENT_COMPLETE+) |
attendee_list_closed_time | Recorded in ProgramChangeLog |
4.1.7 Close-Out Readiness API
New endpoint: GET /api/v2/programs/{id}/close-out-readiness
{
"budgetInACT": true,
"attendeeListClosed": true,
"tovCompleted": false,
"allAttendeesReconciled": false,
"allSignInsRecorded": false,
"projectTasksCompleted": true,
"readyToClose": false,
"blockers": [
"TOV not calculated",
"12 attendees unreconciled",
"45 sign-in statuses missing"
]
}4.2 Speaker Management Domain
4.2.1 Entity Decomposition
Current: Speaker (88 fields) - god entity
Target:
Speaker (core - ~20 fields)
├── speaker_id (PK, BIGINT)
├── company_id (FK)
├── first_name, last_name, middle_name, suffix, title
├── email_address, phone, mobile_phone
├── speaker_type, speaker_level, status (ENUM)
├── npi_number
├── specialty, board_certification, expertise
├── image_path
├── approved (BOOLEAN), deleted (BOOLEAN)
└── created_at, updated_at
SpeakerAddress (1:N)
├── id (PK)
├── speaker_id (FK)
├── address_type (ENUM: OFFICE, HOME, W9, NPI, MAILING)
├── street1, street2, city, state, zip, country_id
└── is_primary (BOOLEAN)
SpeakerFinancial (1:1)
├── speaker_id (FK, unique)
├── ssn_tax_id (ENCRYPTED)
├── payment_method
├── travel_fee
├── webcast_honoraria, local_honoraria
└── honoraria_cap
SpeakerCredential (1:N)
├── id (PK)
├── speaker_id (FK)
├── credential_type (ENUM: MEDICAL_LICENSE, DEA, BOARD_CERT, NPI)
├── license_number, state_of_license
├── expiration_date (DATE, not String!)
└── verified (BOOLEAN)
SpeakerProfile (1:1) - extended optional info
├── speaker_id (FK, unique)
├── institution_affiliation, affiliation_listing
├── business_name, website, publications, resume_file_id
├── primary_language, other_language
├── travel_preference, travel_companion_required (BOOLEAN)
├── handicapped_accessible (BOOLEAN)
├── category (ENUM: PATIENT, CAREGIVER)
├── classification, ambassador_type, audience_type
└── unavailable_days, required_notice4.2.2 Contract Model Redesign
Current: SpeakerContract with 15+ hardcoded honoraria fields (liveHonorarium, virtualHonorarium, peerToPeerHonorarium, etc.)
Target: Flexible honoraria items
SpeakerContract (~15 fields)
├── contract_id (PK)
├── speaker_id (FK)
├── product_id (FK)
├── contract_name, contract_type
├── status (ENUM: DRAFT, PENDING_SIGNATURE, SIGNED, EXPIRED, TERMINATED)
├── start_date, end_date
├── program_quantity_limit
├── file_id (FK to File)
├── signature, signed_date
└── created_at, updated_at
ContractHonorariaItem (1:N)
├── id (PK)
├── contract_id (FK)
├── honoraria_type (ENUM: LIVE, VIRTUAL, PEER_TO_PEER, WEBCAST, TRAINING, ADVISORY, etc.)
├── amount (DECIMAL)
├── currency (default USD)
└── effective_from, effective_to
ContractTerritory (N:N)
├── contract_id (FK)
├── org_node_id (FK) -- district or territory
└── created_at4.2.3 Speaker Status State Machine
INACTIVE → W9_PENDING → CONTRACT_CREATION_PENDING
→ CONTRACT_SIGNATURE_PENDING → ACTIVE → EXPIRED
Simplified to:
DRAFT → ONBOARDING → CONTRACT_PENDING → ACTIVE → SUSPENDED → EXPIREDTransitions:
- DRAFT → ONBOARDING: Profile complete, W9 submitted
- ONBOARDING → CONTRACT_PENDING: Contract created, awaiting signature
- CONTRACT_PENDING → ACTIVE: Contract signed
- ACTIVE → SUSPENDED: Compliance issue, training expired
- ACTIVE → EXPIRED: Contract end date passed
- SUSPENDED → ACTIVE: Issue resolved
4.3 Attendee & Registration Domain
4.3.1 Entity Refinements
Attendee (~30 fields, down from 70+)
├── attendee_id (PK, BIGINT with UUID external_id)
├── program_id (FK)
├── registration_site_id (FK)
├── first_name, last_name, title, email
├── npi_number
├── registration_status (ENUM: PENDING, INVITED, ACCEPTED, REGISTERED, CANCELLED, DECLINED, REP_INVITED)
├── reconciliation_status (ENUM: NOT_RECONCILED, RECONCILED, NOT_HCP)
├── sign_in_status (ENUM: NOT_SIGNED_IN, CHECKED_IN, SIGNED_IN, NO_SHOW) -- DEFAULT NOT_SIGNED_IN
├── registration_source (ENUM: OPEN, EMAIL, SALES_REP, PLANNER, WALK_IN, COPY, AUTO, BATCH, MANUAL)
├── attendee_type_id
├── target_id (FK, nullable)
├── created_at, updated_at, deleted (BOOLEAN)
└── external_id (UUID for external systems)
AttendeeSignature (1:1)
├── attendee_id (FK, unique)
├── signature_data (TEXT)
├── signed_at
├── latitude, longitude
└── ip_address
AttendeeAccommodation (1:1)
├── attendee_id (FK, unique)
├── need_sleeping_room (BOOLEAN)
├── check_in_date, check_out_date
├── room_type, special_requests
└── confirmation_number
AttendeeVirtualInfo (1:1)
├── attendee_id (FK, unique)
├── virtual_meeting_id (FK)
├── registrant_id, join_url
├── join_time, leave_time, duration_minutes
└── activities (JSONB - timestamped join/leave events)4.3.2 Registration Status State Machine
PENDING → INVITED ──→ ACCEPTED ──→ REGISTERED
──→ DECLINED
──→ CANCELLED
→ REP_INVITED (same flow as INVITED)
REGISTERED → CANCELLED (before event)4.3.3 Reconciliation Service (Unified)
Current: 3 independent reconciliation paths (NPI, Target, Salesforce)
Target: Unified ReconciliationService with strategy pattern
public interface ReconciliationStrategy {
ReconciliationResult reconcile(Attendee attendee);
}
@Service
public class ReconciliationService {
private final List<ReconciliationStrategy> strategies; // NPI, Target, Salesforce
public ReconciliationResult reconcile(Long attendeeId) {
Attendee attendee = attendeeRepository.findById(attendeeId);
for (ReconciliationStrategy strategy : strategies) {
ReconciliationResult result = strategy.reconcile(attendee);
if (result.isMatch() && result.isUnambiguous()) {
attendee.setReconciliationStatus(RECONCILED);
attendee.setReconciliationSource(result.getSource());
return result;
}
}
return ReconciliationResult.notReconciled();
}
}4.3.4 Frequency Check
- Configurable period: MONTHLY or FISCAL_YEAR
- Topic-based filtering
- Category bypass rules
- Notification email on violation detection
4.4 Budget & Financial Domain
4.4.1 Core Problem
Current: Budget allocation is Geography-First (Region → District → ProgramType). This doesn't match pharmaceutical industry reality where Brand (drug) is the primary budget owner.
4.4.2 Brand-First Budget Model
New Hierarchy:
Tenant Total (FY2026)
├── Brand Master: Keytruda ($5M)
│ ├── East Region ($2M)
│ │ ├── District 1 ($500K)
│ │ │ └── ProgramType: Advisory Board ($200K)
│ │ │ └── ProgramType: Speaker Program ($300K)
│ │ └── District 2 ($500K)
│ ├── West Region ($2M)
│ └── National Reserve ($1M) -- shared pool
├── Brand Master: Welireg ($1M)
│ └── ...
└── Brand Master: Gardasil ($2M)
└── ...4.4.3 New Budget Entities
t_budget_pool (core)
├── pool_id (PK)
├── product_id (FK)
├── pool_name
├── pool_type (ENUM: MASTER, REGIONAL, DISTRICT, RESERVE, SHARED)
├── fiscal_period_id (FK)
├── parent_pool_id (FK, nullable - hierarchical)
├── org_node_id (FK, nullable)
├── brand_id (FK, nullable)
├── program_type_id (FK, nullable)
├── team_id (FK, nullable)
├── initial_amount, additional_amount
├── cap_type (ENUM: AMOUNT, QUANTITY, BOTH)
├── allow_overspend (BOOLEAN)
├── overspend_threshold (DECIMAL)
└── created_at, updated_at
t_fiscal_period
├── period_id (PK)
├── product_id (FK)
├── parent_period_id (FK, nullable)
├── period_type (ENUM: YEAR, HALF, QUARTER, MONTH)
├── period_name (e.g., "FY2026", "Q1 2026", "Jan 2026")
├── start_date, end_date
└── status (ENUM: OPEN, CLOSED)
t_budget_transaction (unified audit trail)
├── transaction_id (PK)
├── source_pool_id (FK)
├── target_pool_id (FK, nullable)
├── transaction_type (ENUM: ALLOCATE, UNALLOCATE, TRANSFER, ADJUST, CARRYOVER)
├── amount
├── status (ENUM: PENDING, APPROVED, REJECTED)
├── approved_by, approved_at
├── comment
└── created_at, created_by
t_budget_consumption (links pool to programs)
├── consumption_id (PK)
├── pool_id (FK)
├── program_id (FK)
├── consumption_type (ENUM: ESTIMATED, COMMITTED, ACTUAL, CANCELLED)
├── amount
└── created_at, updated_at
t_budget_item (retained, per program)
├── budget_item_id (PK)
├── program_id (FK)
├── budget_version (ENUM: SOW, EST, BILL, ACT, CXL)
├── category_id (FK to BudgetCategory)
├── description
├── quantity, unit_cost, tax, gratuity
├── total_amount (computed)
└── status (ENUM: ACTIVE, INACTIVE)
t_budget_change_log
├── id (PK)
├── program_id (FK)
├── old_status, new_status
├── old_budget_version, new_budget_version
├── old_version_total, new_version_total
├── budget_items_snapshot (JSONB)
├── comment
└── created_at, created_by4.4.4 Budget Cap Methods (Simplified)
Current: 5 integer enums mixing two dimensions
Target: Two independent settings
cap_type: AMOUNT | QUANTITY | BOTH
sharing_mode: STRICT (per ProgramType) | SHARED_POOL (flexible across types)4.4.5 Budget Sufficiency Check
When creating a program:
- Find applicable BudgetPool (brand + region/district + program_type)
- Sum existing ESTIMATED + COMMITTED consumptions
- Compare remaining capacity vs new program estimated cost
- If insufficient: WAITLISTED (or reject if
disallowProgramIfBudgetReached)
4.4.6 Backward Compatibility
Database views for legacy API consumers:
CREATE VIEW v_legacy_region_budget AS
SELECT pool_id AS budget_locale_id, 0 AS locale_type, ...
FROM t_budget_pool WHERE pool_type = 'REGIONAL';
CREATE VIEW v_legacy_district_budget AS
SELECT pool_id AS budget_locale_id, 1 AS locale_type, ...
FROM t_budget_pool WHERE pool_type = 'DISTRICT';4.5 Compliance & Regulatory Domain
4.5.1 Core Functions
- Aggregate Spend Reporting: Track total spend per HCP for Sunshine Act
- Transfer of Value (TOV): Calculate per-attendee spend for disclosure
- Document Audit: Compliance checklist per ServiceType
- PLID Matching: Provider Lookup ID verification
4.5.2 TOV Calculation
Preconditions (all must be met):
- Budget Version = ACT (program in RECONCILED status)
- Attendee List = Closed (program past EVENT_COMPLETE)
- TOV flag enabled in product configuration
Formula:
attendeeTOV = totalBudget / headcount
(HALF_UP rounding, 4 decimal places)Manual Adjustment: Planners can override with reason tracking.
4.5.3 Aggregate Spend
- Configurable fields per product (t_aggregate_spend_field_mapping)
- Multiple export formats: Standard, Porzio, Custom
- Real-time query across all programs for an HCP
4.5.4 Document Audit Checklist
t_compliance_checklist
├── id (PK)
├── service_type_id (FK)
├── document_name, description
├── required (BOOLEAN)
├── sequence
└── status (ACTIVE/INACTIVE)
t_program_compliance_document
├── id (PK)
├── program_id (FK)
├── checklist_id (FK)
├── file_id (FK, nullable)
├── status (ENUM: PENDING, UPLOADED, READY_FOR_REVIEW, APPROVED)
├── ready_for_review_time
└── reviewed_by, reviewed_at4.6 User & Permission Domain
4.6.1 Authentication Redesign
Current: In-memory Guava Cache tokens, SSO with insecure cookies, legacy plaintext passwords
Target: Standard JWT with Redis session management
Authentication Flow:
1. User authenticates via Password or SSO/SAML
2. pharmagin-auth issues JWT pair:
- Access Token (RS256, 15-minute expiry)
- Refresh Token (stored in Redis, 7-day expiry)
3. Access Token in Authorization header
4. Refresh Token in HttpOnly/Secure/SameSite cookie
5. Redis stores session metadata for real-time permission updates4.6.2 Unified RBAC Model
Current: Dual role systems (UserRole + ProductRole) at different scopes
Target: Single unified model with scope differentiation
t_role
├── role_id (PK)
├── role_name
├── scope (ENUM: INSTANCE, COMPANY, PRODUCT)
├── description
└── status (ACTIVE/INACTIVE)
t_role_permission
├── role_id (FK)
├── permission (VARCHAR) -- e.g., "PROGRAM_CREATE", "BUDGET_VIEW"
└── PRIMARY KEY (role_id, permission)
t_user_role_assignment
├── id (PK)
├── user_id (FK)
├── role_id (FK)
├── scope_type (ENUM: INSTANCE, COMPANY, PRODUCT)
├── scope_id (INT) -- instance_id, company_id, or product_id
├── org_node_id (FK, nullable) -- geography restriction
└── effective_from, effective_to4.6.3 Predefined Roles
Plannerview:
- ADMIN: Full platform access
- PLANNER: Program management, registration, budgets
Salesview:
- SALES_ADMIN: Full product visibility + user management
- UPPER_MANAGER: All programs + reports
- REGIONAL_MANAGER: Region-scoped data
- DISTRICT_MANAGER: District-scoped data
- SALES_PERSON: Own territory only
Speakerview:
- SPEAKER_ADMIN: Content + contract + training management
- SPEAKER: Profile + contract + training + expense
4.6.4 Permission Enforcement
Every controller method requires @PreAuthorize:
@GetMapping("/programs")
@PreAuthorize("hasPermission('PROGRAM_VIEW')")
public Page<ProgramResponse> listPrograms(...) { ... }
@PostMapping("/programs")
@PreAuthorize("hasPermission('PROGRAM_CREATE')")
public ProgramResponse createProgram(...) { ... }Geography-based data filtering via DataScopeFilter:
@Component
public class DataScopeFilter {
public Predicate<Program> forCurrentUser() {
UserContext ctx = SecurityContextHolder.getContext();
return program -> {
if (ctx.hasRole("SALES_ADMIN") || ctx.hasRole("UPPER_MANAGER")) return true;
if (ctx.hasRole("REGIONAL_MANAGER"))
return ctx.getRegionIds().contains(program.getRegionId());
if (ctx.hasRole("DISTRICT_MANAGER"))
return ctx.getDistrictIds().contains(program.getDistrictId());
// SALES_PERSON: own territory
return ctx.getTerritoryIds().contains(program.getTerritoryId());
};
}
}4.7 Communication & Notification Domain
4.7.1 Unified Notification Model
Current: 5 fragmented alert systems (Alert, CustomAlert, MeetingAlert, BudgetAlert, SpeakerAlert)
Target: Single notification table
t_notification
├── notification_id (PK)
├── notification_type (ENUM: ALERT, REMINDER, SYSTEM, APPROVAL_REQUEST)
├── category (ENUM: PROGRAM, BUDGET, SPEAKER, TRAINING, EXPENSE, COMPLIANCE, CUSTOM)
├── target_user_id (FK)
├── target_role (nullable, for role-based notifications)
├── subject, message
├── reference_type (ENUM: PROGRAM, SPEAKER, ATTENDEE, BUDGET, etc.)
├── reference_id (BIGINT)
├── read (BOOLEAN, default false)
├── read_at
├── created_at
└── expires_at4.7.2 Email Architecture
Reliable Delivery via Message Queue:
Email Request → RabbitMQ → pharmagin-worker → SendGrid API
↓
t_email_log (status tracking)
↓
Retry on failure (3 attempts, exponential backoff)t_email_log
├── id (PK)
├── to_email, from_email, subject
├── template_id (FK, nullable)
├── status (ENUM: QUEUED, SENT, DELIVERED, OPENED, BOUNCED, FAILED)
├── retry_count
├── sendgrid_message_id
├── error_message
├── sent_at, delivered_at
└── created_at4.7.3 Split Mail Table
Current: t_mail mixes communication emails and invitation templates
Target:
t_email_template (for invitation templates)
├── template_id (PK)
├── template_name, subject_template, body_template
├── template_type (ENUM: INVITATION, CONFIRMATION, CANCELLATION, REMINDER, CUSTOM)
├── product_id, service_type_id
├── status (ACTIVE/INACTIVE)
└── created_at, updated_at
t_scheduled_email
├── id (PK)
├── template_id (FK)
├── schedule_config (JSONB - cron expression, send conditions)
├── product_id
├── status (ACTIVE/PAUSED/COMPLETED)
└── next_run_at4.7.4 Template Engine
Extract reusable EmailTemplateEngine with 40+ merge fields:
@Component
public class EmailTemplateEngine {
public String render(String template, Map<String, Object> context) {
// Velocity/Handlebars template rendering
// Context includes: program, speaker, attendee, planner, registrationSite, etc.
}
}4.8 Geographic & Territory Domain
4.8.1 Unified Organization Hierarchy (OrgNode)
Current: 4 hardcoded tables (SalesTeam/SalesForce, Region, District, Territory)
Target: Flexible tree structure
t_org_node
├── node_id (PK)
├── product_id (FK)
├── parent_node_id (FK, nullable)
├── node_type_id (FK to t_org_node_type)
├── node_name
├── node_level (INT, depth from root)
├── node_path (VARCHAR) -- materialized path: "/1/5/12/45/"
├── effective_from (DATE)
├── effective_to (DATE, nullable)
├── metadata (JSONB)
├── status (ENUM: ACTIVE, INACTIVE)
└── created_at, updated_at
t_org_node_type
├── type_id (PK)
├── product_id (FK)
├── type_name (e.g., "Sales Force", "Region", "District", "Territory")
├── type_code (ENUM: SALES_FORCE, REGION, DISTRICT, TERRITORY, CUSTOM)
├── node_level (default level for this type)
├── budget_enabled (BOOLEAN)
├── manager_enabled (BOOLEAN)
├── label_override (VARCHAR, nullable) -- customer-specific label
└── sequence
t_org_node_manager
├── id (PK)
├── node_id (FK)
├── user_id (FK)
├── role (ENUM: MANAGER, PLANNER, ASSISTANT)
├── effective_from, effective_to
└── is_primary (BOOLEAN)Benefits:
- Variable depth (3 layers for small client, 5+ for enterprise)
- Realignment support via effective dates
- budget_enabled flag per type
- Materialized path for fast tree queries:
-- All descendants of Region node_id=5
SELECT * FROM t_org_node WHERE node_path LIKE '/1/5/%';
-- All ancestors of Territory node_id=45
SELECT * FROM t_org_node WHERE '/1/5/12/45/' LIKE node_path || '%';4.8.2 Planner Assignment
- Planner assigned per OrgNode
- Cascades downward (Territory inherits from District if not explicitly set)
- Lookup:
findPlannerForNode(nodeId)walks up tree until planner found
4.8.3 Naming Consolidation
| Current | Target |
|---|---|
| t_sales_team | t_org_node (type=SALES_FORCE) |
| t_region | t_org_node (type=REGION) |
| t_district | t_org_node (type=DISTRICT) |
| t_territory | t_org_node (type=TERRITORY) |
| SalesTeam/SalesForce confusion | Unified as OrgNode |
| sales_team_id / sales_force_id dual fields | Single org_node_id |
4.9 Reporting & Analytics Domain
4.9.1 Unified Report Framework
Current: 4 completely different mechanisms (standard, ad-hoc, R/Python scripts, data extract)
Target: Unified ReportEngine
ReportEngine
├── DataSourceProvider (SQL query builders with permission filtering)
├── ReportDefinition (DB-stored report configs)
├── FieldRegistry (DB-stored field definitions, replaces Java static Map)
├── ExportFormatter (Excel, CSV, PDF via unified interface)
└── AsyncExecutor (large reports via message queue)4.9.2 Report Security
- Eliminate: ProcessBuilder script execution (R/Python) - replace with JasperReports or Java-based templates
- Eliminate: Raw SQL data extracts - all queries go through permission filter
- Add:
ReportPermissionFilterthat applies Product/Region/District scoping to all queries
4.9.3 Async Report Generation
For reports taking >5 seconds:
Request → Create ReportJob (status=QUEUED) → Message Queue
→ pharmagin-worker executes query → Generate file → Upload to S3
→ Update ReportJob (status=COMPLETED, fileUrl=...)
→ Notify user (notification + email)4.9.4 Ad-Hoc Report Builder
Migrate field storage from Java static Map to database:
t_report_field
├── field_id (PK)
├── field_name, field_label
├── data_source (table/join path)
├── field_type (TEXT, NUMBER, DATE, BOOLEAN)
├── aggregatable (BOOLEAN)
├── filterable (BOOLEAN)
├── default_visible (BOOLEAN)
└── sequence4.10 Configuration & Feature Management Domain
4.10.1 Feature Flag Migration
Current: 165+ flags in YAML (require restart), scattered naming
Target: Database-driven with runtime toggle
t_feature_flag
├── flag_id (PK)
├── flag_key (VARCHAR, unique) -- e.g., "program.approvalEnabled"
├── flag_name (display name)
├── scope (ENUM: AGENCY, COMPANY, PRODUCT)
├── scope_id (INT) -- agency_id, company_id, or product_id
├── enabled (BOOLEAN)
├── description
├── category (VARCHAR) -- e.g., "program", "registration", "speaker"
├── updated_by, updated_at
└── created_at
-- API: GET /api/v2/admin/feature-flags?scope=PRODUCT&scopeId=1
-- API: PUT /api/v2/admin/feature-flags/{id} { enabled: true }4.10.2 Naming Convention
All flags use positive naming: xxxEnabled
| Current | Target |
|---|---|
disableSurveyAlerts | surveyAlertsEnabled |
disablePresentationDelete | presentationDeleteEnabled |
navigation.disableTopics | navigation.topicsEnabled |
navigation.disableTraining | navigation.trainingEnabled |
4.10.3 Configuration Sources (Priority)
1. Database t_feature_flag (highest priority, runtime)
2. Database t_product.product_config (product-specific overrides)
3. YAML via Spring Cloud Config (defaults/fallback)4.10.4 Public API Safety
Separate endpoints with DTO filtering:
/api/v2/public/config → Returns ONLY branding, UI labels, navigation flags
/api/v2/admin/config → Returns full config (requires ADMIN role)Never expose: API keys, SFTP passwords, SendGrid credentials, internal URLs.
4.10.5 Fiscal Period
Dynamic generation replacing hardcoded 2025:
public List<FiscalPeriod> getAvailablePeriods(int productId) {
int currentYear = Year.now().getValue();
return generatePeriods(currentYear - 5, currentYear + 2); // Rolling window
}4.11 Content & Document Domain
4.11.1 Unified Asset Model
Current: Content + Document with different schemas, magic numbers for types
Target:
t_asset
├── asset_id (PK)
├── asset_type (ENUM: PRESENTATION, MODULE, CASE_STUDY, DOCUMENT, TEMPLATE, OTHER)
├── asset_name, description
├── file_id (FK to t_file)
├── company_id, product_id
├── status (ENUM: ACTIVE, INACTIVE, ARCHIVED)
├── deleted (BOOLEAN)
└── created_at, updated_at, created_by
t_asset_group (N:N)
├── asset_id (FK)
└── speaker_group_id (FK)
t_asset_topic (N:N)
├── asset_id (FK)
└── topic_id (FK)4.11.2 File Storage Migration
Current: Local filesystem with MD5 deduplication
Target: S3/MinIO with SHA-256
t_file
├── file_id (PK)
├── file_name (original name)
├── file_path (S3 key)
├── file_size, content_type
├── checksum (SHA-256)
├── storage_backend (ENUM: LOCAL, S3)
└── created_at4.11.3 Document Template Service
Split into focused services:
TemplateContextBuilder -- Builds merge field context from Program/Speaker/Attendee
DocumentRenderer -- XDocReport + Velocity rendering
FileStorageService -- S3/MinIO upload/download
PdfConversionService -- Configurable strategy (Docx4J for simple, CloudConvert for complex)4.11.4 Security Fixes
- Fix SQL injection in FileService (parameterized queries)
- File upload validation: Whitelist extensions, size limits, content-type verification
- Remove filesystem directory listing endpoint
- Auth on document downloads: Require valid session, not just attendeeId
4.12 Survey & Feedback Domain
4.12.1 Unified Survey System
Current: Backend dynamic templates + hardcoded Speaker Survey (per companyId in frontend)
Target: All surveys use backend dynamic template system
t_survey
├── survey_id (PK)
├── survey_name
├── survey_type (ENUM: ATTENDEE, SPEAKER, SALES_REP, ATTENDEE_IN_PROGRAM)
├── content (JSONB with strong-typed DTO)
├── status (ENUM: DRAFT, ACTIVE, INACTIVE)
├── deleted (BOOLEAN)
└── created_at, updated_at
t_survey_product (N:N, replaces JSONB products array)
├── survey_id (FK)
├── product_id (FK)
└── program_type_id (FK, nullable)
t_survey_response
├── response_id (PK) -- ADD primary key!
├── survey_id (FK)
├── attendee_id (FK, nullable)
├── program_id (FK)
├── respondent_user_id (FK)
├── response_data (JSONB with typed DTO)
└── created_at4.12.2 Cleanup
- Remove SurveyMigrationLog and migration API
- Remove hardcoded Speaker Survey from speakerview
- Migrate all company-specific surveys to database templates
4.13 Expense Management Domain
4.13.1 Expense State Machine
DRAFT → PENDING_APPROVED → APPROVED
→ REJECTED → DRAFT (resubmit)4.13.2 Key Changes
t_expense
├── expense_id (PK)
├── program_id (FK)
├── speaker_id (FK)
├── status (ENUM: DRAFT, PENDING_APPROVED, APPROVED, REJECTED)
├── total_amount (COMPUTED from items, not frontend-supplied)
├── approved_amount, reject_reason
├── receipt_file_id (FK to t_file) -- renamed from field_id
├── check_number, issue_date
├── deleted (BOOLEAN) -- SOFT delete, not hard
└── created_at, updated_at
t_expense_item
├── expense_item_id (PK)
├── expense_id (FK)
├── category_id (FK)
├── vendor, description
├── amount, currency
├── mileage (nullable)
├── receipt_file_id (FK to t_file) -- use File module, not base64
└── created_at4.13.3 Key Fixes
- Soft delete (replace hard delete)
- Auto-calculate total:
expense.total_amount = SUM(expense_item.amount) - File upload via File Module: Not inline base64
- Use JaVers for audit trail (replace ExpenseHistory full-record copy)
- Move Planner approval UI to Plannerview (currently in Speakerview)
4.14 Virtual Program Domain
4.14.1 Independent Storage
Current: Virtual meeting info in JSONB fields on MeetingRequest and Attendee
Target: Independent tables
t_virtual_meeting
├── virtual_meeting_id (PK)
├── program_id (FK)
├── service_type (ENUM: ZOOM_MEETING, ZOOM_WEBINAR, THIRD_PARTY)
├── external_meeting_id (VARCHAR) -- Zoom meeting ID
├── host_email, topic
├── start_url, join_url, password
├── start_time, duration, timezone
├── status (ENUM: SCHEDULED, IN_PROGRESS, ENDED, CANCELLED)
└── created_at, updated_at
t_virtual_meeting_attendee
├── id (PK)
├── virtual_meeting_id (FK)
├── attendee_id (FK)
├── registrant_id (VARCHAR)
├── join_url (attendee-specific)
├── status (ENUM: REGISTERED, JOINED, LEFT, NO_SHOW)
├── join_time, leave_time, duration_minutes
└── activities (JSONB - timestamped events)4.14.2 Key Fixes
- Split VirtualProgramServiceFactory: Pure Factory + VirtualProgramOrchestrationService
- Per-product token caching:
ConcurrentHashMap<Integer, CachedToken>instead of global - Remove legacy JWT signature (generateZoomSignature)
- Merge 6 webhook endpoints into 2 (/meeting, /webinar) with event type dispatch
- Implement Third-Party type: Basic URL/password manual entry
4.15 Integration Domain
4.15.1 Unified Integration Client
@Component
public class IntegrationClient {
// Resilience4j decorators
private final CircuitBreaker circuitBreaker;
private final Retry retry;
private final RateLimiter rateLimiter;
private final TimeLimiter timeLimiter;
public <T> T execute(String integrationName, Supplier<T> call) {
return Decorators.ofSupplier(call)
.withCircuitBreaker(circuitBreaker)
.withRetry(retry)
.withRateLimiter(rateLimiter)
.withTimeLimiter(timeLimiter)
.decorate()
.get();
}
}4.15.2 Integration Improvements
| Integration | Current | Target |
|---|---|---|
| Salesforce | SOAP Partner API, string concatenation SOQL | REST API, parameterized queries, per-product connections |
| Zoom | Global token cache, 6 webhook endpoints | Per-product tokens, 2 webhook endpoints |
| SendGrid | Incremental sync, no retry | Message queue with retry + status tracking |
| SFTP | PromiscuousVerifier, password auth | Host key verification, SSH key auth, connection pooling |
| CloudConvert | Synchronous, exception swallowing | Async with callback, proper error handling |
| Google Maps | Self-built RestTemplate | WebClient with connection pooling |
| SSO | Dual services (pharmagin-sso + pharmagin-login) | Merged into pharmagin-auth with Spring Security SAML2 |
4.15.3 Credential Management
Current: All credentials in plaintext YAML
Target: AWS Secrets Manager or HashiCorp Vault
# application.yml
integrations:
salesforce:
credentials: ${SALESFORCE_CREDENTIALS_SECRET_ARN}
zoom:
credentials: ${ZOOM_CREDENTIALS_SECRET_ARN}
sendgrid:
api-key: ${SENDGRID_API_KEY_SECRET_ARN}5. Unified Data Model
5.1 Naming Conventions
| Rule | Convention |
|---|---|
| Table prefix | t_ |
| Primary key | {entity}_id (BIGINT, auto-increment) |
| Foreign key | {referenced_entity}_id |
| Boolean fields | is_xxx or positive verb (e.g., deleted, approved) |
| Timestamps | created_at, updated_at (TIMESTAMPTZ) |
| Audit fields | created_by, updated_by (user_id reference) |
| Soft delete | deleted (BOOLEAN, DEFAULT false) -- consistent everywhere |
| Status fields | PostgreSQL ENUM types or VARCHAR with CHECK constraint |
| JSON fields | Strong-typed DTO in Java, JSONB in PostgreSQL |
5.2 Entity Relationship Summary
Product (Tenant)
├── Brand (hierarchy: BU → Brand → Indication)
│ ├── BudgetPool (Brand-First allocation)
│ ├── SpeakerContract (per Brand)
│ └── ProgramType (N:N via BrandProgramType)
│ └── ServiceType
│ └── Program (core lifecycle entity)
│ ├── ProgramLocation, ProgramRequestor, ProgramFinancial
│ ├── ProgramSpeaker (1:N, dynamic)
│ ├── ProgramApproval (1:N, structured)
│ ├── RegistrationSite
│ │ └── Attendee
│ │ ├── AttendeeSignature, AttendeeAccommodation
│ │ └── AttendeeVirtualInfo
│ ├── BudgetItem (per version)
│ ├── BudgetChangeLog
│ ├── Expense → ExpenseItem
│ ├── ProgramComplianceDocument
│ ├── ProgramTask
│ ├── ProgramChangeLog
│ └── VirtualMeeting → VirtualMeetingAttendee
├── OrgNode (flexible hierarchy)
│ └── OrgNodeManager
├── Team (cross-geography)
│ ├── TeamBrand (N:N)
│ └── TeamProgramType (N:N)
├── Speaker
│ ├── SpeakerAddress (1:N)
│ ├── SpeakerFinancial (1:1)
│ ├── SpeakerCredential (1:N)
│ ├── SpeakerProfile (1:1)
│ └── SpeakerContract → ContractHonorariaItem
└── Configuration
├── FeatureFlag
├── FiscalPeriod (hierarchy)
└── Theme (branding)5.3 ID Strategy
| Entity | Current | Target |
|---|---|---|
| Most entities | Integer IDENTITY | BIGINT IDENTITY |
| Attendee | UUID String | BIGINT IDENTITY + UUID external_id |
| Target | Long | BIGINT |
| All new entities | N/A | BIGINT IDENTITY |
5.4 JSONB Fields (Typed)
All JSONB fields must have corresponding Java DTOs:
| Entity | JSONB Field | Required DTO |
|---|---|---|
| ProgramApproval | (eliminated) | Replaced by structured table |
| Survey.content | content | SurveyContentDTO (Page → Field → Option) |
| SurveyResponse.response_data | response | SurveyResponseDTO |
| ScheduledEmail.schedule_config | config | ScheduleConfigDTO |
| VirtualMeetingAttendee.activities | activities | List<ActivityEventDTO> |
| OrgNode.metadata | metadata | Map<String, Object> (extensible) |
6. API Architecture
6.1 API Design Principles
- Unified namespace:
/api/v2/{domain}/{resource} - REST semantics: GET (read), POST (create), PUT (full update), PATCH (partial/state change), DELETE
- Pagination: Cursor-based for lists,
?page=1&size=20&sort=createdAt,desc - Error format: RFC 7807 Problem Details
- Auth: Bearer JWT in Authorization header
- Versioning: URL path (
/v2/)
6.2 API Namespace Map
| Domain | Base Path | Key Endpoints |
|---|---|---|
| Program | /api/v2/programs | CRUD, status transitions, close-out readiness |
| Speaker | /api/v2/speakers | CRUD, contracts, training, presentations |
| Attendee | /api/v2/programs/{id}/attendees | Registration, reconciliation, sign-in |
| Budget | /api/v2/budgets | Pools, allocations, transactions, items |
| Compliance | /api/v2/compliance | TOV, aggregate spend, document audit |
| User | /api/v2/users | CRUD, roles, permissions |
| Auth | /api/v2/auth | Login, SSO, token refresh, logout |
| Communication | /api/v2/notifications | Alerts, emails, templates |
| Geography | /api/v2/org-nodes | Hierarchy CRUD, planner assignment |
| Config | /api/v2/config | Feature flags, dictionaries |
| Content | /api/v2/assets | Files, templates, presentations |
| Survey | /api/v2/surveys | Templates, responses, statistics |
| Expense | /api/v2/expenses | Reports, items, approval |
| Virtual | /api/v2/virtual-meetings | CRUD, webhooks |
| Report | /api/v2/reports | Standard, ad-hoc, export |
6.3 Program API Examples
# Lifecycle
POST /api/v2/programs Create program
GET /api/v2/programs/{id} Get program detail
PUT /api/v2/programs/{id} Update program
PATCH /api/v2/programs/{id}/status Change status (state machine)
GET /api/v2/programs/{id}/close-out-readiness Close-out checklist
DELETE /api/v2/programs/{id} Soft delete
# Sub-resources
GET /api/v2/programs/{id}/speakers List assigned speakers
POST /api/v2/programs/{id}/speakers Assign speaker
GET /api/v2/programs/{id}/attendees List attendees
POST /api/v2/programs/{id}/attendees Add attendee
GET /api/v2/programs/{id}/budget-items List budget items
GET /api/v2/programs/{id}/change-log View change log
GET /api/v2/programs/{id}/compliance Compliance checklist
GET /api/v2/programs/{id}/tasks Project tasks6.4 Status Change API
Single command endpoint for all status transitions:
PATCH /api/v2/programs/{id}/status
{
"targetStatus": "REGISTRATION_CLOSED",
"comment": "Registration deadline reached",
"force": false
}
Response:
{
"programId": 123,
"previousStatus": "REGISTRATION_OPEN",
"currentStatus": "REGISTRATION_CLOSED",
"budgetVersion": "BILL",
"timestamp": "2026-02-21T10:30:00Z",
"changeLogId": 456
}
Error Response (guard violation):
{
"type": "phase-guard-violation",
"title": "Status transition not allowed",
"status": 409,
"detail": "Cannot transition from REGISTRATION_OPEN to CLOSED",
"blockers": [
"Attendee list not closed",
"TOV not calculated"
]
}7. Frontend Architecture
7.1 Technology Migration
| Aspect | Current | Target |
|---|---|---|
| Language | JavaScript | TypeScript |
| React | 15-16.x | 18+ |
| Router | React Router 3-4.x | React Router 6+ |
| UI Library | Ant Design 3.x | Ant Design 5.x |
| State | Redux (manual boilerplate) | Redux Toolkit + RTK Query |
| Build | CRACO | Vite |
| Micro-frontend | Single-SPA (SystemJS) | Single-SPA (ES modules) or Module Federation |
| CSS | CSS/LESS | CSS Modules / Tailwind |
7.2 Shared Component Library
Create @pharmagin/ui shared package:
@pharmagin/ui
├── components/
│ ├── ProgramTable -- Reusable program listing
│ ├── AttendeeTable -- Reusable attendee listing
│ ├── BudgetSummary -- Budget display widget
│ ├── StatusBadge -- Program/Speaker status badges
│ ├── SurveyRenderer -- Unified survey form renderer
│ └── FileUpload -- Standardized file upload
├── hooks/
│ ├── useAuth -- Authentication state
│ ├── usePermission -- Permission checking
│ ├── useFeatureFlag -- Feature flag queries
│ └── useNotification -- Notification system
├── api/
│ ├── apiClient -- Axios/fetch wrapper with JWT
│ └── endpoints/ -- RTK Query endpoint definitions
└── types/
├── program.ts -- Generated from Java DTOs
├── speaker.ts
├── attendee.ts
└── config.ts7.3 Unified "Program" Terminology
| Current (inconsistent) | Target |
|---|---|
| Plannerview: "Meeting" | Program |
| Salesview: "Program" | Program |
| Speakerview: "Meeting" | Program |
| Code: MeetingRequest | Program |
| Code: Meeting | Registration Site |
7.4 Frontend Structure
pharmagin-plannerview/
├── root-config/ # Single-SPA orchestrator
└── apps/
└── planner/ # Main planner app (TypeScript, Vite)
├── pages/
│ ├── Programs/ # Program listing + detail
│ ├── Speakers/ # Speaker management
│ ├── Budget/ # Budget allocation
│ ├── Reports/ # Reporting
│ ├── Configuration/ # Feature flags + dictionaries
│ └── Users/ # User management
└── shared/ # Shared within planner
pharmagin-speakerview/
├── root-config/
└── apps/
└── speaker/
├── pages/
│ ├── Dashboard/
│ ├── Profile/
│ ├── Contracts/
│ ├── Training/
│ ├── Presentations/
│ ├── Expenses/
│ └── Surveys/
└── shared/
pharmagin-salesview/
├── pages/
│ ├── Programs/
│ ├── Speakers/
│ ├── Reports/
│ └── Settings/
└── shared/8. Security Architecture
8.1 Authentication
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Client │────→│ pharmagin- │────→│ Redis │
│ (Browser) │←────│ auth │ │ (Session) │
└──────────┘ └──────────────┘ └──────────┘
│ │
│ Access Token (15min) │ Refresh Token (7 days)
│ in Authorization header │ HttpOnly/Secure cookie
│ │
▼ │
┌──────────┐ │
│ pharmagin│ Validates JWT signature │
│ -web │ Checks claims (iss, aud, exp) │
│ │ Loads permissions from Redis │
└──────────┘8.2 Critical Security Fixes
| Issue | Fix |
|---|---|
| SQL Injection (4 locations) | Parameterized queries everywhere |
| RCE (ProcessBuilder) | Remove script execution, use JasperReports |
| API Key Exposure | Separate public/admin config APIs |
| Data Extract unfiltered | Remove; all queries through permission filter |
| SSO Cookie insecure | HttpOnly, Secure, SameSite=Strict |
| Plaintext passwords | Remove legacy comparison, BCrypt only |
| JWT claim validation | Validate iss, aud, sub, exp, iat |
| Unprotected Controllers | @PreAuthorize on every endpoint |
| SFTP no host key | Enable StrictHostKeyChecking |
| XSS (dangerouslySetInnerHTML) | Sanitize HTML, use DOMPurify |
| File directory listing | Remove DocXManagementController endpoint |
| Unauthenticated document URL | Require valid session |
8.3 Input Validation
@PostMapping("/programs")
public ProgramResponse createProgram(
@Valid @RequestBody CreateProgramRequest request) { ... }
public class CreateProgramRequest {
@NotBlank @Size(max = 200)
private String programName;
@NotNull @Positive
private Long programTypeId;
@NotNull
private LocalDateTime meetingStartTime;
@Size(max = 5000)
private String notes;
// No raw SQL, no file paths, no script content
}8.4 File Upload Security
@Component
public class FileUploadValidator {
private static final Set<String> ALLOWED_EXTENSIONS =
Set.of("pdf", "doc", "docx", "xls", "xlsx", "png", "jpg", "jpeg", "gif");
private static final long MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
public void validate(MultipartFile file) {
// 1. Check extension whitelist
// 2. Check file size
// 3. Verify content-type matches extension
// 4. Scan for malicious content (optional: ClamAV integration)
}
}9. Infrastructure & DevOps
9.1 Service Dependencies
pharmagin-web ──→ PostgreSQL 15+
──→ Redis 7+ (cache, sessions, feature flags)
──→ S3/MinIO (file storage)
──→ RabbitMQ (async tasks)
pharmagin-auth ──→ PostgreSQL (user/auth tables)
──→ Redis (tokens, sessions)
pharmagin-worker ──→ PostgreSQL (read)
──→ RabbitMQ (consume)
──→ S3/MinIO (report output)
──→ External APIs (SendGrid, Salesforce, Zoom, SFTP)
pharmagin-config ──→ Git repository (YAML files)9.2 Observability
Application → Micrometer Metrics → Prometheus → Grafana (dashboards)
→ Structured Logging (JSON) → ELK/Loki (log aggregation)
→ Spring Actuator → Health Checks
Every request:
- MDC correlation ID (X-Request-ID)
- User ID, Product ID in log context
- External integration call duration metrics
- Database query timing9.3 Health Checks
/actuator/health
├── db: PostgreSQL connectivity
├── redis: Redis connectivity
├── s3: S3/MinIO connectivity
├── rabbitmq: RabbitMQ connectivity
├── diskSpace: Local disk availability
└── custom:
├── salesforce: Last successful sync timestamp
├── zoom: Token refresh status
└── sendgrid: API availability10. Migration Strategy
10.1 Phase Overview
| Phase | Duration | Focus | Risk |
|---|---|---|---|
| Phase 0 | 2-4 weeks | Security hardening (no schema changes) | LOW |
| Phase 1 | 4-6 weeks | Backend foundation (auth, state machine, guard) | MEDIUM |
| Phase 2 | 6-8 weeks | Data model migration (entity decomposition) | HIGH |
| Phase 3 | 4-6 weeks | Budget & Organization redesign | HIGH |
| Phase 4 | 6-8 weeks | Frontend rewrite | MEDIUM |
| Phase 5 | 4-6 weeks | Integration modernization | MEDIUM |
| Phase 6 | 2-4 weeks | Reporting & Analytics | LOW |
10.2 Phase 0: Security Hardening (Immediate)
No schema changes. Minimal risk.
- Fix SQL injection (4 locations) - parameterize queries
- Remove ProcessBuilder script execution
- Add @PreAuthorize to all unprotected controllers (8 controllers)
- Separate public config API - exclude credentials
- Fix SSO Cookie flags - HttpOnly, Secure, SameSite
- Remove legacy plaintext password comparison
- Validate JWT claims properly
- Enable SFTP host key verification
- Sanitize HTML in Speakerview
10.3 Phase 1: Backend Foundation
JWT Authentication (pharmagin-auth service)
- Implement RS256 JWT issuance
- Redis session storage
- Refresh token rotation
- Merge pharmagin-sso + pharmagin-login
Program State Machine
- Create ProgramStatus enum with 14 states
- Implement ProgramStateMachine with legal transitions
- Create ProgramPhaseGuard with operation matrix
- Add guard checks to all write service methods
Budget Version Derivation
- Make budgetVersionId a computed property
- Create t_budget_change_log table
- Refactor BudgetItemService.copy() → ProgramService.advancePhase()
10.4 Phase 2: Data Model Migration
Entity Decomposition (parallel tracks):
- MeetingRequest (98 fields) → Program + ProgramLocation + ProgramRequestor + ProgramSpeaker + ProgramFinancial + ProgramApproval + ProgramCustomField
- Speaker (88 fields) → Speaker + SpeakerAddress + SpeakerFinancial + SpeakerCredential + SpeakerProfile
- Meeting (77 fields) → RegistrationSite (simplified)
Data Migration Scripts:
-- Example: Extract ProgramLocation from MeetingRequest
INSERT INTO t_program_location (program_id, venue, city, state, postal_code, ...)
SELECT meeting_request_id, venue, city, state, postal_code, ...
FROM t_meeting_request;
-- Example: Extract ProgramSpeaker (from hardcoded 3 slots to 1:N)
INSERT INTO t_program_speaker (program_id, speaker_id, role, sequence)
SELECT meeting_request_id, first_speaker, 'PRIMARY', 1
FROM t_meeting_request WHERE first_speaker IS NOT NULL
UNION ALL
SELECT meeting_request_id, second_speaker, 'SECONDARY', 2
FROM t_meeting_request WHERE second_speaker IS NOT NULL
UNION ALL
SELECT meeting_request_id, third_speaker, 'TERTIARY', 3
FROM t_meeting_request WHERE third_speaker IS NOT NULL;- Status Migration (15 old → 14 new):
-- Mapping rules
UPDATE t_program SET status = CASE
WHEN old_status IN (0, 5, 9) THEN 'DRAFT' -- Assigned, Estimate, Registered
WHEN old_status = 11 THEN 'WAITLISTED'
WHEN old_status = 12 THEN 'PENDING_APPROVAL'
WHEN old_status = 13 THEN 'DENIED'
WHEN old_status = 6 THEN
CASE
WHEN reg_site_status = 1 THEN 'REGISTRATION_OPEN'
WHEN reg_site_status = 3 AND attendee_list_status = 0 THEN 'EVENT_COMPLETE'
WHEN reg_site_status = 3 THEN 'REGISTRATION_CLOSED'
ELSE 'PLANNING'
END
WHEN old_status = 1 THEN 'REGISTRATION_CLOSED' -- Billing
WHEN old_status = 4 THEN 'CLOSED'
WHEN old_status IN (2, 3) THEN 'CANCELLED' -- Cancelled + Cancelled Closed
WHEN old_status IN (7, 8) THEN 'POSTPONED' -- Postponed + Postponed Closed
WHEN old_status = 10 THEN 'VOID'
WHEN old_status = 14 THEN 'REOPENED'
END;- Backward Compatibility Views:
-- Legacy API can read old format
CREATE VIEW v_legacy_meeting_request AS
SELECT p.program_id AS meeting_request_id,
p.status AS meeting_status,
p.status.deriveBudgetVersion() AS budget_version_id,
rs.status AS reg_site_status,
...
FROM t_program p
JOIN t_registration_site rs ON p.registration_site_id = rs.id;10.5 Phase 3: Budget & Organization Redesign
OrgNode Tree (parallel to existing geography):
- Create t_org_node, t_org_node_type, t_org_node_manager
- Populate from existing Region/District/Territory tables
- Dual-write period for transition
Budget Pool Model:
- Create t_budget_pool, t_fiscal_period, t_budget_transaction, t_budget_consumption
- Populate Brand Master pools from existing brand data
- Migrate existing budget_cap_locale → budget_pool
- Legacy views for old API
Brand Repositioning:
- Enhance t_brand with hierarchy (BU → Brand → Indication)
- Create t_brand_program_type (N:N)
- Migrate 1:N brand-program-type to N:N
10.6 Phase 4: Frontend Rewrite
- Create shared library
@pharmagin/uifirst - Migrate Salesview (standalone React SPA, simplest)
- Migrate Speakerview (medium complexity)
- Migrate Plannerview (most complex, last)
Each migration:
- TypeScript + Vite + React 18 + Ant Design 5 + RTK Query
- Unified "Program" terminology
- Shared authentication, API client, feature flags
10.7 Phase 5: Integration Modernization
- Salesforce: SOAP → REST API, parameterized queries
- Email: Direct SendGrid → RabbitMQ → Worker → SendGrid (with retry)
- Zoom: Per-product tokens, merged webhook endpoints
- SFTP: SSH key auth, connection pooling, host key verification
- Credentials: YAML → Secrets Manager
10.8 Phase 6: Reporting & Analytics
- Unified ReportEngine with permission filtering
- Remove ProcessBuilder scripts (already done in Phase 0)
- Async report generation via message queue
- Field config migration from Java static Map to database
11. Implementation Roadmap
11.1 Priority Matrix
| Priority | Items | Rationale |
|---|---|---|
| P0 | Security hardening (Phase 0) | Critical vulnerabilities |
| P0 | JWT auth + state machine (Phase 1) | Foundation for everything |
| P0 | Program entity decomposition (Phase 2) | Unblocks all domain work |
| P1 | Budget Pool + Brand-First (Phase 3) | Core business requirement |
| P1 | OrgNode tree (Phase 3) | Enables flexible geography |
| P1 | Frontend shared library (Phase 4) | Unblocks frontend migration |
| P2 | Frontend migration (Phase 4) | Technology modernization |
| P2 | Integration modernization (Phase 5) | Reliability improvement |
| P2 | Reporting unification (Phase 6) | Operational improvement |
| P3 | Feature flag runtime toggle | Developer experience |
| P3 | Observability stack | Operations improvement |
11.2 Success Metrics
| Metric | Current | Target |
|---|---|---|
| Critical security vulnerabilities | 18 | 0 |
| God classes (>600 LOC) | 12 | 0 |
| God entities (>70 fields) | 4 | 0 |
| Unprotected API endpoints | 80+ | 0 |
| Status transition bugs possible | Many (no state machine) | 0 (formal state machine) |
| Feature flag change deployment | Restart required | Runtime toggle |
| File storage scalability | Single server | Distributed (S3) |
| Auth scalability | Single instance (Guava) | Horizontal (Redis + JWT) |
| Frontend build time | CRACO (~60s) | Vite (~5s) |
| TypeScript coverage | 0% | 100% |
| Test coverage | Unknown/Low | >80% |
11.3 Risk Mitigation
| Risk | Mitigation |
|---|---|
| Data loss during migration | Full database backup before each phase; migration scripts tested on staging |
| API breaking changes | Backward-compatible views + /v2/ namespace coexistence |
| Frontend regression | Component-by-component migration; E2E tests before/after |
| Performance degradation | Load testing after each phase; query analysis |
| Integration disruption | Feature flags for new integration paths; gradual rollout |
| Team knowledge gap | Document each phase; code review gates |
Appendix A: Complete Entity Count (Target)
| Domain | Entities | Key Tables |
|---|---|---|
| Program | 10 | program, program_location, program_requestor, program_speaker, program_financial, program_approval, program_custom_field, registration_site, program_change_log, program_task |
| Speaker | 6 | speaker, speaker_address, speaker_financial, speaker_credential, speaker_profile, speaker_contract + contract_honoraria_item + contract_territory |
| Attendee | 4 | attendee, attendee_signature, attendee_accommodation, attendee_virtual_info |
| Budget | 6 | budget_pool, fiscal_period, budget_transaction, budget_consumption, budget_item, budget_change_log |
| Compliance | 3 | compliance_checklist, program_compliance_document, aggregate_spend_config |
| User | 5 | unified_user, role, role_permission, user_role_assignment, user_password_history |
| Communication | 5 | notification, email_log, email_template, scheduled_email, unsubscribed_email |
| Geography | 3 | org_node, org_node_type, org_node_manager |
| Config | 3 | feature_flag, theme, dictionary_entry |
| Content | 4 | asset, asset_group, asset_topic, file |
| Survey | 3 | survey, survey_product, survey_response |
| Expense | 3 | expense, expense_item, expense_category |
| Virtual | 2 | virtual_meeting, virtual_meeting_attendee |
| Reporting | 2 | report_field, report_job |
| Total | ~59 | Down from 138+ (consolidation + decomposition) |
Appendix B: Removed/Deprecated Features
The following features have been removed and should NOT be referenced in new code:
- PharmaginConnect (PhysicianView) - mobile app
- PVM (Agora-based video, type 4)
- Firebase - backend SDK, FCM push
- Fee for Service contracts
- Lantheus speaker custom fields
- Plus Package v2 endpoints
- Sales Budget / Travel Form / Program Objective flags
- Activities feature
- ProgramFieldConfiguration (5 fields)
- QIMS (reconciliation type 5)
- Contract Type / MSA Notification flags
- MedPro (reconciliation type 6)
- Online Training / Video flags
- Presentation Manager flags
- site-builder module (plannerview)
Appendix C: Glossary
| Term | Definition |
|---|---|
| Program | A speaker engagement event (formerly "MeetingRequest") |
| Registration Site | The registration portal for a program (formerly "Meeting") |
| Brand | A pharmaceutical drug brand (e.g., Keytruda); primary budget dimension |
| TOV | Transfer of Value - per-attendee spend calculation for compliance |
| HCP | Healthcare Professional - a speaker or attendee |
| NPI | National Provider Identifier - unique HCP identifier |
| PLID | Provider Lookup ID - compliance verification |
| SOW | Statement of Work - initial budget version |
| EST | Estimate - planning stage budget version |
| BILL | Billing - execution stage budget version |
| ACT | Actual - final recorded budget version |
| CXL | Cancellation - cancelled/postponed budget version |
| OrgNode | Organization Node - flexible hierarchy element (replaces fixed Region/District/Territory) |
| BudgetPool | A budget allocation container in the Brand-First hierarchy |
| PhaseGuard | Backend component enforcing lifecycle-based operation permissions |