159 lines
5.8 KiB
TypeScript
159 lines
5.8 KiB
TypeScript
|
|
import { describe, it, expect } from 'vitest';
|
||
|
|
import { FreshnessCalculatorService } from './freshness-calculator.service.js';
|
||
|
|
import { ItemStatus, FreshnessUrgency, FreshnessSource, StorageLocation } from '@meshitrack/shared';
|
||
|
|
|
||
|
|
describe(FreshnessCalculatorService.name, () => {
|
||
|
|
const service = new FreshnessCalculatorService();
|
||
|
|
|
||
|
|
const baseItem = {
|
||
|
|
status: ItemStatus.SEALED,
|
||
|
|
storageLocation: StorageLocation.FRIDGE,
|
||
|
|
purchaseDate: new Date('2024-01-01'),
|
||
|
|
expirationDate: undefined,
|
||
|
|
openedDate: undefined,
|
||
|
|
preparedDate: undefined,
|
||
|
|
};
|
||
|
|
|
||
|
|
const rule = {
|
||
|
|
shelfLifeDays: 14,
|
||
|
|
openedLifeDays: 7,
|
||
|
|
freezerLifeDays: 90,
|
||
|
|
};
|
||
|
|
|
||
|
|
describe('calculate', () => {
|
||
|
|
it('uses packaging expiration date when present', () => {
|
||
|
|
const item = { ...baseItem, expirationDate: new Date('2099-12-31') };
|
||
|
|
const result = service.calculate(item, rule);
|
||
|
|
expect(result.source).toBe(FreshnessSource.PACKAGING);
|
||
|
|
expect(result.estimatedExpiryDate).toEqual(new Date('2099-12-31'));
|
||
|
|
});
|
||
|
|
|
||
|
|
it('falls back to 7-day default when no rule provided', () => {
|
||
|
|
const result = service.calculate(baseItem, null);
|
||
|
|
expect(result.source).toBe(FreshnessSource.RULE);
|
||
|
|
const expected = new Date('2024-01-01');
|
||
|
|
expected.setDate(expected.getDate() + 7);
|
||
|
|
expect(result.estimatedExpiryDate).toEqual(expected);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('uses freezerLifeDays for freezer storage', () => {
|
||
|
|
const item = { ...baseItem, storageLocation: StorageLocation.FREEZER };
|
||
|
|
const result = service.calculate(item, rule);
|
||
|
|
expect(result.source).toBe(FreshnessSource.RULE);
|
||
|
|
const expected = new Date('2024-01-01');
|
||
|
|
expected.setDate(expected.getDate() + 90);
|
||
|
|
expect(result.estimatedExpiryDate).toEqual(expected);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('uses shelfLifeDays for sealed items in freezer without freezerLifeDays', () => {
|
||
|
|
const item = { ...baseItem, storageLocation: StorageLocation.FREEZER };
|
||
|
|
const ruleNoFreezer = { shelfLifeDays: 14, openedLifeDays: 7 };
|
||
|
|
const result = service.calculate(item, ruleNoFreezer);
|
||
|
|
const expected = new Date('2024-01-01');
|
||
|
|
expected.setDate(expected.getDate() + 14);
|
||
|
|
expect(result.estimatedExpiryDate).toEqual(expected);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('uses openedLifeDays for opened items', () => {
|
||
|
|
const openedDate = new Date('2024-01-05');
|
||
|
|
const item = {
|
||
|
|
...baseItem,
|
||
|
|
status: ItemStatus.OPENED,
|
||
|
|
openedDate,
|
||
|
|
};
|
||
|
|
const result = service.calculate(item, rule);
|
||
|
|
const expected = new Date('2024-01-05');
|
||
|
|
expected.setDate(expected.getDate() + 7);
|
||
|
|
expect(result.estimatedExpiryDate).toEqual(expected);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('uses openedLifeDays for prepared items with openedDate', () => {
|
||
|
|
const openedDate = new Date('2024-01-05');
|
||
|
|
const item = {
|
||
|
|
...baseItem,
|
||
|
|
status: ItemStatus.PREPARED,
|
||
|
|
openedDate,
|
||
|
|
};
|
||
|
|
const result = service.calculate(item, rule);
|
||
|
|
const expected = new Date('2024-01-05');
|
||
|
|
expected.setDate(expected.getDate() + 7);
|
||
|
|
expect(result.estimatedExpiryDate).toEqual(expected);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('uses shelfLifeDays for opened item without openedDate', () => {
|
||
|
|
const item = { ...baseItem, status: ItemStatus.OPENED };
|
||
|
|
const result = service.calculate(item, rule);
|
||
|
|
const expected = new Date('2024-01-01');
|
||
|
|
expected.setDate(expected.getDate() + 14);
|
||
|
|
expect(result.estimatedExpiryDate).toEqual(expected);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('uses shelfLifeDays for sealed items', () => {
|
||
|
|
const result = service.calculate(baseItem, rule);
|
||
|
|
const expected = new Date('2024-01-01');
|
||
|
|
expected.setDate(expected.getDate() + 14);
|
||
|
|
expect(result.estimatedExpiryDate).toEqual(expected);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('computes daysRemaining and urgency', () => {
|
||
|
|
const future = new Date();
|
||
|
|
future.setDate(future.getDate() + 10);
|
||
|
|
const item = { ...baseItem, expirationDate: future };
|
||
|
|
const result = service.calculate(item, rule);
|
||
|
|
expect(result.daysRemaining).toBeGreaterThan(5);
|
||
|
|
expect(result.urgency).toBe(FreshnessUrgency.FRESH);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('isActive', () => {
|
||
|
|
it('returns true for sealed', () => {
|
||
|
|
expect(service.isActive('sealed')).toBe(true);
|
||
|
|
});
|
||
|
|
it('returns true for opened', () => {
|
||
|
|
expect(service.isActive('opened')).toBe(true);
|
||
|
|
});
|
||
|
|
it('returns true for prepared', () => {
|
||
|
|
expect(service.isActive('prepared')).toBe(true);
|
||
|
|
});
|
||
|
|
it('returns false for consumed', () => {
|
||
|
|
expect(service.isActive('consumed')).toBe(false);
|
||
|
|
});
|
||
|
|
it('returns false for discarded', () => {
|
||
|
|
expect(service.isActive('discarded')).toBe(false);
|
||
|
|
});
|
||
|
|
it('returns false for expired', () => {
|
||
|
|
expect(service.isActive('expired')).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('mapUrgency', () => {
|
||
|
|
it('returns FRESH for > 5 days', () => {
|
||
|
|
expect(service.mapUrgency(6)).toBe(FreshnessUrgency.FRESH);
|
||
|
|
});
|
||
|
|
it('returns USE_SOON for 2-5 days', () => {
|
||
|
|
expect(service.mapUrgency(3)).toBe(FreshnessUrgency.USE_SOON);
|
||
|
|
});
|
||
|
|
it('returns URGENT for 0-1 days', () => {
|
||
|
|
expect(service.mapUrgency(1)).toBe(FreshnessUrgency.URGENT);
|
||
|
|
});
|
||
|
|
it('returns CHECK for -1 to -3 days', () => {
|
||
|
|
expect(service.mapUrgency(-1)).toBe(FreshnessUrgency.CHECK);
|
||
|
|
});
|
||
|
|
it('returns EXPIRED for < -3 days', () => {
|
||
|
|
expect(service.mapUrgency(-4)).toBe(FreshnessUrgency.EXPIRED);
|
||
|
|
});
|
||
|
|
it('returns USE_SOON for exactly 2', () => {
|
||
|
|
expect(service.mapUrgency(2)).toBe(FreshnessUrgency.USE_SOON);
|
||
|
|
});
|
||
|
|
it('returns USE_SOON for exactly 5', () => {
|
||
|
|
expect(service.mapUrgency(5)).toBe(FreshnessUrgency.USE_SOON);
|
||
|
|
});
|
||
|
|
it('returns URGENT for exactly 0', () => {
|
||
|
|
expect(service.mapUrgency(0)).toBe(FreshnessUrgency.URGENT);
|
||
|
|
});
|
||
|
|
it('returns CHECK for exactly -3', () => {
|
||
|
|
expect(service.mapUrgency(-3)).toBe(FreshnessUrgency.CHECK);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|