Implement Backend API Core (Phase 1)
Complete API layer with Clean Architecture:
- Application DTOs (proposals, line items, customers, users, files, audit, dashboard)
- Service interfaces (IProposalService, ILineItemService, ICustomerService, IAuditService, IS3Service, IJobPublisher)
- FluentValidation validators for all create requests
- Infrastructure service implementations (ProposalService, LineItemService, CustomerService, AuditService, S3Service, SqsJobPublisher, ProposalNumberGenerator)
- Secrets Manager connection string resolver for RDS
- API controllers: Proposals (CRUD + approve/send/revise), LineItems (CRUD + bulk), Customers, Users, Files (presigned upload/download), Admin (dashboard)
- Middleware: GlobalExceptionHandler (ProblemDetails), ValidationFilter (FluentValidation pipeline)
- CurrentUserService (Cognito JWT claims -> User entity, auto-provisioning)
- Full DI configuration in Program.cs with Lambda hosting
- Role-based authorization (dispatchers, admins, sysadmins)
- CORS configured for proposals.seahaven.com + localhost
2026-05-16 18:49:48 -04:00
|
|
|
using System.Text.Json;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using ProposalSystem.Application.DTOs;
|
|
|
|
|
using ProposalSystem.Application.Interfaces;
|
|
|
|
|
using ProposalSystem.Domain.Entities;
|
|
|
|
|
using ProposalSystem.Infrastructure.Data;
|
|
|
|
|
|
|
|
|
|
namespace ProposalSystem.Infrastructure.Services;
|
|
|
|
|
|
|
|
|
|
public class CustomerService : ICustomerService
|
|
|
|
|
{
|
|
|
|
|
private readonly ProposalDbContext _db;
|
|
|
|
|
|
|
|
|
|
public CustomerService(ProposalDbContext db)
|
|
|
|
|
{
|
|
|
|
|
_db = db;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<IReadOnlyList<CustomerResponse>> SearchAsync(string? query, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
var q = _db.Customers.AsQueryable();
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(query))
|
|
|
|
|
{
|
|
|
|
|
var search = query.ToLower();
|
|
|
|
|
q = q.Where(c => c.Name.ToLower().Contains(search));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var customers = await q
|
|
|
|
|
.OrderBy(c => c.Name)
|
|
|
|
|
.Take(50)
|
|
|
|
|
.ToListAsync(ct);
|
|
|
|
|
|
2026-06-18 12:40:55 -04:00
|
|
|
return customers.Select(MapToResponse).ToList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<PagedResponse<CustomerResponse>> ListAsync(int page, int pageSize, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
page = Math.Max(1, page);
|
|
|
|
|
pageSize = Math.Clamp(pageSize, 1, 100);
|
|
|
|
|
|
|
|
|
|
var query = _db.Customers.AsNoTracking().AsQueryable();
|
|
|
|
|
|
|
|
|
|
var totalCount = await query.CountAsync(ct);
|
|
|
|
|
|
|
|
|
|
var items = await query
|
|
|
|
|
.OrderBy(c => c.Name)
|
|
|
|
|
.Skip((page - 1) * pageSize)
|
|
|
|
|
.Take(pageSize)
|
|
|
|
|
.ToListAsync(ct);
|
|
|
|
|
|
|
|
|
|
return new PagedResponse<CustomerResponse>(
|
|
|
|
|
items.Select(MapToResponse).ToList(),
|
|
|
|
|
totalCount,
|
|
|
|
|
page,
|
|
|
|
|
pageSize
|
|
|
|
|
);
|
Implement Backend API Core (Phase 1)
Complete API layer with Clean Architecture:
- Application DTOs (proposals, line items, customers, users, files, audit, dashboard)
- Service interfaces (IProposalService, ILineItemService, ICustomerService, IAuditService, IS3Service, IJobPublisher)
- FluentValidation validators for all create requests
- Infrastructure service implementations (ProposalService, LineItemService, CustomerService, AuditService, S3Service, SqsJobPublisher, ProposalNumberGenerator)
- Secrets Manager connection string resolver for RDS
- API controllers: Proposals (CRUD + approve/send/revise), LineItems (CRUD + bulk), Customers, Users, Files (presigned upload/download), Admin (dashboard)
- Middleware: GlobalExceptionHandler (ProblemDetails), ValidationFilter (FluentValidation pipeline)
- CurrentUserService (Cognito JWT claims -> User entity, auto-provisioning)
- Full DI configuration in Program.cs with Lambda hosting
- Role-based authorization (dispatchers, admins, sysadmins)
- CORS configured for proposals.seahaven.com + localhost
2026-05-16 18:49:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<CustomerResponse> CreateAsync(CreateCustomerRequest request, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
|
var customer = new Customer
|
|
|
|
|
{
|
|
|
|
|
Id = Guid.NewGuid(),
|
|
|
|
|
Name = request.Name,
|
|
|
|
|
Addresses = request.Addresses != null ? JsonSerializer.Serialize(request.Addresses) : null,
|
2026-06-18 12:40:55 -04:00
|
|
|
ContactEmail = request.ContactEmail,
|
Implement Backend API Core (Phase 1)
Complete API layer with Clean Architecture:
- Application DTOs (proposals, line items, customers, users, files, audit, dashboard)
- Service interfaces (IProposalService, ILineItemService, ICustomerService, IAuditService, IS3Service, IJobPublisher)
- FluentValidation validators for all create requests
- Infrastructure service implementations (ProposalService, LineItemService, CustomerService, AuditService, S3Service, SqsJobPublisher, ProposalNumberGenerator)
- Secrets Manager connection string resolver for RDS
- API controllers: Proposals (CRUD + approve/send/revise), LineItems (CRUD + bulk), Customers, Users, Files (presigned upload/download), Admin (dashboard)
- Middleware: GlobalExceptionHandler (ProblemDetails), ValidationFilter (FluentValidation pipeline)
- CurrentUserService (Cognito JWT claims -> User entity, auto-provisioning)
- Full DI configuration in Program.cs with Lambda hosting
- Role-based authorization (dispatchers, admins, sysadmins)
- CORS configured for proposals.seahaven.com + localhost
2026-05-16 18:49:48 -04:00
|
|
|
CreatedAt = now,
|
|
|
|
|
UpdatedAt = now,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_db.Customers.Add(customer);
|
|
|
|
|
await _db.SaveChangesAsync(ct);
|
|
|
|
|
|
2026-06-18 12:40:55 -04:00
|
|
|
return MapToResponse(customer);
|
Implement Backend API Core (Phase 1)
Complete API layer with Clean Architecture:
- Application DTOs (proposals, line items, customers, users, files, audit, dashboard)
- Service interfaces (IProposalService, ILineItemService, ICustomerService, IAuditService, IS3Service, IJobPublisher)
- FluentValidation validators for all create requests
- Infrastructure service implementations (ProposalService, LineItemService, CustomerService, AuditService, S3Service, SqsJobPublisher, ProposalNumberGenerator)
- Secrets Manager connection string resolver for RDS
- API controllers: Proposals (CRUD + approve/send/revise), LineItems (CRUD + bulk), Customers, Users, Files (presigned upload/download), Admin (dashboard)
- Middleware: GlobalExceptionHandler (ProblemDetails), ValidationFilter (FluentValidation pipeline)
- CurrentUserService (Cognito JWT claims -> User entity, auto-provisioning)
- Full DI configuration in Program.cs with Lambda hosting
- Role-based authorization (dispatchers, admins, sysadmins)
- CORS configured for proposals.seahaven.com + localhost
2026-05-16 18:49:48 -04:00
|
|
|
}
|
|
|
|
|
|
2026-06-18 12:40:55 -04:00
|
|
|
public async Task<CustomerResponse?> UpdateAsync(Guid id, UpdateCustomerRequest request, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
var customer = await _db.Customers.FindAsync(new object[] { id }, ct);
|
|
|
|
|
if (customer == null) return null;
|
|
|
|
|
|
|
|
|
|
if (request.Name != null)
|
|
|
|
|
customer.Name = request.Name;
|
|
|
|
|
|
|
|
|
|
if (request.Addresses != null)
|
|
|
|
|
customer.Addresses = JsonSerializer.Serialize(request.Addresses);
|
|
|
|
|
|
|
|
|
|
// ContactEmail is nullable — allow setting it to empty string to clear, or a value to set.
|
|
|
|
|
// Since UpdateCustomerRequest uses string?, we check if the property was explicitly provided.
|
|
|
|
|
// With records and JSON deserialization, null means "not provided" vs empty string means "clear".
|
|
|
|
|
if (request.ContactEmail != null)
|
|
|
|
|
customer.ContactEmail = string.IsNullOrEmpty(request.ContactEmail) ? null : request.ContactEmail;
|
|
|
|
|
|
|
|
|
|
customer.UpdatedAt = DateTime.UtcNow;
|
|
|
|
|
await _db.SaveChangesAsync(ct);
|
|
|
|
|
|
|
|
|
|
return MapToResponse(customer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static CustomerResponse MapToResponse(Customer c) => new(
|
|
|
|
|
c.Id,
|
|
|
|
|
c.Name,
|
|
|
|
|
DeserializeAddresses(c.Addresses),
|
|
|
|
|
c.ContactEmail,
|
|
|
|
|
c.CreatedAt
|
|
|
|
|
);
|
|
|
|
|
|
Implement Backend API Core (Phase 1)
Complete API layer with Clean Architecture:
- Application DTOs (proposals, line items, customers, users, files, audit, dashboard)
- Service interfaces (IProposalService, ILineItemService, ICustomerService, IAuditService, IS3Service, IJobPublisher)
- FluentValidation validators for all create requests
- Infrastructure service implementations (ProposalService, LineItemService, CustomerService, AuditService, S3Service, SqsJobPublisher, ProposalNumberGenerator)
- Secrets Manager connection string resolver for RDS
- API controllers: Proposals (CRUD + approve/send/revise), LineItems (CRUD + bulk), Customers, Users, Files (presigned upload/download), Admin (dashboard)
- Middleware: GlobalExceptionHandler (ProblemDetails), ValidationFilter (FluentValidation pipeline)
- CurrentUserService (Cognito JWT claims -> User entity, auto-provisioning)
- Full DI configuration in Program.cs with Lambda hosting
- Role-based authorization (dispatchers, admins, sysadmins)
- CORS configured for proposals.seahaven.com + localhost
2026-05-16 18:49:48 -04:00
|
|
|
private static List<string> DeserializeAddresses(string? json)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(json)) return new List<string>();
|
|
|
|
|
return JsonSerializer.Deserialize<List<string>>(json) ?? new List<string>();
|
|
|
|
|
}
|
|
|
|
|
}
|