gate->current_scope(); return match ( true ) { 'admin.bookings.list' === $route_id => $this->list_bookings( $request, $scope ), 'admin.calendar' === $route_id => $this->calendar( $request, $scope ), 'admin.bookings.get' === $route_id => $this->get_booking( $request, $scope ), 'admin.bookings.status' === $route_id => $this->change_status( $request ), 'admin.bookings.delete' === $route_id => $this->delete_booking( $request, $scope ), 'admin.bookings.export' === $route_id => RestResponder::ok( [ 'items' => [], 'format' => 'json', ] ), 'admin.reports.get' === $route_id => $this->financial_report( $request, $scope ), 'admin.reports.export' === $route_id => $this->financial_report_export( $request, $scope ), 'admin.bookings.create' === $route_id => $this->create_booking( $request ), 'admin.bookings.update' === $route_id => $this->update_booking( $request ), 'admin.bookings.reschedule' === $route_id => $this->reschedule_booking( $request ), 'admin.services.list' === $route_id => RestResponder::ok( $this->list_services->handle() ), 'admin.services.get' === $route_id => $this->get_service( $request ), 'admin.services.create' === $route_id => $this->create_service( $request ), 'admin.services.update' === $route_id => $this->update_service( $request ), 'admin.services.delete' === $route_id => $this->delete_service( $request ), 'admin.service_durations.list' === $route_id => RestResponder::ok( $this->list_service_durations( $request ) ), 'admin.service_durations.create' === $route_id => $this->create_service_duration( $request ), 'admin.service_durations.update' === $route_id => $this->update_service_duration( $request ), 'admin.service_durations.delete' === $route_id => $this->delete_service_duration( $request ), 'admin.service_extras.list' === $route_id => RestResponder::ok( $this->mutate_service_extra->list( (int) ( $request->get_param( 'service_id' ) ?? 0 ) ) ), 'admin.service_extras.create' === $route_id => $this->create_service_extra( $request ), 'admin.service_extras.update' === $route_id => $this->update_service_extra( $request ), 'admin.service_extras.delete' === $route_id => $this->delete_service_extra( $request ), 'admin.agents.list' === $route_id => RestResponder::ok( $this->list_agents->handle( $scope ) ), 'admin.agents.get' === $route_id => $this->get_agent( $request, $scope ), 'admin.agents.create' === $route_id => $this->create_agent( $request ), 'admin.agents.update' === $route_id => $this->update_agent( $request ), 'admin.agents.delete' === $route_id => $this->delete_agent( $request ), 'admin.agents.schedule.get' === $route_id => $this->get_schedule( $request, $scope ), 'admin.agents.overrides.list' === $route_id => $this->list_overrides( $request, $scope ), 'admin.locations.list' === $route_id => $this->list_locations( $request ), 'admin.locations.get' === $route_id => $this->get_location( $request ), 'admin.locations.create' === $route_id => $this->create_location( $request ), 'admin.locations.update' === $route_id => $this->update_location( $request ), 'admin.locations.delete' === $route_id => $this->delete_location( $request ), 'admin.customers.list' === $route_id => $this->handle_list_customers( $request ), 'admin.customers.get' === $route_id => $this->get_customer( $request ), 'admin.customers.create' === $route_id => $this->create_customer( $request ), 'admin.customers.update' === $route_id => $this->update_customer( $request ), 'admin.customers.delete' === $route_id => $this->delete_customer( $request ), 'admin.orders.list' === $route_id => RestResponder::ok( $this->abandoned_admin->list() ), 'admin.orders.recovery' === $route_id => $this->send_order_recovery( $request ), 'admin.payments.list' === $route_id => RestResponder::ok( [ 'items' => [] ] ), 'admin.payments.refund' === $route_id => $this->refund_payment( $request ), 'admin.coupons.list' === $route_id => $this->list_coupons( $request ), 'admin.coupons.get' === $route_id => $this->get_coupon( $request ), 'admin.coupons.create' === $route_id => $this->create_coupon( $request ), 'admin.coupons.update' === $route_id => $this->update_coupon( $request ), 'admin.coupons.delete' === $route_id => $this->delete_coupon( $request ), 'admin.settings.get' === $route_id => RestResponder::ok( [ 'settings' => $this->settings->all_public() ] ), 'admin.settings.update' === $route_id => $this->update_settings( $request ), 'admin.health' === $route_id => RestResponder::ok( $this->health->snapshot() ), 'admin.system_report' === $route_id => RestResponder::ok( $this->system_report->handle() ), 'admin.wizard.state' === $route_id => RestResponder::ok( $this->wizard->get_state() ), 'admin.wizard.business' === $route_id => $this->save_wizard_business( $request ), 'admin.wizard.run' === $route_id => $this->run_wizard_step( $request ), 'admin.notification_templates.list' === $route_id => RestResponder::ok( $this->list_notification_templates->handle() ), 'admin.notification_templates.get' === $route_id => RestResponder::ok( $this->notification_admin->get( absint( $request->get_param( 'id' ) ) ) ), 'admin.notification_templates.update' === $route_id => RestResponder::ok( $this->notification_admin->update( absint( $request->get_param( 'id' ) ), $this->json_payload( $request ) ) ), 'admin.notification_templates.preview' === $route_id => $this->preview_notification_template( $request ), 'admin.notification_templates.test' === $route_id => RestResponder::ok( $this->notification_admin->test_send( absint( $request->get_param( 'id' ) ) ) ), 'admin.notification_variables.list' === $route_id => RestResponder::ok( $this->notification_admin->variables() ), 'admin.notification_log.list' === $route_id => RestResponder::ok( $this->list_notification_log->handle( [ 'page' => max( 1, absint( $request->get_param( 'page' ) ?? 1 ) ), 'per_page' => min( 200, max( 1, absint( $request->get_param( 'per_page' ) ?? 50 ) ) ), 'status' => $request->get_param( 'status' ), 'channel' => $request->get_param( 'channel' ), 'booking_id' => $request->get_param( 'booking_id' ), 'search' => $request->get_param( 'search' ), ] ) ), 'admin.notification_log.retry' === $route_id => RestResponder::ok( $this->retry_notification_log->handle( absint( $request->get_param( 'id' ) ) ) ), 'admin.booking_forms.list' === $route_id => RestResponder::ok( $this->list_booking_forms_for_request( $request ) ), 'admin.booking_forms.create' === $route_id => $this->create_booking_form( $request ), 'admin.booking_forms.get' === $route_id => $this->get_booking_form( $request ), 'admin.booking_forms.update' === $route_id => $this->update_booking_form( $request ), 'admin.booking_forms.delete' === $route_id => $this->delete_booking_form( $request ), 'admin.booking_forms.preview_session' === $route_id => $this->preview_booking_form_session( $request ), str_starts_with( $route_id, 'admin.automations.' ) || str_starts_with( $route_id, 'admin.automation_runs.' ) => $this->handle_automations( $route_id, $request ), 'admin.search' === $route_id => RestResponder::ok( $this->admin_search->handle( self::string_param( $request, 'q', 'search' ), $scope ) ), 'admin.activity.list' === $route_id => $this->list_activity( $request ), 'admin.reminders.list' === $route_id => RestResponder::ok( $this->reminder_admin->list() ), 'admin.reminders.create' === $route_id => RestResponder::ok( $this->reminder_admin->create( $this->json_payload( $request ) ), 201 ), 'admin.reminders.update' === $route_id => RestResponder::ok( $this->reminder_admin->update( absint( $request->get_param( 'id' ) ), $this->json_payload( $request ) ) ), 'admin.reminders.delete' === $route_id => RestResponder::ok( $this->reminder_admin->delete( absint( $request->get_param( 'id' ) ) ) ), str_starts_with( $route_id, 'admin.webhooks.' ) => RestResponder::ok( [ 'items' => [], 'ok' => true, ] ), default => RestResponder::error( 'bookline_unknown_route', __( 'Unknown route.', 'buildler' ), 404 ), }; } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Automation admin routes (docs/20 §10). * * @param string $route_id Route id. * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ private function handle_automations( string $route_id, WP_REST_Request $request ): WP_REST_Response|WP_Error { $id = absint( $request->get_param( 'id' ) ); $page = max( 1, absint( $request->get_param( 'page' ) ?? 1 ) ); $per_page = min( 100, max( 1, absint( $request->get_param( 'per_page' ) ?? 20 ) ) ); $json = $request->get_json_params(); $payload = array_merge( is_array( $request->get_params() ) ? $request->get_params() : [], is_array( $json ) ? $json : [] ); return match ( $route_id ) { 'admin.automations.list' => RestResponder::ok( $this->automation_admin->list( $page, $per_page ) ), 'admin.automations.create' => RestResponder::ok( $this->automation_admin->create( $payload ), 201 ), 'admin.automations.recipes' => RestResponder::ok( $this->automation_admin->recipes() ), 'admin.automations.from_recipe' => RestResponder::ok( $this->automation_admin->from_recipe( sanitize_key( (string) $request->get_param( 'key' ) ) ), 201 ), 'admin.automations.catalogue' => RestResponder::ok( $this->automation_admin->catalogue( is_string( $request->get_param( 'trigger' ) ) ? (string) $request->get_param( 'trigger' ) : null ) ), 'admin.automations.get' => RestResponder::ok( $this->automation_admin->get( $id ) ), 'admin.automations.update' => $this->update_automation( $id, $payload ), 'admin.automations.delete' => RestResponder::ok( $this->automation_admin->delete( $id ) ), 'admin.automations.test' => RestResponder::ok( $this->automation_admin->test( $id, $payload ) ), 'admin.automations.preview' => RestResponder::ok( $this->automation_admin->preview( $id ) ), 'admin.automations.enable' => RestResponder::ok( $this->automation_admin->enable( $id, $payload ) ), 'admin.automation_runs.list' => RestResponder::ok( $this->automation_admin->list_runs( [ 'status' => $request->get_param( 'status' ), 'search' => $request->get_param( 'search' ), 'automation_id' => $request->get_param( 'automation_id' ), 'customer_id' => $request->get_param( 'customer_id' ), ], $page, $per_page ) ), 'admin.automation_runs.get' => RestResponder::ok( $this->automation_admin->get_run( $id ) ), 'admin.automation_runs.cancel' => RestResponder::ok( $this->automation_admin->cancel_run( $id ) ), 'admin.automation_runs.rerun' => RestResponder::ok( $this->automation_admin->rerun( $id ) ), 'admin.automation_runs.process' => RestResponder::ok( $this->automation_admin->process_run( $id ) ), 'admin.automation_runs.skip_wait' => RestResponder::ok( $this->automation_admin->skip_wait( $id ) ), 'admin.automation_runs.process_all' => RestResponder::ok( $this->automation_admin->process_all_queued() ), default => RestResponder::error( 'bookline_unknown_route', __( 'Unknown route.', 'buildler' ), 404 ), }; } /** * PATCH automation; is_enabled is a disable shortcut (enable is the ceremony). * * @param int $id Id. * @param array $payload Payload. */ private function update_automation( int $id, array $payload ): WP_REST_Response { if ( array_key_exists( 'is_enabled', $payload ) && false === $payload['is_enabled'] ) { return RestResponder::ok( $this->automation_admin->disable( $id ) ); } return RestResponder::ok( $this->automation_admin->update( $id, $payload ) ); } /** * List bookings for admin tables (dashboard, bookings screen). * * @param WP_REST_Request $request Request. * @param \Bookline\Application\Auth\AccessScope $scope Scope. */ private function list_bookings( WP_REST_Request $request, \Bookline\Application\Auth\AccessScope $scope ): WP_REST_Response|WP_Error { $range = $this->resolve_booking_range( $request, 'list' ); if ( $range instanceof WP_Error ) { return $range; } [ $from, $to ] = $range; $bookings = $this->list_bookings->handle( $scope, $from, $to ); $customer_id = (int) ( $request->get_param( 'customer_id' ) ?? 0 ); $items = []; foreach ( $bookings as $booking ) { if ( $customer_id > 0 && $booking->customer_id() !== $customer_id ) { continue; } $items[] = $this->booking_to_admin_array( $booking ); } return RestResponder::ok( [ 'items' => $items, 'total' => count( $items ), ] ); } /** * Calendar window listing. * * @param WP_REST_Request $request Request. * @param \Bookline\Application\Auth\AccessScope $scope Scope. */ private function calendar( WP_REST_Request $request, \Bookline\Application\Auth\AccessScope $scope ): WP_REST_Response|WP_Error { $range = $this->resolve_booking_range( $request, 'calendar' ); if ( $range instanceof WP_Error ) { return $range; } [ $from, $to ] = $range; $bookings = $this->list_bookings->handle( $scope, $from, $to ); $agent_id = absint( $request->get_param( 'agent_id' ) ); $service_id = absint( $request->get_param( 'service_id' ) ); $location_id = absint( $request->get_param( 'location_id' ) ); $status_filter = self::string_param( $request, 'status' ); $items = []; foreach ( $bookings as $booking ) { if ( $agent_id > 0 && $booking->agent_id() !== $agent_id ) { continue; } if ( $service_id > 0 && $booking->service_id() !== $service_id ) { continue; } if ( $location_id > 0 && $booking->location_id() !== $location_id ) { continue; } if ( '' !== $status_filter && $booking->status()->value !== $status_filter ) { continue; } $items[] = $this->booking_to_admin_array( $booking ); } return RestResponder::ok( [ 'items' => $items ] ); } /** * Financial report JSON. * * @param WP_REST_Request $request Request. * @param \Bookline\Application\Auth\AccessScope $scope Scope. */ private function financial_report( WP_REST_Request $request, \Bookline\Application\Auth\AccessScope $scope ): WP_REST_Response|WP_Error { $range = $this->resolve_booking_range( $request, 'reports' ); if ( $range instanceof WP_Error ) { return $range; } [ $from, $to ] = $range; try { return RestResponder::ok( $this->reports->summarise( $scope, $from, $to, $this->report_timezone( $request ) ) ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Financial report CSV payload. * * @param WP_REST_Request $request Request. * @param \Bookline\Application\Auth\AccessScope $scope Scope. */ private function financial_report_export( WP_REST_Request $request, \Bookline\Application\Auth\AccessScope $scope ): WP_REST_Response|WP_Error { $range = $this->resolve_booking_range( $request, 'reports' ); if ( $range instanceof WP_Error ) { return $range; } [ $from, $to ] = $range; try { return RestResponder::ok( $this->reports->csv( $scope, $from, $to, $this->report_timezone( $request ) ) ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * IANA zone for report day buckets. * * @param WP_REST_Request $request Request. */ private function report_timezone( WP_REST_Request $request ): string { $tz = self::string_param( $request, 'timezone' ); if ( '' !== $tz ) { return $tz; } return 'UTC'; } /** * Create a booking from the calendar (docs/08 §2.3). * * @param WP_REST_Request $request Request. */ private function create_booking( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { $booking = $this->create_admin_booking->handle( $this->json_payload( $request ) ); return RestResponder::ok( $this->booking_to_admin_array( $booking ), 201 ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Drag-reschedule from the calendar (docs/08 §2.3). * * @param WP_REST_Request $request Request. */ private function reschedule_booking( WP_REST_Request $request ): WP_REST_Response|WP_Error { $payload = $this->json_payload( $request ); $start = (string) ( $payload['start_at'] ?? $payload['start_utc'] ?? '' ); $end = (string) ( $payload['end_at'] ?? $payload['end_utc'] ?? '' ); try { $booking = $this->admin_reschedule->handle( absint( $request->get_param( 'id' ) ), $start, $end ); return RestResponder::ok( $this->booking_to_admin_array( $booking ) ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Send an abandoned-cart recovery email (docs/19 §5). * * @param WP_REST_Request $request Request. */ private function send_order_recovery( WP_REST_Request $request ): WP_REST_Response|WP_Error { $base = function_exists( 'home_url' ) ? home_url( '/' ) : '/'; try { return RestResponder::ok( $this->abandoned_admin->send_recovery( absint( $request->get_param( 'id' ) ), $base ) ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * JSON body merged with request params. * * @param WP_REST_Request $request Request. * @return array */ private function json_payload( WP_REST_Request $request ): array { $json = $request->get_json_params(); $params = $request->get_params(); return array_merge( is_array( $params ) ? $params : [], is_array( $json ) ? $json : [] ); } /** * Preview a notification template. * * @param WP_REST_Request $request Request. */ private function preview_notification_template( WP_REST_Request $request ): WP_REST_Response { $payload = $this->json_payload( $request ); $booking_id = isset( $payload['booking_id'] ) ? (int) $payload['booking_id'] : absint( $request->get_param( 'booking_id' ) ); return RestResponder::ok( $this->notification_admin->preview( absint( $request->get_param( 'id' ) ), $booking_id > 0 ? $booking_id : null ) ); } /** * Activity log list. * * @param WP_REST_Request $request Request. */ private function list_activity( WP_REST_Request $request ): WP_REST_Response { $actor = self::string_param( $request, 'actor' ); $subject = self::string_param( $request, 'subject' ); return RestResponder::ok( $this->activity_log->list_for_admin( max( 1, absint( $request->get_param( 'page' ) ?? 1 ) ), min( 200, max( 1, absint( $request->get_param( 'per_page' ) ?? 50 ) ) ), '' !== $actor ? $actor : null, '' !== $subject ? $subject : null ) ); } /** * Resolve a UTC date range from query params. * * @param WP_REST_Request $request Request. * @param string $mode `list` or `calendar`. * @return array{0: DateTimeImmutable, 1: DateTimeImmutable}|WP_Error */ private function resolve_booking_range( WP_REST_Request $request, string $mode ): array|WP_Error { $from_raw = self::string_param( $request, 'from', 'date_from' ); $to_raw = self::string_param( $request, 'to', 'date_to' ); if ( '' !== $from_raw && '' !== $to_raw ) { try { return [ new DateTimeImmutable( $from_raw, new DateTimeZone( 'UTC' ) ), new DateTimeImmutable( $to_raw, new DateTimeZone( 'UTC' ) ), ]; } catch ( \Exception $e ) { return RestResponder::error( 'bookline_invalid_dates', __( 'Invalid calendar range.', 'buildler' ), 400 ); } } $period = self::string_param( $request, 'period' ); $now = $this->clock->now()->setTimezone( new DateTimeZone( 'UTC' ) ); if ( 'today' === $period ) { $from = $now->setTime( 0, 0, 0 ); return [ $from, $from->modify( '+1 day' ) ]; } if ( 'month' === $period ) { $from = $now->modify( 'first day of this month' )->setTime( 0, 0, 0 ); return [ $from, $from->modify( '+1 month' ) ]; } if ( 'week' === $period ) { $from = $now->modify( 'monday this week' )->setTime( 0, 0, 0 ); return [ $from, $from->modify( '+7 days' ) ]; } if ( 'reports' === $mode ) { $from = $now->modify( 'first day of this month' )->setTime( 0, 0, 0 ); return [ $from, $from->modify( '+1 month' ) ]; } if ( 'calendar' === $mode ) { $from = $now->modify( 'monday this week' )->setTime( 0, 0, 0 ); return [ $from, $from->modify( '+7 days' ) ]; } // Admin list default: a year back and two years forward so new // bookings are not hidden when the operator picks a later slot. $from = $now->modify( '-1 year' )->setTime( 0, 0, 0 ); return [ $from, $now->modify( '+2 years' )->setTime( 23, 59, 59 ) ]; } /** * Map a booking entity to the admin SPA list shape. * * Includes legacy `start_utc`/`end_utc` plus SPA aliases `start_at`/`end_at`. * * @param Booking $booking Booking entity. * @return array */ private function booking_to_admin_array( Booking $booking ): array { $customer = $this->customers->find_by_id( $booking->customer_id() ); $service = $this->services->find_by_id( $booking->service_id() ); $agent = null !== $booking->agent_id() ? $this->agents->find_by_id( $booking->agent_id() ) : null; $location = null !== $booking->location_id() ? $this->locations->find_by_id( $booking->location_id() ) : null; $customer_name = ''; if ( null !== $customer ) { $customer_name = trim( $customer->first_name() . ' ' . ( $customer->last_name() ?? '' ) ); if ( '' === $customer_name ) { $customer_name = $customer->first_name(); } } $start = $booking->start_utc()->format( 'c' ); $end = $booking->end_utc()->format( 'c' ); return [ 'id' => $booking->id(), 'uuid' => $booking->uuid()->to_string(), 'agent_id' => $booking->agent_id(), 'service_id' => $booking->service_id(), 'location_id' => $booking->location_id(), 'timezone' => $booking->timezone()->to_string(), 'customer_id' => $booking->customer_id(), 'status' => $booking->status()->value, 'start_utc' => $start, 'end_utc' => $end, 'start_at' => $start, 'end_at' => $end, 'service_name' => null !== $service ? $service->name() : '', 'customer_name' => $customer_name, 'agent_name' => null !== $agent ? $agent->display_name() : '', 'location_name' => null !== $location ? $location->name() : '', 'title' => $customer_name, 'total_minor' => $booking->price()->amount_minor(), 'notes' => $booking->notes(), 'internal_notes' => $booking->internal_notes(), 'meeting_join_url' => $booking->meeting_join_url(), ]; } /** * Payments attached to a booking or its order. * * @param Booking $booking Booking. * @return list> */ private function payments_for_booking( Booking $booking ): array { $id = $booking->id(); if ( null === $id ) { return []; } $items = []; foreach ( $this->payments->find_for_booking( $id, $booking->order_id() ) as $payment ) { $pid = $payment->id(); if ( null === $pid ) { continue; } $items[] = [ 'id' => $pid, 'type' => $payment->type()->value, 'status' => $payment->status()->value, 'amount_minor' => $payment->amount()->amount_minor(), 'refunded_minor' => $payment->refunded()->amount_minor(), 'currency' => $payment->amount()->currency(), 'gateway' => $payment->gateway(), 'can_refund' => $this->payment_can_refund( $payment ), ]; } return $items; } /** * Whether staff can refund this payment from the booking drawer. * * @param Payment $payment Payment. */ private function payment_can_refund( Payment $payment ): bool { if ( PaymentType::Refund === $payment->type() ) { return false; } if ( ! in_array( $payment->status(), [ PaymentStatus::Succeeded, PaymentStatus::PartiallyRefunded ], true ) ) { return false; } return $payment->amount()->amount_minor() > $payment->refunded()->amount_minor(); } /** * Paginated booking-form list for admin. * * @param WP_REST_Request $request Request. * @return array{items: list>, total: int, page: int, per_page: int} */ private function list_booking_forms_for_request( WP_REST_Request $request ): array { $page = (int) ( $request->get_param( 'page' ) ?? 1 ); $per_page = (int) ( $request->get_param( 'per_page' ) ?? 20 ); return $this->list_booking_forms->handle( $page, $per_page ); } /** * Fetch a single booking form. * * @param WP_REST_Request $request Request. */ private function get_booking_form( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { return RestResponder::ok( $this->get_booking_form_handler->handle( (int) $request['id'] ) ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Create a booking form. * * @param WP_REST_Request $request Request. */ private function create_booking_form( WP_REST_Request $request ): WP_REST_Response|WP_Error { $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = []; } try { $form = $this->mutate_booking_form_handler->create( $params ); return RestResponder::ok( BookingFormPresenter::to_admin_array( $form ) ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Update a booking form. * * @param WP_REST_Request $request Request. */ private function update_booking_form( WP_REST_Request $request ): WP_REST_Response|WP_Error { $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = []; } try { $form = $this->mutate_booking_form_handler->update( (int) $request['id'], $params ); return RestResponder::ok( BookingFormPresenter::to_admin_array( $form ) ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Delete a booking form. * * @param WP_REST_Request $request Request. */ private function delete_booking_form( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { $this->mutate_booking_form_handler->delete( (int) $request['id'] ); return RestResponder::ok( [ 'deleted' => true ] ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Start a preview session from unsaved editor draft. * * @param WP_REST_Request $request Request. */ private function preview_booking_form_session( WP_REST_Request $request ): WP_REST_Response|WP_Error { $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = []; } try { return RestResponder::ok( $this->preview_booking_form_handler->handle( $params ) ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Private helper. * * @param WP_REST_Request $request Request. * @param \Bookline\Application\Auth\AccessScope $scope Scope. */ private function get_booking( WP_REST_Request $request, \Bookline\Application\Auth\AccessScope $scope ): WP_REST_Response|WP_Error { $booking = $this->bookings->find_by_id( (int) $request['id'] ); if ( null === $booking ) { return RestResponder::error( 'bookline_not_found', __( 'Booking not found.', 'buildler' ), 404 ); } if ( ! $scope->allows_agent( $booking->agent_id() ) ) { return RestResponder::error( 'bookline_forbidden', __( 'You do not have permission to access this resource.', 'buildler' ), 403 ); } $payload = $this->booking_to_admin_array( $booking ); $payload['payments'] = $this->payments_for_booking( $booking ); return RestResponder::ok( $payload ); } /** * Private helper. * * @param WP_REST_Request $request Request. */ private function change_status( WP_REST_Request $request ): WP_REST_Response|WP_Error { $raw = (string) $request['status']; try { $next = BookingStatus::from( $raw ); } catch ( \ValueError ) { return RestResponder::error( 'bookline_invalid', __( 'Unknown booking status.', 'buildler' ), 400 ); } $raw_notify = $request->get_param( 'notify' ); $notify = true === $raw_notify || 1 === $raw_notify || '1' === $raw_notify || 'true' === $raw_notify; $id = (int) $request['id']; if ( BookingStatus::Cancelled === $next ) { $booking = $this->cancel_booking->handle( $id, CancelledBy::Admin, null, true, $notify ); } else { $booking = $this->status->handle( $id, $next, $notify ); } return RestResponder::ok( $this->booking_to_admin_array( $booking ) ); } /** * Patch internal notes on a booking (full field update is a later milestone). * * @param WP_REST_Request $request Request. */ private function update_booking( WP_REST_Request $request ): WP_REST_Response|WP_Error { $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = []; } if ( ! array_key_exists( 'internal_notes', $params ) ) { return RestResponder::error( 'bookline_invalid', __( 'Nothing to update.', 'buildler' ), 400 ); } $raw = $params['internal_notes']; if ( null !== $raw && ! is_string( $raw ) ) { return RestResponder::error( 'bookline_invalid', __( 'Internal notes must be a string.', 'buildler' ), 400 ); } $booking = $this->update_notes->handle( (int) $request['id'], $raw ); return RestResponder::ok( $this->booking_to_admin_array( $booking ) ); } /** * Private helper. * * @param WP_REST_Request $request Request. * @param \Bookline\Application\Auth\AccessScope $scope Scope. */ private function delete_booking( WP_REST_Request $request, \Bookline\Application\Auth\AccessScope $scope ): WP_REST_Response|WP_Error { $booking = $this->bookings->find_by_id( (int) $request['id'] ); if ( null === $booking ) { return RestResponder::error( 'bookline_not_found', __( 'Booking not found.', 'buildler' ), 404 ); } if ( ! $scope->allows_agent( $booking->agent_id() ) ) { return RestResponder::error( 'bookline_forbidden', __( 'You do not have permission to access this resource.', 'buildler' ), 403 ); } $this->bookings->soft_delete( $booking, $this->clock->now() ); return RestResponder::ok( [ 'deleted' => true ] ); } /** * Private helper. * * @param WP_REST_Request $request Request. */ private function get_service( WP_REST_Request $request ): WP_REST_Response|WP_Error { $service = $this->services->find_by_id( (int) $request['id'] ); if ( null === $service ) { return RestResponder::error( 'bookline_not_found', __( 'Service not found.', 'buildler' ), 404 ); } $meta = $service->meta(); $provider = 'none'; if ( is_array( $meta ) && isset( $meta['meeting_provider'] ) && is_string( $meta['meeting_provider'] ) ) { $provider = $meta['meeting_provider']; } return RestResponder::ok( [ 'id' => $service->id(), 'uuid' => $service->uuid()->to_string(), 'name' => $service->name(), 'status' => $service->status()->value, 'description' => $service->description(), 'short_description' => $service->short_description(), 'colour' => $service->colour(), 'capacity_min' => $service->capacity_min(), 'capacity_max' => $service->capacity_max(), 'buffer_before_min' => $service->buffer_before_min(), 'buffer_after_min' => $service->buffer_after_min(), 'lead_time_min' => $service->lead_time_min(), 'horizon_days' => $service->horizon_days(), 'slot_step_min' => $service->slot_step_min(), 'deposit_type' => $service->deposit_type()->value, 'deposit_value' => $service->deposit_value(), 'tax_class' => $service->tax_class(), 'requires_approval' => $service->requires_approval(), 'sort_order' => $service->sort_order(), 'meeting_provider' => $provider, ] ); } /** * Private helper. * * @param WP_REST_Request $request Request. * @param \Bookline\Application\Auth\AccessScope $scope Scope. */ private function get_agent( WP_REST_Request $request, \Bookline\Application\Auth\AccessScope $scope ): WP_REST_Response|WP_Error { $agent = $this->agents->find_by_id( (int) $request['id'] ); if ( null === $agent || ! $scope->allows_agent( $agent->id() ) ) { return RestResponder::error( 'bookline_not_found', __( 'Agent not found.', 'buildler' ), 404 ); } $payload = [ 'id' => $agent->id(), 'display_name' => $agent->display_name(), 'email' => $agent->email()->to_string(), ]; if ( function_exists( 'apply_filters' ) ) { $filtered = apply_filters( 'bookline/admin/agent_payload', $payload, $agent ); if ( is_array( $filtered ) ) { $payload = $filtered; } } return RestResponder::ok( $payload ); } /** * Private helper. * * @param WP_REST_Request $request Request. * @param \Bookline\Application\Auth\AccessScope $scope Scope. */ private function get_schedule( WP_REST_Request $request, \Bookline\Application\Auth\AccessScope $scope ): WP_REST_Response|WP_Error { $id = (int) $request['id']; if ( ! $scope->allows_agent( $id ) ) { return RestResponder::error( 'bookline_forbidden', __( 'You do not have permission to access this resource.', 'buildler' ), 403 ); } $rows = $this->schedules->find_by_owner( ScheduleOwnerType::Agent, $id ); $items = []; foreach ( $rows as $row ) { $items[] = [ 'id' => $row->id(), 'weekday' => $row->day_of_week(), 'start_time' => $row->start_local(), 'end_time' => $row->end_local(), ]; } return RestResponder::ok( [ 'items' => $items ] ); } /** * Private helper. * * @param WP_REST_Request $request Request. * @param \Bookline\Application\Auth\AccessScope $scope Scope. */ private function list_overrides( WP_REST_Request $request, \Bookline\Application\Auth\AccessScope $scope ): WP_REST_Response|WP_Error { $id = (int) $request['id']; if ( ! $scope->allows_agent( $id ) ) { return RestResponder::error( 'bookline_forbidden', __( 'You do not have permission to access this resource.', 'buildler' ), 403 ); } $from = $this->clock->now()->modify( '-30 days' ); $to = $this->clock->now()->modify( '+90 days' ); $rows = $this->overrides->find_by_owner_date_range( ScheduleOwnerType::Agent, $id, $from, $to ); $items = []; foreach ( $rows as $row ) { $items[] = [ 'id' => $row->id(), 'date' => $row->date_local()->format( 'Y-m-d' ), ]; } return RestResponder::ok( [ 'items' => $items ] ); } /** * List locations. * * @param WP_REST_Request $request Request. */ private function list_locations( WP_REST_Request $request ): WP_REST_Response { $limit = min( 200, max( 1, absint( $request->get_param( 'per_page' ) ?? 100 ) ) ); $offset = max( 0, absint( $request->get_param( 'offset' ) ?? 0 ) ); $items = []; foreach ( $this->locations->find_stored( $limit, $offset ) as $location ) { $items[] = $this->location_payload( $location ); } return RestResponder::ok( [ 'items' => $items ] ); } /** * Private helper. * * @param WP_REST_Request $request Request. */ private function get_location( WP_REST_Request $request ): WP_REST_Response|WP_Error { $location = $this->locations->find_by_id( (int) $request['id'] ); if ( null === $location ) { return RestResponder::error( 'bookline_not_found', __( 'Location not found.', 'buildler' ), 404 ); } return RestResponder::ok( $this->location_payload( $location ) ); } /** * Create a location. * * @param WP_REST_Request $request Request. */ private function create_location( WP_REST_Request $request ): WP_REST_Response|WP_Error { $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = []; } try { $location = $this->mutate_location->create( $params ); return RestResponder::ok( [ 'id' => $location->id() ] ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Update a location. * * @param WP_REST_Request $request Request. */ private function update_location( WP_REST_Request $request ): WP_REST_Response|WP_Error { $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = []; } try { $location = $this->mutate_location->update( (int) $request['id'], $params ); return RestResponder::ok( [ 'id' => $location->id() ] ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Delete a location. * * @param WP_REST_Request $request Request. */ private function delete_location( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { $this->mutate_location->delete( (int) $request['id'] ); return RestResponder::ok( [ 'deleted' => true ] ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Admin location resource. * * @param Location $location Location. * @return array */ private function location_payload( Location $location ): array { return [ 'id' => $location->id(), 'uuid' => $location->uuid()->to_string(), 'name' => $location->name(), 'slug' => $location->slug(), 'address_line1' => $location->address_line1(), 'address_line2' => $location->address_line2(), 'city' => $location->city(), 'region' => $location->region(), 'postcode' => $location->postcode(), 'country' => $location->country(), 'latitude' => $location->latitude(), 'longitude' => $location->longitude(), 'timezone' => $location->timezone()->to_string(), 'phone' => null === $location->phone() ? null : $location->phone()->to_string(), 'email' => null === $location->email() ? null : $location->email()->to_string(), 'status' => $location->status()->value, 'colour' => $location->colour(), ]; } /** * Private helper. * * @param WP_REST_Request $request Request. */ private function get_customer( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { return RestResponder::ok( $this->get_customer_handler->handle( (int) $request['id'] ) ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * List customers for admin. * * @param WP_REST_Request $request Request. * @return WP_REST_Response */ private function handle_list_customers( WP_REST_Request $request ): WP_REST_Response { $page = (int) ( $request->get_param( 'page' ) ?? 1 ); $per_page = (int) ( $request->get_param( 'per_page' ) ?? 20 ); $search = $request->get_param( 'search' ); $search = is_string( $search ) && '' !== trim( $search ) ? trim( $search ) : null; $result = $this->list_customers->handle( $page, $per_page, $search ); return RestResponder::ok( $result ); } /** * Private helper. * * @param WP_REST_Request $request Request. */ private function get_coupon( WP_REST_Request $request ): WP_REST_Response|WP_Error { $coupon = $this->coupons->find_by_id( (int) $request['id'] ); if ( null === $coupon ) { return RestResponder::error( 'bookline_not_found', __( 'Coupon not found.', 'buildler' ), 404 ); } return RestResponder::ok( $this->mutate_coupon->to_public( $coupon ) ); } /** * List coupons. * * @param WP_REST_Request $request Request. */ private function list_coupons( WP_REST_Request $request ): WP_REST_Response { $page = max( 1, (int) ( $request->get_param( 'page' ) ?? 1 ) ); $per_page = max( 1, min( 100, (int) ( $request->get_param( 'per_page' ) ?? 20 ) ) ); $items = []; foreach ( $this->coupons->find_page( $per_page, ( $page - 1 ) * $per_page ) as $coupon ) { $items[] = $this->mutate_coupon->to_public( $coupon ); } return RestResponder::ok( [ 'items' => $items ] ); } /** * Create a coupon. * * @param WP_REST_Request $request Request. */ private function create_coupon( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { $coupon = $this->mutate_coupon->create( $this->coupon_payload( $request ) ); return RestResponder::ok( $this->mutate_coupon->to_public( $coupon ) ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Update a coupon. * * @param WP_REST_Request $request Request. */ private function update_coupon( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { $coupon = $this->mutate_coupon->update( (int) $request['id'], $this->coupon_payload( $request ) ); return RestResponder::ok( $this->mutate_coupon->to_public( $coupon ) ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Deactivate a coupon. * * @param WP_REST_Request $request Request. */ private function delete_coupon( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { $this->mutate_coupon->deactivate( (int) $request['id'] ); return RestResponder::ok( [ 'deleted' => true ] ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Sanitised coupon fields from the request. * * @param WP_REST_Request $request Request. * @return array */ private function coupon_payload( WP_REST_Request $request ): array { $json = $request->get_json_params(); $src = is_array( $json ) ? $json : []; $out = []; foreach ( [ 'code', 'type', 'currency', 'status' ] as $key ) { if ( isset( $src[ $key ] ) && is_string( $src[ $key ] ) ) { $out[ $key ] = sanitize_text_field( $src[ $key ] ); } } foreach ( [ 'value_minor', 'min_spend_minor', 'usage_limit', 'usage_limit_per_customer' ] as $key ) { if ( isset( $src[ $key ] ) && is_numeric( $src[ $key ] ) ) { $out[ $key ] = (int) $src[ $key ]; } } foreach ( [ 'service_ids', 'agent_ids' ] as $key ) { if ( isset( $src[ $key ] ) && is_array( $src[ $key ] ) ) { $out[ $key ] = $src[ $key ]; } } return $out; } /** * Private helper. * * @param WP_REST_Request $request Request. */ private function update_settings( WP_REST_Request $request ): WP_REST_Response|WP_Error { $settings = $request->get_param( 'settings' ); if ( ! is_array( $settings ) ) { return RestResponder::error( 'bookline_invalid_settings', __( 'Settings must be an object.', 'buildler' ), 400 ); } foreach ( $settings as $key => $value ) { if ( ! is_string( $key ) ) { continue; } if ( ! $this->settings->is_known( $key ) ) { continue; } // Presence flags from all_public() must not overwrite secrets. if ( is_array( $value ) ) { continue; } $this->settings->set( $key, $value ); } return RestResponder::ok( [ 'settings' => $this->settings->all_public() ] ); } /** * Refund a succeeded payment (full or partial). * * @param WP_REST_Request $request Request. */ private function refund_payment( WP_REST_Request $request ): WP_REST_Response { $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = []; } $amount = isset( $params['amount_minor'] ) ? (int) $params['amount_minor'] : 0; $reason = is_string( $params['reason'] ?? null ) ? (string) $params['reason'] : ''; return RestResponder::ok( $this->refunds->handle( (int) $request['id'], $amount, $reason ) ); } /** * Read the first non-empty string param from a list of keys. * * @param WP_REST_Request $request Request. * @param string ...$keys Parameter keys in preference order. */ private static function string_param( WP_REST_Request $request, string ...$keys ): string { foreach ( $keys as $key ) { $value = $request->get_param( $key ); if ( is_string( $value ) && '' !== $value ) { return $value; } } return ''; } /** * Dispatch a wizard step (docs/08 §2.1). * * @param WP_REST_Request $request Request. */ private function run_wizard_step( WP_REST_Request $request ): WP_REST_Response|WP_Error { $step = (string) $request->get_param( 'step' ); $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = []; } try { $this->wizard->run_step( $step, $params ); return RestResponder::ok( $this->wizard->get_state() ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Save wizard business step. * * @param WP_REST_Request $request Request. */ private function save_wizard_business( WP_REST_Request $request ): WP_REST_Response|WP_Error { $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = []; } try { $this->wizard->run_step( 'business', $params ); return RestResponder::ok( $this->wizard->get_state() ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Create a service. * * @param WP_REST_Request $request Request. */ private function create_service( WP_REST_Request $request ): WP_REST_Response|WP_Error { $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = []; } try { $service = $this->mutate_service->create( $params ); return RestResponder::ok( [ 'id' => $service->id() ] ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Update a service. * * @param WP_REST_Request $request Request. */ private function update_service( WP_REST_Request $request ): WP_REST_Response|WP_Error { $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = []; } try { $service = $this->mutate_service->update( (int) $request['id'], $params ); return RestResponder::ok( [ 'id' => $service->id() ] ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Delete a service. * * @param WP_REST_Request $request Request. */ private function delete_service( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { $this->mutate_service->delete( (int) $request['id'] ); return RestResponder::ok( [ 'deleted' => true ] ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * List service durations for a service. * * @param WP_REST_Request $request Request. * @return array */ private function list_service_durations( WP_REST_Request $request ): array { $service_id = (int) ( $request->get_param( 'service_id' ) ?? 0 ); if ( $service_id < 1 ) { return [ 'items' => [], 'total' => 0, ]; } return $this->mutate_service_duration->list_for_service( $service_id ); } /** * Create a service duration. * * @param WP_REST_Request $request Request. */ private function create_service_duration( WP_REST_Request $request ): WP_REST_Response|WP_Error { $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = []; } try { $duration = $this->mutate_service_duration->create( $params ); return RestResponder::ok( [ 'id' => $duration->id() ] ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Update a service duration. * * @param WP_REST_Request $request Request. */ private function update_service_duration( WP_REST_Request $request ): WP_REST_Response|WP_Error { $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = []; } try { $duration = $this->mutate_service_duration->update( (int) $request['id'], $params ); return RestResponder::ok( [ 'id' => $duration->id() ] ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Delete a service duration. * * @param WP_REST_Request $request Request. */ private function delete_service_duration( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { $this->mutate_service_duration->delete( (int) $request['id'] ); return RestResponder::ok( [ 'deleted' => true ] ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Create a service extra. * * @param WP_REST_Request $request Request. */ private function create_service_extra( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { $row = $this->mutate_service_extra->create( $this->extra_payload( $request ) ); return RestResponder::ok( $row, 201 ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Update a service extra. * * @param WP_REST_Request $request Request. */ private function update_service_extra( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { $row = $this->mutate_service_extra->update( (int) $request['id'], $this->extra_payload( $request ) ); return RestResponder::ok( $row ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Delete a service extra. * * @param WP_REST_Request $request Request. */ private function delete_service_extra( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { $this->mutate_service_extra->delete( (int) $request['id'] ); return RestResponder::ok( [ 'deleted' => true ] ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Sanitised extra fields from the request. * * @param WP_REST_Request $request Request. * @return array */ private function extra_payload( WP_REST_Request $request ): array { $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = []; } $out = []; if ( isset( $params['name'] ) ) { $out['name'] = sanitize_text_field( (string) $params['name'] ); } if ( array_key_exists( 'description', $params ) ) { $out['description'] = is_string( $params['description'] ) ? sanitize_textarea_field( $params['description'] ) : ''; } if ( isset( $params['price_minor'] ) ) { $out['price_minor'] = (int) $params['price_minor']; } if ( isset( $params['currency'] ) ) { $out['currency'] = sanitize_text_field( (string) $params['currency'] ); } if ( isset( $params['duration_min'] ) ) { $out['duration_min'] = (int) $params['duration_min']; } if ( isset( $params['max_quantity'] ) ) { $out['max_quantity'] = (int) $params['max_quantity']; } if ( isset( $params['image_id'] ) ) { $out['image_id'] = (int) $params['image_id']; } if ( isset( $params['status'] ) ) { $out['status'] = sanitize_key( (string) $params['status'] ); } if ( isset( $params['sort_order'] ) ) { $out['sort_order'] = (int) $params['sort_order']; } if ( isset( $params['service_id'] ) ) { $out['service_id'] = (int) $params['service_id']; } if ( isset( $params['service_ids'] ) && is_array( $params['service_ids'] ) ) { $ids = []; foreach ( $params['service_ids'] as $id ) { $n = (int) $id; if ( $n > 0 ) { $ids[] = $n; } } $out['service_ids'] = array_values( array_unique( $ids ) ); } return $out; } /** * Create an agent. * * @param WP_REST_Request $request Request. */ private function create_agent( WP_REST_Request $request ): WP_REST_Response|WP_Error { $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = []; } try { $agent = $this->mutate_agent->create( $params ); return RestResponder::ok( [ 'id' => $agent->id() ] ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Update an agent. * * @param WP_REST_Request $request Request. */ private function update_agent( WP_REST_Request $request ): WP_REST_Response|WP_Error { $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = []; } try { $agent = $this->mutate_agent->update( (int) $request['id'], $params ); return RestResponder::ok( [ 'id' => $agent->id() ] ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Delete an agent. * * @param WP_REST_Request $request Request. */ private function delete_agent( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { $this->mutate_agent->delete( (int) $request['id'] ); return RestResponder::ok( [ 'deleted' => true ] ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Create a customer. * * @param WP_REST_Request $request Request. */ private function create_customer( WP_REST_Request $request ): WP_REST_Response|WP_Error { $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = []; } try { $customer = $this->mutate_customer->create( $params ); return RestResponder::ok( [ 'id' => $customer->id() ] ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Update a customer. * * @param WP_REST_Request $request Request. */ private function update_customer( WP_REST_Request $request ): WP_REST_Response|WP_Error { $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = []; } try { $customer = $this->mutate_customer->update( (int) $request['id'], $params ); return RestResponder::ok( [ 'id' => $customer->id() ] ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } /** * Delete a customer. * * @param WP_REST_Request $request Request. */ private function delete_customer( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { $this->mutate_customer->delete( (int) $request['id'] ); return RestResponder::ok( [ 'deleted' => true ] ); } catch ( DomainException $e ) { return RestResponder::from_domain( $e ); } } }