diff --git a/sql/clear-down-ports.sql b/sql/clear-down-ports.sql new file mode 100644 index 0000000..93c20bf --- /dev/null +++ b/sql/clear-down-ports.sql @@ -0,0 +1,5 @@ +DELETE FROM VOYAGE_EVENT; +DELETE FROM PORT; + +DELETE FROM SQLITE_SEQUENCE WHERE NAME='VOYAGE_EVENT'; +DELETE FROM SQLITE_SEQUENCE WHERE NAME='PORT'; diff --git a/sql/prune-ports.sql b/sql/prune-ports.sql new file mode 100644 index 0000000..fe95300 --- /dev/null +++ b/sql/prune-ports.sql @@ -0,0 +1,6 @@ +DELETE FROM PORT +WHERE Code NOT IN ( + SELECT DISTINCT( p.Code ) + FROM PORT p + INNER JOIN VOYAGE_EVENT ve ON ve.Port_Id = p.Id +); diff --git a/src/ShippingRecorder.Api/Controllers/ExportController.cs b/src/ShippingRecorder.Api/Controllers/ExportController.cs index a251593..47ca90a 100644 --- a/src/ShippingRecorder.Api/Controllers/ExportController.cs +++ b/src/ShippingRecorder.Api/Controllers/ExportController.cs @@ -18,6 +18,7 @@ public class ExportController : Controller private readonly IBackgroundQueue _vesselTypeQueue; private readonly IBackgroundQueue _voyageQueue; private readonly IBackgroundQueue _sightingQueue; + private readonly IBackgroundQueue _portQueue; public ExportController( IBackgroundQueue countryQueue, @@ -26,7 +27,8 @@ public ExportController( IBackgroundQueue vesselQueue, IBackgroundQueue vesselTypeQueue, IBackgroundQueue voyageQueue, - IBackgroundQueue sightingQueue) + IBackgroundQueue sightingQueue, + IBackgroundQueue portQueue) { _countryQueue = countryQueue; _locationQueue = locationQueue; @@ -35,6 +37,7 @@ public ExportController( _vesselTypeQueue = vesselTypeQueue; _voyageQueue = voyageQueue; _sightingQueue = sightingQueue; + _portQueue = portQueue; } [HttpPost] @@ -99,5 +102,14 @@ public IActionResult ExportVoyages([FromBody] VoyageExportWorkItem item) _voyageQueue.Enqueue(item); return Accepted(); } + + [HttpPost] + [Route("ports")] + public IActionResult ExportPorts([FromBody] PortExportWorkItem item) + { + item.JobName = "Port Export"; + _portQueue.Enqueue(item); + return Accepted(); + } } } diff --git a/src/ShippingRecorder.Api/Entities/PortExportWorkItem.cs b/src/ShippingRecorder.Api/Entities/PortExportWorkItem.cs new file mode 100644 index 0000000..79931bf --- /dev/null +++ b/src/ShippingRecorder.Api/Entities/PortExportWorkItem.cs @@ -0,0 +1,8 @@ +using ShippingRecorder.Entities.Jobs; + +namespace ShippingRecorder.Api.Entities +{ + public class PortExportWorkItem : ExportWorkItem + { + } +} diff --git a/src/ShippingRecorder.Api/Program.cs b/src/ShippingRecorder.Api/Program.cs index 0cbf2e5..fa03a09 100644 --- a/src/ShippingRecorder.Api/Program.cs +++ b/src/ShippingRecorder.Api/Program.cs @@ -122,6 +122,10 @@ public static void Main(string[] args) builder.Services.AddSingleton, BackgroundQueue>(); builder.Services.AddHostedService(); + // Add the port exporter hosted service + builder.Services.AddSingleton, BackgroundQueue>(); + builder.Services.AddHostedService(); + // Add the voyage importer hosted service builder.Services.AddSingleton, BackgroundQueue>(); builder.Services.AddHostedService(); diff --git a/src/ShippingRecorder.Api/Services/PortExportService.cs b/src/ShippingRecorder.Api/Services/PortExportService.cs new file mode 100644 index 0000000..7317553 --- /dev/null +++ b/src/ShippingRecorder.Api/Services/PortExportService.cs @@ -0,0 +1,46 @@ +using ShippingRecorder.Api.Entities; +using ShippingRecorder.Api.Interfaces; +using ShippingRecorder.DataExchange.Export; +using Microsoft.Extensions.Options; +using ShippingRecorder.Entities.Interfaces; +using ShippingRecorder.Entities.Config; + +namespace ShippingRecorder.Api.Services +{ + public class PortExportService : BackgroundQueueProcessor + { + private readonly ShippingRecorderApplicationSettings _settings; + + public PortExportService( + ILogger> logger, + IBackgroundQueue queue, + IServiceScopeFactory serviceScopeFactory, + IOptions settings) + : base(logger, queue, serviceScopeFactory) + { + _settings = settings.Value; + } + + /// + /// Export the data + /// + /// + /// + /// + protected override async Task ProcessWorkItemAsync(PortExportWorkItem item, IShippingRecorderFactory factory) + { + // Get the list of items to export + MessageLogger.LogInformation("Retrieving ports for export"); + var ports = await factory.Ports.ListAsync(x => true, 1, int.MaxValue).ToListAsync(); + + // Get the full path to the export file + var filePath = Path.Combine(_settings.ExportPath, item.FileName); + + // Export the items + MessageLogger.LogInformation($"Exporting {ports.Count} ports to {filePath}"); + var exporter = new PortExporter(factory); + await exporter.ExportAsync(ports, filePath); + MessageLogger.LogInformation("Port export completed"); + } + } +} \ No newline at end of file diff --git a/src/ShippingRecorder.BusinessLogic/Config/ManagerCommandLineParser.cs b/src/ShippingRecorder.BusinessLogic/Config/ManagerCommandLineParser.cs index 5d59fd2..df32a04 100644 --- a/src/ShippingRecorder.BusinessLogic/Config/ManagerCommandLineParser.cs +++ b/src/ShippingRecorder.BusinessLogic/Config/ManagerCommandLineParser.cs @@ -12,17 +12,19 @@ public ManagerCommandLineParser(IHelpGenerator generator) : base(generator) Add(CommandLineOptionType.ImportCountries, false, "--import-countries", "-ic", "Import countries from a CSV file", 1, 1); Add(CommandLineOptionType.ImportLocations, false, "--import-locations", "-il", "Import locations from a CSV file", 1, 1); Add(CommandLineOptionType.ImportOperators, false, "--import-operators", "-io", "Import operators from a CSV file", 1, 1); - Add(CommandLineOptionType.ImportVesselTypes, false, "--import-vessel-types", "-ivt", "Import vessel types from a CSV file", 1, 1); + Add(CommandLineOptionType.ImportPorts, false, "--import-ports", "-ip", "Import ports from a CSV file", 1, 1); + Add(CommandLineOptionType.ImportSightings, false, "--import-sightings", "-is", "Import sightings from a CSV file", 1, 1); Add(CommandLineOptionType.ImportVessels, false, "--import-vessels", "-iv", "Import vessels from a CSV file", 1, 1); + Add(CommandLineOptionType.ImportVesselTypes, false, "--import-vessel-types", "-ivt", "Import vessel types from a CSV file", 1, 1); Add(CommandLineOptionType.ImportVoyages, false, "--import-voyages", "-ivo", "Import voyages from a CSV file", 1, 1); - Add(CommandLineOptionType.ImportSightings, false, "--import-sightings", "-is", "Import sightings from a CSV file", 1, 1); Add(CommandLineOptionType.ExportCountries, false, "--export-countries", "-ec", "Export countries to a CSV file", 1, 1); Add(CommandLineOptionType.ExportLocations, false, "--export-locations", "-el", "Export locations to a CSV file", 1, 1); Add(CommandLineOptionType.ExportOperators, false, "--export-operators", "-eo", "Export operators to a CSV file", 1, 1); - Add(CommandLineOptionType.ExportVesselTypes, false, "--export-vessel-typess", "-evt", "Export vessel types to a CSV file", 1, 1); + Add(CommandLineOptionType.ExportPorts, false, "--export-ports", "-ep", "Export ports to a CSV file", 1, 1); + Add(CommandLineOptionType.ExportSightings, false, "--export-sightings", "-es", "Export sightings to a CSV file", 1, 1); Add(CommandLineOptionType.ExportVessels, false, "--export-vessels", "-ev", "Export vessels to a CSV file", 1, 1); + Add(CommandLineOptionType.ExportVesselTypes, false, "--export-vessel-typess", "-evt", "Export vessel types to a CSV file", 1, 1); Add(CommandLineOptionType.ExportVoyages, false, "--export-voyages", "-evo", "Export voyages to a CSV file", 1, 1); - Add(CommandLineOptionType.ExportSightings, false, "--export-sightings", "-es", "Export sightings to a CSV file", 1, 1); Add(CommandLineOptionType.AddUser, false, "--add-user", "-au", "Add a user to the database", 2, 2); Add(CommandLineOptionType.SetPassword, false, "--set-password", "-sp", "Set the password for an existing user", 2, 2); Add(CommandLineOptionType.DeleteUser, false, "--delete-user", "-du", "Delete an existing user", 1, 1); diff --git a/src/ShippingRecorder.Client/ApiClient/PortClient.cs b/src/ShippingRecorder.Client/ApiClient/PortClient.cs index 10652c5..aa0ba4d 100644 --- a/src/ShippingRecorder.Client/ApiClient/PortClient.cs +++ b/src/ShippingRecorder.Client/ApiClient/PortClient.cs @@ -14,6 +14,7 @@ public class PortClient : ShippingRecorderClientBase, IPortClient { private const string RouteKey = "Port"; private const string ImportRouteKey = "ImportPort"; + private const string ExportRouteKey = "ExportPort"; public PortClient( IShippingRecorderHttpClient client, @@ -36,8 +37,6 @@ public async Task GetAsync(string code) string baseRoute = @$"{Settings.ApiRoutes.First(r => r.Name == RouteKey).Route}"; var route = $"{baseRoute}/unlocode/{code}"; string json = await SendDirectAsync(route, null, HttpMethod.Get); - - // TODO: Port port = Deserialize(json); return port; } @@ -142,5 +141,18 @@ public async Task ImportFromFileContentAsync(string content) /// public async Task ImportFromFileAsync(string filePath) => await ImportFromFileContentAsync(File.ReadAllText(filePath)); + + /// + /// Request an export of ports to a named file in the export port + /// + /// + /// + /// + public async Task ExportAsync(string fileName) + { + dynamic data = new{ FileName = fileName }; + var json = Serialize(data); + await SendIndirectAsync(ExportRouteKey, json, HttpMethod.Post); + } } } diff --git a/src/ShippingRecorder.Client/Interfaces/IPortClient.cs b/src/ShippingRecorder.Client/Interfaces/IPortClient.cs index 5e5efb0..bd8ec5a 100644 --- a/src/ShippingRecorder.Client/Interfaces/IPortClient.cs +++ b/src/ShippingRecorder.Client/Interfaces/IPortClient.cs @@ -5,7 +5,7 @@ namespace ShippingRecorder.Client.Interfaces { - public interface IPortClient : IPortsRetriever, IImporter + public interface IPortClient : IPortsRetriever, IImporter, IExporter { Task AddAsync(long countryId, string code, string name); Task DeleteAsync(long id); diff --git a/src/ShippingRecorder.DataExchange/Entities/ExportablePort.cs b/src/ShippingRecorder.DataExchange/Entities/ExportablePort.cs index 76d24a3..9422b61 100644 --- a/src/ShippingRecorder.DataExchange/Entities/ExportablePort.cs +++ b/src/ShippingRecorder.DataExchange/Entities/ExportablePort.cs @@ -9,6 +9,7 @@ namespace ShippingRecorder.DataExchange.Entities public class ExportablePort : ExportableEntityBase { /// + /// Country Code /// UN/LOCODE /// Port Name /// diff --git a/src/ShippingRecorder.DataExchange/Export/PortExporter.cs b/src/ShippingRecorder.DataExchange/Export/PortExporter.cs new file mode 100644 index 0000000..b753c22 --- /dev/null +++ b/src/ShippingRecorder.DataExchange/Export/PortExporter.cs @@ -0,0 +1,62 @@ +using ShippingRecorder.DataExchange.Entities; +using ShippingRecorder.DataExchange.Interfaces; +using ShippingRecorder.DataExchange.Extensions; +using ShippingRecorder.Entities.Interfaces; +using System; +using System.Threading.Tasks; +using System.Linq; +using System.Collections.Generic; +using ShippingRecorder.Entities.Db; + +namespace ShippingRecorder.DataExchange.Export +{ + public class PortExporter : IPortExporter + { + private readonly IShippingRecorderFactory _factory; + + public event EventHandler> RecordExport; + + public PortExporter(IShippingRecorderFactory factory) + => _factory = factory; + + /// + /// Export the ports to a CSV file + /// + /// + public async Task ExportAsync(string file) + { + var ports = await _factory.Ports.ListAsync(x => true, 1, int.MaxValue).ToListAsync(); + await ExportAsync(ports, file); + } + + /// + /// Export a collection of ports to a CSV file + /// + /// + /// +#pragma warning disable CS1998 + public async Task ExportAsync(IEnumerable ports, string file) + { + // Convert the ports to exportable (flattened hierarchy) ports + var exportable = ports.ToExportable(); + + // Configure an exporter to export them + var exporter = new CsvExporter(ExportableEntityBase.DateFormat); + exporter.RecordExport += OnRecordExported; + + // Export the records + exporter.Export(exportable, file, ','); + } +#pragma warning restore CS1998 + + /// + /// Handler for port export notifications + /// + /// + /// + private void OnRecordExported(object _, ExportEventArgs e) + { + RecordExport?.Invoke(this, e); + } + } +} diff --git a/src/ShippingRecorder.DataExchange/Extensions/CountryExtensions.cs b/src/ShippingRecorder.DataExchange/Extensions/CountryExtensions.cs index 2964ece..4bd19b0 100644 --- a/src/ShippingRecorder.DataExchange/Extensions/CountryExtensions.cs +++ b/src/ShippingRecorder.DataExchange/Extensions/CountryExtensions.cs @@ -19,15 +19,15 @@ public static ExportableCountry ToExportable(this Country country) }; /// - /// Return a collection of exportable countrys from a collection of countrys + /// Return a collection of exportable countries from a collection of countries /// - /// + /// /// - public static IEnumerable ToExportable(this IEnumerable countrys) + public static IEnumerable ToExportable(this IEnumerable countries) { var exportable = new List(); - foreach (var country in countrys) + foreach (var country in countries) { exportable.Add(country.ToExportable()); } diff --git a/src/ShippingRecorder.DataExchange/Extensions/PortExtensions.cs b/src/ShippingRecorder.DataExchange/Extensions/PortExtensions.cs new file mode 100644 index 0000000..7b7cbc8 --- /dev/null +++ b/src/ShippingRecorder.DataExchange/Extensions/PortExtensions.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using ShippingRecorder.DataExchange.Entities; +using ShippingRecorder.Entities.Db; + +namespace ShippingRecorder.DataExchange.Extensions +{ + public static class PortExtensions + { + /// + /// Return an exportable port from a port + /// + /// + /// + public static ExportablePort ToExportable(this Port port) + => new() + { + Code = port.Code, + Name = port.Name + }; + + /// + /// Return a collection of exportable ports from a collection of ports + /// + /// + /// + public static IEnumerable ToExportable(this IEnumerable ports) + { + var exportable = new List(); + + foreach (var port in ports) + { + exportable.Add(port.ToExportable()); + } + + return exportable; + } + } +} diff --git a/src/ShippingRecorder.DataExchange/Interfaces/IPortExporter.cs b/src/ShippingRecorder.DataExchange/Interfaces/IPortExporter.cs new file mode 100644 index 0000000..abb20f3 --- /dev/null +++ b/src/ShippingRecorder.DataExchange/Interfaces/IPortExporter.cs @@ -0,0 +1,9 @@ +using ShippingRecorder.DataExchange.Entities; +using ShippingRecorder.Entities.Db; + +namespace ShippingRecorder.DataExchange.Interfaces +{ + public interface IPortExporter : IExporter + { + } +} \ No newline at end of file diff --git a/src/ShippingRecorder.Entities/Config/CommandLineOptionType.cs b/src/ShippingRecorder.Entities/Config/CommandLineOptionType.cs index 656cc37..ed91798 100644 --- a/src/ShippingRecorder.Entities/Config/CommandLineOptionType.cs +++ b/src/ShippingRecorder.Entities/Config/CommandLineOptionType.cs @@ -7,6 +7,7 @@ public enum CommandLineOptionType ExportCountries, ExportLocations, ExportOperators, + ExportPorts, ExportSightings, ExportVessels, ExportVesselTypes, diff --git a/src/ShippingRecorder.Manager/Logic/ExportHandler.cs b/src/ShippingRecorder.Manager/Logic/ExportHandler.cs index 7f94dcc..542dab4 100644 --- a/src/ShippingRecorder.Manager/Logic/ExportHandler.cs +++ b/src/ShippingRecorder.Manager/Logic/ExportHandler.cs @@ -86,11 +86,11 @@ public async Task HandleOperatorExportAsync() => await HandleExport(CommandLineOptionType.ExportOperators); /// - /// Handle the vessel type export command + /// Handle the port export command /// /// - public async Task HandleVesselTypeExportAsync() - => await HandleExport(CommandLineOptionType.ExportVesselTypes); + public async Task HandlePortExportAsync() + => await HandleExport(CommandLineOptionType.ExportPorts); /// /// Handle the sightings export command @@ -106,6 +106,13 @@ public async Task HandleSightingExportAsync() public async Task HandleVesselExportAsync() => await HandleExport(CommandLineOptionType.ExportVessels); + /// + /// Handle the vessel type export command + /// + /// + public async Task HandleVesselTypeExportAsync() + => await HandleExport(CommandLineOptionType.ExportVesselTypes); + /// /// Handle the voyage export command /// diff --git a/src/ShippingRecorder.Manager/Logic/ImportHandler.cs b/src/ShippingRecorder.Manager/Logic/ImportHandler.cs index b2be288..b3e12ed 100644 --- a/src/ShippingRecorder.Manager/Logic/ImportHandler.cs +++ b/src/ShippingRecorder.Manager/Logic/ImportHandler.cs @@ -91,11 +91,11 @@ public async Task HandlePortImportAsync() => await HandleImport(CommandLineOptionType.ImportPorts, ExportablePort.CsvRecordPattern); /// - /// Handle the vessel type import command + /// Handle the sightings import command /// /// - public async Task HandleVesselTypeImportAsync() - => await HandleImport(CommandLineOptionType.ImportVesselTypes, ExportableVesselType.CsvRecordPattern); + public async Task HandleSightingImportAsync() + => await HandleImport(CommandLineOptionType.ImportSightings, ExportableSighting.CsvRecordPattern); /// /// Handle the vessels import command @@ -105,17 +105,17 @@ public async Task HandleVesselImportAsync() => await HandleImport(CommandLineOptionType.ImportVessels, ExportableVessel.CsvRecordPattern); /// - /// Handle the voyage import command + /// Handle the vessel type import command /// /// - public async Task HandleVoyageImportAsync() - => await HandleImport(CommandLineOptionType.ImportVoyages, ExportableVoyage.CsvRecordPattern); + public async Task HandleVesselTypeImportAsync() + => await HandleImport(CommandLineOptionType.ImportVesselTypes, ExportableVesselType.CsvRecordPattern); /// - /// Handle the sightings import command + /// Handle the voyage import command /// /// - public async Task HandleSightingImportAsync() - => await HandleImport(CommandLineOptionType.ImportSightings, ExportableSighting.CsvRecordPattern); + public async Task HandleVoyageImportAsync() + => await HandleImport(CommandLineOptionType.ImportVoyages, ExportableVoyage.CsvRecordPattern); } } \ No newline at end of file diff --git a/src/ShippingRecorder.Manager/Program.cs b/src/ShippingRecorder.Manager/Program.cs index dd0ce0c..48b65d2 100644 --- a/src/ShippingRecorder.Manager/Program.cs +++ b/src/ShippingRecorder.Manager/Program.cs @@ -87,10 +87,10 @@ public static async Task Main(string[] args) await new ImportHandler(settings, parser, factory).HandlePortImportAsync(); } - // If a CSV file containing vessel type details has been supplied, import it - if (parser.IsPresent(CommandLineOptionType.ImportVesselTypes)) + // If a CSV file containing sighting details has been supplied, import it + if (parser.IsPresent(CommandLineOptionType.ImportSightings)) { - await new ImportHandler(settings, parser, factory).HandleVesselTypeImportAsync(); + await new ImportHandler(settings, parser, factory).HandleSightingImportAsync(); } // If a CSV file containing vessel details has been supplied, import it @@ -99,16 +99,16 @@ public static async Task Main(string[] args) await new ImportHandler(settings, parser, factory).HandleVesselImportAsync(); } - // If a CSV file containing voyage details has been supplied, import it - if (parser.IsPresent(CommandLineOptionType.ImportVoyages)) + // If a CSV file containing vessel type details has been supplied, import it + if (parser.IsPresent(CommandLineOptionType.ImportVesselTypes)) { - await new ImportHandler(settings, parser, factory).HandleVoyageImportAsync(); + await new ImportHandler(settings, parser, factory).HandleVesselTypeImportAsync(); } - // If a CSV file containing sighting details has been supplied, import it - if (parser.IsPresent(CommandLineOptionType.ImportSightings)) + // If a CSV file containing voyage details has been supplied, import it + if (parser.IsPresent(CommandLineOptionType.ImportVoyages)) { - await new ImportHandler(settings, parser, factory).HandleSightingImportAsync(); + await new ImportHandler(settings, parser, factory).HandleVoyageImportAsync(); } // If a countries export has been requested, run the export @@ -129,16 +129,16 @@ public static async Task Main(string[] args) await new ExportHandler(settings, parser, factory).HandleOperatorExportAsync(); } - // If a sighting export has been requested, run the export - if (parser.IsPresent(CommandLineOptionType.ExportSightings)) + // If a ports export has been requested, run the export + if (parser.IsPresent(CommandLineOptionType.ExportPorts)) { - await new ExportHandler(settings, parser, factory).HandleSightingExportAsync(); + await new ExportHandler(settings, parser, factory).HandlePortExportAsync(); } - // If a vessel type export has been requested, run the export - if (parser.IsPresent(CommandLineOptionType.ExportVesselTypes)) + // If a sighting export has been requested, run the export + if (parser.IsPresent(CommandLineOptionType.ExportSightings)) { - await new ExportHandler(settings, parser, factory).HandleVesselTypeExportAsync(); + await new ExportHandler(settings, parser, factory).HandleSightingExportAsync(); } // If a vessel export has been requested, run the export @@ -147,6 +147,12 @@ public static async Task Main(string[] args) await new ExportHandler(settings, parser, factory).HandleVesselExportAsync(); } + // If a vessel type export has been requested, run the export + if (parser.IsPresent(CommandLineOptionType.ExportVesselTypes)) + { + await new ExportHandler(settings, parser, factory).HandleVesselTypeExportAsync(); + } + // If a voyage export has been requested, run the export if (parser.IsPresent(CommandLineOptionType.ExportVoyages)) { diff --git a/src/ShippingRecorder.Manager/fred.csv b/src/ShippingRecorder.Manager/fred.csv deleted file mode 100644 index 46b75f3..0000000 --- a/src/ShippingRecorder.Manager/fred.csv +++ /dev/null @@ -1,60 +0,0 @@ -IMO,Operator,Number,Event Type,Port,Date -"9781877","Aida Cruises","AIDACOSMA-2025-11-26","Depart","ESSCT","26-Nov-2025" -"9781877","Aida Cruises","AIDACOSMA-2025-11-26","Arrive","ESFUE","27-Nov-2025" -"9781877","Aida Cruises","AIDACOSMA-2025-11-26","Arrive","ESACE","28-Nov-2025" -"9781877","Aida Cruises","AIDACOSMA-2025-11-26","Depart","ESACE","28-Nov-2025" -"9781877","Aida Cruises","AIDACOSMA-2025-11-26","Arrive","ESLPA","29-Nov-2025" -"9781877","Aida Cruises","AIDACOSMA-2025-11-26","Depart","ESLPA","29-Nov-2025" -"9781877","Aida Cruises","AIDACOSMA-2025-11-26","Arrive","PTFNC","01-Dec-2025" -"9781877","Aida Cruises","AIDACOSMA-2025-11-26","Depart","PTFNC","01-Dec-2025" -"9781877","Aida Cruises","AIDACOSMA-2025-11-26","Arrive","ESSPC","02-Dec-2025" -"9781877","Aida Cruises","AIDACOSMA-2025-11-26","Depart","ESSPC","02-Dec-2025" -"9781877","Aida Cruises","AIDACOSMA-2025-11-26","Arrive","ESSCT","03-Dec-2025" -"9128532","Norwegian Cruise Line","NORWEGIANSKY-2025-11-24","Depart","PTLIS","24-Nov-2025" -"9128532","Norwegian Cruise Line","NORWEGIANSKY-2025-11-24","Arrive","MACAS","26-Nov-2025" -"9128532","Norwegian Cruise Line","NORWEGIANSKY-2025-11-24","Depart","MACAS","26-Nov-2025" -"9128532","Norwegian Cruise Line","NORWEGIANSKY-2025-11-24","Arrive","MAAGA","27-Nov-2025" -"9128532","Norwegian Cruise Line","NORWEGIANSKY-2025-11-24","Depart","MAAGA","27-Nov-2025" -"9128532","Norwegian Cruise Line","NORWEGIANSKY-2025-11-24","Arrive","ESACE","28-Nov-2025" -"9128532","Norwegian Cruise Line","NORWEGIANSKY-2025-11-24","Depart","ESACE","28-Nov-2025" -"9128532","Norwegian Cruise Line","NORWEGIANSKY-2025-11-24","Arrive","ESSCT","29-Nov-2025" -"9128532","Norwegian Cruise Line","NORWEGIANSKY-2025-11-24","Depart","ESSCT","29-Nov-2025" -"9128532","Norwegian Cruise Line","NORWEGIANSKY-2025-11-24","Arrive","ESBCN","06-Dec-2025" -"9226906","P&O Cruises","J519","Depart","GBSOU","17-Nov-2025" -"9226906","P&O Cruises","J519","Arrive","ESLKN","19-Nov-2025" -"9226906","P&O Cruises","J519","Depart","ESLKN","19-Nov-2025" -"9226906","P&O Cruises","J519","Arrive","PTFNC","22-Nov-2025" -"9226906","P&O Cruises","J519","Depart","PTFNC","23-Nov-2025" -"9226906","P&O Cruises","J519","Arrive","ESSCT","24-Nov-2025" -"9226906","P&O Cruises","J519","Depart","ESSCT","25-Nov-2025" -"9226906","P&O Cruises","J519","Arrive","ESLPA","26-Nov-2025" -"9226906","P&O Cruises","J519","Depart","ESACE","27-Nov-2025" -"9226906","P&O Cruises","J519","Arrive","ESCAD","29-Nov-2025" -"9226906","P&O Cruises","J519","Depart","ESCAD","29-Nov-2025" -"9226906","P&O Cruises","J519","Arrive","GBSOU","03-Dec-2025" -"9226906","P&O Cruises","J519","Depart","ESLPA","26-Jan-2026" -"9226906","P&O Cruises","J519","Arrive","ESACE","27-Jan-2026" -"8913162","Phoenix Reisen Gmbh","AMADEA-2025-11-15","Depart","DEEHV","15-Nov-2025" -"8913162","Phoenix Reisen Gmbh","AMADEA-2025-11-15","Arrive","ESACE","28-Nov-2025" -"8913162","Phoenix Reisen Gmbh","AMADEA-2025-11-15","Depart","ESACE","28-Nov-2025" -"8913162","Phoenix Reisen Gmbh","AMADEA-2025-11-15","Arrive","DEEHV","07-Dec-2025" -"9814040","Ponant","LEBOUGAINVILLE-2025-11-25","Depart","ESLPA","25-Nov-2025" -"9814040","Ponant","LEBOUGAINVILLE-2025-11-25","Arrive","ESLCR","26-Nov-2025" -"9814040","Ponant","LEBOUGAINVILLE-2025-11-25","Depart","ESLCR","26-Nov-2025" -"9814040","Ponant","LEBOUGAINVILLE-2025-11-25","Arrive","ESACE","27-Nov-2025" -"9814040","Ponant","LEBOUGAINVILLE-2025-11-25","Depart","ESACE","27-Nov-2025" -"9814040","Ponant","LEBOUGAINVILLE-2025-11-25","Arrive","ESSCT","28-Nov-2025" -"9814040","Ponant","LEBOUGAINVILLE-2025-11-25","Depart","ESSCT","28-Nov-2025" -"9814040","Ponant","LEBOUGAINVILLE-2025-11-25","Arrive","SNDKR","05-Dec-2025" -"8420878","Windstar Cruises","ST20251120-10D","Depart","ESSCT","20-Nov-2025" -"8420878","Windstar Cruises","ST20251120-10D","Arrive","ESSSG","21-Nov-2025" -"8420878","Windstar Cruises","ST20251120-10D","Depart","ESSSG","21-Nov-2025" -"8420878","Windstar Cruises","ST20251120-10D","Arrive","ESSPC","22-Nov-2025" -"8420878","Windstar Cruises","ST20251120-10D","Depart","ESSPC","22-Nov-2025" -"8420878","Windstar Cruises","ST20251120-10D","Arrive","PTFNC","24-Nov-2025" -"8420878","Windstar Cruises","ST20251120-10D","Depart","PTFNC","25-Nov-2025" -"8420878","Windstar Cruises","ST20251120-10D","Arrive","ESACE","27-Nov-2025" -"8420878","Windstar Cruises","ST20251120-10D","Depart","ESACE","27-Nov-2025" -"8420878","Windstar Cruises","ST20251120-10D","Arrive","ESLPA","28-Nov-2025" -"8420878","Windstar Cruises","ST20251120-10D","Depart","ESLPA","29-Nov-2025" -"8420878","Windstar Cruises","ST20251120-10D","Arrive","ESSCT","30-Nov-2025" diff --git a/src/ShippingRecorder.Mvc/Controllers/DataExchangeControllerBase.cs b/src/ShippingRecorder.Mvc/Controllers/DataExchangeControllerBase.cs index 607a115..f8e7e26 100644 --- a/src/ShippingRecorder.Mvc/Controllers/DataExchangeControllerBase.cs +++ b/src/ShippingRecorder.Mvc/Controllers/DataExchangeControllerBase.cs @@ -45,6 +45,7 @@ public DataExchangeControllerBase( _exporters.Add(DataExchangeType.Countries, countryClient); _exporters.Add(DataExchangeType.Locations, locationClient); _exporters.Add(DataExchangeType.Operators, operatorClient); + _exporters.Add(DataExchangeType.Ports, portClient); _exporters.Add(DataExchangeType.Sightings, sightingClient); _exporters.Add(DataExchangeType.Vessels, vesselClient); _exporters.Add(DataExchangeType.VesselTypes, vesselTypeClient); diff --git a/src/ShippingRecorder.Mvc/appsettings.json b/src/ShippingRecorder.Mvc/appsettings.json index 0f64ecb..cee0e18 100644 --- a/src/ShippingRecorder.Mvc/appsettings.json +++ b/src/ShippingRecorder.Mvc/appsettings.json @@ -142,6 +142,10 @@ { "Name": "ExportVoyage", "Route": "/export/voyages" + }, + { + "Name": "ExportPort", + "Route": "/export/ports" } ], "UseCustomErrorPageInDevelopment": true, diff --git a/src/ShippingRecorder.Tests/Client/PortClientTest.cs b/src/ShippingRecorder.Tests/Client/PortClientTest.cs index d766e59..7071010 100644 --- a/src/ShippingRecorder.Tests/Client/PortClientTest.cs +++ b/src/ShippingRecorder.Tests/Client/PortClientTest.cs @@ -176,5 +176,23 @@ public async Task ImportFromFileTest() Assert.AreEqual(expectedRoute, _httpClient.Requests[0].Uri); Assert.AreEqual(json, await _httpClient.Requests[0].Content.ReadAsStringAsync()); } + + [TestMethod] + public async Task ExportTest() + { + _filePath = Path.ChangeExtension(Path.GetTempFileName(), "csv"); + _httpClient.AddResponse(""); + + var json = JsonSerializer.Serialize(new { FileName = _filePath }); + var expectedRoute = _settings.ApiRoutes.First(x => x.Name == "ExportPort").Route; + + await _client.ExportAsync(_filePath); + + Assert.AreEqual($"Bearer {ApiToken}", _httpClient.DefaultRequestHeaders.Authorization.ToString()); + Assert.AreEqual($"{_settings.ApiUrl}", _httpClient.BaseAddress.ToString()); + Assert.AreEqual(HttpMethod.Post, _httpClient.Requests[0].Method); + Assert.AreEqual(expectedRoute, _httpClient.Requests[0].Uri); + Assert.AreEqual(json, await _httpClient.Requests[0].Content.ReadAsStringAsync()); + } } } diff --git a/src/ShippingRecorder.Tests/Export/PortExporterTest.cs b/src/ShippingRecorder.Tests/Export/PortExporterTest.cs new file mode 100644 index 0000000..45cf7cc --- /dev/null +++ b/src/ShippingRecorder.Tests/Export/PortExporterTest.cs @@ -0,0 +1,66 @@ +using ShippingRecorder.Data; +using ShippingRecorder.DataExchange.Entities; +using ShippingRecorder.DataExchange.Export; +using ShippingRecorder.Entities.Interfaces; +using ShippingRecorder.Tests.Mocks; +using Moq; +using ShippingRecorder.Entities.Db; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.IO; +using System.Threading.Tasks; +using ShippingRecorder.BusinessLogic.Factory; + +namespace ShippingRecorder.Tests.Export +{ + [TestClass] + public class PortExportTest + { + private readonly Port _port = DataGenerator.CreatePort(); + + private string _filePath; + + [TestCleanup] + public void CleanUp() + { + if (!string.IsNullOrEmpty(_filePath) && File.Exists(_filePath)) + { + File.Delete(_filePath); + } + } + + [TestMethod] + public void FromCsvRecordTest() + { + var record = $@"""{_port.Code}"",""{_port.Name}"""; + var exportable = ExportablePort.FromCsv(record); + Assert.AreEqual(_port.Code, exportable.Code); + Assert.AreEqual(_port.Name, exportable.Name); + } + + [TestMethod] + public async Task ExportTest() + { + var context = ShippingRecorderDbContextFactory.CreateInMemoryDbContext(); + await context.Countries.AddAsync(_port.Country); + await context.Ports.AddAsync(_port); + await context.SaveChangesAsync(); + + var factory = new ShippingRecorderFactory(context, new MockFileLogger()); + var exporter = new PortExporter(factory); + + _filePath = Path.ChangeExtension(Path.GetTempFileName(), "csv"); + await exporter.ExportAsync(_filePath); + + var info = new FileInfo(_filePath); + Assert.AreEqual(info.FullName, _filePath); + Assert.IsGreaterThan(0, info.Length); + + var records = File.ReadAllLines(_filePath); + Assert.HasCount(2, records); + + var exportable = ExportablePort.FromCsv(records[1]); + Assert.AreEqual(_port.Code, exportable.Code); + Assert.AreEqual(_port.Name, exportable.Name); + } + } +} \ No newline at end of file diff --git a/src/ShippingRecorder.Tests/Mocks/DataGenerator.cs b/src/ShippingRecorder.Tests/Mocks/DataGenerator.cs index 9f8a390..78d893e 100644 --- a/src/ShippingRecorder.Tests/Mocks/DataGenerator.cs +++ b/src/ShippingRecorder.Tests/Mocks/DataGenerator.cs @@ -135,8 +135,8 @@ public static Port CreatePort() Id = RandomId(), CountryId = country.Id, Country = country, - Code = RandomWord(5, 5).CleanCode(), - Name = RandomWord() + Code = $"{country.Code}{RandomWord(3, 3)}".CleanCode(), + Name = RandomWord().TitleCase() }; }