Compare commits
9 Commits
dev
...
8d170ba3f9
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d170ba3f9 | |||
| fa7b006373 | |||
| c8359c5fc3 | |||
| 59ebe70ed3 | |||
| 76366cbc30 | |||
| b0bb87d97c | |||
| 1efa392f36 | |||
| b57b3ab228 | |||
| a88ca4056c |
@@ -0,0 +1,533 @@
|
||||
namespace TakeoutSaaS.TenantApi.Contracts.Finance;
|
||||
|
||||
/// <summary>
|
||||
/// 保存发票设置请求。
|
||||
/// </summary>
|
||||
public sealed class FinanceInvoiceSettingSaveRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 企业名称。
|
||||
/// </summary>
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 纳税人识别号。
|
||||
/// </summary>
|
||||
public string TaxpayerNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 注册地址。
|
||||
/// </summary>
|
||||
public string? RegisteredAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 注册电话。
|
||||
/// </summary>
|
||||
public string? RegisteredPhone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开户银行。
|
||||
/// </summary>
|
||||
public string? BankName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 银行账号。
|
||||
/// </summary>
|
||||
public string? BankAccount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用电子普通发票。
|
||||
/// </summary>
|
||||
public bool EnableElectronicNormalInvoice { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用电子专用发票。
|
||||
/// </summary>
|
||||
public bool EnableElectronicSpecialInvoice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用自动开票。
|
||||
/// </summary>
|
||||
public bool EnableAutoIssue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 自动开票单张最大金额。
|
||||
/// </summary>
|
||||
public decimal AutoIssueMaxAmount { get; set; } = 10_000m;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发票记录列表请求。
|
||||
/// </summary>
|
||||
public sealed class FinanceInvoiceRecordListRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 开始日期(yyyy-MM-dd)。
|
||||
/// </summary>
|
||||
public string? StartDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束日期(yyyy-MM-dd)。
|
||||
/// </summary>
|
||||
public string? EndDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态(pending/issued/voided)。
|
||||
/// </summary>
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类型(normal/special)。
|
||||
/// </summary>
|
||||
public string? InvoiceType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键词(发票号/公司名/申请人)。
|
||||
/// </summary>
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; set; } = 10;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发票记录详情请求。
|
||||
/// </summary>
|
||||
public sealed class FinanceInvoiceRecordDetailRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 发票记录 ID。
|
||||
/// </summary>
|
||||
public string RecordId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发票开票请求。
|
||||
/// </summary>
|
||||
public sealed class FinanceInvoiceRecordIssueRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 发票记录 ID。
|
||||
/// </summary>
|
||||
public string RecordId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 接收邮箱(可选)。
|
||||
/// </summary>
|
||||
public string? ContactEmail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开票备注。
|
||||
/// </summary>
|
||||
public string? IssueRemark { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发票作废请求。
|
||||
/// </summary>
|
||||
public sealed class FinanceInvoiceRecordVoidRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 发票记录 ID。
|
||||
/// </summary>
|
||||
public string RecordId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 作废原因。
|
||||
/// </summary>
|
||||
public string VoidReason { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发票申请请求。
|
||||
/// </summary>
|
||||
public sealed class FinanceInvoiceRecordApplyRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 申请人。
|
||||
/// </summary>
|
||||
public string ApplicantName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 开票抬头(公司名)。
|
||||
/// </summary>
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 纳税人识别号。
|
||||
/// </summary>
|
||||
public string? TaxpayerNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发票类型(normal/special)。
|
||||
/// </summary>
|
||||
public string InvoiceType { get; set; } = "normal";
|
||||
|
||||
/// <summary>
|
||||
/// 开票金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联订单号。
|
||||
/// </summary>
|
||||
public string OrderNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 接收邮箱。
|
||||
/// </summary>
|
||||
public string? ContactEmail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 联系电话。
|
||||
/// </summary>
|
||||
public string? ContactPhone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 申请备注。
|
||||
/// </summary>
|
||||
public string? ApplyRemark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 申请时间(可空)。
|
||||
/// </summary>
|
||||
public DateTime? AppliedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发票设置响应。
|
||||
/// </summary>
|
||||
public sealed class FinanceInvoiceSettingResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 企业名称。
|
||||
/// </summary>
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 纳税人识别号。
|
||||
/// </summary>
|
||||
public string TaxpayerNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 注册地址。
|
||||
/// </summary>
|
||||
public string? RegisteredAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 注册电话。
|
||||
/// </summary>
|
||||
public string? RegisteredPhone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开户银行。
|
||||
/// </summary>
|
||||
public string? BankName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 银行账号。
|
||||
/// </summary>
|
||||
public string? BankAccount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用电子普通发票。
|
||||
/// </summary>
|
||||
public bool EnableElectronicNormalInvoice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用电子专用发票。
|
||||
/// </summary>
|
||||
public bool EnableElectronicSpecialInvoice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用自动开票。
|
||||
/// </summary>
|
||||
public bool EnableAutoIssue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 自动开票单张最大金额。
|
||||
/// </summary>
|
||||
public decimal AutoIssueMaxAmount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发票统计响应。
|
||||
/// </summary>
|
||||
public sealed class FinanceInvoiceStatsResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 本月已开票金额。
|
||||
/// </summary>
|
||||
public decimal CurrentMonthIssuedAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本月已开票张数。
|
||||
/// </summary>
|
||||
public int CurrentMonthIssuedCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 待开票数量。
|
||||
/// </summary>
|
||||
public int PendingCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已作废数量。
|
||||
/// </summary>
|
||||
public int VoidedCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发票记录列表项响应。
|
||||
/// </summary>
|
||||
public sealed class FinanceInvoiceRecordResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 记录 ID。
|
||||
/// </summary>
|
||||
public string RecordId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 发票号码。
|
||||
/// </summary>
|
||||
public string InvoiceNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 申请人。
|
||||
/// </summary>
|
||||
public string ApplicantName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 开票抬头(公司名)。
|
||||
/// </summary>
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 发票类型编码。
|
||||
/// </summary>
|
||||
public string InvoiceType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 发票类型文案。
|
||||
/// </summary>
|
||||
public string InvoiceTypeText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联订单号。
|
||||
/// </summary>
|
||||
public string OrderNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态编码。
|
||||
/// </summary>
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态文案。
|
||||
/// </summary>
|
||||
public string StatusText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 申请时间(本地显示字符串)。
|
||||
/// </summary>
|
||||
public string AppliedAt { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发票记录详情响应。
|
||||
/// </summary>
|
||||
public sealed class FinanceInvoiceRecordDetailResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 记录 ID。
|
||||
/// </summary>
|
||||
public string RecordId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 发票号码。
|
||||
/// </summary>
|
||||
public string InvoiceNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 申请人。
|
||||
/// </summary>
|
||||
public string ApplicantName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 开票抬头(公司名)。
|
||||
/// </summary>
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 纳税人识别号。
|
||||
/// </summary>
|
||||
public string? TaxpayerNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发票类型编码。
|
||||
/// </summary>
|
||||
public string InvoiceType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 发票类型文案。
|
||||
/// </summary>
|
||||
public string InvoiceTypeText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联订单号。
|
||||
/// </summary>
|
||||
public string OrderNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 接收邮箱。
|
||||
/// </summary>
|
||||
public string? ContactEmail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 联系电话。
|
||||
/// </summary>
|
||||
public string? ContactPhone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 申请备注。
|
||||
/// </summary>
|
||||
public string? ApplyRemark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态编码。
|
||||
/// </summary>
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态文案。
|
||||
/// </summary>
|
||||
public string StatusText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 申请时间(本地显示字符串)。
|
||||
/// </summary>
|
||||
public string AppliedAt { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 开票时间(本地显示字符串)。
|
||||
/// </summary>
|
||||
public string? IssuedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开票人 ID。
|
||||
/// </summary>
|
||||
public string? IssuedByUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开票备注。
|
||||
/// </summary>
|
||||
public string? IssueRemark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 作废时间(本地显示字符串)。
|
||||
/// </summary>
|
||||
public string? VoidedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 作废人 ID。
|
||||
/// </summary>
|
||||
public string? VoidedByUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 作废原因。
|
||||
/// </summary>
|
||||
public string? VoidReason { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发票开票结果响应。
|
||||
/// </summary>
|
||||
public sealed class FinanceInvoiceIssueResultResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 记录 ID。
|
||||
/// </summary>
|
||||
public string RecordId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 发票号码。
|
||||
/// </summary>
|
||||
public string InvoiceNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 开票抬头。
|
||||
/// </summary>
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 接收邮箱。
|
||||
/// </summary>
|
||||
public string? ContactEmail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开票时间(本地显示字符串)。
|
||||
/// </summary>
|
||||
public string IssuedAt { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态编码。
|
||||
/// </summary>
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态文案。
|
||||
/// </summary>
|
||||
public string StatusText { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发票记录分页响应。
|
||||
/// </summary>
|
||||
public sealed class FinanceInvoiceRecordListResultResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 列表项。
|
||||
/// </summary>
|
||||
public List<FinanceInvoiceRecordResponse> Items { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
public int Page { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总条数。
|
||||
/// </summary>
|
||||
public int TotalCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 统计。
|
||||
/// </summary>
|
||||
public FinanceInvoiceStatsResponse Stats { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
namespace TakeoutSaaS.TenantApi.Contracts.Finance;
|
||||
|
||||
/// <summary>
|
||||
/// 到账统计请求。
|
||||
/// </summary>
|
||||
public sealed class FinanceSettlementStatsRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public string StoreId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账筛选请求。
|
||||
/// </summary>
|
||||
public class FinanceSettlementFilterRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public string StoreId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 开始日期(yyyy-MM-dd)。
|
||||
/// </summary>
|
||||
public string? StartDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束日期(yyyy-MM-dd)。
|
||||
/// </summary>
|
||||
public string? EndDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 渠道(wechat/alipay)。
|
||||
/// </summary>
|
||||
public string? Channel { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账列表请求。
|
||||
/// </summary>
|
||||
public sealed class FinanceSettlementListRequest : FinanceSettlementFilterRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; set; } = 20;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账明细请求。
|
||||
/// </summary>
|
||||
public sealed class FinanceSettlementDetailRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public string StoreId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 到账日期(yyyy-MM-dd)。
|
||||
/// </summary>
|
||||
public string ArrivedDate { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 渠道(wechat/alipay)。
|
||||
/// </summary>
|
||||
public string Channel { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账统计响应。
|
||||
/// </summary>
|
||||
public sealed class FinanceSettlementStatsResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 今日到账。
|
||||
/// </summary>
|
||||
public decimal TodayArrivedAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 昨日到账。
|
||||
/// </summary>
|
||||
public decimal YesterdayArrivedAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本月到账。
|
||||
/// </summary>
|
||||
public decimal CurrentMonthArrivedAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本月交易笔数。
|
||||
/// </summary>
|
||||
public int CurrentMonthTransactionCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账账户信息响应。
|
||||
/// </summary>
|
||||
public sealed class FinanceSettlementAccountResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 银行名称。
|
||||
/// </summary>
|
||||
public string BankName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 开户名。
|
||||
/// </summary>
|
||||
public string BankAccountName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 脱敏银行账号。
|
||||
/// </summary>
|
||||
public string BankAccountNoMasked { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 脱敏微信商户号。
|
||||
/// </summary>
|
||||
public string WechatMerchantNoMasked { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 脱敏支付宝 PID。
|
||||
/// </summary>
|
||||
public string AlipayPidMasked { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 结算周期文案。
|
||||
/// </summary>
|
||||
public string SettlementPeriodText { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账列表行响应。
|
||||
/// </summary>
|
||||
public sealed class FinanceSettlementListItemResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 到账日期。
|
||||
/// </summary>
|
||||
public string ArrivedDate { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 渠道编码。
|
||||
/// </summary>
|
||||
public string Channel { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 渠道文案。
|
||||
/// </summary>
|
||||
public string ChannelText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 交易笔数。
|
||||
/// </summary>
|
||||
public int TransactionCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 到账金额。
|
||||
/// </summary>
|
||||
public decimal ArrivedAmount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账列表响应。
|
||||
/// </summary>
|
||||
public sealed class FinanceSettlementListResultResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 列表项。
|
||||
/// </summary>
|
||||
public List<FinanceSettlementListItemResponse> Items { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 总数。
|
||||
/// </summary>
|
||||
public int Total { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
public int Page { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账明细行响应。
|
||||
/// </summary>
|
||||
public sealed class FinanceSettlementDetailItemResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单号。
|
||||
/// </summary>
|
||||
public string OrderNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付时间。
|
||||
/// </summary>
|
||||
public string PaidAt { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账明细响应。
|
||||
/// </summary>
|
||||
public sealed class FinanceSettlementDetailResultResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 明细列表。
|
||||
/// </summary>
|
||||
public List<FinanceSettlementDetailItemResponse> Items { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账导出响应。
|
||||
/// </summary>
|
||||
public sealed class FinanceSettlementExportResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件名。
|
||||
/// </summary>
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 文件内容(Base64)。
|
||||
/// </summary>
|
||||
public string FileContentBase64 { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 导出总数。
|
||||
/// </summary>
|
||||
public int TotalCount { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
using System.Globalization;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Commands;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Queries;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
using TakeoutSaaS.TenantApi.Contracts.Finance;
|
||||
|
||||
namespace TakeoutSaaS.TenantApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 财务中心发票管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/tenant/v{version:apiVersion}/finance/invoice")]
|
||||
public sealed class FinanceInvoiceController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
private const string ViewPermission = "tenant:finance:invoice:view";
|
||||
private const string IssuePermission = "tenant:finance:invoice:issue";
|
||||
private const string VoidPermission = "tenant:finance:invoice:void";
|
||||
private const string SettingsPermission = "tenant:finance:invoice:settings";
|
||||
|
||||
/// <summary>
|
||||
/// 查询发票设置详情。
|
||||
/// </summary>
|
||||
[HttpGet("settings/detail")]
|
||||
[PermissionAuthorize(ViewPermission, SettingsPermission)]
|
||||
[ProducesResponseType(typeof(ApiResponse<FinanceInvoiceSettingResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<FinanceInvoiceSettingResponse>> SettingsDetail(CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new GetFinanceInvoiceSettingDetailQuery(), cancellationToken);
|
||||
return ApiResponse<FinanceInvoiceSettingResponse>.Ok(MapSetting(result));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存发票设置。
|
||||
/// </summary>
|
||||
[HttpPost("settings/save")]
|
||||
[PermissionAuthorize(SettingsPermission)]
|
||||
[ProducesResponseType(typeof(ApiResponse<FinanceInvoiceSettingResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<FinanceInvoiceSettingResponse>> SettingsSave(
|
||||
[FromBody] FinanceInvoiceSettingSaveRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new SaveFinanceInvoiceSettingCommand
|
||||
{
|
||||
CompanyName = request.CompanyName,
|
||||
TaxpayerNumber = request.TaxpayerNumber,
|
||||
RegisteredAddress = request.RegisteredAddress,
|
||||
RegisteredPhone = request.RegisteredPhone,
|
||||
BankName = request.BankName,
|
||||
BankAccount = request.BankAccount,
|
||||
EnableElectronicNormalInvoice = request.EnableElectronicNormalInvoice,
|
||||
EnableElectronicSpecialInvoice = request.EnableElectronicSpecialInvoice,
|
||||
EnableAutoIssue = request.EnableAutoIssue,
|
||||
AutoIssueMaxAmount = request.AutoIssueMaxAmount
|
||||
}, cancellationToken);
|
||||
|
||||
return ApiResponse<FinanceInvoiceSettingResponse>.Ok(MapSetting(result));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询发票记录分页。
|
||||
/// </summary>
|
||||
[HttpGet("record/list")]
|
||||
[PermissionAuthorize(ViewPermission, IssuePermission, VoidPermission)]
|
||||
[ProducesResponseType(typeof(ApiResponse<FinanceInvoiceRecordListResultResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<FinanceInvoiceRecordListResultResponse>> RecordList(
|
||||
[FromQuery] FinanceInvoiceRecordListRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new GetFinanceInvoiceRecordListQuery
|
||||
{
|
||||
StartDateUtc = ParseDateOrNull(request.StartDate, nameof(request.StartDate)),
|
||||
EndDateUtc = ParseDateOrNull(request.EndDate, nameof(request.EndDate)),
|
||||
Status = ParseStatusOrNull(request.Status),
|
||||
InvoiceType = ParseInvoiceTypeOrNull(request.InvoiceType),
|
||||
Keyword = request.Keyword,
|
||||
Page = request.Page,
|
||||
PageSize = request.PageSize
|
||||
}, cancellationToken);
|
||||
|
||||
return ApiResponse<FinanceInvoiceRecordListResultResponse>.Ok(new FinanceInvoiceRecordListResultResponse
|
||||
{
|
||||
Items = result.Items.Select(MapRecord).ToList(),
|
||||
Page = result.Page,
|
||||
PageSize = result.PageSize,
|
||||
TotalCount = result.TotalCount,
|
||||
Stats = new FinanceInvoiceStatsResponse
|
||||
{
|
||||
CurrentMonthIssuedAmount = result.Stats.CurrentMonthIssuedAmount,
|
||||
CurrentMonthIssuedCount = result.Stats.CurrentMonthIssuedCount,
|
||||
PendingCount = result.Stats.PendingCount,
|
||||
VoidedCount = result.Stats.VoidedCount
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询发票记录详情。
|
||||
/// </summary>
|
||||
[HttpGet("record/detail")]
|
||||
[PermissionAuthorize(ViewPermission, IssuePermission, VoidPermission)]
|
||||
[ProducesResponseType(typeof(ApiResponse<FinanceInvoiceRecordDetailResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<FinanceInvoiceRecordDetailResponse>> RecordDetail(
|
||||
[FromQuery] FinanceInvoiceRecordDetailRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new GetFinanceInvoiceRecordDetailQuery
|
||||
{
|
||||
RecordId = StoreApiHelpers.ParseRequiredSnowflake(request.RecordId, nameof(request.RecordId))
|
||||
}, cancellationToken);
|
||||
|
||||
return ApiResponse<FinanceInvoiceRecordDetailResponse>.Ok(MapRecordDetail(result));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发票开票。
|
||||
/// </summary>
|
||||
[HttpPost("record/issue")]
|
||||
[PermissionAuthorize(IssuePermission)]
|
||||
[ProducesResponseType(typeof(ApiResponse<FinanceInvoiceIssueResultResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<FinanceInvoiceIssueResultResponse>> RecordIssue(
|
||||
[FromBody] FinanceInvoiceRecordIssueRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new IssueFinanceInvoiceRecordCommand
|
||||
{
|
||||
RecordId = StoreApiHelpers.ParseRequiredSnowflake(request.RecordId, nameof(request.RecordId)),
|
||||
ContactEmail = request.ContactEmail,
|
||||
IssueRemark = request.IssueRemark
|
||||
}, cancellationToken);
|
||||
|
||||
return ApiResponse<FinanceInvoiceIssueResultResponse>.Ok(MapIssueResult(result));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 作废发票。
|
||||
/// </summary>
|
||||
[HttpPost("record/void")]
|
||||
[PermissionAuthorize(VoidPermission)]
|
||||
[ProducesResponseType(typeof(ApiResponse<FinanceInvoiceRecordDetailResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<FinanceInvoiceRecordDetailResponse>> RecordVoid(
|
||||
[FromBody] FinanceInvoiceRecordVoidRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new VoidFinanceInvoiceRecordCommand
|
||||
{
|
||||
RecordId = StoreApiHelpers.ParseRequiredSnowflake(request.RecordId, nameof(request.RecordId)),
|
||||
VoidReason = request.VoidReason
|
||||
}, cancellationToken);
|
||||
|
||||
return ApiResponse<FinanceInvoiceRecordDetailResponse>.Ok(MapRecordDetail(result));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 申请发票。
|
||||
/// </summary>
|
||||
[HttpPost("record/apply")]
|
||||
[PermissionAuthorize(ViewPermission, IssuePermission)]
|
||||
[ProducesResponseType(typeof(ApiResponse<FinanceInvoiceRecordDetailResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<FinanceInvoiceRecordDetailResponse>> RecordApply(
|
||||
[FromBody] FinanceInvoiceRecordApplyRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new ApplyFinanceInvoiceRecordCommand
|
||||
{
|
||||
ApplicantName = request.ApplicantName,
|
||||
CompanyName = request.CompanyName,
|
||||
TaxpayerNumber = request.TaxpayerNumber,
|
||||
InvoiceType = request.InvoiceType,
|
||||
Amount = request.Amount,
|
||||
OrderNo = request.OrderNo,
|
||||
ContactEmail = request.ContactEmail,
|
||||
ContactPhone = request.ContactPhone,
|
||||
ApplyRemark = request.ApplyRemark,
|
||||
AppliedAt = request.AppliedAt
|
||||
}, cancellationToken);
|
||||
|
||||
return ApiResponse<FinanceInvoiceRecordDetailResponse>.Ok(MapRecordDetail(result));
|
||||
}
|
||||
|
||||
private static DateTime? ParseDateOrNull(string? value, string fieldName)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value)
|
||||
? null
|
||||
: StoreApiHelpers.ParseDateOnly(value, fieldName);
|
||||
}
|
||||
|
||||
private static TenantInvoiceStatus? ParseStatusOrNull(string? value)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalized switch
|
||||
{
|
||||
"pending" => TenantInvoiceStatus.Pending,
|
||||
"issued" => TenantInvoiceStatus.Issued,
|
||||
"voided" => TenantInvoiceStatus.Voided,
|
||||
_ => throw new BusinessException(ErrorCodes.BadRequest, "status 参数不合法")
|
||||
};
|
||||
}
|
||||
|
||||
private static TenantInvoiceType? ParseInvoiceTypeOrNull(string? value)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalized switch
|
||||
{
|
||||
"normal" => TenantInvoiceType.Normal,
|
||||
"special" => TenantInvoiceType.Special,
|
||||
_ => throw new BusinessException(ErrorCodes.BadRequest, "invoiceType 参数不合法")
|
||||
};
|
||||
}
|
||||
|
||||
private static FinanceInvoiceSettingResponse MapSetting(FinanceInvoiceSettingDto source)
|
||||
{
|
||||
return new FinanceInvoiceSettingResponse
|
||||
{
|
||||
CompanyName = source.CompanyName,
|
||||
TaxpayerNumber = source.TaxpayerNumber,
|
||||
RegisteredAddress = source.RegisteredAddress,
|
||||
RegisteredPhone = source.RegisteredPhone,
|
||||
BankName = source.BankName,
|
||||
BankAccount = source.BankAccount,
|
||||
EnableElectronicNormalInvoice = source.EnableElectronicNormalInvoice,
|
||||
EnableElectronicSpecialInvoice = source.EnableElectronicSpecialInvoice,
|
||||
EnableAutoIssue = source.EnableAutoIssue,
|
||||
AutoIssueMaxAmount = source.AutoIssueMaxAmount
|
||||
};
|
||||
}
|
||||
|
||||
private static FinanceInvoiceRecordResponse MapRecord(FinanceInvoiceRecordDto source)
|
||||
{
|
||||
return new FinanceInvoiceRecordResponse
|
||||
{
|
||||
RecordId = source.RecordId.ToString(),
|
||||
InvoiceNo = source.InvoiceNo,
|
||||
ApplicantName = source.ApplicantName,
|
||||
CompanyName = source.CompanyName,
|
||||
InvoiceType = source.InvoiceType,
|
||||
InvoiceTypeText = source.InvoiceTypeText,
|
||||
Amount = source.Amount,
|
||||
OrderNo = source.OrderNo,
|
||||
Status = source.Status,
|
||||
StatusText = source.StatusText,
|
||||
AppliedAt = source.AppliedAt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)
|
||||
};
|
||||
}
|
||||
|
||||
private static FinanceInvoiceRecordDetailResponse MapRecordDetail(FinanceInvoiceRecordDetailDto source)
|
||||
{
|
||||
return new FinanceInvoiceRecordDetailResponse
|
||||
{
|
||||
RecordId = source.RecordId.ToString(),
|
||||
InvoiceNo = source.InvoiceNo,
|
||||
ApplicantName = source.ApplicantName,
|
||||
CompanyName = source.CompanyName,
|
||||
TaxpayerNumber = source.TaxpayerNumber,
|
||||
InvoiceType = source.InvoiceType,
|
||||
InvoiceTypeText = source.InvoiceTypeText,
|
||||
Amount = source.Amount,
|
||||
OrderNo = source.OrderNo,
|
||||
ContactEmail = source.ContactEmail,
|
||||
ContactPhone = source.ContactPhone,
|
||||
ApplyRemark = source.ApplyRemark,
|
||||
Status = source.Status,
|
||||
StatusText = source.StatusText,
|
||||
AppliedAt = source.AppliedAt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
|
||||
IssuedAt = source.IssuedAt?.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
|
||||
IssuedByUserId = source.IssuedByUserId?.ToString(),
|
||||
IssueRemark = source.IssueRemark,
|
||||
VoidedAt = source.VoidedAt?.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
|
||||
VoidedByUserId = source.VoidedByUserId?.ToString(),
|
||||
VoidReason = source.VoidReason
|
||||
};
|
||||
}
|
||||
|
||||
private static FinanceInvoiceIssueResultResponse MapIssueResult(FinanceInvoiceIssueResultDto source)
|
||||
{
|
||||
return new FinanceInvoiceIssueResultResponse
|
||||
{
|
||||
RecordId = source.RecordId.ToString(),
|
||||
InvoiceNo = source.InvoiceNo,
|
||||
CompanyName = source.CompanyName,
|
||||
Amount = source.Amount,
|
||||
ContactEmail = source.ContactEmail,
|
||||
IssuedAt = source.IssuedAt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
|
||||
Status = source.Status,
|
||||
StatusText = source.StatusText
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
using System.Globalization;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
|
||||
using TakeoutSaaS.Application.App.Finance.Settlement.Queries;
|
||||
using TakeoutSaaS.Application.App.Stores.Services;
|
||||
using TakeoutSaaS.Domain.Payments.Enums;
|
||||
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
using TakeoutSaaS.TenantApi.Contracts.Finance;
|
||||
|
||||
namespace TakeoutSaaS.TenantApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 财务中心到账查询。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/tenant/v{version:apiVersion}/finance/settlement")]
|
||||
public sealed class FinanceSettlementController(
|
||||
IMediator mediator,
|
||||
TakeoutAppDbContext dbContext,
|
||||
StoreContextService storeContextService) : BaseApiController
|
||||
{
|
||||
private const string ViewPermission = "tenant:finance:settlement:view";
|
||||
private const string ExportPermission = "tenant:finance:settlement:export";
|
||||
|
||||
/// <summary>
|
||||
/// 查询到账统计。
|
||||
/// </summary>
|
||||
[HttpGet("stats")]
|
||||
[PermissionAuthorize(ViewPermission)]
|
||||
[ProducesResponseType(typeof(ApiResponse<FinanceSettlementStatsResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<FinanceSettlementStatsResponse>> Stats(
|
||||
[FromQuery] FinanceSettlementStatsRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||
|
||||
var stats = await mediator.Send(new GetFinanceSettlementStatsQuery
|
||||
{
|
||||
StoreId = storeId
|
||||
}, cancellationToken);
|
||||
|
||||
return ApiResponse<FinanceSettlementStatsResponse>.Ok(new FinanceSettlementStatsResponse
|
||||
{
|
||||
TodayArrivedAmount = stats.TodayArrivedAmount,
|
||||
YesterdayArrivedAmount = stats.YesterdayArrivedAmount,
|
||||
CurrentMonthArrivedAmount = stats.CurrentMonthArrivedAmount,
|
||||
CurrentMonthTransactionCount = stats.CurrentMonthTransactionCount
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询到账账户信息。
|
||||
/// </summary>
|
||||
[HttpGet("account")]
|
||||
[PermissionAuthorize(ViewPermission)]
|
||||
[ProducesResponseType(typeof(ApiResponse<FinanceSettlementAccountResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<FinanceSettlementAccountResponse>> Account(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var account = await mediator.Send(new GetFinanceSettlementAccountQuery(), cancellationToken);
|
||||
if (account is null)
|
||||
{
|
||||
return ApiResponse<FinanceSettlementAccountResponse>.Error(ErrorCodes.NotFound, "结算账户信息不存在");
|
||||
}
|
||||
|
||||
return ApiResponse<FinanceSettlementAccountResponse>.Ok(new FinanceSettlementAccountResponse
|
||||
{
|
||||
BankName = account.BankName,
|
||||
BankAccountName = account.BankAccountName,
|
||||
BankAccountNoMasked = account.BankAccountNoMasked,
|
||||
WechatMerchantNoMasked = account.WechatMerchantNoMasked,
|
||||
AlipayPidMasked = account.AlipayPidMasked,
|
||||
SettlementPeriodText = account.SettlementPeriodText
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询到账汇总列表。
|
||||
/// </summary>
|
||||
[HttpGet("list")]
|
||||
[PermissionAuthorize(ViewPermission)]
|
||||
[ProducesResponseType(typeof(ApiResponse<FinanceSettlementListResultResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<FinanceSettlementListResultResponse>> List(
|
||||
[FromQuery] FinanceSettlementListRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var parsed = await ParseFilterAsync(request, cancellationToken);
|
||||
|
||||
var result = await mediator.Send(new SearchFinanceSettlementListQuery
|
||||
{
|
||||
StoreId = parsed.StoreId,
|
||||
StartAt = parsed.StartAt,
|
||||
EndAt = parsed.EndAt,
|
||||
PaymentMethod = parsed.PaymentMethod,
|
||||
Page = Math.Max(1, request.Page),
|
||||
PageSize = Math.Clamp(request.PageSize, 1, 200)
|
||||
}, cancellationToken);
|
||||
|
||||
return ApiResponse<FinanceSettlementListResultResponse>.Ok(new FinanceSettlementListResultResponse
|
||||
{
|
||||
Items = result.Items.Select(MapListItem).ToList(),
|
||||
Total = result.Total,
|
||||
Page = result.Page,
|
||||
PageSize = result.PageSize
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询到账明细(展开行)。
|
||||
/// </summary>
|
||||
[HttpGet("detail")]
|
||||
[PermissionAuthorize(ViewPermission)]
|
||||
[ProducesResponseType(typeof(ApiResponse<FinanceSettlementDetailResultResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<FinanceSettlementDetailResultResponse>> Detail(
|
||||
[FromQuery] FinanceSettlementDetailRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||
|
||||
var arrivedDate = ParseRequiredDate(request.ArrivedDate, nameof(request.ArrivedDate));
|
||||
var paymentMethod = ParseRequiredSettlementChannel(request.Channel);
|
||||
|
||||
var result = await mediator.Send(new GetFinanceSettlementDetailQuery
|
||||
{
|
||||
StoreId = storeId,
|
||||
ArrivedDate = arrivedDate,
|
||||
PaymentMethod = paymentMethod,
|
||||
Take = 50
|
||||
}, cancellationToken);
|
||||
|
||||
return ApiResponse<FinanceSettlementDetailResultResponse>.Ok(new FinanceSettlementDetailResultResponse
|
||||
{
|
||||
Items = result.Items.Select(MapDetailItem).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出到账汇总 CSV。
|
||||
/// </summary>
|
||||
[HttpGet("export")]
|
||||
[PermissionAuthorize(ExportPermission)]
|
||||
[ProducesResponseType(typeof(ApiResponse<FinanceSettlementExportResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<FinanceSettlementExportResponse>> Export(
|
||||
[FromQuery] FinanceSettlementFilterRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var parsed = await ParseFilterAsync(request, cancellationToken);
|
||||
|
||||
var result = await mediator.Send(new ExportFinanceSettlementCsvQuery
|
||||
{
|
||||
StoreId = parsed.StoreId,
|
||||
StartAt = parsed.StartAt,
|
||||
EndAt = parsed.EndAt,
|
||||
PaymentMethod = parsed.PaymentMethod
|
||||
}, cancellationToken);
|
||||
|
||||
return ApiResponse<FinanceSettlementExportResponse>.Ok(new FinanceSettlementExportResponse
|
||||
{
|
||||
FileName = result.FileName,
|
||||
FileContentBase64 = result.FileContentBase64,
|
||||
TotalCount = result.TotalCount
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<(long StoreId, DateTime? StartAt, DateTime? EndAt, PaymentMethod? PaymentMethod)> ParseFilterAsync(
|
||||
FinanceSettlementFilterRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||
|
||||
var startAt = ParseDateOrNull(request.StartDate);
|
||||
var endAt = ParseDateOrNull(request.EndDate)?.AddDays(1);
|
||||
if (startAt.HasValue && endAt.HasValue && startAt >= endAt)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "开始日期不能晚于结束日期");
|
||||
}
|
||||
|
||||
return (storeId, startAt, endAt, ParseOptionalSettlementChannel(request.Channel));
|
||||
}
|
||||
|
||||
private async Task EnsureStoreAccessibleAsync(long storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, storeId, cancellationToken);
|
||||
}
|
||||
|
||||
private static DateTime ParseRequiredDate(string? value, string parameterName)
|
||||
{
|
||||
return ParseDateOrNull(value)
|
||||
?? throw new BusinessException(ErrorCodes.BadRequest, $"{parameterName} 必填,格式为 yyyy-MM-dd");
|
||||
}
|
||||
|
||||
private static DateTime? ParseDateOrNull(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (DateTime.TryParseExact(
|
||||
value,
|
||||
"yyyy-MM-dd",
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None,
|
||||
out var parsed))
|
||||
{
|
||||
return DateTime.SpecifyKind(parsed.Date, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "日期格式必须为 yyyy-MM-dd");
|
||||
}
|
||||
|
||||
private static PaymentMethod ParseRequiredSettlementChannel(string? channel)
|
||||
{
|
||||
return ParseOptionalSettlementChannel(channel)
|
||||
?? throw new BusinessException(ErrorCodes.BadRequest, "channel 必填,仅支持 wechat 或 alipay");
|
||||
}
|
||||
|
||||
private static PaymentMethod? ParseOptionalSettlementChannel(string? channel)
|
||||
{
|
||||
return (channel ?? string.Empty).Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"wechat" => PaymentMethod.WeChatPay,
|
||||
"alipay" => PaymentMethod.Alipay,
|
||||
"" => null,
|
||||
_ => throw new BusinessException(ErrorCodes.BadRequest, "channel 仅支持 wechat 或 alipay")
|
||||
};
|
||||
}
|
||||
|
||||
private static FinanceSettlementListItemResponse MapListItem(FinanceSettlementListItemDto source)
|
||||
{
|
||||
return new FinanceSettlementListItemResponse
|
||||
{
|
||||
ArrivedDate = source.ArrivedDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||
Channel = source.Channel,
|
||||
ChannelText = source.ChannelText,
|
||||
TransactionCount = source.TransactionCount,
|
||||
ArrivedAmount = source.ArrivedAmount
|
||||
};
|
||||
}
|
||||
|
||||
private static FinanceSettlementDetailItemResponse MapDetailItem(FinanceSettlementDetailItemDto source)
|
||||
{
|
||||
return new FinanceSettlementDetailItemResponse
|
||||
{
|
||||
OrderNo = source.OrderNo,
|
||||
Amount = source.Amount,
|
||||
PaidAt = source.PaidAt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 申请发票记录命令。
|
||||
/// </summary>
|
||||
public sealed class ApplyFinanceInvoiceRecordCommand : IRequest<FinanceInvoiceRecordDetailDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 申请人。
|
||||
/// </summary>
|
||||
public string ApplicantName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 开票抬头(公司名)。
|
||||
/// </summary>
|
||||
public string CompanyName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 纳税人识别号。
|
||||
/// </summary>
|
||||
public string? TaxpayerNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 发票类型(normal/special)。
|
||||
/// </summary>
|
||||
public string InvoiceType { get; init; } = "normal";
|
||||
|
||||
/// <summary>
|
||||
/// 开票金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联订单号。
|
||||
/// </summary>
|
||||
public string OrderNo { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 接收邮箱。
|
||||
/// </summary>
|
||||
public string? ContactEmail { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 联系电话。
|
||||
/// </summary>
|
||||
public string? ContactPhone { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 申请备注。
|
||||
/// </summary>
|
||||
public string? ApplyRemark { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 申请时间(可空,默认当前 UTC)。
|
||||
/// </summary>
|
||||
public DateTime? AppliedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 开票命令。
|
||||
/// </summary>
|
||||
public sealed class IssueFinanceInvoiceRecordCommand : IRequest<FinanceInvoiceIssueResultDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 发票记录 ID。
|
||||
/// </summary>
|
||||
public long RecordId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 接收邮箱(可选,传入会覆盖原值)。
|
||||
/// </summary>
|
||||
public string? ContactEmail { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 开票备注。
|
||||
/// </summary>
|
||||
public string? IssueRemark { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 保存发票设置命令。
|
||||
/// </summary>
|
||||
public sealed class SaveFinanceInvoiceSettingCommand : IRequest<FinanceInvoiceSettingDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 企业名称。
|
||||
/// </summary>
|
||||
public string CompanyName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 纳税人识别号。
|
||||
/// </summary>
|
||||
public string TaxpayerNumber { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 注册地址。
|
||||
/// </summary>
|
||||
public string? RegisteredAddress { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 注册电话。
|
||||
/// </summary>
|
||||
public string? RegisteredPhone { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 开户银行。
|
||||
/// </summary>
|
||||
public string? BankName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 银行账号。
|
||||
/// </summary>
|
||||
public string? BankAccount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用电子普通发票。
|
||||
/// </summary>
|
||||
public bool EnableElectronicNormalInvoice { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用电子专用发票。
|
||||
/// </summary>
|
||||
public bool EnableElectronicSpecialInvoice { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用自动开票。
|
||||
/// </summary>
|
||||
public bool EnableAutoIssue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 自动开票单张最大金额。
|
||||
/// </summary>
|
||||
public decimal AutoIssueMaxAmount { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 作废发票命令。
|
||||
/// </summary>
|
||||
public sealed class VoidFinanceInvoiceRecordCommand : IRequest<FinanceInvoiceRecordDetailDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 发票记录 ID。
|
||||
/// </summary>
|
||||
public long RecordId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 作废原因。
|
||||
/// </summary>
|
||||
public string VoidReason { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 发票开票结果 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceInvoiceIssueResultDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 记录 ID。
|
||||
/// </summary>
|
||||
public long RecordId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发票号码。
|
||||
/// </summary>
|
||||
public string InvoiceNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 开票抬头。
|
||||
/// </summary>
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 接收邮箱。
|
||||
/// </summary>
|
||||
public string? ContactEmail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开票时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime IssuedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态编码。
|
||||
/// </summary>
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态文案。
|
||||
/// </summary>
|
||||
public string StatusText { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 发票记录详情 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceInvoiceRecordDetailDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 记录 ID。
|
||||
/// </summary>
|
||||
public long RecordId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发票号码。
|
||||
/// </summary>
|
||||
public string InvoiceNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 申请人。
|
||||
/// </summary>
|
||||
public string ApplicantName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 开票抬头(公司名)。
|
||||
/// </summary>
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 纳税人识别号。
|
||||
/// </summary>
|
||||
public string? TaxpayerNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发票类型编码。
|
||||
/// </summary>
|
||||
public string InvoiceType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 发票类型文案。
|
||||
/// </summary>
|
||||
public string InvoiceTypeText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联订单号。
|
||||
/// </summary>
|
||||
public string OrderNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 接收邮箱。
|
||||
/// </summary>
|
||||
public string? ContactEmail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 联系电话。
|
||||
/// </summary>
|
||||
public string? ContactPhone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 申请备注。
|
||||
/// </summary>
|
||||
public string? ApplyRemark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态编码。
|
||||
/// </summary>
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态文案。
|
||||
/// </summary>
|
||||
public string StatusText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 申请时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime AppliedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开票时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime? IssuedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开票人 ID。
|
||||
/// </summary>
|
||||
public long? IssuedByUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开票备注。
|
||||
/// </summary>
|
||||
public string? IssueRemark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 作废时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime? VoidedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 作废人 ID。
|
||||
/// </summary>
|
||||
public long? VoidedByUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 作废原因。
|
||||
/// </summary>
|
||||
public string? VoidReason { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 发票记录列表项 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceInvoiceRecordDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 记录 ID。
|
||||
/// </summary>
|
||||
public long RecordId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发票号码。
|
||||
/// </summary>
|
||||
public string InvoiceNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 申请人。
|
||||
/// </summary>
|
||||
public string ApplicantName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 开票抬头(公司名)。
|
||||
/// </summary>
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 发票类型编码。
|
||||
/// </summary>
|
||||
public string InvoiceType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 发票类型文案。
|
||||
/// </summary>
|
||||
public string InvoiceTypeText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联订单号。
|
||||
/// </summary>
|
||||
public string OrderNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态编码。
|
||||
/// </summary>
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态文案。
|
||||
/// </summary>
|
||||
public string StatusText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 申请时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime AppliedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 发票记录分页结果 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceInvoiceRecordListResultDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 列表项。
|
||||
/// </summary>
|
||||
public List<FinanceInvoiceRecordDto> Items { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
public int Page { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总条数。
|
||||
/// </summary>
|
||||
public int TotalCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 统计。
|
||||
/// </summary>
|
||||
public FinanceInvoiceStatsDto Stats { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 发票设置 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceInvoiceSettingDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 企业名称。
|
||||
/// </summary>
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 纳税人识别号。
|
||||
/// </summary>
|
||||
public string TaxpayerNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 注册地址。
|
||||
/// </summary>
|
||||
public string? RegisteredAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 注册电话。
|
||||
/// </summary>
|
||||
public string? RegisteredPhone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开户银行。
|
||||
/// </summary>
|
||||
public string? BankName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 银行账号。
|
||||
/// </summary>
|
||||
public string? BankAccount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用电子普通发票。
|
||||
/// </summary>
|
||||
public bool EnableElectronicNormalInvoice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用电子专用发票。
|
||||
/// </summary>
|
||||
public bool EnableElectronicSpecialInvoice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用自动开票。
|
||||
/// </summary>
|
||||
public bool EnableAutoIssue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 自动开票单张最大金额。
|
||||
/// </summary>
|
||||
public decimal AutoIssueMaxAmount { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 发票统计 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceInvoiceStatsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 本月已开票金额。
|
||||
/// </summary>
|
||||
public decimal CurrentMonthIssuedAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本月已开票张数。
|
||||
/// </summary>
|
||||
public int CurrentMonthIssuedCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 待开票数量。
|
||||
/// </summary>
|
||||
public int PendingCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已作废数量。
|
||||
/// </summary>
|
||||
public int VoidedCount { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Commands;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
using TakeoutSaaS.Domain.Tenants.Entities;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice;
|
||||
|
||||
/// <summary>
|
||||
/// 发票模块 DTO 构造器。
|
||||
/// </summary>
|
||||
internal static class FinanceInvoiceDtoFactory
|
||||
{
|
||||
public static FinanceInvoiceSettingDto CreateDefaultSettingDto()
|
||||
{
|
||||
return new FinanceInvoiceSettingDto
|
||||
{
|
||||
CompanyName = string.Empty,
|
||||
TaxpayerNumber = string.Empty,
|
||||
RegisteredAddress = null,
|
||||
RegisteredPhone = null,
|
||||
BankName = null,
|
||||
BankAccount = null,
|
||||
EnableElectronicNormalInvoice = true,
|
||||
EnableElectronicSpecialInvoice = false,
|
||||
EnableAutoIssue = false,
|
||||
AutoIssueMaxAmount = 10_000m
|
||||
};
|
||||
}
|
||||
|
||||
public static FinanceInvoiceSettingDto ToSettingDto(TenantInvoiceSetting source)
|
||||
{
|
||||
return new FinanceInvoiceSettingDto
|
||||
{
|
||||
CompanyName = source.CompanyName,
|
||||
TaxpayerNumber = source.TaxpayerNumber,
|
||||
RegisteredAddress = source.RegisteredAddress,
|
||||
RegisteredPhone = source.RegisteredPhone,
|
||||
BankName = source.BankName,
|
||||
BankAccount = source.BankAccount,
|
||||
EnableElectronicNormalInvoice = source.EnableElectronicNormalInvoice,
|
||||
EnableElectronicSpecialInvoice = source.EnableElectronicSpecialInvoice,
|
||||
EnableAutoIssue = source.EnableAutoIssue,
|
||||
AutoIssueMaxAmount = decimal.Round(source.AutoIssueMaxAmount, 2, MidpointRounding.AwayFromZero)
|
||||
};
|
||||
}
|
||||
|
||||
public static TenantInvoiceSetting CreateSettingEntity(
|
||||
SaveFinanceInvoiceSettingCommand request,
|
||||
string companyName,
|
||||
string taxpayerNumber,
|
||||
string? registeredAddress,
|
||||
string? registeredPhone,
|
||||
string? bankName,
|
||||
string? bankAccount,
|
||||
decimal autoIssueMaxAmount)
|
||||
{
|
||||
return new TenantInvoiceSetting
|
||||
{
|
||||
CompanyName = companyName,
|
||||
TaxpayerNumber = taxpayerNumber,
|
||||
RegisteredAddress = registeredAddress,
|
||||
RegisteredPhone = registeredPhone,
|
||||
BankName = bankName,
|
||||
BankAccount = bankAccount,
|
||||
EnableElectronicNormalInvoice = request.EnableElectronicNormalInvoice,
|
||||
EnableElectronicSpecialInvoice = request.EnableElectronicSpecialInvoice,
|
||||
EnableAutoIssue = request.EnableAutoIssue,
|
||||
AutoIssueMaxAmount = autoIssueMaxAmount
|
||||
};
|
||||
}
|
||||
|
||||
public static void ApplySettingChanges(
|
||||
TenantInvoiceSetting entity,
|
||||
SaveFinanceInvoiceSettingCommand request,
|
||||
string companyName,
|
||||
string taxpayerNumber,
|
||||
string? registeredAddress,
|
||||
string? registeredPhone,
|
||||
string? bankName,
|
||||
string? bankAccount,
|
||||
decimal autoIssueMaxAmount)
|
||||
{
|
||||
entity.CompanyName = companyName;
|
||||
entity.TaxpayerNumber = taxpayerNumber;
|
||||
entity.RegisteredAddress = registeredAddress;
|
||||
entity.RegisteredPhone = registeredPhone;
|
||||
entity.BankName = bankName;
|
||||
entity.BankAccount = bankAccount;
|
||||
entity.EnableElectronicNormalInvoice = request.EnableElectronicNormalInvoice;
|
||||
entity.EnableElectronicSpecialInvoice = request.EnableElectronicSpecialInvoice;
|
||||
entity.EnableAutoIssue = request.EnableAutoIssue;
|
||||
entity.AutoIssueMaxAmount = autoIssueMaxAmount;
|
||||
}
|
||||
|
||||
public static FinanceInvoiceStatsDto ToStatsDto(TenantInvoiceRecordStatsSnapshot source)
|
||||
{
|
||||
return new FinanceInvoiceStatsDto
|
||||
{
|
||||
CurrentMonthIssuedAmount = decimal.Round(source.CurrentMonthIssuedAmount, 2, MidpointRounding.AwayFromZero),
|
||||
CurrentMonthIssuedCount = source.CurrentMonthIssuedCount,
|
||||
PendingCount = source.PendingCount,
|
||||
VoidedCount = source.VoidedCount
|
||||
};
|
||||
}
|
||||
|
||||
public static FinanceInvoiceRecordDto ToRecordDto(TenantInvoiceRecord source)
|
||||
{
|
||||
return new FinanceInvoiceRecordDto
|
||||
{
|
||||
RecordId = source.Id,
|
||||
InvoiceNo = source.InvoiceNo,
|
||||
ApplicantName = source.ApplicantName,
|
||||
CompanyName = source.CompanyName,
|
||||
InvoiceType = FinanceInvoiceMapping.ToInvoiceTypeText(source.InvoiceType),
|
||||
InvoiceTypeText = FinanceInvoiceMapping.ToInvoiceTypeDisplayText(source.InvoiceType),
|
||||
Amount = decimal.Round(source.Amount, 2, MidpointRounding.AwayFromZero),
|
||||
OrderNo = source.OrderNo,
|
||||
Status = FinanceInvoiceMapping.ToStatusText(source.Status),
|
||||
StatusText = FinanceInvoiceMapping.ToStatusDisplayText(source.Status),
|
||||
AppliedAt = source.AppliedAt
|
||||
};
|
||||
}
|
||||
|
||||
public static FinanceInvoiceRecordDetailDto ToRecordDetailDto(TenantInvoiceRecord source)
|
||||
{
|
||||
return new FinanceInvoiceRecordDetailDto
|
||||
{
|
||||
RecordId = source.Id,
|
||||
InvoiceNo = source.InvoiceNo,
|
||||
ApplicantName = source.ApplicantName,
|
||||
CompanyName = source.CompanyName,
|
||||
TaxpayerNumber = source.TaxpayerNumber,
|
||||
InvoiceType = FinanceInvoiceMapping.ToInvoiceTypeText(source.InvoiceType),
|
||||
InvoiceTypeText = FinanceInvoiceMapping.ToInvoiceTypeDisplayText(source.InvoiceType),
|
||||
Amount = decimal.Round(source.Amount, 2, MidpointRounding.AwayFromZero),
|
||||
OrderNo = source.OrderNo,
|
||||
ContactEmail = source.ContactEmail,
|
||||
ContactPhone = source.ContactPhone,
|
||||
ApplyRemark = source.ApplyRemark,
|
||||
Status = FinanceInvoiceMapping.ToStatusText(source.Status),
|
||||
StatusText = FinanceInvoiceMapping.ToStatusDisplayText(source.Status),
|
||||
AppliedAt = source.AppliedAt,
|
||||
IssuedAt = source.IssuedAt,
|
||||
IssuedByUserId = source.IssuedByUserId,
|
||||
IssueRemark = source.IssueRemark,
|
||||
VoidedAt = source.VoidedAt,
|
||||
VoidedByUserId = source.VoidedByUserId,
|
||||
VoidReason = source.VoidReason
|
||||
};
|
||||
}
|
||||
|
||||
public static FinanceInvoiceIssueResultDto ToIssueResultDto(TenantInvoiceRecord source)
|
||||
{
|
||||
return new FinanceInvoiceIssueResultDto
|
||||
{
|
||||
RecordId = source.Id,
|
||||
InvoiceNo = source.InvoiceNo,
|
||||
CompanyName = source.CompanyName,
|
||||
Amount = decimal.Round(source.Amount, 2, MidpointRounding.AwayFromZero),
|
||||
ContactEmail = source.ContactEmail,
|
||||
IssuedAt = source.IssuedAt ?? DateTime.UtcNow,
|
||||
Status = FinanceInvoiceMapping.ToStatusText(source.Status),
|
||||
StatusText = FinanceInvoiceMapping.ToStatusDisplayText(source.Status)
|
||||
};
|
||||
}
|
||||
|
||||
public static TenantInvoiceRecord CreateRecordEntity(
|
||||
long tenantId,
|
||||
string invoiceNo,
|
||||
string applicantName,
|
||||
string companyName,
|
||||
string? taxpayerNumber,
|
||||
TenantInvoiceType invoiceType,
|
||||
decimal amount,
|
||||
string orderNo,
|
||||
string? contactEmail,
|
||||
string? contactPhone,
|
||||
string? applyRemark,
|
||||
DateTime appliedAt)
|
||||
{
|
||||
return new TenantInvoiceRecord
|
||||
{
|
||||
TenantId = tenantId,
|
||||
InvoiceNo = invoiceNo,
|
||||
ApplicantName = applicantName,
|
||||
CompanyName = companyName,
|
||||
TaxpayerNumber = taxpayerNumber,
|
||||
InvoiceType = invoiceType,
|
||||
Amount = amount,
|
||||
OrderNo = orderNo,
|
||||
ContactEmail = contactEmail,
|
||||
ContactPhone = contactPhone,
|
||||
ApplyRemark = applyRemark,
|
||||
Status = TenantInvoiceStatus.Pending,
|
||||
AppliedAt = appliedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
using System.Net.Mail;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice;
|
||||
|
||||
/// <summary>
|
||||
/// 发票模块映射与参数标准化。
|
||||
/// </summary>
|
||||
internal static class FinanceInvoiceMapping
|
||||
{
|
||||
public static TenantInvoiceType ParseInvoiceTypeRequired(string? value)
|
||||
{
|
||||
return ParseInvoiceTypeOptional(value)
|
||||
?? throw new BusinessException(ErrorCodes.BadRequest, "invoiceType 参数不合法");
|
||||
}
|
||||
|
||||
public static TenantInvoiceType? ParseInvoiceTypeOptional(string? value)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalized switch
|
||||
{
|
||||
"normal" => TenantInvoiceType.Normal,
|
||||
"special" => TenantInvoiceType.Special,
|
||||
_ => throw new BusinessException(ErrorCodes.BadRequest, "invoiceType 参数不合法")
|
||||
};
|
||||
}
|
||||
|
||||
public static TenantInvoiceStatus? ParseStatusOptional(string? value)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalized switch
|
||||
{
|
||||
"pending" => TenantInvoiceStatus.Pending,
|
||||
"issued" => TenantInvoiceStatus.Issued,
|
||||
"voided" => TenantInvoiceStatus.Voided,
|
||||
_ => throw new BusinessException(ErrorCodes.BadRequest, "status 参数不合法")
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToInvoiceTypeText(TenantInvoiceType value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
TenantInvoiceType.Normal => "normal",
|
||||
TenantInvoiceType.Special => "special",
|
||||
_ => "normal"
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToInvoiceTypeDisplayText(TenantInvoiceType value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
TenantInvoiceType.Normal => "普票",
|
||||
TenantInvoiceType.Special => "专票",
|
||||
_ => "普票"
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToStatusText(TenantInvoiceStatus value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
TenantInvoiceStatus.Pending => "pending",
|
||||
TenantInvoiceStatus.Issued => "issued",
|
||||
TenantInvoiceStatus.Voided => "voided",
|
||||
_ => "pending"
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToStatusDisplayText(TenantInvoiceStatus value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
TenantInvoiceStatus.Pending => "待开票",
|
||||
TenantInvoiceStatus.Issued => "已开票",
|
||||
TenantInvoiceStatus.Voided => "已作废",
|
||||
_ => "待开票"
|
||||
};
|
||||
}
|
||||
|
||||
public static string NormalizeCompanyName(string? value)
|
||||
{
|
||||
return NormalizeRequiredText(value, "companyName", 128);
|
||||
}
|
||||
|
||||
public static string NormalizeApplicantName(string? value)
|
||||
{
|
||||
return NormalizeRequiredText(value, "applicantName", 64);
|
||||
}
|
||||
|
||||
public static string NormalizeOrderNo(string? value)
|
||||
{
|
||||
return NormalizeRequiredText(value, "orderNo", 32);
|
||||
}
|
||||
|
||||
public static string NormalizeTaxpayerNumber(string? value)
|
||||
{
|
||||
return NormalizeRequiredText(value, "taxpayerNumber", 64);
|
||||
}
|
||||
|
||||
public static string? NormalizeOptionalTaxpayerNumber(string? value)
|
||||
{
|
||||
return NormalizeOptionalText(value, "taxpayerNumber", 64);
|
||||
}
|
||||
|
||||
public static string? NormalizeOptionalKeyword(string? value)
|
||||
{
|
||||
return NormalizeOptionalText(value, "keyword", 64);
|
||||
}
|
||||
|
||||
public static string? NormalizeOptionalEmail(string? value)
|
||||
{
|
||||
var normalized = NormalizeOptionalText(value, "contactEmail", 128);
|
||||
if (normalized is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ = new MailAddress(normalized);
|
||||
return normalized;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "contactEmail 参数不合法");
|
||||
}
|
||||
}
|
||||
|
||||
public static string? NormalizeOptionalPhone(string? value)
|
||||
{
|
||||
return NormalizeOptionalText(value, "contactPhone", 32);
|
||||
}
|
||||
|
||||
public static string? NormalizeOptionalRemark(string? value, string fieldName, int maxLength = 256)
|
||||
{
|
||||
return NormalizeOptionalText(value, fieldName, maxLength);
|
||||
}
|
||||
|
||||
public static string NormalizeVoidReason(string? value)
|
||||
{
|
||||
return NormalizeRequiredText(value, "voidReason", 256);
|
||||
}
|
||||
|
||||
public static decimal NormalizeAmount(decimal value)
|
||||
{
|
||||
if (value <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "amount 参数不合法");
|
||||
}
|
||||
|
||||
return decimal.Round(value, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
public static decimal NormalizeAutoIssueMaxAmount(decimal value)
|
||||
{
|
||||
if (value <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "autoIssueMaxAmount 参数不合法");
|
||||
}
|
||||
|
||||
return decimal.Round(value, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
public static (DateTime? StartUtc, DateTime? EndUtc) NormalizeDateRange(DateTime? startUtc, DateTime? endUtc)
|
||||
{
|
||||
DateTime? normalizedStart = null;
|
||||
DateTime? normalizedEnd = null;
|
||||
|
||||
if (startUtc.HasValue)
|
||||
{
|
||||
var utcValue = NormalizeUtc(startUtc.Value);
|
||||
normalizedStart = new DateTime(utcValue.Year, utcValue.Month, utcValue.Day, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
if (endUtc.HasValue)
|
||||
{
|
||||
var utcValue = NormalizeUtc(endUtc.Value);
|
||||
normalizedEnd = new DateTime(utcValue.Year, utcValue.Month, utcValue.Day, 0, 0, 0, DateTimeKind.Utc)
|
||||
.AddDays(1)
|
||||
.AddTicks(-1);
|
||||
}
|
||||
|
||||
if (normalizedStart.HasValue && normalizedEnd.HasValue && normalizedStart > normalizedEnd)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "开始日期不能晚于结束日期");
|
||||
}
|
||||
|
||||
return (normalizedStart, normalizedEnd);
|
||||
}
|
||||
|
||||
public static DateTime NormalizeUtc(DateTime value)
|
||||
{
|
||||
return value.Kind switch
|
||||
{
|
||||
DateTimeKind.Utc => value,
|
||||
DateTimeKind.Local => value.ToUniversalTime(),
|
||||
_ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
|
||||
};
|
||||
}
|
||||
|
||||
public static string BuildInvoiceNo(DateTime nowUtc)
|
||||
{
|
||||
var utcNow = NormalizeUtc(nowUtc);
|
||||
return $"INV{utcNow:yyyyMMddHHmmssfff}{Random.Shared.Next(100, 999)}";
|
||||
}
|
||||
|
||||
private static string NormalizeRequiredText(string? value, string fieldName, int maxLength)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 参数不合法");
|
||||
}
|
||||
|
||||
if (normalized.Length > maxLength)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 长度不能超过 {maxLength}");
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static string? NormalizeOptionalText(string? value, string fieldName, int maxLength)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim();
|
||||
if (normalized.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (normalized.Length > maxLength)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 长度不能超过 {maxLength}");
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Commands;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
using TakeoutSaaS.Domain.Tenants.Entities;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 申请发票处理器。
|
||||
/// </summary>
|
||||
public sealed class ApplyFinanceInvoiceRecordCommandHandler(
|
||||
ITenantInvoiceRepository repository,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUserAccessor)
|
||||
: IRequestHandler<ApplyFinanceInvoiceRecordCommand, FinanceInvoiceRecordDetailDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceInvoiceRecordDetailDto> Handle(
|
||||
ApplyFinanceInvoiceRecordCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var invoiceType = FinanceInvoiceMapping.ParseInvoiceTypeRequired(request.InvoiceType);
|
||||
var applicantName = FinanceInvoiceMapping.NormalizeApplicantName(request.ApplicantName);
|
||||
var companyName = FinanceInvoiceMapping.NormalizeCompanyName(request.CompanyName);
|
||||
var taxpayerNumber = FinanceInvoiceMapping.NormalizeOptionalTaxpayerNumber(request.TaxpayerNumber);
|
||||
var amount = FinanceInvoiceMapping.NormalizeAmount(request.Amount);
|
||||
var orderNo = FinanceInvoiceMapping.NormalizeOrderNo(request.OrderNo);
|
||||
var contactEmail = FinanceInvoiceMapping.NormalizeOptionalEmail(request.ContactEmail);
|
||||
var contactPhone = FinanceInvoiceMapping.NormalizeOptionalPhone(request.ContactPhone);
|
||||
var applyRemark = FinanceInvoiceMapping.NormalizeOptionalRemark(request.ApplyRemark, "applyRemark");
|
||||
var appliedAt = request.AppliedAt.HasValue
|
||||
? FinanceInvoiceMapping.NormalizeUtc(request.AppliedAt.Value)
|
||||
: DateTime.UtcNow;
|
||||
|
||||
if (invoiceType == TenantInvoiceType.Special && string.IsNullOrWhiteSpace(taxpayerNumber))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "专票必须填写纳税人识别号");
|
||||
}
|
||||
|
||||
var setting = await repository.GetSettingAsync(tenantId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.BadRequest, "请先完成发票设置");
|
||||
EnsureTypeEnabled(setting, invoiceType);
|
||||
|
||||
var invoiceNo = await GenerateInvoiceNoAsync(tenantId, cancellationToken);
|
||||
var entity = FinanceInvoiceDtoFactory.CreateRecordEntity(
|
||||
tenantId,
|
||||
invoiceNo,
|
||||
applicantName,
|
||||
companyName,
|
||||
taxpayerNumber,
|
||||
invoiceType,
|
||||
amount,
|
||||
orderNo,
|
||||
contactEmail,
|
||||
contactPhone,
|
||||
applyRemark,
|
||||
appliedAt);
|
||||
|
||||
if (setting.EnableAutoIssue && amount <= setting.AutoIssueMaxAmount)
|
||||
{
|
||||
entity.Status = TenantInvoiceStatus.Issued;
|
||||
entity.IssuedAt = DateTime.UtcNow;
|
||||
entity.IssuedByUserId = currentUserAccessor.IsAuthenticated ? currentUserAccessor.UserId : null;
|
||||
entity.IssueRemark = "系统自动开票";
|
||||
}
|
||||
|
||||
await repository.AddRecordAsync(entity, cancellationToken);
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return FinanceInvoiceDtoFactory.ToRecordDetailDto(entity);
|
||||
}
|
||||
|
||||
private static void EnsureTypeEnabled(TenantInvoiceSetting setting, TenantInvoiceType type)
|
||||
{
|
||||
if (type == TenantInvoiceType.Normal && !setting.EnableElectronicNormalInvoice)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "电子普通发票未启用");
|
||||
}
|
||||
|
||||
if (type == TenantInvoiceType.Special && !setting.EnableElectronicSpecialInvoice)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "电子专用发票未启用");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> GenerateInvoiceNoAsync(long tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
for (var index = 0; index < 10; index += 1)
|
||||
{
|
||||
var invoiceNo = FinanceInvoiceMapping.BuildInvoiceNo(DateTime.UtcNow);
|
||||
var exists = await repository.ExistsInvoiceNoAsync(tenantId, invoiceNo, cancellationToken);
|
||||
if (!exists)
|
||||
{
|
||||
return invoiceNo;
|
||||
}
|
||||
}
|
||||
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "生成发票号码失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Queries;
|
||||
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 发票记录详情查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetFinanceInvoiceRecordDetailQueryHandler(
|
||||
ITenantInvoiceRepository repository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetFinanceInvoiceRecordDetailQuery, FinanceInvoiceRecordDetailDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceInvoiceRecordDetailDto> Handle(
|
||||
GetFinanceInvoiceRecordDetailQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var record = await repository.FindRecordByIdAsync(tenantId, request.RecordId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "发票记录不存在");
|
||||
|
||||
return FinanceInvoiceDtoFactory.ToRecordDetailDto(record);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Queries;
|
||||
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 发票记录分页查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetFinanceInvoiceRecordListQueryHandler(
|
||||
ITenantInvoiceRepository repository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetFinanceInvoiceRecordListQuery, FinanceInvoiceRecordListResultDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceInvoiceRecordListResultDto> Handle(
|
||||
GetFinanceInvoiceRecordListQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var keyword = FinanceInvoiceMapping.NormalizeOptionalKeyword(request.Keyword);
|
||||
var (startUtc, endUtc) = FinanceInvoiceMapping.NormalizeDateRange(request.StartDateUtc, request.EndDateUtc);
|
||||
var page = Math.Max(1, request.Page);
|
||||
var pageSize = Math.Clamp(request.PageSize, 1, 200);
|
||||
|
||||
var (items, totalCount) = await repository.SearchRecordsAsync(
|
||||
tenantId,
|
||||
startUtc,
|
||||
endUtc,
|
||||
request.Status,
|
||||
request.InvoiceType,
|
||||
keyword,
|
||||
page,
|
||||
pageSize,
|
||||
cancellationToken);
|
||||
|
||||
var statsSnapshot = await repository.GetStatsAsync(tenantId, DateTime.UtcNow, cancellationToken);
|
||||
|
||||
return new FinanceInvoiceRecordListResultDto
|
||||
{
|
||||
Items = items.Select(FinanceInvoiceDtoFactory.ToRecordDto).ToList(),
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
TotalCount = totalCount,
|
||||
Stats = FinanceInvoiceDtoFactory.ToStatsDto(statsSnapshot)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Queries;
|
||||
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 发票设置详情查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetFinanceInvoiceSettingDetailQueryHandler(
|
||||
ITenantInvoiceRepository repository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetFinanceInvoiceSettingDetailQuery, FinanceInvoiceSettingDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceInvoiceSettingDto> Handle(
|
||||
GetFinanceInvoiceSettingDetailQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var setting = await repository.GetSettingAsync(tenantId, cancellationToken);
|
||||
|
||||
return setting is null
|
||||
? FinanceInvoiceDtoFactory.CreateDefaultSettingDto()
|
||||
: FinanceInvoiceDtoFactory.ToSettingDto(setting);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Commands;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
using TakeoutSaaS.Domain.Tenants.Entities;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 发票开票处理器。
|
||||
/// </summary>
|
||||
public sealed class IssueFinanceInvoiceRecordCommandHandler(
|
||||
ITenantInvoiceRepository repository,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUserAccessor)
|
||||
: IRequestHandler<IssueFinanceInvoiceRecordCommand, FinanceInvoiceIssueResultDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceInvoiceIssueResultDto> Handle(
|
||||
IssueFinanceInvoiceRecordCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var record = await repository.FindRecordByIdAsync(tenantId, request.RecordId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "发票记录不存在");
|
||||
|
||||
if (record.Status != TenantInvoiceStatus.Pending)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "仅待开票记录允许开票");
|
||||
}
|
||||
|
||||
var setting = await repository.GetSettingAsync(tenantId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.BadRequest, "请先完成发票设置");
|
||||
EnsureTypeEnabled(setting, record.InvoiceType);
|
||||
|
||||
record.ContactEmail = FinanceInvoiceMapping.NormalizeOptionalEmail(request.ContactEmail) ?? record.ContactEmail;
|
||||
record.IssueRemark = FinanceInvoiceMapping.NormalizeOptionalRemark(request.IssueRemark, "issueRemark");
|
||||
record.Status = TenantInvoiceStatus.Issued;
|
||||
record.IssuedAt = DateTime.UtcNow;
|
||||
record.IssuedByUserId = currentUserAccessor.IsAuthenticated ? currentUserAccessor.UserId : null;
|
||||
|
||||
await repository.UpdateRecordAsync(record, cancellationToken);
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return FinanceInvoiceDtoFactory.ToIssueResultDto(record);
|
||||
}
|
||||
|
||||
private static void EnsureTypeEnabled(TenantInvoiceSetting setting, TenantInvoiceType type)
|
||||
{
|
||||
if (type == TenantInvoiceType.Normal && !setting.EnableElectronicNormalInvoice)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "电子普通发票未启用");
|
||||
}
|
||||
|
||||
if (type == TenantInvoiceType.Special && !setting.EnableElectronicSpecialInvoice)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "电子专用发票未启用");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Commands;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 保存发票设置处理器。
|
||||
/// </summary>
|
||||
public sealed class SaveFinanceInvoiceSettingCommandHandler(
|
||||
ITenantInvoiceRepository repository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<SaveFinanceInvoiceSettingCommand, FinanceInvoiceSettingDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceInvoiceSettingDto> Handle(
|
||||
SaveFinanceInvoiceSettingCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!request.EnableElectronicNormalInvoice && !request.EnableElectronicSpecialInvoice)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "至少启用一种发票类型");
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var companyName = FinanceInvoiceMapping.NormalizeCompanyName(request.CompanyName);
|
||||
var taxpayerNumber = FinanceInvoiceMapping.NormalizeTaxpayerNumber(request.TaxpayerNumber);
|
||||
var registeredAddress = FinanceInvoiceMapping.NormalizeOptionalRemark(request.RegisteredAddress, "registeredAddress", 256);
|
||||
var registeredPhone = FinanceInvoiceMapping.NormalizeOptionalPhone(request.RegisteredPhone);
|
||||
var bankName = FinanceInvoiceMapping.NormalizeOptionalRemark(request.BankName, "bankName", 128);
|
||||
var bankAccount = FinanceInvoiceMapping.NormalizeOptionalRemark(request.BankAccount, "bankAccount", 64);
|
||||
var autoIssueMaxAmount = FinanceInvoiceMapping.NormalizeAutoIssueMaxAmount(request.AutoIssueMaxAmount);
|
||||
|
||||
var setting = await repository.GetSettingAsync(tenantId, cancellationToken);
|
||||
if (setting is null)
|
||||
{
|
||||
setting = FinanceInvoiceDtoFactory.CreateSettingEntity(
|
||||
request,
|
||||
companyName,
|
||||
taxpayerNumber,
|
||||
registeredAddress,
|
||||
registeredPhone,
|
||||
bankName,
|
||||
bankAccount,
|
||||
autoIssueMaxAmount);
|
||||
|
||||
await repository.AddSettingAsync(setting, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
FinanceInvoiceDtoFactory.ApplySettingChanges(
|
||||
setting,
|
||||
request,
|
||||
companyName,
|
||||
taxpayerNumber,
|
||||
registeredAddress,
|
||||
registeredPhone,
|
||||
bankName,
|
||||
bankAccount,
|
||||
autoIssueMaxAmount);
|
||||
|
||||
await repository.UpdateSettingAsync(setting, cancellationToken);
|
||||
}
|
||||
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
return FinanceInvoiceDtoFactory.ToSettingDto(setting);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Commands;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 发票作废处理器。
|
||||
/// </summary>
|
||||
public sealed class VoidFinanceInvoiceRecordCommandHandler(
|
||||
ITenantInvoiceRepository repository,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUserAccessor)
|
||||
: IRequestHandler<VoidFinanceInvoiceRecordCommand, FinanceInvoiceRecordDetailDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceInvoiceRecordDetailDto> Handle(
|
||||
VoidFinanceInvoiceRecordCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var record = await repository.FindRecordByIdAsync(tenantId, request.RecordId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "发票记录不存在");
|
||||
|
||||
if (record.Status != TenantInvoiceStatus.Issued)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "仅已开票记录允许作废");
|
||||
}
|
||||
|
||||
record.Status = TenantInvoiceStatus.Voided;
|
||||
record.VoidReason = FinanceInvoiceMapping.NormalizeVoidReason(request.VoidReason);
|
||||
record.VoidedAt = DateTime.UtcNow;
|
||||
record.VoidedByUserId = currentUserAccessor.IsAuthenticated ? currentUserAccessor.UserId : null;
|
||||
|
||||
await repository.UpdateRecordAsync(record, cancellationToken);
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return FinanceInvoiceDtoFactory.ToRecordDetailDto(record);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询发票记录详情。
|
||||
/// </summary>
|
||||
public sealed class GetFinanceInvoiceRecordDetailQuery : IRequest<FinanceInvoiceRecordDetailDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 发票记录 ID。
|
||||
/// </summary>
|
||||
public long RecordId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询发票记录分页。
|
||||
/// </summary>
|
||||
public sealed class GetFinanceInvoiceRecordListQuery : IRequest<FinanceInvoiceRecordListResultDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 开始日期(UTC)。
|
||||
/// </summary>
|
||||
public DateTime? StartDateUtc { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束日期(UTC)。
|
||||
/// </summary>
|
||||
public DateTime? EndDateUtc { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态筛选。
|
||||
/// </summary>
|
||||
public TenantInvoiceStatus? Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 类型筛选。
|
||||
/// </summary>
|
||||
public TenantInvoiceType? InvoiceType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键词。
|
||||
/// </summary>
|
||||
public string? Keyword { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
public int Page { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; init; } = 20;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Invoice.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Invoice.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询发票设置详情。
|
||||
/// </summary>
|
||||
public sealed class GetFinanceInvoiceSettingDetailQuery : IRequest<FinanceInvoiceSettingDto>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
namespace TakeoutSaaS.Application.App.Finance.Settlement.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 到账查询汇总行 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceSettlementListItemDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 到账日期(UTC 日期)。
|
||||
/// </summary>
|
||||
public DateTime ArrivedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 渠道编码(wechat/alipay)。
|
||||
/// </summary>
|
||||
public string Channel { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 渠道文案。
|
||||
/// </summary>
|
||||
public string ChannelText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 交易笔数。
|
||||
/// </summary>
|
||||
public int TransactionCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 到账金额。
|
||||
/// </summary>
|
||||
public decimal ArrivedAmount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账查询分页结果 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceSettlementListResultDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 列表项。
|
||||
/// </summary>
|
||||
public List<FinanceSettlementListItemDto> Items { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 总数。
|
||||
/// </summary>
|
||||
public int Total { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前页码。
|
||||
/// </summary>
|
||||
public int Page { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账明细行 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceSettlementDetailItemDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单号。
|
||||
/// </summary>
|
||||
public string OrderNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime PaidAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账明细结果 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceSettlementDetailResultDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 明细列表。
|
||||
/// </summary>
|
||||
public List<FinanceSettlementDetailItemDto> Items { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账统计 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceSettlementStatsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 今日到账金额。
|
||||
/// </summary>
|
||||
public decimal TodayArrivedAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 昨日到账金额。
|
||||
/// </summary>
|
||||
public decimal YesterdayArrivedAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本月到账金额。
|
||||
/// </summary>
|
||||
public decimal CurrentMonthArrivedAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本月交易笔数。
|
||||
/// </summary>
|
||||
public int CurrentMonthTransactionCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账账户信息 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceSettlementAccountDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 银行名称。
|
||||
/// </summary>
|
||||
public string BankName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 开户名。
|
||||
/// </summary>
|
||||
public string BankAccountName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 脱敏银行账号。
|
||||
/// </summary>
|
||||
public string BankAccountNoMasked { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 脱敏微信商户号。
|
||||
/// </summary>
|
||||
public string WechatMerchantNoMasked { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 脱敏支付宝 PID。
|
||||
/// </summary>
|
||||
public string AlipayPidMasked { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 结算周期文案。
|
||||
/// </summary>
|
||||
public string SettlementPeriodText { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账导出 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceSettlementExportDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件名。
|
||||
/// </summary>
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 文件内容 Base64。
|
||||
/// </summary>
|
||||
public string FileContentBase64 { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 导出总数。
|
||||
/// </summary>
|
||||
public int TotalCount { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
|
||||
using TakeoutSaaS.Application.App.Finance.Settlement.Queries;
|
||||
using TakeoutSaaS.Domain.Finance.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Settlement.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 到账汇总导出处理器。
|
||||
/// </summary>
|
||||
public sealed class ExportFinanceSettlementCsvQueryHandler(
|
||||
IFinanceTransactionRepository financeTransactionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<ExportFinanceSettlementCsvQuery, FinanceSettlementExportDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceSettlementExportDto> Handle(
|
||||
ExportFinanceSettlementCsvQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var rows = await financeTransactionRepository.ListSettlementForExportAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.StartAt,
|
||||
request.EndAt,
|
||||
request.PaymentMethod,
|
||||
cancellationToken);
|
||||
|
||||
var list = rows.Select(FinanceSettlementMapping.ToListItem).ToList();
|
||||
var csv = BuildCsv(list);
|
||||
return new FinanceSettlementExportDto
|
||||
{
|
||||
FileName = $"settlement-{request.StoreId}-{DateTime.UtcNow:yyyyMMddHHmmss}.csv",
|
||||
FileContentBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(csv)),
|
||||
TotalCount = list.Count
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildCsv(IReadOnlyList<FinanceSettlementListItemDto> rows)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append('\uFEFF');
|
||||
sb.AppendLine("到账日期,支付渠道,交易笔数,到账金额");
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
sb.AppendLine(string.Join(',',
|
||||
Escape(row.ArrivedDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)),
|
||||
Escape(row.ChannelText),
|
||||
Escape(row.TransactionCount.ToString(CultureInfo.InvariantCulture)),
|
||||
Escape(FinanceSettlementMapping.FormatAmount(row.ArrivedAmount))));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string Escape(string? value)
|
||||
{
|
||||
var normalized = value ?? string.Empty;
|
||||
if (normalized.Contains(',') || normalized.Contains('"') || normalized.Contains('\n'))
|
||||
{
|
||||
return $"\"{normalized.Replace("\"", "\"\"", StringComparison.Ordinal)}\"";
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using System.Globalization;
|
||||
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
|
||||
using TakeoutSaaS.Domain.Finance.Models;
|
||||
using TakeoutSaaS.Domain.Payments.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Settlement.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 到账查询映射辅助。
|
||||
/// </summary>
|
||||
internal static class FinanceSettlementMapping
|
||||
{
|
||||
/// <summary>
|
||||
/// 支付方式转渠道编码。
|
||||
/// </summary>
|
||||
public static string ToChannelCode(PaymentMethod paymentMethod)
|
||||
{
|
||||
return paymentMethod switch
|
||||
{
|
||||
PaymentMethod.WeChatPay => "wechat",
|
||||
PaymentMethod.Alipay => "alipay",
|
||||
_ => "unknown"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 支付方式转渠道文案。
|
||||
/// </summary>
|
||||
public static string ToChannelText(PaymentMethod paymentMethod)
|
||||
{
|
||||
return paymentMethod switch
|
||||
{
|
||||
PaymentMethod.WeChatPay => "微信支付",
|
||||
PaymentMethod.Alipay => "支付宝",
|
||||
_ => "未知渠道"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 映射到账汇总行。
|
||||
/// </summary>
|
||||
public static FinanceSettlementListItemDto ToListItem(FinanceSettlementListItemSnapshot source)
|
||||
{
|
||||
return new FinanceSettlementListItemDto
|
||||
{
|
||||
ArrivedDate = source.ArrivedDate,
|
||||
Channel = ToChannelCode(source.PaymentMethod),
|
||||
ChannelText = ToChannelText(source.PaymentMethod),
|
||||
TransactionCount = source.TransactionCount,
|
||||
ArrivedAmount = decimal.Round(source.ArrivedAmount, 2, MidpointRounding.AwayFromZero)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 映射到账明细行。
|
||||
/// </summary>
|
||||
public static FinanceSettlementDetailItemDto ToDetailItem(FinanceSettlementDetailItemSnapshot source)
|
||||
{
|
||||
return new FinanceSettlementDetailItemDto
|
||||
{
|
||||
OrderNo = source.OrderNo,
|
||||
Amount = decimal.Round(source.Amount, 2, MidpointRounding.AwayFromZero),
|
||||
PaidAt = source.PaidAt
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化金额(导出场景)。
|
||||
/// </summary>
|
||||
public static string FormatAmount(decimal value)
|
||||
{
|
||||
return decimal.Round(value, 2, MidpointRounding.AwayFromZero)
|
||||
.ToString("0.00", CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
|
||||
using TakeoutSaaS.Application.App.Finance.Settlement.Queries;
|
||||
using TakeoutSaaS.Domain.Finance.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Settlement.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 到账账户信息查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetFinanceSettlementAccountQueryHandler(
|
||||
IFinanceTransactionRepository financeTransactionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetFinanceSettlementAccountQuery, FinanceSettlementAccountDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceSettlementAccountDto?> Handle(
|
||||
GetFinanceSettlementAccountQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var snapshot = await financeTransactionRepository.GetSettlementAccountAsync(
|
||||
tenantId,
|
||||
cancellationToken);
|
||||
|
||||
if (snapshot is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new FinanceSettlementAccountDto
|
||||
{
|
||||
BankName = snapshot.BankName,
|
||||
BankAccountName = snapshot.BankAccountName,
|
||||
BankAccountNoMasked = snapshot.BankAccountNoMasked,
|
||||
WechatMerchantNoMasked = snapshot.WechatMerchantNoMasked,
|
||||
AlipayPidMasked = snapshot.AlipayPidMasked,
|
||||
SettlementPeriodText = snapshot.SettlementPeriodText
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
|
||||
using TakeoutSaaS.Application.App.Finance.Settlement.Queries;
|
||||
using TakeoutSaaS.Domain.Finance.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Settlement.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 到账明细查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetFinanceSettlementDetailQueryHandler(
|
||||
IFinanceTransactionRepository financeTransactionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetFinanceSettlementDetailQuery, FinanceSettlementDetailResultDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceSettlementDetailResultDto> Handle(
|
||||
GetFinanceSettlementDetailQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var rows = await financeTransactionRepository.GetSettlementDetailsAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.ArrivedDate,
|
||||
request.PaymentMethod,
|
||||
request.Take,
|
||||
cancellationToken);
|
||||
|
||||
return new FinanceSettlementDetailResultDto
|
||||
{
|
||||
Items = rows.Select(FinanceSettlementMapping.ToDetailItem).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
|
||||
using TakeoutSaaS.Application.App.Finance.Settlement.Queries;
|
||||
using TakeoutSaaS.Domain.Finance.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Settlement.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 到账统计查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetFinanceSettlementStatsQueryHandler(
|
||||
IFinanceTransactionRepository financeTransactionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetFinanceSettlementStatsQuery, FinanceSettlementStatsDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceSettlementStatsDto> Handle(
|
||||
GetFinanceSettlementStatsQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var snapshot = await financeTransactionRepository.GetSettlementStatsAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
DateTime.UtcNow,
|
||||
cancellationToken);
|
||||
|
||||
return new FinanceSettlementStatsDto
|
||||
{
|
||||
TodayArrivedAmount = snapshot.TodayArrivedAmount,
|
||||
YesterdayArrivedAmount = snapshot.YesterdayArrivedAmount,
|
||||
CurrentMonthArrivedAmount = snapshot.CurrentMonthArrivedAmount,
|
||||
CurrentMonthTransactionCount = snapshot.CurrentMonthTransactionCount
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
|
||||
using TakeoutSaaS.Application.App.Finance.Settlement.Queries;
|
||||
using TakeoutSaaS.Domain.Finance.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Settlement.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 到账汇总分页查询处理器。
|
||||
/// </summary>
|
||||
public sealed class SearchFinanceSettlementListQueryHandler(
|
||||
IFinanceTransactionRepository financeTransactionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<SearchFinanceSettlementListQuery, FinanceSettlementListResultDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceSettlementListResultDto> Handle(
|
||||
SearchFinanceSettlementListQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var normalizedPage = Math.Max(1, request.Page);
|
||||
var normalizedPageSize = Math.Clamp(request.PageSize, 1, 200);
|
||||
|
||||
var snapshot = await financeTransactionRepository.SearchSettlementPageAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.StartAt,
|
||||
request.EndAt,
|
||||
request.PaymentMethod,
|
||||
normalizedPage,
|
||||
normalizedPageSize,
|
||||
cancellationToken);
|
||||
|
||||
return new FinanceSettlementListResultDto
|
||||
{
|
||||
Items = snapshot.Items.Select(FinanceSettlementMapping.ToListItem).ToList(),
|
||||
Total = snapshot.TotalCount,
|
||||
Page = normalizedPage,
|
||||
PageSize = normalizedPageSize
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
|
||||
using TakeoutSaaS.Domain.Payments.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Settlement.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 导出到账汇总 CSV。
|
||||
/// </summary>
|
||||
public sealed class ExportFinanceSettlementCsvQuery : IRequest<FinanceSettlementExportDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间(UTC,闭区间)。
|
||||
/// </summary>
|
||||
public DateTime? StartAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间(UTC,开区间)。
|
||||
/// </summary>
|
||||
public DateTime? EndAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付方式筛选。
|
||||
/// </summary>
|
||||
public PaymentMethod? PaymentMethod { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Settlement.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询到账账户信息。
|
||||
/// </summary>
|
||||
public sealed class GetFinanceSettlementAccountQuery : IRequest<FinanceSettlementAccountDto?>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
|
||||
using TakeoutSaaS.Domain.Payments.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Settlement.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询到账明细。
|
||||
/// </summary>
|
||||
public sealed class GetFinanceSettlementDetailQuery : IRequest<FinanceSettlementDetailResultDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 到账日期(UTC 日期)。
|
||||
/// </summary>
|
||||
public DateTime ArrivedDate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 渠道(微信/支付宝)。
|
||||
/// </summary>
|
||||
public PaymentMethod PaymentMethod { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 限制条数。
|
||||
/// </summary>
|
||||
public int Take { get; init; } = 20;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Settlement.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询到账统计。
|
||||
/// </summary>
|
||||
public sealed class GetFinanceSettlementStatsQuery : IRequest<FinanceSettlementStatsDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
|
||||
using TakeoutSaaS.Domain.Payments.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Settlement.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询到账汇总分页。
|
||||
/// </summary>
|
||||
public sealed class SearchFinanceSettlementListQuery : IRequest<FinanceSettlementListResultDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间(UTC,闭区间)。
|
||||
/// </summary>
|
||||
public DateTime? StartAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间(UTC,开区间)。
|
||||
/// </summary>
|
||||
public DateTime? EndAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付方式筛选。
|
||||
/// </summary>
|
||||
public PaymentMethod? PaymentMethod { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
public int Page { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; init; } = 20;
|
||||
}
|
||||
@@ -60,6 +60,16 @@ public sealed record SubmitTenantVerificationCommand : IRequest<TenantVerificati
|
||||
/// </summary>
|
||||
public string? BankName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 微信商户号。
|
||||
/// </summary>
|
||||
public string? WeChatMerchantNo { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付宝 PID。
|
||||
/// </summary>
|
||||
public string? AlipayPid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 其他补充资料 JSON。
|
||||
/// </summary>
|
||||
|
||||
@@ -71,6 +71,16 @@ public sealed class TenantVerificationDto
|
||||
/// </summary>
|
||||
public string? BankName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 微信商户号。
|
||||
/// </summary>
|
||||
public string? WeChatMerchantNo { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付宝 PID。
|
||||
/// </summary>
|
||||
public string? AlipayPid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 附加资料(JSON)。
|
||||
/// </summary>
|
||||
|
||||
@@ -54,6 +54,8 @@ public sealed class SubmitTenantVerificationCommandHandler(
|
||||
profile.BankAccountName = request.BankAccountName;
|
||||
profile.BankAccountNumber = request.BankAccountNumber;
|
||||
profile.BankName = request.BankName;
|
||||
profile.WeChatMerchantNo = request.WeChatMerchantNo;
|
||||
profile.AlipayPid = request.AlipayPid;
|
||||
profile.AdditionalDataJson = request.AdditionalDataJson;
|
||||
profile.Status = TenantVerificationStatus.Pending;
|
||||
profile.SubmittedAt = DateTime.UtcNow;
|
||||
|
||||
@@ -31,6 +31,8 @@ internal static class TenantMapping
|
||||
BankAccountName = profile.BankAccountName,
|
||||
BankAccountNumber = profile.BankAccountNumber,
|
||||
BankName = profile.BankName,
|
||||
WeChatMerchantNo = profile.WeChatMerchantNo,
|
||||
AlipayPid = profile.AlipayPid,
|
||||
AdditionalDataJson = profile.AdditionalDataJson,
|
||||
SubmittedAt = profile.SubmittedAt,
|
||||
ReviewRemarks = profile.ReviewRemarks,
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
using TakeoutSaaS.Domain.Payments.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Finance.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 到账查询汇总行。
|
||||
/// </summary>
|
||||
public sealed record FinanceSettlementListItemSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// 到账日期(UTC 日期)。
|
||||
/// </summary>
|
||||
public required DateTime ArrivedDate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付方式。
|
||||
/// </summary>
|
||||
public required PaymentMethod PaymentMethod { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 交易笔数。
|
||||
/// </summary>
|
||||
public required int TransactionCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 到账金额。
|
||||
/// </summary>
|
||||
public required decimal ArrivedAmount { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账查询明细行。
|
||||
/// </summary>
|
||||
public sealed record FinanceSettlementDetailItemSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单号。
|
||||
/// </summary>
|
||||
public required string OrderNo { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付金额。
|
||||
/// </summary>
|
||||
public required decimal Amount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付时间(UTC)。
|
||||
/// </summary>
|
||||
public required DateTime PaidAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账查询分页快照。
|
||||
/// </summary>
|
||||
public sealed record FinanceSettlementPageSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// 列表项。
|
||||
/// </summary>
|
||||
public required IReadOnlyList<FinanceSettlementListItemSnapshot> Items { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 总数。
|
||||
/// </summary>
|
||||
public required int TotalCount { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账概览统计快照。
|
||||
/// </summary>
|
||||
public sealed record FinanceSettlementStatsSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// 今日到账。
|
||||
/// </summary>
|
||||
public required decimal TodayArrivedAmount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 昨日到账。
|
||||
/// </summary>
|
||||
public required decimal YesterdayArrivedAmount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 本月到账。
|
||||
/// </summary>
|
||||
public required decimal CurrentMonthArrivedAmount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 本月交易笔数。
|
||||
/// </summary>
|
||||
public required int CurrentMonthTransactionCount { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到账账户信息快照。
|
||||
/// </summary>
|
||||
public sealed record FinanceSettlementAccountSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// 银行名称。
|
||||
/// </summary>
|
||||
public required string BankName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 开户名。
|
||||
/// </summary>
|
||||
public required string BankAccountName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 脱敏银行账号。
|
||||
/// </summary>
|
||||
public required string BankAccountNoMasked { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 微信商户号(脱敏)。
|
||||
/// </summary>
|
||||
public required string WechatMerchantNoMasked { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付宝 PID(脱敏)。
|
||||
/// </summary>
|
||||
public required string AlipayPidMasked { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 结算周期文案。
|
||||
/// </summary>
|
||||
public required string SettlementPeriodText { get; init; }
|
||||
}
|
||||
@@ -63,4 +63,55 @@ public interface IFinanceTransactionRepository
|
||||
PaymentMethod? paymentMethod,
|
||||
string? keyword,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 查询到账概览统计。
|
||||
/// </summary>
|
||||
Task<FinanceSettlementStatsSnapshot> GetSettlementStatsAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
DateTime currentUtc,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 查询到账账户信息。
|
||||
/// </summary>
|
||||
Task<FinanceSettlementAccountSnapshot?> GetSettlementAccountAsync(
|
||||
long tenantId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 查询到账汇总分页。
|
||||
/// </summary>
|
||||
Task<FinanceSettlementPageSnapshot> SearchSettlementPageAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
DateTime? startAt,
|
||||
DateTime? endAt,
|
||||
PaymentMethod? paymentMethod,
|
||||
int page,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 查询到账明细。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<FinanceSettlementDetailItemSnapshot>> GetSettlementDetailsAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
DateTime arrivedDate,
|
||||
PaymentMethod paymentMethod,
|
||||
int take,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 查询到账导出数据。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<FinanceSettlementListItemSnapshot>> ListSettlementForExportAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
DateTime? startAt,
|
||||
DateTime? endAt,
|
||||
PaymentMethod? paymentMethod,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Tenants.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 租户发票记录。
|
||||
/// </summary>
|
||||
public sealed class TenantInvoiceRecord : MultiTenantEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 发票号码。
|
||||
/// </summary>
|
||||
public string InvoiceNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 申请人。
|
||||
/// </summary>
|
||||
public string ApplicantName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 开票抬头(公司名)。
|
||||
/// </summary>
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 纳税人识别号快照。
|
||||
/// </summary>
|
||||
public string? TaxpayerNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发票类型。
|
||||
/// </summary>
|
||||
public TenantInvoiceType InvoiceType { get; set; } = TenantInvoiceType.Normal;
|
||||
|
||||
/// <summary>
|
||||
/// 开票金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联订单号。
|
||||
/// </summary>
|
||||
public string OrderNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 接收邮箱。
|
||||
/// </summary>
|
||||
public string? ContactEmail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 联系电话。
|
||||
/// </summary>
|
||||
public string? ContactPhone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 申请备注。
|
||||
/// </summary>
|
||||
public string? ApplyRemark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发票状态。
|
||||
/// </summary>
|
||||
public TenantInvoiceStatus Status { get; set; } = TenantInvoiceStatus.Pending;
|
||||
|
||||
/// <summary>
|
||||
/// 申请时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime AppliedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// 开票时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime? IssuedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开票人 ID。
|
||||
/// </summary>
|
||||
public long? IssuedByUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开票备注。
|
||||
/// </summary>
|
||||
public string? IssueRemark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 作废时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime? VoidedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 作废人 ID。
|
||||
/// </summary>
|
||||
public long? VoidedByUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 作废原因。
|
||||
/// </summary>
|
||||
public string? VoidReason { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Tenants.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 租户发票开票基础设置。
|
||||
/// </summary>
|
||||
public sealed class TenantInvoiceSetting : MultiTenantEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 企业名称。
|
||||
/// </summary>
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 纳税人识别号。
|
||||
/// </summary>
|
||||
public string TaxpayerNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 注册地址。
|
||||
/// </summary>
|
||||
public string? RegisteredAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 注册电话。
|
||||
/// </summary>
|
||||
public string? RegisteredPhone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开户银行。
|
||||
/// </summary>
|
||||
public string? BankName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 银行账号。
|
||||
/// </summary>
|
||||
public string? BankAccount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用电子普通发票。
|
||||
/// </summary>
|
||||
public bool EnableElectronicNormalInvoice { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用电子专用发票。
|
||||
/// </summary>
|
||||
public bool EnableElectronicSpecialInvoice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用自动开票。
|
||||
/// </summary>
|
||||
public bool EnableAutoIssue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 自动开票单张最大金额。
|
||||
/// </summary>
|
||||
public decimal AutoIssueMaxAmount { get; set; } = 10_000m;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
@@ -63,6 +64,18 @@ public sealed class TenantVerificationProfile : AuditableEntityBase
|
||||
/// </summary>
|
||||
public string? BankName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 微信商户号。
|
||||
/// </summary>
|
||||
[MaxLength(64)]
|
||||
public string? WeChatMerchantNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付宝 PID。
|
||||
/// </summary>
|
||||
[MaxLength(64)]
|
||||
public string? AlipayPid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 附加资料(JSON)。
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace TakeoutSaaS.Domain.Tenants.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 租户发票状态。
|
||||
/// </summary>
|
||||
public enum TenantInvoiceStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 待开票。
|
||||
/// </summary>
|
||||
Pending = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 已开票。
|
||||
/// </summary>
|
||||
Issued = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 已作废。
|
||||
/// </summary>
|
||||
Voided = 3
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace TakeoutSaaS.Domain.Tenants.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 租户发票类型。
|
||||
/// </summary>
|
||||
public enum TenantInvoiceType
|
||||
{
|
||||
/// <summary>
|
||||
/// 电子普通发票。
|
||||
/// </summary>
|
||||
Normal = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 电子专用发票。
|
||||
/// </summary>
|
||||
Special = 2
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using TakeoutSaaS.Domain.Tenants.Entities;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 租户发票仓储契约。
|
||||
/// </summary>
|
||||
public interface ITenantInvoiceRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询租户发票设置。
|
||||
/// </summary>
|
||||
Task<TenantInvoiceSetting?> GetSettingAsync(long tenantId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增发票设置。
|
||||
/// </summary>
|
||||
Task AddSettingAsync(TenantInvoiceSetting entity, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 更新发票设置。
|
||||
/// </summary>
|
||||
Task UpdateSettingAsync(TenantInvoiceSetting entity, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询发票记录。
|
||||
/// </summary>
|
||||
Task<(IReadOnlyList<TenantInvoiceRecord> Items, int TotalCount)> SearchRecordsAsync(
|
||||
long tenantId,
|
||||
DateTime? startUtc,
|
||||
DateTime? endUtc,
|
||||
TenantInvoiceStatus? status,
|
||||
TenantInvoiceType? invoiceType,
|
||||
string? keyword,
|
||||
int page,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取发票页统计。
|
||||
/// </summary>
|
||||
Task<TenantInvoiceRecordStatsSnapshot> GetStatsAsync(
|
||||
long tenantId,
|
||||
DateTime nowUtc,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 根据标识查询发票记录。
|
||||
/// </summary>
|
||||
Task<TenantInvoiceRecord?> FindRecordByIdAsync(
|
||||
long tenantId,
|
||||
long recordId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 判断租户下发票号码是否已存在。
|
||||
/// </summary>
|
||||
Task<bool> ExistsInvoiceNoAsync(
|
||||
long tenantId,
|
||||
string invoiceNo,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增发票记录。
|
||||
/// </summary>
|
||||
Task AddRecordAsync(TenantInvoiceRecord entity, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 更新发票记录。
|
||||
/// </summary>
|
||||
Task UpdateRecordAsync(TenantInvoiceRecord entity, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 持久化变更。
|
||||
/// </summary>
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发票页面统计快照。
|
||||
/// </summary>
|
||||
public sealed record TenantInvoiceRecordStatsSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// 本月已开票金额。
|
||||
/// </summary>
|
||||
public decimal CurrentMonthIssuedAmount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 本月已开票张数。
|
||||
/// </summary>
|
||||
public int CurrentMonthIssuedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 待开票张数。
|
||||
/// </summary>
|
||||
public int PendingCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 已作废张数。
|
||||
/// </summary>
|
||||
public int VoidedCount { get; init; }
|
||||
}
|
||||
@@ -69,6 +69,7 @@ public static class AppServiceCollectionExtensions
|
||||
services.AddScoped<ITenantQuotaUsageRepository, EfTenantQuotaUsageRepository>();
|
||||
services.AddScoped<ITenantQuotaUsageHistoryRepository, EfTenantQuotaUsageHistoryRepository>();
|
||||
services.AddScoped<ITenantVisibilityRoleRuleRepository, TenantVisibilityRoleRuleRepository>();
|
||||
services.AddScoped<ITenantInvoiceRepository, EfTenantInvoiceRepository>();
|
||||
services.AddScoped<IInventoryRepository, EfInventoryRepository>();
|
||||
services.AddScoped<IQuotaPackageRepository, EfQuotaPackageRepository>();
|
||||
services.AddScoped<IStatisticsRepository, EfStatisticsRepository>();
|
||||
|
||||
@@ -95,6 +95,14 @@ public sealed class TakeoutAppDbContext(
|
||||
/// </summary>
|
||||
public DbSet<TenantVisibilityRoleRule> TenantVisibilityRoleRules => Set<TenantVisibilityRoleRule>();
|
||||
/// <summary>
|
||||
/// 租户发票设置。
|
||||
/// </summary>
|
||||
public DbSet<TenantInvoiceSetting> TenantInvoiceSettings => Set<TenantInvoiceSetting>();
|
||||
/// <summary>
|
||||
/// 租户发票记录。
|
||||
/// </summary>
|
||||
public DbSet<TenantInvoiceRecord> TenantInvoiceRecords => Set<TenantInvoiceRecord>();
|
||||
/// <summary>
|
||||
/// 成本录入汇总。
|
||||
/// </summary>
|
||||
public DbSet<FinanceCostEntry> FinanceCostEntries => Set<FinanceCostEntry>();
|
||||
@@ -534,6 +542,8 @@ public sealed class TakeoutAppDbContext(
|
||||
ConfigureTenantAnnouncementRead(modelBuilder.Entity<TenantAnnouncementRead>());
|
||||
ConfigureTenantVerificationProfile(modelBuilder.Entity<TenantVerificationProfile>());
|
||||
ConfigureTenantVisibilityRoleRule(modelBuilder.Entity<TenantVisibilityRoleRule>());
|
||||
ConfigureTenantInvoiceSetting(modelBuilder.Entity<TenantInvoiceSetting>());
|
||||
ConfigureTenantInvoiceRecord(modelBuilder.Entity<TenantInvoiceRecord>());
|
||||
ConfigureFinanceCostEntry(modelBuilder.Entity<FinanceCostEntry>());
|
||||
ConfigureFinanceCostEntryItem(modelBuilder.Entity<FinanceCostEntryItem>());
|
||||
ConfigureQuotaPackage(modelBuilder.Entity<QuotaPackage>());
|
||||
@@ -1053,6 +1063,52 @@ public sealed class TakeoutAppDbContext(
|
||||
builder.HasIndex(x => x.TenantId).IsUnique();
|
||||
}
|
||||
|
||||
private static void ConfigureTenantInvoiceSetting(EntityTypeBuilder<TenantInvoiceSetting> builder)
|
||||
{
|
||||
builder.ToTable("finance_invoice_settings");
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.TenantId).IsRequired();
|
||||
builder.Property(x => x.CompanyName).HasMaxLength(128).IsRequired();
|
||||
builder.Property(x => x.TaxpayerNumber).HasMaxLength(64).IsRequired();
|
||||
builder.Property(x => x.RegisteredAddress).HasMaxLength(256);
|
||||
builder.Property(x => x.RegisteredPhone).HasMaxLength(32);
|
||||
builder.Property(x => x.BankName).HasMaxLength(128);
|
||||
builder.Property(x => x.BankAccount).HasMaxLength(64);
|
||||
builder.Property(x => x.EnableElectronicNormalInvoice).IsRequired();
|
||||
builder.Property(x => x.EnableElectronicSpecialInvoice).IsRequired();
|
||||
builder.Property(x => x.EnableAutoIssue).IsRequired();
|
||||
builder.Property(x => x.AutoIssueMaxAmount).HasPrecision(18, 2).IsRequired();
|
||||
|
||||
builder.HasIndex(x => x.TenantId).IsUnique();
|
||||
}
|
||||
|
||||
private static void ConfigureTenantInvoiceRecord(EntityTypeBuilder<TenantInvoiceRecord> builder)
|
||||
{
|
||||
builder.ToTable("finance_invoice_records");
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.TenantId).IsRequired();
|
||||
builder.Property(x => x.InvoiceNo).HasMaxLength(32).IsRequired();
|
||||
builder.Property(x => x.ApplicantName).HasMaxLength(64).IsRequired();
|
||||
builder.Property(x => x.CompanyName).HasMaxLength(128).IsRequired();
|
||||
builder.Property(x => x.TaxpayerNumber).HasMaxLength(64);
|
||||
builder.Property(x => x.InvoiceType).HasConversion<int>().IsRequired();
|
||||
builder.Property(x => x.Amount).HasPrecision(18, 2).IsRequired();
|
||||
builder.Property(x => x.OrderNo).HasMaxLength(32).IsRequired();
|
||||
builder.Property(x => x.ContactEmail).HasMaxLength(128);
|
||||
builder.Property(x => x.ContactPhone).HasMaxLength(32);
|
||||
builder.Property(x => x.ApplyRemark).HasMaxLength(256);
|
||||
builder.Property(x => x.Status).HasConversion<int>().IsRequired();
|
||||
builder.Property(x => x.AppliedAt).IsRequired();
|
||||
builder.Property(x => x.IssueRemark).HasMaxLength(256);
|
||||
builder.Property(x => x.VoidReason).HasMaxLength(256);
|
||||
|
||||
builder.HasIndex(x => new { x.TenantId, x.InvoiceNo }).IsUnique();
|
||||
builder.HasIndex(x => new { x.TenantId, x.OrderNo });
|
||||
builder.HasIndex(x => new { x.TenantId, x.Status, x.AppliedAt });
|
||||
builder.HasIndex(x => new { x.TenantId, x.Status, x.IssuedAt });
|
||||
builder.HasIndex(x => new { x.TenantId, x.InvoiceType, x.AppliedAt });
|
||||
}
|
||||
|
||||
private static void ConfigureFinanceCostEntry(EntityTypeBuilder<FinanceCostEntry> builder)
|
||||
{
|
||||
builder.ToTable("finance_cost_entries");
|
||||
|
||||
@@ -171,6 +171,203 @@ public sealed class EfFinanceTransactionRepository(TakeoutAppDbContext context)
|
||||
return rows.Select(MapToRecord).ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceSettlementStatsSnapshot> GetSettlementStatsAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
DateTime currentUtc,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var utcNow = NormalizeUtc(currentUtc);
|
||||
var todayStart = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, 0, 0, 0, DateTimeKind.Utc);
|
||||
var tomorrowStart = todayStart.AddDays(1);
|
||||
var yesterdayStart = todayStart.AddDays(-1);
|
||||
var monthStart = new DateTime(utcNow.Year, utcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
var monthEnd = monthStart.AddMonths(1);
|
||||
|
||||
var query = BuildSettlementPaymentQuery(
|
||||
tenantId,
|
||||
storeId,
|
||||
startAt: null,
|
||||
endAt: null,
|
||||
paymentMethod: null);
|
||||
|
||||
var summary = await query
|
||||
.GroupBy(_ => 1)
|
||||
.Select(group => new
|
||||
{
|
||||
TodayArrivedAmount = group
|
||||
.Where(item => item.PaidAt >= todayStart && item.PaidAt < tomorrowStart)
|
||||
.Sum(item => item.Amount),
|
||||
YesterdayArrivedAmount = group
|
||||
.Where(item => item.PaidAt >= yesterdayStart && item.PaidAt < todayStart)
|
||||
.Sum(item => item.Amount),
|
||||
CurrentMonthArrivedAmount = group
|
||||
.Where(item => item.PaidAt >= monthStart && item.PaidAt < monthEnd)
|
||||
.Sum(item => item.Amount),
|
||||
CurrentMonthTransactionCount = group
|
||||
.Count(item => item.PaidAt >= monthStart && item.PaidAt < monthEnd)
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (summary is null)
|
||||
{
|
||||
return new FinanceSettlementStatsSnapshot
|
||||
{
|
||||
TodayArrivedAmount = 0,
|
||||
YesterdayArrivedAmount = 0,
|
||||
CurrentMonthArrivedAmount = 0,
|
||||
CurrentMonthTransactionCount = 0
|
||||
};
|
||||
}
|
||||
|
||||
return new FinanceSettlementStatsSnapshot
|
||||
{
|
||||
TodayArrivedAmount = decimal.Round(summary.TodayArrivedAmount, 2, MidpointRounding.AwayFromZero),
|
||||
YesterdayArrivedAmount = decimal.Round(summary.YesterdayArrivedAmount, 2, MidpointRounding.AwayFromZero),
|
||||
CurrentMonthArrivedAmount = decimal.Round(summary.CurrentMonthArrivedAmount, 2, MidpointRounding.AwayFromZero),
|
||||
CurrentMonthTransactionCount = summary.CurrentMonthTransactionCount
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceSettlementAccountSnapshot?> GetSettlementAccountAsync(
|
||||
long tenantId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var profile = await context.TenantVerificationProfiles
|
||||
.AsNoTracking()
|
||||
.Where(item => item.TenantId == tenantId && item.DeletedAt == null)
|
||||
.Select(item => new
|
||||
{
|
||||
item.BankName,
|
||||
item.BankAccountName,
|
||||
item.BankAccountNumber,
|
||||
item.WeChatMerchantNo,
|
||||
item.AlipayPid
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (profile is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new FinanceSettlementAccountSnapshot
|
||||
{
|
||||
BankName = (profile.BankName ?? string.Empty).Trim(),
|
||||
BankAccountName = (profile.BankAccountName ?? string.Empty).Trim(),
|
||||
BankAccountNoMasked = MaskBankAccountNo(profile.BankAccountNumber),
|
||||
WechatMerchantNoMasked = MaskWechatMerchantNo(profile.WeChatMerchantNo),
|
||||
AlipayPidMasked = MaskAlipayPid(profile.AlipayPid),
|
||||
SettlementPeriodText = "T+1 自动到账"
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceSettlementPageSnapshot> SearchSettlementPageAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
DateTime? startAt,
|
||||
DateTime? endAt,
|
||||
PaymentMethod? paymentMethod,
|
||||
int page,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizedPage = Math.Max(1, page);
|
||||
var normalizedPageSize = Math.Clamp(pageSize, 1, 200);
|
||||
|
||||
var groupedQuery = BuildSettlementPaymentQuery(tenantId, storeId, startAt, endAt, paymentMethod)
|
||||
.GroupBy(item => new { ArrivedDate = item.PaidAt.Date, item.PaymentMethod })
|
||||
.Select(group => new FinanceSettlementListItemSnapshot
|
||||
{
|
||||
ArrivedDate = DateTime.SpecifyKind(group.Key.ArrivedDate, DateTimeKind.Utc),
|
||||
PaymentMethod = group.Key.PaymentMethod,
|
||||
TransactionCount = group.Count(),
|
||||
ArrivedAmount = decimal.Round(group.Sum(item => item.Amount), 2, MidpointRounding.AwayFromZero)
|
||||
});
|
||||
|
||||
var totalCount = await groupedQuery.CountAsync(cancellationToken);
|
||||
if (totalCount == 0)
|
||||
{
|
||||
return new FinanceSettlementPageSnapshot
|
||||
{
|
||||
Items = [],
|
||||
TotalCount = 0
|
||||
};
|
||||
}
|
||||
|
||||
var items = await groupedQuery
|
||||
.OrderByDescending(item => item.ArrivedDate)
|
||||
.ThenBy(item => item.PaymentMethod)
|
||||
.Skip((normalizedPage - 1) * normalizedPageSize)
|
||||
.Take(normalizedPageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return new FinanceSettlementPageSnapshot
|
||||
{
|
||||
Items = items,
|
||||
TotalCount = totalCount
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<FinanceSettlementDetailItemSnapshot>> GetSettlementDetailsAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
DateTime arrivedDate,
|
||||
PaymentMethod paymentMethod,
|
||||
int take,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var arrivedDay = NormalizeUtc(arrivedDate);
|
||||
var dayStart = new DateTime(arrivedDay.Year, arrivedDay.Month, arrivedDay.Day, 0, 0, 0, DateTimeKind.Utc);
|
||||
var dayEnd = dayStart.AddDays(1);
|
||||
var normalizedTake = Math.Clamp(take, 1, 200);
|
||||
|
||||
return await BuildSettlementPaymentQuery(
|
||||
tenantId,
|
||||
storeId,
|
||||
dayStart,
|
||||
dayEnd,
|
||||
paymentMethod)
|
||||
.OrderByDescending(item => item.PaidAt)
|
||||
.ThenByDescending(item => item.PaymentRecordId)
|
||||
.Select(item => new FinanceSettlementDetailItemSnapshot
|
||||
{
|
||||
OrderNo = item.OrderNo,
|
||||
Amount = decimal.Round(item.Amount, 2, MidpointRounding.AwayFromZero),
|
||||
PaidAt = item.PaidAt
|
||||
})
|
||||
.Take(normalizedTake)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<FinanceSettlementListItemSnapshot>> ListSettlementForExportAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
DateTime? startAt,
|
||||
DateTime? endAt,
|
||||
PaymentMethod? paymentMethod,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await BuildSettlementPaymentQuery(tenantId, storeId, startAt, endAt, paymentMethod)
|
||||
.GroupBy(item => new { ArrivedDate = item.PaidAt.Date, item.PaymentMethod })
|
||||
.Select(group => new FinanceSettlementListItemSnapshot
|
||||
{
|
||||
ArrivedDate = DateTime.SpecifyKind(group.Key.ArrivedDate, DateTimeKind.Utc),
|
||||
PaymentMethod = group.Key.PaymentMethod,
|
||||
TransactionCount = group.Count(),
|
||||
ArrivedAmount = decimal.Round(group.Sum(item => item.Amount), 2, MidpointRounding.AwayFromZero)
|
||||
})
|
||||
.OrderByDescending(item => item.ArrivedDate)
|
||||
.ThenBy(item => item.PaymentMethod)
|
||||
.Take(20_000)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private IQueryable<TransactionProjection> BuildQuery(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
@@ -385,6 +582,50 @@ public sealed class EfFinanceTransactionRepository(TakeoutAppDbContext context)
|
||||
return query;
|
||||
}
|
||||
|
||||
private IQueryable<SettlementPaymentProjection> BuildSettlementPaymentQuery(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
DateTime? startAt,
|
||||
DateTime? endAt,
|
||||
PaymentMethod? paymentMethod)
|
||||
{
|
||||
var query =
|
||||
from payment in context.PaymentRecords.AsNoTracking()
|
||||
join order in context.Orders.AsNoTracking()
|
||||
on payment.OrderId equals order.Id
|
||||
where payment.TenantId == tenantId
|
||||
&& order.TenantId == tenantId
|
||||
&& order.StoreId == storeId
|
||||
&& payment.Status == PaymentStatus.Paid
|
||||
&& payment.PaidAt.HasValue
|
||||
&& (payment.Method == PaymentMethod.WeChatPay || payment.Method == PaymentMethod.Alipay)
|
||||
select new SettlementPaymentProjection
|
||||
{
|
||||
PaymentRecordId = payment.Id,
|
||||
OrderNo = order.OrderNo,
|
||||
PaymentMethod = payment.Method,
|
||||
Amount = payment.Amount,
|
||||
PaidAt = payment.PaidAt!.Value
|
||||
};
|
||||
|
||||
if (startAt.HasValue)
|
||||
{
|
||||
query = query.Where(item => item.PaidAt >= startAt.Value);
|
||||
}
|
||||
|
||||
if (endAt.HasValue)
|
||||
{
|
||||
query = query.Where(item => item.PaidAt < endAt.Value);
|
||||
}
|
||||
|
||||
if (paymentMethod.HasValue)
|
||||
{
|
||||
query = query.Where(item => item.PaymentMethod == paymentMethod.Value);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
private static FinanceTransactionRecord MapToRecord(TransactionProjection source)
|
||||
{
|
||||
return new FinanceTransactionRecord
|
||||
@@ -503,4 +744,60 @@ public sealed class EfFinanceTransactionRepository(TakeoutAppDbContext context)
|
||||
|
||||
public int? PointBalanceAfterChange { get; init; }
|
||||
}
|
||||
|
||||
private sealed class SettlementPaymentProjection
|
||||
{
|
||||
public required long PaymentRecordId { get; init; }
|
||||
|
||||
public required string OrderNo { get; init; }
|
||||
|
||||
public required PaymentMethod PaymentMethod { get; init; }
|
||||
|
||||
public required decimal Amount { get; init; }
|
||||
|
||||
public required DateTime PaidAt { get; init; }
|
||||
}
|
||||
|
||||
private static DateTime NormalizeUtc(DateTime value)
|
||||
{
|
||||
return value.Kind switch
|
||||
{
|
||||
DateTimeKind.Utc => value,
|
||||
DateTimeKind.Local => value.ToUniversalTime(),
|
||||
_ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
|
||||
};
|
||||
}
|
||||
|
||||
private static string MaskBankAccountNo(string? value)
|
||||
{
|
||||
var digits = new string((value ?? string.Empty).Where(char.IsDigit).ToArray());
|
||||
if (digits.Length >= 4)
|
||||
{
|
||||
return $"****{digits[^4..]}";
|
||||
}
|
||||
|
||||
return digits;
|
||||
}
|
||||
|
||||
private static string MaskWechatMerchantNo(string? value)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim();
|
||||
if (normalized.Length >= 4)
|
||||
{
|
||||
return $"{normalized[..2]}{new string('x', normalized.Length - 2)}";
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static string MaskAlipayPid(string? value)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim();
|
||||
if (normalized.Length > 6)
|
||||
{
|
||||
return $"{normalized[..4]}{new string('x', normalized.Length - 4)}";
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Tenants.Entities;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 租户发票仓储 EF Core 实现。
|
||||
/// </summary>
|
||||
public sealed class EfTenantInvoiceRepository(TakeoutAppDbContext context) : ITenantInvoiceRepository
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task<TenantInvoiceSetting?> GetSettingAsync(long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.TenantInvoiceSettings
|
||||
.Where(item => item.TenantId == tenantId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddSettingAsync(TenantInvoiceSetting entity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.TenantInvoiceSettings.AddAsync(entity, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateSettingAsync(TenantInvoiceSetting entity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.TenantInvoiceSettings.Update(entity);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<(IReadOnlyList<TenantInvoiceRecord> Items, int TotalCount)> SearchRecordsAsync(
|
||||
long tenantId,
|
||||
DateTime? startUtc,
|
||||
DateTime? endUtc,
|
||||
TenantInvoiceStatus? status,
|
||||
TenantInvoiceType? invoiceType,
|
||||
string? keyword,
|
||||
int page,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizedPage = Math.Max(1, page);
|
||||
var normalizedPageSize = Math.Clamp(pageSize, 1, 500);
|
||||
|
||||
var query = BuildRecordQuery(tenantId, startUtc, endUtc, status, invoiceType, keyword);
|
||||
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
if (totalCount == 0)
|
||||
{
|
||||
return ([], 0);
|
||||
}
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(item => item.AppliedAt)
|
||||
.ThenByDescending(item => item.Id)
|
||||
.Skip((normalizedPage - 1) * normalizedPageSize)
|
||||
.Take(normalizedPageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return (items, totalCount);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TenantInvoiceRecordStatsSnapshot> GetStatsAsync(
|
||||
long tenantId,
|
||||
DateTime nowUtc,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var utcNow = NormalizeUtc(nowUtc);
|
||||
var monthStart = new DateTime(utcNow.Year, utcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
var summary = await context.TenantInvoiceRecords
|
||||
.AsNoTracking()
|
||||
.Where(item => item.TenantId == tenantId)
|
||||
.GroupBy(_ => 1)
|
||||
.Select(group => new
|
||||
{
|
||||
CurrentMonthIssuedAmount = group
|
||||
.Where(item =>
|
||||
item.Status == TenantInvoiceStatus.Issued &&
|
||||
item.IssuedAt.HasValue &&
|
||||
item.IssuedAt.Value >= monthStart &&
|
||||
item.IssuedAt.Value <= utcNow)
|
||||
.Sum(item => item.Amount),
|
||||
CurrentMonthIssuedCount = group
|
||||
.Count(item =>
|
||||
item.Status == TenantInvoiceStatus.Issued &&
|
||||
item.IssuedAt.HasValue &&
|
||||
item.IssuedAt.Value >= monthStart &&
|
||||
item.IssuedAt.Value <= utcNow),
|
||||
PendingCount = group.Count(item => item.Status == TenantInvoiceStatus.Pending),
|
||||
VoidedCount = group.Count(item => item.Status == TenantInvoiceStatus.Voided)
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (summary is null)
|
||||
{
|
||||
return new TenantInvoiceRecordStatsSnapshot();
|
||||
}
|
||||
|
||||
return new TenantInvoiceRecordStatsSnapshot
|
||||
{
|
||||
CurrentMonthIssuedAmount = summary.CurrentMonthIssuedAmount,
|
||||
CurrentMonthIssuedCount = summary.CurrentMonthIssuedCount,
|
||||
PendingCount = summary.PendingCount,
|
||||
VoidedCount = summary.VoidedCount
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TenantInvoiceRecord?> FindRecordByIdAsync(
|
||||
long tenantId,
|
||||
long recordId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.TenantInvoiceRecords
|
||||
.Where(item => item.TenantId == tenantId && item.Id == recordId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> ExistsInvoiceNoAsync(
|
||||
long tenantId,
|
||||
string invoiceNo,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.TenantInvoiceRecords
|
||||
.AsNoTracking()
|
||||
.AnyAsync(
|
||||
item => item.TenantId == tenantId && item.InvoiceNo == invoiceNo,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddRecordAsync(TenantInvoiceRecord entity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.TenantInvoiceRecords.AddAsync(entity, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateRecordAsync(TenantInvoiceRecord entity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.TenantInvoiceRecords.Update(entity);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private IQueryable<TenantInvoiceRecord> BuildRecordQuery(
|
||||
long tenantId,
|
||||
DateTime? startUtc,
|
||||
DateTime? endUtc,
|
||||
TenantInvoiceStatus? status,
|
||||
TenantInvoiceType? invoiceType,
|
||||
string? keyword)
|
||||
{
|
||||
var query = context.TenantInvoiceRecords
|
||||
.AsNoTracking()
|
||||
.Where(item => item.TenantId == tenantId);
|
||||
|
||||
if (startUtc.HasValue)
|
||||
{
|
||||
var normalizedStart = NormalizeUtc(startUtc.Value);
|
||||
query = query.Where(item => item.AppliedAt >= normalizedStart);
|
||||
}
|
||||
|
||||
if (endUtc.HasValue)
|
||||
{
|
||||
var normalizedEnd = NormalizeUtc(endUtc.Value);
|
||||
query = query.Where(item => item.AppliedAt <= normalizedEnd);
|
||||
}
|
||||
|
||||
if (status.HasValue)
|
||||
{
|
||||
query = query.Where(item => item.Status == status.Value);
|
||||
}
|
||||
|
||||
if (invoiceType.HasValue)
|
||||
{
|
||||
query = query.Where(item => item.InvoiceType == invoiceType.Value);
|
||||
}
|
||||
|
||||
var normalizedKeyword = (keyword ?? string.Empty).Trim();
|
||||
if (!string.IsNullOrWhiteSpace(normalizedKeyword))
|
||||
{
|
||||
var like = $"%{normalizedKeyword}%";
|
||||
query = query.Where(item =>
|
||||
EF.Functions.ILike(item.InvoiceNo, like) ||
|
||||
EF.Functions.ILike(item.CompanyName, like) ||
|
||||
EF.Functions.ILike(item.ApplicantName, like) ||
|
||||
EF.Functions.ILike(item.OrderNo, like));
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
private static DateTime NormalizeUtc(DateTime value)
|
||||
{
|
||||
return value.Kind switch
|
||||
{
|
||||
DateTimeKind.Utc => value,
|
||||
DateTimeKind.Local => value.ToUniversalTime(),
|
||||
_ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
|
||||
};
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTenantVerificationSettlementChannels : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AlipayPid",
|
||||
table: "tenant_verification_profiles",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
comment: "支付宝 PID。");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "WeChatMerchantNo",
|
||||
table: "tenant_verification_profiles",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
comment: "微信商户号。");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AlipayPid",
|
||||
table: "tenant_verification_profiles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "WeChatMerchantNo",
|
||||
table: "tenant_verification_profiles");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Migrations;
|
||||
|
||||
/// <summary>
|
||||
/// 新增财务中心发票管理表结构。
|
||||
/// </summary>
|
||||
[DbContext(typeof(TakeoutAppDbContext))]
|
||||
[Migration("20260305103000_AddFinanceInvoiceModule")]
|
||||
public sealed class AddFinanceInvoiceModule : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "finance_invoice_records",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
InvoiceNo = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false, comment: "发票号码。"),
|
||||
ApplicantName = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false, comment: "申请人。"),
|
||||
CompanyName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false, comment: "开票抬头(公司名)。"),
|
||||
TaxpayerNumber = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true, comment: "纳税人识别号快照。"),
|
||||
InvoiceType = table.Column<int>(type: "integer", nullable: false, comment: "发票类型。"),
|
||||
Amount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "开票金额。"),
|
||||
OrderNo = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false, comment: "关联订单号。"),
|
||||
ContactEmail = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true, comment: "接收邮箱。"),
|
||||
ContactPhone = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true, comment: "联系电话。"),
|
||||
ApplyRemark = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true, comment: "申请备注。"),
|
||||
Status = table.Column<int>(type: "integer", nullable: false, comment: "发票状态。"),
|
||||
AppliedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "申请时间(UTC)。"),
|
||||
IssuedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "开票时间(UTC)。"),
|
||||
IssuedByUserId = table.Column<long>(type: "bigint", nullable: true, comment: "开票人 ID。"),
|
||||
IssueRemark = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true, comment: "开票备注。"),
|
||||
VoidedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "作废时间(UTC)。"),
|
||||
VoidedByUserId = table.Column<long>(type: "bigint", nullable: true, comment: "作废人 ID。"),
|
||||
VoidReason = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true, comment: "作废原因。"),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||
UpdatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_finance_invoice_records", x => x.Id);
|
||||
},
|
||||
comment: "租户发票记录。");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "finance_invoice_settings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
CompanyName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false, comment: "企业名称。"),
|
||||
TaxpayerNumber = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false, comment: "纳税人识别号。"),
|
||||
RegisteredAddress = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true, comment: "注册地址。"),
|
||||
RegisteredPhone = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true, comment: "注册电话。"),
|
||||
BankName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true, comment: "开户银行。"),
|
||||
BankAccount = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true, comment: "银行账号。"),
|
||||
EnableElectronicNormalInvoice = table.Column<bool>(type: "boolean", nullable: false, comment: "是否启用电子普通发票。"),
|
||||
EnableElectronicSpecialInvoice = table.Column<bool>(type: "boolean", nullable: false, comment: "是否启用电子专用发票。"),
|
||||
EnableAutoIssue = table.Column<bool>(type: "boolean", nullable: false, comment: "是否启用自动开票。"),
|
||||
AutoIssueMaxAmount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "自动开票单张最大金额。"),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||
UpdatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_finance_invoice_settings", x => x.Id);
|
||||
},
|
||||
comment: "租户发票开票基础设置。");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_finance_invoice_records_TenantId_InvoiceNo",
|
||||
table: "finance_invoice_records",
|
||||
columns: new[] { "TenantId", "InvoiceNo" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_finance_invoice_records_TenantId_InvoiceType_AppliedAt",
|
||||
table: "finance_invoice_records",
|
||||
columns: new[] { "TenantId", "InvoiceType", "AppliedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_finance_invoice_records_TenantId_OrderNo",
|
||||
table: "finance_invoice_records",
|
||||
columns: new[] { "TenantId", "OrderNo" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_finance_invoice_records_TenantId_Status_AppliedAt",
|
||||
table: "finance_invoice_records",
|
||||
columns: new[] { "TenantId", "Status", "AppliedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_finance_invoice_records_TenantId_Status_IssuedAt",
|
||||
table: "finance_invoice_records",
|
||||
columns: new[] { "TenantId", "Status", "IssuedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_finance_invoice_settings_TenantId",
|
||||
table: "finance_invoice_settings",
|
||||
column: "TenantId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "finance_invoice_records");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "finance_invoice_settings");
|
||||
}
|
||||
}
|
||||
@@ -9711,6 +9711,11 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("text")
|
||||
.HasComment("附加资料(JSON)。");
|
||||
|
||||
b.Property<string>("AlipayPid")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasComment("支付宝 PID。");
|
||||
|
||||
b.Property<string>("BankAccountName")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
@@ -9810,6 +9815,11 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||
|
||||
b.Property<string>("WeChatMerchantNo")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasComment("微信商户号。");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TenantId")
|
||||
|
||||
Reference in New Issue
Block a user