__( 'Vizyt bookings', 'vizyt' ), 'callback' => [ self::class, 'export' ], ]; return $exporters; } public static function erasers( $erasers ): array { $erasers = (array) $erasers; $erasers['vizyt'] = [ 'eraser_friendly_name' => __( 'Vizyt bookings', 'vizyt' ), 'callback' => [ self::class, 'erase' ], ]; return $erasers; } /** * @return array{data: array}>, done: bool} */ public static function export( $email, $page = 1 ): array { global $wpdb; $page = max( 1, (int) $page ); $customers = self::customers( (string) $email ); if ( ! $customers ) { return [ 'data' => [], 'done' => true ]; } $data = []; if ( 1 === $page ) { foreach ( $customers as $customer ) { $data[] = self::item( 'vizyt-customer', __( 'Booking customer', 'vizyt' ), __( 'Your details as the business keeps them for bookings.', 'vizyt' ), 'vizyt-customer-' . (int) $customer->id, [ __( 'First name', 'vizyt' ) => $customer->first_name, __( 'Last name', 'vizyt' ) => $customer->last_name, __( 'Email', 'vizyt' ) => $customer->email, __( 'Phone', 'vizyt' ) => $customer->phone, __( 'Date of birth', 'vizyt' ) => $customer->date_of_birth, __( 'Notes', 'vizyt' ) => $customer->notes, __( 'Tags', 'vizyt' ) => implode( ', ', Customer::tagList( $customer->tags ) ), __( 'Created', 'vizyt' ) => $customer->created_at, ] ); } } $ids = array_map( static fn( $c ) => (int) $c->id, $customers ); $bookings = (array) $wpdb->get_results( $wpdb->prepare( "SELECT b.*, s.name AS service_name, st.name AS staff_name FROM {$wpdb->prefix}vizyt_bookings b LEFT JOIN {$wpdb->prefix}vizyt_services s ON s.id = b.service_id LEFT JOIN {$wpdb->prefix}vizyt_staff st ON st.id = b.staff_id WHERE b.customer_id IN (" . implode( ',', array_fill( 0, count( $ids ), '%d' ) ) . ') ORDER BY b.id ASC LIMIT %d OFFSET %d', array_merge( $ids, [ self::PER_PAGE, ( $page - 1 ) * self::PER_PAGE ] ) ) ); foreach ( $bookings as $booking ) { $data[] = self::item( 'vizyt-bookings', __( 'Bookings', 'vizyt' ), __( 'Appointments booked with the business.', 'vizyt' ), 'vizyt-booking-' . (int) $booking->id, [ __( 'Booking', 'vizyt' ) => '#' . (int) $booking->id, __( 'Service', 'vizyt' ) => $booking->service_name, __( 'Specialist', 'vizyt' ) => $booking->staff_name, __( 'Start', 'vizyt' ) => $booking->start_datetime, __( 'End', 'vizyt' ) => $booking->end_datetime, __( 'Status', 'vizyt' ) => $booking->status, __( 'Price', 'vizyt' ) => $booking->price . ' ' . $booking->currency, __( 'Cancel reason', 'vizyt' ) => $booking->cancel_reason, __( 'Notes', 'vizyt' ) => $booking->internal_note, __( 'Created', 'vizyt' ) => $booking->created_at, ] ); } if ( $bookings ) { $booking_ids = array_map( static fn( $b ) => (int) $b->id, $bookings ); $events = (array) $wpdb->get_results( $wpdb->prepare( 'SELECT id, booking_id, type, meta, created_at FROM %i WHERE booking_id IN (' . implode( ',', array_fill( 0, count( $booking_ids ), '%d' ) ) . ') ORDER BY id ASC', array_merge( [ BookingEvent::table() ], $booking_ids ) ) ); foreach ( $events as $event ) { $meta = $event->meta ? json_decode( (string) $event->meta, true ) : []; $details = []; foreach ( is_array( $meta ) ? $meta : [] as $key => $value ) { $details[] = $key . ': ' . ( is_bool( $value ) ? ( $value ? 'yes' : 'no' ) : (string) $value ); } $data[] = self::item( 'vizyt-booking-history', __( 'Booking history', 'vizyt' ), __( 'What happened to each booking.', 'vizyt' ), 'vizyt-booking-event-' . (int) $event->id, [ __( 'Booking', 'vizyt' ) => '#' . (int) $event->booking_id, __( 'Event', 'vizyt' ) => $event->type, __( 'Date', 'vizyt' ) => $event->created_at . ' UTC', __( 'Details', 'vizyt' ) => implode( '; ', $details ), ] ); } } return [ 'data' => $data, 'done' => count( $bookings ) < self::PER_PAGE, ]; } /** * @return array{items_removed: bool, items_retained: bool, messages: string[], done: bool} */ public static function erase( $email, $page = 1 ): array { $customers = self::customers( (string) $email ); if ( ! $customers ) { return [ 'items_removed' => false, 'items_retained' => false, 'messages' => [], 'done' => true ]; } $kept = 0; $erased = 0; $failed = 0; foreach ( $customers as $customer ) { $result = Customer::anonymise( (int) $customer->id ); if ( false === $result ) { ++$failed; // rolled back: this customer is unchanged continue; } ++$erased; $kept += $result; } $messages = []; if ( $kept > 0 ) { $messages[] = __( 'Vizyt bookings were kept without personal details, for the business statistics.', 'vizyt' ); } if ( $failed > 0 ) { $messages[] = __( 'Some Vizyt customer details could not be erased because of a database error. Nothing was changed for them; try again.', 'vizyt' ); } return [ 'items_removed' => $erased > 0, 'items_retained' => $kept > 0 || $failed > 0, 'messages' => $messages, 'done' => true, ]; } /** * Customer rows with this email (several rows may share one). * * @return object[] */ private static function customers( string $email ): array { global $wpdb; $email = sanitize_email( $email ); if ( '' === $email ) { return []; } return (array) $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}vizyt_customers WHERE email = %s ORDER BY id ASC", $email ) ); } /** * One exporter item; empty values are left out. * * @param array $fields label => value */ private static function item( string $group, string $label, string $description, string $item_id, array $fields ): array { $data = []; foreach ( $fields as $name => $value ) { if ( null !== $value && '' !== (string) $value ) { $data[] = [ 'name' => (string) $name, 'value' => (string) $value ]; } } return [ 'group_id' => $group, 'group_label' => $label, 'group_description' => $description, 'item_id' => $item_id, 'data' => $data, ]; } }