# SaveAdHocFieldDefinitionContacts - Advanced Documentation ## Overview Creates or updates a single contact ad hoc field definition (for person contacts, company contacts, or both), including its dropdown options and role-based view/edit permissions. The caller passes one field definition object per call. This is a long-running operation - it declares a `[LongRunningId] string ProcessID` parameter and is invoked through `ProcessStatus.Run`. This operation performs no dependent widget or form regeneration of any kind - the field is validated, saved, committed, and the cache is cleared. `IsSynced` is not used for this ad hoc field type and is ignored - it is never read or set anywhere in the method. ## Business Rules - The field definition is validated before any write: field label is required, must be a valid field name (cannot contain `,`, `"`, `#`), data type is required, and category is required (`ValidateFieldDefinition`) - The field label must be unique for the company within `vwAdHocFieldDefinitionsBaboContacts` only - There is no automatic sort order assignment for new fields in this method - `SortOrder` is taken directly from the incoming `Item.SortOrder` - `Item.UsedBy` is read but is not persisted as its own sub-type. It is translated into two boolean scalar fields on the entity itself: `IsPerson` (set true when `Item.UsedBy` contains an entry whose `Name` matches the configured `ContactsUsedByPersonName` app setting) and `IsCompany` (set true when it contains an entry matching the configured `ContactsUsedByCompanyName` app setting). There is no `UsedBy` sub-type for this operation. - Dropdown options, edit roles, and view roles are synced as Aptify sub-types (`AdHocFieldDefinitionsBaboContactEditRoles`, `AdHocFieldDefinitionsBaboContactViewRoles`, `AdHocFieldDefinitionsBaboContactDropdownOptions`) - existing rows not present in the incoming lists are removed, and missing rows are added. The edit and view role sub-types are synced with an explicit `RoleID` key; the dropdown option sub-type is synced without an explicit key argument, so the sub-type's own name field is used as the match key. - There is no pre-save widget-field recalculation step for this operation - There is no post-commit dependent-widget or form regeneration step - `UpdateFieldForDependentWidgets` is never called and no other widget search runs - The ad hoc field cache is cleared exactly once, after the transaction commits, via `ClearAdHocCache(AdHocFieldType.Contact, CompanyID)`. That single call performs two internal cache-clearing actions: it clears the Contact ad hoc field definitions cache and each affected field's dropdown-option cache for the firm (which in turn always clears the advanced-search field cache for the firm), and it separately clears the contact search field cache for the firm. ## Permissions & Security - Caller must be logged in (`curPerson.ID >= 1`) - Caller must have a company selected (`curCompany.ID >= 1`) - Caller must be a member of the current company (`IsPartOfCompany` check) - Caller must have the **ManageFirm** permission, checked inside the controller method (`Controllers.Role.Permissions.ManageFirm`) - The saved field definition is always scoped to the caller's current company (`CompanyID` is set from `_UnitOfWork.CurrentCompany.ID`, not from the incoming `Item`) ## Data Flow 1. Validate authentication, company membership, and company selection (in the service method, before the controller is invoked) 2. Check ManageFirm permission again inside the controller method 3. Validate the field definition shape (`ValidateFieldDefinition`) 4. Report progress via `ProcessStatus.SetProcessStatus(ProcessID, "Saving Field [{Item.FieldLabel}]")` 5. Query `vwAdHocFieldDefinitionsBaboContacts` for an existing field label match for this company (excluding the current record's ID); throw if found 6. Get (or create) the `AdHocFieldDefinitionsBaboContacts` Aptify entity object for `Item.ID` and set its scalar fields (`CompanyID`, `CategoryID`, `FieldLabel`, `DataType`, `SortOrder`, `IsPerson`, `IsCompany`, `Description`, `IsReadOnly`, `Format`) 7. Sync the edit role, view role, and dropdown option sub-types 8. Begin transaction 9. Save the entity object; throw if the save fails 10. Commit transaction 11. Set `Item.ID` to the saved record's ID 12. Clear the ad hoc field cache for `Contact` fields 13. Return the saved `Item` ## Side Effects - **Database write**: The `AdHocFieldDefinitionsBaboContacts` entity record is created or updated via Aptify - **Database writes**: Dropdown option, edit role, and view role sub-type rows are synced (added/removed) to match the incoming definition - **Cache invalidation**: The Contact ad hoc field definitions cache and each field's dropdown-option cache for the firm are cleared once, after commit - **Cache invalidation**: The advanced-search field cache for the firm is cleared once, as part of that same cache-clearing cascade - **Cache invalidation**: The contact search field cache for the firm is cleared once, as a separate step inside the same `ClearAdHocCache` call ## Error Conditions - `RadoloException("You must be logged in to perform this action.")` - caller not authenticated - `RadoloException("You must have a company selected to perform this action.")` - no company context - `RadoloException("You can only perform this action for a company you are a member of.")` - cross-company attempt - `RadoloException("You do not have permission to save person contact adhoc fields. Please contact your administrator.")` - missing ManageFirm permission - `RadoloException("Field Label is required.")` - missing field label - `RadoloException("Field Label can not contain ',\",#.")` - invalid field label characters - `RadoloException("Data Type is required.")` - missing data type - `RadoloException("Category is required.")` - missing or invalid category - `RadoloException("An ad hoc field with this field label[{FieldLabel}] already exists.")` - duplicate field label within contact ad hoc fields - `RadoloException("There was a problem saving this ad hoc field definition : {error}")` - Aptify entity save failed - There is no catch block anywhere in this method - any exception from validation, the duplicate check, or the save propagates unmodified to the caller ## Usage Notes - This is a long-running operation wrapped in `ProcessStatus.Run` with a `[LongRunningId]` `ProcessID` parameter, and the controller method does call `ProcessStatus.SetProcessStatus` once, before the duplicate-label check, to report "Saving Field [{Item.FieldLabel}]" - This operation has no dependent-widget regeneration step at all, so cost does not scale with `UsedBy` or anything else - `Item.UsedBy` determines only the `IsPerson`/`IsCompany` scalar flags on the saved record - it is read for that purpose but is never stored as its own list of rows - Because there is no catch block, a failure at any stage (validation, duplicate check, or save) always surfaces to the caller as an exception - there is no scenario where this call reports success while a downstream step silently failed - The incoming `Item.CompanyID` is not trusted; the field is always saved under the caller's current company