206 lines
5.6 KiB
Go
206 lines
5.6 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
|
|
CustomCategoryName 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,
|
|
CustomCategoryName: req.CustomCategoryName,
|
|
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,
|
|
"custom_category_name": req.CustomCategoryName,
|
|
"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,
|
|
CustomCategoryName: row.CustomCategoryName,
|
|
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
|
|
CustomCategoryName 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
|
|
}
|