Spaces:
Sleeping
Sleeping
Download src/services/emiService.js from riad-hf/rr-locker-api: direct link, hf CLI and curl.
- Browser
- Download file 5.13 kB
-
https://huggingface.co/spaces/riad-hf/rr-locker-api/resolve/main/src/services/emiService.js
- Command line
-
hf download hf://spaces/riad-hf/rr-locker-api/src/services/emiService.js
-
curl -L -o emiService.js https://huggingface.co/spaces/riad-hf/rr-locker-api/resolve/main/src/services/emiService.js
5.13 kB
| import { Op } from 'sequelize'; | |
| import crypto from 'crypto'; | |
| import { | |
| EMIContract, EMIPayment, Device, | |
| } from '../models/index.js'; | |
| import { DeviceService } from './deviceService.js'; | |
| import { utcNow } from '../utils/time.js'; | |
| /** Add whole months to a "YYYY-MM-DD" date string, clamping the day. */ | |
| function addMonths(dateStr, months, dayOverride = null) { | |
| const d = new Date(`${dateStr}T00:00:00Z`); | |
| const targetMonth = d.getUTCMonth() + months; | |
| const year = d.getUTCFullYear() + Math.floor(targetMonth / 12); | |
| const month = ((targetMonth % 12) + 12) % 12; | |
| let day = dayOverride != null ? dayOverride : d.getUTCDate(); | |
| const lastDay = new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); | |
| day = Math.min(day, lastDay); | |
| const mm = String(month + 1).padStart(2, '0'); | |
| const dd = String(day).padStart(2, '0'); | |
| return `${year}-${mm}-${dd}`; | |
| } | |
| function todayStr() { | |
| return new Date().toISOString().slice(0, 10); | |
| } | |
| function round2(n) { | |
| return Math.round((Number(n) + Number.EPSILON) * 100) / 100; | |
| } | |
| export const EMIService = { | |
| generateContractNumber() { | |
| const ts = new Date().toISOString().slice(0, 10).replace(/-/g, ''); | |
| return `EMI-${ts}-${crypto.randomUUID().replace(/-/g, '').slice(0, 8).toUpperCase()}`; | |
| }, | |
| calculateEmi(principal, rate, tenure) { | |
| const p = Number(principal); | |
| const r = Number(rate); | |
| if (r === 0) return p / tenure; | |
| const totalInterest = p * (r / 100) * (tenure / 12); | |
| const totalAmount = p + totalInterest; | |
| return round2(totalAmount / tenure); | |
| }, | |
| async createContract(data, userId = null) { | |
| const principal = round2(Number(data.product_price) - Number(data.down_payment)); | |
| const emiAmount = this.calculateEmi(principal, data.interest_rate, data.tenure_months); | |
| const totalAmount = round2(emiAmount * data.tenure_months); | |
| const endDate = addMonths(data.start_date, data.tenure_months); | |
| const contract = await EMIContract.create({ | |
| contract_number: this.generateContractNumber(), | |
| customer_id: data.customer_id, | |
| device_id: data.device_id, | |
| product_name: data.product_name, | |
| product_price: data.product_price, | |
| down_payment: data.down_payment, | |
| principal_amount: principal, | |
| interest_rate: data.interest_rate, | |
| tenure_months: data.tenure_months, | |
| emi_amount: emiAmount, | |
| total_amount: totalAmount, | |
| start_date: data.start_date, | |
| end_date: endDate, | |
| emi_due_day: data.emi_due_day, | |
| grace_period_days: data.grace_period_days, | |
| notes: data.notes ?? null, | |
| created_by: userId, | |
| }); | |
| const dueDay = Math.min(data.emi_due_day, 28); | |
| const payments = []; | |
| for (let i = 0; i < data.tenure_months; i += 1) { | |
| payments.push({ | |
| contract_id: contract.id, | |
| installment_number: i + 1, | |
| due_amount: emiAmount, | |
| due_date: addMonths(data.start_date, i + 1, dueDay), | |
| status: 'pending', | |
| }); | |
| } | |
| await EMIPayment.bulkCreate(payments); | |
| const device = await Device.findByPk(data.device_id); | |
| if (device) { | |
| device.customer_id = data.customer_id; | |
| await device.save(); | |
| } | |
| return contract; | |
| }, | |
| async recordPayment(paymentId, data, userId = null) { | |
| const payment = await EMIPayment.findByPk(paymentId); | |
| if (!payment) return null; | |
| payment.paid_amount = data.paid_amount; | |
| payment.late_fee = data.late_fee ?? 0; | |
| payment.payment_method = data.payment_method ?? null; | |
| payment.payment_reference = data.payment_reference ?? null; | |
| payment.paid_date = utcNow(); | |
| payment.recorded_by = userId; | |
| payment.status = Number(payment.paid_amount) >= Number(payment.due_amount) ? 'paid' : 'partial'; | |
| const contract = await EMIContract.findByPk(payment.contract_id); | |
| contract.total_paid = round2(Number(contract.total_paid) + Number(payment.paid_amount)); | |
| if (payment.status === 'paid') contract.emis_paid += 1; | |
| if (contract.emis_paid >= contract.tenure_months) { | |
| contract.status = 'completed'; | |
| const device = await Device.findByPk(contract.device_id); | |
| if (device && device.status === 'locked') { | |
| await DeviceService.updateDeviceStatus(device.id, 'active', userId, 'Contract completed'); | |
| } | |
| } | |
| await contract.save(); | |
| await payment.save(); | |
| return payment; | |
| }, | |
| getOverduePayments(asOfDate = null) { | |
| const cutoff = asOfDate || todayStr(); | |
| return EMIPayment.findAll({ | |
| where: { | |
| due_date: { [Op.lt]: cutoff }, | |
| status: { [Op.in]: ['PENDING', 'PARTIAL'] }, | |
| }, | |
| }); | |
| }, | |
| getPaymentsDueSoon(days = 3) { | |
| const today = todayStr(); | |
| const dueDate = addMonths(today, 0); // base | |
| const d = new Date(`${today}T00:00:00Z`); | |
| d.setUTCDate(d.getUTCDate() + days); | |
| const future = d.toISOString().slice(0, 10); | |
| return EMIPayment.findAll({ | |
| where: { | |
| due_date: { [Op.lte]: future, [Op.gte]: today }, | |
| status: 'PENDING', | |
| }, | |
| }); | |
| }, | |
| }; | |
| export default EMIService; | |