|null $meta Opaque meta. * @param DateTimeImmutable $created_at Created (UTC). * @param DateTimeImmutable $updated_at Updated (UTC). * @param DateTimeImmutable|null $deleted_at Soft-delete instant. * * @throws \Bookline\Domain\Shared\DomainException When invariants fail. */ public function __construct( private ?int $id, private Uuid $uuid, private ?int $category_id, private string $name, private string $slug, private ?string $description, private ?string $short_description, private ?int $image_id, private ?string $colour, private ServiceStatus $status, private int $capacity_min, private int $capacity_max, private int $buffer_before_min, private int $buffer_after_min, private ?int $lead_time_min, private ?int $horizon_days, private ?int $slot_step_min, private DepositType $deposit_type, private int $deposit_value, private ?string $tax_class, private bool $requires_approval, private int $sort_order, private ?array $meta, private DateTimeImmutable $created_at, private DateTimeImmutable $updated_at, private ?DateTimeImmutable $deleted_at = null, ) { $this->assert_invariants(); } /** * Validate entity invariants. * * @throws DomainException On invariant breach. */ private function assert_invariants(): void { if ( '' === trim( $this->name ) ) { throw new DomainException( 'service.name_empty', 'Service name must not be empty.' ); } if ( '' === trim( $this->slug ) ) { throw new DomainException( 'service.slug_empty', 'Service slug must not be empty.' ); } if ( $this->capacity_min < 1 ) { throw new DomainException( 'service.capacity_min', 'capacity_min must be at least 1.' ); } if ( $this->capacity_max < $this->capacity_min ) { throw new DomainException( 'service.capacity_max', 'capacity_max must be >= capacity_min.' ); } if ( $this->buffer_before_min < 0 || $this->buffer_after_min < 0 ) { throw new DomainException( 'service.buffer_negative', 'Buffers must not be negative.' ); } if ( null !== $this->colour && 1 !== preg_match( '/^#[0-9A-Fa-f]{6}$/', $this->colour ) ) { throw new DomainException( 'service.colour_invalid', 'Colour must be #RRGGBB.' ); } if ( DepositType::Percent === $this->deposit_type && ( $this->deposit_value < 0 || $this->deposit_value > 100 ) ) { throw new DomainException( 'service.deposit_percent', 'Percent deposit must be 0–100.' ); } if ( DepositType::Fixed === $this->deposit_type && $this->deposit_value < 0 ) { throw new DomainException( 'service.deposit_fixed', 'Fixed deposit must not be negative.' ); } if ( DepositType::None === $this->deposit_type && 0 !== $this->deposit_value ) { throw new DomainException( 'service.deposit_none', 'Deposit value must be 0 when type is none.' ); } } /** * Database id. * * @since 0.1.0 */ public function id(): ?int { return $this->id; } /** * Public UUID. * * @since 0.1.0 */ public function uuid(): Uuid { return $this->uuid; } /** * Category id. * * @since 0.1.0 */ public function category_id(): ?int { return $this->category_id; } /** * Name. * * @since 0.1.0 */ public function name(): string { return $this->name; } /** * Slug. * * @since 0.1.0 */ public function slug(): string { return $this->slug; } /** * Description. * * @since 0.1.0 */ public function description(): ?string { return $this->description; } /** * Short description. * * @since 0.1.0 */ public function short_description(): ?string { return $this->short_description; } /** * Image id. * * @since 0.1.0 */ public function image_id(): ?int { return $this->image_id; } /** * Colour. * * @since 0.1.0 */ public function colour(): ?string { return $this->colour; } /** * Status. * * @since 0.1.0 */ public function status(): ServiceStatus { return $this->status; } /** * Capacity min. * * @since 0.1.0 */ public function capacity_min(): int { return $this->capacity_min; } /** * Capacity max. * * @since 0.1.0 */ public function capacity_max(): int { return $this->capacity_max; } /** * Buffer before min. * * @since 0.1.0 */ public function buffer_before_min(): int { return $this->buffer_before_min; } /** * Buffer after min. * * @since 0.1.0 */ public function buffer_after_min(): int { return $this->buffer_after_min; } /** * Lead time min. * * @since 0.1.0 */ public function lead_time_min(): ?int { return $this->lead_time_min; } /** * Horizon days. * * @since 0.1.0 */ public function horizon_days(): ?int { return $this->horizon_days; } /** * Slot step min. * * @since 0.1.0 */ public function slot_step_min(): ?int { return $this->slot_step_min; } /** * Deposit type. * * @since 0.1.0 */ public function deposit_type(): DepositType { return $this->deposit_type; } /** * Deposit value. * * @since 0.1.0 */ public function deposit_value(): int { return $this->deposit_value; } /** * Tax class. * * @since 0.1.0 */ public function tax_class(): ?string { return $this->tax_class; } /** * Requires approval. * * @since 0.1.0 */ public function requires_approval(): bool { return $this->requires_approval; } /** * Sort order. * * @since 0.1.0 */ public function sort_order(): int { return $this->sort_order; } /** * Opaque meta payload. * * @return array|null * * @since 0.1.0 */ public function meta(): ?array { return $this->meta; } /** * Created at. * * @since 0.1.0 */ public function created_at(): DateTimeImmutable { return $this->created_at; } /** * Updated at. * * @since 0.1.0 */ public function updated_at(): DateTimeImmutable { return $this->updated_at; } /** * Deleted at. * * @since 0.1.0 */ public function deleted_at(): ?DateTimeImmutable { return $this->deleted_at; } /** * Whether soft-deleted. * * @since 0.1.0 */ public function is_deleted(): bool { return null !== $this->deleted_at; } /** * Assign the database id after insert. * * @param int $id New id. * * @throws \Bookline\Domain\Shared\DomainException When id is already set or invalid. * * @since 0.1.0 */ public function assign_id( int $id ): void { if ( null !== $this->id ) { throw new DomainException( 'service.id_assigned', 'Service id is already assigned.' ); } if ( $id < 1 ) { throw new DomainException( 'service.id_invalid', 'Service id must be positive.' ); } $this->id = $id; } /** * Soft-delete at the given UTC instant. * * @param DateTimeImmutable $when Deletion instant (UTC). * * @since 0.1.0 */ public function soft_delete( DateTimeImmutable $when ): void { if ( null !== $this->deleted_at ) { return; } $this->deleted_at = $when; $this->updated_at = $when; $this->status = ServiceStatus::Archived; } }