From 77e34af8e2a94f244dd666fe7d57bbbb7fc88d7b Mon Sep 17 00:00:00 2001 From: seaznCode Date: Thu, 13 Nov 2025 20:13:16 +0100 Subject: [PATCH] feat: implement Coffee management functionality with CRUD operations and S3 integration --- controller/admin/CoffeeController.js | 225 ++++++++++++++++++ database/createDb.js | 31 +++ .../subscriptions/CoffeeRepository.js | 89 +++++++ routes/deleteRoutes.js | 3 + routes/getRoutes.js | 3 + routes/patchRoutes.js | 3 + routes/postRoutes.js | 3 + routes/putRoutes.js | 11 + services/subscriptions/CoffeeService.js | 114 +++++++++ 9 files changed, 482 insertions(+) create mode 100644 controller/admin/CoffeeController.js create mode 100644 repositories/subscriptions/CoffeeRepository.js create mode 100644 services/subscriptions/CoffeeService.js diff --git a/controller/admin/CoffeeController.js b/controller/admin/CoffeeController.js new file mode 100644 index 0000000..a09a2c6 --- /dev/null +++ b/controller/admin/CoffeeController.js @@ -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' }); + } +}; diff --git a/database/createDb.js b/database/createDb.js index e06ccda..c68330d 100644 --- a/database/createDb.js +++ b/database/createDb.js @@ -562,6 +562,31 @@ async function createDatabase() { `); 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 --- await connection.query(` 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_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 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 diff --git a/repositories/subscriptions/CoffeeRepository.js b/repositories/subscriptions/CoffeeRepository.js new file mode 100644 index 0000000..38c321a --- /dev/null +++ b/repositories/subscriptions/CoffeeRepository.js @@ -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(); diff --git a/routes/deleteRoutes.js b/routes/deleteRoutes.js index 85ac9b4..f550b17 100644 --- a/routes/deleteRoutes.js +++ b/routes/deleteRoutes.js @@ -5,6 +5,7 @@ const authMiddleware = require('../middleware/authMiddleware'); const AdminUserController = require('../controller/admin/AdminUserController'); const DocumentTemplateController = require('../controller/documentTemplate/DocumentTemplateController'); const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); +const CoffeeController = require('../controller/admin/CoffeeController'); // Helper middlewares for company-stamp function adminOnly(req, res, next) { @@ -28,5 +29,7 @@ router.delete('/document-templates/:id', authMiddleware, DocumentTemplateControl // Company-stamp 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; diff --git a/routes/getRoutes.js b/routes/getRoutes.js index e3a82e8..4f669b3 100644 --- a/routes/getRoutes.js +++ b/routes/getRoutes.js @@ -15,6 +15,7 @@ const UserController = require('../controller/auth/UserController'); 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'); // small helpers copied from original files 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/active', authMiddleware, adminOnly, forceCompanyForAdmin, CompanyStampController.activeMine); router.get('/company-stamps/all', authMiddleware, adminOnly, forceCompanyForAdmin, CompanyStampController.listAll); +// Admin: coffee products +router.get('/admin/coffee', authMiddleware, adminOnly, CoffeeController.list); // Matrix GETs diff --git a/routes/patchRoutes.js b/routes/patchRoutes.js index 7dc9503..bd4ff51 100644 --- a/routes/patchRoutes.js +++ b/routes/patchRoutes.js @@ -4,6 +4,7 @@ const router = express.Router(); const authMiddleware = require('../middleware/authMiddleware'); const DocumentTemplateController = require('../controller/documentTemplate/DocumentTemplateController'); const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added +const CoffeeController = require('../controller/admin/CoffeeController'); const AdminUserController = require('../controller/admin/AdminUserController'); // 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-user-profile/:id', authMiddleware, adminOnly, AdminUserController.updateUserProfile); 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 diff --git a/routes/postRoutes.js b/routes/postRoutes.js index 1c83b06..aceaecc 100644 --- a/routes/postRoutes.js +++ b/routes/postRoutes.js @@ -16,6 +16,7 @@ const CompanyRegisterController = require('../controller/register/CompanyRegiste const PersonalDocumentController = require('../controller/documents/PersonalDocumentController'); const CompanyDocumentController = require('../controller/documents/CompanyDocumentController'); const ContractUploadController = require('../controller/documents/ContractUploadController'); +const CoffeeController = require('../controller/admin/CoffeeController'); const PersonalProfileController = require('../controller/profile/PersonalProfileController'); const CompanyProfileController = require('../controller/profile/CompanyProfileController'); const AdminUserController = require('../controller/admin/AdminUserController'); @@ -115,6 +116,8 @@ function forceCompanyForAdmin(req, res, next) { // Company-stamp POST 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) router.post('/register/personal', (req, res) => { diff --git a/routes/putRoutes.js b/routes/putRoutes.js index 219fe37..3142928 100644 --- a/routes/putRoutes.js +++ b/routes/putRoutes.js @@ -4,13 +4,24 @@ const router = express.Router(); const authMiddleware = require('../middleware/authMiddleware'); const AdminUserController = require('../controller/admin/AdminUserController'); const DocumentTemplateController = require('../controller/documentTemplate/DocumentTemplateController'); +const CoffeeController = require('../controller/admin/CoffeeController'); const multer = require('multer'); 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) router.put('/admin/users/:id/permissions', authMiddleware, AdminUserController.updateUserPermissions); // PUT /document-templates/:id (moved from routes/documentTemplates.js) 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; diff --git a/services/subscriptions/CoffeeService.js b/services/subscriptions/CoffeeService.js new file mode 100644 index 0000000..56233d6 --- /dev/null +++ b/services/subscriptions/CoffeeService.js @@ -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();