feat(收口): 补齐 8 月迭代缺口并同步 Spec 与证据链
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- 新增六对成对迁移 000232–000237:H5 弹窗类型、退款结算标识与申请人备注、优先轮询事实字段与两个新终态、通道阈值命中留痕、手机号最近解绑人、提现资格校验留痕 - 退款:原因必填与申请人备注、来源支付与渠道流水冻结、线下处理流水号补录审计、按订单查询可选退款方式、企微审批材料补齐且新增字段缺失映射即明确失败 - 优先轮询:人工关闭、有效期到期独立周期任务、失败与过期人工重触发、事实字段与异常重试查询、资产解析端点只读投影 - 通道阈值:命中事实同事务留痕与命中记录查询;员工账单:列表筛选与详情投影;商户池:列表投影与统计周期语义;H5:弹窗类型与类别排序 - 手机号:有效关联数量与最近解绑人、短信验证码失败次数限制;导出:佣金明细十五列与报表序号列 - 时间筛选:三处新增筛选纳入统一严格解析契约,员工账单产生时间参数改名 - 同步 12 份主 Spec 需求、两端点与异步任务证据链,门禁 context-health 与 OpenSpec 校验通过
This commit is contained in:
@@ -5,8 +5,10 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
|
||||
employeecollectiondomain "github.com/break/junhong_cmp_fiber/internal/domain/employeecollection"
|
||||
@@ -15,11 +17,27 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/utils"
|
||||
)
|
||||
|
||||
// billDateLayout 是账单筛选使用的自然日格式。
|
||||
// billDateLayout 是核销申请列表筛选使用的自然日格式。
|
||||
// 账单列表与统计已改用统一时间筛选(start_time/end_time 严格解析),不再使用本格式;
|
||||
// 核销申请列表端点不在本次统一时间筛选的受影响端点表内,按 As-Is 保留其既有日期语义。
|
||||
const billDateLayout = "2006-01-02"
|
||||
|
||||
// parseBillDate 解析自然日筛选值;空值表示不筛选。仅供核销申请列表使用。
|
||||
func parseBillDate(value string) (*time.Time, error) {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := time.ParseInLocation(billDateLayout, trimmed, time.Local)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "账单筛选日期格式必须为 YYYY-MM-DD")
|
||||
}
|
||||
return &parsed, nil
|
||||
}
|
||||
|
||||
// BillQuery 查询员工代收款账单列表、统计与详情。
|
||||
// 列表与统计共用 applyBillScope 生成的同一个 *gorm.DB,禁止两处各写一套筛选。
|
||||
type BillQuery struct {
|
||||
@@ -127,11 +145,143 @@ func (q *BillQuery) Detail(ctx context.Context, id uint) (*dto.EmployeeCollectio
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
operations, err := q.billOperations(ctx, bill.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sourceOrder := q.billSourceSnapshot(ctx, &bill)
|
||||
return &dto.EmployeeCollectionBillDetailResponse{
|
||||
Bill: billResponse, Refunds: refunds, Allocations: allocations, Applications: applications,
|
||||
Operations: operations, SourceOrder: sourceOrder,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// billOperations 读取该账单维度的既有审计事实投影。
|
||||
//
|
||||
// 审计事实按资源读取而非按平台事件列表读取:账单详情对欠款人本人可见,
|
||||
// 因此不得复用仅超级管理员与平台账号可用的平台审计查询(那会让本人详情的操作日志恒为空)。
|
||||
// 只投影动作、操作账号名称快照、操作时间与白名单化的前后值摘要,不返回凭证内容与完整收款原文。
|
||||
func (q *BillQuery) billOperations(ctx context.Context, billID uint) ([]*dto.EmployeeCollectionBillOperationLogResponse, error) {
|
||||
resourceID := strconv.FormatUint(uint64(billID), 10)
|
||||
var rows []billOperationRow
|
||||
if err := q.db.WithContext(ctx).Table("tb_audit_event AS event").
|
||||
Select("event.id, event.action_code, event.action_name, event.summary, event.actor_name, "+
|
||||
"event.occurred_at, event.result, resource.relation, resource.before_data, resource.after_data").
|
||||
Joins("JOIN tb_audit_event_resource AS resource ON resource.audit_event_id = event.id").
|
||||
Where("resource.resource_type = ? AND resource.resource_id = ?",
|
||||
constants.AuditResourceEmployeeCollectionBill, resourceID).
|
||||
Order("event.occurred_at ASC, event.id ASC").
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询账单操作日志失败")
|
||||
}
|
||||
result := make([]*dto.EmployeeCollectionBillOperationLogResponse, 0, len(rows))
|
||||
seen := make(map[uint]int, len(rows))
|
||||
for index := range rows {
|
||||
row := rows[index]
|
||||
// 同一事件可能同时以主要资源与关联资源引用同一张账单:按事件去重并优先保留主要资源的前后值。
|
||||
if position, exists := seen[row.EventID]; exists && row.Relation != "primary" {
|
||||
continue
|
||||
} else if exists {
|
||||
result[position] = billOperationResponse(row)
|
||||
continue
|
||||
}
|
||||
seen[row.EventID] = len(result)
|
||||
result = append(result, billOperationResponse(row))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// billOperationRow 是账单操作日志的审计行扫描结果。
|
||||
type billOperationRow struct {
|
||||
EventID uint `gorm:"column:id"`
|
||||
ActionCode string `gorm:"column:action_code"`
|
||||
ActionName string `gorm:"column:action_name"`
|
||||
Summary string `gorm:"column:summary"`
|
||||
ActorName string `gorm:"column:actor_name"`
|
||||
OccurredAt time.Time `gorm:"column:occurred_at"`
|
||||
Result string `gorm:"column:result"`
|
||||
Relation string `gorm:"column:relation"`
|
||||
BeforeData datatypes.JSON `gorm:"column:before_data"`
|
||||
AfterData datatypes.JSON `gorm:"column:after_data"`
|
||||
}
|
||||
|
||||
// billOperationResponse 把审计行投影为对外操作日志。
|
||||
func billOperationResponse(row billOperationRow) *dto.EmployeeCollectionBillOperationLogResponse {
|
||||
return &dto.EmployeeCollectionBillOperationLogResponse{
|
||||
ActionCode: row.ActionCode, ActionName: row.ActionName, Summary: row.Summary,
|
||||
OperatorName: row.ActorName, OccurredAt: row.OccurredAt, Result: row.Result,
|
||||
BeforeSummary: billOperationSummary(row.BeforeData), AfterSummary: billOperationSummary(row.AfterData),
|
||||
}
|
||||
}
|
||||
|
||||
// billOperationSummaryKeys 是操作日志允许展示的账单字段白名单。
|
||||
// 采用白名单而非黑名单:审计前后值随用例演进会加入新字段,黑名单一旦漏项就会把
|
||||
// 收款凭证或完整收款原文带出接口。
|
||||
var billOperationSummaryKeys = []string{
|
||||
"bill_id", "source_type", "source_no", "status", "receivable_amount", "received_amount",
|
||||
"reserved_amount", "closed_reason", "closed_at", "refund_id", "outcome", "reduced_amount",
|
||||
"application_id", "attempt_no", "amount", "decided_at", "terminal_reason", "payment_method_code",
|
||||
"payment_method_name", "paid_amount",
|
||||
}
|
||||
|
||||
// billOperationSummary 按白名单提取审计前后值摘要;不可解析时返回空摘要而不阻断详情。
|
||||
func billOperationSummary(raw datatypes.JSON) map[string]any {
|
||||
summary := make(map[string]any)
|
||||
if len(raw) == 0 {
|
||||
return summary
|
||||
}
|
||||
decoded := make(map[string]any)
|
||||
if err := sonic.Unmarshal(raw, &decoded); err != nil {
|
||||
return summary
|
||||
}
|
||||
for _, key := range billOperationSummaryKeys {
|
||||
if value, exists := decoded[key]; exists {
|
||||
summary[key] = value
|
||||
}
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
// billSourceSnapshot 读取来源订单或来源充值记录的只读快照投影。
|
||||
//
|
||||
// 来源记录不可读(已删除或查询失败)或字段缺失时返回空值,且 MUST NOT 阻断详情响应:
|
||||
// 账单本身是权威事实,来源快照只是补充解释,缺来源不应让账单详情不可查看。
|
||||
func (q *BillQuery) billSourceSnapshot(ctx context.Context, bill *model.EmployeeCollectionBill) *dto.EmployeeCollectionBillSourceSnapshotResponse {
|
||||
snapshot := &dto.EmployeeCollectionBillSourceSnapshotResponse{
|
||||
SourceType: bill.SourceType,
|
||||
SourceTypeName: constants.GetEmployeeCollectionSourceTypeName(bill.SourceType),
|
||||
SourceNo: bill.SourceNo,
|
||||
}
|
||||
switch bill.SourceType {
|
||||
case constants.EmployeeCollectionSourceTypeOrder:
|
||||
var order model.Order
|
||||
if err := q.db.WithContext(ctx).
|
||||
Select("id", "order_no", "actual_paid_amount", "asset_identifier", "created_at").
|
||||
Where("id = ?", bill.SourceID).First(&order).Error; err != nil {
|
||||
return snapshot
|
||||
}
|
||||
snapshot.SourceNo = order.OrderNo
|
||||
if order.ActualPaidAmount != nil {
|
||||
snapshot.Amount = *order.ActualPaidAmount
|
||||
}
|
||||
snapshot.AssetIdentifier = order.AssetIdentifier
|
||||
createdAt := order.CreatedAt
|
||||
snapshot.CreatedAt = &createdAt
|
||||
case constants.EmployeeCollectionSourceTypeRecharge:
|
||||
var record model.AgentRechargeRecord
|
||||
if err := q.db.WithContext(ctx).
|
||||
Select("id", "recharge_no", "amount", "created_at").
|
||||
Where("id = ?", bill.SourceID).First(&record).Error; err != nil {
|
||||
return snapshot
|
||||
}
|
||||
snapshot.SourceNo = record.RechargeNo
|
||||
snapshot.Amount = record.Amount
|
||||
createdAt := record.CreatedAt
|
||||
snapshot.CreatedAt = &createdAt
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
// applyBillScope 生成同时应用可见性与筛选条件的账单查询。
|
||||
// 列表、统计与关闭后的可见性校验都必须走这一处,避免筛选或数据范围分叉。
|
||||
func (q *BillQuery) applyBillScope(ctx context.Context, filter billFilter) (*gorm.DB, error) {
|
||||
@@ -142,35 +292,44 @@ func (q *BillQuery) applyBillScope(ctx context.Context, filter billFilter) (*gor
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return applyBillFilters(query, filter)
|
||||
return applyBillFilters(ctx, q.db, query, filter)
|
||||
}
|
||||
|
||||
// billFilter 账单列表与统计共用的筛选条件,屏蔽两种请求 DTO 的字段差异。
|
||||
type billFilter struct {
|
||||
BillID *uint
|
||||
SourceType *string
|
||||
SourceNo *string
|
||||
Status *int
|
||||
DebtorAccountID *uint
|
||||
DebtorKeyword *string
|
||||
CustomerKeyword *string
|
||||
CustomerID *uint
|
||||
CreatedFrom *string
|
||||
CreatedTo *string
|
||||
StartTime *string
|
||||
EndTime *string
|
||||
DecidedStart *string
|
||||
DecidedEnd *string
|
||||
}
|
||||
|
||||
// billFilterOfList 投影账单列表请求的筛选字段。
|
||||
func billFilterOfList(request dto.EmployeeCollectionBillListRequest) billFilter {
|
||||
return billFilter{
|
||||
SourceType: request.SourceType, SourceNo: request.SourceNo, Status: request.Status,
|
||||
DebtorAccountID: request.DebtorAccountID, CustomerID: request.CustomerID,
|
||||
CreatedFrom: request.CreatedFrom, CreatedTo: request.CreatedTo,
|
||||
BillID: request.BillID, SourceType: request.SourceType, SourceNo: request.SourceNo, Status: request.Status,
|
||||
DebtorAccountID: request.DebtorAccountID, DebtorKeyword: request.DebtorKeyword,
|
||||
CustomerKeyword: request.CustomerKeyword, CustomerID: request.CustomerID,
|
||||
StartTime: request.StartTime, EndTime: request.EndTime,
|
||||
DecidedStart: request.DecidedStartTime, DecidedEnd: request.DecidedEndTime,
|
||||
}
|
||||
}
|
||||
|
||||
// billFilterOfStatistics 投影账单统计请求的筛选字段。
|
||||
func billFilterOfStatistics(request dto.EmployeeCollectionBillStatisticsRequest) billFilter {
|
||||
return billFilter{
|
||||
SourceType: request.SourceType, SourceNo: request.SourceNo, Status: request.Status,
|
||||
DebtorAccountID: request.DebtorAccountID, CustomerID: request.CustomerID,
|
||||
CreatedFrom: request.CreatedFrom, CreatedTo: request.CreatedTo,
|
||||
BillID: request.BillID, SourceType: request.SourceType, SourceNo: request.SourceNo, Status: request.Status,
|
||||
DebtorAccountID: request.DebtorAccountID, DebtorKeyword: request.DebtorKeyword,
|
||||
CustomerKeyword: request.CustomerKeyword, CustomerID: request.CustomerID,
|
||||
StartTime: request.StartTime, EndTime: request.EndTime,
|
||||
DecidedStart: request.DecidedStartTime, DecidedEnd: request.DecidedEndTime,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +346,12 @@ func applyBillVisibility(ctx context.Context, query *gorm.DB) (*gorm.DB, error)
|
||||
}
|
||||
|
||||
// applyBillFilters 应用账单列表与统计共用的筛选条件。
|
||||
func applyBillFilters(query *gorm.DB, request billFilter) (*gorm.DB, error) {
|
||||
// db 仅用于构造相关子查询,不得携带外层筛选,避免子查询被账单条件污染。
|
||||
func applyBillFilters(ctx context.Context, db, query *gorm.DB, request billFilter) (*gorm.DB, error) {
|
||||
if request.BillID != nil && *request.BillID > 0 {
|
||||
// 账单编号即账单记录标识(主键),不新增编号列,也不按前缀或范围匹配。
|
||||
query = query.Where("tb_employee_collection_bill.id = ?", *request.BillID)
|
||||
}
|
||||
if request.DebtorAccountID != nil && *request.DebtorAccountID > 0 {
|
||||
query = query.Where("debtor_account_id = ?", *request.DebtorAccountID)
|
||||
}
|
||||
@@ -219,38 +383,97 @@ func applyBillFilters(query *gorm.DB, request billFilter) (*gorm.DB, error) {
|
||||
customerID := strconv.FormatUint(uint64(*request.CustomerID), 10)
|
||||
query = query.Where("customer_snapshot->>'buyer_id' = ? OR customer_snapshot->>'shop_id' = ?", customerID, customerID)
|
||||
}
|
||||
if request.CreatedFrom != nil {
|
||||
from, err := parseBillDate(*request.CreatedFrom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if from != nil {
|
||||
query = query.Where("created_at >= ?", *from)
|
||||
}
|
||||
debtorKeyword, err := billKeyword(request.DebtorKeyword, "欠款人姓名或账号")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if request.CreatedTo != nil {
|
||||
to, err := parseBillDate(*request.CreatedTo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if debtorKeyword != "" {
|
||||
pattern := "%" + debtorKeyword + "%"
|
||||
// 欠款人姓名或账号:优先匹配账单创建时冻结的账号名称快照;
|
||||
// 同时匹配后台账号当前登录名,使账号改名后仍可按“账号”口径查到既有账单。
|
||||
query = query.Where(
|
||||
"tb_employee_collection_bill.debtor_snapshot->>'account_name' ILIKE ? OR EXISTS ("+
|
||||
"SELECT 1 FROM tb_account AS debtor_account WHERE debtor_account.deleted_at IS NULL "+
|
||||
"AND debtor_account.id = tb_employee_collection_bill.debtor_account_id "+
|
||||
"AND debtor_account.username ILIKE ?)", pattern, pattern)
|
||||
}
|
||||
customerKeyword, err := billKeyword(request.CustomerKeyword, "客户名称")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if customerKeyword != "" {
|
||||
pattern := "%" + customerKeyword + "%"
|
||||
// 客户名称:来源订单取买家昵称快照;代理线下充值的快照只冻结店铺ID,
|
||||
// 因此按店铺ID关联既有店铺表取当前店铺名称,避免为筛选新增冗余快照列。
|
||||
query = query.Where(
|
||||
"tb_employee_collection_bill.customer_snapshot->>'buyer_nickname' ILIKE ? OR EXISTS ("+
|
||||
"SELECT 1 FROM tb_shop AS customer_shop WHERE customer_shop.deleted_at IS NULL "+
|
||||
"AND customer_shop.id::text = tb_employee_collection_bill.customer_snapshot->>'shop_id' "+
|
||||
"AND customer_shop.shop_name ILIKE ?)", pattern, pattern)
|
||||
}
|
||||
// 产生时间:统一严格解析器 + 闭区间(含两端);旧参数名与旧格式不再接受。
|
||||
startTime, endTime, err := utils.ParseTimeRange(optionalTimeValue(request.StartTime), optionalTimeValue(request.EndTime))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if startTime != nil {
|
||||
query = query.Where("created_at >= ?", *startTime)
|
||||
}
|
||||
if endTime != nil {
|
||||
query = query.Where("created_at <= ?", *endTime)
|
||||
}
|
||||
// 核销通过时间:经「状态为已通过的分摊」关联到申请表的审批终态时间(decided_at)。
|
||||
// MUST NOT 使用分摊表的释放时间:该列同时承担「审批通过转为已核销」与「驳回或释放预占」两种语义。
|
||||
decidedStart, decidedEnd, err := utils.ParseTimeRange(optionalTimeValue(request.DecidedStart), optionalTimeValue(request.DecidedEnd))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if decidedStart != nil || decidedEnd != nil {
|
||||
sub := db.WithContext(ctx).
|
||||
Model(&model.EmployeeCollectionApplicationAllocation{}).
|
||||
Select("1").
|
||||
Joins("JOIN tb_employee_collection_application AS decided_application ON decided_application.id = tb_employee_collection_application_allocation.application_id").
|
||||
Where("tb_employee_collection_application_allocation.bill_id = tb_employee_collection_bill.id").
|
||||
Where("tb_employee_collection_application_allocation.status = ?", constants.EmployeeCollectionAllocationStatusApproved).
|
||||
Where("decided_application.decided_at IS NOT NULL")
|
||||
if decidedStart != nil {
|
||||
sub = sub.Where("decided_application.decided_at >= ?", *decidedStart)
|
||||
}
|
||||
if to != nil {
|
||||
query = query.Where("created_at < ?", to.AddDate(0, 0, 1))
|
||||
if decidedEnd != nil {
|
||||
sub = sub.Where("decided_application.decided_at <= ?", *decidedEnd)
|
||||
}
|
||||
query = query.Where("EXISTS (?)", sub)
|
||||
}
|
||||
return query, nil
|
||||
}
|
||||
|
||||
// parseBillDate 解析自然日筛选值;空值表示不筛选。
|
||||
func parseBillDate(value string) (*time.Time, error) {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
// billKeywordMaxLength 限制模糊筛选的输入长度,避免超长子串匹配放大查询代价。
|
||||
const billKeywordMaxLength = 50
|
||||
|
||||
// billKeyword 归一化模糊筛选输入:去两端的空白后按子串匹配,空值表示不筛选。
|
||||
// 超长输入按参数非法拒绝,与 DTO 的长度上限一致,避免查询层成为绕过入口。
|
||||
func billKeyword(value *string, fieldName string) (string, error) {
|
||||
if value == nil {
|
||||
return "", nil
|
||||
}
|
||||
trimmed := strings.TrimSpace(*value)
|
||||
if trimmed == "" {
|
||||
return nil, nil
|
||||
return "", nil
|
||||
}
|
||||
parsed, err := time.ParseInLocation(billDateLayout, trimmed, time.Local)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "账单筛选日期格式必须为 YYYY-MM-DD")
|
||||
if utf8.RuneCountInString(trimmed) > billKeywordMaxLength {
|
||||
return "", errors.New(errors.CodeInvalidParam, fieldName+"筛选最长 "+strconv.Itoa(billKeywordMaxLength)+" 字符")
|
||||
}
|
||||
return &parsed, nil
|
||||
return trimmed, nil
|
||||
}
|
||||
|
||||
// optionalTimeValue 读取可选时间筛选值:nil 与空串等价(该端不筛选)。
|
||||
// MUST NOT 去空白:统一时间筛选契约把前后带空白的取值视为格式非法,
|
||||
// 在这里 trim 会把本应被拒绝的取值静默接受,与既有拒绝集不一致。
|
||||
func optionalTimeValue(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
// billRefunds 读取账单的来源订单退款冲销关联。
|
||||
@@ -474,8 +697,16 @@ func ProjectBill(bill *model.EmployeeCollectionBill) (*dto.EmployeeCollectionBil
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "解析账单客户快照失败")
|
||||
}
|
||||
}
|
||||
// 未核销金额 = 应收 − 已核销;剩余可核销金额 = 应收 − 已核销 − 审批中预占。
|
||||
// 两者并列返回:前者是运营口径,后者是服务端并发核销防超额校验使用的口径。
|
||||
// 已关闭账单的未核销余额作废,两者都为 0。
|
||||
unsettled := int64(0)
|
||||
remaining := int64(0)
|
||||
if bill.Status != constants.EmployeeCollectionBillStatusClosed {
|
||||
unsettled = bill.ReceivableAmount - bill.ReceivedAmount
|
||||
if unsettled < 0 {
|
||||
unsettled = 0
|
||||
}
|
||||
remaining = bill.ReceivableAmount - bill.ReceivedAmount - bill.ReservedAmount
|
||||
if remaining < 0 {
|
||||
remaining = 0
|
||||
@@ -488,7 +719,7 @@ func ProjectBill(bill *model.EmployeeCollectionBill) (*dto.EmployeeCollectionBil
|
||||
DebtorAccountID: bill.DebtorAccountID, DebtorSnapshot: debtorSnapshot,
|
||||
CustomerSnapshot: customerSnapshot,
|
||||
ReceivableAmount: bill.ReceivableAmount, ReceivedAmount: bill.ReceivedAmount,
|
||||
ReservedAmount: bill.ReservedAmount, RemainingAmount: remaining,
|
||||
UnsettledAmount: unsettled, ReservedAmount: bill.ReservedAmount, RemainingAmount: remaining,
|
||||
Status: bill.Status, StatusName: constants.GetEmployeeCollectionBillStatusName(bill.Status),
|
||||
ApprovalPending: bill.ReservedAmount > 0,
|
||||
ClosedReason: bill.ClosedReason, ClosedAt: bill.ClosedAt,
|
||||
|
||||
Reference in New Issue
Block a user