# SaveCalendarEvent - Advanced Documentation ## Overview Creates or updates a calendar event with permission validation, resource conflict checking, and recurrence pattern enforcement. Syncs with external calendar providers (Microsoft Graph, Google Calendar) for connected organizers. Handles all-day time zone conversion, recurring event exceptions, and linked claim field updates. ## Business Rules - **All-day events**: Start and end dates are converted to the user's local time zone. The end date is set to the last second of the final day (local midnight minus 1 second), then both are converted back to UTC for storage. - **Validation requirements**: - Title is required. - Start date and end date must both be valid (after `BLANK_DATE`). - Start date must be before end date. - New events must have at least one attendee; existing events may omit attendees (null means no change). - Event status, if provided, must be a valid `EventStatusType` enum value. - Events with a RecurrenceRule must be marked as `IsRecurringEvent`. - Recurring events cannot overlap themselves (daily, weekly, monthly patterns validated against the event duration). - Weekly recurring events with ByDay rules must start on one of the specified days. - Monthly/yearly recurring events with ByMonthDay/ByMonth rules must start on the specified day/month. - **Private events**: Only existing attendees or editors can update a private event. - **Event edit permissions**: Non-attendees cannot update an existing event unless they have editor access. - **Resource conflict checking**: Each resource is checked for time overlaps with other events in the same firm. Recurring event instances (via RecurrenceID) are handled by checking against the parent event ID. - **Organizer selection for new events**: 1. If disconnecting from a recurring parent, use the parent's organizer. 2. Otherwise, if the creator is an attendee and has a connected calendar, use the creator. 3. If the creator is not valid, iterate through attendees to find the first one with a valid external calendar connection. - **External calendar sync**: New events are immediately saved to the organizer's external calendar (Graph/Google). If the external save fails, a Process Flow Run is created to retry later. - **Recurring event instances**: When an event has a `RecurrenceID`, it is being disconnected from a recurring parent. The recurrence fields are cleared, linked fields are nulled, and a recurrence exception is added to the parent event. - **Recurring event date changes**: If start or end dates change on a recurring event with existing exceptions, all exceptions are cleared and disconnected instances are deleted (mimicking Outlook behavior). - **Reminders**: If a reminder is set, `ReminderDate` is calculated from the start date (or next valid recurring instance). Changing the reminder date nulls `ReminderDateSent` to allow re-notification. - **Digest reminders**: Reminder dates are calculated based on local start date minus the configured number of days. - **Attendee removal notifications**: When attendees are removed from a non-externally-synced event, removal emails are sent to each removed attendee. - **New event emails**: For new events without an external calendar sync, creation notification emails are sent to all attendees. - **Linked claim fields**: If the event's start date changed and `UpdateLinkedFields` is true, the linked claim ad hoc or system field is updated to match. ## Permissions & Security - User must be logged in (checked at both API layer and controller). - User must be a member of the current company (checked at API layer via `IsPartOfCompany`). - `HasPermissionForPrivateEvent`: For existing private events, the current user must be an attendee or an editor. - `HasPermissionToUpdateEvent`: For existing events, the current user must be an attendee (new events skip this check). - All referenced person IDs (ad hoc field staff, attendees) are validated as firm members via `ValidatePersonWasPartOfFirm`. - No specific named permission is required -- any authenticated firm member can create/edit events they are part of. ## Data Flow 1. SecureApi validates authentication and company membership, then calls `Calendar.SaveEvent(FirmID, PersonID, Event, UpdateLinkedFields=true)`. 2. All-day event date adjustment applied if applicable. 3. `Validate` checks required fields, date ordering, attendees, status, and recurrence rules. 4. Aptify generic entity loaded for the event. 5. Existing attendees and editors retrieved from the entity for permission checks. 6. Private event and edit permission checks executed. 7. Referenced person IDs validated against the firm. 8. Private event editors synced. For new events, the creator is added as an editor. 9. Associated claims synced via `CalendarEventAssociatedClaims` sub-entity. 10. Attendee changes processed: organizer removal tracked, removal emails sent for non-synced events, attendees synced. 11. Contacts, documents, and resources synced (with resource conflict checking). 12. Event type set; if changed, the removed type name is stored for tracking. 13. Digest reminders synced with calculated reminder dates. 14. Scalar fields written (Description, dates, location, title, FirmID, AllDayEvent, Reminder, linked fields, status). 15. Reminder date calculated and set based on recurrence state. 16. TimeZone stored on the entity. 17. CreatedByID set for new events. 18. Recurring event disconnection handled (RecurrenceID processing, parent exception added). 19. Recurrence rule formatted and set; date changes on recurring events with exceptions trigger exception cleanup. 20. Ad hoc fields synced. 21. Timestamp updated if entity is dirty or new. 22. Transaction started. 23. External calendar organizer determined (parent organizer, creator, or first connected attendee). 24. Event entity saved to database. 25. For new events with a valid organizer, external calendar event created (with Process Flow Run fallback on failure). 26. Transaction committed (always, even if external save fails). 27. New event emails sent for events without external sync. 28. Linked claim fields updated if start date changed and `UpdateLinkedFields` is true. 29. Event returned with updated ID and timestamp. ## Side Effects - **Database writes**: Calendar event record with sub-entities (attendees, editors, associated claims, contacts, documents, resources, ad hoc fields, digest reminders). Recurrence exceptions on parent events. Process Flow Runs for failed external saves. - **External calendar sync**: Creates event in Microsoft Graph or Google Calendar for the organizer. On failure, creates a Process Flow Run for deferred retry. - **Email notifications**: - Attendee removal emails when attendees are removed from non-synced events. - Event creation emails to all attendees for new non-synced events. - **Recurring event cleanup**: Deleting disconnected instances when a recurring event's dates change. - **Linked claim field updates**: Claim ad hoc or system date fields updated when the event start date changes. - **Parent event modification**: Recurrence exception added to parent when creating a disconnected instance. ## 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. - `RadoloException("You do not have permission to update this private event...")` if the user is not an attendee or editor of a private event. - `RadoloException("You do not have permission to update this event...")` if the user is not an attendee of an existing event. - `RadoloException` from `Validate` for missing title, invalid dates, missing attendees, invalid status, or recurrence rule issues. - `RadoloException("The following resource is currently in use during the selected day/time: {name}.")` for resource scheduling conflicts. - `RadoloException("There was a problem saving this Calendar Event : {LastError}")` on entity save failure. - `RadoloException("We could not save this Process Flow Run: {LastError}")` if the fallback retry record fails to save. - External calendar provider exceptions are caught and logged; a Process Flow Run is created instead of failing the save. - The SecureApi catch block attaches `EventID` to exception data before publishing. ## Usage Notes - The SecureApi always passes `UpdateLinkedFields=true`, meaning linked claim fields are always updated when the event start date changes. - Resource conflict detection uses a SQL query that checks for time overlap, accounting for both direct events and recurring event instances (via RecurrenceID). - The organizer selection logic is complex: it tries the parent's organizer (for recurring instances), then the current user, then iterates attendees. Each candidate is validated for calendar connectivity before being accepted. - External calendar save failures are gracefully handled -- the internal event is always saved, and a Process Flow Run is created for deferred retry. This ensures the event is never lost even if Graph/Google is down. - The `_ADD_` prefixed values set on the entity (e.g., `_ADD_IsNewEvent`, `_ADD_LastUpdatedByPersonID`) are metadata flags consumed by the entity's save logic or downstream process components, not stored as database fields. - The transaction always commits even if the external calendar save fails (the commit is in a `finally` block).