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) }) }