Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b779c1841 | ||
|
|
e47e08b9f0 | ||
|
|
f0c63c7ded | ||
|
|
66f4cd2959 | ||
|
|
6a21e92aa6 | ||
|
|
88e8cfbf40 | ||
|
|
559908a150 | ||
|
|
e3acb2166d | ||
|
|
e6f70f4ab4 | ||
|
|
6c8ee3e203 | ||
|
|
cb7ed41296 | ||
|
|
a168d318b2 | ||
|
|
839d17de0d | ||
|
|
9e1d04da80 | ||
|
|
a721c60ad9 | ||
|
|
64c64e8d95 | ||
|
|
e30f19e939 | ||
|
|
d33b1d28dd | ||
|
|
36e2f70da2 | ||
|
|
9572097d38 | ||
|
|
ff4d10edcc | ||
|
|
379f975e7f | ||
|
|
6bf7499981 | ||
|
|
cd69cea2ac | ||
|
|
e3a75ccbc6 | ||
|
|
5eda5fbb76 | ||
|
|
4d1c01776f | ||
|
|
3408f6ea4f | ||
|
|
e2d2058960 | ||
|
|
02147cffdb | ||
|
|
e13c83013b | ||
|
|
c713a5448d | ||
|
|
091fa6f01c | ||
|
|
c8505081a5 | ||
|
|
7afe0cf6c1 | ||
|
|
9e656b7745 | ||
|
|
76df36b18f | ||
|
|
98d56fefd7 | ||
|
|
df697ed37c | ||
|
|
659b2bbf19 | ||
|
|
0bc810cab6 | ||
|
|
62a7dde443 | ||
|
|
3866030eae | ||
|
|
8d68bfc22a | ||
|
|
8222698fa8 | ||
|
|
2a378f098d | ||
|
|
00e54c27c6 | ||
|
|
a9f1ced0d5 | ||
|
|
48e95e5242 | ||
|
|
d0b0097032 | ||
|
|
81c45350e8 | ||
|
|
2fa34fe5ab | ||
|
|
0815620028 | ||
|
|
d5b6fb3a38 | ||
|
|
fd2cd68d73 | ||
|
|
b4fbf16869 | ||
|
|
d9130e75a4 | ||
|
|
e2537d77c4 | ||
|
|
fcbc9e78c2 | ||
|
|
9042cd4ead | ||
|
|
1c3de212ff | ||
|
|
72a534a8e0 | ||
|
|
74e55b2f48 | ||
|
|
a3dc58d19b | ||
|
|
5a878bf4b3 | ||
|
|
dee331fb0d |
@@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"appHostPath": "../src/FiscalOS.AppHost/FiscalOS.AppHost.csproj"
|
"appHostPath": "../src/FiscalOS.AppHost/FiscalOS.AppHost.csproj",
|
||||||
|
"defaultWatchEnabled": "true"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.git/
|
||||||
|
.vscode/
|
||||||
|
.aspire/
|
||||||
|
Coverage/
|
||||||
|
tests/
|
||||||
|
docker/data/
|
||||||
|
docker/keys/
|
||||||
|
**/bin/
|
||||||
|
**/obj/
|
||||||
|
**/node_modules/
|
||||||
|
**/.env
|
||||||
+2
-1
@@ -84,6 +84,7 @@ dotnet_diagnostic.IDE0058.severity = none
|
|||||||
dotnet_diagnostic.IDE0100.severity = none
|
dotnet_diagnostic.IDE0100.severity = none
|
||||||
dotnet_diagnostic.CA1515.severity = none
|
dotnet_diagnostic.CA1515.severity = none
|
||||||
dotnet_diagnostic.CA1848.severity = none
|
dotnet_diagnostic.CA1848.severity = none
|
||||||
|
dotnet_diagnostic.CA1873.severity = none
|
||||||
|
|
||||||
# var preferences
|
# var preferences
|
||||||
csharp_style_var_elsewhere = true:suggestion
|
csharp_style_var_elsewhere = true:suggestion
|
||||||
@@ -94,7 +95,7 @@ csharp_style_var_when_type_is_apparent = true:suggestion
|
|||||||
csharp_style_expression_bodied_accessors = true:silent
|
csharp_style_expression_bodied_accessors = true:silent
|
||||||
csharp_style_expression_bodied_constructors = false:silent
|
csharp_style_expression_bodied_constructors = false:silent
|
||||||
csharp_style_expression_bodied_indexers = true:silent
|
csharp_style_expression_bodied_indexers = true:silent
|
||||||
csharp_style_expression_bodied_lambdas = true:suggestion
|
csharp_style_expression_bodied_lambdas = false:silent
|
||||||
csharp_style_expression_bodied_local_functions = false:silent
|
csharp_style_expression_bodied_local_functions = false:silent
|
||||||
csharp_style_expression_bodied_methods = false:silent
|
csharp_style_expression_bodied_methods = false:silent
|
||||||
csharp_style_expression_bodied_operators = false:silent
|
csharp_style_expression_bodied_operators = false:silent
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
name: Deploy
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
env:
|
||||||
|
WEB_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/fiscalos-web
|
||||||
|
API_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/fiscalos-api
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build and push web image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/Dockerfile.web
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.WEB_IMAGE }}:latest
|
||||||
|
${{ env.WEB_IMAGE }}:${{ github.sha }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
- name: Build and push API image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/Dockerfile.api
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.API_IMAGE }}:latest
|
||||||
|
${{ env.API_IMAGE }}:${{ github.sha }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build-and-push
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
- name: Copy compose file to server
|
||||||
|
uses: appleboy/scp-action@v1.0.0
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.DEPLOY_HOST }}
|
||||||
|
username: ${{ secrets.DEPLOY_USER }}
|
||||||
|
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
source: docker/docker-compose.prod.yml
|
||||||
|
target: ${{ secrets.DEPLOY_PATH }}
|
||||||
|
strip_components: 1
|
||||||
|
- name: Deploy on server
|
||||||
|
uses: appleboy/ssh-action@v1
|
||||||
|
env:
|
||||||
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
IMAGE_TAG: ${{ github.sha }}
|
||||||
|
JWT_AUDIENCE: ${{ secrets.JWT_AUDIENCE }}
|
||||||
|
JWT_ISSUER: ${{ secrets.JWT_ISSUER }}
|
||||||
|
JWT_SECRET: ${{ secrets.JWT_SECRET }}
|
||||||
|
JWT_EXPIRY_IN_MINUTES: ${{ secrets.JWT_EXPIRY_IN_MINUTES }}
|
||||||
|
FILE_KEY_RING_PRIMARY_KEY_ID: ${{ secrets.FILE_KEY_RING_PRIMARY_KEY_ID }}
|
||||||
|
PLAID_CLIENT_ID: ${{ secrets.PLAID_CLIENT_ID }}
|
||||||
|
PLAID_SECRET: ${{ secrets.PLAID_SECRET }}
|
||||||
|
PLAID_WEBHOOK: ${{ secrets.PLAID_WEBHOOK }}
|
||||||
|
SEQ_SERVER_URL: ${{ secrets.SEQ_SERVER_URL }}
|
||||||
|
SEQ_API_KEY_HEADER: ${{ secrets.SEQ_API_KEY_HEADER }}
|
||||||
|
SEQ_API_KEY: ${{ secrets.SEQ_API_KEY }}
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.DEPLOY_HOST }}
|
||||||
|
username: ${{ secrets.DEPLOY_USER }}
|
||||||
|
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
envs: DOCKERHUB_USERNAME,IMAGE_TAG,JWT_AUDIENCE,JWT_ISSUER,JWT_SECRET,JWT_EXPIRY_IN_MINUTES,FILE_KEY_RING_PRIMARY_KEY_ID,PLAID_CLIENT_ID,PLAID_SECRET,PLAID_WEBHOOK,SEQ_SERVER_URL,SEQ_API_KEY_HEADER,SEQ_API_KEY
|
||||||
|
script: |
|
||||||
|
cd ${{ secrets.DEPLOY_PATH }}
|
||||||
|
cat > .env << EOF
|
||||||
|
DOCKERHUB_USERNAME=${DOCKERHUB_USERNAME}
|
||||||
|
IMAGE_TAG=${IMAGE_TAG}
|
||||||
|
JWT_AUDIENCE=${JWT_AUDIENCE}
|
||||||
|
JWT_ISSUER=${JWT_ISSUER}
|
||||||
|
JWT_SECRET=${JWT_SECRET}
|
||||||
|
JWT_EXPIRY_IN_MINUTES=${JWT_EXPIRY_IN_MINUTES}
|
||||||
|
FILE_KEY_RING_PRIMARY_KEY_ID=${FILE_KEY_RING_PRIMARY_KEY_ID}
|
||||||
|
PLAID_CLIENT_ID=${PLAID_CLIENT_ID}
|
||||||
|
PLAID_SECRET=${PLAID_SECRET}
|
||||||
|
PLAID_WEBHOOK=${PLAID_WEBHOOK}
|
||||||
|
SEQ_SERVER_URL=${SEQ_SERVER_URL}
|
||||||
|
SEQ_API_KEY_HEADER=${SEQ_API_KEY_HEADER}
|
||||||
|
SEQ_API_KEY=${SEQ_API_KEY}
|
||||||
|
EOF
|
||||||
|
docker compose -f docker-compose.prod.yml pull
|
||||||
|
docker compose -f docker-compose.prod.yml up -d --remove-orphans
|
||||||
|
docker image prune -f
|
||||||
@@ -3,10 +3,17 @@
|
|||||||
##
|
##
|
||||||
## Get latest from `dotnet new gitignore`
|
## Get latest from `dotnet new gitignore`
|
||||||
|
|
||||||
|
# coverage
|
||||||
|
coverage/
|
||||||
|
|
||||||
# appsettings
|
# appsettings
|
||||||
appsettings*.json
|
appsettings*.json
|
||||||
!appsettings.Example.json
|
!appsettings.Example.json
|
||||||
|
|
||||||
|
# docker bind-mount data
|
||||||
|
docker/data/
|
||||||
|
docker/keys/
|
||||||
|
|
||||||
# dotenv files
|
# dotenv files
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
|||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "aspire",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Aspire: Launch default apphost",
|
||||||
|
"program": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+6
-1
@@ -10,5 +10,10 @@
|
|||||||
"source.fixAll": "explicit"
|
"source.fixAll": "explicit"
|
||||||
},
|
},
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"cSpell.words": ["DOCKERHUB", "Dtos", "Encryptor", "fiscalos", "Validatable"],
|
||||||
|
"python-envs.defaultEnvManager": "ms-python.python:system",
|
||||||
|
"search.exclude": {
|
||||||
|
"**/Migrations": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,10 @@
|
|||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/tests/">
|
<Folder Name="/tests/">
|
||||||
<Project Path="tests/FiscalOS.API.Tests/FiscalOS.API.Tests.csproj" />
|
<Project Path="tests/FiscalOS.API.Tests/FiscalOS.API.Tests.csproj" />
|
||||||
|
<Project Path="tests/FiscalOS.AppHost.Tests/FiscalOS.AppHost.Tests.csproj" />
|
||||||
<Project Path="tests/FiscalOS.Core.Tests/FiscalOS.Core.Tests.csproj" />
|
<Project Path="tests/FiscalOS.Core.Tests/FiscalOS.Core.Tests.csproj" />
|
||||||
<Project Path="tests/FiscalOS.Infra.Tests/FiscalOS.Infra.Tests.csproj" />
|
<Project Path="tests/FiscalOS.Infra.Tests/FiscalOS.Infra.Tests.csproj" />
|
||||||
|
<Project Path="tests/FiscalOS.Tests.Common/FiscalOS.Tests.Common.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Project Path="src/FiscalOS.AppHost/FiscalOS.AppHost.csproj" />
|
<Project Path="src/FiscalOS.AppHost/FiscalOS.AppHost.csproj" />
|
||||||
<Project Path="src/FiscalOS.ServiceDefaults/FiscalOS.ServiceDefaults.csproj" />
|
<Project Path="src/FiscalOS.ServiceDefaults/FiscalOS.ServiceDefaults.csproj" />
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
|
WORKDIR /repo
|
||||||
|
|
||||||
|
COPY Directory.Build.props .
|
||||||
|
COPY .editorconfig .
|
||||||
|
COPY src/.editorconfig src/
|
||||||
|
COPY src/Directory.Build.props src/
|
||||||
|
COPY src/Directory.Packages.props src/
|
||||||
|
|
||||||
|
COPY src/FiscalOS.Core/FiscalOS.Core.csproj src/FiscalOS.Core/
|
||||||
|
COPY src/FiscalOS.Infra/FiscalOS.Infra.csproj src/FiscalOS.Infra/
|
||||||
|
COPY src/FiscalOS.ServiceDefaults/FiscalOS.ServiceDefaults.csproj src/FiscalOS.ServiceDefaults/
|
||||||
|
COPY src/FiscalOS.API/FiscalOS.API.csproj src/FiscalOS.API/
|
||||||
|
COPY src/FiscalOS.AdminCLI/FiscalOS.AdminCLI.csproj src/FiscalOS.AdminCLI/
|
||||||
|
|
||||||
|
RUN dotnet restore src/FiscalOS.API/FiscalOS.API.csproj
|
||||||
|
RUN dotnet restore src/FiscalOS.AdminCLI/FiscalOS.AdminCLI.csproj
|
||||||
|
|
||||||
|
COPY src/ src/
|
||||||
|
|
||||||
|
RUN dotnet publish src/FiscalOS.API/FiscalOS.API.csproj \
|
||||||
|
-c Release \
|
||||||
|
-o /publish/api \
|
||||||
|
--no-restore
|
||||||
|
RUN dotnet publish src/FiscalOS.AdminCLI/FiscalOS.AdminCLI.csproj \
|
||||||
|
-c Release \
|
||||||
|
-o /publish/admincli \
|
||||||
|
--no-restore
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=build /publish/api .
|
||||||
|
COPY --from=build /publish/admincli /usr/local/lib/admincli
|
||||||
|
|
||||||
|
ENV PATH="/usr/local/lib/admincli:${PATH}"
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
ENTRYPOINT ["dotnet", "FiscalOS.API.dll"]
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
FROM node:24-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY src/FiscalOS.Web/package.json src/FiscalOS.Web/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY src/FiscalOS.Web/ .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:alpine AS final
|
||||||
|
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: ${DOCKERHUB_USERNAME}/fiscalos-web:${IMAGE_TAG:-latest}
|
||||||
|
ports:
|
||||||
|
- "8081:80"
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
api:
|
||||||
|
image: ${DOCKERHUB_USERNAME}/fiscalos-api:${IMAGE_TAG:-latest}
|
||||||
|
environment:
|
||||||
|
- ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
- AppDbContextOptions__DatabaseFilePath=/data/fiscalos.db
|
||||||
|
- JwtOptions__Audience=${JWT_AUDIENCE}
|
||||||
|
- JwtOptions__Issuer=${JWT_ISSUER}
|
||||||
|
- JwtOptions__Secret=${JWT_SECRET}
|
||||||
|
- JwtOptions__ExpiryInMinutes=${JWT_EXPIRY_IN_MINUTES}
|
||||||
|
- FileKeyRingOptions__KeysDirectoryPath=/keys
|
||||||
|
- FileKeyRingOptions__PrimaryKeyId=${FILE_KEY_RING_PRIMARY_KEY_ID}
|
||||||
|
- PlaidClientOptions__ClientId=${PLAID_CLIENT_ID}
|
||||||
|
- PlaidClientOptions__Secret=${PLAID_SECRET}
|
||||||
|
- PlaidClientOptions__Webhook=${PLAID_WEBHOOK}
|
||||||
|
- SeqOptions__ServerUrl=${SEQ_SERVER_URL}
|
||||||
|
- SeqOptions__ApiKeyHeader=${SEQ_API_KEY_HEADER}
|
||||||
|
- SeqOptions__ApiKey=${SEQ_API_KEY}
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
- ./keys:/keys
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile.web
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile.api
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
- AppDbContextOptions__DatabaseFilePath=/data/fiscalos.db
|
||||||
|
- JwtOptions__Audience=${JWT_AUDIENCE}
|
||||||
|
- JwtOptions__Issuer=${JWT_ISSUER}
|
||||||
|
- JwtOptions__Secret=${JWT_SECRET}
|
||||||
|
- JwtOptions__ExpiryInMinutes=${JWT_EXPIRY_IN_MINUTES}
|
||||||
|
- FileKeyRingOptions__KeysDirectoryPath=/keys
|
||||||
|
- FileKeyRingOptions__PrimaryKeyId=${FILE_KEY_RING_PRIMARY_KEY_ID}
|
||||||
|
- PlaidClientOptions__ClientId=${PLAID_CLIENT_ID}
|
||||||
|
- PlaidClientOptions__Secret=${PLAID_SECRET}
|
||||||
|
- PlaidClientOptions__Webhook=${PLAID_WEBHOOK}
|
||||||
|
- SeqOptions__ServerUrl=${SEQ_SERVER_URL}
|
||||||
|
- SeqOptions__ApiKeyHeader=${SEQ_API_KEY_HEADER}
|
||||||
|
- SeqOptions__ApiKey=${SEQ_API_KEY}
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
- ./keys:/keys
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://api:8080/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"sdk": {
|
||||||
|
"version": "10.0.103",
|
||||||
|
"rollForward": "latestPatch"
|
||||||
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"runner": "Microsoft.Testing.Platform"
|
"runner": "Microsoft.Testing.Platform"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[*.cs]
|
||||||
|
|
||||||
|
dotnet_code_quality.ca1711.allowed_suffixes = Queue
|
||||||
|
dotnet_diagnostic.CA1031.severity = none
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageVersion Include="Aspire.Hosting.JavaScript" Version="13.1.1" />
|
<PackageVersion Include="Aspire.Hosting.JavaScript" Version="13.1.2" />
|
||||||
<PackageVersion Include="Going.Plaid" Version="6.56.0" />
|
<PackageVersion Include="Going.Plaid" Version="6.56.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using FiscalOS.API.Accounts.Add;
|
||||||
|
|
||||||
namespace FiscalOS.API.Accounts;
|
namespace FiscalOS.API.Accounts;
|
||||||
|
|
||||||
internal static class AccountsExtensions
|
internal static class AccountsExtensions
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Account = FiscalOS.Core.Accounts.Account;
|
||||||
|
|
||||||
namespace FiscalOS.API.Accounts.Add;
|
namespace FiscalOS.API.Accounts.Add;
|
||||||
|
|
||||||
internal static class Endpoint
|
internal static class Endpoint
|
||||||
@@ -13,15 +15,20 @@ internal static class Endpoint
|
|||||||
HttpContext httpContext,
|
HttpContext httpContext,
|
||||||
[FromBody] Request request,
|
[FromBody] Request request,
|
||||||
[FromServices] AppDbContext appDbContext,
|
[FromServices] AppDbContext appDbContext,
|
||||||
|
[FromServices] IEncryptor encryptor,
|
||||||
|
[FromServices] IPlaidAccountService plaidAccountService,
|
||||||
|
[FromServices] IAsyncQueue<SyncUpdatesQueueItem> queue,
|
||||||
CancellationToken ct
|
CancellationToken ct
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var userId = httpContext.GetUserId();
|
var userId = httpContext.GetUserId();
|
||||||
|
|
||||||
var user = await appDbContext.Users
|
var user = await appDbContext.Users
|
||||||
.Include(u => u.Institutions.Where(i => i.Metadata is PlaidMetadata && ((PlaidMetadata)i.Metadata).PlaidId == request.PlaidInstitutionId))
|
.Include(u => u.Institutions.Where(i => i.Metadata is PlaidInstitutionMetadata && ((PlaidInstitutionMetadata)i.Metadata).PlaidId == request.ProviderInstitutionId))
|
||||||
.Include(u => u.Accounts.Where(a => a.Metadata is PlaidAccountMetadata && ((PlaidAccountMetadata)a.Metadata).PlaidId == request.PlaidAccountId))
|
.ThenInclude(i => i.Metadata)
|
||||||
|
.Include(u => u.Accounts.Where(a => a.Metadata is PlaidAccountMetadata && ((PlaidAccountMetadata)a.Metadata).PlaidId == request.ProviderAccountId))
|
||||||
.ThenInclude(a => a.Metadata)
|
.ThenInclude(a => a.Metadata)
|
||||||
|
.AsSplitQuery()
|
||||||
.SingleOrDefaultAsync(u => u.Id == userId, ct);
|
.SingleOrDefaultAsync(u => u.Id == userId, ct);
|
||||||
|
|
||||||
if (user is null)
|
if (user is null)
|
||||||
@@ -33,7 +40,17 @@ internal static class Endpoint
|
|||||||
{
|
{
|
||||||
return Results.ValidationProblem(new Dictionary<string, string[]>
|
return Results.ValidationProblem(new Dictionary<string, string[]>
|
||||||
{
|
{
|
||||||
["PlaidInstitutionId"] = ["The PlaidInstitutionId field is invalid. No institution connected with the given PlaidInstitutionId was found for the user."],
|
[nameof(Request.ProviderInstitutionId)] = [$"The {nameof(Request.ProviderInstitutionId)} field is invalid."],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var plaidInstitutionMetadata = (PlaidInstitutionMetadata?)user.Institutions.First().Metadata;
|
||||||
|
|
||||||
|
if (plaidInstitutionMetadata is null)
|
||||||
|
{
|
||||||
|
return Results.ValidationProblem(new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[nameof(Request.ProviderInstitutionId)] = [$"The {nameof(Request.ProviderInstitutionId)} field is invalid."],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,14 +59,14 @@ internal static class Endpoint
|
|||||||
return Results.Conflict();
|
return Results.Conflict();
|
||||||
}
|
}
|
||||||
|
|
||||||
var accountMetadata = PlaidAccountMetadata.From(request.PlaidAccountId, request.PlaidAccountName);
|
var decryptedAccessToken = await encryptor.DecryptAsyncFor(user, plaidInstitutionMetadata.EncryptedAccessToken, ct);
|
||||||
var account = Account.From(user.Institutions.First().Id, request.PlaidAccountName, accountMetadata);
|
var accountMetadata = PlaidAccountMetadata.From(request.ProviderAccountId, request.ProviderAccountName);
|
||||||
var balance = Balance.From(request.AccountCurrentBalance, request.AccountAvailableBalance, request.AccountCurrencyCode);
|
var account = Account.From(request.ProviderAccountName, accountMetadata);
|
||||||
|
|
||||||
account.AddBalance(balance);
|
|
||||||
user.AddAccount(account);
|
user.AddAccount(account);
|
||||||
|
user.Institutions.First().AddAccount(account);
|
||||||
|
|
||||||
await appDbContext.SaveChangesAsync(ct);
|
await appDbContext.SaveChangesAsync(ct);
|
||||||
|
await queue.EnqueueAsync(new(plaidInstitutionMetadata.ItemId), ct);
|
||||||
|
|
||||||
return Results.Ok();
|
return Results.Ok();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,36 +2,27 @@ namespace FiscalOS.API.Accounts.Add;
|
|||||||
|
|
||||||
public record Request : IValidatableObject
|
public record Request : IValidatableObject
|
||||||
{
|
{
|
||||||
public string PlaidInstitutionId { get; init; } = string.Empty;
|
public string ProviderInstitutionId { get; init; } = string.Empty;
|
||||||
public string PlaidAccountId { get; init; } = string.Empty;
|
public string ProviderAccountId { get; init; } = string.Empty;
|
||||||
public string PlaidAccountName { get; init; } = string.Empty;
|
public string ProviderAccountName { get; init; } = string.Empty;
|
||||||
public decimal AccountCurrentBalance { get; init; }
|
|
||||||
public decimal AccountAvailableBalance { get; init; }
|
|
||||||
public string AccountCurrencyCode { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(PlaidInstitutionId))
|
if (string.IsNullOrWhiteSpace(ProviderInstitutionId))
|
||||||
{
|
{
|
||||||
var fieldName = nameof(PlaidInstitutionId);
|
var fieldName = nameof(ProviderInstitutionId);
|
||||||
yield return new($"The {fieldName} field is required.", [fieldName]);
|
yield return new($"The {fieldName} field is required.", [fieldName]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(PlaidAccountId))
|
if (string.IsNullOrWhiteSpace(ProviderAccountId))
|
||||||
{
|
{
|
||||||
var fieldName = nameof(PlaidAccountId);
|
var fieldName = nameof(ProviderAccountId);
|
||||||
yield return new($"The {fieldName} field is required.", [fieldName]);
|
yield return new($"The {fieldName} field is required.", [fieldName]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(PlaidAccountName))
|
if (string.IsNullOrWhiteSpace(ProviderAccountName))
|
||||||
{
|
{
|
||||||
var fieldName = nameof(PlaidAccountName);
|
var fieldName = nameof(ProviderAccountName);
|
||||||
yield return new($"The {fieldName} field is required.", [fieldName]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(AccountCurrencyCode))
|
|
||||||
{
|
|
||||||
var fieldName = nameof(AccountCurrencyCode);
|
|
||||||
yield return new($"The {fieldName} field is required.", [fieldName]);
|
yield return new($"The {fieldName} field is required.", [fieldName]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
using FiscalOS.API.Auth.Login;
|
||||||
|
using FiscalOS.API.Auth.Logout;
|
||||||
|
using FiscalOS.API.Auth.Refresh;
|
||||||
|
|
||||||
namespace FiscalOS.API.Auth;
|
namespace FiscalOS.API.Auth;
|
||||||
|
|
||||||
internal static class AuthExtensions
|
internal static class AuthExtensions
|
||||||
@@ -9,6 +13,7 @@ internal static class AuthExtensions
|
|||||||
var authGroup = app.MapGroup(RouteGroupPrefix);
|
var authGroup = app.MapGroup(RouteGroupPrefix);
|
||||||
|
|
||||||
authGroup.MapLoginEndpoint();
|
authGroup.MapLoginEndpoint();
|
||||||
|
authGroup.MapLogoutEndpoint();
|
||||||
|
|
||||||
authGroup.MapRefreshEndpoint()
|
authGroup.MapRefreshEndpoint()
|
||||||
.RequireAuthorization(Schemes.AllowExpiredTokens);
|
.RequireAuthorization(Schemes.AllowExpiredTokens);
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
namespace FiscalOS.API.Auth.Logout;
|
||||||
|
|
||||||
|
internal static class Endpoint
|
||||||
|
{
|
||||||
|
private const string Route = "/logout";
|
||||||
|
|
||||||
|
public static RouteHandlerBuilder MapLogoutEndpoint(this RouteGroupBuilder groupBuilder)
|
||||||
|
{
|
||||||
|
return groupBuilder.MapPost(Route, HandleAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> HandleAsync(
|
||||||
|
HttpContext httpContext,
|
||||||
|
[FromServices] AppDbContext appDbContext,
|
||||||
|
CancellationToken ct
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var userId = httpContext.GetUserId();
|
||||||
|
var refreshToken = httpContext.GetRefreshTokenFromCookie();
|
||||||
|
|
||||||
|
var user = await appDbContext.Users
|
||||||
|
.Include(u => u.RefreshTokens
|
||||||
|
.Where(r => r.Token == refreshToken)
|
||||||
|
)
|
||||||
|
.Where(u => u.Id == userId)
|
||||||
|
.SingleOrDefaultAsync(ct);
|
||||||
|
|
||||||
|
if (user is null || user.RefreshTokens.Any() is false)
|
||||||
|
{
|
||||||
|
return Results.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
user.RefreshTokens.First().Revoke();
|
||||||
|
await appDbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
httpContext.ExpireRefreshTokenCookie();
|
||||||
|
|
||||||
|
return Results.NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using FiscalOS.Infra.Common;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Diagnostics;
|
||||||
|
|
||||||
|
internal sealed class GlobalExceptionHandler(
|
||||||
|
IProblemDetailsService problemDetailsService,
|
||||||
|
ILogger<GlobalExceptionHandler> logger
|
||||||
|
) : IExceptionHandler
|
||||||
|
{
|
||||||
|
public async ValueTask<bool> TryHandleAsync(
|
||||||
|
HttpContext httpContext,
|
||||||
|
Exception exception,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
)
|
||||||
|
{
|
||||||
|
switch (exception)
|
||||||
|
{
|
||||||
|
case PlaidException plaidException:
|
||||||
|
logger.LogError(
|
||||||
|
plaidException,
|
||||||
|
"Unhandled Plaid exception. PlaidRequestId: {PlaidRequestId}, PlaidStatusCode: {PlaidStatusCode}, PlaidErrorCode: {PlaidErrorCode}, PlaidErrorType: {PlaidErrorType}, PlaidErrorMessage: {PlaidErrorMessage}",
|
||||||
|
plaidException.RequestId,
|
||||||
|
plaidException.StatusCode,
|
||||||
|
plaidException.ErrorCode,
|
||||||
|
plaidException.ErrorType,
|
||||||
|
plaidException.ErrorMessage
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.LogError(exception, "An unhandled exception occurred");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||||
|
|
||||||
|
return await problemDetailsService.TryWriteAsync(new ProblemDetailsContext
|
||||||
|
{
|
||||||
|
HttpContext = httpContext,
|
||||||
|
Exception = exception,
|
||||||
|
ProblemDetails = new ProblemDetails
|
||||||
|
{
|
||||||
|
Type = exception.GetType().Name,
|
||||||
|
Title = "Uh oh something has gone wrong.",
|
||||||
|
Detail = exception.Message
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
namespace FiscalOS.API.Common;
|
||||||
|
|
||||||
|
internal sealed record PagedQuery
|
||||||
|
{
|
||||||
|
public int PageNumber { get; init; }
|
||||||
|
public int PageSize { get; init; }
|
||||||
|
|
||||||
|
public static async ValueTask<PagedQuery> BindAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
var pageNumber = int.TryParse(context.Request.Query["pageNumber"], out var pn) ? pn : 1;
|
||||||
|
var pageSize = int.TryParse(context.Request.Query["pageSize"], out var ps) ? ps : 1000;
|
||||||
|
|
||||||
|
return new PagedQuery
|
||||||
|
{
|
||||||
|
PageNumber = pageNumber,
|
||||||
|
PageSize = pageSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, string[]> Validate()
|
||||||
|
{
|
||||||
|
var validationResults = new Dictionary<string, string[]>();
|
||||||
|
|
||||||
|
if (PageNumber <= 0)
|
||||||
|
{
|
||||||
|
validationResults[nameof(PageNumber)] = ["PageNumber must be greater than 0."];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PageSize <= 0 || PageSize > 1000)
|
||||||
|
{
|
||||||
|
validationResults[nameof(PageSize)] = ["PageSize must be between 1 and 1000."];
|
||||||
|
}
|
||||||
|
|
||||||
|
return validationResults;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
namespace FiscalOS.API.Common;
|
||||||
|
|
||||||
|
internal sealed record PagedResponse<T>
|
||||||
|
{
|
||||||
|
public int PageNumber { get; init; }
|
||||||
|
public int PageSize { get; init; }
|
||||||
|
public int TotalItems { get; init; }
|
||||||
|
public int TotalPages { get; init; }
|
||||||
|
public T[] Items { get; init; } = [];
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
private PagedResponse()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PagedResponse<T> From(
|
||||||
|
int pageNumber,
|
||||||
|
int pageSize,
|
||||||
|
int totalItems,
|
||||||
|
T[] items
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var totalPages = (int)Math.Ceiling((double)totalItems / pageSize);
|
||||||
|
|
||||||
|
return new PagedResponse<T>
|
||||||
|
{
|
||||||
|
PageNumber = pageNumber,
|
||||||
|
PageSize = pageSize,
|
||||||
|
TotalItems = totalItems,
|
||||||
|
TotalPages = totalPages,
|
||||||
|
Items = items
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,21 @@ internal static class HttpContextExtensions
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void ExpireRefreshTokenCookie(this HttpContext context)
|
||||||
|
{
|
||||||
|
context.Response.Cookies.Append(
|
||||||
|
RefreshTokenCookieName,
|
||||||
|
string.Empty,
|
||||||
|
new CookieOptions
|
||||||
|
{
|
||||||
|
HttpOnly = true,
|
||||||
|
Expires = DateTimeOffset.MinValue,
|
||||||
|
SameSite = SameSiteMode.Strict,
|
||||||
|
Secure = true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static string GetRefreshTokenFromCookie(this HttpContext context)
|
public static string GetRefreshTokenFromCookie(this HttpContext context)
|
||||||
{
|
{
|
||||||
return context.Request.Cookies[RefreshTokenCookieName] ?? string.Empty;
|
return context.Request.Cookies[RefreshTokenCookieName] ?? string.Empty;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Institution = FiscalOS.Core.Accounts.Institution;
|
||||||
|
|
||||||
namespace FiscalOS.API.Institutions.Connect;
|
namespace FiscalOS.API.Institutions.Connect;
|
||||||
|
|
||||||
internal static class Endpoint
|
internal static class Endpoint
|
||||||
@@ -13,7 +15,7 @@ internal static class Endpoint
|
|||||||
HttpContext httpContext,
|
HttpContext httpContext,
|
||||||
[FromBody] Request request,
|
[FromBody] Request request,
|
||||||
[FromServices] AppDbContext appDbContext,
|
[FromServices] AppDbContext appDbContext,
|
||||||
[FromServices] PlaidService plaidService,
|
[FromServices] IPlaidAccountService plaidAccountService,
|
||||||
[FromServices] IEncryptor encryptor,
|
[FromServices] IEncryptor encryptor,
|
||||||
CancellationToken ct
|
CancellationToken ct
|
||||||
)
|
)
|
||||||
@@ -23,8 +25,8 @@ internal static class Endpoint
|
|||||||
var user = await appDbContext.Users
|
var user = await appDbContext.Users
|
||||||
.Include(u => u.Institutions
|
.Include(u => u.Institutions
|
||||||
.Where(
|
.Where(
|
||||||
i => i.Metadata is PlaidMetadata &&
|
i => i.Metadata is PlaidInstitutionMetadata &&
|
||||||
((PlaidMetadata)i.Metadata).PlaidId == request.PlaidInstitutionId
|
((PlaidInstitutionMetadata)i.Metadata).PlaidId == request.PlaidInstitutionId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.ThenInclude(i => i.Metadata)
|
.ThenInclude(i => i.Metadata)
|
||||||
@@ -40,12 +42,11 @@ internal static class Endpoint
|
|||||||
return Results.Conflict();
|
return Results.Conflict();
|
||||||
}
|
}
|
||||||
|
|
||||||
var (itemId, accessToken) = await plaidService.ExchangeTokenAsync(request.PublicToken);
|
var (itemId, accessToken) = await plaidAccountService.ExchangeTokenAsync(request.PublicToken);
|
||||||
var item = await plaidService.GetItemAsync(accessToken);
|
var item = await plaidAccountService.GetItemAsync(accessToken);
|
||||||
|
|
||||||
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, accessToken, ct);
|
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, accessToken, ct);
|
||||||
|
|
||||||
var plaidMetadata = PlaidMetadata.From(item.InstitutionId, item.InstitutionName, encryptedAccessToken);
|
var plaidMetadata = PlaidInstitutionMetadata.From(item.InstitutionId, item.InstitutionName, encryptedAccessToken, itemId);
|
||||||
var institution = Institution.From(item.InstitutionName, plaidMetadata);
|
var institution = Institution.From(item.InstitutionName, plaidMetadata);
|
||||||
|
|
||||||
user.AddInstitution(institution);
|
user.AddInstitution(institution);
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
namespace FiscalOS.API.Institutions.Get;
|
||||||
|
|
||||||
|
internal static class Endpoint
|
||||||
|
{
|
||||||
|
private const string Route = "/";
|
||||||
|
|
||||||
|
public static RouteHandlerBuilder MapGetEndpoint(this RouteGroupBuilder groupBuilder)
|
||||||
|
{
|
||||||
|
return groupBuilder.MapGet(Route, HandleAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> HandleAsync(
|
||||||
|
HttpContext httpContext,
|
||||||
|
[FromServices] AppDbContext appDbContext
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var userId = httpContext.GetUserId();
|
||||||
|
|
||||||
|
var user = await appDbContext.Users
|
||||||
|
.Include(u => u.Institutions)
|
||||||
|
.ThenInclude(i => i.Accounts)
|
||||||
|
.ThenInclude(a => a.Metadata)
|
||||||
|
.FirstOrDefaultAsync(u => u.Id == userId);
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
{
|
||||||
|
return Results.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var institutionDtos = user.Institutions
|
||||||
|
.Select(InstitutionDto.From)
|
||||||
|
.OrderBy(dto => dto.Name);
|
||||||
|
|
||||||
|
return Results.Ok(Response.From(institutionDtos));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using Account = FiscalOS.Core.Accounts.Account;
|
||||||
|
using Institution = FiscalOS.Core.Accounts.Institution;
|
||||||
|
|
||||||
|
namespace FiscalOS.API.Institutions.Get;
|
||||||
|
|
||||||
|
internal sealed record Response
|
||||||
|
{
|
||||||
|
public IEnumerable<InstitutionDto> Institutions { get; init; } = [];
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
private Response()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Response From(IEnumerable<InstitutionDto> institutions)
|
||||||
|
{
|
||||||
|
return new Response
|
||||||
|
{
|
||||||
|
Institutions = institutions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record AccountDto
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public string ProviderId { get; init; } = string.Empty;
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
private AccountDto()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AccountDto From(Account account)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Id = account.Id.ToString(),
|
||||||
|
ProviderId = account.Metadata is PlaidAccountMetadata plaidMetadata
|
||||||
|
? plaidMetadata.PlaidId
|
||||||
|
: string.Empty,
|
||||||
|
Name = account.Name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record InstitutionDto
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
public IEnumerable<AccountDto> Accounts { get; init; } = [];
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
private InstitutionDto()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InstitutionDto From(Institution institution)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Id = institution.Id.ToString(),
|
||||||
|
Name = institution.Name,
|
||||||
|
Accounts = institution.Accounts.Select(AccountDto.From),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ internal static class Endpoint
|
|||||||
HttpContext httpContext,
|
HttpContext httpContext,
|
||||||
[FromRoute] Guid id,
|
[FromRoute] Guid id,
|
||||||
[FromServices] AppDbContext appDbContext,
|
[FromServices] AppDbContext appDbContext,
|
||||||
[FromServices] PlaidService plaidService,
|
[FromServices] IPlaidAccountService plaidAccountService,
|
||||||
[FromServices] IEncryptor encryptor,
|
[FromServices] IEncryptor encryptor,
|
||||||
CancellationToken ct
|
CancellationToken ct
|
||||||
)
|
)
|
||||||
@@ -37,14 +37,14 @@ internal static class Endpoint
|
|||||||
|
|
||||||
var institution = user.Institutions.First();
|
var institution = user.Institutions.First();
|
||||||
|
|
||||||
if (institution.Metadata is not PlaidMetadata plaidMetadata)
|
if (institution.Metadata is not PlaidInstitutionMetadata plaidMetadata)
|
||||||
{
|
{
|
||||||
return Results.BadRequest();
|
return Results.BadRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
var accessToken = await encryptor.DecryptAsyncFor(user, plaidMetadata.EncryptedAccessToken, ct);
|
var accessToken = await encryptor.DecryptAsyncFor(user, plaidMetadata.EncryptedAccessToken, ct);
|
||||||
var accounts = await plaidService.GetAccountsAsync(accessToken);
|
var accounts = await plaidAccountService.GetAccountsAsync(accessToken);
|
||||||
var accountsDtos = accounts.Select(AccountDto.FromPlaidAccount);
|
var accountsDtos = accounts.Select(a => AvailableAccountDto.From(plaidMetadata, a));
|
||||||
|
|
||||||
return Results.Ok(Response.From(accountsDtos));
|
return Results.Ok(Response.From(accountsDtos));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ namespace FiscalOS.API.Institutions.GetAvailable;
|
|||||||
|
|
||||||
internal sealed record Response
|
internal sealed record Response
|
||||||
{
|
{
|
||||||
public IEnumerable<AccountDto> Accounts { get; init; } = [];
|
public IEnumerable<AvailableAccountDto> Accounts { get; init; } = [];
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
private Response()
|
private Response()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Response From(IEnumerable<AccountDto> accounts)
|
public static Response From(IEnumerable<AvailableAccountDto> accounts)
|
||||||
{
|
{
|
||||||
return new Response
|
return new Response
|
||||||
{
|
{
|
||||||
@@ -18,8 +18,9 @@ internal sealed record Response
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed record AccountDto
|
internal sealed record AvailableAccountDto
|
||||||
{
|
{
|
||||||
|
public string ProviderInstitutionId { get; init; } = string.Empty;
|
||||||
public string ProviderId { get; init; } = string.Empty;
|
public string ProviderId { get; init; } = string.Empty;
|
||||||
public string ProviderName { get; init; } = string.Empty;
|
public string ProviderName { get; init; } = string.Empty;
|
||||||
public decimal CurrentBalance { get; init; }
|
public decimal CurrentBalance { get; init; }
|
||||||
@@ -27,14 +28,18 @@ internal sealed record AccountDto
|
|||||||
public string CurrencyCode { get; init; } = string.Empty;
|
public string CurrencyCode { get; init; } = string.Empty;
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
private AccountDto()
|
private AvailableAccountDto()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AccountDto FromPlaidAccount(Going.Plaid.Entity.Account plaidAccount)
|
public static AvailableAccountDto From(
|
||||||
|
PlaidInstitutionMetadata plaidMetadata,
|
||||||
|
Going.Plaid.Entity.Account plaidAccount
|
||||||
|
)
|
||||||
{
|
{
|
||||||
return new AccountDto
|
return new AvailableAccountDto
|
||||||
{
|
{
|
||||||
|
ProviderInstitutionId = plaidMetadata.PlaidId,
|
||||||
ProviderId = plaidAccount.AccountId,
|
ProviderId = plaidAccount.AccountId,
|
||||||
ProviderName = plaidAccount.Name,
|
ProviderName = plaidAccount.Name,
|
||||||
AvailableBalance = plaidAccount.Available,
|
AvailableBalance = plaidAccount.Available,
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
using FiscalOS.API.Institutions.Connect;
|
||||||
|
using FiscalOS.API.Institutions.Get;
|
||||||
|
using FiscalOS.API.Institutions.GetAvailable;
|
||||||
|
using FiscalOS.API.Institutions.Link;
|
||||||
|
|
||||||
namespace FiscalOS.API.Institutions;
|
namespace FiscalOS.API.Institutions;
|
||||||
|
|
||||||
internal static class InstitutionsExtensions
|
internal static class InstitutionsExtensions
|
||||||
@@ -12,6 +17,7 @@ internal static class InstitutionsExtensions
|
|||||||
institutionsGroup.MapConnectEndpoint();
|
institutionsGroup.MapConnectEndpoint();
|
||||||
institutionsGroup.MapGetAvailableEndpoint();
|
institutionsGroup.MapGetAvailableEndpoint();
|
||||||
institutionsGroup.MapLinkEndpoint();
|
institutionsGroup.MapLinkEndpoint();
|
||||||
|
institutionsGroup.MapGetEndpoint();
|
||||||
|
|
||||||
return institutionsGroup;
|
return institutionsGroup;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ internal static class Endpoint
|
|||||||
private static async Task<IResult> HandleAsync(
|
private static async Task<IResult> HandleAsync(
|
||||||
HttpContext httpContext,
|
HttpContext httpContext,
|
||||||
[FromServices] AppDbContext appDbContext,
|
[FromServices] AppDbContext appDbContext,
|
||||||
[FromServices] PlaidService plaidService
|
[FromServices] IPlaidAccountService plaidAccountService
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var userId = httpContext.GetUserId();
|
var userId = httpContext.GetUserId();
|
||||||
@@ -24,7 +24,7 @@ internal static class Endpoint
|
|||||||
return Results.Unauthorized();
|
return Results.Unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
var linkToken = await plaidService.CreateLinkTokenAsync(user.Id.ToString());
|
var linkToken = await plaidAccountService.CreateLinkTokenAsync(user.Id.ToString());
|
||||||
|
|
||||||
return Results.Ok(Response.From(linkToken));
|
return Results.Ok(Response.From(linkToken));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
using FiscalOS.ServiceDefaults;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
builder.AddServiceDefaults();
|
builder.AddServiceDefaults();
|
||||||
|
|
||||||
builder.Services.AddValidation();
|
builder.Services.AddValidation();
|
||||||
|
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
|
||||||
builder.Services.AddProblemDetails();
|
builder.Services.AddProblemDetails();
|
||||||
|
|
||||||
|
builder.Services.ConfigureHttpJsonOptions(static options =>
|
||||||
|
{
|
||||||
|
options.SerializerOptions.Converters.Add(new EnumConverterFactory());
|
||||||
|
options.SerializerOptions.Converters.Add(new WebhookBaseConverter());
|
||||||
|
});
|
||||||
|
|
||||||
builder.Services.AddInfrastructure();
|
builder.Services.AddInfrastructure();
|
||||||
|
|
||||||
builder.Services.AddAuthentication(static o =>
|
builder.Services.AddAuthentication(static o =>
|
||||||
@@ -35,11 +40,12 @@ app.UseHttpsRedirection();
|
|||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.UseStatusCodePages();
|
app.UseStatusCodePages();
|
||||||
|
app.UseExceptionHandler();
|
||||||
|
|
||||||
app.MapDefaultEndpoints();
|
app.MapDefaultEndpoints();
|
||||||
|
|
||||||
app.MapAuthEndpoints();
|
app.MapAuthEndpoints();
|
||||||
app.MapAccountsEndpoints();
|
app.MapAccountsEndpoints();
|
||||||
app.MapInstitutionsEndpoints();
|
app.MapInstitutionsEndpoints();
|
||||||
|
app.MapTransactionsGroup();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
namespace FiscalOS.API.Transactions.DeleteById;
|
||||||
|
|
||||||
|
internal static class Endpoint
|
||||||
|
{
|
||||||
|
private const string Route = "/{id}";
|
||||||
|
|
||||||
|
public static RouteHandlerBuilder MapDeleteEndpoint(this RouteGroupBuilder groupBuilder)
|
||||||
|
{
|
||||||
|
return groupBuilder.MapDelete(Route, HandleAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> HandleAsync(
|
||||||
|
HttpContext httpContext,
|
||||||
|
[FromRoute] Guid id,
|
||||||
|
[FromServices] AppDbContext appDbContext,
|
||||||
|
CancellationToken ct
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var userId = httpContext.GetUserId();
|
||||||
|
|
||||||
|
var existingTransaction = await appDbContext.Transactions
|
||||||
|
.SingleOrDefaultAsync(t => t.UserId == userId && t.Id == id, ct);
|
||||||
|
|
||||||
|
if (existingTransaction is null)
|
||||||
|
{
|
||||||
|
return Results.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
appDbContext.Transactions.Remove(existingTransaction);
|
||||||
|
await appDbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
return Results.NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
namespace FiscalOS.API.Transactions.FireWebhook;
|
||||||
|
|
||||||
|
internal static class Endpoint
|
||||||
|
{
|
||||||
|
private const string Route = "/fire-webhook";
|
||||||
|
|
||||||
|
public static RouteHandlerBuilder MapFireWebhookEndpoint(this RouteGroupBuilder groupBuilder)
|
||||||
|
{
|
||||||
|
return groupBuilder.MapPost(Route, HandleAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> HandleAsync(
|
||||||
|
HttpContext httpContext,
|
||||||
|
[FromBody] Request request,
|
||||||
|
[FromServices] AppDbContext appDbContext,
|
||||||
|
[FromServices] IEncryptor encryptor,
|
||||||
|
[FromServices] PlaidClient plaidClient,
|
||||||
|
CancellationToken ct
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var userId = httpContext.GetUserId();
|
||||||
|
|
||||||
|
var user = await appDbContext.Users
|
||||||
|
.Include(u => u.Accounts.Where(a => a.Id.ToString() == request.AccountId))
|
||||||
|
.ThenInclude(static a => a.Institution!)
|
||||||
|
.ThenInclude(static i => i.Metadata)
|
||||||
|
.FirstOrDefaultAsync(u => u.Id == userId, ct);
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
{
|
||||||
|
return Results.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.Accounts.Any() is false)
|
||||||
|
{
|
||||||
|
return Results.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var plaidMetadata = (PlaidInstitutionMetadata?)user.Accounts.First().Institution?.Metadata;
|
||||||
|
|
||||||
|
if (plaidMetadata is null)
|
||||||
|
{
|
||||||
|
return Results.InternalServerError("No metadata associated with account");
|
||||||
|
}
|
||||||
|
|
||||||
|
var accessToken = await encryptor.DecryptAsyncFor(user, plaidMetadata.EncryptedAccessToken, ct);
|
||||||
|
|
||||||
|
var fireEventResponse = await plaidClient.SandboxItemFireWebhookAsync(new()
|
||||||
|
{
|
||||||
|
AccessToken = accessToken,
|
||||||
|
WebhookType = SandboxItemFireWebhookRequestWebhookTypeEnum.Transactions,
|
||||||
|
WebhookCode = SandboxItemFireWebhookRequestWebhookCodeEnum.SyncUpdatesAvailable,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fireEventResponse.IsSuccessStatusCode is false)
|
||||||
|
{
|
||||||
|
return Results.InternalServerError(fireEventResponse.Error?.ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Results.Ok(new { fireEventResponse.RequestId, fireEventResponse.WebhookFired });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace FiscalOS.API.Transactions.FireWebhook;
|
||||||
|
|
||||||
|
public sealed record Request(
|
||||||
|
string AccountId,
|
||||||
|
string WebhookType,
|
||||||
|
string WebhookCode
|
||||||
|
);
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
namespace FiscalOS.API.Transactions.Get;
|
||||||
|
|
||||||
|
internal static class Endpoint
|
||||||
|
{
|
||||||
|
private const string Route = "/";
|
||||||
|
|
||||||
|
public static RouteHandlerBuilder MapGetEndpoint(this RouteGroupBuilder groupBuilder)
|
||||||
|
{
|
||||||
|
return groupBuilder.MapGet(Route, HandleAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> HandleAsync(
|
||||||
|
HttpContext httpContext,
|
||||||
|
PagedQuery pagedQuery,
|
||||||
|
[FromServices] AppDbContext appDbContext,
|
||||||
|
CancellationToken ct
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var pagedQueryValidationResults = pagedQuery.Validate();
|
||||||
|
|
||||||
|
if (pagedQueryValidationResults.Count is not 0)
|
||||||
|
{
|
||||||
|
return Results.ValidationProblem(pagedQueryValidationResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = httpContext.GetUserId();
|
||||||
|
var query = appDbContext.Transactions
|
||||||
|
.Where(t => t.UserId == userId);
|
||||||
|
|
||||||
|
var transactions = await query
|
||||||
|
.Skip((pagedQuery.PageNumber - 1) * pagedQuery.PageSize)
|
||||||
|
.Take(pagedQuery.PageSize)
|
||||||
|
// TODO: Migrate Date to DateTime
|
||||||
|
// to avoid in-memory sorting
|
||||||
|
.AsAsyncEnumerable()
|
||||||
|
.OrderByDescending(t => t.Date)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
var count = await query
|
||||||
|
.Where(t => t.UserId == userId)
|
||||||
|
.CountAsync(ct);
|
||||||
|
|
||||||
|
var transactionDtos = transactions.Select(TransactionDto.From).ToArray();
|
||||||
|
var pagedResponse = PagedResponse<TransactionDto>.From(
|
||||||
|
pagedQuery.PageNumber,
|
||||||
|
pagedQuery.PageSize,
|
||||||
|
count,
|
||||||
|
transactionDtos
|
||||||
|
);
|
||||||
|
|
||||||
|
return Results.Ok(pagedResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Transaction = FiscalOS.Core.Transactions.Transaction;
|
||||||
|
|
||||||
|
namespace FiscalOS.API.Transactions;
|
||||||
|
|
||||||
|
internal sealed record TransactionDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; init; }
|
||||||
|
public string MerchantName { get; init; } = string.Empty;
|
||||||
|
public decimal Amount { get; init; }
|
||||||
|
public DateTimeOffset Date { get; init; }
|
||||||
|
public string Description { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
private TransactionDto()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TransactionDto From(Transaction transaction)
|
||||||
|
{
|
||||||
|
return new TransactionDto
|
||||||
|
{
|
||||||
|
Id = transaction.Id,
|
||||||
|
MerchantName = transaction.MerchantName,
|
||||||
|
Amount = transaction.Amount,
|
||||||
|
Date = transaction.Date,
|
||||||
|
Description = transaction.Description
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using FiscalOS.API.Transactions.DeleteById;
|
||||||
|
using FiscalOS.API.Transactions.FireWebhook;
|
||||||
|
using FiscalOS.API.Transactions.Get;
|
||||||
|
using FiscalOS.API.Transactions.Webhook;
|
||||||
|
|
||||||
|
namespace FiscalOS.API.Transactions;
|
||||||
|
|
||||||
|
internal static class TransactionsExtensions
|
||||||
|
{
|
||||||
|
private const string RouteGroupPrefix = "/transactions";
|
||||||
|
|
||||||
|
public static RouteGroupBuilder MapTransactionsGroup(this WebApplication app)
|
||||||
|
{
|
||||||
|
var transactionsGroup = app.MapGroup(RouteGroupPrefix)
|
||||||
|
.RequireAuthorization();
|
||||||
|
|
||||||
|
transactionsGroup.MapGetEndpoint();
|
||||||
|
transactionsGroup.MapDeleteEndpoint();
|
||||||
|
transactionsGroup.MapWebhookEndpoint().AllowAnonymous();
|
||||||
|
|
||||||
|
if (app.Environment.IsProduction() is false)
|
||||||
|
{
|
||||||
|
transactionsGroup.MapFireWebhookEndpoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactionsGroup;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
namespace FiscalOS.API.Transactions.Webhook;
|
||||||
|
|
||||||
|
internal static class Endpoint
|
||||||
|
{
|
||||||
|
private const string Route = "/webhook";
|
||||||
|
|
||||||
|
public static RouteHandlerBuilder MapWebhookEndpoint(this RouteGroupBuilder groupBuilder)
|
||||||
|
{
|
||||||
|
return groupBuilder.MapPost(Route, HandleAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> HandleAsync(
|
||||||
|
[FromBody] WebhookBase request,
|
||||||
|
[FromServices] ILogger<Program> logger,
|
||||||
|
[FromServices] IAsyncQueue<SyncUpdatesQueueItem> queue,
|
||||||
|
CancellationToken ct
|
||||||
|
)
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"Received {WebhookType} webhook with {WebhookCode} payload",
|
||||||
|
request.WebhookType,
|
||||||
|
request.WebhookCode
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (request)
|
||||||
|
{
|
||||||
|
case SyncUpdatesAvailableWebhook webhook:
|
||||||
|
await queue.EnqueueAsync(new(webhook.ItemId), ct);
|
||||||
|
logger.LogInformation(
|
||||||
|
"Enqueued {WebhookType} webhook with {WebhookCode} payload",
|
||||||
|
request.WebhookType,
|
||||||
|
request.WebhookCode
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Results.Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,23 +3,26 @@ global using System.Security.Claims;
|
|||||||
global using System.Text.Json.Serialization;
|
global using System.Text.Json.Serialization;
|
||||||
|
|
||||||
global using FiscalOS.API.Accounts;
|
global using FiscalOS.API.Accounts;
|
||||||
global using FiscalOS.API.Accounts.Add;
|
|
||||||
global using FiscalOS.API.Auth;
|
global using FiscalOS.API.Auth;
|
||||||
global using FiscalOS.API.Auth.Login;
|
global using FiscalOS.API.Common;
|
||||||
global using FiscalOS.API.Auth.Refresh;
|
|
||||||
global using FiscalOS.API.Http;
|
global using FiscalOS.API.Http;
|
||||||
global using FiscalOS.API.Institutions;
|
global using FiscalOS.API.Institutions;
|
||||||
global using FiscalOS.API.Institutions.Connect;
|
global using FiscalOS.API.Transactions;
|
||||||
global using FiscalOS.API.Institutions.GetAvailable;
|
|
||||||
global using FiscalOS.API.Institutions.Link;
|
|
||||||
global using FiscalOS.Core.Accounts;
|
|
||||||
global using FiscalOS.Core.Authentication;
|
global using FiscalOS.Core.Authentication;
|
||||||
global using FiscalOS.Core.Identity;
|
global using FiscalOS.Core.Identity;
|
||||||
|
global using FiscalOS.Core.Queuing;
|
||||||
global using FiscalOS.Core.Security;
|
global using FiscalOS.Core.Security;
|
||||||
global using FiscalOS.Infra.Accounts.Plaid;
|
global using FiscalOS.Infra.Accounts.Plaid;
|
||||||
global using FiscalOS.Infra.Authentication;
|
global using FiscalOS.Infra.Authentication;
|
||||||
global using FiscalOS.Infra.Data;
|
global using FiscalOS.Infra.Data;
|
||||||
global using FiscalOS.Infra.DependencyInjection;
|
global using FiscalOS.Infra.DependencyInjection;
|
||||||
|
global using FiscalOS.Infra.Transactions.Plaid;
|
||||||
|
global using FiscalOS.ServiceDefaults;
|
||||||
|
|
||||||
|
global using Going.Plaid;
|
||||||
|
global using Going.Plaid.Converters;
|
||||||
|
global using Going.Plaid.Entity;
|
||||||
|
global using Going.Plaid.Webhook;
|
||||||
|
|
||||||
global using Microsoft.AspNetCore.Mvc;
|
global using Microsoft.AspNetCore.Mvc;
|
||||||
global using Microsoft.EntityFrameworkCore;
|
global using Microsoft.EntityFrameworkCore;
|
||||||
@@ -21,6 +21,12 @@
|
|||||||
},
|
},
|
||||||
"PlaidClientOptions": {
|
"PlaidClientOptions": {
|
||||||
"ClientId": "ClientId",
|
"ClientId": "ClientId",
|
||||||
"Secret": "Secret"
|
"Secret": "Secret",
|
||||||
|
"Webhook": "Webhook"
|
||||||
|
},
|
||||||
|
"SeqOptions": {
|
||||||
|
"ServerUrl": "ServerUrl",
|
||||||
|
"ApiKeyHeader": "ApiKeyHeader",
|
||||||
|
"ApiKey": "ApiKey"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,6 +47,52 @@ internal sealed class App(
|
|||||||
var keyRingEntry = await keyRing.SaveKeyAsync(key);
|
var keyRingEntry = await keyRing.SaveKeyAsync(key);
|
||||||
_console.MarkupLine($"Key generated with id [green]{keyRingEntry.KeyId}[/] generated successfully.");
|
_console.MarkupLine($"Key generated with id [green]{keyRingEntry.KeyId}[/] generated successfully.");
|
||||||
break;
|
break;
|
||||||
|
case Commands.RemoveInstitution:
|
||||||
|
var institutionId = await _console.AskAsync<string>("Enter the [green]institution id[/] of the item you want to remove:", cancellationToken);
|
||||||
|
|
||||||
|
var institution = await appDbContext.Institutions
|
||||||
|
.Include(i => i.User)
|
||||||
|
.Include(i => i.Metadata)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (institution is null)
|
||||||
|
{
|
||||||
|
_console.MarkupLine($"Institution with id [green]{institutionId}[/] not found.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (institution.User is null)
|
||||||
|
{
|
||||||
|
_console.MarkupLine($"Institution with id [green]{institutionId}[/] has no user.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (institution.Metadata is not PlaidInstitutionMetadata plaidInstitutionMetadata)
|
||||||
|
{
|
||||||
|
_console.MarkupLine($"Institution with id [green]{institutionId}[/] has no plaid metadata.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var accessToken = await encryptor.DecryptAsyncFor(institution.User, plaidInstitutionMetadata.EncryptedAccessToken, cancellationToken);
|
||||||
|
|
||||||
|
var plaidClient = scope.ServiceProvider.GetRequiredService<PlaidClient>();
|
||||||
|
|
||||||
|
var removeItemResponse = await plaidClient.ItemRemoveAsync(new()
|
||||||
|
{
|
||||||
|
AccessToken = accessToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (removeItemResponse.IsSuccessStatusCode is false)
|
||||||
|
{
|
||||||
|
_console.MarkupLine($"Failed to remove item for institution id [green]{institutionId}[/]. Plaid error: {removeItemResponse.Error?.ErrorMessage}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
appDbContext.Institutions.Remove(institution);
|
||||||
|
await appDbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_console.MarkupLine($"Institution with id [green]{institutionId}[/] removed successfully.");
|
||||||
|
break;
|
||||||
case Commands.Exit:
|
case Commands.Exit:
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ internal static class Commands
|
|||||||
{
|
{
|
||||||
public const string CreateUser = "Create User";
|
public const string CreateUser = "Create User";
|
||||||
public const string GenerateKey = "Generate Key";
|
public const string GenerateKey = "Generate Key";
|
||||||
|
public const string RemoveInstitution = "Remove Institution";
|
||||||
public const string Exit = "Exit";
|
public const string Exit = "Exit";
|
||||||
|
|
||||||
public static readonly string[] All = [
|
public static readonly string[] All = [
|
||||||
CreateUser,
|
CreateUser,
|
||||||
GenerateKey,
|
GenerateKey,
|
||||||
|
RemoveInstitution,
|
||||||
Exit
|
Exit
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
|
<AssemblyName>fiscaloscli</AssemblyName>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
await Host.CreateDefaultBuilder(args)
|
await Host.CreateDefaultBuilder(args)
|
||||||
.ConfigureAppConfiguration(
|
.ConfigureAppConfiguration(
|
||||||
static c => c.SetBasePath(AppContext.BaseDirectory)
|
static c => c.SetBasePath(AppContext.BaseDirectory)
|
||||||
.AddJsonFile("appsettings.json")
|
.AddJsonFile("appsettings.json", optional: true)
|
||||||
.AddEnvironmentVariables()
|
.AddEnvironmentVariables()
|
||||||
)
|
)
|
||||||
.ConfigureLogging(static c => c.ClearProviders())
|
.ConfigureLogging(static c => c.ClearProviders())
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ global using FiscalOS.AdminCLI;
|
|||||||
global using FiscalOS.Core.Authentication;
|
global using FiscalOS.Core.Authentication;
|
||||||
global using FiscalOS.Core.Identity;
|
global using FiscalOS.Core.Identity;
|
||||||
global using FiscalOS.Core.Security;
|
global using FiscalOS.Core.Security;
|
||||||
|
global using FiscalOS.Infra.Accounts.Plaid;
|
||||||
global using FiscalOS.Infra.Data;
|
global using FiscalOS.Infra.Data;
|
||||||
global using FiscalOS.Infra.DependencyInjection;
|
global using FiscalOS.Infra.DependencyInjection;
|
||||||
|
|
||||||
|
global using Going.Plaid;
|
||||||
|
|
||||||
|
global using Microsoft.EntityFrameworkCore;
|
||||||
global using Microsoft.Extensions.Configuration;
|
global using Microsoft.Extensions.Configuration;
|
||||||
global using Microsoft.Extensions.DependencyInjection;
|
global using Microsoft.Extensions.DependencyInjection;
|
||||||
global using Microsoft.Extensions.Hosting;
|
global using Microsoft.Extensions.Hosting;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
using FiscalOS.AppHost;
|
||||||
|
|
||||||
var builder = DistributedApplication.CreateBuilder(args);
|
var builder = DistributedApplication.CreateBuilder(args);
|
||||||
|
|
||||||
var api = builder.AddProject<Projects.FiscalOS_API>("API")
|
var api = builder.AddProject<Projects.FiscalOS_API>(ProjectNames.API)
|
||||||
.WithHttpHealthCheck("/health");
|
.WithHttpHealthCheck("/health");
|
||||||
|
|
||||||
builder.AddViteApp("web", "../FiscalOS.Web")
|
builder.AddViteApp(ProjectNames.Web, "../FiscalOS.Web")
|
||||||
.WithReference(api)
|
.WithReference(api)
|
||||||
.WithHttpsEndpoint(port: 7061, env: "PORT");
|
.WithHttpsEndpoint(port: 7061, env: "PORT");
|
||||||
|
|
||||||
builder.Build().Run();
|
builder.Build().Run();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Aspire.AppHost.Sdk/13.1.1">
|
<Project Sdk="Aspire.AppHost.Sdk/13.1.2">
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\FiscalOS.API\FiscalOS.API.csproj" />
|
<ProjectReference Include="..\FiscalOS.API\FiscalOS.API.csproj" />
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace FiscalOS.AppHost;
|
||||||
|
|
||||||
|
public static class ProjectNames
|
||||||
|
{
|
||||||
|
public const string API = "api";
|
||||||
|
public const string Web = "web";
|
||||||
|
}
|
||||||
@@ -11,11 +11,20 @@ public sealed class Account : Entity
|
|||||||
private readonly List<Balance> _balances = [];
|
private readonly List<Balance> _balances = [];
|
||||||
public IEnumerable<Balance> Balances => _balances;
|
public IEnumerable<Balance> Balances => _balances;
|
||||||
|
|
||||||
public static Account From(Guid institutionId, string name, AccountMetadata accountMetadata)
|
private readonly List<Transaction> _transactions = [];
|
||||||
|
public IEnumerable<Transaction> Transactions => _transactions;
|
||||||
|
|
||||||
|
private Account()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Account From(
|
||||||
|
string name,
|
||||||
|
AccountMetadata accountMetadata
|
||||||
|
)
|
||||||
{
|
{
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
InstitutionId = institutionId,
|
|
||||||
Name = name,
|
Name = name,
|
||||||
Metadata = accountMetadata,
|
Metadata = accountMetadata,
|
||||||
};
|
};
|
||||||
@@ -23,8 +32,15 @@ public sealed class Account : Entity
|
|||||||
|
|
||||||
public void AddBalance(Balance balance)
|
public void AddBalance(Balance balance)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(balance);
|
ArgumentNullException.ThrowIfNull(balance, nameof(balance));
|
||||||
|
|
||||||
_balances.Add(balance);
|
_balances.Add(balance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AddTransaction(Transaction transaction)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(transaction, nameof(transaction));
|
||||||
|
|
||||||
|
_transactions.Add(transaction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,8 @@ public sealed class Balance : Entity
|
|||||||
string currencyCode
|
string currencyCode
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(currencyCode, nameof(currencyCode));
|
||||||
|
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
Current = current,
|
Current = current,
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ namespace FiscalOS.Core.Accounts;
|
|||||||
public sealed class Institution : Entity
|
public sealed class Institution : Entity
|
||||||
{
|
{
|
||||||
public Guid UserId { get; init; }
|
public Guid UserId { get; init; }
|
||||||
|
public User? User { get; init; }
|
||||||
public string Name { get; init; } = string.Empty;
|
public string Name { get; init; } = string.Empty;
|
||||||
public InstitutionMetadata? Metadata { get; init; }
|
public InstitutionMetadata? Metadata { get; init; }
|
||||||
|
|
||||||
|
private readonly List<Account> _accounts = [];
|
||||||
|
public IEnumerable<Account> Accounts => _accounts;
|
||||||
|
|
||||||
private Institution()
|
private Institution()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -21,4 +25,11 @@ public sealed class Institution : Entity
|
|||||||
Metadata = metadata
|
Metadata = metadata
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AddAccount(Account account)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(account, nameof(account));
|
||||||
|
|
||||||
|
_accounts.Add(account);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,9 @@ public sealed class User : Entity
|
|||||||
private readonly List<Account> _accounts = [];
|
private readonly List<Account> _accounts = [];
|
||||||
public IEnumerable<Account> Accounts => _accounts;
|
public IEnumerable<Account> Accounts => _accounts;
|
||||||
|
|
||||||
|
private readonly List<Transaction> _transactions = [];
|
||||||
|
public IEnumerable<Transaction> Transactions => _transactions;
|
||||||
|
|
||||||
private User()
|
private User()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -60,4 +63,11 @@ public sealed class User : Entity
|
|||||||
|
|
||||||
_accounts.Add(account);
|
_accounts.Add(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AddTransaction(Transaction transaction)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(transaction, nameof(transaction));
|
||||||
|
|
||||||
|
_transactions.Add(transaction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace FiscalOS.Core.Queuing;
|
||||||
|
|
||||||
|
public interface IAsyncQueue<T>
|
||||||
|
{
|
||||||
|
Task EnqueueAsync(T item, CancellationToken cancellationToken = default);
|
||||||
|
Task<T> DequeueAsync(CancellationToken cancellationToken = default);
|
||||||
|
ValueTask DisposeAsync();
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace FiscalOS.Core.Queuing;
|
||||||
|
|
||||||
|
public interface IAsyncQueueProcessor<T>
|
||||||
|
{
|
||||||
|
Task ProcessAsync(T item, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
namespace FiscalOS.Core.Transactions;
|
||||||
|
|
||||||
|
public sealed class Transaction : Entity
|
||||||
|
{
|
||||||
|
public Guid UserId { get; private set; }
|
||||||
|
public User? User { get; init; }
|
||||||
|
|
||||||
|
public Guid AccountId { get; init; }
|
||||||
|
public Account? Account { get; init; }
|
||||||
|
|
||||||
|
public string MerchantName { get; private set; } = string.Empty;
|
||||||
|
public string Description { get; init; } = string.Empty;
|
||||||
|
public decimal Amount { get; init; }
|
||||||
|
public DateTimeOffset Date { get; init; }
|
||||||
|
|
||||||
|
public TransactionMetadata? Metadata { get; init; }
|
||||||
|
|
||||||
|
private Transaction()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Transaction From(
|
||||||
|
string merchantName,
|
||||||
|
string description,
|
||||||
|
decimal? amount,
|
||||||
|
DateTimeOffset? date,
|
||||||
|
TransactionMetadata metadata
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return From(Guid.Empty, Guid.Empty, Guid.Empty, merchantName, description, amount, date, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Transaction From(
|
||||||
|
Guid userId,
|
||||||
|
Guid accountId,
|
||||||
|
string merchantName,
|
||||||
|
string description,
|
||||||
|
decimal? amount,
|
||||||
|
DateTimeOffset? date,
|
||||||
|
TransactionMetadata metadata
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return From(Guid.Empty, userId, accountId, merchantName, description, amount, date, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Transaction From(
|
||||||
|
Guid id,
|
||||||
|
Guid userId,
|
||||||
|
Guid accountId,
|
||||||
|
string merchantName,
|
||||||
|
string description,
|
||||||
|
decimal? amount,
|
||||||
|
DateTimeOffset? date,
|
||||||
|
TransactionMetadata metadata
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(merchantName, nameof(merchantName));
|
||||||
|
ArgumentNullException.ThrowIfNull(description, nameof(description));
|
||||||
|
|
||||||
|
if (amount.HasValue is false)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date.HasValue is false)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(date));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
UserId = userId,
|
||||||
|
AccountId = accountId,
|
||||||
|
MerchantName = merchantName,
|
||||||
|
Description = description,
|
||||||
|
Amount = amount.Value,
|
||||||
|
Date = date.Value,
|
||||||
|
Metadata = metadata,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace FiscalOS.Core.Transactions;
|
||||||
|
|
||||||
|
public abstract class TransactionMetadata : Entity
|
||||||
|
{
|
||||||
|
private readonly string _type;
|
||||||
|
|
||||||
|
public string Type => _type;
|
||||||
|
|
||||||
|
public Guid TransactionId { get; init; }
|
||||||
|
|
||||||
|
protected TransactionMetadata(string type)
|
||||||
|
{
|
||||||
|
_type = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
global using FiscalOS.Core.Accounts;
|
global using FiscalOS.Core.Accounts;
|
||||||
global using FiscalOS.Core.Data;
|
global using FiscalOS.Core.Data;
|
||||||
global using FiscalOS.Core.Identity;
|
global using FiscalOS.Core.Identity;
|
||||||
global using FiscalOS.Core.Security;
|
global using FiscalOS.Core.Security;
|
||||||
|
global using FiscalOS.Core.Transactions;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
[*.cs]
|
[*.cs]
|
||||||
|
|
||||||
dotnet_diagnostic.CA1062.severity = none
|
dotnet_diagnostic.CA1062.severity = none
|
||||||
dotnet_diagnostic.CA5404.severity = none
|
dotnet_diagnostic.CA5404.severity = none
|
||||||
@@ -5,12 +5,16 @@ public sealed class PlaidAccountMetadata : AccountMetadata
|
|||||||
public const string TypeValue = Providers.Plaid;
|
public const string TypeValue = Providers.Plaid;
|
||||||
public string PlaidId { get; init; } = string.Empty;
|
public string PlaidId { get; init; } = string.Empty;
|
||||||
public string PlaidName { get; init; } = string.Empty;
|
public string PlaidName { get; init; } = string.Empty;
|
||||||
|
public string? Cursor { get; private set; }
|
||||||
|
|
||||||
private PlaidAccountMetadata() : base(TypeValue)
|
private PlaidAccountMetadata() : base(TypeValue)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PlaidAccountMetadata From(string plaidId, string plaidName)
|
public static PlaidAccountMetadata From(
|
||||||
|
string plaidId,
|
||||||
|
string plaidName
|
||||||
|
)
|
||||||
{
|
{
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
@@ -18,4 +22,11 @@ public sealed class PlaidAccountMetadata : AccountMetadata
|
|||||||
PlaidName = plaidName,
|
PlaidName = plaidName,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetCursor(string cursor)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(cursor, nameof(cursor));
|
||||||
|
|
||||||
|
Cursor = cursor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
namespace FiscalOS.Infra.Accounts.Plaid;
|
||||||
|
|
||||||
|
public interface IPlaidAccountService
|
||||||
|
{
|
||||||
|
Task<string> CreateLinkTokenAsync(string id);
|
||||||
|
Task<(string ItemId, string AccessToken)> ExchangeTokenAsync(string publicToken);
|
||||||
|
Task<List<Going.Plaid.Entity.Account>> GetAccountsAsync(string accessToken);
|
||||||
|
Task<ItemWithConsentFields> GetItemAsync(string accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PlaidAccountService : IPlaidAccountService
|
||||||
|
{
|
||||||
|
private readonly IOptions<PlaidClientOptions> _options;
|
||||||
|
private readonly PlaidClient _client;
|
||||||
|
|
||||||
|
private PlaidAccountService(PlaidClient client, IOptions<PlaidClientOptions> options)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlaidAccountService From(IServiceProvider sp)
|
||||||
|
{
|
||||||
|
var client = sp.GetRequiredService<PlaidClient>();
|
||||||
|
var options = sp.GetRequiredService<IOptions<PlaidClientOptions>>();
|
||||||
|
return new(client, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> CreateLinkTokenAsync(string id)
|
||||||
|
{
|
||||||
|
var assemblyName = Assembly.GetExecutingAssembly().GetName().FullName;
|
||||||
|
var environmentName = System.Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
|
||||||
|
|
||||||
|
var linkTokenResponse = await _client.LinkTokenCreateAsync(new()
|
||||||
|
{
|
||||||
|
ClientName = $"{assemblyName}_{environmentName}",
|
||||||
|
Products = [Products.Transactions],
|
||||||
|
CountryCodes = [CountryCode.Us],
|
||||||
|
Language = Language.English,
|
||||||
|
User = new()
|
||||||
|
{
|
||||||
|
ClientUserId = id,
|
||||||
|
},
|
||||||
|
Webhook = _options.Value.Webhook,
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (linkTokenResponse.IsSuccessStatusCode is false)
|
||||||
|
{
|
||||||
|
throw new PlaidException(
|
||||||
|
"Unable to create link token",
|
||||||
|
linkTokenResponse.Error,
|
||||||
|
linkTokenResponse.RequestId,
|
||||||
|
(int?)linkTokenResponse.StatusCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return linkTokenResponse.LinkToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(string ItemId, string AccessToken)> ExchangeTokenAsync(string publicToken)
|
||||||
|
{
|
||||||
|
var publicTokenResponse = await _client.ItemPublicTokenExchangeAsync(new()
|
||||||
|
{
|
||||||
|
PublicToken = publicToken,
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (publicTokenResponse.IsSuccessStatusCode is false)
|
||||||
|
{
|
||||||
|
throw new PlaidException(
|
||||||
|
"Unable to exchange public token for access token",
|
||||||
|
publicTokenResponse.Error,
|
||||||
|
publicTokenResponse.RequestId,
|
||||||
|
(int?)publicTokenResponse.StatusCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (publicTokenResponse.ItemId, publicTokenResponse.AccessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Going.Plaid.Entity.Account>> GetAccountsAsync(string accessToken)
|
||||||
|
{
|
||||||
|
var accountResponse = await _client.AccountsGetAsync(new()
|
||||||
|
{
|
||||||
|
AccessToken = accessToken,
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (accountResponse.IsSuccessStatusCode is false)
|
||||||
|
{
|
||||||
|
throw new PlaidException(
|
||||||
|
"Unable to retrieve accounts",
|
||||||
|
accountResponse.Error,
|
||||||
|
accountResponse.RequestId,
|
||||||
|
(int?)accountResponse.StatusCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [.. accountResponse.Accounts];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ItemWithConsentFields> GetItemAsync(string accessToken)
|
||||||
|
{
|
||||||
|
var itemResponse = await _client.ItemGetAsync(new()
|
||||||
|
{
|
||||||
|
AccessToken = accessToken,
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (itemResponse.IsSuccessStatusCode is false)
|
||||||
|
{
|
||||||
|
throw new PlaidException(
|
||||||
|
"Unable to retrieve item",
|
||||||
|
itemResponse.Error,
|
||||||
|
itemResponse.RequestId,
|
||||||
|
(int?)itemResponse.StatusCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemResponse.Item;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ public sealed record PlaidClientOptions
|
|||||||
{
|
{
|
||||||
public string ClientId { get; init; } = string.Empty;
|
public string ClientId { get; init; } = string.Empty;
|
||||||
public string Secret { get; init; } = string.Empty;
|
public string Secret { get; init; } = string.Empty;
|
||||||
|
public string Webhook { get; init; } = string.Empty;
|
||||||
|
|
||||||
public IOptions<PlaidOptions> ToPlaidOptions()
|
public IOptions<PlaidOptions> ToPlaidOptions()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
namespace FiscalOS.Infra.Accounts.Plaid;
|
|
||||||
|
|
||||||
public class PlaidException : Exception
|
|
||||||
{
|
|
||||||
public PlaidException()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public PlaidException(string message) : base(message)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public PlaidException(string message, Exception innerException) : base(message, innerException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+10
-6
@@ -1,31 +1,35 @@
|
|||||||
namespace FiscalOS.Infra.Accounts.Plaid;
|
namespace FiscalOS.Infra.Accounts.Plaid;
|
||||||
|
|
||||||
public sealed class PlaidMetadata : InstitutionMetadata
|
public sealed class PlaidInstitutionMetadata : InstitutionMetadata
|
||||||
{
|
{
|
||||||
public const string TypeValue = Providers.Plaid;
|
public const string TypeValue = Providers.Plaid;
|
||||||
public string PlaidId { get; init; } = string.Empty;
|
public string PlaidId { get; init; } = string.Empty;
|
||||||
public string PlaidName { get; init; } = string.Empty;
|
public string PlaidName { get; init; } = string.Empty;
|
||||||
public string EncryptedAccessToken { get; init; } = string.Empty;
|
public string EncryptedAccessToken { get; init; } = string.Empty;
|
||||||
|
public string ItemId { get; init; } = string.Empty;
|
||||||
|
|
||||||
private PlaidMetadata() : base(TypeValue)
|
private PlaidInstitutionMetadata() : base(TypeValue)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PlaidMetadata From(
|
public static PlaidInstitutionMetadata From(
|
||||||
string? plaidId,
|
string? plaidId,
|
||||||
string? plaidName,
|
string? plaidName,
|
||||||
string encryptedAccessToken
|
string encryptedAccessToken,
|
||||||
|
string itemId
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(plaidId, nameof(plaidId));
|
ArgumentNullException.ThrowIfNull(plaidId, nameof(plaidId));
|
||||||
ArgumentNullException.ThrowIfNull(plaidName, nameof(plaidName));
|
ArgumentNullException.ThrowIfNull(plaidName, nameof(plaidName));
|
||||||
ArgumentNullException.ThrowIfNull(encryptedAccessToken, nameof(encryptedAccessToken));
|
ArgumentNullException.ThrowIfNull(encryptedAccessToken, nameof(encryptedAccessToken));
|
||||||
|
ArgumentNullException.ThrowIfNull(itemId, nameof(itemId));
|
||||||
|
|
||||||
return new PlaidMetadata
|
return new()
|
||||||
{
|
{
|
||||||
PlaidId = plaidId,
|
PlaidId = plaidId,
|
||||||
PlaidName = plaidName,
|
PlaidName = plaidName,
|
||||||
EncryptedAccessToken = encryptedAccessToken
|
EncryptedAccessToken = encryptedAccessToken,
|
||||||
|
ItemId = itemId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace FiscalOS.Infra.Accounts.Plaid;
|
|
||||||
|
|
||||||
public sealed class PlaidService
|
|
||||||
{
|
|
||||||
private readonly PlaidClient _client;
|
|
||||||
|
|
||||||
private PlaidService(PlaidClient client)
|
|
||||||
{
|
|
||||||
_client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PlaidService From(IServiceProvider sp)
|
|
||||||
{
|
|
||||||
var client = sp.GetRequiredService<PlaidClient>();
|
|
||||||
return new(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> CreateLinkTokenAsync(string id)
|
|
||||||
{
|
|
||||||
var assemblyName = Assembly.GetExecutingAssembly().GetName().FullName;
|
|
||||||
var environmentName = System.Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
|
|
||||||
|
|
||||||
var ltr = await _client.LinkTokenCreateAsync(new()
|
|
||||||
{
|
|
||||||
ClientName = $"{assemblyName}_{environmentName}",
|
|
||||||
Products = [Products.Transactions],
|
|
||||||
CountryCodes = [CountryCode.Us],
|
|
||||||
Language = Language.English,
|
|
||||||
User = new()
|
|
||||||
{
|
|
||||||
ClientUserId = id,
|
|
||||||
},
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (ltr.IsSuccessStatusCode is false)
|
|
||||||
{
|
|
||||||
throw new PlaidException("Unable to create link token");
|
|
||||||
}
|
|
||||||
|
|
||||||
return ltr.LinkToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<(string ItemId, string AccessToken)> ExchangeTokenAsync(string publicToken)
|
|
||||||
{
|
|
||||||
var ptr = await _client.ItemPublicTokenExchangeAsync(new()
|
|
||||||
{
|
|
||||||
PublicToken = publicToken,
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (ptr.IsSuccessStatusCode is false)
|
|
||||||
{
|
|
||||||
throw new PlaidException("Unable to exchange public token for access token");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (ptr.ItemId, ptr.AccessToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<Going.Plaid.Entity.Account>> GetAccountsAsync(string accessToken)
|
|
||||||
{
|
|
||||||
var ar = await _client.AccountsGetAsync(new()
|
|
||||||
{
|
|
||||||
AccessToken = accessToken,
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (ar.IsSuccessStatusCode is false)
|
|
||||||
{
|
|
||||||
throw new PlaidException("Unable to retrieve accounts");
|
|
||||||
}
|
|
||||||
|
|
||||||
return [.. ar.Accounts];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ItemWithConsentFields> GetItemAsync(string accessToken)
|
|
||||||
{
|
|
||||||
var ar = await _client.ItemGetAsync(new()
|
|
||||||
{
|
|
||||||
AccessToken = accessToken,
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (ar.IsSuccessStatusCode is false)
|
|
||||||
{
|
|
||||||
throw new PlaidException("Unable to retrieve item");
|
|
||||||
}
|
|
||||||
|
|
||||||
return ar.Item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
namespace FiscalOS.Infra.Common;
|
||||||
|
|
||||||
|
public class PlaidException : Exception
|
||||||
|
{
|
||||||
|
public PlaidError? Error { get; }
|
||||||
|
public string? RequestId { get; }
|
||||||
|
public int? StatusCode { get; }
|
||||||
|
public string? ErrorCode => Error?.ErrorCode;
|
||||||
|
public string? ErrorType => Error?.ErrorType;
|
||||||
|
public string? ErrorMessage => Error?.ErrorMessage;
|
||||||
|
|
||||||
|
public PlaidException()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlaidException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlaidException(string message, Exception innerException) : base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlaidException(
|
||||||
|
string message,
|
||||||
|
PlaidError? error = null,
|
||||||
|
string? plaidRequestId = null,
|
||||||
|
int? statusCode = null,
|
||||||
|
Exception? innerException = null
|
||||||
|
) : base(message, innerException)
|
||||||
|
{
|
||||||
|
Error = error;
|
||||||
|
RequestId = plaidRequestId;
|
||||||
|
StatusCode = statusCode;
|
||||||
|
|
||||||
|
if (error is not null)
|
||||||
|
{
|
||||||
|
Data[nameof(ErrorCode)] = error.ErrorCode;
|
||||||
|
Data[nameof(ErrorType)] = error.ErrorType;
|
||||||
|
Data[nameof(ErrorMessage)] = error.ErrorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plaidRequestId is not null)
|
||||||
|
{
|
||||||
|
Data[nameof(RequestId)] = plaidRequestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusCode is not null)
|
||||||
|
{
|
||||||
|
Data[nameof(StatusCode)] = statusCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace FiscalOS.Infra.Accounts;
|
namespace FiscalOS.Infra.Common;
|
||||||
|
|
||||||
public static class Providers
|
public static class Providers
|
||||||
{
|
{
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Account = FiscalOS.Core.Accounts.Account;
|
using Account = FiscalOS.Core.Accounts.Account;
|
||||||
using Institution = FiscalOS.Core.Accounts.Institution;
|
using Institution = FiscalOS.Core.Accounts.Institution;
|
||||||
|
using Transaction = FiscalOS.Core.Transactions.Transaction;
|
||||||
|
|
||||||
namespace FiscalOS.Infra.Data;
|
namespace FiscalOS.Infra.Data;
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ public sealed class AppDbContext(
|
|||||||
|
|
||||||
public DbSet<User> Users => Set<User>();
|
public DbSet<User> Users => Set<User>();
|
||||||
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
|
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
|
||||||
|
public DbSet<Institution> Institutions => Set<Institution>();
|
||||||
|
public DbSet<Transaction> Transactions => Set<Transaction>();
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
@@ -63,7 +66,7 @@ public sealed class AppDbContext(
|
|||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
eb.HasMany(static u => u.Institutions)
|
eb.HasMany(static u => u.Institutions)
|
||||||
.WithOne()
|
.WithOne(static i => i.User)
|
||||||
.HasForeignKey(static i => i.UserId)
|
.HasForeignKey(static i => i.UserId)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
@@ -96,19 +99,20 @@ public sealed class AppDbContext(
|
|||||||
modelBuilder.Entity<InstitutionMetadata>(static eb =>
|
modelBuilder.Entity<InstitutionMetadata>(static eb =>
|
||||||
{
|
{
|
||||||
eb.HasDiscriminator(static m => m.Type)
|
eb.HasDiscriminator(static m => m.Type)
|
||||||
.HasValue<PlaidMetadata>(PlaidMetadata.TypeValue);
|
.HasValue<PlaidInstitutionMetadata>(PlaidInstitutionMetadata.TypeValue);
|
||||||
|
|
||||||
eb.Property(static m => m.InstitutionId);
|
eb.Property(static m => m.InstitutionId);
|
||||||
eb.Property(static m => m.Type);
|
eb.Property(static m => m.Type);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<PlaidMetadata>(static eb =>
|
modelBuilder.Entity<PlaidInstitutionMetadata>(static eb =>
|
||||||
{
|
{
|
||||||
eb.HasBaseType<InstitutionMetadata>();
|
eb.HasBaseType<InstitutionMetadata>();
|
||||||
|
|
||||||
eb.Property(static m => m.PlaidId);
|
eb.Property(static m => m.PlaidId);
|
||||||
eb.Property(static m => m.PlaidName);
|
eb.Property(static m => m.PlaidName);
|
||||||
eb.Property(static m => m.EncryptedAccessToken);
|
eb.Property(static m => m.EncryptedAccessToken);
|
||||||
|
eb.Property(static m => m.ItemId);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<Account>(static eb =>
|
modelBuilder.Entity<Account>(static eb =>
|
||||||
@@ -116,7 +120,7 @@ public sealed class AppDbContext(
|
|||||||
eb.Property(static a => a.Name);
|
eb.Property(static a => a.Name);
|
||||||
|
|
||||||
eb.HasOne(static a => a.Institution)
|
eb.HasOne(static a => a.Institution)
|
||||||
.WithMany()
|
.WithMany(static i => i.Accounts)
|
||||||
.HasForeignKey(static a => a.InstitutionId)
|
.HasForeignKey(static a => a.InstitutionId)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
@@ -146,6 +150,7 @@ public sealed class AppDbContext(
|
|||||||
|
|
||||||
eb.Property(static m => m.PlaidId);
|
eb.Property(static m => m.PlaidId);
|
||||||
eb.Property(static m => m.PlaidName);
|
eb.Property(static m => m.PlaidName);
|
||||||
|
eb.Property(static m => m.Cursor);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<Balance>(static eb =>
|
modelBuilder.Entity<Balance>(static eb =>
|
||||||
@@ -155,5 +160,40 @@ public sealed class AppDbContext(
|
|||||||
eb.Property(static b => b.Available);
|
eb.Property(static b => b.Available);
|
||||||
eb.Property(static b => b.CurrencyCode);
|
eb.Property(static b => b.CurrencyCode);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<Transaction>(static eb =>
|
||||||
|
{
|
||||||
|
eb.HasOne(static t => t.User)
|
||||||
|
.WithMany(static u => u.Transactions)
|
||||||
|
.HasForeignKey(static t => t.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
eb.HasOne(static t => t.Account)
|
||||||
|
.WithMany(static a => a.Transactions)
|
||||||
|
.HasForeignKey(static t => t.AccountId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
eb.Property(static t => t.MerchantName);
|
||||||
|
eb.Property(static t => t.Description);
|
||||||
|
eb.Property(static t => t.Amount);
|
||||||
|
eb.Property(static t => t.Date);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<TransactionMetadata>(static eb =>
|
||||||
|
{
|
||||||
|
eb.HasDiscriminator(static m => m.Type)
|
||||||
|
.HasValue(PlaidTransactionMetadata.TypeValue);
|
||||||
|
|
||||||
|
eb.Property(static m => m.TransactionId);
|
||||||
|
eb.Property(static m => m.Type);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<PlaidTransactionMetadata>(static eb =>
|
||||||
|
{
|
||||||
|
eb.HasDiscriminator(static m => m.Type)
|
||||||
|
.HasValue(PlaidTransactionMetadata.TypeValue);
|
||||||
|
|
||||||
|
eb.Property(static m => m.PlaidId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
|
||||||
namespace FiscalOS.Infra.DependencyInjection;
|
namespace FiscalOS.Infra.DependencyInjection;
|
||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
public static class ServiceCollectionExtensions
|
||||||
@@ -31,9 +33,28 @@ public static class ServiceCollectionExtensions
|
|||||||
var logger = sp.GetRequiredService<ILogger<PlaidClient>>();
|
var logger = sp.GetRequiredService<ILogger<PlaidClient>>();
|
||||||
var clientOptions = sp.GetRequiredService<IOptions<PlaidClientOptions>>();
|
var clientOptions = sp.GetRequiredService<IOptions<PlaidClientOptions>>();
|
||||||
var options = clientOptions.Value.ToPlaidOptions();
|
var options = clientOptions.Value.ToPlaidOptions();
|
||||||
|
|
||||||
|
var env = System.Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
|
||||||
|
|
||||||
|
if (env is not null && env.Equals("Production", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
options.Value.Environment = Going.Plaid.Environment.Production;
|
||||||
|
}
|
||||||
|
|
||||||
return new PlaidClient(options, factory, logger);
|
return new PlaidClient(options, factory, logger);
|
||||||
});
|
});
|
||||||
services.AddSingleton(PlaidService.From);
|
services.AddSingleton<IPlaidAccountService>(PlaidAccountService.From);
|
||||||
|
services.AddSingleton<IPlaidTransactionService>(PlaidTransactionService.From);
|
||||||
|
|
||||||
|
services.AddScoped<IPlaidAddedTransactionHandler>(PlaidAddedTransactionHandler.From);
|
||||||
|
services.AddScoped<IPlaidModifiedTransactionHandler>(PlaidModifiedTransactionHandler.From);
|
||||||
|
services.AddScoped<IPlaidRemovedTransactionHandler>(PlaidRemovedTransactionHandler.From);
|
||||||
|
services.AddScoped<IPlaidTransactionProcessor>(PlaidTransactionProcessor.From);
|
||||||
|
services.AddScoped<IPlaidTransactionSyncer>(PlaidTransactionSyncer.From);
|
||||||
|
|
||||||
|
services.AddSingleton<IAsyncQueue<SyncUpdatesQueueItem>>(ChannelAsyncQueue<SyncUpdatesQueueItem>.From);
|
||||||
|
services.AddScoped<IAsyncQueueProcessor<SyncUpdatesQueueItem>>(SyncUpdatesProcessor.From);
|
||||||
|
services.AddHostedService(AsyncQueueHostedService<SyncUpdatesQueueItem>.From);
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
|||||||
+374
@@ -0,0 +1,374 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FiscalOS.Infra.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260226123411_AddItemIdToPlaidMetadata")]
|
||||||
|
partial class AddItemIdToPlaidMetadata
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("InstitutionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InstitutionId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Account");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AccountMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Available")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Current")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId");
|
||||||
|
|
||||||
|
b.ToTable("Balance");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Institution");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("InstitutionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InstitutionId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("InstitutionMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ExpiresAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Revoked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("RefreshTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedDataKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptionKeyId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("HashedPassword")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedAccessToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ItemId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||||
|
.WithMany("Accounts")
|
||||||
|
.HasForeignKey("InstitutionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||||
|
.WithMany("Accounts")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Institution");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithMany("Balances")
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||||
|
.WithMany("Institutions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany("RefreshTokens")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Balances");
|
||||||
|
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
|
b.Navigation("Institutions");
|
||||||
|
|
||||||
|
b.Navigation("RefreshTokens");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddItemIdToPlaidMetadata : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ItemId",
|
||||||
|
table: "InstitutionMetadata",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ItemId",
|
||||||
|
table: "InstitutionMetadata");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+376
@@ -0,0 +1,376 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FiscalOS.Infra.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260226131150_AddNavigationPropertyToInstitutionForUser")]
|
||||||
|
partial class AddNavigationPropertyToInstitutionForUser
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("InstitutionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InstitutionId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Account");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AccountMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Available")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Current")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId");
|
||||||
|
|
||||||
|
b.ToTable("Balance");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Institutions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("InstitutionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InstitutionId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("InstitutionMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ExpiresAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Revoked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("RefreshTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedDataKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptionKeyId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("HashedPassword")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedAccessToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ItemId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||||
|
.WithMany("Accounts")
|
||||||
|
.HasForeignKey("InstitutionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||||
|
.WithMany("Accounts")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Institution");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithMany("Balances")
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany("Institutions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany("RefreshTokens")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Balances");
|
||||||
|
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
|
b.Navigation("Institutions");
|
||||||
|
|
||||||
|
b.Navigation("RefreshTokens");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddNavigationPropertyToInstitutionForUser : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Account_Institution_InstitutionId",
|
||||||
|
table: "Account");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Institution_Users_UserId",
|
||||||
|
table: "Institution");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_InstitutionMetadata_Institution_InstitutionId",
|
||||||
|
table: "InstitutionMetadata");
|
||||||
|
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_Institution",
|
||||||
|
table: "Institution");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "Institution",
|
||||||
|
newName: "Institutions");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Institution_UserId",
|
||||||
|
table: "Institutions",
|
||||||
|
newName: "IX_Institutions_UserId");
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_Institutions",
|
||||||
|
table: "Institutions",
|
||||||
|
column: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Account_Institutions_InstitutionId",
|
||||||
|
table: "Account",
|
||||||
|
column: "InstitutionId",
|
||||||
|
principalTable: "Institutions",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_InstitutionMetadata_Institutions_InstitutionId",
|
||||||
|
table: "InstitutionMetadata",
|
||||||
|
column: "InstitutionId",
|
||||||
|
principalTable: "Institutions",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Institutions_Users_UserId",
|
||||||
|
table: "Institutions",
|
||||||
|
column: "UserId",
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Account_Institutions_InstitutionId",
|
||||||
|
table: "Account");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_InstitutionMetadata_Institutions_InstitutionId",
|
||||||
|
table: "InstitutionMetadata");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Institutions_Users_UserId",
|
||||||
|
table: "Institutions");
|
||||||
|
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_Institutions",
|
||||||
|
table: "Institutions");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "Institutions",
|
||||||
|
newName: "Institution");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Institutions_UserId",
|
||||||
|
table: "Institution",
|
||||||
|
newName: "IX_Institution_UserId");
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_Institution",
|
||||||
|
table: "Institution",
|
||||||
|
column: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Account_Institution_InstitutionId",
|
||||||
|
table: "Account",
|
||||||
|
column: "InstitutionId",
|
||||||
|
principalTable: "Institution",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Institution_Users_UserId",
|
||||||
|
table: "Institution",
|
||||||
|
column: "UserId",
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_InstitutionMetadata_Institution_InstitutionId",
|
||||||
|
table: "InstitutionMetadata",
|
||||||
|
column: "InstitutionId",
|
||||||
|
principalTable: "Institution",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+376
@@ -0,0 +1,376 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FiscalOS.Infra.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260226224648_RenamePlaidMetadataEntity")]
|
||||||
|
partial class RenamePlaidMetadataEntity
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("InstitutionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InstitutionId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Account");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AccountMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Available")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Current")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId");
|
||||||
|
|
||||||
|
b.ToTable("Balance");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Institutions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("InstitutionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InstitutionId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("InstitutionMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ExpiresAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Revoked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("RefreshTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedDataKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptionKeyId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("HashedPassword")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidInstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedAccessToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ItemId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||||
|
.WithMany("Accounts")
|
||||||
|
.HasForeignKey("InstitutionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||||
|
.WithMany("Accounts")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Institution");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithMany("Balances")
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany("Institutions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany("RefreshTokens")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Balances");
|
||||||
|
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
|
b.Navigation("Institutions");
|
||||||
|
|
||||||
|
b.Navigation("RefreshTokens");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RenamePlaidMetadataEntity : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+504
@@ -0,0 +1,504 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FiscalOS.Infra.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260228123108_AddTransactionAndTransactionMetadataModels")]
|
||||||
|
partial class AddTransactionAndTransactionMetadataModels
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("InstitutionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InstitutionId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Account");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AccountMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Available")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Current")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId");
|
||||||
|
|
||||||
|
b.ToTable("Balance");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Institutions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("InstitutionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InstitutionId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("InstitutionMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ExpiresAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Revoked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("RefreshTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedDataKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptionKeyId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("HashedPassword")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AccountId1")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Date")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("MerchantName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId1");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Transaction");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("TransactionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(5)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TransactionId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("TransactionMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("Plaid");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidInstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedAccessToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ItemId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Transactions.Plaid.PlaidTransactionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Transactions.TransactionMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||||
|
.WithMany("Accounts")
|
||||||
|
.HasForeignKey("InstitutionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||||
|
.WithMany("Accounts")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Institution");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithMany("Balances")
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany("Institutions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany("RefreshTokens")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", "Account")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("AccountId1");
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Account");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Transactions.Transaction", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Transactions.TransactionMetadata", "TransactionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Balances");
|
||||||
|
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
|
b.Navigation("Institutions");
|
||||||
|
|
||||||
|
b.Navigation("RefreshTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddTransactionAndTransactionMetadataModels : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Transaction",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
AccountId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
MerchantName = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Amount = table.Column<decimal>(type: "TEXT", nullable: false),
|
||||||
|
Date = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||||
|
AccountId1 = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Transaction", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Transaction_Account_AccountId",
|
||||||
|
column: x => x.AccountId,
|
||||||
|
principalTable: "Account",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Transaction_Account_AccountId1",
|
||||||
|
column: x => x.AccountId1,
|
||||||
|
principalTable: "Account",
|
||||||
|
principalColumn: "Id");
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Transaction_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TransactionMetadata",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
Type = table.Column<string>(type: "TEXT", maxLength: 5, nullable: false),
|
||||||
|
TransactionId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
PlaidId = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TransactionMetadata", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TransactionMetadata_Transaction_TransactionId",
|
||||||
|
column: x => x.TransactionId,
|
||||||
|
principalTable: "Transaction",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Transaction_AccountId",
|
||||||
|
table: "Transaction",
|
||||||
|
column: "AccountId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Transaction_AccountId1",
|
||||||
|
table: "Transaction",
|
||||||
|
column: "AccountId1");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Transaction_UserId",
|
||||||
|
table: "Transaction",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TransactionMetadata_TransactionId",
|
||||||
|
table: "TransactionMetadata",
|
||||||
|
column: "TransactionId",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TransactionMetadata");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Transaction");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+508
@@ -0,0 +1,508 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FiscalOS.Infra.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260301104650_AddCursorToAccountModel")]
|
||||||
|
partial class AddCursorToAccountModel
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("InstitutionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InstitutionId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Account");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AccountMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Available")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Current")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId");
|
||||||
|
|
||||||
|
b.ToTable("Balance");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Institutions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("InstitutionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InstitutionId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("InstitutionMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ExpiresAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Revoked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("RefreshTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedDataKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptionKeyId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("HashedPassword")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AccountId1")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Date")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("MerchantName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId1");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Transaction");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("TransactionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(5)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TransactionId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("TransactionMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("Plaid");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("Cursor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidInstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedAccessToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ItemId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Transactions.Plaid.PlaidTransactionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Transactions.TransactionMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||||
|
.WithMany("Accounts")
|
||||||
|
.HasForeignKey("InstitutionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||||
|
.WithMany("Accounts")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Institution");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithMany("Balances")
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany("Institutions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany("RefreshTokens")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", "Account")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("AccountId1");
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Account");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Transactions.Transaction", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Transactions.TransactionMetadata", "TransactionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Balances");
|
||||||
|
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
|
b.Navigation("Institutions");
|
||||||
|
|
||||||
|
b.Navigation("RefreshTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddCursorToAccountModel : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Cursor",
|
||||||
|
table: "AccountMetadata",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Cursor",
|
||||||
|
table: "AccountMetadata");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+507
@@ -0,0 +1,507 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FiscalOS.Infra.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260302015827_AddInstitutionsDbSet")]
|
||||||
|
partial class AddInstitutionsDbSet
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("InstitutionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InstitutionId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Account");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AccountMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Available")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Current")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId");
|
||||||
|
|
||||||
|
b.ToTable("Balance");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Institutions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("InstitutionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InstitutionId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("InstitutionMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ExpiresAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Revoked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("RefreshTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedDataKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptionKeyId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("HashedPassword")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AccountId1")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Date")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("MerchantName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId1");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("TransactionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(5)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TransactionId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("TransactionMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("Plaid");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("Cursor")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidInstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedAccessToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ItemId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Transactions.Plaid.PlaidTransactionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Transactions.TransactionMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||||
|
.WithMany("Accounts")
|
||||||
|
.HasForeignKey("InstitutionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||||
|
.WithMany("Accounts")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Institution");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithMany("Balances")
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany("Institutions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany("RefreshTokens")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", "Account")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("AccountId1");
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Account");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Transactions.Transaction", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Transactions.TransactionMetadata", "TransactionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Balances");
|
||||||
|
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
|
b.Navigation("Institutions");
|
||||||
|
|
||||||
|
b.Navigation("RefreshTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddInstitutionsDbSet : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Transaction_Account_AccountId",
|
||||||
|
table: "Transaction");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Transaction_Account_AccountId1",
|
||||||
|
table: "Transaction");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Transaction_Users_UserId",
|
||||||
|
table: "Transaction");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_TransactionMetadata_Transaction_TransactionId",
|
||||||
|
table: "TransactionMetadata");
|
||||||
|
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_Transaction",
|
||||||
|
table: "Transaction");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "Transaction",
|
||||||
|
newName: "Transactions");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Transaction_UserId",
|
||||||
|
table: "Transactions",
|
||||||
|
newName: "IX_Transactions_UserId");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Transaction_AccountId1",
|
||||||
|
table: "Transactions",
|
||||||
|
newName: "IX_Transactions_AccountId1");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Transaction_AccountId",
|
||||||
|
table: "Transactions",
|
||||||
|
newName: "IX_Transactions_AccountId");
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_Transactions",
|
||||||
|
table: "Transactions",
|
||||||
|
column: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_TransactionMetadata_Transactions_TransactionId",
|
||||||
|
table: "TransactionMetadata",
|
||||||
|
column: "TransactionId",
|
||||||
|
principalTable: "Transactions",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Transactions_Account_AccountId",
|
||||||
|
table: "Transactions",
|
||||||
|
column: "AccountId",
|
||||||
|
principalTable: "Account",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Transactions_Account_AccountId1",
|
||||||
|
table: "Transactions",
|
||||||
|
column: "AccountId1",
|
||||||
|
principalTable: "Account",
|
||||||
|
principalColumn: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Transactions_Users_UserId",
|
||||||
|
table: "Transactions",
|
||||||
|
column: "UserId",
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_TransactionMetadata_Transactions_TransactionId",
|
||||||
|
table: "TransactionMetadata");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Transactions_Account_AccountId",
|
||||||
|
table: "Transactions");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Transactions_Account_AccountId1",
|
||||||
|
table: "Transactions");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Transactions_Users_UserId",
|
||||||
|
table: "Transactions");
|
||||||
|
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_Transactions",
|
||||||
|
table: "Transactions");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "Transactions",
|
||||||
|
newName: "Transaction");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Transactions_UserId",
|
||||||
|
table: "Transaction",
|
||||||
|
newName: "IX_Transaction_UserId");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Transactions_AccountId1",
|
||||||
|
table: "Transaction",
|
||||||
|
newName: "IX_Transaction_AccountId1");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Transactions_AccountId",
|
||||||
|
table: "Transaction",
|
||||||
|
newName: "IX_Transaction_AccountId");
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_Transaction",
|
||||||
|
table: "Transaction",
|
||||||
|
column: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Transaction_Account_AccountId",
|
||||||
|
table: "Transaction",
|
||||||
|
column: "AccountId",
|
||||||
|
principalTable: "Account",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Transaction_Account_AccountId1",
|
||||||
|
table: "Transaction",
|
||||||
|
column: "AccountId1",
|
||||||
|
principalTable: "Account",
|
||||||
|
principalColumn: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Transaction_Users_UserId",
|
||||||
|
table: "Transaction",
|
||||||
|
column: "UserId",
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_TransactionMetadata_Transaction_TransactionId",
|
||||||
|
table: "TransactionMetadata",
|
||||||
|
column: "TransactionId",
|
||||||
|
principalTable: "Transaction",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+518
@@ -0,0 +1,518 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FiscalOS.Infra.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260303124406_AddTransactionsNavigationPropertyToUser")]
|
||||||
|
partial class AddTransactionsNavigationPropertyToUser
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("InstitutionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InstitutionId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Account");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AccountMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Available")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Current")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId");
|
||||||
|
|
||||||
|
b.ToTable("Balance");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Institutions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("InstitutionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InstitutionId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("InstitutionMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ExpiresAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Revoked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("RefreshTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedDataKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptionKeyId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("HashedPassword")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AccountId1")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Date")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("MerchantName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserId1")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId1");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId1");
|
||||||
|
|
||||||
|
b.ToTable("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("TransactionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(5)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TransactionId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("TransactionMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("Plaid");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("Cursor")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidInstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedAccessToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ItemId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Transactions.Plaid.PlaidTransactionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Transactions.TransactionMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||||
|
.WithMany("Accounts")
|
||||||
|
.HasForeignKey("InstitutionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||||
|
.WithMany("Accounts")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Institution");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithMany("Balances")
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany("Institutions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany("RefreshTokens")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", "Account")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("AccountId1");
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("UserId1");
|
||||||
|
|
||||||
|
b.Navigation("Account");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Transactions.Transaction", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Transactions.TransactionMetadata", "TransactionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Balances");
|
||||||
|
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
|
b.Navigation("Institutions");
|
||||||
|
|
||||||
|
b.Navigation("RefreshTokens");
|
||||||
|
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddTransactionsNavigationPropertyToUser : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "UserId1",
|
||||||
|
table: "Transactions",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Transactions_UserId1",
|
||||||
|
table: "Transactions",
|
||||||
|
column: "UserId1");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Transactions_Users_UserId1",
|
||||||
|
table: "Transactions",
|
||||||
|
column: "UserId1",
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Transactions_Users_UserId1",
|
||||||
|
table: "Transactions");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Transactions_UserId1",
|
||||||
|
table: "Transactions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "UserId1",
|
||||||
|
table: "Transactions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+500
@@ -0,0 +1,500 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FiscalOS.Infra.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260303130637_UpdateAppDbContextToIncludeTransactionNavigationProperties")]
|
||||||
|
partial class UpdateAppDbContextToIncludeTransactionNavigationProperties
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("InstitutionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InstitutionId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Account");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AccountMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Available")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Current")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId");
|
||||||
|
|
||||||
|
b.ToTable("Balance");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Institutions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("InstitutionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InstitutionId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("InstitutionMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ExpiresAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Revoked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("RefreshTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedDataKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptionKeyId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("HashedPassword")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Date")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("MerchantName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("TransactionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(5)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TransactionId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("TransactionMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("Plaid");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("Cursor")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidInstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedAccessToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ItemId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Transactions.Plaid.PlaidTransactionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Transactions.TransactionMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||||
|
.WithMany("Accounts")
|
||||||
|
.HasForeignKey("InstitutionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", null)
|
||||||
|
.WithMany("Accounts")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Institution");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", null)
|
||||||
|
.WithMany("Balances")
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany("Institutions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany("RefreshTokens")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", "Account")
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Account");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Transactions.Transaction", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Transactions.TransactionMetadata", "TransactionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Balances");
|
||||||
|
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
|
b.Navigation("Institutions");
|
||||||
|
|
||||||
|
b.Navigation("RefreshTokens");
|
||||||
|
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class UpdateAppDbContextToIncludeTransactionNavigationProperties : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Transactions_Account_AccountId1",
|
||||||
|
table: "Transactions");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Transactions_Users_UserId1",
|
||||||
|
table: "Transactions");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Transactions_AccountId1",
|
||||||
|
table: "Transactions");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Transactions_UserId1",
|
||||||
|
table: "Transactions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AccountId1",
|
||||||
|
table: "Transactions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "UserId1",
|
||||||
|
table: "Transactions");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "AccountId1",
|
||||||
|
table: "Transactions",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "UserId1",
|
||||||
|
table: "Transactions",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Transactions_AccountId1",
|
||||||
|
table: "Transactions",
|
||||||
|
column: "AccountId1");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Transactions_UserId1",
|
||||||
|
table: "Transactions",
|
||||||
|
column: "UserId1");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Transactions_Account_AccountId1",
|
||||||
|
table: "Transactions",
|
||||||
|
column: "AccountId1",
|
||||||
|
principalTable: "Account",
|
||||||
|
principalColumn: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Transactions_Users_UserId1",
|
||||||
|
table: "Transactions",
|
||||||
|
column: "UserId1",
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -135,7 +135,7 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.ToTable("Institution");
|
b.ToTable("Institutions");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
@@ -241,10 +241,86 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
b.ToTable("Users");
|
b.ToTable("Users");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Date")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("MerchantName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("TransactionId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(5)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TransactionId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("TransactionMetadata");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Type").HasValue("Plaid");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
|
||||||
{
|
{
|
||||||
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("Cursor")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("PlaidId")
|
b.Property<string>("PlaidId")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
@@ -256,7 +332,7 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
b.HasDiscriminator().HasValue("Plaid");
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidMetadata", b =>
|
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidInstitutionMetadata", b =>
|
||||||
{
|
{
|
||||||
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
|
||||||
|
|
||||||
@@ -264,6 +340,10 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ItemId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("PlaidId")
|
b.Property<string>("PlaidId")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
@@ -275,10 +355,21 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
b.HasDiscriminator().HasValue("Plaid");
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Infra.Transactions.Plaid.PlaidTransactionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("FiscalOS.Core.Transactions.TransactionMetadata");
|
||||||
|
|
||||||
|
b.Property<string>("PlaidId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Plaid");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
|
||||||
.WithMany()
|
.WithMany("Accounts")
|
||||||
.HasForeignKey("InstitutionId")
|
.HasForeignKey("InstitutionId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
@@ -312,11 +403,13 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("FiscalOS.Core.Identity.User", null)
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
.WithMany("Institutions")
|
.WithMany("Institutions")
|
||||||
.HasForeignKey("UserId")
|
.HasForeignKey("UserId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
|
||||||
@@ -339,15 +432,47 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
b.Navigation("User");
|
b.Navigation("User");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Accounts.Account", "Account")
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FiscalOS.Core.Identity.User", "User")
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Account");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FiscalOS.Core.Transactions.Transaction", null)
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("FiscalOS.Core.Transactions.TransactionMetadata", "TransactionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Balances");
|
b.Navigation("Balances");
|
||||||
|
|
||||||
b.Navigation("Metadata");
|
b.Navigation("Metadata");
|
||||||
|
|
||||||
|
b.Navigation("Transactions");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
|
||||||
{
|
{
|
||||||
|
b.Navigation("Accounts");
|
||||||
|
|
||||||
b.Navigation("Metadata");
|
b.Navigation("Metadata");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -358,6 +483,13 @@ namespace FiscalOS.Infra.Migrations
|
|||||||
b.Navigation("Institutions");
|
b.Navigation("Institutions");
|
||||||
|
|
||||||
b.Navigation("RefreshTokens");
|
b.Navigation("RefreshTokens");
|
||||||
|
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Metadata");
|
||||||
});
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
namespace FiscalOS.Infra.Queuing;
|
||||||
|
|
||||||
|
internal class AsyncQueueHostedService<T> : BackgroundService where T : notnull
|
||||||
|
{
|
||||||
|
private readonly IAsyncQueue<T> _queue;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ILogger<AsyncQueueHostedService<T>> _logger;
|
||||||
|
|
||||||
|
private AsyncQueueHostedService(
|
||||||
|
IAsyncQueue<T> queue,
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
ILogger<AsyncQueueHostedService<T>> logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_queue = queue;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AsyncQueueHostedService<T> From(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
var queue = serviceProvider.GetRequiredService<IAsyncQueue<T>>();
|
||||||
|
var logger = serviceProvider.GetRequiredService<ILogger<AsyncQueueHostedService<T>>>();
|
||||||
|
return new(queue, serviceProvider, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Queue hosted service for {ItemType} starting", typeof(T).Name);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var item = await _queue.DequeueAsync(stoppingToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var processor = scope.ServiceProvider.GetRequiredService<IAsyncQueueProcessor<T>>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await processor.ProcessAsync(item, stoppingToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error processing queue item of type {ItemType}", typeof(T).Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Queue hosted service for {ItemType} stopped", typeof(T).Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
namespace FiscalOS.Infra.Queuing;
|
||||||
|
|
||||||
|
internal class ChannelAsyncQueue<T> : IAsyncQueue<T>
|
||||||
|
{
|
||||||
|
private readonly Channel<T> _channel = Channel.CreateUnbounded<T>();
|
||||||
|
|
||||||
|
private ChannelAsyncQueue()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ChannelAsyncQueue<T> From(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
return new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EnqueueAsync(T item, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _channel.Writer.WriteAsync(item, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T> DequeueAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _channel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
_channel.Writer.Complete();
|
||||||
|
await _channel.Reader.Completion.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
using Account = FiscalOS.Core.Accounts.Account;
|
||||||
|
using Transaction = FiscalOS.Core.Transactions.Transaction;
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||||
|
|
||||||
|
internal interface IPlaidAddedTransactionHandler
|
||||||
|
{
|
||||||
|
Task<int> HandleAsync(
|
||||||
|
Account account,
|
||||||
|
IEnumerable<Going.Plaid.Entity.Transaction> added,
|
||||||
|
CancellationToken ct
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PlaidAddedTransactionHandler : IPlaidAddedTransactionHandler
|
||||||
|
{
|
||||||
|
private readonly ILogger<PlaidAddedTransactionHandler> _logger;
|
||||||
|
private readonly AppDbContext _appDbContext;
|
||||||
|
|
||||||
|
private PlaidAddedTransactionHandler(
|
||||||
|
ILogger<PlaidAddedTransactionHandler> logger,
|
||||||
|
AppDbContext appDbContext
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_appDbContext = appDbContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlaidAddedTransactionHandler From(
|
||||||
|
ILogger<PlaidAddedTransactionHandler> logger,
|
||||||
|
AppDbContext appDbContext
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return new(logger, appDbContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlaidAddedTransactionHandler From(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
return new(
|
||||||
|
serviceProvider.GetRequiredService<ILogger<PlaidAddedTransactionHandler>>(),
|
||||||
|
serviceProvider.GetRequiredService<AppDbContext>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> HandleAsync(Account account, IEnumerable<Going.Plaid.Entity.Transaction> added, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var existingTransactionIds = added.Select(t => t.TransactionId);
|
||||||
|
var existingTransactions = await _appDbContext.Transactions
|
||||||
|
.Include(t => t.Metadata)
|
||||||
|
.Where(t => t.Metadata is PlaidTransactionMetadata && existingTransactionIds.Contains(((PlaidTransactionMetadata)t.Metadata).PlaidId))
|
||||||
|
.ToListAsync(ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
var addedCount = 0;
|
||||||
|
|
||||||
|
foreach (var addedTransaction in added)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (addedTransaction.Pending.GetValueOrDefault())
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Skipping pending transaction {TransactionId} for account {AccountId} as it has not been posted yet",
|
||||||
|
addedTransaction.TransactionId,
|
||||||
|
account.Id
|
||||||
|
);
|
||||||
|
addedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingTransaction = existingTransactions.FirstOrDefault(
|
||||||
|
t => t.Metadata is PlaidTransactionMetadata metadata && metadata.PlaidId == addedTransaction.TransactionId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingTransaction is not null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Skipping added transaction {PlaidTransactionId} for account {AccountId} as it has already been added as transaction {TransactionId}",
|
||||||
|
addedTransaction.TransactionId,
|
||||||
|
account.Id,
|
||||||
|
existingTransaction.Id
|
||||||
|
);
|
||||||
|
addedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var transactionMetadata = PlaidTransactionMetadata.From(addedTransaction.TransactionId);
|
||||||
|
var transaction = Transaction.From(
|
||||||
|
account.UserId,
|
||||||
|
account.Id,
|
||||||
|
addedTransaction.Merchant,
|
||||||
|
addedTransaction.Description,
|
||||||
|
addedTransaction.CanonicalAmount,
|
||||||
|
addedTransaction.PostedDate,
|
||||||
|
transactionMetadata
|
||||||
|
);
|
||||||
|
account.AddTransaction(transaction);
|
||||||
|
addedCount++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
ex,
|
||||||
|
"Failed to added transaction {PlaidTransactionId} to account {AccountId}",
|
||||||
|
account.Id,
|
||||||
|
addedTransaction.TransactionId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Added {AddedCount} transactions to account {AccountId}", addedCount, account.Id);
|
||||||
|
|
||||||
|
return addedCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||||
|
|
||||||
|
public static class PlaidExtensions
|
||||||
|
{
|
||||||
|
#pragma warning disable CA1034
|
||||||
|
extension(Going.Plaid.Entity.Transaction transaction)
|
||||||
|
{
|
||||||
|
public string Merchant => transaction.MerchantName ?? "Unknown merchant";
|
||||||
|
public string Description => transaction.OriginalDescription ?? "";
|
||||||
|
public decimal? CanonicalAmount => transaction.Amount is decimal amount ? -amount : null;
|
||||||
|
public DateTimeOffset PostedDate => transaction.Datetime ?? (
|
||||||
|
transaction.Date.HasValue
|
||||||
|
? new DateTimeOffset(
|
||||||
|
transaction.Date.Value.Year,
|
||||||
|
transaction.Date.Value.Month,
|
||||||
|
transaction.Date.Value.Day,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
TimeSpan.Zero
|
||||||
|
)
|
||||||
|
: throw new InvalidOperationException($"Transaction {transaction.TransactionId} does not have a valid date or datetime")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
using Account = FiscalOS.Core.Accounts.Account;
|
||||||
|
using Transaction = FiscalOS.Core.Transactions.Transaction;
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||||
|
|
||||||
|
internal interface IPlaidModifiedTransactionHandler
|
||||||
|
{
|
||||||
|
Task<int> HandleAsync(
|
||||||
|
Account account,
|
||||||
|
IEnumerable<Going.Plaid.Entity.Transaction> modified,
|
||||||
|
CancellationToken ct
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PlaidModifiedTransactionHandler : IPlaidModifiedTransactionHandler
|
||||||
|
{
|
||||||
|
private readonly ILogger<PlaidModifiedTransactionHandler> _logger;
|
||||||
|
private readonly AppDbContext _appDbContext;
|
||||||
|
|
||||||
|
private PlaidModifiedTransactionHandler(
|
||||||
|
ILogger<PlaidModifiedTransactionHandler> logger,
|
||||||
|
AppDbContext appDbContext
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_appDbContext = appDbContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlaidModifiedTransactionHandler From(
|
||||||
|
ILogger<PlaidModifiedTransactionHandler> logger,
|
||||||
|
AppDbContext appDbContext
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return new(logger, appDbContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlaidModifiedTransactionHandler From(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
return new(
|
||||||
|
serviceProvider.GetRequiredService<ILogger<PlaidModifiedTransactionHandler>>(),
|
||||||
|
serviceProvider.GetRequiredService<AppDbContext>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> HandleAsync(Account account, IEnumerable<Going.Plaid.Entity.Transaction> modified, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var modifiedTransactionIds = modified.Select(t => t.TransactionId);
|
||||||
|
var existingModifiedTransactions = await _appDbContext.Transactions
|
||||||
|
.Include(t => t.Metadata)
|
||||||
|
.Where(t => t.Metadata is PlaidTransactionMetadata && modifiedTransactionIds.Contains(((PlaidTransactionMetadata)t.Metadata).PlaidId))
|
||||||
|
.ToListAsync(ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
var modifiedCount = 0;
|
||||||
|
|
||||||
|
foreach (var existing in existingModifiedTransactions)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (existing.Metadata is not PlaidTransactionMetadata plaidMetadata)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Existing transaction {TransactionId} has non-Plaid metadata. Skipping update for this transaction.",
|
||||||
|
existing.Id
|
||||||
|
);
|
||||||
|
modifiedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var plaidModifiedTransaction = modified.FirstOrDefault(t => t.TransactionId == plaidMetadata.PlaidId);
|
||||||
|
|
||||||
|
if (plaidModifiedTransaction is null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"No corresponding modified transaction found in Plaid response for existing transaction {TransactionId}. Skipping update for this transaction.",
|
||||||
|
existing.Id
|
||||||
|
);
|
||||||
|
modifiedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newTransactionData = Transaction.From(
|
||||||
|
existing.Id,
|
||||||
|
existing.UserId,
|
||||||
|
existing.AccountId,
|
||||||
|
plaidModifiedTransaction.Merchant,
|
||||||
|
plaidModifiedTransaction.Description,
|
||||||
|
plaidModifiedTransaction.CanonicalAmount,
|
||||||
|
plaidModifiedTransaction.PostedDate,
|
||||||
|
plaidMetadata
|
||||||
|
);
|
||||||
|
|
||||||
|
_appDbContext.Entry(existing).CurrentValues.SetValues(newTransactionData);
|
||||||
|
modifiedCount++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
ex,
|
||||||
|
"Failed to update transaction {TransactionId} for account {AccountId}",
|
||||||
|
account.Id,
|
||||||
|
existing.Id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Modified {ModifiedCount} transactions for account {AccountId}", modifiedCount, account.Id);
|
||||||
|
|
||||||
|
return modifiedCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||||
|
|
||||||
|
internal interface IPlaidRemovedTransactionHandler
|
||||||
|
{
|
||||||
|
Task<int> HandleAsync(IEnumerable<RemovedTransaction> removed, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PlaidRemovedTransactionHandler : IPlaidRemovedTransactionHandler
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _appDbContext;
|
||||||
|
private readonly ILogger<PlaidRemovedTransactionHandler> _logger;
|
||||||
|
|
||||||
|
private PlaidRemovedTransactionHandler(
|
||||||
|
AppDbContext appDbContext,
|
||||||
|
ILogger<PlaidRemovedTransactionHandler> logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_appDbContext = appDbContext;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlaidRemovedTransactionHandler From(
|
||||||
|
AppDbContext appDbContext,
|
||||||
|
ILogger<PlaidRemovedTransactionHandler> logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return new(appDbContext, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlaidRemovedTransactionHandler From(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
return new(
|
||||||
|
serviceProvider.GetRequiredService<AppDbContext>(),
|
||||||
|
serviceProvider.GetRequiredService<ILogger<PlaidRemovedTransactionHandler>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> HandleAsync(IEnumerable<RemovedTransaction> removed, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var removedCount = 0;
|
||||||
|
var removedIdsList = removed.Select(t => t.TransactionId);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
removedCount = await _appDbContext.Transactions
|
||||||
|
.Include(t => t.Metadata)
|
||||||
|
.Where(t => t.Metadata is PlaidTransactionMetadata && removedIdsList.Contains(((PlaidTransactionMetadata)t.Metadata).PlaidId))
|
||||||
|
.ExecuteDeleteAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
ex,
|
||||||
|
"Failed to removed transactions {TransactionIds}",
|
||||||
|
removedIdsList
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Removed {RemovedCount} transactions", removedCount);
|
||||||
|
|
||||||
|
return removedCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||||
|
|
||||||
|
public sealed class PlaidTransactionMetadata : TransactionMetadata
|
||||||
|
{
|
||||||
|
public const string TypeValue = Providers.Plaid;
|
||||||
|
public string PlaidId { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
private PlaidTransactionMetadata() : base(TypeValue)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlaidTransactionMetadata From(string? plaidId)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(plaidId, nameof(plaidId));
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
PlaidId = plaidId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
using Account = FiscalOS.Core.Accounts.Account;
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||||
|
|
||||||
|
internal interface IPlaidTransactionProcessor
|
||||||
|
{
|
||||||
|
Task<bool> ProcessAsync(
|
||||||
|
Account account,
|
||||||
|
TransactionsSyncResponse syncResponse,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PlaidTransactionProcessor : IPlaidTransactionProcessor
|
||||||
|
{
|
||||||
|
private readonly IPlaidAddedTransactionHandler _addedHandler;
|
||||||
|
private readonly IPlaidModifiedTransactionHandler _modifiedHandler;
|
||||||
|
private readonly IPlaidRemovedTransactionHandler _removedHandler;
|
||||||
|
private readonly ILogger<PlaidTransactionProcessor> _logger;
|
||||||
|
|
||||||
|
private PlaidTransactionProcessor(
|
||||||
|
IPlaidAddedTransactionHandler addedHandler,
|
||||||
|
IPlaidModifiedTransactionHandler modifiedHandler,
|
||||||
|
IPlaidRemovedTransactionHandler removedHandler,
|
||||||
|
ILogger<PlaidTransactionProcessor> logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_addedHandler = addedHandler;
|
||||||
|
_modifiedHandler = modifiedHandler;
|
||||||
|
_removedHandler = removedHandler;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static PlaidTransactionProcessor From(
|
||||||
|
IPlaidAddedTransactionHandler addedHandler,
|
||||||
|
IPlaidModifiedTransactionHandler modifiedHandler,
|
||||||
|
IPlaidRemovedTransactionHandler removedHandler,
|
||||||
|
ILogger<PlaidTransactionProcessor> logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return new(addedHandler, modifiedHandler, removedHandler, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static PlaidTransactionProcessor From(IServiceProvider provider)
|
||||||
|
{
|
||||||
|
return new(
|
||||||
|
provider.GetRequiredService<IPlaidAddedTransactionHandler>(),
|
||||||
|
provider.GetRequiredService<IPlaidModifiedTransactionHandler>(),
|
||||||
|
provider.GetRequiredService<IPlaidRemovedTransactionHandler>(),
|
||||||
|
provider.GetRequiredService<ILogger<PlaidTransactionProcessor>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ProcessAsync(
|
||||||
|
Account account,
|
||||||
|
TransactionsSyncResponse syncResponse,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var numAdded = await _addedHandler.HandleAsync(
|
||||||
|
account,
|
||||||
|
syncResponse.Added,
|
||||||
|
cancellationToken
|
||||||
|
).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var numModified = await _modifiedHandler.HandleAsync(
|
||||||
|
account,
|
||||||
|
syncResponse.Modified,
|
||||||
|
cancellationToken
|
||||||
|
).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var numRemoved = await _removedHandler.HandleAsync(
|
||||||
|
syncResponse.Removed,
|
||||||
|
cancellationToken
|
||||||
|
).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Processed Plaid transactions sync for account {AccountId}: {NumAdded} of {TotalAdded} added, {NumModified} of {TotalModified} modified, {NumRemoved} of {TotalRemoved} removed",
|
||||||
|
account.Id,
|
||||||
|
numAdded,
|
||||||
|
syncResponse.Added.Count,
|
||||||
|
numModified,
|
||||||
|
syncResponse.Modified.Count,
|
||||||
|
numRemoved,
|
||||||
|
syncResponse.Removed.Count
|
||||||
|
);
|
||||||
|
|
||||||
|
return numAdded == syncResponse.Added.Count &&
|
||||||
|
numModified == syncResponse.Modified.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||||
|
|
||||||
|
internal interface IPlaidTransactionService
|
||||||
|
{
|
||||||
|
IAsyncEnumerable<TransactionsSyncResponse> SyncTransactionsForAccountAsync(
|
||||||
|
string accessToken,
|
||||||
|
string accountId,
|
||||||
|
string? initialCursor = null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PlaidTransactionService : IPlaidTransactionService
|
||||||
|
{
|
||||||
|
private readonly PlaidClient _client;
|
||||||
|
|
||||||
|
private PlaidTransactionService(PlaidClient client)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlaidTransactionService From(IServiceProvider sp)
|
||||||
|
{
|
||||||
|
var client = sp.GetRequiredService<PlaidClient>();
|
||||||
|
return new(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<TransactionsSyncResponse> SyncTransactionsForAccountAsync(
|
||||||
|
string accessToken,
|
||||||
|
string accountId,
|
||||||
|
string? initialCursor = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
TransactionsSyncResponse? response;
|
||||||
|
var cursor = initialCursor;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
response = await _client.TransactionsSyncAsync(new()
|
||||||
|
{
|
||||||
|
AccessToken = accessToken,
|
||||||
|
Cursor = cursor,
|
||||||
|
Count = 500,
|
||||||
|
Options = new()
|
||||||
|
{
|
||||||
|
AccountId = accountId,
|
||||||
|
}
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (response.Error?.ErrorCode is "TRANSACTIONS_SYNC_MUTATION_DURING_PAGINATION")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = response.NextCursor;
|
||||||
|
yield return response;
|
||||||
|
} while (response is not null && response.HasMore);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
using Account = FiscalOS.Core.Accounts.Account;
|
||||||
|
|
||||||
|
namespace FiscalOS.Infra.Transactions.Plaid;
|
||||||
|
|
||||||
|
internal interface IPlaidTransactionSyncer
|
||||||
|
{
|
||||||
|
Task SyncTransactionsForAccountAsync(
|
||||||
|
Account account,
|
||||||
|
string decryptedAccessToken,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PlaidTransactionSyncer : IPlaidTransactionSyncer
|
||||||
|
{
|
||||||
|
private readonly ILogger<PlaidTransactionSyncer> _logger;
|
||||||
|
private readonly IPlaidTransactionService _plaidTransactionService;
|
||||||
|
private readonly IPlaidTransactionProcessor _transactionProcessor;
|
||||||
|
|
||||||
|
private PlaidTransactionSyncer(
|
||||||
|
ILogger<PlaidTransactionSyncer> logger,
|
||||||
|
IPlaidTransactionService plaidTransactionService,
|
||||||
|
IPlaidTransactionProcessor plaidTransactionProcessor
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_plaidTransactionService = plaidTransactionService;
|
||||||
|
_transactionProcessor = plaidTransactionProcessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlaidTransactionSyncer From(
|
||||||
|
ILogger<PlaidTransactionSyncer> logger,
|
||||||
|
IPlaidTransactionService plaidTransactionService,
|
||||||
|
IPlaidTransactionProcessor plaidTransactionProcessor
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return new(logger, plaidTransactionService, plaidTransactionProcessor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlaidTransactionSyncer From(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
return new(
|
||||||
|
serviceProvider.GetRequiredService<ILogger<PlaidTransactionSyncer>>(),
|
||||||
|
serviceProvider.GetRequiredService<IPlaidTransactionService>(),
|
||||||
|
serviceProvider.GetRequiredService<IPlaidTransactionProcessor>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SyncTransactionsForAccountAsync(Account account, string decryptedAccessToken, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (account.Metadata is not PlaidAccountMetadata plaidAccountMetadata)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Unable to sync transactions for account {AccountId} as it has no Plaid metadata", account.Id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var responses = _plaidTransactionService.SyncTransactionsForAccountAsync(
|
||||||
|
decryptedAccessToken,
|
||||||
|
plaidAccountMetadata.PlaidId,
|
||||||
|
plaidAccountMetadata.Cursor
|
||||||
|
).ConfigureAwait(false);
|
||||||
|
|
||||||
|
await foreach (var response in responses)
|
||||||
|
{
|
||||||
|
if (response.IsSuccessStatusCode is false)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Failed to sync transactions for request {RequestId} for account {AccountId}: {ErrorMessage}",
|
||||||
|
response.RequestId,
|
||||||
|
account.Id,
|
||||||
|
response.Error?.ErrorMessage
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.Accounts.Any() is false)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("No transactions to sync for request {RequestId} for account {AccountId}", response.RequestId, account.Id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var plaidAccount = response.Accounts
|
||||||
|
.FirstOrDefault(pa => pa.AccountId == plaidAccountMetadata.PlaidId);
|
||||||
|
|
||||||
|
if (plaidAccount is null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Failed to sync transactions for request {RequestId} for account {AccountId}: No corresponding account received in Plaid response",
|
||||||
|
response.RequestId,
|
||||||
|
account.Id
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var balance = Balance.From(plaidAccount.Current, plaidAccount.Available, plaidAccount.CurrencyCode);
|
||||||
|
account.AddBalance(balance);
|
||||||
|
|
||||||
|
var isProcessedSuccessfully = await _transactionProcessor.ProcessAsync(account, response, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (isProcessedSuccessfully is false)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Unable to process transactions for request {RequestId} for account {AccountId} successfully",
|
||||||
|
response.RequestId,
|
||||||
|
account.Id
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
plaidAccountMetadata.SetCursor(response.NextCursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user