const Transaction = require('./transactionModel') const Property = require('../property/propertyModel') const AppError = require('../../globalErrorHandler/AppError') const mongoose = require('mongoose') const ObjectID = mongoose.Schema.Types.ObjectId const { client } = require('../../setup/typesense') const TsApiFeatures = require('../../utils/TsApiFeatures') const filterObj = (obj, ...allowedFields) => { const newObj = {} Object.keys(obj).forEach((el) => { if (allowedFields.includes(el)) newObj[el] = obj[el] }) return newObj } const createTransaction = async (data) => { if (data.type == 'withdraw') { const result = await Transaction.aggregate([ { $match: { propertyId: ObjectID, status: 'finished', }, }, { $group: { _id: '$propertyId', totalInventory: { $sum: '$value', }, }, }, ]) if (result[0]?.totalInventory <= 0) throw new AppError('Invalid Transaction', 404, {}) } return Transaction.create({ type: data.type, status: data.status, source: data.source, value: data.type === 'withdraw' ? data.value * -1 : data.value, date: data.Date, description: data.description, userId: data.userId, propertyId: data.propertyId || data.belongingId, userName: data.userName, }) } const getUserTransaction = async (userId) => { return Transaction.find({ userId }, null, null) } const getTransactionByBelongingId = async (propertyId) => { return Transaction.find({ propertyId }, null, null) } const getBelongingInventory = async (propertyId) => { const property = await Property.findById(propertyId, null, null) if (!property) { throw new AppError('NoPropertyFound', 404, {}) } const result = await Transaction.aggregate([ { $match: { propertyId: property._id, $or: [{ status: 'finished' }, { status: 'pending' }], }, }, { $facet: { finishedInventory: [ { $match: { status: 'finished' } }, { $group: { _id: null, total: { $sum: '$value' }, }, }, ], pendingDepositsInventory: [ { $match: { status: 'pending', type: 'deposit', }, }, { $group: { _id: null, total: { $sum: '$value' }, }, }, ], pendingWithdrawsInventory: [ { $match: { status: 'pending', type: 'withdraw', }, }, { $group: { _id: null, total: { $sum: '$value' }, }, }, ], }, }, { $project: { finishedInventory: { $ifNull: [{ $arrayElemAt: ['$finishedInventory.total', 0] }, 0] }, pendingDepositsInventory: { $ifNull: [{ $arrayElemAt: ['$pendingDepositsInventory.total', 0] }, 0] }, pendingWithdrawsInventory: { $ifNull: [{ $arrayElemAt: ['$pendingWithdrawsInventory.total', 0] }, 0] }, }, }, ]) return { ...result[0] } } const updateTransaction = async (transactionId, data) => { const filteredBody = filterObj(data, 'status') const transaction = await Transaction.findOneAndUpdate({ _id: transactionId }, filteredBody, { new: true }) if (!transaction) { throw new AppError('NoTransactionFound', 404, {}) } return transaction } const deleteTransaction = async (transactionId) => { const transaction = await Transaction.findOneAndDelete({ _id: transactionId }) if (!transaction) { throw AppError('NoTransactionFound', 404, {}) } return null } const ensureCollectionExists = async (forceRecreate = false) => { try { const collections = await client.collections().retrieve() const collectionExists = collections.some((collection) => collection.name === 'transactions') if (collectionExists && forceRecreate) { await client.collections('transactions').delete() } if (!collectionExists || forceRecreate) { const schema = getTypeSenseSchema() await client.collections().create(schema) console.log('Collection "transactions" created successfully') } } catch (err) { console.error('Error ensuring transactions collection exists:', err) } } const searchTransactions = async (inputQuery) => { await ensureCollectionExists(false) inputQuery = updateInputQuery(inputQuery, {}) const tsFeatures = new TsApiFeatures(client, 'transactions', inputQuery).filter().facet().sort().limitFields().paginate().search() let { facets, items, totalCount, pagesCount } = await tsFeatures.execute() return { facets, transactions: items, totalCount, pagesCount } } const updateInputQuery = (inputQuery, extraQueries) => { for (const [key, value] of Object.entries(extraQueries)) { inputQuery[key] = value } inputQuery.query_by = 'description,_id' return inputQuery } const indexAllTransactions = async () => { const batchSize = 20 const count = await Transaction.countDocuments({}, null) const rounds = Math.max(1, Math.ceil(count / batchSize)) await ensureCollectionExists(true) for (let i = 1; i <= rounds; i++) { const transactions = await Transaction.find({}, null, null) .skip((i - 1) * batchSize) .limit(batchSize) .populate('propertyId') if (transactions.length === 0) break let documents = [] for (const transaction of transactions) { const data = { _id: transaction._id.toString(), type: transaction.type || '', status: transaction.status || '', source: transaction.source || '', value: transaction.value || 0, description: transaction.description || '', createdAt: Math.floor(new Date(transaction.createdAt || Date.now()).getTime() / 1000), belongingName: transaction.propertyId?.name || '', belongingId: transaction.propertyId?._id ? transaction.propertyId._id.toString() : '', userName: transaction.userName || '', } documents.push(data) } if (documents.length > 0) { await client.collections('transactions').documents().import(documents, { action: 'emplace' }) } } return { totalCount: count } } const getTypeSenseSchema = () => { return { name: 'transactions', enable_nested_fields: true, fields: [ { name: '_id', type: 'string', facet: false, infix: false }, { name: 'type', type: 'string', facet: false }, { name: 'status', type: 'string', facet: false }, { name: 'source', type: 'string', facet: false }, { name: 'value', type: 'int64', facet: false }, { name: 'description', type: 'string', facet: false, infix: true }, { name: 'createdAt', type: 'int64', facet: false }, { name: 'belongingName', type: 'string', facet: false, infix: true }, { name: 'belongingId', type: 'string', facet: false, infix: true }, { name: 'userName', type: 'string', facet: false, infix: true }, ], default_sorting_field: 'createdAt', } } module.exports = { createTransaction, getUserTransaction, getBelongingInventory, updateTransaction, deleteTransaction, getTransactionByBelongingId, indexAllTransactions, searchTransactions, }