148 lines
6.0 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 mysql
import (
"context"
"fmt"
"strings"
"time"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
"hyapp/services/wallet-service/internal/domain/ledger"
)
// FreezeSalaryWithdrawal 在同一事务里把工资 available 转入 frozen并落交易、分录和余额 outbox。
func (r *Repository) FreezeSalaryWithdrawal(ctx context.Context, command ledger.SalaryWithdrawalCommand) (ledger.SalaryWithdrawalReceipt, error) {
return r.applySalaryWithdrawal(ctx, command, salaryWithdrawalMutation{
BizType: bizTypeSalaryWithdrawalFreeze,
AvailableDelta: -command.SalaryUSDMinor,
FrozenDelta: command.SalaryUSDMinor,
ExternalRef: command.WithdrawalRef,
})
}
// SettleSalaryWithdrawal 在审核通过时只扣 frozen这保证提交申请时已经冻结的金额不会被重复扣 available。
func (r *Repository) SettleSalaryWithdrawal(ctx context.Context, command ledger.SalaryWithdrawalCommand) (ledger.SalaryWithdrawalReceipt, error) {
return r.applySalaryWithdrawal(ctx, command, salaryWithdrawalMutation{
BizType: bizTypeSalaryWithdrawalSettle,
AvailableDelta: 0,
FrozenDelta: -command.SalaryUSDMinor,
ExternalRef: command.WithdrawalApplicationID,
})
}
// ReleaseSalaryWithdrawal 在拒绝或创建申请失败回滚时把 frozen 返还 available。
func (r *Repository) ReleaseSalaryWithdrawal(ctx context.Context, command ledger.SalaryWithdrawalCommand) (ledger.SalaryWithdrawalReceipt, error) {
return r.applySalaryWithdrawal(ctx, command, salaryWithdrawalMutation{
BizType: bizTypeSalaryWithdrawalRelease,
AvailableDelta: command.SalaryUSDMinor,
FrozenDelta: -command.SalaryUSDMinor,
ExternalRef: command.WithdrawalApplicationID,
})
}
type salaryWithdrawalMutation struct {
BizType string
AvailableDelta int64
FrozenDelta int64
ExternalRef string
}
func (r *Repository) applySalaryWithdrawal(ctx context.Context, command ledger.SalaryWithdrawalCommand, mutation salaryWithdrawalMutation) (ledger.SalaryWithdrawalReceipt, error) {
if r == nil || r.db == nil {
return ledger.SalaryWithdrawalReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
ctx = contextWithCommandApp(ctx, command.AppCode)
command.AppCode = appcode.FromContext(ctx)
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return ledger.SalaryWithdrawalReceipt{}, err
}
defer func() { _ = tx.Rollback() }()
requestHash := salaryWithdrawalRequestHash(command, mutation.BizType)
if txRow, exists, err := r.lookupTransactionWithConflictCode(ctx, tx, command.CommandID, requestHash, mutation.BizType, xerr.IdempotencyConflict); err != nil || exists {
if err != nil || !exists {
return ledger.SalaryWithdrawalReceipt{}, err
}
return r.receiptForSalaryWithdrawalTransaction(ctx, tx, txRow.TransactionID)
}
nowMs := time.Now().UTC().UnixMilli()
account, err := r.lockAccount(ctx, tx, command.UserID, command.SalaryAssetType, false, nowMs)
if err != nil {
return ledger.SalaryWithdrawalReceipt{}, err
}
if account.AvailableAmount+mutation.AvailableDelta < 0 || account.FrozenAmount+mutation.FrozenDelta < 0 {
return ledger.SalaryWithdrawalReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance")
}
transactionID := transactionID(command.AppCode, command.CommandID)
availableAfter := account.AvailableAmount + mutation.AvailableDelta
frozenAfter := account.FrozenAmount + mutation.FrozenDelta
metadata := salaryWithdrawalMetadata{
AppCode: command.AppCode,
UserID: command.UserID,
SalaryAssetType: command.SalaryAssetType,
SalaryUSDMinor: command.SalaryUSDMinor,
PointFeeAmount: command.PointFeeAmount,
PointNetAmount: command.PointNetAmount,
PointsPerUSD: command.PointsPerUSD,
PointWithdrawFeeBPS: command.PointWithdrawFeeBPS,
OperatorUserID: command.OperatorUserID,
Reason: command.Reason,
WithdrawalRef: command.WithdrawalRef,
WithdrawalApplicationID: command.WithdrawalApplicationID,
AvailableAfter: availableAfter,
FrozenAfter: frozenAfter,
Version: account.Version + 1,
CreatedAtMS: nowMs,
}
if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, mutation.BizType, requestHash, strings.TrimSpace(mutation.ExternalRef), metadata, nowMs); err != nil {
return ledger.SalaryWithdrawalReceipt{}, err
}
if err := r.applyAccountDelta(ctx, tx, account, mutation.AvailableDelta, mutation.FrozenDelta, nowMs); err != nil {
return ledger.SalaryWithdrawalReceipt{}, err
}
if err := r.insertEntry(ctx, tx, walletEntry{
TransactionID: transactionID,
UserID: command.UserID,
AssetType: command.SalaryAssetType,
AvailableDelta: mutation.AvailableDelta,
FrozenDelta: mutation.FrozenDelta,
AvailableAfter: availableAfter,
FrozenAfter: frozenAfter,
CreatedAtMS: nowMs,
}); err != nil {
return ledger.SalaryWithdrawalReceipt{}, err
}
if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
balanceChangedEvent(transactionID, command.CommandID, command.UserID, command.SalaryAssetType, mutation.AvailableDelta, mutation.FrozenDelta, availableAfter, frozenAfter, account.Version+1, metadata, nowMs, mutation.BizType),
}); err != nil {
return ledger.SalaryWithdrawalReceipt{}, err
}
if err := tx.Commit(); err != nil {
return ledger.SalaryWithdrawalReceipt{}, err
}
return receiptFromSalaryWithdrawalMetadata(transactionID, metadata), nil
}
func salaryWithdrawalRequestHash(command ledger.SalaryWithdrawalCommand, bizType string) string {
return stableHash(fmt.Sprintf("salary_withdrawal|%s|%s|%d|%s|%d|%d|%d|%d|%d|%d|%s|%s|%s",
bizType,
appcode.Normalize(command.AppCode),
command.UserID,
strings.ToUpper(strings.TrimSpace(command.SalaryAssetType)),
command.SalaryUSDMinor,
command.PointFeeAmount,
command.PointNetAmount,
command.PointsPerUSD,
command.PointWithdrawFeeBPS,
command.OperatorUserID,
strings.TrimSpace(command.Reason),
strings.TrimSpace(command.WithdrawalRef),
strings.TrimSpace(command.WithdrawalApplicationID),
))
}