Implement stores and refills, improve testing
This commit is contained in:
parent
9f416903ef
commit
5536acd67d
137 changed files with 21218 additions and 221 deletions
165
packages/web/src/services/__tests__/api-client.test.ts
Normal file
165
packages/web/src/services/__tests__/api-client.test.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Create a fresh ApiClient for each test by re-importing
|
||||
let apiClient: typeof import('../api-client').apiClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
// Reset module to get a fresh singleton
|
||||
vi.resetModules();
|
||||
const mod = await import('../api-client');
|
||||
apiClient = mod.apiClient;
|
||||
});
|
||||
|
||||
function mockFetch(body: unknown, status = 200) {
|
||||
return vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
statusText: status === 200 ? 'OK' : 'Error',
|
||||
json: () => Promise.resolve(body),
|
||||
} as Response);
|
||||
}
|
||||
|
||||
describe('ApiClient', () => {
|
||||
describe('token management', () => {
|
||||
it('hasToken returns false initially', () => {
|
||||
expect(apiClient.hasToken).toBe(false);
|
||||
});
|
||||
|
||||
it('hasToken returns true after setting token', () => {
|
||||
apiClient.accessToken = 'test-token';
|
||||
expect(apiClient.hasToken).toBe(true);
|
||||
});
|
||||
|
||||
it('includes Authorization header when token is set', async () => {
|
||||
const spy = mockFetch({ data: [] });
|
||||
apiClient.accessToken = 'my-jwt';
|
||||
|
||||
await apiClient.get('/test');
|
||||
|
||||
const headers = spy.mock.calls[0][1]?.headers as Record<string, string>;
|
||||
expect(headers['Authorization']).toBe('Bearer my-jwt');
|
||||
});
|
||||
|
||||
it('omits Authorization header when no token', async () => {
|
||||
const spy = mockFetch({ data: [] });
|
||||
|
||||
await apiClient.get('/test');
|
||||
|
||||
const headers = spy.mock.calls[0][1]?.headers as Record<string, string>;
|
||||
expect(headers['Authorization']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('makes GET request to correct URL', async () => {
|
||||
const spy = mockFetch({ id: '1' });
|
||||
|
||||
await apiClient.get('/households/hh1/stores');
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/households/hh1/stores'),
|
||||
expect.objectContaining({ headers: expect.any(Object) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns parsed JSON body', async () => {
|
||||
mockFetch({ id: '1', name: 'CVS' });
|
||||
|
||||
const result = await apiClient.get('/test');
|
||||
|
||||
expect(result).toEqual({ id: '1', name: 'CVS' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('post', () => {
|
||||
it('makes POST request with JSON body', async () => {
|
||||
const spy = mockFetch({ id: '1' });
|
||||
const body = { name: 'Walgreens' };
|
||||
|
||||
await apiClient.post('/stores', body);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/stores'),
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('handles post without body', async () => {
|
||||
const spy = mockFetch({ id: '1' });
|
||||
|
||||
await apiClient.post('/test');
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({ body: undefined }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('patch', () => {
|
||||
it('makes PATCH request with JSON body', async () => {
|
||||
const spy = mockFetch({ id: '1' });
|
||||
const body = { name: 'Updated' };
|
||||
|
||||
await apiClient.patch('/stores/1', body);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/stores/1'),
|
||||
expect.objectContaining({
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('makes DELETE request', async () => {
|
||||
const spy = mockFetch(undefined, 204);
|
||||
|
||||
await apiClient.delete('/stores/1');
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/stores/1'),
|
||||
expect.objectContaining({ method: 'DELETE' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('throws with message from error response body', async () => {
|
||||
mockFetch({ message: 'Store not found' }, 404);
|
||||
|
||||
await expect(apiClient.get('/test')).rejects.toThrow('Store not found');
|
||||
});
|
||||
|
||||
it('throws with status code when body has no message', async () => {
|
||||
mockFetch({}, 500);
|
||||
|
||||
await expect(apiClient.get('/test')).rejects.toThrow('Request failed: 500');
|
||||
});
|
||||
|
||||
it('throws with status text when JSON parsing fails', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
||||
ok: false,
|
||||
status: 502,
|
||||
statusText: 'Bad Gateway',
|
||||
json: () => Promise.reject(new Error('parse error')),
|
||||
} as Response);
|
||||
|
||||
await expect(apiClient.get('/test')).rejects.toThrow('Request failed: 502 Bad Gateway');
|
||||
});
|
||||
|
||||
it('returns undefined for 204 No Content', async () => {
|
||||
mockFetch(null, 204);
|
||||
|
||||
const result = await apiClient.get('/test');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
62
packages/web/src/services/__tests__/cabinet-events.test.ts
Normal file
62
packages/web/src/services/__tests__/cabinet-events.test.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../api-client', () => ({
|
||||
apiClient: { get: mockGet },
|
||||
}));
|
||||
|
||||
import { listCabinetEvents, getEventsByItem, getSpendingSummary } from '../cabinet-events';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('cabinet-events service', () => {
|
||||
it('listCabinetEvents with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listCabinetEvents('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/cabinet-events');
|
||||
});
|
||||
|
||||
it('listCabinetEvents builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listCabinetEvents('hh1', { medicineId: 'med-1', eventType: 'dispense', startDate: '2026-01-01', endDate: '2026-02-01' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('medicineId=med-1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('eventType=dispense'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('startDate=2026-01-01'));
|
||||
});
|
||||
|
||||
it('getEventsByItem builds URL with cabinetItemId', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getEventsByItem('hh1', 'ci-1', { limit: 5 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('/cabinet-events/by-item/ci-1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('limit=5'));
|
||||
});
|
||||
|
||||
it('listCabinetEvents with cursor and limit', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listCabinetEvents('hh1', { cursor: 'cur1', limit: 10 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('limit=10'));
|
||||
});
|
||||
|
||||
it('getEventsByItem with cursor', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getEventsByItem('hh1', 'ci-1', { cursor: 'cur2' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur2'));
|
||||
});
|
||||
|
||||
it('getSpendingSummary with no query', async () => {
|
||||
mockGet.mockResolvedValue({});
|
||||
await getSpendingSummary('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/cabinet-events/spending-summary');
|
||||
});
|
||||
|
||||
it('getSpendingSummary builds query string', async () => {
|
||||
mockGet.mockResolvedValue({});
|
||||
await getSpendingSummary('hh1', { period: 'month', medicineId: 'med-1' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('period=month'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('medicineId=med-1'));
|
||||
});
|
||||
});
|
||||
91
packages/web/src/services/__tests__/cabinet.test.ts
Normal file
91
packages/web/src/services/__tests__/cabinet.test.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost, mockPatch, mockDelete } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
mockPatch: vi.fn(),
|
||||
mockDelete: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost, patch: mockPatch, delete: mockDelete },
|
||||
}));
|
||||
|
||||
import {
|
||||
listCabinetItems, getCabinetItem, getCabinetSummary, getExpiringSoon,
|
||||
createCabinetItem, updateCabinetItem, adjustCabinetItemQuantity, deleteCabinetItem,
|
||||
} from '../cabinet';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('cabinet service', () => {
|
||||
it('listCabinetItems with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listCabinetItems('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/cabinet');
|
||||
});
|
||||
|
||||
it('listCabinetItems builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listCabinetItems('hh1', { medicineId: 'med-1', status: 'active', expiringWithin: 30 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('medicineId=med-1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('status=active'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('expiringWithin=30'));
|
||||
});
|
||||
|
||||
it('listCabinetItems with cursor and limit', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listCabinetItems('hh1', { cursor: 'cur1', limit: 20 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('limit=20'));
|
||||
});
|
||||
|
||||
it('getCabinetItem calls GET', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'ci-1' });
|
||||
await getCabinetItem('hh1', 'ci-1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/cabinet/ci-1');
|
||||
});
|
||||
|
||||
it('getCabinetSummary calls GET', async () => {
|
||||
mockGet.mockResolvedValue({});
|
||||
await getCabinetSummary('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/cabinet/summary');
|
||||
});
|
||||
|
||||
it('getExpiringSoon uses default days', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getExpiringSoon('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/cabinet/expiring-soon?days=30');
|
||||
});
|
||||
|
||||
it('getExpiringSoon uses custom days', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getExpiringSoon('hh1', 7);
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/cabinet/expiring-soon?days=7');
|
||||
});
|
||||
|
||||
it('createCabinetItem calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'ci-1' });
|
||||
const data = { medicineId: 'med-1' } as never;
|
||||
await createCabinetItem('hh1', data);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/cabinet', data);
|
||||
});
|
||||
|
||||
it('updateCabinetItem calls PATCH', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'ci-1' });
|
||||
await updateCabinetItem('hh1', 'ci-1', { notes: 'x' } as never);
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/cabinet/ci-1', { notes: 'x' });
|
||||
});
|
||||
|
||||
it('adjustCabinetItemQuantity calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'ci-1' });
|
||||
await adjustCabinetItemQuantity('hh1', 'ci-1', { adjustment: -5, reason: 'used' } as never);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/cabinet/ci-1/adjust', { adjustment: -5, reason: 'used' });
|
||||
});
|
||||
|
||||
it('deleteCabinetItem calls DELETE', async () => {
|
||||
mockDelete.mockResolvedValue(undefined);
|
||||
await deleteCabinetItem('hh1', 'ci-1');
|
||||
expect(mockDelete).toHaveBeenCalledWith('/households/hh1/cabinet/ci-1');
|
||||
});
|
||||
});
|
||||
47
packages/web/src/services/__tests__/households.test.ts
Normal file
47
packages/web/src/services/__tests__/households.test.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost, mockPatch } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
mockPatch: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost, patch: mockPatch },
|
||||
}));
|
||||
|
||||
import { createHousehold, getHousehold, updateHousehold, generateInviteCode, joinHousehold } from '../households';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('households service', () => {
|
||||
it('createHousehold calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'hh1' });
|
||||
await createHousehold('My Home');
|
||||
expect(mockPost).toHaveBeenCalledWith('/households', { name: 'My Home' });
|
||||
});
|
||||
|
||||
it('getHousehold calls GET', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'hh1' });
|
||||
await getHousehold('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1');
|
||||
});
|
||||
|
||||
it('updateHousehold calls PATCH', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'hh1' });
|
||||
await updateHousehold('hh1', { name: 'Updated' });
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1', { name: 'Updated' });
|
||||
});
|
||||
|
||||
it('generateInviteCode calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'hh1' });
|
||||
await generateInviteCode('hh1');
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/invite');
|
||||
});
|
||||
|
||||
it('joinHousehold calls POST with invite code', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'hh1' });
|
||||
await joinHousehold('ABC123');
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/join', { inviteCode: 'ABC123' });
|
||||
});
|
||||
});
|
||||
62
packages/web/src/services/__tests__/medicine-prices.test.ts
Normal file
62
packages/web/src/services/__tests__/medicine-prices.test.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost },
|
||||
}));
|
||||
|
||||
import { recordPrice, getPriceHistory, compareStores, getPriceAnalytics } from '../medicine-prices';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('medicine-prices service', () => {
|
||||
it('recordPrice calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'pr-1' });
|
||||
const data = { medicineProductId: 'mp-1', storeId: 'st-1', price: 10 } as never;
|
||||
await recordPrice('hh1', data);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/medicine-prices', data);
|
||||
});
|
||||
|
||||
it('getPriceHistory with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getPriceHistory('hh1', 'med-1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/medicine-prices/history/med-1');
|
||||
});
|
||||
|
||||
it('getPriceHistory builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getPriceHistory('hh1', 'med-1', { storeId: 'st-1', startDate: '2026-01-01' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('storeId=st-1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('startDate=2026-01-01'));
|
||||
});
|
||||
|
||||
it('getPriceHistory with endDate, cursor and limit', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getPriceHistory('hh1', 'med-1', { endDate: '2026-12-31', cursor: 'cur1', limit: 10 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('endDate=2026-12-31'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('limit=10'));
|
||||
});
|
||||
|
||||
it('compareStores calls GET with medicineId', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await compareStores('hh1', 'med-1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/medicine-prices/compare/med-1');
|
||||
});
|
||||
|
||||
it('getPriceAnalytics uses default period', async () => {
|
||||
mockGet.mockResolvedValue({});
|
||||
await getPriceAnalytics('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/medicine-prices/analytics?period=month');
|
||||
});
|
||||
|
||||
it('getPriceAnalytics uses custom period', async () => {
|
||||
mockGet.mockResolvedValue({});
|
||||
await getPriceAnalytics('hh1', 'year');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/medicine-prices/analytics?period=year');
|
||||
});
|
||||
});
|
||||
94
packages/web/src/services/__tests__/medicines.test.ts
Normal file
94
packages/web/src/services/__tests__/medicines.test.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost, mockPatch, mockDelete } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
mockPatch: vi.fn(),
|
||||
mockDelete: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost, patch: mockPatch, delete: mockDelete },
|
||||
}));
|
||||
|
||||
import {
|
||||
listMedicines, getMedicine, createMedicine, updateMedicine, deleteMedicine,
|
||||
listMedicineProducts, createMedicineProduct, updateMedicineProduct, deleteMedicineProduct,
|
||||
} from '../medicines';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('medicines service', () => {
|
||||
it('listMedicines with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listMedicines('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/medicines');
|
||||
});
|
||||
|
||||
it('listMedicines builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listMedicines('hh1', { q: 'aspirin', category: 'pain', form: 'tablet', limit: 10 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('q=aspirin'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('category=pain'));
|
||||
});
|
||||
|
||||
it('listMedicines with cursor', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listMedicines('hh1', { cursor: 'cur1' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur1'));
|
||||
});
|
||||
|
||||
it('getMedicine calls GET', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'med-1' });
|
||||
await getMedicine('hh1', 'med-1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/medicines/med-1');
|
||||
});
|
||||
|
||||
it('createMedicine calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'med-1' });
|
||||
await createMedicine('hh1', { name: 'Aspirin' } as never);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/medicines', { name: 'Aspirin' });
|
||||
});
|
||||
|
||||
it('updateMedicine calls PATCH', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'med-1' });
|
||||
await updateMedicine('hh1', 'med-1', { name: 'Updated' } as never);
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/medicines/med-1', { name: 'Updated' });
|
||||
});
|
||||
|
||||
it('deleteMedicine calls DELETE', async () => {
|
||||
mockDelete.mockResolvedValue(undefined);
|
||||
await deleteMedicine('hh1', 'med-1');
|
||||
expect(mockDelete).toHaveBeenCalledWith('/households/hh1/medicines/med-1');
|
||||
});
|
||||
|
||||
it('listMedicineProducts builds URL with medicineId', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listMedicineProducts('hh1', 'med-1', { limit: 5 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('/medicines/med-1/products'));
|
||||
});
|
||||
|
||||
it('listMedicineProducts with cursor', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listMedicineProducts('hh1', 'med-1', { cursor: 'cur1' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur1'));
|
||||
});
|
||||
|
||||
it('createMedicineProduct calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'mp-1' });
|
||||
await createMedicineProduct('hh1', 'med-1', { brand: 'Bayer' } as never);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/medicines/med-1/products', { brand: 'Bayer' });
|
||||
});
|
||||
|
||||
it('updateMedicineProduct uses medicine-products path', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'mp-1' });
|
||||
await updateMedicineProduct('hh1', 'mp-1', { brand: 'Updated' } as never);
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/medicine-products/mp-1', { brand: 'Updated' });
|
||||
});
|
||||
|
||||
it('deleteMedicineProduct calls DELETE', async () => {
|
||||
mockDelete.mockResolvedValue(undefined);
|
||||
await deleteMedicineProduct('hh1', 'mp-1');
|
||||
expect(mockDelete).toHaveBeenCalledWith('/households/hh1/medicine-products/mp-1');
|
||||
});
|
||||
});
|
||||
62
packages/web/src/services/__tests__/organizer.test.ts
Normal file
62
packages/web/src/services/__tests__/organizer.test.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost },
|
||||
}));
|
||||
|
||||
import { listFills, getFill, previewFill, executeFill, undoFill } from '../organizer';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('organizer service', () => {
|
||||
it('listFills with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listFills('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/organizer/fills');
|
||||
});
|
||||
|
||||
it('listFills builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listFills('hh1', { regimenId: 'reg-1', status: 'active' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('regimenId=reg-1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('status=active'));
|
||||
});
|
||||
|
||||
it('listFills with cursor and limit', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listFills('hh1', { cursor: 'cur1', limit: 5 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('limit=5'));
|
||||
});
|
||||
|
||||
it('getFill calls GET', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'f-1' });
|
||||
await getFill('hh1', 'f-1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/organizer/fills/f-1');
|
||||
});
|
||||
|
||||
it('previewFill calls POST', async () => {
|
||||
mockPost.mockResolvedValue({});
|
||||
const data = { regimenId: 'reg-1' } as never;
|
||||
await previewFill('hh1', data);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/organizer/preview', data);
|
||||
});
|
||||
|
||||
it('executeFill calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'f-1' });
|
||||
const data = { regimenId: 'reg-1' } as never;
|
||||
await executeFill('hh1', data);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/organizer/fill', data);
|
||||
});
|
||||
|
||||
it('undoFill calls POST with empty body', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'f-1' });
|
||||
await undoFill('hh1', 'f-1');
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/organizer/fills/f-1/undo', {});
|
||||
});
|
||||
});
|
||||
62
packages/web/src/services/__tests__/purchases.test.ts
Normal file
62
packages/web/src/services/__tests__/purchases.test.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost, mockPatch, mockDelete } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
mockPatch: vi.fn(),
|
||||
mockDelete: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost, patch: mockPatch, delete: mockDelete },
|
||||
}));
|
||||
|
||||
import { listPurchases, getPurchase, createPurchase, updatePurchase, receivePurchase, deletePurchase } from '../purchases';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('purchases service', () => {
|
||||
it('listPurchases with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listPurchases('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/purchases');
|
||||
});
|
||||
|
||||
it('listPurchases builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listPurchases('hh1', { status: 'ordered', storeId: 'st-1', limit: 10 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('status=ordered'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('storeId=st-1'));
|
||||
});
|
||||
|
||||
it('getPurchase calls GET', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'p-1' });
|
||||
await getPurchase('hh1', 'p-1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/purchases/p-1');
|
||||
});
|
||||
|
||||
it('createPurchase calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'p-1' });
|
||||
const data = { storeId: 'st-1', items: [] } as never;
|
||||
await createPurchase('hh1', data);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/purchases', data);
|
||||
});
|
||||
|
||||
it('updatePurchase calls PATCH', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'p-1' });
|
||||
await updatePurchase('hh1', 'p-1', { notes: 'updated' } as never);
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/purchases/p-1', { notes: 'updated' });
|
||||
});
|
||||
|
||||
it('receivePurchase calls POST with empty body', async () => {
|
||||
mockPost.mockResolvedValue({ addedCount: 1, priceRecordsCreated: 0 });
|
||||
await receivePurchase('hh1', 'p-1');
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/purchases/p-1/receive', {});
|
||||
});
|
||||
|
||||
it('deletePurchase calls DELETE', async () => {
|
||||
mockDelete.mockResolvedValue({ _id: 'p-1' });
|
||||
await deletePurchase('hh1', 'p-1');
|
||||
expect(mockDelete).toHaveBeenCalledWith('/households/hh1/purchases/p-1');
|
||||
});
|
||||
});
|
||||
80
packages/web/src/services/__tests__/refills.test.ts
Normal file
80
packages/web/src/services/__tests__/refills.test.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost, mockPatch } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
mockPatch: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost, patch: mockPatch },
|
||||
}));
|
||||
|
||||
import {
|
||||
getRefillAlerts, listRefillLists, createRefillList, getRefillList,
|
||||
updateRefillList, updateRefillListItem, addToCabinet,
|
||||
} from '../refills';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('refills service', () => {
|
||||
it('getRefillAlerts with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getRefillAlerts('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/refills/alerts');
|
||||
});
|
||||
|
||||
it('getRefillAlerts builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getRefillAlerts('hh1', { thresholdDays: 14, userId: 'u-1' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('thresholdDays=14'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('userId=u-1'));
|
||||
});
|
||||
|
||||
it('listRefillLists with query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listRefillLists('hh1', { status: 'active' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('status=active'));
|
||||
});
|
||||
|
||||
it('listRefillLists with cursor and limit', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listRefillLists('hh1', { cursor: 'cur1', limit: 25 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('limit=25'));
|
||||
});
|
||||
|
||||
it('createRefillList calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'rl-1' });
|
||||
const data = { name: 'Weekly' } as never;
|
||||
await createRefillList('hh1', data);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/refills/lists', data);
|
||||
});
|
||||
|
||||
it('getRefillList calls GET', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'rl-1' });
|
||||
await getRefillList('hh1', 'rl-1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/refills/lists/rl-1');
|
||||
});
|
||||
|
||||
it('updateRefillList calls PATCH', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'rl-1' });
|
||||
await updateRefillList('hh1', 'rl-1', { name: 'Updated' } as never);
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/refills/lists/rl-1', { name: 'Updated' });
|
||||
});
|
||||
|
||||
it('updateRefillListItem calls PATCH with nested path', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'rl-1' });
|
||||
await updateRefillListItem('hh1', 'rl-1', 'item-1', { purchased: true } as never);
|
||||
expect(mockPatch).toHaveBeenCalledWith(
|
||||
'/households/hh1/refills/lists/rl-1/items/item-1',
|
||||
{ purchased: true },
|
||||
);
|
||||
});
|
||||
|
||||
it('addToCabinet calls POST with empty body', async () => {
|
||||
mockPost.mockResolvedValue({});
|
||||
await addToCabinet('hh1', 'rl-1');
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/refills/lists/rl-1/add-to-cabinet', {});
|
||||
});
|
||||
});
|
||||
68
packages/web/src/services/__tests__/regimens.test.ts
Normal file
68
packages/web/src/services/__tests__/regimens.test.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost, mockPatch, mockDelete } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
mockPatch: vi.fn(),
|
||||
mockDelete: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost, patch: mockPatch, delete: mockDelete },
|
||||
}));
|
||||
|
||||
import { listRegimens, getRegimen, getBurnRates, createRegimen, updateRegimen, deleteRegimen } from '../regimens';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('regimens service', () => {
|
||||
it('listRegimens with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listRegimens('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/regimens');
|
||||
});
|
||||
|
||||
it('listRegimens builds query string with isActive', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listRegimens('hh1', { isActive: true, limit: 10 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('isActive=true'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('limit=10'));
|
||||
});
|
||||
|
||||
it('listRegimens with cursor', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listRegimens('hh1', { cursor: 'cur1' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur1'));
|
||||
});
|
||||
|
||||
it('getRegimen calls GET', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'reg-1' });
|
||||
await getRegimen('hh1', 'reg-1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/regimens/reg-1');
|
||||
});
|
||||
|
||||
it('getBurnRates calls GET', async () => {
|
||||
mockGet.mockResolvedValue({});
|
||||
await getBurnRates('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/regimens/burn-rate');
|
||||
});
|
||||
|
||||
it('createRegimen calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'reg-1' });
|
||||
const data = { name: 'Morning' } as never;
|
||||
await createRegimen('hh1', data);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/regimens', data);
|
||||
});
|
||||
|
||||
it('updateRegimen calls PATCH', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'reg-1' });
|
||||
await updateRegimen('hh1', 'reg-1', { name: 'Updated' } as never);
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/regimens/reg-1', { name: 'Updated' });
|
||||
});
|
||||
|
||||
it('deleteRegimen calls DELETE', async () => {
|
||||
mockDelete.mockResolvedValue(undefined);
|
||||
await deleteRegimen('hh1', 'reg-1');
|
||||
expect(mockDelete).toHaveBeenCalledWith('/households/hh1/regimens/reg-1');
|
||||
});
|
||||
});
|
||||
56
packages/web/src/services/__tests__/stores.test.ts
Normal file
56
packages/web/src/services/__tests__/stores.test.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost, mockPatch, mockDelete } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
mockPatch: vi.fn(),
|
||||
mockDelete: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost, patch: mockPatch, delete: mockDelete },
|
||||
}));
|
||||
|
||||
import { listStores, getStore, createStore, updateStore, deactivateStore } from '../stores';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('stores service', () => {
|
||||
it('listStores calls GET with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listStores('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/stores');
|
||||
});
|
||||
|
||||
it('listStores builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listStores('hh1', { tags: 'pharmacy', search: 'cvs', cursor: 'c1', limit: 5 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('tags=pharmacy'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('search=cvs'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('limit=5'));
|
||||
});
|
||||
|
||||
it('getStore calls GET with id', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'st-1' });
|
||||
await getStore('hh1', 'st-1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/stores/st-1');
|
||||
});
|
||||
|
||||
it('createStore calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'st-1' });
|
||||
await createStore('hh1', { name: 'CVS' } as never);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/stores', { name: 'CVS' });
|
||||
});
|
||||
|
||||
it('updateStore calls PATCH', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'st-1' });
|
||||
await updateStore('hh1', 'st-1', { name: 'Updated' } as never);
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/stores/st-1', { name: 'Updated' });
|
||||
});
|
||||
|
||||
it('deactivateStore calls DELETE', async () => {
|
||||
mockDelete.mockResolvedValue({ _id: 'st-1' });
|
||||
await deactivateStore('hh1', 'st-1');
|
||||
expect(mockDelete).toHaveBeenCalledWith('/households/hh1/stores/st-1');
|
||||
});
|
||||
});
|
||||
60
packages/web/src/services/medicine-prices.ts
Normal file
60
packages/web/src/services/medicine-prices.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { apiClient } from './api-client';
|
||||
import type { z } from 'zod/v4';
|
||||
import type {
|
||||
MedicinePriceRecordResponseSchema,
|
||||
MedicinePriceHistoryResponseSchema,
|
||||
StoreComparisonResponseSchema,
|
||||
MedicineSpendingAnalyticsResponseSchema,
|
||||
CreateMedicinePriceRecordSchema,
|
||||
} from '@meshitrack/shared';
|
||||
|
||||
type MedicinePriceRecordResponse = z.infer<typeof MedicinePriceRecordResponseSchema>;
|
||||
type MedicinePriceHistoryResponse = z.infer<typeof MedicinePriceHistoryResponseSchema>;
|
||||
type StoreComparisonResponse = z.infer<typeof StoreComparisonResponseSchema>;
|
||||
type MedicineSpendingAnalyticsResponse = z.infer<typeof MedicineSpendingAnalyticsResponseSchema>;
|
||||
type CreateMedicinePriceRecordInput = z.infer<typeof CreateMedicinePriceRecordSchema>;
|
||||
|
||||
export async function recordPrice(
|
||||
householdId: string,
|
||||
data: CreateMedicinePriceRecordInput,
|
||||
): Promise<MedicinePriceRecordResponse> {
|
||||
return apiClient.post<MedicinePriceRecordResponse>(
|
||||
`/households/${householdId}/medicine-prices`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getPriceHistory(
|
||||
householdId: string,
|
||||
medicineId: string,
|
||||
query?: { storeId?: string; startDate?: string; endDate?: string; cursor?: string; limit?: number },
|
||||
): Promise<MedicinePriceHistoryResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (query?.storeId) params.set('storeId', query.storeId);
|
||||
if (query?.startDate) params.set('startDate', query.startDate);
|
||||
if (query?.endDate) params.set('endDate', query.endDate);
|
||||
if (query?.cursor) params.set('cursor', query.cursor);
|
||||
if (query?.limit) params.set('limit', String(query.limit));
|
||||
const qs = params.toString();
|
||||
return apiClient.get<MedicinePriceHistoryResponse>(
|
||||
`/households/${householdId}/medicine-prices/history/${medicineId}${qs ? `?${qs}` : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function compareStores(
|
||||
householdId: string,
|
||||
medicineId: string,
|
||||
): Promise<StoreComparisonResponse> {
|
||||
return apiClient.get<StoreComparisonResponse>(
|
||||
`/households/${householdId}/medicine-prices/compare/${medicineId}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getPriceAnalytics(
|
||||
householdId: string,
|
||||
period: 'month' | 'quarter' | 'year' = 'month',
|
||||
): Promise<MedicineSpendingAnalyticsResponse> {
|
||||
return apiClient.get<MedicineSpendingAnalyticsResponse>(
|
||||
`/households/${householdId}/medicine-prices/analytics?period=${period}`,
|
||||
);
|
||||
}
|
||||
69
packages/web/src/services/purchases.ts
Normal file
69
packages/web/src/services/purchases.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { apiClient } from './api-client';
|
||||
import type { z } from 'zod/v4';
|
||||
import type {
|
||||
PurchaseResponseSchema,
|
||||
PurchaseListResponseSchema,
|
||||
CreatePurchaseSchema,
|
||||
UpdatePurchaseSchema,
|
||||
PurchaseQuerySchema,
|
||||
} from '@meshitrack/shared';
|
||||
|
||||
type PurchaseResponse = z.infer<typeof PurchaseResponseSchema>;
|
||||
type PurchaseListResponse = z.infer<typeof PurchaseListResponseSchema>;
|
||||
type CreatePurchaseInput = z.infer<typeof CreatePurchaseSchema>;
|
||||
type UpdatePurchaseInput = z.infer<typeof UpdatePurchaseSchema>;
|
||||
type PurchaseQueryInput = z.infer<typeof PurchaseQuerySchema>;
|
||||
|
||||
export async function listPurchases(
|
||||
householdId: string,
|
||||
query?: Partial<PurchaseQueryInput>,
|
||||
): Promise<PurchaseListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (query?.status) params.set('status', query.status);
|
||||
if (query?.storeId) params.set('storeId', query.storeId);
|
||||
if (query?.cursor) params.set('cursor', query.cursor);
|
||||
if (query?.limit) params.set('limit', String(query.limit));
|
||||
const qs = params.toString();
|
||||
return apiClient.get<PurchaseListResponse>(
|
||||
`/households/${householdId}/purchases${qs ? `?${qs}` : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getPurchase(
|
||||
householdId: string,
|
||||
id: string,
|
||||
): Promise<PurchaseResponse> {
|
||||
return apiClient.get<PurchaseResponse>(`/households/${householdId}/purchases/${id}`);
|
||||
}
|
||||
|
||||
export async function createPurchase(
|
||||
householdId: string,
|
||||
data: CreatePurchaseInput,
|
||||
): Promise<PurchaseResponse> {
|
||||
return apiClient.post<PurchaseResponse>(`/households/${householdId}/purchases`, data);
|
||||
}
|
||||
|
||||
export async function updatePurchase(
|
||||
householdId: string,
|
||||
id: string,
|
||||
data: UpdatePurchaseInput,
|
||||
): Promise<PurchaseResponse> {
|
||||
return apiClient.patch<PurchaseResponse>(
|
||||
`/households/${householdId}/purchases/${id}`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export async function receivePurchase(
|
||||
householdId: string,
|
||||
id: string,
|
||||
): Promise<{ addedCount: number; priceRecordsCreated: number }> {
|
||||
return apiClient.post<{ addedCount: number; priceRecordsCreated: number }>(
|
||||
`/households/${householdId}/purchases/${id}/receive`,
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
export async function deletePurchase(householdId: string, id: string): Promise<PurchaseResponse> {
|
||||
return apiClient.delete<PurchaseResponse>(`/households/${householdId}/purchases/${id}`);
|
||||
}
|
||||
96
packages/web/src/services/refills.ts
Normal file
96
packages/web/src/services/refills.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import { apiClient } from './api-client';
|
||||
import type { z } from 'zod/v4';
|
||||
import type {
|
||||
RefillAlertResponseSchema,
|
||||
RefillListResponseSchema,
|
||||
RefillListListResponseSchema,
|
||||
AddToCabinetResponseSchema,
|
||||
CreateRefillListSchema,
|
||||
UpdateRefillListSchema,
|
||||
UpdateRefillListItemSchema,
|
||||
} from '@meshitrack/shared';
|
||||
|
||||
type RefillAlertResponse = z.infer<typeof RefillAlertResponseSchema>;
|
||||
type RefillListResponse = z.infer<typeof RefillListResponseSchema>;
|
||||
type RefillListListResponse = z.infer<typeof RefillListListResponseSchema>;
|
||||
type AddToCabinetResponse = z.infer<typeof AddToCabinetResponseSchema>;
|
||||
type CreateRefillListInput = z.infer<typeof CreateRefillListSchema>;
|
||||
type UpdateRefillListInput = z.infer<typeof UpdateRefillListSchema>;
|
||||
type UpdateRefillListItemInput = z.infer<typeof UpdateRefillListItemSchema>;
|
||||
|
||||
export async function getRefillAlerts(
|
||||
householdId: string,
|
||||
query?: { thresholdDays?: number; userId?: string },
|
||||
): Promise<{ data: RefillAlertResponse[] }> {
|
||||
const params = new URLSearchParams();
|
||||
if (query?.thresholdDays) params.set('thresholdDays', String(query.thresholdDays));
|
||||
if (query?.userId) params.set('userId', query.userId);
|
||||
const qs = params.toString();
|
||||
return apiClient.get<{ data: RefillAlertResponse[] }>(
|
||||
`/households/${householdId}/refills/alerts${qs ? `?${qs}` : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function listRefillLists(
|
||||
householdId: string,
|
||||
query?: { status?: string; cursor?: string; limit?: number },
|
||||
): Promise<RefillListListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (query?.status) params.set('status', query.status);
|
||||
if (query?.cursor) params.set('cursor', query.cursor);
|
||||
if (query?.limit) params.set('limit', String(query.limit));
|
||||
const qs = params.toString();
|
||||
return apiClient.get<RefillListListResponse>(
|
||||
`/households/${householdId}/refills/lists${qs ? `?${qs}` : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function createRefillList(
|
||||
householdId: string,
|
||||
data: CreateRefillListInput,
|
||||
): Promise<RefillListResponse> {
|
||||
return apiClient.post<RefillListResponse>(
|
||||
`/households/${householdId}/refills/lists`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getRefillList(
|
||||
householdId: string,
|
||||
id: string,
|
||||
): Promise<RefillListResponse> {
|
||||
return apiClient.get<RefillListResponse>(`/households/${householdId}/refills/lists/${id}`);
|
||||
}
|
||||
|
||||
export async function updateRefillList(
|
||||
householdId: string,
|
||||
id: string,
|
||||
data: UpdateRefillListInput,
|
||||
): Promise<RefillListResponse> {
|
||||
return apiClient.patch<RefillListResponse>(
|
||||
`/households/${householdId}/refills/lists/${id}`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateRefillListItem(
|
||||
householdId: string,
|
||||
listId: string,
|
||||
itemId: string,
|
||||
data: UpdateRefillListItemInput,
|
||||
): Promise<RefillListResponse> {
|
||||
return apiClient.patch<RefillListResponse>(
|
||||
`/households/${householdId}/refills/lists/${listId}/items/${itemId}`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export async function addToCabinet(
|
||||
householdId: string,
|
||||
listId: string,
|
||||
): Promise<AddToCabinetResponse> {
|
||||
return apiClient.post<AddToCabinetResponse>(
|
||||
`/households/${householdId}/refills/lists/${listId}/add-to-cabinet`,
|
||||
{},
|
||||
);
|
||||
}
|
||||
51
packages/web/src/services/stores.ts
Normal file
51
packages/web/src/services/stores.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { apiClient } from './api-client';
|
||||
import type { z } from 'zod/v4';
|
||||
import type {
|
||||
StoreResponseSchema,
|
||||
StoreListResponseSchema,
|
||||
CreateStoreSchema,
|
||||
UpdateStoreSchema,
|
||||
} from '@meshitrack/shared';
|
||||
|
||||
type StoreResponse = z.infer<typeof StoreResponseSchema>;
|
||||
type StoreListResponse = z.infer<typeof StoreListResponseSchema>;
|
||||
type CreateStoreInput = z.infer<typeof CreateStoreSchema>;
|
||||
type UpdateStoreInput = z.infer<typeof UpdateStoreSchema>;
|
||||
|
||||
export async function listStores(
|
||||
householdId: string,
|
||||
query?: { tags?: string; search?: string; cursor?: string; limit?: number },
|
||||
): Promise<StoreListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (query?.tags) params.set('tags', query.tags);
|
||||
if (query?.search) params.set('search', query.search);
|
||||
if (query?.cursor) params.set('cursor', query.cursor);
|
||||
if (query?.limit) params.set('limit', String(query.limit));
|
||||
const qs = params.toString();
|
||||
return apiClient.get<StoreListResponse>(
|
||||
`/households/${householdId}/stores${qs ? `?${qs}` : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getStore(householdId: string, id: string): Promise<StoreResponse> {
|
||||
return apiClient.get<StoreResponse>(`/households/${householdId}/stores/${id}`);
|
||||
}
|
||||
|
||||
export async function createStore(
|
||||
householdId: string,
|
||||
data: CreateStoreInput,
|
||||
): Promise<StoreResponse> {
|
||||
return apiClient.post<StoreResponse>(`/households/${householdId}/stores`, data);
|
||||
}
|
||||
|
||||
export async function updateStore(
|
||||
householdId: string,
|
||||
id: string,
|
||||
data: UpdateStoreInput,
|
||||
): Promise<StoreResponse> {
|
||||
return apiClient.patch<StoreResponse>(`/households/${householdId}/stores/${id}`, data);
|
||||
}
|
||||
|
||||
export async function deactivateStore(householdId: string, id: string): Promise<StoreResponse> {
|
||||
return apiClient.delete<StoreResponse>(`/households/${householdId}/stores/${id}`);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue