Skip to main content

Original/Copy/Amendment Tracking System

Executive Summary

This document defines the comprehensive system for tracking original records, true copies, certified copies, and amendments within the BIO-QMS platform, ensuring full compliance with FDA 21 CFR Part 11 requirements for electronic records in regulated biomedical and pharmaceutical environments.

Regulatory Context:

  • 21 CFR 11.10(a): System validation to ensure accuracy, reliability, and performance
  • 21 CFR 11.10(e): Documentation of record copies as true copies
  • 21 CFR 11.10(k)(1): Records retained in non-rewritable, non-erasable form
  • 21 CFR 11.50: Signed records contain signature manifestations

Key Capabilities:

  1. Immutable original record identification with cryptographic fingerprinting
  2. Verified true copy generation with automatic hash validation
  3. Certified copy workflow with qualified e-signatures
  4. Complete amendment chain with before/after diff tracking
  5. Comprehensive chain of custody logging
  6. Version control with semantic versioning

1. Original Record Identification

1.1 Data Model

TypeScript Interface

/**
* Original record metadata and identification
* Compliance: 21 CFR 11.10(k)(1) - non-rewritable original identification
*/
interface OriginalRecord {
// Immutable identifiers
original_id: string; // UUID v7 (time-ordered)
record_status: RecordStatus; // Enum, set once at creation
is_original: boolean; // Immutable flag, always true for originals

// Creation metadata
created_by: string; // User ID
created_at: Date; // ISO 8601 timestamp (UTC)
created_from_ip: string; // IPv4/IPv6 address
created_from_device: string; // Device fingerprint

// System context
system_version: string; // Semantic version (e.g., "2.3.1")
module: string; // Module name (e.g., "capa", "deviation")
record_type: string; // Type identifier (e.g., "CAPA_RECORD")
tenant_id: string; // Multi-tenant isolation

// Cryptographic integrity
content_hash: string; // SHA-256 of record content at creation
hash_algorithm: 'SHA-256'; // Algorithm identifier
hash_timestamp: Date; // When hash was computed

// Content snapshot
original_content: JSONB; // Immutable copy of initial state
content_schema_version: string; // Schema version for future migrations

// Audit metadata
regulatory_context: RegulatoryContext;
retention_policy: RetentionPolicy;
classification: DataClassification;
}

enum RecordStatus {
ORIGINAL = 'ORIGINAL',
TRUE_COPY = 'TRUE_COPY',
CERTIFIED_COPY = 'CERTIFIED_COPY',
AMENDMENT = 'AMENDMENT'
}

interface RegulatoryContext {
applicable_regulations: string[]; // e.g., ["21_CFR_PART_11", "ISO_13485"]
regulatory_event_type?: string; // e.g., "PRODUCT_RELEASE", "AUDIT"
gxp_category: 'GMP' | 'GLP' | 'GCP' | 'GDP';
}

interface RetentionPolicy {
retention_period_years: number; // Minimum retention (e.g., 7 years)
destruction_date?: Date; // Calculated destruction eligibility
legal_hold: boolean; // Litigation/investigation hold
retention_basis: string; // Regulatory citation
}

enum DataClassification {
CRITICAL_QUALITY = 'CRITICAL_QUALITY',
QUALITY_IMPACT = 'QUALITY_IMPACT',
NON_QUALITY = 'NON_QUALITY'
}

PostgreSQL Schema

-- Original records table
-- Compliance: 21 CFR 11.10(k)(1) - non-rewritable records
CREATE TABLE original_records (
-- Primary identifiers
original_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
record_status VARCHAR(20) NOT NULL CHECK (record_status IN ('ORIGINAL', 'TRUE_COPY', 'CERTIFIED_COPY', 'AMENDMENT')),
is_original BOOLEAN NOT NULL DEFAULT TRUE,

-- Creation metadata
created_by UUID NOT NULL REFERENCES users(user_id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_from_ip INET NOT NULL,
created_from_device VARCHAR(255),

-- System context
system_version VARCHAR(20) NOT NULL,
module VARCHAR(50) NOT NULL,
record_type VARCHAR(100) NOT NULL,
tenant_id UUID NOT NULL REFERENCES tenants(tenant_id),

-- Cryptographic integrity
content_hash VARCHAR(64) NOT NULL, -- SHA-256 = 64 hex chars
hash_algorithm VARCHAR(20) NOT NULL DEFAULT 'SHA-256',
hash_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),

-- Content snapshot (immutable)
original_content JSONB NOT NULL,
content_schema_version VARCHAR(20) NOT NULL,

-- Regulatory metadata
applicable_regulations TEXT[] NOT NULL,
regulatory_event_type VARCHAR(100),
gxp_category VARCHAR(10) NOT NULL CHECK (gxp_category IN ('GMP', 'GLP', 'GCP', 'GDP')),

-- Retention policy
retention_period_years INTEGER NOT NULL,
destruction_date DATE,
legal_hold BOOLEAN NOT NULL DEFAULT FALSE,
retention_basis TEXT NOT NULL,

-- Data classification
data_classification VARCHAR(30) NOT NULL CHECK (data_classification IN ('CRITICAL_QUALITY', 'QUALITY_IMPACT', 'NON_QUALITY')),

-- Audit trail
created_at_server TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_accessed_at TIMESTAMPTZ,
access_count INTEGER NOT NULL DEFAULT 0,

-- Constraints
CONSTRAINT original_status_check CHECK (
(is_original = TRUE AND record_status = 'ORIGINAL') OR
(is_original = FALSE AND record_status != 'ORIGINAL')
)
);

-- Immutability enforcement via trigger
CREATE OR REPLACE FUNCTION enforce_original_immutability()
RETURNS TRIGGER AS $$
BEGIN
-- Allow INSERT only
IF TG_OP = 'UPDATE' THEN
-- Only allow updates to audit fields
IF OLD.original_id IS DISTINCT FROM NEW.original_id OR
OLD.record_status IS DISTINCT FROM NEW.record_status OR
OLD.is_original IS DISTINCT FROM NEW.is_original OR
OLD.content_hash IS DISTINCT FROM NEW.content_hash OR
OLD.original_content IS DISTINCT FROM NEW.original_content THEN
RAISE EXCEPTION 'Original record fields are immutable (21 CFR 11.10(k)(1))';
END IF;
END IF;

IF TG_OP = 'DELETE' THEN
RAISE EXCEPTION 'Original records cannot be deleted (21 CFR 11.10(k)(1))';
END IF;

RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER enforce_original_immutability_trigger
BEFORE UPDATE OR DELETE ON original_records
FOR EACH ROW EXECUTE FUNCTION enforce_original_immutability();

-- Indexes for performance
CREATE INDEX idx_original_records_created_by ON original_records(created_by);
CREATE INDEX idx_original_records_created_at ON original_records(created_at);
CREATE INDEX idx_original_records_tenant ON original_records(tenant_id);
CREATE INDEX idx_original_records_module_type ON original_records(module, record_type);
CREATE INDEX idx_original_records_hash ON original_records(content_hash);

-- GIN index for JSONB content search
CREATE INDEX idx_original_records_content_gin ON original_records USING GIN(original_content);

1.2 Hash Generation

/**
* Generate cryptographic hash of record content
* Compliance: 21 CFR 11.10(k)(1) - integrity verification
*/
import crypto from 'crypto';

interface HashableContent {
[key: string]: any;
}

export class RecordHashGenerator {
private readonly algorithm = 'sha256';

/**
* Generate deterministic SHA-256 hash of record content
* Normalizes JSON to ensure consistent ordering
*/
generateHash(content: HashableContent): string {
// Normalize JSON (sort keys, remove whitespace)
const normalized = this.normalizeContent(content);

// Generate hash
const hash = crypto
.createHash(this.algorithm)
.update(normalized)
.digest('hex');

return hash;
}

/**
* Normalize content for deterministic hashing
*/
private normalizeContent(content: HashableContent): string {
// Sort keys recursively, stringify without whitespace
return JSON.stringify(content, Object.keys(content).sort());
}

/**
* Verify content against existing hash
*/
verifyHash(content: HashableContent, expectedHash: string): boolean {
const actualHash = this.generateHash(content);
return actualHash === expectedHash;
}

/**
* Generate hash with metadata
*/
generateHashWithMetadata(content: HashableContent): {
hash: string;
algorithm: string;
timestamp: Date;
} {
return {
hash: this.generateHash(content),
algorithm: this.algorithm.toUpperCase(),
timestamp: new Date()
};
}
}

1.3 API Endpoints

/**
* Original record creation API
* POST /api/v1/records/original
*/
interface CreateOriginalRecordRequest {
module: string;
record_type: string;
content: HashableContent;
regulatory_context: RegulatoryContext;
retention_policy: RetentionPolicy;
data_classification: DataClassification;
}

interface CreateOriginalRecordResponse {
original_id: string;
content_hash: string;
created_at: Date;
record_status: 'ORIGINAL';
}

/**
* Original record retrieval
* GET /api/v1/records/original/:original_id
*/
interface GetOriginalRecordResponse extends OriginalRecord {
current_copy_count: number;
current_amendment_count: number;
latest_amendment_version?: string;
}

2. True Copy Generation

2.1 Data Model

TypeScript Interface

/**
* True copy metadata
* Compliance: 21 CFR 11.10(e) - documentation of copies as true copies
*/
interface TrueCopy {
// Copy identifiers
copy_id: string; // UUID v7
source_original_id: string; // Reference to original
copy_number: number; // Sequential per original
record_status: RecordStatus.TRUE_COPY;

// Copy generation metadata
copy_created_at: Date;
copy_created_by: string; // User ID
copy_purpose: CopyPurpose;
copy_destination?: string; // e.g., "Regulatory Submission Package"

// Verification
verification_hash: string; // Must match original content_hash
verification_status: VerificationStatus;
verification_timestamp: Date;
hash_match_confirmed: boolean;

// Copy content
copy_content: JSONB; // Exact replica of original_content

// Watermark/distinguishing marks
watermark_text: string; // "TRUE COPY - NOT ORIGINAL"
copy_metadata: CopyMetadata;

// Audit
accessed_count: number;
last_accessed_at?: Date;
exported: boolean;
export_timestamp?: Date;
}

enum CopyPurpose {
REGULATORY_SUBMISSION = 'REGULATORY_SUBMISSION',
EXTERNAL_AUDIT = 'EXTERNAL_AUDIT',
INTERNAL_REVIEW = 'INTERNAL_REVIEW',
BACKUP = 'BACKUP',
ARCHIVAL = 'ARCHIVAL',
LEGAL_DISCOVERY = 'LEGAL_DISCOVERY'
}

enum VerificationStatus {
PENDING = 'PENDING',
VERIFIED = 'VERIFIED',
FAILED = 'FAILED'
}

interface CopyMetadata {
original_created_at: Date;
original_created_by: string;
original_module: string;
original_record_type: string;
copy_generation_method: 'AUTOMATIC' | 'MANUAL_REQUEST';
verification_algorithm: string;
}

PostgreSQL Schema

-- True copies table
CREATE TABLE true_copies (
-- Primary identifiers
copy_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source_original_id UUID NOT NULL REFERENCES original_records(original_id),
copy_number INTEGER NOT NULL,
record_status VARCHAR(20) NOT NULL DEFAULT 'TRUE_COPY',

-- Copy generation metadata
copy_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
copy_created_by UUID NOT NULL REFERENCES users(user_id),
copy_purpose VARCHAR(50) NOT NULL,
copy_destination VARCHAR(255),

-- Verification
verification_hash VARCHAR(64) NOT NULL,
verification_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
verification_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
hash_match_confirmed BOOLEAN NOT NULL,

-- Copy content
copy_content JSONB NOT NULL,

-- Watermark
watermark_text TEXT NOT NULL DEFAULT 'TRUE COPY - NOT ORIGINAL',
copy_metadata JSONB NOT NULL,

-- Audit
accessed_count INTEGER NOT NULL DEFAULT 0,
last_accessed_at TIMESTAMPTZ,
exported BOOLEAN NOT NULL DEFAULT FALSE,
export_timestamp TIMESTAMPTZ,

-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES tenants(tenant_id),

-- Constraints
UNIQUE(source_original_id, copy_number),
CHECK (record_status = 'TRUE_COPY'),
CHECK (verification_status IN ('PENDING', 'VERIFIED', 'FAILED'))
);

-- Auto-increment copy_number per original
CREATE OR REPLACE FUNCTION set_copy_number()
RETURNS TRIGGER AS $$
BEGIN
SELECT COALESCE(MAX(copy_number), 0) + 1
INTO NEW.copy_number
FROM true_copies
WHERE source_original_id = NEW.source_original_id;

RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER set_copy_number_trigger
BEFORE INSERT ON true_copies
FOR EACH ROW
WHEN (NEW.copy_number IS NULL)
EXECUTE FUNCTION set_copy_number();

-- Indexes
CREATE INDEX idx_true_copies_source ON true_copies(source_original_id);
CREATE INDEX idx_true_copies_created_at ON true_copies(copy_created_at);
CREATE INDEX idx_true_copies_created_by ON true_copies(copy_created_by);
CREATE INDEX idx_true_copies_tenant ON true_copies(tenant_id);

2.2 True Copy Generation Logic

/**
* True copy generation service
* Compliance: 21 CFR 11.10(e) - true copy documentation
*/
export class TrueCopyService {
constructor(
private hashGenerator: RecordHashGenerator,
private auditLogger: AuditLogger
) {}

/**
* Generate verified true copy of original record
*/
async generateTrueCopy(request: {
original_id: string;
purpose: CopyPurpose;
destination?: string;
created_by: string;
}): Promise<TrueCopy> {
// 1. Fetch original record
const original = await this.fetchOriginal(request.original_id);

// 2. Verify original integrity
const hashValid = this.hashGenerator.verifyHash(
original.original_content,
original.content_hash
);

if (!hashValid) {
throw new Error('Original record integrity check failed - hash mismatch');
}

// 3. Generate copy hash from original content
const copyHash = this.hashGenerator.generateHash(original.original_content);

// 4. Verify copy hash matches original
const hashMatch = copyHash === original.content_hash;

// 5. Create true copy record
const trueCopy: TrueCopy = {
copy_id: this.generateUUID(),
source_original_id: original.original_id,
copy_number: await this.getNextCopyNumber(original.original_id),
record_status: RecordStatus.TRUE_COPY,
copy_created_at: new Date(),
copy_created_by: request.created_by,
copy_purpose: request.purpose,
copy_destination: request.destination,
verification_hash: copyHash,
verification_status: hashMatch ? VerificationStatus.VERIFIED : VerificationStatus.FAILED,
verification_timestamp: new Date(),
hash_match_confirmed: hashMatch,
copy_content: original.original_content, // Exact replica
watermark_text: 'TRUE COPY - NOT ORIGINAL',
copy_metadata: {
original_created_at: original.created_at,
original_created_by: original.created_by,
original_module: original.module,
original_record_type: original.record_type,
copy_generation_method: 'AUTOMATIC',
verification_algorithm: 'SHA-256'
},
accessed_count: 0,
exported: false
};

// 6. Persist true copy
await this.saveTrueCopy(trueCopy);

// 7. Audit log
await this.auditLogger.log({
event_type: 'TRUE_COPY_GENERATED',
original_id: original.original_id,
copy_id: trueCopy.copy_id,
user_id: request.created_by,
verification_status: trueCopy.verification_status,
purpose: request.purpose
});

return trueCopy;
}

/**
* Batch true copy generation
*/
async generateBatchTrueCopies(request: {
original_ids: string[];
purpose: CopyPurpose;
destination?: string;
created_by: string;
}): Promise<TrueCopy[]> {
const copies = await Promise.all(
request.original_ids.map(original_id =>
this.generateTrueCopy({
original_id,
purpose: request.purpose,
destination: request.destination,
created_by: request.created_by
})
)
);

return copies;
}

/**
* Export true copy package (e.g., for regulatory submission)
*/
async exportTrueCopyPackage(request: {
copy_ids: string[];
package_format: 'PDF' | 'ZIP' | 'XML';
include_metadata: boolean;
}): Promise<Blob> {
// Implementation for export package generation
// Include watermarks, metadata, verification certificates
throw new Error('Not implemented');
}
}

2.3 API Endpoints

/**
* POST /api/v1/records/true-copy
*/
interface GenerateTrueCopyRequest {
original_id: string;
purpose: CopyPurpose;
destination?: string;
}

interface GenerateTrueCopyResponse {
copy_id: string;
copy_number: number;
verification_status: VerificationStatus;
hash_match_confirmed: boolean;
copy_created_at: Date;
}

/**
* POST /api/v1/records/true-copy/batch
*/
interface GenerateBatchTrueCopiesRequest {
original_ids: string[];
purpose: CopyPurpose;
destination?: string;
}

interface GenerateBatchTrueCopiesResponse {
copies: GenerateTrueCopyResponse[];
total_count: number;
successful_count: number;
failed_count: number;
}

3. Certified Copy Workflow

3.1 Data Model

TypeScript Interface

/**
* Certified copy with qualified e-signature
* Compliance: 21 CFR 11.50 - signature manifestations
*/
interface CertifiedCopy extends TrueCopy {
record_status: RecordStatus.CERTIFIED_COPY;

// Certification metadata
certification_id: string;
certifier_id: string; // Must have Compliance Officer role
certification_date: Date;
certification_purpose: string; // Free text, required

// E-signature
e_signature: ESignature;
signature_manifestation: SignatureManifestation;

// Certification statement
certification_statement: string; // Template-based
certification_template_id: string;

// Certificate of authenticity
certificate_id: string;
certificate_generated_at: Date;
certificate_pdf_url: string; // S3/blob storage URL

// Workflow tracking
workflow_status: CertificationWorkflowStatus;
workflow_initiated_at: Date;
workflow_completed_at?: Date;
approval_chain: ApprovalStep[];
}

interface ESignature {
signature_id: string;
signer_id: string;
signer_name: string;
signer_role: string;
signature_timestamp: Date;
signature_meaning: SignatureMeaning;
signature_method: 'PASSWORD' | 'BIOMETRIC' | 'TOKEN' | 'PKI';
password_hash?: string; // If password-based
biometric_hash?: string; // If biometric
certificate_serial?: string; // If PKI
}

enum SignatureMeaning {
CERTIFIED_AS_TRUE_COPY = 'CERTIFIED_AS_TRUE_COPY',
APPROVED = 'APPROVED',
REVIEWED = 'REVIEWED',
WITNESSED = 'WITNESSED'
}

interface SignatureManifestation {
// 21 CFR 11.50 required elements
printed_name: string;
signature_date: Date;
signature_meaning_text: string; // Human-readable

// Additional context
signature_reason: string;
signature_location?: string; // e.g., "Quality Assurance Department"
}

enum CertificationWorkflowStatus {
INITIATED = 'INITIATED',
PENDING_REVIEW = 'PENDING_REVIEW',
PENDING_APPROVAL = 'PENDING_APPROVAL',
CERTIFIED = 'CERTIFIED',
REJECTED = 'REJECTED'
}

interface ApprovalStep {
step_number: number;
step_type: 'REVIEW' | 'APPROVE' | 'CERTIFY';
assigned_to: string; // User ID or role
completed_by?: string;
completed_at?: Date;
decision: 'PENDING' | 'APPROVED' | 'REJECTED';
comments?: string;
e_signature?: ESignature;
}

PostgreSQL Schema

-- Certified copies table
CREATE TABLE certified_copies (
-- Inherits from true_copies structure
copy_id UUID PRIMARY KEY REFERENCES true_copies(copy_id),

-- Certification metadata
certification_id UUID NOT NULL DEFAULT gen_random_uuid(),
certifier_id UUID NOT NULL REFERENCES users(user_id),
certification_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
certification_purpose TEXT NOT NULL,

-- E-signature
e_signature JSONB NOT NULL,
signature_manifestation JSONB NOT NULL,

-- Certification statement
certification_statement TEXT NOT NULL,
certification_template_id UUID REFERENCES certification_templates(template_id),

-- Certificate of authenticity
certificate_id UUID NOT NULL DEFAULT gen_random_uuid(),
certificate_generated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
certificate_pdf_url TEXT,

-- Workflow tracking
workflow_status VARCHAR(30) NOT NULL DEFAULT 'INITIATED',
workflow_initiated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
workflow_completed_at TIMESTAMPTZ,
approval_chain JSONB NOT NULL DEFAULT '[]'::jsonb,

-- Constraints
CHECK (workflow_status IN ('INITIATED', 'PENDING_REVIEW', 'PENDING_APPROVAL', 'CERTIFIED', 'REJECTED'))
);

-- Certification templates
CREATE TABLE certification_templates (
template_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
template_name VARCHAR(255) NOT NULL,
regulation VARCHAR(100) NOT NULL, -- e.g., "FDA_21_CFR_PART_11"
template_text TEXT NOT NULL, -- With placeholders like {{certifier_name}}
variables JSONB NOT NULL, -- List of available placeholders
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID NOT NULL REFERENCES users(user_id),
tenant_id UUID NOT NULL REFERENCES tenants(tenant_id)
);

-- E-signatures table (for audit trail)
CREATE TABLE e_signatures (
signature_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
signer_id UUID NOT NULL REFERENCES users(user_id),
signature_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
signature_meaning VARCHAR(50) NOT NULL,
signature_method VARCHAR(20) NOT NULL,
password_hash VARCHAR(255),
biometric_hash VARCHAR(255),
certificate_serial VARCHAR(255),

-- What was signed
signed_object_type VARCHAR(50) NOT NULL, -- e.g., "CERTIFIED_COPY"
signed_object_id UUID NOT NULL,

-- Signature manifestation
printed_name VARCHAR(255) NOT NULL,
signature_meaning_text TEXT NOT NULL,
signature_reason TEXT,
signature_location VARCHAR(255),

-- Audit
ip_address INET NOT NULL,
user_agent TEXT,

tenant_id UUID NOT NULL REFERENCES tenants(tenant_id)
);

CREATE INDEX idx_e_signatures_signer ON e_signatures(signer_id);
CREATE INDEX idx_e_signatures_timestamp ON e_signatures(signature_timestamp);
CREATE INDEX idx_e_signatures_object ON e_signatures(signed_object_type, signed_object_id);

3.2 Certification Workflow

/**
* Certification workflow service
* Compliance: 21 CFR 11.50 - signature manifestations
*/
export class CertificationWorkflowService {
constructor(
private trueCopyService: TrueCopyService,
private eSignatureService: ESignatureService,
private templateService: CertificationTemplateService,
private pdfGenerator: CertificatePDFGenerator
) {}

/**
* Initiate certification workflow
* Step 1: Generate true copy
*/
async initiateCertification(request: {
original_id: string;
certification_purpose: string;
certification_template_id: string;
initiated_by: string;
}): Promise<CertifiedCopy> {
// 1. Generate true copy first
const trueCopy = await this.trueCopyService.generateTrueCopy({
original_id: request.original_id,
purpose: CopyPurpose.REGULATORY_SUBMISSION,
created_by: request.initiated_by
});

// 2. Verify copy is valid
if (trueCopy.verification_status !== VerificationStatus.VERIFIED) {
throw new Error('Cannot certify copy - verification failed');
}

// 3. Initialize certification workflow
const certifiedCopy: CertifiedCopy = {
...trueCopy,
record_status: RecordStatus.CERTIFIED_COPY,
certification_id: this.generateUUID(),
certifier_id: '', // Assigned during approval
certification_date: new Date(),
certification_purpose: request.certification_purpose,
certification_template_id: request.certification_template_id,
workflow_status: CertificationWorkflowStatus.INITIATED,
workflow_initiated_at: new Date(),
approval_chain: [
{
step_number: 1,
step_type: 'REVIEW',
assigned_to: 'ROLE:QA_REVIEWER',
decision: 'PENDING'
},
{
step_number: 2,
step_type: 'APPROVE',
assigned_to: 'ROLE:QA_MANAGER',
decision: 'PENDING'
},
{
step_number: 3,
step_type: 'CERTIFY',
assigned_to: 'ROLE:COMPLIANCE_OFFICER',
decision: 'PENDING'
}
]
};

await this.saveCertifiedCopy(certifiedCopy);

return certifiedCopy;
}

/**
* Step 2: Review content
*/
async reviewCertification(request: {
certification_id: string;
reviewer_id: string;
decision: 'APPROVED' | 'REJECTED';
comments?: string;
e_signature_credentials: ESignatureCredentials;
}): Promise<CertifiedCopy> {
const certifiedCopy = await this.fetchCertifiedCopy(request.certification_id);

// Verify reviewer has authority
await this.verifyUserRole(request.reviewer_id, 'QA_REVIEWER');

// Apply e-signature
const signature = await this.eSignatureService.sign({
signer_id: request.reviewer_id,
signed_object_type: 'CERTIFIED_COPY',
signed_object_id: certifiedCopy.certification_id,
signature_meaning: SignatureMeaning.REVIEWED,
credentials: request.e_signature_credentials
});

// Update approval chain
const reviewStep = certifiedCopy.approval_chain.find(s => s.step_type === 'REVIEW');
reviewStep.completed_by = request.reviewer_id;
reviewStep.completed_at = new Date();
reviewStep.decision = request.decision;
reviewStep.comments = request.comments;
reviewStep.e_signature = signature;

// Update workflow status
if (request.decision === 'APPROVED') {
certifiedCopy.workflow_status = CertificationWorkflowStatus.PENDING_APPROVAL;
} else {
certifiedCopy.workflow_status = CertificationWorkflowStatus.REJECTED;
}

await this.updateCertifiedCopy(certifiedCopy);

return certifiedCopy;
}

/**
* Step 3: Approve certification
*/
async approveCertification(request: {
certification_id: string;
approver_id: string;
decision: 'APPROVED' | 'REJECTED';
comments?: string;
e_signature_credentials: ESignatureCredentials;
}): Promise<CertifiedCopy> {
const certifiedCopy = await this.fetchCertifiedCopy(request.certification_id);

// Verify approver has authority
await this.verifyUserRole(request.approver_id, 'QA_MANAGER');

// Apply e-signature
const signature = await this.eSignatureService.sign({
signer_id: request.approver_id,
signed_object_type: 'CERTIFIED_COPY',
signed_object_id: certifiedCopy.certification_id,
signature_meaning: SignatureMeaning.APPROVED,
credentials: request.e_signature_credentials
});

// Update approval chain
const approveStep = certifiedCopy.approval_chain.find(s => s.step_type === 'APPROVE');
approveStep.completed_by = request.approver_id;
approveStep.completed_at = new Date();
approveStep.decision = request.decision;
approveStep.comments = request.comments;
approveStep.e_signature = signature;

// Update workflow status
if (request.decision === 'APPROVED') {
certifiedCopy.workflow_status = CertificationWorkflowStatus.PENDING_APPROVAL;
} else {
certifiedCopy.workflow_status = CertificationWorkflowStatus.REJECTED;
}

await this.updateCertifiedCopy(certifiedCopy);

return certifiedCopy;
}

/**
* Step 4: Final certification by Compliance Officer
*/
async finalizeCertification(request: {
certification_id: string;
certifier_id: string;
e_signature_credentials: ESignatureCredentials;
}): Promise<CertifiedCopy> {
const certifiedCopy = await this.fetchCertifiedCopy(request.certification_id);

// Verify certifier has Compliance Officer role
await this.verifyUserRole(request.certifier_id, 'COMPLIANCE_OFFICER');

// Generate certification statement from template
const template = await this.templateService.getTemplate(
certifiedCopy.certification_template_id
);
const statement = await this.templateService.renderTemplate(template, {
certifier_name: await this.getUserName(request.certifier_id),
certification_date: new Date().toISOString(),
original_id: certifiedCopy.source_original_id,
copy_id: certifiedCopy.copy_id,
purpose: certifiedCopy.certification_purpose
});

// Apply e-signature
const signature = await this.eSignatureService.sign({
signer_id: request.certifier_id,
signed_object_type: 'CERTIFIED_COPY',
signed_object_id: certifiedCopy.certification_id,
signature_meaning: SignatureMeaning.CERTIFIED_AS_TRUE_COPY,
credentials: request.e_signature_credentials
});

// Update certification
certifiedCopy.certifier_id = request.certifier_id;
certifiedCopy.certification_date = new Date();
certifiedCopy.certification_statement = statement;
certifiedCopy.e_signature = signature;
certifiedCopy.signature_manifestation = {
printed_name: signature.signer_name,
signature_date: signature.signature_timestamp,
signature_meaning_text: 'Certified as True Copy',
signature_reason: certifiedCopy.certification_purpose,
signature_location: 'Quality Assurance Department'
};

// Update approval chain
const certifyStep = certifiedCopy.approval_chain.find(s => s.step_type === 'CERTIFY');
certifyStep.completed_by = request.certifier_id;
certifyStep.completed_at = new Date();
certifyStep.decision = 'APPROVED';
certifyStep.e_signature = signature;

// Update workflow status
certifiedCopy.workflow_status = CertificationWorkflowStatus.CERTIFIED;
certifiedCopy.workflow_completed_at = new Date();

// Generate certificate of authenticity PDF
const certificatePdf = await this.pdfGenerator.generateCertificate(certifiedCopy);
certifiedCopy.certificate_pdf_url = await this.uploadCertificate(certificatePdf);
certifiedCopy.certificate_generated_at = new Date();

await this.updateCertifiedCopy(certifiedCopy);

return certifiedCopy;
}
}

3.3 Certification Statement Template

/**
* Default FDA 21 CFR Part 11 certification template
*/
const FDA_CERTIFICATION_TEMPLATE = `
CERTIFICATE OF AUTHENTICITY
TRUE COPY CERTIFICATION

I, {{certifier_name}}, Compliance Officer, hereby certify that this document is a true, accurate, and complete copy of the original electronic record maintained in the BIO-QMS system.

Original Record Details:
- Original Record ID: {{original_id}}
- Copy ID: {{copy_id}}
- Original Creation Date: {{original_created_at}}
- Original Created By: {{original_created_by}}
- Module: {{module}}
- Record Type: {{record_type}}

Verification:
- Hash Algorithm: SHA-256
- Original Hash: {{original_hash}}
- Copy Hash: {{copy_hash}}
- Hash Match: {{hash_match}}
- Verification Date: {{verification_date}}

This certification is made in accordance with:
- 21 CFR Part 11.10(e) - Documentation of copies as true copies
- 21 CFR Part 11.50 - Signature manifestations

Purpose of Certification:
{{certification_purpose}}

This certified copy is intended for: {{destination}}

I affirm that this copy was generated through a validated electronic system that ensures the accuracy, reliability, and integrity of electronic records.

Date: {{certification_date}}

Electronically Signed By:
{{certifier_name}}
Compliance Officer
{{signature_timestamp}}

Signature Meaning: Certified as True Copy
Signature Method: {{signature_method}}

---
This is a certified copy. The original electronic record is maintained in the BIO-QMS system and is available upon request.
Certificate ID: {{certificate_id}}
Generated: {{certificate_generated_at}}
`;

4. Amendment Chain

4.1 Data Model

TypeScript Interface

/**
* Amendment record
* Compliance: 21 CFR 11.10(e) - audit trail of amendments
*/
interface Amendment {
// Amendment identifiers
amendment_id: string; // UUID v7
original_id: string; // Reference to original record
amendment_number: number; // Sequential, starts at 1
version: string; // Semantic version (e.g., "1.1.0")

// Amendment metadata
amendment_date: Date;
amended_by: string; // User ID
amendment_reason_category: AmendmentReasonCategory;
amendment_reason_text: string; // Free text explanation

// Before/after tracking
content_before: JSONB; // State before amendment
content_after: JSONB; // State after amendment
diff: JSONB; // Structured diff

// Change summary
fields_modified: string[]; // List of modified field paths
change_type: ChangeType;
impact_assessment: ImpactAssessment;

// Approval workflow
amendment_status: AmendmentStatus;
approval_chain: ApprovalStep[];

// E-signatures
author_signature: ESignature; // Person who made change
reviewer_signature?: ESignature; // Optional reviewer
approver_signature?: ESignature; // Required for critical amendments

// Rollback capability
can_rollback: boolean;
rollback_amendment_id?: string; // If this amendment was a rollback
rolled_back_by?: string; // Amendment that rolled this back

// Regulatory
regulatory_impact: boolean; // Does this affect compliance?
validation_impact: boolean; // Does this require revalidation?
notification_sent: boolean; // Were stakeholders notified?
notification_timestamp?: Date;
}

enum AmendmentReasonCategory {
CORRECTION = 'CORRECTION', // Fix an error
CLARIFICATION = 'CLARIFICATION', // Add clarity, no substantive change
UPDATE = 'UPDATE', // New information
REGULATORY_CHANGE = 'REGULATORY_CHANGE', // Regulation update
PROCESS_IMPROVEMENT = 'PROCESS_IMPROVEMENT',
ROLLBACK = 'ROLLBACK' // Undo previous amendment
}

enum ChangeType {
MINOR = 'MINOR', // Formatting, typos
MODERATE = 'MODERATE', // Clarifications, updates
MAJOR = 'MAJOR' // Substantive changes affecting compliance
}

interface ImpactAssessment {
quality_impact: 'NONE' | 'LOW' | 'MEDIUM' | 'HIGH';
compliance_impact: 'NONE' | 'LOW' | 'MEDIUM' | 'HIGH';
validation_required: boolean;
affected_processes: string[];
stakeholders_to_notify: string[];
}

enum AmendmentStatus {
DRAFT = 'DRAFT',
PENDING_REVIEW = 'PENDING_REVIEW',
PENDING_APPROVAL = 'PENDING_APPROVAL',
APPROVED = 'APPROVED',
REJECTED = 'REJECTED',
ACTIVE = 'ACTIVE'
}

PostgreSQL Schema

-- Amendments table
CREATE TABLE amendments (
-- Primary identifiers
amendment_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
original_id UUID NOT NULL REFERENCES original_records(original_id),
amendment_number INTEGER NOT NULL,
version VARCHAR(20) NOT NULL, -- Semantic version

-- Amendment metadata
amendment_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
amended_by UUID NOT NULL REFERENCES users(user_id),
amendment_reason_category VARCHAR(50) NOT NULL,
amendment_reason_text TEXT NOT NULL,

-- Before/after tracking
content_before JSONB NOT NULL,
content_after JSONB NOT NULL,
diff JSONB NOT NULL,

-- Change summary
fields_modified TEXT[] NOT NULL,
change_type VARCHAR(20) NOT NULL,
impact_assessment JSONB NOT NULL,

-- Approval workflow
amendment_status VARCHAR(30) NOT NULL DEFAULT 'DRAFT',
approval_chain JSONB NOT NULL DEFAULT '[]'::jsonb,

-- E-signatures
author_signature JSONB NOT NULL,
reviewer_signature JSONB,
approver_signature JSONB,

-- Rollback capability
can_rollback BOOLEAN NOT NULL DEFAULT TRUE,
rollback_amendment_id UUID REFERENCES amendments(amendment_id),
rolled_back_by UUID REFERENCES amendments(amendment_id),

-- Regulatory
regulatory_impact BOOLEAN NOT NULL DEFAULT FALSE,
validation_impact BOOLEAN NOT NULL DEFAULT FALSE,
notification_sent BOOLEAN NOT NULL DEFAULT FALSE,
notification_timestamp TIMESTAMPTZ,

-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES tenants(tenant_id),

-- Constraints
UNIQUE(original_id, amendment_number),
CHECK (amendment_status IN ('DRAFT', 'PENDING_REVIEW', 'PENDING_APPROVAL', 'APPROVED', 'REJECTED', 'ACTIVE')),
CHECK (change_type IN ('MINOR', 'MODERATE', 'MAJOR'))
);

-- Auto-increment amendment_number
CREATE OR REPLACE FUNCTION set_amendment_number()
RETURNS TRIGGER AS $$
BEGIN
SELECT COALESCE(MAX(amendment_number), 0) + 1
INTO NEW.amendment_number
FROM amendments
WHERE original_id = NEW.original_id;

RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER set_amendment_number_trigger
BEFORE INSERT ON amendments
FOR EACH ROW
WHEN (NEW.amendment_number IS NULL)
EXECUTE FUNCTION set_amendment_number();

-- Indexes
CREATE INDEX idx_amendments_original ON amendments(original_id);
CREATE INDEX idx_amendments_date ON amendments(amendment_date);
CREATE INDEX idx_amendments_amended_by ON amendments(amended_by);
CREATE INDEX idx_amendments_status ON amendments(amendment_status);
CREATE INDEX idx_amendments_tenant ON amendments(tenant_id);

-- GIN index for diff search
CREATE INDEX idx_amendments_diff_gin ON amendments USING GIN(diff);

4.2 Amendment Workflow

/**
* Amendment service
* Compliance: 21 CFR 11.10(e) - audit trail of changes
*/
export class AmendmentService {
constructor(
private diffGenerator: DiffGenerator,
private versionGenerator: VersionGenerator,
private eSignatureService: ESignatureService,
private notificationService: NotificationService
) {}

/**
* Create amendment draft
*/
async createAmendment(request: {
original_id: string;
amended_by: string;
reason_category: AmendmentReasonCategory;
reason_text: string;
content_after: JSONB;
change_type: ChangeType;
impact_assessment: ImpactAssessment;
}): Promise<Amendment> {
// 1. Fetch current state
const original = await this.fetchOriginal(request.original_id);
const currentContent = await this.getCurrentContent(request.original_id);

// 2. Generate diff
const diff = this.diffGenerator.generateDiff(
currentContent,
request.content_after
);

// 3. Extract modified fields
const fieldsModified = this.diffGenerator.extractModifiedFields(diff);

// 4. Generate next version number
const currentVersion = await this.getCurrentVersion(request.original_id);
const nextVersion = this.versionGenerator.generateNextVersion(
currentVersion,
request.change_type
);

// 5. Create amendment draft
const amendment: Amendment = {
amendment_id: this.generateUUID(),
original_id: request.original_id,
amendment_number: 0, // Set by trigger
version: nextVersion,
amendment_date: new Date(),
amended_by: request.amended_by,
amendment_reason_category: request.reason_category,
amendment_reason_text: request.reason_text,
content_before: currentContent,
content_after: request.content_after,
diff: diff,
fields_modified: fieldsModified,
change_type: request.change_type,
impact_assessment: request.impact_assessment,
amendment_status: AmendmentStatus.DRAFT,
approval_chain: this.generateApprovalChain(request.change_type),
author_signature: null, // Applied on submit
can_rollback: true,
regulatory_impact: request.impact_assessment.compliance_impact !== 'NONE',
validation_impact: request.impact_assessment.validation_required,
notification_sent: false
};

await this.saveAmendment(amendment);

return amendment;
}

/**
* Submit amendment for approval
*/
async submitAmendment(request: {
amendment_id: string;
e_signature_credentials: ESignatureCredentials;
}): Promise<Amendment> {
const amendment = await this.fetchAmendment(request.amendment_id);

// Apply author signature
const signature = await this.eSignatureService.sign({
signer_id: amendment.amended_by,
signed_object_type: 'AMENDMENT',
signed_object_id: amendment.amendment_id,
signature_meaning: SignatureMeaning.APPROVED,
credentials: request.e_signature_credentials
});

amendment.author_signature = signature;

// Update status based on change type
if (amendment.change_type === ChangeType.MINOR) {
// Minor changes auto-approve
amendment.amendment_status = AmendmentStatus.ACTIVE;
} else {
amendment.amendment_status = AmendmentStatus.PENDING_REVIEW;
}

await this.updateAmendment(amendment);

// Notify reviewers/approvers
await this.notificationService.notifyApprovers(amendment);

return amendment;
}

/**
* Approve amendment
*/
async approveAmendment(request: {
amendment_id: string;
approver_id: string;
e_signature_credentials: ESignatureCredentials;
}): Promise<Amendment> {
const amendment = await this.fetchAmendment(request.amendment_id);

// Verify approver has authority
await this.verifyApprovalAuthority(request.approver_id, amendment.change_type);

// Apply approver signature
const signature = await this.eSignatureService.sign({
signer_id: request.approver_id,
signed_object_type: 'AMENDMENT',
signed_object_id: amendment.amendment_id,
signature_meaning: SignatureMeaning.APPROVED,
credentials: request.e_signature_credentials
});

amendment.approver_signature = signature;
amendment.amendment_status = AmendmentStatus.ACTIVE;

await this.updateAmendment(amendment);

// Apply the amendment to the record
await this.applyAmendment(amendment);

// Notify stakeholders
if (amendment.impact_assessment.stakeholders_to_notify.length > 0) {
await this.notificationService.notifyStakeholders(amendment);
amendment.notification_sent = true;
amendment.notification_timestamp = new Date();
await this.updateAmendment(amendment);
}

return amendment;
}

/**
* Rollback amendment (creates new amendment)
*/
async rollbackAmendment(request: {
amendment_id_to_rollback: string;
rolled_back_by: string;
rollback_reason: string;
e_signature_credentials: ESignatureCredentials;
}): Promise<Amendment> {
const originalAmendment = await this.fetchAmendment(request.amendment_id_to_rollback);

if (!originalAmendment.can_rollback) {
throw new Error('This amendment cannot be rolled back');
}

// Create new amendment that reverses the original
const rollbackAmendment = await this.createAmendment({
original_id: originalAmendment.original_id,
amended_by: request.rolled_back_by,
reason_category: AmendmentReasonCategory.ROLLBACK,
reason_text: `Rollback of Amendment #${originalAmendment.amendment_number}: ${request.rollback_reason}`,
content_after: originalAmendment.content_before, // Revert to before state
change_type: originalAmendment.change_type,
impact_assessment: originalAmendment.impact_assessment
});

rollbackAmendment.rollback_amendment_id = originalAmendment.amendment_id;

// Mark original as rolled back
originalAmendment.rolled_back_by = rollbackAmendment.amendment_id;
originalAmendment.can_rollback = false;
await this.updateAmendment(originalAmendment);

// Submit rollback amendment
await this.submitAmendment({
amendment_id: rollbackAmendment.amendment_id,
e_signature_credentials: request.e_signature_credentials
});

return rollbackAmendment;
}

/**
* Generate approval chain based on change type
*/
private generateApprovalChain(changeType: ChangeType): ApprovalStep[] {
switch (changeType) {
case ChangeType.MINOR:
return []; // No approval needed

case ChangeType.MODERATE:
return [
{
step_number: 1,
step_type: 'REVIEW',
assigned_to: 'ROLE:QA_REVIEWER',
decision: 'PENDING'
}
];

case ChangeType.MAJOR:
return [
{
step_number: 1,
step_type: 'REVIEW',
assigned_to: 'ROLE:QA_REVIEWER',
decision: 'PENDING'
},
{
step_number: 2,
step_type: 'APPROVE',
assigned_to: 'ROLE:QA_MANAGER',
decision: 'PENDING'
}
];
}
}
}

4.3 Diff Generation

/**
* Generate structured diff between two JSONB objects
*/
export class DiffGenerator {
/**
* Generate diff using deep comparison
*/
generateDiff(before: JSONB, after: JSONB): JSONB {
const diff = {
added: {},
modified: {},
removed: {}
};

this.comparePaths(before, after, '', diff);

return diff;
}

/**
* Recursively compare object paths
*/
private comparePaths(before: any, after: any, path: string, diff: any): void {
// Handle null/undefined
if (before === null || before === undefined) {
if (after !== null && after !== undefined) {
diff.added[path] = after;
}
return;
}

if (after === null || after === undefined) {
diff.removed[path] = before;
return;
}

// Handle objects
if (typeof before === 'object' && typeof after === 'object') {
const beforeKeys = Object.keys(before);
const afterKeys = Object.keys(after);

// Check for added keys
for (const key of afterKeys) {
if (!beforeKeys.includes(key)) {
const newPath = path ? `${path}.${key}` : key;
diff.added[newPath] = after[key];
}
}

// Check for removed keys
for (const key of beforeKeys) {
if (!afterKeys.includes(key)) {
const newPath = path ? `${path}.${key}` : key;
diff.removed[newPath] = before[key];
}
}

// Check for modified keys
for (const key of beforeKeys) {
if (afterKeys.includes(key)) {
const newPath = path ? `${path}.${key}` : key;
this.comparePaths(before[key], after[key], newPath, diff);
}
}

return;
}

// Handle primitives
if (before !== after) {
diff.modified[path] = {
before: before,
after: after
};
}
}

/**
* Extract list of modified field paths
*/
extractModifiedFields(diff: JSONB): string[] {
const fields = new Set<string>();

for (const path of Object.keys(diff.added)) {
fields.add(path);
}

for (const path of Object.keys(diff.modified)) {
fields.add(path);
}

for (const path of Object.keys(diff.removed)) {
fields.add(path);
}

return Array.from(fields).sort();
}
}

4.4 Version Generator

/**
* Semantic version generator
*/
export class VersionGenerator {
/**
* Generate next version based on change type
* Major: X.0.0 (breaking changes affecting compliance)
* Minor: X.Y.0 (clarifications, updates)
* Patch: X.Y.Z (typo corrections, formatting)
*/
generateNextVersion(current: string, changeType: ChangeType): string {
const parts = current.split('.').map(Number);
const [major, minor, patch] = parts;

switch (changeType) {
case ChangeType.MAJOR:
return `${major + 1}.0.0`;

case ChangeType.MODERATE:
return `${major}.${minor + 1}.0`;

case ChangeType.MINOR:
return `${major}.${minor}.${patch + 1}`;
}
}

/**
* Parse semantic version
*/
parseVersion(version: string): { major: number; minor: number; patch: number } {
const parts = version.split('.').map(Number);
return {
major: parts[0],
minor: parts[1],
patch: parts[2]
};
}

/**
* Compare versions
*/
compareVersions(v1: string, v2: string): number {
const p1 = this.parseVersion(v1);
const p2 = this.parseVersion(v2);

if (p1.major !== p2.major) return p1.major - p2.major;
if (p1.minor !== p2.minor) return p1.minor - p2.minor;
return p1.patch - p2.patch;
}
}

5. Version Control

5.1 Version History Query

/**
* Version history service
*/
export class VersionHistoryService {
/**
* Get complete version history for a record
*/
async getVersionHistory(original_id: string): Promise<VersionHistoryTimeline> {
const original = await this.fetchOriginal(original_id);
const amendments = await this.fetchAmendments(original_id);

const timeline: VersionHistoryTimeline = {
original_id,
original_created_at: original.created_at,
original_version: '1.0.0',
current_version: await this.getCurrentVersion(original_id),
total_amendments: amendments.length,
versions: []
};

// Add original as version 1.0.0
timeline.versions.push({
version: '1.0.0',
amendment_number: 0,
amendment_date: original.created_at,
amended_by: original.created_by,
change_type: null,
reason: 'Original creation',
fields_modified: [],
content_snapshot: original.original_content
});

// Add each amendment
for (const amendment of amendments) {
timeline.versions.push({
version: amendment.version,
amendment_number: amendment.amendment_number,
amendment_date: amendment.amendment_date,
amended_by: amendment.amended_by,
change_type: amendment.change_type,
reason: amendment.amendment_reason_text,
fields_modified: amendment.fields_modified,
content_snapshot: amendment.content_after,
diff: amendment.diff
});
}

return timeline;
}

/**
* Compare two versions
*/
async compareVersions(
original_id: string,
version1: string,
version2: string
): Promise<VersionComparison> {
const history = await this.getVersionHistory(original_id);

const v1 = history.versions.find(v => v.version === version1);
const v2 = history.versions.find(v => v.version === version2);

if (!v1 || !v2) {
throw new Error('Version not found');
}

const diff = this.diffGenerator.generateDiff(
v1.content_snapshot,
v2.content_snapshot
);

return {
original_id,
version_from: version1,
version_to: version2,
date_from: v1.amendment_date,
date_to: v2.amendment_date,
diff,
fields_modified: this.diffGenerator.extractModifiedFields(diff)
};
}

/**
* Get version tree visualization data
*/
async getVersionTree(original_id: string): Promise<VersionTreeNode> {
const history = await this.getVersionHistory(original_id);

// Build tree structure
const root: VersionTreeNode = {
version: '1.0.0',
amendment_number: 0,
date: history.original_created_at,
author: history.versions[0].amended_by,
change_type: null,
children: []
};

let currentNode = root;

for (let i = 1; i < history.versions.length; i++) {
const version = history.versions[i];

const node: VersionTreeNode = {
version: version.version,
amendment_number: version.amendment_number,
date: version.amendment_date,
author: version.amended_by,
change_type: version.change_type,
children: []
};

currentNode.children.push(node);
currentNode = node;
}

return root;
}
}

interface VersionHistoryTimeline {
original_id: string;
original_created_at: Date;
original_version: string;
current_version: string;
total_amendments: number;
versions: VersionSnapshot[];
}

interface VersionSnapshot {
version: string;
amendment_number: number;
amendment_date: Date;
amended_by: string;
change_type: ChangeType | null;
reason: string;
fields_modified: string[];
content_snapshot: JSONB;
diff?: JSONB;
}

interface VersionComparison {
original_id: string;
version_from: string;
version_to: string;
date_from: Date;
date_to: Date;
diff: JSONB;
fields_modified: string[];
}

interface VersionTreeNode {
version: string;
amendment_number: number;
date: Date;
author: string;
change_type: ChangeType | null;
children: VersionTreeNode[];
}

6. Chain of Custody

6.1 Data Model

/**
* Chain of custody tracking
* Compliance: 21 CFR 11.10(e) - audit trail of access
*/
interface CustodyEvent {
// Event identifiers
custody_event_id: string; // UUID v7
original_id: string; // Record being accessed
event_type: CustodyEventType;
event_timestamp: Date;

// Actor
user_id: string;
user_name: string;
user_role: string;

// Access details
access_method: AccessMethod;
access_purpose: string; // Required free text
access_location: string; // IP address
access_device: string; // Device fingerprint

// Transfer details (if applicable)
transferred_to?: string; // User ID or external party
transfer_destination?: string; // e.g., "FDA Submission Portal"
transfer_method?: TransferMethod;

// Download/print details (if applicable)
download_format?: DownloadFormat;
download_file_hash?: string; // SHA-256 of downloaded file
print_job_id?: string;

// External distribution
external_party?: string; // e.g., "FDA", "Auditor"
distribution_tracking_number?: string;

// Audit
session_id: string;
tenant_id: string;
}

enum CustodyEventType {
VIEW = 'VIEW',
DOWNLOAD = 'DOWNLOAD',
PRINT = 'PRINT',
TRANSFER = 'TRANSFER',
EXTERNAL_DISTRIBUTION = 'EXTERNAL_DISTRIBUTION',
EXPORT = 'EXPORT',
COPY_GENERATED = 'COPY_GENERATED'
}

enum AccessMethod {
WEB_UI = 'WEB_UI',
API = 'API',
MOBILE_APP = 'MOBILE_APP',
EXPORT_PACKAGE = 'EXPORT_PACKAGE'
}

enum TransferMethod {
EMAIL = 'EMAIL',
SECURE_FILE_TRANSFER = 'SECURE_FILE_TRANSFER',
PHYSICAL_MEDIA = 'PHYSICAL_MEDIA',
PORTAL_UPLOAD = 'PORTAL_UPLOAD'
}

enum DownloadFormat {
PDF = 'PDF',
JSON = 'JSON',
XML = 'XML',
CSV = 'CSV'
}

PostgreSQL Schema

-- Chain of custody events
CREATE TABLE custody_events (
-- Event identifiers
custody_event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
original_id UUID NOT NULL REFERENCES original_records(original_id),
event_type VARCHAR(50) NOT NULL,
event_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),

-- Actor
user_id UUID NOT NULL REFERENCES users(user_id),
user_name VARCHAR(255) NOT NULL,
user_role VARCHAR(100) NOT NULL,

-- Access details
access_method VARCHAR(30) NOT NULL,
access_purpose TEXT NOT NULL,
access_location INET NOT NULL,
access_device VARCHAR(255) NOT NULL,

-- Transfer details
transferred_to UUID REFERENCES users(user_id),
transfer_destination VARCHAR(255),
transfer_method VARCHAR(50),

-- Download/print details
download_format VARCHAR(20),
download_file_hash VARCHAR(64),
print_job_id VARCHAR(100),

-- External distribution
external_party VARCHAR(255),
distribution_tracking_number VARCHAR(100),

-- Audit
session_id UUID NOT NULL,
tenant_id UUID NOT NULL REFERENCES tenants(tenant_id),

-- Constraints
CHECK (event_type IN ('VIEW', 'DOWNLOAD', 'PRINT', 'TRANSFER', 'EXTERNAL_DISTRIBUTION', 'EXPORT', 'COPY_GENERATED'))
);

-- Indexes
CREATE INDEX idx_custody_events_original ON custody_events(original_id);
CREATE INDEX idx_custody_events_timestamp ON custody_events(event_timestamp);
CREATE INDEX idx_custody_events_user ON custody_events(user_id);
CREATE INDEX idx_custody_events_type ON custody_events(event_type);
CREATE INDEX idx_custody_events_tenant ON custody_events(tenant_id);

6.2 Custody Tracking Service

/**
* Chain of custody service
*/
export class ChainOfCustodyService {
/**
* Log custody event
*/
async logCustodyEvent(event: Partial<CustodyEvent>): Promise<CustodyEvent> {
const custodyEvent: CustodyEvent = {
custody_event_id: this.generateUUID(),
event_timestamp: new Date(),
...event
} as CustodyEvent;

await this.saveCustodyEvent(custodyEvent);

return custodyEvent;
}

/**
* Log view access
*/
async logView(request: {
original_id: string;
user_id: string;
purpose: string;
access_method: AccessMethod;
}): Promise<void> {
await this.logCustodyEvent({
original_id: request.original_id,
event_type: CustodyEventType.VIEW,
user_id: request.user_id,
access_purpose: request.purpose,
access_method: request.access_method,
access_location: await this.getClientIP(),
access_device: await this.getDeviceFingerprint(),
session_id: await this.getSessionID()
});
}

/**
* Log download with hash verification
*/
async logDownload(request: {
original_id: string;
user_id: string;
purpose: string;
format: DownloadFormat;
file_content: Buffer;
}): Promise<void> {
const fileHash = this.hashGenerator.generateHash(request.file_content);

await this.logCustodyEvent({
original_id: request.original_id,
event_type: CustodyEventType.DOWNLOAD,
user_id: request.user_id,
access_purpose: request.purpose,
access_method: AccessMethod.WEB_UI,
access_location: await this.getClientIP(),
access_device: await this.getDeviceFingerprint(),
download_format: request.format,
download_file_hash: fileHash,
session_id: await this.getSessionID()
});
}

/**
* Log external distribution (e.g., regulatory submission)
*/
async logExternalDistribution(request: {
original_id: string;
user_id: string;
external_party: string;
destination: string;
tracking_number?: string;
transfer_method: TransferMethod;
}): Promise<void> {
await this.logCustodyEvent({
original_id: request.original_id,
event_type: CustodyEventType.EXTERNAL_DISTRIBUTION,
user_id: request.user_id,
access_purpose: `External distribution to ${request.external_party}`,
access_method: AccessMethod.EXPORT_PACKAGE,
access_location: await this.getClientIP(),
access_device: await this.getDeviceFingerprint(),
external_party: request.external_party,
transfer_destination: request.destination,
transfer_method: request.transfer_method,
distribution_tracking_number: request.tracking_number,
session_id: await this.getSessionID()
});
}

/**
* Get custody chain report
*/
async getCustodyChainReport(original_id: string): Promise<CustodyChainReport> {
const events = await this.fetchCustodyEvents(original_id);

return {
original_id,
total_events: events.length,
first_access: events[0]?.event_timestamp,
last_access: events[events.length - 1]?.event_timestamp,
view_count: events.filter(e => e.event_type === CustodyEventType.VIEW).length,
download_count: events.filter(e => e.event_type === CustodyEventType.DOWNLOAD).length,
print_count: events.filter(e => e.event_type === CustodyEventType.PRINT).length,
external_distribution_count: events.filter(e => e.event_type === CustodyEventType.EXTERNAL_DISTRIBUTION).length,
unique_users: new Set(events.map(e => e.user_id)).size,
events
};
}
}

interface CustodyChainReport {
original_id: string;
total_events: number;
first_access?: Date;
last_access?: Date;
view_count: number;
download_count: number;
print_count: number;
external_distribution_count: number;
unique_users: number;
events: CustodyEvent[];
}

7. React UI Components

7.1 Amendment History Timeline

/**
* Amendment history timeline component
*/
import React from 'react';
import { Timeline, Card, Tag, Tooltip } from 'antd';
import { FileTextOutlined, UserOutlined, CalendarOutlined } from '@ant-design/icons';

interface AmendmentHistoryTimelineProps {
original_id: string;
}

export const AmendmentHistoryTimeline: React.FC<AmendmentHistoryTimelineProps> = ({
original_id
}) => {
const [history, setHistory] = React.useState<VersionHistoryTimeline | null>(null);

React.useEffect(() => {
// Fetch version history
fetch(`/api/v1/records/${original_id}/history`)
.then(res => res.json())
.then(setHistory);
}, [original_id]);

if (!history) return <div>Loading...</div>;

return (
<Card title="Amendment History">
<Timeline mode="left">
{history.versions.map((version, idx) => (
<Timeline.Item
key={version.version}
color={idx === 0 ? 'green' : version.change_type === 'MAJOR' ? 'red' : 'blue'}
label={<span>{new Date(version.amendment_date).toLocaleDateString()}</span>}
>
<div>
<strong>Version {version.version}</strong>
{version.change_type && (
<Tag color={
version.change_type === 'MAJOR' ? 'red' :
version.change_type === 'MODERATE' ? 'orange' : 'green'
}>
{version.change_type}
</Tag>
)}
</div>
<div style={{ color: '#666', fontSize: '0.9em' }}>
<UserOutlined /> {version.amended_by}
</div>
<div style={{ marginTop: 8 }}>
{version.reason}
</div>
{version.fields_modified.length > 0 && (
<div style={{ marginTop: 8 }}>
<strong>Modified fields:</strong>{' '}
{version.fields_modified.slice(0, 3).join(', ')}
{version.fields_modified.length > 3 && (
<Tooltip title={version.fields_modified.join(', ')}>
<span style={{ color: '#1890ff', cursor: 'pointer' }}>
{' '}+{version.fields_modified.length - 3} more
</span>
</Tooltip>
)}
</div>
)}
</Timeline.Item>
))}
</Timeline>
</Card>
);
};

7.2 Version Comparison Viewer

/**
* Version comparison diff viewer
*/
import React from 'react';
import { Card, Select, Button, Descriptions } from 'antd';
import { DiffEditor } from '@monaco-editor/react';

interface VersionComparisonViewerProps {
original_id: string;
}

export const VersionComparisonViewer: React.FC<VersionComparisonViewerProps> = ({
original_id
}) => {
const [history, setHistory] = React.useState<VersionHistoryTimeline | null>(null);
const [version1, setVersion1] = React.useState<string>('');
const [version2, setVersion2] = React.useState<string>('');
const [comparison, setComparison] = React.useState<VersionComparison | null>(null);

React.useEffect(() => {
fetch(`/api/v1/records/${original_id}/history`)
.then(res => res.json())
.then(data => {
setHistory(data);
if (data.versions.length >= 2) {
setVersion1(data.versions[0].version);
setVersion2(data.versions[1].version);
}
});
}, [original_id]);

const handleCompare = () => {
fetch(`/api/v1/records/${original_id}/compare?v1=${version1}&v2=${version2}`)
.then(res => res.json())
.then(setComparison);
};

if (!history) return <div>Loading...</div>;

return (
<Card title="Version Comparison">
<div style={{ marginBottom: 16 }}>
<Select
value={version1}
onChange={setVersion1}
style={{ width: 200, marginRight: 16 }}
>
{history.versions.map(v => (
<Select.Option key={v.version} value={v.version}>
Version {v.version}
</Select.Option>
))}
</Select>

<span style={{ margin: '0 8px' }}>vs</span>

<Select
value={version2}
onChange={setVersion2}
style={{ width: 200, marginRight: 16 }}
>
{history.versions.map(v => (
<Select.Option key={v.version} value={v.version}>
Version {v.version}
</Select.Option>
))}
</Select>

<Button type="primary" onClick={handleCompare}>
Compare
</Button>
</div>

{comparison && (
<>
<Descriptions bordered size="small" style={{ marginBottom: 16 }}>
<Descriptions.Item label="From Version">
{comparison.version_from}
</Descriptions.Item>
<Descriptions.Item label="To Version">
{comparison.version_to}
</Descriptions.Item>
<Descriptions.Item label="Date Range">
{new Date(comparison.date_from).toLocaleDateString()} -{' '}
{new Date(comparison.date_to).toLocaleDateString()}
</Descriptions.Item>
<Descriptions.Item label="Fields Modified" span={3}>
{comparison.fields_modified.join(', ')}
</Descriptions.Item>
</Descriptions>

<DiffEditor
height="500px"
language="json"
original={JSON.stringify(
history.versions.find(v => v.version === version1)?.content_snapshot,
null,
2
)}
modified={JSON.stringify(
history.versions.find(v => v.version === version2)?.content_snapshot,
null,
2
)}
options={{
readOnly: true,
renderSideBySide: true
}}
/>
</>
)}
</Card>
);
};

7.3 Chain of Custody Report

/**
* Chain of custody report component
*/
import React from 'react';
import { Card, Table, Tag, Tooltip } from 'antd';
import {
EyeOutlined,
DownloadOutlined,
PrinterOutlined,
SendOutlined
} from '@ant-design/icons';

interface ChainOfCustodyReportProps {
original_id: string;
}

export const ChainOfCustodyReport: React.FC<ChainOfCustodyReportProps> = ({
original_id
}) => {
const [report, setReport] = React.useState<CustodyChainReport | null>(null);

React.useEffect(() => {
fetch(`/api/v1/records/${original_id}/custody-chain`)
.then(res => res.json())
.then(setReport);
}, [original_id]);

if (!report) return <div>Loading...</div>;

const getEventIcon = (eventType: string) => {
switch (eventType) {
case 'VIEW': return <EyeOutlined />;
case 'DOWNLOAD': return <DownloadOutlined />;
case 'PRINT': return <PrinterOutlined />;
case 'EXTERNAL_DISTRIBUTION': return <SendOutlined />;
default: return null;
}
};

const columns = [
{
title: 'Timestamp',
dataIndex: 'event_timestamp',
key: 'timestamp',
render: (ts: string) => new Date(ts).toLocaleString(),
sorter: (a, b) => new Date(a.event_timestamp).getTime() - new Date(b.event_timestamp).getTime()
},
{
title: 'Event Type',
dataIndex: 'event_type',
key: 'type',
render: (type: string) => (
<Tag icon={getEventIcon(type)} color="blue">
{type}
</Tag>
)
},
{
title: 'User',
dataIndex: 'user_name',
key: 'user',
render: (name: string, record: CustodyEvent) => (
<Tooltip title={record.user_role}>
{name}
</Tooltip>
)
},
{
title: 'Purpose',
dataIndex: 'access_purpose',
key: 'purpose',
ellipsis: true
},
{
title: 'Location',
dataIndex: 'access_location',
key: 'location'
},
{
title: 'Method',
dataIndex: 'access_method',
key: 'method'
},
{
title: 'External Party',
dataIndex: 'external_party',
key: 'external',
render: (party: string) => party || '-'
}
];

return (
<Card
title="Chain of Custody Report"
extra={
<div>
Total Events: {report.total_events} |
Unique Users: {report.unique_users} |
External Distributions: {report.external_distribution_count}
</div>
}
>
<Table
columns={columns}
dataSource={report.events}
rowKey="custody_event_id"
pagination={{ pageSize: 20 }}
/>
</Card>
);
};

8. API Endpoints Summary

8.1 Original Records

// Create original record
POST /api/v1/records/original
Request: CreateOriginalRecordRequest
Response: CreateOriginalRecordResponse

// Get original record
GET /api/v1/records/original/:original_id
Response: GetOriginalRecordResponse

// Get original record metadata
GET /api/v1/records/original/:original_id/metadata
Response: OriginalRecord (metadata only)

8.2 True Copies

// Generate true copy
POST /api/v1/records/true-copy
Request: GenerateTrueCopyRequest
Response: GenerateTrueCopyResponse

// Generate batch true copies
POST /api/v1/records/true-copy/batch
Request: GenerateBatchTrueCopiesRequest
Response: GenerateBatchTrueCopiesResponse

// Get true copy
GET /api/v1/records/true-copy/:copy_id
Response: TrueCopy

// Export true copy package
POST /api/v1/records/true-copy/export
Request: ExportTrueCopyPackageRequest
Response: Blob (ZIP/PDF)

8.3 Certified Copies

// Initiate certification
POST /api/v1/records/certified-copy/initiate
Request: InitiateCertificationRequest
Response: CertifiedCopy

// Review certification
POST /api/v1/records/certified-copy/:certification_id/review
Request: ReviewCertificationRequest
Response: CertifiedCopy

// Approve certification
POST /api/v1/records/certified-copy/:certification_id/approve
Request: ApproveCertificationRequest
Response: CertifiedCopy

// Finalize certification
POST /api/v1/records/certified-copy/:certification_id/finalize
Request: FinalizeCertificationRequest
Response: CertifiedCopy

// Get certified copy
GET /api/v1/records/certified-copy/:copy_id
Response: CertifiedCopy

// Download certificate PDF
GET /api/v1/records/certified-copy/:copy_id/certificate.pdf
Response: Blob (PDF)

8.4 Amendments

// Create amendment draft
POST /api/v1/records/:original_id/amendments
Request: CreateAmendmentRequest
Response: Amendment

// Submit amendment for approval
POST /api/v1/records/amendments/:amendment_id/submit
Request: SubmitAmendmentRequest
Response: Amendment

// Approve amendment
POST /api/v1/records/amendments/:amendment_id/approve
Request: ApproveAmendmentRequest
Response: Amendment

// Rollback amendment
POST /api/v1/records/amendments/:amendment_id/rollback
Request: RollbackAmendmentRequest
Response: Amendment

// Get amendment
GET /api/v1/records/amendments/:amendment_id
Response: Amendment

// Get amendments for record
GET /api/v1/records/:original_id/amendments
Response: Amendment[]

8.5 Version History

// Get version history
GET /api/v1/records/:original_id/history
Response: VersionHistoryTimeline

// Compare versions
GET /api/v1/records/:original_id/compare?v1={version1}&v2={version2}
Response: VersionComparison

// Get version tree
GET /api/v1/records/:original_id/version-tree
Response: VersionTreeNode

// Get specific version snapshot
GET /api/v1/records/:original_id/versions/:version
Response: VersionSnapshot

8.6 Chain of Custody

// Log custody event
POST /api/v1/records/:original_id/custody
Request: LogCustodyEventRequest
Response: CustodyEvent

// Get custody chain report
GET /api/v1/records/:original_id/custody-chain
Response: CustodyChainReport

// Get custody events (paginated)
GET /api/v1/records/:original_id/custody-events?page={page}&limit={limit}
Response: PaginatedCustodyEvents

9. Compliance Mapping

9.1 FDA 21 CFR Part 11 Requirements

RegulationRequirementImplementation
§11.10(a)Validation of systems to ensure accuracy, reliability, consistent intended performance- Automated hash verification
- Immutability enforcement via DB triggers
- E-signature validation
§11.10(e)Use of secure, computer-generated, time-stamped audit trails to independently record the date and time of operator entries and actions- custody_events table with timestamptz
- Immutable audit trail (no DELETE allowed)
- Automatic timestamp generation
§11.10(e)Documentation of record copies as true copies- True copy verification via hash matching
- Watermark distinguishing copies
- Copy metadata tracking
- Copy count per original
§11.10(k)(1)Use of electronic records in non-rewritable, non-erasable format- original_records table immutability trigger
- Amendment chain (no UPDATE/DELETE on originals)
- SHA-256 hash for integrity verification
§11.10(k)(2)Protection of records to enable their accurate and ready retrieval throughout the records retention period- retention_policy with calculated destruction dates
- legal_hold flag
- Multi-level backup strategy
§11.50Signed electronic records shall contain information associated with the signing- signature_manifestation with printed name, date, meaning
- e_signatures table with full audit trail
- Signature method tracking
§11.70Signature/record linking- e_signature.signed_object_id foreign key
- Signature embedded in certified_copies and amendments
- Hash verification of signed content

9.2 ISO 13485 Medical Device QMS

RequirementImplementation
4.2.4 Control of records- Immutable original records
- Version control with semantic versioning
- Retention policy enforcement
4.2.5 Documentation requirements- Complete amendment history
- Before/after diff tracking
- Approval chain documentation
7.5.11 Traceability- Chain of custody tracking
- External distribution logging
- Transfer tracking

9.3 EU GMP Annex 11 Computerised Systems

RequirementImplementation
6. Audit Trail- custody_events immutable log
- Timestamped with user identification
- Reason for change captured (amendment_reason_text)
8. Printouts- Print job logging in custody_events
- Watermark on copies
- Certificate of authenticity for certified copies
9. Audit Trail Review- Chain of custody report
- Version history timeline
- Amendment history visualization

10. Testing & Validation

10.1 Unit Tests

/**
* Hash generation tests
*/
describe('RecordHashGenerator', () => {
it('should generate consistent hash for same content', () => {
const generator = new RecordHashGenerator();
const content = { field1: 'value1', field2: 'value2' };

const hash1 = generator.generateHash(content);
const hash2 = generator.generateHash(content);

expect(hash1).toBe(hash2);
});

it('should generate different hash for different content', () => {
const generator = new RecordHashGenerator();
const content1 = { field1: 'value1' };
const content2 = { field1: 'value2' };

const hash1 = generator.generateHash(content1);
const hash2 = generator.generateHash(content2);

expect(hash1).not.toBe(hash2);
});

it('should verify hash correctly', () => {
const generator = new RecordHashGenerator();
const content = { field1: 'value1' };
const hash = generator.generateHash(content);

const isValid = generator.verifyHash(content, hash);

expect(isValid).toBe(true);
});
});

/**
* True copy tests
*/
describe('TrueCopyService', () => {
it('should generate verified true copy', async () => {
const service = new TrueCopyService(hashGenerator, auditLogger);

const trueCopy = await service.generateTrueCopy({
original_id: 'original-123',
purpose: CopyPurpose.REGULATORY_SUBMISSION,
created_by: 'user-456'
});

expect(trueCopy.verification_status).toBe(VerificationStatus.VERIFIED);
expect(trueCopy.hash_match_confirmed).toBe(true);
});

it('should fail verification if original is tampered', async () => {
// Test implementation
});
});

/**
* Amendment tests
*/
describe('AmendmentService', () => {
it('should create amendment with diff', async () => {
const service = new AmendmentService(diffGenerator, versionGenerator, eSignatureService, notificationService);

const amendment = await service.createAmendment({
original_id: 'original-123',
amended_by: 'user-456',
reason_category: AmendmentReasonCategory.CORRECTION,
reason_text: 'Fix typo in field1',
content_after: { field1: 'corrected_value' },
change_type: ChangeType.MINOR,
impact_assessment: { /* ... */ }
});

expect(amendment.fields_modified).toContain('field1');
expect(amendment.diff.modified).toHaveProperty('field1');
});

it('should auto-approve minor amendments', async () => {
// Test implementation
});

it('should require approval for major amendments', async () => {
// Test implementation
});
});

10.2 Integration Tests

/**
* End-to-end certification workflow test
*/
describe('Certification Workflow', () => {
it('should complete full certification workflow', async () => {
// 1. Create original
const original = await createOriginalRecord({ /* ... */ });

// 2. Initiate certification
const certification = await initiateCertification({
original_id: original.original_id,
certification_purpose: 'FDA submission',
certification_template_id: 'fda-template-1'
});

expect(certification.workflow_status).toBe(CertificationWorkflowStatus.INITIATED);

// 3. Review
await reviewCertification({
certification_id: certification.certification_id,
reviewer_id: 'qa-reviewer',
decision: 'APPROVED'
});

// 4. Approve
await approveCertification({
certification_id: certification.certification_id,
approver_id: 'qa-manager',
decision: 'APPROVED'
});

// 5. Finalize
const finalCertification = await finalizeCertification({
certification_id: certification.certification_id,
certifier_id: 'compliance-officer'
});

expect(finalCertification.workflow_status).toBe(CertificationWorkflowStatus.CERTIFIED);
expect(finalCertification.certificate_pdf_url).toBeTruthy();
});
});

11. Deployment & Operations

11.1 Database Migration

-- Run migrations in order
\i migrations/001_create_original_records.sql
\i migrations/002_create_true_copies.sql
\i migrations/003_create_certified_copies.sql
\i migrations/004_create_amendments.sql
\i migrations/005_create_custody_events.sql
\i migrations/006_create_e_signatures.sql
\i migrations/007_create_indexes.sql
\i migrations/008_create_triggers.sql

11.2 Monitoring & Alerts

# Datadog monitors
monitors:
- name: "Original Record Hash Verification Failures"
type: metric alert
query: "sum(last_5m):sum:bioqms.true_copy.verification_failed{*} > 5"
message: "Hash verification failures detected - potential data integrity issue"

- name: "High Custody Event Volume"
type: metric alert
query: "sum(last_1h):sum:bioqms.custody_events.count{*} > 10000"
message: "Unusual custody event volume - investigate potential audit log attack"

- name: "Certification Workflow Stuck"
type: metric alert
query: "avg(last_1h):avg:bioqms.certification.pending_duration{*} > 86400"
message: "Certification workflows stuck in pending state for >24 hours"

12. Summary

This document defines a comprehensive original/copy/amendment tracking system that ensures full compliance with FDA 21 CFR Part 11, ISO 13485, and EU GMP Annex 11 requirements.

Key Features:

  1. Immutable originals with cryptographic hash verification
  2. Verified true copies with automatic hash validation
  3. Certified copies with qualified e-signatures and approval workflows
  4. Complete amendment chain with before/after diff tracking
  5. Semantic versioning (major.minor.patch)
  6. Chain of custody logging for all access and distribution
  7. Version comparison and timeline visualization
  8. Rollback capability (creates new amendment, not DELETE)

Compliance Coverage:

  • ✅ 21 CFR 11.10(a) - Validated system with hash verification
  • ✅ 21 CFR 11.10(e) - Audit trails and true copy documentation
  • ✅ 21 CFR 11.10(k)(1) - Non-rewritable, non-erasable records
  • ✅ 21 CFR 11.50 - Signature manifestations
  • ✅ ISO 13485 4.2.4 - Record control
  • ✅ EU GMP Annex 11.6 - Audit trail

Next Steps:

  1. Implement React UI components (amendment timeline, version comparison, custody report)
  2. Deploy database schema and triggers
  3. Conduct IQ/OQ/PQ validation
  4. Train users on amendment workflows
  5. Establish SOPs for certified copy generation and regulatory submissions

Document Control:

  • Version: 1.0.0
  • Approved By: [Compliance Officer Name]
  • Approval Date: [YYYY-MM-DD]
  • Next Review: [YYYY-MM-DD + 1 year]
  • Change History: See amendment chain in QMS

This document was generated as part of Task D.5.6: Build Original/Copy/Amendment Tracking System for the BIO-QMS platform.