Skip to main content

webscheduler-architecture

WebScheduler CI4 architecture, foundation, design order, global contracts, canonical service catalog, key system files, and the business-context resolver. Use whenever you're designing or modifying service/controller/model boundaries, deciding where new logic belongs, looking up the canonical service for a domain, or resolving the active business ID. Triggers on phrases like "where should this live", "which service", "service boundary", "design", "architecture", "is there already a service for", "current_business_id", "business context", or any new feature scaffolding in the WebScheduler / Frontend Dev project.

ソース情報

リポジトリ
niloc95/xscheduler_ci4
ソースの最終更新活動
2026年7月24日 12:05
検出された SKILL.md の言語
英語
スター
1
フォーク
0

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
webscheduler-architecture
description
WebScheduler CI4 architecture, foundation, design order, global contracts, canonical service catalog, key system files, and the business-context resolver. Use whenever you're designing or modifying service/controller/model boundaries, deciding where new logic belongs, looking up the canonical service for a domain, or resolving the active business ID. Triggers on phrases like "where should this live", "which service", "service boundary", "design", "architecture", "is there already a service for", "current_business_id", "business context", or any new feature scaffolding in the WebScheduler / Frontend Dev project.
# WebScheduler — Architecture & Service Catalog ## 1. What You Are Building WebScheduler CI4 is a professional appointment scheduling system built on CodeIgniter 4. **Architecture style:** service-oriented CI4 monolith with server-rendered views, REST-style APIs, Vite-managed frontend assets, and custom scheduler/public-booking modules. ## 2. Confirmed Stack - **Backend:** PHP 8.1+, CodeIgniter 4 - **Frontend:** Vite 6, Tailwind CSS 3.4, SCSS, plain JavaScript modules, Chart.js 4, Luxon 3 - **Data:** MySQL/MariaDB runtime only - **Auth:** session-based - **Authorization:** route filters plus service-level checks ## 3. No Framework Drift - Do not introduce React/Vue/Alpine without explicit approval. - Do not introduce TypeScript without explicit scoped approval. ## 4. Final Design Order (Mandatory) Always design and implement in this order: 1. Data and storage contract 2. Service logic 3. API contract 4. View/UI rendering If a UI change starts before service/data contract clarity, stop and fix boundaries first. ## 5. Global Contracts ### 5.1 Services Own Business Logic Scheduling, booking, availability, timezone conversion, notification dispatch, and query scoping belong in services. ### 5.2 Controllers Stay Thin Controllers parse requests, call services, and shape responses. Controllers are **not** alternate business layers. ### 5.3 Models Stay Data-Focused Models encapsulate data access and query composition. Keep model behavior compatible with existing schema-safe patterns. ### 5.4 Views Render, They Do Not Decide Views must not perform business logic, authorization, or timezone calculations. ### 5.5 API Envelope See the `api-contract` skill for full envelope rules. Success: `{ "data": {}, "meta": {} }` Error: `{ "error": { "message": "", "code": "", "details": {} } }` ## 6. Canonical Service Catalog ### Core Scheduling - `AvailabilityService` — slot generation and business-hours constraint - `AppointmentBookingService` — booking pipeline (validate → persist → notify) - `ConflictService` — appointment conflict detection - `ScheduleValidationService` — provider schedule validation - `BusinessHoursService` — global business hours from `xs_settings` ### Appointment Namespace (`App\Services\Appointment\`) - `AppointmentQueryService` — appointment reads and list queries - `AppointmentStatus` — status enum and notification event mapping - `AppointmentMutationService` — appointment writes (status changes etc.) - `AppointmentAvailabilityService` — availability checks per appointment - `AppointmentFormSubmissionService` / `AppointmentFormMutationService` — form create/edit pipeline - `AppointmentFormContextService` / `AppointmentFormGuardService` / `AppointmentFormResponseService` — form guards and response shaping - `AppointmentCustomFieldService` — custom field validation and storage - `AppointmentManualNotificationService` — manual notification resend - `AppointmentFormatterService` — appointment data formatting for views - `AppointmentDateTimeNormalizer` — datetime normalization helpers ### Calendar Views (`App\Services\Calendar\`) - `DayViewService` / `WeekViewService` / `MonthViewService` — server-side view model builders - `CalendarConfigService` — calendar display configuration - `TimeGridService` — time-grid computation shared by day/week views - `ProviderWorkingHoursTrait` — working hours shared logic (has known debt — see `scheduling` skill) ### Users / Customers - `UserManagementMutationService` — user create/edit/delete mutations - `UserManagementContextService` — user management page context - `UserDeletionService` — safe user removal with cascade checks - `CustomerAppointmentService` — provider/staff customer scoping - `CustomerService` — customer CRUD; `insertCustomer()` — admin create (audited via `AuditLogModel`); `updateCustomerById()` — admin update (audited); `upsertCustomer()` — find-or-create for the public booking flow only - `CustomerDeletionService` — safe customer removal - `CustomerCustomFieldService` — custom field validation - `ProfilePageService` — `/profile` page view-data builder only (cards, activity feed, stats). Does **not** own mutations; profile saves and audit logging via `logProfileActivity()` live in `Profile.php` controller - `AuthorizationService` — RBAC helpers ### Services / Categories - `ServiceMutationService` — service and category create/update/delete; provider assignment with `transStart/transComplete` transaction wrapping; audit logging via `AuditLogModel`. Used by `Services`, `ServiceCategories`, and `Api/V1/Services` controllers ### Public Booking - `PublicBookingService` — public booking pipeline - `BookingLinkService` — canonical URL builder for booking links and `/r/{reference}` short links - `BookingSettingsService` — booking form configuration, custom field validation rules - `BookingMetricsService` — booking statistics ### Dashboard - `DashboardService` — dashboard data aggregation - `DashboardPageService` — dashboard page context builder - `DashboardApiService` — dashboard API endpoint data - `AppointmentDashboardContextService` — appointment dashboard context ### Notifications - `AppointmentEventService` — appointment event publishing - `AppointmentNotificationService` — appointment notification coordination - `NotificationQueueService` — queue write and enqueue logic - `NotificationQueueDispatcher` — queue processing and dispatch - `NotificationPolicyService` — delivery policy rules - `NotificationEmailService` — email delivery via `MailerService` - `NotificationSmsService` — SMS delivery (Clickatell/Twilio) - `NotificationWhatsAppService` — WhatsApp delivery (Meta Cloud API) - `NotificationTemplateService` — template loading and placeholder rendering - `NotificationDeliveryLogService` — delivery log queries - `NotificationOptOutService` — opt-out management - `NotificationReminderHeartbeatService` — reminder offset scheduling heartbeat - `NotificationAutomationStatusService` — automation on/off status - `NotificationBusinessOptionsService` — business-level notification options - `NotificationCenterService` — notification center UI data - `NotificationCatalog` — constants (event types, defaults) ### Settings (`App\Services\Settings\`) - `SettingsPageService` — settings page context - `SettingsApiService` — settings API endpoints - `GeneralSettingsService` — general/localization settings mutations - `NotificationSettingsService` — notification settings mutations - `IntegrationSettingsService` — integrations hub mutations (routes by intent + channel to individual integration services) - `LocalizationSettingsService` — timezone/locale reads ### Integrations (`App\Services\`) Six services follow the `HandlesNotificationIntegrations` trait pattern — each owns one channel in `xs_business_integrations`: - `WebhookIntegrationService` — HMAC-signed webhook endpoints; fires `appointment.*` events (channel: `webhook`) - `GoogleCalendarIntegrationService` — OAuth 2.0 two-way calendar sync; credentials stored encrypted in DB, not .env (channel: `google_calendar`) - `StripeIntegrationService` — deposit / no-show fee collection; secret key never returned in public data (channel: `stripe`) - `ZoomIntegrationService` — Server-to-Server OAuth; cached access tokens; `createMeeting()` returns join URL (channel: `zoom`) - `JitsiIntegrationService` — public or self-hosted Jitsi Meet; optional JaaS credentials; `generateMeetingLink()` uses appointment hash for room name (channel: `jitsi`) - `PayFastIntegrationService` — South African gateway; sandbox/live toggle; `buildPaymentUrl()` generates signed redirect (channel: `payfast`) **New controllers:** - `App\Controllers\OAuthCallback` — `GET /oauth/google/authorize` and `GET /oauth/google/callback` - `App\Controllers\Api\V1\Integrations` — `GET/POST /api/v1/integrations/{index|save|test|disconnect}` ### Infrastructure - `MailerService` — sole email transport layer (see `notifications` skill) - `TimezoneService` — UTC/local conversion (see `database` skill) - `GlobalSearchService` — cross-entity search - `PhoneNumberService` — phone number formatting/normalization - `BookingMetricsService` — booking statistics - `FormResponseTrait` (`App\Traits\FormResponseTrait`) — standardised JSON envelope for form-surface controllers; `formSuccess(redirect, message)` → HTTP 200; `formError(message, errors, status)` → HTTP 422 by default. Applied to: `Services`, `ServiceCategories`, `CustomerManagement` - `MutationResult` (`App\Services\MutationResult`) — readonly DTO for mutation service return values; named constructors `::ok(message, redirect, entityId)` and `::fail(message, errors, statusCode)`; `toArray()` for array interop. All **new** mutation services must return this type ## 7. Key System Files - `app/Config/Routes.php` - `app/Config/Filters.php` - `app/Controllers/Api/BaseApiController.php` - `app/Database/MigrationBase.php` - `app/Models/AppointmentModel.php` - `app/Models/UserModel.php` - `app/Models/SettingModel.php` - `app/Models/AppointmentCustomFieldModel.php` - `app/Services/MailerService.php` - `app/Services/AvailabilityService.php` - `app/Services/BusinessHoursService.php` - `app/Services/BookingLinkService.php` - `app/Services/Calendar/DayViewService.php` - `app/Services/Calendar/WeekViewService.php` - `app/Services/Calendar/ProviderWorkingHoursTrait.php` - `resources/js/spa.js` - `resources/js/app.js` - `resources/js/theme-bootstrap.js` - `resources/js/dark-mode.js` - `resources/js/layout/app-layout-init.js` - `resources/js/public-booking.js` - `resources/js/public-booking-bootstrap.js` - `resources/js/modules/scheduler/scheduler-core.js` - `resources/js/modules/scheduler/time-grid-utils.js` - `vite.config.js` ## 8. Business Context Resolver Contract (Owner Section) All business-ID resolution rules live here. The notifications skill references back to this section. ### 8.1 `current_business_id()` Helper `app/Helpers/permissions_helper.php` exposes `current_business_id(?int $default = null): int`. It is the **single canonical way** to resolve the active business context for the current request. Load it with `helper('permissions')` (already loaded by `BaseController`). ### 8.2 Resolution Priority (Strict) 1. **Request params** — `GET`/`POST` `business_id` or `businessId` 2. **API token context** — `api_identity()?->businessId()`, i.e. `xs_api_keys.business_id` for a Bearer-token request 3. **Session keys** — `session()->get('business_id')` or `session()->get('active_business_id')` 4. **Session user sub-keys** — `session()->get('user')['business_id' | 'active_business_id' | 'businessId' | 'activeBusinessId']` 5. **Fallback** — `$default ?? NotificationCatalog::BUSINESS_ID_DEFAULT` (value `1`) The result is always `max(1, resolved_value)`. `api_identity()` (also in `permissions_helper.php`) returns the request-scoped `App\Services\ApiIdentity`, or `null` outside a request context. There is no businesses table yet, so `xs_api_keys.business_id` is forward-compatibility only and always resolves to `1` today. ### 8.2a Acting-Identity Resolution The same helper file owns identity resolution, which follows the same "token first, session second" rule: | Helper | Returns | | --- | --- | | `current_identity_user()` | Token-bound user, else `session()->get('user')` | | `current_user_id()` | Token-bound user id, else `session()->get('user_id')` | | `current_user_role()` / `has_role()` | Derived from `current_identity_user()` | | `resolve_active_role($roles, $fallback)` | Highest-privilege role (`admin > provider > staff`) — shared by `Auth::login()` and `ApiIdentity` so the two identity shapes cannot drift | **Never read `session()->get('user_id')` / `session()->get('user')` directly in code reachable from an API route.** See `api-contract` §Authentication for the full rationale. ### 8.3 Service-Level Usage Pattern Services that scope data to a business must use a protected delegate so the resolver is overridable in unit tests: ```php protected function resolveBusinessId(): int { helper('permissions'); return current_business_id(); } ``` ### 8.4 Current Service Coverage | Service | Scope switch | | --- | --- | | `NotificationCenterService` | `getNotifications()`, `getUnreadCount()` | | `NotificationSettingsService` | `getIndexData()`, `save()` | Do not use `NotificationCatalog::BUSINESS_ID_DEFAULT` directly in service methods that serve UI requests. Route all UI-facing scoping through `resolveBusinessId()` / `current_business_id()`. ### 8.5 Auth Channel Exception `Auth::sendResetEmail()` still passes `NotificationCatalog::BUSINESS_ID_DEFAULT` directly to `MailerService`. This is intentional — password reset has no session context. Do not route it through `current_business_id()`. ## 8A. Currency & Localization Settings Contract (Owner Section) **Scope:** this section owns **currency** and the **localization settings resolution model**. Timezone *conversion and storage integrity* — UTC rules, `TimezoneService` method contract, `stored_timezone` semantics, notification/JS pipeline — is owned by the `database` skill §5. Do not restate those rules here; link to them. Currency and timezone are **business-wide singletons** stored in `xs_settings`. There is no per-provider, per-customer, or per-gateway override. A relocation or rebrand is a single settings change plus (for timezone) a data migration. | Concern | Canonical key | Canonical reader | Full contract | | --- | --- | --- | --- | | Currency | `localization.currency` | `LocalizationSettingsService::getCurrency()` / `getCurrencySymbol()` / `formatCurrency()` | this section | | Timezone | `localization.timezone` | `LocalizationSettingsService::getTimezone()`, `TimezoneService::businessTimezone()` | `database` skill §5 | ### 8A.1 View-Layer Entry Points `format_currency()`, `get_app_currency()`, and `get_app_currency_symbol()` in `app/Helpers/currency_helper.php` are the **only** approved ways to render money in a view. Never hand-roll `number_format()` with a literal symbol. For JS, pass the symbol down as a `data-currency-symbol` attribute and format via `resources/js/currency.js`. Do not re-derive the symbol client-side. ### 8A.2 Settings Key Casing All `xs_settings` keys are **lowercase.dotted**. `SettingModel::getByKeys()` matches keys exactly and returns `null` for a miss, so a mis-cased key silently falls through to the caller's default forever — it does not error. `setting('Localization.currency')` pinned the entire app to ZAR for the lifetime of that bug. Grep check #8 in the `rules` skill guards this. ### 8A.3 `stored_timezone` Reference only — full contract in `database` skill §5.3. In short: the column records the zone the appointment *belongs to*, which is not the zone its input arrived in, and `createAppointment()` takes the two as separate parameters. ### 8A.4 Payment Gateway Currency Gateways do **not** own a currency. `PaymentService` charges in `localization.currency` for both Stripe and PayFast. The legacy `currency` key inside the encrypted Stripe config is retained for round-trip compatibility only and is never read. PayFast settles in ZAR exclusively and is gated off in `servicePaymentMeta()` and `initiatePayFast()` when the business bills in anything else. ### 8A.5 Known Limitations - `format_currency()` always prefixes the symbol and uses `.`/`,` separators. Correct for USD/GBP/ZAR; wrong grouping for EUR/CHF and wrong precision for zero-decimal JPY. `ext-intl` is not a dependency.
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る