feat: add Plaid transaction handling and async queue processing
- introduced new migration to add Cursor column to AccountMetadata. - updated AppDbContext model snapshot to reflect changes in the database schema. - implemented async queue processing with ChannelAsyncQueue and AsyncQueueHostedService. - created Plaid transaction handlers for added, modified, and removed transactions. - developed PlaidTransactionService and PlaidTransactionSyncer for syncing transactions. - added SyncUpdatesProcessor to handle sync updates from Plaid. - updated tests to accommodate changes in institution and account metadata handling.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
[*.cs]
|
||||
|
||||
dotnet_diagnostic.CA1062.severity = none
|
||||
dotnet_diagnostic.CA5404.severity = none
|
||||
dotnet_diagnostic.CA5404.severity = none
|
||||
@@ -1,3 +1,6 @@
|
||||
|
||||
using FiscalOS.Infra.Common;
|
||||
|
||||
namespace FiscalOS.Infra.Accounts.Plaid;
|
||||
|
||||
public sealed class PlaidAccountMetadata : AccountMetadata
|
||||
@@ -5,12 +8,16 @@ public sealed class PlaidAccountMetadata : AccountMetadata
|
||||
public const string TypeValue = Providers.Plaid;
|
||||
public string PlaidId { get; init; } = string.Empty;
|
||||
public string PlaidName { get; init; } = string.Empty;
|
||||
public string? Cursor { get; private set; }
|
||||
|
||||
private PlaidAccountMetadata() : base(TypeValue)
|
||||
{
|
||||
}
|
||||
|
||||
public static PlaidAccountMetadata From(string plaidId, string plaidName)
|
||||
public static PlaidAccountMetadata From(
|
||||
string plaidId,
|
||||
string plaidName
|
||||
)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
@@ -18,4 +25,11 @@ public sealed class PlaidAccountMetadata : AccountMetadata
|
||||
PlaidName = plaidName,
|
||||
};
|
||||
}
|
||||
|
||||
public void SetCursor(string cursor)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(cursor, nameof(cursor));
|
||||
|
||||
Cursor = cursor;
|
||||
}
|
||||
}
|
||||
+17
-9
@@ -1,22 +1,29 @@
|
||||
|
||||
|
||||
using System.Reflection;
|
||||
|
||||
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 PlaidService(PlaidClient client)
|
||||
private PlaidAccountService(PlaidClient client, IOptions<PlaidClientOptions> options)
|
||||
{
|
||||
_client = client;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public static PlaidService From(IServiceProvider sp)
|
||||
public static PlaidAccountService From(IServiceProvider sp)
|
||||
{
|
||||
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)
|
||||
@@ -34,6 +41,7 @@ public sealed class PlaidService
|
||||
{
|
||||
ClientUserId = id,
|
||||
},
|
||||
Webhook = _options.Value.Webhook,
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (ltr.IsSuccessStatusCode is false)
|
||||
@@ -88,4 +96,4 @@ public sealed class PlaidService
|
||||
|
||||
return ar.Item;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ public sealed record PlaidClientOptions
|
||||
{
|
||||
public string ClientId { get; init; } = string.Empty;
|
||||
public string Secret { get; init; } = string.Empty;
|
||||
public string Webhook { get; init; } = string.Empty;
|
||||
|
||||
public IOptions<PlaidOptions> ToPlaidOptions()
|
||||
{
|
||||
|
||||
+12
-6
@@ -1,31 +1,37 @@
|
||||
using FiscalOS.Infra.Common;
|
||||
|
||||
namespace FiscalOS.Infra.Accounts.Plaid;
|
||||
|
||||
public sealed class PlaidMetadata : InstitutionMetadata
|
||||
public sealed class PlaidInstitutionMetadata : InstitutionMetadata
|
||||
{
|
||||
public const string TypeValue = Providers.Plaid;
|
||||
public string PlaidId { get; init; } = string.Empty;
|
||||
public string PlaidName { get; init; } = string.Empty;
|
||||
public string EncryptedAccessToken { get; init; } = string.Empty;
|
||||
public string ItemId { get; init; } = string.Empty;
|
||||
|
||||
private PlaidMetadata() : base(TypeValue)
|
||||
private PlaidInstitutionMetadata() : base(TypeValue)
|
||||
{
|
||||
}
|
||||
|
||||
public static PlaidMetadata From(
|
||||
public static PlaidInstitutionMetadata From(
|
||||
string? plaidId,
|
||||
string? plaidName,
|
||||
string encryptedAccessToken
|
||||
string encryptedAccessToken,
|
||||
string itemId
|
||||
)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(plaidId, nameof(plaidId));
|
||||
ArgumentNullException.ThrowIfNull(plaidName, nameof(plaidName));
|
||||
ArgumentNullException.ThrowIfNull(encryptedAccessToken, nameof(encryptedAccessToken));
|
||||
ArgumentNullException.ThrowIfNull(itemId, nameof(itemId));
|
||||
|
||||
return new PlaidMetadata
|
||||
return new()
|
||||
{
|
||||
PlaidId = plaidId,
|
||||
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
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace FiscalOS.Infra.Accounts;
|
||||
namespace FiscalOS.Infra.Common;
|
||||
|
||||
public static class Providers
|
||||
{
|
||||
@@ -1,5 +1,9 @@
|
||||
using FiscalOS.Core.Transactions;
|
||||
using FiscalOS.Infra.Transactions.Plaid;
|
||||
|
||||
using Account = FiscalOS.Core.Accounts.Account;
|
||||
using Institution = FiscalOS.Core.Accounts.Institution;
|
||||
using Transaction = FiscalOS.Core.Transactions.Transaction;
|
||||
|
||||
namespace FiscalOS.Infra.Data;
|
||||
|
||||
@@ -14,6 +18,8 @@ public sealed class AppDbContext(
|
||||
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
|
||||
public DbSet<Institution> Institutions => Set<Institution>();
|
||||
public DbSet<Transaction> Transactions => Set<Transaction>();
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
@@ -63,7 +69,7 @@ public sealed class AppDbContext(
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
eb.HasMany(static u => u.Institutions)
|
||||
.WithOne()
|
||||
.WithOne(static i => i.User)
|
||||
.HasForeignKey(static i => i.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
@@ -96,19 +102,20 @@ public sealed class AppDbContext(
|
||||
modelBuilder.Entity<InstitutionMetadata>(static eb =>
|
||||
{
|
||||
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.Type);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<PlaidMetadata>(static eb =>
|
||||
modelBuilder.Entity<PlaidInstitutionMetadata>(static eb =>
|
||||
{
|
||||
eb.HasBaseType<InstitutionMetadata>();
|
||||
|
||||
eb.Property(static m => m.PlaidId);
|
||||
eb.Property(static m => m.PlaidName);
|
||||
eb.Property(static m => m.EncryptedAccessToken);
|
||||
eb.Property(static m => m.ItemId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Account>(static eb =>
|
||||
@@ -146,6 +153,7 @@ public sealed class AppDbContext(
|
||||
|
||||
eb.Property(static m => m.PlaidId);
|
||||
eb.Property(static m => m.PlaidName);
|
||||
eb.Property(static m => m.Cursor);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Balance>(static eb =>
|
||||
@@ -155,5 +163,40 @@ public sealed class AppDbContext(
|
||||
eb.Property(static b => b.Available);
|
||||
eb.Property(static b => b.CurrencyCode);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Transaction>(static eb =>
|
||||
{
|
||||
eb.HasOne(static t => t.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(static t => t.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
eb.HasOne(static t => t.Account)
|
||||
.WithMany()
|
||||
.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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
namespace FiscalOS.Infra.DependencyInjection;
|
||||
|
||||
using FiscalOS.Core.Queuing;
|
||||
using FiscalOS.Infra.Accounts.Plaid;
|
||||
using FiscalOS.Infra.Queuing;
|
||||
using FiscalOS.Infra.Transactions.Plaid;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
|
||||
@@ -33,7 +38,18 @@ public static class ServiceCollectionExtensions
|
||||
var options = clientOptions.Value.ToPlaidOptions();
|
||||
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;
|
||||
}
|
||||
|
||||
+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
|
||||
}
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ namespace FiscalOS.Infra.Migrations
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Institution");
|
||||
b.ToTable("Institutions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||
@@ -241,10 +241,92 @@ namespace FiscalOS.Infra.Migrations
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<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");
|
||||
@@ -256,7 +338,7 @@ namespace FiscalOS.Infra.Migrations
|
||||
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");
|
||||
|
||||
@@ -264,6 +346,10 @@ namespace FiscalOS.Infra.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
@@ -275,10 +361,21 @@ namespace FiscalOS.Infra.Migrations
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Infra.Transactions.Plaid.PlaidTransactionMetadata", b =>
|
||||
{
|
||||
b.HasBaseType("FiscalOS.Core.Transactions.TransactionMetadata");
|
||||
|
||||
b.Property<string>("PlaidId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasDiscriminator().HasValue("Plaid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||
.WithMany()
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("InstitutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
@@ -312,11 +409,13 @@ namespace FiscalOS.Infra.Migrations
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||
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 =>
|
||||
@@ -339,15 +438,51 @@ namespace FiscalOS.Infra.Migrations
|
||||
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");
|
||||
});
|
||||
|
||||
@@ -359,6 +494,11 @@ namespace FiscalOS.Infra.Migrations
|
||||
|
||||
b.Navigation("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||
{
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
#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,54 @@
|
||||
using Account = FiscalOS.Core.Accounts.Account;
|
||||
using Transaction = FiscalOS.Core.Transactions.Transaction;
|
||||
|
||||
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||
|
||||
internal interface IPlaidAddedTransactionHandler
|
||||
{
|
||||
void Handle(Account account, IEnumerable<Going.Plaid.Entity.Transaction> added);
|
||||
}
|
||||
|
||||
internal sealed class PlaidAddedTransactionHandler : IPlaidAddedTransactionHandler
|
||||
{
|
||||
private readonly ILogger<PlaidAddedTransactionHandler> _logger;
|
||||
|
||||
private PlaidAddedTransactionHandler(ILogger<PlaidAddedTransactionHandler> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public static PlaidAddedTransactionHandler From(IServiceProvider serviceProvider)
|
||||
{
|
||||
return new(
|
||||
serviceProvider.GetRequiredService<ILogger<PlaidAddedTransactionHandler>>()
|
||||
);
|
||||
}
|
||||
|
||||
public void Handle(Account account, IEnumerable<Going.Plaid.Entity.Transaction> added)
|
||||
{
|
||||
foreach (var addedTransaction in added)
|
||||
{
|
||||
if (addedTransaction.Pending.GetValueOrDefault())
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Skipping pending transaction {TransactionId} for account {AccountId} as it has not been posted yet",
|
||||
addedTransaction.TransactionId,
|
||||
account.Id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
var transactionMetadata = PlaidTransactionMetadata.From(addedTransaction.TransactionId);
|
||||
var transaction = Transaction.From(
|
||||
account.UserId,
|
||||
account.Id,
|
||||
addedTransaction.MerchantName,
|
||||
addedTransaction.OriginalDescription,
|
||||
addedTransaction.Amount,
|
||||
addedTransaction.Datetime,
|
||||
transactionMetadata
|
||||
);
|
||||
account.AddTransaction(transaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using Account = FiscalOS.Core.Accounts.Account;
|
||||
using Transaction = FiscalOS.Core.Transactions.Transaction;
|
||||
|
||||
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||
|
||||
internal interface IPlaidModifiedTransactionHandler
|
||||
{
|
||||
void Handle(Account account, IEnumerable<Going.Plaid.Entity.Transaction> modified);
|
||||
}
|
||||
|
||||
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(IServiceProvider serviceProvider)
|
||||
{
|
||||
return new(
|
||||
serviceProvider.GetRequiredService<ILogger<PlaidModifiedTransactionHandler>>(),
|
||||
serviceProvider.GetRequiredService<AppDbContext>()
|
||||
);
|
||||
}
|
||||
|
||||
public void Handle(Account account, IEnumerable<Going.Plaid.Entity.Transaction> modified)
|
||||
{
|
||||
var modifiedTransactionIds = modified.Select(t => t.TransactionId);
|
||||
var existingModifiedTransactions = _appDbContext.Transactions
|
||||
.Where(t => t.Metadata is PlaidTransactionMetadata && modifiedTransactionIds.Contains(((PlaidTransactionMetadata)t.Metadata).PlaidId))
|
||||
.ToList();
|
||||
|
||||
foreach (var existing in existingModifiedTransactions)
|
||||
{
|
||||
if (existing.Metadata is not PlaidTransactionMetadata plaidMetadata)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Existing transaction {TransactionId} has non-Plaid metadata. Skipping update for this transaction.",
|
||||
existing.Id
|
||||
);
|
||||
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
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
var newTransactionData = Transaction.From(
|
||||
existing.UserId,
|
||||
existing.AccountId,
|
||||
plaidModifiedTransaction.MerchantName,
|
||||
plaidModifiedTransaction.OriginalDescription,
|
||||
plaidModifiedTransaction.Amount,
|
||||
plaidModifiedTransaction.Datetime,
|
||||
plaidMetadata
|
||||
);
|
||||
|
||||
_appDbContext.Entry(existing).CurrentValues.SetValues(newTransactionData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||
|
||||
internal interface IPlaidRemovedTransactionHandler
|
||||
{
|
||||
Task HandleAsync(IEnumerable<RemovedTransaction> removed, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
internal sealed class PlaidRemovedTransactionHandler : IPlaidRemovedTransactionHandler
|
||||
{
|
||||
private readonly AppDbContext _appDbContext;
|
||||
|
||||
private PlaidRemovedTransactionHandler(AppDbContext appDbContext)
|
||||
{
|
||||
_appDbContext = appDbContext;
|
||||
}
|
||||
|
||||
public static PlaidRemovedTransactionHandler From(IServiceProvider serviceProvider)
|
||||
{
|
||||
return new(
|
||||
serviceProvider.GetRequiredService<AppDbContext>()
|
||||
);
|
||||
}
|
||||
|
||||
public async Task HandleAsync(IEnumerable<RemovedTransaction> removed, CancellationToken cancellationToken)
|
||||
{
|
||||
var removedIdsList = removed.Select(t => t.TransactionId);
|
||||
|
||||
await _appDbContext.Transactions
|
||||
.Where(t => t.Metadata is PlaidTransactionMetadata && removedIdsList.Contains(((PlaidTransactionMetadata)t.Metadata).PlaidId))
|
||||
.ExecuteDeleteAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using FiscalOS.Core.Transactions;
|
||||
using FiscalOS.Infra.Common;
|
||||
|
||||
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);
|
||||
|
||||
return new()
|
||||
{
|
||||
PlaidId = plaidId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using Account = FiscalOS.Core.Accounts.Account;
|
||||
|
||||
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||
|
||||
internal interface IPlaidTransactionProcessor
|
||||
{
|
||||
Task 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(IServiceProvider provider)
|
||||
{
|
||||
return new(
|
||||
provider.GetRequiredService<IPlaidAddedTransactionHandler>(),
|
||||
provider.GetRequiredService<IPlaidModifiedTransactionHandler>(),
|
||||
provider.GetRequiredService<IPlaidRemovedTransactionHandler>()
|
||||
);
|
||||
}
|
||||
|
||||
public async Task ProcessAsync(Account account, TransactionsSyncResponse syncResponse, CancellationToken cancellationToken)
|
||||
{
|
||||
_addedHandler.Handle(account, syncResponse.Added);
|
||||
_modifiedHandler.Handle(account, syncResponse.Modified);
|
||||
await _removedHandler.HandleAsync(syncResponse.Removed, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -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,93 @@
|
||||
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(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;
|
||||
}
|
||||
|
||||
plaidAccountMetadata.SetCursor(response.NextCursor);
|
||||
var balance = Balance.From(plaidAccount.Current, plaidAccount.Available, plaidAccount.CurrencyCode);
|
||||
account.AddBalance(balance);
|
||||
|
||||
await _transactionProcessor.ProcessAsync(account, response, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
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)
|
||||
{
|
||||
await _plaidTransactionSyncer.SyncTransactionsForAccountAsync(account, decryptedAccessToken, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
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,28 @@
|
||||
global using System.IdentityModel.Tokens.Jwt;
|
||||
global using System.IO.Abstractions;
|
||||
global using System.Reflection;
|
||||
global using System.Security.Claims;
|
||||
global using System.Security.Cryptography;
|
||||
global using System.Text;
|
||||
global using System.Text.Json;
|
||||
global using System.Threading.Channels;
|
||||
|
||||
global using FiscalOS.Core.Accounts;
|
||||
global using FiscalOS.Core.Authentication;
|
||||
global using FiscalOS.Core.Data;
|
||||
global using FiscalOS.Core.Identity;
|
||||
global using FiscalOS.Core.Queuing;
|
||||
global using FiscalOS.Core.Security;
|
||||
global using FiscalOS.Infra.Accounts.Plaid;
|
||||
global using FiscalOS.Infra.Authentication;
|
||||
global using FiscalOS.Infra.Authorization;
|
||||
global using FiscalOS.Infra.Common;
|
||||
global using FiscalOS.Infra.Data;
|
||||
global using FiscalOS.Infra.Security;
|
||||
|
||||
global using Going.Plaid;
|
||||
global using Going.Plaid.Entity;
|
||||
global using Going.Plaid.Transactions;
|
||||
|
||||
global using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
global using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
Reference in New Issue
Block a user