package wallet import ( "context" "strings" "hyapp/pkg/appcode" "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/domain/ledger" ) const ( defaultPointWithdrawalFeeBPS int64 = 500 defaultPointWithdrawalPointsPerUSD int64 = 100000 minPointWithdrawalGrossPoints int64 = 1000000 ) // FreezePointWithdrawal 把 Huwaa 收益积分从 available 冻结到 frozen;它复用钱包单边冻结账本,但不开放旧工资资产。 func (s *Service) FreezePointWithdrawal(ctx context.Context, command ledger.PointWithdrawalCommand) (ledger.PointWithdrawalReceipt, error) { if err := normalizePointWithdrawalCommand(&command, false); err != nil { return ledger.PointWithdrawalReceipt{}, err } if command.Reason == "" { command.Reason = "point withdrawal freeze" } if s.repository == nil { return ledger.PointWithdrawalReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) ctx = appcode.WithContext(ctx, command.AppCode) receipt, err := s.repository.FreezeSalaryWithdrawal(ctx, pointWithdrawalAsSalaryCommand(command)) if err != nil { return ledger.PointWithdrawalReceipt{}, err } return pointWithdrawalReceiptFromSalary(receipt), nil } // SettlePointWithdrawal 在财务审核通过时扣掉 frozen 积分;冻结时已扣 available,因此这里不能再次扣 available。 func (s *Service) SettlePointWithdrawal(ctx context.Context, command ledger.PointWithdrawalCommand) (ledger.PointWithdrawalReceipt, error) { if err := normalizePointWithdrawalCommand(&command, true); err != nil { return ledger.PointWithdrawalReceipt{}, err } if command.Reason == "" { command.Reason = "point withdrawal approved" } if s.repository == nil { return ledger.PointWithdrawalReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) ctx = appcode.WithContext(ctx, command.AppCode) receipt, err := s.repository.SettleSalaryWithdrawal(ctx, pointWithdrawalAsSalaryCommand(command)) if err != nil { return ledger.PointWithdrawalReceipt{}, err } return pointWithdrawalReceiptFromSalary(receipt), nil } // ReleasePointWithdrawal 在审核拒绝或申请落库失败时把 frozen 积分返还 available,保证用户不会被永久冻结。 func (s *Service) ReleasePointWithdrawal(ctx context.Context, command ledger.PointWithdrawalCommand) (ledger.PointWithdrawalReceipt, error) { if err := normalizePointWithdrawalCommand(&command, false); err != nil { return ledger.PointWithdrawalReceipt{}, err } if command.Reason == "" { command.Reason = "point withdrawal released" } if s.repository == nil { return ledger.PointWithdrawalReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) ctx = appcode.WithContext(ctx, command.AppCode) receipt, err := s.repository.ReleaseSalaryWithdrawal(ctx, pointWithdrawalAsSalaryCommand(command)) if err != nil { return ledger.PointWithdrawalReceipt{}, err } return pointWithdrawalReceiptFromSalary(receipt), nil } func normalizePointWithdrawalCommand(command *ledger.PointWithdrawalCommand, requireOperator bool) error { if command == nil || strings.TrimSpace(command.CommandID) == "" || command.UserID <= 0 || command.GrossPointAmount <= 0 { return xerr.New(xerr.InvalidArgument, "point withdrawal command is incomplete") } if requireOperator && command.OperatorUserID <= 0 { return xerr.New(xerr.InvalidArgument, "operator_user_id is required") } command.CommandID = strings.TrimSpace(command.CommandID) command.AppCode = appcode.Normalize(command.AppCode) if command.AppCode != "huwaa" { return xerr.New(xerr.InvalidArgument, "point withdrawal is only enabled for huwaa") } command.AssetType = strings.ToUpper(strings.TrimSpace(command.AssetType)) if !ledger.ValidPointWithdrawalAssetType(command.AssetType) { return xerr.New(xerr.InvalidArgument, "point asset_type is invalid") } if command.GrossPointAmount < minPointWithdrawalGrossPoints { return xerr.New(xerr.InvalidArgument, "point withdrawal amount is below minimum") } if command.PointsPerUSD <= 0 { command.PointsPerUSD = defaultPointWithdrawalPointsPerUSD } if command.FeeBPS <= 0 { command.FeeBPS = defaultPointWithdrawalFeeBPS } if command.FeeBPS < 0 || command.FeeBPS > 10000 { return xerr.New(xerr.InvalidArgument, "point withdrawal fee_bps is invalid") } expectedFee := command.GrossPointAmount * command.FeeBPS / 10000 if command.FeePointAmount == 0 { command.FeePointAmount = expectedFee } if command.FeePointAmount != expectedFee { return xerr.New(xerr.InvalidArgument, "point withdrawal fee is invalid") } expectedNet := command.GrossPointAmount - command.FeePointAmount if command.NetPointAmount == 0 { command.NetPointAmount = expectedNet } if command.NetPointAmount != expectedNet || command.NetPointAmount < 0 { return xerr.New(xerr.InvalidArgument, "point withdrawal net amount is invalid") } command.Reason = strings.TrimSpace(command.Reason) command.WithdrawalRef = strings.TrimSpace(command.WithdrawalRef) command.WithdrawalApplicationID = strings.TrimSpace(command.WithdrawalApplicationID) if len(command.CommandID) > 128 || len(command.Reason) > 512 || len(command.WithdrawalRef) > 128 || len(command.WithdrawalApplicationID) > 64 { return xerr.New(xerr.InvalidArgument, "point withdrawal text fields are too long") } return nil } func pointWithdrawalAsSalaryCommand(command ledger.PointWithdrawalCommand) ledger.SalaryWithdrawalCommand { return ledger.SalaryWithdrawalCommand{ AppCode: command.AppCode, CommandID: command.CommandID, UserID: command.UserID, SalaryAssetType: command.AssetType, SalaryUSDMinor: command.GrossPointAmount, PointFeeAmount: command.FeePointAmount, PointNetAmount: command.NetPointAmount, PointsPerUSD: command.PointsPerUSD, PointWithdrawFeeBPS: command.FeeBPS, OperatorUserID: command.OperatorUserID, Reason: command.Reason, WithdrawalRef: command.WithdrawalRef, WithdrawalApplicationID: command.WithdrawalApplicationID, } } func pointWithdrawalReceiptFromSalary(receipt ledger.SalaryWithdrawalReceipt) ledger.PointWithdrawalReceipt { return ledger.PointWithdrawalReceipt{ TransactionID: receipt.TransactionID, UserID: receipt.UserID, AssetType: receipt.SalaryAssetType, GrossPointAmount: receipt.SalaryUSDMinor, FeePointAmount: receipt.PointFeeAmount, NetPointAmount: receipt.PointNetAmount, PointsPerUSD: receipt.PointsPerUSD, FeeBPS: receipt.PointWithdrawFeeBPS, AvailableAfter: receipt.AvailableAfter, FrozenAfter: receipt.FrozenAfter, Version: receipt.Version, CreatedAtMS: receipt.CreatedAtMS, } }