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");
+ }
+}