"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
var FeesService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.FeesService = void 0;
const common_1 = require("@nestjs/common");
const mongoose_1 = require("@nestjs/mongoose");
const mongoose_2 = require("mongoose");
const crypto = __importStar(require("crypto"));
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const crypto_1 = require("crypto");
const razorpay_1 = __importDefault(require("razorpay"));
const timing_safe_equal_1 = require("../common/utils/timing-safe-equal");
const url_helper_1 = require("../common/helpers/url.helper");
const fee_structure_schema_1 = require("./schemas/fee-structure.schema");
const fee_assignment_schema_1 = require("./schemas/fee-assignment.schema");
const fee_payment_schema_1 = require("./schemas/fee-payment.schema");
const student_schema_1 = require("../students/schemas/student.schema");
const academic_year_schema_1 = require("../academic-year/academic-year.schema");
const invoices_service_1 = require("../invoices/invoices.service");
const fees_installment_dates_1 = require("./fees.installment-dates");
const MONTHS = [
    '', 'January', 'February', 'March', 'April', 'May', 'June',
    'July', 'August', 'September', 'October', 'November', 'December',
];
let FeesService = FeesService_1 = class FeesService {
    structureModel;
    assignmentModel;
    studentModel;
    academicYearModel;
    feePaymentModel;
    invoicesService;
    logger = new common_1.Logger(FeesService_1.name);
    razor = null;
    razorpayKeyId;
    razorpayKeySecret;
    razorpayWebhookSecret;
    constructor(structureModel, assignmentModel, studentModel, academicYearModel, feePaymentModel, invoicesService) {
        this.structureModel = structureModel;
        this.assignmentModel = assignmentModel;
        this.studentModel = studentModel;
        this.academicYearModel = academicYearModel;
        this.feePaymentModel = feePaymentModel;
        this.invoicesService = invoicesService;
        this.razorpayKeyId = process.env.RAZORPAY_KEY_ID || '';
        this.razorpayKeySecret = process.env.RAZORPAY_KEY_SECRET || '';
        this.razorpayWebhookSecret = process.env.RAZORPAY_WEBHOOK_SECRET || '';
    }
    getRazorpay() {
        if (this.razor)
            return this.razor;
        if (!this.razorpayKeyId || !this.razorpayKeySecret) {
            throw new common_1.BadRequestException('Online payments are not configured. Set RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET.');
        }
        this.razor = new razorpay_1.default({ key_id: this.razorpayKeyId, key_secret: this.razorpayKeySecret });
        return this.razor;
    }
    async createStructure(dto) {
        const classOverlap = dto.classIds.includes('all')
            ? {}
            : { $or: [{ classIds: { $in: dto.classIds } }, { classIds: 'all' }] };
        const existing = await this.structureModel.findOne({
            name: dto.name.trim(),
            academicYearId: dto.academicYearId,
            feeType: dto.feeType,
            ...classOverlap,
        }).lean();
        if (existing) {
            throw new common_1.BadRequestException('A fee structure with this name, type, and overlapping class already exists.');
        }
        const structure = await this.structureModel.create({
            name: dto.name.trim(),
            feeType: dto.feeType,
            academicYearId: dto.academicYearId,
            amount: dto.amount,
            classIds: dto.classIds,
            dueMonth: dto.dueMonth ?? 1,
            dueDay: dto.dueDay,
            startDate: dto.startDate ? new Date(dto.startDate) : undefined,
            endDate: dto.endDate ? new Date(dto.endDate) : undefined,
            enableSubFees: dto.enableSubFees ?? false,
            subFees: dto.subFees ?? [],
            lateFine: dto.lateFine ?? { enabled: false, type: 'per_day', amountPerDay: 0, percentage: 0, slabs: [] },
            isActive: dto.isActive ?? true,
        });
        const assignmentStats = await this.generateAssignments(structure.toObject());
        return {
            ...structure.toObject(),
            assignmentStats,
        };
    }
    async getStructures(academicYearId, classId, activeOnly) {
        const q = {};
        if (academicYearId)
            q.academicYearId = academicYearId;
        if (classId)
            q.$or = [{ classIds: classId }, { classIds: 'all' }];
        if (activeOnly)
            q.isActive = true;
        return this.structureModel.find(q).sort({ createdAt: -1 }).limit(500).lean();
    }
    async getStructure(id) {
        const s = await this.structureModel.findById(id).lean();
        if (!s)
            throw new common_1.NotFoundException('Fee structure not found');
        return s;
    }
    async updateStructure(id, dto) {
        if (dto.academicYearId || dto.feeType) {
            throw new common_1.BadRequestException('Cannot change academic year or fee type. Delete and recreate the structure instead.');
        }
        const paidOrPartialCount = await this.assignmentModel.countDocuments({
            feeStructureId: id,
            status: { $in: ['paid', 'partial'] },
        });
        if (paidOrPartialCount > 0) {
            throw new common_1.BadRequestException('Cannot update fee structure — some students have already paid.');
        }
        const { academicYearId: _ay, feeType: _ft, ...safeDto } = dto;
        if (safeDto.startDate !== undefined) {
            safeDto.startDate = safeDto.startDate ? new Date(safeDto.startDate) : undefined;
        }
        if (safeDto.endDate !== undefined) {
            safeDto.endDate = safeDto.endDate ? new Date(safeDto.endDate) : undefined;
        }
        const updated = await this.structureModel
            .findByIdAndUpdate(id, { $set: safeDto }, { new: true, runValidators: true })
            .lean();
        if (!updated)
            throw new common_1.NotFoundException('Fee structure not found');
        await this.regenerateUnpaidAssignments(id);
        return updated;
    }
    async deleteStructure(id) {
        const structure = await this.structureModel.findById(id).lean();
        if (!structure)
            throw new common_1.NotFoundException('Fee structure not found');
        const paidOrPartialCount = await this.assignmentModel.countDocuments({
            feeStructureId: id,
            status: { $in: ['paid', 'partial'] },
        });
        if (paidOrPartialCount > 0) {
            throw new common_1.BadRequestException('Cannot delete fee structure — some students have already paid.');
        }
        await this.assignmentModel.deleteMany({ feeStructureId: id, status: { $nin: ['paid', 'partial'] } });
        const racePaid = await this.assignmentModel.countDocuments({ feeStructureId: id, status: { $in: ['paid', 'partial'] } });
        if (racePaid > 0) {
            throw new common_1.BadRequestException('A payment was recorded while deleting. The fee structure has been preserved.');
        }
        await this.structureModel.findByIdAndDelete(id);
        return { deleted: true };
    }
    async duplicateStructure(sourceId, targetAcademicYearId) {
        const source = await this.structureModel.findById(sourceId).lean();
        if (!source)
            throw new common_1.NotFoundException('Source fee structure not found');
        if (targetAcademicYearId === source.academicYearId) {
            throw new common_1.BadRequestException('Cannot duplicate to the same academic year');
        }
        const targetAy = await this.academicYearModel.findById(targetAcademicYearId).lean();
        if (!targetAy)
            throw new common_1.NotFoundException('Target academic year not found');
        const classOverlap = source.classIds.includes('all')
            ? {}
            : { $or: [{ classIds: { $in: source.classIds } }, { classIds: 'all' }] };
        const existing = await this.structureModel.findOne({
            name: source.name,
            academicYearId: targetAcademicYearId,
            feeType: source.feeType,
            ...classOverlap,
        }).lean();
        if (existing) {
            throw new common_1.BadRequestException('A fee structure with this name, type, and overlapping class already exists in the target year.');
        }
        const newStructure = await this.structureModel.create({
            name: source.name,
            feeType: source.feeType,
            academicYearId: targetAcademicYearId,
            amount: source.amount,
            classIds: source.classIds,
            dueMonth: source.dueMonth,
            dueDay: source.dueDay,
            enableSubFees: source.enableSubFees,
            subFees: source.subFees,
            lateFine: source.lateFine,
            isActive: true,
        });
        const { docsCreated, studentCount } = await this.generateAssignments(newStructure.toObject());
        return {
            structure: newStructure.toObject(),
            assignmentsGenerated: docsCreated,
            studentCount,
        };
    }
    getActiveDiscountAmount(assignment) {
        if (!assignment.discountType || !assignment.discountAmount)
            return 0;
        if ((assignment.paidAmount || 0) > 0)
            return assignment.discountAmount || 0;
        if (assignment.discountValidUntil && new Date(assignment.discountValidUntil) < new Date())
            return 0;
        return assignment.discountAmount || 0;
    }
    calculateDiscountAmount(type, value, feeAmount) {
        if (type === 'amount')
            return Math.min(value, feeAmount);
        return Math.round(feeAmount * Math.min(value, 100) / 100);
    }
    async enrichAssignments(assignments) {
        return assignments.map((a) => {
            const activeDiscount = this.getActiveDiscountAmount(a);
            const discountActive = activeDiscount > 0;
            let netPayable;
            if (a.status === 'paid') {
                netPayable = 0;
            }
            else {
                netPayable = a.amount + (a.fineAmount || 0) - activeDiscount - (a.paidAmount || 0);
            }
            return {
                ...a,
                discountActive,
                netPayable: Math.max(0, netPayable),
                collectedByName: a.collectedBy ? 'Admin' : undefined,
            };
        });
    }
    async applyDiscount(assignmentId, dto, adminId) {
        if (dto.discountType === 'percentage' && dto.discountValue > 100) {
            throw new common_1.BadRequestException('Percentage discount cannot exceed 100%');
        }
        if (dto.discountValidUntil) {
            const validUntil = new Date(dto.discountValidUntil);
            if (isNaN(validUntil.getTime()))
                throw new common_1.BadRequestException('Invalid validity date');
            if (validUntil < new Date())
                throw new common_1.BadRequestException('Validity date must be in the future');
        }
        const assignment = await this.assignmentModel.findOne({ _id: assignmentId, status: { $in: ['pending', 'partial'] } }).lean();
        if (!assignment)
            throw new common_1.NotFoundException('Unpaid fee assignment not found');
        const discountAmount = this.calculateDiscountAmount(dto.discountType, dto.discountValue, assignment.amount);
        const updated = await this.assignmentModel.findOneAndUpdate({ _id: assignmentId, status: { $in: ['pending', 'partial'] } }, {
            $set: {
                discountType: dto.discountType,
                discountValue: dto.discountValue,
                discountAmount,
                discountReason: dto.discountReason || undefined,
                discountValidUntil: dto.discountValidUntil ? new Date(dto.discountValidUntil) : undefined,
                discountAppliedBy: adminId,
            },
        }, { new: true }).lean();
        if (!updated)
            throw new common_1.BadRequestException('Assignment was paid while applying discount');
        const [enriched] = await this.enrichAssignments([updated]);
        return enriched;
    }
    async updateDiscount(assignmentId, dto, adminId) {
        const assignment = await this.assignmentModel.findOne({ _id: assignmentId, status: { $in: ['pending', 'partial'] } }).lean();
        if (!assignment)
            throw new common_1.NotFoundException('Unpaid fee assignment not found');
        if (!assignment.discountType)
            throw new common_1.BadRequestException('No discount exists on this assignment');
        if (!dto.discountType && !dto.discountValue && !dto.discountReason && dto.discountValidUntil === undefined) {
            throw new common_1.BadRequestException('At least one field must be provided');
        }
        const $set = { discountAppliedBy: adminId };
        const effectiveType = dto.discountType || assignment.discountType;
        const effectiveValue = dto.discountValue ?? assignment.discountValue;
        if (effectiveType === 'percentage' && effectiveValue > 100) {
            throw new common_1.BadRequestException('Percentage discount cannot exceed 100%');
        }
        if (dto.discountType)
            $set.discountType = dto.discountType;
        if (dto.discountValue !== undefined)
            $set.discountValue = dto.discountValue;
        if (dto.discountReason !== undefined)
            $set.discountReason = dto.discountReason;
        if (dto.discountType || dto.discountValue !== undefined) {
            $set.discountAmount = this.calculateDiscountAmount(effectiveType, effectiveValue, assignment.amount);
        }
        const $unset = {};
        if (dto.discountValidUntil === null) {
            $unset.discountValidUntil = 1;
        }
        else if (dto.discountValidUntil) {
            const validUntil = new Date(dto.discountValidUntil);
            if (isNaN(validUntil.getTime()))
                throw new common_1.BadRequestException('Invalid validity date');
            if (validUntil < new Date())
                throw new common_1.BadRequestException('Validity date must be in the future');
            $set.discountValidUntil = validUntil;
        }
        const updateOp = { $set };
        if (Object.keys($unset).length > 0)
            updateOp.$unset = $unset;
        const updated = await this.assignmentModel.findOneAndUpdate({ _id: assignmentId, status: { $in: ['pending', 'partial'] } }, updateOp, { new: true }).lean();
        if (!updated)
            throw new common_1.BadRequestException('Assignment was paid while updating discount');
        const [enriched] = await this.enrichAssignments([updated]);
        return enriched;
    }
    async removeDiscount(assignmentId) {
        const assignment = await this.assignmentModel.findOne({ _id: assignmentId, status: { $in: ['pending', 'partial'] } }).lean();
        if (!assignment)
            throw new common_1.NotFoundException('Unpaid fee assignment not found');
        const updated = await this.assignmentModel.findOneAndUpdate({ _id: assignmentId, status: { $in: ['pending', 'partial'] } }, {
            $unset: {
                discountType: 1,
                discountValue: 1,
                discountReason: 1,
                discountValidUntil: 1,
                discountAppliedBy: 1,
            },
            $set: { discountAmount: 0 },
        }, { new: true }).lean();
        if (!updated)
            throw new common_1.BadRequestException('Assignment was paid while removing discount');
        const [enriched] = await this.enrichAssignments([updated]);
        return enriched;
    }
    async getAssignments(filter) {
        const q = {};
        if (filter.academicYearId)
            q.academicYearId = filter.academicYearId;
        if (filter.classId)
            q.classId = filter.classId;
        if (filter.studentId)
            q.studentId = filter.studentId;
        if (filter.feeStructureId)
            q.feeStructureId = filter.feeStructureId;
        if (filter.status) {
            if (filter.status === 'overdue') {
                q.status = { $in: ['pending', 'partial'] };
                q.dueDate = { $lt: new Date() };
            }
            else if (filter.status === 'pending') {
                q.status = 'pending';
                q.dueDate = { $gte: new Date() };
            }
            else {
                q.status = filter.status;
            }
        }
        const sortOrder = filter.status === 'paid' ? { paidAt: -1 } : { dueDate: 1 };
        const assignments = await this.assignmentModel.find(q).sort(sortOrder).limit(5000).lean();
        const withFines = await this.recalculateFines(assignments);
        const enriched = await this.enrichAssignments(withFines);
        const studentIds = [...new Set(enriched.map((a) => a.studentId))];
        if (studentIds.length > 0) {
            const students = await this.studentModel
                .find({ _id: { $in: studentIds } }, { name: 1, uniqueId: 1, rollNumber: 1 })
                .lean();
            const studentMap = new Map(students.map((s) => [String(s._id), s]));
            return enriched.map((a) => {
                const stu = studentMap.get(a.studentId);
                return { ...a, studentName: stu?.name, studentUniqueId: stu?.uniqueId, studentRollNumber: stu?.rollNumber };
            });
        }
        return enriched;
    }
    async getStudentDashboard(studentId, academicYearId) {
        const student = await this.studentModel.findById(studentId, {
            name: 1, classId: 1, academicYearId: 1, rollNumber: 1,
            sectionId: 1, documents: 1, profileImageUrl: 1, uniqueId: 1,
            guardianName: 1, father: 1, mother: 1, phone: 1, email: 1,
        }).lean();
        if (!student)
            throw new common_1.NotFoundException('Student not found');
        const ayId = academicYearId || student.academicYearId;
        if (student.classId) {
            await this.syncStudentAssignments(studentId, String(student.classId), ayId);
        }
        const raw = await this.assignmentModel
            .find({ studentId, academicYearId: ayId })
            .sort({ dueDate: 1 })
            .lean();
        const withFines = await this.recalculateFines(raw);
        const assignments = await this.enrichAssignments(withFines);
        const totalFee = assignments.reduce((s, a) => s + a.amount + (a.fineAmount || 0), 0);
        const totalPaid = assignments.reduce((s, a) => {
            if (a.status === 'paid') {
                const pa = a.paidAmount || 0;
                if (pa > 0)
                    return s + pa;
                return s + a.amount + (a.fineAmount || 0) - (a.discountAmount || 0);
            }
            return s + (a.paidAmount || 0);
        }, 0);
        const paidDiscount = assignments
            .filter((a) => a.status === 'paid')
            .reduce((s, a) => s + (a.discountAmount || 0), 0);
        const totalDiscount = assignments
            .filter((a) => a.status !== 'paid')
            .reduce((s, a) => s + (a.discountActive ? (a.discountAmount || 0) : 0), 0);
        const dueAmount = Math.max(0, totalFee - totalPaid - totalDiscount - paidDiscount);
        const unpaid = assignments.filter((a) => a.status !== 'paid');
        const nextDueDate = unpaid.length > 0
            ? new Date(Math.min(...unpaid.map((a) => new Date(a.dueDate).getTime()))).toISOString()
            : null;
        return {
            student,
            summary: { totalFee, totalDiscount, paidAmount: totalPaid, dueAmount, nextDueDate },
            assignments,
        };
    }
    async createRazorpayOrder(assignmentId, studentId, requestedAmount) {
        const assignment = await this.assignmentModel.findOne({
            _id: assignmentId,
            studentId,
        });
        if (!assignment)
            throw new common_1.NotFoundException('Fee assignment not found');
        if (assignment.status === 'paid') {
            throw new common_1.BadRequestException('This fee is already paid.');
        }
        const fine = await this.calculateFineForAssignment(assignment.toObject());
        const activeDiscount = this.getActiveDiscountAmount(assignment.toObject());
        const remainingBase = assignment.amount - (assignment.paidAmount || 0);
        const netPayable = Math.max(0, remainingBase + fine - activeDiscount);
        let orderAmount = netPayable;
        if (requestedAmount !== undefined && requestedAmount !== null) {
            if (requestedAmount < 1)
                throw new common_1.BadRequestException('Payment amount must be at least 1.');
            if (requestedAmount > netPayable) {
                throw new common_1.BadRequestException(`Amount exceeds remaining balance of ${netPayable}.`);
            }
            orderAmount = requestedAmount;
        }
        if (orderAmount <= 0) {
            throw new common_1.BadRequestException('Nothing to pay.');
        }
        if (assignment.razorpayOrderId && Math.abs((assignment.razorpayOrderAmount || 0) - orderAmount) < 0.01) {
            return {
                orderId: assignment.razorpayOrderId,
                amount: Math.round(orderAmount * 100),
                currency: 'INR',
                keyId: this.razorpayKeyId,
                assignmentId: String(assignment._id),
            };
        }
        const order = await this.getRazorpay().orders.create({
            amount: Math.round(orderAmount * 100),
            currency: 'INR',
            receipt: `fee_${assignmentId}_${Date.now()}`,
            notes: { assignmentId, studentId },
        });
        assignment.razorpayOrderId = order.id;
        assignment.razorpayOrderAmount = orderAmount;
        assignment.fineAmount = fine;
        assignment.discountAmount = activeDiscount;
        await assignment.save();
        return {
            orderId: order.id,
            amount: order.amount,
            currency: order.currency,
            keyId: this.razorpayKeyId,
            assignmentId: String(assignment._id),
        };
    }
    async verifyRazorpayPayment(assignmentId, studentId, body) {
        if (!this.razorpayKeySecret) {
            throw new common_1.BadRequestException('Payment verification is not configured.');
        }
        const sign = crypto
            .createHmac('sha256', this.razorpayKeySecret)
            .update(`${body.razorpay_order_id}|${body.razorpay_payment_id}`)
            .digest('hex');
        if (!(0, timing_safe_equal_1.timingSafeEqual)(sign, body.razorpay_signature)) {
            throw new common_1.BadRequestException('Payment signature verification failed');
        }
        let assignment = await this.assignmentModel.findOne({
            _id: assignmentId,
            studentId,
            razorpayOrderId: body.razorpay_order_id,
        }).lean();
        if (!assignment) {
            assignment = await this.assignmentModel.findOne({ _id: assignmentId, studentId }).lean();
            if (!assignment)
                throw new common_1.NotFoundException('Fee assignment not found');
            const existingPayment = await this.feePaymentModel.findOne({ assignmentId, razorpayOrderId: body.razorpay_order_id }, { receiptNumber: 1 }).lean();
            if (existingPayment) {
                return { status: assignment.status, receiptNumber: existingPayment.receiptNumber };
            }
            if (assignment.status === 'paid') {
                return { status: 'paid', receiptNumber: assignment.receiptNumber };
            }
            throw new common_1.BadRequestException('This fee was updated by an admin while your payment was processing. Please contact the school office for a refund.');
        }
        if (assignment.status === 'paid') {
            const existingPayment = await this.feePaymentModel.findOne({ assignmentId, razorpayOrderId: body.razorpay_order_id }, { receiptNumber: 1 }).lean();
            return { status: 'paid', receiptNumber: existingPayment?.receiptNumber || assignment.receiptNumber };
        }
        const expectedAmount = assignment.razorpayOrderAmount;
        if (expectedAmount != null && expectedAmount > 0) {
            try {
                const payment = await this.getRazorpay().payments.fetch(body.razorpay_payment_id);
                const expectedPaise = Math.round(expectedAmount * 100);
                if (Number(payment.amount) !== expectedPaise) {
                    throw new common_1.BadRequestException(`Payment amount mismatch: expected ${expectedPaise} paise, got ${payment.amount} paise`);
                }
            }
            catch (err) {
                if (err instanceof common_1.BadRequestException)
                    throw err;
                throw new common_1.BadRequestException('Unable to verify payment amount with Razorpay. Please try again.');
            }
        }
        else if (expectedAmount === 0) {
            throw new common_1.BadRequestException('Invalid order amount.');
        }
        const paymentAmount = (expectedAmount != null && expectedAmount > 0)
            ? expectedAmount
            : (assignment.amount - (assignment.paidAmount || 0));
        const payer = await this.studentModel.findById(studentId, { name: 1 }).lean();
        const payerLabel = payer?.name ? `Online - ${payer.name}` : 'Online Payment';
        try {
            const { receiptNumber, assignment: updated } = await this.recordPaymentCore(assignmentId, studentId, paymentAmount, {
                paymentMethod: 'razorpay',
                razorpayOrderId: body.razorpay_order_id,
                razorpayPaymentId: body.razorpay_payment_id,
                razorpaySignature: body.razorpay_signature,
                collectedByName: payerLabel,
            });
            return { status: updated.status, receiptNumber };
        }
        catch (err) {
            if (err instanceof common_1.BadRequestException && (err.message || '').includes('concurrently')) {
                const existingPayment = await this.feePaymentModel.findOne({ assignmentId, razorpayOrderId: body.razorpay_order_id }, { receiptNumber: 1 }).lean();
                if (existingPayment) {
                    const latest = await this.assignmentModel.findById(assignmentId, { status: 1 }).lean();
                    return { status: latest?.status || 'paid', receiptNumber: existingPayment.receiptNumber };
                }
            }
            throw err;
        }
    }
    async recordPaymentCore(assignmentId, studentId, paymentAmount, details) {
        const query = { _id: assignmentId };
        if (studentId)
            query.studentId = studentId;
        const assignment = await this.assignmentModel.findOne(query).lean();
        if (!assignment)
            throw new common_1.NotFoundException('Fee assignment not found');
        if (assignment.status === 'paid') {
            throw new common_1.BadRequestException('This fee is already paid.');
        }
        const fine = await this.calculateFineForAssignment(assignment);
        const activeDiscount = this.getActiveDiscountAmount(assignment);
        const remainingBase = assignment.amount - (assignment.paidAmount || 0);
        const netPayable = Math.max(0, remainingBase + fine - activeDiscount);
        if (paymentAmount < 1)
            throw new common_1.BadRequestException('Payment amount must be at least 1.');
        if (paymentAmount > netPayable + 1) {
            throw new common_1.BadRequestException(`Amount exceeds remaining balance of ${netPayable}.`);
        }
        const clampedAmount = Math.min(paymentAmount, netPayable);
        const fineComponent = Math.max(0, clampedAmount - remainingBase);
        const newPaidAmount = (assignment.paidAmount || 0) + clampedAmount;
        const totalNeeded = assignment.amount + fine - activeDiscount;
        const newStatus = newPaidAmount >= totalNeeded ? 'paid' : 'partial';
        const setFields = {
            status: newStatus,
            fineAmount: fine,
            discountAmount: activeDiscount,
            paymentMethod: details.paymentMethod,
        };
        const unsetFields = { razorpayOrderId: 1, razorpayOrderAmount: 1 };
        if (details.collectedBy)
            setFields.collectedBy = details.collectedBy;
        else
            unsetFields.collectedBy = 1;
        if (details.transactionId)
            setFields.transactionId = details.transactionId;
        else
            unsetFields.transactionId = 1;
        if (newStatus === 'paid')
            setFields.paidAt = new Date();
        const claimed = await this.assignmentModel.findOneAndUpdate({
            _id: assignmentId,
            status: { $ne: 'paid' },
            paidAmount: assignment.paidAmount || 0,
        }, {
            $inc: { paidAmount: clampedAmount },
            $set: setFields,
            $unset: unsetFields,
        }, { new: true }).lean();
        if (!claimed) {
            throw new common_1.BadRequestException('Payment was modified concurrently. Please retry.');
        }
        let feePayment;
        for (let receiptAttempt = 0; receiptAttempt < 3; receiptAttempt++) {
            const receiptNumber = await this.generateReceiptNumber();
            try {
                feePayment = await this.feePaymentModel.create({
                    assignmentId: String(assignment._id),
                    studentId: assignment.studentId,
                    academicYearId: assignment.academicYearId,
                    amount: clampedAmount,
                    fineComponent,
                    paymentMethod: details.paymentMethod,
                    receiptNumber,
                    razorpayOrderId: details.razorpayOrderId,
                    razorpayPaymentId: details.razorpayPaymentId,
                    razorpaySignature: details.razorpaySignature,
                    transactionId: details.transactionId,
                    collectedBy: details.collectedBy,
                    collectedByName: details.collectedByName,
                    receiptUrl: details.receiptUrl,
                    notes: details.notes,
                    paidAt: new Date(),
                });
                break;
            }
            catch (err) {
                if (err?.code === 11000 && receiptAttempt < 2)
                    continue;
                throw err;
            }
        }
        if (!feePayment)
            throw new common_1.BadRequestException('Failed to generate unique receipt number. Please retry.');
        try {
            await this.assignmentModel.updateOne({ _id: claimed._id }, { $set: { receiptNumber: feePayment.receiptNumber } });
        }
        catch (err) {
            this.logger.warn(`Assignment receiptNumber mirror failed for ${String(claimed._id)}: ${err?.message}`);
        }
        let invoiceWarning = null;
        try {
            const billAmount = (claimed.amount || 0) + (claimed.fineAmount || 0) - (claimed.discountAmount || 0);
            await this.invoicesService.upsertFromFeePayment({
                feeAssignmentId: String(claimed._id),
                studentId: claimed.studentId,
                classId: claimed.classId,
                academicYearId: claimed.academicYearId,
                billAmount,
                totalPaid: claimed.paidAmount || 0,
                paymentMethod: details.paymentMethod,
                generatedBy: details.collectedByName || details.collectedBy || 'System',
                feeName: claimed.feeName,
                installment: claimed.installment,
                fineAmount: claimed.fineAmount || 0,
            });
        }
        catch (err) {
            this.logger.warn(`Invoice upsert failed for assignment=${String(claimed._id)}: ${err?.message}`);
            invoiceWarning = 'Payment recorded, but the invoice listing could not be updated. It will self-heal on the next payment against this fee, or contact support.';
        }
        return { assignment: claimed, payment: feePayment.toObject(), receiptNumber: feePayment.receiptNumber, invoiceWarning };
    }
    async recordOfflinePayment(assignmentId, dto, adminId, receipt) {
        const collectedByName = 'Admin';
        let receiptRelativePath;
        if (receipt) {
            const uploadsDir = path.join(process.cwd(), 'uploads', 'fee-receipts');
            await fs.mkdir(uploadsDir, { recursive: true });
            const filename = `receipt-${(0, crypto_1.randomBytes)(16).toString('hex')}${path.extname(receipt.originalname) || ''}`;
            const filepath = path.join(uploadsDir, filename);
            await fs.writeFile(filepath, receipt.buffer);
            receiptRelativePath = `/uploads/fee-receipts/${filename}`;
        }
        const { assignment, payment, receiptNumber, invoiceWarning } = await this.recordPaymentCore(assignmentId, '', dto.amount, {
            paymentMethod: dto.paymentMethod,
            transactionId: dto.transactionId,
            collectedBy: adminId,
            collectedByName,
            receiptUrl: receiptRelativePath,
            notes: dto.notes,
        });
        const paymentWithFullUrl = payment?.receiptUrl
            ? { ...payment, receiptUrl: (0, url_helper_1.toFullUrl)(payment.receiptUrl) }
            : payment;
        return { ...assignment, receiptNumber, payment: paymentWithFullUrl, invoiceWarning };
    }
    async handleWebhook(signature, rawBody, jsonBody) {
        if (!this.razorpayWebhookSecret)
            throw new common_1.BadRequestException('Webhook not configured');
        const expected = crypto.createHmac('sha256', this.razorpayWebhookSecret).update(rawBody).digest('hex');
        if (!signature || !(0, timing_safe_equal_1.timingSafeEqual)(expected, signature)) {
            this.logger.warn('Webhook received with invalid signature');
            return { received: true };
        }
        const event = jsonBody?.event;
        const entity = jsonBody?.payload?.payment?.entity || jsonBody?.payload?.order?.entity;
        if (event === 'payment.captured' && entity) {
            const notes = entity.notes || {};
            if (notes.type === 'transport')
                return { received: true };
            const orderId = entity.order_id;
            const assignment = await this.assignmentModel.findOne({ razorpayOrderId: orderId }).lean();
            if (!assignment) {
                this.logger.warn(`Webhook: no assignment found for orderId=${orderId}`);
                return { received: true };
            }
            if (assignment.status !== 'paid') {
                const expectedAmount = assignment.razorpayOrderAmount;
                if (expectedAmount != null && expectedAmount > 0) {
                    const expectedPaise = Math.round(expectedAmount * 100);
                    if (Number(entity.amount) !== expectedPaise) {
                        this.logger.warn(`Webhook amount mismatch: orderId=${orderId} expected=${expectedPaise} got=${entity.amount}`);
                        return { received: true };
                    }
                }
                else if (expectedAmount === 0) {
                    this.logger.warn(`Webhook: zero-amount order for orderId=${orderId}, skipping`);
                    return { received: true };
                }
                const webhookPaymentAmount = (expectedAmount != null && expectedAmount > 0)
                    ? expectedAmount
                    : (assignment.amount - (assignment.paidAmount || 0));
                const payer = await this.studentModel.findById(assignment.studentId, { name: 1 }).lean();
                const payerLabel = payer?.name ? `Online - ${payer.name}` : 'Online Payment';
                try {
                    await this.recordPaymentCore(String(assignment._id), assignment.studentId, webhookPaymentAmount, {
                        paymentMethod: 'razorpay',
                        razorpayOrderId: orderId,
                        razorpayPaymentId: entity.id,
                        collectedByName: payerLabel,
                    });
                }
                catch (err) {
                    this.logger.warn(`Webhook: payment recording failed for orderId=${orderId}: ${err?.message}`);
                }
            }
        }
        return { received: true };
    }
    async syncAssignments(structureId, academicYearId) {
        const query = { isActive: true };
        if (structureId)
            query._id = structureId;
        if (academicYearId)
            query.academicYearId = academicYearId;
        const structures = await this.structureModel.find(query).lean();
        let docsCreated = 0;
        const affectedStudentIds = new Set();
        for (const structure of structures) {
            const classIds = structure.classIds || [];
            const studentQuery = { academicYearId: structure.academicYearId, isActive: true };
            if (!classIds.includes('all'))
                studentQuery.classId = { $in: classIds };
            const students = await this.studentModel.find(studentQuery, { _id: 1, classId: 1 }).limit(10000).lean();
            if (students.length === 0)
                continue;
            const ay = await this.academicYearModel.findById(structure.academicYearId).lean();
            if (!ay)
                continue;
            const { effectiveStart, effectiveEnd } = this.resolveEffectiveRange(structure, { startDate: new Date(ay.startDate), endDate: new Date(ay.endDate) });
            const allDocs = this.buildAssignmentDocs(structure, students, effectiveStart, effectiveEnd);
            if (allDocs.length === 0)
                continue;
            const existingPairs = await this.assignmentModel
                .find({ feeStructureId: String(structure._id) }, { studentId: 1, installment: 1 })
                .lean();
            const existingKeys = new Set(existingPairs.map((r) => `${String(r.studentId)}_${r.installment}`));
            const docs = allDocs.filter((d) => !existingKeys.has(`${d.studentId}_${d.installment}`));
            if (docs.length === 0)
                continue;
            const result = await this.assignmentModel.insertMany(docs, { ordered: false }).catch((err) => {
                if (err?.code === 11000 || err?.writeErrors) {
                    const errCount = err.writeErrors?.length ?? 0;
                    const insertedCount = err.insertedDocs?.length ?? Math.max(0, docs.length - errCount);
                    return { insertedDocs: err.insertedDocs ?? [], length: insertedCount };
                }
                throw err;
            });
            const insertedDocs = Array.isArray(result)
                ? result
                : (result.insertedDocs ?? []);
            const insertedCount = Array.isArray(result)
                ? result.length
                : (result.length ?? insertedDocs.length);
            docsCreated += insertedCount;
            for (const d of insertedDocs) {
                if (d?.studentId)
                    affectedStudentIds.add(String(d.studentId));
            }
        }
        return { synced: docsCreated, studentCount: affectedStudentIds.size };
    }
    async regenerateUnpaidAssignments(structureId) {
        const structure = await this.structureModel.findById(structureId).lean();
        if (!structure)
            throw new common_1.NotFoundException('Fee structure not found');
        const classIds = structure.classIds || [];
        const q = { academicYearId: structure.academicYearId, isActive: true };
        if (!classIds.includes('all'))
            q.classId = { $in: classIds };
        const students = await this.studentModel.find(q, { _id: 1, classId: 1 }).limit(10000).lean();
        if (students.length === 0) {
            return { generated: 0, updated: 0, studentCount: 0, deletedOld: 0 };
        }
        const ay = await this.academicYearModel.findById(structure.academicYearId).lean();
        if (!ay)
            throw new common_1.NotFoundException('Academic year not found for this fee structure');
        const { effectiveStart, effectiveEnd } = this.resolveEffectiveRange(structure, { startDate: new Date(ay.startDate), endDate: new Date(ay.endDate) });
        const allDocs = this.buildAssignmentDocs(structure, students, effectiveStart, effectiveEnd);
        const lockedPairs = await this.assignmentModel
            .find({ feeStructureId: structureId, status: { $in: ['paid', 'partial'] } }, { studentId: 1, installment: 1 })
            .lean();
        const lockedKeys = new Set(lockedPairs.map((r) => `${String(r.studentId)}_${r.installment}`));
        const docs = allDocs.filter((d) => !lockedKeys.has(`${d.studentId}_${d.installment}`));
        const ops = docs.map((doc) => ({
            updateOne: {
                filter: {
                    studentId: doc.studentId,
                    feeStructureId: doc.feeStructureId,
                    installment: doc.installment,
                },
                update: {
                    $set: {
                        dueDate: doc.dueDate,
                        amount: doc.amount,
                        baseAmount: doc.baseAmount,
                        subFees: doc.subFees,
                        feeName: doc.feeName,
                        academicYearId: doc.academicYearId,
                        classId: doc.classId,
                    },
                    $setOnInsert: {
                        fineAmount: 0,
                        status: 'pending',
                    },
                },
                upsert: true,
            },
        }));
        let upsertedCount = 0;
        let modifiedCount = 0;
        if (ops.length > 0) {
            for (let i = 0; i < ops.length; i += 5000) {
                const batch = ops.slice(i, i + 5000);
                const result = await this.assignmentModel.bulkWrite(batch, { ordered: false });
                upsertedCount += result.upsertedCount ?? 0;
                modifiedCount += result.modifiedCount ?? 0;
            }
        }
        const validKeys = new Set(docs.map((d) => `${d.studentId}_${d.installment}`));
        const existingUnpaid = await this.assignmentModel
            .find({ feeStructureId: structureId, status: { $nin: ['paid', 'partial'] } }, { _id: 1, studentId: 1, installment: 1 })
            .lean();
        const orphanIds = existingUnpaid
            .filter((e) => !validKeys.has(`${String(e.studentId)}_${e.installment}`))
            .map((e) => e._id);
        let deletedOld = 0;
        if (orphanIds.length > 0) {
            const del = await this.assignmentModel.deleteMany({ _id: { $in: orphanIds } });
            deletedOld = del.deletedCount ?? 0;
        }
        return {
            generated: upsertedCount,
            updated: modifiedCount,
            studentCount: students.length,
            deletedOld,
        };
    }
    async syncStudentAssignments(studentId, classId, academicYearId) {
        const structures = await this.structureModel.find({
            academicYearId,
            isActive: true,
            $or: [{ classIds: classId }, { classIds: 'all' }],
        }).lean();
        if (structures.length === 0)
            return;
        const existingStructureIds = new Set(await this.assignmentModel.distinct('feeStructureId', { studentId, academicYearId }));
        const missingStructures = structures.filter((s) => !existingStructureIds.has(String(s._id)));
        if (missingStructures.length === 0)
            return;
        const ay = await this.academicYearModel.findById(academicYearId).lean();
        if (!ay)
            return;
        for (const structure of missingStructures) {
            const { effectiveStart, effectiveEnd } = this.resolveEffectiveRange(structure, { startDate: new Date(ay.startDate), endDate: new Date(ay.endDate) });
            const docs = this.buildAssignmentDocs(structure, [{ _id: studentId, classId }], effectiveStart, effectiveEnd);
            if (docs.length > 0) {
                await this.assignmentModel.insertMany(docs, { ordered: false }).catch((err) => {
                    if (err?.code === 11000 || err?.writeErrors)
                        return;
                    throw err;
                });
            }
        }
    }
    async generateAssignments(structure) {
        const classIds = structure.classIds || [];
        const q = { academicYearId: structure.academicYearId, isActive: true };
        if (!classIds.includes('all'))
            q.classId = { $in: classIds };
        const students = await this.studentModel.find(q, { _id: 1, classId: 1 }).limit(10000).lean();
        if (students.length === 0)
            return { docsCreated: 0, studentCount: 0 };
        const ay = await this.academicYearModel.findById(structure.academicYearId).lean();
        if (!ay)
            throw new common_1.NotFoundException('Academic year not found for this fee structure');
        const { effectiveStart, effectiveEnd } = this.resolveEffectiveRange(structure, { startDate: new Date(ay.startDate), endDate: new Date(ay.endDate) });
        const docs = this.buildAssignmentDocs(structure, students, effectiveStart, effectiveEnd);
        if (docs.length > 0) {
            for (let i = 0; i < docs.length; i += 5000) {
                await this.assignmentModel.insertMany(docs.slice(i, i + 5000), { ordered: false }).catch((err) => {
                    if (err?.code === 11000 || err?.writeErrors)
                        return;
                    throw err;
                });
            }
        }
        return { docsCreated: docs.length, studentCount: students.length };
    }
    buildAssignmentDocs(structure, students, effectiveStart, effectiveEnd) {
        const subFees = structure.enableSubFees ? (structure.subFees || []) : [];
        const subFeesTotal = subFees.reduce((s, sf) => s + (sf.amount || 0), 0);
        const totalAmount = (structure.amount || 0) + subFeesTotal;
        const base = (stu, installment, dueDate) => ({
            studentId: String(stu._id),
            feeStructureId: String(structure._id),
            academicYearId: structure.academicYearId,
            classId: String(stu.classId),
            feeName: structure.name,
            installment,
            amount: totalAmount,
            baseAmount: structure.amount,
            subFees: subFees.map((sf) => ({ name: sf.name, amount: sf.amount })),
            fineAmount: 0,
            dueDate,
            status: 'pending',
        });
        switch (structure.feeType) {
            case 'one-time':
            case 'yearly': {
                let y;
                let m;
                let day;
                if (structure.startDate) {
                    y = effectiveStart.getUTCFullYear();
                    m = effectiveStart.getUTCMonth() + 1;
                    day = (0, fees_installment_dates_1.clampDayToMonth)(y, m, effectiveStart.getUTCDate());
                }
                else {
                    m = structure.dueMonth || 1;
                    y = (0, fees_installment_dates_1.yearForMonthInAY)(effectiveStart, m);
                    day = (0, fees_installment_dates_1.clampDayToMonth)(y, m, structure.dueDay);
                }
                const dueDate = new Date(Date.UTC(y, m - 1, day));
                const label = structure.feeType === 'one-time' ? 'One Time' : 'Yearly';
                return students.map((stu) => base(stu, label, dueDate));
            }
            case 'quarterly': {
                const endMs = effectiveEnd.getTime();
                const allDates = (0, fees_installment_dates_1.generateQuarterlyDueDates)(effectiveStart, effectiveEnd, structure.dueDay);
                const dates = allDates.filter((d) => d.getTime() <= endMs);
                const labels = (0, fees_installment_dates_1.buildQuarterLabels)(effectiveStart.getUTCMonth() + 1);
                return students.flatMap((stu) => dates.map((dueDate, qi) => base(stu, labels[qi], dueDate)));
            }
            case 'monthly':
            default: {
                const dates = (0, fees_installment_dates_1.generateMonthlyDueDates)(effectiveStart, effectiveEnd, structure.dueDay);
                return students.flatMap((stu) => dates.map((dueDate) => {
                    const m = dueDate.getUTCMonth() + 1;
                    return base(stu, MONTHS[m], dueDate);
                }));
            }
        }
    }
    resolveEffectiveRange(structure, ay) {
        const effectiveStart = structure.startDate
            ? new Date(structure.startDate)
            : new Date(ay.startDate);
        const effectiveEnd = structure.endDate
            ? new Date(structure.endDate)
            : new Date(ay.endDate);
        if (isNaN(effectiveStart.getTime()) || isNaN(effectiveEnd.getTime())) {
            throw new common_1.BadRequestException(`Fee structure "${structure.name}" has invalid start/end dates.`);
        }
        if (effectiveEnd.getTime() <= effectiveStart.getTime()) {
            throw new common_1.BadRequestException(`Fee structure "${structure.name}" has an invalid installment window ` +
                `(start ${effectiveStart.toISOString().slice(0, 10)} is not before ` +
                `end ${effectiveEnd.toISOString().slice(0, 10)}). ` +
                `Either set endDate explicitly or adjust the academic year.`);
        }
        return { effectiveStart, effectiveEnd };
    }
    computeFine(lateFine, daysLate, baseAmount) {
        if (!lateFine?.enabled || daysLate <= 0)
            return 0;
        const fineType = lateFine.type || 'per_day';
        switch (fineType) {
            case 'percentage':
                return Math.round(baseAmount * (lateFine.percentage || 0) / 100);
            case 'day_wise_slab': {
                const slabs = lateFine.slabs || [];
                for (const slab of slabs) {
                    if (daysLate >= slab.startDay && daysLate <= slab.endDay) {
                        return Math.round(slab.amount);
                    }
                }
                if (slabs.length > 0)
                    return Math.round(slabs[slabs.length - 1].amount);
                return 0;
            }
            case 'per_day':
            default:
                return Math.round(daysLate * (lateFine.amountPerDay || 0));
        }
    }
    async calculateFineForAssignment(assignment) {
        if (assignment.status === 'paid')
            return assignment.fineAmount || 0;
        const dueDate = new Date(assignment.dueDate);
        const now = new Date();
        if (now <= dueDate)
            return 0;
        const structure = await this.structureModel
            .findById(assignment.feeStructureId, { lateFine: 1, amount: 1 })
            .lean();
        if (!structure?.lateFine?.enabled)
            return 0;
        const diffMs = now.getTime() - dueDate.getTime();
        const daysLate = Math.floor(diffMs / (1000 * 60 * 60 * 24));
        const remainingBase = assignment.amount - (assignment.paidAmount || 0);
        if (remainingBase <= 0)
            return 0;
        return this.computeFine(structure.lateFine, daysLate, remainingBase);
    }
    async recalculateFines(assignments) {
        if (assignments.length === 0)
            return assignments;
        const unpaid = assignments.filter((a) => a.status !== 'paid');
        if (unpaid.length === 0)
            return assignments;
        const structureIds = [...new Set(unpaid.map((a) => a.feeStructureId))];
        const structures = await this.structureModel
            .find({ _id: { $in: structureIds } }, { lateFine: 1 })
            .lean();
        const structureMap = new Map(structures.map((s) => [String(s._id), s]));
        const now = new Date();
        return assignments.map((a) => {
            if (a.status === 'paid')
                return a;
            const dueDate = new Date(a.dueDate);
            if (now <= dueDate) {
                const currentStatus = (a.paidAmount || 0) > 0 ? 'partial' : 'pending';
                return { ...a, fineAmount: 0, status: currentStatus };
            }
            const structure = structureMap.get(String(a.feeStructureId));
            const diffMs = now.getTime() - dueDate.getTime();
            const daysLate = Math.floor(diffMs / (1000 * 60 * 60 * 24));
            const remainingBase = a.amount - (a.paidAmount || 0);
            const fine = (structure?.lateFine?.enabled && remainingBase > 0)
                ? this.computeFine(structure.lateFine, daysLate, remainingBase)
                : 0;
            return { ...a, fineAmount: fine, status: 'overdue' };
        });
    }
    async verifyReceipt(receiptNumber) {
        const feePayment = await this.feePaymentModel.findOne({ receiptNumber }, { assignmentId: 1, studentId: 1, amount: 1, fineComponent: 1, paymentMethod: 1, paidAt: 1, receiptNumber: 1 }).lean();
        if (feePayment) {
            const student = await this.studentModel.findById(feePayment.studentId, { name: 1, uniqueId: 1, rollNumber: 1, classId: 1 }).lean();
            const assignment = await this.assignmentModel.findById(feePayment.assignmentId, { feeName: 1, installment: 1, amount: 1 }).lean();
            return {
                receiptNumber: feePayment.receiptNumber,
                studentName: student?.name || 'Unknown',
                studentId: student?.uniqueId || '',
                rollNumber: student?.rollNumber || '',
                feeName: assignment?.feeName || '',
                installment: assignment?.installment || '',
                amount: feePayment.amount,
                fineComponent: feePayment.fineComponent || 0,
                totalPaid: feePayment.amount,
                status: 'paid',
                paidAt: feePayment.paidAt,
                paymentMethod: feePayment.paymentMethod,
            };
        }
        const assignment = await this.assignmentModel.findOne({ receiptNumber }, { studentId: 1, feeName: 1, installment: 1, amount: 1, fineAmount: 1, discountAmount: 1, status: 1, paidAt: 1, paymentMethod: 1, receiptNumber: 1, academicYearId: 1 }).lean();
        if (!assignment)
            return null;
        const student = await this.studentModel.findById(assignment.studentId, { name: 1, uniqueId: 1, rollNumber: 1, classId: 1 }).lean();
        return {
            receiptNumber: assignment.receiptNumber,
            studentName: student?.name || 'Unknown',
            studentId: student?.uniqueId || '',
            rollNumber: student?.rollNumber || '',
            feeName: assignment.feeName,
            installment: assignment.installment,
            amount: assignment.amount,
            fineAmount: assignment.fineAmount || 0,
            discountAmount: assignment.discountAmount || 0,
            totalPaid: assignment.amount + (assignment.fineAmount || 0) - (assignment.discountAmount || 0),
            status: assignment.status,
            paidAt: assignment.paidAt,
            paymentMethod: assignment.paymentMethod,
        };
    }
    async getPaymentHistory(assignmentId, studentId) {
        const query = { assignmentId };
        if (studentId)
            query.studentId = studentId;
        const payments = await this.feePaymentModel
            .find(query)
            .sort({ paidAt: 1 })
            .limit(500)
            .lean();
        return payments.map((p) => ({
            ...p,
            collectedByName: p.collectedByName || (p.collectedBy ? 'Admin' : undefined),
            receiptUrl: p.receiptUrl ? (0, url_helper_1.toFullUrl)(p.receiptUrl) : undefined,
        }));
    }
    async generateReceiptNumber() {
        const year = new Date().getFullYear();
        const prefix = `RCPT-${year}-`;
        for (let attempt = 0; attempt < 3; attempt++) {
            const [assignmentCount, paymentCount] = await Promise.all([
                this.assignmentModel.countDocuments({ receiptNumber: { $regex: `^${prefix}\\d+$` } }),
                this.feePaymentModel.countDocuments({ receiptNumber: { $regex: `^${prefix}\\d+$` } }),
            ]);
            const count = assignmentCount + paymentCount;
            const seq = count + 1 + attempt;
            const receiptNumber = `${prefix}${String(seq).padStart(6, '0')}`;
            const [existsA, existsP] = await Promise.all([
                this.assignmentModel.exists({ receiptNumber }),
                this.feePaymentModel.exists({ receiptNumber }),
            ]);
            if (!existsA && !existsP)
                return receiptNumber;
        }
        return `${prefix}${crypto.randomBytes(4).toString('hex').toUpperCase()}`;
    }
};
exports.FeesService = FeesService;
exports.FeesService = FeesService = FeesService_1 = __decorate([
    (0, common_1.Injectable)(),
    __param(0, (0, mongoose_1.InjectModel)(fee_structure_schema_1.FeeStructure.name)),
    __param(1, (0, mongoose_1.InjectModel)(fee_assignment_schema_1.FeeAssignment.name)),
    __param(2, (0, mongoose_1.InjectModel)(student_schema_1.Student.name)),
    __param(3, (0, mongoose_1.InjectModel)(academic_year_schema_1.AcademicYear.name)),
    __param(4, (0, mongoose_1.InjectModel)(fee_payment_schema_1.FeePayment.name)),
    __metadata("design:paramtypes", [mongoose_2.Model,
        mongoose_2.Model,
        mongoose_2.Model,
        mongoose_2.Model,
        mongoose_2.Model,
        invoices_service_1.InvoicesService])
], FeesService);
//# sourceMappingURL=fees.service.js.map