Last Updated: 2025-11-18 Purpose: Document the audit confirmation workflow, question types, and cross-service integration for future modifications
- ✅ 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.
- Overview
- Response Types: Structured Fields vs Document Uploads
- Sequence Diagram
- Question Types
- Data Models
- Cross-Service Architecture
- Event Flow
- Response Data Structure
- Dependencies Diagram
- Adding New Questions
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.
- 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
- 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
CRITICAL DISTINCTION: The audit confirmation has two types of 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-capturedThese fields are part of the BASE confirmation and are always collected - they're standard form inputs/checkboxes/dropdowns.
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)
| 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
FundIssuerAuditConfirmationmodel - If it's a document request → Add to
AuditQuestionenum + update proto + carta-web UI
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
Key Takeaways from Sequence:
- fund-admin initiates and owns workflow state
- carta-web handles all PortCo interaction (auth, UI, document uploads)
- 3 Kafka events coordinate the cross-service flow
- Document IDs from carta-web are stored as references in fund-admin
- PDF generation happens in fund-admin after response received
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")-
AUDIT_CONFIRMATION- Always included - this is the base confirmation document
- Not selectable by auditors (automatic)
- Confirms holdings accuracy
-
Optional Document Requests (auditor-selectable):
CERTIFICATE_OF_INCORPORATION- Legal formation documentsCAP_TABLE- Capitalization tableINCOME_STATEMENT- Financial statementBALANCE_SHEET- Financial statement
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_financialsflag)
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 authenticationclass 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 respondedFile: 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 answersMultiple documents can exist per confirmation (one per answered question).
| 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 | ❌ | ✅ |
- fund-admin: Domain owner for fund administration workflows
- carta-web: Monolith with existing PortCo user management, email infrastructure, document storage
Event: AuditConfirmationsRequested
Proto: carta.proto.events.fa.auditconfirmations.events.v1alpha1.auditconfirmations_requested_pb2
- 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(...)))
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
}File: fund_admin/entity_audit/adapters/audit_confirmations_requested_adapter.py
- Consumes Kafka event
- Sends email to
investor_relations_contacts[].email - Creates IAM invitation using
invite_idandinvite_token - Presents confirmation UI to PortCo users
- Publishes
AuditConfirmationRequestsReceived(delivery confirmation)
Event: AuditConfirmationSubmitted
Proto: carta.proto.events.investorrelations.auditconfirmation.events.v1alpha1.auditconfirmation_submitted_pb2
- Trigger: PortCo completes and submits confirmation in carta-web UI
- PortCo provides: holdings verification, document uploads, signature, company info
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
}File: fund_admin/entity_audit/events/audit_confirmation_submitted_consumer.py
Function: audit_confirmation_submitted_consumer()
- Consumes Kafka event
- Deserializes via
AuditConfirmationSubmittedAdapter.from_proto()(audit_confirmation_submitted_adapter.py:19) - Updates
FundIssuerAuditConfirmationfields:- Response data (holdings_accurate, payments, etc.)
- Issuer info (legal name, address)
- Creates
IssuerAuditConfirmationSignaturerecord - Creates
IssuerAuditConfirmationDocumentrecords
- Generates PDF confirmation document for auditors (issuer_audit_confirmation_service.py:573)
- Sets status to
CONFIRMED - Sends email notification to audit firm
- Sends Slack notification to FA team
Event: AuditConfirmationRequestsReceived
Proto: carta.proto.events.fa.auditconfirmations.events.v1alpha1.audit_confirmation_requests_received_pb2
- Trigger: After carta-web successfully sends email notifications to PortCos
- Confirms delivery happened (for tracking purposes)
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;
}File: fund_admin/entity_audit/events/audit_confirmation_requests_received_consumer.py
- Updates confirmation status:
- If
is_reminder=false:PENDING_SEND→SENT - If
is_reminder=true:SENT→REMINDED
- If
- Sets
sent_timestamp - Increments
notification_count - Updates
last_reminded_date(if reminder) - Sends notification to audit firm (first send only)
holdings_accurate: bool # True if holdings match PortCo records
inaccurate_explanation: str # Required if holdings_accurate=FalseThese 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 | NoneFor 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
}For INCOME_STATEMENT and BALANCE_SHEET:
- PortCo can either:
- Upload documents (creates
IssuerAuditConfirmationDocumentrecords) - Share via "Carta Financials" (sets
has_shared_financials=True, no document upload)
- Upload documents (creates
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)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
}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
}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
| 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 |
| 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 |
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
Key Dependencies:
- Proto definitions must match between fund-admin and carta-web
- Enum values must be identical in both services' proto files
- Deploy order matters: fund-admin first (backwards compatible), then carta-web
- carta-web UI must handle all question types that fund-admin can send
- fund-admin consumer must handle all fields that carta-web can send
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.
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:
- Add field to
FundIssuerAuditConfirmationmodel - Update
AuditConfirmationSubmittedproto to include new field - Update adapter to deserialize new field
- Update PDF template to render new field
- 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()
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=Truefor optional fields - Choose appropriate field type (CharField, DateField, DecimalField, BooleanField, etc.)
- Add help_text for documentation
poetry run python manage.py makemigrations entity_audit
poetry run python manage.py migrateThis creates a migration file that adds the new field to the database.
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!
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 ...
)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 ...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()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.
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.
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_valueB. 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
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 definitionWhy 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 fieldWhy 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
- Create audit confirmation in fund-admin
- Verify email sent to PortCo
- PortCo fills out form including new field
- Verify fund-admin saves the response
- 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
| 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 |
| 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 |
| 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 |
- Uniqueness: One confirmation per (fund, issuer, as_of_date) - enforced by
unique_fund_and_issuer_per_as_of_dateconstraint - Soft Deletes: Uses
Deactivateablepattern - records are markeddeleted_dateinstead of hard deleted
due_datemust be at leastMINIMUM_DUE_DATE_DAYS_AFTER_SEND(20 business days) aftersend_dateas_of_datetypically year-end date- Confirmations expire
AUDIT_CONFIRMATION_EXPIRATION_DAYS(90 days) afterdue_date
- Investor relations contact must have valid email to receive confirmation
- Multiple contacts can be notified but one is marked
is_primary=True
- Each question type can have multiple documents uploaded
- Documents stored in carta-web's document service
- Only UUIDs stored in fund-admin
- Check
AuditQuestionenum in backend - Verify frontend constant in
constants.ts - Confirm checkbox added to
AuditConfirmationRequestFields.tsx - Check i18n messages defined
- Verify proto definitions updated in both services
- Check carta-web consumer handles new question type
- Confirm document upload UI exists in carta-web
- Test
audit_question_documentsincludes new question in response event
- Check consumer logs in fund-admin
- Verify
AuditConfirmationSubmittedAdapter.from_proto()deserializes correctly - Confirm
IssuerAuditConfirmationDocumentrecords created - Check foreign key constraints on document IDs
- Feature Flag:
IRAD_22_AUDIT_CONFIRMScontrols audit confirmation feature - Scheduled Task:
process_fund_issuer_audit_confirmations_for_send_dateruns 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
When modifying this flow:
- Coordinate with carta-web team - proto changes require alignment
- Test cross-service flow - both services must be deployed for E2E testing
- Consider backwards compatibility - old requests may still be in-flight during deployment
- Update this doc - keep it current for future developers