feat: implement Coffee management functionality with CRUD operations and S3 integration
This commit is contained in:
parent
0bc0bd087f
commit
77e34af8e2
225
controller/admin/CoffeeController.js
Normal file
225
controller/admin/CoffeeController.js
Normal 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' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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
|
||||||
|
|||||||
89
repositories/subscriptions/CoffeeRepository.js
Normal file
89
repositories/subscriptions/CoffeeRepository.js
Normal 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();
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
114
services/subscriptions/CoffeeService.js
Normal file
114
services/subscriptions/CoffeeService.js
Normal 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();
|
||||||
Loading…
Reference in New Issue
Block a user