feat(finance): 订单页人工录单按单实际金额计入成本
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
import {
|
||||
approveManualExpense,
|
||||
createManualExpense,
|
||||
createManualOrderCost,
|
||||
getManualExpense,
|
||||
getManualExpenseOverview,
|
||||
listManualExpenseCostItems,
|
||||
@@ -184,6 +185,33 @@ router.post(
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/finance/manual-order-costs',
|
||||
requireAnyAdminPermission(['fund.expense', 'finance.config']),
|
||||
createJsonHandler((req) => createManualOrderCost(bodyOf(req), sessionOf(req)), {
|
||||
successMessage: '订单人工成本已录入',
|
||||
errorMessage: '录入订单人工成本失败',
|
||||
scope: '[admin/finance/manual-order-costs]',
|
||||
audit: (req, data) => {
|
||||
const result = data as {
|
||||
record?: { id?: number; expenseNo?: string; amountFen?: number; orderId?: number }
|
||||
effective?: boolean
|
||||
}
|
||||
return {
|
||||
action: 'manual_order_cost_created',
|
||||
targetType: 'manual_expense',
|
||||
targetId: String(result.record?.id || ''),
|
||||
data: {
|
||||
expenseNo: result.record?.expenseNo || '',
|
||||
amountFen: result.record?.amountFen || 0,
|
||||
orderId: result.record?.orderId || req.body?.orderId || '',
|
||||
effective: result.effective === true,
|
||||
},
|
||||
}
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/finance/manual-expenses/:expenseId/approve',
|
||||
requireAnyAdminPermission(['finance.review']),
|
||||
|
||||
@@ -424,6 +424,106 @@ export async function createManualExpense(payload: JsonObject, session: AdminSes
|
||||
})
|
||||
}
|
||||
|
||||
export async function createManualOrderCost(payload: JsonObject, session: AdminSession) {
|
||||
const orderId = normalizeId(payload.orderId ?? payload.order_id)
|
||||
const requestedItemId = normalizeOptionalId(payload.orderItemId ?? payload.order_item_id)
|
||||
const itemsResult = await query<{ id: number }>(
|
||||
'SELECT id FROM order_items WHERE order_id = $1 ORDER BY id ASC',
|
||||
[orderId],
|
||||
)
|
||||
if (itemsResult.rows.length === 0) {
|
||||
throw createHttpError('订单没有订单项,无法录入人工成本', {
|
||||
statusCode: 400,
|
||||
errorCode: 'manual_order_cost_items_missing',
|
||||
})
|
||||
}
|
||||
let orderItemId = requestedItemId
|
||||
if (orderItemId) {
|
||||
if (!itemsResult.rows.some((row) => Number(row.id) === orderItemId)) {
|
||||
throw createHttpError('订单项不属于该订单', {
|
||||
statusCode: 400,
|
||||
errorCode: 'manual_order_cost_item_mismatch',
|
||||
})
|
||||
}
|
||||
} else if (itemsResult.rows.length === 1) {
|
||||
orderItemId = Number(itemsResult.rows[0]?.id || 0)
|
||||
} else {
|
||||
throw createHttpError('订单有多个订单项,请选择要计入的订单项', {
|
||||
statusCode: 400,
|
||||
errorCode: 'manual_order_cost_item_required',
|
||||
})
|
||||
}
|
||||
|
||||
const costItemResult = await query<{ id: number }>(
|
||||
`SELECT id FROM cost_items
|
||||
WHERE code = 'sys_manual_service' AND status = 'active'
|
||||
LIMIT 1`,
|
||||
)
|
||||
const costItemId = Number(costItemResult.rows[0]?.id || 0)
|
||||
if (!costItemId) {
|
||||
throw createHttpError('人工客服履约成本项未启用', {
|
||||
statusCode: 409,
|
||||
errorCode: 'manual_order_cost_item_unavailable',
|
||||
})
|
||||
}
|
||||
|
||||
const amountFen = normalizeAmountFen(payload.amountFen ?? payload.amount_fen)
|
||||
const note = optionalText(payload.note, 500)
|
||||
const expenseCategory = normalizeStatus(
|
||||
payload.expenseCategory ?? payload.expense_category,
|
||||
EXPENSE_CATEGORIES,
|
||||
'service_fee',
|
||||
'支出分类无效',
|
||||
)
|
||||
const payeeName = optionalText(payload.payeeName ?? payload.payee_name, 120) || '内部人工'
|
||||
const evidenceJson = normalizeEvidence(payload.evidenceJson ?? payload.evidence_json)
|
||||
if (!hasEvidence(evidenceJson)) {
|
||||
evidenceJson.note = note || '订单页人工录单'
|
||||
}
|
||||
const canApprove =
|
||||
session.role === 'admin' ||
|
||||
session.role === 'finance' ||
|
||||
(session.permissions || []).includes('finance.review')
|
||||
const effective = payload.effective !== false && canApprove
|
||||
|
||||
const created = await createManualExpense(
|
||||
{
|
||||
orderId,
|
||||
orderItemId,
|
||||
costItemId,
|
||||
amountFen,
|
||||
expenseCategory,
|
||||
payeeName,
|
||||
payeeType: payload.payeeType ?? payload.payee_type ?? 'other',
|
||||
paymentMethod: payload.paymentMethod ?? payload.payment_method ?? 'other',
|
||||
note: note || '订单页人工录单',
|
||||
evidenceJson,
|
||||
status: 'submitted',
|
||||
},
|
||||
session,
|
||||
)
|
||||
|
||||
if (!effective) {
|
||||
return { record: created, effective: false, reason: 'pending_review' as const }
|
||||
}
|
||||
|
||||
try {
|
||||
const approved = await approveManualExpense(
|
||||
created.id,
|
||||
{ note: '订单页人工录单直接生效' },
|
||||
session,
|
||||
)
|
||||
return { record: approved, effective: true, reason: 'approved' as const }
|
||||
} catch (error) {
|
||||
return {
|
||||
record: created,
|
||||
effective: false,
|
||||
reason: 'approval_failed' as const,
|
||||
message: error instanceof Error ? error.message : '自动生效失败',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function approveManualExpense(
|
||||
expenseId: number | string,
|
||||
payload: JsonObject,
|
||||
|
||||
@@ -94,8 +94,7 @@ export async function rebuildSalesDailyFacts(
|
||||
COUNT(*)::int AS snapshot_count,
|
||||
COUNT(*) FILTER (WHERE cs.snapshot_stage = 'actual')::int AS actual_count
|
||||
FROM cost_snapshots cs
|
||||
WHERE cs.subject_type = 'order_item'
|
||||
AND cs.subject_id = oi.id
|
||||
WHERE cs.order_item_id = oi.id
|
||||
AND cs.superseded_at IS NULL
|
||||
) cost ON TRUE
|
||||
WHERE o.pay_status IN ('paid', 'refunded')
|
||||
|
||||
Reference in New Issue
Block a user