---
title: Long-note persistence
summary: Persists long-form notes for a customer or contact in a separate table from the root row, and treats a blank note as a delete rather than an empty save.
status: draft
owner: SAIF
last_reviewed: 2026-07-30
library: SAIF.Business.CustomerContact
reference_type: package
operation_id: OP-CC-08
trigger: Caller reads or writes clsCustomer.LongNoteText or clsContact.LongNoteText, then the parent saves
---

# Long-note persistence

`clsCustomerNote` and `clsContactNote` are near-identical wrappers that store a customer's or contact's long-form note in `COMPANY_NOTE` or `PERSON_NOTE`, keyed by the owner's ID rather than embedded in the root `COMPANY`/`PERSON` row. Both classes are merged into this single operation page because they share the same retrieve/fill/save/delete pattern and differ only in their owning table and key column.

| Field | Value |
| --- | --- |
| **Operation ID** | OP-CC-08 |
| **Trigger** | Caller sets `clsCustomer.LongNoteText` or `clsContact.LongNoteText`, or reads it after a retrieve; persistence happens when the parent's `Save()` cascades to the note child in `OnSaveComplete` |
| **Inputs** | `NoteText` (up to 4000 characters), owning `CustomerID` or `ContactID` |
| **Outputs** | Persisted or deleted `COMPANY_NOTE` / `PERSON_NOTE` row |
| **Primary components** | `clsCustomerNote.GetNote`, `clsCustomerNote.AfterRetrieve`, `clsContactNote.GetNote`, `clsContactNote.AfterRetrieve`, `clsCustomer.OnSave`, `clsContact.OnSave` |
| **Entry point** | `Business\SAIF.Business.CustomerContact\Customer\clsCustomerNote.vb`, `Business\SAIF.Business.CustomerContact\Contact\clsContactNote.vb` |

## Steps

1. **Expose the note through the parent.** `clsCustomer.LongNoteText` proxies `CustomerNote.NoteText`; `clsContact.LongNoteText` proxies `ContactNote.NoteText`. The `CustomerNote`/`ContactNote` property lazily calls `clsCustomerNote.GetNote(dpi, customerID)` / `clsContactNote.GetNote(dpi, contactID)` on first access.
2. **Retrieve the note row.** `GetNote` runs the BOF retrieve pipeline keyed by the owner ID (customer: [SQL-040](../sql/SQL-040.md) against [COMPANY_NOTE](../database-tables.md#company-note); contact: [SQL-033](../sql/SQL-033.md) against [PERSON_NOTE](../database-tables.md#person-note)).
3. **Synthesize a note object when none exists.** `AfterRetrieve` fills every returned row and calls `MarkOld()`; if the resulting `NoteText` is blank (no row was found, or the row's text is empty), it calls `Create()` and `MarkClean()` so callers always receive a usable, non-dirty note object even when no `COMPANY_NOTE`/`PERSON_NOTE` row exists.
4. **Delete-on-blank at parent save time** ([BR-009](../business-rules.md#br-009-blank-long-notes-are-deleted-rather-than-stored-as-empty-text)). In the parent's `OnSave()`, if the note `Is Not IsNew` and `NoteText.Trim.Length = 0`, the parent calls `CustomerNote.Delete()` / `ContactNote.Delete()` before the save cascade runs, so an existing note that has been cleared out is removed rather than persisted as an empty string.
5. **Persist the note in the parent's save cascade.** `OnSaveComplete` calls `SaveChildObject(CustomerNote)` / `SaveChildObject(ContactNote)` alongside the other child collections, which runs whichever of insert/update/delete applies:
   - Insert: `NOTE_TEXT` and the owner key (customer: [SQL-012](../sql/SQL-012.md); contact: [SQL-006](../sql/SQL-006.md)).
   - Update: `NOTE_TEXT` by owner key (customer: [SQL-070](../sql/SQL-070.md); contact: [SQL-067](../sql/SQL-067.md)).
   - Delete: by owner key (customer: [SQL-086](../sql/SQL-086.md); contact: [SQL-080](../sql/SQL-080.md)).
   - On failure (rows affected ≠ 1) for any of insert/update/delete: the corresponding `InsertError`/`UpdateError`/`DeleteError` broken rule is added and the note object is not marked clean/new.

## Process flow

The following diagram visualizes the shared retrieve/blank-delete/save cycle used by both note types.

```mermaid
flowchart TD
	Start([Parent retrieves CustomerNote/ContactNote]) --> Query[Query COMPANY_NOTE/PERSON_NOTE<br>by owner key]
	Query --> Found{Row found with<br>non-blank NoteText?}
	Found -->|Yes| MarkOld[Fill row; MarkOld]
	Found -->|No| Synthesize[Create blank note object; MarkClean]
	MarkOld --> Ready([Note object ready for LongNoteText access])
	Synthesize --> Ready
	Ready --> ParentSave([Parent Save runs])
	ParentSave --> BlankCheck{Note not new<br>and NoteText blank?}
	BlankCheck -->|Yes| DeleteNote[Delete note row]
	BlankCheck -->|No| SaveNote[Insert or update note row]
	DeleteNote --> Done([Save cascade continues])
	SaveNote --> Done
```

## Failure and edge-case behavior
- A blank note is deleted rather than persisted as empty text, so a customer/contact that never had a note (or whose note was cleared) always retrieves 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)).
- The blank-delete check only applies when the note `Is Not IsNew`; a brand-new note that is created and left blank is simply never inserted, since `OnSaveComplete`'s `SaveChildObject` call has nothing dirty to persist.*
- A contention failure (rows affected ≠ 1) on note insert/update/delete surfaces as a broken rule on the note object, not on the parent customer/contact, so callers must check both objects' broken-rule collections after `Save()`.*

> **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\clsCustomerNote.vb`
- `Business\SAIF.Business.CustomerContact\Contact\clsContactNote.vb`
- `Business\SAIF.Business.CustomerContact\Customer\clsCustomer.vb`
- `Business\SAIF.Business.CustomerContact\Contact\clsContact.vb`
