163 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
}
}