feat: implement Coffee management functionality with CRUD operations and S3 integration

This commit is contained in:
seaznCode 2025-11-13 20:13:16 +01:00
parent 0bc0bd087f
commit 77e34af8e2
9 changed files with 482 additions and 0 deletions

View File

@ -0,0 +1,225 @@
const { S3Client, PutObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
const CoffeeService = require('../../services/subscriptions/CoffeeService');
const { logger } = require('../../middleware/logger');
function buildPictureUrlFromKey(key) {
const endpoint = process.env.EXOSCALE_ENDPOINT || '';
const bucket = process.env.EXOSCALE_BUCKET || '';
// If using S3-compatible endpoint with virtual-hosted-style, construct URL accordingly
if (endpoint.startsWith('http')) {
return `${endpoint.replace(/\/$/, '')}/${bucket}/${key}`;
}
return key; // fallback: store key only
}
exports.list = async (req, res) => {
const rows = await CoffeeService.list();
const items = (rows || []).map(r => ({
...r,
pictureUrl: r.object_storage_id ? buildPictureUrlFromKey(r.object_storage_id) : ''
}));
res.json(items);
};
exports.create = async (req, res) => {
try {
const { title, description, quantity, price } = req.body;
const currency = req.body.currency || 'EUR';
const tax_rate = req.body.tax_rate !== undefined ? Number(req.body.tax_rate) : null;
const is_featured = req.body.is_featured === 'true' || req.body.is_featured === true ? true : false;
const billing_interval = req.body.billing_interval || null; // 'day'|'week'|'month'|'year'
const interval_count = req.body.interval_count !== undefined ? Number(req.body.interval_count) : null; // supports 6 months
const sku = req.body.sku || null;
const slug = req.body.slug || null;
const state = req.body.state === 'false' || req.body.state === false ? false : true; // default available
// If file uploaded, push to Exoscale and set object_storage_id
let object_storage_id = null;
let original_filename = null;
let uploadedKey = null;
if (req.file) {
const s3 = new S3Client({
region: process.env.EXOSCALE_REGION,
endpoint: process.env.EXOSCALE_ENDPOINT,
credentials: {
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
},
});
const key = `coffee/products/${Date.now()}_${req.file.originalname}`;
await s3.send(new PutObjectCommand({
Bucket: process.env.EXOSCALE_BUCKET,
Key: key,
Body: req.file.buffer,
ContentType: req.file.mimetype,
}));
object_storage_id = key;
original_filename = req.file.originalname;
uploadedKey = key;
logger.info('[CoffeeController.create] uploaded picture', { key });
}
const created = await CoffeeService.create({
title,
description,
quantity: Number(quantity),
price: Number(price),
currency,
tax_rate,
is_featured,
billing_interval,
interval_count,
sku,
slug,
object_storage_id,
original_filename,
state,
});
res.status(201).json({
...created,
pictureUrl: created.object_storage_id ? buildPictureUrlFromKey(created.object_storage_id) : ''
});
} catch (e) {
logger.error('[CoffeeController.create] error', { msg: e.message, stack: e.stack?.split('\n')[0] });
// best-effort cleanup of uploaded object on failure
try {
if (object_storage_id) {
const s3 = new S3Client({
region: process.env.EXOSCALE_REGION,
endpoint: process.env.EXOSCALE_ENDPOINT,
credentials: {
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
},
});
await s3.send(new DeleteObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: object_storage_id }));
}
} catch (cleanupErr) {
logger.warn('[CoffeeController.create] cleanup failed', { msg: cleanupErr.message });
}
if (e.code === 'VALIDATION_ERROR') return res.status(400).json({ error: e.message, fields: e.errors });
res.status(500).json({ error: 'Failed to create product' });
}
};
exports.update = async (req, res) => {
try {
const id = parseInt(req.params.id, 10);
const { title, description, quantity, price } = req.body;
const currency = req.body.currency;
const tax_rate = req.body.tax_rate !== undefined ? Number(req.body.tax_rate) : undefined;
const is_featured = req.body.is_featured === undefined ? undefined : (req.body.is_featured === 'true' || req.body.is_featured === true);
const billing_interval = req.body.billing_interval;
const interval_count = req.body.interval_count !== undefined ? Number(req.body.interval_count) : undefined;
const sku = req.body.sku;
const slug = req.body.slug;
const state = req.body.state === undefined ? undefined : (req.body.state === 'false' || req.body.state === false ? false : true);
let object_storage_id;
let original_filename;
let uploadedKey = null;
if (req.file) {
const s3 = new S3Client({
region: process.env.EXOSCALE_REGION,
endpoint: process.env.EXOSCALE_ENDPOINT,
credentials: {
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
},
});
const key = `coffee/products/${Date.now()}_${req.file.originalname}`;
await s3.send(new PutObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: key, Body: req.file.buffer, ContentType: req.file.mimetype }));
object_storage_id = key;
original_filename = req.file.originalname;
uploadedKey = key;
logger.info('[CoffeeController.update] uploaded new picture', { id, key });
}
const current = await CoffeeService.get(id);
if (!current) return res.status(404).json({ error: 'Not found' });
const updated = await CoffeeService.update(id, {
title: title ?? current.title,
description: description ?? current.description,
quantity: quantity !== undefined ? Number(quantity) : current.quantity,
price: price !== undefined ? Number(price) : current.price,
currency: currency !== undefined ? currency : current.currency,
tax_rate: tax_rate !== undefined ? tax_rate : current.tax_rate,
is_featured: is_featured !== undefined ? is_featured : !!current.is_featured,
billing_interval: billing_interval !== undefined ? billing_interval : current.billing_interval,
interval_count: interval_count !== undefined ? interval_count : current.interval_count,
sku: sku !== undefined ? sku : current.sku,
slug: slug !== undefined ? slug : current.slug,
object_storage_id: object_storage_id !== undefined ? object_storage_id : current.object_storage_id,
original_filename: original_filename !== undefined ? original_filename : current.original_filename,
state: state !== undefined ? state : !!current.state,
});
res.json({
...updated,
pictureUrl: updated?.object_storage_id ? buildPictureUrlFromKey(updated.object_storage_id) : ''
});
} catch (e) {
logger.error('[CoffeeController.update] error', { msg: e.message });
// best-effort cleanup of newly uploaded object on failure
try {
if (object_storage_id) {
const s3 = new S3Client({
region: process.env.EXOSCALE_REGION,
endpoint: process.env.EXOSCALE_ENDPOINT,
credentials: {
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
},
});
await s3.send(new DeleteObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: object_storage_id }));
}
} catch (cleanupErr) {
logger.warn('[CoffeeController.update] cleanup failed', { msg: cleanupErr.message });
}
if (e.code === 'VALIDATION_ERROR') return res.status(400).json({ error: e.message, fields: e.errors });
res.status(500).json({ error: 'Failed to update product' });
}
};
exports.setState = async (req, res) => {
try {
const id = parseInt(req.params.id, 10);
const { state } = req.body; // boolean
const updated = await CoffeeService.setState(id, !!state);
res.json({
...updated,
pictureUrl: updated?.object_storage_id ? buildPictureUrlFromKey(updated.object_storage_id) : ''
});
} catch (e) {
res.status(400).json({ error: e.message });
}
};
exports.remove = async (req, res) => {
try {
const id = parseInt(req.params.id, 10);
// fetch current for object_storage_id
const current = await CoffeeService.get(id);
const ok = await CoffeeService.delete(id);
if (!ok) return res.status(404).json({ error: 'Not found' });
// best-effort delete object from storage
try {
if (current && current.object_storage_id) {
const s3 = new S3Client({
region: process.env.EXOSCALE_REGION,
endpoint: process.env.EXOSCALE_ENDPOINT,
credentials: {
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
},
});
await s3.send(new DeleteObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: current.object_storage_id }));
}
} catch (cleanupErr) {
logger.warn('[CoffeeController.remove] storage delete failed', { msg: cleanupErr.message });
}
res.status(204).end();
} catch (e) {
res.status(500).json({ error: 'Failed to delete product' });
}
};

View File

@ -562,6 +562,31 @@ async function createDatabase() {
`); `);
console.log('✅ Company stamps table created/verified'); console.log('✅ Company stamps table created/verified');
// --- Coffee / Subscriptions Table ---
await connection.query(`
CREATE TABLE IF NOT EXISTS coffee_table (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(200) NOT NULL,
description TEXT NOT NULL,
quantity INT UNSIGNED NOT NULL DEFAULT 0,
price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
currency CHAR(3) NOT NULL DEFAULT 'EUR',
tax_rate DECIMAL(5,2) NULL,
is_featured BOOLEAN NOT NULL DEFAULT FALSE,
billing_interval ENUM('day','week','month','year') NULL,
interval_count INT UNSIGNED NULL,
sku VARCHAR(100) NULL,
slug VARCHAR(200) NULL,
object_storage_id VARCHAR(255) NULL,
original_filename VARCHAR(255) NULL,
state BOOLEAN NOT NULL DEFAULT TRUE, -- available=true, unavailable=false
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_slug (slug)
);
`);
console.log('✅ Coffee table created/verified');
// --- Matrix: Global 5-ary tree config and relations --- // --- Matrix: Global 5-ary tree config and relations ---
await connection.query(` await connection.query(`
CREATE TABLE IF NOT EXISTS matrix_config ( CREATE TABLE IF NOT EXISTS matrix_config (
@ -672,6 +697,12 @@ async function createDatabase() {
await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_company', 'company_id'); await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_company', 'company_id');
await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_active', 'is_active'); await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_active', 'is_active');
// Coffee products
await ensureIndex(connection, 'coffee_table', 'idx_coffee_state', 'state');
await ensureIndex(connection, 'coffee_table', 'idx_coffee_updated_at', 'updated_at');
await ensureIndex(connection, 'coffee_table', 'idx_coffee_billing', 'billing_interval, interval_count');
await ensureIndex(connection, 'coffee_table', 'idx_coffee_sku', 'sku');
// Matrix indexes // Matrix indexes
await ensureIndex(connection, 'user_tree_edges', 'idx_user_tree_edges_parent', 'parent_user_id'); await ensureIndex(connection, 'user_tree_edges', 'idx_user_tree_edges_parent', 'parent_user_id');
// child_user_id already has a UNIQUE constraint; extra index not needed // child_user_id already has a UNIQUE constraint; extra index not needed

View File

@ -0,0 +1,89 @@
const db = require('../../database/database');
const { logger } = require('../../middleware/logger');
class CoffeeRepository {
async listAll(conn) {
const cx = conn || db;
const [rows] = await cx.query('SELECT * FROM coffee_table ORDER BY id DESC');
return rows || [];
}
async getById(id, conn) {
const cx = conn || db;
const [rows] = await cx.query('SELECT * FROM coffee_table WHERE id = ? LIMIT 1', [id]);
return rows && rows[0] ? rows[0] : null;
}
async create(data, conn) {
const cx = conn || db;
const sql = `INSERT INTO coffee_table (
title, description, quantity, price, currency, tax_rate, is_featured,
billing_interval, interval_count, sku, slug,
object_storage_id, original_filename,
state, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`;
const params = [
data.title,
data.description,
data.quantity,
data.price,
data.currency,
data.tax_rate,
data.is_featured,
data.billing_interval,
data.interval_count,
data.sku,
data.slug,
data.object_storage_id,
data.original_filename,
data.state
];
const [result] = await cx.query(sql, params);
logger.info('[CoffeeRepository.create] insert', { id: result.insertId });
return { id: result.insertId, ...data };
}
async update(id, data, conn) {
const cx = conn || db;
const sql = `UPDATE coffee_table
SET title = ?, description = ?, quantity = ?, price = ?, currency = ?, tax_rate = ?, is_featured = ?,
billing_interval = ?, interval_count = ?, sku = ?, slug = ?,
object_storage_id = ?, original_filename = ?,
state = ?, updated_at = NOW()
WHERE id = ?`;
const params = [
data.title,
data.description,
data.quantity,
data.price,
data.currency,
data.tax_rate,
data.is_featured,
data.billing_interval,
data.interval_count,
data.sku,
data.slug,
data.object_storage_id,
data.original_filename,
data.state,
id
];
const [result] = await cx.query(sql, params);
logger.info('[CoffeeRepository.update] update', { id, affected: result.affectedRows });
return result.affectedRows > 0;
}
async setState(id, state, conn) {
const cx = conn || db;
const [result] = await cx.query('UPDATE coffee_table SET state = ?, updated_at = NOW() WHERE id = ?', [state, id]);
return result.affectedRows > 0;
}
async delete(id, conn) {
const cx = conn || db;
const [result] = await cx.query('DELETE FROM coffee_table WHERE id = ?', [id]);
return result.affectedRows > 0;
}
}
module.exports = new CoffeeRepository();

View File

@ -5,6 +5,7 @@ const authMiddleware = require('../middleware/authMiddleware');
const AdminUserController = require('../controller/admin/AdminUserController'); const AdminUserController = require('../controller/admin/AdminUserController');
const DocumentTemplateController = require('../controller/documentTemplate/DocumentTemplateController'); const DocumentTemplateController = require('../controller/documentTemplate/DocumentTemplateController');
const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); const CompanyStampController = require('../controller/companyStamp/CompanyStampController');
const CoffeeController = require('../controller/admin/CoffeeController');
// Helper middlewares for company-stamp // Helper middlewares for company-stamp
function adminOnly(req, res, next) { function adminOnly(req, res, next) {
@ -28,5 +29,7 @@ router.delete('/document-templates/:id', authMiddleware, DocumentTemplateControl
// Company-stamp DELETE // Company-stamp DELETE
router.delete('/company-stamps/:id', authMiddleware, adminOnly, forceCompanyForAdmin, CompanyStampController.delete); router.delete('/company-stamps/:id', authMiddleware, adminOnly, forceCompanyForAdmin, CompanyStampController.delete);
// Admin: delete coffee product
router.delete('/admin/coffee/:id', authMiddleware, adminOnly, CoffeeController.remove);
module.exports = router; module.exports = router;

View File

@ -15,6 +15,7 @@ const UserController = require('../controller/auth/UserController');
const UserStatusController = require('../controller/auth/UserStatusController'); 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');
// small helpers copied from original files // small helpers copied from original files
function adminOnly(req, res, next) { function adminOnly(req, res, next) {
@ -104,6 +105,8 @@ router.get('/api/document-templates', authMiddleware, adminOnly, DocumentTemplat
router.get('/company-stamps/mine', authMiddleware, adminOnly, forceCompanyForAdmin, CompanyStampController.listMine); router.get('/company-stamps/mine', authMiddleware, adminOnly, forceCompanyForAdmin, CompanyStampController.listMine);
router.get('/company-stamps/mine/active', authMiddleware, adminOnly, forceCompanyForAdmin, CompanyStampController.activeMine); router.get('/company-stamps/mine/active', authMiddleware, adminOnly, forceCompanyForAdmin, CompanyStampController.activeMine);
router.get('/company-stamps/all', authMiddleware, adminOnly, forceCompanyForAdmin, CompanyStampController.listAll); router.get('/company-stamps/all', authMiddleware, adminOnly, forceCompanyForAdmin, CompanyStampController.listAll);
// Admin: coffee products
router.get('/admin/coffee', authMiddleware, adminOnly, CoffeeController.list);
// Matrix GETs // Matrix GETs

View File

@ -4,6 +4,7 @@ const router = express.Router();
const authMiddleware = require('../middleware/authMiddleware'); const authMiddleware = require('../middleware/authMiddleware');
const DocumentTemplateController = require('../controller/documentTemplate/DocumentTemplateController'); const DocumentTemplateController = require('../controller/documentTemplate/DocumentTemplateController');
const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added
const CoffeeController = require('../controller/admin/CoffeeController');
const AdminUserController = require('../controller/admin/AdminUserController'); const AdminUserController = require('../controller/admin/AdminUserController');
// Helper middlewares for company-stamp // Helper middlewares for company-stamp
@ -32,6 +33,8 @@ router.patch('/admin/unarchive-user/:id', authMiddleware, adminOnly, AdminUserCo
router.patch('/admin/update-verification/:id', authMiddleware, adminOnly, AdminUserController.updateUserVerification); router.patch('/admin/update-verification/:id', authMiddleware, adminOnly, AdminUserController.updateUserVerification);
router.patch('/admin/update-user-profile/:id', authMiddleware, adminOnly, AdminUserController.updateUserProfile); router.patch('/admin/update-user-profile/:id', authMiddleware, adminOnly, AdminUserController.updateUserProfile);
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
router.patch('/admin/coffee/:id/state', authMiddleware, adminOnly, CoffeeController.setState);
// Add other PATCH routes here as needed // Add other PATCH routes here as needed

View File

@ -16,6 +16,7 @@ const CompanyRegisterController = require('../controller/register/CompanyRegiste
const PersonalDocumentController = require('../controller/documents/PersonalDocumentController'); const PersonalDocumentController = require('../controller/documents/PersonalDocumentController');
const CompanyDocumentController = require('../controller/documents/CompanyDocumentController'); const CompanyDocumentController = require('../controller/documents/CompanyDocumentController');
const ContractUploadController = require('../controller/documents/ContractUploadController'); const ContractUploadController = require('../controller/documents/ContractUploadController');
const CoffeeController = require('../controller/admin/CoffeeController');
const PersonalProfileController = require('../controller/profile/PersonalProfileController'); const PersonalProfileController = require('../controller/profile/PersonalProfileController');
const CompanyProfileController = require('../controller/profile/CompanyProfileController'); const CompanyProfileController = require('../controller/profile/CompanyProfileController');
const AdminUserController = require('../controller/admin/AdminUserController'); const AdminUserController = require('../controller/admin/AdminUserController');
@ -115,6 +116,8 @@ function forceCompanyForAdmin(req, res, next) {
// Company-stamp POST // Company-stamp POST
router.post('/company-stamps', authMiddleware, adminOnly, forceCompanyForAdmin, CompanyStampController.upload); router.post('/company-stamps', authMiddleware, adminOnly, forceCompanyForAdmin, CompanyStampController.upload);
// Admin: create coffee product (supports multipart file 'picture')
router.post('/admin/coffee', authMiddleware, adminOnly, upload.single('picture'), CoffeeController.create);
// Existing registration handlers (keep) // Existing registration handlers (keep)
router.post('/register/personal', (req, res) => { router.post('/register/personal', (req, res) => {

View File

@ -4,13 +4,24 @@ const router = express.Router();
const authMiddleware = require('../middleware/authMiddleware'); const authMiddleware = require('../middleware/authMiddleware');
const AdminUserController = require('../controller/admin/AdminUserController'); const AdminUserController = require('../controller/admin/AdminUserController');
const DocumentTemplateController = require('../controller/documentTemplate/DocumentTemplateController'); const DocumentTemplateController = require('../controller/documentTemplate/DocumentTemplateController');
const CoffeeController = require('../controller/admin/CoffeeController');
const multer = require('multer'); const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() }); const upload = multer({ storage: multer.memoryStorage() });
// Helper middleware for admin-only routes (keeps consistency with other route files)
function adminOnly(req, res, next) {
if (!req.user || !['admin','super_admin'].includes(req.user.role)) {
return res.status(403).json({ error: 'Admin role required' });
}
next();
}
// PUT /admin/users/:id/permissions (moved from routes/admin.js) // PUT /admin/users/:id/permissions (moved from routes/admin.js)
router.put('/admin/users/:id/permissions', authMiddleware, AdminUserController.updateUserPermissions); router.put('/admin/users/:id/permissions', authMiddleware, AdminUserController.updateUserPermissions);
// PUT /document-templates/:id (moved from routes/documentTemplates.js) // PUT /document-templates/:id (moved from routes/documentTemplates.js)
router.put('/document-templates/:id', authMiddleware, upload.single('file'), DocumentTemplateController.updateTemplate); router.put('/document-templates/:id', authMiddleware, upload.single('file'), DocumentTemplateController.updateTemplate);
// Admin: update coffee product (supports picture file replacement)
router.put('/admin/coffee/:id', authMiddleware, adminOnly, upload.single('picture'), CoffeeController.update);
module.exports = router; module.exports = router;

View File

@ -0,0 +1,114 @@
const CoffeeRepository = require('../../repositories/subscriptions/CoffeeRepository');
const UnitOfWork = require('../../database/UnitOfWork');
const { logger } = require('../../middleware/logger');
function validate(data) {
const errors = [];
if (!data.title || String(data.title).trim() === '') errors.push('title');
if (!data.description || String(data.description).trim() === '') errors.push('description');
const q = Number(data.quantity);
if (!Number.isFinite(q) || q < 0) errors.push('quantity');
const price = Number(data.price);
if (!Number.isFinite(price) || price < 0) errors.push('price');
// state is boolean (available=true/unavailable=false)
if (typeof data.state !== 'boolean') errors.push('state');
// currency optional; default EUR if missing
if (data.currency && String(data.currency).length > 3) errors.push('currency');
// tax_rate optional must be >= 0 if provided
if (data.tax_rate !== undefined && data.tax_rate !== null) {
const tr = Number(data.tax_rate);
if (!Number.isFinite(tr) || tr < 0) errors.push('tax_rate');
}
// is_featured boolean
if (typeof data.is_featured !== 'boolean') errors.push('is_featured');
// billing_interval/interval_count validation
if (data.billing_interval !== undefined || data.interval_count !== undefined) {
const allowed = ['day','week','month','year'];
if (data.billing_interval && !allowed.includes(String(data.billing_interval))) errors.push('billing_interval');
if (data.interval_count !== undefined) {
const ic = Number(data.interval_count);
if (!Number.isFinite(ic) || ic <= 0) errors.push('interval_count');
}
}
return errors;
}
class CoffeeService {
async list() {
return CoffeeRepository.listAll();
}
async get(id) {
return CoffeeRepository.getById(id);
}
async create(data) {
const errors = validate(data);
if (errors.length) {
logger.warn('[CoffeeService.create] validation_failed', { errors });
throw Object.assign(new Error('Validation failed'), { code: 'VALIDATION_ERROR', errors });
}
const uow = new UnitOfWork();
try {
await uow.start();
const result = await CoffeeRepository.create(data, uow.connection);
await uow.commit();
return result;
} catch (e) {
try { await uow.rollback(e); } catch(_) {}
throw e;
}
}
async update(id, data) {
const errors = validate(data);
if (errors.length) {
logger.warn('[CoffeeService.update] validation_failed', { id, errors });
throw Object.assign(new Error('Validation failed'), { code: 'VALIDATION_ERROR', errors });
}
const uow = new UnitOfWork();
try {
await uow.start();
await CoffeeRepository.update(id, data, uow.connection);
const updated = await CoffeeRepository.getById(id, uow.connection);
await uow.commit();
return updated;
} catch (e) {
try { await uow.rollback(e); } catch(_) {}
throw e;
}
}
async setState(id, state) {
const uow = new UnitOfWork();
try {
await uow.start();
await CoffeeRepository.setState(id, !!state, uow.connection);
const updated = await CoffeeRepository.getById(id, uow.connection);
await uow.commit();
return updated;
} catch (e) {
try { await uow.rollback(e); } catch(_) {}
throw e;
}
}
async delete(id) {
const uow = new UnitOfWork();
try {
await uow.start();
const ok = await CoffeeRepository.delete(id, uow.connection);
await uow.commit();
return ok;
} catch (e) {
try { await uow.rollback(e); } catch(_) {}
throw e;
}
}
}
module.exports = new CoffeeService();