Add tests for API fixture cleanup and optimize customer trace removal logic
This commit is contained in:
@@ -26,6 +26,7 @@ class economic_v2_distribution_service
|
||||
private array $department_name_cache = [];
|
||||
private array $department_excluded_cache = [];
|
||||
private array $customer_name_cache = [];
|
||||
private array $customer_inclusion_cache = [];
|
||||
private array $product_cache = [];
|
||||
private array $product_department_price_cache = [];
|
||||
private array $discount_resolution_cache = [];
|
||||
@@ -230,6 +231,9 @@ class economic_v2_distribution_service
|
||||
$version_rows = $this->fetchVehicleSubscriptionVersionRows($from_ts, $to_ts);
|
||||
foreach ($version_rows as $row) {
|
||||
$customer_number = (int)$row['customer_number'];
|
||||
if (!$this->shouldIncludeCustomerNumber($customer_number)) {
|
||||
continue;
|
||||
}
|
||||
$reg = (string)$row['reg'];
|
||||
$version_id = (int)$row['id'];
|
||||
$vehicle_type = (int)$row['vehicle_type'];
|
||||
@@ -360,6 +364,9 @@ class economic_v2_distribution_service
|
||||
foreach ($orders as $order) {
|
||||
$order_id = (int)$order['id'];
|
||||
$customer_number = (int)$order['customer_id'];
|
||||
if (!$this->shouldIncludeCustomerNumber($customer_number)) {
|
||||
continue;
|
||||
}
|
||||
$department_id = (int)$order['department_id'];
|
||||
$created_at = (string)$order['created_at'];
|
||||
|
||||
@@ -637,11 +644,11 @@ class economic_v2_distribution_service
|
||||
{
|
||||
try {
|
||||
return $this->buildCustomerEnvelope($customer_number, $transaction_map);
|
||||
} catch (Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'id' => null,
|
||||
'customer_number' => $customer_number,
|
||||
'customer_name' => $this->getCustomerName($customer_number),
|
||||
'customer_name' => $this->customer_name_cache[$customer_number] ?? 'Unknown Customer',
|
||||
'transactions' => array_values($transaction_map),
|
||||
'requires_action' => false,
|
||||
'meta' => [],
|
||||
@@ -673,6 +680,9 @@ class economic_v2_distribution_service
|
||||
if ($invoice_id <= 0 || $customer_number <= 0 || $invoice_date === '') {
|
||||
continue;
|
||||
}
|
||||
if (!$this->shouldIncludeCustomerNumber($customer_number)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$month_key = substr($invoice_date, 0, 7);
|
||||
foreach ($this->parseBookedDepartment75InvoiceLines($invoice_id, (array)($invoice_lines[$invoice_id] ?? []), $warnings) as $line) {
|
||||
@@ -1038,6 +1048,9 @@ class economic_v2_distribution_service
|
||||
foreach ($orders as $order) {
|
||||
$order_id = (int)$order['id'];
|
||||
$customer_number = (int)$order['customer_id'];
|
||||
if (!$this->shouldIncludeCustomerNumber($customer_number)) {
|
||||
continue;
|
||||
}
|
||||
$department_id = (int)$order['department_id'];
|
||||
$created_at = (string)$order['created_at'];
|
||||
|
||||
@@ -1115,6 +1128,9 @@ class economic_v2_distribution_service
|
||||
foreach ($orders as $order) {
|
||||
$order_id = (int)$order['id'];
|
||||
$customer_number = (int)$order['customer_id'];
|
||||
if (!$this->shouldIncludeCustomerNumber($customer_number)) {
|
||||
continue;
|
||||
}
|
||||
$department_id = (int)$order['department_id'];
|
||||
$created_at = (string)$order['created_at'];
|
||||
|
||||
@@ -1589,6 +1605,32 @@ class economic_v2_distribution_service
|
||||
return (string)$this->customer_name_cache[$customer_number];
|
||||
}
|
||||
|
||||
protected function shouldIncludeCustomerNumber(int $customer_number): bool
|
||||
{
|
||||
if ($customer_number <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (array_key_exists($customer_number, $this->customer_inclusion_cache)) {
|
||||
return (bool)$this->customer_inclusion_cache[$customer_number];
|
||||
}
|
||||
|
||||
$rows = (new users_o())->getFieldsWhere(['customer_number' => $customer_number], ['id']);
|
||||
if (empty($rows)) {
|
||||
$this->customer_inclusion_cache[$customer_number] = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->getCustomerName($customer_number);
|
||||
} catch (\Throwable $e) {
|
||||
$this->customer_inclusion_cache[$customer_number] = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->customer_inclusion_cache[$customer_number] = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function isDepartmentEligible(int $department_id): bool
|
||||
{
|
||||
if ($department_id === self::SYSTEM_ORDER_DEPARTMENT_ID || $department_id <= 0) {
|
||||
|
||||
@@ -0,0 +1,530 @@
|
||||
<?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) . "'";
|
||||
}
|
||||
@@ -11,6 +11,15 @@ use RuntimeException;
|
||||
final class ApiFixtures
|
||||
{
|
||||
private static int $sequence = 0;
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private array $tableExistsCache = [];
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private array $tableColumnExistsCache = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly mysqli $db,
|
||||
@@ -53,13 +62,13 @@ final class ApiFixtures
|
||||
]);
|
||||
|
||||
$this->cleanup->add(function () use ($userId, $customerNumber): void {
|
||||
$this->deleteWhere('customer_attributes', ['user_id' => $userId]);
|
||||
$this->deleteWhere('tokens', ['user_id' => $userId]);
|
||||
$this->deleteById('users', $userId);
|
||||
$this->purgeCustomerTraceData($userId, $customerNumber);
|
||||
$this->deleteRedisKey('user_id_from_customer_number_' . $customerNumber);
|
||||
$this->deleteRedisKey('customer_number_from_user_id_' . $userId);
|
||||
$this->deleteRedisKey('users_' . $customerNumber . '_economic_customer_name');
|
||||
$this->deleteRedisKey('`users`_' . $customerNumber . '_economic_customer_name');
|
||||
$this->deleteRedisKey('users_' . $userId . '_economic_customer');
|
||||
$this->deleteRedisKey('`users`_' . $userId . '_economic_customer');
|
||||
$this->deleteRedisPattern('perm:user:' . $userId . ':*');
|
||||
});
|
||||
|
||||
@@ -604,6 +613,64 @@ final class ApiFixtures
|
||||
$this->cleanup->add(fn() => $this->deleteWhere($table, $conditions));
|
||||
}
|
||||
|
||||
private function purgeCustomerTraceData(int $userId, int $customerNumber): void
|
||||
{
|
||||
$invoiceCollectionIds = $this->fetchIntColumnWhere('collected_order_invoices', 'id', [
|
||||
'customer_number' => $customerNumber,
|
||||
]);
|
||||
$orderIds = $this->fetchIntColumnWhere('orders', 'id', [
|
||||
'customer_id' => $customerNumber,
|
||||
]);
|
||||
$vehicleIds = $this->fetchIntColumnWhere('customer_vehicles', 'id', [
|
||||
'customer_id' => $customerNumber,
|
||||
]);
|
||||
|
||||
if ($orderIds !== []) {
|
||||
$this->deleteWhereInIfPossible('order_items', 'order_id', $orderIds);
|
||||
$this->deleteWhereInIfPossible('economic_module_orders', 'id', $orderIds);
|
||||
$this->deleteWhereInIfPossible('stripe_module_orders', 'id', $orderIds);
|
||||
$this->deleteWhereInIfPossible('stripe_payment_intents', 'order_id', $orderIds);
|
||||
$this->deleteWhereInIfPossible('object_attachments', 'object_id', $orderIds, [
|
||||
'object_type' => 'orders',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($vehicleIds !== []) {
|
||||
$this->deleteWhereInIfPossible('customer_vehicles_addons', 'vehicle_id', $vehicleIds);
|
||||
$this->deleteWhereInIfPossible('object_attachments', 'object_id', $vehicleIds, [
|
||||
'object_type' => 'customer_vehicles',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($invoiceCollectionIds !== []) {
|
||||
$this->deleteWhereInIfPossible('object_attachments', 'object_id', $invoiceCollectionIds, [
|
||||
'object_type' => 'collected_order_invoices',
|
||||
]);
|
||||
}
|
||||
|
||||
$this->deleteWhereIfPossible('customer_attributes', ['user_id' => $userId]);
|
||||
$this->deleteWhereIfPossible('tokens', ['user_id' => $userId]);
|
||||
$this->deleteWhereIfPossible('user_key_value_pairs', ['user_id' => $userId]);
|
||||
$this->deleteWhereIfPossible('price_overrides', ['user_id' => $userId]);
|
||||
$this->deleteWhereIfPossible('customer_default_department', ['customer_number' => $customerNumber]);
|
||||
$this->deleteWhereIfPossible('customer_fixed_pricing', ['customer_number' => $customerNumber]);
|
||||
$this->deleteWhereIfPossible('customer_fixed_pricing_versions', ['customer_number' => $customerNumber]);
|
||||
$this->deleteWhereIfPossible('customer_vehicle_subscription_versions', ['customer_number' => $customerNumber]);
|
||||
$this->deleteWhereIfPossible('customer_discount_override_versions', ['customer_number' => $customerNumber]);
|
||||
$this->deleteWhereIfPossible('customer_discount_override_versions', ['user_id' => $userId]);
|
||||
$this->deleteWhereIfPossible('subuser_grants', ['billing_customer_number' => $customerNumber]);
|
||||
$this->deleteWhereIfPossible('system_search_economic_customer_index', ['customer_number' => $customerNumber]);
|
||||
$this->deleteWhereIfPossible('system_search_economic_customer_index', ['user_id' => $userId]);
|
||||
$this->deleteWhereIfPossible('object_attachments', [
|
||||
'object_type' => 'users',
|
||||
'object_id' => $userId,
|
||||
]);
|
||||
$this->deleteWhereIfPossible('orders', ['customer_id' => $customerNumber]);
|
||||
$this->deleteWhereIfPossible('customer_vehicles', ['customer_id' => $customerNumber]);
|
||||
$this->deleteWhereIfPossible('collected_order_invoices', ['customer_number' => $customerNumber]);
|
||||
$this->deleteById('users', $userId);
|
||||
}
|
||||
|
||||
private function seedCustomerNameCache(int $customerNumber, string $name): void
|
||||
{
|
||||
$payload = [
|
||||
@@ -711,6 +778,139 @@ final class ApiFixtures
|
||||
$statement->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $conditions
|
||||
*/
|
||||
private function deleteWhereIfPossible(string $table, array $conditions): void
|
||||
{
|
||||
if ($conditions === [] || !$this->tableExists($table)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (array_keys($conditions) as $column) {
|
||||
if (!$this->tableHasColumn($table, (string)$column)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->deleteWhere($table, $conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $conditions
|
||||
* @return array<int, int>
|
||||
*/
|
||||
private function fetchIntColumnWhere(string $table, string $column, array $conditions): array
|
||||
{
|
||||
if (!$this->tableExists($table) || !$this->tableHasColumn($table, $column)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach (array_keys($conditions) as $conditionColumn) {
|
||||
if (!$this->tableHasColumn($table, (string)$conditionColumn)) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
$table = $this->sanitizeIdentifier($table);
|
||||
$column = $this->sanitizeIdentifier($column);
|
||||
$parts = [];
|
||||
$types = '';
|
||||
$values = [];
|
||||
|
||||
foreach ($conditions as $conditionColumn => $value) {
|
||||
$conditionColumn = $this->sanitizeIdentifier((string)$conditionColumn);
|
||||
if ($value === null) {
|
||||
$parts[] = '`' . $conditionColumn . '` IS NULL';
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts[] = '`' . $conditionColumn . '` = ?';
|
||||
[$type, $normalizedValue] = $this->normalizeValue($value);
|
||||
$types .= $type;
|
||||
$values[] = $normalizedValue;
|
||||
}
|
||||
|
||||
$sql = 'SELECT `' . $column . '` FROM `' . $table . '`';
|
||||
if ($parts !== []) {
|
||||
$sql .= ' WHERE ' . implode(' AND ', $parts);
|
||||
}
|
||||
|
||||
$statement = $this->db->prepare($sql);
|
||||
if ($statement === false) {
|
||||
throw new RuntimeException('Failed to prepare select for ' . $table . '.');
|
||||
}
|
||||
|
||||
if ($values !== []) {
|
||||
$statement->bind_param($types, ...$values);
|
||||
}
|
||||
|
||||
$statement->execute();
|
||||
$statement->bind_result($selectedValue);
|
||||
|
||||
$selected = [];
|
||||
while ($statement->fetch()) {
|
||||
$selectedValue = (int)$selectedValue;
|
||||
if ($selectedValue > 0) {
|
||||
$selected[] = $selectedValue;
|
||||
}
|
||||
}
|
||||
|
||||
$statement->close();
|
||||
|
||||
return array_values(array_unique($selected));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $values
|
||||
* @param array<string, mixed> $conditions
|
||||
*/
|
||||
private function deleteWhereInIfPossible(string $table, string $column, array $values, array $conditions = []): void
|
||||
{
|
||||
$values = array_values(array_unique(array_filter(
|
||||
array_map('intval', $values),
|
||||
static fn(int $value): bool => $value > 0
|
||||
)));
|
||||
if ($values === [] || !$this->tableExists($table) || !$this->tableHasColumn($table, $column)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (array_keys($conditions) as $conditionColumn) {
|
||||
if (!$this->tableHasColumn($table, (string)$conditionColumn)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$table = $this->sanitizeIdentifier($table);
|
||||
$column = $this->sanitizeIdentifier($column);
|
||||
$parts = ['`' . $column . '` IN (' . implode(', ', array_fill(0, count($values), '?')) . ')'];
|
||||
$types = str_repeat('i', count($values));
|
||||
$boundValues = $values;
|
||||
|
||||
foreach ($conditions as $conditionColumn => $value) {
|
||||
$conditionColumn = $this->sanitizeIdentifier((string)$conditionColumn);
|
||||
if ($value === null) {
|
||||
$parts[] = '`' . $conditionColumn . '` IS NULL';
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts[] = '`' . $conditionColumn . '` = ?';
|
||||
[$type, $normalizedValue] = $this->normalizeValue($value);
|
||||
$types .= $type;
|
||||
$boundValues[] = $normalizedValue;
|
||||
}
|
||||
|
||||
$sql = 'DELETE FROM `' . $table . '` WHERE ' . implode(' AND ', $parts);
|
||||
$statement = $this->db->prepare($sql);
|
||||
if ($statement === false) {
|
||||
throw new RuntimeException('Failed to prepare delete for ' . $table . '.');
|
||||
}
|
||||
|
||||
$statement->bind_param($types, ...$boundValues);
|
||||
$statement->execute();
|
||||
$statement->close();
|
||||
}
|
||||
|
||||
private function deleteById(string $table, int $id): void
|
||||
{
|
||||
$table = $this->sanitizeIdentifier($table);
|
||||
@@ -912,6 +1112,65 @@ final class ApiFixtures
|
||||
return date('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
private function tableExists(string $table): bool
|
||||
{
|
||||
$table = $this->sanitizeIdentifier($table);
|
||||
if (array_key_exists($table, $this->tableExistsCache)) {
|
||||
return $this->tableExistsCache[$table];
|
||||
}
|
||||
|
||||
$escapedTable = $this->db->real_escape_string($table);
|
||||
$result = $this->db->query(
|
||||
"SELECT 1
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = '{$escapedTable}'
|
||||
LIMIT 1"
|
||||
);
|
||||
if ($result === false) {
|
||||
return $this->tableExistsCache[$table] = false;
|
||||
}
|
||||
|
||||
$exists = $result->fetch_assoc() !== null;
|
||||
$result->free();
|
||||
|
||||
return $this->tableExistsCache[$table] = $exists;
|
||||
}
|
||||
|
||||
private function tableHasColumn(string $table, string $column): bool
|
||||
{
|
||||
$table = $this->sanitizeIdentifier($table);
|
||||
$column = $this->sanitizeIdentifier($column);
|
||||
$cacheKey = $table . ':' . $column;
|
||||
|
||||
if (array_key_exists($cacheKey, $this->tableColumnExistsCache)) {
|
||||
return $this->tableColumnExistsCache[$cacheKey];
|
||||
}
|
||||
|
||||
if (!$this->tableExists($table)) {
|
||||
return $this->tableColumnExistsCache[$cacheKey] = false;
|
||||
}
|
||||
|
||||
$escapedTable = $this->db->real_escape_string($table);
|
||||
$escapedColumn = $this->db->real_escape_string($column);
|
||||
$result = $this->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 $this->tableColumnExistsCache[$cacheKey] = false;
|
||||
}
|
||||
|
||||
$exists = $result->fetch_assoc() !== null;
|
||||
$result->free();
|
||||
|
||||
return $this->tableColumnExistsCache[$cacheKey] = $exists;
|
||||
}
|
||||
|
||||
private function uniqueCustomerNumber(): int
|
||||
{
|
||||
return 80000000 + (++self::$sequence);
|
||||
|
||||
+10
@@ -53,6 +53,7 @@ if (!class_exists('TestableEconomicV2BookedDepartment75DistributionService')) {
|
||||
public array $subscriptionPrices = [];
|
||||
public array $stubBookedInvoices = [];
|
||||
public array $stubBookedInvoiceLines = [];
|
||||
public ?array $includedCustomers = null;
|
||||
|
||||
public function __construct(?economic_v2_versioning_service $versioning = null)
|
||||
{
|
||||
@@ -128,6 +129,15 @@ if (!class_exists('TestableEconomicV2BookedDepartment75DistributionService')) {
|
||||
return $department_id > 0 && $department_id !== 10;
|
||||
}
|
||||
|
||||
protected function shouldIncludeCustomerNumber(int $customer_number): bool
|
||||
{
|
||||
if ($this->includedCustomers === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array($customer_number, $this->includedCustomers, true);
|
||||
}
|
||||
|
||||
protected function parseDepartmentMap(array $department_map): array
|
||||
{
|
||||
$parsed = [];
|
||||
|
||||
+133
@@ -54,6 +54,7 @@ if (!class_exists('TestableEconomicV2DistributionService')) {
|
||||
public array $stubVersionRows = [];
|
||||
public array $subscriptionPrices = [];
|
||||
public array $versionTableState = [];
|
||||
public ?array $includedCustomers = null;
|
||||
|
||||
public function __construct(?economic_v2_versioning_service $versioning = null)
|
||||
{
|
||||
@@ -89,6 +90,16 @@ if (!class_exists('TestableEconomicV2DistributionService')) {
|
||||
return (float)($this->subscriptionPrices[$vehicle_type] ?? 0.0);
|
||||
}
|
||||
|
||||
protected function getProductDepartmentPrice(int $product_id, int $department_id): float
|
||||
{
|
||||
return 100.0;
|
||||
}
|
||||
|
||||
protected function resolveDiscountForProduct(int $customer_number, int $product_id, string $timestamp): ?array
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function buildCustomerEnvelope(int $customer_number, array $transaction_map): array
|
||||
{
|
||||
return [
|
||||
@@ -118,6 +129,15 @@ if (!class_exists('TestableEconomicV2DistributionService')) {
|
||||
return $department_id > 0 && $department_id !== 10;
|
||||
}
|
||||
|
||||
protected function shouldIncludeCustomerNumber(int $customer_number): bool
|
||||
{
|
||||
if ($this->includedCustomers === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array($customer_number, $this->includedCustomers, true);
|
||||
}
|
||||
|
||||
protected function parseDepartmentMap(array $department_map): array
|
||||
{
|
||||
$parsed = [];
|
||||
@@ -261,3 +281,116 @@ it('falls back to system orders for wash subscriptions when regular orders yield
|
||||
expect($result['collective_results']['total_subscription_price'])->toBe(299.0);
|
||||
expect($result['warnings'])->toContain('System order fallback used for wash subscriptions (department 10).');
|
||||
});
|
||||
|
||||
it('excludes orphaned customer traces from fixed pricing and customer price distributions', function (): void {
|
||||
$versioning = new FakeEconomicV2DistributionVersioningService();
|
||||
$versioning->fixedVersion = [
|
||||
'id' => 91,
|
||||
'price' => 499.95,
|
||||
'description' => 'Fixed pricing agreement',
|
||||
'source' => 'test.fixed',
|
||||
'confidence' => 1.0,
|
||||
'inferred' => false,
|
||||
'effective_from' => '2026-01-01 00:00:00',
|
||||
'effective_to' => null,
|
||||
];
|
||||
|
||||
$service = new TestableEconomicV2DistributionService($versioning);
|
||||
$service->includedCustomers = [12345];
|
||||
$service->stubOrders = [
|
||||
[
|
||||
'id' => 501,
|
||||
'customer_id' => 80000038,
|
||||
'department_id' => 1,
|
||||
'created_at' => '2026-01-15 10:00:00',
|
||||
'include_in_invoice' => 1,
|
||||
'reg_1' => '',
|
||||
'reference' => '',
|
||||
],
|
||||
[
|
||||
'id' => 502,
|
||||
'customer_id' => 12345,
|
||||
'department_id' => 1,
|
||||
'created_at' => '2026-01-16 10:00:00',
|
||||
'include_in_invoice' => 1,
|
||||
'reg_1' => '',
|
||||
'reference' => '',
|
||||
],
|
||||
];
|
||||
$service->stubOrderItems = [
|
||||
501 => [[
|
||||
'product_id' => 61,
|
||||
'price' => 250.0,
|
||||
'quantity' => 1,
|
||||
'reference' => '',
|
||||
]],
|
||||
502 => [[
|
||||
'product_id' => 61,
|
||||
'price' => 300.0,
|
||||
'quantity' => 1,
|
||||
'reference' => '',
|
||||
]],
|
||||
];
|
||||
|
||||
$fixedPricing = $service->getFixedPricingDistribution('2026-01-01', '2026-01-31');
|
||||
$customerPrices = $service->getCustomerPricesDistribution('2026-01-01', '2026-01-31');
|
||||
|
||||
expect(array_column($fixedPricing['customers'], 'customer_number'))->toBe([12345]);
|
||||
expect($fixedPricing['collective_results']['total_fixed_price'])->toBe(499.95);
|
||||
expect($fixedPricing['collective_results']['total_original_price'])->toBe(300.0);
|
||||
|
||||
expect(array_column($customerPrices['customers'], 'customer_number'))->toBe([12345]);
|
||||
expect($customerPrices['collective_results']['total_discount_amount'])->toBe(0.0);
|
||||
});
|
||||
|
||||
it('excludes orphaned customers from subscription fallback versions', function (): void {
|
||||
$versioning = new FakeEconomicV2DistributionVersioningService();
|
||||
|
||||
$service = new TestableEconomicV2DistributionService($versioning);
|
||||
$service->includedCustomers = [12345];
|
||||
$service->stubOrders = [[
|
||||
'id' => 701,
|
||||
'customer_id' => 12345,
|
||||
'department_id' => 4,
|
||||
'created_at' => '2026-01-15 10:00:00',
|
||||
'include_in_invoice' => 1,
|
||||
'reg_1' => 'AB12345',
|
||||
'reference' => '',
|
||||
]];
|
||||
$service->subscriptionPrices = [
|
||||
77 => 299.0,
|
||||
];
|
||||
$service->stubVersionRows = [
|
||||
[
|
||||
'id' => 41,
|
||||
'customer_number' => 80000038,
|
||||
'reg' => 'ZZ99999',
|
||||
'vehicle_type' => 77,
|
||||
'wash_subscription' => 1,
|
||||
'source' => 'test.version',
|
||||
'confidence' => 1.0,
|
||||
'inferred' => false,
|
||||
'effective_from' => '2026-01-01 00:00:00',
|
||||
'effective_to' => null,
|
||||
],
|
||||
[
|
||||
'id' => 42,
|
||||
'customer_number' => 12345,
|
||||
'reg' => 'AB12345',
|
||||
'vehicle_type' => 77,
|
||||
'wash_subscription' => 1,
|
||||
'source' => 'test.version',
|
||||
'confidence' => 1.0,
|
||||
'inferred' => false,
|
||||
'effective_from' => '2026-01-01 00:00:00',
|
||||
'effective_to' => null,
|
||||
],
|
||||
];
|
||||
|
||||
$result = $service->getWashSubscriptionsDistribution('2026-01-01', '2026-01-31');
|
||||
|
||||
expect(array_column($result['customers'], 'customer_number'))->toBe([12345]);
|
||||
expect($result['warnings'])->toBe([
|
||||
'Fallback allocation used for subscription AB12345 customer 12345 in 2026-01',
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user