Refactor invoice PDF generation and VAT validation

- Updated PDF template to display a fixed software code instead of "SOFTWARE".
- Changed VAT label to include tax label dynamically and set to 0% for certain conditions.
- Enhanced JavaScript for VAT number validation with asynchronous checks against the VIES database.
- Implemented debounce for VAT number input to optimize validation calls.
- Updated country settings to include country codes for VAT validation.
- Modified email sending functions in webhook handlers to use dynamic attachment names for invoices.
This commit is contained in:
“VeLiTi”
2026-02-06 16:02:56 +01:00
parent 4b83f596f1
commit 3131c2c5b2
12 changed files with 542 additions and 163 deletions

View File

@@ -1482,6 +1482,48 @@ function showPaymentModal(option) {
modal.appendChild(modalContent);
document.body.appendChild(modal);
// VAT number validation state
let vatValidationInProgress = false;
let vatValidationResult = null;
// Function to check VAT number against VIES database (via server-side proxy)
async function checkVATNumber(countryCode, vatNumber) {
if (!countryCode || !vatNumber) {
return null;
}
// Use server-side proxy to avoid CORS issues
const serviceToken = document.getElementById("servicetoken")?.innerHTML || '';
const url = link + '/v2/vat_check';
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer ' + serviceToken
},
body: JSON.stringify({
countryCode: countryCode,
vatNumber: vatNumber
})
});
if (!response.ok) {
console.warn('VAT check HTTP error:', response.status);
return null;
}
const data = await response.json();
console.log('VIES response:', data);
return data;
} catch (error) {
console.error('Error checking VAT:', error);
return null;
}
}
// Function to calculate and update tax
function updateTaxDisplay() {
const selectedCountry = document.getElementById("paymentCountry").value;
@@ -1500,12 +1542,12 @@ function showPaymentModal(option) {
// Netherlands: always take the tax percentage
taxRate = countryTaxRate;
} else if (isEU) {
if (vatNumber) {
// EU with VAT number: 0% VAT, reverse charge
if (vatNumber && vatValidationResult && vatValidationResult.valid === true) {
// EU with VALID VAT number: 0% VAT, reverse charge
taxRate = 0;
vatNote = 'Reverse charge VAT';
} else {
// EU without VAT number: use country VAT
// EU without VAT number or invalid VAT: use country VAT
taxRate = countryTaxRate;
vatNote = 'Local VAT';
}
@@ -1564,9 +1606,112 @@ function showPaymentModal(option) {
}
}
// Debounce timer for VAT validation
let vatValidationTimeout = null;
// Function to validate VAT number with visual feedback
async function validateVATNumber() {
const selectedCountry = document.getElementById("paymentCountry").value;
const vatNumber = document.getElementById("paymentVatNumber").value.trim();
const vatInput = document.getElementById("paymentVatNumber");
// Reset validation state
vatValidationResult = null;
vatInput.style.borderColor = '';
// Remove any existing validation message
const existingMessage = document.getElementById('vatValidationMessage');
if (existingMessage) {
existingMessage.remove();
}
if (!vatNumber) {
updateTaxDisplay();
return;
}
if (!selectedCountry || typeof COUNTRIES === 'undefined' || !COUNTRIES) {
updateTaxDisplay();
return;
}
const countryData = Object.values(COUNTRIES).find(c => c.country === selectedCountry);
if (!countryData || countryData.eu !== 1 || !countryData.country_code) {
updateTaxDisplay();
return;
}
// For Netherlands, don't validate VAT (always apply VAT)
if (selectedCountry === 'Netherlands') {
updateTaxDisplay();
return;
}
// Show validating state
vatInput.style.borderColor = '#ffc107';
vatValidationInProgress = true;
const validationMsg = document.createElement('div');
validationMsg.id = 'vatValidationMessage';
validationMsg.style.cssText = 'margin-top: 5px; font-size: 12px; color: #ffc107;';
validationMsg.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Validating VAT number...';
vatInput.parentNode.appendChild(validationMsg);
// Call VIES API
const result = await checkVATNumber(countryData.country_code, vatNumber);
vatValidationInProgress = false;
if (result && result.valid === true) {
// VAT number is valid
vatValidationResult = result;
vatInput.style.borderColor = '#28a745';
validationMsg.style.color = '#28a745';
validationMsg.innerHTML = '<i class="fa-solid fa-check-circle"></i> Valid VAT number';
// Format VAT number as CountryCode + VatNumber (e.g., DE115235681)
const formattedVAT = result.countryCode + result.vatNumber;
if (vatInput.value !== formattedVAT) {
vatInput.value = formattedVAT;
}
} else {
// VAT number is invalid or check failed
vatValidationResult = null;
vatInput.style.borderColor = '#dc3545';
validationMsg.style.color = '#dc3545';
validationMsg.innerHTML = '<i class="fa-solid fa-times-circle"></i> Invalid VAT number or validation failed';
}
// Update tax display with new validation result
updateTaxDisplay();
}
// Add event listeners to country select and VAT number to update tax
document.getElementById("paymentCountry").addEventListener('change', updateTaxDisplay);
document.getElementById("paymentVatNumber").addEventListener('input', updateTaxDisplay);
document.getElementById("paymentCountry").addEventListener('change', () => {
vatValidationResult = null;
const vatInput = document.getElementById("paymentVatNumber");
vatInput.style.borderColor = '';
const existingMessage = document.getElementById('vatValidationMessage');
if (existingMessage) {
existingMessage.remove();
}
updateTaxDisplay();
// Validate VAT if already entered
if (vatInput.value.trim()) {
if (vatValidationTimeout) {
clearTimeout(vatValidationTimeout);
}
vatValidationTimeout = setTimeout(validateVATNumber, 500);
}
});
document.getElementById("paymentVatNumber").addEventListener('input', () => {
// Debounce VAT validation
if (vatValidationTimeout) {
clearTimeout(vatValidationTimeout);
}
vatValidationTimeout = setTimeout(validateVATNumber, 1000);
});
// Close modal on cancel
document.getElementById("cancelPayment").onclick = () => {