"use strict";
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 __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var TransactionsService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TransactionsService = void 0;
const axios_1 = require("@nestjs/axios");
const common_1 = require("@nestjs/common");
const client_1 = require("../../generated/prisma/client");
const rxjs_1 = require("rxjs");
const bootstrap_table_response_dto_1 = require("../commons/dto/bootstrap-table-response.dto");
const runum_service_1 = require("../commons/services/runum.service");
const prisma_query_builder_1 = require("../commons/utils/prisma-query-builder");
const notes_service_1 = require("../note/notes.service");
const prisma_service_1 = require("../prisma/prisma.service");
const storage_service_1 = require("../storage/storage.service");
const config_1 = require("@nestjs/config");
let TransactionsService = TransactionsService_1 = class TransactionsService {
    constructor(prisma, storage, runum, notesService, httpService, configService) {
        this.prisma = prisma;
        this.storage = storage;
        this.runum = runum;
        this.notesService = notesService;
        this.httpService = httpService;
        this.configService = configService;
        this.logger = new common_1.Logger(TransactionsService_1.name);
        this.MAX_EXPORT_MONTHS = 3;
        this.includeAll = {
            items: {
                include: {
                    images: true,
                    docs: true,
                }
            },
            bank: true,
            docs: true,
            customer: true,
            payments: {
                include: {
                    transferUser: {
                        select: {
                            id: true,
                            firstName: true,
                            lastName: true,
                            email: true,
                        }
                    }
                }
            },
            headStoreUser: {
                select: {
                    id: true,
                    firstName: true,
                    lastName: true,
                    email: true,
                }
            },
            completedByUser: {
                select: {
                    id: true,
                    firstName: true,
                    lastName: true,
                    email: true,
                }
            },
            branch: true,
            createdByUser: {
                select: {
                    id: true,
                    firstName: true,
                    lastName: true,
                    email: true,
                }
            },
        };
    }
    async checkBranchAccess(user, branchId) {
        const role = user?.role?.code;
        const userId = user?.id;
        const userEmail = user?.email;
        const context = {
            userEmail,
            role,
            branchId,
        };
        this.logger.debug(`[ACCESS_CHECK] Checking branch access`, JSON.stringify(context));
        const isRestrictedRole = ['FRONTLINER', 'HEAD_STORE'].includes(role || '');
        if (isRestrictedRole && !branchId) {
            this.logger.warn(`[BAD_REQUEST] Missing branchId for restricted role`, JSON.stringify(context));
            throw new common_1.BadRequestException('branchId is required for your role');
        }
        if (branchId) {
            const branch = await this.prisma.branch.findUnique({
                where: { id: branchId },
                select: { id: true },
            });
            if (!branch) {
                this.logger.warn(`[NOT_FOUND] Branch not found`, JSON.stringify(context));
                throw new common_1.NotFoundException('Branch not found');
            }
            if (isRestrictedRole) {
                const exists = await this.prisma.userBranch.findUnique({
                    where: { userId_branchId: { userId, branchId } },
                });
                if (!exists) {
                    this.logger.warn(`[FORBIDDEN] User not assigned to branch`, JSON.stringify(context));
                    throw new common_1.ForbiddenException('You are not assigned to this branch');
                }
            }
        }
        this.logger.debug(`[ACCESS_GRANTED] Branch access granted`, JSON.stringify(context));
    }
    async generateTrxCode(branchId) {
        const branch = await this.prisma.branch.findUniqueOrThrow({
            where: { id: branchId },
            select: { code: true },
        });
        const today = new Date();
        const dd = String(today.getDate()).padStart(2, '0');
        const mm = String(today.getMonth() + 1).padStart(2, '0');
        const yy = String(today.getFullYear()).slice(-2);
        const dateStr = `${dd}${mm}${yy}`;
        const reset = `${branchId}${dateStr}`;
        const runumId = `TRX_${branchId}`;
        const seq = await this.runum.getNextNumber(runumId, reset);
        const seqStr = String(seq).padStart(3, '0');
        return `${branch.code}${dateStr}${seqStr}`;
    }
    normalizeValue(value) {
        if (value === null || value === undefined)
            return null;
        if (value instanceof client_1.Prisma.Decimal) {
            return value.toString();
        }
        if (value instanceof Date) {
            return value.getTime();
        }
        return value;
    }
    async aggregateTransactions(start, endExclusive, branchId) {
        const result = await this.prisma.$queryRaw(client_1.Prisma.sql `
      SELECT
        COUNT(*) FILTER (WHERE status = 'COMPLETED') as success_count,
        COUNT(*) FILTER (WHERE status IN ('REJECTED','CANCELED')) as bounce_count,

        SUM("totalAmount") FILTER (WHERE status = 'COMPLETED') as success_amount,
        SUM("totalAmount") FILTER (WHERE status IN ('REJECTED','CANCELED')) as bounce_amount,

        SUM("totalNetWeight") FILTER (WHERE status = 'COMPLETED') as success_weight,
        SUM("totalNetWeight") FILTER (WHERE status IN ('REJECTED','CANCELED')) as bounce_weight

      FROM "Transaction"
      WHERE "createdAt" >= ${start}
        AND "createdAt" < ${endExclusive}
        ${branchId
            ? client_1.Prisma.sql `AND "branchId" = ${branchId}`
            : client_1.Prisma.empty}
    `);
        const row = result[0] ?? {};
        return {
            transactionCount: {
                success: Number(row.success_count ?? 0),
                bounce: Number(row.bounce_count ?? 0),
            },
            totalAmount: {
                success: Number(row.success_amount ?? 0),
                bounce: Number(row.bounce_amount ?? 0),
            },
            totalNetWeight: {
                success: Number(row.success_weight ?? 0),
                bounce: Number(row.bounce_weight ?? 0),
            },
        };
    }
    buildAuditLogs(oldObj, newObj, entityType, entityId, userId, headerEntityType, headerEntityId) {
        const logs = [];
        for (const key of Object.keys(newObj)) {
            const oldValue = this.normalizeValue(oldObj[key]);
            const newValue = this.normalizeValue(newObj[key]);
            if (oldValue === newValue)
                continue;
            logs.push({
                entityType,
                entityId,
                fieldName: key,
                headerEntityType,
                headerEntityId,
                oldValue: oldObj[key]?.toString() ?? null,
                newValue: newObj[key]?.toString() ?? null,
                changedBy: userId
            });
        }
        return logs;
    }
    computeChange(current, previous) {
        const value = current - previous;
        let percent = null;
        if (previous !== 0) {
            percent = Number(((value / previous) * 100).toFixed(2));
        }
        let trend = 'flat';
        if (value > 0)
            trend = 'up';
        if (value < 0)
            trend = 'down';
        return {
            value,
            percent,
            trend,
        };
    }
    async findAll(query, user) {
        const logContext = {
            userEmail: user?.email,
            query,
            filter: query?.filter,
        };
        this.logger.debug(`[QUERY_RECEIVED] findAll`, JSON.stringify(logContext));
        const allowedFilters = {
            status: 'enum',
            branchId: 'exact',
            createdAt_gte: 'date',
            createdAt_lte: 'date',
            customerId: 'exact',
        };
        const where = (0, prisma_query_builder_1.buildWhereClause)(query, allowedFilters, ['trxCode', 'items.itemCode']);
        if (!user.role?.accessAllBranch) {
            const userBranchIds = user.branches.map(b => b.branchId);
            where.branchId = {
                in: userBranchIds,
            };
        }
        const allowedSortColumns = ['trxCode', 'status', 'totalAmount', 'createdAt'];
        const orderBy = {};
        if (query.sort && allowedSortColumns.includes(query.sort)) {
            orderBy[query.sort] = query.sortOrder;
        }
        else {
            orderBy.createdAt = 'desc';
        }
        const [total, rows] = await this.prisma.$transaction([
            this.prisma.transaction.count({ where }),
            this.prisma.transaction.findMany({
                where,
                skip: query.skip,
                take: query.take,
                orderBy,
                include: this.includeAll,
            }),
        ]);
        return new bootstrap_table_response_dto_1.BootstrapTableResponseDto(total, rows);
    }
    async findOne(id) {
        const transaction = await this.prisma.transaction.findUniqueOrThrow({
            where: { id },
            include: this.includeAll,
        });
        const notes = await this.prisma.note.findMany({
            where: { entityType: 'TRANSACTION', entityId: id },
            orderBy: { createdAt: 'asc' },
        });
        return { ...transaction, notes };
    }
    async getAuditLogsForTransaction(id, query) {
        const where = {
            headerEntityType: 'TRANSACTION',
            headerEntityId: id,
        };
        const allowedSortColumns = ['changedAt', 'fieldName', 'entityType', 'oldValue', 'newValue'];
        const orderBy = {};
        if (query.sort && allowedSortColumns.includes(query.sort)) {
            orderBy[query.sort] = query.sortOrder;
        }
        else {
            orderBy.changedAt = 'desc';
        }
        const [total, rows] = await this.prisma.$transaction([
            this.prisma.auditLog.count({ where }),
            this.prisma.auditLog.findMany({
                where,
                skip: query.skip,
                take: query.take,
                orderBy,
                include: {
                    changedByUser: {
                        select: {
                            id: true,
                            firstName: true,
                            lastName: true,
                            email: true,
                        },
                    },
                },
            }),
        ]);
        return new bootstrap_table_response_dto_1.BootstrapTableResponseDto(total, rows);
    }
    async update(id, dto, user) {
        const transaction = await this.prisma.transaction.findUniqueOrThrow({
            where: { id },
            include: {
                items: {
                    include: { images: true }
                }
            }
        });
        const isHeadStoreAssignment = transaction.status === 'OPEN' &&
            dto.status === 'WAITING_PAYMENT';
        return this.prisma.$transaction(async (tx) => {
            const updatedTransaction = await tx.transaction.update({
                where: { id },
                data: {
                    status: dto.status,
                    customerId: dto.customerId,
                    referralSource: dto.referralSource,
                    knowsFounder: dto.knowsFounder,
                    totalGrossWeight: dto.totalGrossWeight,
                    totalNetWeight: dto.totalNetWeight,
                    totalAmount: dto.totalAmount,
                    bankId: dto.bankId,
                    bankAccountNo: dto.bankAccountNo,
                    bankAccountName: dto.bankAccountName,
                    ...(isHeadStoreAssignment && { headStoreId: user.id }),
                }
            });
            const auditLogs = [];
            auditLogs.push(...this.buildAuditLogs(transaction, updatedTransaction, "TRANSACTION", updatedTransaction.id, user.id, "TRANSACTION", updatedTransaction.id));
            if (dto.items?.length) {
                for (const item of dto.items) {
                    const originalItem = transaction.items.find(i => i.id === item.id);
                    const updatedItem = await tx.transactionItem.update({
                        where: { id: item.id },
                        data: {
                            itemCode: item.itemCode,
                            itemTypeId: item.itemTypeId,
                            caratId: item.caratId,
                            purityPercent: item.purityPercent,
                            grossWeight: item.grossWeight,
                            netWeight: item.netWeight,
                            pricePerGram: item.pricePerGram,
                            pricePerGramAt: item.pricePerGramAt,
                        }
                    });
                    if (originalItem) {
                        auditLogs.push(...this.buildAuditLogs(originalItem, updatedItem, "TRANSACTION_ITEM", updatedItem.id, user.id, "TRANSACTION", updatedTransaction.id));
                    }
                }
            }
            if (auditLogs.length > 0) {
                await tx.auditLog.createMany({
                    data: auditLogs
                });
            }
            return tx.transaction.findUnique({
                where: { id },
                include: {
                    items: {
                        include: { images: true }
                    }
                }
            });
        });
    }
    async remove(id) {
        await this.prisma.transaction.findUniqueOrThrow({
            where: { id },
        });
        return this.prisma.transaction.delete({
            where: { id },
        });
    }
    async cancel(id, dto, user) {
        const transaction = await this.prisma.transaction.findUniqueOrThrow({
            where: { id },
        });
        if (transaction.status === 'CANCELED') {
            throw new common_1.BadRequestException('Transaction is already cancelled');
        }
        if (transaction.status === 'COMPLETED') {
            throw new common_1.BadRequestException('Cannot cancel a completed transaction');
        }
        return this.prisma.$transaction(async (tx) => {
            const updatedTransaction = await tx.transaction.update({
                where: { id },
                data: {
                    status: 'CANCELED',
                    cancellationReason: dto.reason,
                    cancelledBy: user.id,
                    cancelledAt: new Date(),
                },
                include: {
                    items: {
                        include: { images: true }
                    }
                }
            });
            const auditLogs = [];
            auditLogs.push(...this.buildAuditLogs(transaction, updatedTransaction, "TRANSACTION", updatedTransaction.id, user.id, "TRANSACTION", updatedTransaction.id));
            if (auditLogs.length > 0) {
                await tx.auditLog.createMany({
                    data: auditLogs
                });
            }
            return updatedTransaction;
        });
    }
    async getSummary(filter) {
        const counts = await this.prisma.transaction.groupBy({
            by: ['status'],
            where: {
                ...(filter?.branchId && { branchId: filter.branchId }),
            },
            _count: { id: true },
        });
        const summary = counts.reduce((acc, item) => {
            acc[item.status] = item._count.id;
            return acc;
        }, {});
        const allStatuses = [
            'OPEN',
            'WAITING_PAYMENT',
            'PAID_WAITING_HS_REVIEW',
            'COMPLETED',
            'CANCELED',
        ];
        for (const status of allStatuses) {
            if (!summary[status])
                summary[status] = 0;
        }
        return summary;
    }
    async create(dto, user) {
        const role = await this.prisma.role.findUnique({
            where: { id: user.roleId },
        });
        console.log("create timestamp at transactions.service", new Date().toISOString());
        if (!role || !['FRONTLINER', 'SUPER_ADMIN', 'HEAD_STORE'].includes(role.code)) {
            throw new common_1.ForbiddenException('Only FRONTLINER, HEAD_STORE and SUPER_ADMIN can create transaction');
        }
        const branch = await this.prisma.branch.findUnique({
            where: { id: dto.branchId },
            select: { id: true },
        });
        if (!branch) {
            throw new common_1.NotFoundException('Branch not found');
        }
        if (role.code !== 'SUPER_ADMIN') {
            const userBranch = await this.prisma.userBranch.findUnique({
                where: {
                    userId_branchId: {
                        userId: user.id,
                        branchId: dto.branchId,
                    },
                },
            });
            if (!userBranch) {
                throw new common_1.ForbiddenException('User is not assigned to the selected branch');
            }
        }
        const branchId = dto.branchId;
        const trxCode = await this.generateTrxCode(dto.branchId);
        let totalNet = new client_1.Prisma.Decimal(0);
        let totalGross = new client_1.Prisma.Decimal(0);
        let totalAmount = new client_1.Prisma.Decimal(0);
        const today = new Date();
        const dd = String(today.getDate()).padStart(2, '0');
        const mm = String(today.getMonth() + 1).padStart(2, '0');
        const yy = String(today.getFullYear()).slice(-2);
        const dateStr = `${dd}${mm}${yy}`;
        console.log("create timestamp before API call at transactions.service", new Date().toISOString());
        const disablePriceValidation = process.env.DISABLE_PRICE_VALIDATION === 'true';
        let priceData = {};
        if (!disablePriceValidation) {
            console.log("create timestamp before API call at transactions.service", new Date().toISOString());
            let url = this.configService.get('COMPARISON_API_URL') || "https://super-emas-be.onrender.com/api/comparison-data'";
            const { data } = await (0, rxjs_1.firstValueFrom)(this.httpService.get(url));
            console.log("create timestamp after API call at transactions.service", new Date().toISOString());
            priceData = data.priceData;
        }
        console.log("create timestamp before item processing at transactions.service", new Date().toISOString());
        const itemsData = await Promise.all(dto.items.map(async (item) => {
            const gross = new client_1.Prisma.Decimal(item.grossWeight);
            const net = new client_1.Prisma.Decimal(item.netWeight);
            if (net.gt(gross)) {
                throw new common_1.BadRequestException(`Net weight cannot exceed gross weight`);
            }
            if (!disablePriceValidation) {
                if (!priceData[item.caratId]) {
                    throw new common_1.BadRequestException(`Invalid caratId: ${item.caratId}`);
                }
            }
            const clientPrice = new client_1.Prisma.Decimal(item.pricePerGram);
            const apiPrice = disablePriceValidation ? clientPrice : new client_1.Prisma.Decimal(priceData[item.caratId].buybackPrice);
            if (!clientPrice.equals(apiPrice)) {
                throw new common_1.BadRequestException(`Price mismatch for ${item.caratId}. Expected ${apiPrice}, received ${clientPrice}`);
            }
            const total = net.mul(apiPrice);
            totalNet = totalNet.add(net);
            totalGross = totalGross.add(gross);
            totalAmount = totalAmount.add(total);
            return {
                itemCode: item.itemCode,
                itemTypeId: item.itemTypeId,
                grossWeight: gross,
                netWeight: net,
                caratId: item.caratId,
                pricePerGram: apiPrice,
                pricePerGramAt: new Date(item.pricePerGramAt),
                purityPercent: item.purityPercent,
                totalPrice: total,
            };
        }));
        if (dto.status === client_1.TransactionStatus.CANCELED) {
            if (!dto.reason) {
                throw new common_1.BadRequestException('A reason is required when cancelling a transaction');
            }
        }
        console.log("create timestamp after item processing at transactions.service", new Date().toISOString());
        return this.prisma.$transaction(async (tx) => {
            const transaction = await tx.transaction.create({
                data: {
                    trxCode,
                    branchId,
                    customerId: dto.customerId,
                    createdBy: user.id,
                    status: dto.status,
                    referralSource: dto.referralSource,
                    knowsFounder: dto.knowsFounder,
                    bankId: dto.bankId,
                    bankAccountNo: dto.bankAccountNo,
                    bankAccountName: dto.bankAccountName,
                    totalGrossWeight: totalGross,
                    totalNetWeight: totalNet,
                    totalAmount,
                    items: { create: itemsData },
                },
                include: { items: true },
            });
            for (let i = 0; i < dto.items.length; i++) {
                const itemDto = dto.items[i];
                const createdItem = transaction.items[i];
                if (!itemDto.tempPhotoIds?.length)
                    continue;
                const tempPhotos = await tx.tempPhoto.findMany({
                    where: { id: { in: itemDto.tempPhotoIds } },
                });
                console.log('tempPhotoIds received:', itemDto.tempPhotoIds);
                console.log('tempPhotos found:', tempPhotos);
                for (const [order, temp] of tempPhotos.entries()) {
                    const permanentUrl = await this.storage.promoteToPermament(temp.url, `items/${createdItem.id}`);
                    await tx.transactionItemPhoto.create({
                        data: {
                            itemId: createdItem.id,
                            url: permanentUrl,
                            filename: temp.filename,
                            mimetype: temp.mimetype,
                            size: temp.size,
                            order,
                        },
                    });
                    await tx.tempPhoto.delete({ where: { id: temp.id } });
                }
            }
            const docUploads = [
                { ids: dto.serahTerimaIds ?? [], type: 'SERAH_TERIMA' },
                { ids: dto.suratPernyataanIds ?? [], type: 'SURAT_PERNYATAAN' },
            ];
            for (const { ids, type } of docUploads) {
                if (!ids.length)
                    continue;
                const tempDocs = await tx.tempPhoto.findMany({
                    where: { id: { in: ids } },
                });
                for (const [order, temp] of tempDocs.entries()) {
                    const permanentUrl = await this.storage.promoteToPermament(temp.url, `transactions/${transaction.id}/docs`);
                    await tx.transactionDoc.create({
                        data: {
                            transactionId: transaction.id,
                            type,
                            url: permanentUrl,
                            filename: temp.filename,
                            mimetype: temp.mimetype,
                            size: temp.size,
                            order,
                        },
                    });
                    await tx.tempPhoto.delete({ where: { id: temp.id } });
                }
            }
            if (dto.status === client_1.TransactionStatus.CANCELED) {
                await tx.transaction.update({
                    where: { id: transaction.id },
                    data: {
                        status: 'CANCELED',
                        cancellationReason: dto.reason,
                        cancelledBy: user.id,
                        cancelledAt: new Date(),
                    },
                });
            }
            return tx.transaction.findUnique({
                where: { id: transaction.id },
                include: this.includeAll,
            });
        });
    }
    async addPayment(id, dto, user) {
        const transaction = await this.prisma.transaction.findUniqueOrThrow({
            where: { id },
            include: { payments: true }
        });
        if (transaction.status === 'CANCELED') {
            throw new common_1.BadRequestException('Cannot add payment to a cancelled transaction');
        }
        if (transaction.status === 'COMPLETED') {
            throw new common_1.BadRequestException('Transaction is already completed');
        }
        return this.prisma.$transaction(async (tx) => {
            const auditLogs = [];
            const temp = await tx.tempPhoto.findUniqueOrThrow({
                where: { id: dto.tempPhotoId },
            });
            const permanentUrl = await this.storage.promoteToPermament(temp.url, `transactions/${id}/payments`);
            await tx.tempPhoto.delete({ where: { id: temp.id } });
            const oldPayment = transaction.payments;
            const payment = await tx.payment.upsert({
                where: { transactionId: id },
                create: {
                    transactionId: id,
                    transferBy: dto.transferBy,
                    transferDate: new Date(),
                    proofUrl: permanentUrl,
                    filename: temp.filename,
                    mimetype: temp.mimetype,
                    size: temp.size,
                },
                update: {
                    transferBy: dto.transferBy,
                    transferDate: new Date(),
                    proofUrl: permanentUrl,
                    filename: temp.filename,
                    mimetype: temp.mimetype,
                    size: temp.size,
                },
            });
            if (oldPayment) {
                auditLogs.push(...this.buildAuditLogs(oldPayment, payment, "PAYMENT", payment.id, user.id, "TRANSACTION", id));
            }
            else {
                auditLogs.push({
                    entityType: "PAYMENT",
                    entityId: payment.id,
                    headerEntityType: "TRANSACTION",
                    headerEntityId: id,
                    fieldName: "CREATE",
                    oldValue: null,
                    newValue: "Payment created",
                    createdBy: user.id
                });
            }
            const updatedTransaction = await tx.transaction.update({
                where: { id },
                data: { status: 'PAID_WAITING_HS_REVIEW' },
            });
            auditLogs.push(...this.buildAuditLogs(transaction, updatedTransaction, "TRANSACTION", updatedTransaction.id, user.id, "TRANSACTION", updatedTransaction.id));
            if (auditLogs.length > 0) {
                await tx.auditLog.createMany({
                    data: auditLogs
                });
            }
            return payment;
        });
    }
    async complete(id, note, user) {
        const transaction = await this.findOne(id);
        if (!transaction) {
            throw new common_1.NotFoundException(`Transaction ${id} not found`);
        }
        if (transaction.status !== 'PAID_WAITING_HS_REVIEW') {
            throw new common_1.BadRequestException(`Transaction must be PAID_WAITING_HS_REVIEW to complete. Current: ${transaction.status}`);
        }
        return this.prisma.$transaction(async (tx) => {
            const updatedTransaction = await tx.transaction.update({
                where: { id },
                data: {
                    status: 'COMPLETED',
                    completedBy: user.id,
                    completedAt: new Date(),
                },
            });
            await this.notesService.createNote('TRANSACTION', id, 'COMPLETED', note, user.id);
            const auditLogs = [];
            auditLogs.push(...this.buildAuditLogs(transaction, updatedTransaction, "TRANSACTION", updatedTransaction.id, user.id, "TRANSACTION", updatedTransaction.id));
            if (auditLogs.length > 0) {
                await tx.auditLog.createMany({
                    data: auditLogs
                });
            }
            return updatedTransaction;
        });
    }
    async getTransactionsForExport(query, user) {
        let start;
        let end;
        const isAllBranch = user.role?.accessAllBranch === true;
        const userBranchIds = (user.branches ?? []).map((b) => b.branchId);
        if (!query.startDate && !query.limit) {
            throw new common_1.BadRequestException('Provide at least startDate or limit to export');
        }
        if (query.startDate && query.endDate) {
            start = new Date(query.startDate);
            end = new Date(query.endDate);
            end.setHours(23, 59, 59, 999);
            if (start > end) {
                throw new common_1.BadRequestException('startDate cannot be after endDate');
            }
            const diffMonths = (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24 * 30);
            if (diffMonths > this.MAX_EXPORT_MONTHS) {
                throw new common_1.BadRequestException(`Export date range cannot exceed ${this.MAX_EXPORT_MONTHS} months`);
            }
        }
        let branchFilter = {};
        if (isAllBranch) {
            if (query.branchId) {
                branchFilter = { branchId: query.branchId };
            }
        }
        else {
            branchFilter = userBranchIds.length > 0
                ? { branchId: { in: userBranchIds } }
                : { branchId: 'NO_BRANCH_ACCESS' };
        }
        const where = {
            ...branchFilter,
            ...(query.status && { status: query.status }),
            ...(start || end
                ? {
                    createdAt: {
                        ...(start && { gte: start }),
                        ...(end && { lte: end }),
                    },
                }
                : {}),
        };
        return this.prisma.transaction.findMany({
            where,
            include: {
                customer: true,
                items: {
                    include: {
                        itemType: true,
                    },
                },
                bank: true,
                branch: true,
                createdByUser: {
                    select: { id: true, firstName: true, lastName: true, email: true },
                },
                headStoreUser: {
                    select: { id: true, firstName: true, lastName: true, email: true },
                },
            },
            orderBy: { createdAt: 'asc' },
            ...(query.limit !== undefined && { take: query.limit }),
            ...(query.offset !== undefined && { skip: query.offset }),
        });
    }
    async generateItemId() {
        const today = new Date();
        const dd = String(today.getDate()).padStart(2, '0');
        const mm = String(today.getMonth() + 1).padStart(2, '0');
        const yy = String(today.getFullYear()).slice(-2);
        const dateStr = `${yy}${mm}${dd}`;
        const runumId = `TXN_ITEMS`;
        const reset = `${dateStr}`;
        const seq = await this.runum.getNextNumber(runumId, reset);
        const seqStr = String(seq).padStart(3, '0');
        return `${"ITM-"}${dateStr}${seqStr}`;
    }
    async getAnalyticsChart(query, user) {
        const { startDate, endDate, groupBy, branchId } = query;
        const start = new Date(`${startDate}T00:00:00.000Z`);
        const endExclusive = new Date(`${endDate}T00:00:00.000Z`);
        endExclusive.setDate(endExclusive.getDate() + 1);
        await this.checkBranchAccess(user, branchId);
        const rows = await this.prisma.$queryRawUnsafe(`
    SELECT
      date_trunc('${groupBy}', "createdAt") as period,

      COUNT(*) FILTER (WHERE status IN ('COMPLETED')) as success_count,
      SUM("totalAmount") FILTER (WHERE status IN ('COMPLETED')) as success_amount,
      SUM("totalNetWeight") FILTER (WHERE status IN ('COMPLETED')) as success_weight,

      COUNT(*) FILTER (WHERE status IN ('REJECTED','CANCELED')) as bounce_count,
      SUM("totalAmount") FILTER (WHERE status IN ('REJECTED','CANCELED')) as bounce_amount,
      SUM("totalNetWeight") FILTER (WHERE status IN ('REJECTED','CANCELED')) as bounce_weight

    FROM "Transaction"
    WHERE "createdAt" >= $1
      AND "createdAt" < $2
      ${branchId ? 'AND "branchId" = $3' : ''}
    GROUP BY period
    ORDER BY period
  `, ...(branchId ? [start, endExclusive, branchId] : [start, endExclusive]));
        const labels = [];
        const successCount = [];
        const successAmount = [];
        const successWeight = [];
        const bounceCount = [];
        const bounceAmount = [];
        const bounceWeight = [];
        for (const r of rows) {
            labels.push(r.period.toISOString().slice(0, 10));
            successCount.push(Number(r.success_count ?? 0));
            successAmount.push(Number(r.success_amount ?? 0));
            successWeight.push(Number(r.success_weight ?? 0));
            bounceCount.push(Number(r.bounce_count ?? 0));
            bounceAmount.push(Number(r.bounce_amount ?? 0));
            bounceWeight.push(Number(r.bounce_weight ?? 0));
        }
        return {
            labels,
            series: {
                transactionCount: {
                    success: successCount,
                    bounce: bounceCount,
                },
                totalAmount: {
                    success: successAmount,
                    bounce: bounceAmount,
                },
                totalNetWeight: {
                    success: successWeight,
                    bounce: bounceWeight,
                }
            }
        };
    }
    async getAnalyticsSummary(query, user) {
        const { startDate, endDate, branchId } = query;
        await this.checkBranchAccess(user, branchId);
        const start = new Date(`${startDate}T00:00:00.000Z`);
        const endExclusive = new Date(`${endDate}T00:00:00.000Z`);
        endExclusive.setDate(endExclusive.getDate() + 1);
        const diffDays = Math.ceil((endExclusive.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
        const prevEndExclusive = new Date(start);
        const prevStart = new Date(prevEndExclusive);
        prevStart.setDate(prevStart.getDate() - diffDays);
        const [current, previous] = await Promise.all([
            this.aggregateTransactions(start, endExclusive, branchId),
            this.aggregateTransactions(prevStart, prevEndExclusive, branchId),
        ]);
        const buildMetric = (metric) => ({
            currentPeriod: {
                start,
                end: new Date(endExclusive.getTime() - 1),
                success: current[metric].success,
                bounce: current[metric].bounce,
            },
            previousPeriod: {
                start: prevStart,
                end: new Date(prevEndExclusive.getTime() - 1),
                success: previous[metric].success,
                bounce: previous[metric].bounce,
            },
            changes: {
                success: this.computeChange(current[metric].success, previous[metric].success),
                bounce: this.computeChange(current[metric].bounce, previous[metric].bounce),
            },
        });
        return {
            transactionCount: buildMetric('transactionCount'),
            totalAmount: buildMetric('totalAmount'),
            totalNetWeight: buildMetric('totalNetWeight'),
        };
    }
    async getPerformanceSummary(query, user) {
        const { startDate, endDate, branchId } = query;
        const start = new Date(`${startDate}T00:00:00.000Z`);
        const endExclusive = new Date(`${endDate}T00:00:00.000Z`);
        endExclusive.setDate(endExclusive.getDate() + 1);
        await this.checkBranchAccess(user, branchId);
        const result = await this.prisma.$queryRaw(client_1.Prisma.sql `

    SELECT

      COUNT(*) as all_count,
      SUM("totalGrossWeight") as all_gross_weight,
      SUM("totalAmount") as all_amount,

      COUNT(*) FILTER (WHERE status = 'COMPLETED') as completed_count,
      SUM("totalGrossWeight") FILTER (WHERE status = 'COMPLETED') as completed_gross_weight,
      SUM("totalAmount") FILTER (WHERE status = 'COMPLETED') as completed_amount,

      COUNT(*) FILTER (WHERE status = 'CANCELED') as canceled_count,
      SUM("totalGrossWeight") FILTER (WHERE status = 'CANCELED') as canceled_gross_weight,
      SUM("totalAmount") FILTER (WHERE status = 'CANCELED') as canceled_amount,

      COUNT(*) FILTER (WHERE status = 'REJECTED') as rejected_count,
      SUM("totalGrossWeight") FILTER (WHERE status = 'REJECTED') as rejected_gross_weight,
      SUM("totalAmount") FILTER (WHERE status = 'REJECTED') as rejected_amount

    FROM "Transaction"

    WHERE "createdAt" >= ${start}
    AND "createdAt" < ${endExclusive}
    ${branchId ? client_1.Prisma.sql `AND "branchId" = ${branchId}` : client_1.Prisma.empty}

  `);
        const row = result[0];
        return {
            transactionCount: {
                created: Number(row.all_count ?? 0),
                completed: Number(row.completed_count ?? 0),
                canceled: Number(row.canceled_count ?? 0),
                rejected: Number(row.rejected_count ?? 0),
            },
            totalGrossWeight: {
                created: Number(row.all_gross_weight ?? 0),
                completed: Number(row.completed_gross_weight ?? 0),
                canceled: Number(row.canceled_gross_weight ?? 0),
                rejected: Number(row.rejected_gross_weight ?? 0),
            },
            totalAmount: {
                created: Number(row.all_amount ?? 0),
                completed: Number(row.completed_amount ?? 0),
                canceled: Number(row.canceled_amount ?? 0),
                rejected: Number(row.rejected_amount ?? 0),
            }
        };
    }
    async getCancellationReasons(query, user) {
        const { startDate, endDate, branchId } = query;
        const start = new Date(`${startDate}T00:00:00.000Z`);
        const endExclusive = new Date(`${endDate}T00:00:00.000Z`);
        endExclusive.setDate(endExclusive.getDate() + 1);
        await this.checkBranchAccess(user, branchId);
        const rows = await this.prisma.$queryRaw(client_1.Prisma.sql `

    SELECT
      ds.name AS reason,
      COUNT(t.id) AS count

    FROM "DetailSetting" ds
    JOIN "HeaderSetting" hs
      ON hs.id = ds."headerId"

    LEFT JOIN "Transaction" t
      ON t."cancellationReason" = ds.name
      AND t.status = 'CANCELED'
      AND t."createdAt" >= ${start}
      AND t."createdAt" < ${endExclusive}
      ${branchId ? client_1.Prisma.sql `AND t."branchId" = ${branchId}` : client_1.Prisma.empty}

    WHERE hs.code = 'CANCELLATION_REASON'
      AND ds.status = 'ACTIVE'

    GROUP BY ds.name
    ORDER BY count DESC
  `);
        return rows.map(r => ({
            reason: r.reason,
            count: Number(r.count),
        }));
    }
    async getPerformanceChart(query, user) {
        const { startDate, endDate, groupBy = 'day', branchId } = query;
        const start = new Date(`${startDate}T00:00:00.000Z`);
        const endExclusive = new Date(`${endDate}T00:00:00.000Z`);
        endExclusive.setDate(endExclusive.getDate() + 1);
        await this.checkBranchAccess(user, branchId);
        const rows = await this.prisma.$queryRaw(client_1.Prisma.sql `

    SELECT
      date_trunc(${groupBy}, "createdAt") as period,

      COUNT(*) as created_count,
      SUM("totalGrossWeight") as created_weight,
      SUM("totalAmount") as created_amount,

      COUNT(*) FILTER (WHERE status = 'COMPLETED') as success_count,
      SUM("totalGrossWeight") FILTER (WHERE status = 'COMPLETED') as success_weight,
      SUM("totalAmount") FILTER (WHERE status = 'COMPLETED') as success_amount,

      COUNT(*) FILTER (WHERE status = 'CANCELED') as canceled_count,
      SUM("totalGrossWeight") FILTER (WHERE status = 'CANCELED') as canceled_weight,
      SUM("totalAmount") FILTER (WHERE status = 'CANCELED') as canceled_amount,

      COUNT(*) FILTER (WHERE status = 'OPEN') as sla_open,
      COUNT(*) FILTER (WHERE status = 'WAITING_PAYMENT') as sla_payment,
      COUNT(*) FILTER (WHERE status = 'PAID_WAITING_HS_REVIEW') as sla_review_hs,
      COUNT(*) FILTER (WHERE status = 'COMPLETED') as sla_completed

    FROM "Transaction"

    WHERE "createdAt" >= ${start}
      AND "createdAt" < ${endExclusive}
      ${branchId ? client_1.Prisma.sql `AND "branchId" = ${branchId}` : client_1.Prisma.empty}

    GROUP BY period
    ORDER BY period desc

  `);
        const labels = [];
        const createdCount = [];
        const successCount = [];
        const canceledCount = [];
        const createdWeight = [];
        const successWeight = [];
        const canceledWeight = [];
        const createdAmount = [];
        const successAmount = [];
        const canceledAmount = [];
        const slaOpen = [];
        const slaPayment = [];
        const slaReviewHS = [];
        const slaCompleted = [];
        for (const r of rows) {
            labels.push(r.period.toISOString().slice(0, 10));
            createdCount.push(Number(r.created_count ?? 0));
            successCount.push(Number(r.success_count ?? 0));
            canceledCount.push(Number(r.canceled_count ?? 0));
            createdWeight.push(Number(r.created_weight ?? 0));
            successWeight.push(Number(r.success_weight ?? 0));
            canceledWeight.push(Number(r.canceled_weight ?? 0));
            createdAmount.push(Number(r.created_amount ?? 0));
            successAmount.push(Number(r.success_amount ?? 0));
            canceledAmount.push(Number(r.canceled_amount ?? 0));
            slaOpen.push(Number(r.sla_open ?? 0));
            slaPayment.push(Number(r.sla_payment ?? 0));
            slaReviewHS.push(Number(r.sla_review_hs ?? 0));
            slaCompleted.push(Number(r.sla_completed ?? 0));
        }
        return {
            labels,
            series: {
                transactionCount: {
                    created: createdCount,
                    success: successCount,
                    canceled: canceledCount
                },
                totalGrossWeight: {
                    created: createdWeight,
                    success: successWeight,
                    canceled: canceledWeight
                },
                totalAmount: {
                    created: createdAmount,
                    success: successAmount,
                    canceled: canceledAmount
                },
                sla: {
                    open: slaOpen,
                    waitingPayment: slaPayment,
                    paidWaitingHsReview: slaReviewHS,
                    completed: slaCompleted
                }
            }
        };
    }
};
exports.TransactionsService = TransactionsService;
exports.TransactionsService = TransactionsService = TransactionsService_1 = __decorate([
    (0, common_1.Injectable)(),
    __metadata("design:paramtypes", [prisma_service_1.PrismaService,
        storage_service_1.StorageService,
        runum_service_1.RunumService,
        notes_service_1.NotesService,
        axios_1.HttpService,
        config_1.ConfigService])
], TransactionsService);
//# sourceMappingURL=transactions.service.js.map