Contrat résidentiel: conformité CRTC 2026-43 (fin du clawback)
Installation = vraie créance financée (champ install_fee : 240$ std → 10$/mois, 120$ simple → 5$/mois). Résiliation résidentielle ne facture que le solde restant de l'install (service rendu) ; promotions/carte-cadeau jamais récupérées (termination_fee_benefits=0). Récap reformulé (financement install, plus de "promotion non étalée"). Conforme avant l'échéance du 12 juin. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ba76b8f3cc
commit
45c31e555f
|
|
@ -62,15 +62,21 @@ function calculateTerminationFee (contract) {
|
|||
termination_fee_total: fee,
|
||||
}
|
||||
} else {
|
||||
// Résidentiel: only benefit residual value (+ current month max)
|
||||
fee = totalBenefitRemaining
|
||||
// Résidentiel (CRTC 2026-43) : AUCUN clawback de promotion. On ne facture
|
||||
// que le SOLDE de l'installation financée — un service déjà rendu, donc
|
||||
// facturable. Les promotions/carte-cadeau ne sont JAMAIS récupérées.
|
||||
const installFee = contract.install_fee || 0
|
||||
const installRemaining = round2(installFee * monthsRemaining / (contract.duration_months || 24))
|
||||
fee = installRemaining
|
||||
feeBreakdown = {
|
||||
type: 'residential',
|
||||
months_remaining: monthsRemaining,
|
||||
monthly_rate: monthlyRate,
|
||||
termination_fee_benefits: totalBenefitRemaining,
|
||||
termination_fee_remaining: 0,
|
||||
termination_fee_total: totalBenefitRemaining,
|
||||
install_fee: installFee,
|
||||
install_remaining: installRemaining,
|
||||
termination_fee_benefits: 0,
|
||||
termination_fee_remaining: installRemaining,
|
||||
termination_fee_total: installRemaining,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -180,6 +186,7 @@ async function handle (req, res, method, path) {
|
|||
end_date: endDate,
|
||||
duration_months: duration,
|
||||
monthly_rate: body.monthly_rate || 0,
|
||||
install_fee: body.install_fee || 0,
|
||||
service_location: body.service_location || '',
|
||||
quotation: body.quotation || '',
|
||||
subscription: body.subscription || '',
|
||||
|
|
@ -878,15 +885,15 @@ async function createTerminationInvoice (contract, calc, reason) {
|
|||
income_account: '', // will use default
|
||||
})
|
||||
} else {
|
||||
// Residential: charge benefit residual
|
||||
for (const b of calc.benefits) {
|
||||
if (b.remaining_value <= 0) continue
|
||||
// Résidentiel : solde de l'installation financée (service rendu) — PAS une pénalité.
|
||||
if (calc.install_remaining > 0) {
|
||||
items.push({
|
||||
item_name: `Résiliation — ${b.description}`,
|
||||
description: `Valeur résiduelle: ${b.benefit_value}$ - ${b.months_recognized} mois reconnus ` +
|
||||
`(${b.monthly_recognition}$/mois). Restant: ${b.remaining_value}$`,
|
||||
item_name: "Solde de l'installation (financement restant)",
|
||||
description: `Installation ${calc.install_fee}$ financée sur ${contract.duration_months || 24} mois. ` +
|
||||
`Solde restant après ${calc.months_elapsed} mois : ${calc.install_remaining}$. ` +
|
||||
`Service déjà réalisé — aucune pénalité de résiliation, aucune promotion récupérée.`,
|
||||
qty: 1,
|
||||
rate: b.remaining_value,
|
||||
rate: calc.install_remaining,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -931,9 +938,8 @@ function renderAcceptancePage (contract, token) {
|
|||
const TARGO_LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 326.39 70.35" width="130" height="28"><path fill="#019547" d="M25.83,15H8.41a4.14,4.14,0,0,1-3.89-2.71L0,1.18H66.59L62.07,12.27A4.14,4.14,0,0,1,58.18,15H40.76V69.19H25.81Z"/><path fill="#019547" d="M90.74.68h18.55a5.63,5.63,0,0,1,5.63,5.6l.26,62.91H99.55L99.76,54H71.87L66.41,65.51a6.45,6.45,0,0,1-5.84,3.68H49.35L73.53,12.11A18.7,18.7,0,0,1,90.74.68M100,40.73l.07-26h-8a6.75,6.75,0,0,0-6.26,4.18L76.73,40.73Z"/><path fill="#019547" d="M124.51,6.81a5.64,5.64,0,0,1,5.65-5.65H155.6c8.64,0,15.37,2.44,19.81,6.91,3.81,3.78,5.84,9.14,5.84,15.53v.18c0,11-5.92,17.87-14.59,21.1l16.61,24.29h-14a6.51,6.51,0,0,1-5.39-2.87L151.21,47.41H139.44V69.17h-15Zm30.12,27.41c7.28,0,11.45-3.89,11.45-9.62v-.21c0-6.42-4.46-9.7-11.74-9.7H139.46V34.22Z"/><path fill="#019547" d="M184.74,35.37v-.18C184.74,15.85,199.8,0,220.4,0,232.65,0,240,3.31,247.13,9.33l-6.28,7.57a5.18,5.18,0,0,1-6.84,1,23.71,23.71,0,0,0-14.11-4.13c-10.88,0-19.52,9.62-19.52,21.18v.19c0,12.43,8.54,21.57,20.6,21.57a24,24,0,0,0,14.09-4.07V42.91h-8.68A6.41,6.41,0,0,1,220,36.53V30h29.54V59.52a44,44,0,0,1-29,10.78c-21.18,0-35.74-14.8-35.74-34.93"/><path fill="#019547" d="M254.09,35.37v-.18C254.09,15.85,269.33,0,290.33,0s36.06,15.66,36.06,35v.18c0,19.34-15.27,35.19-36.24,35.19s-36.06-15.64-36.06-35m56.63,0v-.18c0-11.67-8.54-21.39-20.6-21.39S269.73,23.34,269.73,35v.18c0,11.67,8.54,21.37,20.6,21.37S310.72,47,310.72,35.37"/></svg>`
|
||||
|
||||
const isCommercial = contract.contract_type === 'Commercial'
|
||||
const penaltyText = isCommercial
|
||||
? `la totalité des mensualités restantes au terme (${contract.monthly_rate || 0} $/mois × mois restants)`
|
||||
: `la portion non étalée de la promotion, au prorata des mois restants sur ${duration} mois`
|
||||
const installFee = contract.install_fee || 0
|
||||
const installMonthly = installFee ? round2(installFee / duration) : 0
|
||||
|
||||
return `<!DOCTYPE html><html lang="fr"><head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
|
@ -992,6 +998,7 @@ tfoot td{font-weight:700;color:#00733a;background:#f4fff6;border-top:1px solid #
|
|||
|
||||
<div class="field"><span class="label">Client</span><span class="value">${escapeHtml(contract.customer_name || contract.customer || '')}</span></div>
|
||||
<div class="field"><span class="label">Mensualité</span><span class="value">${contract.monthly_rate || 0} $/mois <span style="color:#9ca3af;font-weight:400;font-size:12px">(+taxes)</span></span></div>
|
||||
${installFee ? `<div class="field"><span class="label">Installation (financée sur ${duration} mois)</span><span class="value">${installMonthly} $/mois <span style="color:#9ca3af;font-weight:400;font-size:12px">(${installFee} $ — service)</span></span></div>` : ''}
|
||||
<div class="field"><span class="label">Durée</span><span class="value">${duration} mois</span></div>
|
||||
<div class="field"><span class="label">Début prévu</span><span class="value">${contract.start_date || 'À déterminer'}</span></div>
|
||||
|
||||
|
|
@ -1000,14 +1007,15 @@ tfoot td{font-weight:700;color:#00733a;background:#f4fff6;border-top:1px solid #
|
|||
<table>
|
||||
<tbody>${benefitRows}</tbody>
|
||||
<tfoot>
|
||||
<tr><td>Valeur totale de la promotion</td><td class="r">${round2(totalBenefit)} $</td></tr>
|
||||
<tr><td colspan="2" style="font-weight:400;color:#00733a;font-size:12px;text-align:left;">Étalée sur ${duration} mois</td></tr>
|
||||
<tr><td>Valeur des promotions (non récupérables)</td><td class="r">${round2(totalBenefit)} $</td></tr>
|
||||
</tfoot>
|
||||
</table>` : ''}
|
||||
|
||||
<div class="callout">
|
||||
<div class="head">Changement avant ${duration} mois ?</div>
|
||||
Pas de pénalité. On récupère simplement ${penaltyText}. Rien de plus.
|
||||
${isCommercial
|
||||
? `Les mensualités restantes du terme demeurent dues (${contract.monthly_rate || 0} $/mois × mois restants).`
|
||||
: `<strong>Aucune pénalité de résiliation.</strong> ${installFee ? `L'installation (${installFee} $) est financée sur ${duration} mois : seul le <strong>solde restant de l'installation</strong> — un service déjà réalisé — demeure dû. ` : ''}Vos promotions et la carte-cadeau ne sont <strong>jamais récupérées</strong> ; le rabais nouveau client cesse simplement.`}
|
||||
</div>
|
||||
|
||||
<div class="legal">
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user