2026-05-14 14:47:23 +09:00
|
|
|
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'] },
|
|
|
|
|
})
|
2026-05-19 10:13:40 +09:00
|
|
|
.sort({ 'freshnessEstimate.daysRemaining': 1, _id: 1 })
|
2026-05-14 14:47:23 +09:00
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
}
|