$completed_steps Completed step names. * @param list $cart Prepared bookings. * @param PreparedBooking $draft Draft being built. * @param CustomerDraft $customer Customer draft. * @param string|null $coupon_code Coupon. * @param array $totals Server totals. * @param DateTimeImmutable $created_at Created UTC. * @param DateTimeImmutable $expires_at Expires UTC. * @param DateTimeImmutable $last_activity_at Last activity UTC. * @param string|null $ip_hash Hashed IP. * @param int|null $intent_id Persisted intent id. * * @throws DomainException When token or step is empty. */ public function __construct( private readonly string $token, private readonly FormConfiguration $form, private readonly ?string $locale, private readonly ?string $customer_timezone, private readonly string $current_step, private readonly array $completed_steps, private readonly array $cart, private readonly PreparedBooking $draft, private readonly CustomerDraft $customer, private readonly ?string $coupon_code, private readonly array $totals, private readonly DateTimeImmutable $created_at, private readonly DateTimeImmutable $expires_at, private readonly DateTimeImmutable $last_activity_at, private readonly ?string $ip_hash = null, private readonly ?int $intent_id = null, ) { if ( '' === $this->token ) { throw new DomainException( 'session.token_empty', 'Session token must not be empty.' ); } if ( '' === $this->current_step ) { throw new DomainException( 'session.step_empty', 'Current step must not be empty.' ); } } /** * Token. */ public function token(): string { return $this->token; } /** * Form configuration. */ public function form(): FormConfiguration { return $this->form; } /** * Form id convenience. */ public function form_id(): int { return $this->form->form_id(); } /** * Locale. */ public function locale(): ?string { return $this->locale; } /** * Customer timezone. */ public function customer_timezone(): ?string { return $this->customer_timezone; } /** * Current step. */ public function current_step(): string { return $this->current_step; } /** * Completed steps. * * @return list */ public function completed_steps(): array { return $this->completed_steps; } /** * Cart. * * @return list */ public function cart(): array { return $this->cart; } /** * Draft. */ public function draft(): PreparedBooking { return $this->draft; } /** * Customer draft. */ public function customer(): CustomerDraft { return $this->customer; } /** * Coupon code. */ public function coupon_code(): ?string { return $this->coupon_code; } /** * Totals. * * @return array */ public function totals(): array { return $this->totals; } /** * Total after discounts in minor units. */ public function total_minor(): int { $total = $this->totals['total_minor'] ?? 0; return is_int( $total ) ? $total : (int) $total; } /** * Created at. */ public function created_at(): DateTimeImmutable { return $this->created_at; } /** * Expires at. */ public function expires_at(): DateTimeImmutable { return $this->expires_at; } /** * Last activity. */ public function last_activity_at(): DateTimeImmutable { return $this->last_activity_at; } /** * IP hash. */ public function ip_hash(): ?string { return $this->ip_hash; } /** * Intent id. */ public function intent_id(): ?int { return $this->intent_id; } /** * Whether the session has expired at the given instant. * * @param DateTimeImmutable $now Now. * @param int $idle_minutes Idle TTL (docs/06: 60). */ public function is_expired( DateTimeImmutable $now, int $idle_minutes = 60 ): bool { if ( $now >= $this->expires_at ) { return true; } $idle_deadline = $this->last_activity_at->modify( '+' . max( 1, $idle_minutes ) . ' minutes' ); return $now >= $idle_deadline; } /** * With a new current step. * * @param string $step Step name. */ public function with_current_step( string $step ): self { return new self( $this->token, $this->form, $this->locale, $this->customer_timezone, $step, $this->completed_steps, $this->cart, $this->draft, $this->customer, $this->coupon_code, $this->totals, $this->created_at, $this->expires_at, $this->last_activity_at, $this->ip_hash, $this->intent_id, ); } /** * With completed steps. * * @param list $completed Completed. */ public function with_completed_steps( array $completed ): self { return new self( $this->token, $this->form, $this->locale, $this->customer_timezone, $this->current_step, array_values( $completed ), $this->cart, $this->draft, $this->customer, $this->coupon_code, $this->totals, $this->created_at, $this->expires_at, $this->last_activity_at, $this->ip_hash, $this->intent_id, ); } /** * Mark a step completed (idempotent). * * @param string $step Step name. */ public function with_step_completed( string $step ): self { $completed = $this->completed_steps; if ( ! in_array( $step, $completed, true ) ) { $completed[] = $step; } return $this->with_completed_steps( $completed ); } /** * With draft. * * @param PreparedBooking $draft Draft. */ public function with_draft( PreparedBooking $draft ): self { return new self( $this->token, $this->form, $this->locale, $this->customer_timezone, $this->current_step, $this->completed_steps, $this->cart, $draft, $this->customer, $this->coupon_code, $this->totals, $this->created_at, $this->expires_at, $this->last_activity_at, $this->ip_hash, $this->intent_id, ); } /** * With customer. * * @param CustomerDraft $customer Customer. */ public function with_customer( CustomerDraft $customer ): self { return new self( $this->token, $this->form, $this->locale, $this->customer_timezone, $this->current_step, $this->completed_steps, $this->cart, $this->draft, $customer, $this->coupon_code, $this->totals, $this->created_at, $this->expires_at, $this->last_activity_at, $this->ip_hash, $this->intent_id, ); } /** * With cart. * * @param list $cart Cart. */ public function with_cart( array $cart ): self { return new self( $this->token, $this->form, $this->locale, $this->customer_timezone, $this->current_step, $this->completed_steps, array_values( $cart ), $this->draft, $this->customer, $this->coupon_code, $this->totals, $this->created_at, $this->expires_at, $this->last_activity_at, $this->ip_hash, $this->intent_id, ); } /** * With coupon. * * @param string|null $code Coupon. */ public function with_coupon_code( ?string $code ): self { $trimmed = null === $code ? null : trim( $code ); return new self( $this->token, $this->form, $this->locale, $this->customer_timezone, $this->current_step, $this->completed_steps, $this->cart, $this->draft, $this->customer, '' === $trimmed ? null : $trimmed, $this->totals, $this->created_at, $this->expires_at, $this->last_activity_at, $this->ip_hash, $this->intent_id, ); } /** * With totals. * * @param array $totals Totals. */ public function with_totals( array $totals ): self { return new self( $this->token, $this->form, $this->locale, $this->customer_timezone, $this->current_step, $this->completed_steps, $this->cart, $this->draft, $this->customer, $this->coupon_code, $totals, $this->created_at, $this->expires_at, $this->last_activity_at, $this->ip_hash, $this->intent_id, ); } /** * With form configuration. * * @param FormConfiguration $form Form. */ public function with_form( FormConfiguration $form ): self { return new self( $this->token, $form, $this->locale, $this->customer_timezone, $this->current_step, $this->completed_steps, $this->cart, $this->draft, $this->customer, $this->coupon_code, $this->totals, $this->created_at, $this->expires_at, $this->last_activity_at, $this->ip_hash, $this->intent_id, ); } /** * Refresh activity timestamp. * * @param DateTimeImmutable $now Now. */ public function with_activity( DateTimeImmutable $now ): self { return new self( $this->token, $this->form, $this->locale, $this->customer_timezone, $this->current_step, $this->completed_steps, $this->cart, $this->draft, $this->customer, $this->coupon_code, $this->totals, $this->created_at, $this->expires_at, $now, $this->ip_hash, $this->intent_id, ); } /** * Serialise session snapshot for API responses. * * @return array */ public function to_array(): array { $cart = []; foreach ( $this->cart as $item ) { $cart[] = $item->to_array(); } return [ 'current_step' => $this->current_step, 'completed_steps' => $this->completed_steps, 'draft' => $this->draft->to_array(), 'cart' => $cart, 'customer_draft' => $this->customer->to_array(), 'coupon_code' => $this->coupon_code, 'totals' => $this->totals, 'locale' => $this->locale, 'customer_timezone' => $this->customer_timezone, 'cart_item_count' => count( $this->cart ), ]; } }