Skip to content

Events — Improvement & Growth Plan

Package: capell-app/events · Kind: plugin · Tier: premium · Product group: Capell Content · Bundle: content-product · Status: Draft

Events adds event records, reusable venues, recurring-occurrence expansion (rlanvin/php-rrule), native RSVP/registration with capacity + waitlist, public .ics calendar feeds (spatie/icalendar-generator), schema.org Event JSON-LD render hooks, an admin calendar page/widget, and frontend Livewire listing + calendar pages. Surfaces: admin, frontend, console. Core Actions: ExpandEventRecurrenceAction / SyncEventOccurrencesAction (occurrence materialization), RegisterForEventOccurrenceAction (locked RSVP), BuildCalendarFeedAction, BuildEventSchemaAction, ProcessDueEventNotificationLogsAction (scheduled reminders). Tables: event_venues, events, event_occurrences, event_registrations, event_notification_logs. Requires admin, frontend, navigation, publishing-studio; supports address, form-builder, seo-suite, tags, and integrates with customer-portal (registration self-service feed) and site-discovery (public URL contributor).

Current marketplace summary (verbatim): “Publish recurring events with venues, capacity-managed RSVPs, subscribable iCal feeds, and Google-ready Event schema — all inside your Capell admin.” Manifest declares the extension card plus 10 committed runner-backed PNG captures covering event CRUD, venues, occurrences, registrations, admin calendar/widget, and public listing/calendar. The .ics feed capture remains committed runner evidence for the protocol route, but it is no longer promoted as buyer-facing marketplace media.

  • 2026-06-03: Replaced the stubbed EventsHealthCheck with real diagnostics and refreshed marketplace copy.
  • 2026-06-04: Wired confirm/cancel row actions into EventRegistrationResource, so staff can change registration status from the shipped admin surface and cancellations trigger the existing waitlist-promotion workflow. Declared the event resource permissions in capell.json.
  • 2026-06-04: Queued registration notifications after commit, added a package doctor command, wired cancellation-driven waitlist promotion through an event listener, and scheduled stale waitlist reconcile.
  • 2026-06-06: Captured and committed all 11 Events screenshot-runner targets, promoted the 10 styled admin/frontend captures into capell.json marketplace media, and retained the .ics feed output as runner evidence in docs/screenshots.json.
  1. Move RSVP confirmation mail out of the DB transaction — shipped: RegisterForEventOccurrenceAction now defers registration notification scheduling until after commit, so confirmation/reminder log creation and notification dispatch do not run while the occurrence row is locked. — src/Actions/RegisterForEventOccurrenceAction.php, src/Actions/ScheduleEventNotificationsAction.php — M
  2. Make EventRegistrationNotification implement ShouldQueue — shipped: EventRegistrationNotification implements ShouldQueueAfterCommit, so confirmation, reminder, and waitlist-promotion notifications queue after commit. — src/Notifications/EventRegistrationNotification.php — S
  3. Done/Shipped: Bound recurrence generation explicitly — recurrence materialization now reads explicit capell-events.recurrence.sync_past_days and sync_horizon_days settings, preserving the existing 31-day/365-day default window while making the cap visible/configurable. DST-crossing weekly recurrence coverage asserts local wall-clock time stability across the Europe/London spring transition. — config/capell-events.php, src/Actions/SyncEventOccurrencesAction.php, tests/Unit/Actions/ExpandEventRecurrenceActionTest.php — M
  4. Done/Shipped: Pass hydrated view data into public Blade instead of Eloquent models — public listing and calendar components now map occurrences to EventOccurrenceViewData before rendering; Blade templates only read pre-hydrated strings/dates/URLs/venue names, and focused render coverage asserts anonymous HTML avoids model/admin identifiers. — src/Actions/BuildEventOccurrenceViewDataAction.php, src/Data/EventOccurrenceViewData.php, resources/views/livewire/page/events-listing.blade.php, resources/views/livewire/event-calendar.blade.php, tests/Feature/PublicEventViewDataTest.php — M
  5. Done/Shipped: Stop sharing the admin-labelled calendar partial with the public page — the public calendar now uses capell-events::generic.event_calendar instead of the admin-oriented label, with render coverage proving the public view does not emit the admin label. — resources/views/livewire/event-calendar.blade.php, resources/lang/en/generic.php, tests/Feature/PublicEventViewDataTest.php — S
  6. Denormalized registration_count is recomputed by aggregate query anyway — why: PromoteWaitlistAction and refreshRegistrationCount call confirmedRegistrationQuantity() (a SUM(quantity) query) then write it back to registration_count. The column exists to avoid that query on read, but remainingCapacity() recomputes the SUM live rather than reading the column — so the denormalization buys nothing on the hot path. Either trust the column on read or drop it. — src/Models/EventOccurrence.php — S
  7. occurrenceUrl() builds the public URL by string concatenation (rtrim($pageUrl,'/').'/'.date) — why: bypasses the URL registry/route layer; brittle if listing-page URL structure changes and not locale-aware for the date segment. Resolve through the page-URL contract used elsewhere. — src/Models/EventOccurrence.php — M
  8. Done/Shipped: Add a per-listing-page feed scope — listing-specific .ics routes now resolve the listing page and pass it into BuildCalendarFeedAction; page meta.event_venue_id / venue_id and event_id / event_ids narrow the feed so scoped routes no longer emit the whole site’s occurrences. — src/Http/Controllers/CalendarFeedController.php, src/Actions/BuildCalendarFeedAction.php, tests/Integration/Actions/BuildCalendarFeedScopeTest.php — M

Declared capabilities[]: events, events-admin, events-console, events-frontend, events-registration-created-event, events-customer-portal-registration-feed. All six are genuinely reachable (registration event is dispatched and consumed by contacts; portal feed provider is registered). Gaps against events-category norms:

  • Customer-portal self-service registration cancellation (table-stakes) — staff can now confirm/cancel registrations from the admin resource and cancellation reaches PromoteWaitlistAction through a package event listener, but the customer-portal feed still lists registrations without a buyer cancel action.
  • Timezone-aware display controls shipped. Public listing/calendar view data now includes event timezone and optional viewer-timezone display, with ?timezone=..., page viewer_timezone/default_timezone metadata, and capell-events.display.default_timezone as the configured fallback. The calendar render contract now passes hydrated occurrence DTOs directly to Blade. — src/Actions/BuildEventOccurrenceViewDataAction.php, src/Livewire/Page/EventsListingPage.php, src/Livewire/EventCalendar.php, config/capell-events.php
  • iCal per-attendee / VALARM reminders (differentiator) — feed emits VEVENTs but no VALARM reminders and no personalized ?token feed per attendee. Reminder emails exist server-side but aren’t reflected in the calendar subscription.
  • Capacity/waitlist UI + automatic promotion (table-stakes) — capacity & waitlist logic is correct in RegisterForEventOccurrenceAction/PromoteWaitlistAction/EventOccurrence; cancellation now dispatches EventRegistrationCancelled and a scheduled reconcile promotes stale waitlisted registrations. Remaining gap: richer public/admin waitlist UI.
  • Ticketing / paid registration (differentiator)booking_mode/booking_url support external booking links only; no integration with capell-app/payments for paid tickets despite payments being in customer-portal’s support graph.
  • Shipped 2026-06-07: recurring-event exception editing is reachable in the occurrence UI. EventOccurrenceResource now exposes cancel and reschedule row actions backed by CancelOccurrenceAction and RescheduleOccurrenceAction, with translated notifications and Livewire table-action coverage. — src/Filament/Resources/Occurrences/EventOccurrenceResource.php, tests/Integration/EventRegistrationResourceWorkflowTest.php
  • Reminder cadence configuration shipped. ScheduleEventNotificationsAction now reads configurable reminder offsets from event notification_settings or capell-events.notifications.reminder_offsets_minutes, supports multiple reminder rows per registration via notification_key, and honors per-event reminder opt-out. — src/Actions/ScheduleEventNotificationsAction.php, database/migrations/2026_06_07_000000_07_add_notification_keys_to_event_notification_logs_table.php
  • Admin registration management shipped; portal cancellation remains. EventRegistrationResource now exposes confirm/cancel row actions backed by UpdateRegistrationStatusAction; cancellation dispatches EventRegistrationCancelled, the registered listener triggers PromoteWaitlistAction, and a scheduled reconcile promotes stale waitlisted registrations. Remaining gap: customer-portal self-service cancellation. — src/Actions/UpdateRegistrationStatusAction.php, src/Events/EventRegistrationCancelled.php, src/Listeners/PromoteWaitlistAfterRegistrationCancelled.php, src/Actions/ReconcileEventWaitlistsAction.php
  • Health checks and package doctor command shipped. EventsHealthCheck now runs recurrence, registration-capacity, calendar-feed, and Event schema diagnostics with package tests, and capell:events-doctor is registered in the package manifest. — src/Health/EventsHealthCheck.php, src/Console/Commands/EventsDoctorCommand.php, capell.json
  • Manifest-vs-reality mismatches narrowed. Four Shield-backed policy subjects are now declared in permissions[], and the marketplace screenshot gallery now lists the extension card plus the styled admin/frontend runner PNG captures from docs/screenshots.json; the .ics feed capture remains technical runner evidence. — capell.json, src/Providers/EventsServiceProvider.php, docs/screenshots.json
  • Mail dispatch moved out of the locked registration transaction. EventRegistrationNotification queues after commit and RegisterForEventOccurrenceAction schedules confirmation/reminder notifications only after the registration transaction commits. — src/Actions/RegisterForEventOccurrenceAction.php, src/Notifications/EventRegistrationNotification.php
  • Timezone/DST correctness unproven. ExpandEventRecurrenceAction builds new RRULE($rule, $event->starts_at->toDateTimeImmutable()) then CarbonImmutable::instance($occurrenceStart)->setTimezone($event->timezone). RRULE expands from the absolute start instant; wall-clock-anchored recurrences (“every Mon 09:00”) can drift by an hour across DST boundaries. A test asserts “expands practical RRULE occurrences inside a range” but no test crosses a DST transition. — src/Actions/ExpandEventRecurrenceAction.php
  • Public-output safety: models in Blade. As §2.4 — anonymous templates receive Eloquent models; safe today only because the upstream query eager-loads. No regression test proves the listing/calendar Blade never lazy-loads or emits admin internals. Capell convention requires anonymous-safety tests for rendering changes. — resources/views/livewire/page/, resources/views/livewire/event-calendar.blade.php
  • cacheable: false on every public surface. Manifest performance.cacheSafety.cacheable: false. Listing, calendar, and .ics feed run live DB queries on every anonymous hit (QueryPublicEventOccurrencesAction with 4 eager loads). Feed controller reportedly sets freshness/ETag headers (test: “serves calendar feeds with freshness headers and conditional etag support”) — good — but the HTML surfaces have no caching story and no declared invalidationSources. — capell.json, src/Livewire/Page/EventsListingPage.php
  • Test gaps. Strong coverage on capacity/waitlist creation, feed, schema, recurrence range, editorial-calendar, portal feed, navigation (47 named tests). Missing: registration cancellation/status transition, automatic waitlist promotion on cancel, health-check behavior, DST recurrence, public-Blade anonymous-leak assertions, per-listing-page feed filtering. — tests/
  • i18n. Lang files exist (enum/form/generic/notification/package/table/validation), but occurrenceUrl() date segment and some calendar formatting use server locale/timezone, not viewer locale. — src/Models/EventOccurrence.php

Critique. Manifest summary, package description, and composer description now use stronger buyer-facing copy. The UI media gap is closed for the static gallery: the marketplace manifest lists the extension card plus the 10 committed Capell runner PNG captures that prove admin and frontend workflows. The .ics feed PNG remains technical runner evidence rather than product media.

Improved summary (1 sentence):

Publish recurring events with venues, capacity-managed RSVPs, subscribable iCal feeds, and Google-ready Event schema — all inside your Capell admin.

Improved description (3–4 sentences):

Events turns Capell into a full event platform: editors create one event with an RRULE recurrence and the package materializes every occurrence, each with its own page, venue, schedule, and capacity. Visitors RSVP with automatic waitlisting and confirmation/reminder emails, while a public .ics feed lets them subscribe in Apple/Google/Outlook calendars. Every occurrence emits schema.org Event JSON-LD for rich results, and an admin calendar plus dashboard widget keep the programme visible. Built on php-rrule and spatie/icalendar-generator, with first-class hooks into Publishing Studio, Site Discovery, and the Customer Portal.

Screenshot/media gaps. Static UI screenshots are now reconciled: the promoted runner captures cover admin index/create/edit, venues, occurrences, registrations, admin calendar/widget, and public listing/calendar. The rendered .ics feed stays in docs/screenshots.json as route evidence. A short GIF of “create recurring event → occurrences appear → public calendar updates” would carry the value prop better than any static shot.

Positioning. Premium / content-product bundle is right. Cross-sell paths via existing deps: Address (venue geocoding/maps), Tags (event categories/filtered feeds), SEO Suite (event sitemaps + schema validation), Customer Portal (“my registrations” + future cancel), Site Discovery (event URLs in canonical registry), Payments (paid ticketing — net-new, see §3). Bundle as a “Programming & Events” extension suite with Address + Tags + Customer Portal.

Differentiators / value props / target buyer. Differentiators: RRULE recurrence with per-occurrence overrides, capacity+waitlist, native iCal subscription feeds, schema.org rich results — without leaving the CMS. Target buyer: marketing/comms teams at venues, conferences, education, nonprofits, and local-services sites already on Capell who run a recurring programme and want SEO + calendar reach without a separate ticketing SaaS.

Keywords/tags (8–12): events, event calendar, recurring events, RRULE, iCalendar, ICS feed, RSVP, waitlist, venues, event schema, JSON-LD, Filament.

ItemBucketEffortImpactSection ref
Wire UpdateRegistrationStatusAction into EventRegistrationResource (confirm/cancel actions)DoneMHigh§4, §3
Auto-trigger PromoteWaitlistAction on cancellation (listener) + scheduled reconcileDoneMHigh§4, §3
Add package-specific doctor command wiring for Events diagnosticsDoneSMed§4
Move RSVP mail out of locked transaction; queue EventRegistrationNotificationDoneMHigh§2.1, §2.2, §4
Capture + commit the 11 screenshots.json targets; sync manifest screenshotsDoneSMed§1, §5
Fix composer description; adopt improved summary/descriptionDoneSMed§5
Declare permissions[] (and any settings) in capell.json to match registered policiesDoneSMed§4
Done/Shipped: Pass typed view data to public Blade; add anonymous-leak rendering testDoneMMed§2.4, §4
Done/Shipped: Add DST-crossing recurrence test; bound recurrence horizon explicitlyDoneMMed§2.3, §4
Done/Shipped: Per-listing-page filtered .ics feed (honor {listingPage})DoneMMed§2.8, §3
Done/Shipped: Viewer-timezone display + per-site default-tz settingDoneMMed§3, §4
Done/Shipped: Configurable multi-reminder cadence + per-event opt-outDoneMLow§3
Shipped 2026-06-07: Verify/expose CancelOccurrenceAction/RescheduleOccurrenceAction in occurrence UIDoneSMed§3
Paid ticketing via capell-app/payments integrationLaterLHigh§3
Customer-portal self-service RSVP cancellationLaterMMed§3
Personalized per-attendee iCal feed + VALARM remindersLaterMLow§3
Resolve occurrenceUrl() through URL registry; drop or trust registration_countLaterMLow§2.6, §2.7