"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 InvoicesService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.InvoicesService = void 0;
const crypto = __importStar(require("crypto"));
const common_1 = require("@nestjs/common");
const mongoose_1 = require("@nestjs/mongoose");
const mongoose_2 = require("mongoose");
const invoice_schema_1 = require("./schemas/invoice.schema");
const student_schema_1 = require("../students/schemas/student.schema");
const class_schema_1 = require("../classes/schemas/class.schema");
let InvoicesService = InvoicesService_1 = class InvoicesService {
    invoiceModel;
    studentModel;
    classModel;
    logger = new common_1.Logger(InvoicesService_1.name);
    constructor(invoiceModel, studentModel, classModel) {
        this.invoiceModel = invoiceModel;
        this.studentModel = studentModel;
        this.classModel = classModel;
    }
    deriveStatus(amount, totalPaid) {
        if (totalPaid >= amount)
            return 'paid';
        if (totalPaid > 0)
            return 'partial';
        return 'pending';
    }
    async generateInvoiceNumber() {
        const year = new Date().getFullYear();
        const prefix = `INV-${year}-`;
        for (let attempt = 0; attempt < 3; attempt++) {
            const count = await this.invoiceModel.countDocuments({
                invoiceNumber: { $regex: `^${prefix}\\d+$` },
            });
            const seq = count + 1 + attempt;
            const invoiceNumber = `${prefix}${String(seq).padStart(6, '0')}`;
            const exists = await this.invoiceModel.exists({ invoiceNumber });
            if (!exists)
                return invoiceNumber;
        }
        return `${prefix}${crypto.randomBytes(4).toString('hex').toUpperCase()}`;
    }
    async generatePaymentId() {
        const year = new Date().getFullYear();
        const prefix = `PAY-${year}-`;
        for (let attempt = 0; attempt < 3; attempt++) {
            const count = await this.invoiceModel.countDocuments({
                paymentId: { $regex: `^${prefix}\\d+$` },
            });
            const seq = count + 1 + attempt;
            const paymentId = `${prefix}${String(seq).padStart(6, '0')}`;
            const exists = await this.invoiceModel.exists({ paymentId });
            if (!exists)
                return paymentId;
        }
        return `${prefix}${crypto.randomBytes(4).toString('hex').toUpperCase()}`;
    }
    async generateReceiptNumber() {
        const year = new Date().getFullYear();
        const prefix = `RCPT-${year}-`;
        for (let attempt = 0; attempt < 3; attempt++) {
            const count = await this.invoiceModel.countDocuments({
                receiptNumber: { $regex: `^${prefix}\\d+$` },
            });
            const seq = count + 1 + attempt;
            const receiptNumber = `${prefix}${String(seq).padStart(6, '0')}`;
            const exists = await this.invoiceModel.exists({ receiptNumber });
            if (!exists)
                return receiptNumber;
        }
        return `${prefix}${crypto.randomBytes(4).toString('hex').toUpperCase()}`;
    }
    async generateTransactionId() {
        const year = new Date().getFullYear();
        const prefix = `TXN-${year}-`;
        for (let attempt = 0; attempt < 3; attempt++) {
            const count = await this.invoiceModel.countDocuments({
                transactionId: { $regex: `^${prefix}\\d+$` },
            });
            const seq = count + 1 + attempt;
            const transactionId = `${prefix}${String(seq).padStart(6, '0')}`;
            const exists = await this.invoiceModel.exists({ transactionId });
            if (!exists)
                return transactionId;
        }
        return `${prefix}${crypto.randomBytes(4).toString('hex').toUpperCase()}`;
    }
    escapeRegex(str) {
        return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }
    buildQuery(filter, academicYearId) {
        const q = {};
        if (academicYearId)
            q.academicYearId = academicYearId;
        if (filter.classId)
            q.classId = filter.classId;
        if (filter.studentId)
            q.studentId = filter.studentId;
        if (filter.status)
            q.status = filter.status;
        if (filter.search) {
            const escaped = this.escapeRegex(filter.search);
            q.$or = [
                { studentName: { $regex: escaped, $options: 'i' } },
                { studentUniqueId: { $regex: escaped, $options: 'i' } },
                { invoiceNumber: { $regex: escaped, $options: 'i' } },
                { paymentId: { $regex: escaped, $options: 'i' } },
            ];
        }
        if (filter.dateFrom || filter.dateTo) {
            q.date = {};
            if (filter.dateFrom)
                q.date.$gte = new Date(filter.dateFrom);
            if (filter.dateTo) {
                const end = new Date(filter.dateTo);
                end.setHours(23, 59, 59, 999);
                q.date.$lte = end;
            }
        }
        return q;
    }
    async create(dto, academicYearId) {
        const student = await this.studentModel
            .findById(dto.studentId, { name: 1, uniqueId: 1, classId: 1 })
            .lean();
        if (!student)
            throw new common_1.NotFoundException('Student not found');
        if (!student.classId) {
            throw new common_1.BadRequestException('Student does not have a class assigned');
        }
        const totalPaid = dto.totalPaid ?? 0;
        if (totalPaid > dto.amount) {
            throw new common_1.BadRequestException('Total paid cannot exceed invoice amount');
        }
        const cls = await this.classModel
            .findById(student.classId, { name: 1 })
            .lean();
        const invoiceNumber = await this.generateInvoiceNumber();
        const paymentId = totalPaid > 0 ? await this.generatePaymentId() : undefined;
        const receiptNumber = totalPaid > 0 ? await this.generateReceiptNumber() : undefined;
        const transactionId = totalPaid > 0
            ? (dto.transactionId?.trim() || await this.generateTransactionId())
            : undefined;
        const balance = Math.max(0, dto.amount - totalPaid);
        const status = this.deriveStatus(dto.amount, totalPaid);
        const generatedBy = 'Admin';
        const invoice = await this.invoiceModel.create({
            invoiceNumber,
            paymentId,
            receiptNumber,
            transactionId,
            studentId: dto.studentId,
            studentName: student.name || '',
            studentUniqueId: student.uniqueId || '',
            classId: String(student.classId),
            className: cls?.name || '',
            academicYearId: academicYearId || '',
            amount: dto.amount,
            totalPaid,
            balance,
            status,
            paymentMode: dto.paymentMode,
            date: dto.date ? new Date(dto.date) : new Date(),
            generatedBy,
            isActive: dto.isActive ?? true,
            notes: dto.notes?.trim() || undefined,
        });
        return invoice.toObject();
    }
    async findAll(filter, academicYearId) {
        const q = this.buildQuery(filter, academicYearId);
        return this.invoiceModel
            .find(q)
            .sort({ date: -1 })
            .limit(5000)
            .lean({ virtuals: true });
    }
    async findOne(id) {
        const invoice = await this.invoiceModel.findById(id).lean({ virtuals: true });
        if (!invoice)
            throw new common_1.NotFoundException('Invoice not found');
        return invoice;
    }
    async update(id, dto) {
        const existing = await this.invoiceModel.findById(id).lean();
        if (!existing)
            throw new common_1.NotFoundException('Invoice not found');
        const $set = {};
        if (dto.amount !== undefined)
            $set.amount = dto.amount;
        if (dto.totalPaid !== undefined)
            $set.totalPaid = dto.totalPaid;
        if (dto.paymentMode !== undefined)
            $set.paymentMode = dto.paymentMode;
        if (dto.date !== undefined)
            $set.date = new Date(dto.date);
        if (dto.notes !== undefined)
            $set.notes = dto.notes.trim();
        if (dto.isActive !== undefined)
            $set.isActive = dto.isActive;
        if (dto.transactionId !== undefined) {
            const trimmed = dto.transactionId.trim();
            if (trimmed)
                $set.transactionId = trimmed;
        }
        const amount = $set.amount ?? existing.amount;
        const totalPaid = $set.totalPaid ?? existing.totalPaid;
        if (totalPaid > amount) {
            throw new common_1.BadRequestException('Total paid cannot exceed invoice amount');
        }
        $set.balance = Math.max(0, amount - totalPaid);
        $set.status = this.deriveStatus(amount, totalPaid);
        const $unset = {};
        if (totalPaid > 0 && !existing.paymentId) {
            $set.paymentId = await this.generatePaymentId();
        }
        else if (totalPaid === 0 && existing.paymentId) {
            $unset.paymentId = 1;
        }
        if (totalPaid > 0 && !existing.receiptNumber) {
            $set.receiptNumber = await this.generateReceiptNumber();
        }
        else if (totalPaid === 0 && existing.receiptNumber) {
            $unset.receiptNumber = 1;
        }
        if (totalPaid > 0 && !existing.transactionId && !$set.transactionId) {
            $set.transactionId = await this.generateTransactionId();
        }
        else if (totalPaid === 0 && existing.transactionId && !$set.transactionId) {
            $unset.transactionId = 1;
        }
        const updateOps = { $set };
        if (Object.keys($unset).length > 0)
            updateOps.$unset = $unset;
        const updateFilter = { _id: id };
        if ($set.paymentId) {
            updateFilter.paymentId = { $exists: false };
        }
        const updated = await this.invoiceModel.findByIdAndUpdate(updateFilter, updateOps, { new: true }).lean({ virtuals: true });
        if (!updated) {
            const current = await this.invoiceModel.findById(id).lean({ virtuals: true });
            if (!current)
                throw new common_1.NotFoundException('Invoice not found');
            return current;
        }
        return updated;
    }
    async toggleActive(id) {
        const updated = await this.invoiceModel.findByIdAndUpdate(id, [{ $set: { isActive: { $not: '$isActive' } } }], { new: true, projection: { isActive: 1 } }).lean();
        if (!updated)
            throw new common_1.NotFoundException('Invoice not found');
        return { _id: id, isActive: updated.isActive };
    }
    async verifyReceipt(lookup) {
        const query = {
            $or: [
                { receiptNumber: lookup },
                { paymentId: lookup },
                { transactionId: lookup },
                { invoiceNumber: lookup },
            ],
        };
        const inv = await this.invoiceModel.findOne(query, {
            invoiceNumber: 1, paymentId: 1, receiptNumber: 1, transactionId: 1,
            studentName: 1, studentUniqueId: 1, className: 1,
            feeName: 1, installment: 1,
            amount: 1, fineAmount: 1, totalPaid: 1, balance: 1,
            status: 1, paymentMode: 1, date: 1,
        }).lean();
        if (!inv)
            return null;
        return {
            receiptNumber: inv.receiptNumber || inv.paymentId || inv.invoiceNumber,
            invoiceNumber: inv.invoiceNumber,
            transactionId: inv.transactionId,
            studentName: inv.studentName,
            studentId: inv.studentUniqueId,
            className: inv.className,
            feeName: inv.feeName || 'Invoice',
            installment: inv.installment || '',
            amount: inv.amount,
            fineAmount: inv.fineAmount || 0,
            totalPaid: inv.totalPaid,
            status: inv.status,
            paidAt: inv.date,
            paymentMethod: inv.paymentMode,
        };
    }
    async delete(id) {
        const result = await this.invoiceModel.findByIdAndDelete(id);
        if (!result)
            throw new common_1.NotFoundException('Invoice not found');
        return { deleted: true };
    }
    async exportAll(filter, academicYearId) {
        const q = this.buildQuery(filter, academicYearId);
        return this.invoiceModel
            .find(q)
            .sort({ date: -1 })
            .limit(10000)
            .lean({ virtuals: true });
    }
    async upsertFromFeePayment(input) {
        const billAmount = Math.max(0, input.billAmount);
        const totalPaid = Math.max(0, Math.min(input.totalPaid, billAmount));
        const balance = Math.max(0, billAmount - totalPaid);
        const status = this.deriveStatus(billAmount, totalPaid);
        const paymentMode = input.paymentMethod === 'razorpay'
            ? 'online'
            : (['cash', 'upi', 'cheque', 'bank_transfer', 'online'].includes(input.paymentMethod)
                ? input.paymentMethod
                : undefined);
        const existing = await this.invoiceModel
            .findOne({ feeAssignmentId: input.feeAssignmentId })
            .lean();
        if (existing) {
            const $set = {
                amount: billAmount,
                fineAmount: Math.max(0, input.fineAmount || 0),
                totalPaid,
                balance,
                status,
                date: new Date(),
            };
            if (paymentMode)
                $set.paymentMode = paymentMode;
            if (input.feeName && !existing.feeName)
                $set.feeName = input.feeName;
            if (input.installment && !existing.installment)
                $set.installment = input.installment;
            const updated = await this.invoiceModel.findByIdAndUpdate(existing._id, { $set }, { new: true }).lean({ virtuals: true });
            return updated;
        }
        const student = await this.studentModel
            .findById(input.studentId, { name: 1, uniqueId: 1 })
            .lean();
        if (!student) {
            this.logger.warn(`upsertFromFeePayment: student ${input.studentId} not found — invoice skipped for assignment ${input.feeAssignmentId}`);
            return null;
        }
        const cls = await this.classModel
            .findById(input.classId, { name: 1 })
            .lean();
        const invoiceNumber = await this.generateInvoiceNumber();
        const paymentId = totalPaid > 0 ? await this.generatePaymentId() : undefined;
        try {
            const invoice = await this.invoiceModel.create({
                invoiceNumber,
                paymentId,
                feeAssignmentId: input.feeAssignmentId,
                feeName: input.feeName,
                installment: input.installment,
                studentId: input.studentId,
                studentName: student.name || '',
                studentUniqueId: student.uniqueId || '',
                classId: input.classId,
                className: cls?.name || '',
                academicYearId: input.academicYearId,
                amount: billAmount,
                fineAmount: Math.max(0, input.fineAmount || 0),
                totalPaid,
                balance,
                status,
                paymentMode,
                generatedBy: input.generatedBy || 'System',
                isActive: true,
            });
            return invoice.toObject();
        }
        catch (err) {
            if (err?.code === 11000) {
                const concurrent = await this.invoiceModel
                    .findOne({ feeAssignmentId: input.feeAssignmentId })
                    .lean();
                if (concurrent) {
                    const $set = {
                        amount: billAmount,
                        fineAmount: Math.max(0, input.fineAmount || 0),
                        totalPaid,
                        balance,
                        status,
                        date: new Date(),
                    };
                    if (paymentMode)
                        $set.paymentMode = paymentMode;
                    return this.invoiceModel.findByIdAndUpdate(concurrent._id, { $set }, { new: true }).lean({ virtuals: true });
                }
            }
            throw err;
        }
    }
};
exports.InvoicesService = InvoicesService;
exports.InvoicesService = InvoicesService = InvoicesService_1 = __decorate([
    (0, common_1.Injectable)(),
    __param(0, (0, mongoose_1.InjectModel)(invoice_schema_1.Invoice.name)),
    __param(1, (0, mongoose_1.InjectModel)(student_schema_1.Student.name)),
    __param(2, (0, mongoose_1.InjectModel)(class_schema_1.Class.name)),
    __metadata("design:paramtypes", [mongoose_2.Model,
        mongoose_2.Model,
        mongoose_2.Model])
], InvoicesService);
//# sourceMappingURL=invoices.service.js.map