id(); if ( null === $id ) { return; } foreach ( $this->automations->all() as $automation ) { if ( ! $automation->is_enabled() ) { continue; } $trigger = $automation->trigger(); if ( TriggerKind::Relative !== $trigger->kind() ) { continue; } $when = $this->due_at_for_booking( $automation, $booking ); if ( null === $when || $when <= $this->clock->now() ) { continue; } $auto_id = (int) $automation->id(); $this->scheduler->cancel( self::HOOK, [ 'automation_id' => $auto_id, 'subject_id' => $id, 'kind' => 'booking', ], 'bookline' ); $this->scheduler->schedule_single( self::HOOK, [ 'automation_id' => $auto_id, 'subject_id' => $id, 'kind' => 'booking', ], $when, 'bookline', 'automation-rel-' . $auto_id . '-booking-' . $id ); } } /** * Cancel relative jobs for a booking. * * @param int $booking_id Booking id. * * @since 0.1.0 */ public function cancel_for_booking( int $booking_id ): void { foreach ( $this->automations->all() as $automation ) { $auto_id = $automation->id(); if ( null === $auto_id ) { continue; } $this->scheduler->cancel( self::HOOK, [ 'automation_id' => $auto_id, 'subject_id' => $booking_id, 'kind' => 'booking', ], 'bookline' ); } } /** * Schedule relative jobs for an abandoned intent. * * @param OrderIntent $intent Intent. * * @since 0.1.0 */ public function schedule_for_intent( OrderIntent $intent ): void { $id = $intent->id(); if ( null === $id ) { return; } foreach ( $this->automations->all() as $automation ) { if ( ! $automation->is_enabled() ) { continue; } $trigger = $automation->trigger(); if ( TriggerKind::Relative !== $trigger->kind() || 'after_intent_abandoned' !== $trigger->event() ) { continue; } $offset = $trigger->offset_minutes() ?? 0; if ( $offset < 0 ) { continue; } $when = $this->clock->now()->add( new DateInterval( 'PT' . $offset . 'M' ) ); $auto_id = (int) $automation->id(); $this->scheduler->schedule_single( self::HOOK, [ 'automation_id' => $auto_id, 'subject_id' => $id, 'kind' => 'intent', ], $when, 'bookline', 'automation-rel-' . $auto_id . '-intent-' . $id ); } } /** * Fire a relative job. * * @param int $automation_id Automation id. * @param int $subject_id Subject id. * @param string $kind booking|intent. * * @since 0.1.0 */ public function fire( int $automation_id, int $subject_id, string $kind = 'booking' ): void { $automation = $this->automations->find_by_id( $automation_id ); if ( null === $automation || ! $automation->is_enabled() ) { return; } $event = $automation->trigger()->event(); $eid = 'relative:' . $automation_id . ':' . $kind . ':' . $subject_id . ':' . $this->clock->now()->format( 'YmdHis' ); if ( 'intent' === $kind ) { $intent = $this->intents->find_by_id( $subject_id ); if ( null === $intent ) { return; } $ctx = $this->factory->for_intent( $event, $eid, $intent ); $this->matcher->handle_one( $automation, $ctx ); return; } $booking = $this->bookings->find_by_id( $subject_id ); if ( null === $booking || $this->factory->booking_is_void( $booking ) ) { return; } $customer = $this->customers->find_by_id( $booking->customer_id() ); $ctx = $this->factory->for_booking( $event, $eid, $booking, $customer ); $this->matcher->handle_one( $automation, $ctx ); } /** * Compute due instant for a booking-relative trigger. * * @param Automation $automation Automation. * @param Booking $booking Booking. */ private function due_at_for_booking( Automation $automation, Booking $booking ): ?DateTimeImmutable { $event = $automation->trigger()->event(); $offset = $automation->trigger()->offset_minutes() ?? 0; $abs = abs( $offset ); $delta = new DateInterval( 'PT' . $abs . 'M' ); return match ( $event ) { 'before_booking_start' => $booking->start_utc()->sub( $delta ), 'after_booking_start' => $booking->start_utc()->add( $delta ), 'after_booking_end', 'after_last_booking' => $booking->end_utc()->add( $delta ), default => null, }; } }