db(); api_fixture_cleanup_ensure_optional_trace_tables($db); $department = api_fixtures()->createDepartment(['name' => 'Fixture Cleanup Department']); $customer = api_fixtures()->createUser(['display_name' => 'Fixture Cleanup Customer']); $cashier = api_fixtures()->createUser(['display_name' => 'Fixture Cleanup Cashier']); $userId = (int)$customer['id']; $customerNumber = (int)$customer['customer_number']; $cashierId = (int)$cashier['id']; $departmentId = (int)$department['id']; api_fixture_cleanup_seed_customer_traces($db, $userId, $customerNumber, $cashierId, $departmentId); expect(api_fixture_cleanup_trace_counts($db, $userId, $customerNumber))->toMatchArray([ 'users' => 1, 'customer_attributes' => 1, 'tokens' => 1, 'user_key_value_pairs' => 1, 'price_overrides' => 1, 'customer_default_department' => 1, 'customer_fixed_pricing' => 1, 'customer_fixed_pricing_versions' => 1, 'customer_vehicle_subscription_versions' => 1, 'customer_discount_override_versions' => 1, 'system_search_economic_customer_index' => 1, 'subuser_grants' => 1, 'collected_order_invoices' => 1, 'orders' => 1, 'order_items' => 1, 'economic_module_orders' => 1, 'stripe_module_orders' => 1, 'stripe_payment_intents' => 1, 'customer_vehicles' => 1, 'customer_vehicles_addons' => 1, 'object_attachments' => 4, ]); api_test_runtime()->endTest(); expect(api_fixture_cleanup_trace_counts($db, $userId, $customerNumber))->toMatchArray([ 'users' => 0, 'customer_attributes' => 0, 'tokens' => 0, 'user_key_value_pairs' => 0, 'price_overrides' => 0, 'customer_default_department' => 0, 'customer_fixed_pricing' => 0, 'customer_fixed_pricing_versions' => 0, 'customer_vehicle_subscription_versions' => 0, 'customer_discount_override_versions' => 0, 'system_search_economic_customer_index' => 0, 'subuser_grants' => 0, 'collected_order_invoices' => 0, 'orders' => 0, 'order_items' => 0, 'economic_module_orders' => 0, 'stripe_module_orders' => 0, 'stripe_payment_intents' => 0, 'customer_vehicles' => 0, 'customer_vehicles_addons' => 0, 'object_attachments' => 0, ]); }); it('restores preserved module config rows during fixture cleanup', function (): void { $db = api_test_runtime()->db(); $module = 'fixture_cleanup'; $variable = 'preserve_module_config_' . bin2hex(random_bytes(4)); $moduleEscaped = $db->real_escape_string($module); $variableEscaped = $db->real_escape_string($variable); $ended = false; try { $db->query( "INSERT INTO `module_config` (`module`, `variable`, `value`, `type`, `created_at`, `updated_at`) " . "VALUES ('{$moduleEscaped}', '{$variableEscaped}', '600100', 'int', '2026-04-14 12:00:00', '2026-04-14 12:00:00')" ); api_fixtures()->preserveModuleConfig($module, $variable); $db->query( "UPDATE `module_config` " . "SET `value` = NULL, `type` = 'string', `updated_at` = '2026-04-14 12:05:00' " . "WHERE `module` = '{$moduleEscaped}' AND `variable` = '{$variableEscaped}'" ); api_test_runtime()->endTest(); $ended = true; $row = api_fixture_cleanup_module_config_row($db, $module, $variable); expect($row) ->not->toBeNull() ->and($row['value'] ?? null)->toBe('600100') ->and($row['type'] ?? null)->toBe('int') ->and($row['created_at'] ?? null)->toBe('2026-04-14 12:00:00') ->and($row['updated_at'] ?? null)->toBe('2026-04-14 12:00:00'); } finally { if (!$ended) { api_test_runtime()->endTest(); } $db->query( "DELETE FROM `module_config` WHERE `module` = '{$moduleEscaped}' AND `variable` = '{$variableEscaped}'" ); } }); function api_fixture_cleanup_ensure_optional_trace_tables(mysqli $db): void { $statements = [ <<<'SQL' CREATE TABLE IF NOT EXISTS `customer_default_department` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `customer_number` INT NOT NULL, `department` INT NOT NULL, `created_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_customer_default_department_customer_number` (`customer_number`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci SQL, <<<'SQL' CREATE TABLE IF NOT EXISTS `customer_fixed_pricing` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `customer_number` INT NOT NULL, `price` INT NOT NULL, `description` VARCHAR(255) NULL, `created_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_customer_fixed_pricing_customer_number` (`customer_number`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci SQL, <<<'SQL' CREATE TABLE IF NOT EXISTS `customer_fixed_pricing_versions` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `customer_number` INT NOT NULL, `price` INT NOT NULL, `description` VARCHAR(255) NULL, `effective_from` DATETIME NOT NULL, `effective_to` DATETIME NULL, `source` VARCHAR(64) NOT NULL DEFAULT 'fixture.test', `confidence` DECIMAL(6,5) NOT NULL DEFAULT 1.00000, `inferred` TINYINT(1) NOT NULL DEFAULT 0, `metadata_json` TEXT NULL, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_customer_fixed_pricing_versions_customer_number` (`customer_number`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci SQL, <<<'SQL' CREATE TABLE IF NOT EXISTS `customer_vehicle_subscription_versions` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `vehicle_id` INT NULL, `customer_number` INT NOT NULL, `reg` VARCHAR(64) NOT NULL, `vehicle_type` INT NOT NULL, `wash_subscription` TINYINT(1) NOT NULL, `effective_from` DATETIME NOT NULL, `effective_to` DATETIME NULL, `source` VARCHAR(64) NOT NULL DEFAULT 'fixture.test', `confidence` DECIMAL(6,5) NOT NULL DEFAULT 1.00000, `inferred` TINYINT(1) NOT NULL DEFAULT 0, `metadata_json` TEXT NULL, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_customer_vehicle_subscription_versions_customer_number` (`customer_number`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci SQL, <<<'SQL' CREATE TABLE IF NOT EXISTS `customer_discount_override_versions` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `user_id` INT NOT NULL, `customer_number` INT NOT NULL, `is_category` TINYINT(1) NOT NULL, `object_id` VARCHAR(64) NOT NULL, `discount` INT NOT NULL, `effective_from` DATETIME NOT NULL, `effective_to` DATETIME NULL, `source` VARCHAR(64) NOT NULL DEFAULT 'fixture.test', `confidence` DECIMAL(6,5) NOT NULL DEFAULT 1.00000, `inferred` TINYINT(1) NOT NULL DEFAULT 0, `metadata_json` TEXT NULL, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_customer_discount_override_versions_customer_number` (`customer_number`), KEY `idx_customer_discount_override_versions_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci SQL, <<<'SQL' CREATE TABLE IF NOT EXISTS `system_search_economic_customer_index` ( `customer_number` INT NOT NULL, `user_id` INT NULL, `local_display_name` VARCHAR(255) NULL, `local_email` VARCHAR(255) NULL, `local_phone` VARCHAR(64) NULL, `economic_name` VARCHAR(255) NULL, `economic_address` VARCHAR(255) NULL, `economic_city` VARCHAR(255) NULL, `economic_zip` VARCHAR(64) NULL, `economic_email` VARCHAR(255) NULL, `economic_cvr` VARCHAR(64) NULL, `economic_mobile_phone` VARCHAR(64) NULL, `economic_barred` TINYINT(1) NULL, `search_text` TEXT NULL, `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`customer_number`), KEY `idx_system_search_economic_customer_index_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci SQL, ]; foreach ($statements as $sql) { $db->query($sql); } } function api_fixture_cleanup_seed_customer_traces( mysqli $db, int $userId, int $customerNumber, int $cashierId, int $departmentId ): void { $now = '2026-04-14 12:00:00'; api_fixture_cleanup_insert($db, 'customer_attributes', [ 'user_id' => $userId, 'attribute' => 'fixture_cleanup_attribute', 'created_at' => $now, ]); api_fixture_cleanup_insert($db, 'tokens', [ 'user_id' => $userId, 'type' => 'AUTH_TOKEN', 'description' => 'Fixture cleanup token', 'token' => 'fixture-cleanup-' . $userId, 'created_at' => $now, ]); api_fixture_cleanup_insert($db, 'user_key_value_pairs', [ 'user_id' => $userId, 'var' => 'fixture_cleanup_key', 'val' => 'fixture_cleanup_value', 'created_at' => $now, 'updated_at' => $now, ]); api_fixture_cleanup_insert($db, 'price_overrides', [ 'user_id' => $userId, 'is_category' => 1, 'product_or_category_id' => 'fixture_cleanup_category', 'percentage' => 25, 'created_at' => $now, 'updated_at' => $now, ]); api_fixture_cleanup_insert($db, 'customer_default_department', [ 'customer_number' => $customerNumber, 'department' => $departmentId, 'created_at' => $now, 'updated_at' => $now, ]); api_fixture_cleanup_insert($db, 'customer_fixed_pricing', [ 'customer_number' => $customerNumber, 'price' => 1999, 'description' => 'Fixture cleanup fixed pricing', 'created_at' => $now, 'updated_at' => $now, ]); api_fixture_cleanup_insert($db, 'customer_fixed_pricing_versions', [ 'customer_number' => $customerNumber, 'price' => 1999, 'description' => 'Fixture cleanup fixed pricing', 'effective_from' => $now, 'source' => 'fixture.test', 'confidence' => 1.00000, 'inferred' => 0, 'metadata_json' => '{}', ]); api_fixture_cleanup_insert($db, 'customer_discount_override_versions', [ 'user_id' => $userId, 'customer_number' => $customerNumber, 'is_category' => 1, 'object_id' => 'fixture_cleanup_category', 'discount' => 25, 'effective_from' => $now, 'source' => 'fixture.test', 'confidence' => 1.00000, 'inferred' => 0, 'metadata_json' => '{}', ]); api_fixture_cleanup_insert($db, 'system_search_economic_customer_index', [ 'customer_number' => $customerNumber, 'user_id' => $userId, 'local_display_name' => 'Fixture Cleanup Customer', 'local_email' => 'fixture-cleanup@example.test', 'search_text' => 'fixture cleanup customer', ]); api_fixture_cleanup_insert($db, 'subuser_grants', [ 'billing_customer_number' => $customerNumber, 'subuser' => $cashierId, 'enabled' => 1, 'note' => 'Fixture cleanup grant', 'permissions' => '[]', 'created_at' => $now, 'updated_at' => $now, 'deleted_at' => null, ]); $invoiceCollectionId = api_fixture_cleanup_insert($db, 'collected_order_invoices', [ 'customer_number' => $customerNumber, 'name' => 'Fixture cleanup invoice', 'notes' => 'Fixture cleanup notes', 'processor' => 0, 'created_at' => $now, 'updated_at' => $now, ]); $orderId = api_fixture_cleanup_insert($db, 'orders', [ 'customer_id' => $customerNumber, 'cashier_id' => $cashierId, 'department_id' => $departmentId, 'reference' => 'FIXTURE-CLEANUP', 'notes' => 'Fixture cleanup order', 'reg_1' => 'FC12345', 'invoice_collection_id' => $invoiceCollectionId, 'using_hand_held' => 0, 'include_in_invoice' => 1, 'created_at' => $now, 'updated_at' => $now, 'completed_at' => $now, 'deleted_at' => null, ]); api_fixture_cleanup_insert($db, 'order_items', [ 'order_id' => $orderId, 'product_id' => 61, 'reference' => 'FIXTURE-CLEANUP-ITEM', 'notes' => 'Fixture cleanup item', 'cashier_id' => $cashierId, 'price' => 250, 'quantity' => 1, 'include_in_invoice' => 1, 'created_at' => $now, 'updated_at' => $now, 'deleted_at' => null, ]); api_fixture_cleanup_insert($db, 'economic_module_orders', [ 'id' => $orderId, 'invoice_draft_id' => 9001, 'invoice_id' => 9002, 'created_at' => $now, 'updated_at' => $now, ]); api_fixture_cleanup_insert($db, 'stripe_module_orders', [ 'id' => $orderId, 'invoice_id' => 'in_fixture_cleanup', 'customer_id' => 'cus_fixture_cleanup', 'created_at' => $now, 'updated_at' => $now, ]); api_fixture_cleanup_insert($db, 'stripe_payment_intents', [ 'order_id' => $orderId, 'payment_intent_id' => 'pi_fixture_cleanup', 'client_secret' => 'secret_fixture_cleanup', 'data' => '{}', 'created_at' => $now, 'updated_at' => $now, ]); $vehicleId = api_fixture_cleanup_insert($db, 'customer_vehicles', [ 'customer_id' => $customerNumber, 'type' => 61, 'reg' => 'FC12345', 'wash_subscription' => 1, 'notes' => 'Fixture cleanup vehicle', 'reference' => 'Fixture cleanup reference', 'created_at' => $now, 'updated_at' => $now, 'deleted_at' => null, ]); api_fixture_cleanup_insert($db, 'customer_vehicles_addons', [ 'vehicle_id' => $vehicleId, 'addon_id' => 71, 'amount' => 1, 'created_at' => $now, 'updated_at' => $now, ]); api_fixture_cleanup_insert($db, 'customer_vehicle_subscription_versions', [ 'vehicle_id' => $vehicleId, 'customer_number' => $customerNumber, 'reg' => 'FC12345', 'vehicle_type' => 61, 'wash_subscription' => 1, 'effective_from' => $now, 'source' => 'fixture.test', 'confidence' => 1.00000, 'inferred' => 0, 'metadata_json' => '{}', ]); $attachmentPayload = '{"fixture_cleanup":true}'; api_fixture_cleanup_insert($db, 'object_attachments', [ 'object_type' => 'users', 'object_id' => $userId, 'content' => $attachmentPayload, 'created_at' => $now, 'updated_at' => $now, 'deleted_at' => null, ]); api_fixture_cleanup_insert($db, 'object_attachments', [ 'object_type' => 'orders', 'object_id' => $orderId, 'content' => $attachmentPayload, 'created_at' => $now, 'updated_at' => $now, 'deleted_at' => null, ]); api_fixture_cleanup_insert($db, 'object_attachments', [ 'object_type' => 'customer_vehicles', 'object_id' => $vehicleId, 'content' => $attachmentPayload, 'created_at' => $now, 'updated_at' => $now, 'deleted_at' => null, ]); api_fixture_cleanup_insert($db, 'object_attachments', [ 'object_type' => 'collected_order_invoices', 'object_id' => $invoiceCollectionId, 'content' => $attachmentPayload, 'created_at' => $now, 'updated_at' => $now, 'deleted_at' => null, ]); } /** * @return array */ function api_fixture_cleanup_trace_counts(mysqli $db, int $userId, int $customerNumber): array { return [ 'users' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `users` WHERE `id` = {$userId}"), 'customer_attributes' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `customer_attributes` WHERE `user_id` = {$userId}"), 'tokens' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `tokens` WHERE `user_id` = {$userId}"), 'user_key_value_pairs' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `user_key_value_pairs` WHERE `user_id` = {$userId}"), 'price_overrides' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `price_overrides` WHERE `user_id` = {$userId}"), 'customer_default_department' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `customer_default_department` WHERE `customer_number` = {$customerNumber}"), 'customer_fixed_pricing' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `customer_fixed_pricing` WHERE `customer_number` = {$customerNumber}"), 'customer_fixed_pricing_versions' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `customer_fixed_pricing_versions` WHERE `customer_number` = {$customerNumber}"), 'customer_vehicle_subscription_versions' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `customer_vehicle_subscription_versions` WHERE `customer_number` = {$customerNumber}"), 'customer_discount_override_versions' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `customer_discount_override_versions` WHERE `customer_number` = {$customerNumber}"), 'system_search_economic_customer_index' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `system_search_economic_customer_index` WHERE `customer_number` = {$customerNumber}"), 'subuser_grants' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `subuser_grants` WHERE `billing_customer_number` = {$customerNumber}"), 'collected_order_invoices' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `collected_order_invoices` WHERE `customer_number` = {$customerNumber}"), 'orders' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `orders` WHERE `customer_id` = {$customerNumber}"), 'order_items' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `order_items` WHERE `order_id` IN (SELECT `id` FROM `orders` WHERE `customer_id` = {$customerNumber})"), 'economic_module_orders' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `economic_module_orders` WHERE `id` IN (SELECT `id` FROM `orders` WHERE `customer_id` = {$customerNumber})"), 'stripe_module_orders' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `stripe_module_orders` WHERE `id` IN (SELECT `id` FROM `orders` WHERE `customer_id` = {$customerNumber})"), 'stripe_payment_intents' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `stripe_payment_intents` WHERE `order_id` IN (SELECT `id` FROM `orders` WHERE `customer_id` = {$customerNumber})"), 'customer_vehicles' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `customer_vehicles` WHERE `customer_id` = {$customerNumber}"), 'customer_vehicles_addons' => api_fixture_cleanup_count($db, "SELECT COUNT(*) AS c FROM `customer_vehicles_addons` WHERE `vehicle_id` IN (SELECT `id` FROM `customer_vehicles` WHERE `customer_id` = {$customerNumber})"), 'object_attachments' => api_fixture_cleanup_count( $db, "SELECT COUNT(*) AS c FROM `object_attachments` WHERE (`object_type` = 'users' AND `object_id` = {$userId}) OR (`object_type` = 'orders' AND `object_id` IN (SELECT `id` FROM `orders` WHERE `customer_id` = {$customerNumber})) OR (`object_type` = 'customer_vehicles' AND `object_id` IN (SELECT `id` FROM `customer_vehicles` WHERE `customer_id` = {$customerNumber})) OR (`object_type` = 'collected_order_invoices' AND `object_id` IN (SELECT `id` FROM `collected_order_invoices` WHERE `customer_number` = {$customerNumber}))" ), ]; } function api_fixture_cleanup_count(mysqli $db, string $sql): int { $result = $db->query($sql); if ($result === false) { throw new RuntimeException('Fixture cleanup assertion query failed: ' . $sql); } $row = $result->fetch_assoc(); $result->free(); return (int)($row['c'] ?? 0); } function api_fixture_cleanup_module_config_row(mysqli $db, string $module, string $variable): ?array { $moduleEscaped = $db->real_escape_string($module); $variableEscaped = $db->real_escape_string($variable); $result = $db->query( "SELECT * FROM `module_config` WHERE `module` = '{$moduleEscaped}' AND `variable` = '{$variableEscaped}' LIMIT 1" ); if ($result === false) { throw new RuntimeException('Fixture cleanup module_config query failed.'); } $row = $result->fetch_assoc(); $result->free(); return $row ?: null; } /** * @param array $data */ function api_fixture_cleanup_insert(mysqli $db, string $table, array $data): int { $filtered = []; foreach ($data as $column => $value) { if (api_fixture_cleanup_has_column($db, $table, $column)) { $filtered[$column] = $value; } } if ($filtered === []) { throw new RuntimeException('No matching columns available for fixture cleanup insert into ' . $table . '.'); } $columns = array_map( static fn(string $column): string => '`' . $column . '`', array_keys($filtered) ); $values = array_map( static fn(mixed $value): string => api_fixture_cleanup_sql_value($db, $value), array_values($filtered) ); $sql = 'INSERT INTO `' . $table . '` (' . implode(', ', $columns) . ') VALUES (' . implode(', ', $values) . ')'; if ($db->query($sql) === false) { throw new RuntimeException('Fixture cleanup insert failed: ' . $sql); } return (int)$db->insert_id; } function api_fixture_cleanup_has_column(mysqli $db, string $table, string $column): bool { static $cache = []; $cacheKey = $table . ':' . $column; if (array_key_exists($cacheKey, $cache)) { return $cache[$cacheKey]; } $escapedTable = $db->real_escape_string($table); $escapedColumn = $db->real_escape_string($column); $result = $db->query( "SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '{$escapedTable}' AND COLUMN_NAME = '{$escapedColumn}' LIMIT 1" ); if ($result === false) { return $cache[$cacheKey] = false; } $exists = $result->fetch_assoc() !== null; $result->free(); return $cache[$cacheKey] = $exists; } function api_fixture_cleanup_sql_value(mysqli $db, mixed $value): string { if ($value === null) { return 'NULL'; } if (is_bool($value)) { return $value ? '1' : '0'; } if (is_int($value) || is_float($value)) { return (string)$value; } return "'" . $db->real_escape_string((string)$value) . "'"; }