Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b45463e56b |
@@ -1,4 +1,3 @@
|
||||
{
|
||||
"appHostPath": "../src/FiscalOS.AppHost/FiscalOS.AppHost.csproj",
|
||||
"defaultWatchEnabled": "true"
|
||||
"appHostPath": "../src/FiscalOS.AppHost/FiscalOS.AppHost.csproj"
|
||||
}
|
||||
+1
-1
@@ -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 = true:suggestion
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
csharp_style_expression_bodied_methods = false:silent
|
||||
csharp_style_expression_bodied_operators = false:silent
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
##
|
||||
## Get latest from `dotnet new gitignore`
|
||||
|
||||
# coverage
|
||||
coverage/
|
||||
|
||||
# appsettings
|
||||
appsettings*.json
|
||||
!appsettings.Example.json
|
||||
|
||||
Vendored
-11
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "aspire",
|
||||
"request": "launch",
|
||||
"name": "Aspire: Launch default apphost",
|
||||
"program": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+1
-6
@@ -10,10 +10,5 @@
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"cSpell.words": ["Encryptor"],
|
||||
"python-envs.defaultEnvManager": "ms-python.python:system",
|
||||
"search.exclude": {
|
||||
"**/Migrations": true
|
||||
}
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[*.cs]
|
||||
|
||||
dotnet_code_quality.ca1711.allowed_suffixes = Queue
|
||||
dotnet_diagnostic.CA1031.severity = none
|
||||
@@ -3,7 +3,7 @@
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Aspire.Hosting.JavaScript" Version="13.1.2" />
|
||||
<PackageVersion Include="Aspire.Hosting.JavaScript" Version="13.1.1" />
|
||||
<PackageVersion Include="Going.Plaid" Version="6.56.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using Account = FiscalOS.Core.Accounts.Account;
|
||||
|
||||
namespace FiscalOS.API.Accounts.Add;
|
||||
|
||||
internal static class Endpoint
|
||||
@@ -15,20 +13,15 @@ internal static class Endpoint
|
||||
HttpContext httpContext,
|
||||
[FromBody] Request request,
|
||||
[FromServices] AppDbContext appDbContext,
|
||||
[FromServices] IEncryptor encryptor,
|
||||
[FromServices] IPlaidAccountService plaidAccountService,
|
||||
[FromServices] IAsyncQueue<SyncUpdatesQueueItem> queue,
|
||||
CancellationToken ct
|
||||
)
|
||||
{
|
||||
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))
|
||||
.ThenInclude(i => i.Metadata)
|
||||
.Include(u => u.Institutions.Where(i => i.Metadata is PlaidMetadata && ((PlaidMetadata)i.Metadata).PlaidId == request.PlaidInstitutionId))
|
||||
.Include(u => u.Accounts.Where(a => a.Metadata is PlaidAccountMetadata && ((PlaidAccountMetadata)a.Metadata).PlaidId == request.PlaidAccountId))
|
||||
.ThenInclude(a => a.Metadata)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync(u => u.Id == userId, ct);
|
||||
|
||||
if (user is null)
|
||||
@@ -44,29 +37,19 @@ internal static class Endpoint
|
||||
});
|
||||
}
|
||||
|
||||
var plaidInstitutionMetadata = (PlaidInstitutionMetadata?)user.Institutions.First().Metadata;
|
||||
|
||||
if (plaidInstitutionMetadata is null)
|
||||
{
|
||||
return Results.ValidationProblem(new Dictionary<string, string[]>
|
||||
{
|
||||
["PlaidInstitutionId"] = ["The PlaidInstitutionId field is invalid. The connected institution has no plaid metadata"],
|
||||
});
|
||||
}
|
||||
|
||||
if (user.Accounts.Any())
|
||||
{
|
||||
return Results.Conflict();
|
||||
}
|
||||
|
||||
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 account = Account.From(user.Institutions.First().Id, request.PlaidAccountName, accountMetadata);
|
||||
var balance = Balance.From(request.AccountCurrentBalance, request.AccountAvailableBalance, request.AccountCurrencyCode);
|
||||
|
||||
account.AddBalance(balance);
|
||||
user.AddAccount(account);
|
||||
user.Institutions.First().AddAccount(account);
|
||||
|
||||
await appDbContext.SaveChangesAsync(ct);
|
||||
await queue.EnqueueAsync(new(plaidInstitutionMetadata.ItemId), ct);
|
||||
|
||||
return Results.Ok();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ 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 decimal AccountCurrentBalance { get; init; }
|
||||
public decimal AccountAvailableBalance { get; init; }
|
||||
public string AccountCurrencyCode { get; init; } = string.Empty;
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
@@ -25,5 +28,11 @@ public record Request : IValidatableObject
|
||||
var fieldName = nameof(PlaidAccountName);
|
||||
yield return new($"The {fieldName} field is required.", [fieldName]);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(AccountCurrencyCode))
|
||||
{
|
||||
var fieldName = nameof(AccountCurrencyCode);
|
||||
yield return new($"The {fieldName} field is required.", [fieldName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ internal static class AuthExtensions
|
||||
var authGroup = app.MapGroup(RouteGroupPrefix);
|
||||
|
||||
authGroup.MapLoginEndpoint();
|
||||
authGroup.MapLogoutEndpoint();
|
||||
|
||||
authGroup.MapRefreshEndpoint()
|
||||
.RequireAuthorization(Schemes.AllowExpiredTokens);
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
internal static class Endpoint
|
||||
{
|
||||
private const string Route = "/logout";
|
||||
|
||||
public static RouteHandlerBuilder MapLogoutEndpoint(this RouteGroupBuilder groupBuilder)
|
||||
{
|
||||
return groupBuilder.MapPost(Route, HandleAsync);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] AppDbContext appDbContext,
|
||||
CancellationToken ct
|
||||
)
|
||||
{
|
||||
var userId = httpContext.GetUserId();
|
||||
var refreshToken = httpContext.GetRefreshTokenFromCookie();
|
||||
|
||||
var user = await appDbContext.Users
|
||||
.Include(u => u.RefreshTokens
|
||||
.Where(r => r.Token == refreshToken)
|
||||
)
|
||||
.Where(u => u.Id == userId)
|
||||
.SingleOrDefaultAsync(ct);
|
||||
|
||||
if (user is null || user.RefreshTokens.Any() is false)
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
user.RefreshTokens.First().Revoke();
|
||||
await appDbContext.SaveChangesAsync(ct);
|
||||
|
||||
httpContext.ExpireRefreshTokenCookie();
|
||||
|
||||
return Results.NoContent();
|
||||
}
|
||||
}
|
||||
@@ -25,21 +25,6 @@ internal static class HttpContextExtensions
|
||||
);
|
||||
}
|
||||
|
||||
public static void ExpireRefreshTokenCookie(this HttpContext context)
|
||||
{
|
||||
context.Response.Cookies.Append(
|
||||
RefreshTokenCookieName,
|
||||
string.Empty,
|
||||
new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
Expires = DateTimeOffset.MinValue,
|
||||
SameSite = SameSiteMode.Strict,
|
||||
Secure = true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static string GetRefreshTokenFromCookie(this HttpContext context)
|
||||
{
|
||||
return context.Request.Cookies[RefreshTokenCookieName] ?? string.Empty;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using Institution = FiscalOS.Core.Accounts.Institution;
|
||||
|
||||
namespace FiscalOS.API.Institutions.Connect;
|
||||
|
||||
internal static class Endpoint
|
||||
@@ -15,7 +13,7 @@ internal static class Endpoint
|
||||
HttpContext httpContext,
|
||||
[FromBody] Request request,
|
||||
[FromServices] AppDbContext appDbContext,
|
||||
[FromServices] IPlaidAccountService plaidAccountService,
|
||||
[FromServices] PlaidService plaidService,
|
||||
[FromServices] IEncryptor encryptor,
|
||||
CancellationToken ct
|
||||
)
|
||||
@@ -25,8 +23,8 @@ internal static class Endpoint
|
||||
var user = await appDbContext.Users
|
||||
.Include(u => u.Institutions
|
||||
.Where(
|
||||
i => i.Metadata is PlaidInstitutionMetadata &&
|
||||
((PlaidInstitutionMetadata)i.Metadata).PlaidId == request.PlaidInstitutionId
|
||||
i => i.Metadata is PlaidMetadata &&
|
||||
((PlaidMetadata)i.Metadata).PlaidId == request.PlaidInstitutionId
|
||||
)
|
||||
)
|
||||
.ThenInclude(i => i.Metadata)
|
||||
@@ -42,11 +40,12 @@ internal static class Endpoint
|
||||
return Results.Conflict();
|
||||
}
|
||||
|
||||
var (itemId, accessToken) = await plaidAccountService.ExchangeTokenAsync(request.PublicToken);
|
||||
var item = await plaidAccountService.GetItemAsync(accessToken);
|
||||
var (itemId, accessToken) = await plaidService.ExchangeTokenAsync(request.PublicToken);
|
||||
var item = await plaidService.GetItemAsync(accessToken);
|
||||
|
||||
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, accessToken, ct);
|
||||
|
||||
var plaidMetadata = PlaidInstitutionMetadata.From(item.InstitutionId, item.InstitutionName, encryptedAccessToken, itemId);
|
||||
var plaidMetadata = PlaidMetadata.From(item.InstitutionId, item.InstitutionName, encryptedAccessToken);
|
||||
var institution = Institution.From(item.InstitutionName, plaidMetadata);
|
||||
|
||||
user.AddInstitution(institution);
|
||||
|
||||
@@ -18,7 +18,6 @@ internal static class Endpoint
|
||||
|
||||
var user = await appDbContext.Users
|
||||
.Include(u => u.Institutions)
|
||||
.ThenInclude(i => i.Accounts)
|
||||
.FirstOrDefaultAsync(u => u.Id == userId);
|
||||
|
||||
if (user is null)
|
||||
@@ -27,7 +26,7 @@ internal static class Endpoint
|
||||
}
|
||||
|
||||
var institutionDtos = user.Institutions
|
||||
.Select(InstitutionDto.From)
|
||||
.Select(InstitutionDto.FromInstitution)
|
||||
.OrderBy(dto => dto.Name);
|
||||
|
||||
return Results.Ok(Response.From(institutionDtos));
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
using Account = FiscalOS.Core.Accounts.Account;
|
||||
using Institution = FiscalOS.Core.Accounts.Institution;
|
||||
|
||||
namespace FiscalOS.API.Institutions.Get;
|
||||
|
||||
internal sealed record Response
|
||||
@@ -21,44 +18,22 @@ internal sealed record Response
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record AccountDto
|
||||
{
|
||||
public string Id { get; init; } = string.Empty;
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonConstructor]
|
||||
private AccountDto()
|
||||
{
|
||||
}
|
||||
|
||||
public static AccountDto From(Account account)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Id = account.Id.ToString(),
|
||||
Name = account.Name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record InstitutionDto
|
||||
{
|
||||
public string Id { get; init; } = string.Empty;
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public IEnumerable<AccountDto> Accounts { get; init; } = [];
|
||||
|
||||
[JsonConstructor]
|
||||
private InstitutionDto()
|
||||
{
|
||||
}
|
||||
|
||||
public static InstitutionDto From(Institution institution)
|
||||
public static InstitutionDto FromInstitution(Institution institution)
|
||||
{
|
||||
return new()
|
||||
return new InstitutionDto
|
||||
{
|
||||
Id = institution.Id.ToString(),
|
||||
Name = institution.Name,
|
||||
Accounts = institution.Accounts.Select(AccountDto.From),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ internal static class Endpoint
|
||||
HttpContext httpContext,
|
||||
[FromRoute] Guid id,
|
||||
[FromServices] AppDbContext appDbContext,
|
||||
[FromServices] IPlaidAccountService plaidAccountService,
|
||||
[FromServices] PlaidService plaidService,
|
||||
[FromServices] IEncryptor encryptor,
|
||||
CancellationToken ct
|
||||
)
|
||||
@@ -37,14 +37,14 @@ internal static class Endpoint
|
||||
|
||||
var institution = user.Institutions.First();
|
||||
|
||||
if (institution.Metadata is not PlaidInstitutionMetadata plaidMetadata)
|
||||
if (institution.Metadata is not PlaidMetadata plaidMetadata)
|
||||
{
|
||||
return Results.BadRequest();
|
||||
}
|
||||
|
||||
var accessToken = await encryptor.DecryptAsyncFor(user, plaidMetadata.EncryptedAccessToken, ct);
|
||||
var accounts = await plaidAccountService.GetAccountsAsync(accessToken);
|
||||
var accountsDtos = accounts.Select(a => AvailableAccountDto.From(plaidMetadata, a));
|
||||
var accounts = await plaidService.GetAccountsAsync(accessToken);
|
||||
var accountsDtos = accounts.Select(AccountDto.FromPlaidAccount);
|
||||
|
||||
return Results.Ok(Response.From(accountsDtos));
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ namespace FiscalOS.API.Institutions.GetAvailable;
|
||||
|
||||
internal sealed record Response
|
||||
{
|
||||
public IEnumerable<AvailableAccountDto> Accounts { get; init; } = [];
|
||||
public IEnumerable<AccountDto> Accounts { get; init; } = [];
|
||||
|
||||
[JsonConstructor]
|
||||
private Response()
|
||||
{
|
||||
}
|
||||
|
||||
public static Response From(IEnumerable<AvailableAccountDto> accounts)
|
||||
public static Response From(IEnumerable<AccountDto> accounts)
|
||||
{
|
||||
return new Response
|
||||
{
|
||||
@@ -18,9 +18,8 @@ internal sealed record Response
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record AvailableAccountDto
|
||||
internal sealed record AccountDto
|
||||
{
|
||||
public string ProviderInstitutionId { get; init; } = string.Empty;
|
||||
public string ProviderId { get; init; } = string.Empty;
|
||||
public string ProviderName { get; init; } = string.Empty;
|
||||
public decimal CurrentBalance { get; init; }
|
||||
@@ -28,18 +27,14 @@ internal sealed record AvailableAccountDto
|
||||
public string CurrencyCode { get; init; } = string.Empty;
|
||||
|
||||
[JsonConstructor]
|
||||
private AvailableAccountDto()
|
||||
private AccountDto()
|
||||
{
|
||||
}
|
||||
|
||||
public static AvailableAccountDto From(
|
||||
PlaidInstitutionMetadata plaidMetadata,
|
||||
Going.Plaid.Entity.Account plaidAccount
|
||||
)
|
||||
public static AccountDto FromPlaidAccount(Going.Plaid.Entity.Account plaidAccount)
|
||||
{
|
||||
return new AvailableAccountDto
|
||||
return new AccountDto
|
||||
{
|
||||
ProviderInstitutionId = plaidMetadata.PlaidId,
|
||||
ProviderId = plaidAccount.AccountId,
|
||||
ProviderName = plaidAccount.Name,
|
||||
AvailableBalance = plaidAccount.Available,
|
||||
|
||||
@@ -12,7 +12,7 @@ internal static class Endpoint
|
||||
private static async Task<IResult> HandleAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] AppDbContext appDbContext,
|
||||
[FromServices] IPlaidAccountService plaidAccountService
|
||||
[FromServices] PlaidService plaidService
|
||||
)
|
||||
{
|
||||
var userId = httpContext.GetUserId();
|
||||
@@ -24,7 +24,7 @@ internal static class Endpoint
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var linkToken = await plaidAccountService.CreateLinkTokenAsync(user.Id.ToString());
|
||||
var linkToken = await plaidService.CreateLinkTokenAsync(user.Id.ToString());
|
||||
|
||||
return Results.Ok(Response.From(linkToken));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using FiscalOS.ServiceDefaults;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.AddServiceDefaults();
|
||||
@@ -5,12 +7,6 @@ builder.AddServiceDefaults();
|
||||
builder.Services.AddValidation();
|
||||
builder.Services.AddProblemDetails();
|
||||
|
||||
builder.Services.ConfigureHttpJsonOptions(static options =>
|
||||
{
|
||||
options.SerializerOptions.Converters.Add(new EnumConverterFactory());
|
||||
options.SerializerOptions.Converters.Add(new WebhookBaseConverter());
|
||||
});
|
||||
|
||||
builder.Services.AddInfrastructure();
|
||||
|
||||
builder.Services.AddAuthentication(static o =>
|
||||
@@ -45,6 +41,5 @@ app.MapDefaultEndpoints();
|
||||
app.MapAuthEndpoints();
|
||||
app.MapAccountsEndpoints();
|
||||
app.MapInstitutionsEndpoints();
|
||||
app.MapTransactionsGroup();
|
||||
|
||||
app.Run();
|
||||
@@ -1,62 +0,0 @@
|
||||
namespace FiscalOS.API.Transactions.FireWebhook;
|
||||
|
||||
internal static class Endpoint
|
||||
{
|
||||
private const string Route = "/fire-webhook";
|
||||
|
||||
public static RouteHandlerBuilder MapFireWebhookEndpoint(this RouteGroupBuilder groupBuilder)
|
||||
{
|
||||
return groupBuilder.MapPost(Route, HandleAsync);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleAsync(
|
||||
HttpContext httpContext,
|
||||
[FromBody] Request request,
|
||||
[FromServices] AppDbContext appDbContext,
|
||||
[FromServices] IEncryptor encryptor,
|
||||
[FromServices] PlaidClient plaidClient,
|
||||
CancellationToken ct
|
||||
)
|
||||
{
|
||||
var userId = httpContext.GetUserId();
|
||||
|
||||
var user = await appDbContext.Users
|
||||
.Include(u => u.Accounts.Where(a => a.Id.ToString() == request.AccountId))
|
||||
.ThenInclude(static a => a.Institution!)
|
||||
.ThenInclude(static i => i.Metadata)
|
||||
.FirstOrDefaultAsync(u => u.Id == userId, ct);
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
if (user.Accounts.Any() is false)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var plaidMetadata = (PlaidInstitutionMetadata?)user.Accounts.First().Institution?.Metadata;
|
||||
|
||||
if (plaidMetadata is null)
|
||||
{
|
||||
return Results.InternalServerError("No metadata associated with account");
|
||||
}
|
||||
|
||||
var accessToken = await encryptor.DecryptAsyncFor(user, plaidMetadata.EncryptedAccessToken, ct);
|
||||
|
||||
var fireEventResponse = await plaidClient.SandboxItemFireWebhookAsync(new()
|
||||
{
|
||||
AccessToken = accessToken,
|
||||
WebhookType = SandboxItemFireWebhookRequestWebhookTypeEnum.Transactions,
|
||||
WebhookCode = SandboxItemFireWebhookRequestWebhookCodeEnum.SyncUpdatesAvailable,
|
||||
});
|
||||
|
||||
if (fireEventResponse.IsSuccessStatusCode is false)
|
||||
{
|
||||
return Results.InternalServerError(fireEventResponse.Error?.ErrorMessage);
|
||||
}
|
||||
|
||||
return Results.Ok(new { fireEventResponse.RequestId, fireEventResponse.WebhookFired });
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace FiscalOS.API.Transactions.FireWebhook;
|
||||
|
||||
public sealed record Request(
|
||||
string AccountId,
|
||||
string WebhookType,
|
||||
string WebhookCode
|
||||
);
|
||||
@@ -1,21 +0,0 @@
|
||||
namespace FiscalOS.API.Transactions;
|
||||
|
||||
internal static class TransactionsExtensions
|
||||
{
|
||||
private const string RouteGroupPrefix = "/transactions";
|
||||
|
||||
public static RouteGroupBuilder MapTransactionsGroup(this WebApplication app)
|
||||
{
|
||||
var transactionsGroup = app.MapGroup(RouteGroupPrefix)
|
||||
.RequireAuthorization();
|
||||
|
||||
if (app.Environment.IsProduction() is false)
|
||||
{
|
||||
transactionsGroup.MapFireWebhookEndpoint();
|
||||
}
|
||||
|
||||
transactionsGroup.MapWebhookEndpoint().AllowAnonymous();
|
||||
|
||||
return transactionsGroup;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
namespace FiscalOS.API.Transactions.Webhook;
|
||||
|
||||
internal static class Endpoint
|
||||
{
|
||||
private const string Route = "/webhook";
|
||||
|
||||
public static RouteHandlerBuilder MapWebhookEndpoint(this RouteGroupBuilder groupBuilder)
|
||||
{
|
||||
return groupBuilder.MapPost(Route, HandleAsync);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleAsync(
|
||||
[FromBody] WebhookBase request,
|
||||
[FromServices] ILogger<Program> logger,
|
||||
[FromServices] IAsyncQueue<SyncUpdatesQueueItem> queue,
|
||||
CancellationToken ct
|
||||
)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Received {WebhookType} webhook with {WebhookCode} payload",
|
||||
request.WebhookType,
|
||||
request.WebhookCode
|
||||
);
|
||||
|
||||
switch (request)
|
||||
{
|
||||
case SyncUpdatesAvailableWebhook webhook:
|
||||
await queue.EnqueueAsync(new(webhook.ItemId), ct);
|
||||
logger.LogInformation(
|
||||
"Enqueued {WebhookType} webhook with {WebhookCode} payload",
|
||||
request.WebhookType,
|
||||
request.WebhookCode
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return Results.Ok();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ global using System.ComponentModel.DataAnnotations;
|
||||
global using System.Security.Claims;
|
||||
global using System.Text.Json.Serialization;
|
||||
|
||||
global using FiscalOS.API.Institutions.Get;
|
||||
global using FiscalOS.API.Accounts;
|
||||
global using FiscalOS.API.Accounts.Add;
|
||||
global using FiscalOS.API.Auth;
|
||||
@@ -10,27 +11,16 @@ global using FiscalOS.API.Auth.Refresh;
|
||||
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.Accounts;
|
||||
global using FiscalOS.Core.Authentication;
|
||||
global using FiscalOS.Core.Identity;
|
||||
global using FiscalOS.Core.Queuing;
|
||||
global using FiscalOS.Core.Security;
|
||||
global using FiscalOS.Infra.Accounts.Plaid;
|
||||
global using FiscalOS.Infra.Authentication;
|
||||
global using FiscalOS.Infra.Data;
|
||||
global using FiscalOS.Infra.DependencyInjection;
|
||||
global using FiscalOS.Infra.Transactions.Plaid;
|
||||
global using FiscalOS.ServiceDefaults;
|
||||
|
||||
global using Going.Plaid;
|
||||
global using Going.Plaid.Converters;
|
||||
global using Going.Plaid.Entity;
|
||||
global using Going.Plaid.Webhook;
|
||||
|
||||
global using Microsoft.AspNetCore.Mvc;
|
||||
global using Microsoft.EntityFrameworkCore;
|
||||
@@ -21,7 +21,6 @@
|
||||
},
|
||||
"PlaidClientOptions": {
|
||||
"ClientId": "ClientId",
|
||||
"Secret": "Secret",
|
||||
"Webhook": "Webhook"
|
||||
"Secret": "Secret"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,52 +47,6 @@ internal sealed class App(
|
||||
var keyRingEntry = await keyRing.SaveKeyAsync(key);
|
||||
_console.MarkupLine($"Key generated with id [green]{keyRingEntry.KeyId}[/] generated successfully.");
|
||||
break;
|
||||
case Commands.RemoveInstitution:
|
||||
var institutionId = await _console.AskAsync<string>("Enter the [green]institution id[/] of the item you want to remove:", cancellationToken);
|
||||
|
||||
var institution = await appDbContext.Institutions
|
||||
.Include(i => i.User)
|
||||
.Include(i => i.Metadata)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (institution is null)
|
||||
{
|
||||
_console.MarkupLine($"Institution with id [green]{institutionId}[/] not found.");
|
||||
break;
|
||||
}
|
||||
|
||||
if (institution.User is null)
|
||||
{
|
||||
_console.MarkupLine($"Institution with id [green]{institutionId}[/] has no user.");
|
||||
break;
|
||||
}
|
||||
|
||||
if (institution.Metadata is not PlaidInstitutionMetadata plaidInstitutionMetadata)
|
||||
{
|
||||
_console.MarkupLine($"Institution with id [green]{institutionId}[/] has no plaid metadata.");
|
||||
break;
|
||||
}
|
||||
|
||||
var accessToken = await encryptor.DecryptAsyncFor(institution.User, plaidInstitutionMetadata.EncryptedAccessToken, cancellationToken);
|
||||
|
||||
var plaidClient = scope.ServiceProvider.GetRequiredService<PlaidClient>();
|
||||
|
||||
var removeItemResponse = await plaidClient.ItemRemoveAsync(new()
|
||||
{
|
||||
AccessToken = accessToken,
|
||||
});
|
||||
|
||||
if (removeItemResponse.IsSuccessStatusCode is false)
|
||||
{
|
||||
_console.MarkupLine($"Failed to remove item for institution id [green]{institutionId}[/]. Plaid error: {removeItemResponse.Error?.ErrorMessage}");
|
||||
break;
|
||||
}
|
||||
|
||||
appDbContext.Institutions.Remove(institution);
|
||||
await appDbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_console.MarkupLine($"Institution with id [green]{institutionId}[/] removed successfully.");
|
||||
break;
|
||||
case Commands.Exit:
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -4,13 +4,11 @@ internal static class Commands
|
||||
{
|
||||
public const string CreateUser = "Create User";
|
||||
public const string GenerateKey = "Generate Key";
|
||||
public const string RemoveInstitution = "Remove Institution";
|
||||
public const string Exit = "Exit";
|
||||
|
||||
public static readonly string[] All = [
|
||||
CreateUser,
|
||||
GenerateKey,
|
||||
RemoveInstitution,
|
||||
Exit
|
||||
];
|
||||
}
|
||||
@@ -2,13 +2,9 @@ global using FiscalOS.AdminCLI;
|
||||
global using FiscalOS.Core.Authentication;
|
||||
global using FiscalOS.Core.Identity;
|
||||
global using FiscalOS.Core.Security;
|
||||
global using FiscalOS.Infra.Accounts.Plaid;
|
||||
global using FiscalOS.Infra.Data;
|
||||
global using FiscalOS.Infra.DependencyInjection;
|
||||
|
||||
global using Going.Plaid;
|
||||
|
||||
global using Microsoft.EntityFrameworkCore;
|
||||
global using Microsoft.Extensions.Configuration;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using Microsoft.Extensions.Hosting;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Aspire.AppHost.Sdk/13.1.2">
|
||||
<Project Sdk="Aspire.AppHost.Sdk/13.1.1">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FiscalOS.API\FiscalOS.API.csproj" />
|
||||
|
||||
@@ -11,20 +11,11 @@ public sealed class Account : Entity
|
||||
private readonly List<Balance> _balances = [];
|
||||
public IEnumerable<Balance> Balances => _balances;
|
||||
|
||||
private readonly List<Transaction> _transactions = [];
|
||||
public IEnumerable<Transaction> Transactions => _transactions;
|
||||
|
||||
private Account()
|
||||
{
|
||||
}
|
||||
|
||||
public static Account From(
|
||||
string name,
|
||||
AccountMetadata accountMetadata
|
||||
)
|
||||
public static Account From(Guid institutionId, string name, AccountMetadata accountMetadata)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
InstitutionId = institutionId,
|
||||
Name = name,
|
||||
Metadata = accountMetadata,
|
||||
};
|
||||
@@ -32,15 +23,8 @@ public sealed class Account : Entity
|
||||
|
||||
public void AddBalance(Balance balance)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(balance, nameof(balance));
|
||||
ArgumentNullException.ThrowIfNull(balance);
|
||||
|
||||
_balances.Add(balance);
|
||||
}
|
||||
|
||||
public void AddTransaction(Transaction transaction)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(transaction, nameof(transaction));
|
||||
|
||||
_transactions.Add(transaction);
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,6 @@ public sealed class Balance : Entity
|
||||
string currencyCode
|
||||
)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(currencyCode, nameof(currencyCode));
|
||||
|
||||
return new()
|
||||
{
|
||||
Current = current,
|
||||
|
||||
@@ -3,13 +3,9 @@ namespace FiscalOS.Core.Accounts;
|
||||
public sealed class Institution : Entity
|
||||
{
|
||||
public Guid UserId { get; init; }
|
||||
public User? User { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public InstitutionMetadata? Metadata { get; init; }
|
||||
|
||||
private readonly List<Account> _accounts = [];
|
||||
public IEnumerable<Account> Accounts => _accounts;
|
||||
|
||||
private Institution()
|
||||
{
|
||||
}
|
||||
@@ -25,11 +21,4 @@ public sealed class Institution : Entity
|
||||
Metadata = metadata
|
||||
};
|
||||
}
|
||||
|
||||
public void AddAccount(Account account)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(account, nameof(account));
|
||||
|
||||
_accounts.Add(account);
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,6 @@ public sealed class User : Entity
|
||||
private readonly List<Account> _accounts = [];
|
||||
public IEnumerable<Account> Accounts => _accounts;
|
||||
|
||||
private readonly List<Transaction> _transactions = [];
|
||||
public IEnumerable<Transaction> Transactions => _transactions;
|
||||
|
||||
private User()
|
||||
{
|
||||
}
|
||||
@@ -63,11 +60,4 @@ public sealed class User : Entity
|
||||
|
||||
_accounts.Add(account);
|
||||
}
|
||||
|
||||
public void AddTransaction(Transaction transaction)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(transaction, nameof(transaction));
|
||||
|
||||
_transactions.Add(transaction);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace FiscalOS.Core.Queuing;
|
||||
|
||||
public interface IAsyncQueue<T>
|
||||
{
|
||||
Task EnqueueAsync(T item, CancellationToken cancellationToken = default);
|
||||
Task<T> DequeueAsync(CancellationToken cancellationToken = default);
|
||||
ValueTask DisposeAsync();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace FiscalOS.Core.Queuing;
|
||||
|
||||
public interface IAsyncQueueProcessor<T>
|
||||
{
|
||||
Task ProcessAsync(T item, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
namespace FiscalOS.Core.Transactions;
|
||||
|
||||
public sealed class Transaction : Entity
|
||||
{
|
||||
public Guid UserId { get; private set; }
|
||||
public User? User { get; init; }
|
||||
|
||||
public Guid AccountId { get; init; }
|
||||
public Account? Account { get; init; }
|
||||
|
||||
public string MerchantName { get; private set; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public decimal Amount { get; init; }
|
||||
public DateTimeOffset Date { get; init; }
|
||||
|
||||
public TransactionMetadata? Metadata { get; init; }
|
||||
|
||||
private Transaction()
|
||||
{
|
||||
}
|
||||
|
||||
public static Transaction From(
|
||||
string merchantName,
|
||||
string description,
|
||||
decimal? amount,
|
||||
DateTimeOffset? date,
|
||||
TransactionMetadata metadata
|
||||
)
|
||||
{
|
||||
return From(Guid.Empty, Guid.Empty, Guid.Empty, merchantName, description, amount, date, metadata);
|
||||
}
|
||||
|
||||
public static Transaction From(
|
||||
Guid userId,
|
||||
Guid accountId,
|
||||
string merchantName,
|
||||
string description,
|
||||
decimal? amount,
|
||||
DateTimeOffset? date,
|
||||
TransactionMetadata metadata
|
||||
)
|
||||
{
|
||||
return From(Guid.Empty, userId, accountId, merchantName, description, amount, date, metadata);
|
||||
}
|
||||
|
||||
public static Transaction From(
|
||||
Guid id,
|
||||
Guid userId,
|
||||
Guid accountId,
|
||||
string merchantName,
|
||||
string description,
|
||||
decimal? amount,
|
||||
DateTimeOffset? date,
|
||||
TransactionMetadata metadata
|
||||
)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(merchantName, nameof(merchantName));
|
||||
ArgumentNullException.ThrowIfNull(description, nameof(description));
|
||||
|
||||
if (amount.HasValue is false)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(amount));
|
||||
}
|
||||
|
||||
if (date.HasValue is false)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(date));
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
Id = id,
|
||||
UserId = userId,
|
||||
AccountId = accountId,
|
||||
MerchantName = merchantName,
|
||||
Description = description,
|
||||
Amount = amount.Value,
|
||||
Date = date.Value,
|
||||
Metadata = metadata,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace FiscalOS.Core.Transactions;
|
||||
|
||||
public abstract class TransactionMetadata : Entity
|
||||
{
|
||||
private readonly string _type;
|
||||
|
||||
public string Type => _type;
|
||||
|
||||
public Guid TransactionId { get; init; }
|
||||
|
||||
protected TransactionMetadata(string type)
|
||||
{
|
||||
_type = type;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
global using FiscalOS.Core.Accounts;
|
||||
global using FiscalOS.Core.Data;
|
||||
global using FiscalOS.Core.Identity;
|
||||
global using FiscalOS.Core.Security;
|
||||
global using FiscalOS.Core.Transactions;
|
||||
global using FiscalOS.Core.Security;
|
||||
@@ -1,4 +1,4 @@
|
||||
[*.cs]
|
||||
|
||||
dotnet_diagnostic.CA1062.severity = none
|
||||
dotnet_diagnostic.CA5404.severity = none
|
||||
dotnet_diagnostic.CA5404.severity = none
|
||||
|
||||
@@ -5,16 +5,12 @@ public sealed class PlaidAccountMetadata : AccountMetadata
|
||||
public const string TypeValue = Providers.Plaid;
|
||||
public string PlaidId { get; init; } = string.Empty;
|
||||
public string PlaidName { get; init; } = string.Empty;
|
||||
public string? Cursor { get; private set; }
|
||||
|
||||
private PlaidAccountMetadata() : base(TypeValue)
|
||||
{
|
||||
}
|
||||
|
||||
public static PlaidAccountMetadata From(
|
||||
string plaidId,
|
||||
string plaidName
|
||||
)
|
||||
public static PlaidAccountMetadata From(string plaidId, string plaidName)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
@@ -22,11 +18,4 @@ public sealed class PlaidAccountMetadata : AccountMetadata
|
||||
PlaidName = plaidName,
|
||||
};
|
||||
}
|
||||
|
||||
public void SetCursor(string cursor)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(cursor, nameof(cursor));
|
||||
|
||||
Cursor = cursor;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ public sealed record PlaidClientOptions
|
||||
{
|
||||
public string ClientId { get; init; } = string.Empty;
|
||||
public string Secret { get; init; } = string.Empty;
|
||||
public string Webhook { get; init; } = string.Empty;
|
||||
|
||||
public IOptions<PlaidOptions> ToPlaidOptions()
|
||||
{
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace FiscalOS.Infra.Common;
|
||||
namespace FiscalOS.Infra.Accounts.Plaid;
|
||||
|
||||
public class PlaidException : Exception
|
||||
{
|
||||
+6
-10
@@ -1,35 +1,31 @@
|
||||
namespace FiscalOS.Infra.Accounts.Plaid;
|
||||
|
||||
public sealed class PlaidInstitutionMetadata : InstitutionMetadata
|
||||
public sealed class PlaidMetadata : InstitutionMetadata
|
||||
{
|
||||
public const string TypeValue = Providers.Plaid;
|
||||
public string PlaidId { get; init; } = string.Empty;
|
||||
public string PlaidName { get; init; } = string.Empty;
|
||||
public string EncryptedAccessToken { get; init; } = string.Empty;
|
||||
public string ItemId { get; init; } = string.Empty;
|
||||
|
||||
private PlaidInstitutionMetadata() : base(TypeValue)
|
||||
private PlaidMetadata() : base(TypeValue)
|
||||
{
|
||||
}
|
||||
|
||||
public static PlaidInstitutionMetadata From(
|
||||
public static PlaidMetadata From(
|
||||
string? plaidId,
|
||||
string? plaidName,
|
||||
string encryptedAccessToken,
|
||||
string itemId
|
||||
string encryptedAccessToken
|
||||
)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(plaidId, nameof(plaidId));
|
||||
ArgumentNullException.ThrowIfNull(plaidName, nameof(plaidName));
|
||||
ArgumentNullException.ThrowIfNull(encryptedAccessToken, nameof(encryptedAccessToken));
|
||||
ArgumentNullException.ThrowIfNull(itemId, nameof(itemId));
|
||||
|
||||
return new()
|
||||
return new PlaidMetadata
|
||||
{
|
||||
PlaidId = plaidId,
|
||||
PlaidName = plaidName,
|
||||
EncryptedAccessToken = encryptedAccessToken,
|
||||
ItemId = itemId,
|
||||
EncryptedAccessToken = encryptedAccessToken
|
||||
};
|
||||
}
|
||||
}
|
||||
+9
-17
@@ -1,29 +1,22 @@
|
||||
|
||||
|
||||
using System.Reflection;
|
||||
|
||||
namespace FiscalOS.Infra.Accounts.Plaid;
|
||||
|
||||
public interface IPlaidAccountService
|
||||
public sealed class PlaidService
|
||||
{
|
||||
Task<string> CreateLinkTokenAsync(string id);
|
||||
Task<(string ItemId, string AccessToken)> ExchangeTokenAsync(string publicToken);
|
||||
Task<List<Going.Plaid.Entity.Account>> GetAccountsAsync(string accessToken);
|
||||
Task<ItemWithConsentFields> GetItemAsync(string accessToken);
|
||||
}
|
||||
|
||||
internal sealed class PlaidAccountService : IPlaidAccountService
|
||||
{
|
||||
private readonly IOptions<PlaidClientOptions> _options;
|
||||
private readonly PlaidClient _client;
|
||||
|
||||
private PlaidAccountService(PlaidClient client, IOptions<PlaidClientOptions> options)
|
||||
private PlaidService(PlaidClient client)
|
||||
{
|
||||
_client = client;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public static PlaidAccountService From(IServiceProvider sp)
|
||||
public static PlaidService From(IServiceProvider sp)
|
||||
{
|
||||
var client = sp.GetRequiredService<PlaidClient>();
|
||||
var options = sp.GetRequiredService<IOptions<PlaidClientOptions>>();
|
||||
return new(client, options);
|
||||
return new(client);
|
||||
}
|
||||
|
||||
public async Task<string> CreateLinkTokenAsync(string id)
|
||||
@@ -41,7 +34,6 @@ internal sealed class PlaidAccountService : IPlaidAccountService
|
||||
{
|
||||
ClientUserId = id,
|
||||
},
|
||||
Webhook = _options.Value.Webhook,
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (ltr.IsSuccessStatusCode is false)
|
||||
@@ -96,4 +88,4 @@ internal sealed class PlaidAccountService : IPlaidAccountService
|
||||
|
||||
return ar.Item;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace FiscalOS.Infra.Common;
|
||||
namespace FiscalOS.Infra.Accounts;
|
||||
|
||||
public static class Providers
|
||||
{
|
||||
@@ -1,6 +1,5 @@
|
||||
using Account = FiscalOS.Core.Accounts.Account;
|
||||
using Institution = FiscalOS.Core.Accounts.Institution;
|
||||
using Transaction = FiscalOS.Core.Transactions.Transaction;
|
||||
|
||||
namespace FiscalOS.Infra.Data;
|
||||
|
||||
@@ -15,8 +14,6 @@ public sealed class AppDbContext(
|
||||
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
|
||||
public DbSet<Institution> Institutions => Set<Institution>();
|
||||
public DbSet<Transaction> Transactions => Set<Transaction>();
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
@@ -66,7 +63,7 @@ public sealed class AppDbContext(
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
eb.HasMany(static u => u.Institutions)
|
||||
.WithOne(static i => i.User)
|
||||
.WithOne()
|
||||
.HasForeignKey(static i => i.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
@@ -99,20 +96,19 @@ public sealed class AppDbContext(
|
||||
modelBuilder.Entity<InstitutionMetadata>(static eb =>
|
||||
{
|
||||
eb.HasDiscriminator(static m => m.Type)
|
||||
.HasValue<PlaidInstitutionMetadata>(PlaidInstitutionMetadata.TypeValue);
|
||||
.HasValue<PlaidMetadata>(PlaidMetadata.TypeValue);
|
||||
|
||||
eb.Property(static m => m.InstitutionId);
|
||||
eb.Property(static m => m.Type);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<PlaidInstitutionMetadata>(static eb =>
|
||||
modelBuilder.Entity<PlaidMetadata>(static eb =>
|
||||
{
|
||||
eb.HasBaseType<InstitutionMetadata>();
|
||||
|
||||
eb.Property(static m => m.PlaidId);
|
||||
eb.Property(static m => m.PlaidName);
|
||||
eb.Property(static m => m.EncryptedAccessToken);
|
||||
eb.Property(static m => m.ItemId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Account>(static eb =>
|
||||
@@ -120,7 +116,7 @@ public sealed class AppDbContext(
|
||||
eb.Property(static a => a.Name);
|
||||
|
||||
eb.HasOne(static a => a.Institution)
|
||||
.WithMany(static i => i.Accounts)
|
||||
.WithMany()
|
||||
.HasForeignKey(static a => a.InstitutionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
@@ -150,7 +146,6 @@ public sealed class AppDbContext(
|
||||
|
||||
eb.Property(static m => m.PlaidId);
|
||||
eb.Property(static m => m.PlaidName);
|
||||
eb.Property(static m => m.Cursor);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Balance>(static eb =>
|
||||
@@ -160,40 +155,5 @@ public sealed class AppDbContext(
|
||||
eb.Property(static b => b.Available);
|
||||
eb.Property(static b => b.CurrencyCode);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Transaction>(static eb =>
|
||||
{
|
||||
eb.HasOne(static t => t.User)
|
||||
.WithMany(static u => u.Transactions)
|
||||
.HasForeignKey(static t => t.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
eb.HasOne(static t => t.Account)
|
||||
.WithMany(static a => a.Transactions)
|
||||
.HasForeignKey(static t => t.AccountId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
eb.Property(static t => t.MerchantName);
|
||||
eb.Property(static t => t.Description);
|
||||
eb.Property(static t => t.Amount);
|
||||
eb.Property(static t => t.Date);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<TransactionMetadata>(static eb =>
|
||||
{
|
||||
eb.HasDiscriminator(static m => m.Type)
|
||||
.HasValue(PlaidTransactionMetadata.TypeValue);
|
||||
|
||||
eb.Property(static m => m.TransactionId);
|
||||
eb.Property(static m => m.Type);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<PlaidTransactionMetadata>(static eb =>
|
||||
{
|
||||
eb.HasDiscriminator(static m => m.Type)
|
||||
.HasValue(PlaidTransactionMetadata.TypeValue);
|
||||
|
||||
eb.Property(static m => m.PlaidId);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -33,18 +33,7 @@ public static class ServiceCollectionExtensions
|
||||
var options = clientOptions.Value.ToPlaidOptions();
|
||||
return new PlaidClient(options, factory, logger);
|
||||
});
|
||||
services.AddSingleton<IPlaidAccountService>(PlaidAccountService.From);
|
||||
services.AddSingleton<IPlaidTransactionService>(PlaidTransactionService.From);
|
||||
|
||||
services.AddScoped<IPlaidAddedTransactionHandler>(PlaidAddedTransactionHandler.From);
|
||||
services.AddScoped<IPlaidModifiedTransactionHandler>(PlaidModifiedTransactionHandler.From);
|
||||
services.AddScoped<IPlaidRemovedTransactionHandler>(PlaidRemovedTransactionHandler.From);
|
||||
services.AddScoped<IPlaidTransactionProcessor>(PlaidTransactionProcessor.From);
|
||||
services.AddScoped<IPlaidTransactionSyncer>(PlaidTransactionSyncer.From);
|
||||
|
||||
services.AddSingleton<IAsyncQueue<SyncUpdatesQueueItem>>(ChannelAsyncQueue<SyncUpdatesQueueItem>.From);
|
||||
services.AddScoped<IAsyncQueueProcessor<SyncUpdatesQueueItem>>(SyncUpdatesProcessor.From);
|
||||
services.AddHostedService(AsyncQueueHostedService<SyncUpdatesQueueItem>.From);
|
||||
services.AddSingleton(PlaidService.From);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
|
||||
-374
@@ -1,374 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FiscalOS.Infra.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260226123411_AddItemIdToPlaidMetadata")]
|
||||
partial class AddItemIdToPlaidMetadata
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("InstitutionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstitutionId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Account");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AccountMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Available")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CurrencyCode")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Current")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.ToTable("Balance");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Institution");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("InstitutionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstitutionId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("InstitutionMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Revoked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Token")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedDataKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptionKeyId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("HashedPassword")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||
|
||||
b.Property<string>("EncryptedAccessToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("InstitutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Institution");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithMany("Balances")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||
.WithMany("Institutions")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany("RefreshTokens")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.Navigation("Balances");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Institutions");
|
||||
|
||||
b.Navigation("RefreshTokens");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddItemIdToPlaidMetadata : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ItemId",
|
||||
table: "InstitutionMetadata",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ItemId",
|
||||
table: "InstitutionMetadata");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
-376
@@ -1,376 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FiscalOS.Infra.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260226131150_AddNavigationPropertyToInstitutionForUser")]
|
||||
partial class AddNavigationPropertyToInstitutionForUser
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("InstitutionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstitutionId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Account");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AccountMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Available")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CurrencyCode")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Current")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.ToTable("Balance");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Institutions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("InstitutionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstitutionId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("InstitutionMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Revoked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Token")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedDataKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptionKeyId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("HashedPassword")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||
|
||||
b.Property<string>("EncryptedAccessToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("InstitutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Institution");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithMany("Balances")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany("Institutions")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany("RefreshTokens")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.Navigation("Balances");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Institutions");
|
||||
|
||||
b.Navigation("RefreshTokens");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddNavigationPropertyToInstitutionForUser : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Account_Institution_InstitutionId",
|
||||
table: "Account");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Institution_Users_UserId",
|
||||
table: "Institution");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_InstitutionMetadata_Institution_InstitutionId",
|
||||
table: "InstitutionMetadata");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_Institution",
|
||||
table: "Institution");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "Institution",
|
||||
newName: "Institutions");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Institution_UserId",
|
||||
table: "Institutions",
|
||||
newName: "IX_Institutions_UserId");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_Institutions",
|
||||
table: "Institutions",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Account_Institutions_InstitutionId",
|
||||
table: "Account",
|
||||
column: "InstitutionId",
|
||||
principalTable: "Institutions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_InstitutionMetadata_Institutions_InstitutionId",
|
||||
table: "InstitutionMetadata",
|
||||
column: "InstitutionId",
|
||||
principalTable: "Institutions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Institutions_Users_UserId",
|
||||
table: "Institutions",
|
||||
column: "UserId",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Account_Institutions_InstitutionId",
|
||||
table: "Account");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_InstitutionMetadata_Institutions_InstitutionId",
|
||||
table: "InstitutionMetadata");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Institutions_Users_UserId",
|
||||
table: "Institutions");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_Institutions",
|
||||
table: "Institutions");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "Institutions",
|
||||
newName: "Institution");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Institutions_UserId",
|
||||
table: "Institution",
|
||||
newName: "IX_Institution_UserId");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_Institution",
|
||||
table: "Institution",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Account_Institution_InstitutionId",
|
||||
table: "Account",
|
||||
column: "InstitutionId",
|
||||
principalTable: "Institution",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Institution_Users_UserId",
|
||||
table: "Institution",
|
||||
column: "UserId",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_InstitutionMetadata_Institution_InstitutionId",
|
||||
table: "InstitutionMetadata",
|
||||
column: "InstitutionId",
|
||||
principalTable: "Institution",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
-376
@@ -1,376 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FiscalOS.Infra.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260226224648_RenamePlaidMetadataEntity")]
|
||||
partial class RenamePlaidMetadataEntity
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("InstitutionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstitutionId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Account");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AccountMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Available")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CurrencyCode")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Current")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.ToTable("Balance");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Institutions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("InstitutionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstitutionId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("InstitutionMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Revoked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Token")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedDataKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptionKeyId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("HashedPassword")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidInstitutionMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||
|
||||
b.Property<string>("EncryptedAccessToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("InstitutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Institution");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithMany("Balances")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany("Institutions")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany("RefreshTokens")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.Navigation("Balances");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Institutions");
|
||||
|
||||
b.Navigation("RefreshTokens");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RenamePlaidMetadataEntity : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
-504
@@ -1,504 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FiscalOS.Infra.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260228123108_AddTransactionAndTransactionMetadataModels")]
|
||||
partial class AddTransactionAndTransactionMetadataModels
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("InstitutionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstitutionId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Account");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AccountMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Available")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CurrencyCode")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Current")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.ToTable("Balance");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Institutions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("InstitutionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstitutionId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("InstitutionMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Revoked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Token")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedDataKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptionKeyId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("HashedPassword")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("AccountId1")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MerchantName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.HasIndex("AccountId1");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Transaction");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("TransactionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(5)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TransactionId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TransactionMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("Plaid");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidInstitutionMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||
|
||||
b.Property<string>("EncryptedAccessToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Transactions.Plaid.PlaidTransactionMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Transactions.TransactionMetadata");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("InstitutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Institution");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithMany("Balances")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany("Institutions")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany("RefreshTokens")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", "Account")
|
||||
.WithMany()
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("AccountId1");
|
||||
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Account");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Transactions.Transaction", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Transactions.TransactionMetadata", "TransactionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.Navigation("Balances");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Institutions");
|
||||
|
||||
b.Navigation("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTransactionAndTransactionMetadataModels : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Transaction",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
AccountId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
MerchantName = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Description = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Amount = table.Column<decimal>(type: "TEXT", nullable: false),
|
||||
Date = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||
AccountId1 = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||
UpdatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Transaction", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Transaction_Account_AccountId",
|
||||
column: x => x.AccountId,
|
||||
principalTable: "Account",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Transaction_Account_AccountId1",
|
||||
column: x => x.AccountId1,
|
||||
principalTable: "Account",
|
||||
principalColumn: "Id");
|
||||
table.ForeignKey(
|
||||
name: "FK_Transaction_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TransactionMetadata",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 5, nullable: false),
|
||||
TransactionId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
PlaidId = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||
UpdatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TransactionMetadata", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_TransactionMetadata_Transaction_TransactionId",
|
||||
column: x => x.TransactionId,
|
||||
principalTable: "Transaction",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Transaction_AccountId",
|
||||
table: "Transaction",
|
||||
column: "AccountId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Transaction_AccountId1",
|
||||
table: "Transaction",
|
||||
column: "AccountId1");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Transaction_UserId",
|
||||
table: "Transaction",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TransactionMetadata_TransactionId",
|
||||
table: "TransactionMetadata",
|
||||
column: "TransactionId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "TransactionMetadata");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Transaction");
|
||||
}
|
||||
}
|
||||
}
|
||||
-508
@@ -1,508 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FiscalOS.Infra.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260301104650_AddCursorToAccountModel")]
|
||||
partial class AddCursorToAccountModel
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("InstitutionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstitutionId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Account");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AccountMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Available")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CurrencyCode")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Current")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.ToTable("Balance");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Institutions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("InstitutionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstitutionId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("InstitutionMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Revoked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Token")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedDataKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptionKeyId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("HashedPassword")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("AccountId1")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MerchantName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.HasIndex("AccountId1");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Transaction");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("TransactionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(5)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TransactionId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TransactionMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("Plaid");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||
|
||||
b.Property<string>("Cursor")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidInstitutionMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||
|
||||
b.Property<string>("EncryptedAccessToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Transactions.Plaid.PlaidTransactionMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Transactions.TransactionMetadata");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("InstitutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Institution");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithMany("Balances")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany("Institutions")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany("RefreshTokens")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", "Account")
|
||||
.WithMany()
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("AccountId1");
|
||||
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Account");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Transactions.Transaction", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Transactions.TransactionMetadata", "TransactionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.Navigation("Balances");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Institutions");
|
||||
|
||||
b.Navigation("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddCursorToAccountModel : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Cursor",
|
||||
table: "AccountMetadata",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Cursor",
|
||||
table: "AccountMetadata");
|
||||
}
|
||||
}
|
||||
}
|
||||
-507
@@ -1,507 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FiscalOS.Infra.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260302015827_AddInstitutionsDbSet")]
|
||||
partial class AddInstitutionsDbSet
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("InstitutionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstitutionId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Account");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AccountMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Available")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CurrencyCode")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Current")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.ToTable("Balance");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Institutions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("InstitutionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstitutionId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("InstitutionMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Revoked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Token")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedDataKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptionKeyId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("HashedPassword")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("AccountId1")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MerchantName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.HasIndex("AccountId1");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("TransactionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(5)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TransactionId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TransactionMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("Plaid");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||
|
||||
b.Property<string>("Cursor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidInstitutionMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||
|
||||
b.Property<string>("EncryptedAccessToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Transactions.Plaid.PlaidTransactionMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Transactions.TransactionMetadata");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("InstitutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Institution");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithMany("Balances")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany("Institutions")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany("RefreshTokens")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", "Account")
|
||||
.WithMany()
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("AccountId1");
|
||||
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Account");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Transactions.Transaction", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Transactions.TransactionMetadata", "TransactionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.Navigation("Balances");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Institutions");
|
||||
|
||||
b.Navigation("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddInstitutionsDbSet : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Transaction_Account_AccountId",
|
||||
table: "Transaction");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Transaction_Account_AccountId1",
|
||||
table: "Transaction");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Transaction_Users_UserId",
|
||||
table: "Transaction");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_TransactionMetadata_Transaction_TransactionId",
|
||||
table: "TransactionMetadata");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_Transaction",
|
||||
table: "Transaction");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "Transaction",
|
||||
newName: "Transactions");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Transaction_UserId",
|
||||
table: "Transactions",
|
||||
newName: "IX_Transactions_UserId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Transaction_AccountId1",
|
||||
table: "Transactions",
|
||||
newName: "IX_Transactions_AccountId1");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Transaction_AccountId",
|
||||
table: "Transactions",
|
||||
newName: "IX_Transactions_AccountId");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_Transactions",
|
||||
table: "Transactions",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_TransactionMetadata_Transactions_TransactionId",
|
||||
table: "TransactionMetadata",
|
||||
column: "TransactionId",
|
||||
principalTable: "Transactions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Transactions_Account_AccountId",
|
||||
table: "Transactions",
|
||||
column: "AccountId",
|
||||
principalTable: "Account",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Transactions_Account_AccountId1",
|
||||
table: "Transactions",
|
||||
column: "AccountId1",
|
||||
principalTable: "Account",
|
||||
principalColumn: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Transactions_Users_UserId",
|
||||
table: "Transactions",
|
||||
column: "UserId",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_TransactionMetadata_Transactions_TransactionId",
|
||||
table: "TransactionMetadata");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Transactions_Account_AccountId",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Transactions_Account_AccountId1",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Transactions_Users_UserId",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_Transactions",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "Transactions",
|
||||
newName: "Transaction");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Transactions_UserId",
|
||||
table: "Transaction",
|
||||
newName: "IX_Transaction_UserId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Transactions_AccountId1",
|
||||
table: "Transaction",
|
||||
newName: "IX_Transaction_AccountId1");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Transactions_AccountId",
|
||||
table: "Transaction",
|
||||
newName: "IX_Transaction_AccountId");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_Transaction",
|
||||
table: "Transaction",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Transaction_Account_AccountId",
|
||||
table: "Transaction",
|
||||
column: "AccountId",
|
||||
principalTable: "Account",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Transaction_Account_AccountId1",
|
||||
table: "Transaction",
|
||||
column: "AccountId1",
|
||||
principalTable: "Account",
|
||||
principalColumn: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Transaction_Users_UserId",
|
||||
table: "Transaction",
|
||||
column: "UserId",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_TransactionMetadata_Transaction_TransactionId",
|
||||
table: "TransactionMetadata",
|
||||
column: "TransactionId",
|
||||
principalTable: "Transaction",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
-518
@@ -1,518 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FiscalOS.Infra.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260303124406_AddTransactionsNavigationPropertyToUser")]
|
||||
partial class AddTransactionsNavigationPropertyToUser
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("InstitutionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstitutionId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Account");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AccountMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Available")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CurrencyCode")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Current")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.ToTable("Balance");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Institutions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("InstitutionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstitutionId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("InstitutionMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Revoked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Token")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedDataKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptionKeyId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("HashedPassword")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("AccountId1")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MerchantName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("UserId1")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.HasIndex("AccountId1");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("UserId1");
|
||||
|
||||
b.ToTable("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("TransactionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(5)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TransactionId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TransactionMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("Plaid");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||
|
||||
b.Property<string>("Cursor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidInstitutionMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||
|
||||
b.Property<string>("EncryptedAccessToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Transactions.Plaid.PlaidTransactionMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Transactions.TransactionMetadata");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("InstitutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Institution");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithMany("Balances")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany("Institutions")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany("RefreshTokens")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", "Account")
|
||||
.WithMany()
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("AccountId1");
|
||||
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("UserId1");
|
||||
|
||||
b.Navigation("Account");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Transactions.Transaction", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Transactions.TransactionMetadata", "TransactionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.Navigation("Balances");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Institutions");
|
||||
|
||||
b.Navigation("RefreshTokens");
|
||||
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTransactionsNavigationPropertyToUser : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "UserId1",
|
||||
table: "Transactions",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Transactions_UserId1",
|
||||
table: "Transactions",
|
||||
column: "UserId1");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Transactions_Users_UserId1",
|
||||
table: "Transactions",
|
||||
column: "UserId1",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Transactions_Users_UserId1",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Transactions_UserId1",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UserId1",
|
||||
table: "Transactions");
|
||||
}
|
||||
}
|
||||
}
|
||||
-500
@@ -1,500 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FiscalOS.Infra.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260303130637_UpdateAppDbContextToIncludeTransactionNavigationProperties")]
|
||||
partial class UpdateAppDbContextToIncludeTransactionNavigationProperties
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("InstitutionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstitutionId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Account");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AccountMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Available")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CurrencyCode")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Current")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.ToTable("Balance");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Institutions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("InstitutionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstitutionId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("InstitutionMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Revoked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Token")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedDataKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptionKeyId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("HashedPassword")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MerchantName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("TransactionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(5)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TransactionId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TransactionMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("Plaid");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||
|
||||
b.Property<string>("Cursor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidInstitutionMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||
|
||||
b.Property<string>("EncryptedAccessToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Transactions.Plaid.PlaidTransactionMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Transactions.TransactionMetadata");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("InstitutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Institution");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||
.WithMany("Balances")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany("Institutions")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany("RefreshTokens")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", "Account")
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Account");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Transactions.Transaction", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Transactions.TransactionMetadata", "TransactionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.Navigation("Balances");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Institutions");
|
||||
|
||||
b.Navigation("RefreshTokens");
|
||||
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class UpdateAppDbContextToIncludeTransactionNavigationProperties : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Transactions_Account_AccountId1",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Transactions_Users_UserId1",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Transactions_AccountId1",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Transactions_UserId1",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AccountId1",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UserId1",
|
||||
table: "Transactions");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "AccountId1",
|
||||
table: "Transactions",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "UserId1",
|
||||
table: "Transactions",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Transactions_AccountId1",
|
||||
table: "Transactions",
|
||||
column: "AccountId1");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Transactions_UserId1",
|
||||
table: "Transactions",
|
||||
column: "UserId1");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Transactions_Account_AccountId1",
|
||||
table: "Transactions",
|
||||
column: "AccountId1",
|
||||
principalTable: "Account",
|
||||
principalColumn: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Transactions_Users_UserId1",
|
||||
table: "Transactions",
|
||||
column: "UserId1",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ namespace FiscalOS.Infra.Migrations
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Institutions");
|
||||
b.ToTable("Institution");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
@@ -241,86 +241,10 @@ namespace FiscalOS.Infra.Migrations
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MerchantName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("TransactionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(5)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TransactionId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TransactionMetadata");
|
||||
|
||||
b.HasDiscriminator<string>("Type").HasValue("Plaid");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||
|
||||
b.Property<string>("Cursor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
@@ -332,7 +256,7 @@ namespace FiscalOS.Infra.Migrations
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidInstitutionMetadata", b =>
|
||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||
|
||||
@@ -340,10 +264,6 @@ namespace FiscalOS.Infra.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
@@ -355,21 +275,10 @@ namespace FiscalOS.Infra.Migrations
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Transactions.Plaid.PlaidTransactionMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Transactions.TransactionMetadata");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||
.WithMany("Accounts")
|
||||
.WithMany()
|
||||
.HasForeignKey("InstitutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
@@ -403,13 +312,11 @@ namespace FiscalOS.Infra.Migrations
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||
.WithMany("Institutions")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
@@ -432,47 +339,15 @@ namespace FiscalOS.Infra.Migrations
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Account", "Account")
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Account");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Transactions.Transaction", null)
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("FiscalOS.Core.Transactions.TransactionMetadata", "TransactionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.Navigation("Balances");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
|
||||
@@ -483,13 +358,6 @@ namespace FiscalOS.Infra.Migrations
|
||||
b.Navigation("Institutions");
|
||||
|
||||
b.Navigation("RefreshTokens");
|
||||
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
namespace FiscalOS.Infra.Queuing;
|
||||
|
||||
internal class AsyncQueueHostedService<T> : BackgroundService where T : notnull
|
||||
{
|
||||
private readonly IAsyncQueue<T> _queue;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<AsyncQueueHostedService<T>> _logger;
|
||||
|
||||
private AsyncQueueHostedService(
|
||||
IAsyncQueue<T> queue,
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<AsyncQueueHostedService<T>> logger
|
||||
)
|
||||
{
|
||||
_queue = queue;
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public static AsyncQueueHostedService<T> From(IServiceProvider serviceProvider)
|
||||
{
|
||||
var queue = serviceProvider.GetRequiredService<IAsyncQueue<T>>();
|
||||
var logger = serviceProvider.GetRequiredService<ILogger<AsyncQueueHostedService<T>>>();
|
||||
return new(queue, serviceProvider, logger);
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("Queue hosted service for {ItemType} starting", typeof(T).Name);
|
||||
|
||||
try
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
var item = await _queue.DequeueAsync(stoppingToken).ConfigureAwait(false);
|
||||
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var processor = scope.ServiceProvider.GetRequiredService<IAsyncQueueProcessor<T>>();
|
||||
|
||||
try
|
||||
{
|
||||
await processor.ProcessAsync(item, stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing queue item of type {ItemType}", typeof(T).Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogInformation("Queue hosted service for {ItemType} stopped", typeof(T).Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
namespace FiscalOS.Infra.Queuing;
|
||||
|
||||
internal class ChannelAsyncQueue<T> : IAsyncQueue<T>
|
||||
{
|
||||
private readonly Channel<T> _channel = Channel.CreateUnbounded<T>();
|
||||
|
||||
private ChannelAsyncQueue()
|
||||
{
|
||||
}
|
||||
|
||||
public static ChannelAsyncQueue<T> From(IServiceProvider serviceProvider)
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
public async Task EnqueueAsync(T item, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _channel.Writer.WriteAsync(item, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<T> DequeueAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _channel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_channel.Writer.Complete();
|
||||
await _channel.Reader.Completion.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
using Account = FiscalOS.Core.Accounts.Account;
|
||||
using Transaction = FiscalOS.Core.Transactions.Transaction;
|
||||
|
||||
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||
|
||||
internal interface IPlaidAddedTransactionHandler
|
||||
{
|
||||
Task<int> HandleAsync(
|
||||
Account account,
|
||||
IEnumerable<Going.Plaid.Entity.Transaction> added,
|
||||
CancellationToken ct
|
||||
);
|
||||
}
|
||||
|
||||
internal sealed class PlaidAddedTransactionHandler : IPlaidAddedTransactionHandler
|
||||
{
|
||||
private readonly ILogger<PlaidAddedTransactionHandler> _logger;
|
||||
private readonly AppDbContext _appDbContext;
|
||||
|
||||
private PlaidAddedTransactionHandler(
|
||||
ILogger<PlaidAddedTransactionHandler> logger,
|
||||
AppDbContext appDbContext
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_appDbContext = appDbContext;
|
||||
}
|
||||
|
||||
public static PlaidAddedTransactionHandler From(
|
||||
ILogger<PlaidAddedTransactionHandler> logger,
|
||||
AppDbContext appDbContext
|
||||
)
|
||||
{
|
||||
return new(logger, appDbContext);
|
||||
}
|
||||
|
||||
public static PlaidAddedTransactionHandler From(IServiceProvider serviceProvider)
|
||||
{
|
||||
return new(
|
||||
serviceProvider.GetRequiredService<ILogger<PlaidAddedTransactionHandler>>(),
|
||||
serviceProvider.GetRequiredService<AppDbContext>()
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<int> HandleAsync(Account account, IEnumerable<Going.Plaid.Entity.Transaction> added, CancellationToken ct)
|
||||
{
|
||||
var existingTransactionIds = added.Select(t => t.TransactionId);
|
||||
var existingTransactions = await _appDbContext.Transactions
|
||||
.Where(t => t.Metadata is PlaidTransactionMetadata && existingTransactionIds.Contains(((PlaidTransactionMetadata)t.Metadata).PlaidId))
|
||||
.ToListAsync(ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var addedCount = 0;
|
||||
|
||||
foreach (var addedTransaction in added)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (addedTransaction.Pending.GetValueOrDefault())
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Skipping pending transaction {TransactionId} for account {AccountId} as it has not been posted yet",
|
||||
addedTransaction.TransactionId,
|
||||
account.Id
|
||||
);
|
||||
addedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var existingTransaction = existingTransactions.FirstOrDefault(
|
||||
t => t.Metadata is PlaidTransactionMetadata metadata && metadata.PlaidId == addedTransaction.TransactionId
|
||||
);
|
||||
|
||||
if (existingTransaction is not null)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Skipping added transaction {PlaidTransactionId} for account {AccountId} as it has already been added as transaction {TransactionId}",
|
||||
addedTransaction.TransactionId,
|
||||
account.Id,
|
||||
existingTransaction.Id
|
||||
);
|
||||
addedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var transactionMetadata = PlaidTransactionMetadata.From(addedTransaction.TransactionId);
|
||||
var transaction = Transaction.From(
|
||||
account.UserId,
|
||||
account.Id,
|
||||
addedTransaction.Merchant,
|
||||
addedTransaction.Description,
|
||||
addedTransaction.Amount,
|
||||
addedTransaction.PostedDate,
|
||||
transactionMetadata
|
||||
);
|
||||
account.AddTransaction(transaction);
|
||||
addedCount++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Failed to added transaction {PlaidTransactionId} to account {AccountId}",
|
||||
account.Id,
|
||||
addedTransaction.TransactionId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Added {AddedCount} transactions to account {AccountId}", addedCount, account.Id);
|
||||
|
||||
return addedCount;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||
|
||||
public static class PlaidExtensions
|
||||
{
|
||||
#pragma warning disable CA1034
|
||||
extension(Going.Plaid.Entity.Transaction transaction)
|
||||
{
|
||||
public string Merchant => transaction.MerchantName ?? "Unknown merchant";
|
||||
public string Description => transaction.OriginalDescription ?? "";
|
||||
public DateTimeOffset PostedDate => transaction.Datetime ?? (
|
||||
transaction.Date.HasValue
|
||||
? new DateTimeOffset(
|
||||
transaction.Date.Value.Year,
|
||||
transaction.Date.Value.Month,
|
||||
transaction.Date.Value.Day,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
TimeSpan.Zero
|
||||
)
|
||||
: throw new InvalidOperationException($"Transaction {transaction.TransactionId} does not have a valid date or datetime")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
using Account = FiscalOS.Core.Accounts.Account;
|
||||
using Transaction = FiscalOS.Core.Transactions.Transaction;
|
||||
|
||||
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||
|
||||
internal interface IPlaidModifiedTransactionHandler
|
||||
{
|
||||
Task<int> HandleAsync(
|
||||
Account account,
|
||||
IEnumerable<Going.Plaid.Entity.Transaction> modified,
|
||||
CancellationToken ct
|
||||
);
|
||||
}
|
||||
|
||||
internal sealed class PlaidModifiedTransactionHandler : IPlaidModifiedTransactionHandler
|
||||
{
|
||||
private readonly ILogger<PlaidModifiedTransactionHandler> _logger;
|
||||
private readonly AppDbContext _appDbContext;
|
||||
|
||||
private PlaidModifiedTransactionHandler(
|
||||
ILogger<PlaidModifiedTransactionHandler> logger,
|
||||
AppDbContext appDbContext
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_appDbContext = appDbContext;
|
||||
}
|
||||
|
||||
public static PlaidModifiedTransactionHandler From(
|
||||
ILogger<PlaidModifiedTransactionHandler> logger,
|
||||
AppDbContext appDbContext
|
||||
)
|
||||
{
|
||||
return new(logger, appDbContext);
|
||||
}
|
||||
|
||||
public static PlaidModifiedTransactionHandler From(IServiceProvider serviceProvider)
|
||||
{
|
||||
return new(
|
||||
serviceProvider.GetRequiredService<ILogger<PlaidModifiedTransactionHandler>>(),
|
||||
serviceProvider.GetRequiredService<AppDbContext>()
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<int> HandleAsync(Account account, IEnumerable<Going.Plaid.Entity.Transaction> modified, CancellationToken ct)
|
||||
{
|
||||
var modifiedTransactionIds = modified.Select(t => t.TransactionId);
|
||||
var existingModifiedTransactions = await _appDbContext.Transactions
|
||||
.Where(t => t.Metadata is PlaidTransactionMetadata && modifiedTransactionIds.Contains(((PlaidTransactionMetadata)t.Metadata).PlaidId))
|
||||
.ToListAsync(ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var modifiedCount = 0;
|
||||
|
||||
foreach (var existing in existingModifiedTransactions)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (existing.Metadata is not PlaidTransactionMetadata plaidMetadata)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Existing transaction {TransactionId} has non-Plaid metadata. Skipping update for this transaction.",
|
||||
existing.Id
|
||||
);
|
||||
modifiedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var plaidModifiedTransaction = modified.FirstOrDefault(t => t.TransactionId == plaidMetadata.PlaidId);
|
||||
|
||||
if (plaidModifiedTransaction is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"No corresponding modified transaction found in Plaid response for existing transaction {TransactionId}. Skipping update for this transaction.",
|
||||
existing.Id
|
||||
);
|
||||
modifiedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var newTransactionData = Transaction.From(
|
||||
existing.Id,
|
||||
existing.UserId,
|
||||
existing.AccountId,
|
||||
plaidModifiedTransaction.Merchant,
|
||||
plaidModifiedTransaction.Description,
|
||||
plaidModifiedTransaction.Amount,
|
||||
plaidModifiedTransaction.PostedDate,
|
||||
plaidMetadata
|
||||
);
|
||||
|
||||
_appDbContext.Entry(existing).CurrentValues.SetValues(newTransactionData);
|
||||
modifiedCount++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Failed to update transaction {TransactionId} for account {AccountId}",
|
||||
account.Id,
|
||||
existing.Id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Modified {ModifiedCount} transactions for account {AccountId}", modifiedCount, account.Id);
|
||||
|
||||
return modifiedCount;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||
|
||||
internal interface IPlaidRemovedTransactionHandler
|
||||
{
|
||||
Task<int> HandleAsync(IEnumerable<RemovedTransaction> removed, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
internal sealed class PlaidRemovedTransactionHandler : IPlaidRemovedTransactionHandler
|
||||
{
|
||||
private readonly AppDbContext _appDbContext;
|
||||
private readonly ILogger<PlaidRemovedTransactionHandler> _logger;
|
||||
|
||||
private PlaidRemovedTransactionHandler(
|
||||
AppDbContext appDbContext,
|
||||
ILogger<PlaidRemovedTransactionHandler> logger
|
||||
)
|
||||
{
|
||||
_appDbContext = appDbContext;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public static PlaidRemovedTransactionHandler From(
|
||||
AppDbContext appDbContext,
|
||||
ILogger<PlaidRemovedTransactionHandler> logger
|
||||
)
|
||||
{
|
||||
return new(appDbContext, logger);
|
||||
}
|
||||
|
||||
public static PlaidRemovedTransactionHandler From(IServiceProvider serviceProvider)
|
||||
{
|
||||
return new(
|
||||
serviceProvider.GetRequiredService<AppDbContext>(),
|
||||
serviceProvider.GetRequiredService<ILogger<PlaidRemovedTransactionHandler>>()
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<int> HandleAsync(IEnumerable<RemovedTransaction> removed, CancellationToken cancellationToken)
|
||||
{
|
||||
var removedCount = 0;
|
||||
var removedIdsList = removed.Select(t => t.TransactionId);
|
||||
|
||||
try
|
||||
{
|
||||
removedCount = await _appDbContext.Transactions
|
||||
.Where(t => t.Metadata is PlaidTransactionMetadata && removedIdsList.Contains(((PlaidTransactionMetadata)t.Metadata).PlaidId))
|
||||
.ExecuteDeleteAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Failed to removed transactions {TransactionIds}",
|
||||
removedIdsList
|
||||
);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Removed {RemovedCount} transactions", removedCount);
|
||||
|
||||
return removedCount;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||
|
||||
public sealed class PlaidTransactionMetadata : TransactionMetadata
|
||||
{
|
||||
public const string TypeValue = Providers.Plaid;
|
||||
public string PlaidId { get; init; } = string.Empty;
|
||||
|
||||
private PlaidTransactionMetadata() : base(TypeValue)
|
||||
{
|
||||
}
|
||||
|
||||
public static PlaidTransactionMetadata From(string? plaidId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(plaidId, nameof(plaidId));
|
||||
|
||||
return new()
|
||||
{
|
||||
PlaidId = plaidId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
using Account = FiscalOS.Core.Accounts.Account;
|
||||
|
||||
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||
|
||||
internal interface IPlaidTransactionProcessor
|
||||
{
|
||||
Task<bool> ProcessAsync(
|
||||
Account account,
|
||||
TransactionsSyncResponse syncResponse,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
internal sealed class PlaidTransactionProcessor : IPlaidTransactionProcessor
|
||||
{
|
||||
private readonly IPlaidAddedTransactionHandler _addedHandler;
|
||||
private readonly IPlaidModifiedTransactionHandler _modifiedHandler;
|
||||
private readonly IPlaidRemovedTransactionHandler _removedHandler;
|
||||
|
||||
private PlaidTransactionProcessor(
|
||||
IPlaidAddedTransactionHandler addedHandler,
|
||||
IPlaidModifiedTransactionHandler modifiedHandler,
|
||||
IPlaidRemovedTransactionHandler removedHandler
|
||||
)
|
||||
{
|
||||
_addedHandler = addedHandler;
|
||||
_modifiedHandler = modifiedHandler;
|
||||
_removedHandler = removedHandler;
|
||||
}
|
||||
|
||||
internal static PlaidTransactionProcessor From(
|
||||
IPlaidAddedTransactionHandler addedHandler,
|
||||
IPlaidModifiedTransactionHandler modifiedHandler,
|
||||
IPlaidRemovedTransactionHandler removedHandler
|
||||
)
|
||||
{
|
||||
return new(addedHandler, modifiedHandler, removedHandler);
|
||||
}
|
||||
|
||||
internal static PlaidTransactionProcessor From(IServiceProvider provider)
|
||||
{
|
||||
return new(
|
||||
provider.GetRequiredService<IPlaidAddedTransactionHandler>(),
|
||||
provider.GetRequiredService<IPlaidModifiedTransactionHandler>(),
|
||||
provider.GetRequiredService<IPlaidRemovedTransactionHandler>()
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<bool> ProcessAsync(
|
||||
Account account,
|
||||
TransactionsSyncResponse syncResponse,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var numAdded = await _addedHandler.HandleAsync(
|
||||
account,
|
||||
syncResponse.Added,
|
||||
cancellationToken
|
||||
).ConfigureAwait(false);
|
||||
|
||||
var numModified = await _modifiedHandler.HandleAsync(
|
||||
account,
|
||||
syncResponse.Modified,
|
||||
cancellationToken
|
||||
).ConfigureAwait(false);
|
||||
|
||||
var numRemoved = await _removedHandler.HandleAsync(
|
||||
syncResponse.Removed,
|
||||
cancellationToken
|
||||
).ConfigureAwait(false);
|
||||
|
||||
return numAdded == syncResponse.Added.Count &&
|
||||
numModified == syncResponse.Modified.Count &&
|
||||
numRemoved == syncResponse.Removed.Count;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||
|
||||
internal interface IPlaidTransactionService
|
||||
{
|
||||
IAsyncEnumerable<TransactionsSyncResponse> SyncTransactionsForAccountAsync(
|
||||
string accessToken,
|
||||
string accountId,
|
||||
string? initialCursor = null
|
||||
);
|
||||
}
|
||||
|
||||
internal sealed class PlaidTransactionService : IPlaidTransactionService
|
||||
{
|
||||
private readonly PlaidClient _client;
|
||||
|
||||
private PlaidTransactionService(PlaidClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public static PlaidTransactionService From(IServiceProvider sp)
|
||||
{
|
||||
var client = sp.GetRequiredService<PlaidClient>();
|
||||
return new(client);
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<TransactionsSyncResponse> SyncTransactionsForAccountAsync(
|
||||
string accessToken,
|
||||
string accountId,
|
||||
string? initialCursor = null
|
||||
)
|
||||
{
|
||||
TransactionsSyncResponse? response;
|
||||
var cursor = initialCursor;
|
||||
|
||||
do
|
||||
{
|
||||
response = await _client.TransactionsSyncAsync(new()
|
||||
{
|
||||
AccessToken = accessToken,
|
||||
Cursor = cursor,
|
||||
Count = 500,
|
||||
Options = new()
|
||||
{
|
||||
AccountId = accountId,
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (response.Error?.ErrorCode is "TRANSACTIONS_SYNC_MUTATION_DURING_PAGINATION")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
cursor = response.NextCursor;
|
||||
yield return response;
|
||||
} while (response is not null && response.HasMore);
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
using Account = FiscalOS.Core.Accounts.Account;
|
||||
|
||||
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||
|
||||
internal interface IPlaidTransactionSyncer
|
||||
{
|
||||
Task SyncTransactionsForAccountAsync(
|
||||
Account account,
|
||||
string decryptedAccessToken,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
internal sealed class PlaidTransactionSyncer : IPlaidTransactionSyncer
|
||||
{
|
||||
private readonly ILogger<PlaidTransactionSyncer> _logger;
|
||||
private readonly IPlaidTransactionService _plaidTransactionService;
|
||||
private readonly IPlaidTransactionProcessor _transactionProcessor;
|
||||
|
||||
private PlaidTransactionSyncer(
|
||||
ILogger<PlaidTransactionSyncer> logger,
|
||||
IPlaidTransactionService plaidTransactionService,
|
||||
IPlaidTransactionProcessor plaidTransactionProcessor
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_plaidTransactionService = plaidTransactionService;
|
||||
_transactionProcessor = plaidTransactionProcessor;
|
||||
}
|
||||
|
||||
public static PlaidTransactionSyncer From(
|
||||
ILogger<PlaidTransactionSyncer> logger,
|
||||
IPlaidTransactionService plaidTransactionService,
|
||||
IPlaidTransactionProcessor plaidTransactionProcessor
|
||||
)
|
||||
{
|
||||
return new(logger, plaidTransactionService, plaidTransactionProcessor);
|
||||
}
|
||||
|
||||
public static PlaidTransactionSyncer From(IServiceProvider serviceProvider)
|
||||
{
|
||||
return new(
|
||||
serviceProvider.GetRequiredService<ILogger<PlaidTransactionSyncer>>(),
|
||||
serviceProvider.GetRequiredService<IPlaidTransactionService>(),
|
||||
serviceProvider.GetRequiredService<IPlaidTransactionProcessor>()
|
||||
);
|
||||
}
|
||||
|
||||
public async Task SyncTransactionsForAccountAsync(Account account, string decryptedAccessToken, CancellationToken cancellationToken)
|
||||
{
|
||||
if (account.Metadata is not PlaidAccountMetadata plaidAccountMetadata)
|
||||
{
|
||||
_logger.LogWarning("Unable to sync transactions for account {AccountId} as it has no Plaid metadata", account.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
var responses = _plaidTransactionService.SyncTransactionsForAccountAsync(
|
||||
decryptedAccessToken,
|
||||
plaidAccountMetadata.PlaidId,
|
||||
plaidAccountMetadata.Cursor
|
||||
).ConfigureAwait(false);
|
||||
|
||||
await foreach (var response in responses)
|
||||
{
|
||||
if (response.IsSuccessStatusCode is false)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Failed to sync transactions for request {RequestId} for account {AccountId}: {ErrorMessage}",
|
||||
response.RequestId,
|
||||
account.Id,
|
||||
response.Error?.ErrorMessage
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (response.Accounts.Any() is false)
|
||||
{
|
||||
_logger.LogInformation("No transactions to sync for request {RequestId} for account {AccountId}", response.RequestId, account.Id);
|
||||
break;
|
||||
}
|
||||
|
||||
var plaidAccount = response.Accounts
|
||||
.FirstOrDefault(pa => pa.AccountId == plaidAccountMetadata.PlaidId);
|
||||
|
||||
if (plaidAccount is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Failed to sync transactions for request {RequestId} for account {AccountId}: No corresponding account received in Plaid response",
|
||||
response.RequestId,
|
||||
account.Id
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
var balance = Balance.From(plaidAccount.Current, plaidAccount.Available, plaidAccount.CurrencyCode);
|
||||
account.AddBalance(balance);
|
||||
|
||||
var isProcessedSuccessfully = await _transactionProcessor.ProcessAsync(account, response, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (isProcessedSuccessfully is false)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to process transactions for request {RequestId} for account {AccountId} successfully",
|
||||
response.RequestId,
|
||||
account.Id
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
plaidAccountMetadata.SetCursor(response.NextCursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||
|
||||
internal sealed class SyncUpdatesProcessor : IAsyncQueueProcessor<SyncUpdatesQueueItem>
|
||||
{
|
||||
private readonly ILogger<SyncUpdatesProcessor> _logger;
|
||||
private readonly AppDbContext _appDbContext;
|
||||
private readonly IEncryptor _encryptor;
|
||||
private readonly IPlaidTransactionSyncer _plaidTransactionSyncer;
|
||||
|
||||
private SyncUpdatesProcessor(
|
||||
ILogger<SyncUpdatesProcessor> logger,
|
||||
AppDbContext appDbContext,
|
||||
IEncryptor encryptor,
|
||||
IPlaidTransactionSyncer plaidTransactionSyncer
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_appDbContext = appDbContext;
|
||||
_encryptor = encryptor;
|
||||
_plaidTransactionSyncer = plaidTransactionSyncer;
|
||||
}
|
||||
|
||||
public static SyncUpdatesProcessor From(IServiceProvider serviceProvider)
|
||||
{
|
||||
return new(
|
||||
serviceProvider.GetRequiredService<ILogger<SyncUpdatesProcessor>>(),
|
||||
serviceProvider.GetRequiredService<AppDbContext>(),
|
||||
serviceProvider.GetRequiredService<IEncryptor>(),
|
||||
serviceProvider.GetRequiredService<IPlaidTransactionSyncer>()
|
||||
);
|
||||
}
|
||||
|
||||
public async Task ProcessAsync(SyncUpdatesQueueItem item, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation("Processing sync updates for item {ItemId}", item.InstitutionItemId);
|
||||
|
||||
var institution = await _appDbContext.Institutions
|
||||
.Include(i => i.User)
|
||||
.Include(i => i.Accounts)
|
||||
.ThenInclude(a => a.Metadata)
|
||||
.Include(i => i.Metadata)
|
||||
.Where(i => i.Metadata is PlaidInstitutionMetadata && ((PlaidInstitutionMetadata)i.Metadata).ItemId == item.InstitutionItemId)
|
||||
.AsSplitQuery()
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (institution is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to process sync updates for item {ItemId} as no institution with matching Plaid item ID was found.",
|
||||
item.InstitutionItemId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (institution.User is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to process sync updates for item {ItemId} as the associated institution {InstitutionId} does not have a user.",
|
||||
item.InstitutionItemId,
|
||||
institution.Id
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (institution.Metadata is not PlaidInstitutionMetadata plaidMetadata)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to process sync updates for item {ItemId} as the associated institution {InstitutionId} does not have Plaid metadata.",
|
||||
item.InstitutionItemId,
|
||||
institution.Id
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var decryptedAccessToken = await _encryptor.DecryptAsyncFor(
|
||||
institution.User,
|
||||
plaidMetadata.EncryptedAccessToken,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
foreach (var account in institution.Accounts)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _plaidTransactionSyncer.SyncTransactionsForAccountAsync(account, decryptedAccessToken, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to sync transactions for account {AccountId}", account.Id);
|
||||
}
|
||||
}
|
||||
|
||||
await _appDbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("Successfully processed sync updates for item {ItemId}", item.InstitutionItemId);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||
|
||||
public sealed record SyncUpdatesQueueItem(string InstitutionItemId);
|
||||
@@ -1,31 +1,23 @@
|
||||
global using System.IdentityModel.Tokens.Jwt;
|
||||
global using System.IO.Abstractions;
|
||||
global using System.Reflection;
|
||||
global using System.Security.Claims;
|
||||
global using System.Security.Cryptography;
|
||||
global using System.Text;
|
||||
global using System.Text.Json;
|
||||
global using System.Threading.Channels;
|
||||
|
||||
global using FiscalOS.Core.Accounts;
|
||||
global using FiscalOS.Core.Authentication;
|
||||
global using FiscalOS.Core.Data;
|
||||
global using FiscalOS.Core.Identity;
|
||||
global using FiscalOS.Core.Queuing;
|
||||
global using FiscalOS.Core.Security;
|
||||
global using FiscalOS.Core.Transactions;
|
||||
global using FiscalOS.Infra.Accounts.Plaid;
|
||||
global using FiscalOS.Infra.Authentication;
|
||||
global using FiscalOS.Infra.Authorization;
|
||||
global using FiscalOS.Infra.Common;
|
||||
global using FiscalOS.Infra.Data;
|
||||
global using FiscalOS.Infra.Queuing;
|
||||
global using FiscalOS.Infra.Security;
|
||||
global using FiscalOS.Infra.Transactions.Plaid;
|
||||
|
||||
global using Going.Plaid;
|
||||
global using Going.Plaid.Entity;
|
||||
global using Going.Plaid.Transactions;
|
||||
|
||||
global using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
global using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
@@ -59,7 +59,7 @@ public static class ServiceDefaultsExtensions
|
||||
|
||||
public static WebApplication MapDefaultEndpoints(this WebApplication app)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(app, nameof(app));
|
||||
ArgumentNullException.ThrowIfNull(app);
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
|
||||
@@ -1,102 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import LeftArrowIcon from '@/components/icons/RightArrowIcon.vue';
|
||||
import { useAuthService } from '@/composables/useAuthService';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import { computed } from 'vue';
|
||||
import LeftArrowIcon from '@/components/icons/RightArrowIcon.vue';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
const authService = useAuthService(userStore);
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
const asideClasses = computed(() => ({
|
||||
collapsed: userStore.user?.sidebarCollapsed,
|
||||
}));
|
||||
const asideClasses = computed(() => ({
|
||||
collapsed: userStore.user?.sidebarCollapsed,
|
||||
}));
|
||||
|
||||
function handleToggleButtonClick() {
|
||||
userStore.toggleSidebar();
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
const logoutResult = await authService.logout();
|
||||
|
||||
if (logoutResult.err) {
|
||||
alert(logoutResult.val.map(e => e.message).join('\n'));
|
||||
return;
|
||||
function handleToggleButtonClick() {
|
||||
userStore.toggleSidebar();
|
||||
}
|
||||
|
||||
userStore.logUserOut();
|
||||
router.push({ path: '/public/login' });
|
||||
}
|
||||
function handleLogout() {
|
||||
// TODO: Also need to log user
|
||||
// out on the server...which is
|
||||
// basically just clearing
|
||||
// refresh token cookie and revoking
|
||||
// it in the database
|
||||
userStore.logUserOut();
|
||||
router.push({ path: '/public/login' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside :class="asideClasses">
|
||||
<button @click="handleToggleButtonClick" type="button" class="toggle-button">
|
||||
<button
|
||||
@click="handleToggleButtonClick"
|
||||
type="button"
|
||||
class="toggle-button"
|
||||
>
|
||||
<LeftArrowIcon />
|
||||
</button>
|
||||
<button class="logout-button" type="button" @click="handleLogout">
|
||||
<button
|
||||
class="logout-button"
|
||||
type="button"
|
||||
@click="handleLogout"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
aside {
|
||||
--sidebar-width: 15.625rem;
|
||||
--button-size: 1.5rem;
|
||||
--transition-duration: 0.5s;
|
||||
--transition-function: ease-in-out;
|
||||
position: relative;
|
||||
width: var(--sidebar-width);
|
||||
height: 100%;
|
||||
z-index: 999;
|
||||
background: var(--bg-surface);
|
||||
transition-property: width, transform;
|
||||
transition-duration: var(--transition-duration);
|
||||
transition-timing-function: var(--transition-function);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 48rem) {
|
||||
aside {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
--sidebar-width: 15.625rem;
|
||||
--button-size: 1.5rem;
|
||||
--transition-duration: 0.5s;
|
||||
--transition-function: ease-in-out;
|
||||
position: relative;
|
||||
width: var(--sidebar-width);
|
||||
height: 100%;
|
||||
z-index: 999;
|
||||
background: var(--bg-surface);
|
||||
transition-property: width, transform;
|
||||
transition-duration: var(--transition-duration);
|
||||
transition-timing-function: var(--transition-function);
|
||||
}
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
background: var(--bg-element);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
@media screen and (max-width: 48rem) {
|
||||
aside {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 3rem;
|
||||
left: calc(-1 * var(--button-size) / 2);
|
||||
height: var(--button-size);
|
||||
width: var(--button-size);
|
||||
background: var(--bg-element);
|
||||
border-radius: 50%;
|
||||
transition-property: transform;
|
||||
transition-duration: var(--transition-duration);
|
||||
transition-timing-function: var(--transition-function);
|
||||
}
|
||||
.logout-button {
|
||||
background: var(--bg-element);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
aside.collapsed {
|
||||
width: calc(0.125rem + var(--button-size) / 2);
|
||||
}
|
||||
.toggle-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 3rem;
|
||||
left: calc(-1 * var(--button-size) / 2);
|
||||
height: var(--button-size);
|
||||
width: var(--button-size);
|
||||
background: var(--bg-element);
|
||||
border-radius: 50%;
|
||||
transition-property: transform;
|
||||
transition-duration: var(--transition-duration);
|
||||
transition-timing-function: var(--transition-function);
|
||||
}
|
||||
|
||||
aside.collapsed .toggle-button {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
aside.collapsed {
|
||||
width: calc(0.125rem + var(--button-size) / 2);
|
||||
}
|
||||
|
||||
.toggle-button svg {
|
||||
--size: 1rem;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
aside.collapsed .toggle-button {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.toggle-button svg {
|
||||
--size: 1rem;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,7 +19,6 @@ export class AuthServiceFactory implements IAuthServiceFactory {
|
||||
export interface IAuthService {
|
||||
login: (username: string, password: string) => Promise<Result<LoginResponse, Error[]>>;
|
||||
refreshToken: () => Promise<Result<LoginResponse, Error[]>>;
|
||||
logout: () => Promise<Result<boolean, Error[]>>;
|
||||
}
|
||||
|
||||
export class AuthService implements IAuthService {
|
||||
@@ -27,30 +26,12 @@ export class AuthService implements IAuthService {
|
||||
private readonly endpoints = {
|
||||
login: '/api/auth/login',
|
||||
refreshToken: '/api/auth/refresh',
|
||||
logout: '/api/auth/logout',
|
||||
};
|
||||
|
||||
constructor(client: IClient) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async logout() {
|
||||
const request = new ClientRequestWithBody(this.endpoints.logout, undefined);
|
||||
|
||||
try {
|
||||
const res = await this.client.post(request);
|
||||
|
||||
if (res.ok === false) {
|
||||
return Err([new Error('Failed to logout. Please try again.')]);
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return Err([new Error('Failed to logout. Please try again.')]);
|
||||
}
|
||||
}
|
||||
|
||||
async login(username: string, password: string) {
|
||||
const request = new ClientRequestWithBody(
|
||||
this.endpoints.login,
|
||||
|
||||
@@ -21,16 +21,12 @@ export interface IInstitutionService {
|
||||
createLinkToken: () => Promise<Result<LinkTokenResponse, Error[]>>;
|
||||
connect: (publicToken: string, plaidInstitutionId: string) => Promise<Result<boolean, Error[]>>;
|
||||
getInstitutions: () => Promise<Result<Institution[], Error[]>>;
|
||||
getAvailableAccounts: (institutionId: string) => Promise<Result<AvailableAccount[], Error[]>>;
|
||||
}
|
||||
|
||||
export class InstitutionService implements IInstitutionService {
|
||||
private readonly client: IClient;
|
||||
private readonly endpoints = {
|
||||
link: '/api/institutions/link',
|
||||
available(id: string) {
|
||||
return `/api/institutions/${id}/available`;
|
||||
},
|
||||
connect: '/api/institutions/connect',
|
||||
institutions: '/api/institutions',
|
||||
};
|
||||
@@ -109,47 +105,13 @@ export class InstitutionService implements IInstitutionService {
|
||||
return Err([new Error('Failed to get institutions')]);
|
||||
}
|
||||
}
|
||||
|
||||
async getAvailableAccounts(institutionId: string) {
|
||||
const request = new ClientRequest(this.endpoints.available(institutionId));
|
||||
|
||||
try {
|
||||
const res = await this.client.get(request);
|
||||
|
||||
if (res.ok === false) {
|
||||
return Err([new Error('Failed to get available accounts')]);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
return Ok(data.accounts as AvailableAccount[]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return Err([new Error('Failed to get available accounts')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type LinkTokenResponse = {
|
||||
linkToken: string;
|
||||
};
|
||||
|
||||
type Account = {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type Institution = {
|
||||
id: string;
|
||||
name: string;
|
||||
accounts: Account[];
|
||||
};
|
||||
|
||||
export type AvailableAccount = {
|
||||
providerInstitutionId: string;
|
||||
providerId: string;
|
||||
providerName: string;
|
||||
currentBalance: number;
|
||||
availableBalance: number;
|
||||
currencyCode: string;
|
||||
};
|
||||
|
||||
@@ -1,210 +1,156 @@
|
||||
<script setup lang="ts">
|
||||
import { useInstitutionService } from '@/composables/useInstitutionService';
|
||||
import type { AvailableAccount, Institution } from '@/services/institutionService';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import { usePlaidLink, type PlaidLinkOptions } from '@jcss/vue-plaid-link';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import { useInstitutionService } from '@/composables/useInstitutionService';
|
||||
import type { Institution } from '@/services/institutionService';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import { usePlaidLink, type PlaidLinkOptions } from '@jcss/vue-plaid-link';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const institutionService = useInstitutionService(userStore);
|
||||
const userStore = useUserStore();
|
||||
const institutionService = useInstitutionService(userStore);
|
||||
|
||||
type InstitutionData =
|
||||
| {
|
||||
status: 'loading';
|
||||
}
|
||||
| { status: 'loaded'; data: Institution[] }
|
||||
| { status: 'errored'; errors: Error[] };
|
||||
type InstitutionData =
|
||||
| {
|
||||
status: 'loading';
|
||||
}
|
||||
| { status: 'loaded'; data: Institution[] }
|
||||
| { status: 'errored'; errors: Error[] };
|
||||
|
||||
const institutionsData = ref<InstitutionData>({ status: 'loading' });
|
||||
const institutionsData = ref<InstitutionData>({ status: 'loading' });
|
||||
|
||||
const plaidOptions = ref<PlaidLinkOptions>({
|
||||
token: '',
|
||||
onSuccess: async (publicToken, metadata) => {
|
||||
if (metadata.institution == null) {
|
||||
alert('Institution information is missing from Plaid response. Please try again.');
|
||||
return;
|
||||
}
|
||||
const plaidOptions = ref<PlaidLinkOptions>({
|
||||
token: '',
|
||||
onSuccess: async (publicToken, metadata) => {
|
||||
if (metadata.institution == null) {
|
||||
alert('Institution information is missing from Plaid response. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
const connectResult = await institutionService.connect(
|
||||
publicToken,
|
||||
metadata.institution.institution_id
|
||||
);
|
||||
const connectResult = await institutionService.connect(
|
||||
publicToken,
|
||||
metadata.institution.institution_id
|
||||
);
|
||||
|
||||
if (connectResult.err) {
|
||||
alert(connectResult.val.map(e => e.message).join('\n'));
|
||||
return;
|
||||
}
|
||||
if (connectResult.err) {
|
||||
alert(connectResult.val.map(e => e.message).join('\n'));
|
||||
return;
|
||||
}
|
||||
|
||||
institutionsData.value = { status: 'loading' };
|
||||
},
|
||||
onLoad: () => console.log('loaded'),
|
||||
onExit: () => console.log('exit'),
|
||||
});
|
||||
institutionsData.value = { status: 'loading' };
|
||||
},
|
||||
onLoad: () => console.log('loaded'),
|
||||
onExit: () => console.log('exit'),
|
||||
});
|
||||
const { open } = usePlaidLink(plaidOptions);
|
||||
|
||||
const { open } = usePlaidLink(plaidOptions);
|
||||
watch(
|
||||
institutionsData,
|
||||
async data => {
|
||||
if (data.status !== 'loading') {
|
||||
return;
|
||||
}
|
||||
|
||||
watch(
|
||||
institutionsData,
|
||||
async data => {
|
||||
if (data.status !== 'loading') {
|
||||
return;
|
||||
}
|
||||
const institutionsResult = await institutionService.getInstitutions();
|
||||
|
||||
const institutionsResult = await institutionService.getInstitutions();
|
||||
if (institutionsResult.err) {
|
||||
institutionsData.value = {
|
||||
status: 'errored',
|
||||
errors: institutionsResult.val,
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (institutionsResult.err) {
|
||||
institutionsData.value = {
|
||||
status: 'errored',
|
||||
errors: institutionsResult.val,
|
||||
status: 'loaded',
|
||||
data: institutionsResult.val,
|
||||
};
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
async function handleAddInstitutionClick() {
|
||||
const linkTokenResult = await institutionService.createLinkToken();
|
||||
|
||||
if (linkTokenResult.err) {
|
||||
alert(linkTokenResult.val.map(e => e.message).join('\n'));
|
||||
return;
|
||||
}
|
||||
|
||||
institutionsData.value = {
|
||||
status: 'loaded',
|
||||
data: institutionsResult.val,
|
||||
plaidOptions.value = {
|
||||
...plaidOptions.value,
|
||||
token: linkTokenResult.val.linkToken,
|
||||
};
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
async function handleAddInstitutionClick() {
|
||||
const linkTokenResult = await institutionService.createLinkToken();
|
||||
await nextTick();
|
||||
|
||||
if (linkTokenResult.err) {
|
||||
alert(linkTokenResult.val.map(e => e.message).join('\n'));
|
||||
return;
|
||||
open();
|
||||
}
|
||||
|
||||
plaidOptions.value = {
|
||||
...plaidOptions.value,
|
||||
token: linkTokenResult.val.linkToken,
|
||||
};
|
||||
|
||||
await nextTick();
|
||||
|
||||
open();
|
||||
}
|
||||
|
||||
const targetInstitutionOfAdd = ref<Institution | null>(null);
|
||||
const availableAccounts = ref<AvailableAccount[]>([]);
|
||||
const targetAccountOfAdd = ref<AvailableAccount | null>(null);
|
||||
|
||||
async function handleAddAccountClick(institution: Institution) {
|
||||
const accountsResult = await institutionService.getAvailableAccounts(institution.id);
|
||||
|
||||
if (accountsResult.err) {
|
||||
alert('Failed to retrieve accounts to add');
|
||||
return;
|
||||
}
|
||||
|
||||
availableAccounts.value = accountsResult.val;
|
||||
targetInstitutionOfAdd.value = institution;
|
||||
}
|
||||
|
||||
async function handleAccountAddClick() {
|
||||
if (targetAccountOfAdd.value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = {
|
||||
plaidInstitutionId: targetAccountOfAdd.value.providerInstitutionId,
|
||||
plaidAccountId: targetAccountOfAdd.value.providerId,
|
||||
plaidAccountName: targetAccountOfAdd.value.providerName,
|
||||
accountAvailableBalance: targetAccountOfAdd.value.availableBalance,
|
||||
accountCurrencyCode: targetAccountOfAdd.value.currencyCode,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/accounts', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${userStore.user?.token}`,
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
if (res.ok === false) {
|
||||
alert('Add failed');
|
||||
return;
|
||||
}
|
||||
|
||||
targetInstitutionOfAdd.value = null;
|
||||
institutionsData.value = { status: 'loading' };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<button class="add-institution-button" type="button" @click="handleAddInstitutionClick">
|
||||
<button
|
||||
class="add-institution-button"
|
||||
type="button"
|
||||
@click="handleAddInstitutionClick"
|
||||
>
|
||||
Add Institution
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="institutionsData.status === 'loaded'" class="institutions-container">
|
||||
<div v-for="institution in institutionsData.data" v-bind:key="institution.id">
|
||||
<div class="institution-card">
|
||||
<div>
|
||||
<div>{{ institution.name }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="add-account-button" type="button" @click="handleAddAccountClick(institution)">
|
||||
Add Account
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="institutionsData.status === 'loaded'"
|
||||
class="institutions-container"
|
||||
>
|
||||
<div
|
||||
class="institution-card"
|
||||
v-for="institution in institutionsData.data"
|
||||
v-bind:key="institution.id"
|
||||
>
|
||||
<div>
|
||||
<div>{{ institution.name }}</div>
|
||||
</div>
|
||||
<div v-if="targetInstitutionOfAdd != null && targetInstitutionOfAdd.id === institution.id">
|
||||
<select v-model="targetAccountOfAdd">
|
||||
<option v-for="account in availableAccounts" v-bind:key="account.providerId" :value="account">
|
||||
{{ account.providerName }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" @click="handleAccountAddClick">
|
||||
Add
|
||||
<div>
|
||||
<button
|
||||
class="add-account-button"
|
||||
type="button"
|
||||
>
|
||||
Add Account
|
||||
</button>
|
||||
</div>
|
||||
<div v-for="accounts in institution.accounts" v-bind:key="accounts.id">
|
||||
<div>{{ accounts.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="institutionsData.status === 'errored'">Failed to load institutions</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.institutions-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
.institutions-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.institution-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
.institution-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.institution-card>div {
|
||||
flex: 1;
|
||||
}
|
||||
.institution-card > div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.institution-card>div:last-of-type {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.institution-card > div:last-of-type {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.add-institution-button,
|
||||
.add-account-button {
|
||||
background: var(--bg-element);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.add-institution-button,
|
||||
.add-account-button {
|
||||
background: var(--bg-element);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,10 +13,7 @@ internal sealed class HttpRequestBuilder
|
||||
{
|
||||
}
|
||||
|
||||
public static HttpRequestBuilder New()
|
||||
{
|
||||
return new();
|
||||
}
|
||||
public static HttpRequestBuilder New() => new();
|
||||
|
||||
public HttpRequestBuilder WithMethod(HttpMethod method)
|
||||
{
|
||||
|
||||
@@ -2,8 +2,6 @@ namespace FiscalOS.API.Tests.Infra;
|
||||
|
||||
public class TestApi : WebApplicationFactory<Program>
|
||||
{
|
||||
private readonly List<Action<IWebHostBuilder>> _additionalConfigs = [];
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
base.ConfigureWebHost(builder);
|
||||
@@ -27,71 +25,5 @@ public class TestApi : WebApplicationFactory<Program>
|
||||
|
||||
c.AddSingleton<IKeyRing>(TestKeyRing.From);
|
||||
});
|
||||
|
||||
foreach (var config in _additionalConfigs)
|
||||
{
|
||||
config(builder);
|
||||
}
|
||||
}
|
||||
|
||||
public TestApi WithAdditionalConfig(Action<IWebHostBuilder> configuration)
|
||||
{
|
||||
var newApi = new TestApi();
|
||||
|
||||
newApi._additionalConfigs.AddRange(_additionalConfigs);
|
||||
newApi._additionalConfigs.Add(configuration);
|
||||
|
||||
return newApi;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(Func<DbContext, CancellationToken, Task> action, CancellationToken ct)
|
||||
{
|
||||
await using var scope = Services.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await action(context, ct);
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(Func<DbContext, CancellationToken, IServiceProvider, Task> action, CancellationToken ct)
|
||||
{
|
||||
await using var scope = Services.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await action(context, ct, scope.ServiceProvider);
|
||||
}
|
||||
|
||||
public async Task<T> ExecuteAsync<T>(Func<DbContext, CancellationToken, Task<T>> action, CancellationToken ct)
|
||||
{
|
||||
await using var scope = Services.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
return await action(context, ct);
|
||||
}
|
||||
|
||||
public async Task<T> ExecuteAsync<T>(Func<DbContext, CancellationToken, IServiceProvider, Task<T>> action, CancellationToken ct)
|
||||
{
|
||||
await using var scope = Services.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
return await action(context, ct, scope.ServiceProvider);
|
||||
}
|
||||
|
||||
public async Task EnsureDbCreatedAsync()
|
||||
{
|
||||
await ExecuteAsync(static async (context, ct) =>
|
||||
{
|
||||
await context.Database.EnsureCreatedAsync(ct);
|
||||
}, TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
public async Task EnsureDbDeletedAsync()
|
||||
{
|
||||
await ExecuteAsync(static async (context, ct) =>
|
||||
{
|
||||
await context.Database.EnsureDeletedAsync(ct);
|
||||
}, TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await EnsureDbDeletedAsync();
|
||||
await base.DisposeAsync();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
using FiscalOS.Core.Queuing;
|
||||
using FiscalOS.Infra.Transactions.Plaid;
|
||||
|
||||
using Account = FiscalOS.Core.Accounts.Account;
|
||||
using Institution = FiscalOS.Core.Accounts.Institution;
|
||||
|
||||
@@ -34,6 +31,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
["PlaidInstitutionId"] = ["The PlaidInstitutionId field is required."],
|
||||
["PlaidAccountId"] = ["The PlaidAccountId field is required."],
|
||||
["PlaidAccountName"] = ["The PlaidAccountName field is required."],
|
||||
["AccountCurrencyCode"] = ["The AccountCurrencyCode field is required."],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -126,7 +124,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
[Fact]
|
||||
public async Task Add_WhenCalledWithPlaidInstitutionIdThatHasNotBeenAdded_ItShouldReturn400WithProblemDetails()
|
||||
{
|
||||
var user = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||
var user = await ExecuteAsync(static async (context, ct, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
@@ -163,7 +161,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
[Fact]
|
||||
public async Task Add_WhenCalledWithPlaidAccountIdThatHasAlreadyBeenAdded_ItShouldReturn409WithProblemDetails()
|
||||
{
|
||||
var (user, institution, account) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||
var (user, institution, account) = await ExecuteAsync(static async (context, ct, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
@@ -171,25 +169,20 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(ct);
|
||||
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
||||
|
||||
await context.AddAsync(user, ct);
|
||||
|
||||
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, "accessToken", ct);
|
||||
var plaidMetadata = PlaidInstitutionMetadata.From(
|
||||
"alreadyExists",
|
||||
"Some Bank",
|
||||
encryptedAccessToken,
|
||||
"itemId"
|
||||
);
|
||||
var plaidMetadata = PlaidMetadata.From("alreadyExists", "Some Bank", encryptedAccessToken);
|
||||
var institution = Institution.From("Some Bank", plaidMetadata);
|
||||
|
||||
await context.AddAsync(institution, ct);
|
||||
|
||||
var plaidAccountMetadata = PlaidAccountMetadata.From("accountId", "Some Account");
|
||||
var account = Account.From("Some Account", plaidAccountMetadata);
|
||||
var account = Account.From(institution.Id, "Some Account", plaidAccountMetadata);
|
||||
|
||||
institution.AddAccount(account);
|
||||
user.AddInstitution(institution);
|
||||
user.AddAccount(account);
|
||||
|
||||
await context.AddAsync(user, ct);
|
||||
await context.SaveChangesAsync(ct);
|
||||
return (user, institution, account);
|
||||
}, TestContext.Current.CancellationToken);
|
||||
@@ -199,7 +192,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
.WithUserId(user.Id)
|
||||
.WithBody(new
|
||||
{
|
||||
plaidInstitutionId = ((PlaidInstitutionMetadata)institution.Metadata!).PlaidId,
|
||||
plaidInstitutionId = ((PlaidMetadata)institution.Metadata!).PlaidId,
|
||||
plaidAccountId = ((PlaidAccountMetadata)account.Metadata!).PlaidId,
|
||||
plaidAccountName = ((PlaidAccountMetadata)account.Metadata).PlaidName,
|
||||
accountCurrencyCode = "USD",
|
||||
@@ -214,18 +207,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
[Fact]
|
||||
public async Task Add_WhenCalledWithNewAccount_ItShouldReturn200()
|
||||
{
|
||||
var mockQueue = new Mock<IAsyncQueue<SyncUpdatesQueueItem>>();
|
||||
|
||||
await using var testApi = Api
|
||||
.WithAdditionalConfig(whb =>
|
||||
{
|
||||
whb.ConfigureTestServices(s =>
|
||||
{
|
||||
s.AddSingleton(mockQueue.Object);
|
||||
});
|
||||
});
|
||||
|
||||
var (user, institution) = await testApi.ExecuteAsync(async (context, ct, sp) =>
|
||||
var (user, institution) = await ExecuteAsync(async (context, ct, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
@@ -235,12 +217,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
||||
|
||||
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, "accessToken", ct);
|
||||
var plaidMetadata = PlaidInstitutionMetadata.From(
|
||||
"id",
|
||||
"Some Bank",
|
||||
encryptedAccessToken,
|
||||
"itemId"
|
||||
);
|
||||
var plaidMetadata = PlaidMetadata.From("id", "Some Bank", encryptedAccessToken);
|
||||
var institution = Institution.From("Some Bank", plaidMetadata);
|
||||
|
||||
user.AddInstitution(institution);
|
||||
@@ -260,7 +237,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
.WithUserId(user.Id)
|
||||
.WithBody(new
|
||||
{
|
||||
plaidInstitutionId = ((PlaidInstitutionMetadata)institution.Metadata!).PlaidId,
|
||||
plaidInstitutionId = ((PlaidMetadata)institution.Metadata!).PlaidId,
|
||||
plaidAccountId = newAccountId,
|
||||
plaidAccountName = newAccountName,
|
||||
accountCurrentBalance = expectedBalance,
|
||||
@@ -269,13 +246,11 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
})
|
||||
.Build();
|
||||
|
||||
var client = testApi.CreateClient();
|
||||
|
||||
var response = await client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var updatedUser = await testApi.ExecuteAsync(
|
||||
var updatedUser = await ExecuteAsync(
|
||||
async (context, ct) => await context.Set<User>()
|
||||
.Include(u => u.Accounts)
|
||||
.ThenInclude(a => a.Metadata)
|
||||
@@ -294,9 +269,12 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
((PlaidAccountMetadata)a.Metadata).PlaidName == newAccountName
|
||||
);
|
||||
|
||||
mockQueue.Verify(m => m.EnqueueAsync(
|
||||
It.IsAny<SyncUpdatesQueueItem>(),
|
||||
It.IsAny<CancellationToken>()
|
||||
), Times.Once());
|
||||
updatedUser.Accounts.First()
|
||||
.Balances.Should().ContainSingle(
|
||||
b => b.AccountId == updatedUser.Accounts.First().Id &&
|
||||
b.Current == expectedBalance &&
|
||||
b.Available == expectedBalance &&
|
||||
b.CurrencyCode == expectedCurrencyCode
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ public class LoginTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
[Fact]
|
||||
public async Task Login_WhenUserExistsButPasswordIsIncorrect_ItShouldReturn401WithProblemDetails()
|
||||
{
|
||||
await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||
await ExecuteAsync(static async (context, ct, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
@@ -70,7 +70,7 @@ public class LoginTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
[Fact]
|
||||
public async Task Login_WhenUserExistsAndPasswordIsCorrect_ItShouldReturn200WithJwtTokenAndSetRefreshCookie()
|
||||
{
|
||||
await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||
await ExecuteAsync(static async (context, ct, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
using FiscalOS.API.Auth.Login;
|
||||
|
||||
namespace FiscalOS.API.Tests.Integration.Auth;
|
||||
|
||||
public class LogoutTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
{
|
||||
private static readonly Uri LogoutUri = new("/auth/logout", UriKind.Relative);
|
||||
private static readonly Uri LoginUri = new("/auth/login", UriKind.Relative);
|
||||
|
||||
[Fact]
|
||||
public async Task Logout_WhenCalled_ItShouldExpireRefreshTokenCookie()
|
||||
{
|
||||
var user = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
|
||||
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(ct);
|
||||
var user = User.From("Stevan", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
||||
context.Add(user);
|
||||
|
||||
await context.SaveChangesAsync(ct);
|
||||
return user;
|
||||
}, TestContext.Current.CancellationToken);
|
||||
|
||||
using var loginRequest = HttpRequestBuilder.New()
|
||||
.Post(LoginUri)
|
||||
.WithBody(new
|
||||
{
|
||||
username = "Stevan",
|
||||
password = "@Password1",
|
||||
})
|
||||
.Build();
|
||||
|
||||
var loginResponse = await Client.SendAsync(loginRequest, TestContext.Current.CancellationToken);
|
||||
var setCookieHeaders = loginResponse.Headers.GetValues("Set-Cookie");
|
||||
var refreshTokenCookieHeader = setCookieHeaders.First(h => h.StartsWith("fiscalos_refresh_cookie", StringComparison.OrdinalIgnoreCase));
|
||||
var refreshTokenCookieParts = refreshTokenCookieHeader.Trim().Split("=");
|
||||
|
||||
var accessToken = (await loginResponse.Should().BeJsonContentOfType<Response>(HttpStatusCode.OK))
|
||||
.Which
|
||||
.AccessToken;
|
||||
|
||||
var userRefreshTokens = await Api.ExecuteAsync(async (context, ct) =>
|
||||
{
|
||||
return await context.Set<RefreshToken>()
|
||||
.Where(r => r.UserId == user.Id)
|
||||
.ToListAsync(ct);
|
||||
}, TestContext.Current.CancellationToken);
|
||||
|
||||
userRefreshTokens.Should()
|
||||
.ContainSingle(r => r.IsExpired(DateTimeOffset.UtcNow) == false && r.Revoked == false);
|
||||
|
||||
using var logoutRequest = HttpRequestBuilder.New()
|
||||
.Post(LogoutUri)
|
||||
.WithBearerToken(accessToken)
|
||||
.WithCookie(refreshTokenCookieParts[0], refreshTokenCookieParts[1])
|
||||
.Build();
|
||||
|
||||
var logoutResponse = await Client.SendAsync(logoutRequest, TestContext.Current.CancellationToken);
|
||||
|
||||
logoutResponse.StatusCode.Should().Be(HttpStatusCode.NoContent);
|
||||
|
||||
logoutResponse.Should().HaveSetCookieHeader("fiscalos_refresh_cookie");
|
||||
|
||||
var updatedUserRefreshTokens = await Api.ExecuteAsync(async (context, ct) =>
|
||||
{
|
||||
return await context.Set<RefreshToken>()
|
||||
.Where(r => r.UserId == user.Id)
|
||||
.ToListAsync(ct);
|
||||
}, TestContext.Current.CancellationToken);
|
||||
|
||||
updatedUserRefreshTokens.Should()
|
||||
.ContainSingle(r => r.Revoked && r.IsExpired(DateTimeOffset.UtcNow) == false);
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
[Fact]
|
||||
public async Task Refresh_WhenCalledWithTokenBelongingToDifferentUser_ItShouldReturn403WithProblemDetails()
|
||||
{
|
||||
var (users, refreshToken) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||
var (users, refreshToken) = await ExecuteAsync(static async (context, ct, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
||||
@@ -61,7 +61,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
|
||||
await response.Should().BeProblemDetails(HttpStatusCode.Forbidden);
|
||||
|
||||
var unrevokedTokensCountForUser2 = await Api.ExecuteAsync(
|
||||
var unrevokedTokensCountForUser2 = await ExecuteAsync(
|
||||
async (context, ct) => await context.Set<RefreshToken>()
|
||||
.Include(t => t.User)
|
||||
.Where(t => t.UserId == users[1].Id && t.Revoked == false)
|
||||
@@ -75,7 +75,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
[Fact]
|
||||
public async Task Refresh_WhenCalledWithRevokedRefreshToken_ItShouldReturn400WithProblemDetails()
|
||||
{
|
||||
var (user, refreshToken) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||
var (user, refreshToken) = await ExecuteAsync(static async (context, ct, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
||||
@@ -108,7 +108,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
[Fact]
|
||||
public async Task Refresh_WhenCalledWithExpiredRefreshToken_ItShouldReturn400WithProblemDetails()
|
||||
{
|
||||
var (user, refreshToken) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||
var (user, refreshToken) = await ExecuteAsync(static async (context, ct, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
||||
@@ -142,7 +142,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
[InlineData(-5)]
|
||||
public async Task Refresh_WhenCalledWithValidRefreshTokenAndExpiredOrNotExpiredAccessToken_ItShouldReturn200WithNewTokensAndSetRefreshCookie(int accessTokenExpiresAtOffset)
|
||||
{
|
||||
var (user, refreshToken) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||
var (user, refreshToken) = await ExecuteAsync(static async (context, ct, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
||||
@@ -174,7 +174,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
response.Should().HaveSetCookieHeader("fiscalos_refresh_cookie");
|
||||
await response.Should().BeJsonContentOfType<API.Auth.Refresh.Response>(HttpStatusCode.OK);
|
||||
|
||||
var oldRefreshTokenInDb = await Api.ExecuteAsync(
|
||||
var oldRefreshTokenInDb = await ExecuteAsync(
|
||||
async (context, ct) => await context.Set<RefreshToken>()
|
||||
.Include(t => t.User)
|
||||
.Where(t => t.UserId == user.Id && t.Token == refreshToken.Token && t.Revoked == true)
|
||||
@@ -184,7 +184,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
|
||||
oldRefreshTokenInDb.Should().NotBeNull();
|
||||
|
||||
var newRefreshTokenInDb = await Api.ExecuteAsync(
|
||||
var newRefreshTokenInDb = await ExecuteAsync(
|
||||
async (context, ct) => await context.Set<RefreshToken>()
|
||||
.Include(t => t.User)
|
||||
.Where(t => t.UserId == user.Id && t.Revoked == false && t.Token != refreshToken.Token)
|
||||
|
||||
@@ -92,7 +92,7 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
[Fact]
|
||||
public async Task Connect_WhenCalledWithPlaidInstitutionIdThatIsAlreadyConnected_ItShouldReturn409WithProblemDetails()
|
||||
{
|
||||
var (user, institution) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||
var (user, institution) = await ExecuteAsync(static async (context, ct, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
@@ -101,12 +101,7 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
||||
|
||||
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, "accessToken", ct);
|
||||
var plaidMetadata = PlaidInstitutionMetadata.From(
|
||||
"alreadyExists",
|
||||
"Some Bank",
|
||||
encryptedAccessToken,
|
||||
"itemId"
|
||||
);
|
||||
var plaidMetadata = PlaidMetadata.From("alreadyExists", "Some Bank", encryptedAccessToken);
|
||||
var institution = Institution.From("Some Bank", plaidMetadata);
|
||||
|
||||
user.AddInstitution(institution);
|
||||
@@ -122,7 +117,7 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
.WithBody(new
|
||||
{
|
||||
publicToken = "token",
|
||||
plaidInstitutionId = ((PlaidInstitutionMetadata)institution.Metadata!).PlaidId,
|
||||
plaidInstitutionId = ((PlaidMetadata)institution.Metadata!).PlaidId,
|
||||
})
|
||||
.Build();
|
||||
|
||||
@@ -136,7 +131,7 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
{
|
||||
var plaidInstitutionId = "ins_109508";
|
||||
|
||||
var (user, publicToken) = await Api.ExecuteAsync(async (context, ct, sp) =>
|
||||
var (user, publicToken) = await ExecuteAsync(async (context, ct, sp) =>
|
||||
{
|
||||
var plaidClient = sp.GetRequiredService<PlaidClient>();
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
@@ -171,7 +166,7 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var updatedUser = await Api.ExecuteAsync(
|
||||
var updatedUser = await ExecuteAsync(
|
||||
async (context, ct) => await context.Set<User>()
|
||||
.Include(u => u.Institutions)
|
||||
.ThenInclude(i => i.Metadata)
|
||||
@@ -184,7 +179,7 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
var institution = updatedUser.Institutions.First();
|
||||
institution.Name.Should().NotBeNullOrEmpty();
|
||||
|
||||
var metadata = institution.Metadata.As<PlaidInstitutionMetadata>();
|
||||
var metadata = institution.Metadata.As<PlaidMetadata>();
|
||||
metadata.PlaidId.Should().Be(plaidInstitutionId);
|
||||
metadata.PlaidName.Should().Be(institution.Name);
|
||||
metadata.EncryptedAccessToken.Should().NotBeNullOrEmpty();
|
||||
|
||||
@@ -39,7 +39,7 @@ public class GetAvailableTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
[Fact]
|
||||
public async Task GetAvailable_WhenCalledWithNonExistentInstitutionId_ItShouldReturn404WithProblemDetails()
|
||||
{
|
||||
var user = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||
var user = await ExecuteAsync(static async (context, ct, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
@@ -65,7 +65,7 @@ public class GetAvailableTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
[Fact]
|
||||
public async Task GetAvailable_WhenCalledWithConnectedInstitution_ItShouldReturn200WithAvailableAccounts()
|
||||
{
|
||||
var (user, institution, expectedAccounts) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||
var (user, institution, expectedAccounts) = await ExecuteAsync(static async (context, ct, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
@@ -92,12 +92,7 @@ public class GetAvailableTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
AccessToken = exchangeTokenResponse.AccessToken,
|
||||
});
|
||||
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, exchangeTokenResponse.AccessToken, ct);
|
||||
var institutionMetadata = PlaidInstitutionMetadata.From(
|
||||
institutionId,
|
||||
"Some Bank",
|
||||
encryptedAccessToken,
|
||||
exchangeTokenResponse.ItemId
|
||||
);
|
||||
var institutionMetadata = PlaidMetadata.From(institutionId, "Some Bank", encryptedAccessToken);
|
||||
var institution = Institution.From("Some Bank", institutionMetadata);
|
||||
|
||||
user.AddInstitution(institution);
|
||||
@@ -115,8 +110,7 @@ public class GetAvailableTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
|
||||
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
var expectedResponseAccounts = expectedAccounts.Select(a => AvailableAccountDto.From((PlaidInstitutionMetadata)institution.Metadata!, a));
|
||||
(await response.Should().BeJsonContentOfType<Response>(HttpStatusCode.OK))
|
||||
.Which.Accounts.Should().BeEquivalentTo(expectedResponseAccounts);
|
||||
.Which.Accounts.Should().BeEquivalentTo(expectedAccounts.Select(AccountDto.FromPlaidAccount));
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ public class GetTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
[Fact]
|
||||
public async Task Get_WhenCalledByUser_ItShouldReturn200WithListOfInstitutions()
|
||||
{
|
||||
var (user, institution) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||
var (user, institution) = await ExecuteAsync(static async (context, ct, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
@@ -45,12 +45,7 @@ public class GetTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
||||
|
||||
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, "accessToken", ct);
|
||||
var plaidMetadata = PlaidInstitutionMetadata.From(
|
||||
"alreadyExists",
|
||||
"Some Bank",
|
||||
encryptedAccessToken,
|
||||
"itemId"
|
||||
);
|
||||
var plaidMetadata = PlaidMetadata.From("alreadyExists", "Some Bank", encryptedAccessToken);
|
||||
var institution = Institution.From("Some Bank", plaidMetadata);
|
||||
|
||||
user.AddInstitution(institution);
|
||||
@@ -70,7 +65,7 @@ public class GetTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
(await res.Should().BeJsonContentOfType<Response>(HttpStatusCode.OK))
|
||||
.Which.Institutions.Should().BeEquivalentTo(
|
||||
[
|
||||
InstitutionDto.From(institution),
|
||||
InstitutionDto.FromInstitution(institution),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public class LinkTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
[Fact]
|
||||
public async Task Link_WhenCalledWithUserWhoExists_ItShouldReturnLinkToken()
|
||||
{
|
||||
var user = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||
var user = await ExecuteAsync(static async (context, ct, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
|
||||
@@ -2,17 +2,51 @@ namespace FiscalOS.API.Tests.Integration;
|
||||
|
||||
public abstract class IntegrationTest(TestApi testApi) : IClassFixture<TestApi>, IAsyncLifetime
|
||||
{
|
||||
protected TestApi Api => testApi;
|
||||
protected HttpClient Client => testApi.CreateClient();
|
||||
public HttpClient Client => testApi.CreateClient();
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await Api.EnsureDbCreatedAsync();
|
||||
await ExecuteAsync(static async (context, ct) =>
|
||||
{
|
||||
await context.Database.EnsureCreatedAsync(ct);
|
||||
}, TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
protected async Task ExecuteAsync(Func<DbContext, CancellationToken, Task> action, CancellationToken ct)
|
||||
{
|
||||
await using var scope = testApi.Services.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await action(context, ct);
|
||||
}
|
||||
|
||||
protected async Task ExecuteAsync(Func<DbContext, CancellationToken, IServiceProvider, Task> action, CancellationToken ct)
|
||||
{
|
||||
await using var scope = testApi.Services.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await action(context, ct, scope.ServiceProvider);
|
||||
}
|
||||
|
||||
protected async Task<T> ExecuteAsync<T>(Func<DbContext, CancellationToken, Task<T>> action, CancellationToken ct)
|
||||
{
|
||||
await using var scope = testApi.Services.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
return await action(context, ct);
|
||||
}
|
||||
|
||||
protected async Task<T> ExecuteAsync<T>(Func<DbContext, CancellationToken, IServiceProvider, Task<T>> action, CancellationToken ct)
|
||||
{
|
||||
await using var scope = testApi.Services.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
return await action(context, ct, scope.ServiceProvider);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await Api.EnsureDbDeletedAsync();
|
||||
await ExecuteAsync(static async (context, ct) =>
|
||||
{
|
||||
await context.Database.EnsureDeletedAsync(ct);
|
||||
}, TestContext.Current.CancellationToken);
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
using Aspire.Hosting;
|
||||
using Aspire.Hosting.Testing;
|
||||
|
||||
namespace FiscalOS.AppHost.Tests.Infra;
|
||||
|
||||
public sealed class AspireFixture : IAsyncLifetime
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
global using Aspire.Hosting;
|
||||
global using Aspire.Hosting.Testing;
|
||||
|
||||
global using FiscalOS.AppHost.Tests.Infra;
|
||||
|
||||
global using Microsoft.Playwright;
|
||||
global using Microsoft.Playwright.Xunit.v3;
|
||||
@@ -1,62 +0,0 @@
|
||||
namespace FiscalOS.Infra.Tests.Data;
|
||||
|
||||
internal sealed class AccountBuilder
|
||||
{
|
||||
private string _name = "accountName";
|
||||
private AccountMetadata? _metadata;
|
||||
private readonly List<Transaction> _transactions = [];
|
||||
|
||||
private AccountBuilder()
|
||||
{
|
||||
}
|
||||
|
||||
public static AccountBuilder Create()
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
public AccountBuilder WithName(string name)
|
||||
{
|
||||
_name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AccountBuilder WithMetadata(Action<AccountMetadataBuilder>? 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<TransactionBuilder>? 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user