Files
api/services/nginx/app/tests/Api/ApiFixturesCleanupTest.php
T

531 lines
21 KiB
PHP

<?php
declare(strict_types=1);
usesApiSuite();
it('removes generated customer traces during fixture cleanup', function (): void {
$db = api_test_runtime()->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,
]);
});
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<string, int>
*/
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);
}
/**
* @param array<string, mixed> $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) . "'";
}