using GB5Shared.DTO.Framework.Login; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using GB5Shared.Validation; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using PartnerDAL.CustomCode.ClientDetail; using PartnerDAL.CustomCode.PartnerProduct; using PartnerDAL.DTO.ClientDetail; using System.ComponentModel.DataAnnotations; namespace PartnerBLL.ClientDetail { // Joins the two halves of the assignment on purpose: MCLIENTDETAILS.PARTNERPRODUCTID // (GB5System, via IClientDetailDAL) is only ever set to a value that IPartnerProductDAL // confirms actually exists in the CALLER'S OWN tenant DB (via LoginDTO) — the same DB // GetPublicBrandByConnection's Hop 2 will read from. Prevents assigning a PartnerProductId // that doesn't exist in this client's own ProductionDB, which would otherwise silently // resolve to nothing (or, if IDs collide across tenants, someone else's product). public class ClientDetailBLL : IClientDetailBLL { private const int UNASSIGNED = -1; private readonly IClientDetailDAL _clientDetailDAL; private readonly IPartnerProductDAL _partnerProductDAL; private readonly ILogger _logger; public ClientDetailBLL(IClientDetailDAL clientDetailDAL, IPartnerProductDAL partnerProductDAL, ILogger logger) { _clientDetailDAL = clientDetailDAL; _partnerProductDAL = partnerProductDAL; _logger = logger; } public async Task> GetClientDetailList(int clientId, LoginDTO loginDTO) { try { return await _clientDetailDAL.GetClientDetailList(clientId).ConfigureAwait(false); } catch (Exception) { throw; } } // Fills the gap confirmed by a full-repo search: no core-framework endpoint creates // MCLIENTDETAILS rows anywhere. CLIENTID/CLIENTSITEID themselves must already exist // (e.g. from OnboardClient's MSERVERCONFIG provisioning) — this only creates the // white-labeling-relevant row so AssignPartnerProduct has something to update. public async Task CreateClientDetail(int clientId, int clientSiteId) { try { if (clientId == 0) throw new ValidationException("ClientId is required."); if (clientSiteId == 0) throw new ValidationException("ClientSiteId is required."); GB5Trace.Step("create-client-detail", new { clientId, clientSiteId }); int rows = await _clientDetailDAL.CreateClientDetail(clientId, clientSiteId).ConfigureAwait(false); return rows > 0 ? SuccessResponse.SaveSuccessMessage : $"A client detail row for ClientId {clientId}, ClientSiteId {clientSiteId} already exists."; } catch (ValidationException vex) { throw new Exception(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("create-client-detail-failed", ex); _logger.LogError(ex, "CreateClientDetail failed for ClientId {ClientId} ClientSiteId {ClientSiteId}", clientId, clientSiteId); throw; } } // Registers a new ConnectionName against an already-existing physical database // (e.g. ConnectionName "Sankar" pointing at DatabaseName "DEMO", alongside other // ConnectionNames already using that same database) — the opposite case from core // provisioning's OnboardClient, which creates a brand-new database per client and // gives the caller no control over ConnectionName/DatabaseName at all. No schema // change to MSERVERCONFIG; every value here is caller-supplied and validated. public async Task RegisterConnection(RegisterConnectionDTO dto, LoginDTO loginDTO) { if (dto == null) throw new ArgumentNullException(nameof(dto)); try { GB5Trace.Step("validate-register-connection", new { dto.ConnectionName, dto.DatabaseName }); if (string.IsNullOrWhiteSpace(dto.ConnectionName)) throw new ValidationException("ConnectionName is required."); if (string.IsNullOrWhiteSpace(dto.DatabaseName)) throw new ValidationException("DatabaseName is required."); if (dto.ClientId == 0) throw new ValidationException("ClientId is required."); if (dto.ClientSiteId == 0) throw new ValidationException("ClientSiteId is required."); dto.ConnectionName = dto.ConnectionName.Trim(); dto.DatabaseName = dto.DatabaseName.Trim(); GB5Trace.Step("register-connection", new { dto.ConnectionName, dto.DatabaseName, dto.ClientId, dto.ClientSiteId }); int newServerConfigId = await _clientDetailDAL.RegisterConnection(dto, loginDTO.UserId).ConfigureAwait(false); return $"{SuccessResponse.SaveSuccessMessage} {newServerConfigId}"; } catch (ValidationException vex) { throw new Exception(vex.Message); } catch (InvalidOperationException ioex) { throw new Exception(ioex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("register-connection-failed", ex); _logger.LogError(ex, "RegisterConnection failed for ConnectionName {ConnectionName} DatabaseName {DatabaseName}", dto.ConnectionName, dto.DatabaseName); throw; } } public async Task AssignPartnerProduct(ClientDetailDTO dto, LoginDTO loginDTO) { if (dto == null) throw new ArgumentNullException(nameof(dto)); try { GB5Trace.Step("validate-assign-partner-product", new { dto.ClientId, dto.ClientSiteId, dto.PartnerProductId }); if (dto.ClientId == 0) throw new ValidationException("ClientId is required."); if (dto.ClientSiteId == 0) throw new ValidationException("ClientSiteId is required."); var existing = await _clientDetailDAL.GetClientDetailBySite(dto.ClientId, dto.ClientSiteId).ConfigureAwait(false); if (existing == null) throw new ValidationException( $"No client site found for ClientId {dto.ClientId}, ClientSiteId {dto.ClientSiteId}."); if (dto.PartnerProductId != UNASSIGNED) { // Confirm the PartnerProductId actually exists in THIS caller's own tenant // DB before writing the assignment centrally — LoginDTO here is the same // authenticated session's tenant connection, i.e. the same ProductionDB // GetPublicBrandByConnection's Hop 2 will read from for this client. var productJson = await _partnerProductDAL.GetPartnerProduct(dto.PartnerProductId, loginDTO).ConfigureAwait(false); var product = string.IsNullOrWhiteSpace(productJson) ? null : JsonConvert.DeserializeObject>(productJson); if (product == null || product.Count == 0) throw new ValidationException( $"PartnerProductId {dto.PartnerProductId} was not found in this client's own database."); } GB5Trace.Step("assign-partner-product", new { dto.ClientId, dto.ClientSiteId, dto.PartnerProductId }); int rows = await _clientDetailDAL.AssignPartnerProduct(dto.ClientId, dto.ClientSiteId, dto.PartnerProductId).ConfigureAwait(false); return rows > 0 ? SuccessResponse.UpdateSuccessMessage : ErrorResponse.UpdateErrorMessage; } catch (ValidationException vex) { throw new Exception(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("assign-partner-product-failed", ex); _logger.LogError(ex, "AssignPartnerProduct failed for ClientId {ClientId} ClientSiteId {ClientSiteId}", dto.ClientId, dto.ClientSiteId); throw; } } } }