feat(finance): 完成发票管理模块后端实现 #7
@@ -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,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,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,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;
|
||||||
|
}
|
||||||
@@ -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<ITenantQuotaUsageRepository, EfTenantQuotaUsageRepository>();
|
||||||
services.AddScoped<ITenantQuotaUsageHistoryRepository, EfTenantQuotaUsageHistoryRepository>();
|
services.AddScoped<ITenantQuotaUsageHistoryRepository, EfTenantQuotaUsageHistoryRepository>();
|
||||||
services.AddScoped<ITenantVisibilityRoleRuleRepository, TenantVisibilityRoleRuleRepository>();
|
services.AddScoped<ITenantVisibilityRoleRuleRepository, TenantVisibilityRoleRuleRepository>();
|
||||||
|
services.AddScoped<ITenantInvoiceRepository, EfTenantInvoiceRepository>();
|
||||||
services.AddScoped<IInventoryRepository, EfInventoryRepository>();
|
services.AddScoped<IInventoryRepository, EfInventoryRepository>();
|
||||||
services.AddScoped<IQuotaPackageRepository, EfQuotaPackageRepository>();
|
services.AddScoped<IQuotaPackageRepository, EfQuotaPackageRepository>();
|
||||||
services.AddScoped<IStatisticsRepository, EfStatisticsRepository>();
|
services.AddScoped<IStatisticsRepository, EfStatisticsRepository>();
|
||||||
|
|||||||
@@ -95,6 +95,14 @@ public sealed class TakeoutAppDbContext(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<TenantVisibilityRoleRule> TenantVisibilityRoleRules => Set<TenantVisibilityRoleRule>();
|
public DbSet<TenantVisibilityRoleRule> TenantVisibilityRoleRules => Set<TenantVisibilityRoleRule>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// 租户发票设置。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<TenantInvoiceSetting> TenantInvoiceSettings => Set<TenantInvoiceSetting>();
|
||||||
|
/// <summary>
|
||||||
|
/// 租户发票记录。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<TenantInvoiceRecord> TenantInvoiceRecords => Set<TenantInvoiceRecord>();
|
||||||
|
/// <summary>
|
||||||
/// 成本录入汇总。
|
/// 成本录入汇总。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<FinanceCostEntry> FinanceCostEntries => Set<FinanceCostEntry>();
|
public DbSet<FinanceCostEntry> FinanceCostEntries => Set<FinanceCostEntry>();
|
||||||
@@ -534,6 +542,8 @@ public sealed class TakeoutAppDbContext(
|
|||||||
ConfigureTenantAnnouncementRead(modelBuilder.Entity<TenantAnnouncementRead>());
|
ConfigureTenantAnnouncementRead(modelBuilder.Entity<TenantAnnouncementRead>());
|
||||||
ConfigureTenantVerificationProfile(modelBuilder.Entity<TenantVerificationProfile>());
|
ConfigureTenantVerificationProfile(modelBuilder.Entity<TenantVerificationProfile>());
|
||||||
ConfigureTenantVisibilityRoleRule(modelBuilder.Entity<TenantVisibilityRoleRule>());
|
ConfigureTenantVisibilityRoleRule(modelBuilder.Entity<TenantVisibilityRoleRule>());
|
||||||
|
ConfigureTenantInvoiceSetting(modelBuilder.Entity<TenantInvoiceSetting>());
|
||||||
|
ConfigureTenantInvoiceRecord(modelBuilder.Entity<TenantInvoiceRecord>());
|
||||||
ConfigureFinanceCostEntry(modelBuilder.Entity<FinanceCostEntry>());
|
ConfigureFinanceCostEntry(modelBuilder.Entity<FinanceCostEntry>());
|
||||||
ConfigureFinanceCostEntryItem(modelBuilder.Entity<FinanceCostEntryItem>());
|
ConfigureFinanceCostEntryItem(modelBuilder.Entity<FinanceCostEntryItem>());
|
||||||
ConfigureQuotaPackage(modelBuilder.Entity<QuotaPackage>());
|
ConfigureQuotaPackage(modelBuilder.Entity<QuotaPackage>());
|
||||||
@@ -1053,6 +1063,52 @@ public sealed class TakeoutAppDbContext(
|
|||||||
builder.HasIndex(x => x.TenantId).IsUnique();
|
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)
|
private static void ConfigureFinanceCostEntry(EntityTypeBuilder<FinanceCostEntry> builder)
|
||||||
{
|
{
|
||||||
builder.ToTable("finance_cost_entries");
|
builder.ToTable("finance_cost_entries");
|
||||||
|
|||||||
@@ -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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user