From 122f997696a2414426a8a4db609718f64f81260f Mon Sep 17 00:00:00 2001 From: ChrisK00 Date: Thu, 7 Dec 2023 21:53:38 +0100 Subject: [PATCH 1/2] import --- subtrack.MAUI/MauiProgram.cs | 1 + subtrack.MAUI/Pages/Settings.razor | 27 ++++++++-- .../Abstractions/ISubscriptionService.cs | 1 + .../Abstractions/ISubscriptionsImporter.cs | 8 +++ subtrack.MAUI/Services/SubscriptionService.cs | 25 +++++++-- .../Services/SubscriptionsImporter.cs | 31 +++++++++++ subtrack.MAUI/Utilities/CsvMappings.cs | 14 +++++ .../Integration/SubscriptionImporterTests.cs | 53 +++++++++++++++++++ 8 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 subtrack.MAUI/Services/Abstractions/ISubscriptionsImporter.cs create mode 100644 subtrack.MAUI/Services/SubscriptionsImporter.cs create mode 100644 subtrack.MAUI/Utilities/CsvMappings.cs create mode 100644 subtrack.Tests/Integration/SubscriptionImporterTests.cs diff --git a/subtrack.MAUI/MauiProgram.cs b/subtrack.MAUI/MauiProgram.cs index 6a42661..2de976d 100644 --- a/subtrack.MAUI/MauiProgram.cs +++ b/subtrack.MAUI/MauiProgram.cs @@ -54,6 +54,7 @@ public static IServiceCollection AddSubtrackServices(this IServiceCollection ser services .AddDbContext(opt => opt.UseSqlite(dbConnectionString)) .AddScoped() + .AddScoped() .AddScoped() .AddScoped() .AddScoped() diff --git a/subtrack.MAUI/Pages/Settings.razor b/subtrack.MAUI/Pages/Settings.razor index 81f4fea..eacab2e 100644 --- a/subtrack.MAUI/Pages/Settings.razor +++ b/subtrack.MAUI/Pages/Settings.razor @@ -1,5 +1,6 @@ @page "/settings" +@inject ISubscriptionsImporter subscriptionImporter @inject ISubscriptionService subscriptionService @using CsvHelper @@ -7,9 +8,12 @@
Import
- +
+ +

@_importResult

+
Last Export: @@ -20,19 +24,36 @@
@code { + private string _importResult = string.Empty; + private async Task ExportSubscriptions() { using var csv = new StringWriter(); using var csvWriter = new CsvWriter(csv, CultureInfo.InvariantCulture); + csvWriter.Context.RegisterClassMap(); var subscriptions = await subscriptionService.GetAllAsync(); - csvWriter.WriteRecords(subscriptions); + await csvWriter.WriteRecordsAsync(subscriptions); await Share.Default.RequestAsync(new ShareTextRequest { Title = "subtrack subs", - Subject = $"subtrack-subs_{DateTime.Now:dd-MM-yyyy-HH.mm}.csv", + Subject = $"subtrack-subs_{DateTime.Now:dd-MM-yyyy}.csv", Text = csv.ToString() }); } + + private async Task ImportSubscriptions(InputFileChangeEventArgs e) + { + try + { + var maxFileSizeInBytes = (long)Math.Round(1.Megabytes().Bytes); + var importedSubscriptions = await subscriptionImporter.ImportFromCsvAsync(e.File.OpenReadStream(maxAllowedSize: maxFileSizeInBytes)); + _importResult = $"imported {importedSubscriptions.Count} subscriptions"; + } + catch (Exception ex) + { + _importResult = ex.Message; + } + } } diff --git a/subtrack.MAUI/Services/Abstractions/ISubscriptionService.cs b/subtrack.MAUI/Services/Abstractions/ISubscriptionService.cs index 58eef58..bba2a4d 100644 --- a/subtrack.MAUI/Services/Abstractions/ISubscriptionService.cs +++ b/subtrack.MAUI/Services/Abstractions/ISubscriptionService.cs @@ -7,6 +7,7 @@ public interface ISubscriptionService { Task> GetAllAsync(GetSubscriptionsFilter? filter = null); Task CreateSubscriptionAsync(Subscription subscriptionToCreate); + Task> CreateSubscriptionsAsync(IEnumerable subscriptionsToCreate); Task GetByIdIfExists(int id); Task Delete(int id); Task Update(Subscription subscriptionToUpdate); diff --git a/subtrack.MAUI/Services/Abstractions/ISubscriptionsImporter.cs b/subtrack.MAUI/Services/Abstractions/ISubscriptionsImporter.cs new file mode 100644 index 0000000..426a7a7 --- /dev/null +++ b/subtrack.MAUI/Services/Abstractions/ISubscriptionsImporter.cs @@ -0,0 +1,8 @@ +using subtrack.DAL.Entities; + +namespace subtrack.MAUI.Services.Abstractions; + +public interface ISubscriptionsImporter +{ + Task> ImportFromCsvAsync(Stream content); +} diff --git a/subtrack.MAUI/Services/SubscriptionService.cs b/subtrack.MAUI/Services/SubscriptionService.cs index 692ca92..bf3dbad 100644 --- a/subtrack.MAUI/Services/SubscriptionService.cs +++ b/subtrack.MAUI/Services/SubscriptionService.cs @@ -66,15 +66,26 @@ public async Task Delete(int id) public async Task CreateSubscriptionAsync(Subscription subscriptionToCreate) { - subscriptionToCreate.LastPayment = subscriptionToCreate.LastPayment.Date; - subscriptionToCreate.FirstPaymentDay = subscriptionToCreate.LastPayment.Day; - AutoPay(subscriptionToCreate); + SetupNewSubscription(subscriptionToCreate); await _context.Subscriptions.AddAsync(subscriptionToCreate); await _context.SaveChangesAsync(); - return subscriptionToCreate; } + public async Task> CreateSubscriptionsAsync(IEnumerable subscriptionsToCreate) + { + var newSubscriptions = subscriptionsToCreate.Select(s => + { + SetupNewSubscription(s); + return s; + }).ToArray(); + + await _context.Subscriptions.AddRangeAsync(newSubscriptions); + await _context.SaveChangesAsync(); + + return newSubscriptions; + } + public async Task MarkNextPaymentAsPaidAsync(int subscriptionId) { var sub = await GetByIdAsync(subscriptionId); @@ -93,6 +104,12 @@ public async Task AutoPayAsync(int subscriptionId) return subscription; } + private void SetupNewSubscription(Subscription subscriptionToCreate) + { + subscriptionToCreate.LastPayment = subscriptionToCreate.LastPayment.Date; + subscriptionToCreate.FirstPaymentDay = subscriptionToCreate.LastPayment.Day; + AutoPay(subscriptionToCreate); + } private void AutoPay(Subscription subscription) { if (!subscription.IsAutoPaid) diff --git a/subtrack.MAUI/Services/SubscriptionsImporter.cs b/subtrack.MAUI/Services/SubscriptionsImporter.cs new file mode 100644 index 0000000..bc5b551 --- /dev/null +++ b/subtrack.MAUI/Services/SubscriptionsImporter.cs @@ -0,0 +1,31 @@ +using CsvHelper; +using subtrack.DAL.Entities; +using subtrack.MAUI.Services.Abstractions; +using subtrack.MAUI.Utilities; +using System.Globalization; + +namespace subtrack.MAUI.Services; +internal class SubscriptionsImporter : ISubscriptionsImporter +{ + private readonly ISubscriptionService _subscriptionService; + + public SubscriptionsImporter(ISubscriptionService subscriptionService) + { + _subscriptionService = subscriptionService; + } + + public async Task> ImportFromCsvAsync(Stream content) + { + using var csv = new StreamReader(content); + using var csvReader = new CsvReader(csv, CultureInfo.InvariantCulture); + csvReader.Context.RegisterClassMap(); + + var subscriptionsToImport = new List(); + await foreach (var subscription in csvReader.GetRecordsAsync()) + { + subscriptionsToImport.Add(subscription); + } + + return await _subscriptionService.CreateSubscriptionsAsync(subscriptionsToImport); + } +} diff --git a/subtrack.MAUI/Utilities/CsvMappings.cs b/subtrack.MAUI/Utilities/CsvMappings.cs new file mode 100644 index 0000000..f93fc68 --- /dev/null +++ b/subtrack.MAUI/Utilities/CsvMappings.cs @@ -0,0 +1,14 @@ +using CsvHelper.Configuration; +using subtrack.DAL.Entities; +using System.Globalization; + +namespace subtrack.MAUI.Utilities; + +public class SubscriptionCsvMapping : ClassMap +{ + public SubscriptionCsvMapping() + { + AutoMap(CultureInfo.InvariantCulture); + Map(m => m.Id).Ignore(); + } +} diff --git a/subtrack.Tests/Integration/SubscriptionImporterTests.cs b/subtrack.Tests/Integration/SubscriptionImporterTests.cs new file mode 100644 index 0000000..9d464a5 --- /dev/null +++ b/subtrack.Tests/Integration/SubscriptionImporterTests.cs @@ -0,0 +1,53 @@ +using CsvHelper.TypeConversion; +using System.Text; + +namespace subtrack.Tests.Integration; + +public class SubscriptionImporterTests : IntegrationTestBase +{ + private ISubscriptionsImporter _sut; + + public SubscriptionImporterTests() + { + _sut = _serviceProvider.GetRequiredService(); + } + + [Fact] + public async Task ImportCsv_InvalidCsv_ShouldThrow() + { + var csvText = @"Name,Description,IsAutoPaid,Cost,FirstPaymentDay,LastPayment,BillingOccurrence,BillingInterval,PrimaryColor,Icon,SecondaryColor +Subscription1,Description1,True,29.99,1,2023-12-01,Monthly,1,,,,,, +"; + var csv = new MemoryStream(Encoding.UTF8.GetBytes(csvText)); + await Assert.ThrowsAsync(async () => await _sut.ImportFromCsvAsync(csv)); + } + + [Fact] + public async Task ImportCsv_EmptyValuesForOptionalProperties_ShouldReturnImportedItems() + { + var csvText = @"Name,Description,IsAutoPaid,Cost,FirstPaymentDay,LastPayment,BillingOccurrence,BillingInterval,PrimaryColor,Icon,SecondaryColor +Subscription1,Description1,True,29.99,1,2023-12-01,Month,1,,,,,, +Subscription2,,True,29.99,1,2023-12-01,Month,1,,,,,, +"; + var csv = new MemoryStream(Encoding.UTF8.GetBytes(csvText)); + + var importedSubs = await _sut.ImportFromCsvAsync(csv); + + Assert.Equal(2, importedSubs.Count); + } + + [Fact] + public async Task ImportCsv_WithSameDetailsAsExisting_ShouldReturnImportedSubs() + { + var existingSubscription = CreateSubscription(DateTime.Today, description: null); + var csvText = $@"Name,Description,IsAutoPaid,Cost,FirstPaymentDay,LastPayment,BillingOccurrence,BillingInterval,PrimaryColor,Icon,SecondaryColor +{existingSubscription.Name},,{existingSubscription.IsAutoPaid},{existingSubscription.Cost},{existingSubscription.FirstPaymentDay},{existingSubscription.LastPayment},{existingSubscription.BillingOccurrence},{existingSubscription.BillingInterval},,,,,, +Subscription1,,True,29.99,1,2023-12-01,Month,1,,,,,, +Subscription2,,False,29.99,1,2022-12-01,Week,1,,,,,, +"; + var csv = new MemoryStream(Encoding.UTF8.GetBytes(csvText)); + var importedSubs = await _sut.ImportFromCsvAsync(csv); + + Assert.Equal(3, importedSubs.Count); + } +} From 1b03cf149c2b5f7a1fc0e294633bd214d9d6b373 Mon Sep 17 00:00:00 2001 From: ChrisK00 Date: Thu, 7 Dec 2023 21:55:11 +0100 Subject: [PATCH 2/2] formatting --- subtrack.Tests/Integration/SubscriptionImporterTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/subtrack.Tests/Integration/SubscriptionImporterTests.cs b/subtrack.Tests/Integration/SubscriptionImporterTests.cs index 9d464a5..ffbab8e 100644 --- a/subtrack.Tests/Integration/SubscriptionImporterTests.cs +++ b/subtrack.Tests/Integration/SubscriptionImporterTests.cs @@ -19,6 +19,7 @@ public async Task ImportCsv_InvalidCsv_ShouldThrow() Subscription1,Description1,True,29.99,1,2023-12-01,Monthly,1,,,,,, "; var csv = new MemoryStream(Encoding.UTF8.GetBytes(csvText)); + await Assert.ThrowsAsync(async () => await _sut.ImportFromCsvAsync(csv)); } @@ -46,6 +47,7 @@ public async Task ImportCsv_WithSameDetailsAsExisting_ShouldReturnImportedSubs() Subscription2,,False,29.99,1,2022-12-01,Week,1,,,,,, "; var csv = new MemoryStream(Encoding.UTF8.GetBytes(csvText)); + var importedSubs = await _sut.ImportFromCsvAsync(csv); Assert.Equal(3, importedSubs.Count);