dev #23
@ -11,7 +11,6 @@ const fs = require('fs/promises');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const pool = require('../../database/database');
|
const pool = require('../../database/database');
|
||||||
|
|
||||||
const CompanySettingsRepository = require('../../repositories/settings/CompanySettingsRepository');
|
|
||||||
const CoffeeShippingFeeService = require('../subscriptions/CoffeeShippingFeeService');
|
const CoffeeShippingFeeService = require('../subscriptions/CoffeeShippingFeeService');
|
||||||
|
|
||||||
class InvoiceService {
|
class InvoiceService {
|
||||||
@ -19,15 +18,6 @@ class InvoiceService {
|
|||||||
this.repo = new InvoiceRepository();
|
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) {
|
_templateHasVars(template, varNames) {
|
||||||
if (!template) return false;
|
if (!template) return false;
|
||||||
return varNames.every((name) => {
|
return varNames.every((name) => {
|
||||||
@ -47,15 +37,19 @@ class InvoiceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_resolvePieceCountForQr(abonement) {
|
_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();
|
const packGroup = String(abonement?.pack_group || '').toLowerCase();
|
||||||
if (packGroup.includes('120')) return 120;
|
if (packGroup.includes('120')) return 120;
|
||||||
if (packGroup.includes('60')) return 60;
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,35 +101,8 @@ class InvoiceService {
|
|||||||
return items;
|
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 }) {
|
async _buildQrCodeImageTag({ abonement }) {
|
||||||
const pieceCount = this._resolvePieceCountForQr(abonement);
|
return '';
|
||||||
if (!pieceCount) return '';
|
|
||||||
|
|
||||||
const dataUri = await this._getCompanySettingsQrDataUri(pieceCount);
|
|
||||||
if (!dataUri) return '';
|
|
||||||
|
|
||||||
return `<img alt="QR Code" src="${this._escapeHtml(dataUri)}" />`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_escapeHtml(value) {
|
_escapeHtml(value) {
|
||||||
@ -312,11 +279,10 @@ class InvoiceService {
|
|||||||
|
|
||||||
_prepareVariablesForTemplate(templateHtml, variables) {
|
_prepareVariablesForTemplate(templateHtml, variables) {
|
||||||
// Ensure backwards compatibility with older templates that only contain {{paymentInfoText}}
|
// 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;
|
if (!templateHtml) return variables;
|
||||||
|
|
||||||
const supportsBankVars = this._templateHasVars(templateHtml, ['bankAccountHolder', 'bankIban', 'bankBic']);
|
const supportsBankVars = this._templateHasVars(templateHtml, ['bankAccountHolder', 'bankIban', 'bankBic']);
|
||||||
const supportsQrVar = this._templateHasVars(templateHtml, ['qrCodeImage']);
|
|
||||||
|
|
||||||
const bankBlock = this._getProfitPlanetBankBlockHtml({
|
const bankBlock = this._getProfitPlanetBankBlockHtml({
|
||||||
bankAccountHolder: variables.bankAccountHolder || 'Profit Planet GmbH',
|
bankAccountHolder: variables.bankAccountHolder || 'Profit Planet GmbH',
|
||||||
@ -330,11 +296,6 @@ class InvoiceService {
|
|||||||
next.paymentInfoText = bankBlock;
|
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;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -559,29 +520,14 @@ class InvoiceService {
|
|||||||
|
|
||||||
if (templateHtml) {
|
if (templateHtml) {
|
||||||
const supportsBankVars = this._templateHasVars(templateHtml, ['bankAccountHolder', 'bankIban', 'bankBic']);
|
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', {
|
logger.info('InvoiceService._sendInvoiceEmail:template_compat', {
|
||||||
invoiceId: invoice?.id,
|
invoiceId: invoice?.id,
|
||||||
lang,
|
lang,
|
||||||
supportsBankVars,
|
supportsBankVars,
|
||||||
supportsQrVar,
|
|
||||||
pieceCountForQr,
|
|
||||||
hasQrImage: Boolean(variables?.qrCodeImage),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const varsForTemplate = this._prepareVariablesForTemplate(templateHtml, variables);
|
const varsForTemplate = this._prepareVariablesForTemplate(templateHtml, variables);
|
||||||
html = this._renderTemplate(templateHtml, varsForTemplate);
|
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 });
|
const htmlForPdf = html || await this._buildFallbackInvoiceHtml({ invoice, items, abonement, lang });
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user