# SaveContactPerson - Advanced Documentation ## Overview Creates or updates a person contact with validation of required fields (FirstName, LastName), phone/email formats, and preferred contact preferences. Verifies phone numbers via SMS integration if configured, uploads photos to S3, and rejects duplicate person contacts. Triggers JSON re-serialization for associated claims. ## Business Rules - The SecureApi layer forces `IsPerson = true` on the contact before passing it to `Contact.Save`, ensuring the correct validation path is used regardless of what the caller sends. - **Phone number normalization**: Legacy phone fields (AreaCode, Number, Extension) are shoehorned into `FullNumber` for backward API compatibility. Country code defaults to 1 for these legacy entries. - **Email normalization**: Email addresses are cleaned, trimmed of leading/trailing `@`, `.`, `[`, `]` characters, and whitespace removed. - **Person-specific validation**: - FirstName is required. - LastName is required. - SocialSecurityNumber, if provided, must match the SSN regex pattern (dashes allowed, spaces stripped). - **Shared contact validation** (applies to both person and company): - Preferred contact method must match available contact data (e.g., "Email" requires at least one email address; "Text" requires a textable phone). - Maximum 15 addresses, 15 email addresses, and 15 phone numbers. - Addresses with any populated field must have a Type. - AddressLine1 is validated to reject placeholder values ("unknown", "No address", "np", "x", "tbd"). - Email addresses must be valid format. - Phone numbers must be valid format per the SMS integration's validation. - **Address filtering**: Completely empty addresses are silently dropped (user does not need to explicitly delete blank address rows). - **Preferred item enforcement**: For addresses, emails, and phones, if no item is marked as preferred, the first item is automatically set as preferred. Only one preferred item is allowed per category. - **Phone verification**: If the company has an SMS integration configured, new or unverified phone numbers are verified via the SMS service (Twilio lookup). - **SSN cleanup**: Dashes and spaces are stripped. Empty values after cleanup are set to null. - **Photo upload**: If a base64-encoded photo is provided (not a redirect URL), it is uploaded to S3 at the path determined by the company GUID and contact GlobalID. The entity's Photo field is set to the current UTC timestamp to force cache invalidation. - **Digital signature/initials**: If provided, file entities are updated via `File.UpdateFileGE`, and old files are deleted after save. - **Name generation**: For person contacts, `Name` is set to null before save, forcing the entity to regenerate it from FirstName/LastName/etc. - **Firm ownership**: Existing contacts can only be saved by the firm that owns them (`FirmID` must match current company). - **Timestamps**: New contacts get `CreatedDate` and `Timestamp` set to `DateTime.UtcNow`. Existing dirty contacts get `Timestamp` updated. - **Post-save**: `CreateEntityFinishContactRun` is called within the transaction to trigger downstream processing (JSON re-serialization for associated claims). - **Duplicate detection**: The SecureApi catches the `IDX_UniquePerson` SQL index violation and translates it to a user-friendly "You cannot create duplicate contacts!" message. ## Permissions & Security - User must be logged in (checked at API layer). - User must have a company selected and be a member of that company (checked at API layer via `IsPartOfCompany`). - No specific named permission is required -- any authenticated firm member can save contacts. - All referenced ad hoc field staff IDs are validated as firm members via `ValidatePersonWasPartOfFirm`. - Existing contacts must belong to the current firm (FirmID check on the entity). ## Data Flow 1. SecureApi validates authentication, company membership, sets `IsPerson = true`, then calls `Contact.Save`. 2. Legacy phone fields normalized to `FullNumber`. 3. Email addresses cleaned and trimmed. 4. SMS integration info retrieved. 5. `ValidatePerson` checks IsPerson flag, required fields (FirstName, LastName), SSN format, preferred contact method consistency, collection limits, address/email/phone format. 6. Empty addresses filtered out. 7. Ad hoc field staff IDs validated against the firm. 8. Entity loaded; firm ownership verified for existing contacts. 9. Preferred item enforcement applied to addresses, emails, and phones. 10. Phone numbers verified via SMS integration for new/changed/unverified numbers (parallel execution). 11. SSN cleaned. 12. All scalar fields written to the entity (FirmID, IsPerson, personal details, contact preferences, accounting external ID, representative, language, etc.). 13. Name set to null (to trigger regeneration for person contacts). 14. Photo uploaded to S3 if base64 data provided. 15. Digital signature and initials files updated if provided. 16. Tags synced via `BaboContactTags` sub-entity. 17. Addresses synced via `BaboContactAddresses` sub-entity (keyed by Type). 18. Phones synced via `BaboContactPhones` sub-entity (keyed by Type). 19. Emails synced via `BaboContactEmails` sub-entity (keyed by Type). 20. Notes synced via `BaboContactNotes` sub-entity; new notes stamped with CreatedByID and CreatedDate. 21. Ad hoc fields synced via `BaboContactAdHocFields` sub-entity with staff permission enforcement. 22. Timestamps set (CreatedDate + Timestamp for new, Timestamp for dirty existing). 23. Transaction started. 24. Entity saved. 25. `CreateEntityFinishContactRun` triggers downstream JSON re-serialization. 26. Transaction committed. 27. Old digital signature/initial files deleted (post-commit). 28. Primary address, email, and phone populated from the preferred or first items. 29. Contact ID set from saved entity and returned. ## Side Effects - **Database writes**: BaboContacts record with sub-entities (addresses, phones, emails, notes, tags, ad hoc fields, relationships). Digital signature/initial file records. - **AWS S3**: Photo uploaded for new/changed photos. Old digital signature/initial files deleted after save. - **SMS/Twilio**: Phone numbers verified via SMS integration lookup for companies with SMS configured. Verification is done in parallel for all phones. - **JSON re-serialization**: `CreateEntityFinishContactRun` triggers re-serialization of associated claim JSON data. This is done within the transaction. - **File cleanup**: Old digital signature and digital initial files deleted post-commit. ## Error Conditions - `RadoloException("You must be logged in...")` if not authenticated. - `RadoloException("You must have a company selected...")` if no company context. - `RadoloException("You can only perform this action for a company you are a member of.")` if user is not a firm member. - `ArgumentNullException` if Contact is null. - `RadoloException` from `ValidatePerson` for missing FirstName, LastName, invalid SSN, preferred contact method mismatch, collection limits exceeded, invalid addresses/emails/phones. - `RadoloException("You cannot change firms on a client...")` if the contact's FirmID does not match the current company. - `RadoloException("You cannot create duplicate contacts!")` when the unique person index is violated (caught at SecureApi layer). - Entity save failures are translated via `ThrowNiceErrorForSave` into user-friendly messages. - `ValidatePersonWasPartOfFirm` throws if ad hoc field staff are not firm members. ## Usage Notes - `SaveContactPerson` and `SaveContactCompany` both call the same `Contact.Save` method -- the only difference is the `IsPerson` flag set at the SecureApi layer. - Sub-entities (addresses, phones, emails) are synced using `Type` as the key field. This means you cannot have two addresses/phones/emails with the same Type -- the second will overwrite the first. - Phone verification happens in parallel (`Task.WhenAll`) for performance, but this means multiple Twilio API calls may execute concurrently. - The contact's `Name` is explicitly set to null for person contacts before save, which forces the entity framework to regenerate it. For company contacts, the Name is passed through directly. - The digital signature and initial file deletion happens outside the transaction. If the file deletion fails, the old file is orphaned but the contact save is not rolled back. - The `CreateEntityFinishContactRun` call within the transaction handles the claim JSON re-serialization trigger. This ensures claim search data stays in sync with contact changes.