---
title: Phone info CRUD and linking
summary: Persists standalone PHONE rows, normalizes their stored value, and maintains the customer/contact link rows that reference them.
status: draft
owner: SAIF
last_reviewed: 2026-07-30
library: SAIF.Business.CustomerContact
reference_type: package
operation_id: OP-CC-07
trigger: Caller adds/edits a clsPhoneInfo in a customer's or contact's Phones collection and the parent saves
---

# Phone info CRUD and linking

`clsPhoneInfo` is a standalone `PHONE` row that concatenates its country code, area code, prefix, number, and extension into a single stored value (unless the number is foreign). As with addresses and emails, the owning collection tracks adds/removes and reconciles the `COMPANY_PHONE` / `PERSON_PHONE` link rows on save.

| Field | Value |
| --- | --- |
| **Operation ID** | OP-CC-07 |
| **Trigger** | Caller adds/removes a `clsPhoneInfo` in `clsCustomer.Phones` or `clsContact.Phones`, then the parent's `Save()` cascades into the collection; or `mdlCustomerContact.CreatePhone` is called directly |
| **Inputs** | `PhoneID`, `Value`/`FormattedValue`, `PhoneCode` (phone type), `IsPrimary`, `IsForeign` |
| **Outputs** | Persisted `PHONE` row; persisted or removed `COMPANY_PHONE` / `PERSON_PHONE` link row |
| **Primary components** | `clsPhoneInfo.Create`, `clsPhoneInfo.SetStoredVersion`, `clsPhoneInfo.AddBusinessRules`, `mdlCustomerContact.CreatePhone`, `clsInfoCollection.Save` |
| **Entry point** | `Business\SAIF.Business.CustomerContact\InfoItems\clsPhoneInfo.vb` |

## Steps

1. **Create a new phone row.** `Create()` assigns `PhoneID` from `GetNextSeqNum("PHONE_SEQ")`.
2. **Normalize the stored value.** `SetStoredVersion()` concatenates `CountryCode`, `AreaCode`, `Prefix`, `Number`, and `Extension` into the persisted phone value; this normalization is skipped when `IsForeign = True`, leaving the foreign value as entered.
3. **Validate** ([BR-007](../business-rules.md#br-007-phone-records-require-an-id-and-formatted-value)). `AddBusinessRules` registers `CommonBusinessRules.NonZeroDecimalRequired` for `PhoneID` and `CommonBusinessRules.TrimTextRequired` for `FormattedValue` (with the `Business_External_Users.ErrMsgPhoneRequired` message).
4. **Persist the root `PHONE` row.** Insert ([SQL-016](../sql/SQL-016.md)/[SQL-017](../sql/SQL-017.md)), update ([SQL-073](../sql/SQL-073.md)/[SQL-074](../sql/SQL-074.md)), and delete ([SQL-090](../sql/SQL-090.md)/[SQL-091](../sql/SQL-091.md)) against [PHONE](../database-tables.md#phone).
5. **Reconcile link rows** through `clsInfoCollection.Save()`, using the same add/remove-tracking pattern used for addresses and emails (see [Address info CRUD and linking](address-info-crud-and-linking.md)): save current phone rows, delete removed link rows (customer: [SQL-087](../sql/SQL-087.md) against [COMPANY_PHONE](../database-tables.md#company-phone); contact: [SQL-081](../sql/SQL-081.md) against [PERSON_PHONE](../database-tables.md#person-phone)), then insert added link rows (customer: [SQL-013](../sql/SQL-013.md); contact: [SQL-007](../sql/SQL-007.md)).
6. **Helper factory flow.** `mdlCustomerContact.CreatePhone(dpi, contact, phoneNumber, isForeignPhone, isPrimary, phoneType)`:
   - Creates `clsPhoneInfo.NewPhone(dpi)`.
   - Sets `Value`, `IsPrimary`, and `IsForeign`.
   - Resolves `PhoneCode` from `mdlCodeMaps.GetPhoneTypes(dpi)(phoneType).Code`.
   - Adds the new phone to `contact.Phones` **only if `phone.IsValid`** — invalid phones are constructed but not attached to the contact.
7. **Resolve the phone-type description.** `PhoneTypeDescription` calls `GetPhoneCode(dpi)(PhoneCode).Description` from the shared `SAIF.Business` code-map module for display purposes only.

## Process flow

The following diagram visualizes phone normalization and the save/link cycle, including the helper factory's validity gate.

```mermaid
flowchart TD
	Start([Caller sets phone fields]) --> Foreign{IsForeign?}
	Foreign -->|No| Normalize[SetStoredVersion:<br>concatenate country/area/prefix/number/ext]
	Foreign -->|Yes| KeepForeign[Keep foreign value as entered]
	Normalize --> Validate{PhoneID and<br>FormattedValue valid?}
	KeepForeign --> Validate
	Validate -->|No| Blocked([Broken rule added])
	Validate -->|Yes| ParentSave([Parent Save cascades to collection])
	ParentSave --> SaveRows[Save each current PHONE row]
	SaveRows --> DeleteLinks[Delete link rows for RemovedItems]
	DeleteLinks --> InsertLinks[Insert link rows for AddedItems]
	InsertLinks --> Done([Collection persisted])
	Helper([mdlCustomerContact.CreatePhone]) --> Build[Build clsPhoneInfo;<br>resolve PhoneCode from mdlCodeMaps]
	Build --> ValidGate{phone.IsValid?}
	ValidGate -->|Yes| Attach[Add to contact.Phones]
	ValidGate -->|No| Discard([Phone object returned<br>but not attached])
```

## Failure and edge-case behavior
- Missing/zero `PhoneID` or a blank `FormattedValue` blocks the save with a broken rule before the `PHONE` insert/update executes ([BR-007](../business-rules.md#br-007-phone-records-require-an-id-and-formatted-value)).
- `mdlCustomerContact.CreatePhone` only attaches the phone to the contact when it is already valid, so a caller relying on this helper never sees an invalid phone silently added to the collection — unlike the equivalent email helper, `CreateEmail`, which adds unconditionally.
- Removing a phone from a collection deletes only the `COMPANY_PHONE` / `PERSON_PHONE` link row, not the standalone `PHONE` row.*
- Foreign phone numbers bypass the country/area/prefix/extension concatenation entirely, so their stored format depends solely on what the caller supplied in `Value`.*

> **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\InfoItems\clsPhoneInfo.vb`
- `Business\SAIF.Business.CustomerContact\InfoItems\clsInfoCollection.vb`
- `Business\SAIF.Business.CustomerContact\Customer\clsCustomerPhoneCollection.vb`
- `Business\SAIF.Business.CustomerContact\Contact\clsContactPhoneCollection.vb`
- `Business\SAIF.Business.CustomerContact\mdlCustomerContact.vb`
