Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Importing subscriptions #161

Merged
merged 3 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions subtrack.MAUI/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public static IServiceCollection AddSubtrackServices(this IServiceCollection ser
services
.AddDbContext<SubtrackDbContext>(opt => opt.UseSqlite(dbConnectionString))
.AddScoped<ISubscriptionService, SubscriptionService>()
.AddScoped<ISubscriptionsImporter, SubscriptionsImporter>()
.AddScoped<IDateProvider, DateProvider>()
.AddScoped<ISubscriptionsCalculator, SubscriptionsCalculator>()
.AddScoped<ISettingsService, SettingsService>()
Expand Down
27 changes: 24 additions & 3 deletions subtrack.MAUI/Pages/Settings.razor
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
@page "/settings"

@inject ISubscriptionsImporter subscriptionImporter
@inject ISubscriptionService subscriptionService

@using CsvHelper

<div class="d-flex justify-content-start mb-3">
<div class="p-2">Import</div>
<div class="p-2">
<InputFile />
<InputFile OnChange="ImportSubscriptions" />
</div>
</div>

<p>@_importResult</p>

<div class="d-flex justify-content-start mb-3">
<div class="p-2">
Last Export:
Expand All @@ -20,19 +24,36 @@
</div>

@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<SubscriptionCsvMapping>();

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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public interface ISubscriptionService
{
Task<IEnumerable<Subscription>> GetAllAsync(GetSubscriptionsFilter? filter = null);
Task<Subscription> CreateSubscriptionAsync(Subscription subscriptionToCreate);
Task<IReadOnlyCollection<Subscription>> CreateSubscriptionsAsync(IEnumerable<Subscription> subscriptionsToCreate);
Task<Subscription?> GetByIdIfExists(int id);
Task Delete(int id);
Task Update(Subscription subscriptionToUpdate);
Expand Down
8 changes: 8 additions & 0 deletions subtrack.MAUI/Services/Abstractions/ISubscriptionsImporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using subtrack.DAL.Entities;

namespace subtrack.MAUI.Services.Abstractions;

public interface ISubscriptionsImporter
{
Task<IReadOnlyCollection<Subscription>> ImportFromCsvAsync(Stream content);
}
25 changes: 21 additions & 4 deletions subtrack.MAUI/Services/SubscriptionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,26 @@ public async Task Delete(int id)

public async Task<Subscription> 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<IReadOnlyCollection<Subscription>> CreateSubscriptionsAsync(IEnumerable<Subscription> subscriptionsToCreate)
{
var newSubscriptions = subscriptionsToCreate.Select(s =>
{
SetupNewSubscription(s);
return s;
}).ToArray();

await _context.Subscriptions.AddRangeAsync(newSubscriptions);
await _context.SaveChangesAsync();

return newSubscriptions;
}

public async Task<Subscription> MarkNextPaymentAsPaidAsync(int subscriptionId)
{
var sub = await GetByIdAsync(subscriptionId);
Expand All @@ -93,6 +104,12 @@ public async Task<Subscription> 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)
Expand Down
31 changes: 31 additions & 0 deletions subtrack.MAUI/Services/SubscriptionsImporter.cs
Original file line number Diff line number Diff line change
@@ -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<IReadOnlyCollection<Subscription>> ImportFromCsvAsync(Stream content)
{
using var csv = new StreamReader(content);
using var csvReader = new CsvReader(csv, CultureInfo.InvariantCulture);
csvReader.Context.RegisterClassMap<SubscriptionCsvMapping>();

var subscriptionsToImport = new List<Subscription>();
await foreach (var subscription in csvReader.GetRecordsAsync<Subscription>())
{
subscriptionsToImport.Add(subscription);
}

return await _subscriptionService.CreateSubscriptionsAsync(subscriptionsToImport);
}
}
14 changes: 14 additions & 0 deletions subtrack.MAUI/Utilities/CsvMappings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using CsvHelper.Configuration;
using subtrack.DAL.Entities;
using System.Globalization;

namespace subtrack.MAUI.Utilities;

public class SubscriptionCsvMapping : ClassMap<Subscription>
{
public SubscriptionCsvMapping()
{
AutoMap(CultureInfo.InvariantCulture);
Map(m => m.Id).Ignore();
}
}
55 changes: 55 additions & 0 deletions subtrack.Tests/Integration/SubscriptionImporterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using CsvHelper.TypeConversion;
using System.Text;

namespace subtrack.Tests.Integration;

public class SubscriptionImporterTests : IntegrationTestBase
{
private ISubscriptionsImporter _sut;

public SubscriptionImporterTests()
{
_sut = _serviceProvider.GetRequiredService<ISubscriptionsImporter>();
}

[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<TypeConverterException>(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);
}
}
Loading