const CompanyStampRepository = require('../../../repositories/stamp/CompanyStampRepository'); const UnitOfWork = require('../../../database/UnitOfWork'); const { logger } = require('../../../middleware/logger'); const ALLOWED_MIME = new Set(['image/png', 'image/jpeg', 'image/webp']); const MAX_IMAGE_BYTES = 500 * 1024; // 500 KB const MAX_BASE64_LENGTH = Math.ceil((MAX_IMAGE_BYTES / 3) * 4) + 16; // safety const STAMP_CACHE_TTL_MS = 5 * 60 * 1000; const _cache = new Map(); // companyId -> { stamp, ts } const PRIMARY_COMPANY_ID = 1; // Our own company (single stamp policy) function stripDataUri(b64, providedMime) { if (!b64) return { pure: '', mime: providedMime }; const m = /^data:([\w/+.-]+);base64,(.+)$/i.exec(b64); if (m) return { mime: m[1], pure: m[2] }; return { pure: b64, mime: providedMime }; } function validateBase64(str) { return /^[A-Za-z0-9+/=\r\n]+$/.test(str); } function decodeSizeApprox(base64) { const cleaned = base64.replace(/[\r\n]/g, ''); const len = cleaned.length; if (!len) return 0; const padding = (cleaned.endsWith('==') ? 2 : cleaned.endsWith('=') ? 1 : 0); return (len * 3) / 4 - padding; } class CompanyStampService { async uploadStamp({ user, base64, mimeType, label, activate = false }) { if (!user || user.user_type !== 'company') throw new Error('Only company users can upload stamps'); const companyId = user.id || user.userId; // Enforce single-stamp rule for primary company BEFORE any mutation if (companyId === PRIMARY_COMPANY_ID) { const existingPrimary = await CompanyStampRepository.findByCompanyId(PRIMARY_COMPANY_ID); if (existingPrimary && existingPrimary.length) { const err = new Error('Primary company stamp already exists'); err.code = 'PRIMARY_STAMP_EXISTS'; err.existing = existingPrimary[0]; throw err; // controller will translate to preview response } } const { pure, mime } = stripDataUri(base64, mimeType); const finalMime = mime || mimeType; if (!ALLOWED_MIME.has(finalMime)) throw new Error('Unsupported MIME type'); if (!validateBase64(pure)) throw new Error('Invalid base64 data'); if (pure.length > MAX_BASE64_LENGTH) throw new Error('Image too large (base64 length)'); const bytes = decodeSizeApprox(pure); if (bytes > MAX_IMAGE_BYTES) throw new Error('Image exceeds size limit'); const uow = new UnitOfWork(); await uow.start(); try { // default inactive until optionally activated const created = await CompanyStampRepository.create( { company_id: companyId, label, mime_type: finalMime, image_base64: pure, is_active: !!activate // treat activate flag directly (single stamp ok) }, uow.connection ); if (activate) { // For primary company single stamp: no need to deactivate; for others keep old logic if (companyId !== PRIMARY_COMPANY_ID) { await CompanyStampRepository.deactivateAllForCompany(companyId, uow.connection); await CompanyStampRepository.activate(created.id, companyId, uow.connection); created.is_active = true; } _cache.delete(companyId); } await uow.commit(); logger.info('CompanyStampService.uploadStamp:success', { id: created.id, activate, companyId }); return created; } catch (e) { await uow.rollback(e); throw e; } } async listCompanyStamps(user) { if (!user || user.user_type !== 'company') throw new Error('Forbidden'); return CompanyStampRepository.findByCompanyId(user.id || user.userId); } async getActiveStampForCompany(companyId) { const cached = _cache.get(companyId); const now = Date.now(); if (cached && now - cached.ts < STAMP_CACHE_TTL_MS) return cached.stamp; const stamp = await CompanyStampRepository.findActiveByCompanyId(companyId); _cache.set(companyId, { stamp, ts: now }); return stamp; } async getMyActive(user) { if (!user || user.user_type !== 'company') throw new Error('Forbidden'); return this.getActiveStampForCompany(user.id || user.userId); } async activateStamp(id, user) { if (!user || user.user_type !== 'company') throw new Error('Forbidden'); const companyId = user.id || user.userId; const uow = new UnitOfWork(); await uow.start(); try { const stamp = await CompanyStampRepository.findById(id, uow.connection); if (!stamp || stamp.company_id !== companyId) throw new Error('Not found'); await CompanyStampRepository.deactivateAllForCompany(companyId, uow.connection); await CompanyStampRepository.activate(id, companyId, uow.connection); await uow.commit(); _cache.delete(companyId); return { ...stamp, is_active: true }; } catch (e) { await uow.rollback(e); throw e; } } async deleteStamp(id, user) { if (!user || user.user_type !== 'company') throw new Error('Forbidden'); const ok = await CompanyStampRepository.delete(id, user.id || user.userId); if (ok) _cache.delete(user.id || user.userId); return ok; } async getProfitPlanetStamp() { // Tries cache (reuse normal cache key) const cached = _cache.get(PRIMARY_COMPANY_ID); const now = Date.now(); if (cached && now - cached.ts < STAMP_CACHE_TTL_MS) return cached.stamp; // Accept active first; if none (single stamp not active), just any let stamp = await CompanyStampRepository.findActiveByCompanyId(PRIMARY_COMPANY_ID); if (!stamp) { const all = await CompanyStampRepository.findByCompanyId(PRIMARY_COMPANY_ID); stamp = all && all[0] ? all[0] : null; } _cache.set(PRIMARY_COMPANY_ID, { stamp, ts: now }); return stamp; } // Convenience for placeholder async getProfitPlanetSignatureTag(sizes = { maxW: 300, maxH: 300 }) { const stamp = await this.getProfitPlanetStamp(); if (!stamp) return ''; return ``; } } module.exports = new CompanyStampService();