---
title: Contact CRUD with child persistence
summary: Creates, retrieves, updates, and deletes the root contact (PERSON) record, blocks deletion while customer relationships exist, and cascades saves to its child collections.
status: draft
owner: SAIF
last_reviewed: 2026-07-30
library: SAIF.Business.CustomerContact
reference_type: package
operation_id: OP-CC-03
trigger: Caller creates or retrieves a clsContact, edits properties or child collections, and calls Save()
---

# Contact CRUD with child persistence

`clsContact` owns the root `PERSON` row and exposes address, email, phone, and long-note children the same way `clsCustomer` does. The key difference is a delete-time business rule: a contact cannot be deleted while it is still linked to any customer through `COMPANY_PERSON`.

| Field | Value |
| --- | --- |
| **Operation ID** | OP-CC-03 |
| **Trigger** | `clsContact.NewContact(dpi)` or `clsContact.GetContact(dpi, contactID)`, followed by property/child edits and `Save()` |
| **Inputs** | Root contact fields (composite `Name`, split into `FirstName`/`LastName`/`MiddleInitial`/`PreferredName`/`NameExtension`), `LongNoteText`, and any of the `Addresses`, `Emails`, `Phones` child collections |
| **Outputs** | Persisted `PERSON` row; persisted child address/email/phone/note rows; delete blocked by a broken rule while relationships exist; `InsertError`/`UpdateError`/`DeleteError` broken rules on contention |
| **Primary components** | `clsContact.Create`, `clsContact.GetContact`, `clsContact.OnSave`, `clsContact.OnSaveComplete`, `clsContact.ValidateNoRelationshipsOnDelete`, `clsContact.GetRelationships` |
| **Entry point** | `Business\SAIF.Business.CustomerContact\Contact\clsContact.vb` |

## Steps

1. **Create a new contact.** `Create()` assigns a new `ContactID` from `GetNextSeqNum("PERSON_SEQ")`, and sets `CreateDate` and `CreateUser` from the data-portal info.
2. **Retrieve an existing contact.** `GetContact(dpi, contactID)` runs the BOF retrieve pipeline against `PERSON` ([SQL-029](../sql/SQL-029.md), [PERSON](../database-tables.md#person)); `Addresses`, `Emails`, `Phones`, and `ContactNote` are lazy-loaded on first access.
3. **Save pre-processing (`OnSave`).**
   - If the contact `IsDeleted`: clear `Emails`, `Phones`, and `Addresses`, call `ContactNote.Delete()`, save those four child objects, and return immediately — the parent delete path below still runs, but no further composite-name or note-blank processing happens on this branch.
   - Otherwise: set `_UpdateDate` and `_UpdateUser`, and copy the composite `Name` value into the discrete `_FirstName`, `_LastName`, `_MiddleInitial`, and `_PreferredName`/`_NameExtension` fields.
   - If the existing `ContactNote` is not new and its `NoteText` is blank, call `ContactNote.Delete()` so the note row is removed rather than stored empty ([BR-009](../business-rules.md#br-009-blank-long-notes-are-deleted-rather-than-stored-as-empty-text)).
4. **Persist the root row.**
   - Insert: `BeforeInsert` binds all root `PERSON` fields ([SQL-003](../sql/SQL-003.md), [PERSON](../database-tables.md#person)).
     - On failure (rows affected ≠ 1): add the `InsertError` broken rule.
   - Update: `BeforeUpdate` re-binds the same field set ([SQL-066](../sql/SQL-066.md), [PERSON](../database-tables.md#person)).
     - On failure (rows affected ≠ 1): add the `UpdateError` broken rule with the DB-contention message.
5. **Validate before delete** ([BR-003](../business-rules.md#br-003-contacts-with-customer-relationships-cannot-be-deleted)). `ValidateOnDelete = True` registers `ValidateNoRelationshipsOnDelete`, which returns `True` unless `IsDeleted`, in which case it returns `Me.CustomerRelationships.Count = 0`.
   - `CustomerRelationships` invokes `GetRelationships()`, which queries `COMPANY_PERSON` joined to `COMPANY` for the contact's identifier ([SQL-028](../sql/SQL-028.md), [COMPANY_PERSON](../database-tables.md#company-person), [COMPANY](../database-tables.md#company)) and materializes a `clsCustomerRelationship` per row, resolving the relationship description through `GetRelationshipCode(dpi)`.
   - On failure: the broken rule blocks the delete before `BeforeDelete` runs.
6. **Delete the root row.** Once the relationship rule passes, `BeforeDelete` issues `Delete PERSON` ([SQL-077](../sql/SQL-077.md), [PERSON](../database-tables.md#person)).
   - On failure (rows affected ≠ 1): add the `DeleteError` broken rule.
7. **Save children after the root row (`OnSaveComplete`).** In this fixed order: `Addresses`, `Emails`, `Phones`, then `ContactNote` (see [Address info CRUD and linking](address-info-crud-and-linking.md), [Email info CRUD and linking](email-info-crud-and-linking.md), [Phone info CRUD and linking](phone-info-crud-and-linking.md), [Long-note persistence](long-note-persistence.md)).

## Process flow

The following diagram visualizes the contact save/delete cascade, including the relationship-based delete guard.

```mermaid
flowchart TD
	Start([Caller calls Save]) --> Deleted{IsDeleted?}
	Deleted -->|Yes| RelRule{CustomerRelationships<br>count = 0?}
	RelRule -->|No| Blocked([Broken rule:<br>cannot delete while related])
	RelRule -->|Yes| ClearChildren[Clear Emails, Phones,<br>Addresses; delete ContactNote;<br>save those four now]
	ClearChildren --> DeleteRoot[Delete PERSON row]
	DeleteRoot --> DelCheck{RowsAffected = 1?}
	DelCheck -->|No| DelError([DeleteError added])
	DelCheck -->|Yes| DoneDel([Contact removed])
	Deleted -->|No| SetAudit[Set UpdateDate, UpdateUser;<br>copy Name into parts]
	SetAudit --> NoteCheck{ContactNote not new<br>and NoteText blank?}
	NoteCheck -->|Yes| DeleteNote[Delete ContactNote row]
	NoteCheck -->|No| RootSave
	DeleteNote --> RootSave[Insert or update PERSON row]
	RootSave --> RowCheck{RowsAffected = 1?}
	RowCheck -->|No| Error([InsertError / UpdateError added])
	RowCheck -->|Yes| Cascade[OnSaveComplete:<br>save Addresses, Emails,<br>Phones, ContactNote]
	Cascade --> Done([Save complete])
```

## Failure and edge-case behavior
- Deletion is blocked outright while any `COMPANY_PERSON` relationship row still references the contact; the caller must remove or reassign relationships first ([BR-003](../business-rules.md#br-003-contacts-with-customer-relationships-cannot-be-deleted)).
- The delete branch of `OnSave` returns immediately after saving the cleared child collections, skipping the composite-name copy and note-blank check performed on the non-delete path; those steps are not needed once the contact is being removed.
- Removing an item from `Addresses`, `Emails`, or `Phones` deletes only the link row (`PERSON_ADDRESS`, `PERSON_EMAIL`, `PERSON_PHONE`); the standalone info row is not deleted as part of this cascade.
- A blank long note is deleted rather than saved as an empty string ([BR-009](../business-rules.md#br-009-blank-long-notes-are-deleted-rather-than-stored-as-empty-text)).

## Related documentation
- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)
- [SQL catalog](../sql/index.md)
- [Database tables](../database-tables.md)

## Sources
- `Business\SAIF.Business.CustomerContact\Contact\clsContact.vb`
