Skip to content

Commit

Permalink
Merge pull request #199 from rprouse/issue/198
Browse files Browse the repository at this point in the history
Download Alectra bills
  • Loading branch information
rprouse authored Oct 27, 2024
2 parents f0d2c89 + a2d4ec2 commit 3e09fd0
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 13 deletions.
2 changes: 1 addition & 1 deletion Guppi.Console/Guppi.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<PackageProjectUrl>https://github.com/rprouse/guppi</PackageProjectUrl>
<RepositoryUrl>https://github.com/rprouse/guppi</RepositoryUrl>
<PackageId>dotnet-guppi</PackageId>
<Version>6.2.1</Version>
<Version>6.3.0</Version>
<PackAsTool>true</PackAsTool>
<ToolCommandName>guppi</ToolCommandName>
<PackageOutputPath>./nupkg</PackageOutputPath>
Expand Down
1 change: 1 addition & 0 deletions Guppi.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ static IServiceProvider ConfigureServices() =>
.AddTransient<IApplication, Application>()
.AddTransient<ISkill, AdventOfCodeSkill>()
.AddTransient<ISkill, AsciiSkill>()
.AddTransient<ISkill, BillsSkill>()
.AddTransient<ISkill, CalendarSkill>()
.AddTransient<ISkill, DictionarySkill>()
.AddTransient<ISkill, GitSkill>()
Expand Down
2 changes: 1 addition & 1 deletion Guppi.Console/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"profiles": {
"Guppi.Console": {
"commandName": "Project",
"commandLineArgs": "cal agenda -t"
"commandLineArgs": "bills alectra"
}
}
}
60 changes: 60 additions & 0 deletions Guppi.Console/Skills/BillsSkill.cs
Original file line number Diff line number Diff line change
@@ -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<Command> 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> { 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();
}
18 changes: 18 additions & 0 deletions Guppi.Core/Configurations/BillConfiguration.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
20 changes: 9 additions & 11 deletions Guppi.Core/Configurations/StravaConfiguration.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
1 change: 1 addition & 0 deletions Guppi.Core/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public static IServiceCollection AddCore(this IServiceCollection services)
// Add the services
.AddTransient<IAdventOfCodeService, AdventOfCodeService>()
.AddTransient<IAsciiService, AsciiService>()
.AddTransient<IBillService, BillService>()
.AddTransient<ICalendarService, CalendarService>()
.AddTransient<IDictionaryService, DictionaryService>()
.AddTransient<IGitService, GitService>()
Expand Down
2 changes: 2 additions & 0 deletions Guppi.Core/Guppi.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.104.1" />
<PackageReference Include="Google.Apis.Tasks.v1" Version="1.68.0.3468" />
<PackageReference Include="Google.Apis.Calendar.v3" Version="1.68.0.3557" />
<PackageReference Include="Ical.Net" Version="4.3.1" />
<PackageReference Include="LibGit2Sharp" Version="0.30.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="Microsoft.Playwright" Version="1.48.0" />
<PackageReference Include="OpenAI" Version="2.0.0" />
<PackageReference Include="Spectre.Console" Version="0.49.1" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
Expand Down
9 changes: 9 additions & 0 deletions Guppi.Core/Interfaces/Services/IBillService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Threading.Tasks;

namespace Guppi.Core.Interfaces.Services;

public interface IBillService
{
Task DownloadAlectraBills();
void Configure();
}
154 changes: 154 additions & 0 deletions Guppi.Core/Services/BillService.cs
Original file line number Diff line number Diff line change
@@ -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<BillConfiguration>("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<BillConfiguration>("billing");
configuration.RunConfiguration("Billing", "Enter credentials for billing providers");
}
}

0 comments on commit 3e09fd0

Please sign in to comment.