get( MigrationRunner::class ); $runner->migrate(); /** @var RolesRegistrar $roles Roles registrar service. */ $roles = $container->get( RolesRegistrar::class ); $roles->register(); /** @var SettingsStore $settings Settings store service. */ $settings = $container->get( SettingsStore::class ); $settings->seed_defaults(); /** @var NotificationTemplateSeeder $templates Template seeder service. */ $templates = $container->get( NotificationTemplateSeeder::class ); $templates->seed(); self::seed_automation_recipes(); self::schedule_heartbeat(); } /** * Apply pending schema and seed gallery recipes when this request is behind. * * Activation is the canonical path (docs/15 M1). Development sites often skip * a deactivate/reactivate cycle, which left M12a recipe_key columns and the * gallery rows missing. * * @since 0.1.0 */ private static function maybe_upgrade(): void { $container = self::container(); /** @var MigrationRunner $runner */ $runner = $container->get( MigrationRunner::class ); if ( $runner->needs_upgrade() ) { try { $runner->migrate(); } catch ( \Throwable ) { // Another request holds the lock, or the schema is not ready yet. return; } } self::seed_automation_recipes(); } /** * Insert missing gallery recipes (idempotent per recipe_key). * * @since 0.1.0 */ private static function seed_automation_recipes(): void { $mark = 'v2:' . implode( ',', ( new RecipeCatalogue() )->keys() ); $have = get_option( 'bookline_recipe_seed', '' ); if ( is_string( $have ) && $have === $mark ) { return; } try { /** @var AutomationRecipeSeeder $seeder */ $seeder = self::container()->get( AutomationRecipeSeeder::class ); $saved = $seeder->seed(); if ( count( ( new RecipeCatalogue() )->keys() ) === $saved ) { update_option( 'bookline_recipe_seed', $mark, false ); } } catch ( \Throwable ) { // Schema may still be mid-migration on this request. return; } } /** * Deactivation: cancel scheduled actions only. * * Action Scheduler is not bundled yet (Milestone 8). When `as_unschedule_all_actions` * is available we cancel the `bookline` group; otherwise this is a no-op stub. */ public static function deactivate(): void { if ( function_exists( 'as_unschedule_all_actions' ) ) { as_unschedule_all_actions( null, [], 'bookline' ); } } /** * Early bootstrap — keep this light: no DB, no HTTP, no file scans. */ public static function on_plugins_loaded(): void { self::register_core_services( self::container() ); self::boot_automation_registries( self::container() ); /** * Fires once core services are registered so add-ons can register. * * @param Registry $registry Typed registries (docs/12 §2). * @since 0.1.0 */ do_action( 'bookline/register', new Registry( self::container() ) ); self::apply_automation_extension_filters( self::container() ); } /** * WordPress init — later feature registration. */ public static function on_init(): void { self::maybe_upgrade(); add_action( 'bookline/holds/expire', static function (): void { /** @var ExpireHoldsJob $job */ $job = self::container()->get( ExpireHoldsJob::class ); $job->run(); } ); add_action( 'bookline/order_intents/cleanup', static function (): void { /** @var IntentCleanupJob $job */ $job = self::container()->get( IntentCleanupJob::class ); $job->run(); } ); add_action( PaymentReconciliationJob::HOOK, static function (): void { /** @var PaymentReconciliationJob $job */ $job = self::container()->get( PaymentReconciliationJob::class ); $job->run(); } ); /** @var Bus $bus */ $bus = self::container()->get( Bus::class ); /** @var BookingNotificationListener $notify_listener */ $notify_listener = self::container()->get( BookingNotificationListener::class ); $bus->subscribe( BookingCreated::class, static fn( $e ) => $notify_listener->handle( $e ) ); $bus->subscribe( BookingRescheduled::class, static fn( $e ) => $notify_listener->handle( $e ) ); $bus->subscribe( BookingCancelled::class, static fn( $e ) => $notify_listener->handle( $e ) ); $bus->subscribe( BookingStatusChanged::class, static fn( $e ) => $notify_listener->handle( $e ) ); /** @var AutomationEventSubscriber $automation_events */ $automation_events = self::container()->get( AutomationEventSubscriber::class ); $bus->subscribe( BookingCreated::class, static fn( $e ) => $automation_events->handle( $e ) ); $bus->subscribe( BookingRescheduled::class, static fn( $e ) => $automation_events->handle( $e ) ); $bus->subscribe( BookingCancelled::class, static fn( $e ) => $automation_events->handle( $e ) ); $bus->subscribe( BookingStatusChanged::class, static fn( $e ) => $automation_events->handle( $e ) ); $bus->subscribe( ReviewRequested::class, static fn( $e ) => $automation_events->handle( $e ) ); $bus->subscribe( IntentAbandoned::class, static fn( $e ) => $automation_events->handle( $e ) ); $bus->subscribe( IntentRecovered::class, static fn( $e ) => $automation_events->handle( $e ) ); $bus->subscribe( CustomerCreated::class, static fn( $e ) => $automation_events->handle( $e ) ); add_action( AutomationExecutor::STEP_HOOK, static function ( int $run_id ): void { /** @var AutomationExecutor $executor */ $executor = self::container()->get( AutomationExecutor::class ); /** @var \Bookline\Domain\Automation\AutomationRunRepository $runs */ $runs = self::container()->get( \Bookline\Domain\Automation\AutomationRunRepository::class ); /** @var AutomationRepository $autos */ $autos = self::container()->get( AutomationRepository::class ); $run = $runs->find_by_id( $run_id ); if ( null === $run ) { return; } $auto = $autos->find_by_id( $run->automation_id() ); if ( null === $auto ) { return; } $executor->process( $run_id, $auto ); }, 10, 1 ); add_action( RelativeTimeScheduler::HOOK, static function ( int $automation_id, int $subject_id, string $kind = 'booking' ): void { /** @var RelativeTimeScheduler $relative */ $relative = self::container()->get( RelativeTimeScheduler::class ); $relative->fire( $automation_id, $subject_id, $kind ); }, 10, 3 ); add_action( NotificationDispatcher::SEND_HOOK, static function ( int $log_id ): void { /** @var NotificationDispatcher $dispatcher */ $dispatcher = self::container()->get( NotificationDispatcher::class ); $dispatcher->process_log( $log_id ); }, 10, 1 ); add_action( HealthMonitor::HEARTBEAT_HOOK, static function (): void { /** @var HealthMonitor $health */ $health = self::container()->get( HealthMonitor::class ); $health->beat(); } ); add_action( 'admin_notices', static function (): void { if ( ! current_user_can( 'manage_options' ) ) { return; } /** @var HealthMonitor $health */ $health = self::container()->get( HealthMonitor::class ); if ( ! $health->is_stale() ) { return; } $snap = $health->snapshot(); $msg = is_string( $snap['fix_guidance'] ?? null ) ? $snap['fix_guidance'] : 'Bookline scheduler heartbeat is stale.'; printf( '

%s

', esc_html( $msg ) ); } ); /** @var HealthMonitor $health_boot */ $health_boot = self::container()->get( HealthMonitor::class ); $health_boot->ensure_scheduled(); /** @var PaymentReconciliationJob $payments_job */ $payments_job = self::container()->get( PaymentReconciliationJob::class ); $payments_job->ensure_scheduled(); /** @var PersonalDataRegistrar $privacy */ $privacy = self::container()->get( PersonalDataRegistrar::class ); $privacy->register(); /** * Fires after Bookline has finished its init work. * * @since 0.1.0 */ do_action( 'bookline/init' ); } /** * Register REST routes under bookline/v1. * * @since 0.1.0 */ public static function register_rest(): void { /** @var RouteRegistrar $registrar */ $registrar = self::container()->get( RouteRegistrar::class ); $registrar->register(); } /** * Register WP-CLI commands. * * @since 0.1.0 */ public static function register_cli(): void { if ( ! class_exists( 'WP_CLI' ) ) { return; } \WP_CLI::add_command( 'bookline migrate', MigrateCommand::class ); \WP_CLI::add_command( 'bookline seed-demo', SeedDemoCommand::class ); \WP_CLI::add_command( 'bookline scrub-automations', ScrubAutomationsCommand::class ); \WP_CLI::add_command( 'bookline e2e-reset', E2eResetCommand::class ); } /** * Public alias used by CLI when services were not yet registered. * * @param Container $container Container. * * @since 0.1.0 */ public static function register_services_for_cli( Container $container ): void { self::register_core_services( $container ); } /** * Register core services on the container. * * @param Container $container Container. */ private static function register_core_services( Container $container ): void { $container->singleton( Container::class, static function () use ( $container ): Container { return $container; } ); $container->singleton( Encryption::class, static function (): Encryption { return new Encryption(); } ); $container->singleton( SettingsStore::class, static function ( Container $c ): SettingsStore { return new SettingsStore( $c->get( Encryption::class ) ); } ); $container->singleton( MigrationRunner::class, static function (): MigrationRunner { return new MigrationRunner(); } ); $container->singleton( RolesRegistrar::class, static function (): RolesRegistrar { return new RolesRegistrar(); } ); $container->singleton( NotificationTemplateSeeder::class, static function (): NotificationTemplateSeeder { return new NotificationTemplateSeeder(); } ); $container->singleton( Clock::class, static function (): Clock { return new SystemClock(); } ); $container->singleton( BusyProviderRegistry::class, static function (): BusyProviderRegistry { $registry = new BusyProviderRegistry(); /** * Register busy-interval providers (calendar sync, time off). * * @param BusyProviderRegistry $registry Registry. * * @since 0.30.0 */ do_action( 'bookline/availability/register_busy', $registry ); $filtered = apply_filters( 'bookline/availability/busy_providers', $registry->all() ); if ( is_array( $filtered ) ) { $registry->replace( $filtered ); } return $registry; } ); $container->singleton( TransactionManager::class, static function (): TransactionManager { return new TransactionManager(); } ); $container->singleton( ServiceRepository::class, static function (): ServiceRepository { return new WpdbServiceRepository(); } ); $container->singleton( ServiceDurationRepository::class, static function (): ServiceDurationRepository { return new WpdbServiceDurationRepository(); } ); $container->singleton( ServiceExtraRepository::class, static function (): ServiceExtraRepository { return new WpdbServiceExtraRepository(); } ); $container->singleton( ServiceCategoryRepository::class, static function (): ServiceCategoryRepository { return new WpdbServiceCategoryRepository(); } ); $container->singleton( AgentRepository::class, static function (): AgentRepository { return new WpdbAgentRepository(); } ); $container->singleton( WeeklyScheduleRepository::class, static function (): WeeklyScheduleRepository { return new WpdbWeeklyScheduleRepository(); } ); $container->singleton( ScheduleOverrideRepository::class, static function (): ScheduleOverrideRepository { return new WpdbScheduleOverrideRepository(); } ); $container->singleton( LocationRepository::class, static function (): LocationRepository { return new WpdbLocationRepository(); } ); $container->singleton( CustomerRepository::class, static function (): CustomerRepository { return new WpdbCustomerRepository(); } ); $container->singleton( BookingRepository::class, static function (): BookingRepository { return new WpdbBookingRepository(); } ); $container->singleton( OrderRepository::class, static function (): OrderRepository { return new WpdbOrderRepository(); } ); $container->singleton( PaymentRepository::class, static function (): PaymentRepository { return new WpdbPaymentRepository(); } ); $container->singleton( CouponRepository::class, static function (): CouponRepository { return new WpdbCouponRepository(); } ); $container->singleton( AutomationRepository::class, static function (): AutomationRepository { return new WpdbAutomationRepository(); } ); $container->singleton( AutomationRunRepository::class, static function (): AutomationRunRepository { return new WpdbAutomationRunRepository(); } ); $container->singleton( AutomationTriggerRegistry::class, static function (): AutomationTriggerRegistry { return new AutomationTriggerRegistry(); } ); $container->singleton( AutomationConditionRegistry::class, static function (): AutomationConditionRegistry { return new AutomationConditionRegistry(); } ); $container->singleton( AutomationHttpClient::class, static function (): AutomationHttpClient { return new WpRemoteAutomationHttp(); } ); $container->singleton( AutomationActionRegistry::class, static function (): AutomationActionRegistry { return new AutomationActionRegistry(); } ); $container->singleton( RecipeCatalogue::class, static function (): RecipeCatalogue { return new RecipeCatalogue(); } ); $container->singleton( AutomationRecipeSeeder::class, static function ( Container $c ): AutomationRecipeSeeder { return new AutomationRecipeSeeder( $c->get( AutomationRepository::class ), $c->get( RecipeCatalogue::class ), $c->get( Clock::class ) ); } ); $container->singleton( AutomationContextFactory::class, static function ( Container $c ): AutomationContextFactory { return new AutomationContextFactory( $c->get( Clock::class ) ); } ); $container->singleton( SubjectAutomationContextRefresher::class, static function ( Container $c ): SubjectAutomationContextRefresher { return new SubjectAutomationContextRefresher( $c->get( BookingRepository::class ), $c->get( CustomerRepository::class ), $c->get( OrderIntentRepository::class ), $c->get( AutomationContextFactory::class ) ); } ); $container->singleton( AutomationExecutor::class, static function ( Container $c ): AutomationExecutor { self::boot_automation_registries( $c ); return new AutomationExecutor( $c->get( AutomationRunRepository::class ), $c->get( AutomationActionRegistry::class ), $c->get( AutomationConditionRegistry::class ), $c->get( Clock::class ), $c->get( JobScheduler::class ), $c->get( SubjectAutomationContextRefresher::class ) ); } ); $container->singleton( AutomationMatcher::class, static function ( Container $c ): AutomationMatcher { self::boot_automation_registries( $c ); $executor = $c->get( AutomationExecutor::class ); $matcher = new AutomationMatcher( $c->get( AutomationRepository::class ), $c->get( AutomationRunRepository::class ), $c->get( AutomationConditionRegistry::class ), $executor, $c->get( Clock::class ), static function ( \Bookline\Domain\Automation\Automation $automation, int $count ): void { unset( $count ); if ( function_exists( 'do_action' ) ) { do_action( 'bookline/automation/rate_cap', $automation ); } } ); $executor->set_reenter( static function ( string $event, AutomationContext $ctx ) use ( $matcher ): void { $fresh = new AutomationContext( $ctx->trigger_event_id() . ':' . $event, $event, $ctx->subject_type(), $ctx->subject_id(), $ctx->customer_id(), $ctx->snapshot_at(), $ctx->fields(), $ctx->depth(), $ctx->ancestor_ids(), $ctx->is_test() ); $matcher->handle( $fresh ); } ); return $matcher; } ); $container->singleton( AutomationCancelService::class, static function ( Container $c ): AutomationCancelService { return new AutomationCancelService( $c->get( AutomationRepository::class ), $c->get( AutomationRunRepository::class ), $c->get( AutomationExecutor::class ) ); } ); $container->singleton( RelativeTimeScheduler::class, static function ( Container $c ): RelativeTimeScheduler { return new RelativeTimeScheduler( $c->get( AutomationRepository::class ), $c->get( BookingRepository::class ), $c->get( CustomerRepository::class ), $c->get( OrderIntentRepository::class ), $c->get( AutomationContextFactory::class ), $c->get( AutomationMatcher::class ), $c->get( JobScheduler::class ), $c->get( Clock::class ) ); } ); $container->singleton( AutomationEventSubscriber::class, static function ( Container $c ): AutomationEventSubscriber { return new AutomationEventSubscriber( $c->get( BookingRepository::class ), $c->get( CustomerRepository::class ), $c->get( OrderIntentRepository::class ), $c->get( AutomationContextFactory::class ), $c->get( AutomationMatcher::class ), $c->get( AutomationCancelService::class ), $c->get( RelativeTimeScheduler::class ) ); } ); $container->singleton( AutomationAdminService::class, static function ( Container $c ): AutomationAdminService { self::boot_automation_registries( $c ); return new AutomationAdminService( $c->get( AutomationRepository::class ), $c->get( AutomationRunRepository::class ), $c->get( AutomationTriggerRegistry::class ), $c->get( AutomationConditionRegistry::class ), $c->get( AutomationActionRegistry::class ), $c->get( RecipeCatalogue::class ), $c->get( AutomationMatcher::class ), $c->get( AutomationExecutor::class ), $c->get( AutomationContextFactory::class ), $c->get( BookingRepository::class ), $c->get( CustomerRepository::class ), $c->get( OrderIntentRepository::class ), $c->get( ReminderRuleRepository::class ), $c->get( Clock::class ), $c->get( NamedFeatureGate::class ) ); } ); $container->singleton( OrderIntentRepository::class, static function (): OrderIntentRepository { return new WpdbOrderIntentRepository(); } ); $container->singleton( CartRepository::class, static function (): CartRepository { return new WpdbCartRepository(); } ); $container->singleton( CartItemRepository::class, static function (): CartItemRepository { return new WpdbCartItemRepository(); } ); $container->singleton( TransactionIntentRepository::class, static function (): TransactionIntentRepository { return new WpdbTransactionIntentRepository(); } ); $container->singleton( EntityMetaRepository::class, static function (): EntityMetaRepository { return new WpdbEntityMetaRepository(); } ); $container->singleton( OrderIntentService::class, static function ( Container $c ): OrderIntentService { return new OrderIntentService( $c->get( OrderIntentRepository::class ), $c->get( CartRepository::class ), $c->get( CartItemRepository::class ), $c->get( TransactionIntentRepository::class ), $c->get( TransactionManager::class ), $c->get( SettingsStore::class ), $c->get( Clock::class ), $c->get( Bus::class ), $c->get( LiveTotalsCalculator::class ) ); } ); $container->singleton( IntentCleanupJob::class, static function ( Container $c ): IntentCleanupJob { return new IntentCleanupJob( $c->get( OrderIntentRepository::class ), $c->get( CartRepository::class ), $c->get( CartItemRepository::class ), $c->get( BookingRepository::class ), $c->get( ReleaseHoldHandler::class ), $c->get( SettingsStore::class ), $c->get( Clock::class ), $c->get( Bus::class ) ); } ); $container->singleton( OrderIntentPersonalDataExporter::class, static function ( Container $c ): OrderIntentPersonalDataExporter { return new OrderIntentPersonalDataExporter( $c->get( OrderIntentRepository::class ) ); } ); $container->singleton( OrderIntentPersonalDataEraser::class, static function ( Container $c ): OrderIntentPersonalDataEraser { return new OrderIntentPersonalDataEraser( $c->get( OrderIntentRepository::class ), $c->get( Clock::class ) ); } ); $container->singleton( PersonalDataRegistrar::class, static function ( Container $c ): PersonalDataRegistrar { return new PersonalDataRegistrar( $c->get( OrderIntentPersonalDataExporter::class ), $c->get( OrderIntentPersonalDataEraser::class ) ); } ); $container->singleton( Bus::class, static function (): Bus { return new Bus(); } ); $container->singleton( JobScheduler::class, static function (): JobScheduler { return new ActionSchedulerAdapter(); } ); $container->singleton( VariableRegistry::class, static function (): VariableRegistry { $registry = new VariableRegistry(); ( new CoreVariableRegistrar() )->register( $registry ); if ( ! function_exists( 'apply_filters' ) ) { return $registry; } /** * Filter the notification variable catalogue. * * @param list $variables Variables. * * @since 0.1.0 */ $filtered = apply_filters( 'bookline/notification/variables', $registry->all() ); if ( ! is_array( $filtered ) ) { return $registry; } $out = new VariableRegistry(); foreach ( $filtered as $variable ) { if ( $variable instanceof VariableDefinition ) { $out->register( $variable ); } } return $out; } ); $container->singleton( TemplateRenderer::class, static function (): TemplateRenderer { return new TemplateRenderer(); } ); $container->singleton( ThemeTemplateLoader::class, static function (): ThemeTemplateLoader { return new ThemeTemplateLoader(); } ); $container->singleton( ChannelRegistry::class, static function ( Container $c ): ChannelRegistry { $registry = new ChannelRegistry(); $registry->register( new EmailChannel( $c->get( TemplateRenderer::class ), $c->get( SettingsStore::class ), $c->get( ThemeTemplateLoader::class ) ) ); return $registry; } ); $container->singleton( AutomationNotificationSender::class, static function ( Container $c ): AutomationNotificationSender { return new AutomationNotificationSender( $c->get( NotificationDispatcher::class ) ); } ); $container->singleton( ListNotificationLogHandler::class, static function ( Container $c ): ListNotificationLogHandler { return new ListNotificationLogHandler( $c->get( NotificationLogRepository::class ) ); } ); $container->singleton( RetryNotificationLogHandler::class, static function ( Container $c ): RetryNotificationLogHandler { return new RetryNotificationLogHandler( $c->get( NotificationLogRepository::class ), $c->get( NotificationDispatcher::class ) ); } ); $container->singleton( NotificationTemplateRepository::class, static function (): NotificationTemplateRepository { return new WpdbNotificationTemplateRepository(); } ); $container->singleton( NotificationLogRepository::class, static function ( Container $c ): NotificationLogRepository { return new WpdbNotificationLogRepository( $c->get( Clock::class ) ); } ); $container->singleton( ReminderRuleRepository::class, static function (): ReminderRuleRepository { return new WpdbReminderRuleRepository(); } ); $container->singleton( ActivityLogRepository::class, static function (): ActivityLogRepository { return new WpdbActivityLogRepository(); } ); $container->singleton( NotificationDispatcher::class, static function ( Container $c ): NotificationDispatcher { return new NotificationDispatcher( $c->get( NotificationTemplateRepository::class ), $c->get( NotificationLogRepository::class ), $c->get( VariableRegistry::class ), $c->get( ChannelRegistry::class ), $c->get( JobScheduler::class ), $c->get( BookingRepository::class ), $c->get( CustomerRepository::class ), $c->get( ServiceRepository::class ), $c->get( AgentRepository::class ), $c->get( LocationRepository::class ), $c->get( SettingsStore::class ), new IcsGenerator(), new CalendarLinks(), $c->get( Clock::class ), new RetryPolicy() ); } ); $container->singleton( ReminderService::class, static function ( Container $c ): ReminderService { return new ReminderService( $c->get( ReminderRuleRepository::class ), new ReminderScheduler(), $c->get( NotificationDispatcher::class ), $c->get( NotificationLogRepository::class ), $c->get( JobScheduler::class ), $c->get( Clock::class ) ); } ); $container->singleton( ReminderCanceller::class, static function ( Container $c ): ReminderCanceller { return $c->get( ReminderService::class ); } ); $container->singleton( HealthMonitor::class, static function ( Container $c ): HealthMonitor { return new HealthMonitor( $c->get( JobScheduler::class ), $c->get( NotificationLogRepository::class ), $c->get( Clock::class ), new SmtpPluginDetector() ); } ); $container->singleton( BookingNotificationListener::class, static function ( Container $c ): BookingNotificationListener { return new BookingNotificationListener( $c->get( NotificationDispatcher::class ), $c->get( ReminderService::class ), $c->get( BookingRepository::class ) ); } ); $container->singleton( BookingPolicy::class, static function ( Container $c ): BookingPolicy { /** @var SettingsStore $settings */ $settings = $c->get( SettingsStore::class ); return new BookingPolicy( (int) $settings->get( 'booking.cancel_window_hours' ), (int) $settings->get( 'booking.reschedule_window_hours' ) ); } ); $container->singleton( GuestCustomerResolver::class, static function ( Container $c ): GuestCustomerResolver { return new GuestCustomerResolver( $c->get( CustomerRepository::class ), $c->get( Clock::class ) ); } ); $container->singleton( CreateBookingHandler::class, static function ( Container $c ): CreateBookingHandler { return new CreateBookingHandler( $c->get( BookingRepository::class ), $c->get( OrderRepository::class ), $c->get( ServiceRepository::class ), $c->get( GuestCustomerResolver::class ), $c->get( TransactionManager::class ), $c->get( Bus::class ), $c->get( Clock::class ) ); } ); $container->singleton( StripeClient::class, static function (): StripeClient { return new StripeHttpClient(); } ); $container->singleton( StripeGateway::class, static function ( Container $c ): StripeGateway { return new StripeGateway( $c->get( SettingsStore::class ), $c->get( StripeClient::class ) ); } ); $container->singleton( PaymentGatewayRegistry::class, static function ( Container $c ): PaymentGatewayRegistry { $registry = new PaymentGatewayRegistry(); $registry->register( $c->get( StripeGateway::class ) ); /** * Register additional payment gateways. * * @param PaymentGatewayRegistry $registry Gateways. * * @since 0.1.0 */ do_action( 'bookline/payments/register', $registry ); return $registry; } ); $container->singleton( WebhookEventDeduper::class, static function (): WebhookEventDeduper { return new WebhookEventDeduper(); } ); $container->singleton( IntentSnapshotCommandFactory::class, static function ( Container $c ): IntentSnapshotCommandFactory { return new IntentSnapshotCommandFactory( $c->get( AgentRepository::class ) ); } ); $container->singleton( CreatePaymentIntentHandler::class, static function ( Container $c ): CreatePaymentIntentHandler { return new CreatePaymentIntentHandler( $c->get( OrderIntentService::class ), $c->get( PaymentGatewayRegistry::class ), $c->get( Clock::class ) ); } ); $container->singleton( ProcessPaymentWebhookHandler::class, static function ( Container $c ): ProcessPaymentWebhookHandler { return new ProcessPaymentWebhookHandler( $c->get( TransactionIntentRepository::class ), $c->get( OrderIntentService::class ), $c->get( CreateBookingHandler::class ), $c->get( IntentSnapshotCommandFactory::class ), $c->get( WebhookEventDeduper::class ), $c->get( Clock::class ) ); } ); $container->singleton( RefundPaymentHandler::class, static function ( Container $c ): RefundPaymentHandler { return new RefundPaymentHandler( $c->get( PaymentRepository::class ), $c->get( PaymentGatewayRegistry::class ), $c->get( Clock::class ) ); } ); $container->singleton( PaymentReconciliationJob::class, static function ( Container $c ): PaymentReconciliationJob { return new PaymentReconciliationJob( $c->get( PaymentRepository::class ), $c->get( PaymentGatewayRegistry::class ), $c->get( JobScheduler::class ), $c->get( Clock::class ) ); } ); $container->singleton( CreateSoftHoldHandler::class, static function ( Container $c ): CreateSoftHoldHandler { return new CreateSoftHoldHandler( $c->get( CreateBookingHandler::class ), $c->get( SettingsStore::class ) ); } ); $container->singleton( RescheduleBookingHandler::class, static function ( Container $c ): RescheduleBookingHandler { return new RescheduleBookingHandler( $c->get( BookingRepository::class ), $c->get( ServiceRepository::class ), $c->get( TransactionManager::class ), $c->get( Bus::class ), $c->get( Clock::class ), $c->get( BookingPolicy::class ), $c->get( ReminderCanceller::class ) ); } ); $container->singleton( AdminRescheduleBookingHandler::class, static function ( Container $c ): AdminRescheduleBookingHandler { return new AdminRescheduleBookingHandler( $c->get( BookingRepository::class ), $c->get( RescheduleBookingHandler::class ) ); } ); $container->singleton( AbandonedAdminService::class, static function ( Container $c ): AbandonedAdminService { return new AbandonedAdminService( $c->get( OrderIntentRepository::class ), $c->get( CartRepository::class ), $c->get( CartItemRepository::class ), $c->get( ServiceRepository::class ), $c->get( NotificationLogRepository::class ), $c->get( NotificationDispatcher::class ), $c->get( Clock::class ) ); } ); $container->singleton( CancelBookingHandler::class, static function ( Container $c ): CancelBookingHandler { return new CancelBookingHandler( $c->get( BookingRepository::class ), $c->get( TransactionManager::class ), $c->get( Bus::class ), $c->get( Clock::class ), $c->get( BookingPolicy::class ), $c->get( ReminderCanceller::class ) ); } ); $container->singleton( ChangeBookingStatusHandler::class, static function ( Container $c ): ChangeBookingStatusHandler { return new ChangeBookingStatusHandler( $c->get( BookingRepository::class ), $c->get( TransactionManager::class ), $c->get( Bus::class ), $c->get( Clock::class ) ); } ); $container->singleton( UpdateBookingNotesHandler::class, static function ( Container $c ): UpdateBookingNotesHandler { return new UpdateBookingNotesHandler( $c->get( BookingRepository::class ), $c->get( Clock::class ) ); } ); $container->singleton( ReleaseHoldHandler::class, static function ( Container $c ): ReleaseHoldHandler { return new ReleaseHoldHandler( $c->get( BookingRepository::class ), $c->get( TransactionManager::class ), $c->get( Bus::class ), $c->get( Clock::class ) ); } ); $container->singleton( ExpireHoldsJob::class, static function ( Container $c ): ExpireHoldsJob { return new ExpireHoldsJob( $c->get( BookingRepository::class ), $c->get( ReleaseHoldHandler::class ), $c->get( Clock::class ) ); } ); $container->singleton( ListBookingsHandler::class, static function ( Container $c ): ListBookingsHandler { return new ListBookingsHandler( $c->get( BookingRepository::class ) ); } ); $container->singleton( ListCustomersHandler::class, static function ( Container $c ): ListCustomersHandler { return new ListCustomersHandler( $c->get( CustomerRepository::class ), $c->get( BookingRepository::class ) ); } ); $container->singleton( GetCustomerHandler::class, static function ( Container $c ): GetCustomerHandler { return new GetCustomerHandler( $c->get( CustomerRepository::class ), $c->get( BookingRepository::class ), $c->get( ServiceRepository::class ) ); } ); $container->singleton( ListServicesHandler::class, static function ( Container $c ): ListServicesHandler { return new ListServicesHandler( $c->get( ServiceRepository::class ), $c->get( ServiceDurationRepository::class ) ); } ); $container->singleton( ListAgentsHandler::class, static function ( Container $c ): ListAgentsHandler { return new ListAgentsHandler( $c->get( AgentRepository::class ) ); } ); $container->singleton( BookingFormRepository::class, static function (): BookingFormRepository { return new WpdbBookingFormRepository(); } ); $container->singleton( FeatureGate::class, static function (): FeatureGate { return new FeatureGate( CatalogueCapacity::limits() ); } ); $container->singleton( NamedFeatureGate::class, static function (): NamedFeatureGate { if ( function_exists( 'bookline_named_feature_gate' ) ) { return bookline_named_feature_gate(); } return new OpenNamedFeatureGate(); } ); $container->singleton( ListBookingFormsHandler::class, static function ( Container $c ): ListBookingFormsHandler { return new ListBookingFormsHandler( $c->get( BookingFormRepository::class ) ); } ); $container->singleton( GetBookingFormHandler::class, static function ( Container $c ): GetBookingFormHandler { return new GetBookingFormHandler( $c->get( BookingFormRepository::class ) ); } ); $container->singleton( MutateBookingFormHandler::class, static function ( Container $c ): MutateBookingFormHandler { return new MutateBookingFormHandler( $c->get( BookingFormRepository::class ), $c->get( Clock::class ), $c->get( NamedFeatureGate::class ) ); } ); $container->singleton( CreatePreviewSessionHandler::class, static function ( Container $c ): CreatePreviewSessionHandler { return new CreatePreviewSessionHandler( $c->get( BookingFormRepository::class ), $c->get( BookingSessionService::class ), $c->get( MutateBookingFormHandler::class ) ); } ); $container->singleton( SystemReportHandler::class, static function ( Container $c ): SystemReportHandler { return new SystemReportHandler( $c->get( HealthMonitor::class ) ); } ); $container->singleton( LiveTotalsCalculator::class, static function ( Container $c ): LiveTotalsCalculator { return new LiveTotalsCalculator( $c->get( SettingsStore::class ), $c->get( Clock::class ), $c->get( ServiceRepository::class ), $c->get( ServiceDurationRepository::class ), $c->get( ServiceExtraRepository::class ), $c->get( CouponRepository::class ), new PriceCalculator(), $c->get( NamedFeatureGate::class ) ); } ); $container->singleton( MutateCouponHandler::class, static function ( Container $c ): MutateCouponHandler { return new MutateCouponHandler( $c->get( CouponRepository::class ), $c->get( Clock::class ), $c->get( NamedFeatureGate::class ) ); } ); $container->singleton( MutateServiceHandler::class, static function ( Container $c ): MutateServiceHandler { return new MutateServiceHandler( $c->get( ServiceRepository::class ), $c->get( Clock::class ), $c->get( NamedFeatureGate::class ) ); } ); $container->singleton( MutateServiceDurationHandler::class, static function ( Container $c ): MutateServiceDurationHandler { return new MutateServiceDurationHandler( $c->get( ServiceDurationRepository::class ), $c->get( ServiceRepository::class ) ); } ); $container->singleton( MutateServiceExtraHandler::class, static function ( Container $c ): MutateServiceExtraHandler { return new MutateServiceExtraHandler( $c->get( ServiceExtraRepository::class ), $c->get( Clock::class ), $c->get( NamedFeatureGate::class ) ); } ); $container->singleton( MutateAgentHandler::class, static function ( Container $c ): MutateAgentHandler { return new MutateAgentHandler( $c->get( AgentRepository::class ), $c->get( Clock::class ) ); } ); $container->singleton( MutateLocationHandler::class, static function ( Container $c ): MutateLocationHandler { return new MutateLocationHandler( $c->get( LocationRepository::class ), $c->get( Clock::class ) ); } ); $container->singleton( MutateCustomerHandler::class, static function ( Container $c ): MutateCustomerHandler { return new MutateCustomerHandler( $c->get( CustomerRepository::class ), $c->get( Clock::class ), $c->get( Bus::class ) ); } ); $container->singleton( WizardService::class, static function ( Container $c ): WizardService { return new WizardService( $c->get( SettingsStore::class ), $c->get( MutateServiceHandler::class ), $c->get( MutateServiceDurationHandler::class ), $c->get( MutateAgentHandler::class ), $c->get( AgentRepository::class ), $c->get( WeeklyScheduleRepository::class ), $c->get( MutateBookingFormHandler::class ) ); } ); $container->singleton( ListNotificationTemplatesHandler::class, static function ( Container $c ): ListNotificationTemplatesHandler { return new ListNotificationTemplatesHandler( $c->get( NotificationTemplateRepository::class ) ); } ); $container->singleton( NotificationAdminService::class, static function ( Container $c ): NotificationAdminService { return new NotificationAdminService( $c->get( NotificationTemplateRepository::class ), $c->get( VariableRegistry::class ), $c->get( TemplateRenderer::class ), $c->get( NotificationLogRepository::class ), $c->get( BookingRepository::class ), $c->get( NotificationDispatcher::class ), $c->get( Clock::class ) ); } ); $container->singleton( ReminderAdminService::class, static function ( Container $c ): ReminderAdminService { return new ReminderAdminService( $c->get( ReminderRuleRepository::class ), $c->get( Clock::class ), $c->get( NamedFeatureGate::class ) ); } ); $container->singleton( FinancialReportsService::class, static function ( Container $c ): FinancialReportsService { return new FinancialReportsService( $c->get( ListBookingsHandler::class ), $c->get( NamedFeatureGate::class ) ); } ); $container->singleton( AdminSearchHandler::class, static function ( Container $c ): AdminSearchHandler { return new AdminSearchHandler( $c->get( ListCustomersHandler::class ), $c->get( ListServicesHandler::class ), $c->get( ListAgentsHandler::class ), $c->get( ListBookingsHandler::class ), $c->get( SettingsStore::class ), $c->get( Clock::class ) ); } ); $container->singleton( CreateAdminBookingHandler::class, static function ( Container $c ): CreateAdminBookingHandler { return new CreateAdminBookingHandler( $c->get( CreateBookingHandler::class ), $c->get( ServiceRepository::class ), $c->get( ServiceDurationRepository::class ), $c->get( SettingsStore::class ) ); } ); $container->singleton( AccessScopeFactory::class, static function ( Container $c ): AccessScopeFactory { return new AccessScopeFactory( $c->get( AgentRepository::class ) ); } ); $container->singleton( RateLimiter::class, static function (): RateLimiter { return new RateLimiter(); } ); $container->singleton( CustomerSessionService::class, static function ( Container $c ): CustomerSessionService { return new CustomerSessionService( $c->get( Clock::class ) ); } ); $container->singleton( PaymentStepRuleSet::class, static function (): PaymentStepRuleSet { return CoreStepBootstrap::payment_rules(); } ); $container->singleton( StepRegistry::class, static function ( Container $c ): StepRegistry { return CoreStepBootstrap::step_registry( $c->get( PaymentStepRuleSet::class ) ); } ); $container->singleton( StepEngine::class, static function ( Container $c ): StepEngine { return new StepEngine( $c->get( StepRegistry::class ) ); } ); $container->singleton( BookingFormScope::class, static function ( Container $c ): BookingFormScope { return new BookingFormScope( $c->get( BookingFormRepository::class ), $c->get( ServiceRepository::class ), $c->get( AgentRepository::class ), $c->get( LocationRepository::class ), ); } ); $container->singleton( BookingSessionService::class, static function ( Container $c ): BookingSessionService { return new BookingSessionService( $c->get( OrderIntentService::class ), $c->get( StepEngine::class ), $c->get( SettingsStore::class ), $c->get( Clock::class ), $c->get( LocationRepository::class ), $c->get( AgentRepository::class ), $c->get( ServiceRepository::class ), $c->get( BookingFormRepository::class ), $c->get( BookingFormScope::class ), $c->get( CreateBookingHandler::class ), $c->get( ServiceExtraRepository::class ), $c->get( NamedFeatureGate::class ) ); } ); $container->singleton( PermissionGate::class, static function ( Container $c ): PermissionGate { return new PermissionGate( $c->get( RateLimiter::class ), $c->get( AccessScopeFactory::class ), $c->get( CustomerSessionService::class ) ); } ); $container->singleton( PublicRestController::class, static function ( Container $c ): PublicRestController { return new PublicRestController( $c->get( BookingSessionService::class ), $c->get( OrderIntentService::class ), $c->get( CustomerSessionService::class ), $c->get( ServiceRepository::class ), $c->get( ServiceCategoryRepository::class ), $c->get( AgentRepository::class ), $c->get( LocationRepository::class ), $c->get( CouponRepository::class ), $c->get( BookingRepository::class ), $c->get( CustomerRepository::class ), $c->get( CancelBookingHandler::class ), $c->get( RescheduleBookingHandler::class ), $c->get( CreatePaymentIntentHandler::class ), $c->get( ProcessPaymentWebhookHandler::class ), $c->get( PaymentGatewayRegistry::class ), $c->get( BookingFormScope::class ), $c->get( NamedFeatureGate::class ), $c->get( BusyProviderRegistry::class ), ); } ); $container->singleton( AdminRestController::class, static function ( Container $c ): AdminRestController { return new AdminRestController( $c->get( PermissionGate::class ), $c->get( ListBookingsHandler::class ), $c->get( ChangeBookingStatusHandler::class ), $c->get( BookingRepository::class ), $c->get( ServiceRepository::class ), $c->get( AgentRepository::class ), $c->get( WeeklyScheduleRepository::class ), $c->get( ScheduleOverrideRepository::class ), $c->get( LocationRepository::class ), $c->get( CustomerRepository::class ), $c->get( CouponRepository::class ), $c->get( SettingsStore::class ), $c->get( Clock::class ), $c->get( HealthMonitor::class ), $c->get( RefundPaymentHandler::class ), $c->get( PaymentRepository::class ), $c->get( ListCustomersHandler::class ), $c->get( ListServicesHandler::class ), $c->get( ListAgentsHandler::class ), $c->get( WizardService::class ), $c->get( ListNotificationTemplatesHandler::class ), $c->get( ListNotificationLogHandler::class ), $c->get( RetryNotificationLogHandler::class ), $c->get( MutateServiceHandler::class ), $c->get( MutateServiceDurationHandler::class ), $c->get( MutateServiceExtraHandler::class ), $c->get( MutateAgentHandler::class ), $c->get( MutateCustomerHandler::class ), $c->get( ListBookingFormsHandler::class ), $c->get( GetBookingFormHandler::class ), $c->get( MutateBookingFormHandler::class ), $c->get( CreatePreviewSessionHandler::class ), $c->get( SystemReportHandler::class ), $c->get( AutomationAdminService::class ), $c->get( CancelBookingHandler::class ), $c->get( UpdateBookingNotesHandler::class ), $c->get( GetCustomerHandler::class ), $c->get( NotificationAdminService::class ), $c->get( ReminderAdminService::class ), $c->get( ActivityLogRepository::class ), $c->get( AdminSearchHandler::class ), $c->get( CreateAdminBookingHandler::class ), $c->get( AdminRescheduleBookingHandler::class ), $c->get( AbandonedAdminService::class ), $c->get( MutateCouponHandler::class ), $c->get( MutateLocationHandler::class ), $c->get( FinancialReportsService::class ) ); } ); $container->singleton( RouteRegistrar::class, static function ( Container $c ): RouteRegistrar { return new RouteRegistrar( $c->get( PermissionGate::class ), $c->get( PublicRestController::class ), $c->get( AdminRestController::class ) ); } ); } /** * Schedule the five-minute heartbeat when Action Scheduler is present. * * Stubbed until Milestone 8 ships Action Scheduler integration. When available, * records a recurring action in the `bookline` group. */ private static function schedule_heartbeat(): void { /** @var HealthMonitor $health */ $health = self::container()->get( HealthMonitor::class ); $health->ensure_scheduled(); } /** * Register core triggers, conditions and actions once. * * @param Container $container Container. */ private static function boot_automation_registries( Container $container ): void { static $done = false; if ( $done ) { return; } $done = true; /** @var AutomationTriggerRegistry $triggers */ $triggers = $container->get( AutomationTriggerRegistry::class ); /** @var AutomationConditionRegistry $conditions */ $conditions = $container->get( AutomationConditionRegistry::class ); /** @var AutomationActionRegistry $actions */ $actions = $container->get( AutomationActionRegistry::class ); ( new CoreAutomationRegistrar() )->register( $triggers, $conditions, $actions, $container->get( AutomationNotificationSender::class ) ); $actions->register( new HttpIntegrationAction( $container->get( AutomationHttpClient::class ) ) ); } /** * Merge filter-provided triggers, conditions and actions (docs/20 §11). * * @param Container $container Container. */ private static function apply_automation_extension_filters( Container $container ): void { if ( ! function_exists( 'apply_filters' ) ) { return; } /** @var AutomationTriggerRegistry $triggers */ $triggers = $container->get( AutomationTriggerRegistry::class ); /** @var AutomationConditionRegistry $conditions */ $conditions = $container->get( AutomationConditionRegistry::class ); /** @var AutomationActionRegistry $actions */ $actions = $container->get( AutomationActionRegistry::class ); $extra_triggers = apply_filters( 'bookline/automation/triggers', $triggers->all() ); if ( is_array( $extra_triggers ) ) { foreach ( $extra_triggers as $item ) { if ( $item instanceof AutomationTrigger ) { $triggers->register( $item ); } } } $extra_conditions = apply_filters( 'bookline/automation/conditions', $conditions->all() ); if ( is_array( $extra_conditions ) ) { foreach ( $extra_conditions as $item ) { if ( $item instanceof AutomationCondition ) { $conditions->register( $item ); } } } $extra_actions = apply_filters( 'bookline/automation/actions', $actions->all() ); if ( is_array( $extra_actions ) ) { foreach ( $extra_actions as $item ) { if ( $item instanceof AutomationAction ) { $actions->register( $item ); } } } } }