mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 16:33:12 +00:00
* refactor(api): enforce service and data-service boundaries * refactor(api): complete feature service boundaries * refactor(identity): enforce service and data boundaries * refactor(vendors): enforce service and data boundaries * refactor(workorders): enforce service and data boundaries * refactor(backend): enforce architecture and optimize dispatch * style(backend): format changed architecture files * fix(architecture): address backend review follow-ups * fix(backend): sanitize exception disclosure in changed API endpoints Replace raw exception-message disclosure (ex.Message) returned to API callers with a stable sanitized public message plus correlated structured internal logging, across the endpoints changed in this PR. - Add SanitizedErrors helper: logs the original exception at Error with a generated correlation id and returns a stable public message referencing it so support can trace without exposing internals. - Inject ILogger<T> into the 14 changed controllers and route every ex.Message/dbex.Message disclosure through the helper, preserving status codes, response shapes, and business data (e.g. OpenWorkOrders). - Leave FluentValidation (vex.Errors) and existing fixed-message catches untouched; out-of-scope controllers (Account/Contact/Employee/Asset/ PMSchedule) are unchanged. - Add focused tests proving internal exception text is not returned and that Error logging carrying the original exception is invoked. * fix(architecture): abstract job run state access * style: format board update service * test: use collection assertion idiom
191 lines
7.1 KiB
C#
191 lines
7.1 KiB
C#
using Data.SeaHavenIndustries;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
using SeaHaven.Services.DTOs;
|
|
using SeaHaven.Services.Interfaces;
|
|
|
|
namespace SeaHaven.Services.Implementation
|
|
{
|
|
public class QuotesService : IQuotesService
|
|
{
|
|
private readonly IQuotesDataService _quotesData;
|
|
private readonly IWorkOrderDataService _workOrderData;
|
|
|
|
public QuotesService(IQuotesDataService quotesData, IWorkOrderDataService workOrderData)
|
|
{
|
|
_quotesData = quotesData;
|
|
_workOrderData = workOrderData;
|
|
}
|
|
|
|
public async Task CreateQuoteAsync(CreateQuoteInput input, CancellationToken cancellationToken)
|
|
{
|
|
var rnd = new Random();
|
|
var id = rnd.Next(1, 99999);
|
|
|
|
var quote = new Quotes
|
|
{
|
|
QuotId = id.ToString(),
|
|
ContactId = input.ContactId,
|
|
PO = input.PO,
|
|
TT = input.TT,
|
|
ProjectName = input.ProjectName,
|
|
Date = input.Date,
|
|
WorkorderId = input.WorkorderId,
|
|
SiteName = input.SiteName,
|
|
SiteId = input.SiteId,
|
|
BillingAddress = input.BillingAddress,
|
|
LineItems = new List<QuotesLineItems>()
|
|
};
|
|
|
|
if (input.LineItems != null && input.LineItems.Any())
|
|
{
|
|
foreach (var li in input.LineItems)
|
|
{
|
|
quote.LineItems.Add(new QuotesLineItems
|
|
{
|
|
LineitemNumber = li.LineitemNumber,
|
|
Lineitem = li.Lineitem,
|
|
Descriptions = li.Descriptions
|
|
});
|
|
}
|
|
}
|
|
|
|
await _quotesData.AddAsync(quote);
|
|
}
|
|
|
|
public async Task<bool> UpdateQuoteAsync(UpdateQuoteInput input, CancellationToken cancellationToken)
|
|
{
|
|
var quote = await _quotesData.GetByIdAsync(input.Id);
|
|
if (quote == null)
|
|
return false;
|
|
|
|
quote.QuotId = input.QuotId;
|
|
quote.ContactId = input.ContactId;
|
|
quote.PO = input.PO;
|
|
quote.TT = input.TT;
|
|
quote.ProjectName = input.ProjectName;
|
|
quote.Date = input.Date;
|
|
quote.WorkorderId = input.WorkorderId;
|
|
quote.SiteName = input.SiteName;
|
|
quote.SiteId = input.SiteId;
|
|
quote.BillingAddress = input.BillingAddress;
|
|
|
|
await _quotesData.DeleteLineItemsByQuoteIdAsync(input.Id, cancellationToken);
|
|
|
|
quote.LineItems ??= new List<QuotesLineItems>();
|
|
|
|
if (input.LineItems != null && input.LineItems.Any())
|
|
{
|
|
foreach (var li in input.LineItems)
|
|
{
|
|
quote.LineItems.Add(new QuotesLineItems
|
|
{
|
|
LineitemNumber = li.LineitemNumber,
|
|
Lineitem = li.Lineitem,
|
|
Descriptions = li.Descriptions
|
|
});
|
|
}
|
|
}
|
|
|
|
await _quotesData.UpdateAsync(quote);
|
|
return true;
|
|
}
|
|
|
|
public async Task<bool> DeleteQuoteAsync(int id, CancellationToken cancellationToken)
|
|
{
|
|
var quote = await _quotesData.GetByIdAsync(id);
|
|
if (quote == null)
|
|
return false;
|
|
|
|
await _quotesData.DeleteLineItemsByQuoteIdAsync(id, cancellationToken);
|
|
await _quotesData.DeleteAsync(id);
|
|
return true;
|
|
}
|
|
|
|
public async Task<PagedQuotesResult> GetQuotesPagedAsync(int page, int pageSize, string search, CancellationToken cancellationToken)
|
|
{
|
|
var (items, totalCount) = await _quotesData.GetQuotesPagedAsync(page, pageSize, search, cancellationToken);
|
|
|
|
return new PagedQuotesResult
|
|
{
|
|
Items = items.Select(item => new QuoteListItemDTO
|
|
{
|
|
Id = item.Id,
|
|
QuoteId = item.QuoteId,
|
|
WorkOrder = item.WorkOrder,
|
|
ProjectName = item.ProjectName,
|
|
TotalAmount = FormatAmount(item.TotalAmount),
|
|
SiteName = item.SiteName,
|
|
SiteId = item.SiteId,
|
|
PO = item.PO,
|
|
TT = item.TT,
|
|
BillingAddress = item.BillingAddress,
|
|
Status = item.Status,
|
|
ContactId = item.ContactId,
|
|
Date = item.Date,
|
|
Lineitems = item.Lineitems.Select(MapLineItem).ToList()
|
|
}).ToList(),
|
|
TotalCount = totalCount
|
|
};
|
|
}
|
|
|
|
public async Task<IReadOnlyList<WorkOrderQuoteDetailDTO>> GetWorkOrderQuoteDetailsAsync(
|
|
int woId, CancellationToken cancellationToken)
|
|
{
|
|
var items = await _quotesData.GetWorkOrderQuoteDetailsAsync(woId, cancellationToken);
|
|
return items.Select(item => new WorkOrderQuoteDetailDTO
|
|
{
|
|
Id = item.Id,
|
|
PO = item.PO,
|
|
TT = item.TT,
|
|
Title = item.Title,
|
|
Name = item.Name
|
|
}).ToList();
|
|
}
|
|
|
|
public async Task<QuoteDetailDTO?> GetQuoteDetailsByIdAsync(
|
|
int id, CancellationToken cancellationToken)
|
|
{
|
|
var item = await _quotesData.GetQuoteDetailsByIdAsync(id, cancellationToken);
|
|
return item == null ? null : new QuoteDetailDTO
|
|
{
|
|
Id = item.Id,
|
|
QuotId = item.QuotId,
|
|
Date = item.Date,
|
|
Name = item.Name,
|
|
PO = item.PO,
|
|
TT = item.TT,
|
|
SiteName = item.SiteName,
|
|
ProjectName = item.ProjectName,
|
|
SiteId = item.SiteId,
|
|
BillingAddress = item.BillingAddress,
|
|
ShippingAddress = item.ShippingAddress,
|
|
TotalAmount = FormatAmount(item.TotalAmount),
|
|
Lineitems = item.Lineitems.Select(MapLineItem).ToList()
|
|
};
|
|
}
|
|
|
|
public async Task<QuoteReportingData> GetReportingAsync(CancellationToken cancellationToken)
|
|
{
|
|
return new QuoteReportingData
|
|
{
|
|
TotalCreatedWorkOrder = await _workOrderData.CountAsync(),
|
|
TotalCompleted = await _workOrderData.CountByStatusAsync("Done"),
|
|
TotalSev1 = await _workOrderData.CountByPriorityAsync("Sev 1"),
|
|
TotalSev2 = await _workOrderData.CountByPriorityAsync("Sev 2"),
|
|
TotalSev3 = await _workOrderData.CountByPriorityAsync("Sev 3"),
|
|
TotalSev4 = await _workOrderData.CountByPriorityAsync("Sev 4"),
|
|
TotalSev5 = await _workOrderData.CountByPriorityAsync("Sev 5")
|
|
};
|
|
}
|
|
|
|
private static QuoteLineItemDTO MapLineItem(
|
|
SeaHaven.DataServices.Models.QuoteLineItemReadModel item) => new()
|
|
{
|
|
Descriptions = item.Descriptions,
|
|
Lineitem = item.Lineitem,
|
|
LineitemNumber = item.LineitemNumber
|
|
};
|
|
|
|
private static string FormatAmount(decimal amount) => $"${amount}";
|
|
}
|
|
}
|