Files
flyer-crawler.projectium.com/src/services/expiryService.server.test.ts
Torben Sorensen 99f5d52d17
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 18m34s
more test fixes
2026-01-19 12:13:04 -08:00

934 lines
28 KiB
TypeScript

// src/services/expiryService.server.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Logger } from 'pino';
import type { Job } from 'bullmq';
import type { ExpiryAlertJobData } from '../types/job-data';
import { createMockLogger } from '../tests/utils/mockLogger';
import type {
InventorySource,
StorageLocation,
ExpiryStatus,
ExpiryRangeSource,
AlertMethod,
UserInventoryItem,
ReceiptStatus,
ReceiptItemStatus,
ExpiryAlertLogRecord,
ExpiryAlertType,
} from '../types/expiry';
// Mock dependencies
vi.mock('./db/index.db', () => ({
expiryRepo: {
addInventoryItem: vi.fn(),
updateInventoryItem: vi.fn(),
markAsConsumed: vi.fn(),
deleteInventoryItem: vi.fn(),
getInventoryItemById: vi.fn(),
getInventory: vi.fn(),
getExpiringItems: vi.fn(),
getExpiredItems: vi.fn(),
getExpiryRangeForItem: vi.fn(),
getExpiryRanges: vi.fn(),
addExpiryRange: vi.fn(),
getUserAlertSettings: vi.fn(),
upsertAlertSettings: vi.fn(),
getUsersWithExpiringItems: vi.fn(),
logAlert: vi.fn(),
markAlertSent: vi.fn(),
getRecipesForExpiringItems: vi.fn(),
},
receiptRepo: {
getReceiptById: vi.fn(),
getReceiptItems: vi.fn(),
updateReceiptItem: vi.fn(),
},
}));
vi.mock('./emailService.server', () => ({
sendEmail: vi.fn(),
}));
vi.mock('./logger.server', () => ({
logger: {
child: vi.fn().mockReturnThis(),
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
// Import after mocks are set up
import {
addInventoryItem,
updateInventoryItem,
markItemConsumed,
deleteInventoryItem,
getInventoryItemById,
getInventory,
getExpiringItemsGrouped,
getExpiringItems,
getExpiredItems,
calculateExpiryDate,
getExpiryRanges,
addExpiryRange,
getAlertSettings,
updateAlertSettings,
processExpiryAlerts,
addItemsFromReceipt,
getRecipeSuggestionsForExpiringItems,
processExpiryAlertJob,
} from './expiryService.server';
import { expiryRepo, receiptRepo } from './db/index.db';
import * as emailService from './emailService.server';
// Helper to create mock alert log record
function createMockAlertLogRecord(
overrides: Partial<ExpiryAlertLogRecord> = {},
): ExpiryAlertLogRecord {
return {
alert_log_id: 1,
user_id: 'user-1',
pantry_item_id: null,
alert_type: 'expiring_soon' as ExpiryAlertType,
alert_method: 'email' as AlertMethod,
item_name: 'Test Item',
expiry_date: null,
days_until_expiry: null,
sent_at: new Date().toISOString(),
...overrides,
};
}
describe('expiryService.server', () => {
let mockLogger: Logger;
beforeEach(() => {
vi.clearAllMocks();
mockLogger = createMockLogger();
});
afterEach(() => {
vi.resetAllMocks();
});
describe('addInventoryItem', () => {
it('should add item to inventory without expiry date', async () => {
const mockItem: UserInventoryItem = {
inventory_id: 1,
user_id: 'user-1',
product_id: null,
master_item_id: null,
item_name: 'Milk',
quantity: 1,
unit: 'gallon',
purchase_date: null,
expiry_date: null,
source: 'manual',
location: 'fridge',
notes: null,
is_consumed: false,
consumed_at: null,
expiry_source: null,
receipt_item_id: null,
pantry_location_id: null,
notification_sent_at: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
days_until_expiry: null,
expiry_status: 'unknown',
};
vi.mocked(expiryRepo.addInventoryItem).mockResolvedValueOnce(mockItem);
const result = await addInventoryItem(
'user-1',
{ item_name: 'Milk', quantity: 1, source: 'manual', location: 'fridge' },
mockLogger,
);
expect(result.inventory_id).toBe(1);
expect(result.item_name).toBe('Milk');
});
it('should calculate expiry date when purchase date and location provided', async () => {
const mockItem: UserInventoryItem = {
inventory_id: 2,
user_id: 'user-1',
product_id: null,
master_item_id: 5,
item_name: 'Milk',
quantity: 1,
unit: 'gallon',
purchase_date: '2024-01-15',
expiry_date: '2024-01-22', // calculated
source: 'manual',
location: 'fridge',
notes: null,
is_consumed: false,
consumed_at: null,
expiry_source: 'calculated',
receipt_item_id: null,
pantry_location_id: null,
notification_sent_at: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
days_until_expiry: 7,
expiry_status: 'fresh',
};
vi.mocked(expiryRepo.getExpiryRangeForItem).mockResolvedValueOnce({
expiry_range_id: 1,
master_item_id: 5,
category_id: null,
item_pattern: null,
storage_location: 'fridge',
min_days: 5,
max_days: 10,
typical_days: 7,
notes: null,
source: 'usda',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
});
vi.mocked(expiryRepo.addInventoryItem).mockResolvedValueOnce(mockItem);
const result = await addInventoryItem(
'user-1',
{
item_name: 'Milk',
master_item_id: 5,
quantity: 1,
source: 'manual',
location: 'fridge',
purchase_date: '2024-01-15',
},
mockLogger,
);
expect(result.expiry_date).toBe('2024-01-22');
});
});
describe('updateInventoryItem', () => {
it('should update inventory item', async () => {
const mockUpdatedItem: UserInventoryItem = {
inventory_id: 1,
user_id: 'user-1',
product_id: null,
master_item_id: null,
item_name: 'Milk',
quantity: 2, // updated
unit: 'gallon',
purchase_date: null,
expiry_date: '2024-01-25',
source: 'manual',
location: 'fridge',
notes: 'Almost gone',
is_consumed: false,
consumed_at: null,
expiry_source: null,
receipt_item_id: null,
pantry_location_id: null,
notification_sent_at: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
days_until_expiry: 5,
expiry_status: 'expiring_soon',
};
vi.mocked(expiryRepo.updateInventoryItem).mockResolvedValueOnce(mockUpdatedItem);
const result = await updateInventoryItem(
1,
'user-1',
{ quantity: 2, notes: 'Almost gone' },
mockLogger,
);
expect(result.quantity).toBe(2);
expect(result.notes).toBe('Almost gone');
});
});
describe('markItemConsumed', () => {
it('should mark item as consumed', async () => {
vi.mocked(expiryRepo.markAsConsumed).mockResolvedValueOnce(undefined);
await markItemConsumed(1, 'user-1', mockLogger);
expect(expiryRepo.markAsConsumed).toHaveBeenCalledWith(1, 'user-1', mockLogger);
});
});
describe('deleteInventoryItem', () => {
it('should delete inventory item', async () => {
vi.mocked(expiryRepo.deleteInventoryItem).mockResolvedValueOnce(undefined);
await deleteInventoryItem(1, 'user-1', mockLogger);
expect(expiryRepo.deleteInventoryItem).toHaveBeenCalledWith(1, 'user-1', mockLogger);
});
});
describe('getInventoryItemById', () => {
it('should return inventory item by ID', async () => {
const mockItem: UserInventoryItem = {
inventory_id: 1,
user_id: 'user-1',
product_id: null,
master_item_id: null,
item_name: 'Eggs',
quantity: 12,
unit: null,
purchase_date: null,
expiry_date: null,
source: 'manual',
location: 'fridge',
notes: null,
is_consumed: false,
consumed_at: null,
expiry_source: null,
receipt_item_id: null,
pantry_location_id: null,
notification_sent_at: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
days_until_expiry: null,
expiry_status: 'unknown',
};
vi.mocked(expiryRepo.getInventoryItemById).mockResolvedValueOnce(mockItem);
const result = await getInventoryItemById(1, 'user-1', mockLogger);
expect(result.item_name).toBe('Eggs');
});
});
describe('getInventory', () => {
it('should return paginated inventory', async () => {
const mockInventory = {
items: [
{
inventory_id: 1,
user_id: 'user-1',
product_id: null,
master_item_id: null,
item_name: 'Butter',
quantity: 1,
unit: null,
purchase_date: null,
expiry_date: null,
source: 'manual' as InventorySource,
location: 'fridge' as StorageLocation,
notes: null,
is_consumed: false,
consumed_at: null,
expiry_source: null,
receipt_item_id: null,
pantry_location_id: null,
notification_sent_at: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
days_until_expiry: null,
expiry_status: 'unknown' as ExpiryStatus,
},
],
total: 1,
};
vi.mocked(expiryRepo.getInventory).mockResolvedValueOnce(mockInventory);
const result = await getInventory({ user_id: 'user-1', limit: 10, offset: 0 }, mockLogger);
expect(result.items).toHaveLength(1);
expect(result.total).toBe(1);
});
it('should filter by location', async () => {
vi.mocked(expiryRepo.getInventory).mockResolvedValueOnce({ items: [], total: 0 });
await getInventory({ user_id: 'user-1', location: 'freezer' }, mockLogger);
expect(expiryRepo.getInventory).toHaveBeenCalledWith(
{ user_id: 'user-1', location: 'freezer' },
mockLogger,
);
});
});
describe('getExpiringItemsGrouped', () => {
it('should return items grouped by expiry urgency', async () => {
const expiringItems = [
createMockInventoryItem({ days_until_expiry: 0 }), // today
createMockInventoryItem({ days_until_expiry: 3 }), // this week
createMockInventoryItem({ days_until_expiry: 15 }), // this month
];
const expiredItems = [createMockInventoryItem({ days_until_expiry: -2 })];
vi.mocked(expiryRepo.getExpiringItems).mockResolvedValueOnce(expiringItems);
vi.mocked(expiryRepo.getExpiredItems).mockResolvedValueOnce(expiredItems);
const result = await getExpiringItemsGrouped('user-1', mockLogger);
expect(result.expiring_today).toHaveLength(1);
expect(result.expiring_this_week).toHaveLength(1);
expect(result.expiring_this_month).toHaveLength(1);
expect(result.already_expired).toHaveLength(1);
expect(result.counts.total).toBe(4);
});
});
describe('getExpiringItems', () => {
it('should return items expiring within specified days', async () => {
const mockItems = [createMockInventoryItem({ days_until_expiry: 5 })];
vi.mocked(expiryRepo.getExpiringItems).mockResolvedValueOnce(mockItems);
const result = await getExpiringItems('user-1', 7, mockLogger);
expect(result).toHaveLength(1);
expect(expiryRepo.getExpiringItems).toHaveBeenCalledWith('user-1', 7, mockLogger);
});
});
describe('getExpiredItems', () => {
it('should return expired items', async () => {
const mockItems = [createMockInventoryItem({ days_until_expiry: -3 })];
vi.mocked(expiryRepo.getExpiredItems).mockResolvedValueOnce(mockItems);
const result = await getExpiredItems('user-1', mockLogger);
expect(result).toHaveLength(1);
});
});
describe('calculateExpiryDate', () => {
it('should calculate expiry date based on storage location', async () => {
vi.mocked(expiryRepo.getExpiryRangeForItem).mockResolvedValueOnce({
expiry_range_id: 1,
master_item_id: null,
category_id: 1,
item_pattern: null,
storage_location: 'fridge',
min_days: 7,
max_days: 14,
typical_days: 10,
notes: null,
source: 'usda',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
});
const result = await calculateExpiryDate(
{
item_name: 'Cheese',
storage_location: 'fridge',
purchase_date: '2024-01-15',
},
mockLogger,
);
expect(result).toBe('2024-01-25'); // 10 days after purchase
});
it('should return null when no expiry range found', async () => {
vi.mocked(expiryRepo.getExpiryRangeForItem).mockResolvedValueOnce(null);
const result = await calculateExpiryDate(
{
item_name: 'Unknown Item',
storage_location: 'pantry',
purchase_date: '2024-01-15',
},
mockLogger,
);
expect(result).toBeNull();
});
});
describe('getExpiryRanges', () => {
it('should return paginated expiry ranges', async () => {
const mockRanges = {
ranges: [
{
expiry_range_id: 1,
master_item_id: null,
category_id: 1,
item_pattern: null,
storage_location: 'fridge' as StorageLocation,
min_days: 7,
max_days: 14,
typical_days: 10,
notes: null,
source: 'usda' as ExpiryRangeSource,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
],
total: 1,
};
vi.mocked(expiryRepo.getExpiryRanges).mockResolvedValueOnce(mockRanges);
const result = await getExpiryRanges({}, mockLogger);
expect(result.ranges).toHaveLength(1);
expect(result.total).toBe(1);
});
});
describe('addExpiryRange', () => {
it('should add new expiry range', async () => {
const mockRange = {
expiry_range_id: 2,
master_item_id: null,
category_id: 2,
item_pattern: null,
storage_location: 'freezer' as StorageLocation,
min_days: 30,
max_days: 90,
typical_days: 60,
notes: 'Best stored in back of freezer',
source: 'manual' as ExpiryRangeSource,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
vi.mocked(expiryRepo.addExpiryRange).mockResolvedValueOnce(mockRange);
const result = await addExpiryRange(
{
category_id: 2,
storage_location: 'freezer',
min_days: 30,
max_days: 90,
typical_days: 60,
notes: 'Best stored in back of freezer',
},
mockLogger,
);
expect(result.typical_days).toBe(60);
});
});
describe('getAlertSettings', () => {
it('should return user alert settings', async () => {
const mockSettings = [
{
expiry_alert_id: 1,
user_id: 'user-1',
days_before_expiry: 3,
alert_method: 'email' as AlertMethod,
is_enabled: true,
last_alert_sent_at: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
];
vi.mocked(expiryRepo.getUserAlertSettings).mockResolvedValueOnce(mockSettings);
const result = await getAlertSettings('user-1', mockLogger);
expect(result).toHaveLength(1);
expect(result[0].alert_method).toBe('email');
});
});
describe('updateAlertSettings', () => {
it('should update alert settings', async () => {
const mockUpdatedSettings = {
expiry_alert_id: 1,
user_id: 'user-1',
days_before_expiry: 5,
alert_method: 'email' as AlertMethod,
is_enabled: true,
last_alert_sent_at: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
vi.mocked(expiryRepo.upsertAlertSettings).mockResolvedValueOnce(mockUpdatedSettings);
const result = await updateAlertSettings(
'user-1',
'email',
{ days_before_expiry: 5 },
mockLogger,
);
expect(result.days_before_expiry).toBe(5);
});
});
describe('processExpiryAlerts', () => {
it('should process alerts for users with expiring items', async () => {
vi.mocked(expiryRepo.getUsersWithExpiringItems).mockResolvedValueOnce([
{
user_id: 'user-1',
email: 'user1@example.com',
alert_method: 'email' as AlertMethod,
days_before_expiry: 3,
},
]);
vi.mocked(expiryRepo.getExpiringItems).mockResolvedValueOnce([
createMockInventoryItem({ days_until_expiry: 2 }),
]);
vi.mocked(emailService.sendEmail).mockResolvedValueOnce(undefined);
vi.mocked(expiryRepo.logAlert).mockResolvedValue(createMockAlertLogRecord());
vi.mocked(expiryRepo.markAlertSent).mockResolvedValue(undefined);
const alertsSent = await processExpiryAlerts(mockLogger);
expect(alertsSent).toBe(1);
});
it('should skip users with no expiring items', async () => {
vi.mocked(expiryRepo.getUsersWithExpiringItems).mockResolvedValueOnce([
{
user_id: 'user-1',
email: 'user1@example.com',
alert_method: 'email' as AlertMethod,
days_before_expiry: 3,
},
]);
vi.mocked(expiryRepo.getExpiringItems).mockResolvedValueOnce([]);
const alertsSent = await processExpiryAlerts(mockLogger);
expect(alertsSent).toBe(0);
});
});
describe('addItemsFromReceipt', () => {
it('should add items from receipt to inventory', async () => {
const mockReceipt = {
receipt_id: 1,
user_id: 'user-1',
store_location_id: null,
receipt_image_url: '/uploads/receipt.jpg',
transaction_date: '2024-01-15',
total_amount_cents: 2500,
status: 'completed' as ReceiptStatus,
raw_text: 'test text',
store_confidence: null,
ocr_provider: null,
error_details: null,
retry_count: 0,
ocr_confidence: null,
currency: 'USD',
created_at: new Date().toISOString(),
processed_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
const mockReceiptItems = [
{
receipt_item_id: 1,
receipt_id: 1,
raw_item_description: 'MILK 2%',
quantity: 1,
price_paid_cents: 399,
master_item_id: 5,
product_id: null,
status: 'matched' as ReceiptItemStatus,
line_number: 1,
match_confidence: 0.95,
is_discount: false,
unit_price_cents: null,
unit_type: null,
added_to_pantry: false,
pantry_item_id: null,
upc_code: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
];
vi.mocked(receiptRepo.getReceiptById).mockResolvedValueOnce(mockReceipt);
vi.mocked(receiptRepo.getReceiptItems).mockResolvedValueOnce(mockReceiptItems);
vi.mocked(expiryRepo.addInventoryItem).mockResolvedValueOnce(
createMockInventoryItem({ inventory_id: 10 }),
);
vi.mocked(receiptRepo.updateReceiptItem).mockResolvedValueOnce(mockReceiptItems[0] as any);
const result = await addItemsFromReceipt(
'user-1',
1,
[{ receipt_item_id: 1, location: 'fridge', include: true }],
mockLogger,
);
expect(result).toHaveLength(1);
expect(receiptRepo.updateReceiptItem).toHaveBeenCalledWith(
1,
expect.objectContaining({ added_to_pantry: true }),
expect.any(Object),
);
});
it('should skip items with include: false', async () => {
const mockReceipt = {
receipt_id: 1,
user_id: 'user-1',
store_location_id: null,
receipt_image_url: '/uploads/receipt.jpg',
transaction_date: '2024-01-15',
total_amount_cents: 2500,
status: 'completed' as ReceiptStatus,
raw_text: 'test text',
store_confidence: null,
ocr_provider: null,
error_details: null,
retry_count: 0,
ocr_confidence: null,
currency: 'USD',
created_at: new Date().toISOString(),
processed_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
vi.mocked(receiptRepo.getReceiptById).mockResolvedValueOnce(mockReceipt);
const result = await addItemsFromReceipt(
'user-1',
1,
[{ receipt_item_id: 1, include: false }],
mockLogger,
);
expect(result).toHaveLength(0);
expect(expiryRepo.addInventoryItem).not.toHaveBeenCalled();
});
});
describe('getRecipeSuggestionsForExpiringItems', () => {
it('should return recipes using expiring items', async () => {
const expiringItems = [
createMockInventoryItem({ master_item_id: 5, days_until_expiry: 2 }),
createMockInventoryItem({ master_item_id: 10, days_until_expiry: 4 }),
];
const mockRecipes = {
recipes: [
{
recipe_id: 1,
recipe_name: 'Quick Breakfast',
description: 'Easy breakfast recipe',
prep_time_minutes: 10,
cook_time_minutes: 15,
servings: 2,
photo_url: null,
matching_master_item_ids: [5],
match_count: 1,
},
],
total: 1,
};
vi.mocked(expiryRepo.getExpiringItems).mockResolvedValueOnce(expiringItems);
vi.mocked(expiryRepo.getRecipesForExpiringItems).mockResolvedValueOnce(mockRecipes);
const result = await getRecipeSuggestionsForExpiringItems('user-1', 7, mockLogger);
expect(result.recipes).toHaveLength(1);
expect(result.recipes[0].matching_items).toHaveLength(1);
expect(result.considered_items).toHaveLength(2);
});
it('should return empty results when no expiring items', async () => {
vi.mocked(expiryRepo.getExpiringItems).mockResolvedValueOnce([]);
const result = await getRecipeSuggestionsForExpiringItems('user-1', 7, mockLogger);
expect(result.recipes).toHaveLength(0);
expect(result.total).toBe(0);
});
});
describe('processExpiryAlertJob', () => {
it('should process user-specific alert job', async () => {
vi.mocked(expiryRepo.getUserAlertSettings).mockResolvedValueOnce([
{
expiry_alert_id: 1,
user_id: 'user-1',
days_before_expiry: 7,
alert_method: 'email' as AlertMethod,
is_enabled: true,
last_alert_sent_at: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
]);
vi.mocked(expiryRepo.getExpiringItems).mockResolvedValueOnce([
createMockInventoryItem({ days_until_expiry: 3 }),
]);
vi.mocked(expiryRepo.logAlert).mockResolvedValue(createMockAlertLogRecord());
vi.mocked(expiryRepo.upsertAlertSettings).mockResolvedValue({
expiry_alert_id: 1,
user_id: 'user-1',
days_before_expiry: 7,
alert_method: 'email' as AlertMethod,
is_enabled: true,
last_alert_sent_at: new Date().toISOString(),
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
});
const mockJob = {
id: 'job-1',
data: {
alertType: 'user_specific' as const,
userId: 'user-1',
daysAhead: 7,
meta: { requestId: 'req-1' },
},
} as Job<ExpiryAlertJobData>;
const result = await processExpiryAlertJob(mockJob, mockLogger);
expect(result.success).toBe(true);
expect(result.alertsSent).toBe(1);
expect(result.usersNotified).toBe(1);
});
it('should process daily check job for all users', async () => {
vi.mocked(expiryRepo.getUsersWithExpiringItems).mockResolvedValueOnce([
{
user_id: 'user-1',
email: 'user1@example.com',
alert_method: 'email' as AlertMethod,
days_before_expiry: 7,
},
{
user_id: 'user-2',
email: 'user2@example.com',
alert_method: 'email' as AlertMethod,
days_before_expiry: 7,
},
]);
vi.mocked(expiryRepo.getUserAlertSettings)
.mockResolvedValueOnce([
{
expiry_alert_id: 1,
user_id: 'user-1',
days_before_expiry: 7,
alert_method: 'email' as AlertMethod,
is_enabled: true,
last_alert_sent_at: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
])
.mockResolvedValueOnce([
{
expiry_alert_id: 2,
user_id: 'user-2',
days_before_expiry: 7,
alert_method: 'email' as AlertMethod,
is_enabled: true,
last_alert_sent_at: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
]);
vi.mocked(expiryRepo.getExpiringItems)
.mockResolvedValueOnce([createMockInventoryItem({ days_until_expiry: 3 })])
.mockResolvedValueOnce([createMockInventoryItem({ days_until_expiry: 5 })]);
vi.mocked(expiryRepo.logAlert).mockResolvedValue(createMockAlertLogRecord());
vi.mocked(expiryRepo.upsertAlertSettings).mockResolvedValue({
expiry_alert_id: 1,
user_id: 'user-1',
days_before_expiry: 7,
alert_method: 'email' as AlertMethod,
is_enabled: true,
last_alert_sent_at: new Date().toISOString(),
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
});
const mockJob = {
id: 'job-2',
data: {
alertType: 'daily_check' as const,
daysAhead: 7,
},
} as Job<ExpiryAlertJobData>;
const result = await processExpiryAlertJob(mockJob, mockLogger);
expect(result.success).toBe(true);
expect(result.usersNotified).toBe(2);
});
it('should handle job processing errors', async () => {
vi.mocked(expiryRepo.getUserAlertSettings).mockRejectedValueOnce(new Error('DB error'));
const mockJob = {
id: 'job-3',
data: {
alertType: 'user_specific' as const,
userId: 'user-1',
},
} as Job<ExpiryAlertJobData>;
await expect(processExpiryAlertJob(mockJob, mockLogger)).rejects.toThrow('DB error');
});
});
});
// Helper function to create mock inventory items
function createMockInventoryItem(
overrides: Partial<{
inventory_id: number;
master_item_id: number | null;
days_until_expiry: number | null;
}>,
): UserInventoryItem {
const daysUntilExpiry = overrides.days_until_expiry ?? 5;
const expiryStatus: ExpiryStatus =
daysUntilExpiry !== null && daysUntilExpiry < 0
? 'expired'
: daysUntilExpiry !== null && daysUntilExpiry <= 7
? 'expiring_soon'
: 'fresh';
return {
inventory_id: overrides.inventory_id ?? 1,
user_id: 'user-1',
product_id: null,
master_item_id: overrides.master_item_id ?? null,
item_name: 'Test Item',
quantity: 1,
unit: null,
purchase_date: null,
expiry_date: '2024-01-25',
source: 'manual' as InventorySource,
location: 'fridge' as StorageLocation,
notes: null,
is_consumed: false,
consumed_at: null,
expiry_source: null,
receipt_item_id: null,
pantry_location_id: null,
notification_sent_at: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
days_until_expiry: daysUntilExpiry,
expiry_status: expiryStatus,
};
}