Files

201 lines
5.1 KiB
Go

package adminfinance
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"time"
"hfb_sys/backend/internal/auditlog"
"hfb_sys/backend/internal/timeutil"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type manualDisbursementRecord struct {
ID uint64
DisbursementNo string
Category string
PayeeName string
AmountCent int64
PaidAt time.Time
Remark string
VoucherURL string
Status string
CreatedBy uint64
VoidedBy *uint64
VoidedAt *time.Time
VoidReason string
CreatedAt time.Time
UpdatedAt time.Time
}
func (manualDisbursementRecord) TableName() string {
return "manual_disbursements"
}
func (r *Repository) CreateManualDisbursement(
ctx context.Context,
req CreateManualDisbursementRequest,
adminID uint64,
meta auditlog.Meta,
) (*ManualDisbursementDTO, error) {
disbursementNo, err := newManualDisbursementNo()
if err != nil {
return nil, err
}
record := manualDisbursementRecord{
DisbursementNo: disbursementNo,
Category: req.Category,
PayeeName: req.PayeeName,
AmountCent: req.AmountCent,
PaidAt: req.PaidAt,
Remark: req.Remark,
VoucherURL: req.VoucherURL,
Status: "paid",
CreatedBy: adminID,
}
err = r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&record).Error; err != nil {
return err
}
id := record.ID
return auditlog.Append(tx, auditlog.Entry{
ActorType: "admin",
ActorID: adminID,
Action: "manual_disbursement_create",
BizType: "manual_disbursement",
BizID: &id,
Meta: meta,
Detail: map[string]any{
"disbursement_no": disbursementNo,
"category": req.Category,
"payee_name": req.PayeeName,
"amount_cent": req.AmountCent,
"paid_at": req.PaidAt,
"has_voucher": req.VoucherURL != "",
},
})
})
if err != nil {
return nil, err
}
return r.findManualDisbursement(ctx, record.ID)
}
func (r *Repository) VoidManualDisbursement(
ctx context.Context,
id uint64,
reason string,
adminID uint64,
meta auditlog.Meta,
) (*ManualDisbursementDTO, error) {
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var record manualDisbursementRecord
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&record, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrManualDisbursementNotFound
}
return err
}
if record.Status != "paid" {
return ErrManualDisbursementNotPaid
}
now := timeutil.ShanghaiNow()
if err := tx.Model(&record).Updates(map[string]any{
"status": "voided",
"voided_by": adminID,
"voided_at": now,
"void_reason": reason,
"updated_at": now,
}).Error; err != nil {
return err
}
bizID := record.ID
return auditlog.Append(tx, auditlog.Entry{
ActorType: "admin",
ActorID: adminID,
Action: "manual_disbursement_void",
BizType: "manual_disbursement",
BizID: &bizID,
Meta: meta,
Detail: map[string]any{
"disbursement_no": record.DisbursementNo,
"amount_cent": record.AmountCent,
"reason": reason,
},
})
})
if err != nil {
return nil, err
}
return r.findManualDisbursement(ctx, id)
}
func (r *Repository) findManualDisbursement(ctx context.Context, id uint64) (*ManualDisbursementDTO, error) {
var row manualDisbursementDetailRow
err := r.db.WithContext(ctx).Table("manual_disbursements AS md").
Select(`md.*,
COALESCE(NULLIF(creator.nickname, ''), creator.username, '') AS created_by_name,
COALESCE(NULLIF(voider.nickname, ''), voider.username, '') AS voided_by_name`).
Joins("LEFT JOIN admin_users AS creator ON creator.id = md.created_by").
Joins("LEFT JOIN admin_users AS voider ON voider.id = md.voided_by").
Where("md.id = ?", id).
Take(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrManualDisbursementNotFound
}
if err != nil {
return nil, err
}
return &ManualDisbursementDTO{
ID: row.ID,
DisbursementNo: row.DisbursementNo,
Category: row.Category,
PayeeName: row.PayeeName,
AmountCent: row.AmountCent,
PaidAt: row.PaidAt,
Remark: row.Remark,
VoucherURL: row.VoucherURL,
Status: row.Status,
CreatedBy: row.CreatedBy,
CreatedByName: row.CreatedByName,
VoidedBy: row.VoidedBy,
VoidedByName: row.VoidedByName,
VoidedAt: row.VoidedAt,
VoidReason: row.VoidReason,
CreatedAt: row.CreatedAt,
}, nil
}
type manualDisbursementDetailRow struct {
ID uint64
DisbursementNo string
Category string
PayeeName string
AmountCent int64
PaidAt time.Time
Remark string
VoucherURL string
Status string
CreatedBy uint64
CreatedByName string
VoidedBy *uint64
VoidedByName string
VoidedAt *time.Time
VoidReason string
CreatedAt time.Time
}
func newManualDisbursementNo() (string, error) {
buf := make([]byte, 4)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return fmt.Sprintf("OD%s%s", timeutil.ShanghaiNow().Format("20060102150405"), hex.EncodeToString(buf)), nil
}