feat: pool management
This commit is contained in:
parent
c62d775e66
commit
8aebdbb607
57
controller/pool/PoolController.js
Normal file
57
controller/pool/PoolController.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
const { createPool, listPools, updatePoolState } = require('../../services/pool/PoolService');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async create(req, res) {
|
||||||
|
try {
|
||||||
|
const { name, description, state } = req.body || {};
|
||||||
|
const actorUserId = req.user && req.user.userId;
|
||||||
|
if (!name) return res.status(400).json({ success: false, message: 'name is required' });
|
||||||
|
if (state && !['active', 'inactive'].includes(state)) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Invalid state. Allowed: active, inactive' });
|
||||||
|
}
|
||||||
|
const pool = await createPool({ name, description, state, actorUserId });
|
||||||
|
return res.status(201).json({ success: true, data: pool });
|
||||||
|
} catch (e) {
|
||||||
|
if (e && (e.code === 'ER_DUP_ENTRY' || e.errno === 1062)) {
|
||||||
|
return res.status(409).json({ success: false, message: 'Pool name already exists' });
|
||||||
|
}
|
||||||
|
if (e && e.status === 400) {
|
||||||
|
return res.status(400).json({ success: false, message: e.message });
|
||||||
|
}
|
||||||
|
console.error('[PoolController.create]', e);
|
||||||
|
return res.status(500).json({ success: false, message: 'Internal server error' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async list(req, res) {
|
||||||
|
try {
|
||||||
|
console.debug('[PoolController.list] GET /api/admin/pools');
|
||||||
|
const pools = await listPools();
|
||||||
|
return res.status(200).json({ success: true, data: pools });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[PoolController.list] error', e);
|
||||||
|
return res.status(500).json({ success: false, message: 'Internal server error' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// NEW: optional state update handler (route can be added later)
|
||||||
|
async updateState(req, res) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params || {};
|
||||||
|
const { state } = req.body || {};
|
||||||
|
const actorUserId = req.user && req.user.userId;
|
||||||
|
if (!id) return res.status(400).json({ success: false, message: 'id is required' });
|
||||||
|
if (!['active', 'inactive', 'archived'].includes(state)) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Invalid state. Allowed: active, inactive, archived' });
|
||||||
|
}
|
||||||
|
const updated = await updatePoolState(id, state, actorUserId);
|
||||||
|
return res.status(200).json({ success: true, data: updated });
|
||||||
|
} catch (e) {
|
||||||
|
if (e && e.status === 400) {
|
||||||
|
return res.status(400).json({ success: false, message: e.message });
|
||||||
|
}
|
||||||
|
console.error('[PoolController.updateState]', e);
|
||||||
|
return res.status(500).json({ success: false, message: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -620,6 +620,61 @@ async function createDatabase() {
|
|||||||
`);
|
`);
|
||||||
console.log('✅ User matrix metadata table created/verified');
|
console.log('✅ User matrix metadata table created/verified');
|
||||||
|
|
||||||
|
// --- Pools (NEW) ---
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS pools (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT NULL,
|
||||||
|
state ENUM('active','inactive','archived') DEFAULT 'active',
|
||||||
|
created_by INT NOT NULL,
|
||||||
|
updated_by INT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uq_pool_name (name),
|
||||||
|
INDEX idx_pool_name (name),
|
||||||
|
INDEX idx_pool_state (state),
|
||||||
|
INDEX idx_pool_created_by (created_by),
|
||||||
|
INDEX idx_pool_updated_by (updated_by),
|
||||||
|
CONSTRAINT fk_pools_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_pools_updated_by FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
console.log('✅ Pools table created/verified');
|
||||||
|
|
||||||
|
// Migrations for existing databases: add 'archived' and actor columns if missing
|
||||||
|
try {
|
||||||
|
await connection.query(`
|
||||||
|
ALTER TABLE pools
|
||||||
|
MODIFY COLUMN state ENUM('active','inactive','archived') DEFAULT 'active'
|
||||||
|
`);
|
||||||
|
console.log('🆙 Pools.state updated to include archived');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('ℹ️ Pools.state alteration skipped:', e.message);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await connection.query(`ALTER TABLE pools ADD COLUMN created_by INT NOT NULL`);
|
||||||
|
await connection.query(`CREATE INDEX idx_pool_created_by ON pools (created_by)`);
|
||||||
|
await connection.query(`
|
||||||
|
ALTER TABLE pools ADD CONSTRAINT fk_pools_created_by
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
`);
|
||||||
|
console.log('🆕 Pools.created_by added with FK and index');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('ℹ️ Pools.created_by add skipped:', e.message);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await connection.query(`ALTER TABLE pools ADD COLUMN updated_by INT NULL`);
|
||||||
|
await connection.query(`CREATE INDEX idx_pool_updated_by ON pools (updated_by)`);
|
||||||
|
await connection.query(`
|
||||||
|
ALTER TABLE pools ADD CONSTRAINT fk_pools_updated_by
|
||||||
|
FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
`);
|
||||||
|
console.log('🆕 Pools.updated_by added with FK and index');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('ℹ️ Pools.updated_by add skipped:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('🎉 Normalized database schema created/updated successfully!');
|
console.log('🎉 Normalized database schema created/updated successfully!');
|
||||||
|
|
||||||
await connection.end();
|
await connection.end();
|
||||||
|
|||||||
14
models/Pool.js
Normal file
14
models/Pool.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
class Pool {
|
||||||
|
constructor({ id = null, name, description = null, state = 'active', created_by = null, updated_by = null, created_at = null, updated_at = null }) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.description = description;
|
||||||
|
this.state = state;
|
||||||
|
this.created_by = created_by;
|
||||||
|
this.updated_by = updated_by;
|
||||||
|
this.created_at = created_at;
|
||||||
|
this.updated_at = updated_at;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Pool;
|
||||||
66
repositories/pool/poolRepository.js
Normal file
66
repositories/pool/poolRepository.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
const Pool = require('../../models/Pool');
|
||||||
|
|
||||||
|
class PoolRepository {
|
||||||
|
constructor(uow) {
|
||||||
|
this.uow = uow;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create({ name, description = null, state = 'active', created_by }) {
|
||||||
|
const conn = this.uow.connection;
|
||||||
|
const [res] = await conn.execute(
|
||||||
|
`INSERT INTO pools (name, description, state, created_by, updated_by)
|
||||||
|
VALUES (?, ?, ?, ?, NULL)`,
|
||||||
|
[name, description, state, created_by]
|
||||||
|
);
|
||||||
|
return new Pool({ id: res.insertId, name, description, state, created_by, updated_by: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll() {
|
||||||
|
const conn = this.uow.connection; // switched to connection
|
||||||
|
try {
|
||||||
|
console.debug('[PoolRepository.findAll] querying pools');
|
||||||
|
const [rows] = await conn.execute(
|
||||||
|
`SELECT id, name, description, state, created_at, updated_at
|
||||||
|
FROM pools
|
||||||
|
ORDER BY created_at DESC`
|
||||||
|
);
|
||||||
|
console.debug('[PoolRepository.findAll] rows fetched', { count: rows.length });
|
||||||
|
return rows.map(r => new Pool(r));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[PoolRepository.findAll] query failed', err);
|
||||||
|
// Surface a consistent error up the stack
|
||||||
|
const e = new Error('Failed to fetch pools');
|
||||||
|
e.status = 500;
|
||||||
|
e.cause = err;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: update state with enforced transitions (active <-> inactive -> archived also supported)
|
||||||
|
async updateState(id, nextState, updated_by) {
|
||||||
|
const conn = this.uow.connection;
|
||||||
|
const [rows] = await conn.execute(`SELECT id, state FROM pools WHERE id = ?`, [id]);
|
||||||
|
if (!rows || rows.length === 0) {
|
||||||
|
const err = new Error('Pool not found');
|
||||||
|
err.status = 404;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const current = rows[0].state;
|
||||||
|
const allowed = ['active', 'inactive', 'archived'];
|
||||||
|
if (!allowed.includes(current) || !allowed.includes(nextState)) {
|
||||||
|
const err = new Error('Invalid state transition');
|
||||||
|
err.status = 400;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
if (current === nextState) {
|
||||||
|
return new Pool({ id, state: current, updated_by }); // no-op
|
||||||
|
}
|
||||||
|
await conn.execute(
|
||||||
|
`UPDATE pools SET state = ?, updated_by = ?, updated_at = NOW() WHERE id = ?`,
|
||||||
|
[nextState, updated_by, id]
|
||||||
|
);
|
||||||
|
return new Pool({ id, state: nextState, updated_by });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PoolRepository;
|
||||||
@ -16,6 +16,7 @@ const UserStatusController = require('../controller/auth/UserStatusController');
|
|||||||
const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added
|
const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added
|
||||||
const MatrixController = require('../controller/matrix/MatrixController'); // <-- added
|
const MatrixController = require('../controller/matrix/MatrixController'); // <-- added
|
||||||
const CoffeeController = require('../controller/admin/CoffeeController');
|
const CoffeeController = require('../controller/admin/CoffeeController');
|
||||||
|
const PoolController = require('../controller/pool/PoolController');
|
||||||
|
|
||||||
// small helpers copied from original files
|
// small helpers copied from original files
|
||||||
function adminOnly(req, res, next) {
|
function adminOnly(req, res, next) {
|
||||||
@ -118,5 +119,8 @@ router.get('/admin/matrix/user-candidates', authMiddleware, adminOnly, MatrixCon
|
|||||||
// NEW: Matrix POST (admin)
|
// NEW: Matrix POST (admin)
|
||||||
router.post('/admin/matrix/add-user', authMiddleware, adminOnly, MatrixController.addUser);
|
router.post('/admin/matrix/add-user', authMiddleware, adminOnly, MatrixController.addUser);
|
||||||
|
|
||||||
|
// NEW: Admin list pools
|
||||||
|
router.get('/admin/pools', authMiddleware, adminOnly, PoolController.list);
|
||||||
|
|
||||||
// export
|
// export
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@ -7,6 +7,7 @@ const CompanyStampController = require('../controller/companyStamp/CompanyStampC
|
|||||||
const CoffeeController = require('../controller/admin/CoffeeController');
|
const CoffeeController = require('../controller/admin/CoffeeController');
|
||||||
const AdminUserController = require('../controller/admin/AdminUserController');
|
const AdminUserController = require('../controller/admin/AdminUserController');
|
||||||
const PersonalProfileController = require('../controller/profile/PersonalProfileController'); // <-- add
|
const PersonalProfileController = require('../controller/profile/PersonalProfileController'); // <-- add
|
||||||
|
const PoolController = require('../controller/pool/PoolController'); // <-- new
|
||||||
|
|
||||||
// Helper middlewares for company-stamp
|
// Helper middlewares for company-stamp
|
||||||
function adminOnly(req, res, next) {
|
function adminOnly(req, res, next) {
|
||||||
@ -36,6 +37,8 @@ router.patch('/admin/update-user-profile/:id', authMiddleware, adminOnly, AdminU
|
|||||||
router.patch('/admin/update-user-status/:id', authMiddleware, adminOnly, AdminUserController.updateUserStatus);
|
router.patch('/admin/update-user-status/:id', authMiddleware, adminOnly, AdminUserController.updateUserStatus);
|
||||||
// Admin: set state for coffee product
|
// Admin: set state for coffee product
|
||||||
router.patch('/admin/coffee/:id/state', authMiddleware, adminOnly, CoffeeController.setState);
|
router.patch('/admin/coffee/:id/state', authMiddleware, adminOnly, CoffeeController.setState);
|
||||||
|
// NEW: Admin pool state update
|
||||||
|
router.patch('/admin/pools/:id/state', authMiddleware, adminOnly, PoolController.updateState);
|
||||||
|
|
||||||
// Personal profile (self-service) - no admin guard
|
// Personal profile (self-service) - no admin guard
|
||||||
router.patch('/profile/personal/basic', authMiddleware, PersonalProfileController.updateBasic);
|
router.patch('/profile/personal/basic', authMiddleware, PersonalProfileController.updateBasic);
|
||||||
|
|||||||
@ -22,6 +22,7 @@ const CompanyProfileController = require('../controller/profile/CompanyProfileCo
|
|||||||
const AdminUserController = require('../controller/admin/AdminUserController');
|
const AdminUserController = require('../controller/admin/AdminUserController');
|
||||||
const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added
|
const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added
|
||||||
const MatrixController = require('../controller/matrix/MatrixController'); // Matrix admin operations
|
const MatrixController = require('../controller/matrix/MatrixController'); // Matrix admin operations
|
||||||
|
const PoolController = require('../controller/pool/PoolController');
|
||||||
|
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
const upload = multer({ storage: multer.memoryStorage() });
|
const upload = multer({ storage: multer.memoryStorage() });
|
||||||
@ -121,6 +122,8 @@ router.post('/company-stamps', authMiddleware, adminOnly, forceCompanyForAdmin,
|
|||||||
router.post('/admin/coffee', authMiddleware, adminOnly, upload.single('picture'), CoffeeController.create);
|
router.post('/admin/coffee', authMiddleware, adminOnly, upload.single('picture'), CoffeeController.create);
|
||||||
// NEW: add user into matrix
|
// NEW: add user into matrix
|
||||||
router.post('/admin/matrix/add-user', authMiddleware, adminOnly, MatrixController.addUser); // already added
|
router.post('/admin/matrix/add-user', authMiddleware, adminOnly, MatrixController.addUser); // already added
|
||||||
|
// NEW: Admin create pool
|
||||||
|
router.post('/admin/pools', authMiddleware, adminOnly, PoolController.create);
|
||||||
|
|
||||||
// Existing registration handlers (keep)
|
// Existing registration handlers (keep)
|
||||||
router.post('/register/personal', (req, res) => {
|
router.post('/register/personal', (req, res) => {
|
||||||
|
|||||||
70
services/pool/PoolService.js
Normal file
70
services/pool/PoolService.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
const UnitOfWork = require('../../database/UnitOfWork');
|
||||||
|
const PoolRepository = require('../../repositories/pool/poolRepository');
|
||||||
|
|
||||||
|
function isValidState(state) {
|
||||||
|
return state === undefined || state === null || state === 'active' || state === 'inactive' || state === 'archived';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createPool({ name, description = null, state = 'active', actorUserId }) {
|
||||||
|
if (!isValidState(state)) {
|
||||||
|
const err = new Error('Invalid state. Allowed: active, inactive, archived');
|
||||||
|
err.status = 400;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const uow = new UnitOfWork();
|
||||||
|
try {
|
||||||
|
console.debug('[PoolService.createPool] start', { name, state });
|
||||||
|
await uow.start();
|
||||||
|
const repo = new PoolRepository(uow);
|
||||||
|
const pool = await repo.create({ name, description, state, created_by: actorUserId });
|
||||||
|
await uow.commit();
|
||||||
|
console.debug('[PoolService.createPool] success', { id: pool.id });
|
||||||
|
return pool;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[PoolService.createPool] error', err);
|
||||||
|
try { await uow.rollback(); } catch (_) { console.warn('[PoolService.createPool] rollback failed'); }
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listPools() {
|
||||||
|
const uow = new UnitOfWork();
|
||||||
|
try {
|
||||||
|
console.debug('[PoolService.listPools] start');
|
||||||
|
// Ensure connection exists even for read-only
|
||||||
|
await uow.start();
|
||||||
|
const repo = new PoolRepository(uow);
|
||||||
|
const pools = await repo.findAll();
|
||||||
|
await uow.commit(); // harmless commit; ensures proper cleanup if UnitOfWork requires it
|
||||||
|
console.debug('[PoolService.listPools] success', { count: pools.length });
|
||||||
|
return pools;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[PoolService.listPools] error', err);
|
||||||
|
try { await uow.rollback(); } catch (_) { console.warn('[PoolService.listPools] rollback failed'); }
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePoolState(id, nextState, actorUserId) {
|
||||||
|
if (!isValidState(nextState) || !['active', 'inactive', 'archived'].includes(nextState)) {
|
||||||
|
const err = new Error('Invalid state. Allowed: active, inactive, archived');
|
||||||
|
err.status = 400;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const uow = new UnitOfWork();
|
||||||
|
try {
|
||||||
|
console.debug('[PoolService.updatePoolState] start', { id, nextState });
|
||||||
|
await uow.start();
|
||||||
|
const repo = new PoolRepository(uow);
|
||||||
|
const updated = await repo.updateState(id, nextState, actorUserId);
|
||||||
|
await uow.commit();
|
||||||
|
console.debug('[PoolService.updatePoolState] success', { id, state: nextState });
|
||||||
|
return updated;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[PoolService.updatePoolState] error', err);
|
||||||
|
try { await uow.rollback(); } catch (_) { console.warn('[PoolService.updatePoolState] rollback failed'); }
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { createPool, listPools, updatePoolState };
|
||||||
Loading…
Reference in New Issue
Block a user