---
title: Business rules
summary: Business rules enforced during request processing.
status: draft
owner: Agency Systems Team
last_reviewed: 2026-09-10
---

# Business rules

`SAIF.Agency.Api` is the only writer of the `AGENCY` schema, so the rules below
are the whole of SAIF's agreed behaviour for agency data: what a valid agency
number looks like, which agencies and agents are visible to a reader, and what
must be true before an agency's contact details may be changed. Contact field
validation is delegated to `SAIF.Framework.Validation` and keeps that library's
`VAL-` identifiers; every other rule is enforced by `AgencyService` and is
reported to the caller as a `400`, `404`, or `409` response.

---

## BR-001 Agency number must be six digits

| Field | Value |
| --- | --- |
| Statement | An agency number is exactly six numeric digits with no separators or alphabetic characters, because it is the key the whole distribution channel is filed under and a malformed value cannot identify an agency. |
| Trigger | Evaluated in the action body, immediately after model binding and before any service call. |
| Enforcement | `AgencyApiBase.ValidateAgencyNumber` applies the `^[0-9]{6}$` pattern and returns `ValidationFailure` when it does not match. |
| Origin | api |
| Reached from | [`GET /v1/agencies/{agencyNumber}`](endpoints/agencies.md#get-v1agenciesagencynumber), [`GET /v1/agencies/{agencyNumber}/agents`](endpoints/agencies.md#get-v1agenciesagencynumberagents), [`PUT /v1/agencies/{agencyNumber}/contact`](endpoints/agencies.md#put-v1agenciesagencynumbercontact) |
| Outcome | On pass, processing continues. On failure the action returns `400 Bad Request` with `code` `AGENCY_NUMBER_FORMAT`; no database call is made and nothing is logged beyond the request entry. |
| Source | `src/SAIF.Agency.Api/Controllers/AgencyApiBase.cs:74` |

---

## BR-002 Agency search must be bounded by a producer code

| Field | Value |
| --- | --- |
| Statement | A search must supply a producer code of one to ten characters. An unbounded search would return the entire distribution channel and is refused, because no consumer has a business need for it and the query would scan the whole `AGENCY` table. |
| Trigger | Evaluated in the action body of `SearchAgencies`, before the repository query is built. |
| Enforcement | `AgenciesController.SearchAgencies` rejects a null, empty, whitespace, or over-long `producerCode` through `AgencyApiBase.ValidationFailure`. |
| Origin | api |
| Reached from | [`GET /v1/agencies`](endpoints/agencies.md#get-v1agencies) |
| Outcome | On pass the search runs. On failure the caller receives `400 Bad Request` with `code` `PRODUCER_CODE_REQUIRED`; a search that is valid but matches nothing returns `200 OK` with an empty array, which is not a failure. |
| Source | `src/SAIF.Agency.Api/Controllers/v1/AgenciesController.cs:96` |

---

## BR-003 Terminated agencies are excluded unless explicitly requested

| Field | Value |
| --- | --- |
| Statement | A terminated agency is invisible to reads by default, so that a consumer attributing new business cannot place it with an agency that no longer holds an appointment; a caller that genuinely needs historical data must ask for it. |
| Trigger | Evaluated in the service layer when the query filter is assembled, on every read of agency data. |
| Enforcement | `AgencyService.BuildAgencyFilter` adds `STATUS <> 'Terminated'` unless the caller passed `includeTerminated=true` on the lookup or `activeOnly=false` on the search. |
| Origin | api |
| Reached from | [`GET /v1/agencies/{agencyNumber}`](endpoints/agencies.md#get-v1agenciesagencynumber), [`GET /v1/agencies`](endpoints/agencies.md#get-v1agencies), `GET /v1/appointments` |
| Outcome | A filtered-out agency is reported as `404 Not Found` with `code` `AGENCY_NOT_FOUND` on the lookup endpoint and is simply absent from the array on the search endpoints. The two behaviours differ deliberately: a lookup names one agency, a search does not. |
| Source | `src/SAIF.Agency.Api.Services/AgencyService.cs:58` |

---

## BR-004 An agency roster returns only agents with an active appointment

| Field | Value |
| --- | --- |
| Statement | The roster of an agency is the set of agents whose appointment at that agency is in effect on the evaluation date. An agent who has left, or whose appointment has not yet started, is not part of the roster even though the appointment record is retained. |
| Trigger | Evaluated in the service layer when the roster query runs, against the `asOfDate` supplied by the caller or the current date. |
| Enforcement | `AgencyService.GetAgentsForAgency` joins `AGENCY_APPOINTMENT` on `EFFECTIVE_DATE <= :asOf` and `(END_DATE IS NULL OR END_DATE > :asOf)`. |
| Origin | api |
| Reached from | [`GET /v1/agencies/{agencyNumber}/agents`](endpoints/agencies.md#get-v1agenciesagencynumberagents) |
| Outcome | Matching agents are returned as `200 OK` with an array; an agency with no agents in effect returns `200 OK` with an empty array rather than `404`, because the agency itself exists. |
| Source | `src/SAIF.Agency.Api.Services/AgencyService.cs:131` |

---

## BR-005 Contact details must be a deliverable email address and phone number

| Field | Value |
| --- | --- |
| Statement | An agency's contact email must be a syntactically deliverable address of at most 255 characters, and its contact phone must reduce to exactly ten digits once punctuation is removed, because renewal notices and claim correspondence are sent to these values without further checking.† |
| Trigger | Evaluated in the service layer when the contact update is applied, before the agency's state is checked. |
| Enforcement | Delegated to `SAIF.Framework.Validation` rules VAL-051 (email form) and VAL-052 (ten digit phone), applied by `ContactValidator.Validate` and raised to the caller as `AgencyValidationException`. |
| Origin | library |
| Reached from | [`PUT /v1/agencies/{agencyNumber}/contact`](endpoints/agencies.md#put-v1agenciesagencynumbercontact) |
| Outcome | On pass the update continues to BR-006. On failure `AgencyApiExceptionFilter` returns `400 Bad Request` with `code` `CONTACT_INVALID` and one `details` entry per failing field, naming VAL-051 or VAL-052; nothing is written. |
| Source | `src/SAIF.Agency.Api.Services/AgencyService.cs:188` |

---

## BR-006 Contact details may only be changed on an active agency

| Field | Value |
| --- | --- |
| Statement | Contact details may be changed only while the agency's status is `Active`. A suspended or terminated agency's contact block is frozen as the record of where correspondence was sent while the appointment was in force. |
| Trigger | Evaluated in the service layer after field validation passes and before the agency is updated. |
| Enforcement | `AgencyService.UpdateContact` raises `AgencyStateConflictException` when `AGENCY.STATUS` is anything other than `Active`. |
| Origin | api |
| Reached from | [`PUT /v1/agencies/{agencyNumber}/contact`](endpoints/agencies.md#put-v1agenciesagencynumbercontact) |
| Outcome | On pass the update proceeds. On failure `AgencyApiExceptionFilter` returns `409 Conflict` with `code` `AGENCY_NOT_ACTIVE`; the unit of work is rolled back and no audit row is written. |
| Source | `src/SAIF.Agency.Api.Services/AgencyService.cs:197` |

---

## BR-007 Every accepted contact change writes an audit row

| Field | Value |
| --- | --- |
| Statement | An accepted change to an agency's contact block must record what the values were, what they became, who changed them, and when, so that a misdirected renewal notice can be traced to the change that caused it. |
| Trigger | Evaluated at persistence, inside the same unit of work that writes the new contact values. |
| Enforcement | `AgencyService.UpdateContact` inserts an `AGENCY_CONTACT_AUDIT` row carrying the previous and new values, `CHANGED_BY` from `AgencyApiBase.Caller`, and the request's correlation id, then commits both statements together. |
| Origin | api |
| Reached from | [`PUT /v1/agencies/{agencyNumber}/contact`](endpoints/agencies.md#put-v1agenciesagencynumbercontact) |
| Outcome | The caller receives `204 No Content` only when both writes commit. If the audit insert fails the whole unit of work rolls back and the caller receives `500 Internal Server Error`; a contact change is never persisted without its audit row. |
| Source | `src/SAIF.Agency.Api.Services/AgencyService.cs:221` |

---

## BR-008 A contact update must carry the expected agency version

| Field | Value |
| --- | --- |
| Statement | An update must state the agency version the caller read, and is refused if the stored version has moved on. Two portal users editing the same agency must not silently overwrite each other's address changes. |
| Trigger | Evaluated at persistence, when the update statement is issued against the version column. |
| Enforcement | `AgencyRepository.UpdateContact` includes `VERSION = :expectedVersion` in the `WHERE` clause and increments `VERSION` on success; a zero row count raises `StaleAgencyVersionException`. |
| Origin | api |
| Reached from | [`PUT /v1/agencies/{agencyNumber}/contact`](endpoints/agencies.md#put-v1agenciesagencynumbercontact) |
| Outcome | On pass, the new version is returned in the `X-Agency-Version` response header with `204 No Content`. On failure the action catches the exception, attaches the current version, and `AgencyApiExceptionFilter` returns `409 Conflict` with `code` `AGENCY_VERSION_STALE`; the caller must re-read the agency and resubmit. |
| Source | `src/SAIF.Agency.Api.Data/AgencyRepository.cs:164` |

---

## BR-009 An agent is returned as licensed only while the license is active

| Field | Value |
| --- | --- |
| Statement | An agent is reported as licensed in a state only while the state license is unexpired and not revoked, because an appointment written against a lapsed license is not enforceable. |
| Trigger | Evaluated in the service layer on every agent read and when an appointment is created. |
| Enforcement | `AgentService.ResolveLicense` compares `AGENT_LICENSE.EXPIRATION_DATE` and `REVOKED_FLAG` against the evaluation date; `AppointmentService.Create` calls the same method before inserting. |
| Origin | api |
| Reached from | `GET /v1/agents/{agentNumber}`, `GET /v1/agents`, `POST /v1/appointments` |
| Outcome | On the read endpoints a lapsed license is returned with `licenseStatus` `Expired` rather than being hidden. On `POST /v1/appointments` it raises `AgencyValidationException` and the caller receives `400 Bad Request` with `code` `AGENT_LICENSE_INACTIVE`. |
| Source | `src/SAIF.Agency.Api.Services/AgentService.cs:87` |

---

## BR-010 An agent holds at most one active appointment at an agency

| Field | Value |
| --- | --- |
| Statement | An agent may hold only one appointment at a given agency at a time. Overlapping appointments would make commission attribution ambiguous, so a new appointment may not start before the previous one at that agency has ended. |
| Trigger | Evaluated in the service layer before an appointment row is inserted, and when an agent's appointment history is read. |
| Enforcement | `AppointmentService.Create` queries `AGENCY_APPOINTMENT` for an existing row with the same agent and agency and no `END_DATE`, raising `AgencyStateConflictException` when one exists; the unique index `AGENCY_APPOINTMENT_UK1` backs the check at the database. |
| Origin | api |
| Reached from | `GET /v1/agents/{agentNumber}/appointments`, `POST /v1/appointments` |
| Outcome | On pass the appointment is inserted and `201 Created` is returned with the new appointment id. On failure the caller receives `409 Conflict` with `code` `APPOINTMENT_ALREADY_ACTIVE` and the existing appointment id in `details`. |
| Source | `src/SAIF.Agency.Api.Services/AppointmentService.cs:64` |

---

> **Note:** Statements marked with † were retrieved from dependency library documentation
> via the Legends CLI RAG service. They describe inherited behavior from the indicated
> origin library and should be verified if the dependency version changes.

## Related documentation
- [API index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Endpoints](endpoints.md)
- [Authentication and authorization](authentication-authorization.md)
- Dependencies
- Configuration
- Operations and processes
- Data model
- [Operational runbook](operational-runbook.md)
- Tech debt

## Sources
- `src/SAIF.Agency.Api/Controllers/AgencyApiBase.cs`
- `src/SAIF.Agency.Api/Controllers/v1/AgenciesController.cs`
- `src/SAIF.Agency.Api.Services/AgencyService.cs`
- `src/SAIF.Agency.Api.Services/AgentService.cs`
- `src/SAIF.Agency.Api.Services/AppointmentService.cs`
- `src/SAIF.Agency.Api.Data/AgencyRepository.cs`
- `src/SAIF.Agency.Api.Data/Maps/AgencyAppointmentMap.cs`
