支持提号完成后财务调整
This commit is contained in:
@@ -316,6 +316,14 @@ func (r *Repository) MarkOfflineSettlement(ctx context.Context, pickupID uint64,
|
||||
pickup.OfflineSettlementStatus != OfflineSettlementStatusPending || pickup.SettleAmountCent <= 0 {
|
||||
return ErrOfflineSettlementCannotMark
|
||||
}
|
||||
pendingTotals, err := pendingPickupFinancialAdjustmentTotals(tx, pickup.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
effectiveSettleAmountCent := pickup.SettleAmountCent + pendingTotals.SettleAdjustmentCent
|
||||
if effectiveSettleAmountCent < 0 {
|
||||
return ErrFinancialAdjustmentInvalid
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
pickup.OfflineSettlementStatus = OfflineSettlementStatusSettled
|
||||
@@ -325,6 +333,18 @@ func (r *Repository) MarkOfflineSettlement(ctx context.Context, pickupID uint64,
|
||||
if err := tx.Save(&pickup).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if pendingTotals.Count > 0 {
|
||||
if err := tx.Model(&model.AdminPickupFinancialAdjustment{}).
|
||||
Where("pickup_id = ? AND status IN ?", pickup.ID, []string{FinancialAdjustmentStatusPending, FinancialAdjustmentStatusRecovery}).
|
||||
Updates(map[string]any{
|
||||
"status": FinancialAdjustmentStatusSettled,
|
||||
"settled_by": adminID,
|
||||
"settled_at": now,
|
||||
"settlement_remark": pickup.OfflineSettlementRemark,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
bid := pickup.ID
|
||||
return auditlog.Append(tx, auditlog.Entry{
|
||||
@@ -336,7 +356,8 @@ func (r *Repository) MarkOfflineSettlement(ctx context.Context, pickupID uint64,
|
||||
Meta: meta,
|
||||
Detail: map[string]any{
|
||||
"pickup_no": pickup.PickupNo,
|
||||
"settle_amount_cent": pickup.SettleAmountCent,
|
||||
"settle_amount_cent": effectiveSettleAmountCent,
|
||||
"settled_adjustment_count": pendingTotals.Count,
|
||||
"before_offline_settlement_status": OfflineSettlementStatusPending,
|
||||
"after_offline_settlement_status": pickup.OfflineSettlementStatus,
|
||||
"remark": pickup.OfflineSettlementRemark,
|
||||
@@ -394,6 +415,170 @@ func (r *Repository) UpdateProfit(ctx context.Context, pickupID uint64, req Upda
|
||||
return r.FindByID(ctx, pickupID)
|
||||
}
|
||||
|
||||
// CreateFinancialAdjustment 为已完成提号追加财务差额,不改写完成时的原始金额。
|
||||
func (r *Repository) CreateFinancialAdjustment(ctx context.Context, pickupID uint64, req FinancialAdjustmentRequest, adminID uint64, meta auditlog.Meta) (*PickupDTO, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var pickup model.AdminPickup
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&pickup, pickupID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrPickupNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
if pickup.Status != StatusCompleted {
|
||||
return ErrFinancialAdjustmentInvalid
|
||||
}
|
||||
|
||||
totals, err := pickupFinancialAdjustmentTotals(tx, pickup.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
profitDeltaCent := int64(0)
|
||||
if req.ProfitAmountCent != nil {
|
||||
profitDeltaCent = *req.ProfitAmountCent - (pickup.ProfitAmountCent + totals.ProfitAdjustmentCent)
|
||||
}
|
||||
settleDeltaCent := int64(0)
|
||||
if req.SettleAmountCent != nil {
|
||||
settleDeltaCent = *req.SettleAmountCent - (pickup.SettleAmountCent + totals.SettleAdjustmentCent)
|
||||
}
|
||||
if profitDeltaCent == 0 && settleDeltaCent == 0 {
|
||||
return ErrFinancialAdjustmentInvalid
|
||||
}
|
||||
|
||||
status := FinancialAdjustmentStatusSettled
|
||||
if settleDeltaCent != 0 {
|
||||
if pickupRequiresOfflineSettlement(pickup) && pickup.OfflineSettlementStatus == OfflineSettlementStatusPending {
|
||||
// 原结算尚未付款时,差额随同一次线下打款处理。
|
||||
status = FinancialAdjustmentStatusPending
|
||||
} else if settleDeltaCent < 0 {
|
||||
status = FinancialAdjustmentStatusRecovery
|
||||
} else if pickupRequiresOfflineSettlement(pickup) {
|
||||
status = FinancialAdjustmentStatusPending
|
||||
}
|
||||
}
|
||||
adjustment := model.AdminPickupFinancialAdjustment{
|
||||
PickupID: pickup.ID,
|
||||
ProfitDeltaCent: profitDeltaCent,
|
||||
SettleDeltaCent: settleDeltaCent,
|
||||
SettlementMode: pickup.SettlementMode,
|
||||
Status: status,
|
||||
Reason: strings.TrimSpace(req.Reason),
|
||||
CreatedBy: adminID,
|
||||
}
|
||||
if err := tx.Create(&adjustment).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 站内钱包的补款在创建调整时立即到账;扣款统一转待追回,避免余额被扣成负数。
|
||||
if pickup.SettlementMode == SettlementModeOwnerWallet && settleDeltaCent > 0 {
|
||||
if err := wallet.AppendEntries(tx, wallet.Entry{
|
||||
UserID: pickup.OwnerID,
|
||||
Direction: "in",
|
||||
AmountCent: settleDeltaCent,
|
||||
BalanceType: "available",
|
||||
BizType: "admin_pickup_adjustment",
|
||||
BizNo: fmt.Sprintf("%s-A%d", pickup.PickupNo, adjustment.ID),
|
||||
Remark: fmt.Sprintf("管理员提号结算补款:%s", pickup.PickupNo),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
bid := adjustment.ID
|
||||
return auditlog.Append(tx, auditlog.Entry{
|
||||
ActorType: "admin",
|
||||
ActorID: adminID,
|
||||
Action: "pickup_financial_adjustment_create",
|
||||
BizType: "admin_pickup_financial_adjustment",
|
||||
BizID: &bid,
|
||||
Meta: meta,
|
||||
Detail: map[string]any{
|
||||
"pickup_id": pickup.ID,
|
||||
"pickup_no": pickup.PickupNo,
|
||||
"profit_delta_cent": profitDeltaCent,
|
||||
"settle_delta_cent": settleDeltaCent,
|
||||
"settlement_mode": pickup.SettlementMode,
|
||||
"adjustment_status": status,
|
||||
"reason": adjustment.Reason,
|
||||
},
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.FindByID(ctx, pickupID)
|
||||
}
|
||||
|
||||
// ListFinancialAdjustments 返回提号的不可变财务调整流水。
|
||||
func (r *Repository) ListFinancialAdjustments(ctx context.Context, pickupID uint64) ([]FinancialAdjustmentDTO, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.AdminPickup{}).Where("id = ?", pickupID).Count(&count).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count == 0 {
|
||||
return nil, ErrPickupNotFound
|
||||
}
|
||||
items := make([]FinancialAdjustmentDTO, 0)
|
||||
if err := r.db.WithContext(ctx).Model(&model.AdminPickupFinancialAdjustment{}).
|
||||
Where("pickup_id = ?", pickupID).
|
||||
Order("id DESC").
|
||||
Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// SettleFinancialAdjustment 确认平台代管的补款或追回调整已在线下处理。
|
||||
func (r *Repository) SettleFinancialAdjustment(ctx context.Context, adjustmentID uint64, req FinancialAdjustmentSettlementRequest, adminID uint64, meta auditlog.Meta) error {
|
||||
if r == nil || r.db == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var adjustment model.AdminPickupFinancialAdjustment
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&adjustment, adjustmentID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrFinancialAdjustmentNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
if adjustment.Status != FinancialAdjustmentStatusPending && adjustment.Status != FinancialAdjustmentStatusRecovery {
|
||||
return ErrFinancialAdjustmentCannotSettle
|
||||
}
|
||||
beforeStatus := adjustment.Status
|
||||
now := time.Now()
|
||||
adjustment.Status = FinancialAdjustmentStatusSettled
|
||||
adjustment.SettledBy = &adminID
|
||||
adjustment.SettledAt = &now
|
||||
adjustment.SettlementRemark = strings.TrimSpace(req.Remark)
|
||||
if err := tx.Save(&adjustment).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
bid := adjustment.ID
|
||||
return auditlog.Append(tx, auditlog.Entry{
|
||||
ActorType: "admin",
|
||||
ActorID: adminID,
|
||||
Action: "pickup_financial_adjustment_settle",
|
||||
BizType: "admin_pickup_financial_adjustment",
|
||||
BizID: &bid,
|
||||
Meta: meta,
|
||||
Detail: map[string]any{
|
||||
"pickup_id": adjustment.PickupID,
|
||||
"profit_delta_cent": adjustment.ProfitDeltaCent,
|
||||
"settle_delta_cent": adjustment.SettleDeltaCent,
|
||||
"before_status": beforeStatus,
|
||||
"after_status": adjustment.Status,
|
||||
"settlement_remark": adjustment.SettlementRemark,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Cancel 取消提号:恢复 listing 为 published + 解锁 in_transaction,账号恢复可租。
|
||||
func (r *Repository) Cancel(ctx context.Context, pickupID uint64, reason string, adminID uint64, meta auditlog.Meta) error {
|
||||
if r == nil || r.db == nil {
|
||||
@@ -560,6 +745,8 @@ func (r *Repository) paginatePickups(db *gorm.DB, page, pageSize int, sellerView
|
||||
if sellerView {
|
||||
item.WebsiteProfitCent = 0
|
||||
item.ProfitAmountCent = 0
|
||||
item.ProfitAdjustmentCent = 0
|
||||
item.EffectiveProfitAmountCent = 0
|
||||
item.BuyerRatio = 0
|
||||
item.ShopName = ""
|
||||
item.SourceChannel = ""
|
||||
@@ -618,7 +805,9 @@ func (r *Repository) ListAvailableListings(ctx context.Context, query AvailableL
|
||||
|
||||
func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
|
||||
return r.db.WithContext(ctx).Table("admin_pickups AS p").
|
||||
Select("p.*, l.listing_no, a.title AS account_title, a.server_region, a.login_platform, owner.phone AS owner_phone").
|
||||
Select(`p.*, l.listing_no, a.title AS account_title, a.server_region, a.login_platform, owner.phone AS owner_phone,
|
||||
COALESCE((SELECT SUM(fa.profit_delta_cent) FROM admin_pickup_financial_adjustments AS fa WHERE fa.pickup_id = p.id), 0) AS profit_adjustment_cent,
|
||||
COALESCE((SELECT SUM(fa.settle_delta_cent) FROM admin_pickup_financial_adjustments AS fa WHERE fa.pickup_id = p.id), 0) AS settle_adjustment_cent`).
|
||||
Joins("JOIN rental_listings AS l ON l.id = p.listing_id").
|
||||
Joins("JOIN game_accounts AS a ON a.id = p.account_id").
|
||||
Joins("JOIN users AS owner ON owner.id = p.owner_id")
|
||||
@@ -626,52 +815,87 @@ func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
|
||||
|
||||
type pickupRow struct {
|
||||
model.AdminPickup
|
||||
ListingNo string
|
||||
AccountTitle string
|
||||
ServerRegion string
|
||||
LoginPlatform string
|
||||
OwnerPhone string
|
||||
ListingNo string
|
||||
AccountTitle string
|
||||
ServerRegion string
|
||||
LoginPlatform string
|
||||
OwnerPhone string
|
||||
ProfitAdjustmentCent int64
|
||||
SettleAdjustmentCent int64
|
||||
}
|
||||
|
||||
func (row pickupRow) toDTO() PickupDTO {
|
||||
return PickupDTO{
|
||||
ID: row.ID,
|
||||
PickupNo: row.PickupNo,
|
||||
ListingID: row.ListingID,
|
||||
ListingNo: row.ListingNo,
|
||||
AccountID: row.AccountID,
|
||||
AccountTitle: row.AccountTitle,
|
||||
ServerRegion: row.ServerRegion,
|
||||
LoginPlatform: row.LoginPlatform,
|
||||
OwnerID: row.OwnerID,
|
||||
OwnerPhone: row.OwnerPhone,
|
||||
AdminID: row.AdminID,
|
||||
Platform: row.Platform,
|
||||
ShopName: row.ShopName,
|
||||
AccountSource: row.AccountSource,
|
||||
SourceChannel: row.SourceChannel,
|
||||
SettlementMode: row.SettlementMode,
|
||||
ListingPriceCent: row.ListingPriceCent,
|
||||
OwnerPriceCent: row.OwnerPriceCent,
|
||||
WebsiteProfitCent: row.WebsiteProfitCent,
|
||||
ProfitAmountCent: row.ProfitAmountCent,
|
||||
SellerRatio: row.SellerRatio,
|
||||
BuyerRatio: row.BuyerRatio,
|
||||
AccountSnapshot: row.AccountSnapshot,
|
||||
SettleAmountCent: row.SettleAmountCent,
|
||||
Status: row.Status,
|
||||
OfflineSettlementStatus: row.OfflineSettlementStatus,
|
||||
OfflineSettlementRemark: row.OfflineSettlementRemark,
|
||||
OfflineSettledBy: row.OfflineSettledBy,
|
||||
OfflineSettledAt: row.OfflineSettledAt,
|
||||
Remark: row.Remark,
|
||||
CompleteRemark: row.CompleteRemark,
|
||||
CreatedAt: row.CreatedAt,
|
||||
CompletedAt: row.CompletedAt,
|
||||
CancelledAt: row.CancelledAt,
|
||||
ID: row.ID,
|
||||
PickupNo: row.PickupNo,
|
||||
ListingID: row.ListingID,
|
||||
ListingNo: row.ListingNo,
|
||||
AccountID: row.AccountID,
|
||||
AccountTitle: row.AccountTitle,
|
||||
ServerRegion: row.ServerRegion,
|
||||
LoginPlatform: row.LoginPlatform,
|
||||
OwnerID: row.OwnerID,
|
||||
OwnerPhone: row.OwnerPhone,
|
||||
AdminID: row.AdminID,
|
||||
Platform: row.Platform,
|
||||
ShopName: row.ShopName,
|
||||
AccountSource: row.AccountSource,
|
||||
SourceChannel: row.SourceChannel,
|
||||
SettlementMode: row.SettlementMode,
|
||||
ListingPriceCent: row.ListingPriceCent,
|
||||
OwnerPriceCent: row.OwnerPriceCent,
|
||||
WebsiteProfitCent: row.WebsiteProfitCent,
|
||||
ProfitAmountCent: row.ProfitAmountCent,
|
||||
ProfitAdjustmentCent: row.ProfitAdjustmentCent,
|
||||
EffectiveProfitAmountCent: row.ProfitAmountCent + row.ProfitAdjustmentCent,
|
||||
SellerRatio: row.SellerRatio,
|
||||
BuyerRatio: row.BuyerRatio,
|
||||
AccountSnapshot: row.AccountSnapshot,
|
||||
SettleAmountCent: row.SettleAmountCent,
|
||||
SettleAdjustmentCent: row.SettleAdjustmentCent,
|
||||
EffectiveSettleAmountCent: row.SettleAmountCent + row.SettleAdjustmentCent,
|
||||
Status: row.Status,
|
||||
OfflineSettlementStatus: row.OfflineSettlementStatus,
|
||||
OfflineSettlementRemark: row.OfflineSettlementRemark,
|
||||
OfflineSettledBy: row.OfflineSettledBy,
|
||||
OfflineSettledAt: row.OfflineSettledAt,
|
||||
Remark: row.Remark,
|
||||
CompleteRemark: row.CompleteRemark,
|
||||
CreatedAt: row.CreatedAt,
|
||||
CompletedAt: row.CompletedAt,
|
||||
CancelledAt: row.CancelledAt,
|
||||
}
|
||||
}
|
||||
|
||||
type pickupAdjustmentTotals struct {
|
||||
ProfitAdjustmentCent int64
|
||||
SettleAdjustmentCent int64
|
||||
}
|
||||
|
||||
type pendingPickupAdjustmentTotals struct {
|
||||
SettleAdjustmentCent int64
|
||||
Count int64
|
||||
}
|
||||
|
||||
func pickupFinancialAdjustmentTotals(db *gorm.DB, pickupID uint64) (pickupAdjustmentTotals, error) {
|
||||
var totals pickupAdjustmentTotals
|
||||
err := db.Model(&model.AdminPickupFinancialAdjustment{}).
|
||||
Select(`COALESCE(SUM(profit_delta_cent), 0) AS profit_adjustment_cent,
|
||||
COALESCE(SUM(settle_delta_cent), 0) AS settle_adjustment_cent`).
|
||||
Where("pickup_id = ?", pickupID).
|
||||
Scan(&totals).Error
|
||||
return totals, err
|
||||
}
|
||||
|
||||
func pendingPickupFinancialAdjustmentTotals(db *gorm.DB, pickupID uint64) (pendingPickupAdjustmentTotals, error) {
|
||||
var totals pendingPickupAdjustmentTotals
|
||||
err := db.Model(&model.AdminPickupFinancialAdjustment{}).
|
||||
Select(`COALESCE(SUM(settle_delta_cent), 0) AS settle_adjustment_cent, COUNT(id) AS count`).
|
||||
Where("pickup_id = ? AND status IN ?", pickupID, []string{FinancialAdjustmentStatusPending, FinancialAdjustmentStatusRecovery}).
|
||||
Scan(&totals).Error
|
||||
return totals, err
|
||||
}
|
||||
|
||||
func pickupSettlementMode(listingMode, accountSource string) string {
|
||||
if listingMode == SettlementModePlatformManaged || accountSource == AccountSourceExternalPlatformManaged {
|
||||
return SettlementModePlatformManaged
|
||||
|
||||
Reference in New Issue
Block a user