From c713a5448d7ebbe8882cff8337df9aa837076cb5 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 10 Mar 2026 07:41:23 -0500 Subject: [PATCH 01/10] tests(tests.common): add class library for sharing code amongst test projects --- .../Data/AccountBuilder.cs | 62 ++++++++++++++++ .../Data/AccountMetadataBuilder.cs | 33 +++++++++ .../Data/InstitutionBuilder.cs | 59 +++++++++++++++ .../Data/InstitutionMetadataBuilder.cs | 52 +++++++++++++ .../Data/TransactionBuilder.cs | 67 +++++++++++++++++ .../Data/TransactionMetadataBuilder.cs | 26 +++++++ .../FiscalOS.Tests.Common/Data/UserBuilder.cs | 73 +++++++++++++++++++ .../FiscalOS.Tests.Common.csproj | 14 ++++ tests/FiscalOS.Tests.Common/Usings.cs | 6 ++ 9 files changed, 392 insertions(+) create mode 100644 tests/FiscalOS.Tests.Common/Data/AccountBuilder.cs create mode 100644 tests/FiscalOS.Tests.Common/Data/AccountMetadataBuilder.cs create mode 100644 tests/FiscalOS.Tests.Common/Data/InstitutionBuilder.cs create mode 100644 tests/FiscalOS.Tests.Common/Data/InstitutionMetadataBuilder.cs create mode 100644 tests/FiscalOS.Tests.Common/Data/TransactionBuilder.cs create mode 100644 tests/FiscalOS.Tests.Common/Data/TransactionMetadataBuilder.cs create mode 100644 tests/FiscalOS.Tests.Common/Data/UserBuilder.cs create mode 100644 tests/FiscalOS.Tests.Common/FiscalOS.Tests.Common.csproj create mode 100644 tests/FiscalOS.Tests.Common/Usings.cs diff --git a/tests/FiscalOS.Tests.Common/Data/AccountBuilder.cs b/tests/FiscalOS.Tests.Common/Data/AccountBuilder.cs new file mode 100644 index 0000000..48ecdb4 --- /dev/null +++ b/tests/FiscalOS.Tests.Common/Data/AccountBuilder.cs @@ -0,0 +1,62 @@ +namespace FiscalOS.Tests.Common.Data; + +public sealed class AccountBuilder +{ + private string _name = "accountName"; + private AccountMetadata? _metadata; + private readonly List _transactions = []; + + private AccountBuilder() + { + } + + public static AccountBuilder Create() + { + return new(); + } + + public AccountBuilder WithName(string name) + { + _name = name; + return this; + } + + public AccountBuilder WithMetadata(Action? action = null) + { + var imb = AccountMetadataBuilder.Create(); + action?.Invoke(imb); + _metadata = imb.Build(); + return this; + } + + public AccountBuilder WithMetadata(AccountMetadata metadata) + { + _metadata = metadata; + return this; + } + + public AccountBuilder WithTransaction(Action? action = null) + { + var tb = TransactionBuilder.Create(); + action?.Invoke(tb); + _transactions.Add(tb.Build()); + return this; + } + + public Account Build() + { + if (_metadata is null) + { + throw new InvalidOperationException($"You must call {nameof(WithMetadata)} prior to building"); + } + + var account = Account.From(_name, _metadata); + + foreach (var transaction in _transactions) + { + account.AddTransaction(transaction); + } + + return account; + } +} \ No newline at end of file diff --git a/tests/FiscalOS.Tests.Common/Data/AccountMetadataBuilder.cs b/tests/FiscalOS.Tests.Common/Data/AccountMetadataBuilder.cs new file mode 100644 index 0000000..9d6f3db --- /dev/null +++ b/tests/FiscalOS.Tests.Common/Data/AccountMetadataBuilder.cs @@ -0,0 +1,33 @@ +namespace FiscalOS.Tests.Common.Data; + +public sealed class AccountMetadataBuilder +{ + private string _plaidId = "plaidId"; + private string _plaidName = "plaidName"; + + private AccountMetadataBuilder() + { + } + + public static AccountMetadataBuilder Create() + { + return new(); + } + + public AccountMetadataBuilder WithPlaidId(string plaidId) + { + _plaidId = plaidId; + return this; + } + + public AccountMetadataBuilder WithPlaidName(string plaidName) + { + _plaidName = plaidName; + return this; + } + + public PlaidAccountMetadata Build() + { + return PlaidAccountMetadata.From(_plaidId, _plaidName); + } +} \ No newline at end of file diff --git a/tests/FiscalOS.Tests.Common/Data/InstitutionBuilder.cs b/tests/FiscalOS.Tests.Common/Data/InstitutionBuilder.cs new file mode 100644 index 0000000..6d08876 --- /dev/null +++ b/tests/FiscalOS.Tests.Common/Data/InstitutionBuilder.cs @@ -0,0 +1,59 @@ +namespace FiscalOS.Tests.Common.Data; + +public sealed class InstitutionBuilder +{ + private string _name = "institutionName"; + private InstitutionMetadata? _metadata; + private readonly List _accounts = []; + + private InstitutionBuilder() + { + } + + public static InstitutionBuilder Create() + { + return new(); + } + + public InstitutionBuilder WithName(string name) + { + _name = name; + return this; + } + + public InstitutionBuilder WithMetadata(Action? action = null) + { + var imb = InstitutionMetadataBuilder.Create(); + action?.Invoke(imb); + _metadata = imb.Build(); + return this; + } + + public InstitutionBuilder WithAccount(Action? action = null) + { + var ab = AccountBuilder.Create(); + action?.Invoke(ab); + _accounts.Add(ab.Build()); + return this; + } + + public Institution Build() + { + if (_metadata is null) + { + throw new InvalidOperationException($"You must call {nameof(WithMetadata)} prior to building"); + } + + var institution = Institution.From( + _name, + _metadata + ); + + foreach (var account in _accounts) + { + institution.AddAccount(account); + } + + return institution; + } +} \ No newline at end of file diff --git a/tests/FiscalOS.Tests.Common/Data/InstitutionMetadataBuilder.cs b/tests/FiscalOS.Tests.Common/Data/InstitutionMetadataBuilder.cs new file mode 100644 index 0000000..93a35be --- /dev/null +++ b/tests/FiscalOS.Tests.Common/Data/InstitutionMetadataBuilder.cs @@ -0,0 +1,52 @@ +namespace FiscalOS.Tests.Common.Data; + +public sealed class InstitutionMetadataBuilder +{ + private string _plaidId = "plaidId"; + private string _plaidName = "plaidName"; + private string _accessToken = "encryptedAccessToken"; + private string _itemId = "itemId"; + + private InstitutionMetadataBuilder() + { + } + + public static InstitutionMetadataBuilder Create() + { + return new(); + } + + public InstitutionMetadataBuilder WithPlaidId(string plaidId) + { + _plaidId = plaidId; + return this; + } + + public InstitutionMetadataBuilder WithPlaidName(string plaidName) + { + _plaidName = plaidName; + return this; + } + + public InstitutionMetadataBuilder WithAccessToken(string accessToken) + { + _accessToken = accessToken; + return this; + } + + public InstitutionMetadataBuilder WithItemId(string itemId) + { + _itemId = itemId; + return this; + } + + public PlaidInstitutionMetadata Build() + { + return PlaidInstitutionMetadata.From( + _plaidId, + _plaidName, + _accessToken, + _itemId + ); + } +} \ No newline at end of file diff --git a/tests/FiscalOS.Tests.Common/Data/TransactionBuilder.cs b/tests/FiscalOS.Tests.Common/Data/TransactionBuilder.cs new file mode 100644 index 0000000..3120475 --- /dev/null +++ b/tests/FiscalOS.Tests.Common/Data/TransactionBuilder.cs @@ -0,0 +1,67 @@ +namespace FiscalOS.Tests.Common.Data; + +public sealed class TransactionBuilder +{ + private string _merchantName = "merchantName"; + private string _description = "description"; + private decimal _amount = 100; + private DateTimeOffset _date = DateTimeOffset.UtcNow; + private TransactionMetadata? _metadata; + + private TransactionBuilder() + { + } + + public static TransactionBuilder Create() + { + return new(); + } + + public TransactionBuilder WithMerchantName(string merchantName) + { + _merchantName = merchantName; + return this; + } + + public TransactionBuilder WithDescription(string description) + { + _description = description; + return this; + } + + public TransactionBuilder WithAmount(decimal amount) + { + _amount = amount; + return this; + } + + public TransactionBuilder WithDate(DateTimeOffset date) + { + _date = date; + return this; + } + + public TransactionBuilder WithMetadata(Action? action = null) + { + var tmb = TransactionMetadataBuilder.Create(); + action?.Invoke(tmb); + _metadata = tmb.Build(); + return this; + } + + public Transaction Build() + { + if (_metadata is null) + { + throw new InvalidOperationException($"You must call {nameof(WithMetadata)} prior to building"); + } + + return Transaction.From( + _merchantName, + _description, + _amount, + _date, + _metadata + ); + } +} \ No newline at end of file diff --git a/tests/FiscalOS.Tests.Common/Data/TransactionMetadataBuilder.cs b/tests/FiscalOS.Tests.Common/Data/TransactionMetadataBuilder.cs new file mode 100644 index 0000000..7f6db3c --- /dev/null +++ b/tests/FiscalOS.Tests.Common/Data/TransactionMetadataBuilder.cs @@ -0,0 +1,26 @@ +namespace FiscalOS.Tests.Common.Data; + +public sealed class TransactionMetadataBuilder +{ + private string _plaidId = "plaidId"; + + private TransactionMetadataBuilder() + { + } + + public static TransactionMetadataBuilder Create() + { + return new(); + } + + public TransactionMetadataBuilder WithPlaidId(string plaidId) + { + _plaidId = plaidId; + return this; + } + + public PlaidTransactionMetadata Build() + { + return PlaidTransactionMetadata.From(_plaidId); + } +} \ No newline at end of file diff --git a/tests/FiscalOS.Tests.Common/Data/UserBuilder.cs b/tests/FiscalOS.Tests.Common/Data/UserBuilder.cs new file mode 100644 index 0000000..6ec90fb --- /dev/null +++ b/tests/FiscalOS.Tests.Common/Data/UserBuilder.cs @@ -0,0 +1,73 @@ +namespace FiscalOS.Tests.Common.Data; + +public sealed class UserBuilder +{ + private string _username = "username"; + private string _password = "hashedPassword"; + private EncryptedDataKey _dataKey = EncryptedDataKey.From( + "keyUsed", + "encryptedKey" + ); + private readonly List _instituions = []; + + private UserBuilder() + { + } + + public static UserBuilder Create() + { + return new(); + } + + public UserBuilder WithUsername(string username) + { + _username = username; + return this; + } + + public UserBuilder WithPassword(string password) + { + _password = password; + return this; + } + + public UserBuilder WithDataKey(EncryptedDataKey key) + { + _dataKey = key; + return this; + } + + public UserBuilder WithInstitution(Action? action = null) + { + var ib = InstitutionBuilder.Create(); + action?.Invoke(ib); + _instituions.Add(ib.Build()); + return this; + } + + public User Build() + { + var user = User.From( + _username, + _password, + _dataKey + ); + + foreach (var institution in _instituions) + { + user.AddInstitution(institution); + + foreach (var account in institution.Accounts) + { + user.AddAccount(account); + + foreach (var transaction in account.Transactions) + { + user.AddTransaction(transaction); + } + } + } + + return user; + } +} \ No newline at end of file diff --git a/tests/FiscalOS.Tests.Common/FiscalOS.Tests.Common.csproj b/tests/FiscalOS.Tests.Common/FiscalOS.Tests.Common.csproj new file mode 100644 index 0000000..0a46908 --- /dev/null +++ b/tests/FiscalOS.Tests.Common/FiscalOS.Tests.Common.csproj @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/tests/FiscalOS.Tests.Common/Usings.cs b/tests/FiscalOS.Tests.Common/Usings.cs new file mode 100644 index 0000000..e6bc17a --- /dev/null +++ b/tests/FiscalOS.Tests.Common/Usings.cs @@ -0,0 +1,6 @@ +global using FiscalOS.Core.Accounts; +global using FiscalOS.Core.Identity; +global using FiscalOS.Core.Security; +global using FiscalOS.Core.Transactions; +global using FiscalOS.Infra.Accounts.Plaid; +global using FiscalOS.Infra.Transactions.Plaid; From e13c83013b5826d902bac9ec32764ac4821aeff3 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 10 Mar 2026 07:41:43 -0500 Subject: [PATCH 02/10] refactor(infra.tests): moved builders to common project --- .../Data/AccountBuilder.cs | 62 ---------------- .../Data/AccountMetadataBuilder.cs | 33 --------- .../Data/InstitutionBuilder.cs | 59 --------------- .../Data/InstitutionMetadataBuilder.cs | 52 ------------- .../Data/TransactionBuilder.cs | 67 ----------------- .../Data/TransactionMetadataBuilder.cs | 26 ------- .../FiscalOS.Infra.Tests/Data/UserBuilder.cs | 73 ------------------- .../FiscalOS.Infra.Tests.csproj | 1 + .../Integration/IntegrationTest.cs | 2 - .../Unit/PlaidTransactionSyncerTests.cs | 2 - tests/FiscalOS.Infra.Tests/Usings.cs | 1 + 11 files changed, 2 insertions(+), 376 deletions(-) delete mode 100644 tests/FiscalOS.Infra.Tests/Data/AccountBuilder.cs delete mode 100644 tests/FiscalOS.Infra.Tests/Data/AccountMetadataBuilder.cs delete mode 100644 tests/FiscalOS.Infra.Tests/Data/InstitutionBuilder.cs delete mode 100644 tests/FiscalOS.Infra.Tests/Data/InstitutionMetadataBuilder.cs delete mode 100644 tests/FiscalOS.Infra.Tests/Data/TransactionBuilder.cs delete mode 100644 tests/FiscalOS.Infra.Tests/Data/TransactionMetadataBuilder.cs delete mode 100644 tests/FiscalOS.Infra.Tests/Data/UserBuilder.cs diff --git a/tests/FiscalOS.Infra.Tests/Data/AccountBuilder.cs b/tests/FiscalOS.Infra.Tests/Data/AccountBuilder.cs deleted file mode 100644 index 3d8e35b..0000000 --- a/tests/FiscalOS.Infra.Tests/Data/AccountBuilder.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace FiscalOS.Infra.Tests.Data; - -internal sealed class AccountBuilder -{ - private string _name = "accountName"; - private AccountMetadata? _metadata; - private readonly List _transactions = []; - - private AccountBuilder() - { - } - - public static AccountBuilder Create() - { - return new(); - } - - public AccountBuilder WithName(string name) - { - _name = name; - return this; - } - - public AccountBuilder WithMetadata(Action? action = null) - { - var imb = AccountMetadataBuilder.Create(); - action?.Invoke(imb); - _metadata = imb.Build(); - return this; - } - - public AccountBuilder WithMetadata(AccountMetadata metadata) - { - _metadata = metadata; - return this; - } - - public AccountBuilder WithTransaction(Action? action = null) - { - var tb = TransactionBuilder.Create(); - action?.Invoke(tb); - _transactions.Add(tb.Build()); - return this; - } - - public Account Build() - { - if (_metadata is null) - { - throw new InvalidOperationException($"You must call {nameof(WithMetadata)} prior to building"); - } - - var account = Account.From(_name, _metadata); - - foreach (var transaction in _transactions) - { - account.AddTransaction(transaction); - } - - return account; - } -} \ No newline at end of file diff --git a/tests/FiscalOS.Infra.Tests/Data/AccountMetadataBuilder.cs b/tests/FiscalOS.Infra.Tests/Data/AccountMetadataBuilder.cs deleted file mode 100644 index 7e7574f..0000000 --- a/tests/FiscalOS.Infra.Tests/Data/AccountMetadataBuilder.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace FiscalOS.Infra.Tests.Data; - -internal sealed class AccountMetadataBuilder -{ - private string _plaidId = "plaidId"; - private string _plaidName = "plaidName"; - - private AccountMetadataBuilder() - { - } - - public static AccountMetadataBuilder Create() - { - return new(); - } - - public AccountMetadataBuilder WithPlaidId(string plaidId) - { - _plaidId = plaidId; - return this; - } - - public AccountMetadataBuilder WithPlaidName(string plaidName) - { - _plaidName = plaidName; - return this; - } - - public PlaidAccountMetadata Build() - { - return PlaidAccountMetadata.From(_plaidId, _plaidName); - } -} \ No newline at end of file diff --git a/tests/FiscalOS.Infra.Tests/Data/InstitutionBuilder.cs b/tests/FiscalOS.Infra.Tests/Data/InstitutionBuilder.cs deleted file mode 100644 index ffdcbf7..0000000 --- a/tests/FiscalOS.Infra.Tests/Data/InstitutionBuilder.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace FiscalOS.Infra.Tests.Data; - -internal sealed class InstitutionBuilder -{ - private string _name = "institutionName"; - private InstitutionMetadata? _metadata; - private readonly List _accounts = []; - - private InstitutionBuilder() - { - } - - public static InstitutionBuilder Create() - { - return new(); - } - - public InstitutionBuilder WithName(string name) - { - _name = name; - return this; - } - - public InstitutionBuilder WithMetadata(Action? action = null) - { - var imb = InstitutionMetadataBuilder.Create(); - action?.Invoke(imb); - _metadata = imb.Build(); - return this; - } - - public InstitutionBuilder WithAccount(Action? action = null) - { - var ab = AccountBuilder.Create(); - action?.Invoke(ab); - _accounts.Add(ab.Build()); - return this; - } - - public Institution Build() - { - if (_metadata is null) - { - throw new InvalidOperationException($"You must call {nameof(WithMetadata)} prior to building"); - } - - var institution = Institution.From( - _name, - _metadata - ); - - foreach (var account in _accounts) - { - institution.AddAccount(account); - } - - return institution; - } -} \ No newline at end of file diff --git a/tests/FiscalOS.Infra.Tests/Data/InstitutionMetadataBuilder.cs b/tests/FiscalOS.Infra.Tests/Data/InstitutionMetadataBuilder.cs deleted file mode 100644 index 4a7a1c6..0000000 --- a/tests/FiscalOS.Infra.Tests/Data/InstitutionMetadataBuilder.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace FiscalOS.Infra.Tests.Data; - -internal sealed class InstitutionMetadataBuilder -{ - private string _plaidId = "plaidId"; - private string _plaidName = "plaidName"; - private string _accessToken = "encryptedAccessToken"; - private string _itemId = "itemId"; - - private InstitutionMetadataBuilder() - { - } - - public static InstitutionMetadataBuilder Create() - { - return new(); - } - - public InstitutionMetadataBuilder WithPlaidId(string plaidId) - { - _plaidId = plaidId; - return this; - } - - public InstitutionMetadataBuilder WithPlaidName(string plaidName) - { - _plaidName = plaidName; - return this; - } - - public InstitutionMetadataBuilder WithAccessToken(string accessToken) - { - _accessToken = accessToken; - return this; - } - - public InstitutionMetadataBuilder WithItemId(string itemId) - { - _itemId = itemId; - return this; - } - - public PlaidInstitutionMetadata Build() - { - return PlaidInstitutionMetadata.From( - _plaidId, - _plaidName, - _accessToken, - _itemId - ); - } -} \ No newline at end of file diff --git a/tests/FiscalOS.Infra.Tests/Data/TransactionBuilder.cs b/tests/FiscalOS.Infra.Tests/Data/TransactionBuilder.cs deleted file mode 100644 index 098420b..0000000 --- a/tests/FiscalOS.Infra.Tests/Data/TransactionBuilder.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace FiscalOS.Infra.Tests.Data; - -internal sealed class TransactionBuilder -{ - private string _merchantName = "merchantName"; - private string _description = "description"; - private decimal _amount = 100; - private DateTimeOffset _date = DateTimeOffset.UtcNow; - private TransactionMetadata? _metadata; - - private TransactionBuilder() - { - } - - public static TransactionBuilder Create() - { - return new(); - } - - public TransactionBuilder WithMerchantName(string merchantName) - { - _merchantName = merchantName; - return this; - } - - public TransactionBuilder WithDescription(string description) - { - _description = description; - return this; - } - - public TransactionBuilder WithAmount(decimal amount) - { - _amount = amount; - return this; - } - - public TransactionBuilder WithDate(DateTimeOffset date) - { - _date = date; - return this; - } - - public TransactionBuilder WithMetadata(Action? action = null) - { - var tmb = TransactionMetadataBuilder.Create(); - action?.Invoke(tmb); - _metadata = tmb.Build(); - return this; - } - - public Transaction Build() - { - if (_metadata is null) - { - throw new InvalidOperationException($"You must call {nameof(WithMetadata)} prior to building"); - } - - return Transaction.From( - _merchantName, - _description, - _amount, - _date, - _metadata - ); - } -} \ No newline at end of file diff --git a/tests/FiscalOS.Infra.Tests/Data/TransactionMetadataBuilder.cs b/tests/FiscalOS.Infra.Tests/Data/TransactionMetadataBuilder.cs deleted file mode 100644 index d8d0715..0000000 --- a/tests/FiscalOS.Infra.Tests/Data/TransactionMetadataBuilder.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace FiscalOS.Infra.Tests.Data; - -internal sealed class TransactionMetadataBuilder -{ - private string _plaidId = "plaidId"; - - private TransactionMetadataBuilder() - { - } - - public static TransactionMetadataBuilder Create() - { - return new(); - } - - public TransactionMetadataBuilder WithPlaidId(string plaidId) - { - _plaidId = plaidId; - return this; - } - - public PlaidTransactionMetadata Build() - { - return PlaidTransactionMetadata.From(_plaidId); - } -} \ No newline at end of file diff --git a/tests/FiscalOS.Infra.Tests/Data/UserBuilder.cs b/tests/FiscalOS.Infra.Tests/Data/UserBuilder.cs deleted file mode 100644 index e4434e9..0000000 --- a/tests/FiscalOS.Infra.Tests/Data/UserBuilder.cs +++ /dev/null @@ -1,73 +0,0 @@ -namespace FiscalOS.Infra.Tests.Data; - -internal sealed class UserBuilder -{ - private string _username = "username"; - private string _password = "hashedPassword"; - private EncryptedDataKey _dataKey = EncryptedDataKey.From( - "keyUsed", - "encryptedKey" - ); - private readonly List _instituions = []; - - private UserBuilder() - { - } - - public static UserBuilder Create() - { - return new(); - } - - public UserBuilder WithUsername(string username) - { - _username = username; - return this; - } - - public UserBuilder WithPassword(string password) - { - _password = password; - return this; - } - - public UserBuilder WithDataKey(EncryptedDataKey key) - { - _dataKey = key; - return this; - } - - public UserBuilder WithInstitution(Action? action = null) - { - var ib = InstitutionBuilder.Create(); - action?.Invoke(ib); - _instituions.Add(ib.Build()); - return this; - } - - public User Build() - { - var user = User.From( - _username, - _password, - _dataKey - ); - - foreach (var institution in _instituions) - { - user.AddInstitution(institution); - - foreach (var account in institution.Accounts) - { - user.AddAccount(account); - - foreach (var transaction in account.Transactions) - { - user.AddTransaction(transaction); - } - } - } - - return user; - } -} \ No newline at end of file diff --git a/tests/FiscalOS.Infra.Tests/FiscalOS.Infra.Tests.csproj b/tests/FiscalOS.Infra.Tests/FiscalOS.Infra.Tests.csproj index e6ca626..90bc9df 100644 --- a/tests/FiscalOS.Infra.Tests/FiscalOS.Infra.Tests.csproj +++ b/tests/FiscalOS.Infra.Tests/FiscalOS.Infra.Tests.csproj @@ -18,6 +18,7 @@ + diff --git a/tests/FiscalOS.Infra.Tests/Integration/IntegrationTest.cs b/tests/FiscalOS.Infra.Tests/Integration/IntegrationTest.cs index 33fc9c3..c036bed 100644 --- a/tests/FiscalOS.Infra.Tests/Integration/IntegrationTest.cs +++ b/tests/FiscalOS.Infra.Tests/Integration/IntegrationTest.cs @@ -1,5 +1,3 @@ -using FiscalOS.Infra.Tests.Data; - namespace FiscalOS.Infra.Tests.Integration; public abstract class IntegrationTest : IAsyncLifetime diff --git a/tests/FiscalOS.Infra.Tests/Unit/PlaidTransactionSyncerTests.cs b/tests/FiscalOS.Infra.Tests/Unit/PlaidTransactionSyncerTests.cs index 1f3e6cc..d9cac63 100644 --- a/tests/FiscalOS.Infra.Tests/Unit/PlaidTransactionSyncerTests.cs +++ b/tests/FiscalOS.Infra.Tests/Unit/PlaidTransactionSyncerTests.cs @@ -1,5 +1,3 @@ -using FiscalOS.Infra.Tests.Data; - namespace FiscalOS.Infra.Tests.Unit; public class PlaidTransactionSyncerTests diff --git a/tests/FiscalOS.Infra.Tests/Usings.cs b/tests/FiscalOS.Infra.Tests/Usings.cs index 1d2c2b9..489f593 100644 --- a/tests/FiscalOS.Infra.Tests/Usings.cs +++ b/tests/FiscalOS.Infra.Tests/Usings.cs @@ -19,6 +19,7 @@ global using FiscalOS.Infra.Data; global using FiscalOS.Infra.Security; global using FiscalOS.Infra.Tests.Assertions; global using FiscalOS.Infra.Tests.Mocks; +global using FiscalOS.Tests.Common.Data; global using FiscalOS.Infra.Transactions.Plaid; global using Going.Plaid; From 02147cffdbdce9a9710e14633b7af4b303381523 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 10 Mar 2026 07:42:03 -0500 Subject: [PATCH 03/10] chore: add new test project to solution --- FiscalOS.slnx | 1 + 1 file changed, 1 insertion(+) diff --git a/FiscalOS.slnx b/FiscalOS.slnx index a34ee68..b24d374 100644 --- a/FiscalOS.slnx +++ b/FiscalOS.slnx @@ -15,6 +15,7 @@ + From e2d20589603faca5b9eca9939e7dfaab3e6f284d Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 10 Mar 2026 07:42:15 -0500 Subject: [PATCH 04/10] chore: update preferences for lambas --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 32a0530..e0fa98f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -94,7 +94,7 @@ csharp_style_var_when_type_is_apparent = true:suggestion csharp_style_expression_bodied_accessors = true:silent csharp_style_expression_bodied_constructors = false:silent csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion +csharp_style_expression_bodied_lambdas = false:silent csharp_style_expression_bodied_local_functions = false:silent csharp_style_expression_bodied_methods = false:silent csharp_style_expression_bodied_operators = false:silent From 3408f6ea4f1383bce68bfe8cfb9c97614e1b9f48 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 10 Mar 2026 07:42:51 -0500 Subject: [PATCH 05/10] feat(api,web): retrieve and display transactions --- .vscode/settings.json | 2 +- .../Accounts/AccountsExtensions.cs | 2 + src/FiscalOS.API/Auth/AuthExtensions.cs | 4 + src/FiscalOS.API/Auth/Logout/Endpoint.cs | 2 + src/FiscalOS.API/Common/PagedQuery.cs | 36 ++++ src/FiscalOS.API/Common/PagedResponse.cs | 34 ++++ .../Institutions/InstitutionsExtensions.cs | 5 + src/FiscalOS.API/Transactions/Get/Endpoint.cs | 53 ++++++ .../Transactions/TransactionDto.cs | 29 +++ .../Transactions/TransactionsExtensions.cs | 9 +- src/FiscalOS.API/Usings.cs | 10 +- .../src/components/NavSidebar.vue | 170 ++++++++++-------- .../src/components/TransactionCard.vue | 17 ++ .../src/composables/useTransactionService.ts | 27 +++ src/FiscalOS.Web/src/main.ts | 24 ++- src/FiscalOS.Web/src/router/index.ts | 4 + .../src/services/transactionService.ts | 72 ++++++++ src/FiscalOS.Web/src/views/HomeView.vue | 6 +- .../src/views/TransactionsView.vue | 47 +++++ tests/Directory.Packages.props | 1 + .../FiscalOS.API.Tests.csproj | 1 + .../Infra/HttpRequestBuilder.cs | 15 +- .../Integration/Transactions/GetTests.cs | 101 +++++++++++ .../Unit/PagedResponseTests.cs | 21 +++ tests/FiscalOS.API.Tests/Usings.cs | 3 + 25 files changed, 595 insertions(+), 100 deletions(-) create mode 100644 src/FiscalOS.API/Common/PagedQuery.cs create mode 100644 src/FiscalOS.API/Common/PagedResponse.cs create mode 100644 src/FiscalOS.API/Transactions/Get/Endpoint.cs create mode 100644 src/FiscalOS.API/Transactions/TransactionDto.cs create mode 100644 src/FiscalOS.Web/src/components/TransactionCard.vue create mode 100644 src/FiscalOS.Web/src/composables/useTransactionService.ts create mode 100644 src/FiscalOS.Web/src/services/transactionService.ts create mode 100644 src/FiscalOS.Web/src/views/TransactionsView.vue create mode 100644 tests/FiscalOS.API.Tests/Integration/Transactions/GetTests.cs create mode 100644 tests/FiscalOS.API.Tests/Unit/PagedResponseTests.cs diff --git a/.vscode/settings.json b/.vscode/settings.json index bb59eac..941053d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,7 +11,7 @@ }, "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", - "cSpell.words": ["Encryptor"], + "cSpell.words": ["Dtos", "Encryptor", "Validatable"], "python-envs.defaultEnvManager": "ms-python.python:system", "search.exclude": { "**/Migrations": true diff --git a/src/FiscalOS.API/Accounts/AccountsExtensions.cs b/src/FiscalOS.API/Accounts/AccountsExtensions.cs index fbb5daa..3abab86 100644 --- a/src/FiscalOS.API/Accounts/AccountsExtensions.cs +++ b/src/FiscalOS.API/Accounts/AccountsExtensions.cs @@ -1,3 +1,5 @@ +using FiscalOS.API.Accounts.Add; + namespace FiscalOS.API.Accounts; internal static class AccountsExtensions diff --git a/src/FiscalOS.API/Auth/AuthExtensions.cs b/src/FiscalOS.API/Auth/AuthExtensions.cs index c22d045..a5f0ad8 100644 --- a/src/FiscalOS.API/Auth/AuthExtensions.cs +++ b/src/FiscalOS.API/Auth/AuthExtensions.cs @@ -1,3 +1,7 @@ +using FiscalOS.API.Auth.Login; +using FiscalOS.API.Auth.Logout; +using FiscalOS.API.Auth.Refresh; + namespace FiscalOS.API.Auth; internal static class AuthExtensions diff --git a/src/FiscalOS.API/Auth/Logout/Endpoint.cs b/src/FiscalOS.API/Auth/Logout/Endpoint.cs index de46d7b..9ef4b03 100644 --- a/src/FiscalOS.API/Auth/Logout/Endpoint.cs +++ b/src/FiscalOS.API/Auth/Logout/Endpoint.cs @@ -1,3 +1,5 @@ +namespace FiscalOS.API.Auth.Logout; + internal static class Endpoint { private const string Route = "/logout"; diff --git a/src/FiscalOS.API/Common/PagedQuery.cs b/src/FiscalOS.API/Common/PagedQuery.cs new file mode 100644 index 0000000..3ef2f4e --- /dev/null +++ b/src/FiscalOS.API/Common/PagedQuery.cs @@ -0,0 +1,36 @@ +namespace FiscalOS.API.Common; + +internal sealed record PagedQuery +{ + public int PageNumber { get; init; } + public int PageSize { get; init; } + + public static async ValueTask BindAsync(HttpContext context) + { + var pageNumber = int.TryParse(context.Request.Query["pageNumber"], out var pn) ? pn : 1; + var pageSize = int.TryParse(context.Request.Query["pageSize"], out var ps) ? ps : 1000; + + return new PagedQuery + { + PageNumber = pageNumber, + PageSize = pageSize + }; + } + + public Dictionary Validate() + { + var validationResults = new Dictionary(); + + if (PageNumber <= 0) + { + validationResults[nameof(PageNumber)] = ["PageNumber must be greater than 0."]; + } + + if (PageSize <= 0 || PageSize > 1000) + { + validationResults[nameof(PageSize)] = ["PageSize must be between 1 and 1000."]; + } + + return validationResults; + } +} \ No newline at end of file diff --git a/src/FiscalOS.API/Common/PagedResponse.cs b/src/FiscalOS.API/Common/PagedResponse.cs new file mode 100644 index 0000000..a895ab3 --- /dev/null +++ b/src/FiscalOS.API/Common/PagedResponse.cs @@ -0,0 +1,34 @@ +namespace FiscalOS.API.Common; + +internal sealed record PagedResponse +{ + public int PageNumber { get; init; } + public int PageSize { get; init; } + public int TotalItems { get; init; } + public int TotalPages { get; init; } + public T[] Items { get; init; } = []; + + [JsonConstructor] + private PagedResponse() + { + } + + public static PagedResponse From( + int pageNumber, + int pageSize, + int totalItems, + T[] items + ) + { + var totalPages = (int)Math.Ceiling((double)totalItems / pageSize); + + return new PagedResponse + { + PageNumber = pageNumber, + PageSize = pageSize, + TotalItems = totalItems, + TotalPages = totalPages, + Items = items + }; + } +} \ No newline at end of file diff --git a/src/FiscalOS.API/Institutions/InstitutionsExtensions.cs b/src/FiscalOS.API/Institutions/InstitutionsExtensions.cs index d783b9f..cac9a6c 100644 --- a/src/FiscalOS.API/Institutions/InstitutionsExtensions.cs +++ b/src/FiscalOS.API/Institutions/InstitutionsExtensions.cs @@ -1,3 +1,8 @@ +using FiscalOS.API.Institutions.Connect; +using FiscalOS.API.Institutions.Get; +using FiscalOS.API.Institutions.GetAvailable; +using FiscalOS.API.Institutions.Link; + namespace FiscalOS.API.Institutions; internal static class InstitutionsExtensions diff --git a/src/FiscalOS.API/Transactions/Get/Endpoint.cs b/src/FiscalOS.API/Transactions/Get/Endpoint.cs new file mode 100644 index 0000000..8617580 --- /dev/null +++ b/src/FiscalOS.API/Transactions/Get/Endpoint.cs @@ -0,0 +1,53 @@ +namespace FiscalOS.API.Transactions.Get; + +internal static class Endpoint +{ + private const string Route = "/"; + + public static RouteHandlerBuilder MapGetEndpoint(this RouteGroupBuilder groupBuilder) + { + return groupBuilder.MapGet(Route, HandleAsync); + } + + private static async Task HandleAsync( + HttpContext httpContext, + PagedQuery pagedQuery, + [FromServices] AppDbContext appDbContext, + CancellationToken ct + ) + { + var pagedQueryValidationResults = pagedQuery.Validate(); + + if (pagedQueryValidationResults.Count is not 0) + { + return Results.ValidationProblem(pagedQueryValidationResults); + } + + var userId = httpContext.GetUserId(); + var query = appDbContext.Transactions + .Where(t => t.UserId == userId); + + var transactions = await query + .Skip((pagedQuery.PageNumber - 1) * pagedQuery.PageSize) + .Take(pagedQuery.PageSize) + // TODO: Migrate Date to DateTime + // to avoid in-memory sorting + .AsAsyncEnumerable() + .OrderByDescending(t => t.Date) + .ToListAsync(ct); + + var count = await query + .Where(t => t.UserId == userId) + .CountAsync(ct); + + var transactionDtos = transactions.Select(TransactionDto.From).ToArray(); + var pagedResponse = PagedResponse.From( + pagedQuery.PageNumber, + pagedQuery.PageSize, + count, + transactionDtos + ); + + return Results.Ok(pagedResponse); + } +} \ No newline at end of file diff --git a/src/FiscalOS.API/Transactions/TransactionDto.cs b/src/FiscalOS.API/Transactions/TransactionDto.cs new file mode 100644 index 0000000..a93d8c6 --- /dev/null +++ b/src/FiscalOS.API/Transactions/TransactionDto.cs @@ -0,0 +1,29 @@ +using Transaction = FiscalOS.Core.Transactions.Transaction; + +namespace FiscalOS.API.Transactions; + +internal sealed record TransactionDto +{ + public Guid Id { get; init; } + public string MerchantName { get; init; } = string.Empty; + public decimal Amount { get; init; } + public DateTimeOffset Date { get; init; } + public string Description { get; init; } = string.Empty; + + [JsonConstructor] + private TransactionDto() + { + } + + public static TransactionDto From(Transaction transaction) + { + return new TransactionDto + { + Id = transaction.Id, + MerchantName = transaction.MerchantName, + Amount = transaction.Amount, + Date = transaction.Date, + Description = transaction.Description + }; + } +} \ No newline at end of file diff --git a/src/FiscalOS.API/Transactions/TransactionsExtensions.cs b/src/FiscalOS.API/Transactions/TransactionsExtensions.cs index 881961a..d3e649c 100644 --- a/src/FiscalOS.API/Transactions/TransactionsExtensions.cs +++ b/src/FiscalOS.API/Transactions/TransactionsExtensions.cs @@ -1,3 +1,7 @@ +using FiscalOS.API.Transactions.FireWebhook; +using FiscalOS.API.Transactions.Get; +using FiscalOS.API.Transactions.Webhook; + namespace FiscalOS.API.Transactions; internal static class TransactionsExtensions @@ -9,13 +13,14 @@ internal static class TransactionsExtensions var transactionsGroup = app.MapGroup(RouteGroupPrefix) .RequireAuthorization(); + transactionsGroup.MapGetEndpoint(); + transactionsGroup.MapWebhookEndpoint().AllowAnonymous(); + if (app.Environment.IsProduction() is false) { transactionsGroup.MapFireWebhookEndpoint(); } - transactionsGroup.MapWebhookEndpoint().AllowAnonymous(); - return transactionsGroup; } } \ No newline at end of file diff --git a/src/FiscalOS.API/Usings.cs b/src/FiscalOS.API/Usings.cs index 54abce4..b5c09bc 100644 --- a/src/FiscalOS.API/Usings.cs +++ b/src/FiscalOS.API/Usings.cs @@ -3,19 +3,11 @@ global using System.Security.Claims; global using System.Text.Json.Serialization; global using FiscalOS.API.Accounts; -global using FiscalOS.API.Accounts.Add; global using FiscalOS.API.Auth; -global using FiscalOS.API.Auth.Login; -global using FiscalOS.API.Auth.Refresh; +global using FiscalOS.API.Common; global using FiscalOS.API.Http; global using FiscalOS.API.Institutions; -global using FiscalOS.API.Institutions.Connect; -global using FiscalOS.API.Institutions.Get; -global using FiscalOS.API.Institutions.GetAvailable; -global using FiscalOS.API.Institutions.Link; global using FiscalOS.API.Transactions; -global using FiscalOS.API.Transactions.FireWebhook; -global using FiscalOS.API.Transactions.Webhook; global using FiscalOS.Core.Authentication; global using FiscalOS.Core.Identity; global using FiscalOS.Core.Queuing; diff --git a/src/FiscalOS.Web/src/components/NavSidebar.vue b/src/FiscalOS.Web/src/components/NavSidebar.vue index 0ef6351..5911724 100644 --- a/src/FiscalOS.Web/src/components/NavSidebar.vue +++ b/src/FiscalOS.Web/src/components/NavSidebar.vue @@ -1,102 +1,120 @@ diff --git a/src/FiscalOS.Web/src/components/TransactionCard.vue b/src/FiscalOS.Web/src/components/TransactionCard.vue new file mode 100644 index 0000000..e1c53c3 --- /dev/null +++ b/src/FiscalOS.Web/src/components/TransactionCard.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/src/FiscalOS.Web/src/composables/useTransactionService.ts b/src/FiscalOS.Web/src/composables/useTransactionService.ts new file mode 100644 index 0000000..7a50ee8 --- /dev/null +++ b/src/FiscalOS.Web/src/composables/useTransactionService.ts @@ -0,0 +1,27 @@ +import { ClientConfig, ClientFactoryKey } from '@/services/client'; +import { TransactionServiceFactoryKey } from '@/services/transactionService'; +import type { UserStore } from '@/stores/userStore'; +import { inject } from 'vue'; + +export function useTransactionService(store: UserStore) { + const clientFactory = inject(ClientFactoryKey); + const transactionServiceFactory = inject(TransactionServiceFactoryKey); + + if (clientFactory === undefined) { + throw new Error('Failed to inject client factory.'); + } + + if (transactionServiceFactory === undefined) { + throw new Error('Failed to inject transaction service factory.'); + } + + const clientConfig = new ClientConfig( + { Authorization: `Bearer ${store.user?.token}` }, + true, + store.refreshAccessToken + ); + const client = clientFactory.create(clientConfig); + const transactionService = transactionServiceFactory.create(client); + + return transactionService; +} diff --git a/src/FiscalOS.Web/src/main.ts b/src/FiscalOS.Web/src/main.ts index 487d805..ad78699 100644 --- a/src/FiscalOS.Web/src/main.ts +++ b/src/FiscalOS.Web/src/main.ts @@ -1,21 +1,29 @@ import './assets/css/main.css'; -import { createApp } from "vue"; -import { createPinia } from "pinia"; +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; -import App from "./App.vue"; -import router from "./router"; -import { ClientFactory, ClientFactoryKey } from "./services/client"; -import { AuthServiceFactory, AuthServiceFactoryKey } from "./services/authService"; -import { InstituionServiceFactoryKey, InstitutionServiceFactory } from './services/institutionService'; +import App from './App.vue'; +import router from './router'; +import { ClientFactory, ClientFactoryKey } from './services/client'; +import { AuthServiceFactory, AuthServiceFactoryKey } from './services/authService'; +import { + InstituionServiceFactoryKey, + InstitutionServiceFactory, +} from './services/institutionService'; +import { + TransactionServiceFactory, + TransactionServiceFactoryKey, +} from './services/transactionService'; const app = createApp(App); app.provide(ClientFactoryKey, new ClientFactory()); app.provide(AuthServiceFactoryKey, new AuthServiceFactory()); app.provide(InstituionServiceFactoryKey, new InstitutionServiceFactory()); +app.provide(TransactionServiceFactoryKey, new TransactionServiceFactory()); app.use(createPinia()); app.use(router); -app.mount("#app"); +app.mount('#app'); diff --git a/src/FiscalOS.Web/src/router/index.ts b/src/FiscalOS.Web/src/router/index.ts index cb10e76..74ebf0c 100644 --- a/src/FiscalOS.Web/src/router/index.ts +++ b/src/FiscalOS.Web/src/router/index.ts @@ -62,6 +62,10 @@ const router = createRouter({ path: '/', component: () => import('../views/HomeView.vue'), }, + { + path: '/transactions', + component: () => import('../views/TransactionsView.vue'), + } ], }, ], diff --git a/src/FiscalOS.Web/src/services/transactionService.ts b/src/FiscalOS.Web/src/services/transactionService.ts new file mode 100644 index 0000000..6b889b1 --- /dev/null +++ b/src/FiscalOS.Web/src/services/transactionService.ts @@ -0,0 +1,72 @@ +import type { InjectionKey } from 'vue'; +import { ClientRequest, type IClient } from './client'; +import { Err, Ok, type Result } from 'ts-results'; + +type TransactionServiceFactoryKeyType = InjectionKey; + +export const TransactionServiceFactoryKey: TransactionServiceFactoryKeyType = + Symbol('AuthServiceFactory'); + +export interface ITransactionServiceFactory { + create: (client: IClient) => ITransactionService; +} + +export class TransactionServiceFactory implements ITransactionServiceFactory { + create(client: IClient): ITransactionService { + return new TransactionService(client); + } +} + +export interface ITransactionService { + get: (pageNumber?: number, pageSize?: number) => Promise, Error[]>>; +} + +export class TransactionService implements ITransactionService { + private readonly client: IClient; + private readonly endpoints = { + get: '/api/transactions', + }; + + constructor(client: IClient) { + this.client = client; + } + + async get(pageNumber: number = 1, pageSize: number = 500) { + const queryParams = new URLSearchParams({ + pageNumber: pageNumber.toString(), + pageSize: pageSize.toString(), + }); + const url = this.endpoints.get + '?' + queryParams.toString(); + const request = new ClientRequest(url); + + try { + const response = await this.client.get(request); + + if (response.ok === false) { + return Err([new Error('Failed to retrieve transactions.')]); + } + + const data = await response.json(); + return Ok(data as Page); + } catch (error) { + console.error(error); + return Err([new Error('Failed to retrieve transactions.')]); + } + } +} + +export type Page = { + pageNumber: number; + pageSize: number; + totalItems: number; + totalPages: number; + items: T[]; +}; + +export type Transaction = { + id: string; + merchantName: string; + description: string; + amount: number; + date: string; +}; diff --git a/src/FiscalOS.Web/src/views/HomeView.vue b/src/FiscalOS.Web/src/views/HomeView.vue index 764b903..1fa4d69 100644 --- a/src/FiscalOS.Web/src/views/HomeView.vue +++ b/src/FiscalOS.Web/src/views/HomeView.vue @@ -147,7 +147,7 @@ async function handleAccountAddClick() {
-
+
{{ institution.name }}
@@ -160,7 +160,7 @@ async function handleAccountAddClick() {
@@ -168,7 +168,7 @@ async function handleAccountAddClick() { Add
-
+
{{ accounts.name }}
diff --git a/src/FiscalOS.Web/src/views/TransactionsView.vue b/src/FiscalOS.Web/src/views/TransactionsView.vue new file mode 100644 index 0000000..f5f6bc5 --- /dev/null +++ b/src/FiscalOS.Web/src/views/TransactionsView.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/tests/Directory.Packages.props b/tests/Directory.Packages.props index 0604d02..731f913 100644 --- a/tests/Directory.Packages.props +++ b/tests/Directory.Packages.props @@ -10,6 +10,7 @@ + \ No newline at end of file diff --git a/tests/FiscalOS.API.Tests/FiscalOS.API.Tests.csproj b/tests/FiscalOS.API.Tests/FiscalOS.API.Tests.csproj index b8cae2a..5f02698 100644 --- a/tests/FiscalOS.API.Tests/FiscalOS.API.Tests.csproj +++ b/tests/FiscalOS.API.Tests/FiscalOS.API.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/tests/FiscalOS.API.Tests/Infra/HttpRequestBuilder.cs b/tests/FiscalOS.API.Tests/Infra/HttpRequestBuilder.cs index 8980f6d..940e2ed 100644 --- a/tests/FiscalOS.API.Tests/Infra/HttpRequestBuilder.cs +++ b/tests/FiscalOS.API.Tests/Infra/HttpRequestBuilder.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.WebUtilities; + namespace FiscalOS.API.Tests.Infra; internal sealed class HttpRequestBuilder @@ -8,6 +10,7 @@ internal sealed class HttpRequestBuilder private string? _bearerToken; private readonly Dictionary _cookies = []; private readonly Dictionary _headers = []; + private readonly Dictionary _queryParameters = []; private HttpRequestBuilder() { @@ -68,6 +71,12 @@ internal sealed class HttpRequestBuilder return this; } + public HttpRequestBuilder WithQueryParameter(string name, string value) + { + _queryParameters[name] = value; + return this; + } + public HttpRequestBuilder Post(Uri uri) { _method = HttpMethod.Post; @@ -103,7 +112,11 @@ internal sealed class HttpRequestBuilder throw new InvalidOperationException("URI must be set before building the request."); } - var request = new HttpRequestMessage(_method, _uri); + var uri = _queryParameters.Count > 0 + ? QueryHelpers.AddQueryString(_uri.ToString(), _queryParameters) + : _uri.ToString(); + + var request = new HttpRequestMessage(_method, uri); if (_body is not null) { diff --git a/tests/FiscalOS.API.Tests/Integration/Transactions/GetTests.cs b/tests/FiscalOS.API.Tests/Integration/Transactions/GetTests.cs new file mode 100644 index 0000000..43f5f87 --- /dev/null +++ b/tests/FiscalOS.API.Tests/Integration/Transactions/GetTests.cs @@ -0,0 +1,101 @@ +using FiscalOS.API.Transactions; + +namespace FiscalOS.API.Tests.Integration.Transactions; + +public class GetTests(TestApi testApi) : IntegrationTest(testApi) +{ + private static readonly Uri GetUri = new("/transactions", UriKind.Relative); + + [Fact] + public async Task Get_WhenNotLoggedIn_ItShouldReturn401WithProblemDetails() + { + using var request = HttpRequestBuilder.New() + .Get(GetUri) + .Build(); + + var response = await Client.SendAsync(request, TestContext.Current.CancellationToken); + + await response.Should().BeProblemDetails(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task Get_WhenCalledWithInvalidPageNumber_ItShouldReturn400WithProblemDetails() + { + using var request = HttpRequestBuilder.New() + .Get(GetUri) + .WithQueryParameter("pageNumber", "-1") + .WithUserId(Guid.NewGuid()) + .Build(); + + var response = await Client.SendAsync(request, TestContext.Current.CancellationToken); + + await response.Should().BeValidationProblemDetails(new Dictionary() + { + ["PageNumber"] = ["PageNumber must be greater than 0."], + }); + } + + [Theory] + [InlineData(0)] + [InlineData(1001)] + public async Task Get_WhenCalledWithInvalidPageSize_ItShouldReturn400WithProblemDetails(int pageSize) + { + using var request = HttpRequestBuilder.New() + .Get(GetUri) + .WithQueryParameter("pageSize", pageSize.ToString(CultureInfo.InvariantCulture)) + .WithUserId(Guid.NewGuid()) + .Build(); + + var response = await Client.SendAsync(request, TestContext.Current.CancellationToken); + + await response.Should().BeValidationProblemDetails(new Dictionary() + { + ["PageSize"] = ["PageSize must be between 1 and 1000."], + }); + } + + [Fact] + public async Task Get_WhenCalled_ItShouldReturn200WithPagedResponseOfTransactions() + { + var user = await Api.ExecuteAsync(static async (context, ct, sp) => + { + var passwordHasher = sp.GetRequiredService(); + var encryptor = sp.GetRequiredService(); + + var user = UserBuilder.Create() + .WithInstitution(static ib => + { + ib.WithMetadata(); + ib.WithAccount(static ab => + { + ab.WithMetadata(); + ab.WithTransaction(static tb => + { + tb.WithMetadata(); + }); + }); + }) + .Build(); + + await context.AddAsync(user, ct); + await context.SaveChangesAsync(ct); + return user; + }, TestContext.Current.CancellationToken); + + using var request = HttpRequestBuilder.New() + .Get(GetUri) + .WithUserId(user.Id) + .Build(); + + var response = await Client.SendAsync(request, TestContext.Current.CancellationToken); + + (await response.Should() + .BeJsonContentOfType>(HttpStatusCode.OK)) + .Which + .Items + .Should() + .BeEquivalentTo( + user.Transactions.Select(TransactionDto.From) + ); + } +} \ No newline at end of file diff --git a/tests/FiscalOS.API.Tests/Unit/PagedResponseTests.cs b/tests/FiscalOS.API.Tests/Unit/PagedResponseTests.cs new file mode 100644 index 0000000..bfbf805 --- /dev/null +++ b/tests/FiscalOS.API.Tests/Unit/PagedResponseTests.cs @@ -0,0 +1,21 @@ +namespace FiscalOS.API.Tests.Unit; + +public class PagedResponseTests +{ + [Fact] + public void From_WhenCalled_ItShouldReturnPageWithCorrectValues() + { + var pageNumber = 1; + var pageSize = 10; + var totalItems = 25; + string[] items = ["test"]; + + var results = PagedResponse.From(pageNumber, pageSize, totalItems, items); + + results.PageNumber.Should().Be(pageNumber); + results.PageSize.Should().Be(pageSize); + results.TotalItems.Should().Be(totalItems); + results.TotalPages.Should().Be(3); + results.Items.Should().BeEquivalentTo(items); + } +} \ No newline at end of file diff --git a/tests/FiscalOS.API.Tests/Usings.cs b/tests/FiscalOS.API.Tests/Usings.cs index a6e01c0..041f1b3 100644 --- a/tests/FiscalOS.API.Tests/Usings.cs +++ b/tests/FiscalOS.API.Tests/Usings.cs @@ -1,3 +1,4 @@ +global using System.Globalization; global using System.IdentityModel.Tokens.Jwt; global using System.Net; global using System.Net.Http.Headers; @@ -10,6 +11,7 @@ global using System.Text.Json; global using AwesomeAssertions.Execution; global using AwesomeAssertions.Primitives; +global using FiscalOS.API.Common; global using FiscalOS.API.Tests.Assertions; global using FiscalOS.API.Tests.Infra; global using FiscalOS.Core.Authentication; @@ -18,6 +20,7 @@ global using FiscalOS.Core.Security; global using FiscalOS.Infra.Accounts.Plaid; global using FiscalOS.Infra.Authentication; global using FiscalOS.Infra.Data; +global using FiscalOS.Tests.Common.Data; global using Going.Plaid; global using Going.Plaid.Entity; From 4d1c01776f0ca0de992ee24ce68b9f4c413cd76e Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:46:21 -0500 Subject: [PATCH 06/10] feat(web): enhance transaction display and account management features --- src/FiscalOS.API/Institutions/Get/Endpoint.cs | 1 + src/FiscalOS.API/Institutions/Get/Response.cs | 4 + src/FiscalOS.Web/src/assets/css/reset.css | 20 +- .../src/components/CircleSpinner.vue | 21 + .../src/components/NavSidebar.vue | 77 ++-- .../src/components/ProtectedLayout.vue | 4 + .../src/components/TransactionCard.vue | 63 ++- src/FiscalOS.Web/src/router/index.ts | 22 +- .../src/services/institutionService.ts | 3 +- src/FiscalOS.Web/src/views/AccountsView.vue | 405 ++++++++++++++++++ src/FiscalOS.Web/src/views/HomeView.vue | 210 --------- .../src/views/TransactionsView.vue | 60 ++- 12 files changed, 616 insertions(+), 274 deletions(-) create mode 100644 src/FiscalOS.Web/src/components/CircleSpinner.vue create mode 100644 src/FiscalOS.Web/src/views/AccountsView.vue delete mode 100644 src/FiscalOS.Web/src/views/HomeView.vue diff --git a/src/FiscalOS.API/Institutions/Get/Endpoint.cs b/src/FiscalOS.API/Institutions/Get/Endpoint.cs index 5df5a15..27c5094 100644 --- a/src/FiscalOS.API/Institutions/Get/Endpoint.cs +++ b/src/FiscalOS.API/Institutions/Get/Endpoint.cs @@ -19,6 +19,7 @@ internal static class Endpoint var user = await appDbContext.Users .Include(u => u.Institutions) .ThenInclude(i => i.Accounts) + .ThenInclude(a => a.Metadata) .FirstOrDefaultAsync(u => u.Id == userId); if (user is null) diff --git a/src/FiscalOS.API/Institutions/Get/Response.cs b/src/FiscalOS.API/Institutions/Get/Response.cs index e0aa56d..55e8e8c 100644 --- a/src/FiscalOS.API/Institutions/Get/Response.cs +++ b/src/FiscalOS.API/Institutions/Get/Response.cs @@ -24,6 +24,7 @@ internal sealed record Response internal sealed record AccountDto { public string Id { get; init; } = string.Empty; + public string ProviderId { get; init; } = string.Empty; public string Name { get; init; } = string.Empty; [JsonConstructor] @@ -36,6 +37,9 @@ internal sealed record AccountDto return new() { Id = account.Id.ToString(), + ProviderId = account.Metadata is PlaidAccountMetadata plaidMetadata + ? plaidMetadata.PlaidId + : string.Empty, Name = account.Name, }; } diff --git a/src/FiscalOS.Web/src/assets/css/reset.css b/src/FiscalOS.Web/src/assets/css/reset.css index 53390eb..8dfe6f6 100644 --- a/src/FiscalOS.Web/src/assets/css/reset.css +++ b/src/FiscalOS.Web/src/assets/css/reset.css @@ -24,6 +24,24 @@ input { color: inherit; } +select { + font-family: inherit; + font-size: inherit; + color: inherit; + background-color: inherit; +} + +option { + font-family: inherit; + font-size: inherit; + color: inherit; + background-color: inherit; +} + +option :hover { + background-color: var(--bg-element); +} + a { text-decoration: none; color: inherit; @@ -41,4 +59,4 @@ img { table { width: 100%; border-collapse: collapse; -} +} \ No newline at end of file diff --git a/src/FiscalOS.Web/src/components/CircleSpinner.vue b/src/FiscalOS.Web/src/components/CircleSpinner.vue new file mode 100644 index 0000000..9cb7d85 --- /dev/null +++ b/src/FiscalOS.Web/src/components/CircleSpinner.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/FiscalOS.Web/src/components/NavSidebar.vue b/src/FiscalOS.Web/src/components/NavSidebar.vue index 5911724..a2c3370 100644 --- a/src/FiscalOS.Web/src/components/NavSidebar.vue +++ b/src/FiscalOS.Web/src/components/NavSidebar.vue @@ -39,23 +39,29 @@ > - - + @@ -82,6 +88,35 @@ } } + aside.collapsed { + width: calc(0.125rem + var(--button-size) / 2); + } + + aside.collapsed .toggle-button { + transform: rotate(180deg); + } + + aside.collapsed .sidebar-content { + opacity: 0; + visibility: hidden; + pointer-events: none; + } + + .sidebar-content { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + gap: 3rem; + flex: 1; + overflow: hidden; + opacity: 1; + visibility: visible; + transition-property: opacity, visibility; + transition-duration: var(--transition-duration); + transition-timing-function: var(--transition-function); + } + .logout-button { background: var(--bg-element); padding: 0.25rem 0.5rem; @@ -104,14 +139,6 @@ transition-timing-function: var(--transition-function); } - aside.collapsed { - width: calc(0.125rem + var(--button-size) / 2); - } - - aside.collapsed .toggle-button { - transform: rotate(180deg); - } - .toggle-button svg { --size: 1rem; width: var(--size); diff --git a/src/FiscalOS.Web/src/components/ProtectedLayout.vue b/src/FiscalOS.Web/src/components/ProtectedLayout.vue index 747a75c..41f6869 100644 --- a/src/FiscalOS.Web/src/components/ProtectedLayout.vue +++ b/src/FiscalOS.Web/src/components/ProtectedLayout.vue @@ -22,5 +22,9 @@ main { flex: 1; padding: 1rem; + overflow: auto; + scroll-behavior: smooth; + scrollbar-color: var(--bg-surface) var(--bg-app); + scrollbar-gutter: stable; } diff --git a/src/FiscalOS.Web/src/components/TransactionCard.vue b/src/FiscalOS.Web/src/components/TransactionCard.vue index e1c53c3..abc41d3 100644 --- a/src/FiscalOS.Web/src/components/TransactionCard.vue +++ b/src/FiscalOS.Web/src/components/TransactionCard.vue @@ -1,17 +1,72 @@ diff --git a/src/FiscalOS.Web/src/router/index.ts b/src/FiscalOS.Web/src/router/index.ts index 74ebf0c..a6063c3 100644 --- a/src/FiscalOS.Web/src/router/index.ts +++ b/src/FiscalOS.Web/src/router/index.ts @@ -23,6 +23,9 @@ const router = createRouter({ { path: 'login', component: () => import('../views/LoginView.vue'), + meta: { + title: 'Login', + }, }, ], }, @@ -60,15 +63,30 @@ const router = createRouter({ children: [ { path: '/', - component: () => import('../views/HomeView.vue'), + redirect: '/accounts', + }, + { + path: '/accounts', + component: () => import('../views/AccountsView.vue'), + meta: { + title: 'Accounts', + }, }, { path: '/transactions', component: () => import('../views/TransactionsView.vue'), - } + meta: { + title: 'Transactions', + }, + }, ], }, ], }); +router.beforeEach((to, _, next) => { + document.title = to.meta.title ? `FiscalOS - ${to.meta.title}` : 'FiscalOS'; + next(); +}); + export default router; diff --git a/src/FiscalOS.Web/src/services/institutionService.ts b/src/FiscalOS.Web/src/services/institutionService.ts index 3d89ec9..c53ae3c 100644 --- a/src/FiscalOS.Web/src/services/institutionService.ts +++ b/src/FiscalOS.Web/src/services/institutionService.ts @@ -136,8 +136,9 @@ type LinkTokenResponse = { type Account = { id: string; + providerId: string; name: string; -} +}; export type Institution = { id: string; diff --git a/src/FiscalOS.Web/src/views/AccountsView.vue b/src/FiscalOS.Web/src/views/AccountsView.vue new file mode 100644 index 0000000..dca2dfd --- /dev/null +++ b/src/FiscalOS.Web/src/views/AccountsView.vue @@ -0,0 +1,405 @@ + + + + + diff --git a/src/FiscalOS.Web/src/views/HomeView.vue b/src/FiscalOS.Web/src/views/HomeView.vue deleted file mode 100644 index 1fa4d69..0000000 --- a/src/FiscalOS.Web/src/views/HomeView.vue +++ /dev/null @@ -1,210 +0,0 @@ - - - - - diff --git a/src/FiscalOS.Web/src/views/TransactionsView.vue b/src/FiscalOS.Web/src/views/TransactionsView.vue index f5f6bc5..b475440 100644 --- a/src/FiscalOS.Web/src/views/TransactionsView.vue +++ b/src/FiscalOS.Web/src/views/TransactionsView.vue @@ -1,47 +1,45 @@ - + From 5eda5fbb76bf8db6b1d254df6f3ca4a10ba79b54 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 11 Mar 2026 05:47:34 -0500 Subject: [PATCH 07/10] feat(web): try to make pretty --- .../src/components/NavSidebar.vue | 89 +++++++++++++++---- .../src/components/ProtectedLayout.vue | 7 ++ .../icons/RightArrowBracketIcon.vue | 10 +++ .../src/views/TransactionsView.vue | 1 + 4 files changed, 90 insertions(+), 17 deletions(-) create mode 100644 src/FiscalOS.Web/src/components/icons/RightArrowBracketIcon.vue diff --git a/src/FiscalOS.Web/src/components/NavSidebar.vue b/src/FiscalOS.Web/src/components/NavSidebar.vue index a2c3370..7b514f1 100644 --- a/src/FiscalOS.Web/src/components/NavSidebar.vue +++ b/src/FiscalOS.Web/src/components/NavSidebar.vue @@ -4,6 +4,7 @@ import { useUserStore } from '@/stores/userStore'; import { computed } from 'vue'; import { RouterLink, useRouter } from 'vue-router'; + import RightArrowBracketIcon from './icons/RightArrowBracketIcon.vue'; const userStore = useUserStore(); const router = useRouter(); @@ -58,6 +59,7 @@ type="button" @click="handleLogout" > + Logout
@@ -88,20 +90,6 @@ } } - aside.collapsed { - width: calc(0.125rem + var(--button-size) / 2); - } - - aside.collapsed .toggle-button { - transform: rotate(180deg); - } - - aside.collapsed .sidebar-content { - opacity: 0; - visibility: hidden; - pointer-events: none; - } - .sidebar-content { display: flex; flex-direction: column; @@ -117,12 +105,64 @@ transition-timing-function: var(--transition-function); } - .logout-button { - background: var(--bg-element); - padding: 0.25rem 0.5rem; + .top { + padding: 1rem; + } + + nav ul { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + nav a { + display: flex; + width: 100%; + padding: 0.5rem 1rem; border-radius: 0.25rem; } + nav a.router-link-exact-active, + nav a:hover { + background: var(--bg-element); + } + + nav a.router-link-exact-active { + position: relative; + } + + nav a.router-link-exact-active::before { + content: ' '; + + position: absolute; + left: 0; + height: 1.25rem; + background: red; + border: 2px solid var(--brand-primary); + } + + .bottom { + padding: 1rem; + } + + .logout-button { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + width: 100%; + background: var(--bg-element); + padding: 0.5rem 1rem; + border-radius: 0.25rem; + } + + .logout-button svg { + --size: 1.5rem; + width: var(--size); + height: var(--size); + fill: var(--text-primary); + } + .toggle-button { display: flex; align-items: center; @@ -143,5 +183,20 @@ --size: 1rem; width: var(--size); height: var(--size); + fill: var(--text-primary); + } + + aside.collapsed { + width: calc(0.125rem + var(--button-size) / 2); + } + + aside.collapsed .toggle-button { + transform: rotate(180deg); + } + + aside.collapsed .sidebar-content { + opacity: 0; + visibility: hidden; + pointer-events: none; } diff --git a/src/FiscalOS.Web/src/components/ProtectedLayout.vue b/src/FiscalOS.Web/src/components/ProtectedLayout.vue index 41f6869..cda6a6d 100644 --- a/src/FiscalOS.Web/src/components/ProtectedLayout.vue +++ b/src/FiscalOS.Web/src/components/ProtectedLayout.vue @@ -22,9 +22,16 @@ main { flex: 1; padding: 1rem; + padding-right: 0rem; overflow: auto; scroll-behavior: smooth; scrollbar-color: var(--bg-surface) var(--bg-app); scrollbar-gutter: stable; } + + @media screen and (max-width: 48rem) { + main { + margin-right: 1rem; + } + } diff --git a/src/FiscalOS.Web/src/components/icons/RightArrowBracketIcon.vue b/src/FiscalOS.Web/src/components/icons/RightArrowBracketIcon.vue new file mode 100644 index 0000000..25bf027 --- /dev/null +++ b/src/FiscalOS.Web/src/components/icons/RightArrowBracketIcon.vue @@ -0,0 +1,10 @@ + diff --git a/src/FiscalOS.Web/src/views/TransactionsView.vue b/src/FiscalOS.Web/src/views/TransactionsView.vue index b475440..1502111 100644 --- a/src/FiscalOS.Web/src/views/TransactionsView.vue +++ b/src/FiscalOS.Web/src/views/TransactionsView.vue @@ -41,5 +41,6 @@ display: flex; flex-direction: column; gap: 1rem; + padding: 1rem; } From e3a75ccbc6f700329b07ec36f175be9d0f808e43 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:51:28 -0500 Subject: [PATCH 08/10] feat(api,web): update account and institution handling to use provider identifiers and enhance UI components for account addition --- src/FiscalOS.API/Accounts/Add/Endpoint.cs | 12 +- src/FiscalOS.API/Accounts/Add/Request.cs | 18 +- .../src/components/AddAccountForm.vue | 76 ++++ .../src/components/InstitutionCard.vue | 225 ++++++++++++ .../src/composables/useAccountService.ts | 7 + .../src/composables/useAuthService.ts | 28 +- .../src/composables/useInstitutionService.ts | 28 +- .../src/composables/useService.ts | 33 ++ .../src/composables/useTransactionService.ts | 24 +- src/FiscalOS.Web/src/main.ts | 6 +- .../src/services/accountService.ts | 58 ++++ .../src/services/institutionService.ts | 5 +- .../src/services/transactionService.ts | 11 +- src/FiscalOS.Web/src/views/AccountsView.vue | 324 ++---------------- .../Integration/Accounts/AddTests.cs | 62 ++-- 15 files changed, 484 insertions(+), 433 deletions(-) create mode 100644 src/FiscalOS.Web/src/components/AddAccountForm.vue create mode 100644 src/FiscalOS.Web/src/components/InstitutionCard.vue create mode 100644 src/FiscalOS.Web/src/composables/useAccountService.ts create mode 100644 src/FiscalOS.Web/src/composables/useService.ts create mode 100644 src/FiscalOS.Web/src/services/accountService.ts diff --git a/src/FiscalOS.API/Accounts/Add/Endpoint.cs b/src/FiscalOS.API/Accounts/Add/Endpoint.cs index d376897..779019a 100644 --- a/src/FiscalOS.API/Accounts/Add/Endpoint.cs +++ b/src/FiscalOS.API/Accounts/Add/Endpoint.cs @@ -24,9 +24,9 @@ internal static class Endpoint var userId = httpContext.GetUserId(); var user = await appDbContext.Users - .Include(u => u.Institutions.Where(i => i.Metadata is PlaidInstitutionMetadata && ((PlaidInstitutionMetadata)i.Metadata).PlaidId == request.PlaidInstitutionId)) + .Include(u => u.Institutions.Where(i => i.Metadata is PlaidInstitutionMetadata && ((PlaidInstitutionMetadata)i.Metadata).PlaidId == request.ProviderInstitutionId)) .ThenInclude(i => i.Metadata) - .Include(u => u.Accounts.Where(a => a.Metadata is PlaidAccountMetadata && ((PlaidAccountMetadata)a.Metadata).PlaidId == request.PlaidAccountId)) + .Include(u => u.Accounts.Where(a => a.Metadata is PlaidAccountMetadata && ((PlaidAccountMetadata)a.Metadata).PlaidId == request.ProviderAccountId)) .ThenInclude(a => a.Metadata) .AsSplitQuery() .SingleOrDefaultAsync(u => u.Id == userId, ct); @@ -40,7 +40,7 @@ internal static class Endpoint { return Results.ValidationProblem(new Dictionary { - ["PlaidInstitutionId"] = ["The PlaidInstitutionId field is invalid. No institution connected with the given PlaidInstitutionId was found for the user."], + [nameof(Request.ProviderInstitutionId)] = [$"The {nameof(Request.ProviderInstitutionId)} field is invalid."], }); } @@ -50,7 +50,7 @@ internal static class Endpoint { return Results.ValidationProblem(new Dictionary { - ["PlaidInstitutionId"] = ["The PlaidInstitutionId field is invalid. The connected institution has no plaid metadata"], + [nameof(Request.ProviderInstitutionId)] = [$"The {nameof(Request.ProviderInstitutionId)} field is invalid."], }); } @@ -60,8 +60,8 @@ internal static class Endpoint } var decryptedAccessToken = await encryptor.DecryptAsyncFor(user, plaidInstitutionMetadata.EncryptedAccessToken, ct); - var accountMetadata = PlaidAccountMetadata.From(request.PlaidAccountId, request.PlaidAccountName); - var account = Account.From(request.PlaidAccountName, accountMetadata); + var accountMetadata = PlaidAccountMetadata.From(request.ProviderAccountId, request.ProviderAccountName); + var account = Account.From(request.ProviderAccountName, accountMetadata); user.AddAccount(account); user.Institutions.First().AddAccount(account); diff --git a/src/FiscalOS.API/Accounts/Add/Request.cs b/src/FiscalOS.API/Accounts/Add/Request.cs index e78fb3d..c5a4a13 100644 --- a/src/FiscalOS.API/Accounts/Add/Request.cs +++ b/src/FiscalOS.API/Accounts/Add/Request.cs @@ -2,27 +2,27 @@ namespace FiscalOS.API.Accounts.Add; public record Request : IValidatableObject { - public string PlaidInstitutionId { get; init; } = string.Empty; - public string PlaidAccountId { get; init; } = string.Empty; - public string PlaidAccountName { get; init; } = string.Empty; + public string ProviderInstitutionId { get; init; } = string.Empty; + public string ProviderAccountId { get; init; } = string.Empty; + public string ProviderAccountName { get; init; } = string.Empty; public IEnumerable Validate(ValidationContext validationContext) { - if (string.IsNullOrWhiteSpace(PlaidInstitutionId)) + if (string.IsNullOrWhiteSpace(ProviderInstitutionId)) { - var fieldName = nameof(PlaidInstitutionId); + var fieldName = nameof(ProviderInstitutionId); yield return new($"The {fieldName} field is required.", [fieldName]); } - if (string.IsNullOrWhiteSpace(PlaidAccountId)) + if (string.IsNullOrWhiteSpace(ProviderAccountId)) { - var fieldName = nameof(PlaidAccountId); + var fieldName = nameof(ProviderAccountId); yield return new($"The {fieldName} field is required.", [fieldName]); } - if (string.IsNullOrWhiteSpace(PlaidAccountName)) + if (string.IsNullOrWhiteSpace(ProviderAccountName)) { - var fieldName = nameof(PlaidAccountName); + var fieldName = nameof(ProviderAccountName); yield return new($"The {fieldName} field is required.", [fieldName]); } } diff --git a/src/FiscalOS.Web/src/components/AddAccountForm.vue b/src/FiscalOS.Web/src/components/AddAccountForm.vue new file mode 100644 index 0000000..b6e2d7f --- /dev/null +++ b/src/FiscalOS.Web/src/components/AddAccountForm.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/src/FiscalOS.Web/src/components/InstitutionCard.vue b/src/FiscalOS.Web/src/components/InstitutionCard.vue new file mode 100644 index 0000000..b354022 --- /dev/null +++ b/src/FiscalOS.Web/src/components/InstitutionCard.vue @@ -0,0 +1,225 @@ + + + + + diff --git a/src/FiscalOS.Web/src/composables/useAccountService.ts b/src/FiscalOS.Web/src/composables/useAccountService.ts new file mode 100644 index 0000000..1473d6d --- /dev/null +++ b/src/FiscalOS.Web/src/composables/useAccountService.ts @@ -0,0 +1,7 @@ +import { AccountServiceFactoryKey } from '@/services/accountService'; +import type { UserStore } from '@/stores/userStore'; +import { useService } from './useService'; + +export function useAccountService(store: UserStore) { + return useService(store, AccountServiceFactoryKey); +} diff --git a/src/FiscalOS.Web/src/composables/useAuthService.ts b/src/FiscalOS.Web/src/composables/useAuthService.ts index 488217c..c958b4d 100644 --- a/src/FiscalOS.Web/src/composables/useAuthService.ts +++ b/src/FiscalOS.Web/src/composables/useAuthService.ts @@ -1,27 +1,7 @@ -import { AuthServiceFactoryKey } from "@/services/authService"; -import { ClientConfig, ClientFactoryKey } from "@/services/client"; -import type { UserStore } from "@/stores/userStore"; -import { inject } from "vue"; +import { AuthServiceFactoryKey } from '@/services/authService'; +import type { UserStore } from '@/stores/userStore'; +import { useService } from './useService'; export function useAuthService(store: UserStore) { - const clientFactory = inject(ClientFactoryKey); - const authServiceFactory = inject(AuthServiceFactoryKey); - - if (clientFactory === undefined) { - throw new Error("Failed to inject client factory.") - } - - if (authServiceFactory === undefined) { - throw new Error("Failed to inject auth service factory.") - } - - const clientConfig = new ClientConfig( - { Authorization: `Bearer ${store.user?.token}`}, - true, - store.refreshAccessToken - ); - const client = clientFactory.create(clientConfig); - const authService = authServiceFactory.create(client); - - return authService; + return useService(store, AuthServiceFactoryKey); } diff --git a/src/FiscalOS.Web/src/composables/useInstitutionService.ts b/src/FiscalOS.Web/src/composables/useInstitutionService.ts index 081138c..f7a5469 100644 --- a/src/FiscalOS.Web/src/composables/useInstitutionService.ts +++ b/src/FiscalOS.Web/src/composables/useInstitutionService.ts @@ -1,27 +1,7 @@ -import { ClientConfig, ClientFactoryKey } from "@/services/client"; -import { InstituionServiceFactoryKey } from "@/services/institutionService"; -import type { UserStore } from "@/stores/userStore"; -import { inject } from "vue"; +import { InstitutionServiceFactoryKey } from '@/services/institutionService'; +import type { UserStore } from '@/stores/userStore'; +import { useService } from './useService'; export function useInstitutionService(store: UserStore) { - const clientFactory = inject(ClientFactoryKey); - const institutionServiceFactory = inject(InstituionServiceFactoryKey); - - if (clientFactory === undefined) { - throw new Error("Failed to inject client factory.") - } - - if (institutionServiceFactory === undefined) { - throw new Error("Failed to inject institution service factory.") - } - - const clientConfig = new ClientConfig( - { Authorization: `Bearer ${store.user?.token}`}, - true, - store.refreshAccessToken - ); - const client = clientFactory.create(clientConfig); - const institutionService = institutionServiceFactory.create(client); - - return institutionService; + return useService(store, InstitutionServiceFactoryKey); } diff --git a/src/FiscalOS.Web/src/composables/useService.ts b/src/FiscalOS.Web/src/composables/useService.ts new file mode 100644 index 0000000..5b89466 --- /dev/null +++ b/src/FiscalOS.Web/src/composables/useService.ts @@ -0,0 +1,33 @@ +import type { InjectionKey } from 'vue'; +import { inject } from 'vue'; +import { ClientConfig, ClientFactoryKey, type IClient } from '@/services/client'; +import type { UserStore } from '@/stores/userStore'; + +export interface IServiceFactory { + create: (client: IClient) => TService; +} + +export function useService( + store: UserStore, + serviceFactoryKey: InjectionKey> +): TService { + const clientFactory = inject(ClientFactoryKey); + const serviceFactory = inject(serviceFactoryKey); + + if (clientFactory === undefined) { + throw new Error('Failed to inject client factory.'); + } + + if (serviceFactory === undefined) { + throw new Error('Failed to inject service factory.'); + } + + const clientConfig = new ClientConfig( + { Authorization: `Bearer ${store.user?.token}` }, + true, + store.refreshAccessToken + ); + + const client = clientFactory.create(clientConfig); + return serviceFactory.create(client); +} diff --git a/src/FiscalOS.Web/src/composables/useTransactionService.ts b/src/FiscalOS.Web/src/composables/useTransactionService.ts index 7a50ee8..98aae38 100644 --- a/src/FiscalOS.Web/src/composables/useTransactionService.ts +++ b/src/FiscalOS.Web/src/composables/useTransactionService.ts @@ -1,27 +1,7 @@ -import { ClientConfig, ClientFactoryKey } from '@/services/client'; import { TransactionServiceFactoryKey } from '@/services/transactionService'; import type { UserStore } from '@/stores/userStore'; -import { inject } from 'vue'; +import { useService } from './useService'; export function useTransactionService(store: UserStore) { - const clientFactory = inject(ClientFactoryKey); - const transactionServiceFactory = inject(TransactionServiceFactoryKey); - - if (clientFactory === undefined) { - throw new Error('Failed to inject client factory.'); - } - - if (transactionServiceFactory === undefined) { - throw new Error('Failed to inject transaction service factory.'); - } - - const clientConfig = new ClientConfig( - { Authorization: `Bearer ${store.user?.token}` }, - true, - store.refreshAccessToken - ); - const client = clientFactory.create(clientConfig); - const transactionService = transactionServiceFactory.create(client); - - return transactionService; + return useService(store, TransactionServiceFactoryKey); } diff --git a/src/FiscalOS.Web/src/main.ts b/src/FiscalOS.Web/src/main.ts index ad78699..cb322af 100644 --- a/src/FiscalOS.Web/src/main.ts +++ b/src/FiscalOS.Web/src/main.ts @@ -8,20 +8,22 @@ import router from './router'; import { ClientFactory, ClientFactoryKey } from './services/client'; import { AuthServiceFactory, AuthServiceFactoryKey } from './services/authService'; import { - InstituionServiceFactoryKey, + InstitutionServiceFactoryKey, InstitutionServiceFactory, } from './services/institutionService'; import { TransactionServiceFactory, TransactionServiceFactoryKey, } from './services/transactionService'; +import { AccountServiceFactory, AccountServiceFactoryKey } from './services/accountService'; const app = createApp(App); app.provide(ClientFactoryKey, new ClientFactory()); app.provide(AuthServiceFactoryKey, new AuthServiceFactory()); -app.provide(InstituionServiceFactoryKey, new InstitutionServiceFactory()); +app.provide(InstitutionServiceFactoryKey, new InstitutionServiceFactory()); app.provide(TransactionServiceFactoryKey, new TransactionServiceFactory()); +app.provide(AccountServiceFactoryKey, new AccountServiceFactory()); app.use(createPinia()); app.use(router); diff --git a/src/FiscalOS.Web/src/services/accountService.ts b/src/FiscalOS.Web/src/services/accountService.ts new file mode 100644 index 0000000..efb0c9a --- /dev/null +++ b/src/FiscalOS.Web/src/services/accountService.ts @@ -0,0 +1,58 @@ +import type { InjectionKey } from 'vue'; +import { ClientRequestWithBody, type IClient } from './client'; +import { Err, Ok, type Result } from 'ts-results'; + +type AccountServiceFactoryKeyType = InjectionKey; + +export const AccountServiceFactoryKey: AccountServiceFactoryKeyType = + Symbol('AccountServiceFactory'); + +export interface IAccountServiceFactory { + create: (client: IClient) => IAccountService; +} + +export class AccountServiceFactory implements IAccountServiceFactory { + create(client: IClient): IAccountService { + return new AccountService(client); + } +} + +export interface IAccountService { + add: ( + providerInstitutionId: string, + providerAccountId: string, + providerAccountName: string + ) => Promise>; +} + +export class AccountService implements IAccountService { + private readonly client: IClient; + private readonly endpoints = { + add: '/api/accounts', + }; + + constructor(client: IClient) { + this.client = client; + } + + async add(providerInstitutionId: string, providerAccountId: string, providerAccountName: string) { + const request = new ClientRequestWithBody(this.endpoints.add, undefined, { + providerInstitutionId, + providerAccountId, + providerAccountName, + }); + + try { + const res = await this.client.post(request); + + if (res.ok === false) { + return Err([new Error('Failed to add account.')]); + } + + return Ok(true); + } catch (error) { + console.error(error); + return Err([new Error('Failed to add account.')]); + } + } +} diff --git a/src/FiscalOS.Web/src/services/institutionService.ts b/src/FiscalOS.Web/src/services/institutionService.ts index c53ae3c..e6f8ecb 100644 --- a/src/FiscalOS.Web/src/services/institutionService.ts +++ b/src/FiscalOS.Web/src/services/institutionService.ts @@ -4,8 +4,9 @@ import { ClientRequest, ClientRequestWithBody, type IClient } from './client'; type InstitutionServiceFactoryKeyType = InjectionKey; -export const InstituionServiceFactoryKey: InstitutionServiceFactoryKeyType = - Symbol('AuthServiceFactory'); +export const InstitutionServiceFactoryKey: InstitutionServiceFactoryKeyType = Symbol( + 'InstitutionServiceFactory' +); export interface IInstitutionServiceFactory { create: (client: IClient) => IInstitutionService; diff --git a/src/FiscalOS.Web/src/services/transactionService.ts b/src/FiscalOS.Web/src/services/transactionService.ts index 6b889b1..e2ba0c3 100644 --- a/src/FiscalOS.Web/src/services/transactionService.ts +++ b/src/FiscalOS.Web/src/services/transactionService.ts @@ -4,8 +4,9 @@ import { Err, Ok, type Result } from 'ts-results'; type TransactionServiceFactoryKeyType = InjectionKey; -export const TransactionServiceFactoryKey: TransactionServiceFactoryKeyType = - Symbol('AuthServiceFactory'); +export const TransactionServiceFactoryKey: TransactionServiceFactoryKeyType = Symbol( + 'TransactionServiceFactory' +); export interface ITransactionServiceFactory { create: (client: IClient) => ITransactionService; @@ -40,13 +41,13 @@ export class TransactionService implements ITransactionService { const request = new ClientRequest(url); try { - const response = await this.client.get(request); + const res = await this.client.get(request); - if (response.ok === false) { + if (res.ok === false) { return Err([new Error('Failed to retrieve transactions.')]); } - const data = await response.json(); + const data = await res.json(); return Ok(data as Page); } catch (error) { console.error(error); diff --git a/src/FiscalOS.Web/src/views/AccountsView.vue b/src/FiscalOS.Web/src/views/AccountsView.vue index dca2dfd..3c40a32 100644 --- a/src/FiscalOS.Web/src/views/AccountsView.vue +++ b/src/FiscalOS.Web/src/views/AccountsView.vue @@ -1,7 +1,7 @@ @@ -259,147 +120,4 @@ gap: 1rem; padding: 1rem; } - - .institution-container { - display: flex; - flex-direction: column; - gap: 0.5rem; - } - - .institution-card, - .account-card, - .add-account-card { - display: flex; - align-items: center; - padding: 1rem; - border-radius: 0.25rem; - background: var(--bg-surface); - } - - .institution-card, - .account-card { - position: relative; - } - - .institution-card > div { - flex: 1; - } - - .institution-card > div:last-of-type { - display: flex; - justify-content: flex-end; - } - - .institution-card::before { - content: ''; - position: absolute; - top: 50%; - left: -0.25rem; - transform: translateY(-50%); - width: 0.5rem; - height: 0.5rem; - border-radius: 50%; - background: var(--brand-primary); - z-index: 1; - } - - .institution-card::after { - content: ''; - position: absolute; - top: 50%; - left: -1px; - height: calc(50% + 0.5rem); - width: 2px; - background: var(--brand-primary); - } - - .add-account-card { - margin-left: 2rem; - } - - .add-account-card { - gap: 0.5rem; - } - - .accounts-list { - display: flex; - flex-direction: column; - gap: 0.5rem; - } - - .account-row { - position: relative; - padding-left: 2rem; - } - - .account-row::before { - content: ''; - position: absolute; - top: 0; - left: -1px; - height: calc(100% + 0.5rem); - width: 2px; - background: var(--brand-primary); - } - - .account-row:not(:first-child)::before { - top: -0.5rem; - height: calc(100% + 1rem); - } - - .account-row:last-child::before { - height: 50%; - } - - .account-row:not(:first-child):last-child::before { - top: -0.5rem; - height: calc(50% + 0.5rem); - } - - .account-row::after { - content: ''; - position: absolute; - top: 50%; - left: -1px; - width: calc(2rem + 1px); - height: 2px; - background: var(--brand-primary); - transform: translateY(-50%); - } - - .account-card::before { - content: ''; - position: absolute; - top: 50%; - left: -0.25rem; - transform: translateY(-50%); - width: 0.5rem; - height: 0.5rem; - border-radius: 50%; - background: var(--brand-primary); - z-index: 1; - } - - .select-account-container { - display: flex; - gap: 0.25rem; - align-items: center; - } - - .select-account-container > select { - padding: 0.25rem; - border-radius: 0.25rem; - } - - .add-institution-button, - .add-account-button { - background: var(--bg-element); - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - } - - .add-account-button:disabled { - cursor: not-allowed; - opacity: 0.6; - } diff --git a/tests/FiscalOS.API.Tests/Integration/Accounts/AddTests.cs b/tests/FiscalOS.API.Tests/Integration/Accounts/AddTests.cs index b9297db..5a291b9 100644 --- a/tests/FiscalOS.API.Tests/Integration/Accounts/AddTests.cs +++ b/tests/FiscalOS.API.Tests/Integration/Accounts/AddTests.cs @@ -1,3 +1,4 @@ +using FiscalOS.API.Accounts.Add; using FiscalOS.Core.Queuing; using FiscalOS.Infra.Transactions.Plaid; @@ -31,9 +32,9 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi) await response.Should().BeValidationProblemDetails(new Dictionary() { - ["PlaidInstitutionId"] = ["The PlaidInstitutionId field is required."], - ["PlaidAccountId"] = ["The PlaidAccountId field is required."], - ["PlaidAccountName"] = ["The PlaidAccountName field is required."], + [nameof(Request.ProviderInstitutionId)] = [$"The {nameof(Request.ProviderInstitutionId)} field is required."], + [nameof(Request.ProviderAccountId)] = [$"The {nameof(Request.ProviderAccountId)} field is required."], + [nameof(Request.ProviderAccountName)] = [$"The {nameof(Request.ProviderAccountName)} field is required."], }); } @@ -45,9 +46,8 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi) .WithUserId(Guid.NewGuid()) .WithBody(new { - plaidAccountId = "accountId", - plaidAccountName = "Some Account", - accountCurrencyCode = "USD", + providerAccountId = "accountId", + providerAccountName = "Some Account", }) .Build(); @@ -55,7 +55,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi) await response.Should().BeValidationProblemDetails(new Dictionary() { - ["PlaidInstitutionId"] = ["The PlaidInstitutionId field is required."], + [nameof(Request.ProviderInstitutionId)] = [$"The {nameof(Request.ProviderInstitutionId)} field is required."], }); } @@ -67,9 +67,8 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi) .WithUserId(Guid.NewGuid()) .WithBody(new { - plaidInstitutionId = "institutionId", - plaidAccountName = "Some Account", - accountCurrencyCode = "USD", + providerInstitutionId = "institutionId", + providerAccountName = "Some Account", }) .Build(); @@ -77,7 +76,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi) await response.Should().BeValidationProblemDetails(new Dictionary() { - ["PlaidAccountId"] = ["The PlaidAccountId field is required."], + [nameof(Request.ProviderAccountId)] = [$"The {nameof(Request.ProviderAccountId)} field is required."], }); } @@ -89,9 +88,8 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi) .WithUserId(Guid.NewGuid()) .WithBody(new { - plaidInstitutionId = "institutionId", - plaidAccountId = "accountId", - accountCurrencyCode = "USD", + providerInstitutionId = "institutionId", + providerAccountId = "accountId", }) .Build(); @@ -99,7 +97,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi) await response.Should().BeValidationProblemDetails(new Dictionary() { - ["PlaidAccountName"] = ["The PlaidAccountName field is required."], + [nameof(Request.ProviderAccountName)] = [$"The {nameof(Request.ProviderAccountName)} field is required."], }); } @@ -111,10 +109,9 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi) .WithUserId(Guid.NewGuid()) .WithBody(new { - plaidInstitutionId = "id", - plaidAccountId = "id", - plaidAccountName = "Some Account", - accountCurrencyCode = "USD", + providerInstitutionId = "id", + providerAccountId = "id", + providerAccountName = "Some Account", }) .Build(); @@ -145,10 +142,9 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi) .WithUserId(user.Id) .WithBody(new { - plaidInstitutionId = "id", - plaidAccountId = "id", - plaidAccountName = "Some Account", - accountCurrencyCode = "USD", + providerInstitutionId = "id", + providerAccountId = "id", + providerAccountName = "Some Account", }) .Build(); @@ -156,7 +152,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi) await response.Should().BeValidationProblemDetails(new Dictionary() { - ["PlaidInstitutionId"] = ["The PlaidInstitutionId field is invalid. No institution connected with the given PlaidInstitutionId was found for the user."], + [nameof(Request.ProviderInstitutionId)] = [$"The {nameof(Request.ProviderInstitutionId)} field is invalid."], }); } @@ -199,10 +195,9 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi) .WithUserId(user.Id) .WithBody(new { - plaidInstitutionId = ((PlaidInstitutionMetadata)institution.Metadata!).PlaidId, - plaidAccountId = ((PlaidAccountMetadata)account.Metadata!).PlaidId, - plaidAccountName = ((PlaidAccountMetadata)account.Metadata).PlaidName, - accountCurrencyCode = "USD", + providerInstitutionId = ((PlaidInstitutionMetadata)institution.Metadata!).PlaidId, + providerAccountId = ((PlaidAccountMetadata)account.Metadata!).PlaidId, + providerAccountName = ((PlaidAccountMetadata)account.Metadata).PlaidName, }) .Build(); @@ -252,20 +247,15 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi) var newAccountId = "newAccountId"; var newAccountName = "New Account"; - var expectedBalance = 100; - var expectedCurrencyCode = "USD"; using var request = HttpRequestBuilder.New() .Post(AddUri) .WithUserId(user.Id) .WithBody(new { - plaidInstitutionId = ((PlaidInstitutionMetadata)institution.Metadata!).PlaidId, - plaidAccountId = newAccountId, - plaidAccountName = newAccountName, - accountCurrentBalance = expectedBalance, - accountAvailableBalance = expectedBalance, - accountCurrencyCode = expectedCurrencyCode, + providerInstitutionId = ((PlaidInstitutionMetadata)institution.Metadata!).PlaidId, + providerAccountId = newAccountId, + providerAccountName = newAccountName, }) .Build(); From cd69cea2ac3c93794ef8677700de7402298dd990 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:13:53 -0500 Subject: [PATCH 09/10] refactor: remove Playwright dependencies and configuration, update test cases, and clean up code formatting - Removed Playwright test dependencies from package.json and deleted playwright.config.ts. - Updated LoginForm component and its tests for improved readability and consistency. - Cleaned up App.vue and PublicLayout.vue by adjusting import formatting. - Fixed missing newlines in CSS and other files for better formatting. - Updated userStore to ensure proper object syntax. - Refactored LoginView to enhance code clarity and maintainability. --- .dockerignore | 11 + .github/workflows/deploy.yml | 94 +++++ .gitignore | 4 + .vscode/settings.json | 2 +- docker/Dockerfile.api | 38 ++ docker/Dockerfile.web | 13 + docker/docker-compose.prod.yml | 29 ++ docker/docker-compose.yml | 33 ++ docker/nginx.conf | 18 + src/FiscalOS.Web/e2e/tsconfig.json | 4 - src/FiscalOS.Web/e2e/vue.spec.ts | 6 - src/FiscalOS.Web/eslint.config.ts | 18 +- src/FiscalOS.Web/package-lock.json | 341 +++++++----------- src/FiscalOS.Web/package.json | 5 +- src/FiscalOS.Web/playwright.config.ts | 45 --- src/FiscalOS.Web/src/App.vue | 2 +- .../src/__tests__/LoginForm.spec.ts | 16 +- src/FiscalOS.Web/src/assets/css/reset.css | 2 +- src/FiscalOS.Web/src/components/LoginForm.vue | 217 +++++------ .../src/components/PublicLayout.vue | 4 +- .../src/components/icons/RightArrowIcon.vue | 8 +- src/FiscalOS.Web/src/stores/userStore.ts | 2 +- src/FiscalOS.Web/src/views/LoginView.vue | 32 +- 23 files changed, 523 insertions(+), 421 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/deploy.yml create mode 100644 docker/Dockerfile.api create mode 100644 docker/Dockerfile.web create mode 100644 docker/docker-compose.prod.yml create mode 100644 docker/docker-compose.yml create mode 100644 docker/nginx.conf delete mode 100644 src/FiscalOS.Web/e2e/tsconfig.json delete mode 100644 src/FiscalOS.Web/e2e/vue.spec.ts delete mode 100644 src/FiscalOS.Web/playwright.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e436ef2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.git/ +.vscode/ +.aspire/ +Coverage/ +tests/ +docker/data/ +docker/keys/ +**/bin/ +**/obj/ +**/node_modules/ +**/.env diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..840c5a9 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,94 @@ +name: Deploy +on: + push: + branches: + - main + workflow_dispatch: +env: + WEB_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/fiscalos-web + API_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/fiscalos-api +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + - name: Log in to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push web image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile.web + push: true + tags: | + ${{ env.WEB_IMAGE }}:latest + ${{ env.WEB_IMAGE }}:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Build and push API image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile.api + push: true + tags: | + ${{ env.API_IMAGE }}:latest + ${{ env.API_IMAGE }}:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + deploy: + runs-on: ubuntu-latest + needs: build-and-push + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Copy compose file to server + uses: appleboy/scp-action@v1.0.0 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + source: docker/docker-compose.prod.yml + target: ${{ secrets.DEPLOY_PATH }} + strip_components: 1 + - name: Deploy on server + uses: appleboy/ssh-action@v1 + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + IMAGE_TAG: ${{ github.sha }} + JWT_AUDIENCE: ${{ secrets.JWT_AUDIENCE }} + JWT_ISSUER: ${{ secrets.JWT_ISSUER }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + JWT_EXPIRY_IN_MINUTES: ${{ secrets.JWT_EXPIRY_IN_MINUTES }} + FILE_KEY_RING_PRIMARY_KEY_ID: ${{ secrets.FILE_KEY_RING_PRIMARY_KEY_ID }} + PLAID_CLIENT_ID: ${{ secrets.PLAID_CLIENT_ID }} + PLAID_SECRET: ${{ secrets.PLAID_SECRET }} + PLAID_WEBHOOK: ${{ secrets.PLAID_WEBHOOK }} + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + envs: DOCKERHUB_USERNAME,IMAGE_TAG,JWT_AUDIENCE,JWT_ISSUER,JWT_SECRET,JWT_EXPIRY_IN_MINUTES,FILE_KEY_RING_PRIMARY_KEY_ID,PLAID_CLIENT_ID,PLAID_SECRET,PLAID_WEBHOOK + script: | + cd ${{ secrets.DEPLOY_PATH }} + cat > .env << EOF + DOCKERHUB_USERNAME=${DOCKERHUB_USERNAME} + IMAGE_TAG=${IMAGE_TAG} + JWT_AUDIENCE=${JWT_AUDIENCE} + JWT_ISSUER=${JWT_ISSUER} + JWT_SECRET=${JWT_SECRET} + JWT_EXPIRY_IN_MINUTES=${JWT_EXPIRY_IN_MINUTES} + FILE_KEY_RING_PRIMARY_KEY_ID=${FILE_KEY_RING_PRIMARY_KEY_ID} + PLAID_CLIENT_ID=${PLAID_CLIENT_ID} + PLAID_SECRET=${PLAID_SECRET} + PLAID_WEBHOOK=${PLAID_WEBHOOK} + EOF + docker compose -f docker-compose.prod.yml pull + docker compose -f docker-compose.prod.yml up -d --remove-orphans + docker image prune -f diff --git a/.gitignore b/.gitignore index 6d8ec37..e9d619b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ coverage/ appsettings*.json !appsettings.Example.json +# docker bind-mount data +docker/data/ +docker/keys/ + # dotenv files .env diff --git a/.vscode/settings.json b/.vscode/settings.json index 941053d..f7e954e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,7 +11,7 @@ }, "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", - "cSpell.words": ["Dtos", "Encryptor", "Validatable"], + "cSpell.words": ["DOCKERHUB", "Dtos", "Encryptor", "fiscalos", "Validatable"], "python-envs.defaultEnvManager": "ms-python.python:system", "search.exclude": { "**/Migrations": true diff --git a/docker/Dockerfile.api b/docker/Dockerfile.api new file mode 100644 index 0000000..44e99a5 --- /dev/null +++ b/docker/Dockerfile.api @@ -0,0 +1,38 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /repo + +COPY Directory.Build.props . +COPY global.json . +COPY src/Directory.Build.props src/ +COPY src/Directory.Packages.props src/ + +COPY src/FiscalOS.Core/FiscalOS.Core.csproj src/FiscalOS.Core/ +COPY src/FiscalOS.Infra/FiscalOS.Infra.csproj src/FiscalOS.Infra/ +COPY src/FiscalOS.ServiceDefaults/FiscalOS.ServiceDefaults.csproj src/FiscalOS.ServiceDefaults/ +COPY src/FiscalOS.API/FiscalOS.API.csproj src/FiscalOS.API/ +COPY src/FiscalOS.AdminCLI/FiscalOS.AdminCLI.csproj src/FiscalOS.AdminCLI/ + +RUN dotnet restore src/FiscalOS.API/FiscalOS.API.csproj && \ + dotnet restore src/FiscalOS.AdminCLI/FiscalOS.AdminCLI.csproj + +COPY src/ src/ + +RUN dotnet publish src/FiscalOS.API/FiscalOS.API.csproj \ + -c Release \ + -o /publish/api \ + --no-restore && \ + dotnet publish src/FiscalOS.AdminCLI/FiscalOS.AdminCLI.csproj \ + -c Release \ + -o /publish/admincli \ + --no-restore + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final +WORKDIR /app + +COPY --from=build /publish/api . +COPY --from=build /publish/admincli /usr/local/lib/admincli + +ENV PATH="/usr/local/lib/admincli:${PATH}" + +EXPOSE 8080 +ENTRYPOINT ["dotnet", "FiscalOS.API.dll"] diff --git a/docker/Dockerfile.web b/docker/Dockerfile.web new file mode 100644 index 0000000..313f854 --- /dev/null +++ b/docker/Dockerfile.web @@ -0,0 +1,13 @@ +FROM node:24-alpine AS build +WORKDIR /app + +COPY src/FiscalOS.Web/package.json src/FiscalOS.Web/package-lock.json ./ +RUN npm ci + +COPY src/FiscalOS.Web/ . +RUN npm run build + +FROM nginx:alpine AS final +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 80 diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml new file mode 100644 index 0000000..ece92a0 --- /dev/null +++ b/docker/docker-compose.prod.yml @@ -0,0 +1,29 @@ +services: + web: + image: ${DOCKERHUB_USERNAME}/fiscalos-web:${IMAGE_TAG:-latest} + ports: + - "80:80" + depends_on: + - api + restart: unless-stopped + + api: + image: ${DOCKERHUB_USERNAME}/fiscalos-api:${IMAGE_TAG:-latest} + ports: + - "8080:8080" + environment: + - ASPNETCORE_ENVIRONMENT=Production + - AppDbContextOptions__DatabaseFilePath=/data/fiscalos.db + - JwtOptions__Audience=${JWT_AUDIENCE} + - JwtOptions__Issuer=${JWT_ISSUER} + - JwtOptions__Secret=${JWT_SECRET} + - JwtOptions__ExpiryInMinutes=${JWT_EXPIRY_IN_MINUTES} + - FileKeyRingOptions__KeysDirectoryPath=/keys + - FileKeyRingOptions__PrimaryKeyId=${FILE_KEY_RING_PRIMARY_KEY_ID} + - PlaidClientOptions__ClientId=${PLAID_CLIENT_ID} + - PlaidClientOptions__Secret=${PLAID_SECRET} + - PlaidClientOptions__Webhook=${PLAID_WEBHOOK} + volumes: + - ./data:/data + - ./keys:/keys + restart: unless-stopped diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..e6c6205 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,33 @@ +services: + web: + build: + context: .. + dockerfile: docker/Dockerfile.web + ports: + - "80:80" + depends_on: + - api + restart: unless-stopped + + api: + build: + context: .. + dockerfile: docker/Dockerfile.api + ports: + - "8080:8080" + environment: + - ASPNETCORE_ENVIRONMENT=Production + - AppDbContextOptions__DatabaseFilePath=/data/fiscalos.db + - JwtOptions__Audience=${JWT_AUDIENCE} + - JwtOptions__Issuer=${JWT_ISSUER} + - JwtOptions__Secret=${JWT_SECRET} + - JwtOptions__ExpiryInMinutes=${JWT_EXPIRY_IN_MINUTES} + - FileKeyRingOptions__KeysDirectoryPath=/keys + - FileKeyRingOptions__PrimaryKeyId=${FILE_KEY_RING_PRIMARY_KEY_ID} + - PlaidClientOptions__ClientId=${PLAID_CLIENT_ID} + - PlaidClientOptions__Secret=${PLAID_SECRET} + - PlaidClientOptions__Webhook=${PLAID_WEBHOOK} + volumes: + - ./data:/data + - ./keys:/keys + restart: unless-stopped diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..2cd7462 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,18 @@ +server { + listen 80; + root /usr/share/nginx/html; + index index.html; + + location /api/ { + proxy_pass http://api:8080/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/src/FiscalOS.Web/e2e/tsconfig.json b/src/FiscalOS.Web/e2e/tsconfig.json deleted file mode 100644 index 9f69f27..0000000 --- a/src/FiscalOS.Web/e2e/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@tsconfig/node24/tsconfig.json", - "include": ["./**/*"] -} diff --git a/src/FiscalOS.Web/e2e/vue.spec.ts b/src/FiscalOS.Web/e2e/vue.spec.ts deleted file mode 100644 index 4e74115..0000000 --- a/src/FiscalOS.Web/e2e/vue.spec.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { test, expect } from '@playwright/test' - -test('visits the app root url', async ({ page }) => { - await page.goto('/') - await expect(page.locator('h1')).toHaveText('You did it!') -}) diff --git a/src/FiscalOS.Web/eslint.config.ts b/src/FiscalOS.Web/eslint.config.ts index 528ec46..ac773e2 100644 --- a/src/FiscalOS.Web/eslint.config.ts +++ b/src/FiscalOS.Web/eslint.config.ts @@ -1,10 +1,9 @@ -import { globalIgnores } from 'eslint/config' -import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' -import pluginVue from 'eslint-plugin-vue' -import pluginPlaywright from 'eslint-plugin-playwright' -import pluginVitest from '@vitest/eslint-plugin' -import pluginOxlint from 'eslint-plugin-oxlint' -import skipFormatting from 'eslint-config-prettier/flat' +import { globalIgnores } from 'eslint/config'; +import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'; +import pluginVue from 'eslint-plugin-vue'; +import pluginVitest from '@vitest/eslint-plugin'; +import pluginOxlint from 'eslint-plugin-oxlint'; +import skipFormatting from 'eslint-config-prettier/flat'; export default defineConfigWithVueTs( { @@ -15,7 +14,6 @@ export default defineConfigWithVueTs( ...pluginVue.configs['flat/essential'], vueTsConfigs.recommended, { - ...pluginPlaywright.configs['flat/recommended'], files: ['e2e/**/*.{test,spec}.{js,ts,jsx,tsx}'], }, { @@ -23,5 +21,5 @@ export default defineConfigWithVueTs( files: ['src/**/__tests__/*'], }, ...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'), - skipFormatting, -) + skipFormatting +); diff --git a/src/FiscalOS.Web/package-lock.json b/src/FiscalOS.Web/package-lock.json index ed7c89c..4bf5188 100644 --- a/src/FiscalOS.Web/package-lock.json +++ b/src/FiscalOS.Web/package-lock.json @@ -16,7 +16,6 @@ "vue-router": "^5.0.2" }, "devDependencies": { - "@playwright/test": "^1.58.2", "@tsconfig/node24": "^24.0.4", "@types/jsdom": "^27.0.0", "@types/node": "^24.10.13", @@ -29,7 +28,6 @@ "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-oxlint": "~1.46.0", - "eslint-plugin-playwright": "^2.5.1", "eslint-plugin-vue": "~10.8.0", "jiti": "^2.6.1", "jsdom": "^28.1.0", @@ -1206,9 +1204,9 @@ } }, "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1280,9 +1278,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1851,22 +1849,6 @@ "node": ">=14" } }, - "node_modules/@playwright/test": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", - "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.58.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -1882,9 +1864,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -1896,9 +1878,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -1910,9 +1892,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -1924,9 +1906,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -1938,9 +1920,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -1952,9 +1934,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -1966,9 +1948,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -1980,9 +1962,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -1994,9 +1976,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -2008,9 +1990,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -2022,9 +2004,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -2036,9 +2018,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -2050,9 +2032,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], @@ -2064,9 +2046,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -2078,9 +2060,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -2092,9 +2074,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -2106,9 +2088,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -2120,9 +2102,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -2134,9 +2116,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -2148,9 +2130,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "cpu": [ "x64" ], @@ -2162,9 +2144,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -2176,9 +2158,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -2190,9 +2172,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -2204,9 +2186,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -2218,9 +2200,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -3257,9 +3239,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -3878,15 +3860,15 @@ "license": "MIT" }, "node_modules/editorconfig": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", - "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", "dev": true, "license": "MIT", "dependencies": { "@one-ini/wasm": "0.1.1", "commander": "^10.0.0", - "minimatch": "9.0.1", + "minimatch": "^9.0.1", "semver": "^7.5.3" }, "bin": { @@ -3896,22 +3878,6 @@ "node": ">=14" } }, - "node_modules/editorconfig/node_modules/minimatch": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", - "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/editorconfig/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -4168,35 +4134,6 @@ "jsonc-parser": "^3.3.1" } }, - "node_modules/eslint-plugin-playwright": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-2.7.0.tgz", - "integrity": "sha512-kUgwDZL3knnuJF53WSf5xNnB1aLPnX8furoh0PSrmmFIfMfIMmY3sNd4gtZ2MUUnaIX1/A9ndYtD7bhV1dj+1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "globals": "^17.3.0" - }, - "engines": { - "node": ">=16.9.0" - }, - "peerDependencies": { - "eslint": ">=8.40.0" - } - }, - "node_modules/eslint-plugin-playwright/node_modules/globals": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", - "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint-plugin-vue": { "version": "10.8.0", "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.8.0.tgz", @@ -4297,9 +4234,9 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -5391,13 +5328,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -5929,38 +5866,6 @@ "pathe": "^2.0.3" } }, - "node_modules/playwright": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", - "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.58.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", - "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -6155,9 +6060,9 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -6171,31 +6076,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, diff --git a/src/FiscalOS.Web/package.json b/src/FiscalOS.Web/package.json index a1c63c7..97d79fa 100644 --- a/src/FiscalOS.Web/package.json +++ b/src/FiscalOS.Web/package.json @@ -8,7 +8,6 @@ "build": "run-p type-check \"build-only {@}\" --", "preview": "vite preview", "test:unit": "vitest", - "test:e2e": "playwright test", "build-only": "vite build", "type-check": "vue-tsc --build", "lint": "run-s lint:*", @@ -25,7 +24,6 @@ "vue-router": "^5.0.2" }, "devDependencies": { - "@playwright/test": "^1.58.2", "@tsconfig/node24": "^24.0.4", "@types/jsdom": "^27.0.0", "@types/node": "^24.10.13", @@ -38,7 +36,6 @@ "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-oxlint": "~1.46.0", - "eslint-plugin-playwright": "^2.5.1", "eslint-plugin-vue": "~10.8.0", "jiti": "^2.6.1", "jsdom": "^28.1.0", @@ -70,4 +67,4 @@ "@vue/shared": "beta", "@vue/compat": "beta" } -} +} \ No newline at end of file diff --git a/src/FiscalOS.Web/playwright.config.ts b/src/FiscalOS.Web/playwright.config.ts deleted file mode 100644 index 455ecd4..0000000 --- a/src/FiscalOS.Web/playwright.config.ts +++ /dev/null @@ -1,45 +0,0 @@ -import process from 'node:process' -import { defineConfig, devices } from '@playwright/test' - -export default defineConfig({ - testDir: './e2e', - timeout: 30 * 1000, - expect: { - timeout: 5000, - }, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, - reporter: 'html', - use: { - actionTimeout: 0, - baseURL: process.env.CI ? 'http://localhost:4173' : 'http://localhost:5173', - trace: 'on-first-retry', - headless: !!process.env.CI, - }, - projects: [ - { - name: 'chromium', - use: { - ...devices['Desktop Chrome'], - }, - }, - { - name: 'firefox', - use: { - ...devices['Desktop Firefox'], - }, - }, - { - name: 'webkit', - use: { - ...devices['Desktop Safari'], - }, - }, - ], - webServer: { - command: process.env.CI ? 'npm run preview' : 'npm run dev', - port: process.env.CI ? 4173 : 5173, - reuseExistingServer: !process.env.CI, - }, -}) diff --git a/src/FiscalOS.Web/src/App.vue b/src/FiscalOS.Web/src/App.vue index 6fc4dc2..e6fe12f 100644 --- a/src/FiscalOS.Web/src/App.vue +++ b/src/FiscalOS.Web/src/App.vue @@ -1,5 +1,5 @@