Users live in Outlook/Microsoft 365, Google Calendar, and Apple iCloud. All three export iCalendar (.ics / RFC 5545), and we already ship Ical.Net (5.2.3) driving RecurrenceExpansionService. This ticket adds the first read path: parse an uploaded .ics and import its events for the current user.
Chosen approach (see #781 discussion): universal .ics first — no per-provider OAuth, no verification lead time; covers all three ecosystems (Apple has no REST calendar API at all, so .ics/CalDAV is the only path there anyway).
Import service (CalendarImportService): upsert by (CreatedBy, ExternalUid) — re-import updates, never duplicates; never touches another user's rows or user-authored events.
RECURRENCE-ID per-instance overrides (a single moved/edited occurrence) are skipped and counted — the internal model stores a series as one document with EXDATE for deletions but has no per-instance override slot. Follow-up if needed.
Ships together with the subscription-feed ticket in one WIP PR.
Part of the unified scheduling epic #781.
Users live in Outlook/Microsoft 365, Google Calendar, and Apple iCloud. All three export **iCalendar (.ics / RFC 5545)**, and we already ship Ical.Net (5.2.3) driving `RecurrenceExpansionService`. This ticket adds the first **read** path: parse an uploaded `.ics` and import its events for the current user.
Chosen approach (see #781 discussion): universal `.ics` first — no per-provider OAuth, no verification lead time; covers all three ecosystems (Apple has no REST calendar API at all, so `.ics`/CalDAV is the only path there anyway).
## Scope
- **Model:** add `ExternalUid` (VEVENT `UID`) + `ImportSource` fields to `CalendarEvent` (nullable, backward-compatible). Idempotency key = `(CreatedBy, ExternalUid)`.
- **Parser** (`SpikerSoft.Business/Domain/Calendar/Import/IcsImportParser`, pure): VEVENT → `CalendarEvent`. `SUMMARY→Title`, `DTSTART/DTEND→UTC`, `RRULE→RecurrenceRule` (verbatim string), `EXDATE→ExceptionDates`, `UID→ExternalUid`, all-day handling, TZID→UTC (incl. Outlook Windows-zone-ID fallback), dedup by UID.
- **Import service** (`CalendarImportService`): upsert by `(CreatedBy, ExternalUid)` — re-import updates, never duplicates; never touches another user's rows or user-authored events.
- **API:** `POST api/calendar/import` (`IFormFile`, size-limited) via `ImportCalendarCommand` (MediatR + `[CacheInvalidation]`); returns `{created, updated, skipped, total}`.
## Known lossiness (documented, not a bug)
- `RECURRENCE-ID` per-instance overrides (a single moved/edited occurrence) are **skipped and counted** — the internal model stores a series as one document with EXDATE for deletions but has no per-instance override slot. Follow-up if needed.
## Tests (tight)
Parser matrix (single event, TZID→UTC, all-day, RRULE preserved, EXDATE, RECURRENCE-ID skip, duplicate-UID dedup, malformed→reject, missing SUMMARY fallback, Windows TZID); import-service upsert (insert / update-same-UID / cross-user isolation / idempotent re-import); command-handler + endpoint.
**Ships together with the subscription-feed ticket in one WIP PR.**
Implemented in spikersoft-backend PR #456 (WIP, ships with #803).
Parser + upsert done and tested against real provider-shaped .ics fixtures (Google IANA VTIMEZONE, Outlook Windows zone id, Apple all-day + folding); the recurring case is asserted by expanding through the real RecurrenceExpansionService so RRULE round-trip + EXDATE + tz are validated as one outcome. CalendarEvent gained ExternalUid + ImportSource; endpoint is POST api/calendar/import.
One extra pre-existing limitation surfaced during review, recorded here so it's not a surprise: DST drift — the recurrence engine expands purely in UTC (ToCalDateTime forces UTC), so a weekly event imported from a DST-observing zone drifts an hour across a DST boundary. Not introduced by import; noted as a known constraint alongside the RECURRENCE-ID skip.
Implemented in spikersoft-backend **PR #456** (WIP, ships with #803).
Parser + upsert done and tested against real provider-shaped `.ics` fixtures (Google IANA VTIMEZONE, Outlook Windows zone id, Apple all-day + folding); the recurring case is asserted by expanding through the **real `RecurrenceExpansionService`** so RRULE round-trip + EXDATE + tz are validated as one outcome. `CalendarEvent` gained `ExternalUid` + `ImportSource`; endpoint is `POST api/calendar/import`.
One extra pre-existing limitation surfaced during review, recorded here so it's not a surprise: **DST drift** — the recurrence engine expands purely in UTC (`ToCalDateTime` forces UTC), so a weekly event imported from a DST-observing zone drifts an hour across a DST boundary. Not introduced by import; noted as a known constraint alongside the `RECURRENCE-ID` skip.
Resolved in spikersoft-backend PR #456 (merged to master, ee541921). One-shot .ics upload shipped: POST api/calendar/import → ImportCalendarCommand → shared IcsImportParser + CalendarImportService (upsert by (CreatedBy, ExternalUid)). CalendarEvent gained ExternalUid + ImportSource. Covers Outlook/Google/Apple exports; tested against real provider-shaped fixtures with the recurring case validated through the real RecurrenceExpansionService. Documented out-of-scope follow-ups (RECURRENCE-ID overrides, pre-existing UTC/DST drift) recorded above. Closing.
Resolved in spikersoft-backend PR #456 (merged to `master`, `ee541921`). One-shot `.ics` upload shipped: `POST api/calendar/import` → `ImportCalendarCommand` → shared `IcsImportParser` + `CalendarImportService` (upsert by `(CreatedBy, ExternalUid)`). `CalendarEvent` gained `ExternalUid` + `ImportSource`. Covers Outlook/Google/Apple exports; tested against real provider-shaped fixtures with the recurring case validated through the real `RecurrenceExpansionService`. Documented out-of-scope follow-ups (RECURRENCE-ID overrides, pre-existing UTC/DST drift) recorded above. Closing.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Part of the unified scheduling epic #781.
Users live in Outlook/Microsoft 365, Google Calendar, and Apple iCloud. All three export iCalendar (.ics / RFC 5545), and we already ship Ical.Net (5.2.3) driving
RecurrenceExpansionService. This ticket adds the first read path: parse an uploaded.icsand import its events for the current user.Chosen approach (see #781 discussion): universal
.icsfirst — no per-provider OAuth, no verification lead time; covers all three ecosystems (Apple has no REST calendar API at all, so.ics/CalDAV is the only path there anyway).Scope
ExternalUid(VEVENTUID) +ImportSourcefields toCalendarEvent(nullable, backward-compatible). Idempotency key =(CreatedBy, ExternalUid).SpikerSoft.Business/Domain/Calendar/Import/IcsImportParser, pure): VEVENT →CalendarEvent.SUMMARY→Title,DTSTART/DTEND→UTC,RRULE→RecurrenceRule(verbatim string),EXDATE→ExceptionDates,UID→ExternalUid, all-day handling, TZID→UTC (incl. Outlook Windows-zone-ID fallback), dedup by UID.CalendarImportService): upsert by(CreatedBy, ExternalUid)— re-import updates, never duplicates; never touches another user's rows or user-authored events.POST api/calendar/import(IFormFile, size-limited) viaImportCalendarCommand(MediatR +[CacheInvalidation]); returns{created, updated, skipped, total}.Known lossiness (documented, not a bug)
RECURRENCE-IDper-instance overrides (a single moved/edited occurrence) are skipped and counted — the internal model stores a series as one document with EXDATE for deletions but has no per-instance override slot. Follow-up if needed.Tests (tight)
Parser matrix (single event, TZID→UTC, all-day, RRULE preserved, EXDATE, RECURRENCE-ID skip, duplicate-UID dedup, malformed→reject, missing SUMMARY fallback, Windows TZID); import-service upsert (insert / update-same-UID / cross-user isolation / idempotent re-import); command-handler + endpoint.
Ships together with the subscription-feed ticket in one WIP PR.
Implemented in spikersoft-backend PR #456 (WIP, ships with #803).
Parser + upsert done and tested against real provider-shaped
.icsfixtures (Google IANA VTIMEZONE, Outlook Windows zone id, Apple all-day + folding); the recurring case is asserted by expanding through the realRecurrenceExpansionServiceso RRULE round-trip + EXDATE + tz are validated as one outcome.CalendarEventgainedExternalUid+ImportSource; endpoint isPOST api/calendar/import.One extra pre-existing limitation surfaced during review, recorded here so it's not a surprise: DST drift — the recurrence engine expands purely in UTC (
ToCalDateTimeforces UTC), so a weekly event imported from a DST-observing zone drifts an hour across a DST boundary. Not introduced by import; noted as a known constraint alongside theRECURRENCE-IDskip.Resolved in spikersoft-backend PR #456 (merged to
master,ee541921). One-shot.icsupload shipped:POST api/calendar/import→ImportCalendarCommand→ sharedIcsImportParser+CalendarImportService(upsert by(CreatedBy, ExternalUid)).CalendarEventgainedExternalUid+ImportSource. Covers Outlook/Google/Apple exports; tested against real provider-shaped fixtures with the recurring case validated through the realRecurrenceExpansionService. Documented out-of-scope follow-ups (RECURRENCE-ID overrides, pre-existing UTC/DST drift) recorded above. Closing.