feat(finance): 人工支出改为从客服备用金扣款
This commit is contained in:
@@ -1532,6 +1532,144 @@ async function resolveAgentAccountId(payload: JsonObject, session: AdminSession)
|
||||
return accountId
|
||||
}
|
||||
|
||||
export type FundExpenseWriteInput = {
|
||||
agentAccountId: number
|
||||
amountFen: number
|
||||
category: string
|
||||
payeeType: string
|
||||
payeeName: string
|
||||
payeeAccount?: string
|
||||
platformName?: string
|
||||
platformAccount?: string
|
||||
orderId?: number | null
|
||||
orderItemId?: number | null
|
||||
paidAt: string
|
||||
evidence: JsonObject[]
|
||||
note: string
|
||||
actorUserId: number
|
||||
allowDuplicate?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 备用金支出唯一写入口:扣账户余额 + 写 fund_entries + 写 manual_expenses。
|
||||
* 人工支出登记与备用金页面都走这里,避免两套扣款逻辑。
|
||||
*/
|
||||
export async function writeFundExpense(
|
||||
client: PoolClient,
|
||||
input: FundExpenseWriteInput,
|
||||
): Promise<{
|
||||
expense: ManualExpenseRow
|
||||
ownerAdminUserId: number
|
||||
ownerName: string
|
||||
balanceAfterFen: number
|
||||
}> {
|
||||
const accounts = await lockAccounts(client, [input.agentAccountId])
|
||||
const account = accounts.get(input.agentAccountId)
|
||||
if (!account || account.account_type !== 'agent') {
|
||||
throw createHttpError('客服备用金账户不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'fund_agent_account_not_found',
|
||||
})
|
||||
}
|
||||
if (account.status !== 'active') {
|
||||
throw createHttpError('客服备用金账户已冻结或关闭', {
|
||||
statusCode: 409,
|
||||
errorCode: 'fund_account_inactive',
|
||||
})
|
||||
}
|
||||
const balance = toNumber(account.balance_fen)
|
||||
if (balance < input.amountFen) {
|
||||
throw createHttpError('备用金余额不足,请先联系主管补拨', {
|
||||
statusCode: 409,
|
||||
errorCode: 'fund_balance_insufficient',
|
||||
})
|
||||
}
|
||||
|
||||
if (!input.allowDuplicate) {
|
||||
const duplicateResult = await client.query<{ expense_no: string }>(
|
||||
`
|
||||
SELECT expense_no
|
||||
FROM manual_expenses
|
||||
WHERE account_id = $1
|
||||
AND amount_fen = $2
|
||||
AND payee_name = $3
|
||||
AND ($4::bigint IS NULL OR order_id = $4)
|
||||
AND created_at >= now() - interval '24 hours'
|
||||
LIMIT 1
|
||||
`,
|
||||
[input.agentAccountId, input.amountFen, input.payeeName, input.orderId || null],
|
||||
)
|
||||
if (duplicateResult.rows[0]) {
|
||||
throw createHttpError(
|
||||
`疑似重复支出(${duplicateResult.rows[0].expense_no}),如确认无误请勾选“确认非重复”`,
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: 'fund_expense_duplicate',
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const now = nowIso()
|
||||
const nextBalance = balance - input.amountFen
|
||||
await client.query('UPDATE fund_accounts SET balance_fen = $1, updated_at = $2 WHERE id = $3', [
|
||||
nextBalance,
|
||||
now,
|
||||
input.agentAccountId,
|
||||
])
|
||||
const entry = await insertEntry(client, {
|
||||
accountId: input.agentAccountId,
|
||||
entryType: 'expense',
|
||||
amountFen: -input.amountFen,
|
||||
balanceAfterFen: nextBalance,
|
||||
occurredAt: input.paidAt,
|
||||
actorUserId: input.actorUserId,
|
||||
evidence: input.evidence,
|
||||
note: input.note,
|
||||
createdAt: now,
|
||||
})
|
||||
const entryRow = requiredRow(entry, '写入支出流水失败')
|
||||
|
||||
const expenseNo = generateNo('FX')
|
||||
const expenseResult = await client.query<ManualExpenseRow>(
|
||||
`
|
||||
INSERT INTO manual_expenses (
|
||||
expense_no, account_id, amount_fen, category, payee_type, payee_name,
|
||||
payee_account, platform_name, platform_account, order_id, order_item_id,
|
||||
paid_at, evidence_json, note, fund_entry_id, created_by_admin_user_id,
|
||||
created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::jsonb, $14, $15, $16, $17, $17)
|
||||
RETURNING *
|
||||
`,
|
||||
[
|
||||
expenseNo,
|
||||
input.agentAccountId,
|
||||
input.amountFen,
|
||||
input.category,
|
||||
input.payeeType,
|
||||
input.payeeName,
|
||||
input.payeeAccount || '',
|
||||
input.platformName || '',
|
||||
input.platformAccount || '',
|
||||
input.orderId || null,
|
||||
input.orderItemId || null,
|
||||
input.paidAt,
|
||||
JSON.stringify(input.evidence),
|
||||
input.note,
|
||||
entryRow.id,
|
||||
input.actorUserId,
|
||||
now,
|
||||
],
|
||||
)
|
||||
|
||||
return {
|
||||
expense: requiredRow(expenseResult.rows[0], '写入支出明细失败'),
|
||||
ownerAdminUserId: Number(account.owner_admin_user_id),
|
||||
ownerName: account.owner_name,
|
||||
balanceAfterFen: nextBalance,
|
||||
}
|
||||
}
|
||||
|
||||
export async function createAdminFundExpense(payload: JsonObject, session: AdminSession) {
|
||||
const agentAccountId = await resolveAgentAccountId(payload, session)
|
||||
const amountFen = normalizeFen(payload.amountFen)
|
||||
@@ -1588,110 +1726,27 @@ export async function createAdminFundExpense(payload: JsonObject, session: Admin
|
||||
}
|
||||
}
|
||||
|
||||
const now = nowIso()
|
||||
const allowDuplicate = payload.allowDuplicate === true
|
||||
|
||||
return withTransaction(async (client) => {
|
||||
const accounts = await lockAccounts(client, [agentAccountId])
|
||||
const account = accounts.get(agentAccountId)
|
||||
if (!account || account.account_type !== 'agent') {
|
||||
throw createHttpError('客服备用金账户不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'fund_agent_account_not_found',
|
||||
})
|
||||
}
|
||||
if (account.status !== 'active') {
|
||||
throw createHttpError('客服备用金账户已冻结或关闭', {
|
||||
statusCode: 409,
|
||||
errorCode: 'fund_account_inactive',
|
||||
})
|
||||
}
|
||||
const balance = toNumber(account.balance_fen)
|
||||
if (balance < amountFen) {
|
||||
throw createHttpError('备用金余额不足,请先联系主管补拨', {
|
||||
statusCode: 409,
|
||||
errorCode: 'fund_balance_insufficient',
|
||||
})
|
||||
}
|
||||
|
||||
if (!allowDuplicate) {
|
||||
const duplicateResult = await client.query<{ expense_no: string }>(
|
||||
`
|
||||
SELECT expense_no
|
||||
FROM manual_expenses
|
||||
WHERE account_id = $1
|
||||
AND amount_fen = $2
|
||||
AND payee_name = $3
|
||||
AND created_at >= now() - interval '24 hours'
|
||||
LIMIT 1
|
||||
`,
|
||||
[agentAccountId, amountFen, payeeName],
|
||||
)
|
||||
if (duplicateResult.rows[0]) {
|
||||
throw createHttpError(
|
||||
`疑似重复支出(${duplicateResult.rows[0].expense_no}),如确认无误请勾选“确认非重复”`,
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: 'fund_expense_duplicate',
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const nextBalance = balance - amountFen
|
||||
await client.query('UPDATE fund_accounts SET balance_fen = $1, updated_at = $2 WHERE id = $3', [
|
||||
nextBalance,
|
||||
now,
|
||||
const written = await writeFundExpense(client, {
|
||||
agentAccountId,
|
||||
])
|
||||
const entry = await insertEntry(client, {
|
||||
accountId: agentAccountId,
|
||||
entryType: 'expense',
|
||||
amountFen: -amountFen,
|
||||
balanceAfterFen: nextBalance,
|
||||
occurredAt: paidAt,
|
||||
actorUserId: session.userId,
|
||||
amountFen,
|
||||
category,
|
||||
payeeType,
|
||||
payeeName,
|
||||
payeeAccount,
|
||||
platformName,
|
||||
platformAccount,
|
||||
orderId: resolvedOrderId,
|
||||
orderItemId,
|
||||
paidAt,
|
||||
evidence,
|
||||
note,
|
||||
createdAt: now,
|
||||
actorUserId: session.userId,
|
||||
allowDuplicate,
|
||||
})
|
||||
const entryRow = requiredRow(entry, '写入支出流水失败')
|
||||
|
||||
const expenseNo = generateNo('FX')
|
||||
const expenseResult = await client.query<ManualExpenseRow>(
|
||||
`
|
||||
INSERT INTO manual_expenses (
|
||||
expense_no, account_id, amount_fen, category, payee_type, payee_name,
|
||||
payee_account, platform_name, platform_account, order_id, order_item_id,
|
||||
paid_at, evidence_json, note, fund_entry_id, created_by_admin_user_id,
|
||||
created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::jsonb, $14, $15, $16, $17, $17)
|
||||
RETURNING *
|
||||
`,
|
||||
[
|
||||
expenseNo,
|
||||
agentAccountId,
|
||||
amountFen,
|
||||
category,
|
||||
payeeType,
|
||||
payeeName,
|
||||
payeeAccount,
|
||||
platformName,
|
||||
platformAccount,
|
||||
resolvedOrderId || null,
|
||||
orderItemId || null,
|
||||
paidAt,
|
||||
JSON.stringify(evidence),
|
||||
note,
|
||||
entryRow.id,
|
||||
session.userId,
|
||||
now,
|
||||
],
|
||||
)
|
||||
|
||||
return {
|
||||
expense: mapManualExpense(requiredRow(expenseResult.rows[0], '写入支出明细失败')),
|
||||
}
|
||||
return { expense: mapManualExpense(written.expense) }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user