Engagements Domain
The Engagements domain handles ESG assessment workflows, where organisations are evaluated against compliance frameworks.
Entity Hierarchy
Key Concepts
| Entity | Description |
|---|---|
| Engagement | Assessment request sent to an organisation. Has respondents who answer. |
| Framework | ESG framework structure (e.g., Modern Slavery Act). Contains pillars. |
| Pillar | Category within framework (e.g., Governance, Due Diligence). Contains controls. |
| Control | Specific requirement to verify. Tested by one or more tests. |
| Test | Question with multiple-choice options. May require evidence. |
| TestResponse | User's answer to a test, linking selected options. |
Scoring System
Scores roll up hierarchically from individual test responses to the overall engagement score:
| Level | Calculation |
|---|---|
| Test Score | Sum of the selected option values |
| Control Score | Σ answered test scores ÷ Σ all test max scores as a percentage |
| Pillar Score | Same percentage basis, aggregated over the pillar's controls |
| Framework Score | Same percentage basis, aggregated over all controls |
| Engagement (mitigation) Score | Allocation-weighted average of pillar percentages: Σ(pillar % × allocation) ÷ Σ allocation |
Note the denominator is all tests, not just answered ones — so an in-progress engagement scores low and climbs as more questions are answered. These partial scores are initialised at creation and recalculated on every saved response, but are only meaningful — and surfaced to the requestor — once the engagement is submitted (see Business Rules).
Domain services for scoring live in packages/core/src/domain/services/scoring/.
Business Rules
- Only invited respondents can answer tests
- At least one framework required per engagement
- Responses are editable only while the engagement is Not Started or Pending; submission locks the engagement and all its responses, and there is no reopen path (finality)
- Scores recalculate on every saved response (initialised at creation); non-submitted engagements read as "None" to the requestor — scores are only surfaced once submitted
- Engagements can have optional due dates
- Test options have associated score values
Common Operations
Creating an Engagement
The CreateEngagement use case demonstrates the platform's transaction support pattern. All database operations are wrapped in a single transaction to ensure atomicity.
import { CreateEngagement } from '@repo/core';
const useCase = new CreateEngagement();
const engagement = await useCase.execute({
organisationId: 'org-123',
frameworkIds: ['framework-1', 'framework-2'],
respondentIds: ['user-1', 'user-2'],
dueDate: new Date('2025-03-01'),
});Transaction Flow:
- Validation phase (outside transaction - reads only)
- Transactional operations (all database writes within
withTransaction) - Side effects (emails sent after transaction commits)
Saving a Test Response
Each answer is saved individually with SaveTestResponse, which recalculates the
engagement's scores after every save. Finalising the whole engagement is a separate
step (SubmitEngagementResponses).
import { SaveTestResponse } from '@repo/core';
const useCase = new SaveTestResponse();
await useCase.execute({
auth0UserId: 'auth0|123',
engagementId: 'engagement-123',
testId: 'test-123',
selectedOptionIds: ['option-1'],
notes: 'Optional respondent note',
// skip: true, // mark the test as skipped instead of answered
});Transaction Support
The Engagements domain uses the platform's transaction support for atomic multi-step operations. This ensures data consistency when creating engagements with multiple respondents.
Using Transactions
import { withTransaction } from '@repo/core';
import { EngagementRepository, UserRepository } from '@repo/core';
const result = await withTransaction(async (tx) => {
// All operations share the same transaction
const engagement = await EngagementRepository.create({
status: EngagementStatus.NotStarted,
accountId,
organisationId,
}, { tx });
await EngagementRepository.addRespondent({
engagementId: engagement.id,
userId: respondent.id,
status: RespondentStatus.Pending,
}, { tx });
return engagement;
});Transaction Guidelines
| Guideline | Description |
|---|---|
| Validation first | Perform read operations outside the transaction |
| Side effects last | Send emails/notifications after transaction commits |
| External APIs | Auth0 calls cannot be rolled back - handle orphaned records if needed |
| DataLoaders | DataLoaders participate in transactions when executionContext is provided |
Error Handling
await withTransaction(async (tx) => {
// If any operation throws, all changes roll back
const engagement = await EngagementRepository.create(data, { tx });
if (!engagement) {
throw new Error('Failed to create engagement');
// Transaction automatically rolls back
}
return engagement;
});Related Files
| Type | Location |
|---|---|
| GraphQL Schema | packages/core/src/infrastructure/neo4j/schemas/engagement.graphql |
| Repository | packages/core/src/infrastructure/repositories/engagement-repository.ts |
| Use Cases | packages/core/src/application/use-cases/engagement/ |
| Scoring Services | packages/core/src/domain/services/scoring/ |
| Transaction Types | packages/core/src/infrastructure/graphql/transaction.ts |
| Transaction Utility | packages/core/src/infrastructure/graphql/with-transaction.ts |