diff --git a/Guppi.Console/Guppi.Console.csproj b/Guppi.Console/Guppi.Console.csproj index 0bb13ba..4422e7c 100644 --- a/Guppi.Console/Guppi.Console.csproj +++ b/Guppi.Console/Guppi.Console.csproj @@ -13,7 +13,7 @@ https://github.com/rprouse/guppi https://github.com/rprouse/guppi dotnet-guppi - 6.2.1 + 6.3.0 true guppi ./nupkg diff --git a/Guppi.Console/Program.cs b/Guppi.Console/Program.cs index c8a0e56..cb5925a 100644 --- a/Guppi.Console/Program.cs +++ b/Guppi.Console/Program.cs @@ -13,6 +13,7 @@ static IServiceProvider ConfigureServices() => .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient() diff --git a/Guppi.Console/Properties/launchSettings.json b/Guppi.Console/Properties/launchSettings.json index 9a590db..23f73bb 100644 --- a/Guppi.Console/Properties/launchSettings.json +++ b/Guppi.Console/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Guppi.Console": { "commandName": "Project", - "commandLineArgs": "cal agenda -t" + "commandLineArgs": "bills alectra" } } } diff --git a/Guppi.Console/Skills/BillsSkill.cs b/Guppi.Console/Skills/BillsSkill.cs new file mode 100644 index 0000000..757caf4 --- /dev/null +++ b/Guppi.Console/Skills/BillsSkill.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.NamingConventionBinder; +using System.Threading.Tasks; +using Guppi.Core.Exceptions; +using Guppi.Core.Extensions; +using Guppi.Core.Interfaces.Services; +using Spectre.Console; + +namespace Guppi.Console.Skills; + +internal class BillsSkill(IBillService service) : ISkill +{ + private readonly IBillService _service = service; + + public IEnumerable GetCommands() + { + var alectra = new Command("alectra", "Download bills from Alectra") + { + }; + alectra.Handler = CommandHandler.Create(async () => await DownloadAlectraBills()); + + var configure = new Command("configure", "Configures the Bill provider"); + configure.AddAlias("config"); + configure.Handler = CommandHandler.Create(() => Configure()); + + var command = new Command("bills", "Download bills from online") + { + alectra, + configure + }; + command.AddAlias("billing"); + command.AddAlias("bill"); + + return new List { command }; + } + + private async Task DownloadAlectraBills() + { + try + { + AnsiConsoleHelper.TitleRule(":high_voltage: Alectra Bills"); + + await _service.DownloadAlectraBills(); + + AnsiConsoleHelper.Rule("white"); + } + catch (UnconfiguredException ue) + { + AnsiConsole.MarkupLine($"[yellow][[:yellow_circle: {ue.Message}]][/]"); + } + catch (UnauthorizedException ue) + { + AnsiConsole.MarkupLine($"[red][[:cross_mark: ${ue.Message}]][/]"); + } + } + + private void Configure() => _service.Configure(); +} diff --git a/Guppi.Core/Configurations/BillConfiguration.cs b/Guppi.Core/Configurations/BillConfiguration.cs new file mode 100644 index 0000000..e3bb574 --- /dev/null +++ b/Guppi.Core/Configurations/BillConfiguration.cs @@ -0,0 +1,18 @@ +using Guppi.Core.Attributes; + +namespace Guppi.Core.Configurations; + +public class BillConfiguration : Configuration +{ + [Display("Alectra Username")] + public string AlectraUsername { get; set; } + + [Display("Alectra Password")] + public string AlectraPassword { get; set; } + + [Display("Enbridge Username")] + public string EnbridgeUsername { get; set; } + + [Display("Enbridge Password")] + public string EnbridgePassword { get; set; } +} diff --git a/Guppi.Core/Configurations/StravaConfiguration.cs b/Guppi.Core/Configurations/StravaConfiguration.cs index 86f4c87..7010238 100644 --- a/Guppi.Core/Configurations/StravaConfiguration.cs +++ b/Guppi.Core/Configurations/StravaConfiguration.cs @@ -1,17 +1,15 @@ -using Guppi.Core; using Guppi.Core.Attributes; -namespace Guppi.Core.Configurations +namespace Guppi.Core.Configurations; + +public class StravaConfiguration : Configuration { - public class StravaConfiguration : Configuration - { - [Display("Client Id")] - public string ClientId { get; set; } + [Display("Client Id")] + public string ClientId { get; set; } - [Display("Client Secret")] - public string ClientSecret { get; set; } + [Display("Client Secret")] + public string ClientSecret { get; set; } - [Hide] - public string RefreshToken { get; set; } - } + [Hide] + public string RefreshToken { get; set; } } diff --git a/Guppi.Core/DependencyInjection.cs b/Guppi.Core/DependencyInjection.cs index f27d54a..b3636a3 100644 --- a/Guppi.Core/DependencyInjection.cs +++ b/Guppi.Core/DependencyInjection.cs @@ -45,6 +45,7 @@ public static IServiceCollection AddCore(this IServiceCollection services) // Add the services .AddTransient() .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient() diff --git a/Guppi.Core/Guppi.Core.csproj b/Guppi.Core/Guppi.Core.csproj index 08b380f..15d2bf7 100644 --- a/Guppi.Core/Guppi.Core.csproj +++ b/Guppi.Core/Guppi.Core.csproj @@ -9,11 +9,13 @@ + + diff --git a/Guppi.Core/Interfaces/Services/IBillService.cs b/Guppi.Core/Interfaces/Services/IBillService.cs new file mode 100644 index 0000000..397d5e6 --- /dev/null +++ b/Guppi.Core/Interfaces/Services/IBillService.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Guppi.Core.Interfaces.Services; + +public interface IBillService +{ + Task DownloadAlectraBills(); + void Configure(); +} diff --git a/Guppi.Core/Services/BillService.cs b/Guppi.Core/Services/BillService.cs new file mode 100644 index 0000000..f58a04f --- /dev/null +++ b/Guppi.Core/Services/BillService.cs @@ -0,0 +1,154 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using ClosedXML.Excel; +using Guppi.Core.Configurations; +using Guppi.Core.Exceptions; +using Guppi.Core.Interfaces.Services; +using Microsoft.Playwright; + +namespace Guppi.Core.Services; + +internal class BillService : IBillService +{ + private readonly BillConfiguration _configuration = Configuration.Load("billing"); + + readonly static string _downloadsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads", "bills"); + + int _row = 2; + IXLWorksheet _worksheet; + + private bool Configured => _configuration.Configured; + + public async Task DownloadAlectraBills() + { + if (!Configured) + { + throw new UnconfiguredException("Please configure the Billing provider"); + } + + // Create an Excel spreadsheet to store the billing data + string path = Path.Combine(_downloadsPath, "Bills.xlsx"); + if (File.Exists(path)) + { + File.Delete(path); + } + var workbook = new ClosedXML.Excel.XLWorkbook(); + _worksheet = workbook.Worksheets.Add("Bills"); + + _worksheet.Cell(1, 1).Value = "Account"; + _worksheet.Cell(1, 2).Value = "Date"; + _worksheet.Cell(1, 3).Value = "Amount"; + + using var playwright = await Playwright.CreateAsync(); + var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = false }); + var context = await browser.NewContextAsync(); + var page = await context.NewPageAsync(); + + // Login to Alectra + await page.GotoAsync("https://myalectra.alectrautilities.com/portal/#/login"); + await page.GetByLabel("Please enter Username").FillAsync(_configuration.AlectraUsername); + await page.GetByLabel("Please enter your password.").FillAsync(_configuration.AlectraPassword); + await page.GetByLabel("Click Here to Sign In").ClickAsync(); + + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + await DownloadBillsForAccount(page, "8501783878"); + await DownloadBillsForAccount(page, "7030931444"); + await DownloadBillsForAccount(page, "9676981145"); + await DownloadBillsForAccount(page, "7076520332"); + + await Task.Delay(5000); + + workbook.SaveAs(path); + } + + private async Task DownloadBillsForAccount(IPage page, string account) + { + // Navigate to Billing History + await page.GotoAsync("https://myalectra.alectrautilities.com/portal/#/billinghistory"); + + // Wait for the bills to load + await page.WaitForSelectorAsync("tr.billing-history-row"); + + // Click the account selector dropdown + var dropdown = await page.QuerySelectorAsync("button#accountSelect"); + await dropdown.ClickAsync(); + + // Click the account in the dropdown + var menu = await page.QuerySelectorAsync($"li[value=\"{account}\"]"); + await menu.ClickAsync(); + + // Wait for the billing history to load + await page.WaitForSelectorAsync("tr.billing-history-row"); + + // Download each bill + var rows = await page.QuerySelectorAllAsync("tr.billing-history-row"); + foreach (var row in rows) + { + var cells = await row.QuerySelectorAllAsync("td"); + var date = await cells[0].InnerTextAsync(); + var amount = await cells[1].InnerTextAsync(); + + if (DateTime.TryParse(date, out DateTime d)) + { + date = d.ToShortDateString(); + } + + if (double.TryParse(amount.Substring(1), out double a)) + { + amount = a.ToString("N2"); + } + + _worksheet.Cell(_row, 1).Value = account; + _worksheet.Cell(_row, 2).Value = date; + _worksheet.Cell(_row, 3).Value = amount; + _row++; + + Console.WriteLine($"{account} {date} {amount}"); + + await DownloadBill(page, account, $"{date} {amount}"); + } + } + + private async Task DownloadBill(IPage page, string account, string bill) + { + // Open the billing page in a new tab + var billingPage = await page.RunAndWaitForPopupAsync(async () => + { + await page.GetByRole(AriaRole.Checkbox, new() { Name = bill }).GetByLabel("Navigate to View Bill PDF").ClickAsync(); + }); + + // Listen for download events so we can specify the + billingPage.Download += async (_, download) => + { + // Ensure the directory exists + Directory.CreateDirectory(_downloadsPath); + + var filePath = Path.Combine(_downloadsPath, $"{account} {bill}.pdf"); + await download.SaveAsAsync(filePath); + + await billingPage.CloseAsync(); + }; + + // This closes the warning popup that appears when the download is initiated + async void billingPage_Dialog_EventHandler(object sender, IDialog dialog) + { + await dialog.AcceptAsync(); + billingPage.Dialog -= billingPage_Dialog_EventHandler; + } + + billingPage.Dialog += billingPage_Dialog_EventHandler; + + // Click the download button + await billingPage.RunAndWaitForDownloadAsync(async () => + { + await billingPage.GetByRole(AriaRole.Img, new() { Name = "Download PDF" }).ClickAsync(); + }); + } + public void Configure() + { + var configuration = Configuration.Load("billing"); + configuration.RunConfiguration("Billing", "Enter credentials for billing providers"); + } +}