M8-R09: add meter close unbind and transfer workflows

This commit is contained in:
2026-08-24 18:37:46 +02:00
parent 8dc3f71aaf
commit b1b6a309cb
4 changed files with 1217 additions and 118 deletions
+34 -1
View File
@@ -106,7 +106,9 @@ describe('useDeclareMeter source binding invalidation', () => {
const { qc, Wrapper } = makeWrapper()
const affectedKeys = [
['energy-meters'], ['energy-source-channels'], ['energy-meter-bindings', 9],
['energy-sources'], ['expose-catalog'], ['energy-costs'], ['energy-costs-summary'],
['energy-sources'], ['energy-source', 'source-1'], ['energy-channel-readings', 'source-1', 'channel-1'],
['expose-catalog'], ['energy-costs', 'electricity'], ['energy-costs-summary', 'electricity'],
['meter-costs', 'thermal', 'month'], ['meter-cost-summary', 'thermal'],
]
for (const queryKey of affectedKeys) qc.setQueryData(queryKey, { cached: true })
const { useDeclareMeter } = await import('./hooks')
@@ -117,6 +119,37 @@ describe('useDeclareMeter source binding invalidation', () => {
})
})
describe('meter lifecycle mutations', () => {
beforeEach(() => vi.clearAllMocks())
it.each([
['Declare auto-handoff', 'post', async (hooks: typeof import('./hooks')) => hooks.useDeclareMeter, { label: 'swap', commodity: 'electricity', started_at: '2026-08-24T12:34', reason: 'meter_swap' }],
['Close meter', 'post', async (hooks: typeof import('./hooks')) => hooks.useCloseMeter, { id: 9, ended_at: '2026-08-24T12:34' }],
['Unbind', 'patch', async (hooks: typeof import('./hooks')) => hooks.useCloseBinding, { uuid: 'old', ended_at: '2026-08-24T12:34' }],
['Transfer', 'post', async (hooks: typeof import('./hooks')) => hooks.useTransferBinding, { id: 10, body: { from_binding_uuid: 'old', to_source_channel_uuid: 'new', effective_at: '2026-08-24T12:34' } }],
['Direct bind', 'post', async (hooks: typeof import('./hooks')) => hooks.useCreateBinding, { id: 10, body: { source_channel_uuid: 'new', started_at: '2026-08-24T12:34' } }],
['Update electricity start', 'patch', async (hooks: typeof import('./hooks')) => hooks.useUpdateMeter, { id: 10, body: { started_at: '2026-08-24T12:34' } }],
['Update heating label', 'patch', async (hooks: typeof import('./hooks')) => hooks.useUpdateMeter, { id: 11, body: { label: 'Heating meter' } }],
['Update hot water start', 'patch', async (hooks: typeof import('./hooks')) => hooks.useUpdateMeter, { id: 12, body: { started_at: '2026-08-24T12:34' } }],
])('%s invalidates every lifecycle view using a fresh QueryClient', async (_name, method, getHook, payload) => {
mockPost.mockResolvedValue({ data: {} })
mockPatch.mockResolvedValue({ data: {} })
const { qc, Wrapper } = makeWrapper()
const affectedKeys = [
['energy-meters'], ['energy-sources'], ['energy-source', 'source-1'], ['energy-source-channels'], ['energy-meter-bindings'],
['energy-channel-readings', 'source-1', 'channel-1'], ['energy-costs', 'electricity'], ['energy-costs-summary', 'electricity'],
['meter-costs', 'thermal', 'month'], ['meter-cost-summary', 'thermal'], ['expose-catalog'],
]
for (const key of affectedKeys) qc.setQueryData(key, { cached: true })
const hooks = await import('./hooks')
const useHook = await getHook(hooks)
const result = renderHook(() => useHook(), { wrapper: Wrapper })
await act(async () => { await result.result.current.mutateAsync(payload as never) })
expect(method === 'post' ? mockPost : mockPatch).toHaveBeenCalled()
for (const key of affectedKeys) expect(qc.getQueryState(key)?.isInvalidated).toBe(true)
})
})
describe('useProfiles', () => {
beforeEach(() => vi.clearAllMocks())