using FrameworkDAL.CustomCode.Port; using FrameworkDAL.CustomCode.ZIP; using FrameworkDAL.DTO.ZIP; using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using GB5Shared.Validation; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using System.Text; using System.Text.Json; using System.Threading.Tasks; namespace FrameworkBLL.ZIP { public class ZIPBLL : IZIPBLL { private readonly IZIPDAL _ZIPDAL; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; public ZIPBLL(IZIPDAL ZIPDAL, AutoNumber AutoNumber, IValidation Validation) { _ZIPDAL = ZIPDAL; _AutoNumber = AutoNumber; _Validation = Validation; } public async Task GetSelectListZIP(CriteriaDTO criteriaDTO, LoginDTO loginDTO) { try { return await _ZIPDAL.GetSelectListZIP(criteriaDTO, loginDTO); } catch (Exception) { throw; } } public async Task GetZIP(int ZIPId, LoginDTO LoginDTO) { try { return await _ZIPDAL.GetZIP(ZIPId, LoginDTO); } catch (Exception) { throw; } } public async Task SaveZIP(ZIPDTO dto, LoginDTO loginDTO) { if (dto == null) throw new ArgumentNullException(nameof(dto)); AutoNumberDTO? autoNumberDTO = null; try { // Validations await _Validation.NotEmpty(dto.ZIPCode, nameof(dto.ZIPCode)); await _Validation.NotEmpty(dto.ZIPName, nameof(dto.ZIPName)); // Convert raw "lat,lng" input into valid WKT before it hits the DAL/SQL dto.ZIPGeolocation = ToWktPoint(dto.ZIPGeolocation); dto.ZIPBorder = ToWktPolygon(dto.ZIPBorder); dto.ZIPModifiedById = loginDTO.UserId; dto.ZIPModifiedOn = DateTime.UtcNow; if (dto.ZIPId == 0) { // Auto Number Generate autoNumberDTO = await _AutoNumber.GetAutoNumber(1, "ZIP", loginDTO); dto.ZIPId = autoNumberDTO.StartNumber; dto.ZIPCreatedById = loginDTO.UserId; dto.ZIPCreatedOn = DateTime.UtcNow; await _ZIPDAL.SaveZIP(dto, loginDTO); return $"{SuccessResponse.SaveSuccessMessage} {dto.ZIPId}"; } else { await _ZIPDAL.UpdateZIP(dto, loginDTO); return $"{SuccessResponse.UpdateSuccessMessage} {dto.ZIPId}"; } } catch (ValidationException vex) { throw new Exception(vex.Message); } catch (Exception ex) { if (autoNumberDTO != null && dto.ZIPId > 0) await _AutoNumber.RollbackAutoNumber("ZIP", dto.ZIPId, loginDTO); throw new Exception($"Failed: {ex.Message}", ex); } } /// /// Converts a raw "lat,lng" string (e.g. "13.0827,80.2707") into a valid /// SQL Server geography WKT point string (e.g. "POINT(80.2707 13.0827)"). /// WKT points are ordered X Y => longitude latitude. /// private string ToWktPoint(string rawGeolocation) { if (string.IsNullOrWhiteSpace(rawGeolocation)) return null; // nothing to convert; treat as optional // Already looks like WKT (e.g. previously converted or passed correctly) — leave as-is if (rawGeolocation.TrimStart().StartsWith("POINT", StringComparison.OrdinalIgnoreCase)) return rawGeolocation; var parts = rawGeolocation.Split(','); if (parts.Length != 2 || !double.TryParse(parts[0].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double lat) || !double.TryParse(parts[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double lng)) { throw new ValidationException("ZIPGeolocation must be in 'lat,lng' format, e.g. '13.0827,80.2707'."); } if (lat < -90 || lat > 90 || lng < -180 || lng > 180) throw new ValidationException("ZIPGeolocation has out-of-range latitude/longitude values."); return $"POINT({lng.ToString(CultureInfo.InvariantCulture)} {lat.ToString(CultureInfo.InvariantCulture)})"; } /// /// Converts a GeoJSON Polygon string into a valid SQL Server geography WKT POLYGON string. /// Expects GeoJSON coordinates in [lng, lat] order (per GeoJSON spec), which WKT also uses. /// private string ToWktPolygon(string rawGeoJson) { if (string.IsNullOrWhiteSpace(rawGeoJson)) return null; // optional field // Already WKT — pass through if (rawGeoJson.TrimStart().StartsWith("POLYGON", StringComparison.OrdinalIgnoreCase)) return rawGeoJson; using var doc = JsonDocument.Parse(rawGeoJson); var root = doc.RootElement; if (!root.TryGetProperty("type", out var typeProp) || !string.Equals(typeProp.GetString(), "Polygon", StringComparison.OrdinalIgnoreCase)) { throw new ValidationException("ZIPBorder must be a GeoJSON Polygon or a valid WKT POLYGON string."); } var coordinates = root.GetProperty("coordinates"); // array of rings var rings = new List(); foreach (var ring in coordinates.EnumerateArray()) { var points = new List(); foreach (var point in ring.EnumerateArray()) { double lng = point[0].GetDouble(); double lat = point[1].GetDouble(); points.Add($"{lng.ToString(CultureInfo.InvariantCulture)} {lat.ToString(CultureInfo.InvariantCulture)}"); } // WKT polygon rings must be closed (first point == last point) if (points.Count > 0 && points[0] != points[^1]) points.Add(points[0]); if (points.Count < 4) throw new ValidationException("ZIPBorder polygon ring must have at least 4 points (closed ring)."); rings.Add($"({string.Join(", ", points)})"); } if (rings.Count == 0) throw new ValidationException("ZIPBorder polygon must contain at least one ring."); return $"POLYGON({string.Join(", ", rings)})"; } public async Task DeleteZIP(int ZIPId, LoginDTO LoginDTO) { try { return await _ZIPDAL.DeleteZIP(ZIPId, LoginDTO); } catch (Exception) { throw; } } } }