1000000 ) { throw new \InvalidArgumentException( 'manual_rights_scope' ); } } /** Each kind has its own complete 100-row continuation, using the same page number. */ public function export( string $type, int $id, int $page = 1 ): array { $this->authorize( $type, $id, $page ); $data = array(); $messages = array(); if ( $type === 'customer' ) { $repo = new PrivacyRepository(); foreach ( array( 'customers', 'appointments', 'payment_records', 'activity_log', 'recurrence', 'attendance', 'notification_attempts', 'public_waitlist', 'groups', 'waitlist', 'payment_plans', 'payment_refunds', 'selection_answers', 'dependants', 'attendees', 'credit_grants', 'credit_applications', 'booking_instructions' )as$kind ) { $data[ $kind ] = $repo->export_rows( $kind, array( $id ), $page ); } foreach ( array( 'jobs', 'state' )as$kind ) { $data[ 'sms_' . $kind ] = ( new RetainedSmsPrivacyRepository() )->export_rows( array( $id ), $page, $kind ); } } elseif ( $type === 'staff' ) { $repo = new StaffPrivacyRepository(); $data['staff'] = $page === 1 ? $repo->export_id( $id ) : array(); $data += $repo->export_schedules( $id, $page ); $messages[] = $this->retained_notice(); } elseif ( $type === 'calendar' ) { $data['calendar_connections'] = $page === 1 ? ( new CalendarConnectionsRepository() )->export_connection( $id ) : array(); $messages[] = $this->calendar_notice(); } else { if ( ! ( new PartyAttendeesRepository() )->present() ) { throw new \RuntimeException( 'manual_rights_missing_party_store' ); } $data[ $type ] = $page === 1 ? $this->party_rows( $type, $id ) : array(); if ( $type === 'dependant' ) { $data['linked_attendees'] = $this->party_rows( 'linked_attendees', $id, $page ); } $messages[] = $this->retained_notice(); } $more = false; foreach ( $data as$rows ) { if ( count( $rows ) === 100 ) { $more = true; } } return array( 'data' => $data, 'has_more' => $more, 'messages' => $messages ); } public function erase( string $type, int $id ): array { $this->authorize( $type, $id ); if ( $type === 'customer' ) { if ( ! ( new PrivacyRepository() )->erase_customer( $id ) ) { throw new \RuntimeException( 'manual_rights_customer_failed' ); } return $this->result( true, true, array( $this->retained_notice() ) ); } if ( $type === 'staff' ) { $out = ( new StaffPrivacyRepository() )->erase_id( $id ); return $this->result( $out['removed'], $out['retained'], $out['messages'] ); } if ( $type === 'calendar' ) { $removed = ( new CalendarConnectionsRepository() )->erase_connection( $id ); return $this->result( $removed, $removed, array( $this->calendar_notice() ) ); } return $this->erase_party( $type, $id ); } private function party_rows( string $type, int $id, int $page = 1 ): array { global $wpdb; $p = $wpdb->prefix . 'abientot_'; if ( $type === 'dependant' ) { $rows = $wpdb->get_results( $wpdb->prepare( "SELECT id,display_name,notes,relation_confirmed,state,created_at FROM %i WHERE id=%d", $p . 'dependants', $id ), ARRAY_A ); } elseif ( $type === 'attendee' ) { $rows = $wpdb->get_results( $wpdb->prepare( "SELECT id,appointment_id,ordinal,display_name,notes,checked_in_at FROM %i WHERE id=%d", $p . 'booking_attendees', $id ), ARRAY_A ); } else { $rows = $wpdb->get_results( $wpdb->prepare( "SELECT id,appointment_id,ordinal,display_name,notes,checked_in_at FROM %i WHERE dependant_id=%d ORDER BY id LIMIT 100 OFFSET %d", $p . 'booking_attendees', $id, ( $page - 1 ) * 100 ), ARRAY_A ); } $this->read_check(); if ( ! is_array( $rows ) ) { throw new \RuntimeException( 'manual_rights_read' ); } return $rows; } private function erase_party( string $type, int $id ): array { global $wpdb; $owned = ! BookingLock::is_held(); $started = false; if ( ! BookingLock::acquire() ) { throw new \RuntimeException( 'manual_rights_lock' ); } try { $this->authorize( $type, $id ); TransactionGuard::prepare_write(); if ( ! ( new PartyAttendeesRepository() )->present() ) { throw new \RuntimeException( 'manual_rights_missing_party_store' ); } $this->write( $wpdb->query( 'START TRANSACTION' ) ); $started = true; $rows = $this->party_rows( $type, $id ); $p = $wpdb->prefix . 'abientot_'; if ( $rows ) { if ( $type === 'attendee' ) { $this->write( $wpdb->query( $wpdb->prepare( "UPDATE %i SET display_name='',notes='',dependant_id=NULL,checked_in_at=NULL,checked_in_by=NULL,revision=revision+1 WHERE id=%d", $p . 'booking_attendees', $id ) ) ); } else { $this->write( $wpdb->query( $wpdb->prepare( "UPDATE %i SET display_name='',notes='',dependant_id=NULL,checked_in_at=NULL,checked_in_by=NULL,revision=revision+1 WHERE dependant_id=%d", $p . 'booking_attendees', $id ) ) ); } if ( $type === 'dependant' ) { $this->write( $wpdb->update( $p . 'dependants', array( 'customer_id' => null, 'display_name' => '', 'notes' => '', 'payload_hash' => '', 'relation_confirmed' => 0, 'state' => 'erased' ), array( 'id' => $id ) ) ); } } $this->write( $wpdb->query( 'COMMIT' ) ); $started = false; return $this->result( (bool) $rows, (bool) $rows, $rows ? array( $this->retained_notice() ) : array() ); } catch ( \Throwable $e ) { if ( $started ) { $wpdb->query( 'ROLLBACK' ); } throw $e; } finally { if ( $owned ) { BookingLock::release(); } } } private function result( bool $removed, bool $retained, array $messages ): array { return array( 'items_removed' => $removed, 'items_retained' => $retained, 'messages' => $messages, 'done' => true ); } private function retained_notice(): string { return __( 'Operational IDs, booking links and financial history are retained. Review other records and backups for personal information.', 'abientot' ); } private function calendar_notice(): string { return __( 'Local calendar credentials and busy data are cleared on erasure. Staff links, provider records and shared calendars require separate review.', 'abientot' ); } private function read_check(): void { global $wpdb; if ( $wpdb->last_error ) { throw new \RuntimeException( 'manual_rights_read' ); } } private function write( $result ): void { if ( $result === false ) { throw new \RuntimeException( 'manual_rights_write' ); } $this->read_check(); } }