>, total: int} */ public function list( int $service_id = 0 ): array { $rows = $service_id > 0 ? $this->extras->find_for_service( $service_id ) : $this->extras->all(); $items = []; foreach ( $rows as $extra ) { $items[] = $this->to_array( $extra ); } return [ 'items' => $items, 'total' => count( $items ), ]; } /** * Create an extra. * * @param array $data Payload. * @return array */ public function create( array $data ): array { $this->named_features()->assert_allows( NamedFeature::ExtrasPackages ); $now = $this->clock->now(); $extra = $this->from_payload( null, $data, $now, $now ); $this->extras->save( $extra ); $id = (int) $extra->id(); $this->extras->replace_links( $id, $this->service_ids_from( $data ) ); $saved = $this->extras->find_by_id( $id ); return $this->to_array( $saved ?? $extra ); } /** * Update an extra. * * @param int $id Extra id. * @param array $data Payload. * @return array * * @throws DomainException When missing. */ public function update( int $id, array $data ): array { $this->named_features()->assert_allows( NamedFeature::ExtrasPackages ); $existing = $this->extras->find_by_id( $id ); if ( null === $existing ) { throw new DomainException( 'extra.not_found', 'Extra not found.' ); } $merged = array_merge( $this->to_payload( $existing ), $data ); $extra = $this->from_payload( $existing->id(), $merged, $existing->created_at(), $this->clock->now(), $existing->uuid() ); $this->extras->save( $extra ); if ( array_key_exists( 'service_ids', $data ) || array_key_exists( 'service_id', $data ) ) { $this->extras->replace_links( $id, $this->service_ids_from( $data ) ); } $saved = $this->extras->find_by_id( $id ); return $this->to_array( $saved ?? $extra ); } /** * Delete an extra. * * @param int $id Extra id. * * @throws DomainException When missing. */ public function delete( int $id ): void { $this->named_features()->assert_allows( NamedFeature::ExtrasPackages ); $existing = $this->extras->find_by_id( $id ); if ( null === $existing ) { throw new DomainException( 'extra.not_found', 'Extra not found.' ); } $this->extras->delete( $existing ); } /** * Admin payload. * * @param ServiceExtra $extra Extra. * @return array */ public function to_array( ServiceExtra $extra ): array { $id = (int) $extra->id(); return [ 'id' => $extra->id(), 'uuid' => $extra->uuid()->to_string(), 'name' => $extra->name(), 'description' => $extra->description(), 'price_minor' => $extra->price()->amount_minor(), 'currency' => $extra->price()->currency(), 'duration_min' => $extra->duration_min(), 'max_quantity' => $extra->max_quantity(), 'image_id' => $extra->image_id(), 'status' => $extra->status()->value, 'sort_order' => $extra->sort_order(), 'service_ids' => $id > 0 ? $this->extras->service_ids_for( $id ) : [], ]; } /** * Fields for merge-on-update. * * @param ServiceExtra $extra Extra. * @return array */ private function to_payload( ServiceExtra $extra ): array { return [ 'name' => $extra->name(), 'description' => $extra->description(), 'price_minor' => $extra->price()->amount_minor(), 'currency' => $extra->price()->currency(), 'duration_min' => $extra->duration_min(), 'max_quantity' => $extra->max_quantity(), 'image_id' => $extra->image_id(), 'status' => $extra->status()->value, 'sort_order' => $extra->sort_order(), ]; } /** * Build an entity from an admin payload. * * @param int|null $id Id. * @param array $data Payload. * @param DateTimeImmutable $created_at Created. * @param DateTimeImmutable $updated_at Updated. * @param Uuid|null $uuid Uuid. */ private function from_payload( ?int $id, array $data, DateTimeImmutable $created_at, DateTimeImmutable $updated_at, ?Uuid $uuid = null ): ServiceExtra { $name = trim( (string) ( $data['name'] ?? '' ) ); $desc = isset( $data['description'] ) ? trim( (string) $data['description'] ) : ''; $currency = strtoupper( trim( (string) ( $data['currency'] ?? 'GBP' ) ) ); $image = isset( $data['image_id'] ) ? (int) $data['image_id'] : 0; return new ServiceExtra( $id, $uuid ?? Uuid::generate(), $name, '' === $desc ? null : $desc, new Money( (int) ( $data['price_minor'] ?? 0 ), $currency ), (int) ( $data['duration_min'] ?? 0 ), max( 1, (int) ( $data['max_quantity'] ?? 1 ) ), $image > 0 ? $image : null, isset( $data['status'] ) ? ServiceExtraStatus::from( (string) $data['status'] ) : ServiceExtraStatus::Active, (int) ( $data['sort_order'] ?? 0 ), $created_at, $updated_at, ); } /** * Service ids from payload (`service_ids` and/or singular `service_id`). * * @param array $data Payload. * @return list */ private function service_ids_from( array $data ): array { $ids = []; if ( isset( $data['service_id'] ) ) { $one = (int) $data['service_id']; if ( $one > 0 ) { $ids[] = $one; } } $raw = $data['service_ids'] ?? null; if ( is_array( $raw ) ) { foreach ( $raw as $id ) { $n = (int) $id; if ( $n > 0 ) { $ids[] = $n; } } } return array_values( array_unique( $ids ) ); } /** * Plan map, or the open directory gate. */ private function named_features(): NamedFeatureGate { return $this->named ?? new OpenNamedFeatureGate(); } }