Skip to content

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). 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).
  4. Insert the relationship row. BeforeInsert binds COMPANY_SEQ_NUM, PERSON_SEQ_NUM, RELATIONSHIP_CODE, and TITLE (SQL-010, COMPANY_PERSON).
  5. Update the relationship row. BeforeUpdate sets RELATIONSHIP_CODE and TITLE (SQL-069, COMPANY_PERSON).
  6. 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.*
  7. Delete the relationship row. The delete criteria match on customer ID, contact ID, and relationship code (SQL-084, COMPANY_PERSON).
  8. Retrieve related contacts for a customer. clsCustomer.Contacts loads clsCustomerContactCollection.GetCustomerContacts(...), which joins COMPANY_PERSON to PERSON for the customer's ID (SQL-038, COMPANY_PERSON, PERSON); each row is loaded into a clsCustomerContact with an embedded clsContact.Load(...).
  9. Retrieve related companies for a contact. clsContact.CustomerRelationships calls GetRelationships(), which joins COMPANY_PERSON to COMPANY for the contact's ID (SQL-028, COMPANY_PERSON, COMPANY) and returns a read-only clsCustomerRelationship list (used by Contact CRUD with child persistence to enforce the no-relationships-on-delete rule).
  10. 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.

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).
  • 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.

Sources

  • Business\SAIF.Business.CustomerContact\Customer\clsCustomerContact.vb
  • Business\SAIF.Business.CustomerContact\Contact\clsContact.vb