Готовый прототип с базовым функционалом системы
This commit is contained in:
mfnefd 2024-12-09 04:27:04 +04:00
commit 8d96d61577
120 changed files with 8733 additions and 0 deletions

25
.dockerignore Normal file
View File

@ -0,0 +1,25 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

5
.env Normal file
View File

@ -0,0 +1,5 @@
POSTGRES_DB="dombudg"
POSTGRES_USER="postgres"
POSTGRES_PASSWORD="postgres"
CONNECTION_STRING="Host=database:5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD};"

39
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,39 @@
{
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/back/Controllers/bin/Debug/net8.0/Controllers.dll",
"args": [],
"cwd": "${workspaceFolder}/back/Controllers",
"stopAtEntry": false,
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
},
{
"name": "Docker .NET Launch",
"type": "docker",
"request": "launch",
"preLaunchTask": "docker-run: debug",
"netCore": {
"appProject": "${workspaceFolder}/back/Controllers/Controllers.csproj"
}
}
]
}

101
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,101 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/back/Api.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/back/Api.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/back/Api.sln"
],
"problemMatcher": "$msCompile"
},
{
"type": "docker-build",
"label": "docker-build: debug",
"dependsOn": [
"build"
],
"dockerBuild": {
"tag": "dombudg:dev",
"target": "base",
"dockerfile": "${workspaceFolder}/back/Controllers/Dockerfile",
"context": "${workspaceFolder}",
"pull": true
},
"netCore": {
"appProject": "${workspaceFolder}/back/Controllers/Controllers.csproj"
}
},
{
"type": "docker-build",
"label": "docker-build: release",
"dependsOn": [
"build"
],
"dockerBuild": {
"tag": "dombudg:latest",
"dockerfile": "${workspaceFolder}/back/Controllers/Dockerfile",
"context": "${workspaceFolder}",
"platform": {
"os": "linux",
"architecture": "amd64"
},
"pull": true
},
"netCore": {
"appProject": "${workspaceFolder}/back/Controllers/Controllers.csproj"
}
},
{
"type": "docker-run",
"label": "docker-run: debug",
"dependsOn": [
"docker-build: debug"
],
"dockerRun": {},
"netCore": {
"appProject": "${workspaceFolder}/back/Controllers/Controllers.csproj",
"enableDebugging": true
}
},
{
"type": "docker-run",
"label": "docker-run: release",
"dependsOn": [
"docker-build: release"
],
"dockerRun": {},
"netCore": {
"appProject": "${workspaceFolder}/back/Controllers/Controllers.csproj"
}
}
]
}

484
back/.gitignore vendored Normal file
View File

@ -0,0 +1,484 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from `dotnet new gitignore`
# dotenv files
.env
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET
project.lock.json
project.fragment.lock.json
artifacts/
# Tye
.tye/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
.idea
##
## Visual studio for Mac
##
# globs
Makefile.in
*.userprefs
*.usertasks
config.make
config.status
aclocal.m4
install-sh
autom4te.cache/
*.tar.gz
tarballs/
test-results/
# Mac bundle stuff
*.dmg
*.app
# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# Vim temporary swap files
*.swp

40
back/Api.sln Normal file
View File

@ -0,0 +1,40 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Controllers", "Controllers\Controllers.csproj", "{BEA05282-6DE5-4D67-983A-EF13AAF8EECE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Services", "Services\Services.csproj", "{B937E273-1D6E-42DF-BD34-14DA5E71FC71}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Contracts", "Contracts\Contracts.csproj", "{B9246DBE-67B0-4B0F-8648-E9ED2EB7172C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "Infrastructure\Infrastructure.csproj", "{A35121D4-7D41-4266-8DA4-87135E8ABF89}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BEA05282-6DE5-4D67-983A-EF13AAF8EECE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BEA05282-6DE5-4D67-983A-EF13AAF8EECE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BEA05282-6DE5-4D67-983A-EF13AAF8EECE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BEA05282-6DE5-4D67-983A-EF13AAF8EECE}.Release|Any CPU.Build.0 = Release|Any CPU
{B937E273-1D6E-42DF-BD34-14DA5E71FC71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B937E273-1D6E-42DF-BD34-14DA5E71FC71}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B937E273-1D6E-42DF-BD34-14DA5E71FC71}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B937E273-1D6E-42DF-BD34-14DA5E71FC71}.Release|Any CPU.Build.0 = Release|Any CPU
{B9246DBE-67B0-4B0F-8648-E9ED2EB7172C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B9246DBE-67B0-4B0F-8648-E9ED2EB7172C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B9246DBE-67B0-4B0F-8648-E9ED2EB7172C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B9246DBE-67B0-4B0F-8648-E9ED2EB7172C}.Release|Any CPU.Build.0 = Release|Any CPU
{A35121D4-7D41-4266-8DA4-87135E8ABF89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A35121D4-7D41-4266-8DA4-87135E8ABF89}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A35121D4-7D41-4266-8DA4-87135E8ABF89}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A35121D4-7D41-4266-8DA4-87135E8ABF89}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,11 @@
namespace Contracts.DTO;
public class ChangeRecordDto
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public Guid? SpendingGroupId { get; set; }
public string? SpendingGroupName { get; set; }
public decimal Sum { get; set; }
public DateTime ChangedAt { get; set; }
}

View File

@ -0,0 +1,10 @@
namespace Contracts.DTO;
public class SpendingGroupDto
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public Guid UserId { get; set; }
public List<ChangeRecordDto> ChangeRecords { get; set; } = new();
public List<SpendingPlanDto> SpendingPlans { get; set; } = new();
}

View File

@ -0,0 +1,10 @@
namespace Contracts.DTO;
public class SpendingPlanDto
{
public Guid Id { get; set; }
public Guid SpendingGroupId { get; set; }
public decimal Sum { get; set; }
public DateTime StartAt { get; set; }
public DateTime EndAt { get; set; }
}

View File

@ -0,0 +1,9 @@
namespace Contracts.DTO;
public class UserDto
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public decimal Balance { get; set; }
}

View File

@ -0,0 +1,7 @@
namespace Contracts.DTO;
public class UserLoginDto
{
public string Name { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}

View File

@ -0,0 +1,16 @@
using Contracts.DTO;
using Contracts.ViewModels;
namespace Contracts.Mappers;
public static class ChangeRecordMapper
{
public static ChangeRecordViewModel ToView(this ChangeRecordDto dto)
=> new()
{
Id = dto.Id,
Sum = dto.Sum,
ChangedAt = dto.ChangedAt.ToString("dd.MM.yyyy"),
SpendingGroupName = dto.SpendingGroupName ?? string.Empty
};
}

View File

@ -0,0 +1,16 @@
using Contracts.DTO;
using Contracts.ViewModels;
namespace Contracts.Mappers;
public static class SpendingGroupMapper
{
public static SpendingGroupViewModel ToView(this SpendingGroupDto dto)
=> new()
{
Id = dto.Id,
Name = dto.Name,
ChangeRecords = dto.ChangeRecords.Select(x => x.ToView()).ToList(),
SpendingPlans = dto.SpendingPlans.Select(x => x.ToView()).ToList()
};
}

View File

@ -0,0 +1,16 @@
using Contracts.DTO;
using Contracts.ViewModels;
namespace Contracts.Mappers;
public static class SpendingPlanMapper
{
public static SpendingPlanViewModel ToView(this SpendingPlanDto dto)
=> new()
{
Id = dto.Id,
StartAt = dto.StartAt.ToString("dd.MM.yyyy"),
EndAt = dto.EndAt.ToString("dd.MM.yyyy"),
Sum = dto.Sum
};
}

View File

@ -0,0 +1,16 @@
using Contracts.DTO;
using Contracts.ViewModels;
namespace Contracts.Mappers;
public static class UserMapper
{
public static UserViewModel ToView(this UserDto user)
=> new()
{
Id = user.Id,
Name = user.Name,
Balance = user.Balance
};
}

View File

@ -0,0 +1,13 @@
using Contracts.DTO;
using Contracts.SearchModels;
namespace Contracts.Repositories;
public interface IChangeRecordRepo
{
Task<ChangeRecordDto> Create(ChangeRecordDto changeRecord);
Task<ChangeRecordDto?> Get(ChangeRecordSearch search);
Task<IEnumerable<ChangeRecordDto>> GetList(ChangeRecordSearch? search = null);
Task<ChangeRecordDto?> Update(ChangeRecordDto changeRecord);
Task<ChangeRecordDto?> Delete(ChangeRecordSearch search);
}

View File

@ -0,0 +1,13 @@
using Contracts.DTO;
using Contracts.SearchModels;
namespace Contracts.Repositories;
public interface ISpendingGroupRepo
{
Task<SpendingGroupDto?> Get(SpendingGroupSearch search);
Task<IEnumerable<SpendingGroupDto>> GetList(SpendingGroupSearch? search = null);
Task<SpendingGroupDto> Create(SpendingGroupDto spendingGroup);
Task<SpendingGroupDto?> Delete(SpendingGroupSearch search);
Task<SpendingGroupDto?> Update(SpendingGroupDto spendingGroup);
}

View File

@ -0,0 +1,13 @@
using Contracts.DTO;
using Contracts.SearchModels;
namespace Contracts.Repositories;
public interface ISpendingPlanRepo
{
Task<SpendingPlanDto> Create(SpendingPlanDto dto);
Task<SpendingPlanDto?> Update(SpendingPlanDto dto);
Task<SpendingPlanDto?> Delete(SpendingPlanSearch search);
Task<SpendingPlanDto?> Get(SpendingPlanSearch search);
Task<IEnumerable<SpendingPlanDto>> GetList(SpendingPlanSearch? search = null);
}

View File

@ -0,0 +1,13 @@
using Contracts.DTO;
using Contracts.SearchModels;
namespace Contracts.Repositories;
public interface IUserRepo
{
public Task<UserDto?> Get(UserSearch search);
public Task<UserDto> Create(UserDto user);
public Task<UserDto?> Update(UserDto user);
public Task<bool> ChangeBalance(UserSearch search, decimal amount);
public Task<UserDto?> Delete(UserSearch search);
}

View File

@ -0,0 +1,10 @@
namespace Contracts.SearchModels;
public class ChangeRecordSearch
{
public Guid? Id { get; set; }
public Guid? SpendingGroupId { get; set; }
public DateTime? From { get; set; }
public DateTime? To { get; set; }
public Guid? UserId { get; set; }
}

View File

@ -0,0 +1,8 @@
namespace Contracts.SearchModels;
public class SpendingGroupSearch
{
public Guid? Id { get; set; }
public string? Name { get; set; }
public Guid? UserId { get; set; }
}

View File

@ -0,0 +1,6 @@
namespace Contracts.SearchModels;
public class SpendingPlanSearch
{
public Guid? Id { get; set; }
}

View File

@ -0,0 +1,7 @@
namespace Contracts.SearchModels;
public class UserSearch
{
public Guid? Id { get; set; }
public string? Name { get; set; }
}

View File

@ -0,0 +1,11 @@
using Contracts.DTO;
using Contracts.SearchModels;
using Contracts.ViewModels;
namespace Contracts.Services;
public interface IAuthService
{
public Task<UserViewModel> Login(UserLoginDto loginData);
public Task<UserViewModel> Register(UserDto user);
}

View File

@ -0,0 +1,13 @@
using Contracts.DTO;
using Contracts.SearchModels;
using Contracts.ViewModels;
namespace Contracts.Services;
public interface IChangeRecordService
{
Task<ChangeRecordViewModel> Create(ChangeRecordDto model);
Task<ChangeRecordViewModel> Update(ChangeRecordDto model);
Task<ChangeRecordViewModel> Delete(ChangeRecordSearch search);
Task<IEnumerable<ChangeRecordViewModel>> GetList(ChangeRecordSearch? search = null);
}

View File

@ -0,0 +1,14 @@
using Contracts.DTO;
using Contracts.SearchModels;
using Contracts.ViewModels;
namespace Contracts.Services;
public interface ISpendingGroupService
{
Task<SpendingGroupViewModel> GetDetails(SpendingGroupSearch search);
Task<IEnumerable<SpendingGroupViewModel>> GetList(SpendingGroupSearch? search = null);
Task<SpendingGroupViewModel> Create(SpendingGroupDto model);
Task<SpendingGroupViewModel> Update(SpendingGroupDto model);
Task<SpendingGroupViewModel> Delete(SpendingGroupSearch search);
}

View File

@ -0,0 +1,14 @@
using Contracts.DTO;
using Contracts.SearchModels;
using Contracts.ViewModels;
namespace Contracts.Services;
public interface ISpendingPlanService
{
Task<SpendingPlanViewModel> GetDetails(SpendingPlanSearch search);
Task<IEnumerable<SpendingPlanViewModel>> GetList(SpendingPlanSearch? search = null);
Task<SpendingPlanViewModel> Create(SpendingPlanDto spendingPlan);
Task<SpendingPlanViewModel> Delete(SpendingPlanSearch search);
Task<SpendingPlanViewModel> Update(SpendingPlanDto spendingPlan);
}

View File

@ -0,0 +1,12 @@
using Contracts.DTO;
using Contracts.SearchModels;
using Contracts.ViewModels;
namespace Contracts.Services;
public interface IUserService
{
public Task<UserViewModel> GetDetails(UserSearch search);
public Task<UserViewModel> UpdateUserData(UserDto user);
public Task<UserViewModel> Delete(UserSearch search);
}

View File

@ -0,0 +1,9 @@
namespace Contracts.ViewModels;
public class ChangeRecordViewModel
{
public Guid Id { get; set; }
public decimal Sum { get; set; }
public string ChangedAt { get; set; } = null!;
public string SpendingGroupName { get; set; } = string.Empty;
}

View File

@ -0,0 +1,9 @@
namespace Contracts.ViewModels;
public class SpendingGroupViewModel
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public List<ChangeRecordViewModel> ChangeRecords { get; set; } = new();
public List<SpendingPlanViewModel> SpendingPlans { get; set; } = new();
}

View File

@ -0,0 +1,9 @@
namespace Contracts.ViewModels;
public class SpendingPlanViewModel
{
public Guid Id { get; set; }
public string StartAt { get; set; } = null!;
public string EndAt { get; set; } = null!;
public decimal Sum { get; set; }
}

View File

@ -0,0 +1,8 @@
namespace Contracts.ViewModels;
public class UserViewModel
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Balance { get; set; }
}

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Contracts\Contracts.csproj" />
<ProjectReference Include="..\Services\Services.csproj" />
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,5 @@
@Controllers_HostAddress = http://localhost:5125
Accept: application/json
###

View File

@ -0,0 +1,63 @@
using Contracts.DTO;
using Contracts.Services;
using Contracts.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Services.Support.Exceptions;
namespace Controllers.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IAuthService _authService;
public AuthController(IAuthService authService)
{
_authService = authService;
}
[HttpPost]
public async Task<ActionResult<UserViewModel>> Login([FromBody] UserLoginDto loginData)
{
try
{
var user = await _authService.Login(loginData);
return Ok(user);
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
catch (UserNotFoundException ex)
{
return NotFound(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpPost("register")]
public async Task<ActionResult<UserViewModel>> Register([FromBody] UserDto user)
{
try
{
var createdUser = await _authService.Register(user);
return CreatedAtAction(nameof(Login), new { name = createdUser.Name }, createdUser);
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
catch (AlreadyExistsException ex)
{
return Conflict(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
}

View File

@ -0,0 +1,100 @@
using Contracts.DTO;
using Contracts.SearchModels;
using Contracts.Services;
using Contracts.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Services.Support.Exceptions;
namespace Controllers.Controllers;
[Route("api/[controller]")]
[ApiController]
public class ChangeRecordController : ControllerBase
{
private readonly IChangeRecordService _changeRecordService;
public ChangeRecordController(IChangeRecordService changeRecordService)
{
_changeRecordService = changeRecordService;
}
[HttpPost]
public async Task<ActionResult<ChangeRecordViewModel>> CreateChangeRecord(
[FromBody] ChangeRecordDto dto)
{
try
{
var record = await _changeRecordService.Create(dto);
return CreatedAtAction(nameof(GetChangeRecords), record);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpGet]
public async Task<ActionResult<IEnumerable<ChangeRecordViewModel>>> GetChangeRecords()
{
try
{
var records = await _changeRecordService.GetList();
return Ok(records);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpGet("filter")]
public async Task<ActionResult<IEnumerable<ChangeRecordViewModel>>> GetChangeRecords(
[FromQuery] ChangeRecordSearch search)
{
try
{
var records = await _changeRecordService.GetList(search);
return Ok(records);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpPatch]
public async Task<ActionResult<ChangeRecordViewModel>> UpdateChangeRecord([FromBody] ChangeRecordDto dto)
{
try
{
var record = await _changeRecordService.Update(dto);
return Ok(record);
}
catch (EntityNotFoundException ex)
{
return NotFound(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpDelete]
public async Task<ActionResult> DeleteChangeRecord([FromQuery] ChangeRecordSearch search)
{
try
{
var record = await _changeRecordService.Delete(search);
return Ok(record);
}
catch (EntityNotFoundException ex)
{
return NotFound(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
}

View File

@ -0,0 +1,126 @@
namespace Controllers.Extensions;
using Contracts.DTO;
using Contracts.SearchModels;
using Contracts.Services;
using Contracts.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Services.Support.Exceptions;
[ApiController]
[Route("api/[controller]")]
public class SpendingGroupController : ControllerBase
{
private readonly ISpendingGroupService _spendingGroupService;
public SpendingGroupController(ISpendingGroupService spendingGroupService)
{
_spendingGroupService = spendingGroupService;
}
[HttpGet("{id}")]
public async Task<ActionResult<SpendingGroupViewModel>> GetSpendingGroup(
Guid id,
[FromQuery] SpendingGroupSearch search)
{
try
{
search ??= new();
search.Id = id;
var group = await _spendingGroupService.GetDetails(search);
return Ok(group);
}
catch (EntityNotFoundException ex)
{
return NotFound(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpGet]
public async Task<ActionResult<List<SpendingGroupViewModel>>> GetSpendingGroups()
{
try
{
var groups = await _spendingGroupService.GetList();
return Ok(groups);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpGet("filter")]
public async Task<ActionResult<List<SpendingGroupViewModel>>> GetSpendingGroups(
[FromQuery] SpendingGroupSearch search)
{
try
{
var groups = await _spendingGroupService.GetList(search);
return Ok(groups);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpPost]
public async Task<ActionResult<SpendingGroupViewModel>> CreateSpendingGroup(
[FromBody] SpendingGroupDto dto)
{
try
{
var group = await _spendingGroupService.Create(dto);
return CreatedAtAction(nameof(GetSpendingGroup), new { id = group.Id }, group);
}
catch (EntityNotFoundException ex)
{
return NotFound(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpPatch]
public async Task<ActionResult<SpendingGroupViewModel>> UpdateSpendingGroup([FromBody] SpendingGroupDto dto)
{
try
{
var group = await _spendingGroupService.Update(dto);
return Ok(group);
}
catch (EntityNotFoundException ex)
{
return NotFound(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpDelete]
public async Task<ActionResult> DeleteSpendingGroup([FromQuery] SpendingGroupSearch search)
{
try
{
var group = await _spendingGroupService.Delete(search);
return Ok(group);
}
catch (EntityNotFoundException ex)
{
return NotFound(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
}

View File

@ -0,0 +1,128 @@
using Contracts.DTO;
using Contracts.SearchModels;
using Contracts.Services;
using Contracts.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Services.Support.Exceptions;
namespace Controllers.Controllers;
[ApiController]
[Route("api/[controller]")]
public class SpendingPlanController : ControllerBase
{
private readonly ISpendingPlanService _spendingPlanService;
public SpendingPlanController(ISpendingPlanService spendingPlanService)
{
_spendingPlanService = spendingPlanService;
}
[HttpGet("{id}")]
public async Task<ActionResult<SpendingPlanViewModel>> GetSpendingPlan(
Guid id,
[FromQuery] SpendingPlanSearch search)
{
try
{
search ??= new();
search.Id = id;
var plan = await _spendingPlanService.GetDetails(search);
return Ok(plan);
}
catch (EntityNotFoundException ex)
{
return NotFound(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpGet]
public async Task<ActionResult<List<SpendingPlanViewModel>>> GetSpendingPlans()
{
try
{
var plans = await _spendingPlanService.GetList();
return Ok(plans);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpGet("filter")]
public async Task<ActionResult<List<SpendingPlanViewModel>>> GetSpendingPlans(
[FromQuery] SpendingPlanSearch search)
{
try
{
var plans = await _spendingPlanService.GetList(search);
return Ok(plans);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpPost]
public async Task<ActionResult<SpendingPlanViewModel>> CreateSpendingPlan(
[FromBody] SpendingPlanDto dto)
{
try
{
var plan = await _spendingPlanService.Create(dto);
return CreatedAtAction(nameof(GetSpendingPlan), new { id = plan.Id }, plan);
}
catch (EntityNotFoundException ex)
{
return NotFound(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpPatch]
public async Task<ActionResult<SpendingPlanViewModel>> UpdateSpendingPlan(
[FromBody] SpendingPlanDto dto)
{
try
{
var plan = await _spendingPlanService.Update(dto);
return Ok(plan);
}
catch (EntityNotFoundException ex)
{
return NotFound(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpDelete]
public async Task<ActionResult> DeleteSpendingPlan([FromQuery] SpendingPlanSearch search)
{
try
{
var plan = await _spendingPlanService.Delete(search);
return Ok(plan);
}
catch (EntityNotFoundException ex)
{
return NotFound(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
}

View File

@ -0,0 +1,73 @@
using Contracts.DTO;
using Contracts.SearchModels;
using Contracts.Services;
using Contracts.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Services.Support.Exceptions;
namespace Controllers.Controllers;
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
private readonly IUserService _userService;
public UserController(IUserService userService)
{
_userService = userService;
}
[HttpGet]
public async Task<ActionResult<UserViewModel>> GetUser([FromQuery] UserSearch search)
{
try
{
var user = await _userService.GetDetails(search);
return Ok(user);
}
catch (UserNotFoundException ex)
{
return NotFound(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpPatch]
public async Task<ActionResult<UserViewModel>> UpdateUser([FromBody] UserDto user)
{
try
{
var updatedUser = await _userService.UpdateUserData(user);
return Ok(updatedUser);
}
catch (UserNotFoundException ex)
{
return NotFound(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpDelete]
public async Task<ActionResult<UserViewModel>> DeleteUser([FromQuery] UserSearch search)
{
try
{
var deletedUser = await _userService.Delete(search);
return Ok(deletedUser);
}
catch (UserNotFoundException ex)
{
return NotFound(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
}

View File

@ -0,0 +1,24 @@
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 5125
ENV ASPNETCORE_URLS=http://+:5125
USER app
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG configuration=Release
WORKDIR /src
COPY ["back/Controllers/Controllers.csproj", "back/Controllers/"]
RUN dotnet restore "back/Controllers/Controllers.csproj"
COPY . .
WORKDIR "/src/back/Controllers"
RUN dotnet build "Controllers.csproj" -c $configuration -o /app/build
FROM build AS publish
ARG configuration=Release
RUN dotnet publish "Controllers.csproj" -c $configuration -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Controllers.dll"]

View File

@ -0,0 +1,19 @@
using Contracts.Services;
using Services.Domain;
namespace Controllers.Extensions;
public static class AddDomainServicesExtension
{
public static void AddDomainServices(this IServiceCollection services)
{
services.AddTransient<IAuthService, AuthService>();
services.AddTransient<IUserService, UserService>();
services.AddTransient<ISpendingGroupService, SpendingGroupService>();
services.AddTransient<IChangeRecordService, ChangeRecordService>();
services.AddTransient<ISpendingPlanService, SpendingPlanService>();
}
}

View File

@ -0,0 +1,17 @@
using Contracts.Repositories;
using Contracts.Services;
using Infrastructure.Repositories;
using Services.Domain;
namespace Controllers.Extensions;
public static class AddReposExtension
{
public static void AddRepos(this IServiceCollection services)
{
services.AddTransient<IUserRepo, UserRepo>();
services.AddTransient<ISpendingGroupRepo, SpendingGroupRepo>();
services.AddTransient<IChangeRecordRepo, ChangeRecordRepo>();
services.AddTransient<ISpendingPlanRepo, SpendingPlanRepo>();
}
}

View File

@ -0,0 +1,31 @@
using Infrastructure;
using Microsoft.EntityFrameworkCore;
namespace Controllers.Extensions;
public static class DatabaseSetupExtension
{
public static void AddDbConnectionService(this IServiceCollection services, IConfiguration config)
{
var connectionString = config.GetConnectionString("DefaultConnection")
?? throw new ArgumentException("Нет строки подключения");
Console.WriteLine("Connection string: " + connectionString);
services.AddDbContext<DatabaseContext>(options => options.UseNpgsql(connectionString));
services.AddSingleton<IDbContextFactory<DatabaseContext>, DbContextFactory>();
}
public static void MigrateDb(this IApplicationBuilder app)
{
try
{
using var scope = app.ApplicationServices.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<IDbContextFactory<DatabaseContext>>();
using var db = context.CreateDbContext();
db.Database.Migrate();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}

View File

@ -0,0 +1,33 @@
using Controllers.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddDbConnectionService(builder.Configuration);
builder.Services.AddRepos();
builder.Services.AddDomainServices();
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MigrateDb();
app.UseCors(builder => builder.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
await app.RunAsync();

View File

@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:47535",
"sslPort": 44340
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5125",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7189;http://localhost:5125",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -0,0 +1,18 @@
using Contracts.DTO;
using Infrastructure.Models;
using Microsoft.EntityFrameworkCore;
namespace Infrastructure;
public class DatabaseContext : DbContext
{
public DatabaseContext(DbContextOptions<DatabaseContext> options)
: base(options)
{
}
public DbSet<User> Users { get; set; } = null!;
public DbSet<SpendingGroup> SpendingGroups { get; set; } = null!;
public DbSet<ChangeRecord> ChangeRecords { get; set; } = null!;
public DbSet<SpendingPlan> SpendingPlans { get; set; } = null!;
}

View File

@ -0,0 +1,20 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Infrastructure;
public class DbContextFactory : IDbContextFactory<DatabaseContext>
{
private readonly IServiceProvider _serviceProvider;
public DbContextFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public DatabaseContext CreateDbContext()
{
var scope = _serviceProvider.CreateScope();
return scope.ServiceProvider.GetRequiredService<DatabaseContext>();
}
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Contracts\Contracts.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.1" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,52 @@
// <auto-generated />
using System;
using Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Infrastructure.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20241125164748_User")]
partial class User
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Infrastructure.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Balance")
.HasColumnType("numeric");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,36 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Infrastructure.Migrations
{
/// <inheritdoc />
public partial class User : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Password = table.Column<string>(type: "text", nullable: false),
Balance = table.Column<decimal>(type: "numeric", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Users");
}
}
}

View File

@ -0,0 +1,88 @@
// <auto-generated />
using System;
using Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Infrastructure.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20241126140310_SpendingGroup")]
partial class SpendingGroup
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Infrastructure.Models.SpendingGroup", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("SpendingGroups");
});
modelBuilder.Entity("Infrastructure.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Balance")
.HasColumnType("numeric");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("Infrastructure.Models.SpendingGroup", b =>
{
b.HasOne("Infrastructure.Models.User", "User")
.WithMany("SpendingGroups")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Infrastructure.Models.User", b =>
{
b.Navigation("SpendingGroups");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,46 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Infrastructure.Migrations
{
/// <inheritdoc />
public partial class SpendingGroup : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SpendingGroups",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SpendingGroups", x => x.Id);
table.ForeignKey(
name: "FK_SpendingGroups_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_SpendingGroups_UserId",
table: "SpendingGroups",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SpendingGroups");
}
}
}

View File

@ -0,0 +1,134 @@
// <auto-generated />
using System;
using Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Infrastructure.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20241126185002_AddChangeRecord")]
partial class AddChangeRecord
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Infrastructure.Models.ChangeRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("ChangedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("SpendingGroupId")
.HasColumnType("uuid");
b.Property<decimal>("Sum")
.HasColumnType("numeric");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("SpendingGroupId");
b.HasIndex("UserId");
b.ToTable("ChangeRecords");
});
modelBuilder.Entity("Infrastructure.Models.SpendingGroup", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("SpendingGroups");
});
modelBuilder.Entity("Infrastructure.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Balance")
.HasColumnType("numeric");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("Infrastructure.Models.ChangeRecord", b =>
{
b.HasOne("Infrastructure.Models.SpendingGroup", "SpendingGroup")
.WithMany()
.HasForeignKey("SpendingGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Infrastructure.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SpendingGroup");
b.Navigation("User");
});
modelBuilder.Entity("Infrastructure.Models.SpendingGroup", b =>
{
b.HasOne("Infrastructure.Models.User", "User")
.WithMany("SpendingGroups")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Infrastructure.Models.User", b =>
{
b.Navigation("SpendingGroups");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,59 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddChangeRecord : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ChangeRecords",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Sum = table.Column<decimal>(type: "numeric", nullable: false),
ChangedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
SpendingGroupId = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ChangeRecords", x => x.Id);
table.ForeignKey(
name: "FK_ChangeRecords_SpendingGroups_SpendingGroupId",
column: x => x.SpendingGroupId,
principalTable: "SpendingGroups",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ChangeRecords_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ChangeRecords_SpendingGroupId",
table: "ChangeRecords",
column: "SpendingGroupId");
migrationBuilder.CreateIndex(
name: "IX_ChangeRecords_UserId",
table: "ChangeRecords",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ChangeRecords");
}
}
}

View File

@ -0,0 +1,139 @@
// <auto-generated />
using System;
using Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Infrastructure.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20241126210947_fixChangeRecord")]
partial class fixChangeRecord
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Infrastructure.Models.ChangeRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("ChangedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("SpendingGroupId")
.HasColumnType("uuid");
b.Property<decimal>("Sum")
.HasColumnType("numeric");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("SpendingGroupId");
b.HasIndex("UserId");
b.ToTable("ChangeRecords");
});
modelBuilder.Entity("Infrastructure.Models.SpendingGroup", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("SpendingGroups");
});
modelBuilder.Entity("Infrastructure.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Balance")
.HasColumnType("numeric");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("Infrastructure.Models.ChangeRecord", b =>
{
b.HasOne("Infrastructure.Models.SpendingGroup", "SpendingGroup")
.WithMany("ChangeRecords")
.HasForeignKey("SpendingGroupId");
b.HasOne("Infrastructure.Models.User", "User")
.WithMany("ChangeRecords")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SpendingGroup");
b.Navigation("User");
});
modelBuilder.Entity("Infrastructure.Models.SpendingGroup", b =>
{
b.HasOne("Infrastructure.Models.User", "User")
.WithMany("SpendingGroups")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Infrastructure.Models.SpendingGroup", b =>
{
b.Navigation("ChangeRecords");
});
modelBuilder.Entity("Infrastructure.Models.User", b =>
{
b.Navigation("ChangeRecords");
b.Navigation("SpendingGroups");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,60 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Infrastructure.Migrations
{
/// <inheritdoc />
public partial class fixChangeRecord : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ChangeRecords_SpendingGroups_SpendingGroupId",
table: "ChangeRecords");
migrationBuilder.AlterColumn<Guid>(
name: "SpendingGroupId",
table: "ChangeRecords",
type: "uuid",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uuid");
migrationBuilder.AddForeignKey(
name: "FK_ChangeRecords_SpendingGroups_SpendingGroupId",
table: "ChangeRecords",
column: "SpendingGroupId",
principalTable: "SpendingGroups",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ChangeRecords_SpendingGroups_SpendingGroupId",
table: "ChangeRecords");
migrationBuilder.AlterColumn<Guid>(
name: "SpendingGroupId",
table: "ChangeRecords",
type: "uuid",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true);
migrationBuilder.AddForeignKey(
name: "FK_ChangeRecords_SpendingGroups_SpendingGroupId",
table: "ChangeRecords",
column: "SpendingGroupId",
principalTable: "SpendingGroups",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@ -0,0 +1,177 @@
// <auto-generated />
using System;
using Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Infrastructure.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20241126222847_AddSpendingPlan")]
partial class AddSpendingPlan
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Infrastructure.Models.ChangeRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("ChangedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("SpendingGroupId")
.HasColumnType("uuid");
b.Property<decimal>("Sum")
.HasColumnType("numeric");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("SpendingGroupId");
b.HasIndex("UserId");
b.ToTable("ChangeRecords");
});
modelBuilder.Entity("Infrastructure.Models.SpendingGroup", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("SpendingGroups");
});
modelBuilder.Entity("Infrastructure.Models.SpendingPlan", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("EndAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("SpendingGroupId")
.HasColumnType("uuid");
b.Property<DateTime>("StartAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("Sum")
.HasColumnType("numeric");
b.HasKey("Id");
b.HasIndex("SpendingGroupId");
b.ToTable("SpendingPlans");
});
modelBuilder.Entity("Infrastructure.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Balance")
.HasColumnType("numeric");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("Infrastructure.Models.ChangeRecord", b =>
{
b.HasOne("Infrastructure.Models.SpendingGroup", "SpendingGroup")
.WithMany("ChangeRecords")
.HasForeignKey("SpendingGroupId");
b.HasOne("Infrastructure.Models.User", "User")
.WithMany("ChangeRecords")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SpendingGroup");
b.Navigation("User");
});
modelBuilder.Entity("Infrastructure.Models.SpendingGroup", b =>
{
b.HasOne("Infrastructure.Models.User", "User")
.WithMany("SpendingGroups")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Infrastructure.Models.SpendingPlan", b =>
{
b.HasOne("Infrastructure.Models.SpendingGroup", "SpendingGroup")
.WithMany("SpendingPlans")
.HasForeignKey("SpendingGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SpendingGroup");
});
modelBuilder.Entity("Infrastructure.Models.SpendingGroup", b =>
{
b.Navigation("ChangeRecords");
b.Navigation("SpendingPlans");
});
modelBuilder.Entity("Infrastructure.Models.User", b =>
{
b.Navigation("ChangeRecords");
b.Navigation("SpendingGroups");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,48 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddSpendingPlan : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SpendingPlans",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
StartAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
EndAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Sum = table.Column<decimal>(type: "numeric", nullable: false),
SpendingGroupId = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SpendingPlans", x => x.Id);
table.ForeignKey(
name: "FK_SpendingPlans_SpendingGroups_SpendingGroupId",
column: x => x.SpendingGroupId,
principalTable: "SpendingGroups",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_SpendingPlans_SpendingGroupId",
table: "SpendingPlans",
column: "SpendingGroupId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SpendingPlans");
}
}
}

View File

@ -0,0 +1,174 @@
// <auto-generated />
using System;
using Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Infrastructure.Migrations
{
[DbContext(typeof(DatabaseContext))]
partial class DatabaseContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Infrastructure.Models.ChangeRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("ChangedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("SpendingGroupId")
.HasColumnType("uuid");
b.Property<decimal>("Sum")
.HasColumnType("numeric");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("SpendingGroupId");
b.HasIndex("UserId");
b.ToTable("ChangeRecords");
});
modelBuilder.Entity("Infrastructure.Models.SpendingGroup", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("SpendingGroups");
});
modelBuilder.Entity("Infrastructure.Models.SpendingPlan", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("EndAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("SpendingGroupId")
.HasColumnType("uuid");
b.Property<DateTime>("StartAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("Sum")
.HasColumnType("numeric");
b.HasKey("Id");
b.HasIndex("SpendingGroupId");
b.ToTable("SpendingPlans");
});
modelBuilder.Entity("Infrastructure.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Balance")
.HasColumnType("numeric");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("Infrastructure.Models.ChangeRecord", b =>
{
b.HasOne("Infrastructure.Models.SpendingGroup", "SpendingGroup")
.WithMany("ChangeRecords")
.HasForeignKey("SpendingGroupId");
b.HasOne("Infrastructure.Models.User", "User")
.WithMany("ChangeRecords")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SpendingGroup");
b.Navigation("User");
});
modelBuilder.Entity("Infrastructure.Models.SpendingGroup", b =>
{
b.HasOne("Infrastructure.Models.User", "User")
.WithMany("SpendingGroups")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Infrastructure.Models.SpendingPlan", b =>
{
b.HasOne("Infrastructure.Models.SpendingGroup", "SpendingGroup")
.WithMany("SpendingPlans")
.HasForeignKey("SpendingGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SpendingGroup");
});
modelBuilder.Entity("Infrastructure.Models.SpendingGroup", b =>
{
b.Navigation("ChangeRecords");
b.Navigation("SpendingPlans");
});
modelBuilder.Entity("Infrastructure.Models.User", b =>
{
b.Navigation("ChangeRecords");
b.Navigation("SpendingGroups");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,24 @@
using Contracts.DTO;
namespace Infrastructure.Models;
public class ChangeRecord
{
public Guid Id { get; set; }
public decimal Sum { get; set; }
public DateTime ChangedAt { get; set; }
public Guid UserId { get; set; }
public User User { get; set; } = null!;
public Guid? SpendingGroupId { get; set; }
public SpendingGroup? SpendingGroup { get; set; }
public void Update(ChangeRecordDto changeRecordDto)
{
Id = changeRecordDto.Id;
Sum = changeRecordDto.Sum;
ChangedAt = changeRecordDto.ChangedAt;
SpendingGroupId = changeRecordDto.SpendingGroupId;
}
}

View File

@ -0,0 +1,12 @@
namespace Infrastructure.Models;
public class SpendingGroup
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public Guid UserId { get; set; }
public User User { get; set; } = null!;
public List<ChangeRecord>? ChangeRecords { get; set; }
public List<SpendingPlan>? SpendingPlans { get; set; }
}

View File

@ -0,0 +1,22 @@
using Contracts.DTO;
namespace Infrastructure.Models;
public class SpendingPlan
{
public Guid Id { get; set; }
public DateTime StartAt { get; set; }
public DateTime EndAt { get; set; }
public decimal Sum { get; set; }
public Guid SpendingGroupId { get; set; }
public SpendingGroup SpendingGroup { get; set; } = null!;
public void Update(SpendingPlanDto spendingPlan)
{
StartAt = spendingPlan.StartAt;
EndAt = spendingPlan.EndAt;
Sum = spendingPlan.Sum;
SpendingGroupId = spendingPlan.SpendingGroupId;
}
}

View File

@ -0,0 +1,27 @@
using System.Reflection.Metadata.Ecma335;
using Contracts.DTO;
namespace Infrastructure.Models;
public class User
{
public Guid Id { get; set; }
public string Name { get; set; } = null!;
public string Password { get; set; } = null!;
public decimal Balance { get; set; }
public List<SpendingGroup>? SpendingGroups { get; set; }
public List<ChangeRecord>? ChangeRecords { get; set; }
public void Update(UserDto userDto)
{
Id = userDto.Id;
if (!string.IsNullOrWhiteSpace(userDto.Name))
{
Name = userDto.Name;
}
if (!string.IsNullOrWhiteSpace(userDto.Password))
{
Password = userDto.Password;
}
}
}

View File

@ -0,0 +1,98 @@
using Contracts.DTO;
using Contracts.Repositories;
using Contracts.SearchModels;
using Infrastructure.Support.Mappers;
using Microsoft.EntityFrameworkCore;
namespace Infrastructure.Repositories;
public class ChangeRecordRepo : IChangeRecordRepo
{
public readonly IDbContextFactory<DatabaseContext> _factory;
public ChangeRecordRepo(IDbContextFactory<DatabaseContext> factory)
{
_factory = factory;
}
public async Task<ChangeRecordDto> Create(ChangeRecordDto changeRecord)
{
using var context = _factory.CreateDbContext();
var createdRecord = await context.ChangeRecords.AddAsync(changeRecord.ToModel());
await context.SaveChangesAsync();
return createdRecord.Entity.ToDto();
}
public async Task<ChangeRecordDto?> Delete(ChangeRecordSearch search)
{
using var context = _factory.CreateDbContext();
var record = await context.ChangeRecords
.FirstOrDefaultAsync(x => x.Id == search.Id);
if (record == null)
{
return null;
}
context.ChangeRecords.Remove(record);
await context.SaveChangesAsync();
return record.ToDto();
}
public async Task<ChangeRecordDto?> Get(ChangeRecordSearch search)
{
using var context = _factory.CreateDbContext();
var record = await context.ChangeRecords
.FirstOrDefaultAsync(x => x.Id == search.Id);
if (record == null)
{
return null;
}
return record.ToDto();
}
public async Task<IEnumerable<ChangeRecordDto>> GetList(ChangeRecordSearch? search = null)
{
using var context = _factory.CreateDbContext();
var query = context.ChangeRecords.AsQueryable();
if (search != null)
{
if (search.SpendingGroupId.HasValue)
{
query = query.Where(x => x.SpendingGroupId == search.SpendingGroupId);
}
if (search.From.HasValue && search.To.HasValue)
{
query = query.Where(x => x.ChangedAt >= search.From && x.ChangedAt <= search.To);
}
if (search.UserId.HasValue)
{
query = query.Where(x => x.UserId == search.UserId);
}
}
return await query.Include(x => x.SpendingGroup).Select(x => x.ToDto()).ToListAsync();
}
public async Task<ChangeRecordDto?> Update(ChangeRecordDto changeRecord)
{
using var context = _factory.CreateDbContext();
var existingRecord = await context.ChangeRecords
.FirstOrDefaultAsync(x => x.Id == changeRecord.Id);
if (existingRecord == null)
{
return null;
}
existingRecord.Update(changeRecord);
context.ChangeRecords.Update(existingRecord);
await context.SaveChangesAsync();
return existingRecord.ToDto();
}
}

View File

@ -0,0 +1,103 @@
using Contracts.DTO;
using Contracts.Repositories;
using Contracts.SearchModels;
using Infrastructure.Support.Mappers;
using Microsoft.EntityFrameworkCore;
namespace Infrastructure.Repositories;
public class SpendingGroupRepo : ISpendingGroupRepo
{
public readonly IDbContextFactory<DatabaseContext> _factory;
public SpendingGroupRepo(IDbContextFactory<DatabaseContext> factory)
{
_factory = factory;
}
public async Task<SpendingGroupDto> Create(SpendingGroupDto spendingGroup)
{
using var context = _factory.CreateDbContext();
var createdGroup = await context.SpendingGroups.AddAsync(spendingGroup.ToModel());
await context.SaveChangesAsync();
return createdGroup.Entity.ToDto();
}
public async Task<SpendingGroupDto?> Delete(SpendingGroupSearch search)
{
using var context = _factory.CreateDbContext();
var group = await context.SpendingGroups
.FirstOrDefaultAsync(x => x.Id == search.Id
|| x.Name == search.Name);
if (group == null)
{
return null;
}
context.SpendingGroups.Remove(group);
await context.SaveChangesAsync();
return group.ToDto();
}
public async Task<SpendingGroupDto?> Get(SpendingGroupSearch search)
{
using var context = _factory.CreateDbContext();
var group = await context.SpendingGroups
.Include(x => x.ChangeRecords)
.Include(x => x.SpendingPlans)
.FirstOrDefaultAsync(x => x.Id == search.Id
|| (!string.IsNullOrWhiteSpace(search.Name)
&& x.Name == search.Name
&& x.UserId == search.UserId));
return group?.ToDto();
}
public async Task<IEnumerable<SpendingGroupDto>> GetList(SpendingGroupSearch? search = null)
{
using var context = _factory.CreateDbContext();
var query = context.SpendingGroups.AsQueryable();
if (search != null)
{
if (search.Id != null)
{
query = query.Where(x => x.Id == search.Id);
}
if (!string.IsNullOrWhiteSpace(search.Name) && search.UserId.HasValue)
{
query = query.Where(x => x.Name.Contains(search.Name, StringComparison.OrdinalIgnoreCase)
&& x.UserId == search.UserId);
}
}
return await query
.Include(x => x.ChangeRecords)
.Include(x => x.SpendingPlans)
.Select(x => x.ToDto())
.ToListAsync();
}
public async Task<SpendingGroupDto?> Update(SpendingGroupDto spendingGroup)
{
using var context = _factory.CreateDbContext();
var existingGroup = await context.SpendingGroups
.FirstOrDefaultAsync(x => x.Id == spendingGroup.Id);
if (existingGroup == null)
{
return null;
}
existingGroup.Name = spendingGroup.Name;
context.SpendingGroups.Update(existingGroup);
await context.SaveChangesAsync();
return existingGroup.ToDto();
}
}

View File

@ -0,0 +1,86 @@
using Contracts.DTO;
using Contracts.Repositories;
using Contracts.SearchModels;
using Infrastructure.Support.Mappers;
using Microsoft.EntityFrameworkCore;
namespace Infrastructure.Repositories;
public class SpendingPlanRepo : ISpendingPlanRepo
{
public readonly IDbContextFactory<DatabaseContext> _factory;
public SpendingPlanRepo(IDbContextFactory<DatabaseContext> factory)
{
_factory = factory;
}
public async Task<SpendingPlanDto> Create(SpendingPlanDto dto)
{
using var context = _factory.CreateDbContext();
var plan = await context.SpendingPlans.AddAsync(dto.ToModel());
await context.SaveChangesAsync();
return plan.Entity.ToDto();
}
public async Task<SpendingPlanDto?> Delete(SpendingPlanSearch search)
{
using var context = _factory.CreateDbContext();
var plan = await context.SpendingPlans.FirstOrDefaultAsync(x => x.Id == search.Id);
if (plan == null)
{
return null;
}
context.SpendingPlans.Remove(plan);
await context.SaveChangesAsync();
return plan.ToDto();
}
public async Task<SpendingPlanDto?> Get(SpendingPlanSearch search)
{
using var context = _factory.CreateDbContext();
var plan = await context.SpendingPlans.FirstOrDefaultAsync(x => x.Id == search.Id);
return plan?.ToDto();
}
public async Task<IEnumerable<SpendingPlanDto>> GetList(SpendingPlanSearch? search = null)
{
using var context = _factory.CreateDbContext();
var query = context.SpendingPlans.AsQueryable();
if (search != null)
{
if (search.Id != null)
{
query = query.Where(x => x.Id == search.Id);
}
}
return await query.Select(x => x.ToDto()).ToListAsync();
}
public async Task<SpendingPlanDto?> Update(SpendingPlanDto dto)
{
using var context = _factory.CreateDbContext();
var plan = await context.SpendingPlans.FirstOrDefaultAsync(x => x.Id == dto.Id);
if (plan == null)
{
return null;
}
plan.Update(dto);
context.SpendingPlans.Update(plan);
await context.SaveChangesAsync();
return plan.ToDto();
}
}

View File

@ -0,0 +1,91 @@
using Contracts.DTO;
using Contracts.Repositories;
using Contracts.SearchModels;
using Infrastructure.Support.Mappers;
using Microsoft.EntityFrameworkCore;
namespace Infrastructure.Repositories;
public class UserRepo : IUserRepo
{
public readonly IDbContextFactory<DatabaseContext> _factory;
public UserRepo(IDbContextFactory<DatabaseContext> factory)
{
_factory = factory;
}
public async Task<bool> ChangeBalance(UserSearch search, decimal amount)
{
using var context = _factory.CreateDbContext();
var user = await context.Users
.FirstOrDefaultAsync(x => x.Id == search.Id
|| x.Name == search.Name);
if (user == null)
{
return false;
}
user.Balance += amount;
context.Users.Update(user);
await context.SaveChangesAsync();
return true;
}
public async Task<UserDto> Create(UserDto user)
{
using var context = _factory.CreateDbContext();
var createdUser = await context.Users.AddAsync(user.ToModel());
await context.SaveChangesAsync();
return createdUser.Entity.ToDto();
}
public async Task<UserDto?> Delete(UserSearch search)
{
using var context = _factory.CreateDbContext();
var user = await context.Users
.FirstOrDefaultAsync(x => x.Id == search.Id
|| x.Name == search.Name);
if (user == null)
{
return null;
}
context.Users.Remove(user);
await context.SaveChangesAsync();
return user.ToDto();
}
public async Task<UserDto?> Get(UserSearch search)
{
using var context = _factory.CreateDbContext();
var user = await context.Users
.FirstOrDefaultAsync(x => x.Id == search.Id
|| x.Name == search.Name);
return user?.ToDto();
}
public async Task<UserDto?> Update(UserDto user)
{
using var context = _factory.CreateDbContext();
var existingUser = await context.Users.FirstOrDefaultAsync(x => x.Id == user.Id);
if (existingUser == null)
{
return null;
}
existingUser.Update(user);
context.Users.Update(existingUser);
await context.SaveChangesAsync();
return existingUser.ToDto();
}
}

View File

@ -0,0 +1,28 @@
using Contracts.DTO;
using Infrastructure.Models;
namespace Infrastructure.Support.Mappers;
public static class ChangeRecordMapper
{
public static ChangeRecordDto ToDto(this ChangeRecord changeRecord)
=> new()
{
Id = changeRecord.Id,
Sum = changeRecord.Sum,
ChangedAt = changeRecord.ChangedAt,
SpendingGroupId = changeRecord.SpendingGroupId,
UserId = changeRecord.UserId,
SpendingGroupName = changeRecord.SpendingGroup?.Name
};
public static ChangeRecord ToModel(this ChangeRecordDto changeRecord)
=> new()
{
Id = changeRecord.Id,
Sum = changeRecord.Sum,
ChangedAt = changeRecord.ChangedAt,
SpendingGroupId = changeRecord.SpendingGroupId,
UserId = changeRecord.UserId
};
}

View File

@ -0,0 +1,24 @@
using Contracts.DTO;
using Infrastructure.Models;
namespace Infrastructure.Support.Mappers;
public static class SpendingGroupMapper
{
public static SpendingGroupDto ToDto(this SpendingGroup group)
=> new()
{
Id = group.Id,
Name = group.Name,
UserId = group.UserId,
ChangeRecords = group.ChangeRecords?.Select(x => x.ToDto()).ToList() ?? [],
SpendingPlans = group.SpendingPlans?.Select(x => x.ToDto()).ToList() ?? []
};
public static SpendingGroup ToModel(this SpendingGroupDto group)
=> new()
{
Id = group.Id,
Name = group.Name,
UserId = group.UserId
};
}

View File

@ -0,0 +1,27 @@
using Contracts.DTO;
using Infrastructure.Models;
namespace Infrastructure.Support.Mappers;
public static class SpendingPlanMapper
{
public static SpendingPlan ToModel(this SpendingPlanDto plan)
=> new()
{
Id = plan.Id,
StartAt = plan.StartAt,
EndAt = plan.EndAt,
Sum = plan.Sum,
SpendingGroupId = plan.SpendingGroupId
};
public static SpendingPlanDto ToDto(this SpendingPlan plan)
=> new()
{
Id = plan.Id,
StartAt = plan.StartAt,
EndAt = plan.EndAt,
Sum = plan.Sum,
SpendingGroupId = plan.SpendingGroupId
};
}

View File

@ -0,0 +1,25 @@
using Contracts.DTO;
using Infrastructure.Models;
namespace Infrastructure.Support.Mappers;
public static class UserMapper
{
public static UserDto ToDto(this User user)
=> new()
{
Id = user.Id,
Name = user.Name,
Balance = user.Balance,
Password = user.Password
};
public static User ToModel(this UserDto user)
=> new()
{
Id = user.Id,
Name = user.Name,
Balance = user.Balance,
Password = user.Password
};
}

View File

@ -0,0 +1,49 @@
using Contracts.DTO;
using Contracts.Mappers;
using Contracts.Repositories;
using Contracts.SearchModels;
using Contracts.Services;
using Contracts.ViewModels;
using Services.Support.Exceptions;
namespace Services.Domain;
public class AuthService : IAuthService
{
private readonly IUserRepo _userRepo;
public AuthService(IUserRepo userRepo)
{
_userRepo = userRepo;
}
public async Task<UserViewModel> Login(UserLoginDto loginData)
{
if (loginData == null || string.IsNullOrWhiteSpace(loginData.Name)
|| string.IsNullOrWhiteSpace(loginData.Password))
{
throw new ArgumentException("Неверные данные для входа");
}
var user = await _userRepo.Get(new UserSearch() { Name = loginData.Name });
if (user == null)
{
throw new UserNotFoundException($"Пользователь {loginData.Name} не найден");
}
return user.ToView();
}
public async Task<UserViewModel> Register(UserDto user)
{
var existingUser = await _userRepo.Get(new UserSearch() { Name = user.Name });
if (existingUser != null)
{
throw new AlreadyExistsException("Такой пользователь уже существует");
}
var createdUser = await _userRepo.Create(user);
return createdUser.ToView();
}
}

View File

@ -0,0 +1,59 @@
using Contracts.DTO;
using Contracts.Mappers;
using Contracts.Repositories;
using Contracts.SearchModels;
using Contracts.Services;
using Contracts.ViewModels;
namespace Services.Domain;
public class ChangeRecordService : IChangeRecordService
{
private readonly IChangeRecordRepo _changeRecordRepo;
private readonly IUserRepo _userRepo;
public ChangeRecordService(IChangeRecordRepo changeRecordRepo, IUserRepo userRepo)
{
_changeRecordRepo = changeRecordRepo;
_userRepo = userRepo;
}
public async Task<ChangeRecordViewModel> Create(ChangeRecordDto model)
{
var record = await _changeRecordRepo.Create(model);
await _userRepo.ChangeBalance(new() { Id = model.UserId }, model.Sum);
return record.ToView();
}
public async Task<ChangeRecordViewModel> Delete(ChangeRecordSearch search)
{
var record = await _changeRecordRepo.Delete(search);
if (record == null)
{
throw new EntryPointNotFoundException("При удалении не получилось найти запись измнения баланса");
}
// Возвращает баланс обратно
await _userRepo.ChangeBalance(new() { Id = record.UserId }, -record.Sum);
return record.ToView();
}
public async Task<IEnumerable<ChangeRecordViewModel>> GetList(ChangeRecordSearch? search = null)
{
var records = await _changeRecordRepo.GetList(search);
return records.Select(x => x.ToView()).ToList();
}
public async Task<ChangeRecordViewModel> Update(ChangeRecordDto model)
{
var record = await _changeRecordRepo.Update(model);
if (record == null)
{
throw new EntryPointNotFoundException("При изменении не получилось найти запись измнения баланса");
}
await _userRepo.ChangeBalance(new() { Id = model.UserId }, model.Sum - record.Sum);
return record.ToView();
}
}

View File

@ -0,0 +1,63 @@
using Contracts.DTO;
using Contracts.Mappers;
using Contracts.Repositories;
using Contracts.SearchModels;
using Contracts.ViewModels;
using Services.Support.Exceptions;
namespace Contracts.Services;
public class SpendingGroupService : ISpendingGroupService
{
private readonly ISpendingGroupRepo _spendingGroupRepo;
public SpendingGroupService(ISpendingGroupRepo spendingGroupRepo)
{
_spendingGroupRepo = spendingGroupRepo;
}
public async Task<SpendingGroupViewModel> Create(SpendingGroupDto model)
{
var group = await _spendingGroupRepo.Create(model);
return group.ToView();
}
public async Task<SpendingGroupViewModel> Delete(SpendingGroupSearch search)
{
var group = await _spendingGroupRepo.Delete(search);
if (group == null)
{
throw new EntityNotFoundException("При удалении не получилось найти группу");
}
return group.ToView();
}
public async Task<SpendingGroupViewModel> GetDetails(SpendingGroupSearch search)
{
var group = await _spendingGroupRepo.Get(search);
if (group == null)
{
throw new EntityNotFoundException("Не удалось найти группу по таким параметрам");
}
return group.ToView();
}
public async Task<IEnumerable<SpendingGroupViewModel>> GetList(SpendingGroupSearch? search = null)
{
var groups = await _spendingGroupRepo.GetList(search);
return groups.Select(x => x.ToView()).ToList();
}
public async Task<SpendingGroupViewModel> Update(SpendingGroupDto model)
{
var group = await _spendingGroupRepo.Update(model);
if (group == null)
{
throw new EntityNotFoundException("При обновлении не получилось найти группу");
}
return group.ToView();
}
}

View File

@ -0,0 +1,61 @@
using Contracts.DTO;
using Contracts.Mappers;
using Contracts.Repositories;
using Contracts.SearchModels;
using Contracts.Services;
using Contracts.ViewModels;
using Services.Support.Exceptions;
namespace Services.Domain;
public class SpendingPlanService : ISpendingPlanService
{
private readonly ISpendingPlanRepo _spendingPlanRepo;
public SpendingPlanService(ISpendingPlanRepo spendingPlanRepo)
{
_spendingPlanRepo = spendingPlanRepo;
}
public async Task<SpendingPlanViewModel> Create(SpendingPlanDto spendingPlan)
{
var plan = await _spendingPlanRepo.Create(spendingPlan);
return plan.ToView();
}
public async Task<SpendingPlanViewModel> Delete(SpendingPlanSearch search)
{
var plan = await _spendingPlanRepo.Delete(search);
if (plan == null)
{
throw new EntityNotFoundException("При удалении не получилось найти план");
}
return plan.ToView();
}
public async Task<SpendingPlanViewModel> GetDetails(SpendingPlanSearch search)
{
var plan = await _spendingPlanRepo.Get(search);
if (plan == null)
{
throw new EntityNotFoundException("Не удалось найти план по таким параметрам");
}
return plan.ToView();
}
public async Task<IEnumerable<SpendingPlanViewModel>> GetList(SpendingPlanSearch? search = null)
{
var plans = await _spendingPlanRepo.GetList(search);
return plans.Select(x => x.ToView()).ToList();
}
public async Task<SpendingPlanViewModel> Update(SpendingPlanDto spendingPlan)
{
var plan = await _spendingPlanRepo.Update(spendingPlan);
if (plan == null)
{
throw new EntityNotFoundException("При обновлении не получилось найти план");
}
return plan.ToView();
}
}

View File

@ -0,0 +1,49 @@
using Contracts.DTO;
using Contracts.Mappers;
using Contracts.Repositories;
using Contracts.SearchModels;
using Contracts.Services;
using Contracts.ViewModels;
using Services.Support.Exceptions;
namespace Services.Domain;
public class UserService : IUserService
{
private readonly IUserRepo _userRepo;
public UserService(IUserRepo userRepo)
{
_userRepo = userRepo;
}
public async Task<UserViewModel> Delete(UserSearch search)
{
var user = await _userRepo.Delete(search);
if (user == null)
{
throw new UserNotFoundException($"Пользователь для удаления не найден");
}
return user.ToView();
}
public async Task<UserViewModel> GetDetails(UserSearch search)
{
var user = await _userRepo.Get(search);
if (user == null)
{
throw new UserNotFoundException($"Пользователь {search.Name} не найден");
}
return user.ToView();
}
public async Task<UserViewModel> UpdateUserData(UserDto user)
{
var updatedUser = await _userRepo.Update(user);
if (updatedUser == null)
{
throw new EntityNotFoundException("При обновлении не получилось найти пользователя с id = " + user.Id);
}
return updatedUser.ToView();
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Contracts\Contracts.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,9 @@
namespace Services.Support.Exceptions;
public class AlreadyExistsException : Exception
{
public AlreadyExistsException(string message) : base(message) { }
public AlreadyExistsException(string message, Exception innerException)
: base(message, innerException) { }
}

View File

@ -0,0 +1,10 @@
namespace Services.Support.Exceptions;
public class EntityNotFoundException : Exception
{
public EntityNotFoundException(string message)
: base(message) { }
public EntityNotFoundException(string message, Exception innerException)
: base(message, innerException) { }
}

View File

@ -0,0 +1,12 @@
using Contracts.SearchModels;
namespace Services.Support.Exceptions;
public class UserNotFoundException : EntityNotFoundException
{
public UserNotFoundException(string message)
: base(message) { }
public UserNotFoundException(string message, Exception innerException)
: base(message, innerException) { }
}

41
docker-compose.debug.yml Normal file
View File

@ -0,0 +1,41 @@
# Please refer https://aka.ms/HTTPSinContainer on how to setup an https developer certificate for your ASP.NET Core service.
services:
api:
image: api
build:
context: .
dockerfile: back/Controllers/Dockerfile
args:
- configuration=Debug
ports:
- 5125:5125
environment:
- ConnectionStrings__DefaultConnection=${CONNECTION_STRING}
- ASPNETCORE_ENVIRONMENT=Development
volumes:
- ~/.vsdbg:/remote_debugger:rw
depends_on:
- database
dombudg:
image: dombudg
build:
context: front
dockerfile: ./Dockerfile
environment:
- VITE_API_URL=http://api:5125
ports:
- 80:80
depends_on:
- api
database:
image: postgres:14
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
driver: local

35
docker-compose.yml Normal file
View File

@ -0,0 +1,35 @@
services:
api:
image: api
build:
context: .
dockerfile: back/Controllers/Dockerfile
ports:
- 5125:5125
environment:
- ConnectionStrings__DefaultConnection=${CONNECTION_STRING}
depends_on:
- database
dombudg:
image: dombudg
build:
context: front
dockerfile: ./Dockerfile
environment:
- VITE_API_URL=http://api:5125
ports:
- 80:80
depends_on:
- api
database:
image: postgres:14
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
driver: local

24
front/.dockerignore Normal file
View File

@ -0,0 +1,24 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/charts
**/docker-compose*
**/compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

26
front/.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env

18
front/Dockerfile Normal file
View File

@ -0,0 +1,18 @@
FROM node as vite-app
ARG VITE_API_URL
WORKDIR /app/client
COPY . .
RUN ["npm", "i"]
RUN ["npm", "run", "build"]
FROM nginx:alpine
COPY nginx.conf /etc/nginx
RUN rm -rf /usr/share/nginx/html/*
COPY --from=vite-app /app/client/dist /usr/share/nginx/html
ENTRYPOINT ["nginx", "-g", "daemon off;"]

38
front/components.d.ts vendored Normal file
View File

@ -0,0 +1,38 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AButton: typeof import('ant-design-vue/es')['Button']
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
ALayout: typeof import('ant-design-vue/es')['Layout']
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
ATable: typeof import('ant-design-vue/es')['Table']
ChangeRecordManager: typeof import('./src/components/support/ChangeRecordManager.vue')['default']
Groups: typeof import('./src/components/pages/Groups.vue')['default']
Header: typeof import('./src/components/main/Header.vue')['default']
Home: typeof import('./src/components/pages/Home.vue')['default']
Login: typeof import('./src/components/pages/Login.vue')['default']
PlanManager: typeof import('./src/components/support/PlanManager.vue')['default']
Plans: typeof import('./src/components/pages/Plans.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SignUp: typeof import('./src/components/pages/SignUp.vue')['default']
SpendingGroupManager: typeof import('./src/components/support/SpendingGroupManager.vue')['default']
}
}

12
front/index.html Normal file
View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ДомБюдж</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

55
front/nginx.conf Normal file
View File

@ -0,0 +1,55 @@
# Запускать в качестве менее привилегированного пользователя по соображениям безопасности..
user nginx;
# Значение auto устанавливает число максимально доступных ядер CPU,
# чтобы обеспечить лучшую производительность.
worker_processes auto;
events { worker_connections 1024; }
http {
server {
# Hide nginx version information.
server_tokens off;
listen 80;
root /usr/share/nginx/html;
include /etc/nginx/mime.types;
location /api/ {
proxy_pass http://api:5125/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix /test;
}
location / {
try_files $uri $uri/ /index.html;
}
gzip on;
gzip_vary on;
gzip_http_version 1.0;
gzip_comp_level 5;
gzip_types
application/atom+xml
application/javascript
application/json
application/rss+xml
application/vnd.ms-fontobject
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/svg+xml
image/x-icon
text/css
text/plain
text/x-component;
gzip_proxied no-cache no-store private expired auth;
gzip_min_length 256;
gunzip on;
}
}

3000
front/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
front/package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "dombudg",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview",
"gen-api": "swagger-typescript-api -r -o ./src/core/api/ --modular -p "
},
"dependencies": {
"@vueuse/core": "^12.0.0",
"ant-design-vue": "^4.2.6",
"dayjs": "^1.11.13",
"pinia": "^2.2.8",
"vue": "^3.5.12",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.4",
"swagger-typescript-api": "^13.0.23",
"typescript": "~5.6.2",
"unplugin-vue-components": "^0.27.5",
"vite": "^5.4.10",
"vue-tsc": "^2.1.8"
}
}

26
front/src/App.vue Normal file
View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import Header from './components/main/Header.vue';
</script>
<template>
<a-layout class="layout">
<Header />
<a-layout-content>
<RouterView />
</a-layout-content>
</a-layout>
</template>
<style scoped>
main {
display: flex;
justify-content: center;
padding: 5vh;
}
.base-page {
display: flex;
flex-direction: column;
min-width: 80dvw;
}
</style>

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import { inject } from 'vue';
import { useUserStore } from '../../store';
import { AuthService } from '../../core/services/auth-service';
import router from '../../router';
const store = useUserStore();
const authService = inject(AuthService.name) as AuthService;
function logout() {
authService.logout();
router.push({ name: 'login' });
}
</script>
<template>
<a-layout-header class="header">
<div class="base-nav">
<div>ДомБюдж</div>
<nav>
<RouterLink :to="{ name: 'home' }">Главная</RouterLink>
<RouterLink :to="{ name: 'groups' }">Группы расходов</RouterLink>
</nav>
</div>
<div v-if="!store.user.id">
<RouterLink :to="{ name: 'login' }">Войти</RouterLink>
</div>
<div v-else>
<label for="logout">Привет, {{ store.user.name }}! Ваш текущий баланс: {{ store.user.balance }}</label>
<a-button
name="logout"
@click="logout()"
danger
style="margin-left: 30px"
>
Выйти
</a-button>
</div>
</a-layout-header>
</template>
<style scoped>
.header {
background-color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.base-nav {
display: inline-flex;
justify-content: left;
}
.base-nav a {
margin-left: 30px;
}
</style>

View File

@ -0,0 +1,72 @@
<script setup lang="ts">
import { useAsyncState } from '@vueuse/core';
import { inject } from 'vue';
import { GroupService } from '../../core/services/group-service';
import SpendingGroupManager from '../support/SpendingGroupManager.vue';
import { DeleteOutlined } from '@ant-design/icons-vue';
const groupService = inject(GroupService.name) as GroupService;
const { state, isReady } = useAsyncState(() => groupService.getList(), []);
const columns = [
{
title: "Название группы",
dataIndex: "name",
key: "name",
},
{
title: "Планы группы",
dataIndex: "plans",
key: "plans",
},
{
title: 'Операция',
dataIndex: 'operation',
key: 'operation',
}
]
const refreshData = () => {
groupService.getList().then(data => {
state.value = data;
isReady.value = true;
});
}
const onDelete = (key: string) => {
groupService.deleteGroup(key)
.then(() => {
refreshData();
})
}
</script>
<template>
<div class="base-page">
<h1>Группы расходов</h1>
<SpendingGroupManager :refreshData="refreshData" />
<a-table :dataSource="state" :columns="columns" v-if="isReady">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'plans'">
<RouterLink :to="{ name: 'plans', params: { groupId: record.id } }" >Планы</RouterLink>
</template>
<template v-else-if="column.dataIndex === 'operation'">
<a-popconfirm
v-if="state?.length"
title="Точно удалить?"
@confirm="onDelete(record.id)"
>
<a><DeleteOutlined /> Удалить</a>
</a-popconfirm>
</template>
</template>
</a-table>
<div v-else>
<a-spin size="large" />
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,76 @@
<script setup lang="ts">
import { useAsyncState } from '@vueuse/core';
import ChangeRecordMenu from '../support/ChangeRecordManager.vue';
import { ChangeRecordService } from '../../core/services/change-record-service';
import { inject } from 'vue';
import { DeleteOutlined } from '@ant-design/icons-vue';
const changeRecordService = inject(ChangeRecordService.name) as ChangeRecordService;
const { state, isReady } = useAsyncState(() => changeRecordService.getList(), []);
const columns = [
{
title: 'Дата',
dataIndex: 'changedAt',
key: 'changedAt',
},
{
title: 'Сумма',
dataIndex: 'sum',
key: 'sum',
},
{
title: 'Группа расходов',
dataIndex: 'spendingGroupName',
key: 'spendingGroupName',
},
{
title: 'Операция',
dataIndex: 'operation',
key: 'operation',
}
]
const refreshData = () => {
changeRecordService.getList().then(data => {
state.value = data;
isReady.value = true;
});
}
const onDelete = (key: string) => {
changeRecordService.deleteRecord(key)
.then(() => {
refreshData();
})
}
</script>
<template>
<div class="base-page">
<h1>История изменений баланса</h1>
<ChangeRecordMenu :refreshData="refreshData" />
<a-table :dataSource="state" :columns="columns" v-if="isReady" >
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'operation'">
<a-popconfirm
v-if="state?.length"
title="Точно удалить?"
@confirm="onDelete(record.id)"
>
<a><DeleteOutlined /> Удалить</a>
</a-popconfirm>
</template>
</template>
</a-table>
<div v-else>
<a-spin size="large" />
</div>
</div>
</template>
<style scoped>
.layout {
}
</style>

View File

@ -0,0 +1,76 @@
<template>
<a-form
:model="formState"
name="login"
class="login-form"
@finish="onFinish"
@finishFailed="onFinishFailed"
>
<a-form-item
label="Логин"
name="name"
:rules="[{ required: true, message: 'Пожалуйста, введите свой логин' }]"
>
<a-input v-model:value="formState.name">
<template #prefix>
<UserOutlined class="site-form-item-icon" />
</template>
</a-input>
</a-form-item>
<a-form-item
label="Пароль"
name="password"
:rules="[{ required: true, message: 'Пароль тоже нужен!' }]"
>
<a-input-password v-model:value="formState.password">
<template #prefix>
<LockOutlined class="site-form-item-icon" />
</template>
</a-input-password>
</a-form-item>
<a-form-item>
<a-button :disabled="disabled" type="primary" html-type="submit" class="login-form-button">
Войти
</a-button>
Или
<RouterLink :to="{ name: 'signup' }">создать аккаунт</RouterLink>
</a-form-item>
</a-form>
</template>
<script lang="ts" setup>
import { reactive, computed, inject } from 'vue';
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue';
import { UserLoginDto } from '../../core/api/data-contracts';
import { RouterLink } from 'vue-router';
import { AuthService } from '../../core/services/auth-service';
import router from '../../router';
const formState = reactive<UserLoginDto>({
name: '',
password: '',
});
const authService = inject(AuthService.name) as AuthService;
console.log(authService);
// Логика формы
const onFinish = async (values: any) => {
console.log('Success:', values);
await authService.login(formState);
router.push({ name: 'home' });
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
const disabled = computed(() => {
return !(formState.name && formState.password);
});
</script>
<style scoped>
.login-form {
max-width: 300px;
}
.login-form-button {
width: 100%;
}
</style>

View File

@ -0,0 +1,79 @@
<script setup lang="ts">
import { useAsyncState } from '@vueuse/core';
import { inject } from 'vue';
import { PlanService } from '../../core/services/plans-service';
import { useRoute } from 'vue-router';
import PlanManager from '../support/PlanManager.vue';
import { DeleteOutlined } from '@ant-design/icons-vue';
const planService = inject(PlanService.name) as PlanService;
const groupId = useRoute().params.groupId as string;
const { state, isReady } = useAsyncState(() => planService.getList(groupId), []);
const columns = [
{
title: "Планируемые расходы",
dataIndex: "sum",
key: "sum",
},
{
title: "Начало плана",
dataIndex: "startAt",
key: "startAt",
},
{
title: "Конец плана",
dataIndex: "endAt",
key: "endAt",
},
{
title: 'Операция',
dataIndex: 'operation',
key: 'operation',
}
]
const refreshData = () => {
planService.getList(groupId).then(data => {
state.value = data;
isReady.value = true;
});
};
const onDelete = (key: string) => {
planService.deletePlan(key)
.then(() => {
refreshData();
})
}
</script>
<template>
<div class="base-page">
<h1>Планы группы</h1>
<PlanManager :groupId="groupId" :refreshData="refreshData"/>
<a-table :dataSource="state" :columns="columns" v-if="isReady">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'operation'">
<a-popconfirm
v-if="state?.length"
title="Точно удалить?"
@confirm="onDelete(record.id)"
>
<a><DeleteOutlined /> Удалить</a>
</a-popconfirm>
</template>
</template>
</a-table>
<div v-else>
<a-spin size="large" />
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,86 @@
<template>
<a-form
:model="formState"
name="signup"
class="signup-form"
@finish="onFinish"
@finishFailed="onFinishFailed"
>
<a-form-item
label="Логин"
name="name"
:rules="[{ required: true, message: 'Пожалуйста, введите свой логин' }]"
>
<a-input v-model:value="formState.name">
<template #prefix>
<UserOutlined class="site-form-item-icon" />
</template>
</a-input>
</a-form-item>
<a-form-item
label="Пароль"
name="password"
:rules="[{ required: true, message: 'Пароль тоже нужен!' }]"
>
<a-input-password v-model:value="formState.password">
<template #prefix>
<LockOutlined class="site-form-item-icon" />
</template>
</a-input-password>
</a-form-item>
<a-form-item
label="Баланс"
name="balance"
:rules="[{ required: true, message: 'Пожалуйста, введите свой баланс' }]"
>
<a-input-number prefix="₽" v-model:value="formState.balance" min="0"/>
</a-form-item>
<a-form-item>
<a-button :disabled="disabled" type="primary" html-type="submit" class="signup-form-button">
Создать
</a-button>
Или
<RouterLink :to="{ name: 'login' }">войти в свой аккаунт</RouterLink>
</a-form-item>
</a-form>
</template>
<script lang="ts" setup>
import { reactive, computed, inject } from 'vue';
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue';
import { UserDto } from '../../core/api/data-contracts';
import { RouterLink } from 'vue-router';
import { AuthService } from '../../core/services/auth-service';
import router from '../../router';
const formState = reactive<UserDto>({
name: '',
password: '',
balance: 0
});
const authService = inject(AuthService.name) as AuthService;
// Логика формы
const onFinish = async (values: any) => {
console.log('Success:', values);
await authService.register(formState);
router.push({ name: 'home' });
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
const disabled = computed(() => {
return !(formState.name && formState.password && formState.balance);
});
</script>
<style scoped>
.signup-form {
max-width: 300px;
}
.signup-form-button {
width: 100%;
}
</style>

Some files were not shown because too many files have changed in this diff Show More