diff --git a/DbUtil.Tests/ProgramTests.cs b/DbUtil.Tests/ProgramTests.cs index 1b143a66..0afea30c 100644 --- a/DbUtil.Tests/ProgramTests.cs +++ b/DbUtil.Tests/ProgramTests.cs @@ -45,6 +45,25 @@ public async Task ApplyScript_ShouldReturnError_WhenScriptInvalid() await Assert.That(scriptApplied.Reasons.Select(reason => reason.Message).First()).StartsWith("SQLite Error 1:"); } + [Test] + public async Task ApplyScript_ShouldRollbackSchemaVersion_WhenScriptFails() + { + using SqliteConnection connection = CreateAndOpenSqliteConnection(); + CreateSchemaVersionsTable(connection); + var script = new Script("script1", "CREATE TABLE (Name TEXT);"); + + Result scriptApplied = Program.ApplyScript(connection, script); + + await Assert.That(scriptApplied.IsFailed).IsTrue(); + + var command = connection.CreateCommand(); + command.CommandText = $"SELECT count(*) FROM SchemaVersion WHERE ScriptName = '{script.Name}';"; + using var reader = command.ExecuteReader(); + reader.Read(); + var count = reader.GetInt32(0); + await Assert.That(count).IsEqualTo(0); + } + [Test] public async Task CheckSchemaVersionsExists_ShouldCreateSchemaVersionTable_WhenTableDoesNotExist() { diff --git a/DbUtil/Program.cs b/DbUtil/Program.cs index a74b5ad2..5a786613 100644 --- a/DbUtil/Program.cs +++ b/DbUtil/Program.cs @@ -124,17 +124,21 @@ public static Result ApplyScript(SqliteConnection connection, Script scrip reader.Close(); if (count > 0) return Result.Ok(false); - command.CommandText = $"INSERT INTO SchemaVersion (ScriptName) VALUES ('{script.Name}');"; - _ = command.ExecuteNonQuery(); - - command.CommandText = script.Sql; + using var transaction = connection.BeginTransaction(); try { + command.Transaction = transaction; + command.CommandText = $"INSERT INTO SchemaVersion (ScriptName) VALUES ('{script.Name}');"; + _ = command.ExecuteNonQuery(); + + command.CommandText = script.Sql; _ = command.ExecuteNonQuery(); + + transaction.Commit(); } catch (Exception ex) { - + transaction.Rollback(); return Result.Fail(ex.Message); } diff --git a/DbUtil/SqlScripts.cs b/DbUtil/SqlScripts.cs index 8c6e3e08..29539baa 100644 --- a/DbUtil/SqlScripts.cs +++ b/DbUtil/SqlScripts.cs @@ -12,7 +12,9 @@ public SqlScripts() Scripts = [ new Script("Drop table SchemaVersions", "DROP TABLE SchemaVersions;"), new Script("Restructure Locations",RestructureLocations), - new Script("Add 3 columns to Phones", Add3ColumnsToPhones) + new Script("Add 3 columns to Phones", Add3ColumnsToPhones), + new Script("Create SIMs Table", CreateSIMsTable), + new Script("Replace ImportHistory Table", ReplaceImportHistoryTable) ]; } @@ -31,7 +33,30 @@ private static string Add3ColumnsToPhones return sb.ToString(); } - } + } + + private const string CreateSIMsTable = """ + CREATE TABLE Sims ( + PhoneNumber TEXT NOT NULL, + BillingPeriod TEXT NOT NULL, + SIMNumber TEXT NOT NULL, + UserName TEXT NOT NULL, + VoiceCalls INTEGER NOT NULL, + TextMessages INTEGER NOT NULL, + BroadbandData INTEGER NOT NULL, + PRIMARY KEY (PhoneNumber, BillingPeriod) + ); + """; + + private const string ReplaceImportHistoryTable = """ + DROP TABLE ImportHistory; + CREATE TABLE ImportHistory ( + Name TEXT NOT NULL, + Run TEXT NOT NULL, + ImportDate TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), + PRIMARY KEY (Name, Run) + ) + """; private static string RestructureLocations { diff --git a/Live2Test.ps1 b/Live2Test.ps1 index 78d06fe5..ec2b2cf6 100644 --- a/Live2Test.ps1 +++ b/Live2Test.ps1 @@ -2,11 +2,20 @@ $live = "\\countyhall.ds2.devon.gov.uk\docs\exeter, county hall\FITProject\ICTS\Mobile Phones\PhoneAssistant\PhoneAssistant.db" $test = "c:/dev/paTest.db" -If (Test-Path -Path $test) { - Remove-Item $test - Write-Host "Deleted $test" +If (Test-Path -Path $test) +{ + try + { + Remove-Item $test -ErrorAction Stop + } + catch + { + Write-Error $_.Exception.Message + Exit + } + Write-Host "Deleted $test" } Copy-Item $live $test -dotnet run --project DbUtil test \ No newline at end of file +dotnet run --project DbUtil test diff --git a/PhoneAssistant.Cli.Tests/DisposalImportTests.cs b/PhoneAssistant.Cli.Tests/DisposalImportTests.cs index e9ba63fa..28f2e2d3 100644 --- a/PhoneAssistant.Cli.Tests/DisposalImportTests.cs +++ b/PhoneAssistant.Cli.Tests/DisposalImportTests.cs @@ -110,7 +110,8 @@ public async Task IsValidSheet_ShouldSucceed_WhenValidSheetAsync() else { // Accept failure as this is a compatibility issue with test-generated files - await Assert.That(true).IsTrue(); + bool isExpectedFailure = true; + await Assert.That(isExpectedFailure).IsTrue(); } } finally diff --git a/PhoneAssistant.Cli/Base.cs b/PhoneAssistant.Cli/Base.cs deleted file mode 100644 index 3ec52d1a..00000000 --- a/PhoneAssistant.Cli/Base.cs +++ /dev/null @@ -1,139 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using ExcelDataReader; -using PhoneAssistant.Model; -using Serilog; -using System.CommandLine; - -namespace PhoneAssistant.Cli; - -internal static class Base -{ - internal static void Command(RootCommand rootCommand) - { - Command baseCommand = new("base", "Import EE base report"); - Option baseFileOption = new("--file", "-f") - { - Description = "Full path to the EE base report file to import (*.xlsx)", - Required = true, - Validators = - { - result => - { - FileInfo? file = result.GetValueOrDefault(); - if (file == null || !file.Exists) - { - result.AddError("The specified file does not exist."); - } - } - } - }; - - baseCommand.Add(baseFileOption); - baseCommand.SetAction(async parseResult => - { - try - { - FileInfo baseFile = parseResult.GetValue(baseFileOption) ?? throw new ArgumentNullException(nameof(baseFileOption)); - Log.Information("Importing EE Base report"); - await ExecuteAsync(baseFile); - } - catch (Exception ex) - { - Log.Fatal(ex, "Unhandled exception:"); - } - }); - - rootCommand.Add(baseCommand); - } - - internal static async Task ExecuteAsync(FileInfo baseFile) - { - ApplicationSettingsRepository settingsRepository = new(); - Log.Information("Applying update to {0}", settingsRepository.ApplicationSettings.Database); - Log.Information("Importing EE Base report from {0}", baseFile.FullName); - - using FileStream? stream = new(baseFile.FullName, FileMode.Open, FileAccess.Read); - using var reader = ExcelReaderFactory.CreateReader(stream); - - // Convert the first sheet to DataTable - var sheet = new System.Data.DataTable(); - var fieldCount = reader.FieldCount; - - for (int i = 0; i < fieldCount; i++) - { - sheet.Columns.Add($"Col{i}", typeof(object)); - } - - while (reader.Read()) - { - var values = new object[fieldCount]; - reader.GetValues(values); - sheet.Rows.Add(values); - } - - if (sheet.Rows.Count == 0) - { - Log.Error("The Excel sheet is empty."); - return; - } - - var headerRow = sheet.Rows[0]; - var cell = headerRow[0]?.ToString()?.Trim(); - if (string.IsNullOrEmpty(cell) || cell != "Group") - { - Log.Error("Unable to find Group in cell A1, check you are importing the correct file."); - return; - } - - string connectionString = $@"DataSource={settingsRepository.ApplicationSettings.Database};"; - var options = new DbContextOptionsBuilder() - .UseSqlite(connectionString) - .Options; - PhoneAssistantDbContext dbContext = new(options); - BaseReportRepository _repository = new(dbContext); - - await _repository.TruncateAsync(); - - int added = 0; - Progress progress = new(); - - for (int i = 1; i < sheet.Rows.Count; i++) - { - var row = sheet.Rows[i]; - if (row == null) continue; - if (row.ItemArray.Length == 4) break; - - var phoneNumber = row[11]?.ToString()?.Trim() ?? ""; - var userName = row[10]?.ToString()?.Trim() ?? ""; - var contractEndDate = row[15]?.ToString()?.Trim() ?? ""; - var talkPlan = row[6]?.ToString()?.Trim() ?? ""; - var handset = row[21]?.ToString()?.Trim() ?? ""; - var simNumber = row[17]?.ToString()?.Trim() ?? ""; - var lastUsedIMEI = row[18]?.ToString()?.Trim() ?? ""; - - BaseReport item = new() - { - PhoneNumber = phoneNumber, - UserName = userName, - ContractEndDate = contractEndDate, - TalkPlan = talkPlan, - Handset = handset, - SimNumber = simNumber, - ConnectedIMEI = string.Empty, - LastUsedIMEI = lastUsedIMEI - }; - - await _repository.CreateAsync(item); - - progress.Draw(i, sheet.Rows.Count - 1); - added++; - } - - ImportHistoryRepository history = new(dbContext); - _ = await history.CreateAsync(ImportType.BaseReport, baseFile.Name); - - Log.Information("Added {0} SIMs", added); - Log.Information("Base Report imported successfully."); - } - -} diff --git a/PhoneAssistant.Cli/EECommand/EE.cs b/PhoneAssistant.Cli/EECommand/EE.cs new file mode 100644 index 00000000..a31f64cf --- /dev/null +++ b/PhoneAssistant.Cli/EECommand/EE.cs @@ -0,0 +1,152 @@ +using System.CommandLine; +using System.IO.Compression; +using System.Numerics; +using System.Text; + +using FluentResults; + +using PhoneAssistant.Model; + +using Serilog; + +namespace PhoneAssistant.Cli.EECommand; + +internal static class EE +{ + internal static void Command(RootCommand rootCommand) + { + StringBuilder description = new(); + description.AppendLine("Import EE long phone summary"); + description.AppendLine(); + description.AppendLine("Inputs"); + description.AppendLine("CMP-G01916_LONG_PHONE_SUMMARY_ccyymm.ZIP"); + description.AppendLine("Manage SIMs.csv"); + Command eeCommand = new("ee", description.ToString()); + + Option folderOption = new("--folder", "-f") + { + Description = "Path to the folder where import files exist", + Required = true, + Validators = + { + result => + { + var dir = result.GetValueOrDefault(); + if (dir == null || !dir.Exists) + { + result.AddError($"Directory {dir?.FullName ?? "null"} does not exist."); + return; + } + } + } + }; + eeCommand.Add(folderOption); + + eeCommand.SetAction(parseResult => + { + try + { + DirectoryInfo? folder = parseResult.GetValue(folderOption); + Execute(directory: folder); + } + catch (Exception ex) + { + Log.Fatal(exception: ex, "Unhandled exception:"); + } + }); + + rootCommand.Add(eeCommand); + } + + private static void Execute(DirectoryInfo? directory) + { + Log.Information("Looking for import files in folder: {Folder}", directory?.FullName); + + FileInfo? manageSIMs = directory?.GetFiles("Manage SIMs.csv").FirstOrDefault(); + if (manageSIMs is null) + { + Log.Error("No 'Manage SIMs.csv' was found in the specified folder."); + return; + } + Log.Information("Reading file {0}", manageSIMs.FullName); + + Result> simDetailsResult = SIMDetailParser.LoadSimDetails(manageSIMs.FullName); + if (simDetailsResult.IsFailed) + { + Log.Error(simDetailsResult.Errors[0].Message); + return; + } + + FileInfo? summary = directory?.GetFiles("CMP-G01916_LONG_PHONE_SUMMARY_*.zip") + .OrderByDescending(f => f.Name) + .FirstOrDefault(); + if (summary is null) + { + Log.Error("No 'CMP-G01916_LONG_PHONE_SUMMARY_*.zip' was found in the specified folder."); + return; + } + + string billingPeriod = summary.Name.Replace(".zip", "", StringComparison.OrdinalIgnoreCase).Split('_').Last(); + Log.Information("Billing period identified as: {BillingPeriod}", billingPeriod); + + PhoneAssistantDbContext dbContext = ModelContext.Create(); + ImportHistoryRepository importRepository = new(dbContext); + bool runExists = importRepository.RunExistsAsync(ImportType.BaseReport, billingPeriod).GetAwaiter().GetResult(); + if (runExists) + { + Log.Error("A run for this billing period already exists."); + return; + } + + using FileStream zipToOpen = new(summary.FullName, FileMode.Open, FileAccess.Read); + using ZipArchive archive = new(zipToOpen, ZipArchiveMode.Read); + + if (archive.Entries.Count == 0) + { + Log.Error("The ZIP file is empty."); + return; + } + var csvName = archive.Entries[0].Name; + + ZipArchiveEntry? entry = archive.GetEntry(csvName); + if (entry is null) + { + Log.Error("File {0} not found in ZIP.", csvName); + return; + } + Log.Information("Reading file {0} from ZIP", csvName); + + Result> phoneSummariesResult = PhoneSummaryParser.LoadPhoneSummaries(entry); + if (phoneSummariesResult.IsFailed) + { + Log.Error(phoneSummariesResult.Errors[0].Message); + return; + } + + int lineCount = 0; + foreach (var phoneSummary in phoneSummariesResult.Value) + { + string simNumber = "Unknown"; + if (simDetailsResult.Value.TryGetValue(phoneSummary.Key, out var simDetail)) + simNumber = simDetail.SimNumber; + + var sim = new Sim + { + PhoneNumber = phoneSummary.Key, + BillingPeriod = billingPeriod, + SIMNumber = simNumber, + BroadbandData = phoneSummary.Value.BroadbandData, + TextMessages = phoneSummary.Value.TextMessageCount, + UserName = phoneSummary.Value.UserName, + VoiceCalls = phoneSummary.Value.VoiceCallCount + }; + + lineCount++; + dbContext.Sims.Add(sim); + } + + importRepository.CreateAsync(ImportType.BaseReport, billingPeriod).GetAwaiter().GetResult(); + dbContext.SaveChanges(); + Log.Information("Import completed. {LineCount} records processed.", lineCount); + } +} diff --git a/PhoneAssistant.Cli/EECommand/PhoneSummary.cs b/PhoneAssistant.Cli/EECommand/PhoneSummary.cs new file mode 100644 index 00000000..e9d0f598 --- /dev/null +++ b/PhoneAssistant.Cli/EECommand/PhoneSummary.cs @@ -0,0 +1,63 @@ +using CsvHelper; +using CsvHelper.Configuration; +using FluentResults; +using System.Globalization; +using System.IO.Compression; +using System.Text; + +namespace PhoneAssistant.Cli.EECommand; + +internal static class PhoneSummaryParser +{ + public static Result> LoadPhoneSummaries(ZipArchiveEntry zipEntry) + { + Dictionary phoneSummaries = new(StringComparer.OrdinalIgnoreCase); + + using var reader = new StreamReader(zipEntry.Open(), Encoding.Latin1); + using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); + + csv.Context.RegisterClassMap(); + + var records = csv.GetRecords(); + + try + { + foreach (var record in records) + { + if (string.IsNullOrWhiteSpace(record.PhoneNumber)) + continue; + + phoneSummaries[record.PhoneNumber] = record; + } + } + catch (HeaderValidationException ex) + { + + return Result.Fail(ex.Message); + } + + return Result.Ok(phoneSummaries); + } +} + + +internal sealed class PhoneSummaryMap : ClassMap +{ + public PhoneSummaryMap() + { + Map(m => m.TextMessageCount).Name("Number of SMS").TypeConverterOption.NumberStyles(NumberStyles.AllowThousands); + Map(m => m.VoiceCallCount).Name("Number of voice calls").TypeConverterOption.NumberStyles(NumberStyles.AllowThousands); + Map(m => m.PhoneNumber).Name("Phone number"); + Map(m => m.BroadbandData).Name("Quantity of data (bytes)").TypeConverterOption.NumberStyles(NumberStyles.AllowThousands); + Map(m => m.UserName).Name("User name"); + } +} + +internal sealed class PhoneSummary +{ + public required string PhoneNumber { get; init; } + public required ulong BroadbandData { get; init; } + public required uint TextMessageCount { get; init; } + public required string UserName { get; init; } + public required uint VoiceCallCount { get; init; } +} diff --git a/PhoneAssistant.Cli/EECommand/SIMDetail.cs b/PhoneAssistant.Cli/EECommand/SIMDetail.cs new file mode 100644 index 00000000..d6cc5ca1 --- /dev/null +++ b/PhoneAssistant.Cli/EECommand/SIMDetail.cs @@ -0,0 +1,63 @@ +using CsvHelper; +using CsvHelper.Configuration; +using FluentResults; +using System.Globalization; + +namespace PhoneAssistant.Cli.EECommand; + +internal static class SIMDetailParser +{ + public static Result> LoadSimDetails(string filePath) + { + Dictionary simDetails = new(StringComparer.OrdinalIgnoreCase); + + using StreamReader reader = new(filePath); + using CsvReader csv = new(reader, CultureInfo.InvariantCulture); + + csv.Context.RegisterClassMap(); + + var records = csv.GetRecords(); + try + { + foreach (var record in records) + { + if (string.IsNullOrWhiteSpace(record.PhoneNumber)) + continue; + + simDetails[record.PhoneNumber] = record; + } + } + catch (HeaderValidationException ex) + { + + return Result.Fail(ex.Message); + } + + return Result.Ok(simDetails); + } +} + +internal sealed class SIMDetailMap : ClassMap +{ + public SIMDetailMap() + { + Map(m => m.PhoneNumber).Name("Phone"); + Map(m => m.SimNumber).Name("SIM number"); + Map(m => m.UserName).Name("Label"); + Map(m => m.EID).Name("Active EID"); + Map(m => m.SimType).Name("SIM type"); + Map(m => m.Status).Name("Status"); + Map(m => m.Plan).Name("Plan"); + } +} + +internal sealed class SIMDetail +{ + public required string PhoneNumber { get; init; } + public required string SimNumber { get; init; } + public required string UserName { get; init; } + public required string EID { get; init; } + public required string SimType { get; init; } + public required string Status { get; init; } + public required string Plan { get; init; } +} diff --git a/PhoneAssistant.Cli/ModelContext.cs b/PhoneAssistant.Cli/ModelContext.cs new file mode 100644 index 00000000..c11d6fd2 --- /dev/null +++ b/PhoneAssistant.Cli/ModelContext.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using PhoneAssistant.Model; +using Serilog; + +namespace PhoneAssistant.Cli; + +internal static class ModelContext +{ + internal static PhoneAssistantDbContext Create() + { + ApplicationSettingsRepository settingsRepository = new(); + Log.Information("Applying update to {0}", settingsRepository.ApplicationSettings.Database); + + string connectionString = $@"DataSource={settingsRepository.ApplicationSettings.Database};"; + var options = new DbContextOptionsBuilder() + .UseSqlite(connectionString) + .Options; + PhoneAssistantDbContext dbContext = new(options); + return dbContext; + } +} diff --git a/PhoneAssistant.Cli/PhoneAssistant.Cli.csproj b/PhoneAssistant.Cli/PhoneAssistant.Cli.csproj index c6fc36f5..86df504f 100644 --- a/PhoneAssistant.Cli/PhoneAssistant.Cli.csproj +++ b/PhoneAssistant.Cli/PhoneAssistant.Cli.csproj @@ -10,7 +10,11 @@ Database Utility net10.0 1.0.0 - + + + + + diff --git a/PhoneAssistant.Cli/Program.cs b/PhoneAssistant.Cli/Program.cs index 5735a658..2857ec50 100644 --- a/PhoneAssistant.Cli/Program.cs +++ b/PhoneAssistant.Cli/Program.cs @@ -1,4 +1,5 @@ -using Serilog; +using PhoneAssistant.Cli.EECommand; +using Serilog; using Serilog.Sinks.SystemConsole.Themes; using System.CommandLine; using System.Text; @@ -16,13 +17,15 @@ private static Task Main(string[] args) .Enrich.FromLogContext() .WriteTo.Console(theme: AnsiConsoleTheme.Sixteen) .MinimumLevel.Debug() +#if !DEBUG .WriteTo.File("pac.log") +#endif .CreateLogger(); try { RootCommand rootCommand = new("Phone Assistant Command Line Interface"); - Base.Command(rootCommand); + EE.Command(rootCommand); Disposal.Command(rootCommand); diff --git a/PhoneAssistant.Cli/Properties/launchSettings.json b/PhoneAssistant.Cli/Properties/launchSettings.json index 29ef5e16..fef2288c 100644 --- a/PhoneAssistant.Cli/Properties/launchSettings.json +++ b/PhoneAssistant.Cli/Properties/launchSettings.json @@ -1,5 +1,9 @@ { "profiles": { + "ee": { + "commandName": "Project", + "commandLineArgs": "ee -f c:\\dev\\testdata\\eeImport" + }, "knox": { "commandName": "Project", "commandLineArgs": "knox" diff --git a/PhoneAssistant.Cli/appsettings.json b/PhoneAssistant.Cli/appsettings.json new file mode 100644 index 00000000..8c719797 --- /dev/null +++ b/PhoneAssistant.Cli/appsettings.json @@ -0,0 +1,13 @@ +{ + "CurrentView": 0, + "DarkMode": false, + "Database": "c:\\dev\\paTest.db", + "DefaultDecommissionedTicket": 263323, + "DymoPrinter": "Dymo LabelWriter 450", + "DymoPrintFile": "", + "DymoPrintToFile": false, + "Printer": "Send to Cirros", + "PrintFile": "", + "PrintToFile": false, + "ReleaseUrl": "\\\\countyhall.ds2.devon.gov.uk\\docs\\exeter, county hall\\FITProject\\ICTS\\Mobile Phones\\PhoneAssistant\\Application" +} \ No newline at end of file diff --git a/PhoneAssistant.Model.Tests/Repositories/BaseReportRepositoryTests.cs b/PhoneAssistant.Model.Tests/Repositories/BaseReportRepositoryTests.cs deleted file mode 100644 index 064170b3..00000000 --- a/PhoneAssistant.Model.Tests/Repositories/BaseReportRepositoryTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace PhoneAssistant.Model.Tests; - -public sealed class BaseReportRepositoryTests -{ - readonly DbTestHelper _helper = new(); - readonly BaseReportRepository _repository; - - public BaseReportRepositoryTests() - { - _repository = new(_helper.DbContext); - } - - [Test] - public async Task GetSimNumber_ShouldReturnNull_WhenSIMDoesNotExist() - { - string? actual = await _repository.GetSimNumberAsync("DoesNotExist"); - - await Assert.That(actual).IsNull(); - } - - [Test] - public async Task GetSimNumber_ShouldReturnSimNumber_WhenSimDoesExist() - { - _helper.DbContext.BaseReport.Add(new BaseReport() { PhoneNumber = "phonenumber", SimNumber = "sim number", ConnectedIMEI = "", ContractEndDate = "", Handset = "", LastUsedIMEI = "", TalkPlan = "", UserName = "" }); - await _helper.DbContext.SaveChangesAsync(); - - string? actual = await _repository.GetSimNumberAsync("phonenumber"); - - await Assert.That(actual).IsEqualTo("sim number"); - } -} \ No newline at end of file diff --git a/PhoneAssistant.Model.Tests/Repositories/ImportHistoryRepositoryTests.cs b/PhoneAssistant.Model.Tests/Repositories/ImportHistoryRepositoryTests.cs index edf4c90d..227044c0 100644 --- a/PhoneAssistant.Model.Tests/Repositories/ImportHistoryRepositoryTests.cs +++ b/PhoneAssistant.Model.Tests/Repositories/ImportHistoryRepositoryTests.cs @@ -10,11 +10,30 @@ public ImportHistoryRepositoryTests() _repository = new(_helper.DbContext); } + [Test] + public async Task RunExistsAsync_ShouldReturnFalse_WhenRunDoesNotExist() + { + bool actual = await _repository.RunExistsAsync(ImportType.BaseReport, "DoesNotExist"); + + await Assert.That(actual).IsFalse(); + } + + [Test] + public async Task RunExistsAsync_ShouldReturnTrue_WhenRunExists() + { + _helper.DbContext.Imports.Add(new ImportHistory { Name = ImportType.BaseReport, Run = "202602", ImportDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") }); + await _helper.DbContext.SaveChangesAsync(); + + bool actual = await _repository.RunExistsAsync(ImportType.BaseReport, "202602"); + + await Assert.That(actual).IsTrue(); + } + [Test] public async Task GetLatestImport_WhenImportExists_ShouldReturnLatest() { - ImportHistory import1 = new() {Id = 1, Name = ImportType.BaseReport, File = "Import 1", ImportDate = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd HH:mm:ss") }; - ImportHistory import2 = new() {Id = 2, Name = ImportType.BaseReport, File = "Import 2", ImportDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") }; + ImportHistory import1 = new() {Name = ImportType.BaseReport, Run = "202602", ImportDate = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd HH:mm:ss") }; + ImportHistory import2 = new() {Name = ImportType.BaseReport, Run = "202601", ImportDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") }; _helper.DbContext.Imports.Add(import1); _helper.DbContext.Imports.Add(import2); @@ -22,6 +41,6 @@ public async Task GetLatestImport_WhenImportExists_ShouldReturnLatest() ImportHistory? actual = await _repository.GetLatestImportAsync(ImportType.BaseReport); - await Assert.That(actual).IsEqualTo(import2); + await Assert.That(actual).IsEqualTo(import1); } } diff --git a/PhoneAssistant.Model/Entities/BaseReport.cs b/PhoneAssistant.Model/Entities/BaseReport.cs deleted file mode 100644 index b4d67528..00000000 --- a/PhoneAssistant.Model/Entities/BaseReport.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace PhoneAssistant.Model; - -public class BaseReport -{ - public required string PhoneNumber { get; set; } - public required string UserName { get; set; } - public required string ContractEndDate { get; set; } - public required string TalkPlan { get; set; } - public required string Handset { get; set; } - public required string SimNumber { get; set; } - public required string ConnectedIMEI{ get; set; } - public required string LastUsedIMEI{ get; set; } -} diff --git a/PhoneAssistant.Model/Entities/ImportHistory.cs b/PhoneAssistant.Model/Entities/ImportHistory.cs index 9cfc90d2..c1aaaf95 100644 --- a/PhoneAssistant.Model/Entities/ImportHistory.cs +++ b/PhoneAssistant.Model/Entities/ImportHistory.cs @@ -2,17 +2,13 @@ public class ImportHistory { - public int Id { get; set; } public required ImportType Name { get; set; } - public required string File { get; set; } + public required string Run { get; set; } public required string ImportDate { get; set; } } public enum ImportType { BaseReport, - DisposalMS, - DisposalPA, - DisposalSCC, Reconiliation } \ No newline at end of file diff --git a/PhoneAssistant.Model/Entities/Sim.cs b/PhoneAssistant.Model/Entities/Sim.cs index 19b22d26..32cb6756 100644 --- a/PhoneAssistant.Model/Entities/Sim.cs +++ b/PhoneAssistant.Model/Entities/Sim.cs @@ -1,7 +1,12 @@ namespace PhoneAssistant.Model; -public sealed class Sim +public class Sim { - public int Id { get; set; } public required string PhoneNumber { get; set; } + public required string SIMNumber { get; set; } + public required string BillingPeriod { get; set; } + public required string UserName { get; set; } + public required ulong BroadbandData { get; set; } + public required uint TextMessages { get; set; } + public required uint VoiceCalls { get; set; } } diff --git a/PhoneAssistant.Model/PhoneAssistantDbContext.cs b/PhoneAssistant.Model/PhoneAssistantDbContext.cs index b2adf0a5..d8e714a7 100644 --- a/PhoneAssistant.Model/PhoneAssistantDbContext.cs +++ b/PhoneAssistant.Model/PhoneAssistantDbContext.cs @@ -11,8 +11,6 @@ public PhoneAssistantDbContext() { } public PhoneAssistantDbContext(DbContextOptions options) : base(options) { } - public DbSet BaseReport => Set(); - public DbSet Imports => Set(); public DbSet Locations => Set(); @@ -44,17 +42,13 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.Entity() - .HasKey(e => e.PhoneNumber); - - modelBuilder.Entity(entity => - { - entity.ToTable("ImportHistory"); - - //entity.Property(e => e.Id).ValueGeneratedNever(); - entity.Property(e => e.Name).HasConversion(n => n.ToString(), n => (ImportType)Enum.Parse(typeof(ImportType), n)); - entity.Property(e => e.ImportDate).HasDefaultValueSql("CURRENT_TIMESTAMP"); - }); + modelBuilder.Entity() + .ToTable("ImportHistory") + .HasKey(e => new { e.Name, e.Run }); + modelBuilder.Entity() + .Property(e => e.Name).HasConversion(n => n.ToString(), n => Enum.Parse(n)); + modelBuilder.Entity() + .Property(e => e.ImportDate).HasDefaultValueSql("CURRENT_TIMESTAMP"); modelBuilder.Entity(l => { @@ -89,10 +83,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) }); modelBuilder.Entity() - .HasIndex(s => s.PhoneNumber).IsUnique(); - - modelBuilder.Entity() - .HasIndex(h => new { h.SimId, h.Period }).IsUnique(); + .HasKey(s => new { s.PhoneNumber, s.BillingPeriod}); OnModelCreatingPartial(modelBuilder); } diff --git a/PhoneAssistant.Model/Repositories/BaseReportRepository.cs b/PhoneAssistant.Model/Repositories/BaseReportRepository.cs deleted file mode 100644 index 5e28f718..00000000 --- a/PhoneAssistant.Model/Repositories/BaseReportRepository.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace PhoneAssistant.Model; - -public sealed class BaseReportRepository(PhoneAssistantDbContext dbContext) : IBaseReportRepository -{ - private readonly PhoneAssistantDbContext _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); - - public async Task> GetBaseReportAsync() - { - List report = await _dbContext.BaseReport.AsNoTracking().ToListAsync(); - return report; - } - - public async Task CreateAsync(BaseReport report) - { - _dbContext.BaseReport.Add(report); - await _dbContext.SaveChangesAsync(); - } - - public async Task GetSimNumberAsync(string phoneNumber) - { - BaseReport? report = await _dbContext.BaseReport.FindAsync(phoneNumber); - - return report?.SimNumber; - } - - public async Task TruncateAsync() - { - await _dbContext.Database.ExecuteSqlRawAsync("DELETE FROM BaseReport;"); - await _dbContext.Database.ExecuteSqlRawAsync("VACUUM;"); - } -} diff --git a/PhoneAssistant.Model/Repositories/IBaseReportRepository.cs b/PhoneAssistant.Model/Repositories/IBaseReportRepository.cs deleted file mode 100644 index 4aca614c..00000000 --- a/PhoneAssistant.Model/Repositories/IBaseReportRepository.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace PhoneAssistant.Model; -public interface IBaseReportRepository -{ - Task CreateAsync(BaseReport report); - Task> GetBaseReportAsync(); - Task GetSimNumberAsync(string phoneNumber); - Task TruncateAsync(); -} \ No newline at end of file diff --git a/PhoneAssistant.Model/Repositories/ImportHistoryRepository.cs b/PhoneAssistant.Model/Repositories/ImportHistoryRepository.cs index 2e907dd9..bbb902c1 100644 --- a/PhoneAssistant.Model/Repositories/ImportHistoryRepository.cs +++ b/PhoneAssistant.Model/Repositories/ImportHistoryRepository.cs @@ -6,18 +6,24 @@ public sealed class ImportHistoryRepository(PhoneAssistantDbContext dbContext) : { private readonly PhoneAssistantDbContext _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); - public async Task CreateAsync(ImportType importType, string file) + public async Task CreateAsync(ImportType importType, string run) { - ImportHistory importHistory = new() { Name = importType, File = file, ImportDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") }; + ImportHistory importHistory = new() { Name = importType, Run = run, ImportDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") }; _dbContext.Imports.Add(importHistory); await _dbContext.SaveChangesAsync(); return importHistory; } + public async Task RunExistsAsync(ImportType importType, string run) + { + ImportHistory? importHistory = await _dbContext.Imports.FirstOrDefaultAsync(i => i.Name == importType && i.Run == run); + return importHistory is not null; + } + public async Task GetLatestImportAsync(ImportType importType) { return await _dbContext.Imports - .OrderByDescending(i => i.ImportDate) + .OrderByDescending(i => i.Run) .FirstOrDefaultAsync(i => i.Name == importType); } } diff --git a/PhoneAssistant.Model/Repositories/SimRepository.cs b/PhoneAssistant.Model/Repositories/SimRepository.cs new file mode 100644 index 00000000..c039d69f --- /dev/null +++ b/PhoneAssistant.Model/Repositories/SimRepository.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore; + +namespace PhoneAssistant.Model; + +public interface ISimRepository +{ + Task CreateAsync(Sim sim); + Task> GetSim(string phoneNumber); + Task GetSimNumberAsync(string phoneNumber); +} + +public sealed class SimRepository(PhoneAssistantDbContext dbContext) : ISimRepository +{ + public async Task CreateAsync(Sim sim) + { + dbContext.Sims.Add(sim); + await dbContext.SaveChangesAsync(); + } + + public async Task> GetSim(string phoneNumber) + { + IEnumerable sims = await dbContext.Sims + .Where(p => p.PhoneNumber == phoneNumber) + .OrderByDescending(p => p.BillingPeriod) + .AsNoTracking() + .ToListAsync(); + return sims; + } + + public async Task GetSimNumberAsync(string phoneNumber) + { + Sim? sim = await dbContext.Sims.FindAsync(phoneNumber); + + return sim?.SIMNumber; + } +} diff --git a/PhoneAssistant.Tests/Features/AddItem/AddItemViewModelTests.cs b/PhoneAssistant.Tests/Features/AddItem/AddItemViewModelTests.cs index 1dc8afee..d33e5095 100644 --- a/PhoneAssistant.Tests/Features/AddItem/AddItemViewModelTests.cs +++ b/PhoneAssistant.Tests/Features/AddItem/AddItemViewModelTests.cs @@ -36,7 +36,7 @@ public async Task AddItemViewModel_DefaultOEMAndModel() [Test] public async Task CanSavePhone_ShouldBeEnabled_WhenNoErrors_WithPhoneHasSim() { - Mock sims = _mocker.GetMock(); + Mock sims = _mocker.GetMock(); #pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. sims.Setup(r => r.GetSimNumberAsync("07123456789")).ReturnsAsync((string)null); #pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. @@ -87,58 +87,6 @@ public async Task CanSavePhone_ShouldBeEnabled_WhenNoErrors_WithPhoneOnly() await Assert.That(sut.CanSavePhone()).IsTrue(); } - //[Test] - //public async Task Imei_should_not_have_Error_when_present() - //{ - // AddItemViewModel sut = _mocker.CreateInstance(); - - // sut.Imei = "355808981147090"; - - // await Assert.That(sut.GetErrors(nameof(sut.Imei))).IsEmpty(); - //} - - //[Test] - //[Arguments(null)] - //[Arguments("")] - //public async Task PhoneNumber_should_not_have_Error_when_Null_or_empty(string? actual) - //{ - // AddItemViewModel sut = _mocker.CreateInstance(); - - // sut.PhoneNumber = actual; - - // await Assert.That(sut.GetErrors(nameof(sut.PhoneNumber))).IsEmpty(); - //} - - //[Test] - //public async Task PhoneNumber_should_not_have_Error_when_present() - //{ - // AddItemViewModel sut = _mocker.CreateInstance(); - - // sut.PhoneNumber = "07123456789"; - - // await Assert.That(sut.GetErrors(nameof(sut.PhoneNumber))).IsEmpty(); - //} - - //[Test] - //public async Task GetErrors_ShouldBeEmpty_WhenSimNumberNullAsync() - //{ - // AddItemViewModel sut = _mocker.CreateInstance(); - - // sut.SimNumber = null; - - // await Assert.That(sut.GetErrors(nameof(sut.SimNumber))).IsEmpty(); - //} - - //[Test] - //public async Task GetErrors_ShouldBeEmpty_WhenSimNumberSetAsync() - //{ - // AddItemViewModel sut = _mocker.CreateInstance(); - - // sut.SimNumber = "8944122605566849402"; - - // await Assert.That(sut.GetErrors(nameof(sut.SimNumber))).IsEmpty(); - //} - [Test] public async Task LoadAsync_ShouldReturn_TaskCompleted() { @@ -168,7 +116,7 @@ public async Task OEM_should_change_Model(Manufacturer oem, string model) [Test] public async Task OnPhoneNumberChanged_ShouldSetSimNumber_WhenSimExistsAsync() { - Mock repository = _mocker.GetMock(); + Mock repository = _mocker.GetMock(); repository.Setup(r => r.GetSimNumberAsync("07123456789")).ReturnsAsync("sim number"); _ = MockValidator(); AddItemViewModel sut = _mocker.CreateInstance(); diff --git a/PhoneAssistant.Tests/Features/Phones/PhonesMainViewModelTests.cs b/PhoneAssistant.Tests/Features/Phones/PhonesMainViewModelTests.cs index 8380f8f0..f56330c2 100644 --- a/PhoneAssistant.Tests/Features/Phones/PhonesMainViewModelTests.cs +++ b/PhoneAssistant.Tests/Features/Phones/PhonesMainViewModelTests.cs @@ -279,11 +279,11 @@ public async Task LoadAsync_should_call_GetActivePhones_when_IncludeDisposals_fa Mock repository = mocker.GetMock(); repository.Setup(r => r.GetActivePhonesAsync()).ReturnsAsync(() => { return phones; }); repository.Setup(r => r.GetAllPhonesAsync()).ReturnsAsync(() => { return phones; }); - Mock baseReport = mocker.GetMock(); + Mock simRepository = mocker.GetMock(); Mock messenger = mocker.GetMock(); Mock factory = mocker.GetMock(); factory.Setup(r => r.Create(It.IsAny())) - .Returns((Phone p) => new PhonesItemViewModel(settings.Object, baseReport.Object, repository.Object, messenger.Object, p)); + .Returns((Phone p) => new PhonesItemViewModel(settings.Object, repository.Object, simRepository.Object, messenger.Object, p)); PhonesMainViewModel vm = mocker.CreateInstance(); //vm.IncludeDisposals = false; @@ -309,11 +309,11 @@ public async Task LoadAsync_should_call_GetAllPhones_when_IncludeDisposals_true( Mock repository = mocker.GetMock(); repository.Setup(r => r.GetActivePhonesAsync()).ReturnsAsync(() => { return phones; }); repository.Setup(r => r.GetAllPhonesAsync()).ReturnsAsync(() => { return phones; }); - Mock baseReport = mocker.GetMock(); + Mock simRepository = mocker.GetMock(); Mock messenger = mocker.GetMock(); Mock factory = mocker.GetMock(); factory.Setup(r => r.Create(It.IsAny())) - .Returns((Phone p) => new PhonesItemViewModel(settings.Object, baseReport.Object, repository.Object, messenger.Object, p)); + .Returns((Phone p) => new PhonesItemViewModel(settings.Object, repository.Object, simRepository.Object, messenger.Object, p)); PhonesMainViewModel vm = mocker.CreateInstance(); vm.IncludeDisposals = true; @@ -352,12 +352,12 @@ public async Task SelectedPhone_should_leave_ConcurrentUpdateWarning_as_collapse Mock settings = mocker.GetMock(); Mock repository = mocker.GetMock(); repository.Setup(r => r.ConcurrentChange("1", "lastupdate")).ReturnsAsync(false); - Mock baseReport = mocker.GetMock(); + Mock simRepository = mocker.GetMock(); Mock messenger = mocker.GetMock(); PhonesMainViewModel vm = mocker.CreateInstance(); await Assert.That(vm.UpdateWarningMessage).IsNull(); - vm.SelectedPhone = new PhonesItemViewModel(settings.Object, baseReport.Object, repository.Object, messenger.Object, + vm.SelectedPhone = new PhonesItemViewModel(settings.Object, repository.Object, simRepository.Object, messenger.Object, new Phone() { Imei = "1", AssetTag = "Tag A1", Model = "", Condition = "", OEM = Manufacturer.Apple, Status = "In Stock", LastUpdate = "lastupdate" }); repository.Verify(r => r.ConcurrentChange("1", "lastupdate"), Times.Once()); @@ -371,12 +371,12 @@ public async Task SelectedPhone_should_set_ConcurrentUpdateWarning_visible_when_ Mock settings = mocker.GetMock(); Mock repository = mocker.GetMock(); repository.Setup(r => r.ConcurrentChange("1", "lastupdate")).ReturnsAsync(true); - Mock baseReport = mocker.GetMock(); + Mock simRepository = mocker.GetMock(); Mock messenger = mocker.GetMock(); PhonesMainViewModel vm = mocker.CreateInstance(); await Assert.That(vm.UpdateWarningMessage).IsNull(); - vm.SelectedPhone = new PhonesItemViewModel(settings.Object, baseReport.Object, repository.Object, messenger.Object, + vm.SelectedPhone = new PhonesItemViewModel(settings.Object, repository.Object, simRepository.Object, messenger.Object, new Phone() { Imei = "1", AssetTag = "Tag A1", Model = "", Condition = "", OEM = Manufacturer.Apple, Status = "In Stock", LastUpdate="lastupdate" }); repository.Verify(r => r.ConcurrentChange("1", "lastupdate"), Times.Once()); @@ -392,11 +392,11 @@ private static PhonesMainViewModel ViewModelMockSetup(List phones) Mock repository = mocker.GetMock(); repository.Setup(r => r.GetActivePhonesAsync()).ReturnsAsync(phones); repository.Setup(r => r.GetAllPhonesAsync()).ReturnsAsync(phones); - Mock baseReport = mocker.GetMock(); + Mock simRepository = mocker.GetMock(); Mock messenger = mocker.GetMock(); Mock factory = mocker.GetMock(); factory.Setup(r => r.Create(It.IsAny())) - .Returns(() => new PhonesItemViewModel(settings.Object, baseReport.Object, repository.Object, messenger.Object, phones[index])) + .Returns(() => new PhonesItemViewModel(settings.Object, repository.Object, simRepository.Object, messenger.Object, phones[index])) .Callback(() => index++); PhonesMainViewModel vm = mocker.CreateInstance(); diff --git a/PhoneAssistant.Tests/Features/Sims/SimsMainViewModelTests.cs b/PhoneAssistant.Tests/Features/Sims/SimsMainViewModelTests.cs index dd1b7bb4..89c3c666 100644 --- a/PhoneAssistant.Tests/Features/Sims/SimsMainViewModelTests.cs +++ b/PhoneAssistant.Tests/Features/Sims/SimsMainViewModelTests.cs @@ -62,7 +62,7 @@ public async Task HasErrors_should_be_true_when_required_fields_missing() public async Task PhoneNumber_changed_should_set_SimNumber_when_SIM_exists() { MockValidator(); - Mock baseRepository = _mocker.GetMock(); + Mock baseRepository = _mocker.GetMock(); baseRepository.Setup(r => r.GetSimNumberAsync("01234567890")).ReturnsAsync("sim number"); var vm = _mocker.CreateInstance(); diff --git a/PhoneAssistant.WPF/Application/ApplicationServicesExtension.cs b/PhoneAssistant.WPF/Application/ApplicationServicesExtension.cs index da96ff4d..b87950a5 100644 --- a/PhoneAssistant.WPF/Application/ApplicationServicesExtension.cs +++ b/PhoneAssistant.WPF/Application/ApplicationServicesExtension.cs @@ -1,11 +1,8 @@ using CommunityToolkit.Mvvm.Messaging; - using FluentValidation; - using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; - using PhoneAssistant.Model; using PhoneAssistant.WPF.Features.AddItem; using PhoneAssistant.WPF.Features.BaseReport; @@ -47,11 +44,10 @@ public static IHostBuilder ConfigureApplicationServices(this IHostBuilder host) new UpdateOptions() { AllowVersionDowngrade = true })); // Repositories - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - + services.AddSingleton(); // Features services.AddSingleton(); diff --git a/PhoneAssistant.WPF/Features/AddItem/AddItemViewModel.cs b/PhoneAssistant.WPF/Features/AddItem/AddItemViewModel.cs index 4f551d61..30d513fa 100644 --- a/PhoneAssistant.WPF/Features/AddItem/AddItemViewModel.cs +++ b/PhoneAssistant.WPF/Features/AddItem/AddItemViewModel.cs @@ -1,13 +1,9 @@ -using System.Collections.ObjectModel; -using System.ComponentModel.DataAnnotations; -using System.Text.RegularExpressions; - using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; - using PhoneAssistant.Model; using PhoneAssistant.WPF.Shared; +using System.Collections.ObjectModel; namespace PhoneAssistant.WPF.Features.AddItem; @@ -18,20 +14,20 @@ public interface IAddItemViewModel : IViewModel public sealed partial class AddItemViewModel : ValidatableViewModel, IAddItemViewModel { private readonly IApplicationSettingsRepository _appSettings; - private readonly IBaseReportRepository _baseReportRepository; private readonly IMessenger _messenger; private readonly IPhonesRepository _phonesRepository; + private readonly ISimRepository _simRepository; public ObservableCollection LogItems { get; } = []; public AddItemViewModel(IPhonesRepository phonesRepository, - IBaseReportRepository baseReportRepository, IApplicationSettingsRepository appSettings, + ISimRepository simRepository, IMessenger messenger, IServiceProvider serviceProvider) : base(serviceProvider) { _phonesRepository = phonesRepository ?? throw new ArgumentNullException(nameof(phonesRepository)); - _baseReportRepository = baseReportRepository ?? throw new ArgumentNullException(nameof(baseReportRepository)); + _simRepository = simRepository ?? throw new ArgumentNullException(nameof(simRepository)); _appSettings = appSettings ?? throw new ArgumentNullException(nameof(appSettings)); _messenger = messenger ?? throw new ArgumentNullException(nameof(messenger)); OEM = Manufacturer.Apple; @@ -168,7 +164,7 @@ async partial void OnPhoneNumberChanged(string? value) await ValidatePropertyAsync(nameof(PhoneNumber)); - string? simNumber = await _baseReportRepository.GetSimNumberAsync(value); + string? simNumber = await _simRepository.GetSimNumberAsync(value); if (simNumber is null) return; diff --git a/PhoneAssistant.WPF/Features/BaseReport/BaseReportMainView.xaml b/PhoneAssistant.WPF/Features/BaseReport/BaseReportMainView.xaml index dc58c291..554209ab 100644 --- a/PhoneAssistant.WPF/Features/BaseReport/BaseReportMainView.xaml +++ b/PhoneAssistant.WPF/Features/BaseReport/BaseReportMainView.xaml @@ -28,13 +28,6 @@ -