Skip to content

Contact CRUD with child persistence

clsContact owns the root PERSON row and exposes address, email, phone, and long-note children the same way clsCustomer does. The key difference is a delete-time business rule: a contact cannot be deleted while it is still linked to any customer through COMPANY_PERSON.

Field Value
Operation ID OP-CC-03
Trigger clsContact.NewContact(dpi) or clsContact.GetContact(dpi, contactID), followed by property/child edits and Save()
Inputs Root contact fields (composite Name, split into FirstName/LastName/MiddleInitial/PreferredName/NameExtension), LongNoteText, and any of the Addresses, Emails, Phones child collections
Outputs Persisted PERSON row; persisted child address/email/phone/note rows; delete blocked by a broken rule while relationships exist; InsertError/UpdateError/DeleteError broken rules on contention
Primary components clsContact.Create, clsContact.GetContact, clsContact.OnSave, clsContact.OnSaveComplete, clsContact.ValidateNoRelationshipsOnDelete, clsContact.GetRelationships
Entry point Business\SAIF.Business.CustomerContact\Contact\clsContact.vb

Steps

  1. Create a new contact. Create() assigns a new ContactID from GetNextSeqNum("PERSON_SEQ"), and sets CreateDate and CreateUser from the data-portal info.
  2. Retrieve an existing contact. GetContact(dpi, contactID) runs the BOF retrieve pipeline against PERSON (SQL-029, PERSON); Addresses, Emails, Phones, and ContactNote are lazy-loaded on first access.
  3. Save pre-processing (OnSave).
  4. If the contact IsDeleted: clear Emails, Phones, and Addresses, call ContactNote.Delete(), save those four child objects, and return immediately — the parent delete path below still runs, but no further composite-name or note-blank processing happens on this branch.
  5. Otherwise: set _UpdateDate and _UpdateUser, and copy the composite Name value into the discrete _FirstName, _LastName, _MiddleInitial, and _PreferredName/_NameExtension fields.
  6. If the existing ContactNote is not new and its NoteText is blank, call ContactNote.Delete() so the note row is removed rather than stored empty (BR-009).
  7. Persist the root row.
  8. Insert: BeforeInsert binds all root PERSON fields (SQL-003, PERSON).
    • On failure (rows affected ≠ 1): add the InsertError broken rule.
  9. Update: BeforeUpdate re-binds the same field set (SQL-066, PERSON).
    • On failure (rows affected ≠ 1): add the UpdateError broken rule with the DB-contention message.
  10. Validate before delete (BR-003). ValidateOnDelete = True registers ValidateNoRelationshipsOnDelete, which returns True unless IsDeleted, in which case it returns Me.CustomerRelationships.Count = 0.
  11. CustomerRelationships invokes GetRelationships(), which queries COMPANY_PERSON joined to COMPANY for the contact's identifier (SQL-028, COMPANY_PERSON, COMPANY) and materializes a clsCustomerRelationship per row, resolving the relationship description through GetRelationshipCode(dpi).
  12. On failure: the broken rule blocks the delete before BeforeDelete runs.
  13. Delete the root row. Once the relationship rule passes, BeforeDelete issues Delete PERSON (SQL-077, PERSON).
  14. On failure (rows affected ≠ 1): add the DeleteError broken rule.
  15. Save children after the root row (OnSaveComplete). In this fixed order: Addresses, Emails, Phones, then ContactNote (see Address info CRUD and linking, Email info CRUD and linking, Phone info CRUD and linking, Long-note persistence).

Process flow

The following diagram visualizes the contact save/delete cascade, including the relationship-based delete guard.

flowchart TD
    Start([Caller calls Save]) --> Deleted{IsDeleted?}
    Deleted -->|Yes| RelRule{CustomerRelationships<br>count = 0?}
    RelRule -->|No| Blocked([Broken rule:<br>cannot delete while related])
    RelRule -->|Yes| ClearChildren[Clear Emails, Phones,<br>Addresses; delete ContactNote;<br>save those four now]
    ClearChildren --> DeleteRoot[Delete PERSON row]
    DeleteRoot --> DelCheck{RowsAffected = 1?}
    DelCheck -->|No| DelError([DeleteError added])
    DelCheck -->|Yes| DoneDel([Contact removed])
    Deleted -->|No| SetAudit[Set UpdateDate, UpdateUser;<br>copy Name into parts]
    SetAudit --> NoteCheck{ContactNote not new<br>and NoteText blank?}
    NoteCheck -->|Yes| DeleteNote[Delete ContactNote row]
    NoteCheck -->|No| RootSave
    DeleteNote --> RootSave[Insert or update PERSON row]
    RootSave --> RowCheck{RowsAffected = 1?}
    RowCheck -->|No| Error([InsertError / UpdateError added])
    RowCheck -->|Yes| Cascade[OnSaveComplete:<br>save Addresses, Emails,<br>Phones, ContactNote]
    Cascade --> Done([Save complete])

Failure and edge-case behavior

  • Deletion is blocked outright while any COMPANY_PERSON relationship row still references the contact; the caller must remove or reassign relationships first (BR-003).
  • The delete branch of OnSave returns immediately after saving the cleared child collections, skipping the composite-name copy and note-blank check performed on the non-delete path; those steps are not needed once the contact is being removed.
  • Removing an item from Addresses, Emails, or Phones deletes only the link row (PERSON_ADDRESS, PERSON_EMAIL, PERSON_PHONE); the standalone info row is not deleted as part of this cascade.
  • A blank long note is deleted rather than saved as an empty string (BR-009).

Sources

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