删除库存和旧回调管理
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { query, withTransaction } from '../db/client.js'
|
||||
import { query } from '../db/client.js'
|
||||
|
||||
type AdminUserRow = {
|
||||
id: number
|
||||
@@ -8,7 +8,6 @@ type AdminUserRow = {
|
||||
status: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
inventory_group_codes: string[]
|
||||
}
|
||||
|
||||
type AdminUserCreateInput = {
|
||||
@@ -39,15 +38,8 @@ type AdminUserListResult = {
|
||||
}
|
||||
|
||||
const ADMIN_USER_SELECT = `
|
||||
SELECT
|
||||
au.*,
|
||||
COALESCE(bindings.inventory_group_codes, ARRAY[]::text[]) AS inventory_group_codes
|
||||
SELECT au.*
|
||||
FROM admin_users au
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT ARRAY_AGG(auigb.inventory_group_code ORDER BY auigb.inventory_group_code) AS inventory_group_codes
|
||||
FROM admin_user_inventory_group_bindings auigb
|
||||
WHERE auigb.admin_user_id = au.id
|
||||
) bindings ON TRUE
|
||||
`
|
||||
|
||||
export async function getAdminUserById(userId: number | string): Promise<AdminUserRow | null> {
|
||||
@@ -183,39 +175,3 @@ export async function countActiveAdminUsers(): Promise<number> {
|
||||
)
|
||||
return Number(result.rows[0]?.total || 0)
|
||||
}
|
||||
|
||||
export async function replaceAdminUserInventoryGroupBindings(
|
||||
userId: number | string,
|
||||
inventoryGroupCodes: unknown[] = [],
|
||||
timestamp: string,
|
||||
): Promise<AdminUserRow | null> {
|
||||
const normalizedUserId = Number(userId)
|
||||
const normalizedCodes = Array.from(new Set((Array.isArray(inventoryGroupCodes) ? inventoryGroupCodes : [])
|
||||
.map((value) => String(value || '').trim())
|
||||
.filter(Boolean)))
|
||||
|
||||
await withTransaction(async (client) => {
|
||||
await client.query(
|
||||
'DELETE FROM admin_user_inventory_group_bindings WHERE admin_user_id = $1',
|
||||
[normalizedUserId],
|
||||
)
|
||||
|
||||
for (const inventoryGroupCode of normalizedCodes) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO admin_user_inventory_group_bindings (
|
||||
admin_user_id,
|
||||
inventory_group_code,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (admin_user_id, inventory_group_code) DO UPDATE
|
||||
SET updated_at = EXCLUDED.updated_at
|
||||
`,
|
||||
[normalizedUserId, inventoryGroupCode, timestamp, timestamp],
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return getAdminUserById(normalizedUserId)
|
||||
}
|
||||
|
||||
@@ -1,497 +0,0 @@
|
||||
import { query, withTransaction } from '../db/client.js'
|
||||
import type {
|
||||
InventoryCreateItemInput,
|
||||
InventoryListQueryInput,
|
||||
InventorySkuSuggestionQueryInput,
|
||||
} from '../types/repository-inputs.js'
|
||||
import type {
|
||||
InventoryItemRow,
|
||||
InventoryListQueryResult,
|
||||
InventorySkuSuggestionRow,
|
||||
} from '../types/repository-rows.js'
|
||||
|
||||
type InventoryGroupCodesInput = string | string[] | null | undefined
|
||||
|
||||
type InventoryGroupListInput = {
|
||||
keyword?: string
|
||||
limit?: number | string
|
||||
}
|
||||
|
||||
const INVENTORY_ITEM_SELECT = `
|
||||
SELECT
|
||||
ii.id,
|
||||
ii.sku_code,
|
||||
ii.batch_no,
|
||||
ii.credential_type,
|
||||
ii.inventory_group_code,
|
||||
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: string,
|
||||
credentialType = 'claim_code',
|
||||
inventoryGroupCodes: InventoryGroupCodesInput = null,
|
||||
): Promise<InventoryItemRow | null> {
|
||||
const normalizedInventoryGroupCodes = normalizeInventoryGroupCodes(inventoryGroupCodes)
|
||||
if (normalizedInventoryGroupCodes && normalizedInventoryGroupCodes.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const params: unknown[] = [skuCode, credentialType]
|
||||
const filters = [
|
||||
'ii.sku_code = $1',
|
||||
'ii.credential_type = $2',
|
||||
`ii.status = 'available'`,
|
||||
]
|
||||
|
||||
if (normalizedInventoryGroupCodes) {
|
||||
params.push(normalizedInventoryGroupCodes)
|
||||
filters.push(`ii.inventory_group_code = ANY($${params.length}::text[])`)
|
||||
}
|
||||
|
||||
const result = await query<InventoryItemRow>(
|
||||
`${INVENTORY_ITEM_SELECT}
|
||||
WHERE ${filters.join(' AND ')}
|
||||
ORDER BY ii.id ASC
|
||||
LIMIT 1`,
|
||||
params,
|
||||
)
|
||||
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function assignReservedInventoryItem(
|
||||
inventoryItemId: number | string,
|
||||
taskId: number | string,
|
||||
updatedAt: string,
|
||||
roleKey = 'primary_code',
|
||||
): Promise<InventoryItemRow | null> {
|
||||
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: number | string): Promise<InventoryItemRow | null> {
|
||||
const result = await query<InventoryItemRow>(
|
||||
`${INVENTORY_ITEM_SELECT}
|
||||
WHERE ii.id = $1
|
||||
LIMIT 1`,
|
||||
[Number(inventoryItemId)],
|
||||
)
|
||||
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function markInventoryItemDelivered(
|
||||
inventoryItemId: number | string,
|
||||
deliveredAt: string,
|
||||
): Promise<InventoryItemRow | null> {
|
||||
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: number | string,
|
||||
reason: string,
|
||||
consumedAt: string,
|
||||
): Promise<InventoryItemRow | null> {
|
||||
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 = '',
|
||||
inventoryGroupCode = '',
|
||||
allowedInventoryGroupCodes = null,
|
||||
}: InventoryListQueryInput = {}): Promise<InventoryListQueryResult> {
|
||||
const normalizedAllowedInventoryGroupCodes = normalizeInventoryGroupCodes(allowedInventoryGroupCodes)
|
||||
if (normalizedAllowedInventoryGroupCodes && normalizedAllowedInventoryGroupCodes.length === 0) {
|
||||
return {
|
||||
items: [],
|
||||
total: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const offset = (page - 1) * pageSize
|
||||
const filters: string[] = []
|
||||
const params: unknown[] = []
|
||||
|
||||
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}`)
|
||||
}
|
||||
|
||||
if (inventoryGroupCode) {
|
||||
params.push(inventoryGroupCode)
|
||||
filters.push(`ii.inventory_group_code = $${params.length}`)
|
||||
}
|
||||
|
||||
if (normalizedAllowedInventoryGroupCodes) {
|
||||
params.push(normalizedAllowedInventoryGroupCodes)
|
||||
filters.push(`ii.inventory_group_code = ANY($${params.length}::text[])`)
|
||||
}
|
||||
|
||||
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
|
||||
const totalResult = await query<{ [column: string]: unknown, total: number }>(
|
||||
`SELECT COUNT(*)::int AS total FROM inventory_items ii ${whereClause}`,
|
||||
params,
|
||||
)
|
||||
|
||||
params.push(pageSize)
|
||||
params.push(offset)
|
||||
const itemsResult = await query<InventoryItemRow>(
|
||||
`${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 listInventorySkuSuggestions({
|
||||
credentialType = '',
|
||||
keyword = '',
|
||||
limit = 50,
|
||||
inventoryGroupCode = '',
|
||||
allowedInventoryGroupCodes = null,
|
||||
}: InventorySkuSuggestionQueryInput = {}): Promise<InventorySkuSuggestionRow[]> {
|
||||
const normalizedAllowedInventoryGroupCodes = normalizeInventoryGroupCodes(allowedInventoryGroupCodes)
|
||||
if (normalizedAllowedInventoryGroupCodes && normalizedAllowedInventoryGroupCodes.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const filters: string[] = []
|
||||
const params: unknown[] = []
|
||||
|
||||
if (credentialType) {
|
||||
params.push(credentialType)
|
||||
filters.push(`credential_type = $${params.length}`)
|
||||
}
|
||||
|
||||
if (keyword) {
|
||||
params.push(`%${keyword}%`)
|
||||
filters.push(`sku_code ILIKE $${params.length}`)
|
||||
}
|
||||
|
||||
if (inventoryGroupCode) {
|
||||
params.push(inventoryGroupCode)
|
||||
filters.push(`inventory_group_code = $${params.length}`)
|
||||
}
|
||||
|
||||
if (normalizedAllowedInventoryGroupCodes) {
|
||||
params.push(normalizedAllowedInventoryGroupCodes)
|
||||
filters.push(`inventory_group_code = ANY($${params.length}::text[])`)
|
||||
}
|
||||
|
||||
const normalizedLimit = Number.isFinite(Number(limit))
|
||||
? Math.max(1, Math.min(Number(limit), 100))
|
||||
: 50
|
||||
|
||||
params.push(normalizedLimit)
|
||||
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
|
||||
const result = await query<InventorySkuSuggestionRow>(
|
||||
`
|
||||
SELECT
|
||||
sku_code,
|
||||
credential_type,
|
||||
inventory_group_code,
|
||||
COUNT(*)::int AS total_count,
|
||||
COUNT(*) FILTER (WHERE status = 'available')::int AS available_count,
|
||||
MAX(updated_at) AS latest_updated_at
|
||||
FROM inventory_items
|
||||
${whereClause}
|
||||
GROUP BY sku_code, credential_type, inventory_group_code
|
||||
ORDER BY
|
||||
COUNT(*) FILTER (WHERE status = 'available') DESC,
|
||||
COUNT(*) DESC,
|
||||
MAX(updated_at) DESC,
|
||||
sku_code ASC,
|
||||
inventory_group_code ASC
|
||||
LIMIT $${params.length}
|
||||
`,
|
||||
params,
|
||||
)
|
||||
|
||||
return result.rows
|
||||
}
|
||||
|
||||
export async function createInventoryItems(rows: InventoryCreateItemInput[]): Promise<number> {
|
||||
let created = 0
|
||||
|
||||
for (const row of rows) {
|
||||
const payloadJson = JSON.stringify(row.payload || { code: row.displayValue })
|
||||
const displayValue = String(row.displayValue || '').trim()
|
||||
const inventoryGroupCode = String(row.inventoryGroupCode || '').trim()
|
||||
const result = await query<{ [column: string]: unknown, id: number }>(
|
||||
`
|
||||
INSERT INTO inventory_items (
|
||||
batch_no,
|
||||
sku_code,
|
||||
credential_type,
|
||||
inventory_group_code,
|
||||
display_value,
|
||||
payload_json,
|
||||
source_type,
|
||||
status,
|
||||
invalid_reason,
|
||||
metadata_json,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6::jsonb, 'static_import', 'available', '', '{}'::jsonb, $7, $8)
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING id
|
||||
`,
|
||||
[
|
||||
row.batchNo || '',
|
||||
row.skuCode,
|
||||
row.credentialType || 'claim_code',
|
||||
inventoryGroupCode,
|
||||
displayValue,
|
||||
payloadJson,
|
||||
row.createdAt,
|
||||
row.updatedAt,
|
||||
],
|
||||
)
|
||||
|
||||
if (result.rows[0]?.id) {
|
||||
created += 1
|
||||
}
|
||||
}
|
||||
|
||||
return created
|
||||
}
|
||||
|
||||
export async function listInventoryGroupCodes({
|
||||
keyword = '',
|
||||
limit = 100,
|
||||
}: InventoryGroupListInput = {}): Promise<string[]> {
|
||||
const params: unknown[] = []
|
||||
const filters = [`inventory_group_code <> ''`]
|
||||
|
||||
if (keyword) {
|
||||
params.push(`%${String(keyword || '').trim()}%`)
|
||||
filters.push(`inventory_group_code ILIKE $${params.length}`)
|
||||
}
|
||||
|
||||
const normalizedLimit = Number.isFinite(Number(limit))
|
||||
? Math.max(1, Math.min(Number(limit), 200))
|
||||
: 100
|
||||
|
||||
params.push(normalizedLimit)
|
||||
const result = await query<{ [column: string]: unknown, inventory_group_code: string }>(
|
||||
`
|
||||
SELECT inventory_group_code
|
||||
FROM inventory_items
|
||||
WHERE ${filters.join(' AND ')}
|
||||
GROUP BY inventory_group_code
|
||||
ORDER BY inventory_group_code ASC
|
||||
LIMIT $${params.length}
|
||||
`,
|
||||
params,
|
||||
)
|
||||
|
||||
return result.rows.map((row) => String(row.inventory_group_code || '').trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
function normalizeInventoryGroupCodes(inventoryGroupCodes: InventoryGroupCodesInput): string[] | null {
|
||||
if (inventoryGroupCodes == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return Array.from(new Set((Array.isArray(inventoryGroupCodes) ? inventoryGroupCodes : [inventoryGroupCodes])
|
||||
.map((value) => String(value || '').trim())
|
||||
.filter(Boolean)))
|
||||
}
|
||||
|
||||
export async function releaseReservedInventoryItem(
|
||||
inventoryItemId: number | string,
|
||||
updatedAt: string,
|
||||
): Promise<InventoryItemRow | null> {
|
||||
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: number | string,
|
||||
invalidReason: string,
|
||||
updatedAt: string,
|
||||
): Promise<InventoryItemRow | null> {
|
||||
const result = await query<{ [column: string]: unknown, id: number }>(
|
||||
`
|
||||
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: number | string,
|
||||
invalidReason: string,
|
||||
updatedAt: string,
|
||||
): Promise<InventoryItemRow | null> {
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict'
|
||||
|
||||
import { resolveOrderItemSyncPlan } from './order-item-repo.js'
|
||||
|
||||
test('resolveOrderItemSyncPlan preserves matching order item ids across repeated webhook syncs', () => {
|
||||
test('resolveOrderItemSyncPlan preserves matching order item ids across repeated source syncs', () => {
|
||||
const existingItems = [
|
||||
{
|
||||
id: 15,
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { query } from '../db/client.js'
|
||||
import type {
|
||||
TaskInventoryBindingRow,
|
||||
TaskInventoryBindingSummaryRow,
|
||||
} from '../types/repository-rows.js'
|
||||
|
||||
export async function listTaskInventoryBindingsByTaskId(
|
||||
taskId: number | string,
|
||||
): Promise<TaskInventoryBindingRow[]> {
|
||||
const result = await query<TaskInventoryBindingRow>(
|
||||
`
|
||||
SELECT
|
||||
tib.id,
|
||||
tib.task_id,
|
||||
tib.inventory_item_id,
|
||||
tib.role_key,
|
||||
tib.quantity,
|
||||
tib.binding_status,
|
||||
tib.consumed_at,
|
||||
tib.released_at,
|
||||
tib.metadata_json,
|
||||
tib.created_at,
|
||||
tib.updated_at,
|
||||
ii.sku_code,
|
||||
ii.batch_no,
|
||||
ii.credential_type,
|
||||
ii.inventory_group_code,
|
||||
ii.display_value,
|
||||
ii.status AS inventory_item_status,
|
||||
ii.invalid_reason
|
||||
FROM task_inventory_bindings tib
|
||||
JOIN inventory_items ii ON ii.id = tib.inventory_item_id
|
||||
WHERE tib.task_id = $1
|
||||
ORDER BY tib.id ASC
|
||||
`,
|
||||
[Number(taskId)],
|
||||
)
|
||||
|
||||
return result.rows
|
||||
}
|
||||
|
||||
export async function getTaskInventoryBindingById(
|
||||
bindingId: number | string,
|
||||
): Promise<TaskInventoryBindingRow | null> {
|
||||
const result = await query<TaskInventoryBindingRow>(
|
||||
`
|
||||
SELECT
|
||||
tib.id,
|
||||
tib.task_id,
|
||||
tib.inventory_item_id,
|
||||
tib.role_key,
|
||||
tib.quantity,
|
||||
tib.binding_status,
|
||||
tib.consumed_at,
|
||||
tib.released_at,
|
||||
tib.metadata_json,
|
||||
tib.created_at,
|
||||
tib.updated_at,
|
||||
ii.sku_code,
|
||||
ii.batch_no,
|
||||
ii.credential_type,
|
||||
ii.inventory_group_code,
|
||||
ii.display_value,
|
||||
ii.status AS inventory_item_status,
|
||||
ii.invalid_reason
|
||||
FROM task_inventory_bindings tib
|
||||
JOIN inventory_items ii ON ii.id = tib.inventory_item_id
|
||||
WHERE tib.id = $1
|
||||
LIMIT 1
|
||||
`,
|
||||
[Number(bindingId)],
|
||||
)
|
||||
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function listTaskInventoryBindingSummariesByTaskIds(
|
||||
taskIds: unknown[] = [],
|
||||
): Promise<TaskInventoryBindingSummaryRow[]> {
|
||||
const normalizedTaskIds = Array.from(new Set((Array.isArray(taskIds) ? taskIds : [])
|
||||
.map((value) => Number(value))
|
||||
.filter((value) => Number.isFinite(value) && value > 0)))
|
||||
|
||||
if (normalizedTaskIds.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const result = await query<TaskInventoryBindingSummaryRow>(
|
||||
`
|
||||
SELECT
|
||||
tib.task_id,
|
||||
COUNT(*)::int AS total_binding_count,
|
||||
COUNT(*) FILTER (WHERE tib.binding_status = 'reserved')::int AS reserved_binding_count,
|
||||
COUNT(*) FILTER (WHERE tib.binding_status = 'consumed')::int AS consumed_binding_count,
|
||||
COUNT(*) FILTER (WHERE tib.binding_status = 'released')::int AS released_binding_count,
|
||||
ARRAY_AGG(DISTINCT tib.role_key ORDER BY tib.role_key) AS role_keys
|
||||
FROM task_inventory_bindings tib
|
||||
WHERE tib.task_id = ANY($1::bigint[])
|
||||
GROUP BY tib.task_id
|
||||
`,
|
||||
[normalizedTaskIds],
|
||||
)
|
||||
|
||||
return result.rows
|
||||
}
|
||||
@@ -42,35 +42,13 @@ const TASK_FIELDS = `
|
||||
ctx.partition_name,
|
||||
ctx.screenshot_path,
|
||||
ctx.artifacts_json,
|
||||
ctx.state_json,
|
||||
inv.inventory_item_id AS primary_inventory_item_id,
|
||||
inv.display_value AS primary_inventory_display_value,
|
||||
inv.credential_type AS primary_inventory_credential_type,
|
||||
inv.binding_status AS primary_inventory_binding_status
|
||||
ctx.state_json
|
||||
`
|
||||
|
||||
const TASK_JOINS = `
|
||||
FROM fulfillment_tasks ft
|
||||
LEFT JOIN claim_tokens ct ON ct.task_id = ft.id
|
||||
LEFT JOIN task_runtime_contexts ctx ON ctx.task_id = ft.id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
tib.inventory_item_id,
|
||||
tib.binding_status,
|
||||
ii.display_value,
|
||||
ii.credential_type
|
||||
FROM task_inventory_bindings tib
|
||||
JOIN inventory_items ii ON ii.id = tib.inventory_item_id
|
||||
WHERE tib.task_id = ft.id AND tib.binding_status IN ('reserved', 'consumed')
|
||||
ORDER BY
|
||||
CASE tib.binding_status
|
||||
WHEN 'reserved' THEN 0
|
||||
WHEN 'consumed' THEN 1
|
||||
ELSE 2
|
||||
END ASC,
|
||||
tib.id DESC
|
||||
LIMIT 1
|
||||
) inv ON TRUE
|
||||
`
|
||||
|
||||
function buildTaskSelect(extraFields = ''): string {
|
||||
@@ -106,7 +84,6 @@ export async function createTask(input: TaskCreateInput): Promise<TaskRow | null
|
||||
profile_id,
|
||||
executor_key,
|
||||
task_status,
|
||||
inventory_status,
|
||||
delivery_status,
|
||||
result_code,
|
||||
result_message,
|
||||
@@ -123,7 +100,7 @@ export async function createTask(input: TaskCreateInput): Promise<TaskRow | null
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
|
||||
$11, $12, $13, $14, $15, $16, $17, $18, $19,
|
||||
$20, $21, $22, $23, $24::jsonb, $25, $26
|
||||
$20, $21, $22, $23::jsonb, $24, $25
|
||||
)
|
||||
RETURNING id
|
||||
`,
|
||||
@@ -140,7 +117,6 @@ export async function createTask(input: TaskCreateInput): Promise<TaskRow | null
|
||||
input.profileId,
|
||||
input.executorKey,
|
||||
input.taskStatus || 'pending_payment',
|
||||
input.inventoryStatus || 'pending',
|
||||
input.deliveryStatus || 'pending',
|
||||
input.resultCode || '',
|
||||
input.resultMessage || '',
|
||||
@@ -180,27 +156,25 @@ export async function updateTask(taskId: number | string, patch: TaskUpdatePatch
|
||||
UPDATE fulfillment_tasks
|
||||
SET
|
||||
task_status = $1,
|
||||
inventory_status = $2,
|
||||
delivery_status = $3,
|
||||
result_code = $4,
|
||||
result_message = $5,
|
||||
claim_token = $6,
|
||||
claim_expires_at = $7,
|
||||
automation_mode = $8,
|
||||
requires_claim = $9,
|
||||
user_action_status = $10,
|
||||
attempt_count = $11,
|
||||
last_error = $12,
|
||||
context_json = $13::jsonb,
|
||||
claimed_at = $14,
|
||||
role_confirmed_at = $15,
|
||||
redeemed_at = $16,
|
||||
updated_at = $17
|
||||
WHERE id = $18
|
||||
delivery_status = $2,
|
||||
result_code = $3,
|
||||
result_message = $4,
|
||||
claim_token = $5,
|
||||
claim_expires_at = $6,
|
||||
automation_mode = $7,
|
||||
requires_claim = $8,
|
||||
user_action_status = $9,
|
||||
attempt_count = $10,
|
||||
last_error = $11,
|
||||
context_json = $12::jsonb,
|
||||
claimed_at = $13,
|
||||
role_confirmed_at = $14,
|
||||
redeemed_at = $15,
|
||||
updated_at = $16
|
||||
WHERE id = $17
|
||||
`,
|
||||
[
|
||||
next.task_status,
|
||||
next.inventory_status,
|
||||
next.delivery_status,
|
||||
next.result_code,
|
||||
next.result_message,
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
import { query } from '../db/client.js'
|
||||
import type {
|
||||
WebhookEventCreateInput,
|
||||
WebhookEventListQueryInput,
|
||||
WebhookEventUpdatePatch,
|
||||
} from '../types/repository-inputs.js'
|
||||
import type {
|
||||
WebhookEventListQueryResult,
|
||||
WebhookEventRow,
|
||||
} from '../types/repository-rows.js'
|
||||
|
||||
export async function createWebhookEvent(input: WebhookEventCreateInput): Promise<WebhookEventRow | null> {
|
||||
const result = await query<WebhookEventRow>(
|
||||
`
|
||||
INSERT INTO webhook_events (
|
||||
provider,
|
||||
platform,
|
||||
shop_id,
|
||||
shop_name,
|
||||
event_type,
|
||||
event_key,
|
||||
signature_valid,
|
||||
headers_json,
|
||||
query_json,
|
||||
body_json,
|
||||
processed,
|
||||
process_error,
|
||||
related_order_id,
|
||||
created_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9::jsonb, $10::jsonb, $11, $12, $13, $14)
|
||||
RETURNING *
|
||||
`,
|
||||
[
|
||||
input.provider,
|
||||
input.platform,
|
||||
input.shopId || '',
|
||||
input.shopName || '',
|
||||
input.eventType,
|
||||
input.eventKey,
|
||||
Boolean(input.signatureValid),
|
||||
input.headersJson || '{}',
|
||||
input.queryJson || '{}',
|
||||
input.bodyJson || '{}',
|
||||
Boolean(input.processed),
|
||||
input.processError || '',
|
||||
input.relatedOrderId || null,
|
||||
input.createdAt,
|
||||
],
|
||||
)
|
||||
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function updateWebhookEvent(
|
||||
eventId: number | string,
|
||||
patch: WebhookEventUpdatePatch,
|
||||
): Promise<WebhookEventRow | null> {
|
||||
const current = await getWebhookEventById(eventId)
|
||||
if (!current) {
|
||||
return null
|
||||
}
|
||||
|
||||
const next = { ...current, ...patch }
|
||||
const result = await query<WebhookEventRow>(
|
||||
`
|
||||
UPDATE webhook_events
|
||||
SET
|
||||
processed = $1,
|
||||
process_error = $2,
|
||||
related_order_id = $3
|
||||
WHERE id = $4
|
||||
RETURNING *
|
||||
`,
|
||||
[Boolean(next.processed), next.process_error || '', next.related_order_id || null, Number(eventId)],
|
||||
)
|
||||
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function getWebhookEventById(eventId: number | string): Promise<WebhookEventRow | null> {
|
||||
const result = await query<WebhookEventRow>('SELECT * FROM webhook_events WHERE id = $1 LIMIT 1', [
|
||||
Number(eventId),
|
||||
])
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function listWebhookEvents({
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
provider = '',
|
||||
platform = '',
|
||||
platformOrderId = '',
|
||||
processed = '',
|
||||
visibility = 'important',
|
||||
relatedOrderId = '',
|
||||
dateFrom = '',
|
||||
dateTo = '',
|
||||
}: WebhookEventListQueryInput = {}): Promise<WebhookEventListQueryResult> {
|
||||
const offset = (page - 1) * pageSize
|
||||
const filters: string[] = []
|
||||
const params: unknown[] = []
|
||||
|
||||
if (provider) {
|
||||
params.push(provider)
|
||||
filters.push(`provider = $${params.length}`)
|
||||
}
|
||||
|
||||
if (platform) {
|
||||
params.push(platform)
|
||||
filters.push(`platform = $${params.length}`)
|
||||
}
|
||||
|
||||
if (platformOrderId) {
|
||||
params.push(`%${platformOrderId}%`)
|
||||
filters.push(`(event_key ILIKE $${params.length} OR body_json::text ILIKE $${params.length})`)
|
||||
}
|
||||
|
||||
if (processed === '0' || processed === '1') {
|
||||
params.push(processed === '1')
|
||||
filters.push(`processed = $${params.length}`)
|
||||
}
|
||||
|
||||
if (visibility === 'ignored') {
|
||||
filters.push(`process_error LIKE 'ignored_%'`)
|
||||
} else if (visibility === 'important') {
|
||||
filters.push(`(process_error = '' OR process_error NOT LIKE 'ignored_%')`)
|
||||
}
|
||||
|
||||
if (relatedOrderId) {
|
||||
params.push(Number(relatedOrderId))
|
||||
filters.push(`related_order_id = $${params.length}`)
|
||||
}
|
||||
|
||||
if (dateFrom) {
|
||||
params.push(dateFrom)
|
||||
filters.push(`created_at >= $${params.length}`)
|
||||
}
|
||||
|
||||
if (dateTo) {
|
||||
params.push(dateTo)
|
||||
filters.push(`created_at <= $${params.length}`)
|
||||
}
|
||||
|
||||
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
|
||||
const totalResult = await query<{ [column: string]: unknown, total: number }>(
|
||||
`SELECT COUNT(*)::int AS total FROM webhook_events ${whereClause}`,
|
||||
params,
|
||||
)
|
||||
|
||||
params.push(pageSize)
|
||||
params.push(offset)
|
||||
const itemsResult = await query<WebhookEventRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM webhook_events
|
||||
${whereClause}
|
||||
ORDER BY id DESC
|
||||
LIMIT $${params.length - 1} OFFSET $${params.length}
|
||||
`,
|
||||
params,
|
||||
)
|
||||
|
||||
return {
|
||||
items: itemsResult.rows,
|
||||
total: Number(totalResult.rows[0]?.total || 0),
|
||||
}
|
||||
}
|
||||
|
||||
export async function listWebhookEventsByOrderId(orderId: number | string): Promise<WebhookEventRow[]> {
|
||||
const result = await query<WebhookEventRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM webhook_events
|
||||
WHERE related_order_id = $1
|
||||
ORDER BY id DESC
|
||||
`,
|
||||
[Number(orderId)],
|
||||
)
|
||||
|
||||
return result.rows
|
||||
}
|
||||
Reference in New Issue
Block a user