dev #23

Merged
Seazn merged 16 commits from dev into main 2026-05-21 17:34:48 +00:00
Showing only changes of commit 10552488c0 - Show all commits

View File

@ -11,7 +11,6 @@ const fs = require('fs/promises');
const path = require('path');
const pool = require('../../database/database');
const CompanySettingsRepository = require('../../repositories/settings/CompanySettingsRepository');
const CoffeeShippingFeeService = require('../subscriptions/CoffeeShippingFeeService');
class InvoiceService {
@ -19,15 +18,6 @@ class InvoiceService {
this.repo = new InvoiceRepository();
}
_inferImageMimeFromBase64(base64) {
const s = String(base64 || '').trim();
if (!s) return 'image/png';
if (s.startsWith('iVBORw0KGgo')) return 'image/png';
if (s.startsWith('/9j/')) return 'image/jpeg';
if (s.startsWith('R0lGOD')) return 'image/gif';
return 'image/png';
}
_templateHasVars(template, varNames) {
if (!template) return false;
return varNames.every((name) => {
@ -47,15 +37,19 @@ class InvoiceService {
}
_resolvePieceCountForQr(abonement) {
const breakdown = Array.isArray(abonement?.pack_breakdown) ? abonement.pack_breakdown : [];
const totalPacks = breakdown.reduce((sum, item) => sum + Number(item?.packs || item?.quantity || 0), 0);
const piecesByPack = totalPacks ? totalPacks * 10 : null;
if (piecesByPack != null) {
if (piecesByPack >= 120) return 120;
if (piecesByPack >= 60) return 60;
return null;
}
const packGroup = String(abonement?.pack_group || '').toLowerCase();
if (packGroup.includes('120')) return 120;
if (packGroup.includes('60')) return 60;
const breakdown = Array.isArray(abonement?.pack_breakdown) ? abonement.pack_breakdown : [];
const totalPacks = breakdown.reduce((sum, item) => sum + Number(item?.packs || 0), 0);
const piecesByPack = totalPacks ? totalPacks * 10 : null;
if (piecesByPack === 60 || piecesByPack === 120) return piecesByPack;
return null;
}
@ -107,35 +101,8 @@ class InvoiceService {
return items;
}
async _getCompanySettingsQrDataUri(pieceCount) {
const safePieceCount = pieceCount === 120 ? 120 : 60;
try {
const repo = new CompanySettingsRepository();
const row = await repo.get();
if (!row) return null;
const raw = safePieceCount === 120 ? row?.qr_code_120_base64 : row?.qr_code_60_base64;
const value = (raw == null) ? '' : String(raw).trim();
if (!value) return null;
if (value.startsWith('data:image/')) return value;
const mime = this._inferImageMimeFromBase64(value);
return `data:${mime};base64,${value}`;
} catch (e) {
logger.warn('InvoiceService._getCompanySettingsQrDataUri:error', {
pieceCount: safePieceCount,
message: e?.message,
});
return null;
}
}
async _buildQrCodeImageTag({ abonement }) {
const pieceCount = this._resolvePieceCountForQr(abonement);
if (!pieceCount) return '';
const dataUri = await this._getCompanySettingsQrDataUri(pieceCount);
if (!dataUri) return '';
return `<img alt="QR Code" src="${this._escapeHtml(dataUri)}" />`;
return '';
}
_escapeHtml(value) {
@ -312,11 +279,10 @@ class InvoiceService {
_prepareVariablesForTemplate(templateHtml, variables) {
// Ensure backwards compatibility with older templates that only contain {{paymentInfoText}}
// by injecting the Profit Planet bank block (and optionally QR) into paymentInfoText.
// by injecting the Profit Planet bank block into paymentInfoText.
if (!templateHtml) return variables;
const supportsBankVars = this._templateHasVars(templateHtml, ['bankAccountHolder', 'bankIban', 'bankBic']);
const supportsQrVar = this._templateHasVars(templateHtml, ['qrCodeImage']);
const bankBlock = this._getProfitPlanetBankBlockHtml({
bankAccountHolder: variables.bankAccountHolder || 'Profit Planet GmbH',
@ -330,11 +296,6 @@ class InvoiceService {
next.paymentInfoText = bankBlock;
}
if (!supportsQrVar && variables.qrCodeImage) {
// Append QR under payment info text when there's no dedicated placeholder
next.paymentInfoText = `${next.paymentInfoText || ''}<br><br>${variables.qrCodeImage}`;
}
return next;
}
@ -559,29 +520,14 @@ class InvoiceService {
if (templateHtml) {
const supportsBankVars = this._templateHasVars(templateHtml, ['bankAccountHolder', 'bankIban', 'bankBic']);
const supportsQrVar = this._templateHasVars(templateHtml, ['qrCodeImage']);
const pieceCountForQr = this._resolvePieceCountForQr(abonement);
logger.info('InvoiceService._sendInvoiceEmail:template_compat', {
invoiceId: invoice?.id,
lang,
supportsBankVars,
supportsQrVar,
pieceCountForQr,
hasQrImage: Boolean(variables?.qrCodeImage),
});
const varsForTemplate = this._prepareVariablesForTemplate(templateHtml, variables);
html = this._renderTemplate(templateHtml, varsForTemplate);
// Final guard: if we still didn't embed QR but we expected one, force local template
const missingQr = variables.qrCodeImage && !html.includes('data:image/png;base64,');
if (missingQr) {
const localTemplate = await this._loadLocalInvoiceTemplateHtml();
if (localTemplate) {
const varsForLocal = this._prepareVariablesForTemplate(localTemplate, variables);
html = this._renderTemplate(localTemplate, varsForLocal);
}
}
}
const htmlForPdf = html || await this._buildFallbackInvoiceHtml({ invoice, items, abonement, lang });