diff --git a/controller/pool/PoolController.js b/controller/pool/PoolController.js new file mode 100644 index 0000000..247e085 --- /dev/null +++ b/controller/pool/PoolController.js @@ -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' }); + } + } +}; \ No newline at end of file diff --git a/database/createDb.js b/database/createDb.js index ed911d8..f987589 100644 --- a/database/createDb.js +++ b/database/createDb.js @@ -620,6 +620,61 @@ async function createDatabase() { `); 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!'); await connection.end(); diff --git a/models/Pool.js b/models/Pool.js new file mode 100644 index 0000000..891fa98 --- /dev/null +++ b/models/Pool.js @@ -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; \ No newline at end of file diff --git a/repositories/pool/poolRepository.js b/repositories/pool/poolRepository.js new file mode 100644 index 0000000..cc28251 --- /dev/null +++ b/repositories/pool/poolRepository.js @@ -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; \ No newline at end of file diff --git a/routes/getRoutes.js b/routes/getRoutes.js index e750760..dbdba8d 100644 --- a/routes/getRoutes.js +++ b/routes/getRoutes.js @@ -16,6 +16,7 @@ const UserStatusController = require('../controller/auth/UserStatusController'); const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added const MatrixController = require('../controller/matrix/MatrixController'); // <-- added const CoffeeController = require('../controller/admin/CoffeeController'); +const PoolController = require('../controller/pool/PoolController'); // small helpers copied from original files function adminOnly(req, res, next) { @@ -118,5 +119,8 @@ router.get('/admin/matrix/user-candidates', authMiddleware, adminOnly, MatrixCon // NEW: Matrix POST (admin) router.post('/admin/matrix/add-user', authMiddleware, adminOnly, MatrixController.addUser); +// NEW: Admin list pools +router.get('/admin/pools', authMiddleware, adminOnly, PoolController.list); + // export module.exports = router; \ No newline at end of file diff --git a/routes/patchRoutes.js b/routes/patchRoutes.js index 78f9a85..b145360 100644 --- a/routes/patchRoutes.js +++ b/routes/patchRoutes.js @@ -7,6 +7,7 @@ const CompanyStampController = require('../controller/companyStamp/CompanyStampC const CoffeeController = require('../controller/admin/CoffeeController'); const AdminUserController = require('../controller/admin/AdminUserController'); const PersonalProfileController = require('../controller/profile/PersonalProfileController'); // <-- add +const PoolController = require('../controller/pool/PoolController'); // <-- new // Helper middlewares for company-stamp 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); // Admin: set state for coffee product 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 router.patch('/profile/personal/basic', authMiddleware, PersonalProfileController.updateBasic); diff --git a/routes/postRoutes.js b/routes/postRoutes.js index 88668a0..60ab59b 100644 --- a/routes/postRoutes.js +++ b/routes/postRoutes.js @@ -22,6 +22,7 @@ const CompanyProfileController = require('../controller/profile/CompanyProfileCo const AdminUserController = require('../controller/admin/AdminUserController'); const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added const MatrixController = require('../controller/matrix/MatrixController'); // Matrix admin operations +const PoolController = require('../controller/pool/PoolController'); const multer = require('multer'); 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); // NEW: add user into matrix 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) router.post('/register/personal', (req, res) => { diff --git a/services/pool/PoolService.js b/services/pool/PoolService.js new file mode 100644 index 0000000..1bf8a74 --- /dev/null +++ b/services/pool/PoolService.js @@ -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 }; \ No newline at end of file