using System.Text.Json; using Microsoft.EntityFrameworkCore; using Tiku.Application.Commerce; using Tiku.Domain.Commerce; namespace Tiku.Infrastructure.Commerce; internal sealed partial class ReconciliationAdministrationService(CommerceAdministrationDependencies dependencies) : CommerceAdministrationServiceBase(dependencies), IReconciliationAdministrationService { public async Task GetReconciliationBatchesAsync( CommerceAdminActor actor, CommerceAdminQuery query, CancellationToken cancellationToken = default) { await AssertAdminAsync(actor, cancellationToken); var batches = commercePersistence.CommerceReconciliationBatches.AsNoTracking() .Where(item => item.TenantId == actor.TenantId); if (!string.IsNullOrWhiteSpace(query.Provider)) { var provider = NormalizeProvider(query.Provider); batches = batches.Where(item => item.Provider == provider); } if (!string.IsNullOrWhiteSpace(query.Status)) batches = batches.Where(item => item.Status == ParseReconciliationBatchStatus(query.Status)); var items = await batches.OrderByDescending(item => item.CreatedAt) .Take(Math.Clamp(query.Limit ?? 50, 1, 200)) .ToArrayAsync(cancellationToken); return new TenantReconciliationBatchList(items); } public async Task CreateReconciliationBatchAsync( CommerceAdminActor actor, CreateReconciliationBatchCommand command, CancellationToken cancellationToken = default) { await AssertAdminAsync(actor, cancellationToken); var batch = new CommerceReconciliationBatch { TenantId = actor.TenantId, CreatedBy = actor.UserId, Provider = NormalizeProvider(command.Provider), BillDate = command.BillDate, BillType = command.BillType, Source = command.Source, SourceName = command.SourceName?.Trim(), SourceHash = command.SourceHash.Trim(), Status = ReconciliationBatchStatus.Pending, Metadata = JsonObjectOrDefault(command.Metadata) }; commercePersistence.CommerceReconciliationBatches.Add(batch); await AddAuditAsync(actor, "commerce.reconciliation_batch.created", "commerce_reconciliation_batches", batch.Id, new { batch.Provider, batch.BillDate }, cancellationToken); await unitOfWork.SaveChangesAsync(cancellationToken); return batch; } public async Task GetReconciliationIssuesAsync( CommerceAdminActor actor, CommerceAdminQuery query, CancellationToken cancellationToken = default) { await AssertAdminAsync(actor, cancellationToken); var issues = commercePersistence.CommerceReconciliationIssues.AsNoTracking() .Where(item => item.TenantId == actor.TenantId); if (!string.IsNullOrWhiteSpace(query.Provider)) { var provider = NormalizeProvider(query.Provider); issues = issues.Where(item => item.Provider == provider); } if (!string.IsNullOrWhiteSpace(query.Status)) issues = issues.Where(item => item.Status == ParseReconciliationIssueStatus(query.Status)); var items = await issues.OrderByDescending(item => item.CreatedAt) .Take(Math.Clamp(query.Limit ?? 50, 1, 200)) .ToArrayAsync(cancellationToken); return new TenantReconciliationIssueList(items); } public async Task UpdateReconciliationIssueAsync( CommerceAdminActor actor, UpdateReconciliationIssueCommand command, CancellationToken cancellationToken = default) { await AssertAdminAsync(actor, cancellationToken); var issue = await commercePersistence.CommerceReconciliationIssues.SingleOrDefaultAsync( item => item.TenantId == actor.TenantId && item.Id == command.IssueId, cancellationToken) ?? throw new CommerceException("Reconciliation issue was not found.", "reconciliation_issue_not_found"); var fromStatus = issue.Status; issue.Status = command.Status; issue.ResolutionType = command.ResolutionType; issue.ResolutionNote = command.Note?.Trim(); issue.AssignedTo = command.AssignedTo ?? issue.AssignedTo; if (command.Status is ReconciliationIssueStatus.Resolved or ReconciliationIssueStatus.Ignored) { issue.ResolvedBy = actor.UserId; issue.ResolvedAt = DateTimeOffset.UtcNow; } commercePersistence.CommerceReconciliationIssueEvents.Add(new CommerceReconciliationIssueEvent { TenantId = actor.TenantId, IssueId = issue.Id, FromStatus = fromStatus, ToStatus = command.Status, EventType = "status_changed", ActorUserId = actor.UserId, Note = command.Note, Details = JsonSerializer.SerializeToElement(new { command.ResolutionType, command.AssignedTo }) }); await AddAuditAsync(actor, "commerce.reconciliation_issue.status_changed", "commerce_reconciliation_issues", issue.Id, new { issue.IssueNo, From = fromStatus, To = command.Status }, cancellationToken); await unitOfWork.SaveChangesAsync(cancellationToken); return issue; } }