拆分接单平台规则
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
export type AfterSalesRecoveryAllocation = {
|
||||
pendingDepositDeductionAmount: number
|
||||
availableDeductionAmount: number
|
||||
debtAmount: number
|
||||
}
|
||||
|
||||
export function resolveAfterSalesRecoveryAllocation(input: {
|
||||
recoveryAmount: number
|
||||
pendingDepositAmount: number
|
||||
availableAmount: number
|
||||
}): AfterSalesRecoveryAllocation {
|
||||
let remaining = Math.max(0, Number(input.recoveryAmount) || 0)
|
||||
const pendingDepositDeductionAmount = Math.min(
|
||||
remaining,
|
||||
Math.max(0, Number(input.pendingDepositAmount) || 0),
|
||||
)
|
||||
remaining -= pendingDepositDeductionAmount
|
||||
const availableDeductionAmount = Math.min(
|
||||
remaining,
|
||||
Math.max(0, Number(input.availableAmount) || 0),
|
||||
)
|
||||
remaining -= availableDeductionAmount
|
||||
return {
|
||||
pendingDepositDeductionAmount,
|
||||
availableDeductionAmount,
|
||||
debtAmount: remaining,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PoolClient } from 'pg'
|
||||
|
||||
import { query, withTransaction } from '../../db/client.js'
|
||||
import { resolveAfterSalesRecoveryAllocation } from './after-sales-recovery.js'
|
||||
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js'
|
||||
import type {
|
||||
WorkerAfterSalesCaseEventRow,
|
||||
@@ -426,8 +427,6 @@ async function recoverAfterSalesAmountWithClient(
|
||||
availableDeductionAmount: number
|
||||
debtAmount: number
|
||||
}> {
|
||||
let remaining = Math.max(0, input.amount)
|
||||
let pendingDepositDeductionAmount = 0
|
||||
const pendingResult = await client.query<WorkerDepositUnfreezeRow>(
|
||||
`
|
||||
SELECT * FROM worker_deposit_unfreezes
|
||||
@@ -439,10 +438,19 @@ async function recoverAfterSalesAmountWithClient(
|
||||
)
|
||||
await ensureWorkerWalletWithClient(client, input.workerId, input.now)
|
||||
let wallet: WorkerWalletRow | null = await getWorkerWalletWithClient(client, input.workerId)
|
||||
const allocation = resolveAfterSalesRecoveryAllocation({
|
||||
recoveryAmount: input.amount,
|
||||
pendingDepositAmount: pendingResult.rows.reduce(
|
||||
(total, row) => total + Math.max(0, Number(row.amount) || 0),
|
||||
0,
|
||||
),
|
||||
availableAmount: Number(wallet?.available_amount || 0),
|
||||
})
|
||||
let pendingRemaining = allocation.pendingDepositDeductionAmount
|
||||
for (const row of pendingResult.rows) {
|
||||
if (remaining <= 0) break
|
||||
const rowAmount = Number(row.amount || 0)
|
||||
const deduction = Math.min(rowAmount, remaining)
|
||||
if (pendingRemaining <= 0) break
|
||||
const rowAmount = Math.max(0, Number(row.amount) || 0)
|
||||
const deduction = Math.min(rowAmount, pendingRemaining)
|
||||
if (deduction <= 0) continue
|
||||
if (deduction >= rowAmount) {
|
||||
await client.query(
|
||||
@@ -478,12 +486,11 @@ async function recoverAfterSalesAmountWithClient(
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
pendingDepositDeductionAmount += deduction
|
||||
remaining -= deduction
|
||||
pendingRemaining -= deduction
|
||||
}
|
||||
|
||||
wallet = await getWorkerWalletWithClient(client, input.workerId)
|
||||
const availableDeductionAmount = Math.min(remaining, Number(wallet?.available_amount || 0))
|
||||
const availableDeductionAmount = allocation.availableDeductionAmount
|
||||
if (availableDeductionAmount > 0) {
|
||||
const nextAvailable = Number(wallet?.available_amount || 0) - availableDeductionAmount
|
||||
await client.query(
|
||||
@@ -507,19 +514,18 @@ async function recoverAfterSalesAmountWithClient(
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
remaining -= availableDeductionAmount
|
||||
}
|
||||
if (remaining > 0) {
|
||||
if (allocation.debtAmount > 0) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_after_sales_debts (
|
||||
case_id, worker_id, original_amount, outstanding_amount, status, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $3, 'open', $4, $4)
|
||||
`,
|
||||
[input.caseId, input.workerId, remaining, input.now],
|
||||
[input.caseId, input.workerId, allocation.debtAmount, input.now],
|
||||
)
|
||||
}
|
||||
return { pendingDepositDeductionAmount, availableDeductionAmount, debtAmount: remaining }
|
||||
return allocation
|
||||
}
|
||||
|
||||
async function createAfterSalesCaseEventWithClient(
|
||||
|
||||
@@ -37,6 +37,23 @@ import {
|
||||
resolveVisibleDelaySeconds,
|
||||
resolveWorkerPermissions,
|
||||
} from './worker-permission-utils.js'
|
||||
import {
|
||||
normalizeRequirementFields,
|
||||
resolveRequirementFields,
|
||||
} from './work-order-requirement-fields.js'
|
||||
export {
|
||||
normalizeRequirementFields,
|
||||
normalizeRequirementFieldsFromPayload,
|
||||
normalizeRequirementFieldsText,
|
||||
resolveRequirementFields,
|
||||
type RequirementField,
|
||||
} from './work-order-requirement-fields.js'
|
||||
export {
|
||||
isWorkOrderSharingFilled,
|
||||
resolveSkuNameQuantity,
|
||||
resolveWorkOrderRulePricing,
|
||||
resolveWorkOrderSharingEnabled,
|
||||
} from './work-order-pricing.js'
|
||||
|
||||
export {
|
||||
ensureWorkerAuthConfigured,
|
||||
@@ -696,158 +713,6 @@ export function mapWallet(wallet: Awaited<ReturnType<typeof addWorkerWalletCredi
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveRequirementFields(workOrder: WorkOrderRow) {
|
||||
return normalizeRequirementFields(safeParseJson(workOrder.requirement_json).fields)
|
||||
}
|
||||
|
||||
export type RequirementField = {
|
||||
key: string
|
||||
label: string
|
||||
required: boolean
|
||||
type: 'text' | 'select'
|
||||
options: string[]
|
||||
mockValue: string
|
||||
}
|
||||
|
||||
export function normalizeRequirementFields(value: unknown) {
|
||||
const rawFields = Array.isArray(value) ? value : []
|
||||
const fields = rawFields
|
||||
.map((item) => {
|
||||
const source =
|
||||
item && typeof item === 'object' && !Array.isArray(item) ? (item as JsonObject) : {}
|
||||
const key = String(source.key || '').trim()
|
||||
if (!key) return null
|
||||
const type = source.type === 'select' ? 'select' : 'text'
|
||||
const options = Array.isArray(source.options)
|
||||
? source.options.map((option) => String(option || '').trim()).filter(Boolean)
|
||||
: []
|
||||
return {
|
||||
key,
|
||||
label: String(source.label || key).trim(),
|
||||
required: source.required !== false,
|
||||
type,
|
||||
options,
|
||||
mockValue: String(source.mockValue || '').trim(),
|
||||
}
|
||||
})
|
||||
.filter((item): item is RequirementField => Boolean(item))
|
||||
|
||||
if (fields.length > 0 && !isDefaultTemplateRequirementFields(fields)) {
|
||||
return fields
|
||||
}
|
||||
return [
|
||||
{
|
||||
key: 'gameId',
|
||||
label: '游戏编号',
|
||||
required: true,
|
||||
type: 'text',
|
||||
options: [],
|
||||
mockValue: 'test_uid',
|
||||
},
|
||||
{
|
||||
key: 'gameNickname',
|
||||
label: '游戏昵称',
|
||||
required: true,
|
||||
type: 'text',
|
||||
options: [],
|
||||
mockValue: '测试角色',
|
||||
},
|
||||
{
|
||||
key: 'system',
|
||||
label: '系统',
|
||||
required: false,
|
||||
type: 'select',
|
||||
options: ['安卓', '苹果'],
|
||||
mockValue: '安卓',
|
||||
},
|
||||
{
|
||||
key: 'serverZone',
|
||||
label: '区服',
|
||||
required: false,
|
||||
type: 'select',
|
||||
options: ['QQ区', '微信区'],
|
||||
mockValue: 'QQ区',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function isDefaultTemplateRequirementFields(fields: RequirementField[]): boolean {
|
||||
if (fields.length === 0) return true
|
||||
const defaultKeys = new Set(['gameId', 'gameNickname', 'system', 'serverZone'])
|
||||
const legacyKeys = new Set(['gameAccount', 'serverName', 'roleName'])
|
||||
return fields.every((field) => defaultKeys.has(field.key) || legacyKeys.has(field.key))
|
||||
}
|
||||
|
||||
export function normalizeRequirementFieldsFromPayload(payload: JsonObject) {
|
||||
const directFields = Array.isArray(payload.fields) ? payload.fields : null
|
||||
const requirement =
|
||||
payload.requirement &&
|
||||
typeof payload.requirement === 'object' &&
|
||||
!Array.isArray(payload.requirement)
|
||||
? (payload.requirement as JsonObject)
|
||||
: {}
|
||||
if (directFields) {
|
||||
return normalizeRequirementFields(directFields)
|
||||
}
|
||||
if (Array.isArray(requirement.fields)) {
|
||||
return normalizeRequirementFields(requirement.fields)
|
||||
}
|
||||
|
||||
const textFields = normalizeRequirementFieldsText(
|
||||
payload.fieldsText || payload.requiredFieldsText,
|
||||
)
|
||||
return textFields.length > 0 ? textFields : normalizeRequirementFields([])
|
||||
}
|
||||
|
||||
export function normalizeRequirementFieldsText(value: unknown) {
|
||||
return String(value || '')
|
||||
.split(/\r?\n/)
|
||||
.map((line, index) => {
|
||||
const text = line.trim()
|
||||
if (!text) return null
|
||||
const delimiter = text.includes(':') ? ':' : ':'
|
||||
if (text.includes(delimiter)) {
|
||||
const [rawKey, ...labelParts] = text.split(delimiter)
|
||||
const key = String(rawKey || '').trim()
|
||||
if (!key) return null
|
||||
const labelWithOptions = labelParts.join(delimiter).trim() || key
|
||||
const [label, rawOptions] = splitFieldOptions(labelWithOptions)
|
||||
return {
|
||||
key,
|
||||
label: label || key,
|
||||
required: true,
|
||||
type: rawOptions.length > 0 ? 'select' : 'text',
|
||||
options: rawOptions,
|
||||
mockValue: '',
|
||||
}
|
||||
}
|
||||
const [plainLabel, plainOptions] = splitFieldOptions(text)
|
||||
return {
|
||||
key: `field${index + 1}`,
|
||||
label: plainLabel,
|
||||
required: true,
|
||||
type: plainOptions.length > 0 ? 'select' : 'text',
|
||||
options: plainOptions,
|
||||
mockValue: '',
|
||||
}
|
||||
})
|
||||
.filter((item): item is RequirementField => Boolean(item))
|
||||
}
|
||||
|
||||
function splitFieldOptions(value: string): [string, string[]] {
|
||||
const text = String(value || '').trim()
|
||||
if (!text.includes('#')) {
|
||||
return [text, []]
|
||||
}
|
||||
const [label, ...optionParts] = text.split('#')
|
||||
const options = optionParts
|
||||
.join('#')
|
||||
.split(/[,,、]/)
|
||||
.map((option) => String(option || '').trim())
|
||||
.filter(Boolean)
|
||||
return [String(label || '').trim(), options]
|
||||
}
|
||||
|
||||
export function resolveMatchingProductRule(
|
||||
order: OrderRow,
|
||||
item: OrderItemRow,
|
||||
@@ -1208,110 +1073,6 @@ export function normalizeWorkProductRuleMatch(payload: JsonObject): JsonObject {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 从商品名称文字中解析规格数量(二次匹配数量)。
|
||||
* 支持阿拉伯数字与中文数字,如:指挥官秘钥1个 → 1、指挥官秘钥15个 → 15、指挥官秘钥三个 → 3。
|
||||
* 解析不到时返回 1。
|
||||
*/
|
||||
export function resolveSkuNameQuantity(productName: unknown): number {
|
||||
const text = String(productName || '').trim()
|
||||
if (!text) return 1
|
||||
|
||||
const arabicMatch = text.match(/(\d+)\s*(?:个|份|张|枚)/)
|
||||
if (arabicMatch) {
|
||||
const parsed = Number(arabicMatch[1])
|
||||
if (Number.isSafeInteger(parsed) && parsed > 0) return parsed
|
||||
}
|
||||
|
||||
const chineseMatch = text.match(/([零一二三四五六七八九十百千两]+)\s*(?:个|份|张|枚)/)
|
||||
if (chineseMatch?.[1]) {
|
||||
const parsed = parseChineseNumber(chineseMatch[1])
|
||||
if (parsed > 0) return parsed
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
export function resolveWorkOrderRulePricing(input: {
|
||||
skuQuantity: number
|
||||
unitPriceFen: number
|
||||
fixedRewardAmount: number
|
||||
sharingEnabled: boolean
|
||||
sharingAutoFromOrder: boolean
|
||||
sharingTotalQuantity: number
|
||||
sharingUnitReward: number
|
||||
}) {
|
||||
const skuQuantity = Math.max(1, Number(input.skuQuantity) || 1)
|
||||
const unitPriceFen = Math.max(0, Number(input.unitPriceFen) || 0)
|
||||
const sharingTotalQuantity = input.sharingAutoFromOrder
|
||||
? skuQuantity
|
||||
: Math.max(1, Number(input.sharingTotalQuantity) || 1)
|
||||
const sharingUnitReward = input.sharingAutoFromOrder
|
||||
? unitPriceFen
|
||||
: Math.max(0, Number(input.sharingUnitReward) || 0)
|
||||
const rewardAmount = input.sharingEnabled
|
||||
? sharingTotalQuantity * sharingUnitReward
|
||||
: unitPriceFen > 0
|
||||
? unitPriceFen * skuQuantity
|
||||
: Math.max(0, Number(input.fixedRewardAmount) || 0)
|
||||
|
||||
return { sharingTotalQuantity, sharingUnitReward, rewardAmount }
|
||||
}
|
||||
|
||||
/** 接单模板仅在实际总份数大于 1 时生成拼单工单。 */
|
||||
export function resolveWorkOrderSharingEnabled(
|
||||
configuredEnabled: boolean,
|
||||
totalQuantity: number,
|
||||
): boolean {
|
||||
return configuredEnabled && Math.max(1, Number(totalQuantity) || 1) > 1
|
||||
}
|
||||
|
||||
/** 判断拼单工单是否已经没有剩余可承接份数。 */
|
||||
export function isWorkOrderSharingFilled(
|
||||
sharingEnabled: boolean,
|
||||
totalQuantity: number,
|
||||
joinedQuantity: number,
|
||||
): boolean {
|
||||
return (
|
||||
sharingEnabled &&
|
||||
Math.max(0, Number(joinedQuantity) || 0) >= Math.max(1, Number(totalQuantity) || 1)
|
||||
)
|
||||
}
|
||||
|
||||
const CHINESE_DIGITS: Record<string, number> = {
|
||||
零: 0,
|
||||
一: 1,
|
||||
二: 2,
|
||||
两: 2,
|
||||
三: 3,
|
||||
四: 4,
|
||||
五: 5,
|
||||
六: 6,
|
||||
七: 7,
|
||||
八: 8,
|
||||
九: 9,
|
||||
}
|
||||
|
||||
function parseChineseNumber(text: string): number {
|
||||
let section = 0
|
||||
let digit = 0
|
||||
for (const char of text) {
|
||||
if (char === '十') {
|
||||
section += (digit || 1) * 10
|
||||
digit = 0
|
||||
} else if (char === '百') {
|
||||
section += (digit || 1) * 100
|
||||
digit = 0
|
||||
} else if (char === '千') {
|
||||
section += (digit || 1) * 1000
|
||||
digit = 0
|
||||
} else if (char in CHINESE_DIGITS) {
|
||||
digit = CHINESE_DIGITS[char] ?? 0
|
||||
}
|
||||
}
|
||||
return section + digit
|
||||
}
|
||||
|
||||
export function matchesOptionalText(ruleValue: unknown, sourceValue: unknown) {
|
||||
const ruleText = String(ruleValue || '')
|
||||
.trim()
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
export function resolveSkuNameQuantity(productName: unknown): number {
|
||||
const text = String(productName || '').trim()
|
||||
if (!text) return 1
|
||||
|
||||
const arabicMatch = text.match(/(\d+)\s*(?:个|份|张|枚)/)
|
||||
if (arabicMatch) {
|
||||
const parsed = Number(arabicMatch[1])
|
||||
if (Number.isSafeInteger(parsed) && parsed > 0) return parsed
|
||||
}
|
||||
|
||||
const chineseMatch = text.match(/([零一二三四五六七八九十百千两]+)\s*(?:个|份|张|枚)/)
|
||||
if (chineseMatch?.[1]) {
|
||||
const parsed = parseChineseNumber(chineseMatch[1])
|
||||
if (parsed > 0) return parsed
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
export function resolveWorkOrderRulePricing(input: {
|
||||
skuQuantity: number
|
||||
unitPriceFen: number
|
||||
fixedRewardAmount: number
|
||||
sharingEnabled: boolean
|
||||
sharingAutoFromOrder: boolean
|
||||
sharingTotalQuantity: number
|
||||
sharingUnitReward: number
|
||||
}) {
|
||||
const skuQuantity = Math.max(1, Number(input.skuQuantity) || 1)
|
||||
const unitPriceFen = Math.max(0, Number(input.unitPriceFen) || 0)
|
||||
const sharingTotalQuantity = input.sharingAutoFromOrder
|
||||
? skuQuantity
|
||||
: Math.max(1, Number(input.sharingTotalQuantity) || 1)
|
||||
const sharingUnitReward = input.sharingAutoFromOrder
|
||||
? unitPriceFen
|
||||
: Math.max(0, Number(input.sharingUnitReward) || 0)
|
||||
const rewardAmount = input.sharingEnabled
|
||||
? sharingTotalQuantity * sharingUnitReward
|
||||
: unitPriceFen > 0
|
||||
? unitPriceFen * skuQuantity
|
||||
: Math.max(0, Number(input.fixedRewardAmount) || 0)
|
||||
|
||||
return { sharingTotalQuantity, sharingUnitReward, rewardAmount }
|
||||
}
|
||||
|
||||
export function resolveWorkOrderSharingEnabled(
|
||||
configuredEnabled: boolean,
|
||||
totalQuantity: number,
|
||||
): boolean {
|
||||
return configuredEnabled && Math.max(1, Number(totalQuantity) || 1) > 1
|
||||
}
|
||||
|
||||
export function isWorkOrderSharingFilled(
|
||||
sharingEnabled: boolean,
|
||||
totalQuantity: number,
|
||||
joinedQuantity: number,
|
||||
): boolean {
|
||||
return (
|
||||
sharingEnabled &&
|
||||
Math.max(0, Number(joinedQuantity) || 0) >= Math.max(1, Number(totalQuantity) || 1)
|
||||
)
|
||||
}
|
||||
|
||||
const CHINESE_DIGITS: Record<string, number> = {
|
||||
零: 0,
|
||||
一: 1,
|
||||
二: 2,
|
||||
两: 2,
|
||||
三: 3,
|
||||
四: 4,
|
||||
五: 5,
|
||||
六: 6,
|
||||
七: 7,
|
||||
八: 8,
|
||||
九: 9,
|
||||
}
|
||||
|
||||
function parseChineseNumber(text: string): number {
|
||||
let section = 0
|
||||
let digit = 0
|
||||
for (const char of text) {
|
||||
if (char === '十') {
|
||||
section += (digit || 1) * 10
|
||||
digit = 0
|
||||
} else if (char === '百') {
|
||||
section += (digit || 1) * 100
|
||||
digit = 0
|
||||
} else if (char === '千') {
|
||||
section += (digit || 1) * 1000
|
||||
digit = 0
|
||||
} else if (char in CHINESE_DIGITS) {
|
||||
digit = CHINESE_DIGITS[char] ?? 0
|
||||
}
|
||||
}
|
||||
return section + digit
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import type { WorkOrderRow } from '../../repositories/worker-platform/index.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import { safeParseJson } from '../admin/admin-query-utils.js'
|
||||
|
||||
export type RequirementField = {
|
||||
key: string
|
||||
label: string
|
||||
required: boolean
|
||||
type: 'text' | 'select'
|
||||
options: string[]
|
||||
mockValue: string
|
||||
}
|
||||
|
||||
export function resolveRequirementFields(workOrder: WorkOrderRow) {
|
||||
return normalizeRequirementFields(safeParseJson(workOrder.requirement_json).fields)
|
||||
}
|
||||
|
||||
export function normalizeRequirementFields(value: unknown) {
|
||||
const rawFields = Array.isArray(value) ? value : []
|
||||
const fields = rawFields
|
||||
.map((item) => {
|
||||
const source =
|
||||
item && typeof item === 'object' && !Array.isArray(item) ? (item as JsonObject) : {}
|
||||
const key = String(source.key || '').trim()
|
||||
if (!key) return null
|
||||
const type = source.type === 'select' ? 'select' : 'text'
|
||||
const options = Array.isArray(source.options)
|
||||
? source.options.map((option) => String(option || '').trim()).filter(Boolean)
|
||||
: []
|
||||
return {
|
||||
key,
|
||||
label: String(source.label || key).trim(),
|
||||
required: source.required !== false,
|
||||
type,
|
||||
options,
|
||||
mockValue: String(source.mockValue || '').trim(),
|
||||
}
|
||||
})
|
||||
.filter((item): item is RequirementField => Boolean(item))
|
||||
|
||||
if (fields.length > 0 && !isDefaultTemplateRequirementFields(fields)) {
|
||||
return fields
|
||||
}
|
||||
return [
|
||||
{
|
||||
key: 'gameId',
|
||||
label: '游戏编号',
|
||||
required: true,
|
||||
type: 'text',
|
||||
options: [],
|
||||
mockValue: 'test_uid',
|
||||
},
|
||||
{
|
||||
key: 'gameNickname',
|
||||
label: '游戏昵称',
|
||||
required: true,
|
||||
type: 'text',
|
||||
options: [],
|
||||
mockValue: '测试角色',
|
||||
},
|
||||
{
|
||||
key: 'system',
|
||||
label: '系统',
|
||||
required: false,
|
||||
type: 'select',
|
||||
options: ['安卓', '苹果'],
|
||||
mockValue: '安卓',
|
||||
},
|
||||
{
|
||||
key: 'serverZone',
|
||||
label: '区服',
|
||||
required: false,
|
||||
type: 'select',
|
||||
options: ['QQ区', '微信区'],
|
||||
mockValue: 'QQ区',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function isDefaultTemplateRequirementFields(fields: RequirementField[]): boolean {
|
||||
if (fields.length === 0) return true
|
||||
const defaultKeys = new Set(['gameId', 'gameNickname', 'system', 'serverZone'])
|
||||
const legacyKeys = new Set(['gameAccount', 'serverName', 'roleName'])
|
||||
return fields.every((field) => defaultKeys.has(field.key) || legacyKeys.has(field.key))
|
||||
}
|
||||
|
||||
export function normalizeRequirementFieldsFromPayload(payload: JsonObject) {
|
||||
const directFields = Array.isArray(payload.fields) ? payload.fields : null
|
||||
const requirement =
|
||||
payload.requirement &&
|
||||
typeof payload.requirement === 'object' &&
|
||||
!Array.isArray(payload.requirement)
|
||||
? (payload.requirement as JsonObject)
|
||||
: {}
|
||||
if (directFields) {
|
||||
return normalizeRequirementFields(directFields)
|
||||
}
|
||||
if (Array.isArray(requirement.fields)) {
|
||||
return normalizeRequirementFields(requirement.fields)
|
||||
}
|
||||
|
||||
const textFields = normalizeRequirementFieldsText(
|
||||
payload.fieldsText || payload.requiredFieldsText,
|
||||
)
|
||||
return textFields.length > 0 ? textFields : normalizeRequirementFields([])
|
||||
}
|
||||
|
||||
export function normalizeRequirementFieldsText(value: unknown) {
|
||||
return String(value || '')
|
||||
.split(/\r?\n/)
|
||||
.map((line, index) => {
|
||||
const text = line.trim()
|
||||
if (!text) return null
|
||||
const delimiter = text.includes(':') ? ':' : ':'
|
||||
if (text.includes(delimiter)) {
|
||||
const [rawKey, ...labelParts] = text.split(delimiter)
|
||||
const key = String(rawKey || '').trim()
|
||||
if (!key) return null
|
||||
const labelWithOptions = labelParts.join(delimiter).trim() || key
|
||||
const [label, rawOptions] = splitFieldOptions(labelWithOptions)
|
||||
return {
|
||||
key,
|
||||
label: label || key,
|
||||
required: true,
|
||||
type: rawOptions.length > 0 ? 'select' : 'text',
|
||||
options: rawOptions,
|
||||
mockValue: '',
|
||||
}
|
||||
}
|
||||
const [plainLabel, plainOptions] = splitFieldOptions(text)
|
||||
return {
|
||||
key: `field${index + 1}`,
|
||||
label: plainLabel,
|
||||
required: true,
|
||||
type: plainOptions.length > 0 ? 'select' : 'text',
|
||||
options: plainOptions,
|
||||
mockValue: '',
|
||||
}
|
||||
})
|
||||
.filter((item): item is RequirementField => Boolean(item))
|
||||
}
|
||||
|
||||
function splitFieldOptions(value: string): [string, string[]] {
|
||||
const text = String(value || '').trim()
|
||||
if (!text.includes('#')) {
|
||||
return [text, []]
|
||||
}
|
||||
const [label, ...optionParts] = text.split('#')
|
||||
const options = optionParts
|
||||
.join('#')
|
||||
.split(/[,,、]/)
|
||||
.map((option) => String(option || '').trim())
|
||||
.filter(Boolean)
|
||||
return [String(label || '').trim(), options]
|
||||
}
|
||||
@@ -44,6 +44,52 @@ import {
|
||||
resolveWorkOrderSharingEnabled,
|
||||
validateWorkerPassword,
|
||||
} from './index.js'
|
||||
import { resolveAfterSalesRecoveryAllocation } from '../../repositories/worker-platform/after-sales-recovery.js'
|
||||
|
||||
test('售后追缴优先扣减待解冻押金', () => {
|
||||
assert.deepEqual(
|
||||
resolveAfterSalesRecoveryAllocation({
|
||||
recoveryAmount: 10_000,
|
||||
pendingDepositAmount: 12_000,
|
||||
availableAmount: 8_000,
|
||||
}),
|
||||
{
|
||||
pendingDepositDeductionAmount: 10_000,
|
||||
availableDeductionAmount: 0,
|
||||
debtAmount: 0,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('售后追缴在押金不足时继续扣减可用余额', () => {
|
||||
assert.deepEqual(
|
||||
resolveAfterSalesRecoveryAllocation({
|
||||
recoveryAmount: 10_000,
|
||||
pendingDepositAmount: 3_000,
|
||||
availableAmount: 8_000,
|
||||
}),
|
||||
{
|
||||
pendingDepositDeductionAmount: 3_000,
|
||||
availableDeductionAmount: 7_000,
|
||||
debtAmount: 0,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('售后追缴在余额不足时形成欠款', () => {
|
||||
assert.deepEqual(
|
||||
resolveAfterSalesRecoveryAllocation({
|
||||
recoveryAmount: 10_000,
|
||||
pendingDepositAmount: 3_000,
|
||||
availableAmount: 2_000,
|
||||
}),
|
||||
{
|
||||
pendingDepositDeductionAmount: 3_000,
|
||||
availableDeductionAmount: 2_000,
|
||||
debtAmount: 5_000,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
function buildOrderItemRow(): OrderItemRow {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user