using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using System.Globalization;
using TakeoutSaaS.Domain.Common.Enums;
using TakeoutSaaS.Domain.Merchants.Entities;
using TakeoutSaaS.Domain.Merchants.Enums;
using TakeoutSaaS.Domain.Merchants.Services;
using TakeoutSaaS.Domain.Stores.Entities;
using TakeoutSaaS.Domain.Stores.Enums;
namespace TakeoutSaaS.Infrastructure.App.Services;
///
/// 商户导出服务实现(PDF)。
///
public sealed class MerchantExportService : IMerchantExportService
{
public MerchantExportService()
{
QuestPDF.Settings.License = LicenseType.Community;
}
///
public Task ExportToPdfAsync(
Merchant merchant,
string? tenantName,
IReadOnlyList stores,
IReadOnlyList auditLogs,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(merchant);
var safeStores = stores ?? Array.Empty();
var safeAuditLogs = auditLogs ?? Array.Empty();
var document = Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(24);
page.DefaultTextStyle(x => x.FontSize(10));
page.Content().Column(column =>
{
column.Spacing(10);
column.Item().Text("Merchant Export").FontSize(16).SemiBold();
column.Item().Element(section => BuildBasicSection(section, merchant, tenantName));
column.Item().Element(section => BuildStoresSection(section, safeStores, cancellationToken));
column.Item().Element(section => BuildAuditSection(section, safeAuditLogs, cancellationToken));
});
});
});
return Task.FromResult(document.GeneratePdf());
}
private static void BuildBasicSection(IContainer container, Merchant merchant, string? tenantName)
{
container.Border(1).BorderColor(Colors.Grey.Lighten2).Padding(10).Column(column =>
{
column.Spacing(4);
column.Item().Text("Basic Information").SemiBold();
column.Item().Text($"Merchant: {merchant.BrandName}");
column.Item().Text($"Tenant: {tenantName ?? "-"} (ID: {merchant.TenantId})");
column.Item().Text($"Operating Mode: {ResolveOperatingMode(merchant.OperatingMode)}");
column.Item().Text($"Status: {merchant.Status}");
column.Item().Text($"Frozen: {(merchant.IsFrozen ? "Yes" : "No")}");
column.Item().Text($"License Number: {merchant.BusinessLicenseNumber ?? "-"}");
column.Item().Text($"Legal Representative: {merchant.LegalPerson ?? "-"}");
column.Item().Text($"Registered Address: {merchant.Address ?? "-"}");
column.Item().Text($"Contact Phone: {merchant.ContactPhone}");
column.Item().Text($"Contact Email: {merchant.ContactEmail ?? "-"}");
column.Item().Text($"Approved At: {FormatDateTime(merchant.ApprovedAt)}");
column.Item().Text($"Approved By: {merchant.ApprovedBy?.ToString() ?? "-"}");
});
}
private static void BuildStoresSection(IContainer container, IReadOnlyList stores, CancellationToken cancellationToken)
{
container.Border(1).BorderColor(Colors.Grey.Lighten2).Padding(10).Column(column =>
{
column.Spacing(4);
column.Item().Text("Stores").SemiBold();
if (stores.Count == 0)
{
column.Item().Text("No stores.");
return;
}
for (var i = 0; i < stores.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var store = stores[i];
column.Item().Text($"{i + 1}. {store.Name} | {ResolveStoreStatus(store.Status)} | {store.Address ?? "-"} | {store.Phone ?? "-"}");
}
});
}
private static void BuildAuditSection(IContainer container, IReadOnlyList auditLogs, CancellationToken cancellationToken)
{
container.Border(1).BorderColor(Colors.Grey.Lighten2).Padding(10).Column(column =>
{
column.Spacing(4);
column.Item().Text("Audit History").SemiBold();
if (auditLogs.Count == 0)
{
column.Item().Text("No audit records.");
return;
}
for (var i = 0; i < auditLogs.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var log = auditLogs[i];
var title = string.IsNullOrWhiteSpace(log.Title) ? log.Action.ToString() : log.Title;
column.Item().Text($"{i + 1}. {title} | {log.OperatorName ?? "-"} | {FormatDateTime(log.CreatedAt)}");
if (!string.IsNullOrWhiteSpace(log.Description))
{
column.Item().Text($" {log.Description}");
}
}
});
}
private static string ResolveOperatingMode(OperatingMode? mode)
=> mode switch
{
OperatingMode.SameEntity => "SameEntity",
OperatingMode.DifferentEntity => "DifferentEntity",
_ => "-"
};
private static string ResolveStoreStatus(StoreStatus status)
=> status switch
{
StoreStatus.Closed => "Closed",
StoreStatus.Preparing => "Preparing",
StoreStatus.Operating => "Operating",
StoreStatus.Suspended => "Suspended",
_ => status.ToString()
};
private static string FormatDateTime(DateTime? value)
=> value.HasValue ? value.Value.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture) : "-";
}