Skip to content

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

  1. Executive Summary
  2. Current System Problems
  3. Target Architecture Overview
  4. 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
  5. Unified Data Model
  6. API Architecture
  7. Frontend Architecture
  8. Security Architecture
  9. Infrastructure & DevOps
  10. Migration Strategy
  11. 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

MetricCount
Backend modules37
Database entities138+
Frontend applications3 (Plannerview, Salesview, Speakerview)
External integrations12
Feature flags165+
API endpoints~200+
Business rules123+
Identified issues332

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

  1. Security First: Eliminate all 18 critical vulnerabilities; RBAC on every endpoint
  2. Domain-Driven Design: Decompose god entities into bounded contexts
  3. Lifecycle State Machine: Formal state machine with progressive locking
  4. Brand-First Budget: Budget allocation driven by brand (drug), not geography alone
  5. Unified Organization Hierarchy: Replace hardcoded 4-level geography with flexible OrgNode tree
  6. Modern Stack: Spring Boot 3.x, React 18+, TypeScript, Vite
  7. Horizontal Scalability: Stateless JWT, distributed cache, object storage

2. Current System Problems

2.1 Critical Security Issues (P0)

IDIssueLocationRisk
S-01SQL InjectionSiteController, FileService, ExpenseMapper, Salesforce SOQLData breach
S-02Remote Code ExecutionCustom Reports ProcessBuilderFull server compromise
S-03API Key Exposure/v1/public/configuration returns SendGrid/SFTP/API keysCredential theft
S-04Unfiltered Data ExtractRaw SQL execution bypassing permission filtersData exfiltration
S-05SSO Cookie InsecureMissing HttpOnly/Secure/SameSite flagsSession hijacking
S-06Legacy Plaintext PasswordOld password comparison still activeAccount compromise
S-07JWT Claim Not ValidatedExternal-Authorization lacks audience/expiry checksToken spoofing
S-088 Controllers UnprotectedUser, Role, Geography, SalesForce, Team, Content, File, ComplianceUnauthorized access

2.2 Architectural Anti-Patterns

PatternInstancesImpact
God Classes (>600 LOC)12AttendeeService 2100+, ProgramService 1577, ProductReportService 1430
God Entities (>70 fields)4MeetingRequest 98, Speaker 88, Meeting 77, Attendee 70+
Naming Confusion6+MeetingRequest=Program, Meeting=RegistrationSite, SalesTeam=SalesForce
JSONB Abuse (untyped)10+approvals, dynamicFields, virtualProgramInfo, content, config
Hardcoded Values15+productId==24, categoryId 9/4, FiscalPeriod 2025, DST 2017-2030
Missing @Id5MeetingChangeLog, FiscalYear, AttendeeSurveyResponse, UserProductRole, SiteTemplate
Soft-Delete Inconsistency7 patternsBoolean, Integer, String, status field, del_flag, deleted, hardcoded
Dual Entry Points7/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

  1. Bounded Contexts: Each domain owns its entities, services, and APIs
  2. Event-Driven: Domain events for cross-domain communication
  3. State Machine First: All lifecycle entities use formal state machines
  4. Progressive Locking: Permissions narrow as lifecycle advances
  5. Brand-First Budget: Budget hierarchy starts from Brand, then geography
  6. Configuration as Data: Feature flags in database with runtime toggle
  7. Security by Default: @PreAuthorize on all endpoints, parameterized queries

3.2 Technology Stack

LayerCurrentTarget
Java817+
Spring Boot2.1.73.2+
Spring SecurityOAuth2/JWT (custom)Spring Security 6 + OAuth2 Resource Server
ORMMyBatis + tk.mybatisMyBatis-Plus or jOOQ
DatabasePostgreSQLPostgreSQL 15+
CacheGuava (in-memory)Redis
Message QueueNoneRabbitMQ or SQS
File StorageLocal filesystemS3/MinIO
ConfigSpring Cloud Config (YAML)Database + Spring Cloud Config (fallback)
React15-16.x18+
UI FrameworkAnt Design 3.xAnt Design 5.x
RouterReact Router 3-4.xReact Router 6+
StateRedux (manual)Redux Toolkit + RTK Query
BuildCRACOVite
LanguageJavaScriptTypeScript
Micro-frontendSingle-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 NameNew NameRationale
MeetingRequestProgramReflects actual business concept
MeetingRegistrationSiteClarifies registration infrastructure role
MeetingProgramTypeProgramTypeSimplifies naming
ProgramServiceTypeServiceTypeSimplifies naming
MeetingChangeLogProgramChangeLogConsistent naming
MeetingProjectTaskProgramTaskConsistent 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):

java
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      Y

Implementation:

java
@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):

  1. REGISTRATION_OPEN → REGISTRATION_CLOSED: When policy_registration_deadline reached
  2. REGISTRATION_CLOSED → EVENT_COMPLETE: When meeting_end_time passed

Condition-Based (Notifications):

  1. EVENT_COMPLETE → Notify Planner: "All attendees reconciled. Ready for Settlement?"
  2. 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 FieldReplacement
budget_version_idDerived from Program Status (automatic)
budget_statusDerived from Program Status (automatic)
reg_site_statusMerged into Program Status (PLANNING/REG_OPEN/REG_CLOSED)
attendee_list_statusMerged into Program Status (EVENT_COMPLETE+)
attendee_list_closed_timeRecorded in ProgramChangeLog

4.1.7 Close-Out Readiness API

New endpoint: GET /api/v2/programs/{id}/close-out-readiness

json
{
  "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_notice

4.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_at

4.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 → EXPIRED

Transitions:

  • 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

java
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_by

4.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:

  1. Find applicable BudgetPool (brand + region/district + program_type)
  2. Sum existing ESTIMATED + COMMITTED consumptions
  3. Compare remaining capacity vs new program estimated cost
  4. If insufficient: WAITLISTED (or reject if disallowProgramIfBudgetReached)

4.4.6 Backward Compatibility

Database views for legacy API consumers:

sql
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

  1. Aggregate Spend Reporting: Track total spend per HCP for Sunshine Act
  2. Transfer of Value (TOV): Calculate per-attendee spend for disclosure
  3. Document Audit: Compliance checklist per ServiceType
  4. 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_at

4.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 updates

4.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_to

4.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:

java
@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:

java
@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_at

4.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_at

4.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_at

4.7.4 Template Engine

Extract reusable EmailTemplateEngine with 40+ merge fields:

java
@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:
sql
-- 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

CurrentTarget
t_sales_teamt_org_node (type=SALES_FORCE)
t_regiont_org_node (type=REGION)
t_districtt_org_node (type=DISTRICT)
t_territoryt_org_node (type=TERRITORY)
SalesTeam/SalesForce confusionUnified as OrgNode
sales_team_id / sales_force_id dual fieldsSingle 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: ReportPermissionFilter that 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)
└── sequence

4.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

CurrentTarget
disableSurveyAlertssurveyAlertsEnabled
disablePresentationDeletepresentationDeleteEnabled
navigation.disableTopicsnavigation.topicsEnabled
navigation.disableTrainingnavigation.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:

java
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_at

4.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_at

4.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_at

4.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

java
@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

IntegrationCurrentTarget
SalesforceSOAP Partner API, string concatenation SOQLREST API, parameterized queries, per-product connections
ZoomGlobal token cache, 6 webhook endpointsPer-product tokens, 2 webhook endpoints
SendGridIncremental sync, no retryMessage queue with retry + status tracking
SFTPPromiscuousVerifier, password authHost key verification, SSH key auth, connection pooling
CloudConvertSynchronous, exception swallowingAsync with callback, proper error handling
Google MapsSelf-built RestTemplateWebClient with connection pooling
SSODual 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

yaml
# 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

RuleConvention
Table prefixt_
Primary key{entity}_id (BIGINT, auto-increment)
Foreign key{referenced_entity}_id
Boolean fieldsis_xxx or positive verb (e.g., deleted, approved)
Timestampscreated_at, updated_at (TIMESTAMPTZ)
Audit fieldscreated_by, updated_by (user_id reference)
Soft deletedeleted (BOOLEAN, DEFAULT false) -- consistent everywhere
Status fieldsPostgreSQL ENUM types or VARCHAR with CHECK constraint
JSON fieldsStrong-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

EntityCurrentTarget
Most entitiesInteger IDENTITYBIGINT IDENTITY
AttendeeUUID StringBIGINT IDENTITY + UUID external_id
TargetLongBIGINT
All new entitiesN/ABIGINT IDENTITY

5.4 JSONB Fields (Typed)

All JSONB fields must have corresponding Java DTOs:

EntityJSONB FieldRequired DTO
ProgramApproval(eliminated)Replaced by structured table
Survey.contentcontentSurveyContentDTO (Page → Field → Option)
SurveyResponse.response_dataresponseSurveyResponseDTO
ScheduledEmail.schedule_configconfigScheduleConfigDTO
VirtualMeetingAttendee.activitiesactivitiesList<ActivityEventDTO>
OrgNode.metadatametadataMap<String, Object> (extensible)

6. API Architecture

6.1 API Design Principles

  1. Unified namespace: /api/v2/{domain}/{resource}
  2. REST semantics: GET (read), POST (create), PUT (full update), PATCH (partial/state change), DELETE
  3. Pagination: Cursor-based for lists, ?page=1&size=20&sort=createdAt,desc
  4. Error format: RFC 7807 Problem Details
  5. Auth: Bearer JWT in Authorization header
  6. Versioning: URL path (/v2/)

6.2 API Namespace Map

DomainBase PathKey Endpoints
Program/api/v2/programsCRUD, status transitions, close-out readiness
Speaker/api/v2/speakersCRUD, contracts, training, presentations
Attendee/api/v2/programs/{id}/attendeesRegistration, reconciliation, sign-in
Budget/api/v2/budgetsPools, allocations, transactions, items
Compliance/api/v2/complianceTOV, aggregate spend, document audit
User/api/v2/usersCRUD, roles, permissions
Auth/api/v2/authLogin, SSO, token refresh, logout
Communication/api/v2/notificationsAlerts, emails, templates
Geography/api/v2/org-nodesHierarchy CRUD, planner assignment
Config/api/v2/configFeature flags, dictionaries
Content/api/v2/assetsFiles, templates, presentations
Survey/api/v2/surveysTemplates, responses, statistics
Expense/api/v2/expensesReports, items, approval
Virtual/api/v2/virtual-meetingsCRUD, webhooks
Report/api/v2/reportsStandard, 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 tasks

6.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

AspectCurrentTarget
LanguageJavaScriptTypeScript
React15-16.x18+
RouterReact Router 3-4.xReact Router 6+
UI LibraryAnt Design 3.xAnt Design 5.x
StateRedux (manual boilerplate)Redux Toolkit + RTK Query
BuildCRACOVite
Micro-frontendSingle-SPA (SystemJS)Single-SPA (ES modules) or Module Federation
CSSCSS/LESSCSS 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.ts

7.3 Unified "Program" Terminology

Current (inconsistent)Target
Plannerview: "Meeting"Program
Salesview: "Program"Program
Speakerview: "Meeting"Program
Code: MeetingRequestProgram
Code: MeetingRegistration 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

IssueFix
SQL Injection (4 locations)Parameterized queries everywhere
RCE (ProcessBuilder)Remove script execution, use JasperReports
API Key ExposureSeparate public/admin config APIs
Data Extract unfilteredRemove; all queries through permission filter
SSO Cookie insecureHttpOnly, Secure, SameSite=Strict
Plaintext passwordsRemove legacy comparison, BCrypt only
JWT claim validationValidate iss, aud, sub, exp, iat
Unprotected Controllers@PreAuthorize on every endpoint
SFTP no host keyEnable StrictHostKeyChecking
XSS (dangerouslySetInnerHTML)Sanitize HTML, use DOMPurify
File directory listingRemove DocXManagementController endpoint
Unauthenticated document URLRequire valid session

8.3 Input Validation

java
@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

java
@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 timing

9.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 availability

10. Migration Strategy

10.1 Phase Overview

PhaseDurationFocusRisk
Phase 02-4 weeksSecurity hardening (no schema changes)LOW
Phase 14-6 weeksBackend foundation (auth, state machine, guard)MEDIUM
Phase 26-8 weeksData model migration (entity decomposition)HIGH
Phase 34-6 weeksBudget & Organization redesignHIGH
Phase 46-8 weeksFrontend rewriteMEDIUM
Phase 54-6 weeksIntegration modernizationMEDIUM
Phase 62-4 weeksReporting & AnalyticsLOW

10.2 Phase 0: Security Hardening (Immediate)

No schema changes. Minimal risk.

  1. Fix SQL injection (4 locations) - parameterize queries
  2. Remove ProcessBuilder script execution
  3. Add @PreAuthorize to all unprotected controllers (8 controllers)
  4. Separate public config API - exclude credentials
  5. Fix SSO Cookie flags - HttpOnly, Secure, SameSite
  6. Remove legacy plaintext password comparison
  7. Validate JWT claims properly
  8. Enable SFTP host key verification
  9. Sanitize HTML in Speakerview

10.3 Phase 1: Backend Foundation

  1. JWT Authentication (pharmagin-auth service)

    • Implement RS256 JWT issuance
    • Redis session storage
    • Refresh token rotation
    • Merge pharmagin-sso + pharmagin-login
  2. 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
  3. 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

  1. 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)
  2. Data Migration Scripts:

sql
-- 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;
  1. Status Migration (15 old → 14 new):
sql
-- 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;
  1. Backward Compatibility Views:
sql
-- 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

  1. 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
  2. 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
  3. 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

  1. Create shared library @pharmagin/ui first
  2. Migrate Salesview (standalone React SPA, simplest)
  3. Migrate Speakerview (medium complexity)
  4. 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

  1. Salesforce: SOAP → REST API, parameterized queries
  2. Email: Direct SendGrid → RabbitMQ → Worker → SendGrid (with retry)
  3. Zoom: Per-product tokens, merged webhook endpoints
  4. SFTP: SSH key auth, connection pooling, host key verification
  5. Credentials: YAML → Secrets Manager

10.8 Phase 6: Reporting & Analytics

  1. Unified ReportEngine with permission filtering
  2. Remove ProcessBuilder scripts (already done in Phase 0)
  3. Async report generation via message queue
  4. Field config migration from Java static Map to database

11. Implementation Roadmap

11.1 Priority Matrix

PriorityItemsRationale
P0Security hardening (Phase 0)Critical vulnerabilities
P0JWT auth + state machine (Phase 1)Foundation for everything
P0Program entity decomposition (Phase 2)Unblocks all domain work
P1Budget Pool + Brand-First (Phase 3)Core business requirement
P1OrgNode tree (Phase 3)Enables flexible geography
P1Frontend shared library (Phase 4)Unblocks frontend migration
P2Frontend migration (Phase 4)Technology modernization
P2Integration modernization (Phase 5)Reliability improvement
P2Reporting unification (Phase 6)Operational improvement
P3Feature flag runtime toggleDeveloper experience
P3Observability stackOperations improvement

11.2 Success Metrics

MetricCurrentTarget
Critical security vulnerabilities180
God classes (>600 LOC)120
God entities (>70 fields)40
Unprotected API endpoints80+0
Status transition bugs possibleMany (no state machine)0 (formal state machine)
Feature flag change deploymentRestart requiredRuntime toggle
File storage scalabilitySingle serverDistributed (S3)
Auth scalabilitySingle instance (Guava)Horizontal (Redis + JWT)
Frontend build timeCRACO (~60s)Vite (~5s)
TypeScript coverage0%100%
Test coverageUnknown/Low>80%

11.3 Risk Mitigation

RiskMitigation
Data loss during migrationFull database backup before each phase; migration scripts tested on staging
API breaking changesBackward-compatible views + /v2/ namespace coexistence
Frontend regressionComponent-by-component migration; E2E tests before/after
Performance degradationLoad testing after each phase; query analysis
Integration disruptionFeature flags for new integration paths; gradual rollout
Team knowledge gapDocument each phase; code review gates

Appendix A: Complete Entity Count (Target)

DomainEntitiesKey Tables
Program10program, program_location, program_requestor, program_speaker, program_financial, program_approval, program_custom_field, registration_site, program_change_log, program_task
Speaker6speaker, speaker_address, speaker_financial, speaker_credential, speaker_profile, speaker_contract + contract_honoraria_item + contract_territory
Attendee4attendee, attendee_signature, attendee_accommodation, attendee_virtual_info
Budget6budget_pool, fiscal_period, budget_transaction, budget_consumption, budget_item, budget_change_log
Compliance3compliance_checklist, program_compliance_document, aggregate_spend_config
User5unified_user, role, role_permission, user_role_assignment, user_password_history
Communication5notification, email_log, email_template, scheduled_email, unsubscribed_email
Geography3org_node, org_node_type, org_node_manager
Config3feature_flag, theme, dictionary_entry
Content4asset, asset_group, asset_topic, file
Survey3survey, survey_product, survey_response
Expense3expense, expense_item, expense_category
Virtual2virtual_meeting, virtual_meeting_attendee
Reporting2report_field, report_job
Total~59Down 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

TermDefinition
ProgramA speaker engagement event (formerly "MeetingRequest")
Registration SiteThe registration portal for a program (formerly "Meeting")
BrandA pharmaceutical drug brand (e.g., Keytruda); primary budget dimension
TOVTransfer of Value - per-attendee spend calculation for compliance
HCPHealthcare Professional - a speaker or attendee
NPINational Provider Identifier - unique HCP identifier
PLIDProvider Lookup ID - compliance verification
SOWStatement of Work - initial budget version
ESTEstimate - planning stage budget version
BILLBilling - execution stage budget version
ACTActual - final recorded budget version
CXLCancellation - cancelled/postponed budget version
OrgNodeOrganization Node - flexible hierarchy element (replaces fixed Region/District/Territory)
BudgetPoolA budget allocation container in the Brand-First hierarchy
PhaseGuardBackend component enforcing lifecycle-based operation permissions