feat(finance): 完善客服钱包与人工履约成本流水
This commit is contained in:
@@ -419,8 +419,8 @@ export async function listPlatformCreditOwners() {
|
||||
SELECT
|
||||
au.id, au.username, COALESCE(NULLIF(au.display_name, ''), au.username) AS display_name
|
||||
FROM admin_users au
|
||||
WHERE au.status = 'active'
|
||||
ORDER BY au.id
|
||||
WHERE au.status = 'active' AND au.role = 'support'
|
||||
ORDER BY display_name, au.id
|
||||
`,
|
||||
)
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
buildManualFulfillmentNote,
|
||||
buildManualFulfillmentReference,
|
||||
} from './manual-fulfillment-note.js'
|
||||
|
||||
test('人工履约编号同时带工单号与平台单号', () => {
|
||||
assert.equal(
|
||||
buildManualFulfillmentReference({
|
||||
workOrderNo: 'WK26102414033739',
|
||||
platformOrderId: '2628501763808879',
|
||||
orderItemId: 10,
|
||||
}),
|
||||
'工单 WK26102414033739 · 平台单号 2628501763808879',
|
||||
)
|
||||
})
|
||||
|
||||
test('人工履约编号在没有工单号时回退平台单号', () => {
|
||||
assert.equal(
|
||||
buildManualFulfillmentReference({
|
||||
workOrderNo: null,
|
||||
platformOrderId: '2628501763808879',
|
||||
orderItemId: 10,
|
||||
}),
|
||||
'平台单号 2628501763808879',
|
||||
)
|
||||
})
|
||||
|
||||
test('人工履约编号完全没有单号时回退订单项', () => {
|
||||
assert.equal(
|
||||
buildManualFulfillmentReference({
|
||||
workOrderNo: null,
|
||||
platformOrderId: '',
|
||||
orderItemId: 10,
|
||||
}),
|
||||
'订单项 #10',
|
||||
)
|
||||
})
|
||||
|
||||
test('人工履约备注包含商品、编号与客服备注并按上限截断', () => {
|
||||
const note = buildManualFulfillmentNote({
|
||||
skuName: '幸运币2个',
|
||||
reference: '工单 WK26102414033739 · 平台单号 2628501763808879',
|
||||
note: '已发放',
|
||||
maxLength: 300,
|
||||
})
|
||||
assert.equal(
|
||||
note,
|
||||
'人工履约 幸运币2个 · 工单 WK26102414033739 · 平台单号 2628501763808879 · 已发放',
|
||||
)
|
||||
|
||||
assert.equal(
|
||||
buildManualFulfillmentNote({
|
||||
skuName: '幸运币2个',
|
||||
reference: '工单 WK26102414033739',
|
||||
note: '',
|
||||
maxLength: 12,
|
||||
}),
|
||||
'人工履约 幸运币2个 ·',
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
/** 人工履约流水备注:商品、可追溯编号与客服备注。 */
|
||||
export function buildManualFulfillmentReference(input: {
|
||||
workOrderNo: string | null
|
||||
platformOrderId: string
|
||||
orderItemId: number
|
||||
}) {
|
||||
const parts: string[] = []
|
||||
if (input.workOrderNo) parts.push(`工单 ${input.workOrderNo}`)
|
||||
if (input.platformOrderId) parts.push(`平台单号 ${input.platformOrderId}`)
|
||||
if (parts.length === 0) parts.push(`订单项 #${input.orderItemId}`)
|
||||
return parts.join(' · ')
|
||||
}
|
||||
|
||||
export function buildManualFulfillmentNote(input: {
|
||||
skuName: string
|
||||
reference: string
|
||||
note: string
|
||||
maxLength: number
|
||||
}) {
|
||||
const parts = [`人工履约 ${input.skuName}`.trim(), input.reference]
|
||||
if (input.note) parts.push(input.note)
|
||||
return parts.filter(Boolean).join(' · ').slice(0, input.maxLength)
|
||||
}
|
||||
@@ -3,6 +3,10 @@ import type { JsonObject } from '../../types/json.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import type { AdminSession } from '../admin/admin-auth-service.js'
|
||||
import {
|
||||
buildManualFulfillmentNote,
|
||||
buildManualFulfillmentReference,
|
||||
} from './manual-fulfillment-note.js'
|
||||
import {
|
||||
consumePlatformCredit,
|
||||
type PlatformCreditKey,
|
||||
@@ -175,6 +179,7 @@ export async function confirmManualFulfillment(payload: JsonObject, session: Adm
|
||||
cost_mode: string
|
||||
credit_unit_price_fen: string
|
||||
work_order_id: number | null
|
||||
work_order_no: string | null
|
||||
work_product_rule_id: number | null
|
||||
fulfilled_at: string | null
|
||||
}>(
|
||||
@@ -203,6 +208,7 @@ export async function confirmManualFulfillment(payload: JsonObject, session: Adm
|
||||
COALESCE(cfg.cost_mode, 'variable') AS cost_mode,
|
||||
COALESCE((fp.config_json->>'rechargeUnitPriceFen')::bigint, 1) AS credit_unit_price_fen,
|
||||
wo.id AS work_order_id,
|
||||
wo.work_order_no,
|
||||
wo.product_rule_id AS work_product_rule_id,
|
||||
fulfilled.fulfilled_at
|
||||
FROM order_items oi
|
||||
@@ -210,7 +216,7 @@ export async function confirmManualFulfillment(payload: JsonObject, session: Adm
|
||||
LEFT JOIN manual_fulfillment_configs cfg ON cfg.id = oi.fulfillment_config_id
|
||||
LEFT JOIN fulfillment_profiles fp ON fp.profile_key = 'manual_' || cfg.platform_key
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT id, product_rule_id FROM work_orders
|
||||
SELECT id, product_rule_id, work_order_no FROM work_orders
|
||||
WHERE order_item_id = oi.id ORDER BY id DESC LIMIT 1
|
||||
) wo ON TRUE
|
||||
LEFT JOIN fulfilled ON fulfilled.order_item_id = oi.id
|
||||
@@ -267,6 +273,11 @@ export async function confirmManualFulfillment(payload: JsonObject, session: Adm
|
||||
|
||||
const note = optionalText(payload.note, 300)
|
||||
const payeeName = optionalText(payload.payeeName ?? payload.payee_name, 120)
|
||||
const fulfillmentReference = buildManualFulfillmentReference({
|
||||
workOrderNo: row.work_order_no,
|
||||
platformOrderId: row.platform_order_id,
|
||||
orderItemId,
|
||||
})
|
||||
|
||||
// 1. 先扣额度(积分平台走积分钱包;备用金现金走现有 fund_* 链路)。
|
||||
let creditEntryId: number | null = null
|
||||
@@ -288,7 +299,12 @@ export async function confirmManualFulfillment(payload: JsonObject, session: Adm
|
||||
orderItemId,
|
||||
paidAt: now,
|
||||
evidence: [],
|
||||
note: buildFundNote(row.sku_name, note),
|
||||
note: buildManualFulfillmentNote({
|
||||
skuName: row.sku_name,
|
||||
reference: fulfillmentReference,
|
||||
note,
|
||||
maxLength: 500,
|
||||
}),
|
||||
actorUserId: session.userId,
|
||||
})
|
||||
})
|
||||
@@ -300,7 +316,13 @@ export async function confirmManualFulfillment(payload: JsonObject, session: Adm
|
||||
platformKey: platformKey as PlatformCreditKey,
|
||||
quantity: creditQuantity,
|
||||
relatedTaskId: null,
|
||||
note: buildCreditNote(row.sku_name, note),
|
||||
platformRef: row.work_order_no || row.platform_order_id,
|
||||
note: buildManualFulfillmentNote({
|
||||
skuName: row.sku_name,
|
||||
reference: fulfillmentReference,
|
||||
note,
|
||||
maxLength: 300,
|
||||
}),
|
||||
})
|
||||
creditEntryId = consumed.entryId
|
||||
balanceAfter = consumed.balanceAfter
|
||||
@@ -500,18 +522,6 @@ function mapPending(row: PendingRow) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildFundNote(skuName: string, note: string) {
|
||||
const parts = [`人工履约 ${skuName}`.trim()]
|
||||
if (note) parts.push(note)
|
||||
return parts.join(' · ').slice(0, 500)
|
||||
}
|
||||
|
||||
function buildCreditNote(skuName: string, note: string) {
|
||||
const parts = [`人工履约 ${skuName}`.trim()]
|
||||
if (note) parts.push(note)
|
||||
return parts.join(' · ').slice(0, 300)
|
||||
}
|
||||
|
||||
function normalizeCostFen(value: unknown) {
|
||||
const parsed = Number(value)
|
||||
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||
|
||||
Reference in New Issue
Block a user