dei mutter
This commit is contained in:
parent
9a0eef16bb
commit
ae73c37d3e
@ -197,11 +197,11 @@ class DocumentTemplateRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// Deactivate other active invoice templates for the same language.
|
||||
// Invoices always use user_type='both', so only lang matters.
|
||||
async deactivateOtherActiveInvoices({ excludeId, lang }, conn) {
|
||||
logger.info('DocumentTemplateRepository.deactivateOtherActiveInvoices:start', { excludeId, lang });
|
||||
// Deactivate other active invoice templates for the same language and user_type.
|
||||
async deactivateOtherActiveInvoices({ excludeId, lang, user_type }, conn) {
|
||||
logger.info('DocumentTemplateRepository.deactivateOtherActiveInvoices:start', { excludeId, lang, user_type });
|
||||
const safeLang = (lang === 'en' || lang === 'de') ? lang : 'en';
|
||||
const safeUserType = (user_type === 'personal' || user_type === 'company' || user_type === 'both') ? user_type : 'both';
|
||||
|
||||
const query = `
|
||||
UPDATE document_templates
|
||||
@ -209,9 +209,10 @@ class DocumentTemplateRepository {
|
||||
WHERE id <> ?
|
||||
AND type = 'invoice'
|
||||
AND lang = ?
|
||||
AND COALESCE(user_type, 'both') = ?
|
||||
AND state = 'active'
|
||||
`;
|
||||
const params = [excludeId, safeLang];
|
||||
const params = [excludeId, safeLang, safeUserType];
|
||||
try {
|
||||
if (conn) {
|
||||
const [res] = await conn.execute(query, params);
|
||||
|
||||
@ -119,6 +119,138 @@ class InvoiceService {
|
||||
return `${numeric.toFixed(2)} ${currency}`;
|
||||
}
|
||||
|
||||
_firstNonEmpty(...values) {
|
||||
for (const value of values) {
|
||||
if (value === undefined || value === null) continue;
|
||||
const normalized = String(value).trim();
|
||||
if (normalized) return normalized;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
_normalizeInvoiceUserType(value) {
|
||||
return String(value || '').trim().toLowerCase() === 'company' ? 'company' : 'personal';
|
||||
}
|
||||
|
||||
async _loadInvoiceUserProfile(userId) {
|
||||
if (!userId) return null;
|
||||
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT u.id, u.email, u.user_type,
|
||||
cp.company_name, cp.registration_number, cp.atu_number, cp.country AS company_country
|
||||
FROM users u
|
||||
LEFT JOIN company_profiles cp ON cp.user_id = u.id
|
||||
WHERE u.id = ?
|
||||
LIMIT 1`,
|
||||
[userId],
|
||||
);
|
||||
|
||||
return rows?.[0] || null;
|
||||
} catch (error) {
|
||||
logger.warn('InvoiceService._loadInvoiceUserProfile:error', {
|
||||
userId,
|
||||
message: error?.message,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async _buildInvoiceBillingContext({ abonement, invoice = null, invoiceUserId = null, userProfile = null } = {}) {
|
||||
const profile = userProfile || await this._loadInvoiceUserProfile(invoiceUserId);
|
||||
const userType = this._normalizeInvoiceUserType(profile?.user_type);
|
||||
|
||||
const shippingFullName = this._firstNonEmpty(
|
||||
[abonement?.first_name, abonement?.last_name].filter(Boolean).join(' '),
|
||||
invoice?.buyer_name,
|
||||
profile?.company_name,
|
||||
abonement?.email,
|
||||
invoice?.buyer_email,
|
||||
'Customer',
|
||||
);
|
||||
|
||||
const customerName = userType === 'company'
|
||||
? this._firstNonEmpty(
|
||||
abonement?.invoice_full_name,
|
||||
profile?.company_name,
|
||||
invoice?.buyer_name,
|
||||
shippingFullName,
|
||||
)
|
||||
: this._firstNonEmpty(
|
||||
abonement?.invoice_full_name,
|
||||
invoice?.buyer_name,
|
||||
shippingFullName,
|
||||
);
|
||||
|
||||
const email = this._firstNonEmpty(
|
||||
abonement?.invoice_email,
|
||||
invoice?.buyer_email,
|
||||
abonement?.email,
|
||||
profile?.email,
|
||||
);
|
||||
|
||||
const street = this._firstNonEmpty(
|
||||
abonement?.invoice_street,
|
||||
invoice?.buyer_street,
|
||||
abonement?.street,
|
||||
);
|
||||
|
||||
const postalCode = this._firstNonEmpty(
|
||||
abonement?.invoice_postal_code,
|
||||
invoice?.buyer_postal_code,
|
||||
abonement?.postal_code,
|
||||
);
|
||||
|
||||
const city = this._firstNonEmpty(
|
||||
abonement?.invoice_city,
|
||||
invoice?.buyer_city,
|
||||
abonement?.city,
|
||||
);
|
||||
|
||||
const country = this._firstNonEmpty(
|
||||
invoice?.buyer_country,
|
||||
abonement?.country,
|
||||
profile?.company_country,
|
||||
);
|
||||
|
||||
return {
|
||||
customerName: customerName || shippingFullName || '-',
|
||||
email,
|
||||
street,
|
||||
postalCode,
|
||||
city,
|
||||
country,
|
||||
postalCity: [postalCode, city].filter(Boolean).join(' '),
|
||||
userType,
|
||||
companyName: this._firstNonEmpty(profile?.company_name, abonement?.invoice_full_name),
|
||||
uidNumber: this._normalizeUid(profile?.atu_number || profile?.registration_number || ''),
|
||||
profile,
|
||||
};
|
||||
}
|
||||
|
||||
_selectInvoiceTemplate(templates, { lang = 'en', userType = 'personal' } = {}) {
|
||||
if (!Array.isArray(templates) || !templates.length) return null;
|
||||
|
||||
const safeLang = lang === 'de' ? 'de' : 'en';
|
||||
const safeUserType = this._normalizeInvoiceUserType(userType);
|
||||
const priorities = [
|
||||
(template) => template?.lang === safeLang && template?.user_type === safeUserType,
|
||||
(template) => template?.lang === safeLang && template?.user_type === 'both',
|
||||
(template) => template?.lang === 'en' && template?.user_type === safeUserType,
|
||||
(template) => template?.lang === 'en' && template?.user_type === 'both',
|
||||
(template) => template?.user_type === safeUserType,
|
||||
(template) => template?.user_type === 'both',
|
||||
() => true,
|
||||
];
|
||||
|
||||
for (const matches of priorities) {
|
||||
const selected = templates.find(matches);
|
||||
if (selected) return selected;
|
||||
}
|
||||
|
||||
return templates[0];
|
||||
}
|
||||
|
||||
async _s3BodyToString(body) {
|
||||
if (!body) return '';
|
||||
if (typeof body.transformToString === 'function') {
|
||||
@ -161,7 +293,7 @@ class InvoiceService {
|
||||
|
||||
_buildInvoiceText({ invoice, items, abonement, lang }) {
|
||||
const isDe = lang === 'de';
|
||||
const customerName = [abonement?.first_name, abonement?.last_name].filter(Boolean).join(' ') || invoice.buyer_name || '-';
|
||||
const customerName = invoice?.buyer_name || [abonement?.first_name, abonement?.last_name].filter(Boolean).join(' ') || '-';
|
||||
const issuedAt = invoice.issued_at ? new Date(invoice.issued_at).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10);
|
||||
|
||||
return [
|
||||
@ -177,7 +309,7 @@ class InvoiceService {
|
||||
|
||||
_buildInvoiceMailText({ invoice, items, abonement, lang }) {
|
||||
const isDe = lang === 'de';
|
||||
const customerName = [abonement?.first_name, abonement?.last_name].filter(Boolean).join(' ') || invoice.buyer_name || '-';
|
||||
const customerName = invoice?.buyer_name || [abonement?.first_name, abonement?.last_name].filter(Boolean).join(' ') || '-';
|
||||
return [
|
||||
isDe ? `Vielen Dank für Ihr Abonnement, ${customerName}.` : `Thank you for your subscription, ${customerName}.`,
|
||||
isDe ? 'Ihre Rechnung ist als PDF im Anhang enthalten.' : 'Your invoice is attached as a PDF.',
|
||||
@ -191,7 +323,7 @@ class InvoiceService {
|
||||
|
||||
_buildInvoiceMailHtml({ invoice, abonement, lang }) {
|
||||
const isDe = lang === 'de';
|
||||
const customerName = [abonement?.first_name, abonement?.last_name].filter(Boolean).join(' ') || invoice.buyer_name || 'Customer';
|
||||
const customerName = invoice?.buyer_name || [abonement?.first_name, abonement?.last_name].filter(Boolean).join(' ') || 'Customer';
|
||||
const logoUrl = process.env.MAIL_LOGO_URL || process.env.BREVO_LOGO_URL || process.env.APP_LOGO_URL || '';
|
||||
|
||||
return `<!doctype html>
|
||||
@ -252,12 +384,12 @@ class InvoiceService {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async _loadInvoiceHtmlTemplate() {
|
||||
async _loadInvoiceHtmlTemplate({ lang = 'en', userType = 'personal' } = {}) {
|
||||
// Load the latest active invoice template from the contract manager (S3)
|
||||
try {
|
||||
const templates = await DocumentTemplateService.getActiveTemplatesForUserType('both', 'invoice');
|
||||
const templates = await DocumentTemplateService.getActiveTemplatesForUserType(userType, 'invoice');
|
||||
if (!Array.isArray(templates) || !templates.length) return null;
|
||||
const selected = templates[0]; // latest active version
|
||||
const selected = this._selectInvoiceTemplate(templates, { lang, userType });
|
||||
if (!selected?.storageKey) return null;
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: process.env.EXOSCALE_BUCKET,
|
||||
@ -332,6 +464,15 @@ class InvoiceService {
|
||||
const vatRate = invoice.vat_rate != null ? Number(invoice.vat_rate) : 0;
|
||||
const taxMode = String(invoice?.context?.tax_mode || 'standard').toLowerCase();
|
||||
const isReverseCharge = taxMode === 'reverse_charge';
|
||||
const invoiceUserId = invoice?.user_id || abonement?.user_id || abonement?.purchaser_user_id || null;
|
||||
const billingContext = await this._buildInvoiceBillingContext({
|
||||
abonement,
|
||||
invoice,
|
||||
invoiceUserId,
|
||||
});
|
||||
const reverseChargeNoticeText = isDe
|
||||
? 'Bei dieser Rechnung handelt es sich um eine Rechnung nach dem Reverse Charge Verfahren. Demnach wird keine Umsatzsteuer ausgewiesen. Die Steuerschuldnerschaft liegt beim Leistungsempfänger. Die Umsatzsteuer ist entsprechend vom Leistungsempfänger anzumelden und abzuführen.'
|
||||
: 'This invoice is issued under the reverse charge procedure. Accordingly, no VAT is shown. The tax liability is transferred to the recipient of the service. The recipient must declare and pay the VAT in accordance with the applicable regulations.';
|
||||
|
||||
// Hardcoded bank info (Profit Planet)
|
||||
const bankAccountHolder = 'Profit Planet GmbH';
|
||||
@ -365,8 +506,11 @@ class InvoiceService {
|
||||
|
||||
// For gift subscriptions: "Bill To" = recipient, "Ordered by" = purchaser
|
||||
// For self subscriptions: "Bill To" = the subscriber
|
||||
let customerName;
|
||||
let customerEmail = '';
|
||||
let customerName = billingContext.customerName || invoice.buyer_name || '-';
|
||||
let customerEmail = billingContext.email || '';
|
||||
let customerStreet = billingContext.street || invoice.buyer_street || '';
|
||||
let customerPostalCity = billingContext.postalCity || [invoice.buyer_postal_code, invoice.buyer_city].filter(Boolean).join(' ');
|
||||
let customerCountry = billingContext.country || invoice.buyer_country || '';
|
||||
let orderedByBlock = '';
|
||||
|
||||
if (isGift) {
|
||||
@ -375,16 +519,16 @@ class InvoiceService {
|
||||
const recipientEmail = abonement?.email || invoice.buyer_email || '';
|
||||
customerName = recipientName || recipientEmail || '-';
|
||||
customerEmail = recipientName ? recipientEmail : '';
|
||||
customerStreet = abonement?.street || invoice?.buyer_street || '';
|
||||
customerPostalCity = [abonement?.postal_code || invoice?.buyer_postal_code, abonement?.city || invoice?.buyer_city].filter(Boolean).join(' ');
|
||||
customerCountry = abonement?.country || invoice?.buyer_country || '';
|
||||
|
||||
// Purchaser info for "Ordered by"
|
||||
const purchaserName = invoice.buyer_name || [abonement?.first_name, abonement?.last_name].filter(Boolean).join(' ') || '';
|
||||
const purchaserName = billingContext.customerName || invoice.buyer_name || [abonement?.first_name, abonement?.last_name].filter(Boolean).join(' ') || '';
|
||||
if (purchaserName) {
|
||||
const orderedByLabel = isDe ? 'Bestellt von' : 'Ordered By';
|
||||
orderedByBlock = `<div class="meta-block"><h3>${this._escapeHtml(orderedByLabel)}</h3><p><span class="highlight">${this._escapeHtml(purchaserName)}</span></p></div>`;
|
||||
}
|
||||
} else {
|
||||
customerName = [abonement?.first_name, abonement?.last_name].filter(Boolean).join(' ') || invoice.buyer_name || '-';
|
||||
customerEmail = abonement?.email || invoice.buyer_email || '';
|
||||
}
|
||||
|
||||
const qrCodeImage = await this._buildQrCodeImageTag({ abonement });
|
||||
@ -406,11 +550,14 @@ class InvoiceService {
|
||||
companyPostalCity: this._escapeHtml(companyInfo.company_postal_city || ''),
|
||||
companyCountry: this._escapeHtml(companyInfo.company_country || 'Germany'),
|
||||
companyLogo: this._buildCompanyLogoTag(companyInfo),
|
||||
invoiceUserType: billingContext.userType,
|
||||
customerName: this._escapeHtml(customerName),
|
||||
customerEmail: this._escapeHtml(customerEmail),
|
||||
customerStreet: this._escapeHtml(invoice.buyer_street || ''),
|
||||
customerPostalCity: this._escapeHtml([invoice.buyer_postal_code, invoice.buyer_city].filter(Boolean).join(' ')),
|
||||
customerCountry: this._escapeHtml(invoice.buyer_country || ''),
|
||||
customerStreet: this._escapeHtml(customerStreet),
|
||||
customerPostalCity: this._escapeHtml(customerPostalCity),
|
||||
customerCountry: this._escapeHtml(customerCountry),
|
||||
customerCompanyName: this._escapeHtml(billingContext.companyName || ''),
|
||||
customerUidNumber: this._escapeHtml(invoice?.context?.uid_number || billingContext.uidNumber || ''),
|
||||
orderedByBlock,
|
||||
issuedAt: this._escapeHtml(issuedAt),
|
||||
dueAt: this._escapeHtml(dueAt),
|
||||
@ -434,6 +581,10 @@ class InvoiceService {
|
||||
: (isReverseCharge
|
||||
? 'Reverse charge applies: VAT liability shifts to the recipient. Please transfer the total amount stating the invoice number as reference.'
|
||||
: 'Please transfer the total amount stating the invoice number as reference.'),
|
||||
reverseChargeClass: isReverseCharge ? 'reverse-charge-active' : 'reverse-charge-inactive',
|
||||
reverseChargeSectionClass: isReverseCharge ? '' : 'is-hidden',
|
||||
standardTaxSectionClass: isReverseCharge ? 'is-hidden' : '',
|
||||
reverseChargeNoticeText: isReverseCharge ? this._escapeHtml(reverseChargeNoticeText) : '',
|
||||
bankAccountHolder: this._escapeHtml(bankAccountHolder),
|
||||
bankIban: this._escapeHtml(bankIban),
|
||||
bankBic: this._escapeHtml(bankBic),
|
||||
@ -447,7 +598,10 @@ class InvoiceService {
|
||||
async _buildFallbackInvoiceHtml({ invoice, items, abonement, lang }) {
|
||||
const variables = await this._buildInvoiceTemplateVariables({ invoice, items, abonement, lang });
|
||||
|
||||
const template = await this._loadInvoiceHtmlTemplate();
|
||||
const template = await this._loadInvoiceHtmlTemplate({
|
||||
lang,
|
||||
userType: variables.invoiceUserType,
|
||||
});
|
||||
if (template) {
|
||||
const varsForTemplate = this._prepareVariablesForTemplate(template, variables);
|
||||
return this._renderTemplate(template, varsForTemplate);
|
||||
@ -469,12 +623,12 @@ class InvoiceService {
|
||||
</html>`;
|
||||
}
|
||||
|
||||
async _loadInvoiceTemplateHtml({ lang = 'en' } = {}) {
|
||||
async _loadInvoiceTemplateHtml({ lang = 'en', userType = 'personal' } = {}) {
|
||||
try {
|
||||
const templates = await DocumentTemplateService.getActiveTemplatesForUserType('both', 'invoice');
|
||||
const templates = await DocumentTemplateService.getActiveTemplatesForUserType(userType, 'invoice');
|
||||
if (!Array.isArray(templates) || !templates.length) return null;
|
||||
|
||||
const selected = templates.find((t) => t.lang === lang) || templates.find((t) => t.lang === 'en') || templates[0];
|
||||
const selected = this._selectInvoiceTemplate(templates, { lang, userType });
|
||||
if (!selected?.storageKey) return null;
|
||||
|
||||
const command = new GetObjectCommand({
|
||||
@ -543,7 +697,10 @@ class InvoiceService {
|
||||
// Build the full set of template variables once – used by both S3 and local paths
|
||||
const variables = await this._buildInvoiceTemplateVariables({ invoice, items, abonement, lang });
|
||||
|
||||
const templateHtml = await this._loadInvoiceTemplateHtml({ lang });
|
||||
const templateHtml = await this._loadInvoiceTemplateHtml({
|
||||
lang,
|
||||
userType: variables.invoiceUserType,
|
||||
});
|
||||
let html = null;
|
||||
|
||||
if (templateHtml) {
|
||||
@ -606,18 +763,10 @@ class InvoiceService {
|
||||
}
|
||||
|
||||
async _loadCompanyTaxProfile(userId) {
|
||||
if (!userId) return null;
|
||||
const [rows] = await pool.query(
|
||||
`SELECT registration_number, atu_number, country
|
||||
FROM company_profiles
|
||||
WHERE user_id = ?
|
||||
LIMIT 1`,
|
||||
[userId],
|
||||
);
|
||||
return rows?.[0] || null;
|
||||
return this._loadInvoiceUserProfile(userId);
|
||||
}
|
||||
|
||||
async resolveTaxDecisionForSubscription({ buyerCountry, invoiceOwnerUserId }) {
|
||||
async resolveTaxDecisionForSubscription({ buyerCountry, invoiceOwnerUserId, invoiceOwnerProfile = null }) {
|
||||
const uow = new UnitOfWork();
|
||||
await uow.start();
|
||||
|
||||
@ -635,20 +784,22 @@ class InvoiceService {
|
||||
|
||||
await uow.commit();
|
||||
|
||||
const companyProfile = await this._loadCompanyTaxProfile(invoiceOwnerUserId);
|
||||
const companyProfile = invoiceOwnerProfile || await this._loadCompanyTaxProfile(invoiceOwnerUserId);
|
||||
const uidCandidate = companyProfile?.atu_number || companyProfile?.registration_number || '';
|
||||
const normalizedUid = this._normalizeUid(uidCandidate);
|
||||
const hasValidUid = this._isLikelyValidUid(normalizedUid);
|
||||
const countryCode = String(country?.country_code || '').toUpperCase();
|
||||
const invoiceUserType = this._normalizeInvoiceUserType(companyProfile?.user_type);
|
||||
|
||||
// Reverse charge for company customers with a valid UID outside seller country (AT).
|
||||
const isReverseCharge = Boolean(hasValidUid && countryCode && countryCode !== 'AT');
|
||||
const isReverseCharge = Boolean(invoiceUserType === 'company' && hasValidUid && countryCode && countryCode !== 'AT');
|
||||
|
||||
return {
|
||||
vatRate: isReverseCharge ? 0 : vatRate,
|
||||
isReverseCharge,
|
||||
countryCode: countryCode || null,
|
||||
uid: hasValidUid ? normalizedUid : null,
|
||||
userType: invoiceUserType,
|
||||
};
|
||||
} catch (e) {
|
||||
await uow.rollback();
|
||||
@ -675,20 +826,28 @@ class InvoiceService {
|
||||
periodEnd,
|
||||
});
|
||||
|
||||
const buyerName = [abonement.first_name, abonement.last_name].filter(Boolean).join(' ') || null;
|
||||
const buyerEmail = abonement.email || null;
|
||||
const userIdForInvoice = actorUserId ?? abonement.user_id ?? abonement.purchaser_user_id ?? null;
|
||||
const invoiceUserProfile = await this._loadInvoiceUserProfile(userIdForInvoice);
|
||||
const billingContext = await this._buildInvoiceBillingContext({
|
||||
abonement,
|
||||
invoiceUserId: userIdForInvoice,
|
||||
userProfile: invoiceUserProfile,
|
||||
});
|
||||
const buyerName = billingContext.customerName || null;
|
||||
const buyerEmail = billingContext.email || null;
|
||||
const addr = {
|
||||
street: abonement.street || null,
|
||||
postal_code: abonement.postal_code || null,
|
||||
city: abonement.city || null,
|
||||
country: abonement.country || null,
|
||||
street: billingContext.street || null,
|
||||
postal_code: billingContext.postalCode || null,
|
||||
city: billingContext.city || null,
|
||||
country: billingContext.country || null,
|
||||
};
|
||||
const currency = abonement.currency || 'EUR';
|
||||
|
||||
// CHANGED: resolve tax mode for this subscription (standard VAT vs reverse charge)
|
||||
const taxDecision = await this.resolveTaxDecisionForSubscription({
|
||||
buyerCountry: addr.country,
|
||||
invoiceOwnerUserId: actorUserId ?? abonement.user_id ?? abonement.purchaser_user_id ?? null,
|
||||
invoiceOwnerUserId: userIdForInvoice,
|
||||
invoiceOwnerProfile: invoiceUserProfile,
|
||||
});
|
||||
const vat_rate = taxDecision?.vatRate ?? null;
|
||||
|
||||
@ -703,11 +862,8 @@ class InvoiceService {
|
||||
tax_mode: taxDecision?.isReverseCharge ? 'reverse_charge' : 'standard',
|
||||
customer_country_code: taxDecision?.countryCode || null,
|
||||
uid_number: taxDecision?.uid || null,
|
||||
invoice_user_type: billingContext.userType,
|
||||
};
|
||||
|
||||
// CHANGED: prioritize token user id for invoice ownership
|
||||
const userIdForInvoice =
|
||||
actorUserId ?? abonement.user_id ?? abonement.purchaser_user_id ?? null;
|
||||
|
||||
console.log('[INVOICE ISSUE] Resolved user_id for invoice:', userIdForInvoice);
|
||||
|
||||
|
||||
@ -140,11 +140,12 @@ class DocumentTemplateService {
|
||||
}, uow.connection);
|
||||
}
|
||||
|
||||
// Enforce singleton active invoice template per lang
|
||||
// Enforce singleton active invoice template per (lang + user_type)
|
||||
if (state === 'active' && current.type === 'invoice') {
|
||||
await DocumentTemplateRepository.deactivateOtherActiveInvoices({
|
||||
excludeId: id,
|
||||
lang: current.lang,
|
||||
user_type: current.user_type || 'both',
|
||||
}, uow.connection);
|
||||
}
|
||||
|
||||
@ -199,12 +200,13 @@ class DocumentTemplateService {
|
||||
}, uow.connection);
|
||||
}
|
||||
|
||||
// If new template is active and is an invoice template, deactivate other active invoices for same lang
|
||||
// If new template is active and is an invoice template, deactivate other active invoices for same lang + user_type
|
||||
const effectiveType = data.type || previous.type;
|
||||
if (nextState === 'active' && effectiveType === 'invoice') {
|
||||
await DocumentTemplateRepository.deactivateOtherActiveInvoices({
|
||||
excludeId: created.id,
|
||||
lang: (data.lang || previous.lang),
|
||||
user_type: (data.user_type || previous.user_type || 'both'),
|
||||
}, uow.connection);
|
||||
}
|
||||
|
||||
|
||||
@ -157,6 +157,21 @@
|
||||
font-weight: 800;
|
||||
color: #1C2B4A;
|
||||
}
|
||||
.reverse-charge-note {
|
||||
background: #fff7ed;
|
||||
border: 1px solid #fdba74;
|
||||
border-radius: 8px;
|
||||
padding: 16px 18px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.reverse-charge-note p {
|
||||
font-size: 12px;
|
||||
color: #9a3412;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.is-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ── Payment Info ──────────────────────────── */
|
||||
.payment-info {
|
||||
@ -305,6 +320,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="reverse-charge-note {{reverseChargeClass}} {{reverseChargeSectionClass}}">
|
||||
<p>{{reverseChargeNoticeText}}</p>
|
||||
</div>
|
||||
|
||||
<!-- Payment Info -->
|
||||
<div class="payment-info">
|
||||
<h3>PAYMENT INFORMATION</h3>
|
||||
|
||||
@ -161,6 +161,20 @@
|
||||
margin: 0;
|
||||
color: #334155;
|
||||
}
|
||||
.reverse-charge-note {
|
||||
margin-bottom: 22px;
|
||||
padding: 15px 18px;
|
||||
border: 1px solid #fdba74;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(180deg, #fff7ed 0%, #fffbeb 100%);
|
||||
}
|
||||
.reverse-charge-note p {
|
||||
margin: 0;
|
||||
color: #9a3412;
|
||||
}
|
||||
.is-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@ -332,6 +346,10 @@
|
||||
<p><strong>{{taxLabel}}</strong> ({{vatRateDisplay}})</p>
|
||||
</div>
|
||||
|
||||
<div class="reverse-charge-note {{reverseChargeClass}} {{reverseChargeSectionClass}}">
|
||||
<p>{{reverseChargeNoticeText}}</p>
|
||||
</div>
|
||||
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@ -161,6 +161,20 @@
|
||||
margin: 0;
|
||||
color: #334155;
|
||||
}
|
||||
.reverse-charge-note {
|
||||
margin-bottom: 22px;
|
||||
padding: 15px 18px;
|
||||
border: 1px solid #fdba74;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(180deg, #fff7ed 0%, #fffbeb 100%);
|
||||
}
|
||||
.reverse-charge-note p {
|
||||
margin: 0;
|
||||
color: #9a3412;
|
||||
}
|
||||
.is-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@ -332,6 +346,10 @@
|
||||
<p><strong>{{taxLabel}}</strong> ({{vatRateDisplay}})</p>
|
||||
</div>
|
||||
|
||||
<div class="reverse-charge-note {{reverseChargeClass}} {{reverseChargeSectionClass}}">
|
||||
<p>{{reverseChargeNoticeText}}</p>
|
||||
</div>
|
||||
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user