mirror of
https://github.com/ivanpaulovich/clean-architecture-manga.git
synced 2025-01-08 11:57:36 +08:00
Adds identity server 4
This commit is contained in:
parent
6b58e209e3
commit
08094b34c1
17
.docker/docker-compose.dcproj
Normal file
17
.docker/docker-compose.dcproj
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" Sdk="Microsoft.Docker.Sdk">
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectVersion>2.1</ProjectVersion>
|
||||
<DockerTargetOS>Linux</DockerTargetOS>
|
||||
<ProjectGuid>a0517af3-3b35-443a-80dc-ff94f10cf056</ProjectGuid>
|
||||
<DockerLaunchAction>LaunchBrowser</DockerLaunchAction>
|
||||
<DockerServiceUrl>{Scheme}://localhost:{ServicePort}/{Scheme}://{ServiceHost}:{ServicePort}</DockerServiceUrl>
|
||||
<DockerServiceName>webapi</DockerServiceName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Include="docker-compose.override.yml">
|
||||
<DependentUpon>docker-compose.yml</DependentUpon>
|
||||
</None>
|
||||
<None Include="docker-compose.yml" />
|
||||
</ItemGroup>
|
||||
</Project>
|
36
.docker/docker-compose.override.yml
Normal file
36
.docker/docker-compose.override.yml
Normal file
@ -0,0 +1,36 @@
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
accounts-api:
|
||||
ports:
|
||||
- "5005:5005"
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- ASPNETCORE_URLS=https://+:5005
|
||||
- ASPNETCORE_HTTPS_PORT=5005
|
||||
- ASPNETCORE_Kestrel__Certificates__Default__Password=MyCertificatePassword
|
||||
- ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx
|
||||
volumes:
|
||||
- ${USERPROFILE}\.aspnet\https:/https:ro
|
||||
identity-server:
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- ASPNETCORE_URLS=https://+:5000
|
||||
- ASPNETCORE_HTTPS_PORT=5000
|
||||
- ASPNETCORE_Kestrel__Certificates__Default__Password=MyCertificatePassword
|
||||
- ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx
|
||||
volumes:
|
||||
- ${USERPROFILE}\.aspnet\https:/https:ro
|
||||
wallet-spa:
|
||||
stdin_open: true # docker run -i
|
||||
tty: true # docker run -t
|
||||
ports:
|
||||
- "5010:5010"
|
||||
sql1:
|
||||
environment:
|
||||
SA_PASSWORD: "<YourStrong!Passw0rd>"
|
||||
ACCEPT_EULA: "Y"
|
||||
ports:
|
||||
- "1433:1433"
|
19
.docker/docker-compose.yml
Normal file
19
.docker/docker-compose.yml
Normal file
@ -0,0 +1,19 @@
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
accounts-api:
|
||||
image: ${DOCKER_REGISTRY-}accounts
|
||||
build:
|
||||
context: ../accounts-api/
|
||||
dockerfile: src/WebApi/Dockerfile
|
||||
identity-server:
|
||||
image: ${DOCKER_REGISTRY-}identityserver
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: identity-server/Dockerfile
|
||||
wallet-spa:
|
||||
image: ${DOCKER_REGISTRY-}wallet
|
||||
build:
|
||||
context: ../wallet-spa/
|
||||
sql1:
|
||||
image: "mcr.microsoft.com/mssql/server:2019-latest"
|
7
.github/workflows/dotnet-core.yml
vendored
7
.github/workflows/dotnet-core.yml
vendored
@ -12,8 +12,9 @@ jobs:
|
||||
docker pull mcr.microsoft.com/mssql/server:2017-latest
|
||||
docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=<YourStrong!Passw0rd>' -p 1433:1433 --name sql1 -d mcr.microsoft.com/mssql/server:2017-latest
|
||||
sleep 10
|
||||
docker exec -i sql1 /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P '<YourStrong!Passw0rd>' -Q 'ALTER LOGIN SA WITH PASSWORD="<YourNewStrong!Passw0rd>"'
|
||||
dotnet tool install --global dotnet-ef
|
||||
dotnet ef database update --project src/Infrastructure --startup-project src/Infrastructure
|
||||
pushd accounts-api
|
||||
dotnet tool update --global dotnet-ef --version 3.1.7
|
||||
dotnet ef database update --project src/Infrastructure --startup-project src/WebApi
|
||||
popd
|
||||
dotnet build
|
||||
dotnet test
|
||||
|
@ -1,2 +0,0 @@
|
||||
*.md
|
||||
Directory.Build.props
|
67
.vscode/launch.json
vendored
67
.vscode/launch.json
vendored
@ -1,67 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch localhost",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "https://localhost:5001",
|
||||
"webRoot": "${workspaceFolder}/src/WebApi/ClientApp"
|
||||
},
|
||||
{
|
||||
"name": "Attach to url with files served from ./src/WebApi/ClientApp",
|
||||
"type": "chrome",
|
||||
"request": "attach",
|
||||
"port": 9222,
|
||||
"url": "https://localhost:5001",
|
||||
"webRoot": "${workspaceFolder}/src/WebApi/ClientApp"
|
||||
},
|
||||
{
|
||||
"name": "Development",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/src/WebApi/bin/Debug/netcoreapp3.1/WebApi.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/WebApi",
|
||||
"stopAtEntry": false,
|
||||
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "^\\s*Now listening on:\\s+(https?://\\S+)"
|
||||
},
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"sourceFileMap": {
|
||||
"/Views": "${workspaceFolder}/Views"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Production",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/src/WebApi/bin/Debug/netcoreapp3.1/WebApi.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/WebApi",
|
||||
"stopAtEntry": false,
|
||||
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "^\\s*Now listening on:\\s+(https?://\\S+)"
|
||||
},
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Production"
|
||||
},
|
||||
"sourceFileMap": {
|
||||
"/Views": "${workspaceFolder}/Views"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -3,205 +3,124 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29519.87
|
||||
MinimumVisualStudioVersion = 15.0.26124.0
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6EE5F03A-7E37-48DB-95BA-3C42942B69AF}"
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{23ED54A6-81AF-4160-97A6-FD3C25C33E30}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "src\Application\Application.csproj", "{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure", "src\Infrastructure\Infrastructure.csproj", "{98E6E94F-804D-4332-8324-4F4DF037DCD3}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi", "src\WebApi\WebApi.csproj", "{EE3837B9-C6EB-4384-B9F9-C441232DBE15}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{6505DBE7-AC3B-4575-BCDC-C34F09D9373B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests", "test\IntegrationTests\IntegrationTests.csproj", "{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "test\UnitTests\UnitTests.csproj", "{5185498B-241B-49DA-BDA7-F08A2ACA4886}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComponentTests", "test\ComponentTests\ComponentTests.csproj", "{95F75E96-7060-4612-A78F-C187D0B93331}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0A5F185E-DE63-4F81-AF29-2B5F6AEC7885}"
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{F6826529-32A9-419B-B7E5-63BE9A0FDA93}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.gitignore = .gitignore
|
||||
.prettierignore = .prettierignore
|
||||
CHANGELOG.md = CHANGELOG.md
|
||||
Directory.Build.props = Directory.Build.props
|
||||
docker-compose.yml = docker-compose.yml
|
||||
.github\workflows\dotnet-core.yml = .github\workflows\dotnet-core.yml
|
||||
global.json = global.json
|
||||
LICENSE = LICENSE
|
||||
nuget.config = nuget.config
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain.Security", "src\Domain.Security\Domain.Security.csproj", "{670FB303-7E15-46BE-A863-731E2EDD3BBD}"
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "accounts-api", "accounts-api", "{51331007-CACA-4676-934B-217999A6B1E2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain.Customers", "src\Domain.Customers\Domain.Customers.csproj", "{6523AA08-D2E2-4C20-AE0A-09407C6A1750}"
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "identity-server", "identity-server", "{5829A64A-52DE-4656-A6C0-C06B1A6195E7}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain.Accounts", "src\Domain.Accounts\Domain.Accounts.csproj", "{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}"
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F4408BAF-2D26-4D97-808C-7A96C4A7F636}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "src\Common\Common.csproj", "{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}"
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{54ECA5D5-FCB9-4427-98A1-6915E0C2C71B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EndToEndTests", "test\EndToEndTests\EndToEndTests.csproj", "{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}"
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{B3ABDDAF-9164-4FC4-862D-07A2D9F62F5D}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
accounts-api\scripts\build.sh = accounts-api\scripts\build.sh
|
||||
accounts-api\scripts\download-tools.sh = accounts-api\scripts\download-tools.sh
|
||||
accounts-api\scripts\format.sh = accounts-api\scripts\format.sh
|
||||
accounts-api\scripts\sql-docker-up.sh = accounts-api\scripts\sql-docker-up.sh
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "accounts-api\src\Application\Application.csproj", "{80E73E2B-3507-4160-BF51-5B1BD3F94E4B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi", "accounts-api\src\WebApi\WebApi.csproj", "{5286EDF8-AE3D-4E7B-89B3-3AA1CE86C082}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure", "accounts-api\src\Infrastructure\Infrastructure.csproj", "{C38A01E0-F1C3-40C5-A8A8-5B3ECD5F59AB}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComponentTests", "accounts-api\test\ComponentTests\ComponentTests.csproj", "{5900EBD8-D50B-4F1E-B326-3C13298C7B73}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EndToEndTests", "accounts-api\test\EndToEndTests\EndToEndTests.csproj", "{82A0002A-969A-450B-BD42-C3065BD69649}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests", "accounts-api\test\IntegrationTests\IntegrationTests.csproj", "{48BBEC7F-651B-4EF2-94E4-E8FFAC3D8E7B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "accounts-api\test\UnitTests\UnitTests.csproj", "{BF05183E-699A-43A8-A5F3-1DB71B0F38B0}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".vscode", ".vscode", "{02D548DA-5DE0-486E-A5D5-9BEDFFC0CA2A}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
accounts-api\.vscode\launch.json = accounts-api\.vscode\launch.json
|
||||
accounts-api\.vscode\settings.json = accounts-api\.vscode\settings.json
|
||||
accounts-api\.vscode\tasks.json = accounts-api\.vscode\tasks.json
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdentityServer", "identity-server\IdentityServer.csproj", "{01537DBF-3C0F-4B83-A089-0D12E5CA06C6}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".docker", ".docker", "{FAA0BAC6-0AA8-4908-A287-D550E9F9CBA8}"
|
||||
EndProject
|
||||
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", ".docker\docker-compose.dcproj", "{A0517AF3-3B35-443A-80DC-FF94F10CF056}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain", "accounts-api\src\Domain\Domain.csproj", "{0925FCA6-083A-4478-80F3-2391987AAF2C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Release|x64.Build.0 = Release|Any CPU
|
||||
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Release|x86.Build.0 = Release|Any CPU
|
||||
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Release|x64.Build.0 = Release|Any CPU
|
||||
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Release|x86.Build.0 = Release|Any CPU
|
||||
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Release|x64.Build.0 = Release|Any CPU
|
||||
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Release|x86.Build.0 = Release|Any CPU
|
||||
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Release|x64.Build.0 = Release|Any CPU
|
||||
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Release|x86.Build.0 = Release|Any CPU
|
||||
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Release|x64.Build.0 = Release|Any CPU
|
||||
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Release|x86.Build.0 = Release|Any CPU
|
||||
{95F75E96-7060-4612-A78F-C187D0B93331}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{95F75E96-7060-4612-A78F-C187D0B93331}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{95F75E96-7060-4612-A78F-C187D0B93331}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{95F75E96-7060-4612-A78F-C187D0B93331}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{95F75E96-7060-4612-A78F-C187D0B93331}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{95F75E96-7060-4612-A78F-C187D0B93331}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{95F75E96-7060-4612-A78F-C187D0B93331}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{95F75E96-7060-4612-A78F-C187D0B93331}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{95F75E96-7060-4612-A78F-C187D0B93331}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{95F75E96-7060-4612-A78F-C187D0B93331}.Release|x64.Build.0 = Release|Any CPU
|
||||
{95F75E96-7060-4612-A78F-C187D0B93331}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{95F75E96-7060-4612-A78F-C187D0B93331}.Release|x86.Build.0 = Release|Any CPU
|
||||
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Release|x64.Build.0 = Release|Any CPU
|
||||
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Release|x86.Build.0 = Release|Any CPU
|
||||
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Release|x64.Build.0 = Release|Any CPU
|
||||
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Release|x86.Build.0 = Release|Any CPU
|
||||
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Release|x86.Build.0 = Release|Any CPU
|
||||
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Release|x64.Build.0 = Release|Any CPU
|
||||
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Release|x86.Build.0 = Release|Any CPU
|
||||
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Release|x64.Build.0 = Release|Any CPU
|
||||
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Release|x86.Build.0 = Release|Any CPU
|
||||
{80E73E2B-3507-4160-BF51-5B1BD3F94E4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{80E73E2B-3507-4160-BF51-5B1BD3F94E4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{80E73E2B-3507-4160-BF51-5B1BD3F94E4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{80E73E2B-3507-4160-BF51-5B1BD3F94E4B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5286EDF8-AE3D-4E7B-89B3-3AA1CE86C082}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5286EDF8-AE3D-4E7B-89B3-3AA1CE86C082}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5286EDF8-AE3D-4E7B-89B3-3AA1CE86C082}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5286EDF8-AE3D-4E7B-89B3-3AA1CE86C082}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C38A01E0-F1C3-40C5-A8A8-5B3ECD5F59AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C38A01E0-F1C3-40C5-A8A8-5B3ECD5F59AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C38A01E0-F1C3-40C5-A8A8-5B3ECD5F59AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C38A01E0-F1C3-40C5-A8A8-5B3ECD5F59AB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5900EBD8-D50B-4F1E-B326-3C13298C7B73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5900EBD8-D50B-4F1E-B326-3C13298C7B73}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5900EBD8-D50B-4F1E-B326-3C13298C7B73}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5900EBD8-D50B-4F1E-B326-3C13298C7B73}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{82A0002A-969A-450B-BD42-C3065BD69649}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{82A0002A-969A-450B-BD42-C3065BD69649}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{82A0002A-969A-450B-BD42-C3065BD69649}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{82A0002A-969A-450B-BD42-C3065BD69649}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{48BBEC7F-651B-4EF2-94E4-E8FFAC3D8E7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{48BBEC7F-651B-4EF2-94E4-E8FFAC3D8E7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{48BBEC7F-651B-4EF2-94E4-E8FFAC3D8E7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{48BBEC7F-651B-4EF2-94E4-E8FFAC3D8E7B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BF05183E-699A-43A8-A5F3-1DB71B0F38B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BF05183E-699A-43A8-A5F3-1DB71B0F38B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BF05183E-699A-43A8-A5F3-1DB71B0F38B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BF05183E-699A-43A8-A5F3-1DB71B0F38B0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{01537DBF-3C0F-4B83-A089-0D12E5CA06C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{01537DBF-3C0F-4B83-A089-0D12E5CA06C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{01537DBF-3C0F-4B83-A089-0D12E5CA06C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{01537DBF-3C0F-4B83-A089-0D12E5CA06C6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A0517AF3-3B35-443A-80DC-FF94F10CF056}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A0517AF3-3B35-443A-80DC-FF94F10CF056}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A0517AF3-3B35-443A-80DC-FF94F10CF056}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A0517AF3-3B35-443A-80DC-FF94F10CF056}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0925FCA6-083A-4478-80F3-2391987AAF2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0925FCA6-083A-4478-80F3-2391987AAF2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0925FCA6-083A-4478-80F3-2391987AAF2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0925FCA6-083A-4478-80F3-2391987AAF2C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2} = {6EE5F03A-7E37-48DB-95BA-3C42942B69AF}
|
||||
{98E6E94F-804D-4332-8324-4F4DF037DCD3} = {6EE5F03A-7E37-48DB-95BA-3C42942B69AF}
|
||||
{EE3837B9-C6EB-4384-B9F9-C441232DBE15} = {6EE5F03A-7E37-48DB-95BA-3C42942B69AF}
|
||||
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6} = {6505DBE7-AC3B-4575-BCDC-C34F09D9373B}
|
||||
{5185498B-241B-49DA-BDA7-F08A2ACA4886} = {6505DBE7-AC3B-4575-BCDC-C34F09D9373B}
|
||||
{95F75E96-7060-4612-A78F-C187D0B93331} = {6505DBE7-AC3B-4575-BCDC-C34F09D9373B}
|
||||
{670FB303-7E15-46BE-A863-731E2EDD3BBD} = {6EE5F03A-7E37-48DB-95BA-3C42942B69AF}
|
||||
{6523AA08-D2E2-4C20-AE0A-09407C6A1750} = {6EE5F03A-7E37-48DB-95BA-3C42942B69AF}
|
||||
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E} = {6EE5F03A-7E37-48DB-95BA-3C42942B69AF}
|
||||
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11} = {6EE5F03A-7E37-48DB-95BA-3C42942B69AF}
|
||||
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485} = {6505DBE7-AC3B-4575-BCDC-C34F09D9373B}
|
||||
{F6826529-32A9-419B-B7E5-63BE9A0FDA93} = {23ED54A6-81AF-4160-97A6-FD3C25C33E30}
|
||||
{F4408BAF-2D26-4D97-808C-7A96C4A7F636} = {51331007-CACA-4676-934B-217999A6B1E2}
|
||||
{54ECA5D5-FCB9-4427-98A1-6915E0C2C71B} = {51331007-CACA-4676-934B-217999A6B1E2}
|
||||
{B3ABDDAF-9164-4FC4-862D-07A2D9F62F5D} = {51331007-CACA-4676-934B-217999A6B1E2}
|
||||
{80E73E2B-3507-4160-BF51-5B1BD3F94E4B} = {F4408BAF-2D26-4D97-808C-7A96C4A7F636}
|
||||
{5286EDF8-AE3D-4E7B-89B3-3AA1CE86C082} = {F4408BAF-2D26-4D97-808C-7A96C4A7F636}
|
||||
{C38A01E0-F1C3-40C5-A8A8-5B3ECD5F59AB} = {F4408BAF-2D26-4D97-808C-7A96C4A7F636}
|
||||
{5900EBD8-D50B-4F1E-B326-3C13298C7B73} = {54ECA5D5-FCB9-4427-98A1-6915E0C2C71B}
|
||||
{82A0002A-969A-450B-BD42-C3065BD69649} = {54ECA5D5-FCB9-4427-98A1-6915E0C2C71B}
|
||||
{48BBEC7F-651B-4EF2-94E4-E8FFAC3D8E7B} = {54ECA5D5-FCB9-4427-98A1-6915E0C2C71B}
|
||||
{BF05183E-699A-43A8-A5F3-1DB71B0F38B0} = {54ECA5D5-FCB9-4427-98A1-6915E0C2C71B}
|
||||
{02D548DA-5DE0-486E-A5D5-9BEDFFC0CA2A} = {51331007-CACA-4676-934B-217999A6B1E2}
|
||||
{01537DBF-3C0F-4B83-A089-0D12E5CA06C6} = {5829A64A-52DE-4656-A6C0-C06B1A6195E7}
|
||||
{A0517AF3-3B35-443A-80DC-FF94F10CF056} = {FAA0BAC6-0AA8-4908-A287-D550E9F9CBA8}
|
||||
{0925FCA6-083A-4478-80F3-2391987AAF2C} = {F4408BAF-2D26-4D97-808C-7A96C4A7F636}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {0941E413-A73D-485D-8EDE-03CE0C6967E1}
|
||||
|
95
README.md
95
README.md
@ -29,37 +29,80 @@ We also support the React client:
|
||||
|
||||
## Build & Run
|
||||
|
||||
Run the following commands:
|
||||
|
||||
Spin up SQL Server:
|
||||
|
||||
```sh
|
||||
pushd src/WebApi/ClientApp
|
||||
cd .docker
|
||||
docker-compose build
|
||||
docker-compose up -d sql1
|
||||
dotnet tool update --global dotnet-ef --version 3.1.7
|
||||
dotnet ef database update --project ../accounts-api/src/Infrastructure --startup-project ../accounts-api/src/WebApi
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Then the following containers should be runnning:
|
||||
|
||||
| Application | Port | Protocol |
|
||||
|------------------ | ----- |--------- |
|
||||
| Wallet SPA | 5010 | HTTPS |
|
||||
| Accounts API | 5005 | HTTPS |
|
||||
| Identity Server | 5000 | HTTPS |
|
||||
| SQL Server | 1433 | TCP |
|
||||
|
||||
Browse to `https://localhost:5010` then click on Log In. Trust the [self-signed certificate](https://stackoverflow.com/questions/21397809/create-a-trusted-self-signed-ssl-cert-for-localhost-for-use-with-express-node).
|
||||
|
||||
|
||||
If you are prefer dotnet commands then start each service individually:
|
||||
|
||||
<details>
|
||||
<summary>Expand to get the dotnet run steps.</summary>
|
||||
|
||||
### Generate Self Signed Certificate
|
||||
|
||||
```sh
|
||||
dotnet dev-certs https --clean
|
||||
dotnet dev-certs https -ep $env:USERPROFILE\.aspnet\https\aspnetapp.pfx -p MyCertificatePassword
|
||||
```
|
||||
|
||||
### Spin up SQL Server in a Docker container
|
||||
|
||||
```sh
|
||||
docker pull mcr.microsoft.com/mssql/server:2017-latest
|
||||
docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=<YourStrong!Passw0rd>' -p 1433:1433 --name sql1 -d mcr.microsoft.com/mssql/server:2017-latest
|
||||
```
|
||||
|
||||
### Create and Seed Accounts Database
|
||||
|
||||
```sh
|
||||
dotnet tool update --global dotnet-ef --version 3.1.6
|
||||
dotnet ef database update --project accounts-api/src/Infrastructure --startup-project accounts-api/src/WebApi
|
||||
```
|
||||
|
||||
### Running Services
|
||||
|
||||
#### Identity Server
|
||||
|
||||
```sh
|
||||
dotnet run --project identity-server/src/IdentityServer.csproj
|
||||
```
|
||||
#### Account API
|
||||
|
||||
```sh
|
||||
dotnet run --project accounts-api/src/WebApi/WebApi.csproj
|
||||
```
|
||||
|
||||
#### Wallett SPA
|
||||
|
||||
```sh
|
||||
pushd wallet-spa/src/ClientApp
|
||||
npm install
|
||||
popd
|
||||
dotnet run --project src/WebApi/WebApi.csproj --launch-profile Development
|
||||
dotnet run --project wallet-spa/src/WalletSPA.csproj --launch-profile WalletSPA
|
||||
```
|
||||
|
||||
Then authenticate into the API by browsing to `https://localhost:5001/api/v1/Login/Google?returnUrl=%2Fswagger%2Findex.html`.
|
||||
|
||||
- App: `http://localhost:5001`
|
||||
- Swagger: `http://localhost:5001/swagger/index.html`
|
||||
|
||||
or try the Docker approach:
|
||||
|
||||
```sh
|
||||
docker build -t my-app . -f src/WebApi/Dockerfile
|
||||
docker run -p 6001:80 my-app
|
||||
```
|
||||
|
||||
- App: `http://localhost:6001`
|
||||
- Swagger: `http://localhost:6001/swagger/index.html`
|
||||
|
||||
## Production Environment Setup
|
||||
|
||||
```sh
|
||||
dotnet ef migrations add "InitialCreate" -o "DataAccess/Migrations" --project src/Infrastructure --startup-project src/Infrastructure
|
||||
```
|
||||
|
||||
```sh
|
||||
dotnet ef database update --project src/Infrastructure --startup-project src/Infrastructure
|
||||
```
|
||||
</details>
|
||||
|
||||
## Motivation
|
||||
|
||||
|
74
accounts-api/.vscode/launch.json
vendored
Normal file
74
accounts-api/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch IdentityServer",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "https://localhost:5000",
|
||||
"webRoot": "${workspaceFolder}/src/IdentityServer"
|
||||
},
|
||||
{
|
||||
"name": "Launch localhost",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "https://localhost:5005",
|
||||
"webRoot": "${workspaceFolder}/src/WebApi/ClientApp"
|
||||
},
|
||||
{
|
||||
"name": "Attach to url with files served from ./src/WebApi/ClientApp",
|
||||
"type": "chrome",
|
||||
"request": "attach",
|
||||
"port": 9222,
|
||||
"url": "https://localhost:5005",
|
||||
"webRoot": "${workspaceFolder}/src/WebApi/ClientApp"
|
||||
},
|
||||
{
|
||||
"name": "Development",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/src/WebApi/bin/Debug/netcoreapp3.1/WebApi.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/WebApi",
|
||||
"stopAtEntry": false,
|
||||
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "^\\s*Now listening on:\\s+(https?://\\S+)"
|
||||
},
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"sourceFileMap": {
|
||||
"/Views": "${workspaceFolder}/Views"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Production",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/src/WebApi/bin/Debug/netcoreapp3.1/WebApi.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/WebApi",
|
||||
"stopAtEntry": false,
|
||||
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "^\\s*Now listening on:\\s+(https?://\\S+)"
|
||||
},
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Production"
|
||||
},
|
||||
"sourceFileMap": {
|
||||
"/Views": "${workspaceFolder}/Views"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
0
scripts/build.sh → accounts-api/scripts/build.sh
Executable file → Normal file
0
scripts/build.sh → accounts-api/scripts/build.sh
Executable file → Normal file
0
scripts/download-tools.sh → accounts-api/scripts/download-tools.sh
Executable file → Normal file
0
scripts/download-tools.sh → accounts-api/scripts/download-tools.sh
Executable file → Normal file
0
scripts/format.sh → accounts-api/scripts/format.sh
Executable file → Normal file
0
scripts/format.sh → accounts-api/scripts/format.sh
Executable file → Normal file
20
accounts-api/scripts/sql-docker-up-windows.sh
Normal file
20
accounts-api/scripts/sql-docker-up-windows.sh
Normal file
@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
docker stop sql1
|
||||
docker rm sql1
|
||||
docker pull mcr.microsoft.com/mssql/server:2017-latest
|
||||
docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=<YourStrong!Passw0rd>' -p 1433:1433 --name sql1 -d mcr.microsoft.com/mssql/server:2017-latest
|
||||
sleep 10
|
||||
docker exec -it sql1 /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P '<YourStrong!Passw0rd>' -Q 'ALTER LOGIN SA WITH PASSWORD="<YourNewStrong!Passw0rd>"'
|
||||
dotnet tool install --global dotnet-ef
|
||||
dotnet ef database update --project src/Infrastructure --startup-project src/Infrastructure
|
||||
|
||||
# query
|
||||
|
||||
# sudo docker exec -it sql1 "bash"
|
||||
# /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P '<YourNewStrong!Passw0rd>'
|
||||
# SELECT Name from sys.Databases
|
||||
# GO
|
||||
# USE MangaDB01
|
||||
# GO
|
||||
# SELECT * FROM Account
|
||||
# GO
|
17
accounts-api/scripts/sql-docker-up.sh
Normal file
17
accounts-api/scripts/sql-docker-up.sh
Normal file
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
sudo docker pull mcr.microsoft.com/mssql/server:2017-latest
|
||||
sudo docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=<YourStrong!Passw0rd>' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest
|
||||
sleep 10
|
||||
dotnet tool install --global dotnet-ef
|
||||
dotnet ef database update --project src/Infrastructure --startup-project src/Infrastructure
|
||||
|
||||
# query
|
||||
|
||||
# sudo docker exec -it sql1 "bash"
|
||||
# /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P '<YourNewStrong!Passw0rd>'
|
||||
# SELECT Name from sys.Databases
|
||||
# GO
|
||||
# USE MangaDB01
|
||||
# GO
|
||||
# SELECT * FROM Account
|
||||
# GO
|
46
accounts-api/src/Application/Application.csproj
Normal file
46
accounts-api/src/Application/Application.csproj
Normal file
@ -0,0 +1,46 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<NoWarn>$(NoWarn);CA1062;1591</NoWarn>
|
||||
<Nullable>enable</Nullable>
|
||||
<NullableReferenceTypes>true</NullableReferenceTypes>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Domain\Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeQuality.Analyzers" Version="3.3.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.12.0.21095">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SecurityCodeScan" Version="3.5.3" PrivateAssets="all" />
|
||||
<PackageReference Update="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.0-beta2.final">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Messages.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Messages.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Messages.Designer.cs">
|
||||
<DependentUpon>Messages.resx</DependentUpon>
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
</Project>
|
12
accounts-api/src/Application/Services/ICurrencyExchange.cs
Normal file
12
accounts-api/src/Application/Services/ICurrencyExchange.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace Application.Services
|
||||
{
|
||||
using System.Threading.Tasks;
|
||||
using Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
public interface ICurrencyExchange
|
||||
{
|
||||
Task<PositiveMoney> Convert(PositiveMoney originalAmount, Currency destinationCurrency);
|
||||
}
|
||||
}
|
18
accounts-api/src/Application/Services/IUserService.cs
Normal file
18
accounts-api/src/Application/Services/IUserService.cs
Normal file
@ -0,0 +1,18 @@
|
||||
// <copyright file="IUserService.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// User Service.
|
||||
/// </summary>
|
||||
public interface IUserService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the Current User Id.
|
||||
/// </summary>
|
||||
/// <returns>User.</returns>
|
||||
string GetCurrentUserId();
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
// <copyright file="CloseAccountInput.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.CloseAccount
|
||||
{
|
||||
using System;
|
||||
using Domain.ValueObjects;
|
||||
using Services;
|
||||
|
||||
/// <summary>
|
||||
/// Close Account Input Message.
|
||||
/// </summary>
|
||||
internal sealed class CloseAccountInput
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CloseAccountInput" /> class.
|
||||
/// </summary>
|
||||
/// <param name="accountId">Account Id.</param>
|
||||
internal CloseAccountInput(Guid accountId)
|
||||
{
|
||||
this.ModelState = new Notification();
|
||||
|
||||
if (accountId != Guid.Empty)
|
||||
{
|
||||
this.AccountId = new AccountId(accountId);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ModelState.Add(nameof(accountId), "AccountId is required.");
|
||||
}
|
||||
}
|
||||
|
||||
internal AccountId AccountId { get; }
|
||||
internal Notification ModelState { get; }
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
// <copyright file="CloseAccountUseCase.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.CloseAccount
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Domain;
|
||||
using Domain.ValueObjects;
|
||||
using Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class CloseAccountUseCase : ICloseAccountUseCase
|
||||
{
|
||||
private readonly IAccountRepository _accountRepository;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
private IOutputPort? _outputPort;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CloseAccountUseCase" /> class.
|
||||
/// </summary>
|
||||
/// <param name="accountRepository">Account Repository.</param>
|
||||
/// <param name="userService">User Service.</param>
|
||||
/// <param name="unitOfWork"></param>
|
||||
public CloseAccountUseCase(
|
||||
IAccountRepository accountRepository,
|
||||
IUserService userService,
|
||||
IUnitOfWork unitOfWork)
|
||||
{
|
||||
this._accountRepository = accountRepository;
|
||||
this._userService = userService;
|
||||
this._unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetOutputPort(IOutputPort outputPort) => this._outputPort = outputPort;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task Execute(Guid accountId)
|
||||
{
|
||||
var input = new CloseAccountInput(accountId);
|
||||
|
||||
if (input.ModelState.IsValid)
|
||||
{
|
||||
string externalUserId = this._userService
|
||||
.GetCurrentUserId();
|
||||
|
||||
return this.CloseAccountInternal(input.AccountId, externalUserId);
|
||||
}
|
||||
|
||||
this._outputPort?.Invalid(input.ModelState);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task CloseAccountInternal(AccountId accountId, string externalUserId)
|
||||
{
|
||||
IAccount account = await this._accountRepository
|
||||
.Find(accountId, externalUserId)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (account is Account closingAccount)
|
||||
{
|
||||
if (!closingAccount.IsClosingAllowed())
|
||||
{
|
||||
this._outputPort?.HasFunds();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.Close(closingAccount)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
this._outputPort?.Ok(closingAccount);
|
||||
return;
|
||||
}
|
||||
|
||||
this._outputPort?.NotFound();
|
||||
}
|
||||
|
||||
private async Task Close(Account closeAccount)
|
||||
{
|
||||
await this._accountRepository
|
||||
.Delete(closeAccount.AccountId)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await this._unitOfWork
|
||||
.Save()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
// <copyright file="ICloseAccountUseCase.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.CloseAccount
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Close Account
|
||||
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#use-case">
|
||||
/// Use
|
||||
/// Case Domain-Driven Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public interface ICloseAccountUseCase
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the use case.
|
||||
/// </summary>
|
||||
/// <param name="accountId">Account Id.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task Execute(Guid accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Output Port.
|
||||
/// </summary>
|
||||
/// <param name="outputPort">Output Port</param>
|
||||
void SetOutputPort(IOutputPort outputPort);
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
// <copyright file="IOutputPort.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.CloseAccount
|
||||
{
|
||||
using Domain;
|
||||
using Services;
|
||||
|
||||
/// <summary>
|
||||
/// Output Port.
|
||||
/// </summary>
|
||||
public interface IOutputPort
|
||||
{
|
||||
/// <summary>
|
||||
/// Invalid input.
|
||||
/// </summary>
|
||||
void Invalid(Notification notification);
|
||||
|
||||
/// <summary>
|
||||
/// Account closed successfully.
|
||||
/// </summary>
|
||||
void Ok(Account account);
|
||||
|
||||
/// <summary>
|
||||
/// Account not found.
|
||||
/// </summary>
|
||||
void NotFound();
|
||||
|
||||
/// <summary>
|
||||
/// Account has funds.
|
||||
/// </summary>
|
||||
void HasFunds();
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
// <copyright file="DepositInput.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.Deposit
|
||||
{
|
||||
using System;
|
||||
using Domain.ValueObjects;
|
||||
using Services;
|
||||
|
||||
/// <summary>
|
||||
/// Deposit Input Message.
|
||||
/// </summary>
|
||||
internal sealed class DepositInput
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DepositInput" /> class.
|
||||
/// </summary>
|
||||
/// <param name="accountId">AccountId.</param>
|
||||
/// <param name="amount">Positive amount to deposit.</param>
|
||||
/// <param name="currency">Currency from amount.</param>
|
||||
internal DepositInput(Guid accountId, decimal amount, string currency)
|
||||
{
|
||||
this.ModelState = new Notification();
|
||||
|
||||
if (accountId != Guid.Empty)
|
||||
{
|
||||
this.AccountId = new AccountId(accountId);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ModelState.Add(nameof(accountId), "AccountId is required.");
|
||||
}
|
||||
|
||||
if (currency == Currency.Dollar.Code ||
|
||||
currency == Currency.Euro.Code ||
|
||||
currency == Currency.BritishPound.Code ||
|
||||
currency == Currency.Canadian.Code ||
|
||||
currency == Currency.Real.Code ||
|
||||
currency == Currency.Krona.Code)
|
||||
{
|
||||
if (amount > 0)
|
||||
{
|
||||
this.Amount = new PositiveMoney(amount, new Currency(currency));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ModelState.Add(nameof(amount), "Amount should be positive.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ModelState.Add(nameof(currency), "Currency is required.");
|
||||
}
|
||||
}
|
||||
|
||||
internal AccountId AccountId { get; }
|
||||
internal PositiveMoney Amount { get; }
|
||||
internal Notification ModelState { get; }
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
// <copyright file="DepositUseCase.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.Deposit
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Domain;
|
||||
using Domain.Credits;
|
||||
using Domain.ValueObjects;
|
||||
using Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class DepositUseCase : IDepositUseCase
|
||||
{
|
||||
private readonly IAccountFactory _accountFactory;
|
||||
private readonly IAccountRepository _accountRepository;
|
||||
private readonly ICurrencyExchange _currencyExchange;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private IOutputPort? _outputPort;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DepositUseCase" /> class.
|
||||
/// </summary>
|
||||
/// <param name="accountRepository">Account Repository.</param>
|
||||
/// <param name="unitOfWork">Unit Of Work.</param>
|
||||
/// <param name="accountFactory"></param>
|
||||
/// <param name="currencyExchange"></param>
|
||||
public DepositUseCase(
|
||||
IAccountRepository accountRepository,
|
||||
IUnitOfWork unitOfWork,
|
||||
IAccountFactory accountFactory,
|
||||
ICurrencyExchange currencyExchange)
|
||||
{
|
||||
this._accountRepository = accountRepository;
|
||||
this._unitOfWork = unitOfWork;
|
||||
this._accountFactory = accountFactory;
|
||||
this._currencyExchange = currencyExchange;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetOutputPort(IOutputPort outputPort) => this._outputPort = outputPort;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task Execute(Guid accountId, decimal amount, string currency)
|
||||
{
|
||||
var input = new DepositInput(accountId, amount, currency);
|
||||
|
||||
if (input.ModelState.IsValid)
|
||||
{
|
||||
return this.DepositInternal(input.AccountId, input.Amount);
|
||||
}
|
||||
|
||||
this._outputPort?.Invalid(input.ModelState);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task DepositInternal(AccountId accountId, PositiveMoney amount)
|
||||
{
|
||||
IAccount account = await this._accountRepository
|
||||
.GetAccount(accountId)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (account is Account depositAccount)
|
||||
{
|
||||
PositiveMoney amountInAccountCurrency =
|
||||
await this._currencyExchange
|
||||
.Convert(amount, depositAccount.Currency)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Credit credit = this._accountFactory
|
||||
.NewCredit(depositAccount, amountInAccountCurrency, DateTime.Now);
|
||||
|
||||
await this.Deposit(depositAccount, credit)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
this._outputPort?.Ok(credit, depositAccount);
|
||||
return;
|
||||
}
|
||||
|
||||
this._outputPort?.NotFound();
|
||||
}
|
||||
|
||||
private async Task Deposit(Account account, Credit credit)
|
||||
{
|
||||
account.Deposit(credit);
|
||||
|
||||
await this._accountRepository
|
||||
.Update(account, credit)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await this._unitOfWork
|
||||
.Save()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
// <copyright file="IDepositUseCase.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.Deposit
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Deposit
|
||||
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#use-case">
|
||||
/// Use
|
||||
/// Case Domain-Driven Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public interface IDepositUseCase
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the Use Case.
|
||||
/// </summary>
|
||||
/// <param name="accountId">AccountId.</param>
|
||||
/// <param name="amount">Positive amount to deposit.</param>
|
||||
/// <param name="currency">Currency from amount.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task Execute(Guid accountId, decimal amount, string currency);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Output Port.
|
||||
/// </summary>
|
||||
/// <param name="outputPort">Output Port</param>
|
||||
void SetOutputPort(IOutputPort outputPort);
|
||||
}
|
||||
}
|
31
accounts-api/src/Application/UseCases/Deposit/IOutputPort.cs
Normal file
31
accounts-api/src/Application/UseCases/Deposit/IOutputPort.cs
Normal file
@ -0,0 +1,31 @@
|
||||
// <copyright file="IDepositOutputPort.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.Deposit
|
||||
{
|
||||
using Domain;
|
||||
using Domain.Credits;
|
||||
using Services;
|
||||
|
||||
/// <summary>
|
||||
/// Output Port.
|
||||
/// </summary>
|
||||
public interface IOutputPort
|
||||
{
|
||||
/// <summary>
|
||||
/// Invalid input.
|
||||
/// </summary>
|
||||
void Invalid(Notification notification);
|
||||
|
||||
/// <summary>
|
||||
/// Deposited.
|
||||
/// </summary>
|
||||
void Ok(Credit credit, Account account);
|
||||
|
||||
/// <summary>
|
||||
/// Not found.
|
||||
/// </summary>
|
||||
void NotFound();
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
// <copyright file="GetAccountInput.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.GetAccount
|
||||
{
|
||||
using System;
|
||||
using Domain.ValueObjects;
|
||||
using Services;
|
||||
|
||||
/// <summary>
|
||||
/// Get Account Details Input Message.
|
||||
/// </summary>
|
||||
internal sealed class GetAccountInput
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GetAccountInput" /> class.
|
||||
/// </summary>
|
||||
/// <param name="accountId">Account Id.</param>
|
||||
internal GetAccountInput(Guid accountId)
|
||||
{
|
||||
this.ModelState = new Notification();
|
||||
|
||||
if (accountId != Guid.Empty)
|
||||
{
|
||||
this.AccountId = new AccountId(accountId);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ModelState.Add(nameof(accountId), "AccountId is required.");
|
||||
}
|
||||
}
|
||||
|
||||
internal AccountId AccountId { get; }
|
||||
internal Notification ModelState { get; }
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
// <copyright file="GetAccountUseCase.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.GetAccount
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Domain;
|
||||
using Domain.ValueObjects;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class GetAccountUseCase : IGetAccountUseCase
|
||||
{
|
||||
private readonly IAccountRepository _accountRepository;
|
||||
private IOutputPort? _outputPort;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GetAccountUseCase" /> class.
|
||||
/// </summary>
|
||||
/// <param name="accountRepository">Account Repository.</param>
|
||||
public GetAccountUseCase(IAccountRepository accountRepository) => this._accountRepository = accountRepository;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetOutputPort(IOutputPort outputPort) => this._outputPort = outputPort;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task Execute(Guid accountId)
|
||||
{
|
||||
var input = new GetAccountInput(accountId);
|
||||
|
||||
if (input.ModelState.IsValid)
|
||||
{
|
||||
return this.GetAccountInternal(input.AccountId);
|
||||
}
|
||||
|
||||
this._outputPort?.Invalid(input.ModelState);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task GetAccountInternal(AccountId accountId)
|
||||
{
|
||||
IAccount account = await this._accountRepository
|
||||
.GetAccount(accountId)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (account is Account getAccount)
|
||||
{
|
||||
this._outputPort?.Ok(getAccount);
|
||||
return;
|
||||
}
|
||||
|
||||
this._outputPort?.NotFound();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
// <copyright file="IGetAccountUseCase.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.GetAccount
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Account
|
||||
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#use-case">
|
||||
/// Use
|
||||
/// Case Domain-Driven Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public interface IGetAccountUseCase
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the Use Case
|
||||
/// </summary>
|
||||
/// <param name="accountId">Account Id.</param>
|
||||
Task Execute(Guid accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the Use Case.
|
||||
/// </summary>
|
||||
/// <param name="outputPort"></param>
|
||||
void SetOutputPort(IOutputPort outputPort);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
// <copyright file="IGetAccountOutputPort.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.GetAccount
|
||||
{
|
||||
using Domain;
|
||||
using Services;
|
||||
|
||||
/// <summary>
|
||||
/// Output Port.
|
||||
/// </summary>
|
||||
public interface IOutputPort
|
||||
{
|
||||
/// <summary>
|
||||
/// Invalid input.
|
||||
/// </summary>
|
||||
void Invalid(Notification notification);
|
||||
|
||||
/// <summary>
|
||||
/// Account closed.
|
||||
/// </summary>
|
||||
void Ok(Account account);
|
||||
|
||||
/// <summary>
|
||||
/// Account closed.
|
||||
/// </summary>
|
||||
void NotFound();
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
// <copyright file="GetAccountsUseCase.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.GetAccounts
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Domain;
|
||||
using Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class GetAccountsUseCase : IGetAccountsUseCase
|
||||
{
|
||||
private readonly IAccountRepository _accountRepository;
|
||||
private readonly IUserService _userService;
|
||||
private IOutputPort? _outputPort;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GetAccountsUseCase" /> class.
|
||||
/// </summary>
|
||||
/// <param name="userService">User Service.</param>
|
||||
/// <param name="accountRepository">Customer Repository.</param>
|
||||
public GetAccountsUseCase(
|
||||
IUserService userService,
|
||||
IAccountRepository accountRepository)
|
||||
{
|
||||
this._userService = userService;
|
||||
this._accountRepository = accountRepository;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetOutputPort(IOutputPort outputPort) => this._outputPort = outputPort;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Execute()
|
||||
{
|
||||
string externalUserId = this._userService
|
||||
.GetCurrentUserId();
|
||||
|
||||
await this.GetAccountsInternal(externalUserId)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task GetAccountsInternal(string externalUserId)
|
||||
{
|
||||
IList<Account>? accounts = await this._accountRepository
|
||||
.GetAccounts(externalUserId)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
this._outputPort?.Ok(accounts);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
// <copyright file="IOutputPort.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.GetAccounts
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Output Port.
|
||||
/// </summary>
|
||||
public interface IOutputPort
|
||||
{
|
||||
/// <summary>
|
||||
/// Listed accounts.
|
||||
/// </summary>
|
||||
void Ok(IList<Account> accounts);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
// <copyright file="IOpenAccountUseCase.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.OpenAccount
|
||||
{
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Open Account
|
||||
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#use-case">
|
||||
/// Use
|
||||
/// Case Domain-Driven Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public interface IOpenAccountUseCase
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the Use Case
|
||||
/// </summary>
|
||||
Task Execute(decimal amount, string currency);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Output Port.
|
||||
/// </summary>
|
||||
/// <param name="outputPort">Output Port</param>
|
||||
void SetOutputPort(IOutputPort outputPort);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
// <copyright file="IOutputPort.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.OpenAccount
|
||||
{
|
||||
using Domain;
|
||||
using Services;
|
||||
|
||||
/// <summary>
|
||||
/// Open Account Output Port.
|
||||
/// </summary>
|
||||
public interface IOutputPort
|
||||
{
|
||||
/// <summary>
|
||||
/// Account open.
|
||||
/// </summary>
|
||||
void Ok(Account account);
|
||||
|
||||
/// <summary>
|
||||
/// Resource not found.
|
||||
/// </summary>
|
||||
void NotFound();
|
||||
|
||||
/// <summary>
|
||||
/// Invalid input.
|
||||
/// </summary>
|
||||
void Invalid(Notification notification);
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
// <copyright file="OpenAccountInput.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.OpenAccount
|
||||
{
|
||||
using Domain.ValueObjects;
|
||||
using Services;
|
||||
|
||||
/// <summary>
|
||||
/// Open Account Input Message.
|
||||
/// </summary>
|
||||
internal sealed class OpenAccountInput
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OpenAccountInput" /> class.
|
||||
/// </summary>
|
||||
public OpenAccountInput(decimal amount, string currency)
|
||||
{
|
||||
this.ModelState = new Notification();
|
||||
|
||||
if (currency == Currency.Dollar.Code ||
|
||||
currency == Currency.Euro.Code ||
|
||||
currency == Currency.BritishPound.Code ||
|
||||
currency == Currency.Canadian.Code ||
|
||||
currency == Currency.Real.Code ||
|
||||
currency == Currency.Krona.Code)
|
||||
{
|
||||
if (amount > 0)
|
||||
{
|
||||
this.Amount = new PositiveMoney(amount, new Currency(currency));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ModelState.Add(nameof(amount), "Amount should be positive.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ModelState.Add(nameof(currency), "Currency is required.");
|
||||
}
|
||||
}
|
||||
|
||||
internal PositiveMoney Amount { get; }
|
||||
internal Notification ModelState { get; }
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
// <copyright file="OpenAccountUseCase.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.OpenAccount
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Domain;
|
||||
using Domain.Credits;
|
||||
using Domain.ValueObjects;
|
||||
using Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class OpenAccountUseCase : IOpenAccountUseCase
|
||||
{
|
||||
private readonly IAccountFactory _accountFactory;
|
||||
private readonly IAccountRepository _accountRepository;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IUserService _userService;
|
||||
private IOutputPort? _outputPort;
|
||||
|
||||
public OpenAccountUseCase(
|
||||
IAccountRepository accountRepository,
|
||||
IUnitOfWork unitOfWork,
|
||||
IUserService userService,
|
||||
IAccountFactory accountFactory)
|
||||
{
|
||||
this._accountRepository = accountRepository;
|
||||
this._unitOfWork = unitOfWork;
|
||||
this._userService = userService;
|
||||
this._accountFactory = accountFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetOutputPort(IOutputPort outputPort) => this._outputPort = outputPort;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task Execute(decimal amount, string currency)
|
||||
{
|
||||
var input = new OpenAccountInput(amount, currency);
|
||||
|
||||
if (input.ModelState.IsValid)
|
||||
{
|
||||
return this.OpenAccountInternal(input.Amount);
|
||||
}
|
||||
|
||||
this._outputPort?.Invalid(input.ModelState);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task OpenAccountInternal(PositiveMoney amountToDeposit)
|
||||
{
|
||||
string externalUserId = this._userService
|
||||
.GetCurrentUserId();
|
||||
|
||||
Account account = this._accountFactory
|
||||
.NewAccount(externalUserId, amountToDeposit.Currency);
|
||||
|
||||
Credit credit = this._accountFactory
|
||||
.NewCredit(account, amountToDeposit, DateTime.Now);
|
||||
|
||||
await this.Deposit(account, credit)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
this._outputPort?.Ok(account);
|
||||
}
|
||||
|
||||
private async Task Deposit(Account account, Credit credit)
|
||||
{
|
||||
account.Deposit(credit);
|
||||
|
||||
await this._accountRepository
|
||||
.Add(account, credit)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await this._unitOfWork
|
||||
.Save()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
// <copyright file="IOutputPort.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.Transfer
|
||||
{
|
||||
using Domain;
|
||||
using Domain.Credits;
|
||||
using Domain.Debits;
|
||||
using Services;
|
||||
|
||||
/// <summary>
|
||||
/// Transfer Output Port.
|
||||
/// </summary>
|
||||
public interface IOutputPort
|
||||
{
|
||||
/// <summary>
|
||||
/// Invalid input.
|
||||
/// </summary>
|
||||
void Invalid(Notification notification);
|
||||
|
||||
/// <summary>
|
||||
/// Resource not found.
|
||||
/// </summary>
|
||||
void NotFound();
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="originAccount"></param>
|
||||
/// <param name="debit"></param>
|
||||
/// <param name="destinationAccount"></param>
|
||||
/// <param name="credit"></param>
|
||||
void Ok(Account originAccount, Debit debit, Account destinationAccount, Credit credit);
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
void OutOfFunds();
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
// <copyright file="ITransferUseCase.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.Transfer
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Transfer
|
||||
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#use-case">
|
||||
/// Use
|
||||
/// Case Domain-Driven Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public interface ITransferUseCase
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the use case.
|
||||
/// </summary>
|
||||
Task Execute(Guid originAccountId, Guid destinationAccountId, decimal amount, string currency);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Output Port.
|
||||
/// </summary>
|
||||
/// <param name="outputPort">Output Port</param>
|
||||
void SetOutputPort(IOutputPort outputPort);
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
// <copyright file="TransferInput.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.Transfer
|
||||
{
|
||||
using System;
|
||||
using Domain.ValueObjects;
|
||||
using Services;
|
||||
|
||||
/// <summary>
|
||||
/// Transfer Input Message.
|
||||
/// </summary>
|
||||
internal sealed class TransferInput
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TransferInput" /> class.
|
||||
/// </summary>
|
||||
internal TransferInput(Guid originAccountId, Guid destinationAccountId, decimal amount, string currency)
|
||||
{
|
||||
this.ModelState = new Notification();
|
||||
|
||||
if (originAccountId != Guid.Empty)
|
||||
{
|
||||
this.OriginAccountId = new AccountId(originAccountId);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ModelState.Add(nameof(originAccountId), "Origin AccountId is required.");
|
||||
}
|
||||
|
||||
if (destinationAccountId != Guid.Empty)
|
||||
{
|
||||
this.DestinationAccountId = new AccountId(destinationAccountId);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ModelState.Add(nameof(destinationAccountId), "Destination AccountId is required.");
|
||||
}
|
||||
|
||||
if (currency == Currency.Dollar.Code ||
|
||||
currency == Currency.Euro.Code ||
|
||||
currency == Currency.BritishPound.Code ||
|
||||
currency == Currency.Canadian.Code ||
|
||||
currency == Currency.Real.Code ||
|
||||
currency == Currency.Krona.Code)
|
||||
{
|
||||
if (amount > 0)
|
||||
{
|
||||
this.TransferAmount = new PositiveMoney(amount, new Currency(currency));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ModelState.Add(nameof(amount), "Amount should be positive.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ModelState.Add(nameof(currency), "Currency is required.");
|
||||
}
|
||||
}
|
||||
|
||||
internal AccountId OriginAccountId { get; }
|
||||
internal AccountId DestinationAccountId { get; }
|
||||
internal PositiveMoney TransferAmount { get; }
|
||||
internal Notification ModelState { get; }
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
// <copyright file="TransferUseCase.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.Transfer
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Domain;
|
||||
using Domain.Credits;
|
||||
using Domain.Debits;
|
||||
using Domain.ValueObjects;
|
||||
using Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class TransferUseCase : ITransferUseCase
|
||||
{
|
||||
private readonly IAccountFactory _accountFactory;
|
||||
private readonly IAccountRepository _accountRepository;
|
||||
private readonly ICurrencyExchange _currencyExchange;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private IOutputPort? _outputPort;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TransferUseCase" /> class.
|
||||
/// </summary>
|
||||
/// <param name="accountRepository">Account Repository.</param>
|
||||
/// <param name="unitOfWork">Unit Of Work.</param>
|
||||
/// <param name="accountFactory"></param>
|
||||
/// <param name="currencyExchange"></param>
|
||||
public TransferUseCase(
|
||||
IAccountRepository accountRepository,
|
||||
IUnitOfWork unitOfWork,
|
||||
IAccountFactory accountFactory,
|
||||
ICurrencyExchange currencyExchange)
|
||||
{
|
||||
this._accountRepository = accountRepository;
|
||||
this._unitOfWork = unitOfWork;
|
||||
this._accountFactory = accountFactory;
|
||||
this._currencyExchange = currencyExchange;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetOutputPort(IOutputPort outputPort) => this._outputPort = outputPort;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task Execute(Guid originAccountId, Guid destinationAccountId, decimal amount, string currency)
|
||||
{
|
||||
var input = new TransferInput(
|
||||
originAccountId,
|
||||
destinationAccountId,
|
||||
amount,
|
||||
currency);
|
||||
|
||||
if (input.ModelState.IsValid)
|
||||
{
|
||||
return this.TransferInternal(input.OriginAccountId, input.DestinationAccountId, input.TransferAmount);
|
||||
}
|
||||
|
||||
this._outputPort?.Invalid(input.ModelState);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task TransferInternal(AccountId originAccountId, AccountId destinationAccountId,
|
||||
PositiveMoney transferAmount)
|
||||
{
|
||||
IAccount originAccount = await this._accountRepository
|
||||
.GetAccount(originAccountId)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
IAccount destinationAccount = await this._accountRepository
|
||||
.GetAccount(destinationAccountId)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (originAccount is Account withdrawAccount && destinationAccount is Account depositAccount)
|
||||
{
|
||||
PositiveMoney localCurrencyAmount =
|
||||
await this._currencyExchange
|
||||
.Convert(transferAmount, withdrawAccount.Currency)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Debit debit = this._accountFactory
|
||||
.NewDebit(withdrawAccount, localCurrencyAmount, DateTime.Now);
|
||||
|
||||
if (withdrawAccount.GetCurrentBalance().Amount - debit.Amount.Amount < 0)
|
||||
{
|
||||
this._outputPort?.OutOfFunds();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.Withdraw(withdrawAccount, debit)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
PositiveMoney destinationCurrencyAmount =
|
||||
await this._currencyExchange
|
||||
.Convert(transferAmount, depositAccount.Currency)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Credit credit = this._accountFactory
|
||||
.NewCredit(depositAccount, destinationCurrencyAmount, DateTime.Now);
|
||||
|
||||
await this.Deposit(depositAccount, credit)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
this._outputPort?.Ok(withdrawAccount, debit, depositAccount, credit);
|
||||
return;
|
||||
}
|
||||
|
||||
this._outputPort?.NotFound();
|
||||
}
|
||||
|
||||
private async Task Deposit(Account account, Credit credit)
|
||||
{
|
||||
account.Deposit(credit);
|
||||
|
||||
await this._accountRepository
|
||||
.Update(account, credit)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await this._unitOfWork
|
||||
.Save()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task Withdraw(Account account, Debit debit)
|
||||
{
|
||||
account.Withdraw(debit);
|
||||
|
||||
await this._accountRepository
|
||||
.Update(account, debit)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await this._unitOfWork
|
||||
.Save()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
// <copyright file="IOutputPort.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.Withdraw
|
||||
{
|
||||
using Domain;
|
||||
using Domain.Debits;
|
||||
using Services;
|
||||
|
||||
/// <summary>
|
||||
/// Output Port.
|
||||
/// </summary>
|
||||
public interface IOutputPort
|
||||
{
|
||||
/// <summary>
|
||||
/// Informs it is out of balance.
|
||||
/// </summary>
|
||||
void OutOfFunds();
|
||||
|
||||
/// <summary>
|
||||
/// Invalid input.
|
||||
/// </summary>
|
||||
void Invalid(Notification notification);
|
||||
|
||||
/// <summary>
|
||||
/// Resource not closed.
|
||||
/// </summary>
|
||||
void NotFound();
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="debit"></param>
|
||||
/// <param name="account"></param>
|
||||
void Ok(Debit debit, Account account);
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
// <copyright file="IWithdrawUseCase.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.Withdraw
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Withdraw
|
||||
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#use-case">
|
||||
/// Use
|
||||
/// Case Domain-Driven Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public interface IWithdrawUseCase
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the use case.
|
||||
/// </summary>
|
||||
/// <param name="accountId">AccountId.</param>
|
||||
/// <param name="amount">Positive amount to withdraw.</param>
|
||||
/// <param name="currency">Currency from amount.</param>
|
||||
Task Execute(Guid accountId, decimal amount, string currency);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Output Port.
|
||||
/// </summary>
|
||||
/// <param name="outputPort">Output Port</param>
|
||||
void SetOutputPort(IOutputPort outputPort);
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
// <copyright file="WithdrawInput.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.Withdraw
|
||||
{
|
||||
using System;
|
||||
using Domain.ValueObjects;
|
||||
using Services;
|
||||
|
||||
/// <summary>
|
||||
/// Withdraw Input Message.
|
||||
/// </summary>
|
||||
internal sealed class WithdrawInput
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WithdrawInput" /> class.
|
||||
/// </summary>
|
||||
internal WithdrawInput(Guid accountId, decimal amount, string currency)
|
||||
{
|
||||
this.ModelState = new Notification();
|
||||
|
||||
if (accountId != Guid.Empty)
|
||||
{
|
||||
this.AccountId = new AccountId(accountId);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ModelState.Add(nameof(accountId), "AccountId is required.");
|
||||
}
|
||||
|
||||
if (currency == Currency.Dollar.Code ||
|
||||
currency == Currency.Euro.Code ||
|
||||
currency == Currency.BritishPound.Code ||
|
||||
currency == Currency.Canadian.Code ||
|
||||
currency == Currency.Real.Code ||
|
||||
currency == Currency.Krona.Code)
|
||||
{
|
||||
if (amount > 0)
|
||||
{
|
||||
this.Amount = new PositiveMoney(amount, new Currency(currency));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ModelState.Add(nameof(amount), "Amount should be positive.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ModelState.Add(nameof(currency), "Currency is required.");
|
||||
}
|
||||
}
|
||||
|
||||
internal AccountId AccountId { get; }
|
||||
internal PositiveMoney Amount { get; }
|
||||
internal Notification ModelState { get; }
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
// <copyright file="WithdrawUseCase.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Application.UseCases.Withdraw
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Domain;
|
||||
using Domain.Debits;
|
||||
using Domain.ValueObjects;
|
||||
using Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class WithdrawUseCase : IWithdrawUseCase
|
||||
{
|
||||
private readonly IAccountFactory _accountFactory;
|
||||
private readonly IAccountRepository _accountRepository;
|
||||
private readonly ICurrencyExchange _currencyExchange;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IUserService _userService;
|
||||
private IOutputPort? _outputPort;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WithdrawUseCase" /> class.
|
||||
/// </summary>
|
||||
/// <param name="accountRepository">Account Repository.</param>
|
||||
/// <param name="unitOfWork">Unit Of Work.</param>
|
||||
/// <param name="accountFactory"></param>
|
||||
/// <param name="userService"></param>
|
||||
/// <param name="currencyExchange"></param>
|
||||
public WithdrawUseCase(
|
||||
IAccountRepository accountRepository,
|
||||
IUnitOfWork unitOfWork,
|
||||
IAccountFactory accountFactory,
|
||||
IUserService userService,
|
||||
ICurrencyExchange currencyExchange)
|
||||
{
|
||||
this._accountRepository = accountRepository;
|
||||
this._unitOfWork = unitOfWork;
|
||||
this._accountFactory = accountFactory;
|
||||
this._userService = userService;
|
||||
this._currencyExchange = currencyExchange;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetOutputPort(IOutputPort outputPort) => this._outputPort = outputPort;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task Execute(Guid accountId, decimal amount, string currency)
|
||||
{
|
||||
var input = new WithdrawInput(accountId, amount, currency);
|
||||
|
||||
if (input.ModelState.IsValid)
|
||||
{
|
||||
return this.WithdrawInternal(input.AccountId, input.Amount);
|
||||
}
|
||||
|
||||
this._outputPort?.Invalid(input.ModelState);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task WithdrawInternal(AccountId accountId, PositiveMoney withdrawAmount)
|
||||
{
|
||||
string externalUserId = this._userService
|
||||
.GetCurrentUserId();
|
||||
|
||||
IAccount account = await this._accountRepository
|
||||
.Find(accountId, externalUserId)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (account is Account withdrawAccount)
|
||||
{
|
||||
PositiveMoney localCurrencyAmount =
|
||||
await this._currencyExchange
|
||||
.Convert(withdrawAmount, withdrawAccount.Currency)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Debit debit = this._accountFactory
|
||||
.NewDebit(withdrawAccount, localCurrencyAmount, DateTime.Now);
|
||||
|
||||
if (withdrawAccount.GetCurrentBalance().Amount - debit.Amount.Amount < 0)
|
||||
{
|
||||
this._outputPort?.OutOfFunds();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.Withdraw(withdrawAccount, debit)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
this._outputPort?.Ok(debit, withdrawAccount);
|
||||
return;
|
||||
}
|
||||
|
||||
this._outputPort?.NotFound();
|
||||
}
|
||||
|
||||
private async Task Withdraw(Account account, Debit debit)
|
||||
{
|
||||
account.Withdraw(debit);
|
||||
|
||||
await this._accountRepository
|
||||
.Update(account, debit)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await this._unitOfWork
|
||||
.Save()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
69
accounts-api/src/Domain/Account.cs
Normal file
69
accounts-api/src/Domain/Account.cs
Normal file
@ -0,0 +1,69 @@
|
||||
// <copyright file="Account.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Domain
|
||||
{
|
||||
using Credits;
|
||||
using Debits;
|
||||
using ValueObjects;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class Account : IAccount
|
||||
{
|
||||
public Account(AccountId accountId, string externalUserId, Currency currency)
|
||||
{
|
||||
this.AccountId = accountId;
|
||||
this.Currency = currency;
|
||||
this.ExternalUserId = externalUserId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ExternalUserId.
|
||||
/// </summary>
|
||||
public string ExternalUserId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Credits List.
|
||||
/// </summary>
|
||||
public CreditsCollection CreditsCollection { get; } = new CreditsCollection();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Debits List.
|
||||
/// </summary>
|
||||
public DebitsCollection DebitsCollection { get; } = new DebitsCollection();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Currency.
|
||||
/// </summary>
|
||||
public Currency Currency { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public AccountId AccountId { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Deposit(Credit credit) => this.CreditsCollection.Add(credit);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Withdraw(Debit debit) => this.DebitsCollection.Add(debit);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsClosingAllowed() => this.GetCurrentBalance()
|
||||
.IsZero();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Money GetCurrentBalance()
|
||||
{
|
||||
PositiveMoney totalCredits = this.CreditsCollection
|
||||
.GetTotal();
|
||||
|
||||
PositiveMoney totalDebits = this.DebitsCollection
|
||||
.GetTotal();
|
||||
|
||||
Money totalAmount = totalCredits
|
||||
.Subtract(totalDebits);
|
||||
|
||||
return totalAmount;
|
||||
}
|
||||
}
|
||||
}
|
38
accounts-api/src/Domain/AccountNull.cs
Normal file
38
accounts-api/src/Domain/AccountNull.cs
Normal file
@ -0,0 +1,38 @@
|
||||
// <copyright file="AccountNull.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Domain
|
||||
{
|
||||
using System;
|
||||
using Credits;
|
||||
using Debits;
|
||||
using ValueObjects;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class AccountNull : IAccount
|
||||
{
|
||||
public static AccountNull Instance { get; } = new AccountNull();
|
||||
|
||||
/// <inheritdoc />
|
||||
public AccountId AccountId => new AccountId(Guid.Empty);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Deposit(Credit credit)
|
||||
{
|
||||
// Null Pattern
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Withdraw(Debit debit)
|
||||
{
|
||||
// Null Pattern
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsClosingAllowed() => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Money GetCurrentBalance() => new Money(0, new Currency());
|
||||
}
|
||||
}
|
66
accounts-api/src/Domain/Credits/Credit.cs
Normal file
66
accounts-api/src/Domain/Credits/Credit.cs
Normal file
@ -0,0 +1,66 @@
|
||||
// <copyright file="Credit.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Domain.Credits
|
||||
{
|
||||
using System;
|
||||
using ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Credit
|
||||
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#entity">
|
||||
/// Entity
|
||||
/// Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public class Credit : ICredit
|
||||
{
|
||||
public Credit(CreditId creditId, AccountId accountId, DateTime transactionDate, decimal value, string currency)
|
||||
{
|
||||
this.CreditId = creditId;
|
||||
this.AccountId = accountId;
|
||||
this.TransactionDate = transactionDate;
|
||||
this.Amount = new PositiveMoney(value, new Currency(currency));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets Description.
|
||||
/// </summary>
|
||||
public static string Description => "Credit";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets Transaction Date.
|
||||
/// </summary>
|
||||
public DateTime TransactionDate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets AccountId.
|
||||
/// </summary>
|
||||
public AccountId AccountId { get; }
|
||||
|
||||
public Account? Account { get; set; }
|
||||
|
||||
public decimal Value => this.Amount.Amount;
|
||||
|
||||
public string Currency => this.Amount.Currency.Code;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets Id.
|
||||
/// </summary>
|
||||
public CreditId CreditId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets Amount.
|
||||
/// </summary>
|
||||
public PositiveMoney Amount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the sum of positive amounts.
|
||||
/// </summary>
|
||||
/// <param name="amount">Positive amount.</param>
|
||||
/// <returns>The positive sum.</returns>
|
||||
public Money Sum(Money amount) => this.Amount.Add(amount);
|
||||
}
|
||||
}
|
24
accounts-api/src/Domain/Credits/CreditNull.cs
Normal file
24
accounts-api/src/Domain/Credits/CreditNull.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// <copyright file="CreditNull.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Domain.Credits
|
||||
{
|
||||
using System;
|
||||
using ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Credit
|
||||
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#entity">
|
||||
/// Entity
|
||||
/// Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public sealed class CreditNull : ICredit
|
||||
{
|
||||
public static CreditNull Instance { get; } = new CreditNull();
|
||||
public CreditId CreditId { get; } = new CreditId(Guid.Empty);
|
||||
public PositiveMoney Amount { get; } = new PositiveMoney(0, new Currency(string.Empty));
|
||||
}
|
||||
}
|
38
accounts-api/src/Domain/Credits/CreditsCollection.cs
Normal file
38
accounts-api/src/Domain/Credits/CreditsCollection.cs
Normal file
@ -0,0 +1,38 @@
|
||||
// <copyright file="CreditsCollection.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Domain.Credits
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Credits
|
||||
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Design-Patterns#first-class-collections">
|
||||
/// First-Class
|
||||
/// Collection Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public sealed class CreditsCollection : List<Credit>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets Total amount.
|
||||
/// </summary>
|
||||
/// <returns>Positive amount.</returns>
|
||||
public PositiveMoney GetTotal()
|
||||
{
|
||||
if (this.Count == 0)
|
||||
{
|
||||
return new PositiveMoney(0, new Currency(string.Empty));
|
||||
}
|
||||
|
||||
PositiveMoney total = new PositiveMoney(0, this.First().Amount.Currency);
|
||||
|
||||
return this.Aggregate(total, (current, credit) =>
|
||||
new PositiveMoney(current.Amount + credit.Amount.Amount, current.Currency));
|
||||
}
|
||||
}
|
||||
}
|
21
accounts-api/src/Domain/Credits/ICredit.cs
Normal file
21
accounts-api/src/Domain/Credits/ICredit.cs
Normal file
@ -0,0 +1,21 @@
|
||||
// <copyright file="ICredit.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Domain.Credits
|
||||
{
|
||||
using ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Credit Entity Interface.
|
||||
/// </summary>
|
||||
public interface ICredit
|
||||
{
|
||||
CreditId CreditId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Amount.
|
||||
/// </summary>
|
||||
PositiveMoney Amount { get; }
|
||||
}
|
||||
}
|
66
accounts-api/src/Domain/Debits/Debit.cs
Normal file
66
accounts-api/src/Domain/Debits/Debit.cs
Normal file
@ -0,0 +1,66 @@
|
||||
// <copyright file="Debit.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Domain.Debits
|
||||
{
|
||||
using System;
|
||||
using ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Debit
|
||||
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#entity">
|
||||
/// Entity
|
||||
/// Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public class Debit : IDebit
|
||||
{
|
||||
public Debit(DebitId DebitId, AccountId accountId, DateTime transactionDate, decimal value, string currency)
|
||||
{
|
||||
this.DebitId = DebitId;
|
||||
this.AccountId = accountId;
|
||||
this.TransactionDate = transactionDate;
|
||||
this.Amount = new PositiveMoney(value, new Currency(currency));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets Description.
|
||||
/// </summary>
|
||||
public static string Description => "Debit";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets Transaction Date.
|
||||
/// </summary>
|
||||
public DateTime TransactionDate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the AccountId.
|
||||
/// </summary>
|
||||
public AccountId AccountId { get; }
|
||||
|
||||
public Account? Account { get; set; }
|
||||
|
||||
public decimal Value => this.Amount.Amount;
|
||||
|
||||
public string Currency => this.Amount.Currency.Code;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets Id.
|
||||
/// </summary>
|
||||
public DebitId DebitId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets Amount.
|
||||
/// </summary>
|
||||
public PositiveMoney Amount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the sum of positive amounts.
|
||||
/// </summary>
|
||||
/// <param name="amount">Positive amount.</param>
|
||||
/// <returns>The positive sum.</returns>
|
||||
public Money Sum(Money amount) => this.Amount.Add(amount);
|
||||
}
|
||||
}
|
24
accounts-api/src/Domain/Debits/DebitNull.cs
Normal file
24
accounts-api/src/Domain/Debits/DebitNull.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// <copyright file="DebitNull.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Domain.Debits
|
||||
{
|
||||
using System;
|
||||
using ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Debit
|
||||
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#entity">
|
||||
/// Entity
|
||||
/// Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public sealed class DebitNull : IDebit
|
||||
{
|
||||
public static DebitNull Instance { get; } = new DebitNull();
|
||||
public DebitId DebitId { get; } = new DebitId(Guid.Empty);
|
||||
public PositiveMoney Amount { get; } = new PositiveMoney(0, new Currency(string.Empty));
|
||||
}
|
||||
}
|
38
accounts-api/src/Domain/Debits/DebitsCollection.cs
Normal file
38
accounts-api/src/Domain/Debits/DebitsCollection.cs
Normal file
@ -0,0 +1,38 @@
|
||||
// <copyright file="DebitsCollection.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Domain.Debits
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Debits
|
||||
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Design-Patterns#first-class-collections">
|
||||
/// First-Class
|
||||
/// Collection Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public sealed class DebitsCollection : List<Debit>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets Total amount.
|
||||
/// </summary>
|
||||
/// <returns>Total.</returns>
|
||||
public PositiveMoney GetTotal()
|
||||
{
|
||||
if (this.Count == 0)
|
||||
{
|
||||
return new PositiveMoney(0, new Currency(string.Empty));
|
||||
}
|
||||
|
||||
PositiveMoney total = new PositiveMoney(0, this.First().Amount.Currency);
|
||||
|
||||
return this.Aggregate(total, (current, credit) =>
|
||||
new PositiveMoney(current.Amount + credit.Amount.Amount, current.Currency));
|
||||
}
|
||||
}
|
||||
}
|
21
accounts-api/src/Domain/Debits/IDebit.cs
Normal file
21
accounts-api/src/Domain/Debits/IDebit.cs
Normal file
@ -0,0 +1,21 @@
|
||||
// <copyright file="IDebit.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Domain.Debits
|
||||
{
|
||||
using ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Debit.
|
||||
/// </summary>
|
||||
public interface IDebit
|
||||
{
|
||||
DebitId DebitId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Amount.
|
||||
/// </summary>
|
||||
PositiveMoney Amount { get; }
|
||||
}
|
||||
}
|
44
accounts-api/src/Domain/Domain.csproj
Normal file
44
accounts-api/src/Domain/Domain.csproj
Normal file
@ -0,0 +1,44 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<NoWarn>$(NoWarn);CA1062;1591</NoWarn>
|
||||
<Nullable>enable</Nullable>
|
||||
<NullableReferenceTypes>true</NullableReferenceTypes>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeQuality.Analyzers" Version="3.3.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.12.0.21095">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SecurityCodeScan" Version="3.5.3" PrivateAssets="all" />
|
||||
<PackageReference Update="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.0-beta2.final">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Messages.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Messages.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Messages.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Messages.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
51
accounts-api/src/Domain/IAccount.cs
Normal file
51
accounts-api/src/Domain/IAccount.cs
Normal file
@ -0,0 +1,51 @@
|
||||
// <copyright file="IAccount.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Domain
|
||||
{
|
||||
using Credits;
|
||||
using Debits;
|
||||
using ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Account
|
||||
/// <see
|
||||
/// href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#aggregate-root">
|
||||
/// Aggregate
|
||||
/// Root Domain-Driven Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public interface IAccount
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets Id.
|
||||
/// </summary>
|
||||
AccountId AccountId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Deposits into account.
|
||||
/// </summary>
|
||||
/// <returns>Credit created.</returns>
|
||||
void Deposit(Credit credit);
|
||||
|
||||
/// <summary>
|
||||
/// Withdraws from account.
|
||||
/// </summary>
|
||||
/// <returns>Debit created.</returns>
|
||||
void Withdraw(Debit debit);
|
||||
|
||||
/// <summary>
|
||||
/// Check if closing account is allowed.
|
||||
/// </summary>
|
||||
/// <returns>True if is allowed.</returns>
|
||||
bool IsClosingAllowed();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current balance considering credits and debits totals.
|
||||
/// </summary>
|
||||
/// <returns>The current balance.</returns>
|
||||
Money GetCurrentBalance();
|
||||
}
|
||||
}
|
49
accounts-api/src/Domain/IAccountFactory.cs
Normal file
49
accounts-api/src/Domain/IAccountFactory.cs
Normal file
@ -0,0 +1,49 @@
|
||||
// <copyright file="IAccountFactory.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Domain
|
||||
{
|
||||
using System;
|
||||
using Credits;
|
||||
using Debits;
|
||||
using ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Account
|
||||
/// <see
|
||||
/// href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#entity-factory">
|
||||
/// Entity
|
||||
/// Factory Domain-Driven Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public interface IAccountFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new Account.
|
||||
/// </summary>
|
||||
/// <param name="externalUserId">ExternalUserId.</param>
|
||||
/// <param name="currency">Currency</param>
|
||||
/// <returns>New Account instance.</returns>
|
||||
Account NewAccount(string externalUserId, Currency currency);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Credit.
|
||||
/// </summary>
|
||||
/// <param name="account">Account object.</param>
|
||||
/// <param name="amountToDeposit">Amount to Deposit.</param>
|
||||
/// <param name="transactionDate">Transaction date.</param>
|
||||
/// <returns>New Credit instance.</returns>
|
||||
Credit NewCredit(Account account, PositiveMoney amountToDeposit, DateTime transactionDate);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Debit.
|
||||
/// </summary>
|
||||
/// <param name="account">Account object.</param>
|
||||
/// <param name="amountToWithdraw">Amount to Withdraw.</param>
|
||||
/// <param name="transactionDate">Transaction date.</param>
|
||||
/// <returns>New Debit instance.</returns>
|
||||
Debit NewDebit(Account account, PositiveMoney amountToWithdraw, DateTime transactionDate);
|
||||
}
|
||||
}
|
74
accounts-api/src/Domain/IAccountRepository.cs
Normal file
74
accounts-api/src/Domain/IAccountRepository.cs
Normal file
@ -0,0 +1,74 @@
|
||||
// <copyright file="IAccountRepository.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Domain
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Credits;
|
||||
using Debits;
|
||||
using ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Account
|
||||
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#repository">
|
||||
/// Repository
|
||||
/// Domain-Driven Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public interface IAccountRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="accountId"></param>
|
||||
/// <returns></returns>
|
||||
Task<IAccount> GetAccount(AccountId accountId);
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="externalUserId"></param>
|
||||
/// <returns></returns>
|
||||
Task<IList<Account>> GetAccounts(string externalUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an Account.
|
||||
/// </summary>
|
||||
/// <param name="account">Account object.</param>
|
||||
/// <param name="credit">Credit object.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task Add(Account account, Credit credit);
|
||||
|
||||
/// <summary>
|
||||
/// Updates an Account.
|
||||
/// </summary>
|
||||
/// <param name="account">Account object.</param>
|
||||
/// <param name="credit">Credit object.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task Update(Account account, Credit credit);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the Account.
|
||||
/// </summary>
|
||||
/// <param name="account">Account object.</param>
|
||||
/// <param name="debit">Debit object.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task Update(Account account, Debit debit);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the Account.
|
||||
/// </summary>
|
||||
/// <param name="accountId">Account Id.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task Delete(AccountId accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Finds an Account.
|
||||
/// </summary>
|
||||
/// <param name="accountId">Account Id.</param>
|
||||
/// <param name="externalUserId">External User Id.</param>
|
||||
/// <returns></returns>
|
||||
Task<IAccount> Find(AccountId accountId, string externalUserId);
|
||||
}
|
||||
}
|
72
accounts-api/src/Domain/Messages.Designer.cs
generated
Normal file
72
accounts-api/src/Domain/Messages.Designer.cs
generated
Normal file
@ -0,0 +1,72 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Domain {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Messages {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Messages() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Domain.Messages", typeof(Messages).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Account has not enough funds..
|
||||
/// </summary>
|
||||
internal static string AccountHasNotEnoughFunds {
|
||||
get {
|
||||
return ResourceManager.GetString("AccountHasNotEnoughFunds", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
38
accounts-api/src/Domain/ValueObjects/AccountId.cs
Normal file
38
accounts-api/src/Domain/ValueObjects/AccountId.cs
Normal file
@ -0,0 +1,38 @@
|
||||
// <copyright file="AccountId.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Domain.ValueObjects
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// AccountId
|
||||
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#entity">
|
||||
/// Entity
|
||||
/// Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public readonly struct AccountId : IEquatable<AccountId>
|
||||
{
|
||||
public Guid Id { get; }
|
||||
|
||||
public AccountId(Guid id) =>
|
||||
this.Id = id;
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is AccountId o && this.Equals(o);
|
||||
|
||||
public bool Equals(AccountId other) => this.Id == other.Id;
|
||||
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(this.Id);
|
||||
|
||||
public static bool operator ==(AccountId left, AccountId right) => left.Equals(right);
|
||||
|
||||
public static bool operator !=(AccountId left, AccountId right) => !(left == right);
|
||||
|
||||
public override string ToString() => this.Id.ToString();
|
||||
}
|
||||
}
|
39
accounts-api/src/Domain/ValueObjects/CreditId.cs
Normal file
39
accounts-api/src/Domain/ValueObjects/CreditId.cs
Normal file
@ -0,0 +1,39 @@
|
||||
// <copyright file="CreditId.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Domain.ValueObjects
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// CreditId
|
||||
/// <see
|
||||
/// href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#value-object">
|
||||
/// Value
|
||||
/// Object Domain-Driven Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public readonly struct CreditId : IEquatable<CreditId>
|
||||
{
|
||||
public Guid Id { get; }
|
||||
|
||||
public CreditId(Guid id) =>
|
||||
this.Id = id;
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is CreditId o && this.Equals(o);
|
||||
|
||||
public bool Equals(CreditId other) => this.Id == other.Id;
|
||||
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(this.Id);
|
||||
|
||||
public static bool operator ==(CreditId left, CreditId right) => left.Equals(right);
|
||||
|
||||
public static bool operator !=(CreditId left, CreditId right) => !(left == right);
|
||||
|
||||
public override string ToString() => this.Id.ToString();
|
||||
}
|
||||
}
|
71
accounts-api/src/Domain/ValueObjects/Currency.cs
Normal file
71
accounts-api/src/Domain/ValueObjects/Currency.cs
Normal file
@ -0,0 +1,71 @@
|
||||
namespace Domain.ValueObjects
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Currency
|
||||
/// <see
|
||||
/// href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#value-object">
|
||||
/// Value Object
|
||||
/// Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public readonly struct Currency : IEquatable<Currency>
|
||||
{
|
||||
public string Code { get; }
|
||||
|
||||
public Currency(string code) =>
|
||||
this.Code = code;
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is Currency o && this.Equals(o);
|
||||
|
||||
public bool Equals(Currency other) => this.Code == other.Code;
|
||||
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(this.Code);
|
||||
|
||||
public static bool operator ==(Currency left, Currency right) => left.Equals(right);
|
||||
|
||||
public static bool operator !=(Currency left, Currency right) => !(left == right);
|
||||
|
||||
/// <summary>
|
||||
/// Dollar.
|
||||
/// </summary>
|
||||
/// <returns>Currency.</returns>
|
||||
public static readonly Currency Dollar = new Currency("USD");
|
||||
|
||||
/// <summary>
|
||||
/// Euro.
|
||||
/// </summary>
|
||||
/// <returns>Currency.</returns>
|
||||
public static readonly Currency Euro = new Currency("EUR");
|
||||
|
||||
/// <summary>
|
||||
/// British Pound.
|
||||
/// </summary>
|
||||
/// <returns>Currency.</returns>
|
||||
public static readonly Currency BritishPound = new Currency("GBP");
|
||||
|
||||
/// <summary>
|
||||
/// Canadian Dollar.
|
||||
/// </summary>
|
||||
/// <returns>Currency.</returns>
|
||||
public static readonly Currency Canadian = new Currency("CAD");
|
||||
|
||||
/// <summary>
|
||||
/// Brazilian Real.
|
||||
/// </summary>
|
||||
/// <returns>Currency.</returns>
|
||||
public static readonly Currency Real = new Currency("BRL");
|
||||
|
||||
/// <summary>
|
||||
/// Swedish Krona.
|
||||
/// </summary>
|
||||
/// <returns>Currency.</returns>
|
||||
public static readonly Currency Krona = new Currency("SEK");
|
||||
|
||||
public override string ToString() => this.Code;
|
||||
}
|
||||
}
|
39
accounts-api/src/Domain/ValueObjects/DebitId.cs
Normal file
39
accounts-api/src/Domain/ValueObjects/DebitId.cs
Normal file
@ -0,0 +1,39 @@
|
||||
// <copyright file="DebitId.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Domain.ValueObjects
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Debit
|
||||
/// <see
|
||||
/// href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#value-object">
|
||||
/// Value
|
||||
/// Object Domain-Driven Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public readonly struct DebitId : IEquatable<DebitId>
|
||||
{
|
||||
public Guid Id { get; }
|
||||
|
||||
public DebitId(Guid id) =>
|
||||
this.Id = id;
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is DebitId o && this.Equals(o);
|
||||
|
||||
public bool Equals(DebitId other) => this.Id == other.Id;
|
||||
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(this.Id);
|
||||
|
||||
public static bool operator ==(DebitId left, DebitId right) => left.Equals(right);
|
||||
|
||||
public static bool operator !=(DebitId left, DebitId right) => !(left == right);
|
||||
|
||||
public override string ToString() => this.Id.ToString();
|
||||
}
|
||||
}
|
43
accounts-api/src/Domain/ValueObjects/Money.cs
Normal file
43
accounts-api/src/Domain/ValueObjects/Money.cs
Normal file
@ -0,0 +1,43 @@
|
||||
// <copyright file="Money.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Domain.ValueObjects
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Money
|
||||
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#entity">
|
||||
/// Entity
|
||||
/// Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public readonly struct Money : IEquatable<Money>
|
||||
{
|
||||
public decimal Amount { get; }
|
||||
public Currency Currency { get; }
|
||||
|
||||
public Money(decimal amount, Currency currency) =>
|
||||
(this.Amount, this.Currency) = (amount, currency);
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is Money o && this.Equals(o);
|
||||
|
||||
public bool Equals(Money other) =>
|
||||
this.Amount == other.Amount &&
|
||||
this.Currency == other.Currency;
|
||||
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(this.Amount, this.Currency);
|
||||
|
||||
public static bool operator ==(Money left, Money right) => left.Equals(right);
|
||||
|
||||
public static bool operator !=(Money left, Money right) => !(left == right);
|
||||
|
||||
public bool IsZero() => this.Amount == 0;
|
||||
|
||||
public override string ToString() => string.Format($"{this.Amount} {this.Currency}");
|
||||
}
|
||||
}
|
47
accounts-api/src/Domain/ValueObjects/PositiveMoney.cs
Normal file
47
accounts-api/src/Domain/ValueObjects/PositiveMoney.cs
Normal file
@ -0,0 +1,47 @@
|
||||
// <copyright file="PositiveMoney.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Domain.ValueObjects
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// PositiveMoney
|
||||
/// <see
|
||||
/// href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#value-object">
|
||||
/// Value Object
|
||||
/// Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public readonly struct PositiveMoney : IEquatable<PositiveMoney>
|
||||
{
|
||||
public decimal Amount { get; }
|
||||
public Currency Currency { get; }
|
||||
|
||||
public PositiveMoney(decimal amount, Currency currency) =>
|
||||
(this.Amount, this.Currency) = (amount, currency);
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is PositiveMoney o && this.Equals(o);
|
||||
|
||||
public bool Equals(PositiveMoney other) =>
|
||||
this.Amount == other.Amount &&
|
||||
this.Currency == other.Currency;
|
||||
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(this.Amount, this.Currency);
|
||||
|
||||
public static bool operator ==(PositiveMoney left, PositiveMoney right) => left.Equals(right);
|
||||
|
||||
public static bool operator !=(PositiveMoney left, PositiveMoney right) => !(left == right);
|
||||
|
||||
public Money Subtract(PositiveMoney totalDebits) =>
|
||||
new Money(Math.Round(this.Amount - totalDebits.Amount, 2), this.Currency);
|
||||
|
||||
public Money Add(Money amount) => new Money(Math.Round(this.Amount + amount.Amount, 2), this.Currency);
|
||||
|
||||
public override string ToString() => string.Format($"{this.Amount} {this.Currency}");
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
namespace Infrastructure.CurrencyExchange
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Application.Services;
|
||||
using Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Fake implementation of the Exchange Service using hardcoded rates
|
||||
/// </summary>
|
||||
public sealed class CurrencyExchangeFake : ICurrencyExchange
|
||||
{
|
||||
private readonly Dictionary<Currency, decimal> _usdRates = new Dictionary<Currency, decimal>
|
||||
{
|
||||
{Currency.Dollar, 1m},
|
||||
{Currency.Euro, 0.89021m},
|
||||
{Currency.Canadian, 1.35737m},
|
||||
{Currency.BritishPound, 0.80668m},
|
||||
{Currency.Krona, 9.31944m},
|
||||
{Currency.Real, 5.46346m}
|
||||
};
|
||||
|
||||
public Task<PositiveMoney> Convert(PositiveMoney originalAmount, Currency destinationCurrency)
|
||||
{
|
||||
// hardcoded rates from https://www.xe.com/currency/usd-us-dollar
|
||||
|
||||
decimal usdAmount = this._usdRates[originalAmount.Currency] / originalAmount.Amount;
|
||||
decimal destinationAmount = this._usdRates[destinationCurrency] / usdAmount;
|
||||
|
||||
return Task.FromResult(
|
||||
new PositiveMoney(
|
||||
destinationAmount,
|
||||
destinationCurrency));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
namespace Infrastructure.CurrencyExchange
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Application.Services;
|
||||
using Domain.ValueObjects;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
/// <summary>
|
||||
/// Real implementation of the Exchange Service using external data source
|
||||
/// </summary>
|
||||
public sealed class CurrencyExchangeService : ICurrencyExchange
|
||||
{
|
||||
public const string HttpClientName = "Fixer";
|
||||
|
||||
[SuppressMessage("Minor Code Smell", "S1075:URIs should not be hardcoded", Justification = "<Pending>")]
|
||||
private const string _exchangeUrl = "https://api.exchangeratesapi.io/latest?base=USD";
|
||||
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
private readonly Dictionary<Currency, decimal> _usdRates = new Dictionary<Currency, decimal>();
|
||||
|
||||
public CurrencyExchangeService(IHttpClientFactory httpClientFactory) =>
|
||||
this._httpClientFactory = httpClientFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Converts allowed currencies into USD.
|
||||
/// </summary>
|
||||
/// <returns>Money.</returns>
|
||||
public async Task<PositiveMoney> Convert(PositiveMoney originalAmount, Currency destinationCurrency)
|
||||
{
|
||||
HttpClient httpClient = this._httpClientFactory.CreateClient(HttpClientName);
|
||||
Uri requestUri = new Uri(_exchangeUrl);
|
||||
|
||||
HttpResponseMessage response = await httpClient.GetAsync(requestUri)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
string responseJson = await response
|
||||
.Content
|
||||
.ReadAsStringAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
this.ParseCurrencies(responseJson);
|
||||
|
||||
decimal usdAmount = this._usdRates[originalAmount.Currency] / originalAmount.Amount;
|
||||
decimal destinationAmount = this._usdRates[destinationCurrency] / usdAmount;
|
||||
|
||||
return new PositiveMoney(
|
||||
destinationAmount,
|
||||
destinationCurrency);
|
||||
}
|
||||
|
||||
private void ParseCurrencies(string responseJson)
|
||||
{
|
||||
var rates = JObject.Parse(responseJson);
|
||||
decimal eur = rates["rates"]![Currency.Euro.Code]!.Value<decimal>();
|
||||
decimal cad = rates["rates"]![Currency.Canadian.Code]!.Value<decimal>();
|
||||
decimal gbh = rates["rates"]![Currency.BritishPound.Code]!.Value<decimal>();
|
||||
decimal sek = rates["rates"]![Currency.Krona.Code]!.Value<decimal>();
|
||||
decimal brl = rates["rates"]![Currency.Real.Code]!.Value<decimal>();
|
||||
|
||||
this._usdRates.Add(Currency.Dollar, 1);
|
||||
this._usdRates.Add(Currency.Euro, eur);
|
||||
this._usdRates.Add(Currency.Canadian, cad);
|
||||
this._usdRates.Add(Currency.BritishPound, gbh);
|
||||
this._usdRates.Add(Currency.Krona, sek);
|
||||
this._usdRates.Add(Currency.Real, brl);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
// <copyright file="AccountConfiguration.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Infrastructure.DataAccess.Configuration
|
||||
{
|
||||
using System;
|
||||
using Domain;
|
||||
using Domain.ValueObjects;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
/// <summary>
|
||||
/// Account Configuration.
|
||||
/// </summary>
|
||||
public sealed class AccountConfiguration : IEntityTypeConfiguration<Account>
|
||||
{
|
||||
/// <summary>
|
||||
/// Configure Account.
|
||||
/// </summary>
|
||||
/// <param name="builder">Builder.</param>
|
||||
public void Configure(EntityTypeBuilder<Account> builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
builder.ToTable("Account");
|
||||
|
||||
builder.Property(b => b.AccountId)
|
||||
.HasConversion(
|
||||
v => v.Id,
|
||||
v => new AccountId(v))
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(credit => credit.Currency)
|
||||
.HasConversion(
|
||||
value => value.Code,
|
||||
value => new Currency(value))
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(b => b.ExternalUserId)
|
||||
.UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction);
|
||||
|
||||
builder.HasMany(x => x.CreditsCollection)
|
||||
.WithOne(b => b.Account!)
|
||||
.HasForeignKey(b => b.AccountId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(x => x.DebitsCollection)
|
||||
.WithOne(b => b.Account!)
|
||||
.HasForeignKey(b => b.AccountId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
// <copyright file="CreditConfiguration.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Infrastructure.DataAccess.Configuration
|
||||
{
|
||||
using System;
|
||||
using Domain.Credits;
|
||||
using Domain.ValueObjects;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
/// <summary>
|
||||
/// Credit Configuration.
|
||||
/// </summary>
|
||||
public sealed class CreditConfiguration : IEntityTypeConfiguration<Credit>
|
||||
{
|
||||
/// <summary>
|
||||
/// Configure Credit.
|
||||
/// </summary>
|
||||
/// <param name="builder">Builder.</param>
|
||||
public void Configure(EntityTypeBuilder<Credit> builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
builder.ToTable("Credit");
|
||||
|
||||
builder.Ignore(e => e.Amount);
|
||||
|
||||
builder.Property(credit => credit.Value)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(credit => credit.Currency)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(credit => credit.CreditId)
|
||||
.HasConversion(
|
||||
value => value.Id,
|
||||
value => new CreditId(value))
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(credit => credit.AccountId)
|
||||
.HasConversion(
|
||||
value => value.Id,
|
||||
value => new AccountId(value))
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(credit => credit.TransactionDate)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(b => b.AccountId)
|
||||
.UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
// <copyright file="DebitConfiguration.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Infrastructure.DataAccess.Configuration
|
||||
{
|
||||
using System;
|
||||
using Domain.Debits;
|
||||
using Domain.ValueObjects;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
/// <summary>
|
||||
/// Debit Configuration.
|
||||
/// </summary>
|
||||
public sealed class DebitConfiguration : IEntityTypeConfiguration<Debit>
|
||||
{
|
||||
/// <summary>
|
||||
/// Configure Debit.
|
||||
/// </summary>
|
||||
/// <param name="builder">Builder.</param>
|
||||
public void Configure(EntityTypeBuilder<Debit> builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
builder.ToTable("Debit");
|
||||
|
||||
builder.Ignore(e => e.Amount);
|
||||
|
||||
builder.Property(debit => debit.Value)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(debit => debit.Currency)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(debit => debit.DebitId)
|
||||
.HasConversion(
|
||||
value => value.Id,
|
||||
value => new DebitId(value))
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(debit => debit.AccountId)
|
||||
.HasConversion(
|
||||
value => value.Id,
|
||||
value => new AccountId(value))
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(debit => debit.TransactionDate)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(b => b.AccountId)
|
||||
.UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction);
|
||||
}
|
||||
}
|
||||
}
|
45
accounts-api/src/Infrastructure/DataAccess/ContextFactory.cs
Normal file
45
accounts-api/src/Infrastructure/DataAccess/ContextFactory.cs
Normal file
@ -0,0 +1,45 @@
|
||||
// <copyright file="ContextFactory.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Infrastructure.DataAccess
|
||||
{
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// ContextFactory.
|
||||
/// </summary>
|
||||
public sealed class ContextFactory : IDesignTimeDbContextFactory<MangaContext>
|
||||
{
|
||||
/// <summary>
|
||||
/// Instantiate a MangaContext.
|
||||
/// </summary>
|
||||
/// <param name="args">Command line args.</param>
|
||||
/// <returns>Manga Context.</returns>
|
||||
public MangaContext CreateDbContext(string[] args)
|
||||
{
|
||||
string connectionString = ReadDefaultConnectionStringFromAppSettings();
|
||||
|
||||
var builder = new DbContextOptionsBuilder<MangaContext>();
|
||||
Console.WriteLine(connectionString);
|
||||
builder.UseSqlServer(connectionString);
|
||||
builder.EnableSensitiveDataLogging();
|
||||
return new MangaContext(builder.Options);
|
||||
}
|
||||
|
||||
private static string ReadDefaultConnectionStringFromAppSettings()
|
||||
{
|
||||
IConfigurationRoot configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.Production.json")
|
||||
.Build();
|
||||
|
||||
string connectionString = configuration.GetValue<string>("PersistenceModule:DefaultConnection");
|
||||
return connectionString;
|
||||
}
|
||||
}
|
||||
}
|
43
accounts-api/src/Infrastructure/DataAccess/EntityFactory.cs
Normal file
43
accounts-api/src/Infrastructure/DataAccess/EntityFactory.cs
Normal file
@ -0,0 +1,43 @@
|
||||
// <copyright file="EntityFactory.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Infrastructure.DataAccess
|
||||
{
|
||||
using System;
|
||||
using Domain;
|
||||
using Domain.Credits;
|
||||
using Domain.Debits;
|
||||
using Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// <see
|
||||
/// href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#entity-factory">
|
||||
/// Entity
|
||||
/// Factory Domain-Driven Design Pattern
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public sealed class EntityFactory : IAccountFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Account NewAccount(string externalUserId, Currency currency)
|
||||
=> new Account(new AccountId(Guid.NewGuid()), externalUserId, currency);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Credit NewCredit(
|
||||
Account account,
|
||||
PositiveMoney amountToDeposit,
|
||||
DateTime transactionDate) =>
|
||||
new Credit(new CreditId(Guid.NewGuid()), account.AccountId, transactionDate,
|
||||
amountToDeposit.Amount, amountToDeposit.Currency.Code);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Debit NewDebit(
|
||||
Account account,
|
||||
PositiveMoney amountToWithdraw,
|
||||
DateTime transactionDate) =>
|
||||
new Debit(new DebitId(Guid.NewGuid()), account.AccountId, transactionDate, amountToWithdraw.Amount,
|
||||
amountToWithdraw.Currency.Code);
|
||||
}
|
||||
}
|
55
accounts-api/src/Infrastructure/DataAccess/MangaContext.cs
Normal file
55
accounts-api/src/Infrastructure/DataAccess/MangaContext.cs
Normal file
@ -0,0 +1,55 @@
|
||||
// <copyright file="MangaContext.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Infrastructure.DataAccess
|
||||
{
|
||||
using System;
|
||||
using Domain;
|
||||
using Domain.Credits;
|
||||
using Domain.Debits;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class MangaContext : DbContext
|
||||
{
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
|
||||
public MangaContext(DbContextOptions options)
|
||||
#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets Accounts
|
||||
/// </summary>
|
||||
public DbSet<Account> Accounts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets Credits
|
||||
/// </summary>
|
||||
public DbSet<Credit> Credits { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets Debits
|
||||
/// </summary>
|
||||
public DbSet<Debit> Debits { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="modelBuilder"></param>
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
if (modelBuilder is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(modelBuilder));
|
||||
}
|
||||
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(MangaContext).Assembly);
|
||||
SeedData.Seed(modelBuilder);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
// <copyright file="MangaContextFake.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Infrastructure.DataAccess
|
||||
{
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using Domain;
|
||||
using Domain.Credits;
|
||||
using Domain.Debits;
|
||||
using Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
public sealed class MangaContextFake
|
||||
{
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
public MangaContextFake()
|
||||
{
|
||||
var credit = new Credit(
|
||||
new CreditId(Guid.NewGuid()),
|
||||
SeedData.DefaultAccountId,
|
||||
DateTime.Now,
|
||||
800,
|
||||
Currency.Dollar.Code);
|
||||
|
||||
var debit = new Debit(
|
||||
new DebitId(Guid.NewGuid()),
|
||||
SeedData.DefaultAccountId,
|
||||
DateTime.Now,
|
||||
300,
|
||||
Currency.Dollar.Code);
|
||||
|
||||
var account = new Account(
|
||||
SeedData.DefaultAccountId,
|
||||
SeedData.DefaultExternalUserId,
|
||||
Currency.Dollar);
|
||||
|
||||
account.CreditsCollection.Add(credit);
|
||||
account.DebitsCollection.Add(debit);
|
||||
|
||||
this.Accounts.Add(account);
|
||||
this.Credits.Add(credit);
|
||||
this.Debits.Add(debit);
|
||||
|
||||
var account2 = new Account(
|
||||
SeedData.SecondAccountId,
|
||||
SeedData.SecondExternalUserId,
|
||||
Currency.Dollar);
|
||||
|
||||
this.Accounts.Add(account2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets Accounts.
|
||||
/// </summary>
|
||||
public Collection<Account> Accounts { get; } = new Collection<Account>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets Credits.
|
||||
/// </summary>
|
||||
public Collection<Credit> Credits { get; } = new Collection<Credit>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets Debits.
|
||||
/// </summary>
|
||||
public Collection<Debit> Debits { get; } = new Collection<Debit>();
|
||||
}
|
||||
}
|
140
accounts-api/src/Infrastructure/DataAccess/Migrations/InitialCreate.Designer.cs
generated
Normal file
140
accounts-api/src/Infrastructure/DataAccess/Migrations/InitialCreate.Designer.cs
generated
Normal file
@ -0,0 +1,140 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Infrastructure.DataAccess;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace Infrastructure.DataAccess.Migrations
|
||||
{
|
||||
[DbContext(typeof(MangaContext))]
|
||||
[Migration("20200821064304_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "3.1.6")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128)
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
modelBuilder.Entity("Domain.Accounts.Account", b =>
|
||||
{
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ExternalUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("AccountId");
|
||||
|
||||
b.ToTable("Account");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
AccountId = new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"),
|
||||
Currency = "USD",
|
||||
ExternalUserId = "197d0438-e04b-453d-b5de-eca05960c6ae"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Accounts.Credits.Credit", b =>
|
||||
{
|
||||
b.Property<Guid>("CreditId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("TransactionDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<decimal>("Value")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.HasKey("CreditId");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.ToTable("Credit");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
CreditId = new Guid("7bf066ba-379a-4e72-a59b-9755fda432ce"),
|
||||
AccountId = new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"),
|
||||
Currency = "USD",
|
||||
TransactionDate = new DateTime(2020, 8, 21, 6, 43, 4, 92, DateTimeKind.Utc).AddTicks(7795),
|
||||
Value = 400m
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Accounts.Debits.Debit", b =>
|
||||
{
|
||||
b.Property<Guid>("DebitId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("TransactionDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<decimal>("Value")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.HasKey("DebitId");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.ToTable("Debit");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
DebitId = new Guid("31ade963-bd69-4afb-9df7-611ae2cfa651"),
|
||||
AccountId = new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"),
|
||||
Currency = "USD",
|
||||
TransactionDate = new DateTime(2020, 8, 21, 6, 43, 4, 93, DateTimeKind.Utc).AddTicks(301),
|
||||
Value = 400m
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Accounts.Credits.Credit", b =>
|
||||
{
|
||||
b.HasOne("Domain.Accounts.Account", "Account")
|
||||
.WithMany("CreditsCollection")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Accounts.Debits.Debit", b =>
|
||||
{
|
||||
b.HasOne("Domain.Accounts.Account", "Account")
|
||||
.WithMany("DebitsCollection")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
namespace Infrastructure.DataAccess.Migrations
|
||||
{
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
"Account",
|
||||
table => new
|
||||
{
|
||||
AccountId = table.Column<Guid>(nullable: false),
|
||||
ExternalUserId = table.Column<string>(nullable: false),
|
||||
Currency = table.Column<string>(nullable: false)
|
||||
},
|
||||
constraints: table => { table.PrimaryKey("PK_Account", x => x.AccountId); });
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"Credit",
|
||||
table => new
|
||||
{
|
||||
CreditId = table.Column<Guid>(nullable: false),
|
||||
TransactionDate = table.Column<DateTime>(nullable: false),
|
||||
AccountId = table.Column<Guid>(nullable: false),
|
||||
Value = table.Column<decimal>(nullable: false),
|
||||
Currency = table.Column<string>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Credit", x => x.CreditId);
|
||||
table.ForeignKey(
|
||||
"FK_Credit_Account_AccountId",
|
||||
x => x.AccountId,
|
||||
"Account",
|
||||
"AccountId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"Debit",
|
||||
table => new
|
||||
{
|
||||
DebitId = table.Column<Guid>(nullable: false),
|
||||
TransactionDate = table.Column<DateTime>(nullable: false),
|
||||
AccountId = table.Column<Guid>(nullable: false),
|
||||
Value = table.Column<decimal>(nullable: false),
|
||||
Currency = table.Column<string>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Debit", x => x.DebitId);
|
||||
table.ForeignKey(
|
||||
"FK_Debit_Account_AccountId",
|
||||
x => x.AccountId,
|
||||
"Account",
|
||||
"AccountId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
"Account",
|
||||
new[] {"AccountId", "Currency", "ExternalUserId"},
|
||||
new object[]
|
||||
{
|
||||
new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"), "USD", "197d0438-e04b-453d-b5de-eca05960c6ae"
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
"Credit",
|
||||
new[] {"CreditId", "AccountId", "Currency", "TransactionDate", "Value"},
|
||||
new object[]
|
||||
{
|
||||
new Guid("7bf066ba-379a-4e72-a59b-9755fda432ce"),
|
||||
new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"), "USD",
|
||||
new DateTime(2020, 8, 21, 6, 43, 4, 92, DateTimeKind.Utc).AddTicks(7795), 400m
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
"Debit",
|
||||
new[] {"DebitId", "AccountId", "Currency", "TransactionDate", "Value"},
|
||||
new object[]
|
||||
{
|
||||
new Guid("31ade963-bd69-4afb-9df7-611ae2cfa651"),
|
||||
new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"), "USD",
|
||||
new DateTime(2020, 8, 21, 6, 43, 4, 93, DateTimeKind.Utc).AddTicks(301), 400m
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_Credit_AccountId",
|
||||
"Credit",
|
||||
"AccountId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_Debit_AccountId",
|
||||
"Debit",
|
||||
"AccountId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
"Credit");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"Debit");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"Account");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
// <auto-generated />
|
||||
|
||||
namespace Infrastructure.DataAccess.Migrations
|
||||
{
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
||||
[DbContext(typeof(MangaContext))]
|
||||
internal class MangaContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "3.1.6")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128)
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
modelBuilder.Entity("Domain.Accounts.Account", b =>
|
||||
{
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ExternalUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("AccountId");
|
||||
|
||||
b.ToTable("Account");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
AccountId = new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"),
|
||||
Currency = "USD",
|
||||
ExternalUserId = "197d0438-e04b-453d-b5de-eca05960c6ae"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Accounts.Credits.Credit", b =>
|
||||
{
|
||||
b.Property<Guid>("CreditId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("TransactionDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<decimal>("Value")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.HasKey("CreditId");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.ToTable("Credit");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
CreditId = new Guid("7bf066ba-379a-4e72-a59b-9755fda432ce"),
|
||||
AccountId = new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"),
|
||||
Currency = "USD",
|
||||
TransactionDate = new DateTime(2020, 8, 21, 6, 43, 4, 92, DateTimeKind.Utc).AddTicks(7795),
|
||||
Value = 400m
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Accounts.Debits.Debit", b =>
|
||||
{
|
||||
b.Property<Guid>("DebitId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("TransactionDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<decimal>("Value")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.HasKey("DebitId");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.ToTable("Debit");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
DebitId = new Guid("31ade963-bd69-4afb-9df7-611ae2cfa651"),
|
||||
AccountId = new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"),
|
||||
Currency = "USD",
|
||||
TransactionDate = new DateTime(2020, 8, 21, 6, 43, 4, 93, DateTimeKind.Utc).AddTicks(301),
|
||||
Value = 400m
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Accounts.Credits.Credit", b =>
|
||||
{
|
||||
b.HasOne("Domain.Accounts.Account", "Account")
|
||||
.WithMany("CreditsCollection")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Accounts.Debits.Debit", b =>
|
||||
{
|
||||
b.HasOne("Domain.Accounts.Account", "Account")
|
||||
.WithMany("DebitsCollection")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
// <copyright file="AccountRepository.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Infrastructure.DataAccess.Repositories
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Domain;
|
||||
using Domain.Credits;
|
||||
using Domain.Debits;
|
||||
using Domain.ValueObjects;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class AccountRepository : IAccountRepository
|
||||
{
|
||||
private readonly MangaContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
public AccountRepository(MangaContext context) => this._context = context ??
|
||||
throw new ArgumentNullException(
|
||||
nameof(context));
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Add(Account account, Credit credit)
|
||||
{
|
||||
await this._context
|
||||
.Accounts
|
||||
.AddAsync(account)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await this._context
|
||||
.Credits
|
||||
.AddAsync(credit)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Delete(AccountId accountId)
|
||||
{
|
||||
Account account = await this._context
|
||||
.Accounts
|
||||
.FindAsync(accountId)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (account != null)
|
||||
{
|
||||
this._context.Accounts.Remove(account);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IAccount> GetAccount(AccountId accountId)
|
||||
{
|
||||
Account account = await this._context
|
||||
.Accounts
|
||||
.Where(e => e.AccountId == accountId)
|
||||
.Select(e => e)
|
||||
.SingleOrDefaultAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (account is Account findAccount)
|
||||
{
|
||||
return await this.LoadTransactions(accountId, findAccount).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return AccountNull.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Update(Account account, Credit credit) => await this._context
|
||||
.Credits
|
||||
.AddAsync(credit)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Update(Account account, Debit debit) => await this._context
|
||||
.Debits
|
||||
.AddAsync(debit)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
public async Task<IAccount> Find(AccountId accountId, string externalUserId)
|
||||
{
|
||||
Account account = await this._context
|
||||
.Accounts
|
||||
.Where(e => e.ExternalUserId == externalUserId && e.AccountId == accountId)
|
||||
.Select(e => e)
|
||||
.SingleOrDefaultAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (account is Account findAccount)
|
||||
{
|
||||
return await this.LoadTransactions(accountId, findAccount).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return AccountNull.Instance;
|
||||
}
|
||||
|
||||
public async Task<IList<Account>> GetAccounts(string externalUserId)
|
||||
{
|
||||
var accounts = await this._context
|
||||
.Accounts
|
||||
.Where(e => e.ExternalUserId == externalUserId)
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return accounts;
|
||||
}
|
||||
|
||||
private async Task<IAccount> LoadTransactions(AccountId accountId, Account findAccount)
|
||||
{
|
||||
List<Credit> credits = await this._context
|
||||
.Credits
|
||||
.Where(e => e.AccountId.Equals(accountId))
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
List<Debit> debits = await this._context
|
||||
.Debits
|
||||
.Where(e => e.AccountId.Equals(accountId))
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
findAccount.CreditsCollection
|
||||
.AddRange(credits);
|
||||
findAccount.DebitsCollection
|
||||
.AddRange(debits);
|
||||
|
||||
return findAccount;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
// <copyright file="AccountRepositoryFake.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Infrastructure.DataAccess.Repositories
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Domain;
|
||||
using Domain.Credits;
|
||||
using Domain.Debits;
|
||||
using Domain.ValueObjects;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class AccountRepositoryFake : IAccountRepository
|
||||
{
|
||||
private readonly MangaContextFake _context;
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
public AccountRepositoryFake(MangaContextFake context) => this._context = context;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Add(Account account, Credit credit)
|
||||
{
|
||||
this._context
|
||||
.Accounts
|
||||
.Add(account);
|
||||
|
||||
this._context
|
||||
.Credits
|
||||
.Add(credit);
|
||||
|
||||
await Task.CompletedTask
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Delete(AccountId accountId)
|
||||
{
|
||||
Account accountOld = this._context
|
||||
.Accounts
|
||||
.SingleOrDefault(e => e.AccountId.Equals(accountId));
|
||||
|
||||
this._context
|
||||
.Accounts
|
||||
.Remove(accountOld);
|
||||
|
||||
await Task.CompletedTask
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IAccount> Find(AccountId accountId, string externalUserId)
|
||||
{
|
||||
Account account = this._context
|
||||
.Accounts
|
||||
.Where(e => e.ExternalUserId == externalUserId && e.AccountId.Equals(accountId))
|
||||
.Select(e => e)
|
||||
.SingleOrDefault();
|
||||
|
||||
if (account == null)
|
||||
{
|
||||
return AccountNull.Instance;
|
||||
}
|
||||
|
||||
return await Task.FromResult(account)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IAccount> GetAccount(AccountId accountId)
|
||||
{
|
||||
Account account = this._context
|
||||
.Accounts
|
||||
.SingleOrDefault(e => e.AccountId.Equals(accountId));
|
||||
|
||||
if (account == null)
|
||||
{
|
||||
return AccountNull.Instance;
|
||||
}
|
||||
|
||||
return await Task.FromResult(account)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IList<Account>> GetAccounts(string externalUserId)
|
||||
{
|
||||
var accounts = this._context
|
||||
.Accounts
|
||||
.Where(e => e.ExternalUserId == externalUserId)
|
||||
.ToList();
|
||||
|
||||
return await Task.FromResult(accounts)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Update(Account account, Credit credit)
|
||||
{
|
||||
Account accountOld = this._context
|
||||
.Accounts
|
||||
.SingleOrDefault(e => e.AccountId.Equals(account.AccountId));
|
||||
|
||||
if (accountOld != null)
|
||||
{
|
||||
this._context.Accounts.Remove(accountOld);
|
||||
}
|
||||
|
||||
this._context.Accounts.Add(account);
|
||||
this._context.Credits.Add(credit);
|
||||
|
||||
await Task.CompletedTask
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Update(Account account, Debit debit)
|
||||
{
|
||||
Account accountOld = this._context
|
||||
.Accounts
|
||||
.SingleOrDefault(e => e.AccountId.Equals(account.AccountId));
|
||||
|
||||
if (accountOld != null)
|
||||
{
|
||||
this._context.Accounts.Remove(accountOld);
|
||||
this._context.Accounts.Add(account);
|
||||
}
|
||||
|
||||
this._context.Debits.Add(debit);
|
||||
|
||||
await Task.CompletedTask
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
73
accounts-api/src/Infrastructure/DataAccess/SeedData.cs
Normal file
73
accounts-api/src/Infrastructure/DataAccess/SeedData.cs
Normal file
@ -0,0 +1,73 @@
|
||||
// <copyright file="SeedData.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Infrastructure.DataAccess
|
||||
{
|
||||
using System;
|
||||
using Domain;
|
||||
using Domain.Credits;
|
||||
using Domain.Debits;
|
||||
using Domain.ValueObjects;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
public static class SeedData
|
||||
{
|
||||
public static readonly string DefaultExternalUserId = "197d0438-e04b-453d-b5de-eca05960c6ae";
|
||||
|
||||
public static readonly AccountId DefaultAccountId =
|
||||
new AccountId(new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"));
|
||||
|
||||
public static readonly AccountId SecondAccountId =
|
||||
new AccountId(new Guid("E82D2EA6-E9D3-444D-A22F-9D65F2F2C65E"));
|
||||
|
||||
public static readonly string SecondExternalUserId = "C70E69BF-EDC7-48E3-BF33-B424F7464C5F";
|
||||
|
||||
public static readonly CreditId DefaultCreditId =
|
||||
new CreditId(new Guid("7BF066BA-379A-4E72-A59B-9755FDA432CE"));
|
||||
|
||||
public static readonly DebitId DefaultDebitId =
|
||||
new DebitId(new Guid("31ADE963-BD69-4AFB-9DF7-611AE2CFA651"));
|
||||
|
||||
public static void Seed(ModelBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
builder.Entity<Account>()
|
||||
.HasData(
|
||||
new
|
||||
{
|
||||
AccountId = DefaultAccountId,
|
||||
ExternalUserId = DefaultExternalUserId,
|
||||
Currency = Currency.Dollar
|
||||
});
|
||||
|
||||
builder.Entity<Credit>()
|
||||
.HasData(
|
||||
new
|
||||
{
|
||||
CreditId = DefaultCreditId,
|
||||
AccountId = DefaultAccountId,
|
||||
TransactionDate = DateTime.UtcNow,
|
||||
Value = 400m,
|
||||
Currency = Currency.Dollar.Code
|
||||
});
|
||||
|
||||
builder.Entity<Debit>()
|
||||
.HasData(
|
||||
new
|
||||
{
|
||||
DebitId = DefaultDebitId,
|
||||
AccountId = DefaultAccountId,
|
||||
TransactionDate = DateTime.UtcNow,
|
||||
Value = 400m,
|
||||
Currency = Currency.Dollar.Code
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
// <copyright file="ExternalUserService.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Infrastructure.ExternalAuthentication
|
||||
{
|
||||
using System.Security.Claims;
|
||||
using Application.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class ExternalUserService : IUserService
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="httpContextAccessor"></param>
|
||||
public ExternalUserService(
|
||||
IHttpContextAccessor httpContextAccessor) =>
|
||||
this._httpContextAccessor = httpContextAccessor;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetCurrentUserId()
|
||||
{
|
||||
ClaimsPrincipal user = this._httpContextAccessor
|
||||
.HttpContext
|
||||
.User;
|
||||
|
||||
string id = user.FindFirst(ClaimTypes.NameIdentifier)?.Value!;
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// <copyright file="TestUserService.cs" company="Ivan Paulovich">
|
||||
// Copyright © Ivan Paulovich. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Infrastructure.ExternalAuthentication
|
||||
{
|
||||
using Application.Services;
|
||||
using DataAccess;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class TestUserService : IUserService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string GetCurrentUserId() => SeedData.DefaultExternalUserId;
|
||||
}
|
||||
}
|
81
accounts-api/src/Infrastructure/Infrastructure.csproj
Normal file
81
accounts-api/src/Infrastructure/Infrastructure.csproj
Normal file
@ -0,0 +1,81 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<NoWarn>$(NoWarn);CA1062;1591</NoWarn>
|
||||
<Nullable>enable</Nullable>
|
||||
<NullableReferenceTypes>true</NullableReferenceTypes>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="appsettings.Production.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="appsettings.Production.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeQuality.Analyzers" Version="3.3.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.12.0.21095">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SecurityCodeScan" Version="3.5.3" PrivateAssets="all" />
|
||||
<PackageReference Update="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.0-beta2.final">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.7" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Application\Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Messages.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Messages.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Messages.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Messages.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="DataAccess\Migrations\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"PersistenceModule": {
|
||||
"DefaultConnection": "Server=192.168.1.75;User Id=sa;Password=<YourStrong!Passw0rd>;Data source=db01"
|
||||
}
|
||||
}
|
25
accounts-api/src/WebApi/Dockerfile
Normal file
25
accounts-api/src/WebApi/Dockerfile
Normal file
@ -0,0 +1,25 @@
|
||||
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
|
||||
WORKDIR /src
|
||||
COPY ["src/WebApi/WebApi.csproj", "src/WebApi/"]
|
||||
COPY ["src/Application/Application.csproj", "src/Application/"]
|
||||
COPY ["src/Domain/Domain.csproj", "src/Domain/"]
|
||||
COPY ["src/Infrastructure/Infrastructure.csproj", "src/Infrastructure/"]
|
||||
RUN dotnet restore "src/WebApi/WebApi.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/src/WebApi"
|
||||
RUN dotnet build "WebApi.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "WebApi.csproj" -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "WebApi.dll"]
|
35
accounts-api/src/WebApi/Modules/ApplicationExtensions.cs
Normal file
35
accounts-api/src/WebApi/Modules/ApplicationExtensions.cs
Normal file
@ -0,0 +1,35 @@
|
||||
namespace WebApi.Modules
|
||||
{
|
||||
using Application.UseCases.CloseAccount;
|
||||
using Application.UseCases.Deposit;
|
||||
using Application.UseCases.GetAccount;
|
||||
using Application.UseCases.GetAccounts;
|
||||
using Application.UseCases.OpenAccount;
|
||||
using Application.UseCases.Transfer;
|
||||
using Application.UseCases.Withdraw;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Adds Use Cases classes.
|
||||
/// </summary>
|
||||
public static class ApplicationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Use Cases to the ServiceCollection.
|
||||
/// </summary>
|
||||
/// <param name="services">Service Collection.</param>
|
||||
/// <returns>The modified instance.</returns>
|
||||
public static IServiceCollection AddUseCases(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ICloseAccountUseCase, CloseAccountUseCase>();
|
||||
services.AddScoped<IDepositUseCase, DepositUseCase>();
|
||||
services.AddScoped<IGetAccountUseCase, GetAccountUseCase>();
|
||||
services.AddScoped<IGetAccountsUseCase, GetAccountsUseCase>();
|
||||
services.AddScoped<IOpenAccountUseCase, OpenAccountUseCase>();
|
||||
services.AddScoped<ITransferUseCase, TransferUseCase>();
|
||||
services.AddScoped<IWithdrawUseCase, WithdrawUseCase>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user