Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save gcrsaldanha/51e122e3ddafeac60a5bf2c414098215 to your computer and use it in GitHub Desktop.

Select an option

Save gcrsaldanha/51e122e3ddafeac60a5bf2c414098215 to your computer and use it in GitHub Desktop.
Audit Confirmation Questions and Responses Architecture - Cross-service documentation for fund-admin and carta-web integration

Audit Confirmation Questions and Responses Architecture

Last Updated: 2025-11-18 Purpose: Document the audit confirmation workflow, question types, and cross-service integration for future modifications

Document Updates

  • Clarified distinction between base confirmation fields (structured form responses) vs audit questions (document uploads)
  • Added Mermaid sequence diagram showing complete cross-service event flow
  • Added dependencies diagram showing what models/events/services need updates
  • Updated step-by-step guide for adding optional structured fields (text, dates, decimals)
  • Added PDF generation details and template update instructions
  • Added deployment strategy with rollback considerations

Target Use Case: Adding optional structured fields (text, dates, amounts) to audit confirmations that appear in the confirmation PDF for auditors.


Table of Contents

  1. Overview
  2. Response Types: Structured Fields vs Document Uploads
  3. Sequence Diagram
  4. Question Types
  5. Data Models
  6. Cross-Service Architecture
  7. Event Flow
  8. Response Data Structure
  9. Dependencies Diagram
  10. Adding New Questions

Overview

Audit confirmations are a fund administration workflow where auditors request portfolio companies (PortCos) to confirm their holdings data and provide supporting documents for annual audits. This is a formal process mandated by auditing firms.

Key Participants

  • Auditors: Create and review audit confirmations (via fund-admin)
  • Fund Admins: Coordinate the confirmation process
  • General Partners (GPs): Optionally approve confirmations before sending
  • Portfolio Companies: Respond to confirmations and upload documents

Services Involved

  • fund-admin: Owns audit workflow state, stores confirmation data
  • carta-web: Handles PortCo UI/UX, sends notifications, manages document uploads
  • Integration: Kafka events for cross-service communication

Response Types: Structured Fields vs Document Uploads

CRITICAL DISTINCTION: The audit confirmation has two types of responses:

1. Base Confirmation Fields (Always Present - Structured Form Responses)

These are NOT document uploads - they are structured form fields that PortCos fill out:

# Required Holdings Verification
holdings_accurate: BooleanField           # Checkbox: Are holdings accurate?
inaccurate_explanation: TextField         # Text input: If no, explain why

# Optional Payment Disclosures (form inputs)
dividends_paid: DecimalField              # Number input
interest_paid: DecimalField               # Number input
transactions_paid: DecimalField           # Number input
others_paid: DecimalField                 # Number input

# Company Information (form fields)
issuer_legal_name: CharField              # Text input: Legal company name
issuer_address: ForeignKey                # Form fields: street, city, state, zip, country
    street_address_1: CharField
    city: CharField
    state_or_province: CharField
    postal_code: CharField
    country: CharField

# Signature
issuer_user_signature: ForeignKey         # E-signature widget
    signature_text: CharField              # Typed name
    signatory_title: CharField             # Text input: Title (e.g., "CFO")
    signature_timestamp: DateTimeField     # Auto-captured

These fields are part of the BASE confirmation and are always collected - they're standard form inputs/checkboxes/dropdowns.

2. Audit Questions (Optional - Document Upload Requests)

The audit_questions ArrayField stores which optional document requests the auditor selected. These DO require file uploads:

audit_questions: ArrayField[str] = [
    "CERTIFICATE_OF_INCORPORATION",  # → File upload required
    "CAP_TABLE",                     # → File upload required
    "INCOME_STATEMENT",              # → File upload OR Carta Financials share
    "BALANCE_SHEET",                 # → File upload OR Carta Financials share
]

Naming Clarification:

  • The term "audit questions" specifically refers to the optional document requests
  • It does NOT refer to the base confirmation fields (holdings verification, payment disclosures, etc.)
  • This can be confusing because the base confirmation also asks "questions" (via form fields)

Summary Table

Response Type Input Method Always Required? Stored Where
Holdings verification Form checkbox + text ✅ Yes FundIssuerAuditConfirmation fields
Payment disclosures Form number inputs ❌ Optional FundIssuerAuditConfirmation fields
Company info Form text inputs ✅ Yes FundIssuerAuditConfirmation fields
Signature E-signature widget ✅ Yes IssuerAuditConfirmationSignature table
Document requests File uploads ❌ Optional (auditor-selected) IssuerAuditConfirmationDocument table

When adding new "questions":

  • If it's a structured field (number, text, checkbox) → Add to FundIssuerAuditConfirmation model
  • If it's a document request → Add to AuditQuestion enum + update proto + carta-web UI

Sequence Diagram

This diagram shows the complete cross-service event flow:

sequenceDiagram
    participant Auditor
    participant FA as fund-admin
    participant Kafka
    participant CW as carta-web
    participant PortCo as Portfolio Company

    Note over Auditor,FA: 1. Create Confirmation Request
    Auditor->>FA: Create audit confirmation<br/>(select issuers, questions, dates)
    FA->>FA: Create FundIssuerAuditConfirmation<br/>status: SCHEDULED

    Note over FA,CW: 2. Publish Request (send_date arrives)
    FA->>FA: Scheduled task runs:<br/>process_fund_issuer_audit_confirmations_for_send_date()
    FA->>FA: Set status: PENDING_SEND
    FA->>Kafka: Publish AuditConfirmationsRequested<br/>(holdings data, audit_questions[], IAM invite)

    Note over CW,PortCo: 3. Carta-web Processes & Notifies
    Kafka->>CW: Consume AuditConfirmationsRequested
    CW->>CW: Create IAM invitation
    CW->>PortCo: Send email with confirmation link
    CW->>Kafka: Publish AuditConfirmationRequestsReceived<br/>(delivery confirmation)

    Note over FA: 4. Update Delivery Status
    Kafka->>FA: Consume AuditConfirmationRequestsReceived
    FA->>FA: Set status: SENT<br/>Set sent_timestamp<br/>Increment notification_count
    FA->>Auditor: Email: "Confirmations sent to X companies"

    Note over PortCo,CW: 5. PortCo Responds
    PortCo->>CW: Click email link (2FA via IAM)
    CW->>PortCo: Show confirmation form<br/>(holdings verification + doc uploads)
    PortCo->>CW: Fill form fields:<br/>- holdings_accurate<br/>- payment disclosures<br/>- company info<br/>- signature
    PortCo->>CW: Upload documents for each<br/>selected audit_question
    PortCo->>CW: Submit confirmation

    Note over CW,FA: 6. Publish Response
    CW->>Kafka: Publish AuditConfirmationSubmitted<br/>(form data + document IDs)

    Note over FA: 7. Process Response
    Kafka->>FA: Consume AuditConfirmationSubmitted
    FA->>FA: Update FundIssuerAuditConfirmation:<br/>- Save form field responses<br/>- Create IssuerAuditConfirmationDocument records<br/>- Generate confirmation PDF
    FA->>FA: Set status: CONFIRMED<br/>Set confirmed_timestamp
    FA->>Auditor: Email: "Company X responded"<br/>Attach confirmation PDF
    FA->>FA: Slack notification to FA team

    Note over Auditor: 8. Review Complete
    Auditor->>FA: Download confirmation PDF
    Auditor->>Auditor: Review holdings + uploaded documents
Loading

Key Takeaways from Sequence:

  1. fund-admin initiates and owns workflow state
  2. carta-web handles all PortCo interaction (auth, UI, document uploads)
  3. 3 Kafka events coordinate the cross-service flow
  4. Document IDs from carta-web are stored as references in fund-admin
  5. PDF generation happens in fund-admin after response received

Question Types

Current Audit Questions

File: fund_admin/entity_audit/models/choices.py

class AuditQuestion(TextChoices):
    AUDIT_CONFIRMATION = ("AUDIT_CONFIRMATION", "Audit confirmation")
    CERTIFICATE_OF_INCORPORATION = ("CERTIFICATE_OF_INCORPORATION", "Certificate of Incorporation")
    CAP_TABLE = ("CAP_TABLE", "Capitalization table")
    INCOME_STATEMENT = ("INCOME_STATEMENT", "Income statement")
    BALANCE_SHEET = ("BALANCE_SHEET", "Balance sheet")

Question Behavior

  1. AUDIT_CONFIRMATION

    • Always included - this is the base confirmation document
    • Not selectable by auditors (automatic)
    • Confirms holdings accuracy
  2. Optional Document Requests (auditor-selectable):

    • CERTIFICATE_OF_INCORPORATION - Legal formation documents
    • CAP_TABLE - Capitalization table
    • INCOME_STATEMENT - Financial statement
    • BALANCE_SHEET - Financial statement

Frontend Question Selection

File: frontend/src/entity-audit/AuditConfirmations/components/AuditConfirmationRequestFields.tsx

Lines 162-247 show the checkbox UI for question selection:

  • "Select all" checkbox for bulk selection
  • Individual checkboxes for each optional question
  • Help text and tooltips explaining each question
  • Special handling for financials (income statement/balance sheet can be shared via has_shared_financials flag)

Data Models

Core Model: FundIssuerAuditConfirmation

File: fund_admin/entity_audit/models/fund_issuer_audit_confirmation.py:104

Key Fields:

# Relationships
fund_id: int                           # The fund requesting confirmation
issuer_id: UUID                        # The portfolio company
investor_relations_contact_id: int     # Contact receiving the request

# Workflow
status: str                            # Lifecycle state (see below)
as_of_date: date                       # Holdings as of this date (usually year-end)
send_date: date                        # When to send to PortCo
due_date: date                         # Response deadline
gp_approval_required: bool             # Whether GP must approve first

# Questions & Responses
audit_questions: list[str]             # ArrayField of selected AuditQuestion values
holdings_accurate: bool | None         # PortCo confirmation
inaccurate_explanation: str | None     # If holdings incorrect, why
has_shared_financials: bool            # Whether financials shared directly

# Optional Payment Disclosures
dividends_paid: Decimal | None
interest_paid: Decimal | None
transactions_paid: Decimal | None
others_paid: Decimal | None

# Issuer Information (from response)
issuer_legal_name: str | None          # Company's legal name
issuer_address: ForeignKey | None      # Company address

# Signatures
issuer_user_signature: ForeignKey | None    # PortCo signatory
gp_signature: ForeignKey | None             # GP approver (if required)

# Tracking
sent_timestamp: datetime | None
confirmed_timestamp: datetime | None
notification_count: int                # How many reminders sent
confirmation_document_id: UUID | None  # Generated PDF for auditors

# IAM Integration (for carta-web)
invite_id: UUID | None                 # IAM invite for PortCo user
invite_token: UUID | None              # Token for authentication

Status Lifecycle

class AuditConfirmationStatus(TextChoices):
    NOT_SCHEDULED = "Not scheduled"        # Initial state
    PENDING_APPROVAL = "Pending approval"  # Awaiting GP approval
    SCHEDULED = "Scheduled"                # Approved, awaiting send date
    PENDING_SEND = "Pending send"          # Ready to send (carta-web picks up)
    SENT = "Sent"                          # Carta-web sent initial email
    SEND_FAIL = "Send failure"             # Email delivery failed
    REMINDED = "Reminded"                  # Reminder sent
    CONFIRMED = "Confirmed"                # PortCo responded

Document Uploads: IssuerAuditConfirmationDocument

File: fund_admin/entity_audit/models/

Links audit confirmations to uploaded documents:

audit_confirmation: ForeignKey[FundIssuerAuditConfirmation]
document_id: UUID                      # Reference to documents service
question_type: str                     # Which AuditQuestion this answers

Multiple documents can exist per confirmation (one per answered question).


Cross-Service Architecture

Service Responsibilities

Responsibility fund-admin carta-web
Create audit requests
Store workflow state
Send email notifications
PortCo UI for response
Document upload storage
Receive responses
Generate PDF for auditors
IAM/2FA authentication

Why This Split?

  • fund-admin: Domain owner for fund administration workflows
  • carta-web: Monolith with existing PortCo user management, email infrastructure, document storage

Event Flow

1. Request Flow: fund-admin → carta-web

Event: AuditConfirmationsRequested Proto: carta.proto.events.fa.auditconfirmations.events.v1alpha1.auditconfirmations_requested_pb2

When Published

  • Trigger: process_fund_issuer_audit_confirmations_for_send_date() scheduled task
  • File: fund_admin/entity_audit/services/issuer_audit_confirmation_request/services/issuer_audit_confirmation_service.py:453
  • Line: 1324 (publish(AuditConfirmationsRequestedAdapter.to_proto(...)))

Event Payload

message AuditConfirmationsRequested {
  int32 firm_carta_id = 1;
  int32 fund_carta_id = 2;

  AuditFirm audit_firm = 3;          // Who's auditing
  Date as_of_date = 4;               // Holdings date
  Date due_date = 5;                 // Response deadline

  repeated IssuerAuditConfirmation issuer_audit_confirmations = 6;
}

message IssuerAuditConfirmation {
  string audit_confirmation_id = 1;   // Fund-admin UUID
  repeated FundAsset assets = 2;      // Holdings to confirm
  repeated AuditQuestions audit_questions = 3;  // Questions asked
  repeated int32 corporation_ids = 4; // Carta-web company IDs
  string invite_id = 5;               // IAM invite
  string invite_token = 6;            // Auth token
  GeneralPartnerSignature gp_signature = 7;  // If GP approved
  repeated InvestorRelationsContact contacts = 8;  // Who to notify
}

Adapter Location

File: fund_admin/entity_audit/adapters/audit_confirmations_requested_adapter.py

What carta-web Does

  1. Consumes Kafka event
  2. Sends email to investor_relations_contacts[].email
  3. Creates IAM invitation using invite_id and invite_token
  4. Presents confirmation UI to PortCo users
  5. Publishes AuditConfirmationRequestsReceived (delivery confirmation)

2. Response Flow: carta-web → fund-admin

Event: AuditConfirmationSubmitted Proto: carta.proto.events.investorrelations.auditconfirmation.events.v1alpha1.auditconfirmation_submitted_pb2

When Published

  • Trigger: PortCo completes and submits confirmation in carta-web UI
  • PortCo provides: holdings verification, document uploads, signature, company info

Event Payload

message AuditConfirmationSubmitted {
  string audit_confirmation_id = 1;   // Links back to fund-admin
  int32 firm_carta_id = 2;
  int32 fund_carta_id = 3;

  bool holdings_accurate = 4;
  string inaccurate_explanation = 5;

  Decimal dividends_paid = 6;
  Decimal interest_paid = 7;
  Decimal transactions_paid = 8;
  Decimal other_paid = 9;

  AuditConfirmationIssuer issuer = 10;  // Legal name + address
  AuditConfirmationSignatory signatory = 11;  // Who signed
  repeated AuditQuestionDocument audit_question_documents = 12;  // Uploaded docs
  bool has_shared_financials = 13;
}

message AuditQuestionDocument {
  string question_type = 1;  // Which AuditQuestion
  string document_id = 2;    // UUID of uploaded document
}

Consumer Location

File: fund_admin/entity_audit/events/audit_confirmation_submitted_consumer.py Function: audit_confirmation_submitted_consumer()

What fund-admin Does

  1. Consumes Kafka event
  2. Deserializes via AuditConfirmationSubmittedAdapter.from_proto() (audit_confirmation_submitted_adapter.py:19)
  3. Updates FundIssuerAuditConfirmation fields:
    • Response data (holdings_accurate, payments, etc.)
    • Issuer info (legal name, address)
    • Creates IssuerAuditConfirmationSignature record
    • Creates IssuerAuditConfirmationDocument records
  4. Generates PDF confirmation document for auditors (issuer_audit_confirmation_service.py:573)
  5. Sets status to CONFIRMED
  6. Sends email notification to audit firm
  7. Sends Slack notification to FA team

3. Delivery Confirmation Flow: carta-web → fund-admin

Event: AuditConfirmationRequestsReceived Proto: carta.proto.events.fa.auditconfirmations.events.v1alpha1.audit_confirmation_requests_received_pb2

When Published

  • Trigger: After carta-web successfully sends email notifications to PortCos
  • Confirms delivery happened (for tracking purposes)

Event Payload

message AuditConfirmationRequestsReceived {
  int32 firm_carta_id = 1;
  int32 fund_carta_id = 2;

  repeated AuditConfirmationRequestReceived requests = 3;
}

message AuditConfirmationRequestReceived {
  string audit_confirmation_id = 1;
  Timestamp sent_timestamp = 2;
  bool is_reminder = 3;
}

Consumer Location

File: fund_admin/entity_audit/events/audit_confirmation_requests_received_consumer.py

What fund-admin Does

  1. Updates confirmation status:
    • If is_reminder=false: PENDING_SENDSENT
    • If is_reminder=true: SENTREMINDED
  2. Sets sent_timestamp
  3. Increments notification_count
  4. Updates last_reminded_date (if reminder)
  5. Sends notification to audit firm (first send only)

Response Data Structure

Holdings Verification (Required)

holdings_accurate: bool                # True if holdings match PortCo records
inaccurate_explanation: str            # Required if holdings_accurate=False

Optional Payment Disclosures

These are asked in the confirmation but not tied to specific questions:

dividends_paid: Decimal | None
interest_paid: Decimal | None
transactions_paid: Decimal | None
others_paid: Decimal | None

Document Uploads (Per Question)

For each selected audit_question, PortCo can upload documents:

# Stored as IssuerAuditConfirmationDocument records
{
    "audit_confirmation_id": UUID,
    "question_type": "CERTIFICATE_OF_INCORPORATION",  # or other AuditQuestion
    "document_id": UUID  # Reference to uploaded file
}

Special Case: Financials

For INCOME_STATEMENT and BALANCE_SHEET:

  • PortCo can either:
    1. Upload documents (creates IssuerAuditConfirmationDocument records)
    2. Share via "Carta Financials" (sets has_shared_financials=True, no document upload)

Logic: issuer_audit_confirmation_service.py:382-400

responded_audit_questions: set[AuditQuestion] = set()
for document in audit_confirmation.issuerauditconfirmationdocument_set.all():
    if document.question_type == AuditQuestion.AUDIT_CONFIRMATION:
        continue  # Skip base confirmation
    responded_audit_questions.add(AuditQuestion(document.question_type))

if has_shared_financials:
    if AuditQuestion.INCOME_STATEMENT in audit_confirmation.audit_questions:
        responded_audit_questions.add(AuditQuestion.INCOME_STATEMENT)
    if AuditQuestion.BALANCE_SHEET in audit_confirmation.audit_questions:
        responded_audit_questions.add(AuditQuestion.BALANCE_SHEET)

Issuer Information (From Response)

PortCo provides/confirms their company details:

issuer_legal_name: str                 # Company's legal name
issuer_address: IssuerAuditConfirmationAddress {
    street_address_1: str
    city: str
    state_or_province: str
    postal_code: str
    country: str
}

Signatures

issuer_user_signature: IssuerAuditConfirmationSignature {
    signature_uuid: UUID
    signature_text: str              # Typed name
    signature_timestamp: datetime
    signatory_title: str             # e.g., "CFO"
    signature_user_id: int | None
}

gp_signature: IssuerAuditConfirmationSignature {
    # Same structure, created during GP approval step
}

Dependencies Diagram

This diagram shows what needs to be updated when adding a new audit question (document request):

graph TB
    subgraph "Decision Point"
        START[New Question Required]
        DECISION{Structured Field<br/>or<br/>Document Upload?}
    end

    subgraph "Path A: Structured Field Addition"
        A1[Add field to<br/>FundIssuerAuditConfirmation model]
        A2[Create Django migration]
        A3[Update AuditConfirmationSubmitted proto]
        A4[Update AuditConfirmationSubmittedAdapter]
        A5[Update carta-web PortCo UI form]
        A6[Update carta-web to publish new field]
        A7[Update confirmation PDF template]
        A8[Add backend tests]
        A9[Add frontend tests]
    end

    subgraph "Path B: Document Upload Addition"
        B1[Add to AuditQuestion enum<br/>fund_admin/entity_audit/models/choices.py]
        B2[Add to frontend constants<br/>frontend/src/entity-audit/common/constants.ts]
        B3[Add checkbox to UI<br/>AuditConfirmationRequestFields.tsx]
        B4[Add i18n strings<br/>AuditConfirmationRequestModal.i18n.tsx]
        B5[Update AuditQuestions proto enum<br/>Both fund-admin AND carta-web]
        B6[Update carta-web PortCo UI<br/>Add document upload for new question]
        B7[Update backend tests]
        B8[Update frontend tests]
    end

    subgraph "Shared Steps for Both Paths"
        S1[Coordinate with carta-web team]
        S2[Deploy fund-admin first]
        S3[Deploy carta-web second]
        S4[Test E2E in staging]
        S5[Update this documentation]
    end

    START --> DECISION
    DECISION -->|Structured Field<br/>checkbox, text, number| A1
    DECISION -->|Document Upload<br/>file upload required| B1

    A1 --> A2 --> A3 --> A4 --> A5 --> A6 --> A7 --> A8 --> A9 --> S1
    B1 --> B2 --> B3 --> B4 --> B5 --> B6 --> B7 --> B8 --> S1

    S1 --> S2 --> S3 --> S4 --> S5

    style DECISION fill:#ffeb3b
    style START fill:#e3f2fd
    style S5 fill:#c8e6c9
Loading

Detailed Dependency Matrix

Adding Document Upload Question (Most Common)

Component Files to Update Why
Backend Model fund_admin/entity_audit/models/choices.py Add to AuditQuestion enum
Frontend Constants frontend/src/entity-audit/common/constants.ts TypeScript constant for question type
Frontend UI frontend/src/entity-audit/AuditConfirmations/components/AuditConfirmationRequestFields.tsx Add checkbox for auditors to select
Frontend i18n frontend/src/entity-audit/AuditConfirmations/components/AuditConfirmationRequestModal.i18n.tsx Display text for checkbox
Proto Definition (fund-admin) carta.proto.events.fa.auditconfirmations.models.v1alpha1 Add to AuditQuestions enum
Proto Definition (carta-web) carta-web proto repo Add to AuditQuestions enum (mirror)
Adapter (outgoing) fund_admin/entity_audit/adapters/audit_confirmations_requested_adapter.py Usually no change (handles array generically)
Adapter (incoming) fund_admin/entity_audit/adapters/audit_confirmation_submitted_adapter.py Usually no change (handles array generically)
carta-web Consumer carta-web codebase Add document upload UI for new question
carta-web Publisher carta-web codebase Include in audit_question_documents[] when publishing
Backend Tests tests/backend/fund_admin/entity_audit/services/issuer_audit_confirmation_request/test_issuer_audit_confirmation_service.py Test new question in list/create flows
Backend Consumer Tests tests/backend/fund_admin/entity_audit/events/test_audit_confirmation_submitted_consumer.py Test response handling
Frontend Tests frontend/src/entity-audit/AuditConfirmations/__tests__/ Test checkbox selection

Adding Structured Field (Less Common)

Component Files to Update Why
Backend Model fund_admin/entity_audit/models/fund_issuer_audit_confirmation.py Add new field to model
Migration Create via makemigrations Database schema change
Proto (response event) carta.proto.events.investorrelations.auditconfirmation.events.v1alpha1.auditconfirmation_submitted_pb2 Add field to AuditConfirmationSubmitted
Adapter (incoming) fund_admin/entity_audit/adapters/audit_confirmation_submitted_adapter.py Deserialize new field from proto
Consumer fund_admin/entity_audit/events/audit_confirmation_submitted_consumer.py Save new field to model
carta-web PortCo UI carta-web codebase Add form input for new field
carta-web Publisher carta-web codebase Include new field in published event
PDF Template fund_admin/entity_audit/services/.../templates/ Include new field in confirmation PDF
Backend Tests Multiple test files Test field save/retrieve
Frontend Tests carta-web tests Test form input

Critical Cross-Service Dependencies

graph LR
    subgraph "fund-admin"
        FA_Model[AuditQuestion enum]
        FA_Proto[Proto: AuditQuestions enum]
        FA_Adapter[audit_confirmations_requested_adapter]
        FA_Consumer[audit_confirmation_submitted_consumer]
    end

    subgraph "Proto Definitions"
        Proto_Request[AuditConfirmationsRequested proto]
        Proto_Response[AuditConfirmationSubmitted proto]
    end

    subgraph "carta-web"
        CW_Consumer[Consumer: AuditConfirmationsRequested]
        CW_UI[PortCo Response UI]
        CW_Publisher[Publisher: AuditConfirmationSubmitted]
    end

    FA_Model -->|defines available questions| FA_Proto
    FA_Proto -->|serializes to| Proto_Request
    Proto_Request -->|consumed by| CW_Consumer
    CW_Consumer -->|displays in| CW_UI
    CW_UI -->|response data| CW_Publisher
    CW_Publisher -->|serializes to| Proto_Response
    Proto_Response -->|consumed by| FA_Consumer
    FA_Consumer -->|saves to| FA_Model

    style Proto_Request fill:#ff9800
    style Proto_Response fill:#ff9800
    style CW_Consumer fill:#f44336
    style CW_Publisher fill:#f44336
Loading

Key Dependencies:

  1. Proto definitions must match between fund-admin and carta-web
  2. Enum values must be identical in both services' proto files
  3. Deploy order matters: fund-admin first (backwards compatible), then carta-web
  4. carta-web UI must handle all question types that fund-admin can send
  5. fund-admin consumer must handle all fields that carta-web can send

Adding New Questions

Your Use Case: Adding Optional Structured Fields

Based on your requirements:

  • Adding optional additional questions (text, dates, etc.)
  • These are structured form fields, not document uploads
  • Need to appear in the confirmation PDF for auditors

This means you're following "Path A: Structured Field Addition" from the dependency diagram above.

PDF Template Information

The confirmation PDF is generated in fund-admin after the PortCo responds:

Template Location:

  • Check: fund_admin/entity_audit/services/issuer_audit_confirmation_request/templates/
  • Or search for PDF generation code in: audit_confirmation_document_service.py

What's in the PDF:

  • Holdings data (assets, values, dates)
  • PortCo responses to base confirmation fields
  • Document upload references (for each audit_question answered)
  • Signatures (PortCo + GP if required)

When Adding New Structured Fields:

  1. Add field to FundIssuerAuditConfirmation model
  2. Update AuditConfirmationSubmitted proto to include new field
  3. Update adapter to deserialize new field
  4. Update PDF template to render new field
  5. Pass new field to PDF generation function in get_completed_issuer_audit_confirmation()

PDF Generation Code:

  • Service: IssuerAuditConfirmationService.get_completed_issuer_audit_confirmation()
  • File: issuer_audit_confirmation_service.py:129
  • Returns: CompletedIssuerAuditConfirmationDomain (dataclass with all PDF data)
  • Generator: AuditConfirmationDocumentService.generate_fund_issuer_audit_confirmation_pdf()

Step-by-Step Guide

1. Add Field to Backend Model (fund-admin)

File: fund_admin/entity_audit/models/fund_issuer_audit_confirmation.py

Add your new field to the model (example for a date field):

class FundIssuerAuditConfirmation(UUIDBaseModel, Deactivateable):
    # ... existing fields ...

    dividends_paid = models.DecimalField(max_digits=20, decimal_places=6, null=True)
    interest_paid = models.DecimalField(max_digits=20, decimal_places=6, null=True)
    transactions_paid = models.DecimalField(max_digits=20, decimal_places=6, null=True)
    others_paid = models.DecimalField(max_digits=20, decimal_places=6, null=True)

    # ADD YOUR NEW FIELD HERE
    new_question_field = models.CharField(max_length=255, null=True, blank=True)
    # OR for date field:
    new_date_field = models.DateField(null=True, blank=True)
    # OR for decimal:
    new_amount_field = models.DecimalField(max_digits=20, decimal_places=6, null=True, blank=True)

Important:

  • Use null=True, blank=True for optional fields
  • Choose appropriate field type (CharField, DateField, DecimalField, BooleanField, etc.)
  • Add help_text for documentation

2. Create Django Migration (fund-admin)

poetry run python manage.py makemigrations entity_audit
poetry run python manage.py migrate

This creates a migration file that adds the new field to the database.

3. Update Proto Definition (Response Event)

File: carta.proto.events.investorrelations.auditconfirmation.events.v1alpha1.auditconfirmation_submitted_pb2

Add your new field to the AuditConfirmationSubmitted message:

message AuditConfirmationSubmitted {
  string audit_confirmation_id = 1;
  int32 firm_carta_id = 2;
  int32 fund_carta_id = 3;

  bool holdings_accurate = 4;
  string inaccurate_explanation = 5;

  Decimal dividends_paid = 6;
  Decimal interest_paid = 7;
  Decimal transactions_paid = 8;
  Decimal other_paid = 9;

  // ADD YOUR NEW FIELD HERE
  string new_question_field = 14;  // For CharField
  // OR
  Date new_date_field = 15;        // For DateField
  // OR
  Decimal new_amount_field = 16;   // For DecimalField

  AuditConfirmationIssuer issuer = 10;
  AuditConfirmationSignatory signatory = 11;
  repeated AuditQuestionDocument audit_question_documents = 12;
  bool has_shared_financials = 13;
}

Important: Use the next available field number!

4. Update Incoming Event Adapter (fund-admin)

File: fund_admin/entity_audit/adapters/audit_confirmation_submitted_adapter.py

Update from_proto() method to deserialize your new field:

@staticmethod
def from_proto(
    audit_confirmation_submitted: AuditConfirmationSubmitted,
) -> AuditConfirmationSubmittedDomain:
    return AuditConfirmationSubmittedDomain(
        # ... existing fields ...
        others_paid=(
            DecimalProtoAdapter.from_proto(audit_confirmation_submitted.other_paid)
            if audit_confirmation_submitted.HasField("other_paid")
            else None
        ),
        # ADD YOUR NEW FIELD HERE
        new_question_field=audit_confirmation_submitted.new_question_field or None,
        # OR for date:
        new_date_field=(
            DateProtoAdapter.from_proto(audit_confirmation_submitted.new_date_field)
            if audit_confirmation_submitted.HasField("new_date_field")
            else None
        ),
        # ... rest of fields ...
    )

5. Update Domain Object (fund-admin)

File: fund_admin/entity_audit/services/issuer_audit_confirmation_request/domain.py

Add your field to AuditConfirmationSubmittedDomain:

@dataclass
class AuditConfirmationSubmittedDomain:
    audit_confirmation_id: UUID
    firm_carta_id: int
    fund_carta_id: int
    holdings_accurate: bool
    inaccurate_explanation: str
    dividends_paid: Decimal | None
    interest_paid: Decimal | None
    transactions_paid: Decimal | None
    others_paid: Decimal | None
    # ADD YOUR NEW FIELD HERE
    new_question_field: str | None
    new_date_field: date | None
    issuer: IssuerDomain
    signatory: AuditConfirmationSignatoryDomain
    audit_question_documents: list[AuditQuestionDocumentDomain]
    has_shared_financials: bool = field(default=False)

Also add to CompletedIssuerAuditConfirmationDomain (for PDF generation):

@dataclass
class CompletedIssuerAuditConfirmationDomain:
    # ... existing fields ...
    others_paid: Decimal | None
    # ADD YOUR NEW FIELD HERE
    new_question_field: str | None
    new_date_field: date | None
    issuer: IssuerDomain
    # ... rest of fields ...

6. Update Consumer (fund-admin)

File: fund_admin/entity_audit/events/audit_confirmation_submitted_consumer.py

Or File: fund_admin/entity_audit/services/issuer_audit_confirmation_request/services/issuer_audit_confirmation_service.py

In update_fund_issuer_audit_confirmation() method, save your new field:

def update_fund_issuer_audit_confirmation(
    self,
    audit_confirmation_submitted: AuditConfirmationSubmittedDomain,
) -> None:
    audit_confirmation = FundIssuerAuditConfirmation.objects.get(
        id=audit_confirmation_submitted.audit_confirmation_id,
        # ...
    )

    # Update existing fields
    audit_confirmation.holdings_accurate = audit_confirmation_submitted.holdings_accurate
    audit_confirmation.dividends_paid = audit_confirmation_submitted.dividends_paid
    # ... other fields ...

    # ADD YOUR NEW FIELD HERE
    audit_confirmation.new_question_field = audit_confirmation_submitted.new_question_field
    audit_confirmation.new_date_field = audit_confirmation_submitted.new_date_field

    # Save to database
    audit_confirmation.save()

7. Update PDF Generation (fund-admin)

A. Update get_completed_issuer_audit_confirmation() to include new field:

File: issuer_audit_confirmation_service.py:129

def get_completed_issuer_audit_confirmation(
    self,
    audit_confirmation: FundIssuerAuditConfirmation,
) -> CompletedIssuerAuditConfirmationDomain:
    return CompletedIssuerAuditConfirmationDomain(
        # ... existing fields ...
        others_paid=audit_confirmation.others_paid,
        # ADD YOUR NEW FIELD HERE
        new_question_field=audit_confirmation.new_question_field,
        new_date_field=audit_confirmation.new_date_field,
        issuer=IssuerDomain(...),
        # ... rest of fields ...
    )

B. Update PDF template to render the new field:

Find the PDF template file (likely in templates/ directory) and add your field to the rendered output.

8. Update carta-web PortCo UI

Location: carta-web monorepo (coordinate with carta-web team)

Add form input for your new field in the PortCo confirmation response UI:

  • Text input for CharField
  • Date picker for DateField
  • Number input for DecimalField
  • Checkbox for BooleanField

Important: The form should publish the new field in AuditConfirmationSubmitted event.

9. Update Tests

Backend tests to update:

A. tests/backend/fund_admin/entity_audit/events/test_audit_confirmation_submitted_consumer.py

Update the test proto creation helper and assertions:

def test_consumer_saves_new_field(
    entity_audit_fixture: EntityAuditFixture,
    mocker: MockerFixture,
) -> None:
    audit_confirmation = ...  # Get test audit confirmation

    new_field_value = "test value"  # or date, decimal, etc.

    audit_confirmation_submitted_proto = _get_audit_confirmation_submitted_proto(
        audit_confirmation=audit_confirmation,
        new_question_field=new_field_value,  # ADD HERE
    )

    audit_confirmation_submitted_consumer(audit_confirmation_submitted_proto)

    audit_confirmation.refresh_from_db()

    # ADD ASSERTION
    assert audit_confirmation.new_question_field == new_field_value

B. tests/backend/fund_admin/entity_audit/services/issuer_audit_confirmation_request/test_issuer_audit_confirmation_service.py

Test the service properly handles and returns the new field.

carta-web tests:

  • Add tests for new form input
  • Test event publishing includes new field

10. Coordinate Deployment

Since this spans multiple services, deployment order is critical:

Step 1: Deploy fund-admin (backwards compatible)

# fund-admin deployment includes:
# - New model field (with null=True for backwards compatibility)
# - Updated consumer to accept new field
# - Updated proto definition

Why first? The consumer can now handle responses WITH the new field, but old carta-web instances (without the new field) will continue to work because the field is optional.

Step 2: Deploy carta-web

# carta-web deployment includes:
# - Updated proto definition (matching fund-admin)
# - New form input in PortCo UI
# - Event publisher sends new field

Why second? Once deployed, carta-web will start sending the new field in responses, and fund-admin (already deployed) will save it.

Step 3: Verify in staging

  1. Create audit confirmation in fund-admin
  2. Verify email sent to PortCo
  3. PortCo fills out form including new field
  4. Verify fund-admin saves the response
  5. Download PDF and verify new field appears

Rollback Strategy:

  • If carta-web needs to rollback, it's safe - fund-admin will simply receive null for the new field
  • If fund-admin needs to rollback, carta-web will start getting errors if it sends the new field

Pro Tip: Add feature flag to carta-web UI to hide new field until fully tested


Key Files Reference

Backend (fund-admin)

File Purpose
fund_admin/entity_audit/models/choices.py Question type enum
fund_admin/entity_audit/models/fund_issuer_audit_confirmation.py Core data model
fund_admin/entity_audit/services/issuer_audit_confirmation_request/services/issuer_audit_confirmation_service.py Business logic
fund_admin/entity_audit/adapters/audit_confirmations_requested_adapter.py Outgoing event adapter
fund_admin/entity_audit/adapters/audit_confirmation_submitted_adapter.py Incoming event adapter
fund_admin/entity_audit/events/audit_confirmation_submitted_consumer.py Response consumer
fund_admin/entity_audit/events/audit_confirmation_requests_received_consumer.py Delivery confirmation consumer

Frontend (fund-admin)

File Purpose
frontend/src/entity-audit/common/constants.ts Question type constants
frontend/src/entity-audit/AuditConfirmations/components/AuditConfirmationRequestFields.tsx Question selection UI
frontend/src/entity-audit/AuditConfirmations/components/AuditConfirmationRequestModal.i18n.tsx Internationalization

Tests

File Purpose
tests/backend/fund_admin/entity_audit/services/issuer_audit_confirmation_request/test_issuer_audit_confirmation_service.py Service tests
tests/backend/fund_admin/entity_audit/events/test_audit_confirmation_submitted_consumer.py Event consumer tests
frontend/src/entity-audit/AuditConfirmations/__tests__/AuditConfirmations.jest.tsx Frontend component tests

Important Constraints

Database Constraints

  • Uniqueness: One confirmation per (fund, issuer, as_of_date) - enforced by unique_fund_and_issuer_per_as_of_date constraint
  • Soft Deletes: Uses Deactivateable pattern - records are marked deleted_date instead of hard deleted

Date Validation

  • due_date must be at least MINIMUM_DUE_DATE_DAYS_AFTER_SEND (20 business days) after send_date
  • as_of_date typically year-end date
  • Confirmations expire AUDIT_CONFIRMATION_EXPIRATION_DAYS (90 days) after due_date

Email Constraints

  • Investor relations contact must have valid email to receive confirmation
  • Multiple contacts can be notified but one is marked is_primary=True

Document Types

  • Each question type can have multiple documents uploaded
  • Documents stored in carta-web's document service
  • Only UUIDs stored in fund-admin

Troubleshooting

Question Not Appearing in UI

  1. Check AuditQuestion enum in backend
  2. Verify frontend constant in constants.ts
  3. Confirm checkbox added to AuditConfirmationRequestFields.tsx
  4. Check i18n messages defined

PortCo Can't Respond to New Question

  1. Verify proto definitions updated in both services
  2. Check carta-web consumer handles new question type
  3. Confirm document upload UI exists in carta-web
  4. Test audit_question_documents includes new question in response event

Response Not Saving

  1. Check consumer logs in fund-admin
  2. Verify AuditConfirmationSubmittedAdapter.from_proto() deserializes correctly
  3. Confirm IssuerAuditConfirmationDocument records created
  4. Check foreign key constraints on document IDs

Related Documentation

  • Feature Flag: IRAD_22_AUDIT_CONFIRMS controls audit confirmation feature
  • Scheduled Task: process_fund_issuer_audit_confirmations_for_send_date runs daily
  • GP Approval: Separate workflow if gp_approval_required=True
  • Reminders: Automatic reminders sent every TOKEN_EXPIRATION_DAYS (14 days)
  • Expiration: Auto-closes confirmations AUDIT_CONFIRMATION_EXPIRATION_DAYS (90 days) past due date

Questions or Issues?

When modifying this flow:

  1. Coordinate with carta-web team - proto changes require alignment
  2. Test cross-service flow - both services must be deployed for E2E testing
  3. Consider backwards compatibility - old requests may still be in-flight during deployment
  4. Update this doc - keep it current for future developers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment