Compare commits
No commits in common. "postgres-impl" and "master" have entirely different histories.
postgres-i
...
master
|
|
@ -8,11 +8,6 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
namespace Asentrafin.Api.Configuration;
|
||||
|
||||
public class PlaidOptions
|
||||
{
|
||||
public string BaseUrl { get; set; } = "https://sandbox.plaid.com";
|
||||
public string ClientId { get; set; } = string.Empty;
|
||||
public string Secret { get; set; } = string.Empty;
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
namespace Asentrafin.Api.Contracts.Plaid;
|
||||
|
||||
public record CreatePlaidItemRequest(
|
||||
string AccessToken,
|
||||
string? PlaidItemId,
|
||||
string? InstitutionId,
|
||||
string? InstitutionName);
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
namespace Asentrafin.Api.Contracts.Plaid;
|
||||
|
||||
public record IngestionSummary(
|
||||
Guid ItemId,
|
||||
int AccountsUpserted,
|
||||
int TransactionsAddedOrModified,
|
||||
int TransactionsRemoved,
|
||||
int SecuritiesUpserted,
|
||||
int HoldingsUpserted,
|
||||
int InvestmentTransactionsUpserted);
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
namespace Asentrafin.Api.Contracts.Plaid;
|
||||
|
||||
public record InvestmentIngestionRequest(DateOnly? StartDate, DateOnly? EndDate);
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
namespace Asentrafin.Api.Contracts.Transactions;
|
||||
|
||||
public record CategorySummaryResponse(
|
||||
Guid CategoryId,
|
||||
string CategoryName,
|
||||
string CategorySlug,
|
||||
int TransactionCount,
|
||||
decimal TotalAmount);
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
namespace Asentrafin.Api.Contracts.Transactions;
|
||||
|
||||
public record TransactionCategoryResponse(
|
||||
Guid Id,
|
||||
Guid? ParentId,
|
||||
string Name,
|
||||
string Slug,
|
||||
string? PlaidPrimary,
|
||||
string? PlaidDetailed,
|
||||
bool IsSystem);
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
using Asentrafin.Api.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Asentrafin.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/database")]
|
||||
public class DatabaseHealthController(AppDbContext dbContext) : ControllerBase
|
||||
{
|
||||
[HttpGet("health")]
|
||||
public async Task<IActionResult> GetHealth(CancellationToken cancellationToken)
|
||||
{
|
||||
var canConnect = await dbContext.Database.CanConnectAsync(cancellationToken);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
database = "postgres",
|
||||
canConnect
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
using Asentrafin.Api.Contracts.Plaid;
|
||||
using Asentrafin.Api.Services.Plaid;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Asentrafin.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/plaid")]
|
||||
public class PlaidIngestionController(IPlaidIngestionService ingestionService) : ControllerBase
|
||||
{
|
||||
[HttpPost("items")]
|
||||
public async Task<ActionResult<object>> CreateItem(
|
||||
CreatePlaidItemRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var itemId = await ingestionService.CreateItemAsync(request, cancellationToken);
|
||||
return CreatedAtAction(nameof(CreateItem), new { itemId }, new { itemId });
|
||||
}
|
||||
|
||||
[HttpPost("items/{itemId:guid}/ingest/accounts")]
|
||||
public async Task<ActionResult<IngestionSummary>> IngestAccounts(
|
||||
Guid itemId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await ingestionService.IngestAccountsAsync(itemId, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("items/{itemId:guid}/ingest/transactions")]
|
||||
public async Task<ActionResult<IngestionSummary>> IngestTransactions(
|
||||
Guid itemId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await ingestionService.IngestTransactionsAsync(itemId, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("items/{itemId:guid}/ingest/investments")]
|
||||
public async Task<ActionResult<IngestionSummary>> IngestInvestments(
|
||||
Guid itemId,
|
||||
InvestmentIngestionRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await ingestionService.IngestInvestmentsAsync(
|
||||
itemId,
|
||||
request.StartDate,
|
||||
request.EndDate,
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("items/{itemId:guid}/ingest/all")]
|
||||
public async Task<ActionResult<IngestionSummary>> IngestAll(
|
||||
Guid itemId,
|
||||
InvestmentIngestionRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await ingestionService.IngestAllAsync(
|
||||
itemId,
|
||||
request.StartDate,
|
||||
request.EndDate,
|
||||
cancellationToken));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
using Asentrafin.Api.Contracts.Transactions;
|
||||
using Asentrafin.Api.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Asentrafin.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/transactions")]
|
||||
public class TransactionAnalyticsController(AppDbContext dbContext) : ControllerBase
|
||||
{
|
||||
[HttpGet("categories")]
|
||||
public async Task<ActionResult<IReadOnlyList<TransactionCategoryResponse>>> GetCategories(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var categories = await dbContext.TransactionCategories
|
||||
.OrderBy(category => category.SortOrder)
|
||||
.ThenBy(category => category.Name)
|
||||
.Select(category => new TransactionCategoryResponse(
|
||||
category.Id,
|
||||
category.ParentId,
|
||||
category.Name,
|
||||
category.Slug,
|
||||
category.PlaidPrimary,
|
||||
category.PlaidDetailed,
|
||||
category.IsSystem))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Ok(categories);
|
||||
}
|
||||
|
||||
[HttpGet("category-summaries")]
|
||||
public async Task<ActionResult<IReadOnlyList<CategorySummaryResponse>>> GetCategorySummaries(
|
||||
[FromQuery] DateOnly? startDate,
|
||||
[FromQuery] DateOnly? endDate,
|
||||
[FromQuery] Guid? accountId,
|
||||
[FromQuery] bool includePending = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = dbContext.PlaidTransactions
|
||||
.AsNoTracking()
|
||||
.Where(transaction => transaction.RemovedAt == null);
|
||||
|
||||
if (!includePending)
|
||||
{
|
||||
query = query.Where(transaction => !transaction.Pending);
|
||||
}
|
||||
|
||||
if (startDate is not null)
|
||||
{
|
||||
query = query.Where(transaction => transaction.Date >= startDate.Value);
|
||||
}
|
||||
|
||||
if (endDate is not null)
|
||||
{
|
||||
query = query.Where(transaction => transaction.Date <= endDate.Value);
|
||||
}
|
||||
|
||||
if (accountId is not null)
|
||||
{
|
||||
query = query.Where(transaction => transaction.PlaidAccountId == accountId.Value);
|
||||
}
|
||||
|
||||
var summaries = await query
|
||||
.Where(transaction => transaction.Classification != null)
|
||||
.GroupBy(transaction => new
|
||||
{
|
||||
transaction.Classification!.Category.Id,
|
||||
transaction.Classification.Category.Name,
|
||||
transaction.Classification.Category.Slug
|
||||
})
|
||||
.Select(group => new CategorySummaryResponse(
|
||||
group.Key.Id,
|
||||
group.Key.Name,
|
||||
group.Key.Slug,
|
||||
group.Count(),
|
||||
group.Sum(transaction => transaction.Amount)))
|
||||
.OrderByDescending(summary => Math.Abs(summary.TotalAmount))
|
||||
.ThenBy(summary => summary.CategoryName)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Ok(summaries);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Asentrafin.Api.Entities;
|
||||
|
||||
namespace Asentrafin.Api.Data;
|
||||
|
||||
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<PlaidItem> PlaidItems => Set<PlaidItem>();
|
||||
public DbSet<PlaidAccount> PlaidAccounts => Set<PlaidAccount>();
|
||||
public DbSet<PlaidTransaction> PlaidTransactions => Set<PlaidTransaction>();
|
||||
public DbSet<PlaidSecurity> PlaidSecurities => Set<PlaidSecurity>();
|
||||
public DbSet<PlaidHolding> PlaidHoldings => Set<PlaidHolding>();
|
||||
public DbSet<PlaidInvestmentTransaction> PlaidInvestmentTransactions => Set<PlaidInvestmentTransaction>();
|
||||
public DbSet<TransactionCategory> TransactionCategories => Set<TransactionCategory>();
|
||||
public DbSet<TransactionClassification> TransactionClassifications => Set<TransactionClassification>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.HasDefaultSchema("app");
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace Asentrafin.Api.Data;
|
||||
|
||||
public class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
|
||||
{
|
||||
public AppDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var connectionString = Environment.GetEnvironmentVariable("ASENTRAFIN_CONNECTION_STRING")
|
||||
?? "Host=localhost;Port=5432;Database=asentrafin;Username=postgres;Password=postgres";
|
||||
|
||||
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseNpgsql(connectionString);
|
||||
|
||||
return new AppDbContext(optionsBuilder.Options);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
using Asentrafin.Api.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Asentrafin.Api.Data.Configurations;
|
||||
|
||||
public class PlaidAccountConfiguration : IEntityTypeConfiguration<PlaidAccount>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PlaidAccount> builder)
|
||||
{
|
||||
builder.ToTable("plaid_accounts");
|
||||
builder.HasKey(account => account.Id);
|
||||
|
||||
builder.Property(account => account.PlaidAccountId).HasMaxLength(128).IsRequired();
|
||||
builder.Property(account => account.Name).HasMaxLength(256).IsRequired();
|
||||
builder.Property(account => account.OfficialName).HasMaxLength(512);
|
||||
builder.Property(account => account.Mask).HasMaxLength(32);
|
||||
builder.Property(account => account.Type).HasMaxLength(64).IsRequired();
|
||||
builder.Property(account => account.Subtype).HasMaxLength(64);
|
||||
builder.Property(account => account.IsoCurrencyCode).HasMaxLength(8);
|
||||
builder.Property(account => account.UnofficialCurrencyCode).HasMaxLength(32);
|
||||
builder.Property(account => account.RawJson).HasColumnType("jsonb");
|
||||
|
||||
builder.HasIndex(account => account.PlaidAccountId).IsUnique();
|
||||
builder.HasOne(account => account.PlaidItem)
|
||||
.WithMany(item => item.Accounts)
|
||||
.HasForeignKey(account => account.PlaidItemId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
using Asentrafin.Api.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Asentrafin.Api.Data.Configurations;
|
||||
|
||||
public class PlaidHoldingConfiguration : IEntityTypeConfiguration<PlaidHolding>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PlaidHolding> builder)
|
||||
{
|
||||
builder.ToTable("plaid_holdings");
|
||||
builder.HasKey(holding => holding.Id);
|
||||
|
||||
builder.Property(holding => holding.IsoCurrencyCode).HasMaxLength(8);
|
||||
builder.Property(holding => holding.UnofficialCurrencyCode).HasMaxLength(32);
|
||||
builder.Property(holding => holding.RawJson).HasColumnType("jsonb");
|
||||
|
||||
builder.HasIndex(holding => new { holding.PlaidAccountId, holding.PlaidSecurityId }).IsUnique();
|
||||
builder.HasOne(holding => holding.PlaidItem)
|
||||
.WithMany(item => item.Holdings)
|
||||
.HasForeignKey(holding => holding.PlaidItemId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne(holding => holding.PlaidAccount)
|
||||
.WithMany(account => account.Holdings)
|
||||
.HasForeignKey(holding => holding.PlaidAccountId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne(holding => holding.PlaidSecurity)
|
||||
.WithMany(security => security.Holdings)
|
||||
.HasForeignKey(holding => holding.PlaidSecurityId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
using Asentrafin.Api.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Asentrafin.Api.Data.Configurations;
|
||||
|
||||
public class PlaidInvestmentTransactionConfiguration : IEntityTypeConfiguration<PlaidInvestmentTransaction>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PlaidInvestmentTransaction> builder)
|
||||
{
|
||||
builder.ToTable("plaid_investment_transactions");
|
||||
builder.HasKey(transaction => transaction.Id);
|
||||
|
||||
builder.Property(transaction => transaction.PlaidInvestmentTransactionId).HasMaxLength(128).IsRequired();
|
||||
builder.Property(transaction => transaction.AccountPlaidId).HasMaxLength(128).IsRequired();
|
||||
builder.Property(transaction => transaction.SecurityPlaidId).HasMaxLength(128);
|
||||
builder.Property(transaction => transaction.Name).HasMaxLength(512).IsRequired();
|
||||
builder.Property(transaction => transaction.Type).HasMaxLength(64);
|
||||
builder.Property(transaction => transaction.Subtype).HasMaxLength(64);
|
||||
builder.Property(transaction => transaction.IsoCurrencyCode).HasMaxLength(8);
|
||||
builder.Property(transaction => transaction.UnofficialCurrencyCode).HasMaxLength(32);
|
||||
builder.Property(transaction => transaction.RawJson).HasColumnType("jsonb");
|
||||
|
||||
builder.HasIndex(transaction => transaction.PlaidInvestmentTransactionId).IsUnique();
|
||||
builder.HasIndex(transaction => new { transaction.PlaidItemId, transaction.Date });
|
||||
builder.HasOne(transaction => transaction.PlaidItem)
|
||||
.WithMany(item => item.InvestmentTransactions)
|
||||
.HasForeignKey(transaction => transaction.PlaidItemId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne(transaction => transaction.PlaidAccount)
|
||||
.WithMany(account => account.InvestmentTransactions)
|
||||
.HasForeignKey(transaction => transaction.PlaidAccountId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne(transaction => transaction.PlaidSecurity)
|
||||
.WithMany(security => security.InvestmentTransactions)
|
||||
.HasForeignKey(transaction => transaction.PlaidSecurityId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
using Asentrafin.Api.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Asentrafin.Api.Data.Configurations;
|
||||
|
||||
public class PlaidItemConfiguration : IEntityTypeConfiguration<PlaidItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PlaidItem> builder)
|
||||
{
|
||||
builder.ToTable("plaid_items");
|
||||
builder.HasKey(item => item.Id);
|
||||
|
||||
builder.Property(item => item.AccessToken).IsRequired();
|
||||
builder.Property(item => item.PlaidItemId).HasMaxLength(128);
|
||||
builder.Property(item => item.InstitutionId).HasMaxLength(128);
|
||||
builder.Property(item => item.InstitutionName).HasMaxLength(256);
|
||||
builder.Property(item => item.TransactionsCursor).HasMaxLength(512);
|
||||
|
||||
builder.HasIndex(item => item.PlaidItemId).IsUnique();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
using Asentrafin.Api.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Asentrafin.Api.Data.Configurations;
|
||||
|
||||
public class PlaidSecurityConfiguration : IEntityTypeConfiguration<PlaidSecurity>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PlaidSecurity> builder)
|
||||
{
|
||||
builder.ToTable("plaid_securities");
|
||||
builder.HasKey(security => security.Id);
|
||||
|
||||
builder.Property(security => security.PlaidSecurityId).HasMaxLength(128).IsRequired();
|
||||
builder.Property(security => security.Cusip).HasMaxLength(32);
|
||||
builder.Property(security => security.Isin).HasMaxLength(32);
|
||||
builder.Property(security => security.Sedol).HasMaxLength(32);
|
||||
builder.Property(security => security.TickerSymbol).HasMaxLength(32);
|
||||
builder.Property(security => security.Name).HasMaxLength(512);
|
||||
builder.Property(security => security.Type).HasMaxLength(64);
|
||||
builder.Property(security => security.IsoCurrencyCode).HasMaxLength(8);
|
||||
builder.Property(security => security.UnofficialCurrencyCode).HasMaxLength(32);
|
||||
builder.Property(security => security.RawJson).HasColumnType("jsonb");
|
||||
|
||||
builder.HasIndex(security => security.PlaidSecurityId).IsUnique();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
using Asentrafin.Api.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Asentrafin.Api.Data.Configurations;
|
||||
|
||||
public class PlaidTransactionConfiguration : IEntityTypeConfiguration<PlaidTransaction>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PlaidTransaction> builder)
|
||||
{
|
||||
builder.ToTable("plaid_transactions");
|
||||
builder.HasKey(transaction => transaction.Id);
|
||||
|
||||
builder.Property(transaction => transaction.PlaidTransactionId).HasMaxLength(128).IsRequired();
|
||||
builder.Property(transaction => transaction.AccountPlaidId).HasMaxLength(128).IsRequired();
|
||||
builder.Property(transaction => transaction.Name).HasMaxLength(512).IsRequired();
|
||||
builder.Property(transaction => transaction.MerchantName).HasMaxLength(256);
|
||||
builder.Property(transaction => transaction.IsoCurrencyCode).HasMaxLength(8);
|
||||
builder.Property(transaction => transaction.UnofficialCurrencyCode).HasMaxLength(32);
|
||||
builder.Property(transaction => transaction.PersonalFinanceCategoryPrimary).HasMaxLength(128);
|
||||
builder.Property(transaction => transaction.PersonalFinanceCategoryDetailed).HasMaxLength(128);
|
||||
builder.Property(transaction => transaction.Categories).HasColumnType("text[]");
|
||||
builder.Property(transaction => transaction.RawJson).HasColumnType("jsonb");
|
||||
|
||||
builder.HasIndex(transaction => transaction.PlaidTransactionId).IsUnique();
|
||||
builder.HasIndex(transaction => new { transaction.PlaidItemId, transaction.Date });
|
||||
builder.HasOne(transaction => transaction.PlaidItem)
|
||||
.WithMany(item => item.Transactions)
|
||||
.HasForeignKey(transaction => transaction.PlaidItemId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne(transaction => transaction.PlaidAccount)
|
||||
.WithMany(account => account.Transactions)
|
||||
.HasForeignKey(transaction => transaction.PlaidAccountId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
using Asentrafin.Api.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Asentrafin.Api.Data.Configurations;
|
||||
|
||||
public class TransactionCategoryConfiguration : IEntityTypeConfiguration<TransactionCategory>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TransactionCategory> builder)
|
||||
{
|
||||
builder.ToTable("transaction_categories");
|
||||
builder.HasKey(category => category.Id);
|
||||
|
||||
builder.Property(category => category.Name).HasMaxLength(256).IsRequired();
|
||||
builder.Property(category => category.Slug).HasMaxLength(256).IsRequired();
|
||||
builder.Property(category => category.PlaidPrimary).HasMaxLength(128);
|
||||
builder.Property(category => category.PlaidDetailed).HasMaxLength(128);
|
||||
|
||||
builder.HasIndex(category => category.Slug).IsUnique();
|
||||
builder.HasIndex(category => new { category.PlaidPrimary, category.PlaidDetailed });
|
||||
builder.HasOne(category => category.Parent)
|
||||
.WithMany(category => category.Children)
|
||||
.HasForeignKey(category => category.ParentId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
using Asentrafin.Api.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Asentrafin.Api.Data.Configurations;
|
||||
|
||||
public class TransactionClassificationConfiguration : IEntityTypeConfiguration<TransactionClassification>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TransactionClassification> builder)
|
||||
{
|
||||
builder.ToTable("transaction_classifications");
|
||||
builder.HasKey(classification => classification.Id);
|
||||
|
||||
builder.Property(classification => classification.Source).HasMaxLength(64).IsRequired();
|
||||
builder.Property(classification => classification.Notes).HasMaxLength(1024);
|
||||
|
||||
builder.HasIndex(classification => classification.PlaidTransactionId).IsUnique();
|
||||
builder.HasIndex(classification => classification.CategoryId);
|
||||
builder.HasOne(classification => classification.PlaidTransaction)
|
||||
.WithOne(transaction => transaction.Classification)
|
||||
.HasForeignKey<TransactionClassification>(classification => classification.PlaidTransactionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne(classification => classification.Category)
|
||||
.WithMany(category => category.Classifications)
|
||||
.HasForeignKey(classification => classification.CategoryId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,568 +0,0 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Asentrafin.Api.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Asentrafin.Api.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260707005627_AddPlaidIngestion")]
|
||||
partial class AddPlaidIngestion
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("app")
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidAccount", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("AvailableBalance")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateTimeOffset?>("BalanceLastUpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<decimal?>("CurrentBalance")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("IsoCurrencyCode")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<decimal?>("Limit")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Mask")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("OfficialName")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("PlaidAccountId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<Guid>("PlaidItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("RawJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Subtype")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("UnofficialCurrencyCode")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaidAccountId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("PlaidItemId");
|
||||
|
||||
b.ToTable("plaid_accounts", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidHolding", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("CostBasis")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<decimal?>("InstitutionPrice")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateOnly?>("InstitutionPriceAsOf")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<decimal?>("InstitutionValue")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("IsoCurrencyCode")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<Guid>("PlaidAccountId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("PlaidItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("PlaidSecurityId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal>("Quantity")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("RawJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("UnofficialCurrencyCode")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaidItemId");
|
||||
|
||||
b.HasIndex("PlaidSecurityId");
|
||||
|
||||
b.HasIndex("PlaidAccountId", "PlaidSecurityId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("plaid_holdings", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidInvestmentTransaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AccountPlaidId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<decimal?>("Fees")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("IsoCurrencyCode")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<Guid?>("PlaidAccountId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PlaidInvestmentTransactionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<Guid>("PlaidItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("PlaidSecurityId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("Price")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<decimal>("Quantity")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("RawJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("SecurityPlaidId")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("Subtype")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("UnofficialCurrencyCode")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaidAccountId");
|
||||
|
||||
b.HasIndex("PlaidInvestmentTransactionId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("PlaidSecurityId");
|
||||
|
||||
b.HasIndex("PlaidItemId", "Date");
|
||||
|
||||
b.ToTable("plaid_investment_transactions", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("InstitutionId")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("InstitutionName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastAccountsSyncedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastInvestmentsSyncedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastTransactionsSyncedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("PlaidItemId")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("TransactionsCursor")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaidItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("plaid_items", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidSecurity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("ClosePrice")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateOnly?>("ClosePriceAsOf")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Cusip")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Isin")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("IsoCurrencyCode")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("PlaidSecurityId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("RawJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Sedol")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("TickerSymbol")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("UnofficialCurrencyCode")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaidSecurityId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("plaid_securities", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidTransaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AccountPlaidId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateOnly?>("AuthorizedDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Categories")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<string>("IsoCurrencyCode")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<string>("MerchantName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<bool>("Pending")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PersonalFinanceCategoryDetailed")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("PersonalFinanceCategoryPrimary")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<Guid?>("PlaidAccountId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("PlaidItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PlaidTransactionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("RawJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTimeOffset?>("RemovedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UnofficialCurrencyCode")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaidAccountId");
|
||||
|
||||
b.HasIndex("PlaidTransactionId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("PlaidItemId", "Date");
|
||||
|
||||
b.ToTable("plaid_transactions", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidAccount", b =>
|
||||
{
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidItem", "PlaidItem")
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("PlaidItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("PlaidItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidHolding", b =>
|
||||
{
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidAccount", "PlaidAccount")
|
||||
.WithMany("Holdings")
|
||||
.HasForeignKey("PlaidAccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidItem", "PlaidItem")
|
||||
.WithMany("Holdings")
|
||||
.HasForeignKey("PlaidItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidSecurity", "PlaidSecurity")
|
||||
.WithMany("Holdings")
|
||||
.HasForeignKey("PlaidSecurityId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("PlaidAccount");
|
||||
|
||||
b.Navigation("PlaidItem");
|
||||
|
||||
b.Navigation("PlaidSecurity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidInvestmentTransaction", b =>
|
||||
{
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidAccount", "PlaidAccount")
|
||||
.WithMany("InvestmentTransactions")
|
||||
.HasForeignKey("PlaidAccountId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidItem", "PlaidItem")
|
||||
.WithMany("InvestmentTransactions")
|
||||
.HasForeignKey("PlaidItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidSecurity", "PlaidSecurity")
|
||||
.WithMany("InvestmentTransactions")
|
||||
.HasForeignKey("PlaidSecurityId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("PlaidAccount");
|
||||
|
||||
b.Navigation("PlaidItem");
|
||||
|
||||
b.Navigation("PlaidSecurity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidTransaction", b =>
|
||||
{
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidAccount", "PlaidAccount")
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("PlaidAccountId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidItem", "PlaidItem")
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("PlaidItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("PlaidAccount");
|
||||
|
||||
b.Navigation("PlaidItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidAccount", b =>
|
||||
{
|
||||
b.Navigation("Holdings");
|
||||
|
||||
b.Navigation("InvestmentTransactions");
|
||||
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidItem", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Holdings");
|
||||
|
||||
b.Navigation("InvestmentTransactions");
|
||||
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidSecurity", b =>
|
||||
{
|
||||
b.Navigation("Holdings");
|
||||
|
||||
b.Navigation("InvestmentTransactions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,362 +0,0 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Asentrafin.Api.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPlaidIngestion : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "app");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "plaid_items",
|
||||
schema: "app",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
AccessToken = table.Column<string>(type: "text", nullable: false),
|
||||
PlaidItemId = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||
InstitutionId = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||
InstitutionName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
TransactionsCursor = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
LastAccountsSyncedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
LastTransactionsSyncedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
LastInvestmentsSyncedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_plaid_items", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "plaid_securities",
|
||||
schema: "app",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
PlaidSecurityId = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
Cusip = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||
Isin = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||
Sedol = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||
TickerSymbol = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||
Name = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
|
||||
Type = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
ClosePrice = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
ClosePriceAsOf = table.Column<DateOnly>(type: "date", nullable: true),
|
||||
IsoCurrencyCode = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true),
|
||||
UnofficialCurrencyCode = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
RawJson = table.Column<string>(type: "jsonb", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_plaid_securities", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "plaid_accounts",
|
||||
schema: "app",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
PlaidItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
PlaidAccountId = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
OfficialName = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
|
||||
Mask = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||
Type = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Subtype = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
CurrentBalance = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
AvailableBalance = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
Limit = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
IsoCurrencyCode = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true),
|
||||
UnofficialCurrencyCode = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||
BalanceLastUpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
RawJson = table.Column<string>(type: "jsonb", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_plaid_accounts", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_plaid_accounts_plaid_items_PlaidItemId",
|
||||
column: x => x.PlaidItemId,
|
||||
principalSchema: "app",
|
||||
principalTable: "plaid_items",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "plaid_holdings",
|
||||
schema: "app",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
PlaidItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
PlaidAccountId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
PlaidSecurityId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Quantity = table.Column<decimal>(type: "numeric", nullable: false),
|
||||
InstitutionPrice = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
InstitutionValue = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
InstitutionPriceAsOf = table.Column<DateOnly>(type: "date", nullable: true),
|
||||
CostBasis = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
IsoCurrencyCode = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true),
|
||||
UnofficialCurrencyCode = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
RawJson = table.Column<string>(type: "jsonb", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_plaid_holdings", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_plaid_holdings_plaid_accounts_PlaidAccountId",
|
||||
column: x => x.PlaidAccountId,
|
||||
principalSchema: "app",
|
||||
principalTable: "plaid_accounts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_plaid_holdings_plaid_items_PlaidItemId",
|
||||
column: x => x.PlaidItemId,
|
||||
principalSchema: "app",
|
||||
principalTable: "plaid_items",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_plaid_holdings_plaid_securities_PlaidSecurityId",
|
||||
column: x => x.PlaidSecurityId,
|
||||
principalSchema: "app",
|
||||
principalTable: "plaid_securities",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "plaid_investment_transactions",
|
||||
schema: "app",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
PlaidItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
PlaidAccountId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
PlaidSecurityId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
PlaidInvestmentTransactionId = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
AccountPlaidId = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
SecurityPlaidId = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||
Date = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||
Quantity = table.Column<decimal>(type: "numeric", nullable: false),
|
||||
Amount = table.Column<decimal>(type: "numeric", nullable: false),
|
||||
Price = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
Fees = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
Type = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
Subtype = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
IsoCurrencyCode = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true),
|
||||
UnofficialCurrencyCode = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
RawJson = table.Column<string>(type: "jsonb", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_plaid_investment_transactions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_plaid_investment_transactions_plaid_accounts_PlaidAccountId",
|
||||
column: x => x.PlaidAccountId,
|
||||
principalSchema: "app",
|
||||
principalTable: "plaid_accounts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_plaid_investment_transactions_plaid_items_PlaidItemId",
|
||||
column: x => x.PlaidItemId,
|
||||
principalSchema: "app",
|
||||
principalTable: "plaid_items",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_plaid_investment_transactions_plaid_securities_PlaidSecurit~",
|
||||
column: x => x.PlaidSecurityId,
|
||||
principalSchema: "app",
|
||||
principalTable: "plaid_securities",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "plaid_transactions",
|
||||
schema: "app",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
PlaidItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
PlaidAccountId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
PlaidTransactionId = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
AccountPlaidId = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
Date = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
AuthorizedDate = table.Column<DateOnly>(type: "date", nullable: true),
|
||||
Name = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||
MerchantName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
Amount = table.Column<decimal>(type: "numeric", nullable: false),
|
||||
Pending = table.Column<bool>(type: "boolean", nullable: false),
|
||||
IsoCurrencyCode = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true),
|
||||
UnofficialCurrencyCode = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||
Categories = table.Column<string[]>(type: "text[]", nullable: false),
|
||||
PersonalFinanceCategoryPrimary = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||
PersonalFinanceCategoryDetailed = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
RemovedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
RawJson = table.Column<string>(type: "jsonb", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_plaid_transactions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_plaid_transactions_plaid_accounts_PlaidAccountId",
|
||||
column: x => x.PlaidAccountId,
|
||||
principalSchema: "app",
|
||||
principalTable: "plaid_accounts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_plaid_transactions_plaid_items_PlaidItemId",
|
||||
column: x => x.PlaidItemId,
|
||||
principalSchema: "app",
|
||||
principalTable: "plaid_items",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_plaid_accounts_PlaidAccountId",
|
||||
schema: "app",
|
||||
table: "plaid_accounts",
|
||||
column: "PlaidAccountId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_plaid_accounts_PlaidItemId",
|
||||
schema: "app",
|
||||
table: "plaid_accounts",
|
||||
column: "PlaidItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_plaid_holdings_PlaidAccountId_PlaidSecurityId",
|
||||
schema: "app",
|
||||
table: "plaid_holdings",
|
||||
columns: new[] { "PlaidAccountId", "PlaidSecurityId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_plaid_holdings_PlaidItemId",
|
||||
schema: "app",
|
||||
table: "plaid_holdings",
|
||||
column: "PlaidItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_plaid_holdings_PlaidSecurityId",
|
||||
schema: "app",
|
||||
table: "plaid_holdings",
|
||||
column: "PlaidSecurityId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_plaid_investment_transactions_PlaidAccountId",
|
||||
schema: "app",
|
||||
table: "plaid_investment_transactions",
|
||||
column: "PlaidAccountId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_plaid_investment_transactions_PlaidInvestmentTransactionId",
|
||||
schema: "app",
|
||||
table: "plaid_investment_transactions",
|
||||
column: "PlaidInvestmentTransactionId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_plaid_investment_transactions_PlaidItemId_Date",
|
||||
schema: "app",
|
||||
table: "plaid_investment_transactions",
|
||||
columns: new[] { "PlaidItemId", "Date" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_plaid_investment_transactions_PlaidSecurityId",
|
||||
schema: "app",
|
||||
table: "plaid_investment_transactions",
|
||||
column: "PlaidSecurityId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_plaid_items_PlaidItemId",
|
||||
schema: "app",
|
||||
table: "plaid_items",
|
||||
column: "PlaidItemId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_plaid_securities_PlaidSecurityId",
|
||||
schema: "app",
|
||||
table: "plaid_securities",
|
||||
column: "PlaidSecurityId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_plaid_transactions_PlaidAccountId",
|
||||
schema: "app",
|
||||
table: "plaid_transactions",
|
||||
column: "PlaidAccountId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_plaid_transactions_PlaidItemId_Date",
|
||||
schema: "app",
|
||||
table: "plaid_transactions",
|
||||
columns: new[] { "PlaidItemId", "Date" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_plaid_transactions_PlaidTransactionId",
|
||||
schema: "app",
|
||||
table: "plaid_transactions",
|
||||
column: "PlaidTransactionId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "plaid_holdings",
|
||||
schema: "app");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "plaid_investment_transactions",
|
||||
schema: "app");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "plaid_transactions",
|
||||
schema: "app");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "plaid_securities",
|
||||
schema: "app");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "plaid_accounts",
|
||||
schema: "app");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "plaid_items",
|
||||
schema: "app");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,700 +0,0 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Asentrafin.Api.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Asentrafin.Api.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260708000510_AddTransactionCategorization")]
|
||||
partial class AddTransactionCategorization
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("app")
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidAccount", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("AvailableBalance")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateTimeOffset?>("BalanceLastUpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<decimal?>("CurrentBalance")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("IsoCurrencyCode")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<decimal?>("Limit")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Mask")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("OfficialName")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("PlaidAccountId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<Guid>("PlaidItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("RawJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Subtype")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("UnofficialCurrencyCode")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaidAccountId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("PlaidItemId");
|
||||
|
||||
b.ToTable("plaid_accounts", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidHolding", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("CostBasis")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<decimal?>("InstitutionPrice")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateOnly?>("InstitutionPriceAsOf")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<decimal?>("InstitutionValue")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("IsoCurrencyCode")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<Guid>("PlaidAccountId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("PlaidItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("PlaidSecurityId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal>("Quantity")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("RawJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("UnofficialCurrencyCode")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaidItemId");
|
||||
|
||||
b.HasIndex("PlaidSecurityId");
|
||||
|
||||
b.HasIndex("PlaidAccountId", "PlaidSecurityId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("plaid_holdings", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidInvestmentTransaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AccountPlaidId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<decimal?>("Fees")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("IsoCurrencyCode")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<Guid?>("PlaidAccountId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PlaidInvestmentTransactionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<Guid>("PlaidItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("PlaidSecurityId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("Price")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<decimal>("Quantity")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("RawJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("SecurityPlaidId")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("Subtype")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("UnofficialCurrencyCode")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaidAccountId");
|
||||
|
||||
b.HasIndex("PlaidInvestmentTransactionId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("PlaidSecurityId");
|
||||
|
||||
b.HasIndex("PlaidItemId", "Date");
|
||||
|
||||
b.ToTable("plaid_investment_transactions", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("InstitutionId")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("InstitutionName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastAccountsSyncedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastInvestmentsSyncedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastTransactionsSyncedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("PlaidItemId")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("TransactionsCursor")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaidItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("plaid_items", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidSecurity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("ClosePrice")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateOnly?>("ClosePriceAsOf")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Cusip")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Isin")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("IsoCurrencyCode")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("PlaidSecurityId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("RawJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Sedol")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("TickerSymbol")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("UnofficialCurrencyCode")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaidSecurityId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("plaid_securities", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidTransaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AccountPlaidId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateOnly?>("AuthorizedDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Categories")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<string>("IsoCurrencyCode")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<string>("MerchantName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<bool>("Pending")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PersonalFinanceCategoryDetailed")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("PersonalFinanceCategoryPrimary")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<Guid?>("PlaidAccountId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("PlaidItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PlaidTransactionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("RawJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTimeOffset?>("RemovedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UnofficialCurrencyCode")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaidAccountId");
|
||||
|
||||
b.HasIndex("PlaidTransactionId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("PlaidItemId", "Date");
|
||||
|
||||
b.ToTable("plaid_transactions", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.TransactionCategory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PlaidDetailed")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("PlaidPrimary")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("PlaidPrimary", "PlaidDetailed");
|
||||
|
||||
b.ToTable("transaction_categories", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.TransactionClassification", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("CategoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("Confidence")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<Guid>("PlaidTransactionId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("PlaidTransactionId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("transaction_classifications", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidAccount", b =>
|
||||
{
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidItem", "PlaidItem")
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("PlaidItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("PlaidItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidHolding", b =>
|
||||
{
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidAccount", "PlaidAccount")
|
||||
.WithMany("Holdings")
|
||||
.HasForeignKey("PlaidAccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidItem", "PlaidItem")
|
||||
.WithMany("Holdings")
|
||||
.HasForeignKey("PlaidItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidSecurity", "PlaidSecurity")
|
||||
.WithMany("Holdings")
|
||||
.HasForeignKey("PlaidSecurityId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("PlaidAccount");
|
||||
|
||||
b.Navigation("PlaidItem");
|
||||
|
||||
b.Navigation("PlaidSecurity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidInvestmentTransaction", b =>
|
||||
{
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidAccount", "PlaidAccount")
|
||||
.WithMany("InvestmentTransactions")
|
||||
.HasForeignKey("PlaidAccountId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidItem", "PlaidItem")
|
||||
.WithMany("InvestmentTransactions")
|
||||
.HasForeignKey("PlaidItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidSecurity", "PlaidSecurity")
|
||||
.WithMany("InvestmentTransactions")
|
||||
.HasForeignKey("PlaidSecurityId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("PlaidAccount");
|
||||
|
||||
b.Navigation("PlaidItem");
|
||||
|
||||
b.Navigation("PlaidSecurity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidTransaction", b =>
|
||||
{
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidAccount", "PlaidAccount")
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("PlaidAccountId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidItem", "PlaidItem")
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("PlaidItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("PlaidAccount");
|
||||
|
||||
b.Navigation("PlaidItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.TransactionCategory", b =>
|
||||
{
|
||||
b.HasOne("Asentrafin.Api.Entities.TransactionCategory", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.TransactionClassification", b =>
|
||||
{
|
||||
b.HasOne("Asentrafin.Api.Entities.TransactionCategory", "Category")
|
||||
.WithMany("Classifications")
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidTransaction", "PlaidTransaction")
|
||||
.WithOne("Classification")
|
||||
.HasForeignKey("Asentrafin.Api.Entities.TransactionClassification", "PlaidTransactionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("PlaidTransaction");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidAccount", b =>
|
||||
{
|
||||
b.Navigation("Holdings");
|
||||
|
||||
b.Navigation("InvestmentTransactions");
|
||||
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidItem", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Holdings");
|
||||
|
||||
b.Navigation("InvestmentTransactions");
|
||||
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidSecurity", b =>
|
||||
{
|
||||
b.Navigation("Holdings");
|
||||
|
||||
b.Navigation("InvestmentTransactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidTransaction", b =>
|
||||
{
|
||||
b.Navigation("Classification");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.TransactionCategory", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("Classifications");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Asentrafin.Api.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTransactionCategorization : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "transaction_categories",
|
||||
schema: "app",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ParentId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
Slug = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
PlaidPrimary = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||
PlaidDetailed = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||
SortOrder = table.Column<int>(type: "integer", nullable: false),
|
||||
IsSystem = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_transaction_categories", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_transaction_categories_transaction_categories_ParentId",
|
||||
column: x => x.ParentId,
|
||||
principalSchema: "app",
|
||||
principalTable: "transaction_categories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "transaction_classifications",
|
||||
schema: "app",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
PlaidTransactionId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CategoryId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Source = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Confidence = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
Notes = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_transaction_classifications", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_transaction_classifications_plaid_transactions_PlaidTransac~",
|
||||
column: x => x.PlaidTransactionId,
|
||||
principalSchema: "app",
|
||||
principalTable: "plaid_transactions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_transaction_classifications_transaction_categories_Category~",
|
||||
column: x => x.CategoryId,
|
||||
principalSchema: "app",
|
||||
principalTable: "transaction_categories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_transaction_categories_ParentId",
|
||||
schema: "app",
|
||||
table: "transaction_categories",
|
||||
column: "ParentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_transaction_categories_PlaidPrimary_PlaidDetailed",
|
||||
schema: "app",
|
||||
table: "transaction_categories",
|
||||
columns: new[] { "PlaidPrimary", "PlaidDetailed" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_transaction_categories_Slug",
|
||||
schema: "app",
|
||||
table: "transaction_categories",
|
||||
column: "Slug",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_transaction_classifications_CategoryId",
|
||||
schema: "app",
|
||||
table: "transaction_classifications",
|
||||
column: "CategoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_transaction_classifications_PlaidTransactionId",
|
||||
schema: "app",
|
||||
table: "transaction_classifications",
|
||||
column: "PlaidTransactionId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "transaction_classifications",
|
||||
schema: "app");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "transaction_categories",
|
||||
schema: "app");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,697 +0,0 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Asentrafin.Api.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Asentrafin.Api.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
partial class AppDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("app")
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidAccount", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("AvailableBalance")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateTimeOffset?>("BalanceLastUpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<decimal?>("CurrentBalance")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("IsoCurrencyCode")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<decimal?>("Limit")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Mask")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("OfficialName")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("PlaidAccountId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<Guid>("PlaidItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("RawJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Subtype")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("UnofficialCurrencyCode")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaidAccountId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("PlaidItemId");
|
||||
|
||||
b.ToTable("plaid_accounts", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidHolding", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("CostBasis")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<decimal?>("InstitutionPrice")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateOnly?>("InstitutionPriceAsOf")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<decimal?>("InstitutionValue")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("IsoCurrencyCode")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<Guid>("PlaidAccountId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("PlaidItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("PlaidSecurityId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal>("Quantity")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("RawJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("UnofficialCurrencyCode")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaidItemId");
|
||||
|
||||
b.HasIndex("PlaidSecurityId");
|
||||
|
||||
b.HasIndex("PlaidAccountId", "PlaidSecurityId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("plaid_holdings", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidInvestmentTransaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AccountPlaidId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<decimal?>("Fees")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("IsoCurrencyCode")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<Guid?>("PlaidAccountId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PlaidInvestmentTransactionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<Guid>("PlaidItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("PlaidSecurityId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("Price")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<decimal>("Quantity")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("RawJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("SecurityPlaidId")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("Subtype")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("UnofficialCurrencyCode")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaidAccountId");
|
||||
|
||||
b.HasIndex("PlaidInvestmentTransactionId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("PlaidSecurityId");
|
||||
|
||||
b.HasIndex("PlaidItemId", "Date");
|
||||
|
||||
b.ToTable("plaid_investment_transactions", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("InstitutionId")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("InstitutionName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastAccountsSyncedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastInvestmentsSyncedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastTransactionsSyncedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("PlaidItemId")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("TransactionsCursor")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaidItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("plaid_items", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidSecurity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("ClosePrice")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateOnly?>("ClosePriceAsOf")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Cusip")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Isin")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("IsoCurrencyCode")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("PlaidSecurityId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("RawJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Sedol")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("TickerSymbol")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("UnofficialCurrencyCode")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaidSecurityId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("plaid_securities", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidTransaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AccountPlaidId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateOnly?>("AuthorizedDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Categories")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<string>("IsoCurrencyCode")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<string>("MerchantName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<bool>("Pending")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PersonalFinanceCategoryDetailed")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("PersonalFinanceCategoryPrimary")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<Guid?>("PlaidAccountId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("PlaidItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PlaidTransactionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("RawJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTimeOffset?>("RemovedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UnofficialCurrencyCode")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaidAccountId");
|
||||
|
||||
b.HasIndex("PlaidTransactionId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("PlaidItemId", "Date");
|
||||
|
||||
b.ToTable("plaid_transactions", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.TransactionCategory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PlaidDetailed")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("PlaidPrimary")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("PlaidPrimary", "PlaidDetailed");
|
||||
|
||||
b.ToTable("transaction_categories", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.TransactionClassification", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("CategoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("Confidence")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<Guid>("PlaidTransactionId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("PlaidTransactionId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("transaction_classifications", "app");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidAccount", b =>
|
||||
{
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidItem", "PlaidItem")
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("PlaidItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("PlaidItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidHolding", b =>
|
||||
{
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidAccount", "PlaidAccount")
|
||||
.WithMany("Holdings")
|
||||
.HasForeignKey("PlaidAccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidItem", "PlaidItem")
|
||||
.WithMany("Holdings")
|
||||
.HasForeignKey("PlaidItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidSecurity", "PlaidSecurity")
|
||||
.WithMany("Holdings")
|
||||
.HasForeignKey("PlaidSecurityId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("PlaidAccount");
|
||||
|
||||
b.Navigation("PlaidItem");
|
||||
|
||||
b.Navigation("PlaidSecurity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidInvestmentTransaction", b =>
|
||||
{
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidAccount", "PlaidAccount")
|
||||
.WithMany("InvestmentTransactions")
|
||||
.HasForeignKey("PlaidAccountId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidItem", "PlaidItem")
|
||||
.WithMany("InvestmentTransactions")
|
||||
.HasForeignKey("PlaidItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidSecurity", "PlaidSecurity")
|
||||
.WithMany("InvestmentTransactions")
|
||||
.HasForeignKey("PlaidSecurityId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("PlaidAccount");
|
||||
|
||||
b.Navigation("PlaidItem");
|
||||
|
||||
b.Navigation("PlaidSecurity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidTransaction", b =>
|
||||
{
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidAccount", "PlaidAccount")
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("PlaidAccountId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidItem", "PlaidItem")
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("PlaidItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("PlaidAccount");
|
||||
|
||||
b.Navigation("PlaidItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.TransactionCategory", b =>
|
||||
{
|
||||
b.HasOne("Asentrafin.Api.Entities.TransactionCategory", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.TransactionClassification", b =>
|
||||
{
|
||||
b.HasOne("Asentrafin.Api.Entities.TransactionCategory", "Category")
|
||||
.WithMany("Classifications")
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Asentrafin.Api.Entities.PlaidTransaction", "PlaidTransaction")
|
||||
.WithOne("Classification")
|
||||
.HasForeignKey("Asentrafin.Api.Entities.TransactionClassification", "PlaidTransactionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("PlaidTransaction");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidAccount", b =>
|
||||
{
|
||||
b.Navigation("Holdings");
|
||||
|
||||
b.Navigation("InvestmentTransactions");
|
||||
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidItem", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
|
||||
b.Navigation("Holdings");
|
||||
|
||||
b.Navigation("InvestmentTransactions");
|
||||
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidSecurity", b =>
|
||||
{
|
||||
b.Navigation("Holdings");
|
||||
|
||||
b.Navigation("InvestmentTransactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.PlaidTransaction", b =>
|
||||
{
|
||||
b.Navigation("Classification");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Asentrafin.Api.Entities.TransactionCategory", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("Classifications");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
namespace Asentrafin.Api.Entities;
|
||||
|
||||
public class PlaidAccount
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid PlaidItemId { get; set; }
|
||||
public PlaidItem PlaidItem { get; set; } = null!;
|
||||
public string PlaidAccountId { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? OfficialName { get; set; }
|
||||
public string? Mask { get; set; }
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string? Subtype { get; set; }
|
||||
public decimal? CurrentBalance { get; set; }
|
||||
public decimal? AvailableBalance { get; set; }
|
||||
public decimal? Limit { get; set; }
|
||||
public string? IsoCurrencyCode { get; set; }
|
||||
public string? UnofficialCurrencyCode { get; set; }
|
||||
public DateTimeOffset? BalanceLastUpdatedAt { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public string RawJson { get; set; } = "{}";
|
||||
|
||||
public ICollection<PlaidTransaction> Transactions { get; set; } = [];
|
||||
public ICollection<PlaidHolding> Holdings { get; set; } = [];
|
||||
public ICollection<PlaidInvestmentTransaction> InvestmentTransactions { get; set; } = [];
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
namespace Asentrafin.Api.Entities;
|
||||
|
||||
public class PlaidHolding
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid PlaidItemId { get; set; }
|
||||
public PlaidItem PlaidItem { get; set; } = null!;
|
||||
public Guid PlaidAccountId { get; set; }
|
||||
public PlaidAccount PlaidAccount { get; set; } = null!;
|
||||
public Guid PlaidSecurityId { get; set; }
|
||||
public PlaidSecurity PlaidSecurity { get; set; } = null!;
|
||||
public decimal Quantity { get; set; }
|
||||
public decimal? InstitutionPrice { get; set; }
|
||||
public decimal? InstitutionValue { get; set; }
|
||||
public DateOnly? InstitutionPriceAsOf { get; set; }
|
||||
public decimal? CostBasis { get; set; }
|
||||
public string? IsoCurrencyCode { get; set; }
|
||||
public string? UnofficialCurrencyCode { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public string RawJson { get; set; } = "{}";
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
namespace Asentrafin.Api.Entities;
|
||||
|
||||
public class PlaidInvestmentTransaction
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid PlaidItemId { get; set; }
|
||||
public PlaidItem PlaidItem { get; set; } = null!;
|
||||
public Guid? PlaidAccountId { get; set; }
|
||||
public PlaidAccount? PlaidAccount { get; set; }
|
||||
public Guid? PlaidSecurityId { get; set; }
|
||||
public PlaidSecurity? PlaidSecurity { get; set; }
|
||||
public string PlaidInvestmentTransactionId { get; set; } = string.Empty;
|
||||
public string AccountPlaidId { get; set; } = string.Empty;
|
||||
public string? SecurityPlaidId { get; set; }
|
||||
public DateOnly Date { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public decimal Quantity { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public decimal? Price { get; set; }
|
||||
public decimal? Fees { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public string? Subtype { get; set; }
|
||||
public string? IsoCurrencyCode { get; set; }
|
||||
public string? UnofficialCurrencyCode { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public string RawJson { get; set; } = "{}";
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
namespace Asentrafin.Api.Entities;
|
||||
|
||||
public class PlaidItem
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string? PlaidItemId { get; set; }
|
||||
public string? InstitutionId { get; set; }
|
||||
public string? InstitutionName { get; set; }
|
||||
public string? TransactionsCursor { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset? LastAccountsSyncedAt { get; set; }
|
||||
public DateTimeOffset? LastTransactionsSyncedAt { get; set; }
|
||||
public DateTimeOffset? LastInvestmentsSyncedAt { get; set; }
|
||||
|
||||
public ICollection<PlaidAccount> Accounts { get; set; } = [];
|
||||
public ICollection<PlaidTransaction> Transactions { get; set; } = [];
|
||||
public ICollection<PlaidHolding> Holdings { get; set; } = [];
|
||||
public ICollection<PlaidInvestmentTransaction> InvestmentTransactions { get; set; } = [];
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
namespace Asentrafin.Api.Entities;
|
||||
|
||||
public class PlaidSecurity
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public string PlaidSecurityId { get; set; } = string.Empty;
|
||||
public string? Cusip { get; set; }
|
||||
public string? Isin { get; set; }
|
||||
public string? Sedol { get; set; }
|
||||
public string? TickerSymbol { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public decimal? ClosePrice { get; set; }
|
||||
public DateOnly? ClosePriceAsOf { get; set; }
|
||||
public string? IsoCurrencyCode { get; set; }
|
||||
public string? UnofficialCurrencyCode { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public string RawJson { get; set; } = "{}";
|
||||
|
||||
public ICollection<PlaidHolding> Holdings { get; set; } = [];
|
||||
public ICollection<PlaidInvestmentTransaction> InvestmentTransactions { get; set; } = [];
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
namespace Asentrafin.Api.Entities;
|
||||
|
||||
public class PlaidTransaction
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid PlaidItemId { get; set; }
|
||||
public PlaidItem PlaidItem { get; set; } = null!;
|
||||
public Guid? PlaidAccountId { get; set; }
|
||||
public PlaidAccount? PlaidAccount { get; set; }
|
||||
public string PlaidTransactionId { get; set; } = string.Empty;
|
||||
public string AccountPlaidId { get; set; } = string.Empty;
|
||||
public DateOnly Date { get; set; }
|
||||
public DateOnly? AuthorizedDate { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? MerchantName { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public bool Pending { get; set; }
|
||||
public string? IsoCurrencyCode { get; set; }
|
||||
public string? UnofficialCurrencyCode { get; set; }
|
||||
public string[] Categories { get; set; } = [];
|
||||
public string? PersonalFinanceCategoryPrimary { get; set; }
|
||||
public string? PersonalFinanceCategoryDetailed { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset? RemovedAt { get; set; }
|
||||
public string RawJson { get; set; } = "{}";
|
||||
public TransactionClassification? Classification { get; set; }
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
namespace Asentrafin.Api.Entities;
|
||||
|
||||
public class TransactionCategory
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid? ParentId { get; set; }
|
||||
public TransactionCategory? Parent { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
public string? PlaidPrimary { get; set; }
|
||||
public string? PlaidDetailed { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsSystem { get; set; } = true;
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
|
||||
public ICollection<TransactionCategory> Children { get; set; } = [];
|
||||
public ICollection<TransactionClassification> Classifications { get; set; } = [];
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
namespace Asentrafin.Api.Entities;
|
||||
|
||||
public class TransactionClassification
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid PlaidTransactionId { get; set; }
|
||||
public PlaidTransaction PlaidTransaction { get; set; } = null!;
|
||||
public Guid CategoryId { get; set; }
|
||||
public TransactionCategory Category { get; set; } = null!;
|
||||
public string Source { get; set; } = "plaid";
|
||||
public decimal? Confidence { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
|
@ -1,26 +1,7 @@
|
|||
using Asentrafin.Api.Data;
|
||||
using Asentrafin.Api.Configuration;
|
||||
using Asentrafin.Api.Services.Plaid;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
|
||||
?? throw new InvalidOperationException("Connection string 'DefaultConnection' was not found.");
|
||||
|
||||
builder.Services.AddDbContextPool<AppDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
|
||||
builder.Services.Configure<PlaidOptions>(builder.Configuration.GetSection("Plaid"));
|
||||
builder.Services.AddHttpClient<IPlaidClient, PlaidClient>((serviceProvider, client) =>
|
||||
{
|
||||
var options = serviceProvider.GetRequiredService<Microsoft.Extensions.Options.IOptions<PlaidOptions>>().Value;
|
||||
client.BaseAddress = new Uri(options.BaseUrl);
|
||||
});
|
||||
builder.Services.AddScoped<IPlaidIngestionService, PlaidIngestionService>();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(options =>
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
using System.Text.Json;
|
||||
|
||||
namespace Asentrafin.Api.Services.Plaid;
|
||||
|
||||
public interface IPlaidClient
|
||||
{
|
||||
Task<JsonDocument> AccountsGetAsync(string accessToken, CancellationToken cancellationToken);
|
||||
Task<JsonDocument> TransactionsSyncAsync(string accessToken, string? cursor, CancellationToken cancellationToken);
|
||||
Task<JsonDocument> InvestmentsHoldingsGetAsync(string accessToken, CancellationToken cancellationToken);
|
||||
Task<JsonDocument> InvestmentsTransactionsGetAsync(
|
||||
string accessToken,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
int count,
|
||||
int offset,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
using Asentrafin.Api.Contracts.Plaid;
|
||||
|
||||
namespace Asentrafin.Api.Services.Plaid;
|
||||
|
||||
public interface IPlaidIngestionService
|
||||
{
|
||||
Task<Guid> CreateItemAsync(CreatePlaidItemRequest request, CancellationToken cancellationToken);
|
||||
Task<IngestionSummary> IngestAccountsAsync(Guid itemId, CancellationToken cancellationToken);
|
||||
Task<IngestionSummary> IngestTransactionsAsync(Guid itemId, CancellationToken cancellationToken);
|
||||
Task<IngestionSummary> IngestInvestmentsAsync(
|
||||
Guid itemId,
|
||||
DateOnly? startDate,
|
||||
DateOnly? endDate,
|
||||
CancellationToken cancellationToken);
|
||||
Task<IngestionSummary> IngestAllAsync(
|
||||
Guid itemId,
|
||||
DateOnly? investmentStartDate,
|
||||
DateOnly? investmentEndDate,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Asentrafin.Api.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Asentrafin.Api.Services.Plaid;
|
||||
|
||||
public class PlaidClient(HttpClient httpClient, IOptions<PlaidOptions> options) : IPlaidClient
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
private readonly PlaidOptions _options = options.Value;
|
||||
|
||||
public Task<JsonDocument> AccountsGetAsync(string accessToken, CancellationToken cancellationToken)
|
||||
{
|
||||
return PostAsync("/accounts/get", BuildAuthedRequest(accessToken), cancellationToken);
|
||||
}
|
||||
|
||||
public Task<JsonDocument> TransactionsSyncAsync(string accessToken, string? cursor, CancellationToken cancellationToken)
|
||||
{
|
||||
var body = BuildAuthedRequest(accessToken);
|
||||
body["cursor"] = cursor;
|
||||
body["count"] = 500;
|
||||
body["options"] = new Dictionary<string, object?>
|
||||
{
|
||||
["include_original_description"] = true,
|
||||
["personal_finance_category_version"] = "v2"
|
||||
};
|
||||
|
||||
return PostAsync("/transactions/sync", body, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<JsonDocument> InvestmentsHoldingsGetAsync(string accessToken, CancellationToken cancellationToken)
|
||||
{
|
||||
return PostAsync("/investments/holdings/get", BuildAuthedRequest(accessToken), cancellationToken);
|
||||
}
|
||||
|
||||
public Task<JsonDocument> InvestmentsTransactionsGetAsync(
|
||||
string accessToken,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
int count,
|
||||
int offset,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var body = BuildAuthedRequest(accessToken);
|
||||
body["start_date"] = startDate.ToString("yyyy-MM-dd");
|
||||
body["end_date"] = endDate.ToString("yyyy-MM-dd");
|
||||
body["options"] = new Dictionary<string, object?>
|
||||
{
|
||||
["count"] = count,
|
||||
["offset"] = offset
|
||||
};
|
||||
|
||||
return PostAsync("/investments/transactions/get", body, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<JsonDocument> PostAsync(
|
||||
string path,
|
||||
Dictionary<string, object?> body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
EnsureConfigured();
|
||||
|
||||
using var response = await httpClient.PostAsJsonAsync(path, body, JsonOptions, cancellationToken);
|
||||
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException(
|
||||
$"Plaid request to {path} failed with {(int)response.StatusCode}: {responseBody}",
|
||||
null,
|
||||
response.StatusCode);
|
||||
}
|
||||
|
||||
return JsonDocument.Parse(responseBody);
|
||||
}
|
||||
|
||||
private Dictionary<string, object?> BuildAuthedRequest(string accessToken)
|
||||
{
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
["client_id"] = _options.ClientId,
|
||||
["secret"] = _options.Secret,
|
||||
["access_token"] = accessToken
|
||||
};
|
||||
}
|
||||
|
||||
private void EnsureConfigured()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_options.ClientId) || string.IsNullOrWhiteSpace(_options.Secret))
|
||||
{
|
||||
throw new InvalidOperationException("Plaid ClientId and Secret must be configured.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,617 +0,0 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using Asentrafin.Api.Contracts.Plaid;
|
||||
using Asentrafin.Api.Data;
|
||||
using Asentrafin.Api.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Asentrafin.Api.Services.Plaid;
|
||||
|
||||
public class PlaidIngestionService(AppDbContext dbContext, IPlaidClient plaidClient) : IPlaidIngestionService
|
||||
{
|
||||
private static readonly Regex SlugSanitizer = new("[^a-z0-9]+", RegexOptions.Compiled);
|
||||
|
||||
public async Task<Guid> CreateItemAsync(CreatePlaidItemRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.AccessToken))
|
||||
{
|
||||
throw new ArgumentException("AccessToken is required.", nameof(request));
|
||||
}
|
||||
|
||||
var existingItem = request.PlaidItemId is null
|
||||
? null
|
||||
: await dbContext.PlaidItems
|
||||
.SingleOrDefaultAsync(item => item.PlaidItemId == request.PlaidItemId, cancellationToken);
|
||||
|
||||
if (existingItem is not null)
|
||||
{
|
||||
existingItem.AccessToken = request.AccessToken;
|
||||
existingItem.InstitutionId = request.InstitutionId;
|
||||
existingItem.InstitutionName = request.InstitutionName;
|
||||
existingItem.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return existingItem.Id;
|
||||
}
|
||||
|
||||
var item = new PlaidItem
|
||||
{
|
||||
AccessToken = request.AccessToken,
|
||||
PlaidItemId = request.PlaidItemId,
|
||||
InstitutionId = request.InstitutionId,
|
||||
InstitutionName = request.InstitutionName
|
||||
};
|
||||
|
||||
dbContext.PlaidItems.Add(item);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return item.Id;
|
||||
}
|
||||
|
||||
public async Task<IngestionSummary> IngestAccountsAsync(Guid itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
var item = await GetItemAsync(itemId, cancellationToken);
|
||||
using var response = await plaidClient.AccountsGetAsync(item.AccessToken, cancellationToken);
|
||||
|
||||
var accountsUpserted = await UpsertAccountsAsync(item, response.RootElement, cancellationToken);
|
||||
item.LastAccountsSyncedAt = DateTimeOffset.UtcNow;
|
||||
item.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new IngestionSummary(item.Id, accountsUpserted, 0, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
public async Task<IngestionSummary> IngestTransactionsAsync(Guid itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
var item = await GetItemAsync(itemId, cancellationToken);
|
||||
var cursor = item.TransactionsCursor;
|
||||
var accountsUpserted = 0;
|
||||
var transactionsUpserted = 0;
|
||||
var transactionsRemoved = 0;
|
||||
var hasMore = true;
|
||||
|
||||
while (hasMore)
|
||||
{
|
||||
using var response = await plaidClient.TransactionsSyncAsync(item.AccessToken, cursor, cancellationToken);
|
||||
var root = response.RootElement;
|
||||
|
||||
accountsUpserted += await UpsertAccountsAsync(item, root, cancellationToken);
|
||||
transactionsUpserted += await UpsertTransactionsAsync(item, root, "added", cancellationToken);
|
||||
transactionsUpserted += await UpsertTransactionsAsync(item, root, "modified", cancellationToken);
|
||||
transactionsRemoved += await MarkTransactionsRemovedAsync(root, cancellationToken);
|
||||
|
||||
cursor = root.String("next_cursor") ?? cursor;
|
||||
hasMore = root.Property("has_more") is { ValueKind: JsonValueKind.True };
|
||||
}
|
||||
|
||||
item.TransactionsCursor = cursor;
|
||||
item.LastTransactionsSyncedAt = DateTimeOffset.UtcNow;
|
||||
item.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new IngestionSummary(item.Id, accountsUpserted, transactionsUpserted, transactionsRemoved, 0, 0, 0);
|
||||
}
|
||||
|
||||
public async Task<IngestionSummary> IngestInvestmentsAsync(
|
||||
Guid itemId,
|
||||
DateOnly? startDate,
|
||||
DateOnly? endDate,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var item = await GetItemAsync(itemId, cancellationToken);
|
||||
using var holdingsResponse = await plaidClient.InvestmentsHoldingsGetAsync(item.AccessToken, cancellationToken);
|
||||
|
||||
var accountsUpserted = await UpsertAccountsAsync(item, holdingsResponse.RootElement, cancellationToken);
|
||||
var securitiesUpserted = await UpsertSecuritiesAsync(holdingsResponse.RootElement, cancellationToken);
|
||||
var holdingsUpserted = await UpsertHoldingsAsync(item, holdingsResponse.RootElement, cancellationToken);
|
||||
|
||||
var rangeEnd = endDate ?? DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
var rangeStart = startDate ?? rangeEnd.AddYears(-2);
|
||||
|
||||
var investmentTransactionsUpserted = 0;
|
||||
var count = 500;
|
||||
var offset = 0;
|
||||
var totalInvestmentTransactions = 0;
|
||||
|
||||
do
|
||||
{
|
||||
using var transactionsResponse = await plaidClient.InvestmentsTransactionsGetAsync(
|
||||
item.AccessToken,
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
count,
|
||||
offset,
|
||||
cancellationToken);
|
||||
|
||||
accountsUpserted += await UpsertAccountsAsync(item, transactionsResponse.RootElement, cancellationToken);
|
||||
securitiesUpserted += await UpsertSecuritiesAsync(transactionsResponse.RootElement, cancellationToken);
|
||||
investmentTransactionsUpserted += await UpsertInvestmentTransactionsAsync(
|
||||
item,
|
||||
transactionsResponse.RootElement,
|
||||
cancellationToken);
|
||||
|
||||
totalInvestmentTransactions = transactionsResponse.RootElement.Int("total_investment_transactions") ?? 0;
|
||||
offset += count;
|
||||
}
|
||||
while (offset < totalInvestmentTransactions);
|
||||
|
||||
item.LastInvestmentsSyncedAt = DateTimeOffset.UtcNow;
|
||||
item.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new IngestionSummary(
|
||||
item.Id,
|
||||
accountsUpserted,
|
||||
0,
|
||||
0,
|
||||
securitiesUpserted,
|
||||
holdingsUpserted,
|
||||
investmentTransactionsUpserted);
|
||||
}
|
||||
|
||||
public async Task<IngestionSummary> IngestAllAsync(
|
||||
Guid itemId,
|
||||
DateOnly? investmentStartDate,
|
||||
DateOnly? investmentEndDate,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var accounts = await IngestAccountsAsync(itemId, cancellationToken);
|
||||
var transactions = await IngestTransactionsAsync(itemId, cancellationToken);
|
||||
var investments = await IngestInvestmentsAsync(itemId, investmentStartDate, investmentEndDate, cancellationToken);
|
||||
|
||||
return new IngestionSummary(
|
||||
itemId,
|
||||
accounts.AccountsUpserted + transactions.AccountsUpserted + investments.AccountsUpserted,
|
||||
transactions.TransactionsAddedOrModified,
|
||||
transactions.TransactionsRemoved,
|
||||
investments.SecuritiesUpserted,
|
||||
investments.HoldingsUpserted,
|
||||
investments.InvestmentTransactionsUpserted);
|
||||
}
|
||||
|
||||
private async Task<PlaidItem> GetItemAsync(Guid itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
return await dbContext.PlaidItems.SingleOrDefaultAsync(item => item.Id == itemId, cancellationToken)
|
||||
?? throw new KeyNotFoundException($"Plaid item '{itemId}' was not found.");
|
||||
}
|
||||
|
||||
private async Task<int> UpsertAccountsAsync(
|
||||
PlaidItem item,
|
||||
JsonElement root,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var accounts = root.Property("accounts");
|
||||
if (accounts is not { ValueKind: JsonValueKind.Array })
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var existingAccounts = await dbContext.PlaidAccounts
|
||||
.Where(account => account.PlaidItemId == item.Id)
|
||||
.ToDictionaryAsync(account => account.PlaidAccountId, cancellationToken);
|
||||
|
||||
var upserted = 0;
|
||||
foreach (var accountJson in accounts.Value.EnumerateArray())
|
||||
{
|
||||
var plaidAccountId = accountJson.String("account_id");
|
||||
if (string.IsNullOrWhiteSpace(plaidAccountId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!existingAccounts.TryGetValue(plaidAccountId, out var account))
|
||||
{
|
||||
account = new PlaidAccount
|
||||
{
|
||||
PlaidItemId = item.Id,
|
||||
PlaidAccountId = plaidAccountId
|
||||
};
|
||||
dbContext.PlaidAccounts.Add(account);
|
||||
existingAccounts[plaidAccountId] = account;
|
||||
}
|
||||
|
||||
var balances = accountJson.Property("balances");
|
||||
account.Name = accountJson.String("name") ?? account.Name;
|
||||
account.OfficialName = accountJson.String("official_name");
|
||||
account.Mask = accountJson.String("mask");
|
||||
account.Type = accountJson.String("type") ?? account.Type;
|
||||
account.Subtype = accountJson.String("subtype");
|
||||
account.CurrentBalance = balances?.Decimal("current");
|
||||
account.AvailableBalance = balances?.Decimal("available");
|
||||
account.Limit = balances?.Decimal("limit");
|
||||
account.IsoCurrencyCode = balances?.String("iso_currency_code");
|
||||
account.UnofficialCurrencyCode = balances?.String("unofficial_currency_code");
|
||||
account.BalanceLastUpdatedAt = balances?.DateTimeOffset("last_updated_datetime");
|
||||
account.RawJson = accountJson.GetRawText();
|
||||
account.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
upserted++;
|
||||
}
|
||||
|
||||
return upserted;
|
||||
}
|
||||
|
||||
private async Task<int> UpsertTransactionsAsync(
|
||||
PlaidItem item,
|
||||
JsonElement root,
|
||||
string arrayName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var transactions = root.Property(arrayName);
|
||||
if (transactions is not { ValueKind: JsonValueKind.Array })
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var accountLookup = await dbContext.PlaidAccounts
|
||||
.Where(account => account.PlaidItemId == item.Id)
|
||||
.ToDictionaryAsync(account => account.PlaidAccountId, account => account.Id, cancellationToken);
|
||||
|
||||
var ids = transactions.Value.EnumerateArray()
|
||||
.Select(transaction => transaction.String("transaction_id"))
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||
.Select(id => id!)
|
||||
.ToArray();
|
||||
|
||||
var existingTransactions = await dbContext.PlaidTransactions
|
||||
.Where(transaction => ids.Contains(transaction.PlaidTransactionId))
|
||||
.ToDictionaryAsync(transaction => transaction.PlaidTransactionId, cancellationToken);
|
||||
|
||||
var upserted = 0;
|
||||
foreach (var transactionJson in transactions.Value.EnumerateArray())
|
||||
{
|
||||
var plaidTransactionId = transactionJson.String("transaction_id");
|
||||
var accountPlaidId = transactionJson.String("account_id");
|
||||
var date = transactionJson.Date("date");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(plaidTransactionId) ||
|
||||
string.IsNullOrWhiteSpace(accountPlaidId) ||
|
||||
date is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!existingTransactions.TryGetValue(plaidTransactionId, out var transaction))
|
||||
{
|
||||
transaction = new PlaidTransaction
|
||||
{
|
||||
PlaidItemId = item.Id,
|
||||
PlaidTransactionId = plaidTransactionId
|
||||
};
|
||||
dbContext.PlaidTransactions.Add(transaction);
|
||||
existingTransactions[plaidTransactionId] = transaction;
|
||||
}
|
||||
|
||||
var personalFinanceCategory = transactionJson.Property("personal_finance_category");
|
||||
transaction.AccountPlaidId = accountPlaidId;
|
||||
transaction.PlaidAccountId = accountLookup.TryGetValue(accountPlaidId, out var accountId)
|
||||
? accountId
|
||||
: null;
|
||||
transaction.Date = date.Value;
|
||||
transaction.AuthorizedDate = transactionJson.Date("authorized_date");
|
||||
transaction.Name = transactionJson.String("name") ?? transaction.Name;
|
||||
transaction.MerchantName = transactionJson.String("merchant_name");
|
||||
transaction.Amount = transactionJson.Decimal("amount") ?? transaction.Amount;
|
||||
transaction.Pending = transactionJson.Bool("pending");
|
||||
transaction.IsoCurrencyCode = transactionJson.String("iso_currency_code");
|
||||
transaction.UnofficialCurrencyCode = transactionJson.String("unofficial_currency_code");
|
||||
transaction.Categories = transactionJson.StringArray("category");
|
||||
transaction.PersonalFinanceCategoryPrimary = personalFinanceCategory?.String("primary");
|
||||
transaction.PersonalFinanceCategoryDetailed = personalFinanceCategory?.String("detailed");
|
||||
transaction.RawJson = transactionJson.GetRawText();
|
||||
transaction.RemovedAt = null;
|
||||
transaction.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
await UpsertPlaidClassificationAsync(transaction, personalFinanceCategory, cancellationToken);
|
||||
upserted++;
|
||||
}
|
||||
|
||||
return upserted;
|
||||
}
|
||||
|
||||
private async Task<int> MarkTransactionsRemovedAsync(JsonElement root, CancellationToken cancellationToken)
|
||||
{
|
||||
var removed = root.Property("removed");
|
||||
if (removed is not { ValueKind: JsonValueKind.Array })
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var removedIds = removed.Value.EnumerateArray()
|
||||
.Select(transaction => transaction.String("transaction_id"))
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||
.Select(id => id!)
|
||||
.ToArray();
|
||||
|
||||
var transactions = await dbContext.PlaidTransactions
|
||||
.Where(transaction => removedIds.Contains(transaction.PlaidTransactionId))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var transaction in transactions)
|
||||
{
|
||||
transaction.RemovedAt = DateTimeOffset.UtcNow;
|
||||
transaction.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
return transactions.Count;
|
||||
}
|
||||
|
||||
private async Task<int> UpsertSecuritiesAsync(JsonElement root, CancellationToken cancellationToken)
|
||||
{
|
||||
var securities = root.Property("securities");
|
||||
if (securities is not { ValueKind: JsonValueKind.Array })
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var ids = securities.Value.EnumerateArray()
|
||||
.Select(security => security.String("security_id"))
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||
.Select(id => id!)
|
||||
.ToArray();
|
||||
|
||||
var existingSecurities = await dbContext.PlaidSecurities
|
||||
.Where(security => ids.Contains(security.PlaidSecurityId))
|
||||
.ToDictionaryAsync(security => security.PlaidSecurityId, cancellationToken);
|
||||
|
||||
var upserted = 0;
|
||||
foreach (var securityJson in securities.Value.EnumerateArray())
|
||||
{
|
||||
var plaidSecurityId = securityJson.String("security_id");
|
||||
if (string.IsNullOrWhiteSpace(plaidSecurityId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!existingSecurities.TryGetValue(plaidSecurityId, out var security))
|
||||
{
|
||||
security = new PlaidSecurity
|
||||
{
|
||||
PlaidSecurityId = plaidSecurityId
|
||||
};
|
||||
dbContext.PlaidSecurities.Add(security);
|
||||
existingSecurities[plaidSecurityId] = security;
|
||||
}
|
||||
|
||||
security.Cusip = securityJson.String("cusip");
|
||||
security.Isin = securityJson.String("isin");
|
||||
security.Sedol = securityJson.String("sedol");
|
||||
security.TickerSymbol = securityJson.String("ticker_symbol");
|
||||
security.Name = securityJson.String("name");
|
||||
security.Type = securityJson.String("type");
|
||||
security.ClosePrice = securityJson.Decimal("close_price");
|
||||
security.ClosePriceAsOf = securityJson.Date("close_price_as_of");
|
||||
security.IsoCurrencyCode = securityJson.String("iso_currency_code");
|
||||
security.UnofficialCurrencyCode = securityJson.String("unofficial_currency_code");
|
||||
security.RawJson = securityJson.GetRawText();
|
||||
security.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
upserted++;
|
||||
}
|
||||
|
||||
return upserted;
|
||||
}
|
||||
|
||||
private async Task<int> UpsertHoldingsAsync(
|
||||
PlaidItem item,
|
||||
JsonElement root,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var holdings = root.Property("holdings");
|
||||
if (holdings is not { ValueKind: JsonValueKind.Array })
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var accountLookup = await dbContext.PlaidAccounts
|
||||
.Where(account => account.PlaidItemId == item.Id)
|
||||
.ToDictionaryAsync(account => account.PlaidAccountId, account => account.Id, cancellationToken);
|
||||
var securityLookup = await dbContext.PlaidSecurities
|
||||
.ToDictionaryAsync(security => security.PlaidSecurityId, security => security.Id, cancellationToken);
|
||||
var existingHoldings = await dbContext.PlaidHoldings
|
||||
.Where(holding => holding.PlaidItemId == item.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var upserted = 0;
|
||||
foreach (var holdingJson in holdings.Value.EnumerateArray())
|
||||
{
|
||||
var accountPlaidId = holdingJson.String("account_id");
|
||||
var securityPlaidId = holdingJson.String("security_id");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(accountPlaidId) ||
|
||||
string.IsNullOrWhiteSpace(securityPlaidId) ||
|
||||
!accountLookup.TryGetValue(accountPlaidId, out var accountId) ||
|
||||
!securityLookup.TryGetValue(securityPlaidId, out var securityId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var holding = existingHoldings.SingleOrDefault(existing =>
|
||||
existing.PlaidAccountId == accountId && existing.PlaidSecurityId == securityId);
|
||||
|
||||
if (holding is null)
|
||||
{
|
||||
holding = new PlaidHolding
|
||||
{
|
||||
PlaidItemId = item.Id,
|
||||
PlaidAccountId = accountId,
|
||||
PlaidSecurityId = securityId
|
||||
};
|
||||
dbContext.PlaidHoldings.Add(holding);
|
||||
existingHoldings.Add(holding);
|
||||
}
|
||||
|
||||
holding.Quantity = holdingJson.Decimal("quantity") ?? holding.Quantity;
|
||||
holding.InstitutionPrice = holdingJson.Decimal("institution_price");
|
||||
holding.InstitutionValue = holdingJson.Decimal("institution_value");
|
||||
holding.InstitutionPriceAsOf = holdingJson.Date("institution_price_as_of");
|
||||
holding.CostBasis = holdingJson.Decimal("cost_basis");
|
||||
holding.IsoCurrencyCode = holdingJson.String("iso_currency_code");
|
||||
holding.UnofficialCurrencyCode = holdingJson.String("unofficial_currency_code");
|
||||
holding.RawJson = holdingJson.GetRawText();
|
||||
holding.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
upserted++;
|
||||
}
|
||||
|
||||
return upserted;
|
||||
}
|
||||
|
||||
private async Task<int> UpsertInvestmentTransactionsAsync(
|
||||
PlaidItem item,
|
||||
JsonElement root,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var transactions = root.Property("investment_transactions");
|
||||
if (transactions is not { ValueKind: JsonValueKind.Array })
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var accountLookup = await dbContext.PlaidAccounts
|
||||
.Where(account => account.PlaidItemId == item.Id)
|
||||
.ToDictionaryAsync(account => account.PlaidAccountId, account => account.Id, cancellationToken);
|
||||
var securityLookup = await dbContext.PlaidSecurities
|
||||
.ToDictionaryAsync(security => security.PlaidSecurityId, security => security.Id, cancellationToken);
|
||||
var ids = transactions.Value.EnumerateArray()
|
||||
.Select(transaction => transaction.String("investment_transaction_id"))
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||
.Select(id => id!)
|
||||
.ToArray();
|
||||
var existingTransactions = await dbContext.PlaidInvestmentTransactions
|
||||
.Where(transaction => ids.Contains(transaction.PlaidInvestmentTransactionId))
|
||||
.ToDictionaryAsync(transaction => transaction.PlaidInvestmentTransactionId, cancellationToken);
|
||||
|
||||
var upserted = 0;
|
||||
foreach (var transactionJson in transactions.Value.EnumerateArray())
|
||||
{
|
||||
var investmentTransactionId = transactionJson.String("investment_transaction_id");
|
||||
var accountPlaidId = transactionJson.String("account_id");
|
||||
var date = transactionJson.Date("date");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(investmentTransactionId) ||
|
||||
string.IsNullOrWhiteSpace(accountPlaidId) ||
|
||||
date is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!existingTransactions.TryGetValue(investmentTransactionId, out var transaction))
|
||||
{
|
||||
transaction = new PlaidInvestmentTransaction
|
||||
{
|
||||
PlaidItemId = item.Id,
|
||||
PlaidInvestmentTransactionId = investmentTransactionId
|
||||
};
|
||||
dbContext.PlaidInvestmentTransactions.Add(transaction);
|
||||
existingTransactions[investmentTransactionId] = transaction;
|
||||
}
|
||||
|
||||
var securityPlaidId = transactionJson.String("security_id");
|
||||
transaction.AccountPlaidId = accountPlaidId;
|
||||
transaction.PlaidAccountId = accountLookup.TryGetValue(accountPlaidId, out var accountId)
|
||||
? accountId
|
||||
: null;
|
||||
transaction.SecurityPlaidId = securityPlaidId;
|
||||
transaction.PlaidSecurityId = securityPlaidId is not null &&
|
||||
securityLookup.TryGetValue(securityPlaidId, out var securityId)
|
||||
? securityId
|
||||
: null;
|
||||
transaction.Date = date.Value;
|
||||
transaction.Name = transactionJson.String("name") ?? transaction.Name;
|
||||
transaction.Quantity = transactionJson.Decimal("quantity") ?? transaction.Quantity;
|
||||
transaction.Amount = transactionJson.Decimal("amount") ?? transaction.Amount;
|
||||
transaction.Price = transactionJson.Decimal("price");
|
||||
transaction.Fees = transactionJson.Decimal("fees");
|
||||
transaction.Type = transactionJson.String("type");
|
||||
transaction.Subtype = transactionJson.String("subtype");
|
||||
transaction.IsoCurrencyCode = transactionJson.String("iso_currency_code");
|
||||
transaction.UnofficialCurrencyCode = transactionJson.String("unofficial_currency_code");
|
||||
transaction.RawJson = transactionJson.GetRawText();
|
||||
transaction.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
upserted++;
|
||||
}
|
||||
|
||||
return upserted;
|
||||
}
|
||||
|
||||
private async Task UpsertPlaidClassificationAsync(
|
||||
PlaidTransaction transaction,
|
||||
JsonElement? personalFinanceCategory,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var primary = personalFinanceCategory?.String("primary") ?? "UNCATEGORIZED";
|
||||
var detailed = personalFinanceCategory?.String("detailed") ?? primary;
|
||||
var category = await GetOrCreateCategoryAsync(primary, detailed, cancellationToken);
|
||||
|
||||
var classification = await dbContext.TransactionClassifications
|
||||
.SingleOrDefaultAsync(
|
||||
existing => existing.PlaidTransactionId == transaction.Id,
|
||||
cancellationToken);
|
||||
|
||||
if (classification is not null && classification.Source != "plaid")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (classification is null)
|
||||
{
|
||||
classification = new TransactionClassification
|
||||
{
|
||||
PlaidTransaction = transaction,
|
||||
Source = "plaid"
|
||||
};
|
||||
dbContext.TransactionClassifications.Add(classification);
|
||||
}
|
||||
|
||||
classification.Category = category;
|
||||
classification.Source = "plaid";
|
||||
classification.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
private async Task<TransactionCategory> GetOrCreateCategoryAsync(
|
||||
string primary,
|
||||
string detailed,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var slug = BuildCategorySlug(detailed);
|
||||
var category = await dbContext.TransactionCategories
|
||||
.SingleOrDefaultAsync(existing => existing.Slug == slug, cancellationToken);
|
||||
|
||||
if (category is not null)
|
||||
{
|
||||
category.PlaidPrimary ??= primary;
|
||||
category.PlaidDetailed ??= detailed;
|
||||
category.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
return category;
|
||||
}
|
||||
|
||||
category = new TransactionCategory
|
||||
{
|
||||
Name = BuildCategoryName(detailed),
|
||||
Slug = slug,
|
||||
PlaidPrimary = primary,
|
||||
PlaidDetailed = detailed,
|
||||
IsSystem = true
|
||||
};
|
||||
|
||||
dbContext.TransactionCategories.Add(category);
|
||||
return category;
|
||||
}
|
||||
|
||||
private static string BuildCategorySlug(string value)
|
||||
{
|
||||
var normalized = value.Trim().ToLowerInvariant().Replace("&", "and");
|
||||
normalized = SlugSanitizer.Replace(normalized, "-").Trim('-');
|
||||
|
||||
return string.IsNullOrWhiteSpace(normalized)
|
||||
? "uncategorized"
|
||||
: normalized;
|
||||
}
|
||||
|
||||
private static string BuildCategoryName(string value)
|
||||
{
|
||||
return string.Join(
|
||||
" ",
|
||||
value.ToLowerInvariant()
|
||||
.Split(['_', '-'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(word => char.ToUpperInvariant(word[0]) + word[1..]));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Asentrafin.Api.Services.Plaid;
|
||||
|
||||
internal static class PlaidJson
|
||||
{
|
||||
public static JsonElement? Property(this JsonElement element, string name)
|
||||
{
|
||||
return element.ValueKind == JsonValueKind.Object && element.TryGetProperty(name, out var value)
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
public static string? String(this JsonElement element, string name)
|
||||
{
|
||||
var value = element.Property(name);
|
||||
return value is { ValueKind: JsonValueKind.String } ? value.Value.GetString() : null;
|
||||
}
|
||||
|
||||
public static decimal? Decimal(this JsonElement element, string name)
|
||||
{
|
||||
var value = element.Property(name);
|
||||
return value is { ValueKind: JsonValueKind.Number } && value.Value.TryGetDecimal(out var parsed)
|
||||
? parsed
|
||||
: null;
|
||||
}
|
||||
|
||||
public static int? Int(this JsonElement element, string name)
|
||||
{
|
||||
var value = element.Property(name);
|
||||
return value is { ValueKind: JsonValueKind.Number } && value.Value.TryGetInt32(out var parsed)
|
||||
? parsed
|
||||
: null;
|
||||
}
|
||||
|
||||
public static bool Bool(this JsonElement element, string name)
|
||||
{
|
||||
var value = element.Property(name);
|
||||
return value is { ValueKind: JsonValueKind.True } ||
|
||||
value is { ValueKind: JsonValueKind.False } && value.Value.GetBoolean();
|
||||
}
|
||||
|
||||
public static DateOnly? Date(this JsonElement element, string name)
|
||||
{
|
||||
var value = element.String(name);
|
||||
return DateOnly.TryParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed)
|
||||
? parsed
|
||||
: null;
|
||||
}
|
||||
|
||||
public static DateTimeOffset? DateTimeOffset(this JsonElement element, string name)
|
||||
{
|
||||
var value = element.String(name);
|
||||
return System.DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed)
|
||||
? parsed
|
||||
: null;
|
||||
}
|
||||
|
||||
public static string[] StringArray(this JsonElement element, string name)
|
||||
{
|
||||
var value = element.Property(name);
|
||||
if (value is not { ValueKind: JsonValueKind.Array })
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return value.Value.EnumerateArray()
|
||||
.Where(item => item.ValueKind == JsonValueKind.String)
|
||||
.Select(item => item.GetString())
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.Select(item => item!)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,4 @@
|
|||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=asentrafin;Username=postgres;Password=postgres"
|
||||
},
|
||||
"Plaid": {
|
||||
"BaseUrl": "https://sandbox.plaid.com",
|
||||
"ClientId": "",
|
||||
"Secret": ""
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
container_name: asentrafin-postgres
|
||||
environment:
|
||||
POSTGRES_DB: asentrafin
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d asentrafin"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "10.0.9",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue