MeshiTrack/packages/api/src/modules/pantry/pantry.repository.ts

199 lines
5.6 KiB
TypeScript

import { PantryItemModel } from '../../schemas/pantry-item.schema.js';
import type { ItemStatus } from '@meshitrack/shared';
interface FindByHouseholdQuery {
storageLocation?: string;
status?: string;
urgency?: string;
productId?: string;
cursor?: string;
limit: number;
}
export class PantryRepository {
public async findByHousehold(householdId: string, query: FindByHouseholdQuery) {
const filter: Record<string, unknown> = { householdId };
if (query.storageLocation) filter['storageLocation'] = query.storageLocation;
if (query.status) {
const statuses = query.status.split(',').filter(Boolean);
filter['status'] = statuses.length === 1 ? statuses[0] : { $in: statuses };
}
if (query.urgency) {
const urgencies = query.urgency.split(',').filter(Boolean);
filter['freshnessEstimate.urgency'] =
urgencies.length === 1 ? urgencies[0] : { $in: urgencies };
}
if (query.productId) filter['productId'] = query.productId;
if (query.cursor) {
const id = Buffer.from(query.cursor, 'base64').toString();
filter['_id'] = { $gt: id };
}
const limit = query.limit;
const items = await PantryItemModel.find(filter)
.sort({ 'freshnessEstimate.daysRemaining': 1, _id: 1 })
.limit(limit + 1)
.lean()
.exec();
const hasMore = items.length > limit;
const data = hasMore ? items.slice(0, limit) : items;
const cursor =
data.length > 0
? Buffer.from(data[data.length - 1]!._id.toString()).toString('base64')
: null;
return { data, pagination: { cursor: hasMore ? cursor : null, hasMore } };
}
public async findById(id: string, householdId: string) {
return PantryItemModel.findOne({ _id: id, householdId }).lean().exec();
}
public async findExpiringSoon(householdId: string, days: number, cursor?: string, limit = 20) {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() + days);
const filter: Record<string, unknown> = {
householdId,
status: { $in: ['sealed', 'opened', 'prepared'] },
'freshnessEstimate.estimatedExpiryDate': { $lte: cutoff },
'freshnessEstimate.daysRemaining': { $gte: -3 },
};
if (cursor) {
const id = Buffer.from(cursor, 'base64').toString();
filter['_id'] = { $gt: id };
}
const items = await PantryItemModel.find(filter)
.sort({ 'freshnessEstimate.daysRemaining': 1, _id: 1 })
.limit(limit + 1)
.lean()
.exec();
const hasMore = items.length > limit;
const data = hasMore ? items.slice(0, limit) : items;
const cursorVal =
data.length > 0
? Buffer.from(data[data.length - 1]!._id.toString()).toString('base64')
: null;
return { data, pagination: { cursor: hasMore ? cursorVal : null, hasMore } };
}
public async findActiveByHousehold(householdId: string) {
return PantryItemModel.find({
householdId,
status: { $in: ['sealed', 'opened', 'prepared'] },
})
.sort({ 'freshnessEstimate.daysRemaining': 1, _id: 1 })
.lean()
.exec();
}
public async create(data: Record<string, unknown>) {
const doc = new PantryItemModel(data);
const saved = await doc.save();
return saved.toObject();
}
public async update(id: string, householdId: string, data: Record<string, unknown>) {
return PantryItemModel.findOneAndUpdate({ _id: id, householdId }, { $set: data }, { new: true })
.lean()
.exec();
}
public async updateFreshness(
id: string,
freshnessEstimate: Record<string, unknown>,
status?: string,
) {
const update: Record<string, unknown> = { freshnessEstimate };
if (status) update['status'] = status;
return PantryItemModel.findByIdAndUpdate(id, { $set: update }).exec();
}
public async delete(id: string, householdId: string) {
return PantryItemModel.findOneAndDelete({ _id: id, householdId }).exec();
}
public async getWasteStats(householdId: string, start: Date, end: Date) {
return PantryItemModel.aggregate([
{
$match: {
householdId,
status: { $in: ['consumed', 'discarded'] },
updatedAt: { $gte: start, $lte: end },
},
},
{
$group: {
_id: null,
totalConsumed: {
$sum: { $cond: [{ $eq: ['$status', 'consumed'] }, 1, 0] },
},
totalDiscarded: {
$sum: { $cond: [{ $eq: ['$status', 'discarded'] }, 1, 0] },
},
},
},
]).exec();
}
public async getTopWastedProducts(householdId: string, start: Date, end: Date, limit = 5) {
return PantryItemModel.aggregate([
{
$match: {
householdId,
status: 'discarded',
updatedAt: { $gte: start, $lte: end },
},
},
{
$group: {
_id: '$productId',
productName: { $first: '$productName' },
count: { $sum: 1 },
},
},
{ $sort: { count: -1 } },
{ $limit: limit },
{
$project: {
productId: '$_id',
productName: 1,
count: 1,
_id: 0,
},
},
]).exec();
}
public async findByIds(ids: string[], householdId: string) {
return PantryItemModel.find({
_id: { $in: ids },
householdId,
})
.lean()
.exec();
}
public async bulkUpdateStatus(
ids: string[],
householdId: string,
status: ItemStatus,
extra: Record<string, unknown> = {},
) {
const result = await PantryItemModel.updateMany(
{ _id: { $in: ids }, householdId },
{ $set: { status, ...extra } },
).exec();
return result.modifiedCount;
}
}