Add inactive item fallback for dashboard change feed lookups
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
+5
-2
@@ -116,8 +116,11 @@ export async function listGroupedStockEntries(store, options = {}) {
|
||||
return fetchAllListPages(store, `${getPath('items')}/grouped`, baseQuery);
|
||||
}
|
||||
|
||||
export async function getStockEntry(store, stockId) {
|
||||
const payload = await apiRequest(store, `${getPath('items')}/${stockId}`);
|
||||
export async function getStockEntry(store, stockId, { allowInactive = false } = {}) {
|
||||
const path = `${getPath('items')}/${stockId}`;
|
||||
const payload = allowInactive
|
||||
? await apiRequest(store, path, { query: { allow_inactive: 1 } })
|
||||
: await apiRequest(store, path);
|
||||
return unwrapEntryPayload(payload);
|
||||
}
|
||||
|
||||
|
||||
@@ -165,7 +165,18 @@ export function dashboardPageData(store) {
|
||||
|
||||
if (missingItemUuids.length) {
|
||||
const results = await Promise.allSettled(
|
||||
missingItemUuids.map((uuid) => getStockEntry(store, uuid)),
|
||||
missingItemUuids.map(async (uuid) => {
|
||||
try {
|
||||
return await getStockEntry(store, uuid);
|
||||
} catch (error) {
|
||||
const status = error?.status || error?.cause?.status;
|
||||
if (status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return getStockEntry(store, uuid, { allowInactive: true });
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
results.forEach((result) => {
|
||||
|
||||
@@ -16,6 +16,7 @@ vi.mock('../../src/api/client.js', () => ({
|
||||
const {
|
||||
adjustStockEntry,
|
||||
applyItemUpsert,
|
||||
getStockEntry,
|
||||
listGroupedStockEntries,
|
||||
listKitchenChanges,
|
||||
listStockEntries,
|
||||
@@ -125,6 +126,35 @@ describe('api/stock', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('getStockEntry fetches item without allow_inactive by default', async () => {
|
||||
apiRequestMock.mockResolvedValueOnce({ uuid_b64: 'item-1', name: 'Milk' });
|
||||
|
||||
const result = await getStockEntry({ config: { database: 'db' } }, 'item-1');
|
||||
|
||||
expect(result).toEqual({ uuid_b64: 'item-1', name: 'Milk' });
|
||||
expect(apiRequestMock).toHaveBeenCalledWith(
|
||||
{ config: { database: 'db' } },
|
||||
'kitchen/items/item-1',
|
||||
);
|
||||
});
|
||||
|
||||
it('getStockEntry forwards allow_inactive when requested', async () => {
|
||||
apiRequestMock.mockResolvedValueOnce({ uuid_b64: 'item-2', active: false });
|
||||
|
||||
const result = await getStockEntry(
|
||||
{ config: { database: 'db' } },
|
||||
'item-2',
|
||||
{ allowInactive: true },
|
||||
);
|
||||
|
||||
expect(result).toEqual({ uuid_b64: 'item-2', active: false });
|
||||
expect(apiRequestMock).toHaveBeenCalledWith(
|
||||
{ config: { database: 'db' } },
|
||||
'kitchen/items/item-2',
|
||||
{ query: { allow_inactive: 1 } },
|
||||
);
|
||||
});
|
||||
|
||||
it('listKitchenChanges returns normalized changes payload', async () => {
|
||||
apiRequestMock.mockResolvedValueOnce({
|
||||
since: 'cursor-1',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const listKitchenChangesMock = vi.fn();
|
||||
const getStockEntryMock = vi.fn();
|
||||
@@ -16,6 +16,12 @@ vi.mock('../../../src/api/locations.js', () => ({
|
||||
const { dashboardPageData, renderDashboardPage } = await import('../../../src/features/dashboard/dashboard-page.js');
|
||||
|
||||
describe('features/dashboard/dashboard-page', () => {
|
||||
beforeEach(() => {
|
||||
listKitchenChangesMock.mockReset();
|
||||
getStockEntryMock.mockReset();
|
||||
fetchLocationsMock.mockReset();
|
||||
});
|
||||
|
||||
it('renders dashboard with recent changes section', () => {
|
||||
const html = renderDashboardPage();
|
||||
expect(html).toContain('Recent changes');
|
||||
@@ -107,6 +113,50 @@ describe('features/dashboard/dashboard-page', () => {
|
||||
expect(getStockEntryMock).toHaveBeenCalledWith(expect.anything(), 'item-uuid-1');
|
||||
});
|
||||
|
||||
it('retries stock event item lookup with allowInactive after 404', async () => {
|
||||
listKitchenChangesMock.mockResolvedValueOnce({
|
||||
since: null,
|
||||
nextCursor: null,
|
||||
changes: [{
|
||||
type: 'stock',
|
||||
action: 'upsert',
|
||||
timestamp: '2026-04-10T10:00:00Z',
|
||||
stock: {
|
||||
item_uuid_b64: 'item-uuid-2',
|
||||
quantity: 1,
|
||||
uom_symbol: 'pcs',
|
||||
},
|
||||
}],
|
||||
});
|
||||
getStockEntryMock
|
||||
.mockRejectedValueOnce(Object.assign(new Error('Not found'), { status: 404 }))
|
||||
.mockResolvedValueOnce({
|
||||
uuid_b64: 'item-uuid-2',
|
||||
name: 'Archived pasta',
|
||||
stock_type: 'measured',
|
||||
});
|
||||
fetchLocationsMock.mockResolvedValueOnce({ flat: [] });
|
||||
|
||||
const store = {
|
||||
isConnected: true,
|
||||
setActiveKitchen: vi.fn(),
|
||||
addAlert: vi.fn(),
|
||||
};
|
||||
const data = dashboardPageData(store);
|
||||
|
||||
await data.refreshChanges();
|
||||
|
||||
expect(getStockEntryMock).toHaveBeenNthCalledWith(1, store, 'item-uuid-2');
|
||||
expect(getStockEntryMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
store,
|
||||
'item-uuid-2',
|
||||
{ allowInactive: true },
|
||||
);
|
||||
expect(data.changeHeadline(data.recentChanges[0])).toBe('Stock saved: Archived pasta');
|
||||
expect(data.changeStateLine(data.recentChanges[0])).toContain('Quantity: 1 pcs');
|
||||
});
|
||||
|
||||
it('keeps empty state when API returns no changes', async () => {
|
||||
listKitchenChangesMock.mockResolvedValueOnce({
|
||||
since: null,
|
||||
|
||||
Reference in New Issue
Block a user