Fix limited backoffice schema compatibility
This commit is contained in:
@@ -112,6 +112,11 @@ class limited_backoffice_service
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, bool>
|
||||||
|
*/
|
||||||
|
private array $columnExistsCache = [];
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
limited_backoffice_schema_bootstrap::ensureTables();
|
limited_backoffice_schema_bootstrap::ensureTables();
|
||||||
@@ -320,6 +325,10 @@ class limited_backoffice_service
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$userDeletedAtSelect = $this->tableHasColumn('users', 'deleted_at')
|
||||||
|
? 'u.`deleted_at` AS `user_deleted_at`'
|
||||||
|
: 'NULL AS `user_deleted_at`';
|
||||||
|
|
||||||
global $db;
|
global $db;
|
||||||
$rows = $db->fetch_all($db->query("
|
$rows = $db->fetch_all($db->query("
|
||||||
SELECT
|
SELECT
|
||||||
@@ -330,7 +339,7 @@ class limited_backoffice_service
|
|||||||
u.`phone_country_code`,
|
u.`phone_country_code`,
|
||||||
u.`phone`,
|
u.`phone`,
|
||||||
u.`group_id`,
|
u.`group_id`,
|
||||||
u.`deleted_at` AS `user_deleted_at`
|
{$userDeletedAtSelect}
|
||||||
FROM `limited_backoffice_employees` lbe
|
FROM `limited_backoffice_employees` lbe
|
||||||
INNER JOIN `users` u ON u.`id` = lbe.`user_id`
|
INNER JOIN `users` u ON u.`id` = lbe.`user_id`
|
||||||
ORDER BY u.`display_name` ASC, lbe.`user_id` ASC
|
ORDER BY u.`display_name` ASC, lbe.`user_id` ASC
|
||||||
@@ -497,13 +506,18 @@ class limited_backoffice_service
|
|||||||
if ($password !== null) {
|
if ($password !== null) {
|
||||||
$userUpdates['password'] = password_hash($password, PASSWORD_DEFAULT);
|
$userUpdates['password'] = password_hash($password, PASSWORD_DEFAULT);
|
||||||
}
|
}
|
||||||
|
$usersHaveDeletedAt = $this->tableHasColumn('users', 'deleted_at');
|
||||||
if ($active) {
|
if ($active) {
|
||||||
$userUpdates['group_id'] = $managedGroupId;
|
$userUpdates['group_id'] = $managedGroupId;
|
||||||
$userUpdates['deleted_at'] = null;
|
if ($usersHaveDeletedAt) {
|
||||||
|
$userUpdates['deleted_at'] = null;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$userUpdates['group_id'] = 0;
|
$userUpdates['group_id'] = 0;
|
||||||
$userUpdates['password'] = null;
|
$userUpdates['password'] = null;
|
||||||
$userUpdates['deleted_at'] = date('Y-m-d H:i:s');
|
if ($usersHaveDeletedAt) {
|
||||||
|
$userUpdates['deleted_at'] = date('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$this->updateUserFields($employeeId, $userUpdates);
|
$this->updateUserFields($employeeId, $userUpdates);
|
||||||
|
|
||||||
@@ -554,6 +568,28 @@ class limited_backoffice_service
|
|||||||
return $db->conn();
|
return $db->conn();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function tableHasColumn(string $table, string $column): bool
|
||||||
|
{
|
||||||
|
$cacheKey = $table . '.' . $column;
|
||||||
|
if (array_key_exists($cacheKey, $this->columnExistsCache)) {
|
||||||
|
return $this->columnExistsCache[$cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
global $db;
|
||||||
|
$tableSql = $this->escapeIdentifierLookup($table);
|
||||||
|
$columnSql = $this->escapeIdentifierLookup($column);
|
||||||
|
$result = $db->query("SHOW COLUMNS FROM `{$tableSql}` LIKE '{$columnSql}'");
|
||||||
|
$exists = $result !== false && is_object($result) && property_exists($result, 'num_rows') && (int)$result->num_rows > 0;
|
||||||
|
|
||||||
|
$this->columnExistsCache[$cacheKey] = $exists;
|
||||||
|
return $exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function escapeIdentifierLookup(string $value): string
|
||||||
|
{
|
||||||
|
return str_replace(['\\', "'", '`'], ['\\\\', "\\'", ''], $value);
|
||||||
|
}
|
||||||
|
|
||||||
private function assertDepartmentAccess(users_o $user, int $departmentId): void
|
private function assertDepartmentAccess(users_o $user, int $departmentId): void
|
||||||
{
|
{
|
||||||
if ($departmentId <= 0) {
|
if ($departmentId <= 0) {
|
||||||
@@ -605,6 +641,14 @@ class limited_backoffice_service
|
|||||||
{
|
{
|
||||||
global $db;
|
global $db;
|
||||||
|
|
||||||
|
$where = [
|
||||||
|
'dc.`department_id` = ?',
|
||||||
|
'dc.`deleted_at` IS NULL',
|
||||||
|
];
|
||||||
|
if ($this->tableHasColumn('products', 'deleted_at')) {
|
||||||
|
$where[] = 'p.`deleted_at` IS NULL';
|
||||||
|
}
|
||||||
|
|
||||||
$statement = $this->mysqli()->prepare(
|
$statement = $this->mysqli()->prepare(
|
||||||
'SELECT
|
'SELECT
|
||||||
c.`id` AS `category_id`,
|
c.`id` AS `category_id`,
|
||||||
@@ -621,9 +665,7 @@ class limited_backoffice_service
|
|||||||
LEFT JOIN `product_department_prices` pdp
|
LEFT JOIN `product_department_prices` pdp
|
||||||
ON pdp.`department_id` = dc.`department_id`
|
ON pdp.`department_id` = dc.`department_id`
|
||||||
AND pdp.`product_id` = p.`id`
|
AND pdp.`product_id` = p.`id`
|
||||||
WHERE dc.`department_id` = ?
|
WHERE ' . implode(' AND ', $where) . '
|
||||||
AND dc.`deleted_at` IS NULL
|
|
||||||
AND p.`deleted_at` IS NULL
|
|
||||||
ORDER BY c.`name` ASC, c.`id` ASC, p.`order_priority` ASC, p.`name` ASC, p.`id` ASC'
|
ORDER BY c.`name` ASC, c.`id` ASC, p.`order_priority` ASC, p.`name` ASC, p.`id` ASC'
|
||||||
);
|
);
|
||||||
if ($statement === false) {
|
if ($statement === false) {
|
||||||
@@ -986,6 +1028,10 @@ class limited_backoffice_service
|
|||||||
private function loadManagedEmployee(int $employeeId): ?array
|
private function loadManagedEmployee(int $employeeId): ?array
|
||||||
{
|
{
|
||||||
global $db;
|
global $db;
|
||||||
|
$userDeletedAtSelect = $this->tableHasColumn('users', 'deleted_at')
|
||||||
|
? 'u.`deleted_at` AS `user_deleted_at`'
|
||||||
|
: 'NULL AS `user_deleted_at`';
|
||||||
|
|
||||||
$statement = $this->mysqli()->prepare(
|
$statement = $this->mysqli()->prepare(
|
||||||
'SELECT
|
'SELECT
|
||||||
lbe.*,
|
lbe.*,
|
||||||
@@ -995,7 +1041,7 @@ class limited_backoffice_service
|
|||||||
u.`phone_country_code`,
|
u.`phone_country_code`,
|
||||||
u.`phone`,
|
u.`phone`,
|
||||||
u.`group_id`,
|
u.`group_id`,
|
||||||
u.`deleted_at` AS `user_deleted_at`
|
' . $userDeletedAtSelect . '
|
||||||
FROM `limited_backoffice_employees` lbe
|
FROM `limited_backoffice_employees` lbe
|
||||||
INNER JOIN `users` u ON u.`id` = lbe.`user_id`
|
INNER JOIN `users` u ON u.`id` = lbe.`user_id`
|
||||||
WHERE lbe.`user_id` = ?
|
WHERE lbe.`user_id` = ?
|
||||||
|
|||||||
@@ -63,11 +63,37 @@ function limited_backoffice_cleanup_created_employee(int $employeeId): void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function limited_backoffice_without_users_deleted_at(callable $callback): void
|
||||||
|
{
|
||||||
|
$db = api_test_runtime()->db();
|
||||||
|
$column = $db->query("SHOW COLUMNS FROM `users` LIKE 'deleted_at'");
|
||||||
|
if ($column === false) {
|
||||||
|
throw new RuntimeException('Unable to inspect users.deleted_at test column.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$hadColumn = (int)$column->num_rows > 0;
|
||||||
|
if ($hadColumn) {
|
||||||
|
$db->query('ALTER TABLE `users` DROP COLUMN `deleted_at`');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$callback();
|
||||||
|
} finally {
|
||||||
|
if ($hadColumn) {
|
||||||
|
$db->query('ALTER TABLE `users` ADD COLUMN `deleted_at` DATETIME NULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
it('lists and updates explicit prices only for assigned departments', function (): void {
|
it('lists and updates explicit prices only for assigned departments', function (): void {
|
||||||
api_test_covers('GET /limited-backoffice/departments', 'happy');
|
api_test_covers('GET /limited-backoffice/departments', 'happy');
|
||||||
api_test_covers('GET /limited-backoffice/departments/{departmentId}/prices', 'happy');
|
api_test_covers('GET /limited-backoffice/departments/{departmentId}/prices', 'happy');
|
||||||
api_test_covers('PUT /limited-backoffice/departments/{departmentId}/prices', 'happy');
|
api_test_covers('PUT /limited-backoffice/departments/{departmentId}/prices', 'happy');
|
||||||
|
|
||||||
|
$productDeletedAtColumn = api_test_runtime()->db()->query("SHOW COLUMNS FROM `products` LIKE 'deleted_at'");
|
||||||
|
expect($productDeletedAtColumn)->not->toBeFalse();
|
||||||
|
expect((int)$productDeletedAtColumn->num_rows)->toBe(0);
|
||||||
|
|
||||||
$department = api_fixtures()->createDepartment(['name' => 'Limited Prices Own']);
|
$department = api_fixtures()->createDepartment(['name' => 'Limited Prices Own']);
|
||||||
$otherDepartment = api_fixtures()->createDepartment(['name' => 'Limited Prices Other']);
|
$otherDepartment = api_fixtures()->createDepartment(['name' => 'Limited Prices Other']);
|
||||||
$category = api_fixtures()->createCategory(['name' => 'Limited Washes']);
|
$category = api_fixtures()->createCategory(['name' => 'Limited Washes']);
|
||||||
@@ -238,12 +264,17 @@ it('rejects invalid price batches and leaves existing prices unchanged', functio
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('creates updates lists and deactivates scoped employees without exposing raw permissions', function (): void {
|
it('creates updates lists and deactivates scoped employees without exposing raw permissions', function (): void {
|
||||||
|
limited_backoffice_without_users_deleted_at(function (): void {
|
||||||
api_test_covers('GET /limited-backoffice/roles', 'happy');
|
api_test_covers('GET /limited-backoffice/roles', 'happy');
|
||||||
api_test_covers('GET /limited-backoffice/employees', 'happy');
|
api_test_covers('GET /limited-backoffice/employees', 'happy');
|
||||||
api_test_covers('POST /limited-backoffice/employees', 'happy');
|
api_test_covers('POST /limited-backoffice/employees', 'happy');
|
||||||
api_test_covers('PUT /limited-backoffice/employees/{employeeId}', 'happy');
|
api_test_covers('PUT /limited-backoffice/employees/{employeeId}', 'happy');
|
||||||
api_test_covers('DELETE /limited-backoffice/employees/{employeeId}', 'happy');
|
api_test_covers('DELETE /limited-backoffice/employees/{employeeId}', 'happy');
|
||||||
|
|
||||||
|
$usersDeletedAtColumn = api_test_runtime()->db()->query("SHOW COLUMNS FROM `users` LIKE 'deleted_at'");
|
||||||
|
expect($usersDeletedAtColumn)->not->toBeFalse();
|
||||||
|
expect((int)$usersDeletedAtColumn->num_rows)->toBe(0);
|
||||||
|
|
||||||
$department = api_fixtures()->createDepartment(['name' => 'Limited Employee Department']);
|
$department = api_fixtures()->createDepartment(['name' => 'Limited Employee Department']);
|
||||||
$session = limited_backoffice_manager_session([(int)$department['id']]);
|
$session = limited_backoffice_manager_session([(int)$department['id']]);
|
||||||
|
|
||||||
@@ -313,12 +344,16 @@ it('creates updates lists and deactivates scoped employees without exposing raw
|
|||||||
->assertSuccess();
|
->assertSuccess();
|
||||||
|
|
||||||
expect($deactivated->data()['active'] ?? true)->toBeFalse();
|
expect($deactivated->data()['active'] ?? true)->toBeFalse();
|
||||||
$userRow = api_test_runtime()->queryOne('SELECT `password`, `group_id`, `deleted_at` FROM `users` WHERE `id` = ' . $employeeId);
|
$userRow = api_test_runtime()->queryOne('SELECT `password`, `group_id` FROM `users` WHERE `id` = ' . $employeeId);
|
||||||
expect($userRow)->not->toBeNull();
|
expect($userRow)->not->toBeNull();
|
||||||
expect(array_key_exists('password', $userRow ?? []))->toBeTrue();
|
expect(array_key_exists('password', $userRow ?? []))->toBeTrue();
|
||||||
expect($userRow['password'])->toBeNull();
|
expect($userRow['password'])->toBeNull();
|
||||||
expect((int)($userRow['group_id'] ?? -1))->toBe(0);
|
expect((int)($userRow['group_id'] ?? -1))->toBe(0);
|
||||||
expect($userRow['deleted_at'] ?? null)->not->toBeNull();
|
$employeeRow = api_test_runtime()->queryOne(
|
||||||
|
'SELECT `deactivated_at` FROM `limited_backoffice_employees` WHERE `user_id` = ' . $employeeId . ' LIMIT 1'
|
||||||
|
);
|
||||||
|
expect($employeeRow['deactivated_at'] ?? null)->not->toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects employee scopes roles raw permissions self edits superusers and shared groups', function (): void {
|
it('rejects employee scopes roles raw permissions self edits superusers and shared groups', function (): void {
|
||||||
|
|||||||
@@ -396,10 +396,8 @@ CREATE TABLE IF NOT EXISTS `products` (
|
|||||||
`max_quantity_per_order` INT NULL,
|
`max_quantity_per_order` INT NULL,
|
||||||
`created_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
`created_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
`updated_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
`updated_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
`deleted_at` DATETIME NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `idx_products_category` (`category`),
|
KEY `idx_products_category` (`category`)
|
||||||
KEY `idx_products_deleted_at` (`deleted_at`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||||
SQL,
|
SQL,
|
||||||
'department_categories' => <<<'SQL'
|
'department_categories' => <<<'SQL'
|
||||||
|
|||||||
Reference in New Issue
Block a user