fix(distribution): 分成改为分主站提价利润,分站自主提价溢价全额归卖方
- 文档:升级至 v0.7,固化号主保底、主站提价利润分账(35/40/25)与分站加价溢价独享逻辑 - 定价:修正货架兑换比例调价方向为调减比例提价(site_ratio = base_ratio - delta) - 分账:分成基数剔除号主成本与分站加价,提价利润按规则切分,分站自主加价溢价 100% 归卖出方 - 前端:更新分销后台提价说明文案,提示调减比例提高售价且溢价归本站
This commit is contained in:
@@ -204,8 +204,8 @@ func SnapshotPaidOrderTx(tx *gorm.DB, orderID uint64, grossPaidCent int64) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
splits := SplitSettlement(ruleFromVersion(rule), result.SplitBaseAmountCent)
|
||||
financial := model.DistributionOrderFinancial{OrderID: order.ID, SiteID: *order.DistributionSiteID, RuleVersionID: rule.ID, GrossPaidAmountCent: grossRent, NetPaidAmountCent: result.NetPaidAmountCent, PaymentFeeBPS: rule.PaymentFeeBPS, PaymentFeeCent: result.PaymentFeeCent, ServiceFeeValue: rule.ServiceFeeValue, ServiceFeeUnit: rule.ServiceFeeUnit, ServiceFeeCent: result.ServiceFeeCent, MarkupAmountCent: markup, TaxEnabled: rule.TaxEnabled, TaxRateBPS: rule.TaxRateBPS, TaxAmountCent: result.TaxAmountCent, SplitBaseAmountCent: result.SplitBaseAmountCent, RoundingAdjustmentCent: SplitRoundingCent(result.SplitBaseAmountCent, result.ServiceFeeCent, splits), SettlementStatus: "pending"}
|
||||
splits := SplitSettlement(ruleFromVersion(rule), result.SplitBaseAmountCent, markup)
|
||||
financial := model.DistributionOrderFinancial{OrderID: order.ID, SiteID: *order.DistributionSiteID, RuleVersionID: rule.ID, GrossPaidAmountCent: grossRent, NetPaidAmountCent: result.NetPaidAmountCent, PaymentFeeBPS: rule.PaymentFeeBPS, PaymentFeeCent: result.PaymentFeeCent, ServiceFeeValue: rule.ServiceFeeValue, ServiceFeeUnit: rule.ServiceFeeUnit, ServiceFeeCent: result.ServiceFeeCent, MarkupAmountCent: markup, TaxEnabled: rule.TaxEnabled, TaxRateBPS: rule.TaxRateBPS, TaxAmountCent: result.TaxAmountCent, SplitBaseAmountCent: result.SplitBaseAmountCent, RoundingAdjustmentCent: SplitRoundingCent(result.SplitBaseAmountCent, result.ServiceFeeCent, markup, splits), SettlementStatus: "pending"}
|
||||
if err := tx.Where("order_id = ?", order.ID).Assign(financial).FirstOrCreate(&financial).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -274,8 +274,8 @@ func ApplyRefundTx(tx *gorm.DB, orderID uint64, refundAmountCent int64) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
splits := SplitSettlement(ruleFromVersion(rule), result.SplitBaseAmountCent)
|
||||
if err := tx.Model(&financial).Updates(map[string]any{"refund_amount_cent": refundAmountCent, "net_paid_amount_cent": result.NetPaidAmountCent, "payment_fee_cent": result.PaymentFeeCent, "service_fee_cent": result.ServiceFeeCent, "split_base_amount_cent": result.SplitBaseAmountCent, "rounding_adjustment_cent": SplitRoundingCent(result.SplitBaseAmountCent, result.ServiceFeeCent, splits), "settlement_status": "pending"}).Error; err != nil {
|
||||
splits := SplitSettlement(ruleFromVersion(rule), result.SplitBaseAmountCent, financial.MarkupAmountCent)
|
||||
if err := tx.Model(&financial).Updates(map[string]any{"refund_amount_cent": refundAmountCent, "net_paid_amount_cent": result.NetPaidAmountCent, "payment_fee_cent": result.PaymentFeeCent, "service_fee_cent": result.ServiceFeeCent, "split_base_amount_cent": result.SplitBaseAmountCent, "rounding_adjustment_cent": SplitRoundingCent(result.SplitBaseAmountCent, result.ServiceFeeCent, financial.MarkupAmountCent, splits), "settlement_status": "pending"}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, split := range splits {
|
||||
|
||||
@@ -146,7 +146,7 @@ func CalculateSettlement(rule Rule, input SettlementInput) (SettlementResult, er
|
||||
}
|
||||
netPaid := input.GrossPaidAmountCent - input.RefundAmountCent
|
||||
paymentFee := roundHalfUp(float64(netPaid) * float64(rule.PaymentFeeBPS) / 10000)
|
||||
profit := netPaid - input.OwnerRentAmountCent - paymentFee
|
||||
profit := netPaid - input.OwnerRentAmountCent - input.MarkupAmountCent - paymentFee
|
||||
if profit < 0 {
|
||||
profit = 0
|
||||
}
|
||||
@@ -212,8 +212,9 @@ type PartySplit struct {
|
||||
AmountCent int64
|
||||
}
|
||||
|
||||
// SplitSettlement 按百分数切利润:收号 35%、卖出 40%。服务费 25% 记在财务快照,不并进参与方。
|
||||
func SplitSettlement(rule Rule, profitCent int64) []PartySplit {
|
||||
// SplitSettlement 按百分数切主站提价利润(收号 35%、卖出 40%),并将分站自主加价溢价(markupCent)100% 独立全额归入卖出方。
|
||||
// 平台服务费 25% 记在财务快照,不并进参与方。
|
||||
func SplitSettlement(rule Rule, profitCent int64, markupCent int64) []PartySplit {
|
||||
roles := []struct {
|
||||
role string
|
||||
ratio float64
|
||||
@@ -242,19 +243,34 @@ func SplitSettlement(rule Rule, profitCent int64) []PartySplit {
|
||||
amount := roundHalfUp(float64(profitCent) * item.ratio / 100)
|
||||
splits = append(splits, PartySplit{PartyRole: item.role, Ratio: item.ratio, AmountCent: maxInt64(amount, 0)})
|
||||
}
|
||||
if markupCent > 0 {
|
||||
sellerRole := "distributor_sell"
|
||||
switch rule.Mode {
|
||||
case ModeTwoPartyDistributorReceive:
|
||||
sellerRole = "platform_sell"
|
||||
case ModeThreeParty:
|
||||
sellerRole = "distributor_b"
|
||||
}
|
||||
for i := range splits {
|
||||
if splits[i].PartyRole == sellerRole {
|
||||
splits[i].AmountCent += markupCent
|
||||
}
|
||||
}
|
||||
}
|
||||
return splits
|
||||
}
|
||||
|
||||
// SplitRemainderCent 返回利润扣除各方分成后的剩余(未扣服务费)。
|
||||
func SplitRemainderCent(splitBase int64, splits []PartySplit) int64 {
|
||||
// SplitRemainderCent 返回主站提价利润扣除各方提价分成后的剩余(未扣服务费)。
|
||||
func SplitRemainderCent(splitBase int64, splits []PartySplit, markupCent int64) int64 {
|
||||
sum := int64(0)
|
||||
for _, item := range splits {
|
||||
sum += item.AmountCent
|
||||
}
|
||||
return splitBase - sum
|
||||
profitSplitSum := sum - markupCent
|
||||
return splitBase - profitSplitSum
|
||||
}
|
||||
|
||||
// SplitRoundingCent 返回利润扣完收号、卖出和服务费后的四舍五入尾差。
|
||||
func SplitRoundingCent(profitCent, serviceFeeCent int64, splits []PartySplit) int64 {
|
||||
return SplitRemainderCent(profitCent, splits) - serviceFeeCent
|
||||
// SplitRoundingCent 返回提价利润扣完收号、卖出和服务费后的四舍五入尾差。
|
||||
func SplitRoundingCent(profitCent, serviceFeeCent, markupCent int64, splits []PartySplit) int64 {
|
||||
return SplitRemainderCent(profitCent, splits, markupCent) - serviceFeeCent
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func TestCalculateSettlementRejectsRefundOverPayment(t *testing.T) {
|
||||
|
||||
func TestSplitSettlementTwoPartyDistributorReceive(t *testing.T) {
|
||||
rule := Rule{Mode: ModeTwoPartyDistributorReceive, DistributorReceiveRatio: 35, PlatformSellRatio: 40}
|
||||
splits := SplitSettlement(rule, 3950)
|
||||
splits := SplitSettlement(rule, 3950, 0)
|
||||
if len(splits) != 2 {
|
||||
t.Fatalf("splits = %#v", splits)
|
||||
}
|
||||
@@ -85,26 +85,34 @@ func TestSplitSettlementTwoPartyDistributorReceive(t *testing.T) {
|
||||
if splits[1].PartyRole != "platform_sell" || splits[1].AmountCent != 1580 {
|
||||
t.Fatalf("platform split = %#v", splits[1])
|
||||
}
|
||||
if got := SplitRoundingCent(3950, 988, splits); got != -1 {
|
||||
if got := SplitRoundingCent(3950, 988, 0, splits); got != -1 {
|
||||
t.Fatalf("rounding = %d, want -1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSettlementDoesNotAddYuanMarkup(t *testing.T) {
|
||||
func TestSplitSettlementAddsShelfMarkupDirectlyToSeller(t *testing.T) {
|
||||
rule := Rule{Mode: ModeTwoPartyPlatformReceive, DistributorSellRatio: 40, PlatformReceiveRatio: 35}
|
||||
splits := SplitSettlement(rule, 10000)
|
||||
if splits[0].PartyRole != "distributor_sell" || splits[0].AmountCent != 4000 {
|
||||
t.Fatalf("distributor sell = %#v", splits[0])
|
||||
// 提价利润 10000 分,分站自主加价 2000 分
|
||||
splits := SplitSettlement(rule, 10000, 2000)
|
||||
// 卖方分站获得 40% (4000) + 2000 (加价) = 6000
|
||||
if splits[0].PartyRole != "distributor_sell" || splits[0].AmountCent != 6000 {
|
||||
t.Fatalf("distributor sell = %#v, want 6000", splits[0])
|
||||
}
|
||||
// 收号方获得 35% = 3500
|
||||
if splits[1].AmountCent != 3500 {
|
||||
t.Fatalf("platform receive = %#v", splits[1])
|
||||
t.Fatalf("platform receive = %#v, want 3500", splits[1])
|
||||
}
|
||||
// 尾差校验:平台服务费 2500,提价利润 10000 - (3500 + 4000) - 2500 = 0
|
||||
if got := SplitRoundingCent(10000, 2500, 2000, splits); got != 0 {
|
||||
t.Fatalf("rounding = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSettlementThreeParty(t *testing.T) {
|
||||
rule := Rule{Mode: ModeThreeParty, DistributorARatio: 35, DistributorBRatio: 40}
|
||||
splits := SplitSettlement(rule, 10000)
|
||||
if len(splits) != 2 || splits[0].AmountCent != 3500 || splits[1].AmountCent != 4000 {
|
||||
// 提价利润 10000,分站B自主加价 1500
|
||||
splits := SplitSettlement(rule, 10000, 1500)
|
||||
if len(splits) != 2 || splits[0].AmountCent != 3500 || splits[1].AmountCent != 5500 {
|
||||
t.Fatalf("splits = %#v", splits)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -934,8 +934,8 @@ func (r *Repository) CreateFinancialSnapshot(ctx context.Context, adminID, order
|
||||
return nil, err
|
||||
}
|
||||
err = r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
splits := SplitSettlement(parsed, result.SplitBaseAmountCent)
|
||||
financial := model.DistributionOrderFinancial{OrderID: orderID, SiteID: req.SiteID, RuleVersionID: req.RuleVersionID, GrossPaidAmountCent: result.GrossPaidAmountCent, RefundAmountCent: result.RefundAmountCent, NetPaidAmountCent: result.NetPaidAmountCent, PaymentFeeBPS: rule.PaymentFeeBPS, PaymentFeeCent: result.PaymentFeeCent, ServiceFeeValue: parsed.ServiceFeeValue, ServiceFeeUnit: parsed.ServiceFeeUnit, ServiceFeeCent: result.ServiceFeeCent, MarkupAmountCent: result.MarkupAmountCent, TaxEnabled: rule.TaxEnabled, TaxRateBPS: rule.TaxRateBPS, TaxAmountCent: result.TaxAmountCent, SplitBaseAmountCent: result.SplitBaseAmountCent, RoundingAdjustmentCent: SplitRoundingCent(result.SplitBaseAmountCent, result.ServiceFeeCent, splits), SettlementStatus: "pending"}
|
||||
splits := SplitSettlement(parsed, result.SplitBaseAmountCent, result.MarkupAmountCent)
|
||||
financial := model.DistributionOrderFinancial{OrderID: orderID, SiteID: req.SiteID, RuleVersionID: req.RuleVersionID, GrossPaidAmountCent: result.GrossPaidAmountCent, RefundAmountCent: result.RefundAmountCent, NetPaidAmountCent: result.NetPaidAmountCent, PaymentFeeBPS: rule.PaymentFeeBPS, PaymentFeeCent: result.PaymentFeeCent, ServiceFeeValue: parsed.ServiceFeeValue, ServiceFeeUnit: parsed.ServiceFeeUnit, ServiceFeeCent: result.ServiceFeeCent, MarkupAmountCent: result.MarkupAmountCent, TaxEnabled: rule.TaxEnabled, TaxRateBPS: rule.TaxRateBPS, TaxAmountCent: result.TaxAmountCent, SplitBaseAmountCent: result.SplitBaseAmountCent, RoundingAdjustmentCent: SplitRoundingCent(result.SplitBaseAmountCent, result.ServiceFeeCent, result.MarkupAmountCent, splits), SettlementStatus: "pending"}
|
||||
if err := tx.Create(&financial).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user