264 lines
8.8 KiB
JavaScript
264 lines
8.8 KiB
JavaScript
// composables/usePaymentCalculator.js
|
|
import { ref, computed } from 'vue';
|
|
|
|
export function usePaymentCalculator() {
|
|
// Inputs cơ bản
|
|
const originPrice = ref(0);
|
|
const discounts = ref([]); // [{ id, name, code, type: 1 (percent) | 2 (fixed), value: number }]
|
|
const paymentPlan = ref([]); // [{cycle, value, type: 1 (percent) | other (fixed), days, ...}]
|
|
const contractAllocationPercentage = ref(100);
|
|
const startDate = ref(new Date());
|
|
|
|
// Thanh toán sớm
|
|
const earlyPaymentCycles = ref(0);
|
|
const earlyDiscountRate = ref(0.0191780821918);
|
|
|
|
// Số tiền đã thanh toán thực tế
|
|
const paidAmount = ref(0);
|
|
|
|
// Tiền gốc
|
|
const originalPrice = computed(() => originPrice.value);
|
|
|
|
// Chi tiết chiết khấu
|
|
const detailedDiscounts = computed(() => {
|
|
const details = [];
|
|
let currentBalance = originPrice.value || 0;
|
|
|
|
discounts.value.forEach(discountData => {
|
|
const d = discountData;
|
|
let amount = 0;
|
|
if (d.type === 1) { // percent
|
|
amount = (currentBalance * Number(d.value)) / 100;
|
|
} else { // fixed
|
|
amount = Number(d.value);
|
|
}
|
|
if (currentBalance - amount < 0) {
|
|
amount = currentBalance;
|
|
}
|
|
currentBalance -= amount;
|
|
|
|
details.push({
|
|
id: d.id,
|
|
name: d.name,
|
|
code: d.code,
|
|
customValue: d.value,
|
|
customType: d.type,
|
|
amount: amount,
|
|
remaining: currentBalance
|
|
});
|
|
});
|
|
return details;
|
|
});
|
|
|
|
// Tổng tiền giảm giá từ discounts
|
|
const totalDiscount = computed(() => {
|
|
return detailedDiscounts.value.reduce((sum, d) => sum + d.amount, 0);
|
|
});
|
|
|
|
// Giá sau chiết khấu
|
|
const salePrice = computed(() => {
|
|
const lastDiscount = detailedDiscounts.value[detailedDiscounts.value.length - 1];
|
|
return lastDiscount ? lastDiscount.remaining : originPrice.value;
|
|
});
|
|
|
|
const allocatedAmount = computed(() => {
|
|
const amount = salePrice.value * (contractAllocationPercentage.value / 100);
|
|
return Math.round(amount / 1000) * 1000;
|
|
});
|
|
|
|
const finalTotal = computed(() => allocatedAmount.value);
|
|
|
|
// Lịch thanh toán gốc (hỗ trợ cả percent và fixed cho paymentPlan.type)
|
|
const originalPaymentSchedule = computed(() => {
|
|
const schedule = [];
|
|
let remaining = allocatedAmount.value;
|
|
|
|
const sortedPlan = [...paymentPlan.value].sort((a, b) => a.cycle - b.cycle);
|
|
|
|
let lastToDateForPeriodCalc = new Date(startDate.value); // Used to calculate the duration of each installment period.
|
|
|
|
sortedPlan.forEach(plan => {
|
|
let amount = 0;
|
|
|
|
// Xử lý linh hoạt theo type của Payment_Plan
|
|
if (plan.type === 1) {
|
|
// percent
|
|
amount = allocatedAmount.value * (plan.value / 100);
|
|
} else {
|
|
// fixed (type 2 hoặc bất kỳ giá trị khác)
|
|
amount = plan.value || 0;
|
|
}
|
|
|
|
const adjustedAmount = Math.min(amount, remaining);
|
|
remaining -= adjustedAmount;
|
|
|
|
const fromDate = new Date(startDate.value); // Per user request, all installments start from the same date.
|
|
const toDate = new Date(startDate.value);
|
|
toDate.setDate(toDate.getDate() + (plan.days || 0));
|
|
|
|
// The duration of the installment period is the difference between this installment's due date and the previous one.
|
|
const daysInPeriod = Math.round((toDate.getTime() - lastToDateForPeriodCalc.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
|
schedule.push({
|
|
cycle: plan.cycle,
|
|
from_date: fromDate,
|
|
to_date: toDate,
|
|
amount: adjustedAmount,
|
|
paid_amount: 0,
|
|
remain_amount: adjustedAmount,
|
|
days: daysInPeriod > 0 ? daysInPeriod : 0, // Use calculated period duration; it cannot be negative.
|
|
status: 1,
|
|
type: plan.type,
|
|
value: plan.value,
|
|
payment_note: `Thanh toán theo kế hoạch đợt số ${plan.cycle}`
|
|
});
|
|
|
|
lastToDateForPeriodCalc = new Date(toDate); // Update the last due date for the next period's duration calculation.
|
|
});
|
|
|
|
// Cộng phần dư (nếu có) vào đợt cuối
|
|
if (remaining > 0 && schedule.length > 0) {
|
|
const last = schedule[schedule.length - 1];
|
|
last.amount += remaining;
|
|
last.remain_amount += remaining;
|
|
}
|
|
|
|
return schedule;
|
|
});
|
|
|
|
// Chiết khấu thanh toán sớm
|
|
const earlyDiscounts = computed(() => {
|
|
if (earlyPaymentCycles.value <= 0) return [];
|
|
|
|
const schedule = originalPaymentSchedule.value;
|
|
const numEarly = Math.min(earlyPaymentCycles.value, schedule.length);
|
|
const actualPaymentDate = new Date(startDate.value);
|
|
const actualPaymentDateOnly = new Date(actualPaymentDate.getFullYear(), actualPaymentDate.getMonth(), actualPaymentDate.getDate());
|
|
|
|
return schedule.slice(0, numEarly).map(item => {
|
|
const originalDate = new Date(item.to_date);
|
|
const originalDateOnly = new Date(originalDate.getFullYear(), originalDate.getMonth(), originalDate.getDate());
|
|
const earlyDays = Math.round((originalDateOnly.getTime() - actualPaymentDateOnly.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
|
if (earlyDays <= 0) {
|
|
return 0;
|
|
}
|
|
return item.amount * earlyDays * (earlyDiscountRate.value / 100);
|
|
});
|
|
});
|
|
|
|
const totalEarlyDiscount = computed(() => {
|
|
const rawTotal = earlyDiscounts.value.reduce((sum, disc) => sum + disc, 0);
|
|
return parseInt(rawTotal);
|
|
});
|
|
|
|
// Chi tiết chiết khấu thanh toán sớm
|
|
const earlyDiscountDetails = computed(() => {
|
|
if (earlyPaymentCycles.value <= 0) return [];
|
|
|
|
const schedule = originalPaymentSchedule.value;
|
|
const numEarly = Math.min(earlyPaymentCycles.value, schedule.length);
|
|
const actualPaymentDate = new Date(startDate.value);
|
|
const actualPaymentDateOnly = new Date(actualPaymentDate.getFullYear(), actualPaymentDate.getMonth(), actualPaymentDate.getDate());
|
|
|
|
return schedule.slice(0, numEarly).map((item, index) => {
|
|
const originalDate = new Date(item.to_date);
|
|
const originalDateOnly = new Date(originalDate.getFullYear(), originalDate.getMonth(), originalDate.getDate());
|
|
const earlyDays = Math.round((originalDateOnly.getTime() - actualPaymentDateOnly.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
|
return {
|
|
cycle: item.cycle,
|
|
original_payment_date: originalDate,
|
|
actual_payment_date: actualPaymentDate,
|
|
early_days: earlyDays > 0 ? earlyDays : 0,
|
|
original_amount: item.amount,
|
|
discount_rate: earlyDiscountRate.value,
|
|
discount_amount: earlyDiscounts.value[index] || 0,
|
|
};
|
|
});
|
|
});
|
|
|
|
// Lịch sau thanh toán sớm (gộp đợt)
|
|
const scheduleAfterEarly = computed(() => {
|
|
const base = originalPaymentSchedule.value;
|
|
const numEarly = Math.min(earlyPaymentCycles.value, base.length);
|
|
|
|
if (numEarly <= 0) return base;
|
|
|
|
const earlyItems = base.slice(0, numEarly);
|
|
const totalEarlyAmount = earlyItems.reduce((sum, i) => sum + i.amount, 0);
|
|
const mergedAmount = totalEarlyAmount - totalEarlyDiscount.value;
|
|
|
|
const originalCycles = earlyItems.map(i => i.cycle);
|
|
|
|
const mergedItem = {
|
|
cycle: 1,
|
|
from_date: earlyItems[0].from_date,
|
|
to_date: earlyItems[0].from_date, // Due date is the start date for the merged installment
|
|
amount: Math.max(0, mergedAmount),
|
|
paid_amount: 0,
|
|
remain_amount: Math.max(0, mergedAmount),
|
|
payment_note: `Thanh toán gộp đợt ${originalCycles.join(', ')} (đã trừ số tiền chiết khấu thanh toán sớm là ${totalEarlyDiscount.value})`,
|
|
days: 0,
|
|
status: 1,
|
|
is_merged: true,
|
|
original_cycles: originalCycles
|
|
};
|
|
|
|
const remainingItems = base.slice(numEarly).map((item, idx) => ({
|
|
...item,
|
|
cycle: idx + 2,
|
|
}));
|
|
|
|
return [mergedItem, ...remainingItems];
|
|
});
|
|
|
|
// Lịch thanh toán cuối cùng
|
|
const finalPaymentSchedule = computed(() => {
|
|
const schedule = scheduleAfterEarly.value.map(item => ({ ...item }));
|
|
let remainingPaid = paidAmount.value || 0;
|
|
|
|
for (let i = 0; i < schedule.length && remainingPaid > 0; i++) {
|
|
const item = schedule[i];
|
|
const canPay = Math.min(remainingPaid, item.remain_amount);
|
|
|
|
item.paid_amount += canPay;
|
|
item.remain_amount -= canPay;
|
|
remainingPaid -= canPay;
|
|
|
|
if (item.remain_amount <= 0) {
|
|
item.status = 2;
|
|
item.remain_amount = 0;
|
|
}
|
|
}
|
|
|
|
return schedule;
|
|
});
|
|
|
|
const totalRemaining = computed(() =>
|
|
finalPaymentSchedule.value.reduce((sum, item) => sum + item.remain_amount, 0)
|
|
);
|
|
|
|
return {
|
|
originPrice,
|
|
discounts,
|
|
paymentPlan,
|
|
contractAllocationPercentage,
|
|
startDate,
|
|
earlyPaymentCycles,
|
|
earlyDiscountRate,
|
|
paidAmount,
|
|
|
|
originalPrice,
|
|
detailedDiscounts,
|
|
totalDiscount,
|
|
salePrice,
|
|
allocatedAmount,
|
|
finalTotal,
|
|
finalPaymentSchedule,
|
|
originalPaymentSchedule,
|
|
earlyDiscountDetails,
|
|
totalEarlyDiscount,
|
|
totalRemaining,
|
|
};
|
|
} |