using System.Text; using System.Text.Json; using LittleShop.DTOs; using LittleShop.Models; namespace LittleShop.Services; public interface IRoyalMailService { Task CreateShipmentAsync(CreateShipmentRequest request); Task GetTrackingInfoAsync(string trackingNumber); Task> GetAvailableServicesAsync(decimal weight, string country); Task CalculateShippingCostAsync(decimal weight, string country, string serviceCode); Task GenerateShippingLabelAsync(string shipmentId); Task CancelShipmentAsync(string shipmentId); } public class RoyalMailShippingService : IRoyalMailService { private readonly HttpClient _httpClient; private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly bool _isProduction; private string? _accessToken; private DateTime _tokenExpiry; public RoyalMailShippingService( HttpClient httpClient, IConfiguration configuration, ILogger logger) { _httpClient = httpClient; _configuration = configuration; _logger = logger; _isProduction = !string.IsNullOrEmpty(_configuration["RoyalMail:ClientId"]); if (_isProduction) { _httpClient.BaseAddress = new Uri("https://api.royalmail.net/"); } else { _logger.LogInformation("Royal Mail service running in development mode (API credentials not configured)"); } } public async Task CreateShipmentAsync(CreateShipmentRequest request) { if (!_isProduction) { return CreateMockShipmentResult(request); } try { await EnsureAuthenticatedAsync(); var payload = new { shipmentType = "Delivery", serviceCode = request.ServiceCode ?? "1st Class", recipientName = request.RecipientName, recipientAddress = new { addressLine1 = request.AddressLine1, addressLine2 = request.AddressLine2, city = request.City, postCode = request.PostCode, country = request.Country }, senderAddress = new { addressLine1 = _configuration["RoyalMail:SenderAddress1"] ?? "123 Business St", city = _configuration["RoyalMail:SenderCity"] ?? "London", postCode = _configuration["RoyalMail:SenderPostCode"] ?? "SW1A 1AA", country = "United Kingdom" }, weight = request.WeightInGrams, dimensions = new { length = request.Length ?? 20, width = request.Width ?? 15, height = request.Height ?? 5 }, value = request.Value, currency = "GBP", reference = request.Reference }; var json = JsonSerializer.Serialize(payload); var content = new StringContent(json, Encoding.UTF8, "application/json"); _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken); var response = await _httpClient.PostAsync("shipping/v2/shipments", content); var responseContent = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) { var result = JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); return new CreateShipmentResult { Success = true, ShipmentId = result?.ShipmentId ?? Guid.NewGuid().ToString(), TrackingNumber = result?.TrackingNumber ?? GenerateMockTrackingNumber(), LabelUrl = result?.LabelUrl, EstimatedDeliveryDate = DateTime.UtcNow.AddDays(2), Cost = request.EstimatedCost ?? 5.50m }; } else { _logger.LogError("Royal Mail API error: {StatusCode} - {Response}", response.StatusCode, responseContent); return new CreateShipmentResult { Success = false, ErrorMessage = $"Royal Mail API error: {response.StatusCode}" }; } } catch (Exception ex) { _logger.LogError(ex, "Error creating Royal Mail shipment"); return new CreateShipmentResult { Success = false, ErrorMessage = ex.Message }; } } public async Task GetTrackingInfoAsync(string trackingNumber) { if (!_isProduction) { return CreateMockTrackingResult(trackingNumber); } try { await EnsureAuthenticatedAsync(); _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken); var response = await _httpClient.GetAsync($"tracking/v2/items/{trackingNumber}"); var responseContent = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) { var result = JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); return new TrackingResult { Success = true, TrackingNumber = trackingNumber, Status = result?.Status ?? "In Transit", LastUpdate = result?.LastUpdate ?? DateTime.UtcNow, EstimatedDelivery = result?.EstimatedDelivery, TrackingEvents = result?.Events?.Select(e => new TrackingEvent { Timestamp = e.Timestamp, Location = e.Location, Description = e.Description, Status = e.Status }).ToList() ?? new List() }; } else { return new TrackingResult { Success = false, TrackingNumber = trackingNumber, ErrorMessage = $"Tracking not found: {response.StatusCode}" }; } } catch (Exception ex) { _logger.LogError(ex, "Error getting tracking info for {TrackingNumber}", trackingNumber); return new TrackingResult { Success = false, TrackingNumber = trackingNumber, ErrorMessage = ex.Message }; } } public async Task> GetAvailableServicesAsync(decimal weight, string country) { if (!_isProduction) { return CreateMockServices(weight, country); } try { await EnsureAuthenticatedAsync(); var query = $"services?weight={weight}&country={country}"; _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken); var response = await _httpClient.GetAsync($"shipping/v2/{query}"); var responseContent = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) { var services = JsonSerializer.Deserialize>(responseContent, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); return services?.Select(s => new RoyalMailServiceOption { Code = s.Code, Name = s.Name, Description = s.Description, Price = s.Price, EstimatedDeliveryDays = s.EstimatedDeliveryDays, MaxWeight = s.MaxWeight, SupportsTracking = s.SupportsTracking }).ToList() ?? new List(); } return new List(); } catch (Exception ex) { _logger.LogError(ex, "Error getting Royal Mail services"); return CreateMockServices(weight, country); } } public async Task CalculateShippingCostAsync(decimal weight, string country, string serviceCode) { if (!_isProduction) { return CalculateMockShippingCost(weight, country, serviceCode); } var services = await GetAvailableServicesAsync(weight, country); var service = services.FirstOrDefault(s => s.Code == serviceCode); return service?.Price ?? 0m; } public async Task GenerateShippingLabelAsync(string shipmentId) { if (!_isProduction) { return GenerateMockLabel(shipmentId); } try { await EnsureAuthenticatedAsync(); _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken); var response = await _httpClient.GetAsync($"shipping/v2/shipments/{shipmentId}/label"); if (response.IsSuccessStatusCode) { return await response.Content.ReadAsByteArrayAsync(); } return null; } catch (Exception ex) { _logger.LogError(ex, "Error generating shipping label for {ShipmentId}", shipmentId); return null; } } public async Task CancelShipmentAsync(string shipmentId) { if (!_isProduction) { _logger.LogInformation("Mock: Cancelled shipment {ShipmentId}", shipmentId); return true; } try { await EnsureAuthenticatedAsync(); _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken); var response = await _httpClient.DeleteAsync($"shipping/v2/shipments/{shipmentId}"); return response.IsSuccessStatusCode; } catch (Exception ex) { _logger.LogError(ex, "Error cancelling shipment {ShipmentId}", shipmentId); return false; } } private async Task EnsureAuthenticatedAsync() { if (!string.IsNullOrEmpty(_accessToken) && DateTime.UtcNow < _tokenExpiry) return; var clientId = _configuration["RoyalMail:ClientId"]; var clientSecret = _configuration["RoyalMail:ClientSecret"]; if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(clientSecret)) { throw new InvalidOperationException("Royal Mail API credentials not configured"); } var authPayload = new { grant_type = "client_credentials", client_id = clientId, client_secret = clientSecret }; var json = JsonSerializer.Serialize(authPayload); var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync("oauth2/token", content); var responseContent = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) { var authResult = JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); _accessToken = authResult?.AccessToken; _tokenExpiry = DateTime.UtcNow.AddSeconds(authResult?.ExpiresIn ?? 3600); _logger.LogInformation("Successfully authenticated with Royal Mail API"); } else { _logger.LogError("Failed to authenticate with Royal Mail API: {StatusCode} - {Response}", response.StatusCode, responseContent); throw new InvalidOperationException("Royal Mail API authentication failed"); } } // Mock methods for development private CreateShipmentResult CreateMockShipmentResult(CreateShipmentRequest request) { var trackingNumber = GenerateMockTrackingNumber(); _logger.LogInformation("Mock: Created shipment for {Recipient} to {City}, {Country} - Tracking: {TrackingNumber}", request.RecipientName, request.City, request.Country, trackingNumber); return new CreateShipmentResult { Success = true, ShipmentId = Guid.NewGuid().ToString(), TrackingNumber = trackingNumber, EstimatedDeliveryDate = DateTime.UtcNow.AddDays(GetMockDeliveryDays(request.Country)), Cost = CalculateMockShippingCost(request.WeightInGrams, request.Country, request.ServiceCode ?? "1st Class"), LabelUrl = $"https://mock-royal-mail/labels/{trackingNumber}.pdf" }; } private TrackingResult CreateMockTrackingResult(string trackingNumber) { var statuses = new[] { "Collected", "In Transit", "Out for Delivery", "Delivered" }; var randomStatus = statuses[new Random().Next(statuses.Length)]; return new TrackingResult { Success = true, TrackingNumber = trackingNumber, Status = randomStatus, LastUpdate = DateTime.UtcNow.AddHours(-2), EstimatedDelivery = DateTime.UtcNow.AddDays(1), TrackingEvents = new List { new() { Timestamp = DateTime.UtcNow.AddDays(-1), Location = "London", Description = "Item collected", Status = "Collected" }, new() { Timestamp = DateTime.UtcNow.AddHours(-6), Location = "Sorting Office", Description = "Item processed", Status = "In Transit" }, new() { Timestamp = DateTime.UtcNow.AddHours(-2), Location = "Local Depot", Description = "Item arriving at delivery office", Status = randomStatus } } }; } private List CreateMockServices(decimal weight, string country) { var services = new List(); if (country.Equals("United Kingdom", StringComparison.OrdinalIgnoreCase)) { services.Add(new RoyalMailServiceOption { Code = "1st Class", Name = "First Class", Description = "Next working day delivery", Price = CalculateMockShippingCost(weight, country, "1st Class"), EstimatedDeliveryDays = 1, MaxWeight = 2000, SupportsTracking = true }); services.Add(new RoyalMailServiceOption { Code = "2nd Class", Name = "Second Class", Description = "2-3 working days delivery", Price = CalculateMockShippingCost(weight, country, "2nd Class"), EstimatedDeliveryDays = 3, MaxWeight = 2000, SupportsTracking = false }); services.Add(new RoyalMailServiceOption { Code = "Signed For", Name = "Signed For 1st Class", Description = "Next working day with signature", Price = CalculateMockShippingCost(weight, country, "Signed For"), EstimatedDeliveryDays = 1, MaxWeight = 2000, SupportsTracking = true }); } else { services.Add(new RoyalMailServiceOption { Code = "International Standard", Name = "International Standard", Description = "5-7 working days to Europe", Price = CalculateMockShippingCost(weight, country, "International Standard"), EstimatedDeliveryDays = 7, MaxWeight = 2000, SupportsTracking = true }); services.Add(new RoyalMailServiceOption { Code = "International Tracked", Name = "International Tracked & Signed", Description = "3-5 working days with tracking", Price = CalculateMockShippingCost(weight, country, "International Tracked"), EstimatedDeliveryDays = 5, MaxWeight = 2000, SupportsTracking = true }); } return services; } private decimal CalculateMockShippingCost(decimal weight, string country, string serviceCode) { var baseRate = country.Equals("United Kingdom", StringComparison.OrdinalIgnoreCase) ? 2.50m : 8.50m; var weightRate = weight / 100 * 0.50m; // £0.50 per 100g var serviceMultiplier = serviceCode switch { "1st Class" => 1.2m, "2nd Class" => 1.0m, "Signed For" => 1.5m, "International Standard" => 2.0m, "International Tracked" => 3.0m, _ => 1.0m }; return Math.Round(baseRate + weightRate * serviceMultiplier, 2); } private int GetMockDeliveryDays(string country) { return country.Equals("United Kingdom", StringComparison.OrdinalIgnoreCase) ? 2 : 7; } private string GenerateMockTrackingNumber() { var random = new Random(); return $"RM{random.Next(100000000, 999999999)}GB"; } private byte[] GenerateMockLabel(string shipmentId) { // Generate a simple mock PDF label (in production this would be the actual Royal Mail label) var mockPdfContent = $"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R /Contents 4 0 R >>\nendobj\n4 0 obj\n<< /Length 44 >>\nstream\nBT\n/F1 12 Tf\n100 700 Td\n(Royal Mail Label - {shipmentId}) Tj\nET\nendstream\nendobj\nxref\n0 5\n0000000000 65535 f \n0000000009 00000 n \n0000000058 00000 n \n0000000115 00000 n \n0000000174 00000 n \ntrailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n267\n%%EOF"; return Encoding.UTF8.GetBytes(mockPdfContent); } } // DTOs and response models public class CreateShipmentRequest { public string RecipientName { get; set; } = string.Empty; public string AddressLine1 { get; set; } = string.Empty; public string? AddressLine2 { get; set; } public string City { get; set; } = string.Empty; public string PostCode { get; set; } = string.Empty; public string Country { get; set; } = "United Kingdom"; public decimal WeightInGrams { get; set; } public decimal? Length { get; set; } public decimal? Width { get; set; } public decimal? Height { get; set; } public decimal Value { get; set; } public string? ServiceCode { get; set; } public string? Reference { get; set; } public decimal? EstimatedCost { get; set; } } public class CreateShipmentResult { public bool Success { get; set; } public string? ShipmentId { get; set; } public string? TrackingNumber { get; set; } public string? LabelUrl { get; set; } public DateTime? EstimatedDeliveryDate { get; set; } public decimal Cost { get; set; } public string? ErrorMessage { get; set; } } public class TrackingResult { public bool Success { get; set; } public string TrackingNumber { get; set; } = string.Empty; public string Status { get; set; } = string.Empty; public DateTime LastUpdate { get; set; } public DateTime? EstimatedDelivery { get; set; } public List TrackingEvents { get; set; } = new(); public string? ErrorMessage { get; set; } } public class TrackingEvent { public DateTime Timestamp { get; set; } public string Location { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public string Status { get; set; } = string.Empty; } public class RoyalMailServiceOption { public string Code { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public decimal Price { get; set; } public int EstimatedDeliveryDays { get; set; } public decimal MaxWeight { get; set; } public bool SupportsTracking { get; set; } } // API Response models public class RoyalMailTokenResponse { public string AccessToken { get; set; } = string.Empty; public int ExpiresIn { get; set; } public string TokenType { get; set; } = string.Empty; } public class RoyalMailShipmentResponse { public string ShipmentId { get; set; } = string.Empty; public string TrackingNumber { get; set; } = string.Empty; public string? LabelUrl { get; set; } public DateTime EstimatedDelivery { get; set; } } public class RoyalMailTrackingResponse { public string Status { get; set; } = string.Empty; public DateTime LastUpdate { get; set; } public DateTime? EstimatedDelivery { get; set; } public List Events { get; set; } = new(); } public class RoyalMailTrackingEvent { public DateTime Timestamp { get; set; } public string Location { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public string Status { get; set; } = string.Empty; } public class RoyalMailShippingServiceResponse { public string Code { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public decimal Price { get; set; } public int EstimatedDeliveryDays { get; set; } public decimal MaxWeight { get; set; } public bool SupportsTracking { get; set; } }