Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac0681047b | ||
|
|
14d8d4dd9e | ||
|
|
9255bd2fc3 | ||
|
|
11727dfa80 | ||
|
|
d875043895 | ||
|
|
64032ec1ab | ||
|
|
a4a0aca2b4 | ||
|
|
5d6ccaa6df | ||
|
|
23823a4c23 |
@@ -6,8 +6,10 @@ internal static class AccountsExtensions
|
|||||||
|
|
||||||
public static RouteGroupBuilder MapAccountsEndpoints(this WebApplication app)
|
public static RouteGroupBuilder MapAccountsEndpoints(this WebApplication app)
|
||||||
{
|
{
|
||||||
var accountsGroup = app.MapGroup(RouteGroupPrefix).RequireAuthorization();
|
var accountsGroup = app.MapGroup(RouteGroupPrefix)
|
||||||
|
.RequireAuthorization();
|
||||||
|
|
||||||
|
accountsGroup.MapAddEndpoint();
|
||||||
accountsGroup.MapConnectEndpoint();
|
accountsGroup.MapConnectEndpoint();
|
||||||
|
|
||||||
return accountsGroup;
|
return accountsGroup;
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using FiscalOS.Core.Accounts;
|
||||||
|
|
||||||
|
namespace FiscalOS.API.Accounts.Add;
|
||||||
|
|
||||||
|
internal static class Endpoint
|
||||||
|
{
|
||||||
|
private const string Route = "/";
|
||||||
|
|
||||||
|
public static RouteHandlerBuilder MapAddEndpoint(this RouteGroupBuilder groupBuilder)
|
||||||
|
{
|
||||||
|
return groupBuilder.MapPost(Route, HandleAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> HandleAsync(
|
||||||
|
HttpContext httpContext,
|
||||||
|
[FromBody] Request request,
|
||||||
|
[FromServices] AppDbContext appDbContext,
|
||||||
|
CancellationToken ct
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var userId = httpContext.GetUserId();
|
||||||
|
|
||||||
|
var user = await appDbContext.Users
|
||||||
|
.Include(u => u.Institutions.Where(i => i.Metadata is PlaidMetadata && ((PlaidMetadata)i.Metadata).PlaidId == request.PlaidInstitutionId))
|
||||||
|
.Include(u => u.Accounts.Where(a => a.Metadata is PlaidAccountMetadata && ((PlaidAccountMetadata)a.Metadata).PlaidId == request.PlaidAccountId))
|
||||||
|
.ThenInclude(a => a.Metadata)
|
||||||
|
.SingleOrDefaultAsync(u => u.Id == userId, ct);
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
{
|
||||||
|
return Results.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.Institutions.Any() is false)
|
||||||
|
{
|
||||||
|
return Results.ValidationProblem(new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
["PlaidInstitutionId"] = ["The PlaidInstitutionId field is invalid. No institution connected with the given PlaidInstitutionId was found for the user."],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.Accounts.Any())
|
||||||
|
{
|
||||||
|
return Results.Conflict();
|
||||||
|
}
|
||||||
|
|
||||||
|
var accountMetadata = PlaidAccountMetadata.From(request.PlaidAccountId, request.PlaidAccountName);
|
||||||
|
var account = Account.From(user.Institutions.First().Id, request.PlaidAccountName, accountMetadata);
|
||||||
|
user.AddAccount(account);
|
||||||
|
|
||||||
|
await appDbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
return Results.Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
namespace FiscalOS.API.Accounts.Add;
|
||||||
|
|
||||||
|
public record Request : IValidatableObject
|
||||||
|
{
|
||||||
|
public string PlaidInstitutionId { get; init; } = string.Empty;
|
||||||
|
public string PlaidAccountId { get; init; } = string.Empty;
|
||||||
|
public string PlaidAccountName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(PlaidInstitutionId))
|
||||||
|
{
|
||||||
|
var fieldName = nameof(PlaidInstitutionId);
|
||||||
|
yield return new($"The {fieldName} field is required.", [fieldName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(PlaidAccountId))
|
||||||
|
{
|
||||||
|
var fieldName = nameof(PlaidAccountId);
|
||||||
|
yield return new($"The {fieldName} field is required.", [fieldName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(PlaidAccountName))
|
||||||
|
{
|
||||||
|
var fieldName = nameof(PlaidAccountName);
|
||||||
|
yield return new($"The {fieldName} field is required.", [fieldName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,20 +34,12 @@ internal static class Endpoint
|
|||||||
|
|
||||||
if (user is null)
|
if (user is null)
|
||||||
{
|
{
|
||||||
return Results.Problem(
|
return Results.Unauthorized();
|
||||||
statusCode: StatusCodes.Status401Unauthorized,
|
|
||||||
title: "Unauthorized",
|
|
||||||
detail: "You are not authorized to connect an institution. Please log in and try again."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.Institutions.Any())
|
if (user.Institutions.Any())
|
||||||
{
|
{
|
||||||
return Results.Problem(
|
return Results.Conflict();
|
||||||
statusCode: StatusCodes.Status409Conflict,
|
|
||||||
title: "Institution already connected",
|
|
||||||
detail: "The user has already connected an institution with the provided Plaid Institution Id."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (itemId, accessToken) = await plaidService.ExchangeTokenAsync(request.PublicToken);
|
var (itemId, accessToken) = await plaidService.ExchangeTokenAsync(request.PublicToken);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ global using System.Security.Claims;
|
|||||||
global using System.Text.Json.Serialization;
|
global using System.Text.Json.Serialization;
|
||||||
|
|
||||||
global using FiscalOS.API.Accounts;
|
global using FiscalOS.API.Accounts;
|
||||||
|
global using FiscalOS.API.Accounts.Add;
|
||||||
global using FiscalOS.API.Accounts.Connect;
|
global using FiscalOS.API.Accounts.Connect;
|
||||||
global using FiscalOS.API.Http;
|
global using FiscalOS.API.Http;
|
||||||
global using FiscalOS.API.Login;
|
global using FiscalOS.API.Login;
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
namespace FiscalOS.Core.Accounts;
|
||||||
|
|
||||||
|
public sealed class Account : Entity
|
||||||
|
{
|
||||||
|
public Guid UserId { get; init; }
|
||||||
|
public Guid InstitutionId { get; init; }
|
||||||
|
public Institution? Institution { get; init; }
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
public AccountMetadata? Metadata { get; init; }
|
||||||
|
|
||||||
|
public static Account From(Guid institutionId, string name, AccountMetadata accountMetadata)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
InstitutionId = institutionId,
|
||||||
|
Name = name,
|
||||||
|
Metadata = accountMetadata,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace FiscalOS.Core.Accounts;
|
||||||
|
|
||||||
|
public abstract class AccountMetadata : Entity
|
||||||
|
{
|
||||||
|
private readonly string _type;
|
||||||
|
|
||||||
|
public string Type => _type;
|
||||||
|
|
||||||
|
public Guid AccountId { get; init; }
|
||||||
|
|
||||||
|
protected AccountMetadata(string type)
|
||||||
|
{
|
||||||
|
_type = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ public sealed class Institution : Entity
|
|||||||
{
|
{
|
||||||
public Guid UserId { get; init; }
|
public Guid UserId { get; init; }
|
||||||
public string Name { get; init; } = string.Empty;
|
public string Name { get; init; } = string.Empty;
|
||||||
public InstitutionMetadata Metadata { get; init; } = null!;
|
public InstitutionMetadata? Metadata { get; init; }
|
||||||
|
|
||||||
private Institution()
|
private Institution()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ public sealed class User : Entity
|
|||||||
private readonly List<Institution> _institutions = [];
|
private readonly List<Institution> _institutions = [];
|
||||||
public IEnumerable<Institution> Institutions => _institutions;
|
public IEnumerable<Institution> Institutions => _institutions;
|
||||||
|
|
||||||
|
private readonly List<Account> _accounts = [];
|
||||||
|
public IEnumerable<Account> Accounts => _accounts;
|
||||||
|
|
||||||
private User()
|
private User()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -50,4 +53,11 @@ public sealed class User : Entity
|
|||||||
|
|
||||||
_institutions.Add(institution);
|
_institutions.Add(institution);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AddAccount(Account account)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(account, nameof(account));
|
||||||
|
|
||||||
|
_accounts.Add(account);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace FiscalOS.Infra.Accounts.Plaid;
|
||||||
|
|
||||||
|
public sealed class PlaidAccountMetadata : AccountMetadata
|
||||||
|
{
|
||||||
|
public const string TypeValue = Providers.Plaid;
|
||||||
|
public string PlaidId { get; init; } = string.Empty;
|
||||||
|
public string PlaidName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
private PlaidAccountMetadata() : base(TypeValue)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlaidAccountMetadata From(string plaidId, string plaidName)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
PlaidId = plaidId,
|
||||||
|
PlaidName = plaidName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ namespace FiscalOS.Infra.Accounts.Plaid;
|
|||||||
|
|
||||||
public sealed class PlaidMetadata : InstitutionMetadata
|
public sealed class PlaidMetadata : InstitutionMetadata
|
||||||
{
|
{
|
||||||
public const string TypeValue = "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;
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace FiscalOS.Infra.Accounts;
|
||||||
|
|
||||||
|
public static class Providers
|
||||||
|
{
|
||||||
|
public const string Plaid = nameof(Plaid);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Account = FiscalOS.Core.Accounts.Account;
|
||||||
using Institution = FiscalOS.Core.Accounts.Institution;
|
using Institution = FiscalOS.Core.Accounts.Institution;
|
||||||
|
|
||||||
namespace FiscalOS.Infra.Data;
|
namespace FiscalOS.Infra.Data;
|
||||||
@@ -65,6 +66,11 @@ public sealed class AppDbContext(
|
|||||||
.WithOne()
|
.WithOne()
|
||||||
.HasForeignKey(static i => i.UserId)
|
.HasForeignKey(static i => i.UserId)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
eb.HasMany(static u => u.Accounts)
|
||||||
|
.WithOne()
|
||||||
|
.HasForeignKey(static a => a.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<RefreshToken>(static eb =>
|
modelBuilder.Entity<RefreshToken>(static eb =>
|
||||||
@@ -104,5 +110,37 @@ public sealed class AppDbContext(
|
|||||||
eb.Property(static m => m.PlaidName);
|
eb.Property(static m => m.PlaidName);
|
||||||
eb.Property(static m => m.EncryptedAccessToken);
|
eb.Property(static m => m.EncryptedAccessToken);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<Account>(static eb =>
|
||||||
|
{
|
||||||
|
eb.Property(static a => a.Name);
|
||||||
|
|
||||||
|
eb.HasOne(static a => a.Institution)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(static a => a.InstitutionId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
eb.HasOne(static a => a.Metadata)
|
||||||
|
.WithOne()
|
||||||
|
.HasForeignKey<AccountMetadata>(static m => m.AccountId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<AccountMetadata>(static eb =>
|
||||||
|
{
|
||||||
|
eb.HasDiscriminator(static m => m.Type)
|
||||||
|
.HasValue<PlaidAccountMetadata>(PlaidAccountMetadata.TypeValue);
|
||||||
|
|
||||||
|
eb.Property(static m => m.AccountId);
|
||||||
|
eb.Property(static m => m.Type);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<PlaidAccountMetadata>(static eb =>
|
||||||
|
{
|
||||||
|
eb.HasBaseType<AccountMetadata>();
|
||||||
|
|
||||||
|
eb.Property(static m => m.PlaidId);
|
||||||
|
eb.Property(static m => m.PlaidName);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+327
@@ -0,0 +1,327 @@
|
|||||||
|
// <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("20260213130218_AccountAndMetadata")]
|
||||||
|
partial class AccountAndMetadata
|
||||||
|
{
|
||||||
|
/// <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>("InstituionId")
|
||||||
|
.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("InstituionId");
|
||||||
|
|
||||||
|
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.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>("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()
|
||||||
|
.HasForeignKey("InstituionId")
|
||||||
|
.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.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("Metadata")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Metadata")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
|
b.Navigation("Institutions");
|
||||||
|
|
||||||
|
b.Navigation("RefreshTokens");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AccountAndMetadata : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Account",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
InstituionId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Account", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Account_Institution_InstituionId",
|
||||||
|
column: x => x.InstituionId,
|
||||||
|
principalTable: "Institution",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Account_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AccountMetadata",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
Type = table.Column<string>(type: "TEXT", maxLength: 21, nullable: false),
|
||||||
|
AccountId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
PlaidId = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
PlaidName = 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_AccountMetadata", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AccountMetadata_Account_AccountId",
|
||||||
|
column: x => x.AccountId,
|
||||||
|
principalTable: "Account",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Account_InstituionId",
|
||||||
|
table: "Account",
|
||||||
|
column: "InstituionId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Account_UserId",
|
||||||
|
table: "Account",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AccountMetadata_AccountId",
|
||||||
|
table: "AccountMetadata",
|
||||||
|
column: "AccountId",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AccountMetadata");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Account");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+327
@@ -0,0 +1,327 @@
|
|||||||
|
// <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("20260213180002_FixAccountInstitutionColumnName")]
|
||||||
|
partial class FixAccountInstitutionColumnName
|
||||||
|
{
|
||||||
|
/// <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.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>("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()
|
||||||
|
.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.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("Metadata")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Metadata")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
|
b.Navigation("Institutions");
|
||||||
|
|
||||||
|
b.Navigation("RefreshTokens");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class FixAccountInstitutionColumnName : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Account_Institution_InstituionId",
|
||||||
|
table: "Account");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "InstituionId",
|
||||||
|
table: "Account",
|
||||||
|
newName: "InstitutionId");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Account_InstituionId",
|
||||||
|
table: "Account",
|
||||||
|
newName: "IX_Account_InstitutionId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Account_Institution_InstitutionId",
|
||||||
|
table: "Account",
|
||||||
|
column: "InstitutionId",
|
||||||
|
principalTable: "Institution",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Account_Institution_InstitutionId",
|
||||||
|
table: "Account");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "InstitutionId",
|
||||||
|
table: "Account",
|
||||||
|
newName: "InstituionId");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Account_InstitutionId",
|
||||||
|
table: "Account",
|
||||||
|
newName: "IX_Account_InstituionId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Account_Institution_InstituionId",
|
||||||
|
table: "Account",
|
||||||
|
column: "InstituionId",
|
||||||
|
principalTable: "Institution",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,69 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
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.Institution", b =>
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -146,6 +209,21 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
b.ToTable("Users");
|
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 =>
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidMetadata", b =>
|
||||||
{
|
{
|
||||||
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||||
@@ -165,6 +243,32 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
b.HasDiscriminator().HasValue("Plaid");
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||||
|
.WithMany()
|
||||||
|
.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.Institution", b =>
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("FiscalOS.Core.Identity.User", null)
|
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||||
@@ -194,6 +298,12 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
b.Navigation("User");
|
b.Navigation("User");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Metadata")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Metadata")
|
b.Navigation("Metadata")
|
||||||
@@ -202,6 +312,8 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
{
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
b.Navigation("Institutions");
|
b.Navigation("Institutions");
|
||||||
|
|
||||||
b.Navigation("RefreshTokens");
|
b.Navigation("RefreshTokens");
|
||||||
|
|||||||
@@ -0,0 +1,304 @@
|
|||||||
|
using Account = FiscalOS.Core.Accounts.Account;
|
||||||
|
using Institution = FiscalOS.Core.Accounts.Institution;
|
||||||
|
|
||||||
|
namespace FiscalOS.API.Tests.Integration;
|
||||||
|
|
||||||
|
public class AddTests(TestApi testApi) : IntegrationTest(testApi)
|
||||||
|
{
|
||||||
|
private static readonly Uri AddUri = new("/accounts", UriKind.Relative);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Add_WhenCalledWhenNotLoggedIn_ItShouldReturn401WithProblemDetails()
|
||||||
|
{
|
||||||
|
var response = await Client.PostAsync(AddUri, null, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
await response.Should().BeProblemDetails(HttpStatusCode.Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Add_WhenCalledWithoutInstitutionIdOrAccountIdOrAccountName_ItShouldReturn400WithProblemDetails()
|
||||||
|
{
|
||||||
|
var jwt = JwtTokenBuilder.New()
|
||||||
|
.WithClaim(JwtRegisteredClaimNames.Sub, Guid.NewGuid().ToString())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
using var content = new StringContent(JsonSerializer.Serialize(new { }), Encoding.UTF8, "application/json");
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post, AddUri)
|
||||||
|
{
|
||||||
|
Content = content
|
||||||
|
};
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||||
|
|
||||||
|
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
await response.Should().BeValidationProblemDetails(new Dictionary<string, string[]>()
|
||||||
|
{
|
||||||
|
["PlaidInstitutionId"] = ["The PlaidInstitutionId field is required."],
|
||||||
|
["PlaidAccountId"] = ["The PlaidAccountId field is required."],
|
||||||
|
["PlaidAccountName"] = ["The PlaidAccountName field is required."],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Add_WhenCalledWithoutInstitutionId_ItShouldReturn400WithProblemDetails()
|
||||||
|
{
|
||||||
|
var jwt = JwtTokenBuilder.New()
|
||||||
|
.WithClaim(JwtRegisteredClaimNames.Sub, Guid.NewGuid().ToString())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
plaidAccountId = "accountId",
|
||||||
|
plaidAccountName = "Some Account",
|
||||||
|
});
|
||||||
|
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post, AddUri)
|
||||||
|
{
|
||||||
|
Content = content
|
||||||
|
};
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||||
|
|
||||||
|
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
await response.Should().BeValidationProblemDetails(new Dictionary<string, string[]>()
|
||||||
|
{
|
||||||
|
["PlaidInstitutionId"] = ["The PlaidInstitutionId field is required."],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Add_WhenCalledWithoutAccountId_ItShouldReturn400WithProblemDetails()
|
||||||
|
{
|
||||||
|
var jwt = JwtTokenBuilder.New()
|
||||||
|
.WithClaim(JwtRegisteredClaimNames.Sub, Guid.NewGuid().ToString())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
plaidInstitutionId = "institutionId",
|
||||||
|
plaidAccountName = "Some Account",
|
||||||
|
});
|
||||||
|
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post, AddUri)
|
||||||
|
{
|
||||||
|
Content = content
|
||||||
|
};
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||||
|
|
||||||
|
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
await response.Should().BeValidationProblemDetails(new Dictionary<string, string[]>()
|
||||||
|
{
|
||||||
|
["PlaidAccountId"] = ["The PlaidAccountId field is required."],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Add_WhenCalledWithoutAccountName_ItShouldReturn400WithProblemDetails()
|
||||||
|
{
|
||||||
|
var jwt = JwtTokenBuilder.New()
|
||||||
|
.WithClaim(JwtRegisteredClaimNames.Sub, Guid.NewGuid().ToString())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
plaidInstitutionId = "institutionId",
|
||||||
|
plaidAccountId = "accountId",
|
||||||
|
});
|
||||||
|
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post, AddUri)
|
||||||
|
{
|
||||||
|
Content = content
|
||||||
|
};
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||||
|
|
||||||
|
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
await response.Should().BeValidationProblemDetails(new Dictionary<string, string[]>()
|
||||||
|
{
|
||||||
|
["PlaidAccountName"] = ["The PlaidAccountName field is required."],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Add_WhenCalledWithNonExistentUser_ItShouldReturn401WithProblemDetails()
|
||||||
|
{
|
||||||
|
var jwt = JwtTokenBuilder.New()
|
||||||
|
.WithClaim(JwtRegisteredClaimNames.Sub, Guid.NewGuid().ToString())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
plaidInstitutionId = "id",
|
||||||
|
plaidAccountId = "id",
|
||||||
|
plaidAccountName = "Some Account",
|
||||||
|
});
|
||||||
|
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post, AddUri)
|
||||||
|
{
|
||||||
|
Content = content,
|
||||||
|
};
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||||
|
|
||||||
|
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
await response.Should().BeProblemDetails(HttpStatusCode.Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Add_WhenCalledWithPlaidInstitutionIdThatHasNotBeenAdded_ItShouldReturn400WithProblemDetails()
|
||||||
|
{
|
||||||
|
var user = await ExecuteAsync(static async (context, ct, sp) =>
|
||||||
|
{
|
||||||
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||||
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||||
|
|
||||||
|
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(ct);
|
||||||
|
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
||||||
|
|
||||||
|
|
||||||
|
await context.AddAsync(user, ct);
|
||||||
|
await context.SaveChangesAsync(ct);
|
||||||
|
return user;
|
||||||
|
}, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var jwt = JwtTokenBuilder.New()
|
||||||
|
.WithClaim(JwtRegisteredClaimNames.Sub, user.Id.ToString())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
plaidInstitutionId = "id",
|
||||||
|
plaidAccountId = "id",
|
||||||
|
plaidAccountName = "Some Account",
|
||||||
|
});
|
||||||
|
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post, AddUri)
|
||||||
|
{
|
||||||
|
Content = content,
|
||||||
|
};
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||||
|
|
||||||
|
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
await response.Should().BeValidationProblemDetails(new Dictionary<string, string[]>()
|
||||||
|
{
|
||||||
|
["PlaidInstitutionId"] = ["The PlaidInstitutionId field is invalid. No institution connected with the given PlaidInstitutionId was found for the user."],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Add_WhenCalledWithPlaidAccountIdThatHasAlreadyBeenAdded_ItShouldReturn409WithProblemDetails()
|
||||||
|
{
|
||||||
|
var (user, institution, account) = await ExecuteAsync(static async (context, ct, sp) =>
|
||||||
|
{
|
||||||
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||||
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||||
|
|
||||||
|
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(ct);
|
||||||
|
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
||||||
|
|
||||||
|
await context.AddAsync(user, ct);
|
||||||
|
|
||||||
|
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, "accessToken", ct);
|
||||||
|
var plaidMetadata = PlaidMetadata.From("alreadyExists", "Some Bank", encryptedAccessToken);
|
||||||
|
var institution = Institution.From("Some Bank", plaidMetadata);
|
||||||
|
|
||||||
|
await context.AddAsync(institution, ct);
|
||||||
|
|
||||||
|
var plaidAccountMetadata = PlaidAccountMetadata.From("accountId", "Some Account");
|
||||||
|
var account = Account.From(institution.Id, "Some Account", plaidAccountMetadata);
|
||||||
|
|
||||||
|
user.AddInstitution(institution);
|
||||||
|
user.AddAccount(account);
|
||||||
|
|
||||||
|
await context.SaveChangesAsync(ct);
|
||||||
|
return (user, institution, account);
|
||||||
|
}, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var jwt = JwtTokenBuilder.New()
|
||||||
|
.WithClaim(JwtRegisteredClaimNames.Sub, user.Id.ToString())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
plaidInstitutionId = ((PlaidMetadata)institution.Metadata!).PlaidId,
|
||||||
|
plaidAccountId = ((PlaidAccountMetadata)account.Metadata!).PlaidId,
|
||||||
|
plaidAccountName = ((PlaidAccountMetadata)account.Metadata).PlaidName,
|
||||||
|
});
|
||||||
|
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post, AddUri)
|
||||||
|
{
|
||||||
|
Content = content,
|
||||||
|
};
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||||
|
|
||||||
|
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
await response.Should().BeProblemDetails(HttpStatusCode.Conflict);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Add_WhenCalledWithNewAccount_ItShouldReturn200()
|
||||||
|
{
|
||||||
|
var (user, institution) = await ExecuteAsync(async (context, ct, sp) =>
|
||||||
|
{
|
||||||
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||||
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||||
|
var plaidClient = sp.GetRequiredService<PlaidClient>();
|
||||||
|
|
||||||
|
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(ct);
|
||||||
|
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
||||||
|
|
||||||
|
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, "accessToken", ct);
|
||||||
|
var plaidMetadata = PlaidMetadata.From("id", "Some Bank", encryptedAccessToken);
|
||||||
|
var institution = Institution.From("Some Bank", plaidMetadata);
|
||||||
|
|
||||||
|
user.AddInstitution(institution);
|
||||||
|
|
||||||
|
await context.AddAsync(user, ct);
|
||||||
|
await context.SaveChangesAsync(ct);
|
||||||
|
return (user, institution);
|
||||||
|
}, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var jwt = JwtTokenBuilder.New()
|
||||||
|
.WithClaim(JwtRegisteredClaimNames.Sub, user.Id.ToString())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var newAccountId = "newAccountId";
|
||||||
|
var newAccountName = "New Account";
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
plaidInstitutionId = ((PlaidMetadata)institution.Metadata!).PlaidId,
|
||||||
|
plaidAccountId = newAccountId,
|
||||||
|
plaidAccountName = newAccountName,
|
||||||
|
});
|
||||||
|
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post, AddUri)
|
||||||
|
{
|
||||||
|
Content = content,
|
||||||
|
};
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||||
|
|
||||||
|
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||||
|
|
||||||
|
var updatedUser = await ExecuteAsync(
|
||||||
|
async (context, ct) => await context.Set<User>()
|
||||||
|
.Include(u => u.Accounts)
|
||||||
|
.ThenInclude(a => a.Metadata)
|
||||||
|
.FirstAsync(u => u.Id == user.Id, ct),
|
||||||
|
TestContext.Current.CancellationToken
|
||||||
|
);
|
||||||
|
|
||||||
|
updatedUser.Accounts.Should().ContainSingle(
|
||||||
|
a => a.Name == newAccountName &&
|
||||||
|
a.InstitutionId == institution.Id &&
|
||||||
|
a.Metadata is PlaidAccountMetadata &&
|
||||||
|
((PlaidAccountMetadata)a.Metadata).PlaidId == newAccountId &&
|
||||||
|
((PlaidAccountMetadata)a.Metadata).PlaidName == newAccountName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -136,7 +136,7 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
var json = JsonSerializer.Serialize(new
|
var json = JsonSerializer.Serialize(new
|
||||||
{
|
{
|
||||||
publicToken = "token",
|
publicToken = "token",
|
||||||
plaidInstitutionId = ((PlaidMetadata)institution.Metadata).PlaidId,
|
plaidInstitutionId = ((PlaidMetadata)institution.Metadata!).PlaidId,
|
||||||
});
|
});
|
||||||
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Post, ConnectUri)
|
using var request = new HttpRequestMessage(HttpMethod.Post, ConnectUri)
|
||||||
@@ -167,13 +167,13 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi)
|
|||||||
await context.AddAsync(user, ct);
|
await context.AddAsync(user, ct);
|
||||||
await context.SaveChangesAsync(ct);
|
await context.SaveChangesAsync(ct);
|
||||||
|
|
||||||
var publicToken = await plaidClient.SandboxPublicTokenCreateAsync(new()
|
var publicTokenResponse = await plaidClient.SandboxPublicTokenCreateAsync(new()
|
||||||
{
|
{
|
||||||
InstitutionId = plaidInstitutionId,
|
InstitutionId = plaidInstitutionId,
|
||||||
InitialProducts = [Products.Transactions],
|
InitialProducts = [Products.Transactions],
|
||||||
});
|
});
|
||||||
|
|
||||||
return (user, publicToken.PublicToken);
|
return (user, publicTokenResponse.PublicToken);
|
||||||
}, TestContext.Current.CancellationToken);
|
}, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
var jwt = JwtTokenBuilder.New()
|
var jwt = JwtTokenBuilder.New()
|
||||||
|
|||||||
Reference in New Issue
Block a user