Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 85 additions & 1 deletion tests/unit/handlers/spare.parts.handlers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ const PART = {
rack: PART_RACK,
info: { location: 'Lab', status: 'active' }
}
const REPLACED_PART = {
id: 'p-out',
code: 'CB-AM-CB_1111-02',
rack: PART_RACK,
info: { location: 'miner.room', status: 'faulty' }
}
const OPEN_WO = {
id: 'wo-1',
code: 'IVI-2-0001',
Expand All @@ -32,12 +38,13 @@ const userMeta = (email = 'op@test') => ({
_info: { authToken: 'tok', user: { metadata: { email } } }
})

function buildCtx ({ wo = OPEN_WO, part = PART, pushResults = {} } = {}) {
function buildCtx ({ wo = OPEN_WO, part = PART, replaced = REPLACED_PART, pushResults = {} } = {}) {
const pushed = []
const handler = async (_key, method, params) => {
if (method === 'listThings') {
if (params.query?.type === 'inventory-work_order') return wo ? [wo] : []
if (params.query?.id === PART.id) return part ? [part] : []
if (params.query?.code) return replaced && params.query.code === replaced.code ? [replaced] : []
return []
}
if (method === 'pushAction') {
Expand Down Expand Up @@ -153,6 +160,83 @@ test('handlers: updateSparePart skips WO checks when only non-move fields change
t.absent(pushed[0].params[0].info.workOrderId, 'no WO injected for non-move')
})

test('handlers: updateSparePart records top-level remarks on the move entry and keeps it off the part', async (t) => {
const { ctx, pushed } = buildCtx()
await handlers.updateSparePart(ctx, {
...userMeta(),
params: { id: PART.id },
body: { rackId: PART_RACK, workOrderId: 'wo-1', remarks: 'Not reachable', info: { location: 'miner.room' } }
})
const woAction = pushed.find(p => p.params[0].rackId === WO_RACK)
const move = woAction.params[0].info.partsMoves[0]
t.is(move.remarks, 'Not reachable')
t.is(move.role, 'original')
const partAction = pushed.find(p => p.params[0].rackId === PART_RACK)
t.absent(partAction.params[0].info.remarks, 'remarks are move metadata, not written to the part record')
})

test('handlers: updateSparePart falls back to info.reason for remarks', async (t) => {
const { ctx, pushed } = buildCtx()
await handlers.updateSparePart(ctx, {
...userMeta(),
params: { id: PART.id },
body: { rackId: PART_RACK, workOrderId: 'wo-1', info: { location: 'miner.room', reason: 'hardware_error' } }
})
const woAction = pushed.find(p => p.params[0].rackId === WO_RACK)
t.is(woAction.params[0].info.partsMoves[0].remarks, 'hardware_error')
})

test('handlers: updateSparePart marks a replacement and links the replaced part', async (t) => {
const { ctx, pushed } = buildCtx()
const out = await handlers.updateSparePart(ctx, {
...userMeta(),
params: { id: PART.id },
body: { rackId: PART_RACK, workOrderId: 'wo-1', replacesPartCode: REPLACED_PART.code, info: { location: 'site.warehouse' } }
})
const woAction = pushed.find(p => p.params[0].rackId === WO_RACK)
const move = woAction.params[0].info.partsMoves[0]
t.is(move.role, 'replacement')
t.is(move.replacesPartCode, REPLACED_PART.code)
t.is(move.replacesPartId, REPLACED_PART.id, 'server resolves the replaced part id from its code')
t.is(out.move.role, 'replacement')
})

test('handlers: updateSparePart 400s when replacesPartCode does not resolve to a part', async (t) => {
const { ctx } = buildCtx({ replaced: null })
await t.exception(
() => handlers.updateSparePart(ctx, {
...userMeta(),
params: { id: PART.id },
body: { rackId: PART_RACK, workOrderId: 'wo-1', replacesPartCode: 'NOPE-0001', info: { location: 'site.warehouse' } }
}),
/ERR_REPLACES_PART_NOT_FOUND/
)
})

test('handlers: updateSparePart rejects a part replacing itself', async (t) => {
const { ctx } = buildCtx()
await t.exception(
() => handlers.updateSparePart(ctx, {
...userMeta(),
params: { id: PART.id },
body: { rackId: PART_RACK, workOrderId: 'wo-1', replacesPartCode: PART.code, info: { location: 'site.warehouse' } }
}),
/ERR_REPLACES_PART_SELF/
)
})

test('handlers: updateSparePart treats a replacement as a move and requires a workOrderId', async (t) => {
const { ctx } = buildCtx()
await t.exception(
() => handlers.updateSparePart(ctx, {
...userMeta(),
params: { id: PART.id },
body: { rackId: PART_RACK, replacesPartCode: REPLACED_PART.code, info: {} }
}),
/ERR_PART_MOVE_REQUIRES_WO/
)
})

function buildRegisterCtx ({ pushResult } = {}) {
const pushed = []
const handler = async (_key, method, params) => {
Expand Down
39 changes: 34 additions & 5 deletions workers/lib/server/handlers/spare.parts.handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,19 @@ async function _loadSparePart (ctx, partId) {
return flattenRpcResults(results).find(t => t?.type !== WORK_ORDER_THING_TYPE) || null
}

async function _loadSparePartByCode (ctx, code) {
const results = await ctx.dataProxy.requestData('listThings', {
query: { code }
})
return flattenRpcResults(results).find(t => t?.type !== WORK_ORDER_THING_TYPE) || null
}

async function updateSparePart (ctx, req) {
const { id } = req.params
const { rackId, workOrderId, info } = req.body
const { rackId, workOrderId, info, remarks, replacesPartCode } = req.body

const movesPart = info.location !== undefined || info.status !== undefined
const isReplacement = typeof replacesPartCode === 'string' && replacesPartCode.length > 0
const movesPart = info.location !== undefined || info.status !== undefined || isReplacement

if (movesPart && !workOrderId) {
const err = new Error('ERR_PART_MOVE_REQUIRES_WO')
Expand All @@ -43,10 +51,12 @@ async function updateSparePart (ctx, req) {

let workOrderCode
let part
let replacedPart
if (movesPart) {
const [wo, p] = await Promise.all([
const [wo, p, rp] = await Promise.all([
_loadWorkOrder(ctx, workOrderId),
_loadSparePart(ctx, id)
_loadSparePart(ctx, id),
isReplacement ? _loadSparePartByCode(ctx, replacesPartCode) : Promise.resolve(null)
])
if (!wo) {
const err = new Error('ERR_WORK_ORDER_NOT_FOUND')
Expand All @@ -63,8 +73,21 @@ async function updateSparePart (ctx, req) {
err.statusCode = 404
throw err
}
if (isReplacement) {
if (replacesPartCode === p.code) {
const err = new Error('ERR_REPLACES_PART_SELF')
err.statusCode = 400
throw err
}
if (!rp) {
const err = new Error('ERR_REPLACES_PART_NOT_FOUND')
err.statusCode = 400
throw err
}
}
workOrderCode = wo.code
part = p
replacedPart = rp
}

const { permissions } = await ctx.authLib.getTokenPerms(req._info.authToken)
Expand Down Expand Up @@ -97,6 +120,7 @@ async function updateSparePart (ctx, req) {
throw err
}

const moveRemarks = remarks ?? info.reason
const moveEntry = {
partId: id,
partCode: part.code,
Expand All @@ -106,10 +130,15 @@ async function updateSparePart (ctx, req) {
toLocation: info.location ?? part.info?.location ?? null,
fromStatus: part.info?.status ?? null,
toStatus: info.status ?? part.info?.status ?? null,
role: 'original',
role: isReplacement ? 'replacement' : 'original',
ts: Date.now(),
user: voter
}
if (moveRemarks !== undefined && moveRemarks !== null) moveEntry.remarks = moveRemarks
if (isReplacement) {
moveEntry.replacesPartCode = replacesPartCode
moveEntry.replacesPartId = replacedPart.id
}

const woRackId = await getWorkOrderRackId(ctx)
const wo = await _loadWorkOrder(ctx, workOrderId)
Expand Down
2 changes: 2 additions & 0 deletions workers/lib/server/schemas/spare.parts.schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ const update = {
properties: {
rackId: { type: 'string', minLength: 1 },
workOrderId: { type: 'string', minLength: 1 },
remarks: { type: 'string', maxLength: 4000 },
replacesPartCode: { type: 'string', minLength: 1, maxLength: 200 },
info: {
type: 'object',
additionalProperties: true,
Expand Down
Loading