Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b030651b3 | ||
|
|
d293c0d75a | ||
|
|
1c0fdc205e | ||
|
|
1a4f7fac8d | ||
|
|
870baf7b6c | ||
|
|
27d2ccbd45 | ||
|
|
66f4046d2e | ||
|
|
0f0ed561c3 | ||
|
|
74793b5800 | ||
|
|
562a708c74 | ||
|
|
19a4520a7b | ||
|
|
33f74ee018 | ||
|
|
2bc9cab43d | ||
|
|
b9d0869ae3 |
+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_accessors = true:silent
|
||||||
csharp_style_expression_bodied_constructors = false:silent
|
csharp_style_expression_bodied_constructors = false:silent
|
||||||
csharp_style_expression_bodied_indexers = true:silent
|
csharp_style_expression_bodied_indexers = true:silent
|
||||||
csharp_style_expression_bodied_lambdas = true:suggestion
|
csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion
|
||||||
csharp_style_expression_bodied_local_functions = false:silent
|
csharp_style_expression_bodied_local_functions = false:silent
|
||||||
csharp_style_expression_bodied_methods = false:silent
|
csharp_style_expression_bodied_methods = false:silent
|
||||||
csharp_style_expression_bodied_operators = false:silent
|
csharp_style_expression_bodied_operators = false:silent
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
##
|
##
|
||||||
## Get latest from `dotnet new gitignore`
|
## Get latest from `dotnet new gitignore`
|
||||||
|
|
||||||
|
# coverage
|
||||||
|
coverage/
|
||||||
|
|
||||||
# appsettings
|
# appsettings
|
||||||
appsettings*.json
|
appsettings*.json
|
||||||
!appsettings.Example.json
|
!appsettings.Example.json
|
||||||
|
|||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "aspire",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Aspire: Launch default apphost",
|
||||||
|
"program": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+6
-1
@@ -10,5 +10,10 @@
|
|||||||
"source.fixAll": "explicit"
|
"source.fixAll": "explicit"
|
||||||
},
|
},
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"cSpell.words": ["Encryptor"],
|
||||||
|
"python-envs.defaultEnvManager": "ms-python.python:system",
|
||||||
|
"search.exclude": {
|
||||||
|
"**/Migrations": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[*.cs]
|
||||||
|
|
||||||
|
dotnet_code_quality.ca1711.allowed_suffixes = Queue
|
||||||
|
dotnet_diagnostic.CA1031.severity = none
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageVersion Include="Aspire.Hosting.JavaScript" Version="13.1.1" />
|
<PackageVersion Include="Aspire.Hosting.JavaScript" Version="13.1.2" />
|
||||||
<PackageVersion Include="Going.Plaid" Version="6.56.0" />
|
<PackageVersion Include="Going.Plaid" Version="6.56.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Account = FiscalOS.Core.Accounts.Account;
|
||||||
|
|
||||||
namespace FiscalOS.API.Accounts.Add;
|
namespace FiscalOS.API.Accounts.Add;
|
||||||
|
|
||||||
internal static class Endpoint
|
internal static class Endpoint
|
||||||
@@ -13,15 +15,20 @@ internal static class Endpoint
|
|||||||
HttpContext httpContext,
|
HttpContext httpContext,
|
||||||
[FromBody] Request request,
|
[FromBody] Request request,
|
||||||
[FromServices] AppDbContext appDbContext,
|
[FromServices] AppDbContext appDbContext,
|
||||||
|
[FromServices] IEncryptor encryptor,
|
||||||
|
[FromServices] IPlaidAccountService plaidAccountService,
|
||||||
|
[FromServices] IAsyncQueue<SyncUpdatesQueueItem> queue,
|
||||||
CancellationToken ct
|
CancellationToken ct
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var userId = httpContext.GetUserId();
|
var userId = httpContext.GetUserId();
|
||||||
|
|
||||||
var user = await appDbContext.Users
|
var user = await appDbContext.Users
|
||||||
.Include(u => u.Institutions.Where(i => i.Metadata is PlaidMetadata && ((PlaidMetadata)i.Metadata).PlaidId == request.PlaidInstitutionId))
|
.Include(u => u.Institutions.Where(i => i.Metadata is PlaidInstitutionMetadata && ((PlaidInstitutionMetadata)i.Metadata).PlaidId == request.PlaidInstitutionId))
|
||||||
|
.ThenInclude(i => i.Metadata)
|
||||||
.Include(u => u.Accounts.Where(a => a.Metadata is PlaidAccountMetadata && ((PlaidAccountMetadata)a.Metadata).PlaidId == request.PlaidAccountId))
|
.Include(u => u.Accounts.Where(a => a.Metadata is PlaidAccountMetadata && ((PlaidAccountMetadata)a.Metadata).PlaidId == request.PlaidAccountId))
|
||||||
.ThenInclude(a => a.Metadata)
|
.ThenInclude(a => a.Metadata)
|
||||||
|
.AsSplitQuery()
|
||||||
.SingleOrDefaultAsync(u => u.Id == userId, ct);
|
.SingleOrDefaultAsync(u => u.Id == userId, ct);
|
||||||
|
|
||||||
if (user is null)
|
if (user is null)
|
||||||
@@ -37,19 +44,29 @@ 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())
|
if (user.Accounts.Any())
|
||||||
{
|
{
|
||||||
return Results.Conflict();
|
return Results.Conflict();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var decryptedAccessToken = await encryptor.DecryptAsyncFor(user, plaidInstitutionMetadata.EncryptedAccessToken, ct);
|
||||||
var accountMetadata = PlaidAccountMetadata.From(request.PlaidAccountId, request.PlaidAccountName);
|
var accountMetadata = PlaidAccountMetadata.From(request.PlaidAccountId, request.PlaidAccountName);
|
||||||
var account = Account.From(user.Institutions.First().Id, request.PlaidAccountName, accountMetadata);
|
var account = Account.From(request.PlaidAccountName, accountMetadata);
|
||||||
var balance = Balance.From(request.AccountCurrentBalance, request.AccountAvailableBalance, request.AccountCurrencyCode);
|
|
||||||
|
|
||||||
account.AddBalance(balance);
|
|
||||||
user.AddAccount(account);
|
user.AddAccount(account);
|
||||||
|
user.Institutions.First().AddAccount(account);
|
||||||
|
|
||||||
await appDbContext.SaveChangesAsync(ct);
|
await appDbContext.SaveChangesAsync(ct);
|
||||||
|
await queue.EnqueueAsync(new(plaidInstitutionMetadata.ItemId), ct);
|
||||||
|
|
||||||
return Results.Ok();
|
return Results.Ok();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ public record Request : IValidatableObject
|
|||||||
public string PlaidInstitutionId { get; init; } = string.Empty;
|
public string PlaidInstitutionId { get; init; } = string.Empty;
|
||||||
public string PlaidAccountId { get; init; } = string.Empty;
|
public string PlaidAccountId { get; init; } = string.Empty;
|
||||||
public string PlaidAccountName { 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)
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
{
|
{
|
||||||
@@ -28,11 +25,5 @@ public record Request : IValidatableObject
|
|||||||
var fieldName = nameof(PlaidAccountName);
|
var fieldName = nameof(PlaidAccountName);
|
||||||
yield return new($"The {fieldName} field is required.", [fieldName]);
|
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]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Institution = FiscalOS.Core.Accounts.Institution;
|
||||||
|
|
||||||
namespace FiscalOS.API.Institutions.Connect;
|
namespace FiscalOS.API.Institutions.Connect;
|
||||||
|
|
||||||
internal static class Endpoint
|
internal static class Endpoint
|
||||||
@@ -13,7 +15,7 @@ internal static class Endpoint
|
|||||||
HttpContext httpContext,
|
HttpContext httpContext,
|
||||||
[FromBody] Request request,
|
[FromBody] Request request,
|
||||||
[FromServices] AppDbContext appDbContext,
|
[FromServices] AppDbContext appDbContext,
|
||||||
[FromServices] PlaidService plaidService,
|
[FromServices] IPlaidAccountService plaidAccountService,
|
||||||
[FromServices] IEncryptor encryptor,
|
[FromServices] IEncryptor encryptor,
|
||||||
CancellationToken ct
|
CancellationToken ct
|
||||||
)
|
)
|
||||||
@@ -23,8 +25,8 @@ internal static class Endpoint
|
|||||||
var user = await appDbContext.Users
|
var user = await appDbContext.Users
|
||||||
.Include(u => u.Institutions
|
.Include(u => u.Institutions
|
||||||
.Where(
|
.Where(
|
||||||
i => i.Metadata is PlaidMetadata &&
|
i => i.Metadata is PlaidInstitutionMetadata &&
|
||||||
((PlaidMetadata)i.Metadata).PlaidId == request.PlaidInstitutionId
|
((PlaidInstitutionMetadata)i.Metadata).PlaidId == request.PlaidInstitutionId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.ThenInclude(i => i.Metadata)
|
.ThenInclude(i => i.Metadata)
|
||||||
@@ -40,12 +42,11 @@ internal static class Endpoint
|
|||||||
return Results.Conflict();
|
return Results.Conflict();
|
||||||
}
|
}
|
||||||
|
|
||||||
var (itemId, accessToken) = await plaidService.ExchangeTokenAsync(request.PublicToken);
|
var (itemId, accessToken) = await plaidAccountService.ExchangeTokenAsync(request.PublicToken);
|
||||||
var item = await plaidService.GetItemAsync(accessToken);
|
var item = await plaidAccountService.GetItemAsync(accessToken);
|
||||||
|
|
||||||
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, accessToken, ct);
|
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, accessToken, ct);
|
||||||
|
|
||||||
var plaidMetadata = PlaidMetadata.From(item.InstitutionId, item.InstitutionName, encryptedAccessToken);
|
var plaidMetadata = PlaidInstitutionMetadata.From(item.InstitutionId, item.InstitutionName, encryptedAccessToken, itemId);
|
||||||
var institution = Institution.From(item.InstitutionName, plaidMetadata);
|
var institution = Institution.From(item.InstitutionName, plaidMetadata);
|
||||||
|
|
||||||
user.AddInstitution(institution);
|
user.AddInstitution(institution);
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
using Account = FiscalOS.Core.Accounts.Account;
|
||||||
|
using Institution = FiscalOS.Core.Accounts.Institution;
|
||||||
|
|
||||||
namespace FiscalOS.API.Institutions.Get;
|
namespace FiscalOS.API.Institutions.Get;
|
||||||
|
|
||||||
internal sealed record Response
|
internal sealed record Response
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ internal static class Endpoint
|
|||||||
HttpContext httpContext,
|
HttpContext httpContext,
|
||||||
[FromRoute] Guid id,
|
[FromRoute] Guid id,
|
||||||
[FromServices] AppDbContext appDbContext,
|
[FromServices] AppDbContext appDbContext,
|
||||||
[FromServices] PlaidService plaidService,
|
[FromServices] IPlaidAccountService plaidAccountService,
|
||||||
[FromServices] IEncryptor encryptor,
|
[FromServices] IEncryptor encryptor,
|
||||||
CancellationToken ct
|
CancellationToken ct
|
||||||
)
|
)
|
||||||
@@ -37,13 +37,13 @@ internal static class Endpoint
|
|||||||
|
|
||||||
var institution = user.Institutions.First();
|
var institution = user.Institutions.First();
|
||||||
|
|
||||||
if (institution.Metadata is not PlaidMetadata plaidMetadata)
|
if (institution.Metadata is not PlaidInstitutionMetadata plaidMetadata)
|
||||||
{
|
{
|
||||||
return Results.BadRequest();
|
return Results.BadRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
var accessToken = await encryptor.DecryptAsyncFor(user, plaidMetadata.EncryptedAccessToken, ct);
|
var accessToken = await encryptor.DecryptAsyncFor(user, plaidMetadata.EncryptedAccessToken, ct);
|
||||||
var accounts = await plaidService.GetAccountsAsync(accessToken);
|
var accounts = await plaidAccountService.GetAccountsAsync(accessToken);
|
||||||
var accountsDtos = accounts.Select(a => AvailableAccountDto.From(plaidMetadata, a));
|
var accountsDtos = accounts.Select(a => AvailableAccountDto.From(plaidMetadata, a));
|
||||||
|
|
||||||
return Results.Ok(Response.From(accountsDtos));
|
return Results.Ok(Response.From(accountsDtos));
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ internal sealed record AvailableAccountDto
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static AvailableAccountDto From(
|
public static AvailableAccountDto From(
|
||||||
PlaidMetadata plaidMetadata,
|
PlaidInstitutionMetadata plaidMetadata,
|
||||||
Going.Plaid.Entity.Account plaidAccount
|
Going.Plaid.Entity.Account plaidAccount
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ internal static class Endpoint
|
|||||||
private static async Task<IResult> HandleAsync(
|
private static async Task<IResult> HandleAsync(
|
||||||
HttpContext httpContext,
|
HttpContext httpContext,
|
||||||
[FromServices] AppDbContext appDbContext,
|
[FromServices] AppDbContext appDbContext,
|
||||||
[FromServices] PlaidService plaidService
|
[FromServices] IPlaidAccountService plaidAccountService
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var userId = httpContext.GetUserId();
|
var userId = httpContext.GetUserId();
|
||||||
@@ -24,7 +24,7 @@ internal static class Endpoint
|
|||||||
return Results.Unauthorized();
|
return Results.Unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
var linkToken = await plaidService.CreateLinkTokenAsync(user.Id.ToString());
|
var linkToken = await plaidAccountService.CreateLinkTokenAsync(user.Id.ToString());
|
||||||
|
|
||||||
return Results.Ok(Response.From(linkToken));
|
return Results.Ok(Response.From(linkToken));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using FiscalOS.ServiceDefaults;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
builder.AddServiceDefaults();
|
builder.AddServiceDefaults();
|
||||||
@@ -7,6 +5,12 @@ builder.AddServiceDefaults();
|
|||||||
builder.Services.AddValidation();
|
builder.Services.AddValidation();
|
||||||
builder.Services.AddProblemDetails();
|
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.AddInfrastructure();
|
||||||
|
|
||||||
builder.Services.AddAuthentication(static o =>
|
builder.Services.AddAuthentication(static o =>
|
||||||
@@ -41,5 +45,6 @@ app.MapDefaultEndpoints();
|
|||||||
app.MapAuthEndpoints();
|
app.MapAuthEndpoints();
|
||||||
app.MapAccountsEndpoints();
|
app.MapAccountsEndpoints();
|
||||||
app.MapInstitutionsEndpoints();
|
app.MapInstitutionsEndpoints();
|
||||||
|
app.MapTransactionsGroup();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace FiscalOS.API.Transactions.FireWebhook;
|
||||||
|
|
||||||
|
public sealed record Request(
|
||||||
|
string AccountId,
|
||||||
|
string WebhookType,
|
||||||
|
string WebhookCode
|
||||||
|
);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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,7 +2,6 @@ global using System.ComponentModel.DataAnnotations;
|
|||||||
global using System.Security.Claims;
|
global using System.Security.Claims;
|
||||||
global using System.Text.Json.Serialization;
|
global using System.Text.Json.Serialization;
|
||||||
|
|
||||||
global using FiscalOS.API.Institutions.Get;
|
|
||||||
global using FiscalOS.API.Accounts;
|
global using FiscalOS.API.Accounts;
|
||||||
global using FiscalOS.API.Accounts.Add;
|
global using FiscalOS.API.Accounts.Add;
|
||||||
global using FiscalOS.API.Auth;
|
global using FiscalOS.API.Auth;
|
||||||
@@ -11,16 +10,27 @@ global using FiscalOS.API.Auth.Refresh;
|
|||||||
global using FiscalOS.API.Http;
|
global using FiscalOS.API.Http;
|
||||||
global using FiscalOS.API.Institutions;
|
global using FiscalOS.API.Institutions;
|
||||||
global using FiscalOS.API.Institutions.Connect;
|
global using FiscalOS.API.Institutions.Connect;
|
||||||
|
global using FiscalOS.API.Institutions.Get;
|
||||||
global using FiscalOS.API.Institutions.GetAvailable;
|
global using FiscalOS.API.Institutions.GetAvailable;
|
||||||
global using FiscalOS.API.Institutions.Link;
|
global using FiscalOS.API.Institutions.Link;
|
||||||
global using FiscalOS.Core.Accounts;
|
global using FiscalOS.API.Transactions;
|
||||||
|
global using FiscalOS.API.Transactions.FireWebhook;
|
||||||
|
global using FiscalOS.API.Transactions.Webhook;
|
||||||
global using FiscalOS.Core.Authentication;
|
global using FiscalOS.Core.Authentication;
|
||||||
global using FiscalOS.Core.Identity;
|
global using FiscalOS.Core.Identity;
|
||||||
|
global using FiscalOS.Core.Queuing;
|
||||||
global using FiscalOS.Core.Security;
|
global using FiscalOS.Core.Security;
|
||||||
global using FiscalOS.Infra.Accounts.Plaid;
|
global using FiscalOS.Infra.Accounts.Plaid;
|
||||||
global using FiscalOS.Infra.Authentication;
|
global using FiscalOS.Infra.Authentication;
|
||||||
global using FiscalOS.Infra.Data;
|
global using FiscalOS.Infra.Data;
|
||||||
global using FiscalOS.Infra.DependencyInjection;
|
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.AspNetCore.Mvc;
|
||||||
global using Microsoft.EntityFrameworkCore;
|
global using Microsoft.EntityFrameworkCore;
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
},
|
},
|
||||||
"PlaidClientOptions": {
|
"PlaidClientOptions": {
|
||||||
"ClientId": "ClientId",
|
"ClientId": "ClientId",
|
||||||
"Secret": "Secret"
|
"Secret": "Secret",
|
||||||
|
"Webhook": "Webhook"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,52 @@ internal sealed class App(
|
|||||||
var keyRingEntry = await keyRing.SaveKeyAsync(key);
|
var keyRingEntry = await keyRing.SaveKeyAsync(key);
|
||||||
_console.MarkupLine($"Key generated with id [green]{keyRingEntry.KeyId}[/] generated successfully.");
|
_console.MarkupLine($"Key generated with id [green]{keyRingEntry.KeyId}[/] generated successfully.");
|
||||||
break;
|
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:
|
case Commands.Exit:
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ internal static class Commands
|
|||||||
{
|
{
|
||||||
public const string CreateUser = "Create User";
|
public const string CreateUser = "Create User";
|
||||||
public const string GenerateKey = "Generate Key";
|
public const string GenerateKey = "Generate Key";
|
||||||
|
public const string RemoveInstitution = "Remove Institution";
|
||||||
public const string Exit = "Exit";
|
public const string Exit = "Exit";
|
||||||
|
|
||||||
public static readonly string[] All = [
|
public static readonly string[] All = [
|
||||||
CreateUser,
|
CreateUser,
|
||||||
GenerateKey,
|
GenerateKey,
|
||||||
|
RemoveInstitution,
|
||||||
Exit
|
Exit
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -2,9 +2,13 @@ global using FiscalOS.AdminCLI;
|
|||||||
global using FiscalOS.Core.Authentication;
|
global using FiscalOS.Core.Authentication;
|
||||||
global using FiscalOS.Core.Identity;
|
global using FiscalOS.Core.Identity;
|
||||||
global using FiscalOS.Core.Security;
|
global using FiscalOS.Core.Security;
|
||||||
|
global using FiscalOS.Infra.Accounts.Plaid;
|
||||||
global using FiscalOS.Infra.Data;
|
global using FiscalOS.Infra.Data;
|
||||||
global using FiscalOS.Infra.DependencyInjection;
|
global using FiscalOS.Infra.DependencyInjection;
|
||||||
|
|
||||||
|
global using Going.Plaid;
|
||||||
|
|
||||||
|
global using Microsoft.EntityFrameworkCore;
|
||||||
global using Microsoft.Extensions.Configuration;
|
global using Microsoft.Extensions.Configuration;
|
||||||
global using Microsoft.Extensions.DependencyInjection;
|
global using Microsoft.Extensions.DependencyInjection;
|
||||||
global using Microsoft.Extensions.Hosting;
|
global using Microsoft.Extensions.Hosting;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Aspire.AppHost.Sdk/13.1.1">
|
<Project Sdk="Aspire.AppHost.Sdk/13.1.2">
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\FiscalOS.API\FiscalOS.API.csproj" />
|
<ProjectReference Include="..\FiscalOS.API\FiscalOS.API.csproj" />
|
||||||
|
|||||||
@@ -11,11 +11,20 @@ public sealed class Account : Entity
|
|||||||
private readonly List<Balance> _balances = [];
|
private readonly List<Balance> _balances = [];
|
||||||
public IEnumerable<Balance> Balances => _balances;
|
public IEnumerable<Balance> Balances => _balances;
|
||||||
|
|
||||||
public static Account From(Guid institutionId, string name, AccountMetadata accountMetadata)
|
private readonly List<Transaction> _transactions = [];
|
||||||
|
public IEnumerable<Transaction> Transactions => _transactions;
|
||||||
|
|
||||||
|
private Account()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Account From(
|
||||||
|
string name,
|
||||||
|
AccountMetadata accountMetadata
|
||||||
|
)
|
||||||
{
|
{
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
InstitutionId = institutionId,
|
|
||||||
Name = name,
|
Name = name,
|
||||||
Metadata = accountMetadata,
|
Metadata = accountMetadata,
|
||||||
};
|
};
|
||||||
@@ -23,8 +32,15 @@ public sealed class Account : Entity
|
|||||||
|
|
||||||
public void AddBalance(Balance balance)
|
public void AddBalance(Balance balance)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(balance);
|
ArgumentNullException.ThrowIfNull(balance, nameof(balance));
|
||||||
|
|
||||||
_balances.Add(balance);
|
_balances.Add(balance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AddTransaction(Transaction transaction)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(transaction, nameof(transaction));
|
||||||
|
|
||||||
|
_transactions.Add(transaction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,8 @@ public sealed class Balance : Entity
|
|||||||
string currencyCode
|
string currencyCode
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(currencyCode, nameof(currencyCode));
|
||||||
|
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
Current = current,
|
Current = current,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ namespace FiscalOS.Core.Accounts;
|
|||||||
public sealed class Institution : Entity
|
public sealed class Institution : Entity
|
||||||
{
|
{
|
||||||
public Guid UserId { get; init; }
|
public Guid UserId { get; init; }
|
||||||
|
public User? User { get; init; }
|
||||||
public string Name { get; init; } = string.Empty;
|
public string Name { get; init; } = string.Empty;
|
||||||
public InstitutionMetadata? Metadata { get; init; }
|
public InstitutionMetadata? Metadata { get; init; }
|
||||||
|
|
||||||
@@ -24,4 +25,11 @@ public sealed class Institution : Entity
|
|||||||
Metadata = metadata
|
Metadata = metadata
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AddAccount(Account account)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(account, nameof(account));
|
||||||
|
|
||||||
|
_accounts.Add(account);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,9 @@ public sealed class User : Entity
|
|||||||
private readonly List<Account> _accounts = [];
|
private readonly List<Account> _accounts = [];
|
||||||
public IEnumerable<Account> Accounts => _accounts;
|
public IEnumerable<Account> Accounts => _accounts;
|
||||||
|
|
||||||
|
private readonly List<Transaction> _transactions = [];
|
||||||
|
public IEnumerable<Transaction> Transactions => _transactions;
|
||||||
|
|
||||||
private User()
|
private User()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -60,4 +63,11 @@ public sealed class User : Entity
|
|||||||
|
|
||||||
_accounts.Add(account);
|
_accounts.Add(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AddTransaction(Transaction transaction)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(transaction, nameof(transaction));
|
||||||
|
|
||||||
|
_transactions.Add(transaction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace FiscalOS.Core.Queuing;
|
||||||
|
|
||||||
|
public interface IAsyncQueue<T>
|
||||||
|
{
|
||||||
|
Task EnqueueAsync(T item, CancellationToken cancellationToken = default);
|
||||||
|
Task<T> DequeueAsync(CancellationToken cancellationToken = default);
|
||||||
|
ValueTask DisposeAsync();
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace FiscalOS.Core.Queuing;
|
||||||
|
|
||||||
|
public interface IAsyncQueueProcessor<T>
|
||||||
|
{
|
||||||
|
Task ProcessAsync(T item, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,3 +2,4 @@ global using FiscalOS.Core.Accounts;
|
|||||||
global using FiscalOS.Core.Data;
|
global using FiscalOS.Core.Data;
|
||||||
global using FiscalOS.Core.Identity;
|
global using FiscalOS.Core.Identity;
|
||||||
global using FiscalOS.Core.Security;
|
global using FiscalOS.Core.Security;
|
||||||
|
global using FiscalOS.Core.Transactions;
|
||||||
@@ -5,12 +5,16 @@ public sealed class PlaidAccountMetadata : AccountMetadata
|
|||||||
public const string TypeValue = Providers.Plaid;
|
public const string TypeValue = Providers.Plaid;
|
||||||
public string PlaidId { get; init; } = string.Empty;
|
public string PlaidId { get; init; } = string.Empty;
|
||||||
public string PlaidName { get; init; } = string.Empty;
|
public string PlaidName { get; init; } = string.Empty;
|
||||||
|
public string? Cursor { get; private set; }
|
||||||
|
|
||||||
private PlaidAccountMetadata() : base(TypeValue)
|
private PlaidAccountMetadata() : base(TypeValue)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PlaidAccountMetadata From(string plaidId, string plaidName)
|
public static PlaidAccountMetadata From(
|
||||||
|
string plaidId,
|
||||||
|
string plaidName
|
||||||
|
)
|
||||||
{
|
{
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
@@ -18,4 +22,11 @@ public sealed class PlaidAccountMetadata : AccountMetadata
|
|||||||
PlaidName = plaidName,
|
PlaidName = plaidName,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetCursor(string cursor)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(cursor, nameof(cursor));
|
||||||
|
|
||||||
|
Cursor = cursor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+16
-8
@@ -1,22 +1,29 @@
|
|||||||
|
|
||||||
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace FiscalOS.Infra.Accounts.Plaid;
|
namespace FiscalOS.Infra.Accounts.Plaid;
|
||||||
|
|
||||||
public sealed class PlaidService
|
public interface IPlaidAccountService
|
||||||
{
|
{
|
||||||
|
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 readonly PlaidClient _client;
|
||||||
|
|
||||||
private PlaidService(PlaidClient client)
|
private PlaidAccountService(PlaidClient client, IOptions<PlaidClientOptions> options)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
|
_options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PlaidService From(IServiceProvider sp)
|
public static PlaidAccountService From(IServiceProvider sp)
|
||||||
{
|
{
|
||||||
var client = sp.GetRequiredService<PlaidClient>();
|
var client = sp.GetRequiredService<PlaidClient>();
|
||||||
return new(client);
|
var options = sp.GetRequiredService<IOptions<PlaidClientOptions>>();
|
||||||
|
return new(client, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> CreateLinkTokenAsync(string id)
|
public async Task<string> CreateLinkTokenAsync(string id)
|
||||||
@@ -34,6 +41,7 @@ public sealed class PlaidService
|
|||||||
{
|
{
|
||||||
ClientUserId = id,
|
ClientUserId = id,
|
||||||
},
|
},
|
||||||
|
Webhook = _options.Value.Webhook,
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
if (ltr.IsSuccessStatusCode is false)
|
if (ltr.IsSuccessStatusCode is false)
|
||||||
@@ -4,6 +4,7 @@ public sealed record PlaidClientOptions
|
|||||||
{
|
{
|
||||||
public string ClientId { get; init; } = string.Empty;
|
public string ClientId { get; init; } = string.Empty;
|
||||||
public string Secret { get; init; } = string.Empty;
|
public string Secret { get; init; } = string.Empty;
|
||||||
|
public string Webhook { get; init; } = string.Empty;
|
||||||
|
|
||||||
public IOptions<PlaidOptions> ToPlaidOptions()
|
public IOptions<PlaidOptions> ToPlaidOptions()
|
||||||
{
|
{
|
||||||
|
|||||||
+10
-6
@@ -1,31 +1,35 @@
|
|||||||
namespace FiscalOS.Infra.Accounts.Plaid;
|
namespace FiscalOS.Infra.Accounts.Plaid;
|
||||||
|
|
||||||
public sealed class PlaidMetadata : InstitutionMetadata
|
public sealed class PlaidInstitutionMetadata : InstitutionMetadata
|
||||||
{
|
{
|
||||||
public const string TypeValue = Providers.Plaid;
|
public const string TypeValue = Providers.Plaid;
|
||||||
public string PlaidId { get; init; } = string.Empty;
|
public string PlaidId { get; init; } = string.Empty;
|
||||||
public string PlaidName { get; init; } = string.Empty;
|
public string PlaidName { get; init; } = string.Empty;
|
||||||
public string EncryptedAccessToken { get; init; } = string.Empty;
|
public string EncryptedAccessToken { get; init; } = string.Empty;
|
||||||
|
public string ItemId { get; init; } = string.Empty;
|
||||||
|
|
||||||
private PlaidMetadata() : base(TypeValue)
|
private PlaidInstitutionMetadata() : base(TypeValue)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PlaidMetadata From(
|
public static PlaidInstitutionMetadata From(
|
||||||
string? plaidId,
|
string? plaidId,
|
||||||
string? plaidName,
|
string? plaidName,
|
||||||
string encryptedAccessToken
|
string encryptedAccessToken,
|
||||||
|
string itemId
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(plaidId, nameof(plaidId));
|
ArgumentNullException.ThrowIfNull(plaidId, nameof(plaidId));
|
||||||
ArgumentNullException.ThrowIfNull(plaidName, nameof(plaidName));
|
ArgumentNullException.ThrowIfNull(plaidName, nameof(plaidName));
|
||||||
ArgumentNullException.ThrowIfNull(encryptedAccessToken, nameof(encryptedAccessToken));
|
ArgumentNullException.ThrowIfNull(encryptedAccessToken, nameof(encryptedAccessToken));
|
||||||
|
ArgumentNullException.ThrowIfNull(itemId, nameof(itemId));
|
||||||
|
|
||||||
return new PlaidMetadata
|
return new()
|
||||||
{
|
{
|
||||||
PlaidId = plaidId,
|
PlaidId = plaidId,
|
||||||
PlaidName = plaidName,
|
PlaidName = plaidName,
|
||||||
EncryptedAccessToken = encryptedAccessToken
|
EncryptedAccessToken = encryptedAccessToken,
|
||||||
|
ItemId = itemId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace FiscalOS.Infra.Accounts.Plaid;
|
namespace FiscalOS.Infra.Common;
|
||||||
|
|
||||||
public class PlaidException : Exception
|
public class PlaidException : Exception
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace FiscalOS.Infra.Accounts;
|
namespace FiscalOS.Infra.Common;
|
||||||
|
|
||||||
public static class Providers
|
public static class Providers
|
||||||
{
|
{
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Account = FiscalOS.Core.Accounts.Account;
|
using Account = FiscalOS.Core.Accounts.Account;
|
||||||
using Institution = FiscalOS.Core.Accounts.Institution;
|
using Institution = FiscalOS.Core.Accounts.Institution;
|
||||||
|
using Transaction = FiscalOS.Core.Transactions.Transaction;
|
||||||
|
|
||||||
namespace FiscalOS.Infra.Data;
|
namespace FiscalOS.Infra.Data;
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ public sealed class AppDbContext(
|
|||||||
|
|
||||||
public DbSet<User> Users => Set<User>();
|
public DbSet<User> Users => Set<User>();
|
||||||
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
|
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
|
||||||
|
public DbSet<Institution> Institutions => Set<Institution>();
|
||||||
|
public DbSet<Transaction> Transactions => Set<Transaction>();
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
@@ -63,7 +66,7 @@ public sealed class AppDbContext(
|
|||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
eb.HasMany(static u => u.Institutions)
|
eb.HasMany(static u => u.Institutions)
|
||||||
.WithOne()
|
.WithOne(static i => i.User)
|
||||||
.HasForeignKey(static i => i.UserId)
|
.HasForeignKey(static i => i.UserId)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
@@ -96,19 +99,20 @@ public sealed class AppDbContext(
|
|||||||
modelBuilder.Entity<InstitutionMetadata>(static eb =>
|
modelBuilder.Entity<InstitutionMetadata>(static eb =>
|
||||||
{
|
{
|
||||||
eb.HasDiscriminator(static m => m.Type)
|
eb.HasDiscriminator(static m => m.Type)
|
||||||
.HasValue<PlaidMetadata>(PlaidMetadata.TypeValue);
|
.HasValue<PlaidInstitutionMetadata>(PlaidInstitutionMetadata.TypeValue);
|
||||||
|
|
||||||
eb.Property(static m => m.InstitutionId);
|
eb.Property(static m => m.InstitutionId);
|
||||||
eb.Property(static m => m.Type);
|
eb.Property(static m => m.Type);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<PlaidMetadata>(static eb =>
|
modelBuilder.Entity<PlaidInstitutionMetadata>(static eb =>
|
||||||
{
|
{
|
||||||
eb.HasBaseType<InstitutionMetadata>();
|
eb.HasBaseType<InstitutionMetadata>();
|
||||||
|
|
||||||
eb.Property(static m => m.PlaidId);
|
eb.Property(static m => m.PlaidId);
|
||||||
eb.Property(static m => m.PlaidName);
|
eb.Property(static m => m.PlaidName);
|
||||||
eb.Property(static m => m.EncryptedAccessToken);
|
eb.Property(static m => m.EncryptedAccessToken);
|
||||||
|
eb.Property(static m => m.ItemId);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<Account>(static eb =>
|
modelBuilder.Entity<Account>(static eb =>
|
||||||
@@ -146,6 +150,7 @@ public sealed class AppDbContext(
|
|||||||
|
|
||||||
eb.Property(static m => m.PlaidId);
|
eb.Property(static m => m.PlaidId);
|
||||||
eb.Property(static m => m.PlaidName);
|
eb.Property(static m => m.PlaidName);
|
||||||
|
eb.Property(static m => m.Cursor);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<Balance>(static eb =>
|
modelBuilder.Entity<Balance>(static eb =>
|
||||||
@@ -155,5 +160,40 @@ public sealed class AppDbContext(
|
|||||||
eb.Property(static b => b.Available);
|
eb.Property(static b => b.Available);
|
||||||
eb.Property(static b => b.CurrencyCode);
|
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,7 +33,18 @@ public static class ServiceCollectionExtensions
|
|||||||
var options = clientOptions.Value.ToPlaidOptions();
|
var options = clientOptions.Value.ToPlaidOptions();
|
||||||
return new PlaidClient(options, factory, logger);
|
return new PlaidClient(options, factory, logger);
|
||||||
});
|
});
|
||||||
services.AddSingleton(PlaidService.From);
|
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);
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
|||||||
+374
@@ -0,0 +1,374 @@
|
|||||||
|
// <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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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
@@ -0,0 +1,376 @@
|
|||||||
|
// <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
@@ -0,0 +1,126 @@
|
|||||||
|
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
@@ -0,0 +1,376 @@
|
|||||||
|
// <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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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
@@ -0,0 +1,504 @@
|
|||||||
|
// <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
@@ -0,0 +1,104 @@
|
|||||||
|
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
@@ -0,0 +1,508 @@
|
|||||||
|
// <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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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
@@ -0,0 +1,507 @@
|
|||||||
|
// <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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
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
@@ -0,0 +1,518 @@
|
|||||||
|
// <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
@@ -0,0 +1,48 @@
|
|||||||
|
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
@@ -0,0 +1,500 @@
|
|||||||
|
// <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
@@ -0,0 +1,78 @@
|
|||||||
|
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.HasIndex("UserId");
|
||||||
|
|
||||||
b.ToTable("Institution");
|
b.ToTable("Institutions");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
@@ -241,10 +241,86 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
b.ToTable("Users");
|
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 =>
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||||
{
|
{
|
||||||
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("Cursor")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("PlaidId")
|
b.Property<string>("PlaidId")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
@@ -256,7 +332,7 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
b.HasDiscriminator().HasValue("Plaid");
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidMetadata", b =>
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidInstitutionMetadata", b =>
|
||||||
{
|
{
|
||||||
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||||
|
|
||||||
@@ -264,6 +340,10 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ItemId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("PlaidId")
|
b.Property<string>("PlaidId")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
@@ -275,10 +355,21 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
b.HasDiscriminator().HasValue("Plaid");
|
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 =>
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||||
.WithMany()
|
.WithMany("Accounts")
|
||||||
.HasForeignKey("InstitutionId")
|
.HasForeignKey("InstitutionId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
@@ -312,11 +403,13 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("FiscalOS.Core.Identity.User", null)
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
.WithMany("Institutions")
|
.WithMany("Institutions")
|
||||||
.HasForeignKey("UserId")
|
.HasForeignKey("UserId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
@@ -339,15 +432,47 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
b.Navigation("User");
|
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 =>
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Balances");
|
b.Navigation("Balances");
|
||||||
|
|
||||||
b.Navigation("Metadata");
|
b.Navigation("Metadata");
|
||||||
|
|
||||||
|
b.Navigation("Transactions");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
{
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
b.Navigation("Metadata");
|
b.Navigation("Metadata");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -358,6 +483,13 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
b.Navigation("Institutions");
|
b.Navigation("Institutions");
|
||||||
|
|
||||||
b.Navigation("RefreshTokens");
|
b.Navigation("RefreshTokens");
|
||||||
|
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Metadata");
|
||||||
});
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||||
|
|
||||||
|
public sealed record SyncUpdatesQueueItem(string InstitutionItemId);
|
||||||
@@ -1,23 +1,31 @@
|
|||||||
global using System.IdentityModel.Tokens.Jwt;
|
global using System.IdentityModel.Tokens.Jwt;
|
||||||
global using System.IO.Abstractions;
|
global using System.IO.Abstractions;
|
||||||
|
global using System.Reflection;
|
||||||
global using System.Security.Claims;
|
global using System.Security.Claims;
|
||||||
global using System.Security.Cryptography;
|
global using System.Security.Cryptography;
|
||||||
global using System.Text;
|
global using System.Text;
|
||||||
global using System.Text.Json;
|
global using System.Text.Json;
|
||||||
|
global using System.Threading.Channels;
|
||||||
|
|
||||||
global using FiscalOS.Core.Accounts;
|
global using FiscalOS.Core.Accounts;
|
||||||
global using FiscalOS.Core.Authentication;
|
global using FiscalOS.Core.Authentication;
|
||||||
global using FiscalOS.Core.Data;
|
global using FiscalOS.Core.Data;
|
||||||
global using FiscalOS.Core.Identity;
|
global using FiscalOS.Core.Identity;
|
||||||
|
global using FiscalOS.Core.Queuing;
|
||||||
global using FiscalOS.Core.Security;
|
global using FiscalOS.Core.Security;
|
||||||
|
global using FiscalOS.Core.Transactions;
|
||||||
global using FiscalOS.Infra.Accounts.Plaid;
|
global using FiscalOS.Infra.Accounts.Plaid;
|
||||||
global using FiscalOS.Infra.Authentication;
|
global using FiscalOS.Infra.Authentication;
|
||||||
global using FiscalOS.Infra.Authorization;
|
global using FiscalOS.Infra.Authorization;
|
||||||
|
global using FiscalOS.Infra.Common;
|
||||||
global using FiscalOS.Infra.Data;
|
global using FiscalOS.Infra.Data;
|
||||||
|
global using FiscalOS.Infra.Queuing;
|
||||||
global using FiscalOS.Infra.Security;
|
global using FiscalOS.Infra.Security;
|
||||||
|
global using FiscalOS.Infra.Transactions.Plaid;
|
||||||
|
|
||||||
global using Going.Plaid;
|
global using Going.Plaid;
|
||||||
global using Going.Plaid.Entity;
|
global using Going.Plaid.Entity;
|
||||||
|
global using Going.Plaid.Transactions;
|
||||||
|
|
||||||
global using Microsoft.AspNetCore.Authentication.JwtBearer;
|
global using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
global using Microsoft.AspNetCore.Authorization;
|
global using Microsoft.AspNetCore.Authorization;
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public static class ServiceDefaultsExtensions
|
|||||||
|
|
||||||
public static WebApplication MapDefaultEndpoints(this WebApplication app)
|
public static WebApplication MapDefaultEndpoints(this WebApplication app)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(app);
|
ArgumentNullException.ThrowIfNull(app, nameof(app));
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ internal sealed class HttpRequestBuilder
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HttpRequestBuilder New() => new();
|
public static HttpRequestBuilder New()
|
||||||
|
{
|
||||||
|
return new();
|
||||||
|
}
|
||||||
|
|
||||||
public HttpRequestBuilder WithMethod(HttpMethod method)
|
public HttpRequestBuilder WithMethod(HttpMethod method)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ namespace FiscalOS.API.Tests.Infra;
|
|||||||
|
|
||||||
public class TestApi : WebApplicationFactory<Program>
|
public class TestApi : WebApplicationFactory<Program>
|
||||||
{
|
{
|
||||||
|
private readonly List<Action<IWebHostBuilder>> _additionalConfigs = [];
|
||||||
|
|
||||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||||
{
|
{
|
||||||
base.ConfigureWebHost(builder);
|
base.ConfigureWebHost(builder);
|
||||||
@@ -25,5 +27,71 @@ public class TestApi : WebApplicationFactory<Program>
|
|||||||
|
|
||||||
c.AddSingleton<IKeyRing>(TestKeyRing.From);
|
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,3 +1,6 @@
|
|||||||
|
using FiscalOS.Core.Queuing;
|
||||||
|
using FiscalOS.Infra.Transactions.Plaid;
|
||||||
|
|
||||||
using Account = FiscalOS.Core.Accounts.Account;
|
using Account = FiscalOS.Core.Accounts.Account;
|
||||||
using Institution = FiscalOS.Core.Accounts.Institution;
|
using Institution = FiscalOS.Core.Accounts.Institution;
|
||||||
|
|
||||||
@@ -31,7 +34,6 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
["PlaidInstitutionId"] = ["The PlaidInstitutionId field is required."],
|
["PlaidInstitutionId"] = ["The PlaidInstitutionId field is required."],
|
||||||
["PlaidAccountId"] = ["The PlaidAccountId field is required."],
|
["PlaidAccountId"] = ["The PlaidAccountId field is required."],
|
||||||
["PlaidAccountName"] = ["The PlaidAccountName field is required."],
|
["PlaidAccountName"] = ["The PlaidAccountName field is required."],
|
||||||
["AccountCurrencyCode"] = ["The AccountCurrencyCode field is required."],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +126,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Add_WhenCalledWithPlaidInstitutionIdThatHasNotBeenAdded_ItShouldReturn400WithProblemDetails()
|
public async Task Add_WhenCalledWithPlaidInstitutionIdThatHasNotBeenAdded_ItShouldReturn400WithProblemDetails()
|
||||||
{
|
{
|
||||||
var user = await ExecuteAsync(static async (context, ct, sp) =>
|
var user = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||||
{
|
{
|
||||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||||
@@ -161,7 +163,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Add_WhenCalledWithPlaidAccountIdThatHasAlreadyBeenAdded_ItShouldReturn409WithProblemDetails()
|
public async Task Add_WhenCalledWithPlaidAccountIdThatHasAlreadyBeenAdded_ItShouldReturn409WithProblemDetails()
|
||||||
{
|
{
|
||||||
var (user, institution, account) = await ExecuteAsync(static async (context, ct, sp) =>
|
var (user, institution, account) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||||
{
|
{
|
||||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||||
@@ -169,20 +171,25 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(ct);
|
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(ct);
|
||||||
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
||||||
|
|
||||||
await context.AddAsync(user, ct);
|
|
||||||
|
|
||||||
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, "accessToken", ct);
|
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, "accessToken", ct);
|
||||||
var plaidMetadata = PlaidMetadata.From("alreadyExists", "Some Bank", encryptedAccessToken);
|
var plaidMetadata = PlaidInstitutionMetadata.From(
|
||||||
|
"alreadyExists",
|
||||||
|
"Some Bank",
|
||||||
|
encryptedAccessToken,
|
||||||
|
"itemId"
|
||||||
|
);
|
||||||
var institution = Institution.From("Some Bank", plaidMetadata);
|
var institution = Institution.From("Some Bank", plaidMetadata);
|
||||||
|
|
||||||
await context.AddAsync(institution, ct);
|
await context.AddAsync(institution, ct);
|
||||||
|
|
||||||
var plaidAccountMetadata = PlaidAccountMetadata.From("accountId", "Some Account");
|
var plaidAccountMetadata = PlaidAccountMetadata.From("accountId", "Some Account");
|
||||||
var account = Account.From(institution.Id, "Some Account", plaidAccountMetadata);
|
var account = Account.From("Some Account", plaidAccountMetadata);
|
||||||
|
|
||||||
|
institution.AddAccount(account);
|
||||||
user.AddInstitution(institution);
|
user.AddInstitution(institution);
|
||||||
user.AddAccount(account);
|
user.AddAccount(account);
|
||||||
|
|
||||||
|
await context.AddAsync(user, ct);
|
||||||
await context.SaveChangesAsync(ct);
|
await context.SaveChangesAsync(ct);
|
||||||
return (user, institution, account);
|
return (user, institution, account);
|
||||||
}, TestContext.Current.CancellationToken);
|
}, TestContext.Current.CancellationToken);
|
||||||
@@ -192,7 +199,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
.WithUserId(user.Id)
|
.WithUserId(user.Id)
|
||||||
.WithBody(new
|
.WithBody(new
|
||||||
{
|
{
|
||||||
plaidInstitutionId = ((PlaidMetadata)institution.Metadata!).PlaidId,
|
plaidInstitutionId = ((PlaidInstitutionMetadata)institution.Metadata!).PlaidId,
|
||||||
plaidAccountId = ((PlaidAccountMetadata)account.Metadata!).PlaidId,
|
plaidAccountId = ((PlaidAccountMetadata)account.Metadata!).PlaidId,
|
||||||
plaidAccountName = ((PlaidAccountMetadata)account.Metadata).PlaidName,
|
plaidAccountName = ((PlaidAccountMetadata)account.Metadata).PlaidName,
|
||||||
accountCurrencyCode = "USD",
|
accountCurrencyCode = "USD",
|
||||||
@@ -207,7 +214,18 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Add_WhenCalledWithNewAccount_ItShouldReturn200()
|
public async Task Add_WhenCalledWithNewAccount_ItShouldReturn200()
|
||||||
{
|
{
|
||||||
var (user, institution) = await ExecuteAsync(async (context, ct, sp) =>
|
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 passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||||
@@ -217,7 +235,12 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
||||||
|
|
||||||
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, "accessToken", ct);
|
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, "accessToken", ct);
|
||||||
var plaidMetadata = PlaidMetadata.From("id", "Some Bank", encryptedAccessToken);
|
var plaidMetadata = PlaidInstitutionMetadata.From(
|
||||||
|
"id",
|
||||||
|
"Some Bank",
|
||||||
|
encryptedAccessToken,
|
||||||
|
"itemId"
|
||||||
|
);
|
||||||
var institution = Institution.From("Some Bank", plaidMetadata);
|
var institution = Institution.From("Some Bank", plaidMetadata);
|
||||||
|
|
||||||
user.AddInstitution(institution);
|
user.AddInstitution(institution);
|
||||||
@@ -237,7 +260,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
.WithUserId(user.Id)
|
.WithUserId(user.Id)
|
||||||
.WithBody(new
|
.WithBody(new
|
||||||
{
|
{
|
||||||
plaidInstitutionId = ((PlaidMetadata)institution.Metadata!).PlaidId,
|
plaidInstitutionId = ((PlaidInstitutionMetadata)institution.Metadata!).PlaidId,
|
||||||
plaidAccountId = newAccountId,
|
plaidAccountId = newAccountId,
|
||||||
plaidAccountName = newAccountName,
|
plaidAccountName = newAccountName,
|
||||||
accountCurrentBalance = expectedBalance,
|
accountCurrentBalance = expectedBalance,
|
||||||
@@ -246,11 +269,13 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
})
|
})
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
var client = testApi.CreateClient();
|
||||||
|
|
||||||
|
var response = await client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||||
|
|
||||||
var updatedUser = await ExecuteAsync(
|
var updatedUser = await testApi.ExecuteAsync(
|
||||||
async (context, ct) => await context.Set<User>()
|
async (context, ct) => await context.Set<User>()
|
||||||
.Include(u => u.Accounts)
|
.Include(u => u.Accounts)
|
||||||
.ThenInclude(a => a.Metadata)
|
.ThenInclude(a => a.Metadata)
|
||||||
@@ -269,12 +294,9 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
((PlaidAccountMetadata)a.Metadata).PlaidName == newAccountName
|
((PlaidAccountMetadata)a.Metadata).PlaidName == newAccountName
|
||||||
);
|
);
|
||||||
|
|
||||||
updatedUser.Accounts.First()
|
mockQueue.Verify(m => m.EnqueueAsync(
|
||||||
.Balances.Should().ContainSingle(
|
It.IsAny<SyncUpdatesQueueItem>(),
|
||||||
b => b.AccountId == updatedUser.Accounts.First().Id &&
|
It.IsAny<CancellationToken>()
|
||||||
b.Current == expectedBalance &&
|
), Times.Once());
|
||||||
b.Available == expectedBalance &&
|
|
||||||
b.CurrencyCode == expectedCurrencyCode
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ public class LoginTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Login_WhenUserExistsButPasswordIsIncorrect_ItShouldReturn401WithProblemDetails()
|
public async Task Login_WhenUserExistsButPasswordIsIncorrect_ItShouldReturn401WithProblemDetails()
|
||||||
{
|
{
|
||||||
await ExecuteAsync(static async (context, ct, sp) =>
|
await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||||
{
|
{
|
||||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||||
@@ -70,7 +70,7 @@ public class LoginTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Login_WhenUserExistsAndPasswordIsCorrect_ItShouldReturn200WithJwtTokenAndSetRefreshCookie()
|
public async Task Login_WhenUserExistsAndPasswordIsCorrect_ItShouldReturn200WithJwtTokenAndSetRefreshCookie()
|
||||||
{
|
{
|
||||||
await ExecuteAsync(static async (context, ct, sp) =>
|
await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||||
{
|
{
|
||||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Refresh_WhenCalledWithTokenBelongingToDifferentUser_ItShouldReturn403WithProblemDetails()
|
public async Task Refresh_WhenCalledWithTokenBelongingToDifferentUser_ItShouldReturn403WithProblemDetails()
|
||||||
{
|
{
|
||||||
var (users, refreshToken) = await ExecuteAsync(static async (context, ct, sp) =>
|
var (users, refreshToken) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||||
{
|
{
|
||||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||||
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
||||||
@@ -61,7 +61,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
|
|
||||||
await response.Should().BeProblemDetails(HttpStatusCode.Forbidden);
|
await response.Should().BeProblemDetails(HttpStatusCode.Forbidden);
|
||||||
|
|
||||||
var unrevokedTokensCountForUser2 = await ExecuteAsync(
|
var unrevokedTokensCountForUser2 = await Api.ExecuteAsync(
|
||||||
async (context, ct) => await context.Set<RefreshToken>()
|
async (context, ct) => await context.Set<RefreshToken>()
|
||||||
.Include(t => t.User)
|
.Include(t => t.User)
|
||||||
.Where(t => t.UserId == users[1].Id && t.Revoked == false)
|
.Where(t => t.UserId == users[1].Id && t.Revoked == false)
|
||||||
@@ -75,7 +75,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Refresh_WhenCalledWithRevokedRefreshToken_ItShouldReturn400WithProblemDetails()
|
public async Task Refresh_WhenCalledWithRevokedRefreshToken_ItShouldReturn400WithProblemDetails()
|
||||||
{
|
{
|
||||||
var (user, refreshToken) = await ExecuteAsync(static async (context, ct, sp) =>
|
var (user, refreshToken) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||||
{
|
{
|
||||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||||
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
||||||
@@ -108,7 +108,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Refresh_WhenCalledWithExpiredRefreshToken_ItShouldReturn400WithProblemDetails()
|
public async Task Refresh_WhenCalledWithExpiredRefreshToken_ItShouldReturn400WithProblemDetails()
|
||||||
{
|
{
|
||||||
var (user, refreshToken) = await ExecuteAsync(static async (context, ct, sp) =>
|
var (user, refreshToken) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||||
{
|
{
|
||||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||||
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
||||||
@@ -142,7 +142,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
[InlineData(-5)]
|
[InlineData(-5)]
|
||||||
public async Task Refresh_WhenCalledWithValidRefreshTokenAndExpiredOrNotExpiredAccessToken_ItShouldReturn200WithNewTokensAndSetRefreshCookie(int accessTokenExpiresAtOffset)
|
public async Task Refresh_WhenCalledWithValidRefreshTokenAndExpiredOrNotExpiredAccessToken_ItShouldReturn200WithNewTokensAndSetRefreshCookie(int accessTokenExpiresAtOffset)
|
||||||
{
|
{
|
||||||
var (user, refreshToken) = await ExecuteAsync(static async (context, ct, sp) =>
|
var (user, refreshToken) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||||
{
|
{
|
||||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||||
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
||||||
@@ -174,7 +174,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
response.Should().HaveSetCookieHeader("fiscalos_refresh_cookie");
|
response.Should().HaveSetCookieHeader("fiscalos_refresh_cookie");
|
||||||
await response.Should().BeJsonContentOfType<API.Auth.Refresh.Response>(HttpStatusCode.OK);
|
await response.Should().BeJsonContentOfType<API.Auth.Refresh.Response>(HttpStatusCode.OK);
|
||||||
|
|
||||||
var oldRefreshTokenInDb = await ExecuteAsync(
|
var oldRefreshTokenInDb = await Api.ExecuteAsync(
|
||||||
async (context, ct) => await context.Set<RefreshToken>()
|
async (context, ct) => await context.Set<RefreshToken>()
|
||||||
.Include(t => t.User)
|
.Include(t => t.User)
|
||||||
.Where(t => t.UserId == user.Id && t.Token == refreshToken.Token && t.Revoked == true)
|
.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();
|
oldRefreshTokenInDb.Should().NotBeNull();
|
||||||
|
|
||||||
var newRefreshTokenInDb = await ExecuteAsync(
|
var newRefreshTokenInDb = await Api.ExecuteAsync(
|
||||||
async (context, ct) => await context.Set<RefreshToken>()
|
async (context, ct) => await context.Set<RefreshToken>()
|
||||||
.Include(t => t.User)
|
.Include(t => t.User)
|
||||||
.Where(t => t.UserId == user.Id && t.Revoked == false && t.Token != refreshToken.Token)
|
.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]
|
[Fact]
|
||||||
public async Task Connect_WhenCalledWithPlaidInstitutionIdThatIsAlreadyConnected_ItShouldReturn409WithProblemDetails()
|
public async Task Connect_WhenCalledWithPlaidInstitutionIdThatIsAlreadyConnected_ItShouldReturn409WithProblemDetails()
|
||||||
{
|
{
|
||||||
var (user, institution) = await ExecuteAsync(static async (context, ct, sp) =>
|
var (user, institution) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||||
{
|
{
|
||||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||||
@@ -101,7 +101,12 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
||||||
|
|
||||||
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, "accessToken", ct);
|
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, "accessToken", ct);
|
||||||
var plaidMetadata = PlaidMetadata.From("alreadyExists", "Some Bank", encryptedAccessToken);
|
var plaidMetadata = PlaidInstitutionMetadata.From(
|
||||||
|
"alreadyExists",
|
||||||
|
"Some Bank",
|
||||||
|
encryptedAccessToken,
|
||||||
|
"itemId"
|
||||||
|
);
|
||||||
var institution = Institution.From("Some Bank", plaidMetadata);
|
var institution = Institution.From("Some Bank", plaidMetadata);
|
||||||
|
|
||||||
user.AddInstitution(institution);
|
user.AddInstitution(institution);
|
||||||
@@ -117,7 +122,7 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
.WithBody(new
|
.WithBody(new
|
||||||
{
|
{
|
||||||
publicToken = "token",
|
publicToken = "token",
|
||||||
plaidInstitutionId = ((PlaidMetadata)institution.Metadata!).PlaidId,
|
plaidInstitutionId = ((PlaidInstitutionMetadata)institution.Metadata!).PlaidId,
|
||||||
})
|
})
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
@@ -131,7 +136,7 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
{
|
{
|
||||||
var plaidInstitutionId = "ins_109508";
|
var plaidInstitutionId = "ins_109508";
|
||||||
|
|
||||||
var (user, publicToken) = await ExecuteAsync(async (context, ct, sp) =>
|
var (user, publicToken) = await Api.ExecuteAsync(async (context, ct, sp) =>
|
||||||
{
|
{
|
||||||
var plaidClient = sp.GetRequiredService<PlaidClient>();
|
var plaidClient = sp.GetRequiredService<PlaidClient>();
|
||||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||||
@@ -166,7 +171,7 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||||
|
|
||||||
var updatedUser = await ExecuteAsync(
|
var updatedUser = await Api.ExecuteAsync(
|
||||||
async (context, ct) => await context.Set<User>()
|
async (context, ct) => await context.Set<User>()
|
||||||
.Include(u => u.Institutions)
|
.Include(u => u.Institutions)
|
||||||
.ThenInclude(i => i.Metadata)
|
.ThenInclude(i => i.Metadata)
|
||||||
@@ -179,7 +184,7 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
var institution = updatedUser.Institutions.First();
|
var institution = updatedUser.Institutions.First();
|
||||||
institution.Name.Should().NotBeNullOrEmpty();
|
institution.Name.Should().NotBeNullOrEmpty();
|
||||||
|
|
||||||
var metadata = institution.Metadata.As<PlaidMetadata>();
|
var metadata = institution.Metadata.As<PlaidInstitutionMetadata>();
|
||||||
metadata.PlaidId.Should().Be(plaidInstitutionId);
|
metadata.PlaidId.Should().Be(plaidInstitutionId);
|
||||||
metadata.PlaidName.Should().Be(institution.Name);
|
metadata.PlaidName.Should().Be(institution.Name);
|
||||||
metadata.EncryptedAccessToken.Should().NotBeNullOrEmpty();
|
metadata.EncryptedAccessToken.Should().NotBeNullOrEmpty();
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ public class GetAvailableTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetAvailable_WhenCalledWithNonExistentInstitutionId_ItShouldReturn404WithProblemDetails()
|
public async Task GetAvailable_WhenCalledWithNonExistentInstitutionId_ItShouldReturn404WithProblemDetails()
|
||||||
{
|
{
|
||||||
var user = await ExecuteAsync(static async (context, ct, sp) =>
|
var user = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||||
{
|
{
|
||||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||||
@@ -65,7 +65,7 @@ public class GetAvailableTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetAvailable_WhenCalledWithConnectedInstitution_ItShouldReturn200WithAvailableAccounts()
|
public async Task GetAvailable_WhenCalledWithConnectedInstitution_ItShouldReturn200WithAvailableAccounts()
|
||||||
{
|
{
|
||||||
var (user, institution, expectedAccounts) = await ExecuteAsync(static async (context, ct, sp) =>
|
var (user, institution, expectedAccounts) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||||
{
|
{
|
||||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||||
@@ -92,7 +92,12 @@ public class GetAvailableTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
AccessToken = exchangeTokenResponse.AccessToken,
|
AccessToken = exchangeTokenResponse.AccessToken,
|
||||||
});
|
});
|
||||||
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, exchangeTokenResponse.AccessToken, ct);
|
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, exchangeTokenResponse.AccessToken, ct);
|
||||||
var institutionMetadata = PlaidMetadata.From(institutionId, "Some Bank", encryptedAccessToken);
|
var institutionMetadata = PlaidInstitutionMetadata.From(
|
||||||
|
institutionId,
|
||||||
|
"Some Bank",
|
||||||
|
encryptedAccessToken,
|
||||||
|
exchangeTokenResponse.ItemId
|
||||||
|
);
|
||||||
var institution = Institution.From("Some Bank", institutionMetadata);
|
var institution = Institution.From("Some Bank", institutionMetadata);
|
||||||
|
|
||||||
user.AddInstitution(institution);
|
user.AddInstitution(institution);
|
||||||
@@ -110,7 +115,8 @@ public class GetAvailableTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
|
|
||||||
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
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))
|
(await response.Should().BeJsonContentOfType<Response>(HttpStatusCode.OK))
|
||||||
.Which.Accounts.Should().BeEquivalentTo(expectedAccounts.Select(AvailableAccountDto.FromPlaidAccount));
|
.Which.Accounts.Should().BeEquivalentTo(expectedResponseAccounts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ public class GetTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Get_WhenCalledByUser_ItShouldReturn200WithListOfInstitutions()
|
public async Task Get_WhenCalledByUser_ItShouldReturn200WithListOfInstitutions()
|
||||||
{
|
{
|
||||||
var (user, institution) = await ExecuteAsync(static async (context, ct, sp) =>
|
var (user, institution) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||||
{
|
{
|
||||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||||
@@ -45,7 +45,12 @@ public class GetTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
||||||
|
|
||||||
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, "accessToken", ct);
|
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, "accessToken", ct);
|
||||||
var plaidMetadata = PlaidMetadata.From("alreadyExists", "Some Bank", encryptedAccessToken);
|
var plaidMetadata = PlaidInstitutionMetadata.From(
|
||||||
|
"alreadyExists",
|
||||||
|
"Some Bank",
|
||||||
|
encryptedAccessToken,
|
||||||
|
"itemId"
|
||||||
|
);
|
||||||
var institution = Institution.From("Some Bank", plaidMetadata);
|
var institution = Institution.From("Some Bank", plaidMetadata);
|
||||||
|
|
||||||
user.AddInstitution(institution);
|
user.AddInstitution(institution);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public class LinkTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Link_WhenCalledWithUserWhoExists_ItShouldReturnLinkToken()
|
public async Task Link_WhenCalledWithUserWhoExists_ItShouldReturnLinkToken()
|
||||||
{
|
{
|
||||||
var user = await ExecuteAsync(static async (context, ct, sp) =>
|
var user = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
||||||
{
|
{
|
||||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||||
|
|||||||
@@ -2,51 +2,17 @@ namespace FiscalOS.API.Tests.Integration;
|
|||||||
|
|
||||||
public abstract class IntegrationTest(TestApi testApi) : IClassFixture<TestApi>, IAsyncLifetime
|
public abstract class IntegrationTest(TestApi testApi) : IClassFixture<TestApi>, IAsyncLifetime
|
||||||
{
|
{
|
||||||
public HttpClient Client => testApi.CreateClient();
|
protected TestApi Api => testApi;
|
||||||
|
protected HttpClient Client => testApi.CreateClient();
|
||||||
|
|
||||||
public async ValueTask InitializeAsync()
|
public async ValueTask InitializeAsync()
|
||||||
{
|
{
|
||||||
await ExecuteAsync(static async (context, ct) =>
|
await Api.EnsureDbCreatedAsync();
|
||||||
{
|
|
||||||
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()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
await ExecuteAsync(static async (context, ct) =>
|
await Api.EnsureDbDeletedAsync();
|
||||||
{
|
|
||||||
await context.Database.EnsureDeletedAsync(ct);
|
|
||||||
}, TestContext.Current.CancellationToken);
|
|
||||||
|
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
using Aspire.Hosting;
|
|
||||||
using Aspire.Hosting.Testing;
|
|
||||||
|
|
||||||
namespace FiscalOS.AppHost.Tests.Infra;
|
namespace FiscalOS.AppHost.Tests.Infra;
|
||||||
|
|
||||||
public sealed class AspireFixture : IAsyncLifetime
|
public sealed class AspireFixture : IAsyncLifetime
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
global using Aspire.Hosting;
|
||||||
|
global using Aspire.Hosting.Testing;
|
||||||
|
|
||||||
global using FiscalOS.AppHost.Tests.Infra;
|
global using FiscalOS.AppHost.Tests.Infra;
|
||||||
|
|
||||||
global using Microsoft.Playwright;
|
global using Microsoft.Playwright;
|
||||||
global using Microsoft.Playwright.Xunit.v3;
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
namespace FiscalOS.Infra.Tests.Data;
|
||||||
|
|
||||||
|
internal sealed class InstitutionBuilder
|
||||||
|
{
|
||||||
|
private string _name = "institutionName";
|
||||||
|
private InstitutionMetadata? _metadata;
|
||||||
|
private readonly List<Account> _accounts = [];
|
||||||
|
|
||||||
|
private InstitutionBuilder()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InstitutionBuilder Create()
|
||||||
|
{
|
||||||
|
return new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public InstitutionBuilder WithName(string name)
|
||||||
|
{
|
||||||
|
_name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InstitutionBuilder WithMetadata(Action<InstitutionMetadataBuilder>? action = null)
|
||||||
|
{
|
||||||
|
var imb = InstitutionMetadataBuilder.Create();
|
||||||
|
action?.Invoke(imb);
|
||||||
|
_metadata = imb.Build();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InstitutionBuilder WithAccount(Action<AccountBuilder>? action = null)
|
||||||
|
{
|
||||||
|
var ab = AccountBuilder.Create();
|
||||||
|
action?.Invoke(ab);
|
||||||
|
_accounts.Add(ab.Build());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Institution Build()
|
||||||
|
{
|
||||||
|
if (_metadata is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"You must call {nameof(WithMetadata)} prior to building");
|
||||||
|
}
|
||||||
|
|
||||||
|
var institution = Institution.From(
|
||||||
|
_name,
|
||||||
|
_metadata
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (var account in _accounts)
|
||||||
|
{
|
||||||
|
institution.AddAccount(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
return institution;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
namespace FiscalOS.Infra.Tests.Data;
|
||||||
|
|
||||||
|
internal sealed class InstitutionMetadataBuilder
|
||||||
|
{
|
||||||
|
private string _plaidId = "plaidId";
|
||||||
|
private string _plaidName = "plaidName";
|
||||||
|
private string _accessToken = "encryptedAccessToken";
|
||||||
|
private string _itemId = "itemId";
|
||||||
|
|
||||||
|
private InstitutionMetadataBuilder()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InstitutionMetadataBuilder Create()
|
||||||
|
{
|
||||||
|
return new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public InstitutionMetadataBuilder WithPlaidId(string plaidId)
|
||||||
|
{
|
||||||
|
_plaidId = plaidId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InstitutionMetadataBuilder WithPlaidName(string plaidName)
|
||||||
|
{
|
||||||
|
_plaidName = plaidName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InstitutionMetadataBuilder WithAccessToken(string accessToken)
|
||||||
|
{
|
||||||
|
_accessToken = accessToken;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InstitutionMetadataBuilder WithItemId(string itemId)
|
||||||
|
{
|
||||||
|
_itemId = itemId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlaidInstitutionMetadata Build()
|
||||||
|
{
|
||||||
|
return PlaidInstitutionMetadata.From(
|
||||||
|
_plaidId,
|
||||||
|
_plaidName,
|
||||||
|
_accessToken,
|
||||||
|
_itemId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
namespace FiscalOS.Infra.Tests.Data;
|
||||||
|
|
||||||
|
internal sealed class TransactionBuilder
|
||||||
|
{
|
||||||
|
private string _merchantName = "merchantName";
|
||||||
|
private string _description = "description";
|
||||||
|
private decimal _amount = 100;
|
||||||
|
private DateTimeOffset _date = DateTimeOffset.UtcNow;
|
||||||
|
private TransactionMetadata? _metadata;
|
||||||
|
|
||||||
|
private TransactionBuilder()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TransactionBuilder Create()
|
||||||
|
{
|
||||||
|
return new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransactionBuilder WithMerchantName(string merchantName)
|
||||||
|
{
|
||||||
|
_merchantName = merchantName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransactionBuilder WithDescription(string description)
|
||||||
|
{
|
||||||
|
_description = description;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransactionBuilder WithAmount(decimal amount)
|
||||||
|
{
|
||||||
|
_amount = amount;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransactionBuilder WithDate(DateTimeOffset date)
|
||||||
|
{
|
||||||
|
_date = date;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransactionBuilder WithMetadata(Action<TransactionMetadataBuilder>? action = null)
|
||||||
|
{
|
||||||
|
var tmb = TransactionMetadataBuilder.Create();
|
||||||
|
action?.Invoke(tmb);
|
||||||
|
_metadata = tmb.Build();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Transaction Build()
|
||||||
|
{
|
||||||
|
if (_metadata is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"You must call {nameof(WithMetadata)} prior to building");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Transaction.From(
|
||||||
|
_merchantName,
|
||||||
|
_description,
|
||||||
|
_amount,
|
||||||
|
_date,
|
||||||
|
_metadata
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
namespace FiscalOS.Infra.Tests.Data;
|
||||||
|
|
||||||
|
internal sealed class TransactionMetadataBuilder
|
||||||
|
{
|
||||||
|
private string _plaidId = "plaidId";
|
||||||
|
|
||||||
|
private TransactionMetadataBuilder()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TransactionMetadataBuilder Create()
|
||||||
|
{
|
||||||
|
return new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransactionMetadataBuilder WithPlaidId(string plaidId)
|
||||||
|
{
|
||||||
|
_plaidId = plaidId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlaidTransactionMetadata Build()
|
||||||
|
{
|
||||||
|
return PlaidTransactionMetadata.From(_plaidId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
namespace FiscalOS.Infra.Tests.Data;
|
||||||
|
|
||||||
|
internal sealed class UserBuilder
|
||||||
|
{
|
||||||
|
private string _username = "username";
|
||||||
|
private string _password = "hashedPassword";
|
||||||
|
private EncryptedDataKey _dataKey = EncryptedDataKey.From(
|
||||||
|
"keyUsed",
|
||||||
|
"encryptedKey"
|
||||||
|
);
|
||||||
|
private readonly List<Institution> _instituions = [];
|
||||||
|
|
||||||
|
private UserBuilder()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UserBuilder Create()
|
||||||
|
{
|
||||||
|
return new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserBuilder WithUsername(string username)
|
||||||
|
{
|
||||||
|
_username = username;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserBuilder WithPassword(string password)
|
||||||
|
{
|
||||||
|
_password = password;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserBuilder WithDataKey(EncryptedDataKey key)
|
||||||
|
{
|
||||||
|
_dataKey = key;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserBuilder WithInstitution(Action<InstitutionBuilder>? action = null)
|
||||||
|
{
|
||||||
|
var ib = InstitutionBuilder.Create();
|
||||||
|
action?.Invoke(ib);
|
||||||
|
_instituions.Add(ib.Build());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User Build()
|
||||||
|
{
|
||||||
|
var user = User.From(
|
||||||
|
_username,
|
||||||
|
_password,
|
||||||
|
_dataKey
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (var institution in _instituions)
|
||||||
|
{
|
||||||
|
user.AddInstitution(institution);
|
||||||
|
|
||||||
|
foreach (var account in institution.Accounts)
|
||||||
|
{
|
||||||
|
user.AddAccount(account);
|
||||||
|
|
||||||
|
foreach (var transaction in account.Transactions)
|
||||||
|
{
|
||||||
|
user.AddTransaction(transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using FiscalOS.Infra.Tests.Data;
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Tests.Integration;
|
||||||
|
|
||||||
|
public abstract class IntegrationTest : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private bool _isDisposed;
|
||||||
|
private readonly FileSystem _fileSystem = new();
|
||||||
|
protected AppDbContext AppDbContext { get; }
|
||||||
|
|
||||||
|
protected IntegrationTest()
|
||||||
|
{
|
||||||
|
var options = Options.Create(new AppDbContextOptions()
|
||||||
|
{
|
||||||
|
DatabaseFilePath = $"{Guid.NewGuid()}.db",
|
||||||
|
});
|
||||||
|
|
||||||
|
AppDbContext = new(options, _fileSystem);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task<User> CreateTestUserAsync()
|
||||||
|
{
|
||||||
|
var user = UserBuilder.Create()
|
||||||
|
.WithInstitution(static ib =>
|
||||||
|
{
|
||||||
|
ib.WithMetadata();
|
||||||
|
ib.WithAccount(static ab =>
|
||||||
|
{
|
||||||
|
ab.WithMetadata();
|
||||||
|
ab.WithTransaction(static tb =>
|
||||||
|
{
|
||||||
|
tb.WithMetadata();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
await AppDbContext.AddAsync(user, TestContext.Current.CancellationToken);
|
||||||
|
await AppDbContext.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask InitializeAsync()
|
||||||
|
{
|
||||||
|
await AppDbContext.Database.MigrateAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await DisposeAsync(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual async Task DisposeAsync(bool disposing)
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
await AppDbContext.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
namespace FiscalOS.Infra.Tests.Integration;
|
||||||
|
|
||||||
|
public class PlaidAddedTransactionHandlerTests : IntegrationTest
|
||||||
|
{
|
||||||
|
private readonly Mock<ILogger<PlaidAddedTransactionHandler>> _mockLogger = new();
|
||||||
|
private readonly PlaidAddedTransactionHandler _sut;
|
||||||
|
|
||||||
|
public PlaidAddedTransactionHandlerTests()
|
||||||
|
{
|
||||||
|
_sut = PlaidAddedTransactionHandler.From(_mockLogger.Object, AppDbContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleAsync_WhenCalledWithNewTransaction_ItShouldAddTheTransaction()
|
||||||
|
{
|
||||||
|
var user = await CreateTestUserAsync();
|
||||||
|
var account = user.Accounts.First();
|
||||||
|
|
||||||
|
var transaction = new Going.Plaid.Entity.Transaction()
|
||||||
|
{
|
||||||
|
TransactionId = Guid.NewGuid().ToString(),
|
||||||
|
MerchantName = "Test Merchant",
|
||||||
|
OriginalDescription = "Test transaction",
|
||||||
|
Amount = 100,
|
||||||
|
Datetime = DateTimeOffset.UtcNow,
|
||||||
|
Pending = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.HandleAsync(account, [transaction], TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
result.Should().Be(1);
|
||||||
|
account.Transactions.Should().HaveCount(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleAsync_WhenCalledWithPendingTransaction_ItShouldSkipTheTransaction()
|
||||||
|
{
|
||||||
|
var user = await CreateTestUserAsync();
|
||||||
|
var account = user.Accounts.First();
|
||||||
|
|
||||||
|
var transaction = new Going.Plaid.Entity.Transaction()
|
||||||
|
{
|
||||||
|
TransactionId = Guid.NewGuid().ToString(),
|
||||||
|
MerchantName = "Test Merchant",
|
||||||
|
OriginalDescription = "Test transaction",
|
||||||
|
Amount = 100,
|
||||||
|
Datetime = DateTimeOffset.UtcNow,
|
||||||
|
Pending = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.HandleAsync(account, [transaction], TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
result.Should().Be(1);
|
||||||
|
account.Transactions.Should().HaveCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleAsync_WhenCalledWithExistingTransaction_ItShouldNotAddTheTransactionToTheDatabase()
|
||||||
|
{
|
||||||
|
var user = await CreateTestUserAsync();
|
||||||
|
var account = user.Accounts.First();
|
||||||
|
var existingTransaction = user.Transactions.First();
|
||||||
|
|
||||||
|
var transaction = new Going.Plaid.Entity.Transaction()
|
||||||
|
{
|
||||||
|
TransactionId = ((PlaidTransactionMetadata)existingTransaction.Metadata!).PlaidId,
|
||||||
|
MerchantName = "Test Merchant",
|
||||||
|
OriginalDescription = "Test transaction",
|
||||||
|
Amount = 100,
|
||||||
|
Datetime = DateTimeOffset.UtcNow,
|
||||||
|
Pending = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.HandleAsync(account, [transaction], TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
result.Should().Be(1);
|
||||||
|
account.Transactions.Should().HaveCount(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
namespace FiscalOS.Infra.Tests.Integration;
|
||||||
|
|
||||||
|
public class PlaidModifiedTransactionHandlerTests : IntegrationTest
|
||||||
|
{
|
||||||
|
private readonly Mock<ILogger<PlaidModifiedTransactionHandler>> _mockLogger = new();
|
||||||
|
private readonly PlaidModifiedTransactionHandler _sut;
|
||||||
|
|
||||||
|
public PlaidModifiedTransactionHandlerTests()
|
||||||
|
{
|
||||||
|
_sut = PlaidModifiedTransactionHandler.From(_mockLogger.Object, AppDbContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleAsync_WhenCalledWithExistingTransaction_ItShouldUpdateTheTransaction()
|
||||||
|
{
|
||||||
|
var user = await CreateTestUserAsync();
|
||||||
|
var account = user.Accounts.First();
|
||||||
|
var existingTransaction = account.Transactions.First();
|
||||||
|
|
||||||
|
var transaction = new Going.Plaid.Entity.Transaction()
|
||||||
|
{
|
||||||
|
TransactionId = ((PlaidTransactionMetadata)existingTransaction.Metadata!).PlaidId,
|
||||||
|
MerchantName = Guid.NewGuid().ToString(),
|
||||||
|
OriginalDescription = "Test transaction",
|
||||||
|
Amount = 100,
|
||||||
|
Datetime = DateTimeOffset.UtcNow,
|
||||||
|
Pending = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.HandleAsync(account, [transaction], TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
result.Should().Be(1);
|
||||||
|
existingTransaction.MerchantName.Should().Be(transaction.MerchantName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
namespace FiscalOS.Infra.Tests.Integration;
|
||||||
|
|
||||||
|
public class PlaidRemovedTransactionHandlerTests : IntegrationTest
|
||||||
|
{
|
||||||
|
private readonly Mock<ILogger<PlaidRemovedTransactionHandler>> _mockLogger = new();
|
||||||
|
private readonly PlaidRemovedTransactionHandler _sut;
|
||||||
|
|
||||||
|
public PlaidRemovedTransactionHandlerTests()
|
||||||
|
{
|
||||||
|
_sut = PlaidRemovedTransactionHandler.From(AppDbContext, _mockLogger.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleAsync_WhenCalled_ItShouldRemoveTheCorrectTransactions()
|
||||||
|
{
|
||||||
|
var user = await CreateTestUserAsync();
|
||||||
|
var testTransaction = user.Transactions.First();
|
||||||
|
|
||||||
|
var transactionToRemove = new Going.Plaid.Entity.RemovedTransaction()
|
||||||
|
{
|
||||||
|
TransactionId = ((PlaidTransactionMetadata)testTransaction.Metadata!).PlaidId,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.HandleAsync([transactionToRemove], TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
result.Should().Be(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
namespace FiscalOS.Infra.Tests.Unit;
|
||||||
|
|
||||||
|
public class PlaidTransactionProcessorTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IPlaidAddedTransactionHandler> _mockAddedHandler = new();
|
||||||
|
private readonly Mock<IPlaidModifiedTransactionHandler> _mockModifiedHandler = new();
|
||||||
|
private readonly Mock<IPlaidRemovedTransactionHandler> _mockRemovedHandler = new();
|
||||||
|
private readonly PlaidTransactionProcessor _sut;
|
||||||
|
|
||||||
|
public PlaidTransactionProcessorTests()
|
||||||
|
{
|
||||||
|
_sut = PlaidTransactionProcessor.From(
|
||||||
|
_mockAddedHandler.Object,
|
||||||
|
_mockModifiedHandler.Object,
|
||||||
|
_mockRemovedHandler.Object
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0, 0, 0, false)]
|
||||||
|
[InlineData(1, 1, 1, true)]
|
||||||
|
[InlineData(0, 1, 1, false)]
|
||||||
|
[InlineData(1, 0, 1, false)]
|
||||||
|
[InlineData(1, 1, 0, false)]
|
||||||
|
public async Task ProcessAsync_WhenCalledAndTransactionsAreHandled_ItShouldReturnCorrectResult(
|
||||||
|
int numAdded,
|
||||||
|
int numModified,
|
||||||
|
int numRemoved,
|
||||||
|
bool expectedResult
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_mockAddedHandler
|
||||||
|
.Setup(
|
||||||
|
static m => m.HandleAsync(
|
||||||
|
It.IsAny<Account>(),
|
||||||
|
It.IsAny<IEnumerable<Going.Plaid.Entity.Transaction>>(),
|
||||||
|
It.IsAny<CancellationToken>()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.ReturnsAsync(numAdded);
|
||||||
|
|
||||||
|
_mockModifiedHandler
|
||||||
|
.Setup(
|
||||||
|
static m => m.HandleAsync(
|
||||||
|
It.IsAny<Account>(),
|
||||||
|
It.IsAny<IEnumerable<Going.Plaid.Entity.Transaction>>(),
|
||||||
|
It.IsAny<CancellationToken>()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.ReturnsAsync(numModified);
|
||||||
|
|
||||||
|
_mockRemovedHandler
|
||||||
|
.Setup(
|
||||||
|
static m => m.HandleAsync(
|
||||||
|
It.IsAny<IEnumerable<Going.Plaid.Entity.RemovedTransaction>>(),
|
||||||
|
It.IsAny<CancellationToken>()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.ReturnsAsync(numRemoved);
|
||||||
|
|
||||||
|
var metadata = PlaidAccountMetadata.From("plaidId", "plaidName");
|
||||||
|
var account = Account.From("name", metadata);
|
||||||
|
var syncResponse = new TransactionsSyncResponse()
|
||||||
|
{
|
||||||
|
Added = [new()],
|
||||||
|
Modified = [new()],
|
||||||
|
Removed = [new()],
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.ProcessAsync(account, syncResponse, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
result.Should().Be(expectedResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
using FiscalOS.Infra.Tests.Data;
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Tests.Unit;
|
||||||
|
|
||||||
|
public class PlaidTransactionSyncerTests
|
||||||
|
{
|
||||||
|
private readonly Mock<ILogger<PlaidTransactionSyncer>> _mockLogger = new();
|
||||||
|
private readonly Mock<IPlaidTransactionService> _mockPlaidTransactionService = new();
|
||||||
|
private readonly Mock<IPlaidTransactionProcessor> _mockPlaidTransactionProcessor = new();
|
||||||
|
private readonly PlaidTransactionSyncer _sut;
|
||||||
|
|
||||||
|
public PlaidTransactionSyncerTests()
|
||||||
|
{
|
||||||
|
_sut = PlaidTransactionSyncer.From(
|
||||||
|
_mockLogger.Object,
|
||||||
|
_mockPlaidTransactionService.Object,
|
||||||
|
_mockPlaidTransactionProcessor.Object
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SyncTransactionsForAccountAsync_WhenAccountHasNoPlaidMetadata_ItShouldNotCallService()
|
||||||
|
{
|
||||||
|
var account = AccountBuilder.Create()
|
||||||
|
.WithName("no-plaid")
|
||||||
|
.WithMetadata(new DummyMetadata())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
await _sut.SyncTransactionsForAccountAsync(account, "token", CancellationToken.None);
|
||||||
|
|
||||||
|
_mockPlaidTransactionService.Verify(
|
||||||
|
s => s.SyncTransactionsForAccountAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string?>()),
|
||||||
|
Times.Never()
|
||||||
|
);
|
||||||
|
|
||||||
|
_mockPlaidTransactionProcessor.Verify(
|
||||||
|
p => p.ProcessAsync(It.IsAny<Account>(), It.IsAny<TransactionsSyncResponse>(), It.IsAny<CancellationToken>()),
|
||||||
|
Times.Never()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SyncTransactionsForAccountAsync_WhenResponseIsUnsuccessful_ItShouldStopProcessing()
|
||||||
|
{
|
||||||
|
var account = AccountBuilder.Create()
|
||||||
|
.WithName("acct")
|
||||||
|
.WithMetadata(amb =>
|
||||||
|
{
|
||||||
|
amb.WithPlaidId("plaid-1")
|
||||||
|
.WithPlaidName("Plaid One");
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var response = new TransactionsSyncResponse()
|
||||||
|
{
|
||||||
|
RequestId = "req-1",
|
||||||
|
Error = new() { ErrorMessage = "boom" }
|
||||||
|
};
|
||||||
|
|
||||||
|
SetStatusCode(response, HttpStatusCode.InternalServerError);
|
||||||
|
|
||||||
|
_mockPlaidTransactionService
|
||||||
|
.Setup(s => s.SyncTransactionsForAccountAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string?>()))
|
||||||
|
.Returns(AsyncResponses(response));
|
||||||
|
|
||||||
|
await _sut.SyncTransactionsForAccountAsync(account, "token", CancellationToken.None);
|
||||||
|
|
||||||
|
_mockPlaidTransactionProcessor.Verify(
|
||||||
|
p => p.ProcessAsync(It.IsAny<Account>(), It.IsAny<TransactionsSyncResponse>(), It.IsAny<CancellationToken>()),
|
||||||
|
Times.Never
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SyncTransactionsForAccountAsync_WhenResponseHasNoAccounts_ItShouldStopProcessing()
|
||||||
|
{
|
||||||
|
var account = AccountBuilder.Create()
|
||||||
|
.WithName("acct")
|
||||||
|
.WithMetadata(amb =>
|
||||||
|
{
|
||||||
|
amb.WithPlaidId("plaid-2")
|
||||||
|
.WithPlaidName("Plaid Two");
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var response = new TransactionsSyncResponse()
|
||||||
|
{
|
||||||
|
RequestId = "req-2",
|
||||||
|
Accounts = []
|
||||||
|
};
|
||||||
|
SetStatusCode(response, HttpStatusCode.OK);
|
||||||
|
|
||||||
|
_mockPlaidTransactionService
|
||||||
|
.Setup(s => s.SyncTransactionsForAccountAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string?>()))
|
||||||
|
.Returns(AsyncResponses(response));
|
||||||
|
|
||||||
|
await _sut.SyncTransactionsForAccountAsync(account, "token", CancellationToken.None);
|
||||||
|
|
||||||
|
_mockPlaidTransactionProcessor.Verify(
|
||||||
|
p => p.ProcessAsync(It.IsAny<Account>(), It.IsAny<TransactionsSyncResponse>(), It.IsAny<CancellationToken>()),
|
||||||
|
Times.Never
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SyncTransactionsForAccountAsync_WhenResponseMissingMatchingAccount_ItShouldStopProcessing()
|
||||||
|
{
|
||||||
|
var account = AccountBuilder.Create()
|
||||||
|
.WithName("acct")
|
||||||
|
.WithMetadata(amb =>
|
||||||
|
{
|
||||||
|
amb.WithPlaidId("plaid-3")
|
||||||
|
.WithPlaidName("Plaid Three");
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var response = new TransactionsSyncResponse()
|
||||||
|
{
|
||||||
|
RequestId = "req-3",
|
||||||
|
Accounts = [new() { AccountId = "other" }]
|
||||||
|
};
|
||||||
|
SetStatusCode(response, HttpStatusCode.OK);
|
||||||
|
|
||||||
|
_mockPlaidTransactionService
|
||||||
|
.Setup(s => s.SyncTransactionsForAccountAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string?>()))
|
||||||
|
.Returns(AsyncResponses(response));
|
||||||
|
|
||||||
|
await _sut.SyncTransactionsForAccountAsync(account, "token", CancellationToken.None);
|
||||||
|
|
||||||
|
_mockPlaidTransactionProcessor.Verify(
|
||||||
|
p => p.ProcessAsync(It.IsAny<Account>(), It.IsAny<TransactionsSyncResponse>(), It.IsAny<CancellationToken>()),
|
||||||
|
Times.Never
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SyncTransactionsForAccountAsync_WhenProcessorReturnsFalse_ItShouldLeaveCursorUnchangedButAddsBalance()
|
||||||
|
{
|
||||||
|
var account = AccountBuilder.Create()
|
||||||
|
.WithName("acct")
|
||||||
|
.WithMetadata(amb =>
|
||||||
|
{
|
||||||
|
amb.WithPlaidId("plaid-4")
|
||||||
|
.WithPlaidName("Plaid Four");
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var response = new TransactionsSyncResponse()
|
||||||
|
{
|
||||||
|
RequestId = "req-4",
|
||||||
|
Accounts = [
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
AccountId = "plaid-4",
|
||||||
|
Balances = new() { Current = 123.45m, Available = 100.00m, IsoCurrencyCode = "USD" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
NextCursor = "next-1"
|
||||||
|
};
|
||||||
|
SetStatusCode(response, HttpStatusCode.OK);
|
||||||
|
|
||||||
|
_mockPlaidTransactionService
|
||||||
|
.Setup(s => s.SyncTransactionsForAccountAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string?>()))
|
||||||
|
.Returns(AsyncResponses(response));
|
||||||
|
|
||||||
|
_mockPlaidTransactionProcessor
|
||||||
|
.Setup(p => p.ProcessAsync(It.IsAny<Account>(), It.IsAny<TransactionsSyncResponse>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(false);
|
||||||
|
|
||||||
|
await _sut.SyncTransactionsForAccountAsync(account, "token", CancellationToken.None);
|
||||||
|
|
||||||
|
account.Balances.Should().HaveCount(1);
|
||||||
|
((PlaidAccountMetadata)account.Metadata!).Cursor.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SyncTransactionsForAccountAsync_WhenProcessorSucceeds_ItShouldUpdateCursorAndAddsBalance()
|
||||||
|
{
|
||||||
|
var account = AccountBuilder.Create()
|
||||||
|
.WithName("acct")
|
||||||
|
.WithMetadata(amb =>
|
||||||
|
{
|
||||||
|
amb.WithPlaidId("plaid-5")
|
||||||
|
.WithPlaidName("Plaid Five");
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var response = new TransactionsSyncResponse()
|
||||||
|
{
|
||||||
|
RequestId = "req-5",
|
||||||
|
Accounts = [
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
AccountId = "plaid-5",
|
||||||
|
Balances = new() { Current = 200m, Available = 150m, IsoCurrencyCode = "EUR" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
NextCursor = "cursor-5"
|
||||||
|
};
|
||||||
|
SetStatusCode(response, HttpStatusCode.OK);
|
||||||
|
|
||||||
|
_mockPlaidTransactionService
|
||||||
|
.Setup(s => s.SyncTransactionsForAccountAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string?>()))
|
||||||
|
.Returns(AsyncResponses(response));
|
||||||
|
|
||||||
|
_mockPlaidTransactionProcessor
|
||||||
|
.Setup(p => p.ProcessAsync(It.IsAny<Account>(), It.IsAny<TransactionsSyncResponse>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(true);
|
||||||
|
|
||||||
|
await _sut.SyncTransactionsForAccountAsync(account, "token", CancellationToken.None);
|
||||||
|
|
||||||
|
account.Balances.Should().HaveCount(1);
|
||||||
|
var balance = account.Balances.Should().ContainSingle().Which;
|
||||||
|
balance.Current.Should().Be(200m);
|
||||||
|
balance.Available.Should().Be(150m);
|
||||||
|
balance.CurrencyCode.Should().Be("EUR");
|
||||||
|
((PlaidAccountMetadata)account.Metadata!).Cursor.Should().Be("cursor-5");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SyncTransactionsForAccountAsync_WhenMultipleSuccessfulResponses_ItShouldProcessAllAndUseLastCursor()
|
||||||
|
{
|
||||||
|
var account = AccountBuilder.Create()
|
||||||
|
.WithName("acct")
|
||||||
|
.WithMetadata(amb =>
|
||||||
|
{
|
||||||
|
amb.WithPlaidId("plaid-6")
|
||||||
|
.WithPlaidName("Plaid Six");
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var r1 = new TransactionsSyncResponse()
|
||||||
|
{
|
||||||
|
RequestId = "req-6-1",
|
||||||
|
Accounts = [
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
AccountId = "plaid-6",
|
||||||
|
Balances = new() { Current = 10m, Available = 10m, IsoCurrencyCode = "USD" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
NextCursor = "cursor-6-1"
|
||||||
|
};
|
||||||
|
SetStatusCode(r1, HttpStatusCode.OK);
|
||||||
|
|
||||||
|
var r2 = new TransactionsSyncResponse()
|
||||||
|
{
|
||||||
|
RequestId = "req-6-2",
|
||||||
|
Accounts = [
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
AccountId = "plaid-6",
|
||||||
|
Balances = new() { Current = 20m, Available = 20m, IsoCurrencyCode = "USD" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
NextCursor = "cursor-6-2"
|
||||||
|
};
|
||||||
|
SetStatusCode(r2, HttpStatusCode.OK);
|
||||||
|
|
||||||
|
_mockPlaidTransactionService
|
||||||
|
.Setup(s => s.SyncTransactionsForAccountAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string?>()))
|
||||||
|
.Returns(AsyncResponses(r1, r2));
|
||||||
|
|
||||||
|
_mockPlaidTransactionProcessor
|
||||||
|
.Setup(p => p.ProcessAsync(It.IsAny<Account>(), It.IsAny<TransactionsSyncResponse>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(true);
|
||||||
|
|
||||||
|
await _sut.SyncTransactionsForAccountAsync(account, "token", CancellationToken.None);
|
||||||
|
|
||||||
|
account.Balances.Should().HaveCount(2);
|
||||||
|
((PlaidAccountMetadata)account.Metadata!).Cursor.Should().Be("cursor-6-2");
|
||||||
|
_mockPlaidTransactionProcessor.Verify(p => p.ProcessAsync(account, It.IsAny<TransactionsSyncResponse>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetStatusCode(ResponseBase response, HttpStatusCode statusCode) =>
|
||||||
|
typeof(ResponseBase)
|
||||||
|
.GetProperty(nameof(ResponseBase.StatusCode))!
|
||||||
|
.GetSetMethod(nonPublic: true)!
|
||||||
|
.Invoke(response, [statusCode]);
|
||||||
|
|
||||||
|
private static async IAsyncEnumerable<TransactionsSyncResponse> AsyncResponses(params TransactionsSyncResponse[] responses)
|
||||||
|
{
|
||||||
|
foreach (var r in responses)
|
||||||
|
{
|
||||||
|
yield return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DummyMetadata : AccountMetadata
|
||||||
|
{
|
||||||
|
public DummyMetadata() : base("dummy")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user