增加分组库存与客服

This commit is contained in:
yml2213
2026-04-23 13:38:21 +08:00
parent 2a05a68959
commit f0b31121d7
31 changed files with 675 additions and 80 deletions
@@ -0,0 +1,17 @@
ALTER TABLE inventory_items
ADD COLUMN IF NOT EXISTS inventory_group_code TEXT NOT NULL DEFAULT '';
CREATE INDEX IF NOT EXISTS idx_inventory_items_group_lookup
ON inventory_items(sku_code, credential_type, inventory_group_code, status, id);
CREATE TABLE IF NOT EXISTS admin_user_inventory_group_bindings (
id BIGSERIAL PRIMARY KEY,
admin_user_id BIGINT NOT NULL REFERENCES admin_users(id) ON DELETE CASCADE,
inventory_group_code TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
UNIQUE(admin_user_id, inventory_group_code)
);
CREATE INDEX IF NOT EXISTS idx_admin_user_inventory_group_bindings_user_id
ON admin_user_inventory_group_bindings(admin_user_id, inventory_group_code);
@@ -1,8 +1,20 @@
import { query } from '../db/client.js'
import { query, withTransaction } from '../db/client.js'
const ADMIN_USER_SELECT = `
SELECT
au.*,
COALESCE(bindings.inventory_group_codes, ARRAY[]::text[]) AS inventory_group_codes
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) {
const result = await query(
'SELECT * FROM admin_users WHERE id = $1 LIMIT 1',
`${ADMIN_USER_SELECT} WHERE au.id = $1 LIMIT 1`,
[Number(userId)],
)
return result.rows[0] || null
@@ -10,7 +22,7 @@ export async function getAdminUserById(userId) {
export async function getAdminUserByUsername(username) {
const result = await query(
'SELECT * FROM admin_users WHERE username = $1 LIMIT 1',
`${ADMIN_USER_SELECT} WHERE au.username = $1 LIMIT 1`,
[String(username || '').trim().toLowerCase()],
)
return result.rows[0] || null
@@ -27,7 +39,7 @@ export async function createAdminUser(input) {
created_at,
updated_at
) VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *
RETURNING id
`,
[
input.username,
@@ -39,7 +51,7 @@ export async function createAdminUser(input) {
],
)
return result.rows[0] || null
return getAdminUserById(result.rows[0]?.id || 0)
}
export async function updateAdminUser(userId, patch) {
@@ -59,7 +71,7 @@ export async function updateAdminUser(userId, patch) {
status = $4,
updated_at = $5
WHERE id = $6
RETURNING *
RETURNING id
`,
[
next.username,
@@ -71,7 +83,7 @@ export async function updateAdminUser(userId, patch) {
],
)
return result.rows[0] || null
return getAdminUserById(result.rows[0]?.id || 0)
}
export async function listAdminUsers({ page = 1, pageSize = 20, username = '', role = '', status = '' } = {}) {
@@ -101,10 +113,9 @@ export async function listAdminUsers({ page = 1, pageSize = 20, username = '', r
params.push(offset)
const itemsResult = await query(
`
SELECT *
FROM admin_users
${ADMIN_USER_SELECT}
${whereClause}
ORDER BY id DESC
ORDER BY au.id DESC
LIMIT $${params.length - 1} OFFSET $${params.length}
`,
params,
@@ -122,3 +133,35 @@ export async function countActiveAdminUsers() {
)
return Number(result.rows[0]?.total || 0)
}
export async function replaceAdminUserInventoryGroupBindings(userId, inventoryGroupCodes = [], timestamp) {
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)
}
+112 -6
View File
@@ -15,6 +15,7 @@ const INVENTORY_ITEM_SELECT = `
ii.sku_code,
ii.batch_no,
ii.credential_type,
ii.inventory_group_code,
ii.display_value,
ii.status,
ii.invalid_reason,
@@ -36,13 +37,34 @@ const INVENTORY_ITEM_SELECT = `
`
/** @returns {Promise<InventoryItemRow | null>} */
export async function findFirstAvailableInventoryItemBySkuCode(skuCode, credentialType = 'tencent_code') {
export async function findFirstAvailableInventoryItemBySkuCode(
skuCode,
credentialType = 'tencent_code',
inventoryGroupCodes = null,
) {
const normalizedInventoryGroupCodes = normalizeInventoryGroupCodes(inventoryGroupCodes)
if (normalizedInventoryGroupCodes && normalizedInventoryGroupCodes.length === 0) {
return null
}
const params = [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(
`${INVENTORY_ITEM_SELECT}
WHERE ii.sku_code = $1 AND ii.credential_type = $2 AND ii.status = 'available'
WHERE ${filters.join(' AND ')}
ORDER BY ii.id ASC
LIMIT 1`,
[skuCode, credentialType],
params,
)
return /** @type {InventoryItemRow | null} */ (result.rows[0] || null)
@@ -162,7 +184,17 @@ export async function listInventoryItems({
credentialType = '',
status = '',
batchNo = '',
inventoryGroupCode = '',
allowedInventoryGroupCodes = null,
} = /** @type {InventoryListQueryInput} */ ({})) {
const normalizedAllowedInventoryGroupCodes = normalizeInventoryGroupCodes(allowedInventoryGroupCodes)
if (normalizedAllowedInventoryGroupCodes && normalizedAllowedInventoryGroupCodes.length === 0) {
return {
items: [],
total: 0,
}
}
const offset = (page - 1) * pageSize
const filters = []
const params = []
@@ -187,6 +219,16 @@ export async function listInventoryItems({
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(
`SELECT COUNT(*)::int AS total FROM inventory_items ii ${whereClause}`,
@@ -215,7 +257,14 @@ export async function listInventorySkuSuggestions({
credentialType = '',
keyword = '',
limit = 50,
inventoryGroupCode = '',
allowedInventoryGroupCodes = null,
} = /** @type {InventorySkuSuggestionQueryInput} */ ({})) {
const normalizedAllowedInventoryGroupCodes = normalizeInventoryGroupCodes(allowedInventoryGroupCodes)
if (normalizedAllowedInventoryGroupCodes && normalizedAllowedInventoryGroupCodes.length === 0) {
return []
}
const filters = []
const params = []
@@ -229,6 +278,16 @@ export async function listInventorySkuSuggestions({
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
@@ -240,17 +299,19 @@ export async function listInventorySkuSuggestions({
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
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
sku_code ASC,
inventory_group_code ASC
LIMIT $${params.length}
`,
params,
@@ -266,12 +327,14 @@ export async function createInventoryItems(rows) {
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(
`
INSERT INTO inventory_items (
batch_no,
sku_code,
credential_type,
inventory_group_code,
display_value,
payload_json,
source_type,
@@ -280,7 +343,7 @@ export async function createInventoryItems(rows) {
metadata_json,
created_at,
updated_at
) VALUES ($1, $2, $3, $4, $5::jsonb, 'static_import', 'available', '', '{}'::jsonb, $6, $7)
) VALUES ($1, $2, $3, $4, $5, $6::jsonb, 'static_import', 'available', '', '{}'::jsonb, $7, $8)
ON CONFLICT DO NOTHING
RETURNING id
`,
@@ -288,6 +351,7 @@ export async function createInventoryItems(rows) {
row.batchNo || '',
row.skuCode,
row.credentialType || 'tencent_code',
inventoryGroupCode,
displayValue,
payloadJson,
row.createdAt,
@@ -303,6 +367,48 @@ export async function createInventoryItems(rows) {
return created
}
export async function listInventoryGroupCodes({
keyword = '',
limit = 100,
} = {}) {
const params = []
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(
`
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) {
if (inventoryGroupCodes == null) {
return null
}
return Array.from(new Set((Array.isArray(inventoryGroupCodes) ? inventoryGroupCodes : [inventoryGroupCodes])
.map((value) => String(value || '').trim())
.filter(Boolean)))
}
/** @returns {Promise<InventoryItemRow | null>} */
export async function releaseReservedInventoryItem(inventoryItemId, updatedAt) {
return withTransaction(async (client) => {
@@ -24,6 +24,7 @@ export async function listTaskInventoryBindingsByTaskId(taskId) {
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
@@ -57,6 +58,7 @@ export async function getTaskInventoryBindingById(bindingId) {
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
+8 -2
View File
@@ -27,7 +27,10 @@ const router = Router()
router.use('/inventory', requireAdminRoles(['admin', 'operator']))
router.get('/inventory', createJsonHandler(
(req) => getAdminInventoryItems(/** @type {AdminInventoryRouteQuery} */ (req.query)),
(req) => getAdminInventoryItems(
/** @type {AdminInventoryRouteQuery} */ (req.query),
req.adminSession || null,
),
{
successMessage: 'ok',
errorMessage: '读取库存列表失败',
@@ -36,7 +39,10 @@ router.get('/inventory', createJsonHandler(
))
router.get('/inventory/sku-suggestions', createJsonHandler(
(req) => getAdminInventorySkuSuggestions(/** @type {AdminInventorySkuSuggestionRouteQuery} */ (req.query)),
(req) => getAdminInventorySkuSuggestions(
/** @type {AdminInventorySkuSuggestionRouteQuery} */ (req.query),
req.adminSession || null,
),
{
successMessage: 'ok',
errorMessage: '读取内部 SKU 建议失败',
+25 -6
View File
@@ -25,7 +25,10 @@ const router = Router()
router.use('/manual-redeem', requireAdminRoles(['admin', 'operator', 'support']))
router.get('/manual-redeem/sku-suggestions', createJsonHandler(
(req) => getAdminInventorySkuSuggestions(/** @type {AdminInventorySkuSuggestionRouteQuery} */ (req.query)),
(req) => getAdminInventorySkuSuggestions(
/** @type {AdminInventorySkuSuggestionRouteQuery} */ (req.query),
req.adminSession || null,
),
{
successMessage: 'ok',
errorMessage: '读取人工兑换 SKU 建议失败',
@@ -60,7 +63,10 @@ router.post('/manual-redeem', createJsonHandler(
))
router.get('/manual-redeem/:taskId', createJsonHandler(
(req) => getAdminManualRedeemDetail((/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId),
(req) => getAdminManualRedeemDetail(
(/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId,
req.adminSession || null,
),
{
successMessage: 'ok',
errorMessage: '读取人工兑换详情失败',
@@ -72,6 +78,7 @@ router.post('/manual-redeem/:taskId/session', createJsonHandler(
(req) => createAdminManualRedeemSession(
(/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId,
/** @type {{ loginType?: string, forceRecreate?: boolean }} */ (req.body),
req.adminSession || null,
),
{
successMessage: '人工兑换登录会话已创建',
@@ -81,7 +88,10 @@ router.post('/manual-redeem/:taskId/session', createJsonHandler(
))
router.get('/manual-redeem/:taskId/session/summary', createJsonHandler(
(req) => getAdminManualRedeemSessionSummary((/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId),
(req) => getAdminManualRedeemSessionSummary(
(/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId,
req.adminSession || null,
),
{
successMessage: 'ok',
errorMessage: '读取人工兑换会话摘要失败',
@@ -90,7 +100,10 @@ router.get('/manual-redeem/:taskId/session/summary', createJsonHandler(
))
router.post('/manual-redeem/:taskId/session/refresh', createJsonHandler(
(req) => reloadAdminManualRedeemSession((/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId),
(req) => reloadAdminManualRedeemSession(
(/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId,
req.adminSession || null,
),
{
successMessage: '人工兑换会话已刷新',
errorMessage: '刷新人工兑换会话失败',
@@ -99,7 +112,10 @@ router.post('/manual-redeem/:taskId/session/refresh', createJsonHandler(
))
router.delete('/manual-redeem/:taskId/session', createJsonHandler(
(req) => closeAdminManualRedeemSession((/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId),
(req) => closeAdminManualRedeemSession(
(/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId,
req.adminSession || null,
),
{
successMessage: '人工兑换会话已关闭',
errorMessage: '关闭人工兑换会话失败',
@@ -159,7 +175,10 @@ router.post('/manual-redeem/:taskId/redeem', createJsonHandler(
))
router.post('/manual-redeem/:taskId/close', createJsonHandler(
(req) => closeAdminManualRedeemTask((/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId),
(req) => closeAdminManualRedeemTask(
(/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId,
req.adminSession || null,
),
{
successMessage: '人工兑换任务已关闭',
errorMessage: '关闭人工兑换任务失败',
+19
View File
@@ -4,6 +4,7 @@ import {
createManagedAdminUser,
getAdminUserList,
resetManagedAdminUserPassword,
updateManagedAdminUserInventoryGroups,
updateManagedAdminUserRole,
updateManagedAdminUserStatus,
} from '../../services/admin/admin-auth-service.js'
@@ -77,6 +78,24 @@ router.post('/users/:userId/status', createJsonHandler(
},
))
router.post('/users/:userId/inventory-groups', createJsonHandler(
(req) => updateManagedAdminUserInventoryGroups(req.params.userId, req.body, req.adminSession),
{
successMessage: '用户库存组已更新',
errorMessage: '更新用户库存组失败',
scope: '[admin/users/:userId/inventory-groups]',
audit: (_req, data) => ({
action: 'admin_user_inventory_groups_updated',
targetType: 'admin_user',
targetId: String(data.user.userId),
data: {
username: data.user.username,
inventoryGroupCodes: data.user.inventoryGroupCodes,
},
}),
},
))
router.post('/users/:userId/reset-password', createJsonHandler(
(req) => resetManagedAdminUserPassword(req.params.userId, req.body),
{
@@ -7,6 +7,7 @@ import {
getAdminUserById,
getAdminUserByUsername,
listAdminUsers,
replaceAdminUserInventoryGroupBindings,
updateAdminUser,
} from '../../repositories/admin-user-repo.js'
import { addHours, nowIso } from '../../utils/time.js'
@@ -127,6 +128,7 @@ export async function verifyAdminSessionToken(token) {
userId: Number(user.id),
username: String(user.username || ''),
role: normalizeAdminRole(user.role),
allowedInventoryGroups: normalizeInventoryGroupCodes(user.inventory_group_codes),
expiresAt,
}
}
@@ -141,6 +143,7 @@ export async function getAdminSessionSummary(token) {
userId: session.userId,
username: session.username,
role: session.role,
inventoryGroupCodes: session.allowedInventoryGroups,
},
}
}
@@ -216,6 +219,18 @@ export async function createManagedAdminUser(payload = {}) {
}
}
export async function updateManagedAdminUserInventoryGroups(userId, payload = {}, session) {
const user = await getRequiredAdminUser(userId)
const inventoryGroupCodes = normalizeInventoryGroupCodes(payload.inventoryGroupCodes)
await ensureAdminUserChangeAllowed(user, {}, session)
const updated = await replaceAdminUserInventoryGroupBindings(user.id, inventoryGroupCodes, nowIso())
return {
user: mapAdminUser(updated),
}
}
export async function updateManagedAdminUserRole(userId, payload = {}, session) {
const user = await getRequiredAdminUser(userId)
const role = normalizeAdminRole(payload.role)
@@ -315,6 +330,7 @@ function createAdminSession(user) {
userId: Number(user.id),
username: String(user.username || ''),
role: normalizeAdminRole(user.role),
inventoryGroupCodes: normalizeInventoryGroupCodes(user.inventory_group_codes),
},
}
}
@@ -445,7 +461,14 @@ function mapAdminUser(user) {
username: String(user.username || ''),
role: normalizeAdminRole(user.role),
status: normalizeAdminUserStatus(user.status),
inventoryGroupCodes: normalizeInventoryGroupCodes(user.inventory_group_codes),
createdAt: user.created_at,
updatedAt: user.updated_at,
}
}
function normalizeInventoryGroupCodes(values) {
return Array.from(new Set((Array.isArray(values) ? values : [])
.map((value) => String(value || '').trim())
.filter(Boolean)))
}
@@ -23,6 +23,7 @@ export async function mapAdminInventoryListItem(item) {
skuCode: item.sku_code,
batchNo: item.batch_no,
credentialType: item.credential_type || 'tencent_code',
inventoryGroupCode: String(item.inventory_group_code || '').trim(),
displayValue: item.display_value,
status: item.status,
reservedByTaskId: item.reserved_by_task_id,
@@ -19,7 +19,7 @@ import { createTaskEvent } from '../../repositories/task-event-repo.js'
import { nowIso } from '../../utils/time.js'
import { randomId } from '../../utils/random.js'
import { closeAdminTask, confirmAdminTaskAssistedRole, redeemAdminTaskAssisted } from './admin-write-service.js'
import { isAssistedClaimTask, parseTaskContext } from './admin-read-shared-helpers.js'
import { createAdminViewerContext, isAssistedClaimTask, parseTaskContext } from './admin-read-shared-helpers.js'
const MANUAL_PROVIDER = 'manual'
const MANUAL_PLATFORM = 'manual_redeem'
@@ -36,6 +36,7 @@ export async function createAdminManualRedeemTask(
payload = /** @type {AdminManualRedeemCreateInput} */ ({}),
session = null,
) {
const viewerContext = createAdminViewerContext(session)
const proofValue = normalizeManualProofValue(payload.proofValue)
const skuCode = String(payload.skuCode || '').trim()
const skuName = String(payload.skuName || '').trim() || skuCode
@@ -81,9 +82,20 @@ export async function createAdminManualRedeemTask(
const primaryRequirement = requirements.find((item) => item.is_required !== false) || requirements[0] || null
const credentialType = String(primaryRequirement?.credential_type || primaryRequirement?.credentialType || 'tencent_code').trim() || 'tencent_code'
const roleKey = String(primaryRequirement?.role_key || primaryRequirement?.roleKey || 'primary_code').trim() || 'primary_code'
const availableInventory = await findFirstAvailableInventoryItemBySkuCode(skuCode, credentialType)
const availableInventory = await findFirstAvailableInventoryItemBySkuCode(
skuCode,
credentialType,
viewerContext.allowedInventoryGroupCodes,
)
if (!availableInventory) {
if (viewerContext.restrictInventoryGroups && viewerContext.allowedInventoryGroupCodes?.length === 0) {
throw createHttpError('当前客服未绑定任何库存组,无法创建人工兑换任务', {
statusCode: 403,
errorCode: 'admin_manual_redeem_inventory_group_unbound',
})
}
throw createHttpError('当前 SKU 没有可用库存,无法创建人工兑换任务', {
statusCode: 409,
errorCode: 'admin_manual_redeem_inventory_unavailable',
@@ -208,6 +220,7 @@ export async function createAdminManualRedeemTask(
credentialType,
}
: null,
inventoryGroupCode: String(availableInventory.inventory_group_code || '').trim(),
manualRedeem: manualContext,
}),
createdAt: now,
@@ -226,6 +239,7 @@ export async function createAdminManualRedeemTask(
taskId: createdTask.id,
credentialType,
roleKey,
inventoryGroupCodes: viewerContext.allowedInventoryGroupCodes,
})
if (!reservedInventory) {
@@ -250,6 +264,10 @@ export async function createAdminManualRedeemTask(
claim_expires_at: claimToken.expired_at,
user_action_status: 'pending_claim',
last_error: '',
context_json: JSON.stringify({
...parseTaskContext(createdTask),
inventoryGroupCode: String(reservedInventory.inventory_group_code || '').trim(),
}),
updated_at: nowIso(),
})
@@ -259,73 +277,80 @@ export async function createAdminManualRedeemTask(
skuCode,
skuName,
inventoryItemId: Number(reservedInventory.id || 0) || null,
inventoryGroupCode: String(reservedInventory.inventory_group_code || '').trim(),
createdBy: manualContext.createdBy,
remark,
}, nowIso())
return getAdminManualRedeemDetail(createdTask.id)
return getAdminManualRedeemDetail(createdTask.id, session)
}
/** @param {AdminEntityIdInput} taskId */
export async function getAdminManualRedeemDetail(taskId) {
const task = await getRequiredManualRedeemTask(taskId)
/** @param {AdminViewerSessionInput | null} [session] */
export async function getAdminManualRedeemDetail(taskId, session = null) {
const task = await getRequiredManualRedeemTask(taskId, session)
const detail = await getClaimDetailForAdminTask(task.id)
return decorateManualRedeemDetail(task, detail)
}
/** @param {AdminEntityIdInput} taskId */
/** @param {{ loginType?: string, forceRecreate?: boolean }} [payload] */
export async function createAdminManualRedeemSession(taskId, payload = {}) {
const task = await getRequiredManualRedeemTask(taskId)
/** @param {AdminViewerSessionInput | null} [session] */
export async function createAdminManualRedeemSession(taskId, payload = {}, session = null) {
const task = await getRequiredManualRedeemTask(taskId, session)
const detail = await createClaimSessionForAdminTask(task.id, payload)
return decorateManualRedeemDetail(task, detail)
}
/** @param {AdminEntityIdInput} taskId */
export async function getAdminManualRedeemSessionSummary(taskId) {
const task = await getRequiredManualRedeemTask(taskId)
/** @param {AdminViewerSessionInput | null} [session] */
export async function getAdminManualRedeemSessionSummary(taskId, session = null) {
const task = await getRequiredManualRedeemTask(taskId, session)
const detail = await getClaimSessionSummaryForAdminTask(task.id)
return decorateManualRedeemDetail(task, detail)
}
/** @param {AdminEntityIdInput} taskId */
export async function reloadAdminManualRedeemSession(taskId) {
const task = await getRequiredManualRedeemTask(taskId)
/** @param {AdminViewerSessionInput | null} [session] */
export async function reloadAdminManualRedeemSession(taskId, session = null) {
const task = await getRequiredManualRedeemTask(taskId, session)
const detail = await reloadClaimSessionForAdminTask(task.id)
return decorateManualRedeemDetail(task, detail)
}
/** @param {AdminEntityIdInput} taskId */
export async function closeAdminManualRedeemSession(taskId) {
const task = await getRequiredManualRedeemTask(taskId)
/** @param {AdminViewerSessionInput | null} [session] */
export async function closeAdminManualRedeemSession(taskId, session = null) {
const task = await getRequiredManualRedeemTask(taskId, session)
const detail = await closeClaimSessionForAdminTask(task.id)
return decorateManualRedeemDetail(task, detail)
}
/** @param {AdminEntityIdInput} taskId */
export async function closeAdminManualRedeemTask(taskId) {
const task = await getRequiredManualRedeemTask(taskId)
/** @param {AdminViewerSessionInput | null} [session] */
export async function closeAdminManualRedeemTask(taskId, session = null) {
const task = await getRequiredManualRedeemTask(taskId, session)
await closeAdminTask(task.id)
return getAdminManualRedeemDetail(task.id)
return getAdminManualRedeemDetail(task.id, session)
}
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminViewerSessionInput | null} [session] */
export async function confirmAdminManualRedeemRole(taskId, session = null) {
const task = await getRequiredManualRedeemTask(taskId)
const task = await getRequiredManualRedeemTask(taskId, session)
await confirmAdminTaskAssistedRole(task.id, session)
return getAdminManualRedeemDetail(task.id)
return getAdminManualRedeemDetail(task.id, session)
}
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminViewerSessionInput | null} [session] */
export async function redeemAdminManualRedeemTask(taskId, session = null) {
const task = await getRequiredManualRedeemTask(taskId)
const task = await getRequiredManualRedeemTask(taskId, session)
await redeemAdminTaskAssisted(task.id, session)
return getAdminManualRedeemDetail(task.id)
return getAdminManualRedeemDetail(task.id, session)
}
async function getRequiredManualRedeemTask(taskId) {
async function getRequiredManualRedeemTask(taskId, session = null) {
const task = await getTaskById(Number(taskId))
if (!task) {
@@ -345,6 +370,25 @@ async function getRequiredManualRedeemTask(taskId) {
})
}
const viewerContext = createAdminViewerContext(session)
if (viewerContext.restrictInventoryGroups) {
if (!Array.isArray(viewerContext.allowedInventoryGroupCodes) || viewerContext.allowedInventoryGroupCodes.length === 0) {
throw createHttpError('当前客服未绑定任何库存组,无法操作人工兑换任务', {
statusCode: 403,
errorCode: 'admin_manual_redeem_inventory_group_unbound',
})
}
const inventoryGroupCode = String(taskContext?.inventoryGroupCode || '').trim()
if (inventoryGroupCode && !viewerContext.allowedInventoryGroupCodes.includes(inventoryGroupCode)) {
throw createHttpError('当前客服无权操作该库存组下的人工兑换任务', {
statusCode: 403,
errorCode: 'admin_manual_redeem_inventory_group_denied',
})
}
}
return task
}
@@ -2,7 +2,11 @@
import { buildClaimUrl } from '../claim/claim-service.js'
import { getClaimTokenById } from '../../repositories/claim-token-repo.js'
import { getInventoryItemById, listInventoryItems, listInventorySkuSuggestions } from '../../repositories/inventory-repo.js'
import {
getInventoryItemById,
listInventoryItems,
listInventorySkuSuggestions,
} from '../../repositories/inventory-repo.js'
import { listOrderItemsByOrderId } from '../../repositories/order-item-repo.js'
import { getOrderById, listOrders } from '../../repositories/order-repo.js'
import { getTaskById, listTasks, listTasksByOrderId } from '../../repositories/task-repo.js'
@@ -313,9 +317,11 @@ export async function getAdminTaskScreenshotPath(taskId, session = null) {
/** @returns {Promise<AdminInventoryListResponse>} */
/** @param {AdminInventoryListQueryInput} [query] */
export async function getAdminInventoryItems(query = /** @type {AdminInventoryListQueryInput} */ ({})) {
/** @param {AdminViewerSessionInput | null} [session] */
export async function getAdminInventoryItems(query = /** @type {AdminInventoryListQueryInput} */ ({}), session = null) {
const page = normalizePage(query.page)
const pageSize = normalizePageSize(query.pageSize)
const viewerContext = createAdminViewerContext(session)
const { items, total } = await listInventoryItems({
page,
pageSize,
@@ -323,6 +329,8 @@ export async function getAdminInventoryItems(query = /** @type {AdminInventoryLi
credentialType: String(query.credentialType || '').trim(),
status: String(query.status || '').trim(),
batchNo: String(query.batchNo || '').trim(),
inventoryGroupCode: String(query.inventoryGroupCode || '').trim(),
allowedInventoryGroupCodes: viewerContext.allowedInventoryGroupCodes,
})
return {
@@ -335,9 +343,12 @@ export async function getAdminInventoryItems(query = /** @type {AdminInventoryLi
/** @param {AdminInventorySkuSuggestionQueryInput} [query] */
export async function getAdminInventorySkuSuggestions(
query = /** @type {AdminInventorySkuSuggestionQueryInput} */ ({}),
session = null,
) {
const viewerContext = createAdminViewerContext(session)
const credentialType = String(query.credentialType || '').trim()
const keyword = String(query.keyword || '').trim()
const inventoryGroupCode = String(query.inventoryGroupCode || '').trim()
const requestedLimit = Number(query.limit)
const limit = Number.isFinite(requestedLimit) && requestedLimit > 0
? Math.min(100, Math.floor(requestedLimit))
@@ -346,12 +357,15 @@ export async function getAdminInventorySkuSuggestions(
credentialType,
keyword,
limit,
inventoryGroupCode,
allowedInventoryGroupCodes: viewerContext.allowedInventoryGroupCodes,
})
return {
items: items.map((item) => ({
skuCode: String(item.sku_code || '').trim(),
credentialType: String(item.credential_type || '').trim() || 'tencent_code',
inventoryGroupCode: String(item.inventory_group_code || '').trim(),
totalCount: Number(item.total_count || 0),
availableCount: Number(item.available_count || 0),
latestUpdatedAt: item.latest_updated_at || null,
@@ -57,9 +57,14 @@ export function getTaskPrimaryClaimTokenId(task) {
export function createAdminViewerContext(session = null) {
const role = normalizeAdminRole(session?.role)
const allowedInventoryGroupCodes = role === 'support'
? normalizeInventoryGroupCodes(session?.allowedInventoryGroups)
: null
return {
role,
allowedInventoryGroupCodes,
restrictInventoryGroups: role === 'support',
canViewSensitiveTaskData: role === 'admin' || role === 'operator',
canManageSensitiveInventory: role === 'admin',
canManageTaskLifecycle: role === 'admin' || role === 'operator',
@@ -200,11 +205,13 @@ export function canRegenerateClaimLinkForViewer(task, viewerContext) {
return true
}
return viewerContext.canOperateAssistedTask && isAssistedClaimTask(task)
return viewerContext.canOperateAssistedTask
&& isAssistedClaimTask(task)
&& canViewerAccessTaskInventoryGroup(task, viewerContext)
}
export function canViewerConfirmAssistedRole(task, viewerContext) {
if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task)) {
if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task) || !canViewerAccessTaskInventoryGroup(task, viewerContext)) {
return false
}
@@ -212,13 +219,42 @@ export function canViewerConfirmAssistedRole(task, viewerContext) {
}
export function canViewerRedeemAssistedTask(task, viewerContext) {
if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task)) {
if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task) || !canViewerAccessTaskInventoryGroup(task, viewerContext)) {
return false
}
return ['role_confirmed', 'redeeming'].includes(String(task?.task_status || '').trim())
}
export function canViewerAccessTaskInventoryGroup(task, viewerContext = createAdminViewerContext()) {
if (!viewerContext.restrictInventoryGroups) {
return true
}
const allowedInventoryGroupCodes = Array.isArray(viewerContext.allowedInventoryGroupCodes)
? viewerContext.allowedInventoryGroupCodes
: []
if (allowedInventoryGroupCodes.length === 0) {
return false
}
const taskContext = parseTaskContext(task)
const inventoryGroupCode = String(taskContext?.inventoryGroupCode || '').trim()
if (!inventoryGroupCode) {
return true
}
return allowedInventoryGroupCodes.includes(inventoryGroupCode)
}
export function normalizeInventoryGroupCodes(values) {
return Array.from(new Set((Array.isArray(values) ? values : [])
.map((value) => String(value || '').trim())
.filter(Boolean)))
}
function mapRedeemResolutionAttempt(value) {
if (!value || typeof value !== 'object') {
return null
@@ -2,6 +2,7 @@ import test from 'node:test'
import assert from 'node:assert/strict'
import {
canViewerConfirmAssistedRole,
canViewerCloseTask,
createAdminViewerContext,
resolveAdminTaskScreenshotUrl,
@@ -37,3 +38,18 @@ test('canViewerCloseTask allows support to close active task but not redeemed ta
assert.equal(canViewerCloseTask({ task_status: 'link_generated' }, viewerContext), true)
assert.equal(canViewerCloseTask({ task_status: 'redeemed' }, viewerContext), false)
})
test('canViewerConfirmAssistedRole blocks support when task inventory group is not bound', () => {
const viewerContext = createAdminViewerContext({
role: 'support',
allowedInventoryGroups: ['A组'],
})
assert.equal(canViewerConfirmAssistedRole({
task_status: 'claimed',
executor_key: 'tencent_claim_assisted',
context_json: {
inventoryGroupCode: 'B组',
},
}, viewerContext), false)
})
@@ -64,6 +64,7 @@ export async function createAdminInventoryItem(payload = /** @type {AdminInvento
const displayValue = String(payload.displayValue || '').trim()
const batchNo = String(payload.batchNo || '').trim()
const credentialType = String(payload.credentialType || 'tencent_code').trim() || 'tencent_code'
const inventoryGroupCode = String(payload.inventoryGroupCode || '').trim()
if (!skuCode || !displayValue) {
throw createHttpError('缺少 skuCode 或 displayValue', {
@@ -79,6 +80,7 @@ export async function createAdminInventoryItem(payload = /** @type {AdminInvento
displayValue,
batchNo,
credentialType,
inventoryGroupCode,
createdAt: now,
updatedAt: now,
},
@@ -93,10 +95,11 @@ export async function createAdminInventoryItem(payload = /** @type {AdminInvento
const { items } = await listInventoryItems({
page: 1,
pageSize: 1,
pageSize: 20,
skuCode,
inventoryGroupCode,
})
const createdItem = items.find((item) => item.display_value === displayValue) || null
const createdItem = items.find((item) => item.display_value === displayValue && item.inventory_group_code === inventoryGroupCode) || null
return {
inventoryItem: createdItem ? await mapAdminInventoryListItem(createdItem) : null,
@@ -121,6 +124,7 @@ export async function importAdminInventoryItems(payload = /** @type {AdminInvent
batchNo: row.batchNo,
displayValue: row.displayValue,
credentialType: row.credentialType,
inventoryGroupCode: row.inventoryGroupCode,
createdAt: now,
updatedAt: now,
}))
@@ -538,6 +542,7 @@ export async function retryAdminTask(taskId) {
taskId: task.id,
credentialType: primaryRequirement?.credentialType || 'tencent_code',
roleKey: primaryRequirement?.roleKey || 'primary_code',
inventoryGroupCodes: resolveTaskInventoryGroupCodes(task),
})
if (!reserved) {
@@ -672,7 +677,7 @@ export async function completeAdminTaskManualDispatch(taskId, payload = {}, sess
/**
* @param {AdminInventoryImportInput} payload
* @returns {Array<Required<Pick<AdminInventoryImportRowInput, 'skuCode' | 'displayValue' | 'batchNo' | 'credentialType'>>>}
* @returns {Array<Required<Pick<AdminInventoryImportRowInput, 'skuCode' | 'displayValue' | 'batchNo' | 'credentialType' | 'inventoryGroupCode'>>>}
*/
function normalizeInventoryImportRows(payload) {
const rows = []
@@ -685,12 +690,13 @@ function normalizeInventoryImportRows(payload) {
const displayValue = String(row?.displayValue || '').trim()
const batchNo = String(row?.batchNo || '').trim()
const credentialType = String(row?.credentialType || payload.credentialType || 'tencent_code').trim() || 'tencent_code'
const inventoryGroupCode = String(row?.inventoryGroupCode || payload.inventoryGroupCode || '').trim()
if (!skuCode || !displayValue) {
continue
}
rows.push({ skuCode, displayValue, batchNo, credentialType })
rows.push({ skuCode, displayValue, batchNo, credentialType, inventoryGroupCode })
}
}
@@ -698,6 +704,7 @@ function normalizeInventoryImportRows(payload) {
const skuCode = String(payload.skuCode || '').trim()
const batchNo = String(payload.batchNo || '').trim()
const credentialType = String(payload.credentialType || 'tencent_code').trim() || 'tencent_code'
const inventoryGroupCode = String(payload.inventoryGroupCode || '').trim()
if (!skuCode) {
throw createHttpError('批量导入时缺少 skuCode', {
@@ -711,7 +718,7 @@ function normalizeInventoryImportRows(payload) {
if (!displayValue) {
continue
}
rows.push({ skuCode, displayValue, batchNo, credentialType })
rows.push({ skuCode, displayValue, batchNo, credentialType, inventoryGroupCode })
}
}
@@ -719,7 +726,7 @@ function normalizeInventoryImportRows(payload) {
}
/**
* @param {Array<Required<Pick<AdminInventoryImportRowInput, 'skuCode' | 'displayValue' | 'batchNo' | 'credentialType'>>>} rows
* @param {Array<Required<Pick<AdminInventoryImportRowInput, 'skuCode' | 'displayValue' | 'batchNo' | 'credentialType' | 'inventoryGroupCode'>>>} rows
*/
function dedupeRows(rows) {
const seen = new Set()
@@ -737,6 +744,12 @@ function dedupeRows(rows) {
return output
}
function resolveTaskInventoryGroupCodes(task) {
const taskContext = parseTaskContext(task)
const inventoryGroupCode = String(taskContext?.inventoryGroupCode || '').trim()
return inventoryGroupCode ? [inventoryGroupCode] : null
}
/**
* @param {Awaited<ReturnType<typeof getRequiredTask>>} task
@@ -964,6 +964,9 @@ export async function redeemClaimTaskWithInventoryFallbackWithDeps(
taskId: context.task.id,
credentialType: currentInventoryItem.credential_type || 'tencent_code',
roleKey: 'primary_code',
inventoryGroupCodes: currentInventoryItem.inventory_group_code
? [String(currentInventoryItem.inventory_group_code).trim()]
: null,
})
if (!replacement || !String(replacement.display_value || '').trim()) {
@@ -989,6 +992,7 @@ export async function redeemClaimTaskWithInventoryFallbackWithDeps(
nextInventoryItemId: replacement.id,
nextCodeMasked: maskCode(replacement.display_value),
credentialType: String(replacement.credential_type || currentInventoryItem.credential_type || ''),
inventoryGroupCode: String(replacement.inventory_group_code || currentInventoryItem.inventory_group_code || '').trim(),
}, replacedAt)
currentInventoryItem = replacement
shouldReloadBeforeNextAttempt = true
@@ -22,11 +22,13 @@ test('redeemClaimTaskWithInventoryFallbackWithDeps invalidates missing CDKEY and
id: 1,
display_value: 'DJQFf7HgONCL4XD4EH',
credential_type: 'tencent_code',
inventory_group_code: 'A组',
}
const replacementInventoryItem = {
id: 2,
display_value: 'ABCD1234EFGH5678',
credential_type: 'tencent_code',
inventory_group_code: 'A组',
}
let redeemCount = 0
@@ -127,6 +129,7 @@ test('redeemClaimTaskWithInventoryFallbackWithDeps invalidates missing CDKEY and
taskId: 42,
credentialType: 'tencent_code',
roleKey: 'primary_code',
inventoryGroupCodes: ['A组'],
},
])
assert.deepEqual(reloadCalls, ['txbs-test'])
@@ -160,6 +163,7 @@ test('redeemClaimTaskWithInventoryFallbackWithDeps invalidates missing CDKEY and
nextInventoryItemId: 2,
nextCodeMasked: 'ABCD****5678',
credentialType: 'tencent_code',
inventoryGroupCode: 'A组',
},
},
],
@@ -5,9 +5,15 @@ import {
} from '../../repositories/inventory-repo.js'
import { nowIso } from '../../utils/time.js'
export async function reserveInventoryForTask({ skuCode, taskId, credentialType = 'tencent_code', roleKey = 'primary_code' }) {
export async function reserveInventoryForTask({
skuCode,
taskId,
credentialType = 'tencent_code',
roleKey = 'primary_code',
inventoryGroupCodes = null,
}) {
return reserveInventoryForTaskWithDeps(
{ skuCode, taskId, credentialType, roleKey },
{ skuCode, taskId, credentialType, roleKey, inventoryGroupCodes },
{
assignReservedInventoryItem,
findFirstAvailableInventoryItemBySkuCode,
@@ -17,7 +23,13 @@ export async function reserveInventoryForTask({ skuCode, taskId, credentialType
}
export async function reserveInventoryForTaskWithDeps(
{ skuCode, taskId, credentialType = 'tencent_code', roleKey = 'primary_code' },
{
skuCode,
taskId,
credentialType = 'tencent_code',
roleKey = 'primary_code',
inventoryGroupCodes = null,
},
{
assignReservedInventoryItem: assignReserved = assignReservedInventoryItem,
findFirstAvailableInventoryItemBySkuCode: findAvailable = findFirstAvailableInventoryItemBySkuCode,
@@ -28,7 +40,11 @@ export async function reserveInventoryForTaskWithDeps(
return null
}
const available = await findAvailable(skuCode, credentialType)
if (Array.isArray(inventoryGroupCodes) && inventoryGroupCodes.length === 0) {
return null
}
const available = await findAvailable(skuCode, credentialType, inventoryGroupCodes)
if (!available) {
return null
@@ -28,8 +28,8 @@ test('reserveInventoryForTaskWithDeps reserves first available inventory item wi
const result = await reserveInventoryForTaskWithDeps(
{ skuCode: 'dnf-cdk-a', taskId: 88 },
{
findFirstAvailableInventoryItemBySkuCode: async (skuCode, credentialType) => {
calls.push(['find', skuCode, credentialType])
findFirstAvailableInventoryItemBySkuCode: async (skuCode, credentialType, inventoryGroupCodes) => {
calls.push(['find', skuCode, credentialType, inventoryGroupCodes])
return { id: 321 }
},
assignReservedInventoryItem: async (inventoryItemId, taskId, updatedAt, roleKey) => {
@@ -41,13 +41,50 @@ test('reserveInventoryForTaskWithDeps reserves first available inventory item wi
)
assert.deepEqual(calls, [
['find', 'dnf-cdk-a', 'tencent_code'],
['find', 'dnf-cdk-a', 'tencent_code', null],
['assign', 321, 88, '2026-04-14T12:00:00.000Z', 'primary_code'],
])
assert.equal(result?.id, 321)
assert.equal(result?.reserved_by_task_id, 88)
})
test('reserveInventoryForTaskWithDeps forwards inventory group filters', async () => {
const calls = []
await reserveInventoryForTaskWithDeps(
{ skuCode: 'dnf-cdk-a', taskId: 88, inventoryGroupCodes: ['A组', 'B组'] },
{
findFirstAvailableInventoryItemBySkuCode: async (skuCode, credentialType, inventoryGroupCodes) => {
calls.push(['find', skuCode, credentialType, inventoryGroupCodes])
return { id: 321 }
},
assignReservedInventoryItem: async () => ({ id: 321 }),
nowIso: () => '2026-04-14T12:00:00.000Z',
},
)
assert.deepEqual(calls, [
['find', 'dnf-cdk-a', 'tencent_code', ['A组', 'B组']],
])
})
test('reserveInventoryForTaskWithDeps returns null when restricted inventory groups are empty', async () => {
let findCalled = false
const result = await reserveInventoryForTaskWithDeps(
{ skuCode: 'dnf-cdk-a', taskId: 12, inventoryGroupCodes: [] },
{
findFirstAvailableInventoryItemBySkuCode: async () => {
findCalled = true
return null
},
},
)
assert.equal(result, null)
assert.equal(findCalled, false)
})
test('releaseReservedInventoryItemsWithDeps skips empty ids and keeps successful releases only', async () => {
const calls = []
const result = await releaseReservedInventoryItemsWithDeps(
+5 -2
View File
@@ -7,6 +7,7 @@ export {}
* userId?: number | string
* username?: string
* role?: string
* allowedInventoryGroups?: string[]
* }} AdminViewerSessionInput
*/
@@ -41,9 +42,10 @@ export {}
* page?: number | string
* pageSize?: number | string
* skuCode?: string
* credentialType?: string
* credentialType?: string
* status?: string
* batchNo?: string
* inventoryGroupCode?: string
* }} AdminInventoryListQueryInput
*/
@@ -51,7 +53,8 @@ export {}
* @typedef {{
* credentialType?: string
* keyword?: string
* limit?: number | string
* limit?: number | string
* inventoryGroupCode?: string
* }} AdminInventorySkuSuggestionQueryInput
*/
@@ -99,6 +99,7 @@ export {}
* skuCode: string
* batchNo: string
* credentialType: string
* inventoryGroupCode: string
* displayValue: string
* status: string
* reservedByTaskId: number | null
@@ -155,6 +156,7 @@ export {}
* @typedef {{
* skuCode: string
* credentialType: string
* inventoryGroupCode: string
* totalCount: number
* availableCount: number
* latestUpdatedAt: string | null
@@ -54,6 +54,10 @@ export {}
* @typedef {import('./admin-write-inputs.js').AdminInventoryInvalidateInput} AdminInventoryInvalidateRouteBody
*/
/**
* @typedef {import('./admin-write-inputs.js').AdminUserInventoryGroupsInput} AdminUserInventoryGroupsRouteBody
*/
/**
* @typedef {import('./admin-write-inputs.js').AdminTaskManualDispatchInput} AdminTaskManualDispatchRouteBody
*/
@@ -8,6 +8,7 @@ export {}
* displayValue?: string
* batchNo?: string
* credentialType?: string
* inventoryGroupCode?: string
* }} AdminInventoryCreateInput
*/
@@ -17,6 +18,7 @@ export {}
* displayValue?: string
* batchNo?: string
* credentialType?: string
* inventoryGroupCode?: string
* }} AdminInventoryImportRowInput
*/
@@ -27,9 +29,16 @@ export {}
* skuCode?: string
* batchNo?: string
* credentialType?: string
* inventoryGroupCode?: string
* }} AdminInventoryImportInput
*/
/**
* @typedef {{
* inventoryGroupCodes?: string[]
* }} AdminUserInventoryGroupsInput
*/
/**
* @typedef {{
* messageTemplate?: string
@@ -6,6 +6,10 @@ export {}
* @typedef {import('./admin-read-models.js').AdminInventoryItemListItem} AdminInventoryItemListItem
*/
/**
* @typedef {import('./admin-read-models.js').AdminInventorySkuSuggestion} AdminInventorySkuSuggestion
*/
/**
* @typedef {{
* inventoryItem: AdminInventoryItemListItem | null
@@ -20,6 +24,12 @@ export {}
* }} AdminInventoryImportResponse
*/
/**
* @typedef {{
* items: AdminInventorySkuSuggestion[]
* }} AdminInventoryGroupListResponse
*/
/**
* @typedef {{
* messageTemplate: string
@@ -148,6 +148,8 @@ export {}
* credentialType?: string
* status?: string
* batchNo?: string
* inventoryGroupCode?: string
* allowedInventoryGroupCodes?: string[] | null
* }} InventoryListQueryInput
*/
@@ -156,6 +158,8 @@ export {}
* credentialType?: string
* keyword?: string
* limit?: number
* inventoryGroupCode?: string
* allowedInventoryGroupCodes?: string[] | null
* }} InventorySkuSuggestionQueryInput
*/
@@ -164,6 +168,7 @@ export {}
* batchNo?: string
* skuCode: string
* credentialType?: string
* inventoryGroupCode?: string
* displayValue: string
* payload?: Record<string, unknown>
* createdAt: string
@@ -93,6 +93,7 @@ export {}
* sku_code: string
* batch_no: string
* credential_type: string
* inventory_group_code: string
* display_value: string
* status: string
* invalid_reason: string
@@ -109,6 +110,7 @@ export {}
* @typedef {{
* sku_code: string
* credential_type: string
* inventory_group_code: string
* total_count: number
* available_count: number
* latest_updated_at: string | null
@@ -152,6 +154,7 @@ export {}
* sku_code: string
* batch_no: string
* credential_type: string
* inventory_group_code: string
* display_value: string
* inventory_status: string
* }} TaskInventoryBindingRow