fix(admin): 恢复接单模板计价配置并统一成本价格链路
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
-- 接单模板显式保存计价方式,金额字段只由商品与成本中心写投影。
|
||||
-- 历史数据按旧运行口径回填:拼单自动按件优先识别,其次识别“单价>0 且固定金额=0”。
|
||||
|
||||
ALTER TABLE work_product_rules
|
||||
ADD COLUMN IF NOT EXISTS pricing_mode TEXT;
|
||||
|
||||
UPDATE work_product_rules
|
||||
SET pricing_mode = CASE
|
||||
WHEN sharing_enabled = TRUE
|
||||
AND sharing_auto_from_order = TRUE
|
||||
AND unit_price_fen > 0 THEN 'unit'
|
||||
WHEN unit_price_fen > 0 AND reward_amount = 0 THEN 'unit'
|
||||
ELSE 'fixed'
|
||||
END
|
||||
WHERE pricing_mode IS NULL
|
||||
OR pricing_mode NOT IN ('fixed', 'unit');
|
||||
|
||||
ALTER TABLE work_product_rules
|
||||
ALTER COLUMN pricing_mode SET DEFAULT 'fixed',
|
||||
ALTER COLUMN pricing_mode SET NOT NULL;
|
||||
|
||||
-- 自动拼单只能依赖每件金额;固定金额模板不能保留“随订单自动算拼单”的旧标志。
|
||||
UPDATE work_product_rules
|
||||
SET sharing_auto_from_order = FALSE
|
||||
WHERE pricing_mode = 'fixed'
|
||||
AND sharing_auto_from_order = TRUE;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'chk_work_product_rules_pricing_mode'
|
||||
AND conrelid = 'work_product_rules'::regclass
|
||||
) THEN
|
||||
ALTER TABLE work_product_rules
|
||||
ADD CONSTRAINT chk_work_product_rules_pricing_mode
|
||||
CHECK (pricing_mode IN ('fixed', 'unit'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMENT ON COLUMN work_product_rules.pricing_mode IS
|
||||
'接单计价方式:fixed 固定整单金额 / unit 按 SKU 数量乘每件金额';
|
||||
|
||||
-- 同步模板商品属性中的计价方式,避免继续从两个金额字段反推。
|
||||
UPDATE product_catalog p
|
||||
SET attributes_json = COALESCE(p.attributes_json, '{}'::jsonb) || jsonb_build_object(
|
||||
'pricingMode', wpr.pricing_mode,
|
||||
'rewardAmountFen', wpr.reward_amount,
|
||||
'unitPriceFen', wpr.unit_price_fen
|
||||
),
|
||||
updated_at = NOW()
|
||||
FROM work_product_rules wpr
|
||||
WHERE p.canonical_sku = LEFT('tpl:' || BTRIM(wpr.rule_key), 64);
|
||||
@@ -288,6 +288,7 @@ export type WorkProductRuleRow = {
|
||||
auto_create: boolean
|
||||
reward_amount: number
|
||||
unit_price_fen: number
|
||||
pricing_mode: 'fixed' | 'unit'
|
||||
required_deposit_amount: number
|
||||
deposit_threshold_amount: number
|
||||
sharing_enabled: boolean
|
||||
|
||||
@@ -356,6 +356,7 @@ export async function upsertWorkProductRule(input: {
|
||||
autoCreate: boolean
|
||||
rewardAmount: number
|
||||
unitPriceFen?: number
|
||||
pricingMode: 'fixed' | 'unit'
|
||||
requiredDepositAmount: number
|
||||
depositThresholdAmount: number
|
||||
sharingEnabled?: boolean
|
||||
@@ -385,7 +386,7 @@ export async function upsertWorkProductRule(input: {
|
||||
timeout_minutes, timeout_policy,
|
||||
acceptance_mode, gift_cooldown_hours, gift_cooldown_minutes,
|
||||
requirement_json, match_json,
|
||||
sort_order, created_at, updated_at
|
||||
sort_order, pricing_mode, created_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
$7, $8, $9, $10, $11,
|
||||
@@ -395,7 +396,7 @@ export async function upsertWorkProductRule(input: {
|
||||
$19, $20,
|
||||
$21, $22, $23,
|
||||
$24::jsonb, $25::jsonb,
|
||||
$26, $27, $28
|
||||
$26, $27, $28, $29
|
||||
)
|
||||
ON CONFLICT (rule_key) DO UPDATE
|
||||
SET
|
||||
@@ -424,6 +425,7 @@ export async function upsertWorkProductRule(input: {
|
||||
requirement_json = EXCLUDED.requirement_json,
|
||||
match_json = EXCLUDED.match_json,
|
||||
sort_order = EXCLUDED.sort_order,
|
||||
pricing_mode = EXCLUDED.pricing_mode,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
RETURNING *
|
||||
`,
|
||||
@@ -454,6 +456,7 @@ export async function upsertWorkProductRule(input: {
|
||||
input.requirementJson,
|
||||
input.matchJson || '{}',
|
||||
input.sortOrder,
|
||||
input.pricingMode,
|
||||
input.now,
|
||||
input.now,
|
||||
],
|
||||
|
||||
@@ -188,13 +188,11 @@ export function mapCostVersion(row: CostVersionRow) {
|
||||
const revenueFen = Number(row.revenue_fen || 0)
|
||||
const rewardAvgFen = Number(row.reward_avg_fen || 0)
|
||||
const rewardFen = Number(row.reward_fen || 0)
|
||||
const ruleUnitPriceFen = Number(row.rule_unit_price_fen || 0)
|
||||
const ruleRewardAmountFen = Number(row.rule_reward_amount || 0)
|
||||
const amountKind =
|
||||
row.cost_item_code === 'sys_work_order_reward' &&
|
||||
row.scope_type === 'rule' &&
|
||||
ruleUnitPriceFen > 0 &&
|
||||
ruleRewardAmountFen === 0
|
||||
row.work_product_rule_pricing_mode === 'unit'
|
||||
? 'unit_price'
|
||||
: 'fixed'
|
||||
return {
|
||||
@@ -214,7 +212,7 @@ export function mapCostVersion(row: CostVersionRow) {
|
||||
workProductRuleName: row.work_product_rule_name || '',
|
||||
workProductRuleKey: row.work_product_rule_key || '',
|
||||
ruleRewardAmountFen,
|
||||
ruleUnitPriceFen,
|
||||
ruleUnitPriceFen: Number(row.rule_unit_price_fen || 0),
|
||||
revenueItemCount,
|
||||
revenueFen,
|
||||
revenueAvgFen: revenueItemCount > 0 ? Math.round(revenueFen / revenueItemCount) : 0,
|
||||
|
||||
@@ -113,6 +113,7 @@ export type CostVersionRow = {
|
||||
work_product_rule_id: number | null
|
||||
work_product_rule_name: string | null
|
||||
work_product_rule_key: string | null
|
||||
work_product_rule_pricing_mode?: 'fixed' | 'unit' | null
|
||||
rule_reward_amount: number | null
|
||||
rule_unit_price_fen: number | null
|
||||
revenue_item_count: number | null
|
||||
|
||||
@@ -324,6 +324,7 @@ export async function listAdminCostVersions(payload: JsonObject = {}) {
|
||||
p.canonical_name,
|
||||
wpr.product_name AS work_product_rule_name,
|
||||
wpr.rule_key AS work_product_rule_key,
|
||||
wpr.pricing_mode AS work_product_rule_pricing_mode,
|
||||
wpr.reward_amount AS rule_reward_amount,
|
||||
wpr.unit_price_fen AS rule_unit_price_fen,
|
||||
rev.revenue_item_count,
|
||||
|
||||
@@ -103,6 +103,25 @@ test('成本解析:卡券渠道按件计,取业务日期前的最新版本',
|
||||
assert.equal(resolution.unitAmountFen, 5000)
|
||||
})
|
||||
|
||||
test('成本解析:接单模板按 SKU 计件时,预估成本按数量乘每件金额', () => {
|
||||
const resolution = resolveCost(
|
||||
item({
|
||||
fulfillment_channel: 'work_order',
|
||||
canonical_product_id: null,
|
||||
quantity: 2,
|
||||
work_product_rule_id: 9,
|
||||
work_product_rule_pricing_mode: 'unit',
|
||||
}),
|
||||
costItems,
|
||||
versions,
|
||||
pricingModes,
|
||||
)
|
||||
assert.ok(resolution)
|
||||
assert.equal(resolution.costVersionId, 101)
|
||||
assert.equal(resolution.unitAmountFen, 840)
|
||||
assert.equal(resolution.totalAmountFen, 1680)
|
||||
})
|
||||
|
||||
test('成本解析:生效日期晚于业务日期的版本不参与', () => {
|
||||
const version = selectCostVersion(versions, {
|
||||
costItemId: 4,
|
||||
|
||||
@@ -62,6 +62,7 @@ export type CostCandidateItem = {
|
||||
basis_at: string
|
||||
work_order_id: number | null
|
||||
work_product_rule_id: number | null
|
||||
work_product_rule_pricing_mode?: 'fixed' | 'unit' | null
|
||||
work_order_status: string | null
|
||||
reward_amount: number | null
|
||||
revenue_fen: number
|
||||
@@ -157,7 +158,14 @@ export function resolveCost(
|
||||
})
|
||||
if (!version) return null
|
||||
|
||||
const pricingMode = pricingModeByItemId.get(costItem.id) || costItem.pricing_mode
|
||||
const pricingMode =
|
||||
item.fulfillment_channel === 'work_order' &&
|
||||
costItem.code === 'sys_work_order_reward' &&
|
||||
item.work_product_rule_pricing_mode
|
||||
? item.work_product_rule_pricing_mode === 'unit'
|
||||
? 'per_item'
|
||||
: 'per_order'
|
||||
: pricingModeByItemId.get(costItem.id) || costItem.pricing_mode
|
||||
const amount = Number(version.amount_fen) || 0
|
||||
const quantity = Math.max(1, Number(item.quantity) || 1)
|
||||
|
||||
@@ -306,11 +314,13 @@ const CANDIDATE_SQL = `
|
||||
), 0)::bigint AS refund_fen,
|
||||
wo.id AS work_order_id,
|
||||
wo.product_rule_id AS work_product_rule_id,
|
||||
wpr.pricing_mode AS work_product_rule_pricing_mode,
|
||||
wo.status AS work_order_status,
|
||||
wo.reward_amount
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
LEFT JOIN work_orders wo ON wo.order_item_id = oi.id
|
||||
LEFT JOIN work_product_rules wpr ON wpr.id = wo.product_rule_id
|
||||
WHERE oi.fulfillment_status = 'assigned'
|
||||
AND oi.fulfillment_channel IN (
|
||||
'affiliate_dash', 'third_party_voucher', 'work_order', 'manual'
|
||||
|
||||
@@ -153,11 +153,7 @@ export async function listAdminProducts(payload: JsonObject = {}) {
|
||||
`
|
||||
SELECT
|
||||
p.*,
|
||||
CASE
|
||||
WHEN tpl_rule.unit_price_fen > 0 AND tpl_rule.reward_amount = 0 THEN 'unit'
|
||||
WHEN tpl_rule.id IS NOT NULL THEN 'fixed'
|
||||
ELSE NULL
|
||||
END AS template_pricing_mode,
|
||||
tpl_rule.pricing_mode AS template_pricing_mode,
|
||||
tpl_rule.enabled AS template_enabled,
|
||||
tpl_rule.sharing_enabled AS template_sharing_enabled,
|
||||
(
|
||||
@@ -1551,10 +1547,11 @@ export async function setAdminProductPrice(
|
||||
enabled: boolean
|
||||
reward_amount: number
|
||||
unit_price_fen: number
|
||||
pricing_mode: 'fixed' | 'unit'
|
||||
sharing_enabled: boolean
|
||||
sharing_auto_from_order: boolean
|
||||
}>(
|
||||
`SELECT id, rule_key, product_name, enabled, reward_amount, unit_price_fen,
|
||||
`SELECT id, rule_key, product_name, enabled, reward_amount, unit_price_fen, pricing_mode,
|
||||
sharing_enabled, sharing_auto_from_order
|
||||
FROM work_product_rules
|
||||
WHERE rule_key = $1
|
||||
@@ -1569,17 +1566,13 @@ export async function setAdminProductPrice(
|
||||
})
|
||||
}
|
||||
const rawPricingMode = text(payload.pricingMode ?? payload.pricing_mode)
|
||||
if (rawPricingMode && rawPricingMode !== 'fixed' && rawPricingMode !== 'unit') {
|
||||
throw createHttpError('接单模板计价方式无效', {
|
||||
if (rawPricingMode) {
|
||||
throw createHttpError('接单模板计价方式只能在【接单模板】中设置', {
|
||||
statusCode: 400,
|
||||
errorCode: 'cost_work_order_pricing_mode_invalid',
|
||||
errorCode: 'cost_work_order_pricing_mode_managed_by_template',
|
||||
})
|
||||
}
|
||||
const pricingMode =
|
||||
rawPricingMode ||
|
||||
(Number(rule.unit_price_fen || 0) > 0 && Number(rule.reward_amount || 0) === 0
|
||||
? 'unit'
|
||||
: 'fixed')
|
||||
const pricingMode: 'fixed' | 'unit' = rule.pricing_mode === 'unit' ? 'unit' : 'fixed'
|
||||
if (pricingMode === 'fixed' && rule.sharing_enabled && rule.sharing_auto_from_order) {
|
||||
throw createHttpError(
|
||||
'拼单自动按件计价与固定金额冲突,请先在接单模板关闭拼单或改为手工拼单价',
|
||||
|
||||
@@ -128,6 +128,7 @@ export async function applyAdminTemplateProductBootstrap(
|
||||
skuNick: template.sku_nick,
|
||||
relSkuId: template.rel_sku_id,
|
||||
sellerIds: template.seller_ids_json,
|
||||
pricingMode: template.pricing_mode === 'unit' ? 'unit' : 'fixed',
|
||||
unitPriceFen: Number(template.unit_price_fen || 0),
|
||||
rewardAmountFen: Number(template.reward_amount || 0),
|
||||
templateEnabled: template.enabled,
|
||||
@@ -282,6 +283,7 @@ type TemplateBootstrapTemplateRow = {
|
||||
category: string
|
||||
unit_price_fen: number
|
||||
reward_amount: number
|
||||
pricing_mode: 'fixed' | 'unit'
|
||||
mapping_id: number | null
|
||||
mapping_type: string
|
||||
sku_nick: string
|
||||
@@ -696,6 +698,7 @@ async function loadTemplateProductBootstrapData(range: { fromDate: string; toDat
|
||||
COALESCE(c.name, '') AS category,
|
||||
r.unit_price_fen,
|
||||
r.reward_amount,
|
||||
r.pricing_mode,
|
||||
m.id AS mapping_id,
|
||||
COALESCE(m.mapping_type, '') AS mapping_type,
|
||||
COALESCE(m.sku_nick, '') AS sku_nick,
|
||||
|
||||
@@ -327,7 +327,7 @@ export async function syncAdminWorkOrderTemplateCostVersions(ruleId?: number) {
|
||||
SELECT
|
||||
wpr.id AS rule_id,
|
||||
CASE
|
||||
WHEN wpr.unit_price_fen > 0 AND wpr.reward_amount = 0 THEN wpr.unit_price_fen
|
||||
WHEN wpr.pricing_mode = 'unit' THEN wpr.unit_price_fen
|
||||
ELSE wpr.reward_amount
|
||||
END AS amount_fen
|
||||
FROM work_product_rules wpr
|
||||
@@ -364,7 +364,7 @@ export async function syncAdminWorkOrderTemplateCostVersions(ruleId?: number) {
|
||||
SELECT
|
||||
wpr.id AS rule_id,
|
||||
CASE
|
||||
WHEN wpr.unit_price_fen > 0 AND wpr.reward_amount = 0 THEN wpr.unit_price_fen
|
||||
WHEN wpr.pricing_mode = 'unit' THEN wpr.unit_price_fen
|
||||
ELSE wpr.reward_amount
|
||||
END AS amount_fen
|
||||
FROM work_product_rules wpr
|
||||
@@ -416,7 +416,7 @@ export async function syncAdminWorkOrderTemplateCostVersions(ruleId?: number) {
|
||||
'import',
|
||||
'active',
|
||||
CASE
|
||||
WHEN wpr.unit_price_fen > 0 AND wpr.reward_amount = 0
|
||||
WHEN wpr.pricing_mode = 'unit'
|
||||
THEN '模板按件发单单价(数量取订单 SKU)'
|
||||
ELSE '模板固定发单金额'
|
||||
END,
|
||||
@@ -469,7 +469,7 @@ export async function syncAdminWorkOrderTemplateCostVersions(ruleId?: number) {
|
||||
SELECT
|
||||
COUNT(*)::int AS rule_count,
|
||||
COUNT(*) FILTER (
|
||||
WHERE wpr.unit_price_fen > 0 AND wpr.reward_amount = 0
|
||||
WHERE wpr.pricing_mode = 'unit'
|
||||
)::int AS unit_price_rule_count,
|
||||
COUNT(v.id) FILTER (
|
||||
WHERE v.scope_type = 'rule' AND v.status = 'active'
|
||||
|
||||
@@ -45,6 +45,7 @@ test(
|
||||
assert.ok(ruleId > 0)
|
||||
assert.ok(productId > 0)
|
||||
assert.equal(created.rule.enabled, false)
|
||||
assert.equal(created.rule.pricingMode, 'fixed')
|
||||
assert.equal(created.costConfigurationRequired, true)
|
||||
|
||||
const product = await query<{ canonical_sku: string; status: string }>(
|
||||
@@ -77,7 +78,6 @@ test(
|
||||
productId,
|
||||
{
|
||||
amountFen: 1234,
|
||||
pricingMode: 'fixed',
|
||||
changeReason: '接单模板成本集成测试',
|
||||
syncOpenOrders: false,
|
||||
},
|
||||
@@ -100,10 +100,11 @@ test(
|
||||
enabled: boolean
|
||||
reward_amount: number
|
||||
unit_price_fen: number
|
||||
pricing_mode: string
|
||||
amount_fen: number
|
||||
}>(
|
||||
`
|
||||
SELECT wpr.enabled, wpr.reward_amount, wpr.unit_price_fen, v.amount_fen
|
||||
SELECT wpr.enabled, wpr.reward_amount, wpr.unit_price_fen, wpr.pricing_mode, v.amount_fen
|
||||
FROM work_product_rules wpr
|
||||
JOIN cost_versions v ON v.work_product_rule_id = wpr.id AND v.status = 'active'
|
||||
WHERE wpr.id = $1
|
||||
@@ -115,6 +116,7 @@ test(
|
||||
assert.equal(projection.rows[0]?.enabled, false)
|
||||
assert.equal(Number(projection.rows[0]?.reward_amount), 1234)
|
||||
assert.equal(Number(projection.rows[0]?.unit_price_fen), 0)
|
||||
assert.equal(projection.rows[0]?.pricing_mode, 'fixed')
|
||||
assert.equal(Number(projection.rows[0]?.amount_fen), 1234)
|
||||
|
||||
await assert.rejects(
|
||||
@@ -124,6 +126,30 @@ test(
|
||||
{
|
||||
amountFen: 2000,
|
||||
pricingMode: 'fixed',
|
||||
syncOpenOrders: false,
|
||||
},
|
||||
{
|
||||
sessionId: `it-${suffix}`,
|
||||
userId: Number(adminUser.id),
|
||||
username: adminUser.username,
|
||||
displayName: adminUser.display_name || adminUser.username,
|
||||
role: 'admin',
|
||||
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
||||
absoluteExpiresAt: new Date(Date.now() + 60_000).toISOString(),
|
||||
sessionVersion: 1,
|
||||
deviceId: 'integration-test',
|
||||
permissions: [],
|
||||
},
|
||||
),
|
||||
/计价方式只能在【接单模板】中设置/,
|
||||
)
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
setAdminProductPrice(
|
||||
productId,
|
||||
{
|
||||
amountFen: 2000,
|
||||
effectiveFrom: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
||||
syncOpenOrders: false,
|
||||
},
|
||||
@@ -143,11 +169,19 @@ test(
|
||||
/暂不支持未来生效/,
|
||||
)
|
||||
|
||||
const unitModeRule = await saveAdminWorkProductRule({
|
||||
ruleId,
|
||||
productName,
|
||||
pricingMode: 'unit',
|
||||
})
|
||||
assert.equal(unitModeRule.rule.pricingMode, 'unit')
|
||||
assert.equal(unitModeRule.rule.unitPriceFen, 1234)
|
||||
assert.equal(unitModeRule.rule.rewardAmount, 0)
|
||||
|
||||
await setAdminProductPrice(
|
||||
productId,
|
||||
{
|
||||
amountFen: 1300,
|
||||
pricingMode: 'unit',
|
||||
changeReason: '接单模板按件成本集成测试',
|
||||
syncOpenOrders: false,
|
||||
},
|
||||
@@ -167,10 +201,11 @@ test(
|
||||
const unitProjection = await query<{
|
||||
reward_amount: number
|
||||
unit_price_fen: number
|
||||
pricing_mode: string
|
||||
amount_fen: number
|
||||
}>(
|
||||
`
|
||||
SELECT wpr.reward_amount, wpr.unit_price_fen, v.amount_fen
|
||||
SELECT wpr.reward_amount, wpr.unit_price_fen, wpr.pricing_mode, v.amount_fen
|
||||
FROM work_product_rules wpr
|
||||
JOIN cost_versions v ON v.work_product_rule_id = wpr.id AND v.status = 'active'
|
||||
WHERE wpr.id = $1
|
||||
@@ -181,8 +216,18 @@ test(
|
||||
)
|
||||
assert.equal(Number(unitProjection.rows[0]?.reward_amount), 0)
|
||||
assert.equal(Number(unitProjection.rows[0]?.unit_price_fen), 1300)
|
||||
assert.equal(unitProjection.rows[0]?.pricing_mode, 'unit')
|
||||
assert.equal(Number(unitProjection.rows[0]?.amount_fen), 1300)
|
||||
|
||||
const fixedModeRule = await saveAdminWorkProductRule({
|
||||
ruleId,
|
||||
productName,
|
||||
pricingMode: 'fixed',
|
||||
})
|
||||
assert.equal(fixedModeRule.rule.pricingMode, 'fixed')
|
||||
assert.equal(fixedModeRule.rule.rewardAmount, 1300)
|
||||
assert.equal(fixedModeRule.rule.unitPriceFen, 0)
|
||||
|
||||
const enabled = await saveAdminWorkProductRule({
|
||||
ruleId,
|
||||
productName,
|
||||
|
||||
@@ -97,10 +97,7 @@ export async function syncWorkOrderTemplateProduct(
|
||||
ruleKey: rule.rule_key,
|
||||
categoryId: rule.category_id ? Number(rule.category_id) : null,
|
||||
templateEnabled: rule.enabled === true,
|
||||
pricingMode:
|
||||
Number(rule.unit_price_fen || 0) > 0 && Number(rule.reward_amount || 0) === 0
|
||||
? 'unit'
|
||||
: 'fixed',
|
||||
pricingMode: rule.pricing_mode === 'unit' ? 'unit' : 'fixed',
|
||||
rewardAmountFen: Number(rule.reward_amount || 0),
|
||||
unitPriceFen: Number(rule.unit_price_fen || 0),
|
||||
sharingEnabled: rule.sharing_enabled === true,
|
||||
|
||||
@@ -28,6 +28,7 @@ export function resolveWorkOrderRulePricing(input: {
|
||||
skuQuantity: number
|
||||
unitPriceFen: number
|
||||
fixedRewardAmount: number
|
||||
pricingMode: 'fixed' | 'unit' | string
|
||||
sharingEnabled: boolean
|
||||
sharingAutoFromOrder: boolean
|
||||
sharingTotalQuantity: number
|
||||
@@ -43,7 +44,7 @@ export function resolveWorkOrderRulePricing(input: {
|
||||
: Math.max(0, Number(input.sharingUnitReward) || 0)
|
||||
const rewardAmount = input.sharingEnabled
|
||||
? sharingTotalQuantity * sharingUnitReward
|
||||
: unitPriceFen > 0
|
||||
: input.pricingMode === 'unit'
|
||||
? unitPriceFen * skuQuantity
|
||||
: Math.max(0, Number(input.fixedRewardAmount) || 0)
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ function buildRule(overrides: Partial<WorkProductRuleRow> = {}): WorkProductRule
|
||||
auto_create: true,
|
||||
reward_amount: 0,
|
||||
unit_price_fen: 0,
|
||||
pricing_mode: 'fixed',
|
||||
required_deposit_amount: 0,
|
||||
deposit_threshold_amount: 20000,
|
||||
sharing_enabled: false,
|
||||
@@ -141,6 +142,7 @@ const commanderRule = buildRule({
|
||||
rule_key: 'bb',
|
||||
product_name: '指挥官密钥',
|
||||
unit_price_fen: 950,
|
||||
pricing_mode: 'unit',
|
||||
sharing_enabled: true,
|
||||
sharing_auto_from_order: true,
|
||||
})
|
||||
@@ -199,6 +201,7 @@ test('抖音:命中指挥官模板后按数量计价并开拼单(9.5 元/个
|
||||
skuQuantity,
|
||||
unitPriceFen: commanderRule.unit_price_fen,
|
||||
fixedRewardAmount: commanderRule.reward_amount,
|
||||
pricingMode: commanderRule.pricing_mode,
|
||||
sharingEnabled: commanderRule.sharing_enabled === true,
|
||||
sharingAutoFromOrder: commanderRule.sharing_auto_from_order === true,
|
||||
sharingTotalQuantity: commanderRule.sharing_total_quantity,
|
||||
|
||||
@@ -108,6 +108,7 @@ function buildWorkOrderPlan(
|
||||
skuQuantity,
|
||||
unitPriceFen,
|
||||
fixedRewardAmount: Number(rule.reward_amount || 0),
|
||||
pricingMode: rule.pricing_mode,
|
||||
sharingEnabled: rule.sharing_enabled === true,
|
||||
sharingAutoFromOrder: rule.sharing_auto_from_order === true,
|
||||
sharingTotalQuantity: Number(rule.sharing_total_quantity || 1),
|
||||
@@ -120,7 +121,7 @@ function buildWorkOrderPlan(
|
||||
return {
|
||||
skuQuantity,
|
||||
unitPriceFen,
|
||||
pricingMode: unitPriceFen > 0 ? 'unit' : 'fixed',
|
||||
pricingMode: rule.pricing_mode === 'unit' ? 'unit' : 'fixed',
|
||||
rewardAmount: pricing.rewardAmount,
|
||||
sharingEnabled,
|
||||
sharingTotalQuantity: sharingEnabled ? pricing.sharingTotalQuantity : 1,
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
upsertWorkProductRule,
|
||||
} from '../../repositories/worker-platform/work-product-rule-repo.js'
|
||||
import type { WorkOrderRow, WorkProductRuleRow } from '../../repositories/worker-platform/types.js'
|
||||
import { withTransaction } from '../../db/client.js'
|
||||
import { query, withTransaction } from '../../db/client.js'
|
||||
import {
|
||||
archiveDeletedWorkOrderTemplateProduct,
|
||||
assertWorkOrderTemplateCanEnable,
|
||||
@@ -394,6 +394,34 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) {
|
||||
const incomingUnitPriceFen = normalizeAmountFen(payload.unitPrice ?? payload.unitPriceYuan, 0)
|
||||
const existingRewardAmount = Number(existingRule?.reward_amount || 0)
|
||||
const existingUnitPriceFen = Number(existingRule?.unit_price_fen || 0)
|
||||
const rawPricingMode = String(payload.pricingMode ?? payload.pricing_mode ?? '').trim()
|
||||
if (rawPricingMode && rawPricingMode !== 'fixed' && rawPricingMode !== 'unit') {
|
||||
throw createHttpError('接单模板计价方式无效', {
|
||||
statusCode: 400,
|
||||
errorCode: 'work_product_rule_pricing_mode_invalid',
|
||||
})
|
||||
}
|
||||
const existingPricingMode = existingRule?.pricing_mode === 'unit' ? 'unit' : 'fixed'
|
||||
const pricingMode: 'fixed' | 'unit' =
|
||||
rawPricingMode === 'unit' ? 'unit' : rawPricingMode === 'fixed' ? 'fixed' : existingPricingMode
|
||||
let carriedPricingAmount =
|
||||
existingPricingMode === 'unit' ? existingUnitPriceFen : existingRewardAmount
|
||||
if (existingRule && pricingMode !== existingPricingMode) {
|
||||
const activeCostResult = await query<{ amount_fen: number }>(
|
||||
`SELECT amount_fen
|
||||
FROM cost_versions
|
||||
WHERE work_product_rule_id = $1
|
||||
AND scope_type = 'rule'
|
||||
AND status = 'active'
|
||||
AND effective_from <= NOW()
|
||||
AND (effective_to IS NULL OR effective_to > NOW())
|
||||
ORDER BY effective_from DESC, id DESC
|
||||
LIMIT 1`,
|
||||
[existingRule.id],
|
||||
)
|
||||
const activeCostAmount = Number(activeCostResult.rows[0]?.amount_fen || 0)
|
||||
if (activeCostAmount > 0) carriedPricingAmount = activeCostAmount
|
||||
}
|
||||
|
||||
// 模板保存只维护业务配置。价格只能由商品与成本中心的统一命令写入。
|
||||
if (
|
||||
@@ -409,8 +437,18 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) {
|
||||
},
|
||||
)
|
||||
}
|
||||
const rewardAmount = existingRewardAmount
|
||||
const unitPriceFen = existingUnitPriceFen
|
||||
const rewardAmount =
|
||||
pricingMode !== existingPricingMode || !existingRule
|
||||
? pricingMode === 'fixed'
|
||||
? carriedPricingAmount
|
||||
: 0
|
||||
: existingRewardAmount
|
||||
const unitPriceFen =
|
||||
pricingMode !== existingPricingMode || !existingRule
|
||||
? pricingMode === 'unit'
|
||||
? carriedPricingAmount
|
||||
: 0
|
||||
: existingUnitPriceFen
|
||||
const sharingEnabled = hasOwnPayload(payload, 'sharingEnabled')
|
||||
? normalizeBoolean(payload.sharingEnabled, false)
|
||||
: hasOwnPayload(payload, 'sharing_enabled')
|
||||
@@ -431,12 +469,14 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) {
|
||||
hasConfiguredSharingValue(payload.sharingTotalQuantity ?? payload.sharing_total_quantity) ||
|
||||
hasConfiguredSharingValue(payload.sharingUnitReward ?? payload.sharingUnitRewardYuan) ||
|
||||
hasConfiguredSharingValue(payload.sharingTotalAmount ?? payload.sharingTotalAmountYuan)
|
||||
const sharingAutoFromOrder = hasOwnPayload(payload, 'sharingAutoFromOrder')
|
||||
const requestedSharingAutoFromOrder = hasOwnPayload(payload, 'sharingAutoFromOrder')
|
||||
? normalizeBoolean(payload.sharingAutoFromOrder, false)
|
||||
: hasOwnPayload(payload, 'sharing_auto_from_order')
|
||||
? normalizeBoolean(payload.sharing_auto_from_order, false)
|
||||
: (existingRule?.sharing_auto_from_order === true && !hasManualSharingConfig) ||
|
||||
(!existingRule && sharingEnabled && unitPriceFen > 0 && !hasManualSharingConfig)
|
||||
(!existingRule && sharingEnabled && pricingMode === 'unit' && !hasManualSharingConfig)
|
||||
const sharingAutoFromOrder =
|
||||
pricingMode === 'unit' && requestedSharingAutoFromOrder && !hasManualSharingConfig
|
||||
const hasManualSharingQuantity = hasConfiguredSharingValue(
|
||||
payload.sharingTotalQuantity ?? payload.sharing_total_quantity,
|
||||
)
|
||||
@@ -445,7 +485,7 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) {
|
||||
)
|
||||
if (
|
||||
sharingEnabled &&
|
||||
unitPriceFen > 0 &&
|
||||
pricingMode === 'unit' &&
|
||||
!sharingAutoFromOrder &&
|
||||
hasManualSharingQuantity !== hasManualSharingUnitReward
|
||||
) {
|
||||
@@ -525,6 +565,7 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) {
|
||||
autoCreate: normalizeBoolean(payload.autoCreate ?? payload.auto_create, true),
|
||||
rewardAmount: finalRewardAmount,
|
||||
unitPriceFen,
|
||||
pricingMode,
|
||||
// 商品规则不配置押金;工单创建后由运营按需手工填写。
|
||||
requiredDepositAmount: 0,
|
||||
depositThresholdAmount: 0,
|
||||
@@ -585,6 +626,7 @@ export function resolveWorkOrderTemplateSyncPrice(input: {
|
||||
skuQuantity,
|
||||
unitPriceFen: Number(input.rule.unit_price_fen || 0),
|
||||
fixedRewardAmount: Number(input.rule.reward_amount || 0),
|
||||
pricingMode: input.rule.pricing_mode,
|
||||
sharingEnabled: input.rule.sharing_enabled === true,
|
||||
sharingAutoFromOrder: input.rule.sharing_auto_from_order === true,
|
||||
sharingTotalQuantity: Number(input.rule.sharing_total_quantity || 1),
|
||||
|
||||
@@ -16,6 +16,7 @@ function rule(
|
||||
sort_order: input.sort_order ?? 100,
|
||||
unit_price_fen: input.unit_price_fen ?? 0,
|
||||
reward_amount: input.reward_amount ?? 0,
|
||||
pricing_mode: input.pricing_mode ?? 'fixed',
|
||||
sharing_enabled: input.sharing_enabled ?? false,
|
||||
sharing_total_quantity: input.sharing_total_quantity ?? 1,
|
||||
sharing_unit_reward: input.sharing_unit_reward ?? 0,
|
||||
|
||||
@@ -231,6 +231,7 @@ export async function syncWorkerOrdersForSourceOrder(
|
||||
skuQuantity,
|
||||
unitPriceFen: Number(rule.unit_price_fen || 0),
|
||||
fixedRewardAmount: Number(rule.reward_amount || 0),
|
||||
pricingMode: rule.pricing_mode,
|
||||
sharingEnabled: rule.sharing_enabled === true,
|
||||
sharingAutoFromOrder: rule.sharing_auto_from_order === true,
|
||||
sharingTotalQuantity: Number(rule.sharing_total_quantity || 1),
|
||||
|
||||
@@ -64,6 +64,7 @@ export function mapWorkProductRule(rule: WorkProductRuleRow | null | undefined)
|
||||
autoCreate: Boolean(rule.auto_create),
|
||||
rewardAmount: Number(rule.reward_amount || 0),
|
||||
unitPriceFen: Number(rule.unit_price_fen || 0),
|
||||
pricingMode: rule.pricing_mode === 'unit' ? 'unit' : 'fixed',
|
||||
sharing: {
|
||||
enabled: rule.sharing_enabled === true,
|
||||
totalQuantity: Number(rule.sharing_total_quantity || 1),
|
||||
|
||||
@@ -745,6 +745,7 @@ function buildProductRule(overrides: Partial<WorkProductRuleRow> = {}): WorkProd
|
||||
auto_create: true,
|
||||
reward_amount: 2500,
|
||||
unit_price_fen: 0,
|
||||
pricing_mode: 'fixed',
|
||||
required_deposit_amount: 0,
|
||||
deposit_threshold_amount: 20000,
|
||||
sharing_enabled: false,
|
||||
@@ -1624,6 +1625,7 @@ test('按件拼单未手动覆盖时,使用匹配数量与模板单价', () =>
|
||||
skuQuantity: 10,
|
||||
unitPriceFen: 20,
|
||||
fixedRewardAmount: 0,
|
||||
pricingMode: 'unit',
|
||||
sharingEnabled: true,
|
||||
sharingAutoFromOrder: true,
|
||||
sharingTotalQuantity: 1,
|
||||
@@ -1642,6 +1644,7 @@ test('按件拼单手动覆盖数量和单价时,保留手工配置', () => {
|
||||
skuQuantity: 10,
|
||||
unitPriceFen: 20,
|
||||
fixedRewardAmount: 0,
|
||||
pricingMode: 'unit',
|
||||
sharingEnabled: true,
|
||||
sharingAutoFromOrder: false,
|
||||
sharingTotalQuantity: 3,
|
||||
@@ -1657,7 +1660,7 @@ test('按件拼单手动覆盖数量和单价时,保留手工配置', () => {
|
||||
|
||||
test('模板同步价格按工单来源规格重新计算普通按件单', () => {
|
||||
const price = resolveWorkOrderTemplateSyncPrice({
|
||||
rule: buildProductRule({ unit_price_fen: 200 }),
|
||||
rule: buildProductRule({ unit_price_fen: 200, pricing_mode: 'unit' }),
|
||||
workOrder: buildWorkOrderRow({
|
||||
product_name: '指挥官秘钥15个',
|
||||
material_json: { source: { skuQuantity: 8 } },
|
||||
@@ -1667,10 +1670,39 @@ test('模板同步价格按工单来源规格重新计算普通按件单', () =>
|
||||
assert.deepEqual(price, { rewardAmount: 1600 })
|
||||
})
|
||||
|
||||
test('模板同步价格以显式计价方式为准,不按两个金额字段反推', () => {
|
||||
const workOrder = buildWorkOrderRow({ product_name: '指挥官秘钥15个' })
|
||||
|
||||
assert.deepEqual(
|
||||
resolveWorkOrderTemplateSyncPrice({
|
||||
rule: buildProductRule({
|
||||
reward_amount: 2500,
|
||||
unit_price_fen: 200,
|
||||
pricing_mode: 'fixed',
|
||||
}),
|
||||
workOrder,
|
||||
}),
|
||||
{ rewardAmount: 2500 },
|
||||
)
|
||||
|
||||
assert.deepEqual(
|
||||
resolveWorkOrderTemplateSyncPrice({
|
||||
rule: buildProductRule({
|
||||
reward_amount: 2500,
|
||||
unit_price_fen: 200,
|
||||
pricing_mode: 'unit',
|
||||
}),
|
||||
workOrder,
|
||||
}),
|
||||
{ rewardAmount: 3000 },
|
||||
)
|
||||
})
|
||||
|
||||
test('模板同步价格为拼单返回新的份额单价', () => {
|
||||
const price = resolveWorkOrderTemplateSyncPrice({
|
||||
rule: buildProductRule({
|
||||
unit_price_fen: 200,
|
||||
pricing_mode: 'unit',
|
||||
sharing_enabled: true,
|
||||
sharing_auto_from_order: true,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user