Files
邹进 909f4d0ec2
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 8m39s
feat: 实名即生效套餐沉默期、延期办理与资产出库时间
- 迁移 000259~000265:套餐记录沉默期字段与扫描索引、套餐默认沉默期、延期政策/流水/订单/批次/明细 5 张表、默认政策与系统配置(总开关默认关闭)、企微审批白名单、卡与设备出库时间及出库记录
- 沉默期设置(订单发放、自动购包、钱包续费)、后台购买时调整(超默认值按政策免审或企微审批)、统一判定与激活闸门、排队与孤儿扫描跳过过期套餐、过期套餐禁止退款、每 10 分钟扫描
- 总开关专用接口与打开前 4 项检查;延期政策;延期资格与计费;H5 自助与后台二维码在线支付(商户池路由)、支付回调与查单恢复、线下登记、特殊/批量延期、异常重试与原路退款;沉默期审批场景与终态处理
- 换货沉默期流水与出库时间;卡/设备列表、详情、导出与出库记录接口;延期订单与即将过期套餐列表及导出;财务流水纳入延期支付
- 沉默期与出库时间回填工具 cmd/silence-backfill;前端接口变更说明

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:32:58 +09:00

471 lines
23 KiB
Go
Raw Permalink 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 packagesilence
import (
"context"
"strings"
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
approvalapp "github.com/break/junhong_cmp_fiber/internal/application/approval"
packagedomain "github.com/break/junhong_cmp_fiber/internal/domain/package"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
)
// DecisionHandler 消费沉默期审批场景 package_silence_approval 的标准终态(技术方案 §11.3)。
// 审批实例的业务 ID 为批次 ID,按批次路径分发:adjust 购买时调整、offline 线下付费延期、special 特殊延期。
// 回调不依赖表单文本;以批次与明细状态条件更新保证重复事件只处理一次。总开关关闭时照常处理。
type DecisionHandler struct {
extensions *ExtensionService
}
// NewDecisionHandler 创建沉默期审批终态消费者。
func NewDecisionHandler(extensions *ExtensionService) *DecisionHandler {
return &DecisionHandler{extensions: extensions}
}
var _ approvalapp.BusinessDecisionHandler = (*DecisionHandler)(nil)
// Handle 幂等消费标准审批终态。
func (h *DecisionHandler) Handle(ctx context.Context, event approvalapp.TerminalDecisionEvent) error {
if h == nil || h.extensions == nil {
return errors.New(errors.CodeInternalError, "沉默期审批终态能力未配置")
}
if event.BusinessType != constants.ApprovalBusinessTypePackageSilence || event.BusinessID == 0 || event.InstanceID == 0 {
return errors.New(errors.CodeInvalidParam, "沉默期审批终态参数无效")
}
ctx = auditcontext.With(ctx, auditcontext.Context{
ActorKind: constants.AuditActorExternalSystem, ActorID: "wecom_approval", ActorName: "企业微信审批",
Source: constants.AuditSourceCallback, CorrelationID: event.CorrelationID, ParentEventID: event.EventID,
})
approved := event.Decision == constants.ApprovalDecisionApproved
if event.Decision == constants.ApprovalDecisionRevokedAfterApproved {
// 通过后撤销:延期已按通过执行,不回滚。
return nil
}
s := h.extensions
var batch *model.PackageSilenceExtensionBatch
var postUsageIDs []uint
executeSpecial := false
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var err error
batch, err = lockBatchForDecision(ctx, tx, event.BusinessID)
if err != nil {
return err
}
if batch.ApprovalInstanceID == nil || *batch.ApprovalInstanceID != event.InstanceID {
return errors.New(errors.CodeConflict, "沉默期批次关联的审批实例不一致")
}
if batch.Status != constants.SilenceBatchStatusPendingApproval {
return nil
}
now := s.now()
approvalStatus := constants.SilenceApprovalStatusRejected
if approved {
approvalStatus = constants.SilenceApprovalStatusApproved
} else if event.Decision == constants.ApprovalDecisionCancelled || event.Decision == constants.ApprovalDecisionDeleted {
approvalStatus = constants.SilenceApprovalStatusCanceled
}
switch batch.Path {
case constants.SilencePathAdjust:
usageID, err := s.applyAdjustDecisionInTx(ctx, tx, batch, approved, approvalStatus, now)
if usageID > 0 {
postUsageIDs = append(postUsageIDs, usageID)
}
return err
case constants.SilencePathOffline:
usageID, err := s.applyOfflineDecisionInTx(ctx, tx, batch, approved, approvalStatus, now)
if usageID > 0 {
postUsageIDs = append(postUsageIDs, usageID)
}
return err
case constants.SilencePathSpecial:
executeSpecial, err = s.applySpecialDecisionInTx(ctx, tx, batch, approved, approvalStatus, now)
return err
default:
return errors.New(errors.CodeInvalidStatus, "沉默期批次路径不支持审批")
}
})
if err != nil {
return err
}
if executeSpecial {
return s.executeBatch(ctx, batch.ID)
}
if batch != nil && batch.Path == constants.SilencePathAdjust {
for _, id := range postUsageIDs {
if _, err := s.resolver.ResolveAndActivate(ctx, id, ResolveOptions{Trigger: constants.SilenceTriggerAdjustApproval, AllowExpire: s.sw.Enabled(ctx)}); err != nil {
return err
}
}
return nil
}
s.afterExecuted(ctx, postUsageIDs)
return nil
}
// applyAdjustDecisionInTx 处理购买时调整审批终态(技术方案 §4.3):通过按 起算日期 + 审批天数 重算截止日;
// 驳回或撤回保持默认值;两种情况都清除审批中标记,提交后立即重新判定。
func (s *ExtensionService) applyAdjustDecisionInTx(ctx context.Context, tx *gorm.DB, batch *model.PackageSilenceExtensionBatch, approved bool, approvalStatus string, now time.Time) (uint, error) {
if batch.AdjustPackageUsageID == nil {
return 0, errors.New(errors.CodeInvalidStatus, "调整申请缺少套餐记录")
}
var usage model.PackageUsage
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", *batch.AdjustPackageUsageID).First(&usage).Error; err != nil {
return 0, errors.Wrap(errors.CodeDatabaseError, err, "锁定套餐记录失败")
}
batchStatus := constants.SilenceBatchStatusRejected
values := map[string]any{"silence_adjust_pending": false, "updated_at": now}
beforeDeadline := copyTimePtr(usage.SilenceDeadlineAt)
days := usage.SilencePeriodDays
if approved && batch.AdjustRequestedDays != nil && usage.SilenceStartAt != nil {
deadline := packagedomain.SilenceDeadline(*usage.SilenceStartAt, *batch.AdjustRequestedDays)
values["silence_period_days"] = *batch.AdjustRequestedDays
values["silence_deadline_at"] = deadline
usage.SilenceDeadlineAt = &deadline
days = batch.AdjustRequestedDays
batchStatus = constants.SilenceBatchStatusCompleted
}
if err := tx.WithContext(ctx).Model(&model.PackageUsage{}).Where("id = ? AND silence_adjust_pending = ?", usage.ID, true).Updates(values).Error; err != nil {
return 0, errors.Wrap(errors.CodeDatabaseError, err, "更新套餐沉默期调整结果失败")
}
usage.SilenceAdjustPending = false
usage.SilencePeriodDays = days
if _, err := postgres.NewPackageSilenceBatchStore(tx).TransitionStatus(ctx, batch.ID, []string{constants.SilenceBatchStatusPendingApproval}, batchStatus, map[string]any{
"approval_status": approvalStatus, "approval_finished_at": now, "completed_at": now,
"succeeded_count": boolToInt(approved), "failed_count": boolToInt(!approved),
}); err != nil {
return 0, errors.Wrap(errors.CodeDatabaseError, err, "更新调整申请状态失败")
}
action, summary := constants.AuditActionUsageSilenceAdjustRejected, "购买时调整沉默期审批未通过,保持默认值"
if approved {
action, summary = constants.AuditActionUsageSilenceAdjustApproved, "购买时调整沉默期审批通过,重算截止日"
}
approvalID, batchID := *batch.ApprovalInstanceID, batch.ID
changeDays := 0
if batch.AdjustRequestedDays != nil && batch.AdjustDefaultDays != nil {
changeDays = *batch.AdjustRequestedDays - *batch.AdjustDefaultDays
}
err := writeRecord(ctx, tx, s.audit, recordInput{
Usage: &usage, EventType: constants.SilenceRecordEventAdjust, Trigger: constants.SilenceTriggerAdjustApproval,
BeforeStatus: copyStringPtr(usage.SilenceStatus), BeforeDeadline: beforeDeadline, DefaultDays: batch.AdjustDefaultDays,
IsAdjusted: approved, ChangeDays: changeDays, Reason: batch.Reason, ApprovalID: &approvalID, BatchID: &batchID,
Extra: map[string]any{"stage": approvalStatus, "requested_days": batch.AdjustRequestedDays},
AuditAction: action, AuditSummary: summary,
})
return usage.ID, err
}
// applyOfflineDecisionInTx 处理线下付费延期审批:通过执行延期,驳回或撤回订单 rejected、不延期、系统不退款。
func (s *ExtensionService) applyOfflineDecisionInTx(ctx context.Context, tx *gorm.DB, batch *model.PackageSilenceExtensionBatch, approved bool, approvalStatus string, now time.Time) (uint, error) {
var order model.PackageSilenceExtensionOrder
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("batch_id = ?", batch.ID).First(&order).Error; err != nil {
return 0, errors.Wrap(errors.CodeDatabaseError, err, "锁定线下延期订单失败")
}
if _, err := postgres.NewPackageSilenceBatchStore(tx).TransitionStatus(ctx, batch.ID, []string{constants.SilenceBatchStatusPendingApproval},
map[bool]string{true: constants.SilenceBatchStatusExecuting, false: constants.SilenceBatchStatusRejected}[approved],
map[string]any{"approval_status": approvalStatus, "approval_finished_at": now}); err != nil {
return 0, errors.Wrap(errors.CodeDatabaseError, err, "更新线下延期批次失败")
}
var usage model.PackageUsage
_ = tx.WithContext(ctx).Where("id = ?", order.PackageUsageID).First(&usage).Error
if !approved {
if _, err := postgres.NewPackageSilenceOrderStore(tx).TransitionStatus(ctx, order.ID, []string{constants.SilenceOrderStatusPendingApproval}, constants.SilenceOrderStatusRejected, nil); err != nil {
return 0, errors.Wrap(errors.CodeDatabaseError, err, "更新线下延期订单为已驳回失败")
}
if err := tx.Model(&model.PackageSilenceExtension{}).Where("batch_id = ? AND status = ?", batch.ID, constants.SilenceExtensionStatusPendingApproval).
Updates(map[string]any{"status": constants.SilenceExtensionStatusRejected, "updated_at": now}).Error; err != nil {
return 0, errors.Wrap(errors.CodeDatabaseError, err, "更新线下延期明细为已驳回失败")
}
if err := tx.Model(&model.PackageSilenceExtensionBatch{}).Where("id = ?", batch.ID).Updates(map[string]any{"failed_count": 1, "completed_at": now}).Error; err != nil {
return 0, errors.Wrap(errors.CodeDatabaseError, err, "更新线下延期批次计数失败")
}
before := order.Status
order.Status = constants.SilenceOrderStatusRejected
if usage.ID > 0 && s.audit != nil {
_ = s.audit.AppendSilenceUsage(ctx, tx, extensionAuditInput(&usage, batch, constants.AuditActionUsageSilenceExtensionRejected, "线下付费延期审批未通过"))
}
return 0, s.writeOrderAudit(ctx, tx, &order, before, "线下付费延期审批未通过,订单已驳回")
}
if usage.ID > 0 && s.audit != nil {
_ = s.audit.AppendSilenceUsage(ctx, tx, extensionAuditInput(&usage, batch, constants.AuditActionUsageSilenceExtensionApproved, "线下付费延期审批通过"))
}
ok, err := s.executeOrderInTx(ctx, tx, &order)
if err != nil || !ok {
return 0, err
}
return order.PackageUsageID, nil
}
// applySpecialDecisionInTx 处理特殊延期审批:通过时明细置为已批准、提交后逐条执行;驳回或撤回不产生任何变化。
func (s *ExtensionService) applySpecialDecisionInTx(ctx context.Context, tx *gorm.DB, batch *model.PackageSilenceExtensionBatch, approved bool, approvalStatus string, now time.Time) (bool, error) {
batchStatus, detailStatus := constants.SilenceBatchStatusRejected, constants.SilenceExtensionStatusRejected
action, summary := constants.AuditActionUsageSilenceExtensionRejected, "特殊延期审批未通过"
if approved {
batchStatus, detailStatus = constants.SilenceBatchStatusExecuting, constants.SilenceExtensionStatusApproved
action, summary = constants.AuditActionUsageSilenceExtensionApproved, "特殊延期审批通过"
}
if _, err := postgres.NewPackageSilenceBatchStore(tx).TransitionStatus(ctx, batch.ID, []string{constants.SilenceBatchStatusPendingApproval}, batchStatus,
map[string]any{"approval_status": approvalStatus, "approval_finished_at": now}); err != nil {
return false, errors.Wrap(errors.CodeDatabaseError, err, "更新特殊延期批次失败")
}
var extensions []model.PackageSilenceExtension
if err := tx.WithContext(ctx).Where("batch_id = ? AND status = ?", batch.ID, constants.SilenceExtensionStatusPendingApproval).Find(&extensions).Error; err != nil {
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询特殊延期明细失败")
}
if err := tx.Model(&model.PackageSilenceExtension{}).Where("batch_id = ? AND status = ?", batch.ID, constants.SilenceExtensionStatusPendingApproval).
Updates(map[string]any{"status": detailStatus, "updated_at": now}).Error; err != nil {
return false, errors.Wrap(errors.CodeDatabaseError, err, "更新特殊延期明细失败")
}
if !approved {
if err := tx.Model(&model.PackageSilenceExtensionBatch{}).Where("id = ?", batch.ID).
Updates(map[string]any{"failed_count": len(extensions), "completed_at": now}).Error; err != nil {
return false, errors.Wrap(errors.CodeDatabaseError, err, "更新特殊延期批次计数失败")
}
}
for index := range extensions {
var usage model.PackageUsage
if err := tx.WithContext(ctx).Where("id = ?", extensions[index].PackageUsageID).First(&usage).Error; err == nil && s.audit != nil {
_ = s.audit.AppendSilenceUsage(ctx, tx, extensionAuditInput(&usage, batch, action, summary))
}
}
return approved, nil
}
// HandleException 后台处理异常明细(权限 package_silence:exception):重试执行或关闭;不得直接改库时间。
// 在线订单关闭时按冻结商户原路退款。
func (s *ExtensionService) HandleException(ctx context.Context, extensionID uint, req dto.SilenceExceptionHandleRequest) (*dto.SilenceExtensionResponse, error) {
if err := s.requirePermission(ctx, constants.PermissionPackageSilenceException); err != nil {
return nil, err
}
reason := strings.TrimSpace(req.Reason)
if reason == "" {
return nil, errors.New(errors.CodeInvalidParam, "处理原因不能为空")
}
extension, err := postgres.NewPackageSilenceExtensionStore(s.db).GetByID(ctx, extensionID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "延期明细不存在")
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询延期明细失败")
}
if _, err := s.loadUsage(ctx, s.db, extension.PackageUsageID, Scope{}); err != nil {
return nil, err
}
if extension.Status != constants.SilenceExtensionStatusException {
return nil, errors.New(errors.CodeInvalidStatus, "只能处理执行异常的明细")
}
operatorID := middleware.GetUserIDFromContext(ctx)
now := s.now()
executed, refundOrderID := false, uint(0)
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
locked, err := postgres.NewPackageSilenceExtensionStore(tx).LockByID(ctx, extensionID)
if err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "锁定延期明细失败")
}
if locked.Status != constants.SilenceExtensionStatusException {
return errors.New(errors.CodeInvalidStatus, "只能处理执行异常的明细")
}
var order *model.PackageSilenceExtensionOrder
if locked.ExtensionOrderID != nil {
if order, err = postgres.NewPackageSilenceOrderStore(tx).LockByID(ctx, *locked.ExtensionOrderID); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "锁定延期订单失败")
}
}
if err := tx.Model(&model.PackageSilenceExtension{}).Where("id = ?", locked.ID).
Updates(map[string]any{"handled_by": operatorID, "handled_at": now}).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "记录异常处理人失败")
}
if req.Action == "close" {
if _, err := postgres.NewPackageSilenceExtensionStore(tx).TransitionStatus(ctx, locked.ID, []string{constants.SilenceExtensionStatusException},
constants.SilenceExtensionStatusClosed, map[string]any{"failure_summary": truncate("人工关闭:"+reason, 500)}); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "关闭异常明细失败")
}
if order != nil && order.PaymentMethod != model.PaymentByOffline && order.Status == constants.SilenceOrderStatusException {
refundOrderID = order.ID
}
if order != nil && order.PaymentMethod == model.PaymentByOffline && order.Status == constants.SilenceOrderStatusException {
if _, err := postgres.NewPackageSilenceOrderStore(tx).TransitionStatus(ctx, order.ID, []string{constants.SilenceOrderStatusException},
constants.SilenceOrderStatusClosed, map[string]any{"closed_at": now, "close_reason": truncate("人工关闭:"+reason, 200)}); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "关闭线下延期订单失败")
}
}
return refreshBatchCountsInTx(ctx, tx, locked.BatchID, now)
}
// 重试:按原明细与快照重新执行
batch, err := postgres.NewPackageSilenceBatchStore(tx).GetByID(ctx, locked.BatchID)
if err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询延期批次失败")
}
if order != nil {
if order.Status != constants.SilenceOrderStatusException {
return errors.New(errors.CodeInvalidStatus, "延期订单不是异常状态,不能重试")
}
if order.RefundNo != nil {
return errors.New(errors.CodeInvalidStatus, "延期订单已发起原路退款,不能重试")
}
ok, err := s.executeOrderInTx(ctx, tx, order)
executed = ok
return err
}
from := []string{constants.SilenceExtensionStatusException}
outcome, err := s.executeExtensionInTx(ctx, tx, locked, executeKind{free: batch.ExtensionType == constants.SilenceExtensionTypeFree},
paidSnapshot{freeLimit: batch.FreeExtensionLimitSnapshot}, from)
if err != nil {
return err
}
if !outcome.executed {
return errors.NewWithData(errors.CodeInvalidStatus, map[string]any{"failure_code": outcome.failureCode}, "重试仍不满足延期条件:"+outcome.failureText)
}
executed = true
return refreshBatchCountsInTx(ctx, tx, locked.BatchID, now)
})
if err != nil {
return nil, err
}
if executed {
s.afterExecuted(ctx, []uint{extension.PackageUsageID})
}
if refundOrderID > 0 {
if err := s.refundOrder(ctx, refundOrderID, "异常明细人工关闭,原路退款:"+reason); err != nil {
return nil, err
}
}
current, err := postgres.NewPackageSilenceExtensionStore(s.db).GetByID(ctx, extensionID)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询延期明细失败")
}
resp := extensionDTO(current)
return &resp, nil
}
// BatchDetail 查询延期批次与明细。
func (s *ExtensionService) BatchDetail(ctx context.Context, batchID uint) (*dto.SilenceBatchResponse, error) {
batch, err := postgres.NewPackageSilenceBatchStore(s.db).GetByID(ctx, batchID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "延期批次不存在")
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询延期批次失败")
}
extensions, err := postgres.NewPackageSilenceExtensionStore(s.db).ListByBatch(ctx, batchID)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询延期明细失败")
}
if len(extensions) > 0 {
if _, err := s.loadUsage(ctx, s.db, extensions[0].PackageUsageID, Scope{}); err != nil {
return nil, err
}
} else if batch.AdjustPackageUsageID != nil {
if _, err := s.loadUsage(ctx, s.db, *batch.AdjustPackageUsageID, Scope{}); err != nil {
return nil, err
}
}
resp := BatchDTO(batch)
for index := range extensions {
resp.Items = append(resp.Items, extensionDTO(&extensions[index]))
}
return resp, nil
}
// Records 按套餐记录查询沉默期流水(后台与 H5)。
func (s *ExtensionService) Records(ctx context.Context, usageID uint, scope Scope) ([]dto.SilenceRecordResponse, error) {
if _, err := s.loadUsage(ctx, s.db, usageID, scope); err != nil {
return nil, err
}
records, err := postgres.NewPackageSilenceRecordStore(s.db).ListByUsage(ctx, usageID)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询沉默期流水失败")
}
items := make([]dto.SilenceRecordResponse, 0, len(records))
for index := range records {
record := &records[index]
items = append(items, dto.SilenceRecordResponse{
ID: record.ID, PackageUsageID: record.PackageUsageID, EventType: record.EventType,
EventName: RecordEventName(record.EventType), TriggerSource: record.TriggerSource, AssetType: record.AssetType,
AssetID: record.AssetID, BeforeStatus: record.BeforeStatus, AfterStatus: record.AfterStatus,
BeforeDeadlineAt: record.BeforeDeadlineAt, AfterDeadlineAt: record.AfterDeadlineAt, PeriodDays: record.PeriodDays,
DefaultDays: record.DefaultDaysSnapshot, IsAdjusted: record.IsAdjusted, ChangeDays: record.ChangeDays,
OperatorID: record.OperatorID, OperatorType: record.OperatorType, Reason: record.Reason,
ApprovalInstanceID: record.ApprovalInstanceID, BatchID: record.BatchID, ExtensionID: record.ExtensionID,
ExchangeNo: record.ExchangeNo, CreatedAt: record.CreatedAt,
})
}
return items, nil
}
// RecordEventName 返回流水事件名称。
func RecordEventName(event string) string {
switch event {
case constants.SilenceRecordEventSet:
return "设置沉默期"
case constants.SilenceRecordEventAdjust:
return "购买时调整"
case constants.SilenceRecordEventExpired:
return "沉默期过期"
case constants.SilenceRecordEventEnded:
return "期内实名结束"
case constants.SilenceRecordEventExtend:
return "延期"
case constants.SilenceRecordEventExchange:
return "换货迁移"
case constants.SilenceRecordEventBackfill:
return "历史回填"
case constants.SilenceRecordEventRealnameBackfill:
return "补写实名时间"
default:
return event
}
}
// BatchDTO 构造批次响应(不含明细)。
func BatchDTO(batch *model.PackageSilenceExtensionBatch) *dto.SilenceBatchResponse {
return &dto.SilenceBatchResponse{
ID: batch.ID, BatchNo: batch.BatchNo, Path: batch.Path, ExtensionType: batch.ExtensionType,
ApprovalRequired: batch.ApprovalRequired, ApprovalInstanceID: batch.ApprovalInstanceID,
ApprovalStatus: batch.ApprovalStatus, Status: batch.Status, Reason: batch.Reason, Basis: batch.Basis,
EvidenceRefs: nonNil([]string(batch.EvidenceRefs)), Amount: batch.Amount, RequestedBy: batch.RequestedBy,
RequestedAt: batch.RequestedAt, RequestedCount: batch.RequestedCount, SucceededCount: batch.SucceededCount,
FailedCount: batch.FailedCount, CompletedAt: batch.CompletedAt,
}
}
func extensionDTO(extension *model.PackageSilenceExtension) dto.SilenceExtensionResponse {
return dto.SilenceExtensionResponse{
ID: extension.ID, BatchID: extension.BatchID, RowNo: extension.RowNo, PackageUsageID: extension.PackageUsageID,
PackageName: extension.PackageNameSnapshot, AssetType: extension.AssetType, AssetIdentifier: extension.AssetIdentifierSnapshot,
ExtensionType: extension.ExtensionType, InputDays: extension.InputDays, Months: extension.Months,
ExtensionDays: extension.ExtensionDays, Amount: extension.Amount, ExtensionOrderID: extension.ExtensionOrderID,
BeforeDeadlineAt: extension.BeforeDeadlineAt, AfterDeadlineAt: extension.AfterDeadlineAt, Status: extension.Status,
FailureCode: extension.FailureCode, FailureSummary: extension.FailureSummary, ExecutedAt: extension.ExecutedAt,
}
}
func extensionAuditInput(usage *model.PackageUsage, batch *model.PackageSilenceExtensionBatch, action, summary string) audit.SilenceAuditInput {
return audit.SilenceAuditInput{
ActionCode: action, Summary: summary, Usage: usage,
AfterData: map[string]any{"batch_status": batch.Status},
Metadata: map[string]any{
"batch_no": batch.BatchNo, "batch_id": batch.ID, "path": batch.Path, "extension_type": batch.ExtensionType,
"approval_required": batch.ApprovalRequired, "approval_instance_id": batch.ApprovalInstanceID,
"amount": batch.Amount, "reason": truncate(batch.Reason, 200), "evidence_refs": []string(batch.EvidenceRefs),
"requested_by": batch.RequestedBy,
},
}
}
func boolToInt(value bool) int {
if value {
return 1
}
return 0
}