feat(finance): 增加保险统计与订单保险明细
This commit is contained in:
@@ -13,6 +13,12 @@ export interface FinanceSummary {
|
||||
platform_income_amount_cent: number
|
||||
normal_order_profit_amount_cent: number
|
||||
pickup_profit_amount_cent: number
|
||||
insurance_order_count: number
|
||||
insurance_profit_amount_cent: number
|
||||
normal_insurance_order_count: number
|
||||
normal_insurance_profit_amount_cent: number
|
||||
premium_insurance_order_count: number
|
||||
premium_insurance_profit_amount_cent: number
|
||||
/** 普通(租赁)订单利润对应的已结算订单数量 */
|
||||
settled_order_count: number
|
||||
/** 线下提号利润对应的已完成提号笔数 */
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
estimateOrderEndAt,
|
||||
orderPlatformFeeBreakdown,
|
||||
paymentBizTypeLabel,
|
||||
paymentPaidAt,
|
||||
paymentStatusLabel,
|
||||
@@ -39,4 +40,45 @@ describe('admin order presentation', () => {
|
||||
).toBe('paid')
|
||||
expect(paymentPaidAt({ updated_at: 'updated', created_at: 'created' })).toBe('updated')
|
||||
})
|
||||
|
||||
it('splits insurance from the final checkout platform income', () => {
|
||||
expect(
|
||||
orderPlatformFeeBreakdown({
|
||||
status: 'completed',
|
||||
platform_fee_cent: 3260,
|
||||
insurance_fee_cent: 1739,
|
||||
insurance_fee_bps: 800,
|
||||
checkout: {
|
||||
status: 'accepted',
|
||||
platform_fee_cent: 4999,
|
||||
insurance_fee_cent: 1739,
|
||||
insurance_fee_bps: 800,
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
baseFeeCent: 3260,
|
||||
insuranceFeeCent: 1739,
|
||||
insuranceFeeBps: 800,
|
||||
totalFeeCent: 4999,
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the order snapshot before settlement and when there is no insurance', () => {
|
||||
expect(
|
||||
orderPlatformFeeBreakdown({
|
||||
status: 'renting',
|
||||
platform_fee_cent: 4000,
|
||||
insurance_fee_cent: 0,
|
||||
checkout: {
|
||||
status: 'submitted',
|
||||
platform_fee_cent: 5000,
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
baseFeeCent: 4000,
|
||||
insuranceFeeCent: 0,
|
||||
insuranceFeeBps: 0,
|
||||
totalFeeCent: 4000,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,62 @@
|
||||
import type { AdminPayment } from '@/features/admin/api/adminPayments'
|
||||
|
||||
type OrderPlatformFeeSource = {
|
||||
status?: string
|
||||
platform_fee_cent?: number
|
||||
insurance_fee_cent?: number
|
||||
insurance_fee_bps?: number
|
||||
checkout?: {
|
||||
status?: string
|
||||
platform_fee_cent?: number
|
||||
insurance_fee_cent?: number
|
||||
insurance_fee_bps?: number
|
||||
} | null
|
||||
}
|
||||
|
||||
export type OrderPlatformFeeBreakdown = {
|
||||
baseFeeCent: number
|
||||
insuranceFeeCent: number
|
||||
insuranceFeeBps: number
|
||||
totalFeeCent: number
|
||||
}
|
||||
|
||||
function nonNegativeCent(value: number | undefined) {
|
||||
const amount = Number(value || 0)
|
||||
return Number.isFinite(amount) ? Math.max(amount, 0) : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单主表保存下单时的平台基础费用,结账单保存结算后的平台收入合计。
|
||||
* 已完成订单优先采用结账单,并从合计中拆出无忧保险,避免客服看到旧快照。
|
||||
*/
|
||||
export function orderPlatformFeeBreakdown(
|
||||
order: OrderPlatformFeeSource | null | undefined
|
||||
): OrderPlatformFeeBreakdown {
|
||||
if (!order) {
|
||||
return { baseFeeCent: 0, insuranceFeeCent: 0, insuranceFeeBps: 0, totalFeeCent: 0 }
|
||||
}
|
||||
|
||||
const orderFeeCent = nonNegativeCent(order.platform_fee_cent)
|
||||
const checkoutFeeCent = nonNegativeCent(order.checkout?.platform_fee_cent)
|
||||
const totalFeeCent =
|
||||
order.status === 'completed' ? Math.max(orderFeeCent, checkoutFeeCent) : orderFeeCent
|
||||
const orderInsuranceFeeCent = nonNegativeCent(order.insurance_fee_cent)
|
||||
const checkoutInsuranceFeeCent = nonNegativeCent(order.checkout?.insurance_fee_cent)
|
||||
const insuranceFeeCent =
|
||||
orderInsuranceFeeCent > 0 ? orderInsuranceFeeCent : checkoutInsuranceFeeCent
|
||||
const insuranceFeeBps = Math.max(
|
||||
Number(order.insurance_fee_bps || 0),
|
||||
insuranceFeeCent > 0 ? Number(order.checkout?.insurance_fee_bps || 0) : 0
|
||||
)
|
||||
|
||||
return {
|
||||
baseFeeCent: Math.max(totalFeeCent - insuranceFeeCent, 0),
|
||||
insuranceFeeCent: Math.min(insuranceFeeCent, totalFeeCent),
|
||||
insuranceFeeBps,
|
||||
totalFeeCent,
|
||||
}
|
||||
}
|
||||
|
||||
export function estimateOrderEndAt(
|
||||
estimatedEndAt?: string,
|
||||
rentedAt?: string,
|
||||
|
||||
@@ -386,6 +386,11 @@
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.section-badge.amber {
|
||||
background: #fffbeb;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
@@ -457,6 +462,20 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.insurance-type-card .sub-card-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.insurance-type-card--normal {
|
||||
border-left: 3px solid #f59e0b;
|
||||
}
|
||||
|
||||
.insurance-type-card--premium {
|
||||
border-left: 3px solid #8b5cf6;
|
||||
}
|
||||
|
||||
.dot-sep {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ import {
|
||||
Coin,
|
||||
CreditCard,
|
||||
DocumentChecked,
|
||||
Medal,
|
||||
Money,
|
||||
Refresh,
|
||||
Search,
|
||||
ShoppingCart,
|
||||
Timer,
|
||||
TrendCharts,
|
||||
Umbrella,
|
||||
Van,
|
||||
Wallet,
|
||||
WarningFilled,
|
||||
@@ -370,6 +372,60 @@ function diffType(value: number | undefined | null) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 无忧保险 -->
|
||||
<div class="section-container">
|
||||
<div class="section-header">
|
||||
<div class="section-title-wrap">
|
||||
<div class="section-badge amber">
|
||||
<el-icon><Umbrella /></el-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="section-title">无忧保险</h2>
|
||||
<p class="section-desc">按结算日统计投保订单数量与保险服务费利润</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sub-metric-grid cols-3">
|
||||
<div class="sub-card">
|
||||
<div class="sub-card-label">保险利润合计</div>
|
||||
<div class="sub-card-value emerald-text">
|
||||
{{ moneyCent(dashboard.summary.insurance_profit_amount_cent) }}
|
||||
</div>
|
||||
<div class="sub-card-sub">
|
||||
<span>{{ dashboard.summary.insurance_order_count }} 笔已结算投保订单</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sub-card insurance-type-card insurance-type-card--normal">
|
||||
<div class="sub-card-label">普通无忧保险</div>
|
||||
<div class="sub-card-value">
|
||||
{{ moneyCent(dashboard.summary.normal_insurance_profit_amount_cent) }}
|
||||
</div>
|
||||
<div class="sub-card-sub">
|
||||
<span>{{ dashboard.summary.normal_insurance_order_count }} 笔投保订单</span>
|
||||
<span class="dot-sep">·</span>
|
||||
<span>保险利润</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sub-card insurance-type-card insurance-type-card--premium">
|
||||
<div class="sub-card-label">
|
||||
<el-icon><Medal /></el-icon>
|
||||
极品专区保险
|
||||
</div>
|
||||
<div class="sub-card-value">
|
||||
{{ moneyCent(dashboard.summary.premium_insurance_profit_amount_cent) }}
|
||||
</div>
|
||||
<div class="sub-card-sub">
|
||||
<span>{{ dashboard.summary.premium_insurance_order_count }} 笔投保订单</span>
|
||||
<span class="dot-sep">·</span>
|
||||
<span>保险利润</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 资金出款与待办审批 -->
|
||||
<div v-if="dashboard.disbursement_summary" class="section-container">
|
||||
<div class="section-header">
|
||||
|
||||
@@ -51,7 +51,11 @@ import { formatGameName, formatListingNo } from '@/shared/utils/listingDisplay'
|
||||
import AdminOrderMetrics, { type AdminOrderMetric } from '../components/AdminOrderMetrics.vue'
|
||||
import AdminOrderPaymentRecords from '../components/AdminOrderPaymentRecords.vue'
|
||||
import ProcessTimeline from '../components/ProcessTimeline.vue'
|
||||
import { estimateOrderEndAt, paymentStatusLabel } from '../utils/adminOrderPresentation'
|
||||
import {
|
||||
estimateOrderEndAt,
|
||||
orderPlatformFeeBreakdown,
|
||||
paymentStatusLabel,
|
||||
} from '../utils/adminOrderPresentation'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -362,6 +366,7 @@ const ownerLossPriceCent = computed(() => {
|
||||
return Math.max(Number(order.value.owner_rent_amount_cent) - ownerCoinBasePriceCent.value, 0)
|
||||
})
|
||||
const hasOwnerRentBreakdown = computed(() => ownerCoinBasePriceCent.value !== null)
|
||||
const platformFeeBreakdown = computed(() => orderPlatformFeeBreakdown(order.value))
|
||||
const fundSplitRows = computed(() => {
|
||||
if (!order.value) return []
|
||||
const rows: FundSplitRow[] = []
|
||||
@@ -398,8 +403,20 @@ const fundSplitRows = computed(() => {
|
||||
} else {
|
||||
rows.push({ label: '号主租金', amountCent: order.value.owner_rent_amount_cent })
|
||||
}
|
||||
const platformFee = platformFeeBreakdown.value
|
||||
if (platformFee.insuranceFeeCent > 0) {
|
||||
rows.push(
|
||||
{ label: '平台基础费用', amountCent: platformFee.baseFeeCent },
|
||||
{
|
||||
label: `无忧租号保险服务费(${(platformFee.insuranceFeeBps / 100).toFixed(1)}%)`,
|
||||
amountCent: platformFee.insuranceFeeCent,
|
||||
},
|
||||
{ label: '平台收入合计', amountCent: platformFee.totalFeeCent }
|
||||
)
|
||||
} else {
|
||||
rows.push({ label: '平台费用', amountCent: platformFee.totalFeeCent })
|
||||
}
|
||||
rows.push(
|
||||
{ label: '平台费用', amountCent: order.value.platform_fee_cent },
|
||||
{ label: '实付押金', amountCent: order.value.deposit_amount_cent },
|
||||
{ label: '原始押金', amountCent: order.value.deposit_original_amount_cent },
|
||||
{ label: '免押额度', amountCent: order.value.deposit_waived_amount_cent }
|
||||
|
||||
Reference in New Issue
Block a user