shoc-backend/SeaHaven.Services/Implementation/QuotesService.cs
Alexandre Brandizzi 7d245eb717
refactor: enforce backend boundaries and optimize dispatch (#30)
* 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
2026-07-24 17:35:34 -03:00

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}";
}
}