---
title: Customer CRUD with child persistence
summary: Creates, retrieves, updates, and deletes the root customer (COMPANY) record and cascades saves to its address, email, phone, contact, and note children.
status: draft
owner: SAIF
last_reviewed: 2026-07-30
library: SAIF.Business.CustomerContact
reference_type: package
operation_id: OP-CC-02
trigger: Caller creates or retrieves a clsCustomer, edits properties or child collections, and calls Save()
---

# Customer CRUD with child persistence

`clsCustomer` owns the root `COMPANY` row and lazily exposes its address, email, phone, customer-contact relationship, and long-note children as properties. A single `Save()` call persists the root row first, then cascades to every child collection in a fixed order so link rows always reference an existing parent.

| Field | Value |
| --- | --- |
| **Operation ID** | OP-CC-02 |
| **Trigger** | `clsCustomer.NewCustomer(dpi)` or `clsCustomer.GetCustomer(dpi, customerID)`, followed by property/child edits and `Save()` |
| **Inputs** | Root customer fields (name, type code, tax ID, active flag, internal reference ID), `LongNoteText`, and any of the `Addresses`, `Emails`, `Phones`, `Contacts` child collections |
| **Outputs** | Persisted `COMPANY` row; persisted child address/email/phone/contact-relationship/note rows; `InsertError`/`UpdateError`/`DeleteError` broken rules on contention |
| **Primary components** | `clsCustomer.Create`, `clsCustomer.GetCustomer`, `clsCustomer.OnSave`, `clsCustomer.OnSaveComplete`, `clsCustomer.BeforeInsert`, `clsCustomer.BeforeUpdate`, `clsCustomer.BeforeDelete` |
| **Entry point** | `Business\SAIF.Business.CustomerContact\Customer\clsCustomer.vb` |

## Steps

1. **Create a new customer.** `Create()` assigns a new `CustomerID` from `GetNextSeqNum("COMPANY_SEQ")` on the data-portal info.
2. **Retrieve an existing customer.** `GetCustomer(dpi, customerID)` runs the BOF retrieve pipeline (`BeforeRetrieve`/`AfterRetrieve`) against `COMPANY` ([SQL-035](../sql/SQL-035.md), [COMPANY](../database-tables.md#company)); the `Addresses`, `Emails`, `Phones`, `Contacts`, and `CustomerNote` child properties are lazy-loaded the first time they are accessed rather than during retrieve.
3. **Save pre-processing (`OnSave`).**
   - If the customer `IsDeleted`: clear `Emails`, `Phones`, and `Addresses` in memory, call `CustomerNote.Delete()`, and immediately save those four child objects before the root delete proceeds.
   - Otherwise: set `_UpdateDate = Now` and `_UpdateUser` from the data-portal info user ID.
   - If the existing `CustomerNote` is not new and its `NoteText` is blank, call `CustomerNote.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 `COMPANY` fields ([SQL-008](../sql/SQL-008.md), [COMPANY](../database-tables.md#company)).
     - On failure (rows affected ≠ 1): add the `InsertError` broken rule.
   - Update: `BeforeUpdate` re-binds the same field set ([SQL-068](../sql/SQL-068.md), [COMPANY](../database-tables.md#company)).
     - On failure (rows affected ≠ 1): add the `UpdateError` broken rule with the DB-contention message.
   - Delete: `BeforeDelete` issues `Delete COMPANY Where COMPANY_SEQ_NUM = :CustomerID` ([SQL-082](../sql/SQL-082.md), [COMPANY](../database-tables.md#company)).
     - On failure (rows affected ≠ 1): add the `DeleteError` broken rule.
5. **Save children after the root row (`OnSaveComplete`).** In this fixed order: `Addresses`, `Emails`, `Phones`, `Contacts`, then `CustomerNote`. Each child collection or object persists through its own criteria and SQL (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), [Customer-contact relationship CRUD](customer-contact-relationship-crud.md), [Long-note persistence](long-note-persistence.md)).
6. **Resolve the customer-type description.** `CustomerTypeDescription` calls `GetCustomerCode(dpi)(CustomerTypeCode).Description` from the shared `SAIF.Business` code-map module to translate the stored type code into a display description; this is a lookup only and performs no persistence.

## Process flow

The following diagram visualizes the customer save cascade.

```mermaid
flowchart TD
	Start([Caller calls Save]) --> Deleted{IsDeleted?}
	Deleted -->|Yes| ClearChildren[Clear Emails, Phones, Addresses;<br>delete CustomerNote;<br>save those four now]
	ClearChildren --> DeleteRoot[Delete COMPANY row]
	Deleted -->|No| SetAudit[Set UpdateDate, UpdateUser]
	SetAudit --> NoteCheck{CustomerNote not new<br>and NoteText blank?}
	NoteCheck -->|Yes| DeleteNote[Delete CustomerNote row]
	NoteCheck -->|No| RootSave
	DeleteNote --> RootSave[Insert or update COMPANY row]
	RootSave --> RowCheck{RowsAffected = 1?}
	RowCheck -->|No| Error([InsertError / UpdateError added])
	RowCheck -->|Yes| Cascade[OnSaveComplete:<br>save Addresses, Emails,<br>Phones, Contacts, CustomerNote]
	DeleteRoot --> RowCheck2{RowsAffected = 1?}
	RowCheck2 -->|No| DeleteError([DeleteError added])
	RowCheck2 -->|Yes| Done2([Customer removed])
	Cascade --> Done([Save complete])
```

## Failure and edge-case behavior
- A contention failure on the root insert/update/delete adds the corresponding broken rule, but the child-save cascade in `OnSaveComplete` still runs unconditionally afterward; there is no explicit short-circuit tied to the root row's row-count check.*
- On delete, `Contacts` (the customer-contact relationship collection) is not cleared or explicitly cleaned up in `OnSave`, unlike `Emails`, `Phones`, and `Addresses`; any `COMPANY_PERSON` cleanup for a deleted customer is not evident in this class.*
- Removing an item from `Addresses`, `Emails`, or `Phones` deletes only the link row (`COMPANY_ADDRESS`, `COMPANY_EMAIL`, `COMPANY_PHONE`); the standalone `ADDRESS`/`EMAIL_ADDRESS`/`PHONE` row is not deleted as part of this cascade.
- A blank long note is deleted rather than saved as an empty string, so retrieving a customer with no note always yields a fresh, clean note object rather than a stored blank row ([BR-009](../business-rules.md#br-009-blank-long-notes-are-deleted-rather-than-stored-as-empty-text)).

> **Note:** Statements marked with `*` were inferred from code structure, naming
> conventions, or contextual evidence where direct confirmation was not available
> in source artifacts.

## 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\Customer\clsCustomer.vb`
