fix: 按实际纯币结算金额计算保险费用
This commit is contained in:
@@ -156,7 +156,7 @@ async function save() {
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
title="费率按号主实际到手租金计算,并在订单结算时扣减;历史订单保留下单时费率快照。协议正文支持 Markdown 表格。"
|
||||
title="费率按实际纯币结算金额计算,额外物品不计入基数,并在订单结算时扣减;历史订单保留下单时费率快照。协议正文支持 Markdown 表格。"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="emit('update:modelValue', false)">取消</el-button>
|
||||
|
||||
@@ -154,7 +154,7 @@ const defaultPremiumZoneTooltip = `极品专区是面向优质号主和高价值
|
||||
1. 发布条件:仅已认证且有效的优质号主可以开启;未开通时需先缴纳平台要求的信用保证金并完成优质号主认证。
|
||||
2. 专区展示:商品审核通过后将在首页“极品专区”展示,获得更聚焦的高端账号曝光。
|
||||
3. 价格规则:系统会按后台配置在基础比例上计算极品专区专享比例,审核页展示的最终价格通过后不再改价。
|
||||
4. 保险服务:可选择无忧租号保险,服务费率按优质号主及极品专区的当前配置执行,并从卖家实际到手租金中扣除。
|
||||
4. 保险服务:可选择无忧租号保险,服务费率按优质号主及极品专区的当前配置执行,并按实际纯币结算金额计算,额外物品不计入保险基数。
|
||||
5. 责任说明:开启专区不会免除商品审核、资料真实性及发布协议中的责任,请如实填写账号资产与出租条件。`
|
||||
|
||||
export const emptyListingPublishOptions: ListingPublishOptions = {
|
||||
@@ -180,7 +180,7 @@ export const emptyListingPublishOptions: ListingPublishOptions = {
|
||||
deposit_tooltip: defaultDepositTooltip,
|
||||
premium_zone_tooltip: defaultPremiumZoneTooltip,
|
||||
compensation_note:
|
||||
'普通赔付方案如下:账号在租期内或祖期外出现封禁,追回、洗号等情况,最高可获赔全款相金+押金\n\n无忧保险赔付方案:按账号市场价值进行赔付(可参照螃蟹,盼之等主流交易平台)。注:选择此方案,需额外抽取订单佣金的8.0%作为服务费:押金须达到推荐押金方可购买。',
|
||||
'普通赔付方案如下:账号在租期内或祖期外出现封禁,追回、洗号等情况,最高可获赔全款相金+押金\n\n无忧保险赔付方案:按账号市场价值进行赔付(可参照螃蟹,盼之等主流交易平台)。无忧保险服务费按实际纯币结算金额计算,额外物品不计入计费基数;具体费率以当前配置为准。押金须达到推荐押金方可购买。',
|
||||
price_placeholder: '',
|
||||
ratio_description: '',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { Order } from '@/features/orders/api/orders'
|
||||
import type { CounterFormState } from '@/features/orders/composables/useOrderActions'
|
||||
import { useCheckoutSimulator } from '@/features/orders/composables/useCheckoutSimulator'
|
||||
|
||||
function orderFixture(): Order {
|
||||
return {
|
||||
id: 1,
|
||||
order_no: 'ORDER-1',
|
||||
listing_id: 1,
|
||||
listing_no: 'LISTING-1',
|
||||
account_id: 1,
|
||||
owner_id: 1,
|
||||
renter_id: 2,
|
||||
title: '测试账号',
|
||||
server_region: '国服',
|
||||
login_platform: '扫码',
|
||||
estimated_duration_hours: 24,
|
||||
display_amount_cent: 25000,
|
||||
rent_amount_cent: 25000,
|
||||
owner_rent_amount_cent: 25000,
|
||||
deposit_amount_cent: 15000,
|
||||
deposit_original_amount_cent: 15000,
|
||||
deposit_waived_amount_cent: 0,
|
||||
insurance_fee_bps: 800,
|
||||
account_snapshot: {
|
||||
haf_coin_amount: 100_000_000,
|
||||
asset_summary: {
|
||||
price_breakdown: {
|
||||
buyer_ratio: 40,
|
||||
seller_ratio: 40,
|
||||
buyer_coin_base_price: 250,
|
||||
seller_coin_base_price: 250,
|
||||
consumable_price: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
status: 'renting',
|
||||
handoff_status: 'received',
|
||||
settlement_status: 'unsettled',
|
||||
created_at: '2026-11-05T00:00:00+08:00',
|
||||
updated_at: '2026-11-05T00:00:00+08:00',
|
||||
}
|
||||
}
|
||||
|
||||
describe('useCheckoutSimulator insurance fee', () => {
|
||||
it('按实际纯币结算金额计费,额外物品不进入保险基数', () => {
|
||||
const form = ref<CounterFormState>({
|
||||
consumableAmountYuan: 50,
|
||||
coin_consumed_m: 50,
|
||||
depositDeductAmountYuan: 0,
|
||||
reason: '',
|
||||
evidenceText: '',
|
||||
})
|
||||
const simulator = useCheckoutSimulator({
|
||||
order: ref(orderFixture()),
|
||||
isOwner: ref(true),
|
||||
isRenter: ref(false),
|
||||
form,
|
||||
})
|
||||
|
||||
expect(simulator.value?.role).toBe('owner')
|
||||
if (!simulator.value || simulator.value.role !== 'owner') {
|
||||
throw new Error('owner simulation missing')
|
||||
}
|
||||
expect(simulator.value.insuranceFeeYuan).toBe(10)
|
||||
expect(simulator.value.rentIncomeYuan).toBe(165)
|
||||
expect(simulator.value.lossCompensationYuan).toBe(50)
|
||||
})
|
||||
})
|
||||
@@ -102,7 +102,7 @@ export function useCheckoutSimulator(options: {
|
||||
|
||||
const grossOwnerRentIncomeYuan = roundMoney(ownerCoinYuan + ownerConsumableYuan)
|
||||
const insuranceFeeYuan = roundMoney(
|
||||
(grossOwnerRentIncomeYuan * Number(o.insurance_fee_bps || 0)) / 10000
|
||||
(ownerCoinYuan * Number(o.insurance_fee_bps || 0)) / 10000
|
||||
)
|
||||
const ownerRentIncomeYuan = Math.max(
|
||||
roundMoney(grossOwnerRentIncomeYuan - insuranceFeeYuan),
|
||||
|
||||
@@ -120,7 +120,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
})
|
||||
const calculatedInsuranceFee = computed(() =>
|
||||
form.compensation_plan === 'insurance'
|
||||
? calculateInsuranceFeeAmount(pricing.calculatedSellerPrice.value, insuranceFeeBps.value)
|
||||
? calculateInsuranceFeeAmount(pricing.calculatedCoinBasePrice.value, insuranceFeeBps.value)
|
||||
: 0
|
||||
)
|
||||
const calculatedTakeHomePrice = computed(() =>
|
||||
|
||||
@@ -948,7 +948,7 @@ const screenshotGuides: Record<string, string> = {
|
||||
|
||||
<div class="coverage-heading">
|
||||
<strong>选择赔付方案</strong>
|
||||
<span>服务费按实际到手租金结算扣除</span>
|
||||
<span>按实际纯币结算金额计费,额外物品不计入</span>
|
||||
</div>
|
||||
<div class="coverage-option-grid">
|
||||
<button
|
||||
@@ -973,7 +973,7 @@ const screenshotGuides: Record<string, string> = {
|
||||
<strong>无忧租号保险</strong>
|
||||
<em class="coverage-rate is-insurance"> {{ insuranceFeePercent.toFixed(1) }}% </em>
|
||||
</span>
|
||||
<span>额外保障账号市场价值</span>
|
||||
<span>额外保障账号市场价值,按纯币结算金额计费</span>
|
||||
<em v-if="isQualityOwner && form.is_premium" class="coverage-special-rate">
|
||||
已享极品专区专享费率
|
||||
</em>
|
||||
@@ -989,7 +989,7 @@ const screenshotGuides: Record<string, string> = {
|
||||
</div>
|
||||
<div>
|
||||
<span v-if="form.compensation_plan === 'insurance'">
|
||||
无忧租号保险({{ insuranceFeePercent.toFixed(1) }}%)
|
||||
无忧租号保险({{ insuranceFeePercent.toFixed(1) }}% · 纯币计费)
|
||||
</span>
|
||||
<span v-else>普通赔付</span>
|
||||
<strong :class="{ 'is-deduction': form.compensation_plan === 'insurance' }">
|
||||
|
||||
@@ -772,7 +772,7 @@ function selectDailyLoss(value: string | number) {
|
||||
|
||||
<div class="coverage-heading">
|
||||
<strong>选择赔付方案</strong>
|
||||
<span>服务费按号主实际到手租金计算,结算时自动扣除</span>
|
||||
<span>保险服务费按实际纯币结算金额计算,额外物品不计入基数</span>
|
||||
</div>
|
||||
<div class="coverage-option-grid">
|
||||
<button
|
||||
@@ -799,7 +799,7 @@ function selectDailyLoss(value: string | number) {
|
||||
{{ insuranceFeePercent.toFixed(1) }}%
|
||||
</em>
|
||||
</span>
|
||||
<span>额外保障账号市场价值,费率按实际到手租金扣除</span>
|
||||
<span>额外保障账号市场价值,费率按实际纯币结算金额扣除</span>
|
||||
<em v-if="isQualityOwner && form.is_premium" class="coverage-special-rate">
|
||||
已享极品专区专享费率
|
||||
</em>
|
||||
@@ -815,7 +815,7 @@ function selectDailyLoss(value: string | number) {
|
||||
</div>
|
||||
<div>
|
||||
<span v-if="form.compensation_plan === 'insurance'">
|
||||
无忧租号保险({{ insuranceFeePercent.toFixed(1) }}%)
|
||||
无忧租号保险({{ insuranceFeePercent.toFixed(1) }}% · 纯币计费)
|
||||
</span>
|
||||
<span v-else>普通赔付</span>
|
||||
<strong :class="{ 'is-deduction': form.compensation_plan === 'insurance' }">
|
||||
@@ -873,7 +873,7 @@ function selectDailyLoss(value: string | number) {
|
||||
<strong>¥{{ formatMoney(calculatedConsumablePrice) }}</strong>
|
||||
</div>
|
||||
<div v-if="form.compensation_plan === 'insurance'">
|
||||
<span>无忧租号保险({{ insuranceFeePercent.toFixed(1) }}%)</span>
|
||||
<span>无忧租号保险({{ insuranceFeePercent.toFixed(1) }}% · 纯币计费)</span>
|
||||
<strong class="is-deduction">-¥{{ formatMoney(calculatedInsuranceFee) }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -15,7 +15,11 @@ describe('calculateInsuranceFeeAmount', () => {
|
||||
expect(calculateInsuranceFeeAmount(238.1, 400)).toBe(9.52)
|
||||
})
|
||||
|
||||
it('保险服务费不超过卖家租金', () => {
|
||||
it('按纯币价格计算保险服务费', () => {
|
||||
expect(calculateInsuranceFeeAmount(250, 800)).toBe(20)
|
||||
})
|
||||
|
||||
it('保险服务费不超过纯币金额', () => {
|
||||
expect(calculateInsuranceFeeAmount(1, 10_000)).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -26,11 +26,11 @@ export function roundMoney(value: number) {
|
||||
return Math.round(value * 10) / 10
|
||||
}
|
||||
|
||||
export function calculateInsuranceFeeAmount(ownerIncomeYuan: number, feeBps: number) {
|
||||
const ownerIncomeCent = Math.max(Math.round(Number(ownerIncomeYuan || 0) * 100), 0)
|
||||
export function calculateInsuranceFeeAmount(pureCoinAmountYuan: number, feeBps: number) {
|
||||
const pureCoinAmountCent = Math.max(Math.round(Number(pureCoinAmountYuan || 0) * 100), 0)
|
||||
const safeFeeBps = Math.max(Math.round(Number(feeBps || 0)), 0)
|
||||
if (ownerIncomeCent <= 0 || safeFeeBps <= 0) return 0
|
||||
return Math.min(Math.round((ownerIncomeCent * safeFeeBps) / 10_000), ownerIncomeCent) / 100
|
||||
if (pureCoinAmountCent <= 0 || safeFeeBps <= 0) return 0
|
||||
return Math.min(Math.round((pureCoinAmountCent * safeFeeBps) / 10_000), pureCoinAmountCent) / 100
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user