163 lines
6.8 KiB
Go
163 lines
6.8 KiB
Go
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,
|
||
}
|
||
}
|