Fair Supply LogoFair Supply - Docs

Engagements Domain

The Engagements domain handles ESG assessment workflows, where organisations are evaluated against compliance frameworks.

Entity Hierarchy

Key Concepts

EntityDescription
EngagementAssessment request sent to an organisation. Has respondents who answer.
FrameworkESG framework structure (e.g., Modern Slavery Act). Contains pillars.
PillarCategory within framework (e.g., Governance, Due Diligence). Contains controls.
ControlSpecific requirement to verify. Tested by one or more tests.
TestQuestion with multiple-choice options. May require evidence.
TestResponseUser's answer to a test, linking selected options.

Scoring System

Scores roll up hierarchically from individual test responses to the overall engagement score:

LevelCalculation
Test ScoreSum of the selected option values
Control ScoreΣ answered test scores ÷ Σ all test max scores as a percentage
Pillar ScoreSame percentage basis, aggregated over the pillar's controls
Framework ScoreSame percentage basis, aggregated over all controls
Engagement (mitigation) ScoreAllocation-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

  1. Only invited respondents can answer tests
  2. At least one framework required per engagement
  3. 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)
  4. Scores recalculate on every saved response (initialised at creation); non-submitted engagements read as "None" to the requestor — scores are only surfaced once submitted
  5. Engagements can have optional due dates
  6. 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:

  1. Validation phase (outside transaction - reads only)
  2. Transactional operations (all database writes within withTransaction)
  3. 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

GuidelineDescription
Validation firstPerform read operations outside the transaction
Side effects lastSend emails/notifications after transaction commits
External APIsAuth0 calls cannot be rolled back - handle orphaned records if needed
DataLoadersDataLoaders 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;
});
TypeLocation
GraphQL Schemapackages/core/src/infrastructure/neo4j/schemas/engagement.graphql
Repositorypackages/core/src/infrastructure/repositories/engagement-repository.ts
Use Casespackages/core/src/application/use-cases/engagement/
Scoring Servicespackages/core/src/domain/services/scoring/
Transaction Typespackages/core/src/infrastructure/graphql/transaction.ts
Transaction Utilitypackages/core/src/infrastructure/graphql/with-transaction.ts

On this page