---
title: Customer-contact relationship CRUD
summary: Persists the COMPANY_PERSON relationship row that links a customer to a contact, independent of the owning PERSON row, and retrieves related parties from either side.
status: draft
owner: SAIF
last_reviewed: 2026-07-30
library: SAIF.Business.CustomerContact
reference_type: package
operation_id: OP-CC-04
trigger: Caller adds/edits a clsCustomerContact through a customer's Contacts collection, or retrieves relationships from either side
---

# Customer-contact relationship CRUD

`clsCustomerContact` is the row object for `COMPANY_PERSON`, the many-to-many relationship between a customer and a contact. It always saves its associated `clsContact` first, then persists its own relationship fields. Related parties can be retrieved from either direction: a customer's contacts, or a contact's customer relationships.

| Field | Value |
| --- | --- |
| **Operation ID** | OP-CC-04 |
| **Trigger** | `clsCustomerContactCollection.NewCustomerContact()` / `AddContact(inContact)` followed by `Save()`, or a read of `clsCustomer.Contacts` / `clsContact.CustomerRelationships` |
| **Inputs** | `RelationshipCode`, `Title`, the associated contact object (new or existing), the owning customer ID and contact ID |
| **Outputs** | Persisted `COMPANY_PERSON` row; collection of related contacts for a customer, or related companies for a contact |
| **Primary components** | `clsCustomerContact.OnSave`, `clsCustomerContact.AddBusinessRules`, `clsCustomerContactCollection.GetCustomerContacts`, `clsContact.GetRelationships` |
| **Entry point** | `Business\SAIF.Business.CustomerContact\Customer\clsCustomerContact.vb` |

## Steps

1. **Create the relationship wrapper.** `clsCustomerContactCollection.NewCustomerContact()` creates a new `clsContact` (`clsContact.NewContact(...)`) and wraps it in a new `clsCustomerContact`; `AddContact(inContact)` wraps an existing contact instead.
2. **Validate the relationship code** ([BR-004](../business-rules.md#br-004-customer-contact-links-require-a-relationship-code)). `AddBusinessRules` registers `CommonBusinessRules.TrimTextRequired` against `RelationshipCode`.
3. **Save pre-processing.** `clsCustomerContact.OnSave()` always calls `SaveChildObject(Contact)` first, so the underlying `PERSON` row is inserted or updated before the `COMPANY_PERSON` row is written — this matters for a brand-new contact, whose `PERSON_SEQ_NUM` must exist before it can be referenced by the relationship insert (see [Contact CRUD with child persistence](contact-crud-with-child-persistence.md)).
4. **Insert the relationship row.** `BeforeInsert` binds `COMPANY_SEQ_NUM`, `PERSON_SEQ_NUM`, `RELATIONSHIP_CODE`, and `TITLE` ([SQL-010](../sql/SQL-010.md), [COMPANY_PERSON](../database-tables.md#company-person)).
5. **Update the relationship row.** `BeforeUpdate` sets `RELATIONSHIP_CODE` and `TITLE` ([SQL-069](../sql/SQL-069.md), [COMPANY_PERSON](../database-tables.md#company-person)).
   - **Uncertain / likely defect:** the update statement's `WHERE` clause matches on `COMPANY_SEQ_NUM`, `PERSON_SEQ_NUM`, **and** `RELATIONSHIP_CODE = :RelationshipCode` using the same (new) parameter value used in the `SET` clause — if the relationship code is being changed, the `WHERE` clause will not match the existing row (which still has the old code), so the update may affect zero rows.*
6. **Delete the relationship row.** The delete criteria match on customer ID, contact ID, and relationship code ([SQL-084](../sql/SQL-084.md), [COMPANY_PERSON](../database-tables.md#company-person)).
7. **Retrieve related contacts for a customer.** `clsCustomer.Contacts` loads `clsCustomerContactCollection.GetCustomerContacts(...)`, which joins `COMPANY_PERSON` to `PERSON` for the customer's ID ([SQL-038](../sql/SQL-038.md), [COMPANY_PERSON](../database-tables.md#company-person), [PERSON](../database-tables.md#person)); each row is loaded into a `clsCustomerContact` with an embedded `clsContact.Load(...)`.
8. **Retrieve related companies for a contact.** `clsContact.CustomerRelationships` calls `GetRelationships()`, which joins `COMPANY_PERSON` to `COMPANY` for the contact's ID ([SQL-028](../sql/SQL-028.md), [COMPANY_PERSON](../database-tables.md#company-person), [COMPANY](../database-tables.md#company)) and returns a read-only `clsCustomerRelationship` list (used by [Contact CRUD with child persistence](contact-crud-with-child-persistence.md) to enforce the no-relationships-on-delete rule).
9. **Resolve the relationship description.** `Relationship` calls `GetRelationshipCode(dpi)(RelationshipCode).Description` from the shared `SAIF.Business` code-map module for display purposes only.

## Process flow

The following diagram visualizes relationship persistence and the two retrieval directions.

```mermaid
flowchart TD
	Start([Caller adds/edits clsCustomerContact]) --> Validate{RelationshipCode populated?}
	Validate -->|No| Blocked([Broken rule: relationship code required])
	Validate -->|Yes| SaveContact[SaveChildObject: save PERSON row first]
	SaveContact --> SaveLink[Insert or update COMPANY_PERSON row]
	SaveLink --> Done([Relationship persisted])
	RetrieveA([clsCustomer.Contacts]) --> QueryA[Join COMPANY_PERSON to PERSON<br>by CustomerID]
	QueryA --> ResultA([Collection of related contacts])
	RetrieveB([clsContact.CustomerRelationships]) --> QueryB[Join COMPANY_PERSON to COMPANY<br>by ContactID]
	QueryB --> ResultB([Collection of related companies])
```

## Failure and edge-case behavior
- Blank or whitespace-only `RelationshipCode` blocks the save with a broken rule before any SQL executes ([BR-004](../business-rules.md#br-004-customer-contact-links-require-a-relationship-code)).
- Because `OnSave` always saves the contact child first, a failed contact save (for example, a contention error on `PERSON`) can leave the relationship row unsaved even though the contact's own save reported the specific error; callers must inspect both objects' broken-rule collections.*
- The relationship-code update binding described in step 5 is a suspected defect: changing `RelationshipCode` on an existing relationship may silently fail to match any row because the `WHERE` clause still filters on the new code value instead of the original.*
- `clsContact.CustomerRelationships` is read-only from the contact side; there is no delete/insert entry point for the relationship exposed through `clsContact` itself — relationship maintenance goes through the customer's `Contacts` collection.*

> **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\clsCustomerContact.vb`
- `Business\SAIF.Business.CustomerContact\Contact\clsContact.vb`
