删除库存和旧回调管理

This commit is contained in:
yml
2026-05-25 23:07:19 +08:00
parent 90ad866ecc
commit 2c7464b1eb
87 changed files with 94 additions and 5794 deletions
@@ -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
}
+18 -44
View File
@@ -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
}