prefix . 'abientot_sms_' . $kind; $exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ); $this->check(); if ( $exists !== $table ) { return null; } $columns = $wpdb->get_col( $wpdb->prepare( 'SHOW COLUMNS FROM %i', $table ) ); $this->check(); $required = $kind === 'jobs' ? array( 'id', 'appointment_id', 'revision', 'kind', 'fingerprint', 'config_digest', 'due_at', 'state', 'provider', 'result_at', 'created_at', 'attempts', 'attempt_day' ) : array( 'appointment_id', 'revision', 'fingerprint', 'start_digest', 'status_slug' ); if ( array_diff( $required, $columns ) || array_diff( $columns, $required ) ) { throw new \RuntimeException( 'Unknown retained SMS schema.' ); } $engine = $wpdb->get_var( $wpdb->prepare( 'SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=%s', $table ) ); $this->check(); if ( strtoupper( (string) $engine ) !== 'INNODB' ) { throw new \RuntimeException( 'Retained SMS transaction unavailable.' ); } return $table; } public function export_rows( array $ids, int $page, string $kind = 'jobs' ): array { global $wpdb; $kind = $kind === 'state' ? 'state' : 'jobs'; $table = $this->table( $kind ); if ( ! $table || ! $ids ) { return array(); } $ids[] = ( max( 1, $page ) - 1 ) * 100; if ( $kind === 'state' ) { $rows = $wpdb->get_results( $wpdb->prepare( "SELECT j.appointment_id AS id,j.revision,j.status_slug FROM %i j INNER JOIN {$wpdb->prefix}abientot_appointments a ON a.id=j.appointment_id WHERE a.customer_id IN (" . implode( ',', array_fill( 0, count( $ids ) - 1, '%d' ) ) . ') ORDER BY j.appointment_id LIMIT 100 OFFSET %d', array_merge( array( $table ), $ids ) ), ARRAY_A ); } else { $rows = $wpdb->get_results( $wpdb->prepare( "SELECT j.id,j.appointment_id,j.kind,j.state,j.created_at,j.due_at,j.result_at,j.attempts FROM %i j INNER JOIN {$wpdb->prefix}abientot_appointments a ON a.id=j.appointment_id WHERE a.customer_id IN (" . implode( ',', array_fill( 0, count( $ids ) - 1, '%d' ) ) . ') ORDER BY j.id LIMIT 100 OFFSET %d', array_merge( array( $table ), $ids ) ), ARRAY_A ); } $this->check(); return $rows; } /** Called inside the shared customer's checked transaction, before identity removal. */ public function erase_customer( int $id ): void { global $wpdb; if ( ! BookingLock::is_held() ) { throw new \RuntimeException( 'Privacy lock unavailable.' ); } foreach ( array( 'jobs', 'state' ) as $kind ) { $table = $this->table( $kind ); if ( $table && false === $wpdb->query( $wpdb->prepare( "DELETE r FROM %i r INNER JOIN {$wpdb->prefix}abientot_appointments a ON a.id=r.appointment_id WHERE a.customer_id=%d", $table, $id ) ) ) { throw new \RuntimeException( 'SMS privacy write failed.' ); } } // Shared erasure clears the complete booking snapshot, including SMS consent/proof. } private function check(): void { global $wpdb; if ( $wpdb->last_error ) { throw new \RuntimeException( 'SMS privacy read failed.' ); } } }