39443-vm/backend/src/db/seeders/20260403090000-align-business-role-matrix.js
2026-04-04 05:14:20 +00:00

247 lines
7.0 KiB
JavaScript

'use strict';
const { v4: uuid } = require('uuid');
const { QueryTypes } = require('sequelize');
const BUSINESS_ROLES = {
SUPER_ADMIN: 'Super Administrator',
ADMIN: 'Administrator',
CONCIERGE: 'Concierge Coordinator',
CUSTOMER: 'Customer',
PLATFORM_OWNER: 'Platform Owner',
OPERATIONS_DIRECTOR: 'Operations Director',
RESERVATIONS_LEAD: 'Reservations Lead',
FINANCE_CONTROLLER: 'Finance Controller',
};
const rolePermissionMatrix = {
[BUSINESS_ROLES.ADMIN]: [
'CREATE_BOOKING_REQUESTS',
'READ_BOOKING_REQUESTS',
'UPDATE_BOOKING_REQUESTS',
'READ_APPROVAL_STEPS',
'UPDATE_APPROVAL_STEPS',
'CREATE_RESERVATIONS',
'READ_RESERVATIONS',
'UPDATE_RESERVATIONS',
'CREATE_SERVICE_REQUESTS',
'READ_SERVICE_REQUESTS',
'UPDATE_SERVICE_REQUESTS',
'READ_INVOICES',
'CREATE_DOCUMENTS',
'READ_DOCUMENTS',
'UPDATE_DOCUMENTS',
'READ_ORGANIZATIONS',
'READ_TENANTS',
'READ_PROPERTIES',
'READ_UNITS',
'READ_NEGOTIATED_RATES',
],
[BUSINESS_ROLES.CONCIERGE]: [
'CREATE_BOOKING_REQUESTS',
'READ_BOOKING_REQUESTS',
'UPDATE_BOOKING_REQUESTS',
'READ_RESERVATIONS',
'CREATE_SERVICE_REQUESTS',
'READ_SERVICE_REQUESTS',
'UPDATE_SERVICE_REQUESTS',
'CREATE_DOCUMENTS',
'READ_DOCUMENTS',
'UPDATE_DOCUMENTS',
'READ_PROPERTIES',
'READ_UNITS',
],
[BUSINESS_ROLES.CUSTOMER]: [
'CREATE_BOOKING_REQUESTS',
'READ_BOOKING_REQUESTS',
'UPDATE_BOOKING_REQUESTS',
'READ_RESERVATIONS',
'CREATE_SERVICE_REQUESTS',
'READ_SERVICE_REQUESTS',
'UPDATE_SERVICE_REQUESTS',
'READ_DOCUMENTS',
],
[BUSINESS_ROLES.PLATFORM_OWNER]: [
'CREATE_BOOKING_REQUESTS',
'READ_BOOKING_REQUESTS',
'UPDATE_BOOKING_REQUESTS',
'READ_APPROVAL_STEPS',
'UPDATE_APPROVAL_STEPS',
'CREATE_RESERVATIONS',
'READ_RESERVATIONS',
'UPDATE_RESERVATIONS',
'CREATE_SERVICE_REQUESTS',
'READ_SERVICE_REQUESTS',
'UPDATE_SERVICE_REQUESTS',
'READ_INVOICES',
'CREATE_DOCUMENTS',
'READ_DOCUMENTS',
'UPDATE_DOCUMENTS',
'READ_ORGANIZATIONS',
'READ_PROPERTIES',
'READ_UNITS',
'READ_NEGOTIATED_RATES',
],
[BUSINESS_ROLES.OPERATIONS_DIRECTOR]: [
'CREATE_BOOKING_REQUESTS',
'READ_BOOKING_REQUESTS',
'UPDATE_BOOKING_REQUESTS',
'READ_APPROVAL_STEPS',
'UPDATE_APPROVAL_STEPS',
'CREATE_RESERVATIONS',
'READ_RESERVATIONS',
'UPDATE_RESERVATIONS',
'CREATE_SERVICE_REQUESTS',
'READ_SERVICE_REQUESTS',
'UPDATE_SERVICE_REQUESTS',
'CREATE_DOCUMENTS',
'READ_DOCUMENTS',
'UPDATE_DOCUMENTS',
'READ_PROPERTIES',
'READ_UNITS',
'READ_NEGOTIATED_RATES',
],
[BUSINESS_ROLES.RESERVATIONS_LEAD]: [
'CREATE_BOOKING_REQUESTS',
'READ_BOOKING_REQUESTS',
'UPDATE_BOOKING_REQUESTS',
'READ_APPROVAL_STEPS',
'UPDATE_APPROVAL_STEPS',
'CREATE_RESERVATIONS',
'READ_RESERVATIONS',
'UPDATE_RESERVATIONS',
'CREATE_SERVICE_REQUESTS',
'READ_SERVICE_REQUESTS',
'UPDATE_SERVICE_REQUESTS',
'READ_DOCUMENTS',
'READ_PROPERTIES',
'READ_UNITS',
'READ_NEGOTIATED_RATES',
],
[BUSINESS_ROLES.FINANCE_CONTROLLER]: [
'READ_BOOKING_REQUESTS',
'READ_RESERVATIONS',
'READ_INVOICES',
'UPDATE_INVOICES',
'READ_DOCUMENTS',
],
};
async function findRoleByName(queryInterface, name) {
const rows = await queryInterface.sequelize.query(
'SELECT "id", "name" FROM "roles" WHERE "name" = :name LIMIT 1',
{
replacements: { name },
type: QueryTypes.SELECT,
},
);
return rows[0] || null;
}
async function ensureRole(queryInterface, name, now) {
const existingRole = await findRoleByName(queryInterface, name);
if (existingRole) {
return existingRole.id;
}
const id = uuid();
await queryInterface.bulkInsert('roles', [
{
id,
name,
globalAccess: false,
createdAt: now,
updatedAt: now,
},
]);
return id;
}
async function getPermissionIdMap(queryInterface, permissionNames) {
const permissions = await queryInterface.sequelize.query(
'SELECT "id", "name" FROM "permissions" WHERE "name" IN (:permissionNames)',
{
replacements: { permissionNames },
type: QueryTypes.SELECT,
},
);
return new Map(permissions.map((permission) => [permission.name, permission.id]));
}
module.exports = {
async up(queryInterface) {
const now = new Date();
const customerRoleId = await ensureRole(queryInterface, BUSINESS_ROLES.CUSTOMER, now);
await queryInterface.bulkUpdate('roles', { globalAccess: true, updatedAt: now }, { name: BUSINESS_ROLES.SUPER_ADMIN });
await queryInterface.sequelize.query(
'UPDATE "roles" SET "globalAccess" = false, "updatedAt" = :updatedAt WHERE "name" IN (:roleNames)',
{
replacements: {
updatedAt: now,
roleNames: [
BUSINESS_ROLES.ADMIN,
BUSINESS_ROLES.CONCIERGE,
BUSINESS_ROLES.CUSTOMER,
BUSINESS_ROLES.PLATFORM_OWNER,
BUSINESS_ROLES.OPERATIONS_DIRECTOR,
BUSINESS_ROLES.RESERVATIONS_LEAD,
BUSINESS_ROLES.FINANCE_CONTROLLER,
],
},
},
);
const roleIds = {};
for (const roleName of Object.keys(rolePermissionMatrix)) {
const role = await findRoleByName(queryInterface, roleName);
if (!role) {
throw new Error(`Role '${roleName}' was not found while aligning the business role matrix.`);
}
roleIds[roleName] = role.id;
}
roleIds[BUSINESS_ROLES.CUSTOMER] = customerRoleId;
const permissionNames = [...new Set(Object.values(rolePermissionMatrix).flat())];
const permissionIdMap = await getPermissionIdMap(queryInterface, permissionNames);
const missingPermissions = permissionNames.filter((permissionName) => !permissionIdMap.get(permissionName));
if (missingPermissions.length > 0) {
throw new Error(`Missing permissions for role matrix alignment: ${missingPermissions.join(', ')}`);
}
const impactedRoleIds = Object.values(roleIds);
await queryInterface.sequelize.query(
'DELETE FROM "rolesPermissionsPermissions" WHERE "roles_permissionsId" IN (:roleIds)',
{
replacements: { roleIds: impactedRoleIds },
},
);
const rows = Object.entries(rolePermissionMatrix).flatMap(([roleName, permissions]) =>
permissions.map((permissionName) => ({
createdAt: now,
updatedAt: now,
roles_permissionsId: roleIds[roleName],
permissionId: permissionIdMap.get(permissionName),
})),
);
if (rows.length > 0) {
await queryInterface.bulkInsert('rolesPermissionsPermissions', rows);
}
await queryInterface.bulkUpdate('users', { app_roleId: customerRoleId, updatedAt: now }, { email: 'client@hello.com' });
await queryInterface.bulkUpdate('users', { app_roleId: roleIds[BUSINESS_ROLES.CONCIERGE], updatedAt: now }, { email: 'john@doe.com' });
},
async down() {
// Intentionally left blank. This seeder aligns live business roles and should not blindly revert production data.
},
};