304 lines
7.8 KiB
JavaScript
304 lines
7.8 KiB
JavaScript
import { query, withTransaction } from '../db/client.js'
|
|
|
|
const INVENTORY_ITEM_SELECT = `
|
|
SELECT
|
|
ii.id,
|
|
ii.sku_code,
|
|
ii.batch_no,
|
|
ii.credential_type,
|
|
ii.display_value,
|
|
ii.status,
|
|
ii.invalid_reason,
|
|
ii.consumed_at AS delivered_at,
|
|
ii.created_at,
|
|
ii.updated_at,
|
|
tib.task_id AS reserved_by_task_id,
|
|
ft.task_no AS reserved_by_task_no,
|
|
ft.platform_order_id
|
|
FROM inventory_items ii
|
|
LEFT JOIN LATERAL (
|
|
SELECT *
|
|
FROM task_inventory_bindings
|
|
WHERE inventory_item_id = ii.id AND binding_status IN ('reserved', 'consumed')
|
|
ORDER BY id DESC
|
|
LIMIT 1
|
|
) tib ON TRUE
|
|
LEFT JOIN fulfillment_tasks ft ON ft.id = tib.task_id
|
|
`
|
|
|
|
export async function findFirstAvailableInventoryItemBySkuCode(skuCode, credentialType = 'tencent_code') {
|
|
const result = await query(
|
|
`${INVENTORY_ITEM_SELECT}
|
|
WHERE ii.sku_code = $1 AND ii.credential_type = $2 AND ii.status = 'available'
|
|
ORDER BY ii.id ASC
|
|
LIMIT 1`,
|
|
[skuCode, credentialType],
|
|
)
|
|
|
|
return result.rows[0] || null
|
|
}
|
|
|
|
export async function assignReservedInventoryItem(inventoryItemId, taskId, updatedAt, roleKey = 'primary_code') {
|
|
return withTransaction(async (client) => {
|
|
const inventoryResult = await client.query(
|
|
`
|
|
UPDATE inventory_items
|
|
SET status = 'reserved', updated_at = $1
|
|
WHERE id = $2 AND status = 'available'
|
|
RETURNING id
|
|
`,
|
|
[updatedAt, Number(inventoryItemId)],
|
|
)
|
|
|
|
if (!inventoryResult.rows[0]) {
|
|
return getInventoryItemById(inventoryItemId)
|
|
}
|
|
|
|
await client.query(
|
|
`
|
|
INSERT INTO task_inventory_bindings (
|
|
task_id,
|
|
inventory_item_id,
|
|
role_key,
|
|
quantity,
|
|
binding_status,
|
|
metadata_json,
|
|
created_at,
|
|
updated_at
|
|
) VALUES ($1, $2, $3, 1, 'reserved', '{}'::jsonb, $4, $4)
|
|
ON CONFLICT (task_id, role_key, inventory_item_id) DO UPDATE
|
|
SET binding_status = 'reserved', updated_at = EXCLUDED.updated_at, released_at = NULL
|
|
`,
|
|
[Number(taskId), Number(inventoryItemId), roleKey, updatedAt],
|
|
)
|
|
|
|
return getInventoryItemById(inventoryItemId)
|
|
})
|
|
}
|
|
|
|
export async function getInventoryItemById(inventoryItemId) {
|
|
const result = await query(
|
|
`${INVENTORY_ITEM_SELECT}
|
|
WHERE ii.id = $1
|
|
LIMIT 1`,
|
|
[Number(inventoryItemId)],
|
|
)
|
|
|
|
return result.rows[0] || null
|
|
}
|
|
|
|
export async function markInventoryItemDelivered(inventoryItemId, deliveredAt) {
|
|
return withTransaction(async (client) => {
|
|
await client.query(
|
|
`
|
|
UPDATE inventory_items
|
|
SET status = 'consumed', consumed_at = $1, updated_at = $1
|
|
WHERE id = $2
|
|
`,
|
|
[deliveredAt, Number(inventoryItemId)],
|
|
)
|
|
|
|
await client.query(
|
|
`
|
|
UPDATE task_inventory_bindings
|
|
SET binding_status = 'consumed', consumed_at = $1, updated_at = $1
|
|
WHERE inventory_item_id = $2 AND binding_status = 'reserved'
|
|
`,
|
|
[deliveredAt, Number(inventoryItemId)],
|
|
)
|
|
|
|
return getInventoryItemById(inventoryItemId)
|
|
})
|
|
}
|
|
|
|
export async function markInventoryItemConsumed(inventoryItemId, reason, consumedAt) {
|
|
return withTransaction(async (client) => {
|
|
await client.query(
|
|
`
|
|
UPDATE inventory_items
|
|
SET
|
|
status = 'consumed',
|
|
invalid_reason = $1,
|
|
consumed_at = $2,
|
|
updated_at = $2
|
|
WHERE id = $3
|
|
`,
|
|
[String(reason || '').trim(), consumedAt, Number(inventoryItemId)],
|
|
)
|
|
|
|
await client.query(
|
|
`
|
|
UPDATE task_inventory_bindings
|
|
SET binding_status = 'consumed', consumed_at = $1, updated_at = $1
|
|
WHERE inventory_item_id = $2 AND binding_status = 'reserved'
|
|
`,
|
|
[consumedAt, Number(inventoryItemId)],
|
|
)
|
|
|
|
return getInventoryItemById(inventoryItemId)
|
|
})
|
|
}
|
|
|
|
export async function listInventoryItems({
|
|
page = 1,
|
|
pageSize = 20,
|
|
skuCode = '',
|
|
credentialType = '',
|
|
status = '',
|
|
batchNo = '',
|
|
} = {}) {
|
|
const offset = (page - 1) * pageSize
|
|
const filters = []
|
|
const params = []
|
|
|
|
if (skuCode) {
|
|
params.push(skuCode)
|
|
filters.push(`ii.sku_code = $${params.length}`)
|
|
}
|
|
|
|
if (status) {
|
|
params.push(status === 'delivered' ? 'consumed' : status)
|
|
filters.push(`ii.status = $${params.length}`)
|
|
}
|
|
|
|
if (credentialType) {
|
|
params.push(credentialType)
|
|
filters.push(`ii.credential_type = $${params.length}`)
|
|
}
|
|
|
|
if (batchNo) {
|
|
params.push(`%${batchNo}%`)
|
|
filters.push(`ii.batch_no ILIKE $${params.length}`)
|
|
}
|
|
|
|
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
|
|
const totalResult = await query(
|
|
`SELECT COUNT(*)::int AS total FROM inventory_items ii ${whereClause}`,
|
|
params,
|
|
)
|
|
|
|
params.push(pageSize)
|
|
params.push(offset)
|
|
const itemsResult = await query(
|
|
`${INVENTORY_ITEM_SELECT}
|
|
${whereClause}
|
|
ORDER BY ii.id DESC
|
|
LIMIT $${params.length - 1} OFFSET $${params.length}`,
|
|
params,
|
|
)
|
|
|
|
return {
|
|
items: itemsResult.rows,
|
|
total: Number(totalResult.rows[0]?.total || 0),
|
|
}
|
|
}
|
|
|
|
export async function createInventoryItems(rows) {
|
|
let created = 0
|
|
|
|
for (const row of rows) {
|
|
const payloadJson = JSON.stringify(row.payload || { code: row.displayValue })
|
|
const displayValue = String(row.displayValue || '').trim()
|
|
const result = await query(
|
|
`
|
|
INSERT INTO inventory_items (
|
|
batch_no,
|
|
sku_code,
|
|
credential_type,
|
|
display_value,
|
|
payload_json,
|
|
source_type,
|
|
status,
|
|
invalid_reason,
|
|
metadata_json,
|
|
created_at,
|
|
updated_at
|
|
) VALUES ($1, $2, $3, $4, $5::jsonb, 'static_import', 'available', '', '{}'::jsonb, $6, $7)
|
|
ON CONFLICT DO NOTHING
|
|
RETURNING id
|
|
`,
|
|
[
|
|
row.batchNo || '',
|
|
row.skuCode,
|
|
row.credentialType || 'tencent_code',
|
|
displayValue,
|
|
payloadJson,
|
|
row.createdAt,
|
|
row.updatedAt,
|
|
],
|
|
)
|
|
|
|
if (result.rows[0]?.id) {
|
|
created += 1
|
|
}
|
|
}
|
|
|
|
return created
|
|
}
|
|
|
|
export async function releaseReservedInventoryItem(inventoryItemId, updatedAt) {
|
|
return withTransaction(async (client) => {
|
|
await client.query(
|
|
`
|
|
UPDATE task_inventory_bindings
|
|
SET binding_status = 'released', released_at = $1, updated_at = $1
|
|
WHERE inventory_item_id = $2 AND binding_status = 'reserved'
|
|
`,
|
|
[updatedAt, Number(inventoryItemId)],
|
|
)
|
|
|
|
await client.query(
|
|
`
|
|
UPDATE inventory_items
|
|
SET status = 'available', updated_at = $1
|
|
WHERE id = $2 AND status = 'reserved'
|
|
`,
|
|
[updatedAt, Number(inventoryItemId)],
|
|
)
|
|
|
|
return getInventoryItemById(inventoryItemId)
|
|
})
|
|
}
|
|
|
|
export async function invalidateInventoryItem(inventoryItemId, invalidReason, updatedAt) {
|
|
const result = await query(
|
|
`
|
|
UPDATE inventory_items
|
|
SET status = 'invalid', invalid_reason = $1, updated_at = $2
|
|
WHERE id = $3 AND status = 'available'
|
|
RETURNING id
|
|
`,
|
|
[invalidReason, updatedAt, Number(inventoryItemId)],
|
|
)
|
|
|
|
if (!result.rows[0]) {
|
|
return getInventoryItemById(inventoryItemId)
|
|
}
|
|
|
|
return getInventoryItemById(inventoryItemId)
|
|
}
|
|
|
|
export async function invalidateReservedInventoryItem(inventoryItemId, invalidReason, updatedAt) {
|
|
return withTransaction(async (client) => {
|
|
await client.query(
|
|
`
|
|
UPDATE task_inventory_bindings
|
|
SET binding_status = 'released', released_at = $1, updated_at = $1
|
|
WHERE inventory_item_id = $2 AND binding_status = 'reserved'
|
|
`,
|
|
[updatedAt, Number(inventoryItemId)],
|
|
)
|
|
|
|
await client.query(
|
|
`
|
|
UPDATE inventory_items
|
|
SET status = 'invalid', invalid_reason = $1, updated_at = $2
|
|
WHERE id = $3
|
|
`,
|
|
[String(invalidReason || '').trim(), updatedAt, Number(inventoryItemId)],
|
|
)
|
|
|
|
return getInventoryItemById(inventoryItemId)
|
|
})
|
|
}
|