markTestSkipped('Set RUN_INTEGRATION_TESTS=1 to run DB integration tests.'); } $host = getenv('CONFIG_DB_HOST') ?: null; $user = getenv('CONFIG_DB_USER') ?: null; $password = getenv('CONFIG_DB_PASSWORD') ?: ''; $database = getenv('CONFIG_DB_DATABASE') ?: null; if (!$host || !$user || !$database) { test()->markTestSkipped('Missing DB env vars: CONFIG_DB_HOST/CONFIG_DB_USER/CONFIG_DB_DATABASE.'); } app_require('classes/db.php'); app_require('classes/department_wash_count_service.php'); $db = new db([ 'host' => $host, 'user' => $user, 'password' => $password, 'database' => $database, 'port' => (int)(getenv('CONFIG_DB_PORT') ?: 3306), ]); $db->connect(); $GLOBALS['db'] = $db; department_wash_count_prepare_tables($db); return $db; } function department_wash_count_prepare_tables(db $db): void { $db->query( 'CREATE TABLE IF NOT EXISTS department_lanes ( id INT AUTO_INCREMENT PRIMARY KEY, department_id INT NULL, name VARCHAR(255) NULL, dynamic_image_id INT NULL, selfserve_enabled TINYINT(1) NOT NULL DEFAULT 0 )' ); $db->query( 'CREATE TABLE IF NOT EXISTS department_selfserve_conditions ( id INT AUTO_INCREMENT PRIMARY KEY, department_id INT NULL, name VARCHAR(255) NULL, product INT NULL, action VARCHAR(64) NULL, order_index INT NOT NULL DEFAULT 0, deleted_at DATETIME NULL )' ); $db->query( 'CREATE TABLE IF NOT EXISTS department_selfserve_tasks ( id INT AUTO_INCREMENT PRIMARY KEY, condition_id INT NULL, product INT NULL, description TEXT NULL, task VARCHAR(64) NULL, buttons TEXT NULL, order_index INT NOT NULL DEFAULT 0, deleted_at DATETIME NULL )' ); $db->query( 'CREATE TABLE IF NOT EXISTS products ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, description TEXT NULL, price INT NOT NULL DEFAULT 0, is_wash TINYINT(1) NOT NULL DEFAULT 0, created_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP )' ); $db->query( 'CREATE TABLE IF NOT EXISTS orders ( id INT AUTO_INCREMENT PRIMARY KEY, customer_id INT NOT NULL, cashier_id INT NOT NULL, reference VARCHAR(255) NULL, notes TEXT NULL, department_id INT NOT NULL, reg_1 VARCHAR(64) NULL, reg_2 VARCHAR(64) NULL, reg_3 VARCHAR(64) NULL, created_at DATETIME NOT NULL, include_in_invoice TINYINT(1) NULL, wash_id VARCHAR(255) NULL, deleted_at DATETIME NULL )' ); $db->query( 'CREATE TABLE IF NOT EXISTS order_items ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, product_id INT NOT NULL, reference VARCHAR(255) NULL, notes TEXT NULL, cashier_id INT NOT NULL, price DECIMAL(10,2) NOT NULL DEFAULT 0, quantity INT NOT NULL DEFAULT 1, deleted_at DATETIME NULL )' ); $db->query( 'CREATE TABLE IF NOT EXISTS selfserve_wash_sessions ( id INT AUTO_INCREMENT PRIMARY KEY, lane_id INT NOT NULL, department_id INT NOT NULL, reg VARCHAR(64) NOT NULL, status VARCHAR(64) NOT NULL, allowed TINYINT(1) NOT NULL DEFAULT 0, wash_started_at DATETIME NULL, machine_start_triggered_at DATETIME NULL, order_id INT NULL, completed_at DATETIME NULL, created_at DATETIME NULL, deleted_at DATETIME NULL )' ); } function department_wash_count_insert_product(db $db, string $suffix, bool $isWash): int { $productNameColumn = null; if ($db->num_rows($db->query("SHOW COLUMNS FROM products LIKE 'name'")) > 0) { $productNameColumn = 'name'; } elseif ($db->num_rows($db->query("SHOW COLUMNS FROM products LIKE 'title'")) > 0) { $productNameColumn = 'title'; } else { test()->markTestSkipped('Products table is missing both name and title columns required by this integration test.'); } $escapedName = $db->escape_string('Wash count product ' . $suffix . ' ' . ($isWash ? 'wash' : 'minute')); $descriptionSql = $db->num_rows($db->query("SHOW COLUMNS FROM products LIKE 'description'")) > 0 ? ', description' : ''; $descriptionValueSql = $descriptionSql !== '' ? ", 'Integration wash count product'" : ''; $db->query( "INSERT INTO products ($productNameColumn$descriptionSql, is_wash) VALUES ('$escapedName'$descriptionValueSql, " . ($isWash ? '1' : '0') . ')' ); return (int)$db->insert_id(); } function department_wash_count_insert_order(db $db, int $departmentId, int $productId, string $reference, string $createdAt, int $price = 100): int { $escapedReference = $db->escape_string($reference); $escapedCreatedAt = $db->escape_string($createdAt); $db->query( "INSERT INTO orders (customer_id, cashier_id, reference, notes, department_id, reg_1, reg_2, reg_3, created_at) VALUES (1, 1, '$escapedReference', 'Integration test', $departmentId, 'COUNT$departmentId', '', '', '$escapedCreatedAt')" ); $orderId = (int)$db->insert_id(); $db->query( "INSERT INTO order_items (order_id, product_id, reference, notes, cashier_id, price, quantity) VALUES ($orderId, $productId, '$escapedReference', 'Integration test', 1, $price, 1)" ); return $orderId; } function department_wash_count_insert_session( db $db, int $departmentId, string $reg, string $status, ?string $completedAt, ?int $orderId = null ): int { $escapedReg = $db->escape_string($reg); $escapedStatus = $db->escape_string($status); $completedAtSql = $completedAt === null ? 'NULL' : "'" . $db->escape_string($completedAt) . "'"; $orderIdSql = $orderId === null ? 'NULL' : (string)$orderId; $db->query( "INSERT INTO selfserve_wash_sessions (lane_id, department_id, reg, status, allowed, wash_started_at, machine_start_triggered_at, order_id, completed_at, created_at) VALUES (1, $departmentId, '$escapedReg', '$escapedStatus', 1, NULL, NULL, $orderIdSql, $completedAtSql, $completedAtSql)" ); return (int)$db->insert_id(); } it('counts completed self-serve minute billing sessions site-wide and dedupes linked wash orders', function (): void { $db = department_wash_count_integration_db(); $service = new department_wash_count_service(); $suffix = (string)random_int(10000, 99999); $departmentId = 700000 + (int)$suffix; $orderIds = []; $productIds = []; $sessionIds = []; try { $washProductId = department_wash_count_insert_product($db, $suffix, true); $minuteProductId = department_wash_count_insert_product($db, $suffix, false); $productIds = [$washProductId, $minuteProductId]; $orderIds[] = department_wash_count_insert_order($db, $departmentId, $washProductId, 'plain-wash-' . $suffix, '2026-04-01 10:15:00', 100); $linkedMinuteOrderId = department_wash_count_insert_order($db, $departmentId, $minuteProductId, 'linked-minute-' . $suffix, '2026-04-01 11:10:00', 25); $orderIds[] = $linkedMinuteOrderId; $linkedWashOrderId = department_wash_count_insert_order($db, $departmentId, $washProductId, 'linked-wash-' . $suffix, '2026-04-01 13:05:00', 100); $orderIds[] = $linkedWashOrderId; $orderIds[] = department_wash_count_insert_order($db, $departmentId, $minuteProductId, 'plain-minute-' . $suffix, '2026-04-01 14:00:00', 25); $sessionIds[] = department_wash_count_insert_session($db, $departmentId, 'LINKMIN' . $suffix, 'COMPLETED', '2026-04-01 12:00:00', $linkedMinuteOrderId); $sessionIds[] = department_wash_count_insert_session($db, $departmentId, 'STAND' . $suffix, 'COMPLETED', '2026-04-01 12:20:00'); $sessionIds[] = department_wash_count_insert_session($db, $departmentId, 'LINKWASH' . $suffix, 'COMPLETED', '2026-04-01 13:30:00', $linkedWashOrderId); $sessionIds[] = department_wash_count_insert_session($db, $departmentId, 'FORCE' . $suffix, 'FORCE_STOPPED', '2026-04-01 15:00:00'); $sessionIds[] = department_wash_count_insert_session($db, $departmentId, 'OPEN' . $suffix, 'COMPLETED', null); expect($service->countInDateRange('2026-04-01 00:00:00', '2026-04-01 23:59:59', $departmentId))->toBe(4); $hourRows = $service->countByHourForDepartments('2026-04-01 00:00:00', '2026-04-01 23:59:59', [$departmentId]); $countsByHour = []; foreach ($hourRows as $row) { $countsByHour[$row['hour_bucket']] = $row['wash_count']; } expect($countsByHour)->toMatchArray([ '2026-04-01 10:00:00' => 1, '2026-04-01 11:00:00' => 1, '2026-04-01 12:00:00' => 1, '2026-04-01 13:00:00' => 1, ]); expect($countsByHour)->not->toHaveKey('2026-04-01 14:00:00') ->and($countsByHour)->not->toHaveKey('2026-04-01 15:00:00'); $summary = $service->transactionSummary('2026-04-01 00:00:00', '2026-04-01 23:59:59', [$departmentId]); expect($summary)->toMatchArray([ 'quantity' => 4, 'products' => 4, 'earnings' => 250, 'washes' => 4, ]); } finally { if ($sessionIds !== []) { $db->query('DELETE FROM selfserve_wash_sessions WHERE id IN (' . implode(',', array_map('intval', $sessionIds)) . ')'); } if ($orderIds !== []) { $orderIdsSql = implode(',', array_map('intval', $orderIds)); $db->query("DELETE FROM order_items WHERE order_id IN ($orderIdsSql)"); $db->query("DELETE FROM orders WHERE id IN ($orderIdsSql)"); } if ($productIds !== []) { $db->query('DELETE FROM products WHERE id IN (' . implode(',', array_map('intval', $productIds)) . ')'); } $db->close(); } });