diff --git a/.editorconfig b/.editorconfig
index 7ad85d7..d377555 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -4,6 +4,7 @@
tab_width = 4
indent_size = 4
end_of_line = lf
+max_line_length = 120
dotnet_diagnostic.ide0001.severity = warning
dotnet_diagnostic.ide0002.severity = warning
@@ -118,7 +119,7 @@ dotnet_diagnostic.ide0035.severity = warning
# IDE0036
dotnet_diagnostic.ide0036.severity = warning
-csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async
+csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, required, unsafe, volatile, async
# IDE0037
dotnet_diagnostic.ide0037.severity = warning
@@ -576,7 +577,7 @@ dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
csharp_style_prefer_utf8_string_literals = true:suggestion
# ReSharper
-resharper_csharp_empty_block_style = together_same_line
+resharper_csharp_empty_block_style = together
csharp_wrap_arguments_style = chop_if_long
csharp_wrap_chained_method_calls = chop_if_long
@@ -588,6 +589,8 @@ resharper_csharp_wrap_parameters_style = chop_if_long
dotnet_diagnostic.CS1591.severity = none
dotnet_diagnostic.CA1062.severity = none
dotnet_diagnostic.CA1034.severity = none
+dotnet_diagnostic.CS1574.severity = none
+dotnet_diagnostic.CS1573.severity = none
dotnet_diagnostic.SA1101.severity = none
dotnet_diagnostic.SA1503.severity = none
@@ -601,5 +604,7 @@ dotnet_diagnostic.SA1604.severity = none
dotnet_diagnostic.SA1629.severity = none
dotnet_diagnostic.SA1602.severity = none
dotnet_diagnostic.SA1615.severity = none
+dotnet_diagnostic.SA1618.severity = none
+dotnet_diagnostic.SA1206.severity = none
resharper_braces_redundant = false
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..6e4b4ff
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,7 @@
+*.cs text eol=lf
+*.slnx text eol=lf
+*.csproj text eol=lf
+*.props text eol=lf
+*.md text eol=lf
+*.txt text eol=lf
+*.json text eol=lf
\ No newline at end of file
diff --git a/.githooks/pre-commit b/.githooks/pre-commit
new file mode 100644
index 0000000..e125735
--- /dev/null
+++ b/.githooks/pre-commit
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+FORBIDDEN_FILES="Directory\.Build\.props \.github.* \.gitattributes"
+STAGED_FILES=$(git --no-pager diff --cached --name-only)
+
+error="false"
+
+for file in $FORBIDDEN_FILES; do
+ if echo "$STAGED_FILES" | grep -q "^$file$"; then
+ echo "Commit blocked: changes to $file are not allowed."
+ error="true"
+ else
+ echo file "$file" OK
+ fi
+done
+
+if [ "$error" = "true" ]
+then
+ exit 1
+else
+ exit 0
+fi
diff --git a/.gitignore b/.gitignore
index 1ca0673..b8c78e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -361,6 +361,4 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
-FodyWeavers.xsd
-
-/docs
\ No newline at end of file
+FodyWeavers.xsd
\ No newline at end of file
diff --git a/Atm.sln b/Atm.sln
deleted file mode 100644
index fee2692..0000000
--- a/Atm.sln
+++ /dev/null
@@ -1,131 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.0.31903.59
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atm.Domain", "src\Atm.Domain\Atm.Domain.csproj", "{A88D25D0-3D4D-48FE-A194-7ADA4CDC7086}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atm.Application.Abstractions", "src\Atm.Application.Abstractions\Atm.Application.Abstractions.csproj", "{3830631E-32C3-4DF6-8846-4B811D476AFE}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atm.Application.Contracts", "src\Atm.Application.Contracts\Atm.Application.Contracts.csproj", "{3EEEC36F-EF22-4958-BDDC-F4EB7D2C1700}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atm.Application", "src\Atm.Application\Atm.Application.csproj", "{38C552F8-6C6C-419E-BDE8-13ECB1EB97F6}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atm.Infrastructure", "src\Atm.Infrastructure\Atm.Infrastructure.csproj", "{685D6187-D16F-4231-A59D-111A4C832F55}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atm.Presentation", "src\Atm.Presentation\Atm.Presentation.csproj", "{72D902C9-8BE5-4408-B0EA-F77BCDD7C534}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atm.Application.Tests", "tests\Atm.Application.Tests\Atm.Application.Tests.csproj", "{2DEDE88D-E43A-4FBC-A2E2-0AE5AD2945B5}"
-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
- {A88D25D0-3D4D-48FE-A194-7ADA4CDC7086}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A88D25D0-3D4D-48FE-A194-7ADA4CDC7086}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A88D25D0-3D4D-48FE-A194-7ADA4CDC7086}.Debug|x64.ActiveCfg = Debug|Any CPU
- {A88D25D0-3D4D-48FE-A194-7ADA4CDC7086}.Debug|x64.Build.0 = Debug|Any CPU
- {A88D25D0-3D4D-48FE-A194-7ADA4CDC7086}.Debug|x86.ActiveCfg = Debug|Any CPU
- {A88D25D0-3D4D-48FE-A194-7ADA4CDC7086}.Debug|x86.Build.0 = Debug|Any CPU
- {A88D25D0-3D4D-48FE-A194-7ADA4CDC7086}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A88D25D0-3D4D-48FE-A194-7ADA4CDC7086}.Release|Any CPU.Build.0 = Release|Any CPU
- {A88D25D0-3D4D-48FE-A194-7ADA4CDC7086}.Release|x64.ActiveCfg = Release|Any CPU
- {A88D25D0-3D4D-48FE-A194-7ADA4CDC7086}.Release|x64.Build.0 = Release|Any CPU
- {A88D25D0-3D4D-48FE-A194-7ADA4CDC7086}.Release|x86.ActiveCfg = Release|Any CPU
- {A88D25D0-3D4D-48FE-A194-7ADA4CDC7086}.Release|x86.Build.0 = Release|Any CPU
- {3830631E-32C3-4DF6-8846-4B811D476AFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3830631E-32C3-4DF6-8846-4B811D476AFE}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3830631E-32C3-4DF6-8846-4B811D476AFE}.Debug|x64.ActiveCfg = Debug|Any CPU
- {3830631E-32C3-4DF6-8846-4B811D476AFE}.Debug|x64.Build.0 = Debug|Any CPU
- {3830631E-32C3-4DF6-8846-4B811D476AFE}.Debug|x86.ActiveCfg = Debug|Any CPU
- {3830631E-32C3-4DF6-8846-4B811D476AFE}.Debug|x86.Build.0 = Debug|Any CPU
- {3830631E-32C3-4DF6-8846-4B811D476AFE}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3830631E-32C3-4DF6-8846-4B811D476AFE}.Release|Any CPU.Build.0 = Release|Any CPU
- {3830631E-32C3-4DF6-8846-4B811D476AFE}.Release|x64.ActiveCfg = Release|Any CPU
- {3830631E-32C3-4DF6-8846-4B811D476AFE}.Release|x64.Build.0 = Release|Any CPU
- {3830631E-32C3-4DF6-8846-4B811D476AFE}.Release|x86.ActiveCfg = Release|Any CPU
- {3830631E-32C3-4DF6-8846-4B811D476AFE}.Release|x86.Build.0 = Release|Any CPU
- {3EEEC36F-EF22-4958-BDDC-F4EB7D2C1700}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3EEEC36F-EF22-4958-BDDC-F4EB7D2C1700}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3EEEC36F-EF22-4958-BDDC-F4EB7D2C1700}.Debug|x64.ActiveCfg = Debug|Any CPU
- {3EEEC36F-EF22-4958-BDDC-F4EB7D2C1700}.Debug|x64.Build.0 = Debug|Any CPU
- {3EEEC36F-EF22-4958-BDDC-F4EB7D2C1700}.Debug|x86.ActiveCfg = Debug|Any CPU
- {3EEEC36F-EF22-4958-BDDC-F4EB7D2C1700}.Debug|x86.Build.0 = Debug|Any CPU
- {3EEEC36F-EF22-4958-BDDC-F4EB7D2C1700}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3EEEC36F-EF22-4958-BDDC-F4EB7D2C1700}.Release|Any CPU.Build.0 = Release|Any CPU
- {3EEEC36F-EF22-4958-BDDC-F4EB7D2C1700}.Release|x64.ActiveCfg = Release|Any CPU
- {3EEEC36F-EF22-4958-BDDC-F4EB7D2C1700}.Release|x64.Build.0 = Release|Any CPU
- {3EEEC36F-EF22-4958-BDDC-F4EB7D2C1700}.Release|x86.ActiveCfg = Release|Any CPU
- {3EEEC36F-EF22-4958-BDDC-F4EB7D2C1700}.Release|x86.Build.0 = Release|Any CPU
- {38C552F8-6C6C-419E-BDE8-13ECB1EB97F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {38C552F8-6C6C-419E-BDE8-13ECB1EB97F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {38C552F8-6C6C-419E-BDE8-13ECB1EB97F6}.Debug|x64.ActiveCfg = Debug|Any CPU
- {38C552F8-6C6C-419E-BDE8-13ECB1EB97F6}.Debug|x64.Build.0 = Debug|Any CPU
- {38C552F8-6C6C-419E-BDE8-13ECB1EB97F6}.Debug|x86.ActiveCfg = Debug|Any CPU
- {38C552F8-6C6C-419E-BDE8-13ECB1EB97F6}.Debug|x86.Build.0 = Debug|Any CPU
- {38C552F8-6C6C-419E-BDE8-13ECB1EB97F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {38C552F8-6C6C-419E-BDE8-13ECB1EB97F6}.Release|Any CPU.Build.0 = Release|Any CPU
- {38C552F8-6C6C-419E-BDE8-13ECB1EB97F6}.Release|x64.ActiveCfg = Release|Any CPU
- {38C552F8-6C6C-419E-BDE8-13ECB1EB97F6}.Release|x64.Build.0 = Release|Any CPU
- {38C552F8-6C6C-419E-BDE8-13ECB1EB97F6}.Release|x86.ActiveCfg = Release|Any CPU
- {38C552F8-6C6C-419E-BDE8-13ECB1EB97F6}.Release|x86.Build.0 = Release|Any CPU
- {685D6187-D16F-4231-A59D-111A4C832F55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {685D6187-D16F-4231-A59D-111A4C832F55}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {685D6187-D16F-4231-A59D-111A4C832F55}.Debug|x64.ActiveCfg = Debug|Any CPU
- {685D6187-D16F-4231-A59D-111A4C832F55}.Debug|x64.Build.0 = Debug|Any CPU
- {685D6187-D16F-4231-A59D-111A4C832F55}.Debug|x86.ActiveCfg = Debug|Any CPU
- {685D6187-D16F-4231-A59D-111A4C832F55}.Debug|x86.Build.0 = Debug|Any CPU
- {685D6187-D16F-4231-A59D-111A4C832F55}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {685D6187-D16F-4231-A59D-111A4C832F55}.Release|Any CPU.Build.0 = Release|Any CPU
- {685D6187-D16F-4231-A59D-111A4C832F55}.Release|x64.ActiveCfg = Release|Any CPU
- {685D6187-D16F-4231-A59D-111A4C832F55}.Release|x64.Build.0 = Release|Any CPU
- {685D6187-D16F-4231-A59D-111A4C832F55}.Release|x86.ActiveCfg = Release|Any CPU
- {685D6187-D16F-4231-A59D-111A4C832F55}.Release|x86.Build.0 = Release|Any CPU
- {72D902C9-8BE5-4408-B0EA-F77BCDD7C534}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {72D902C9-8BE5-4408-B0EA-F77BCDD7C534}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {72D902C9-8BE5-4408-B0EA-F77BCDD7C534}.Debug|x64.ActiveCfg = Debug|Any CPU
- {72D902C9-8BE5-4408-B0EA-F77BCDD7C534}.Debug|x64.Build.0 = Debug|Any CPU
- {72D902C9-8BE5-4408-B0EA-F77BCDD7C534}.Debug|x86.ActiveCfg = Debug|Any CPU
- {72D902C9-8BE5-4408-B0EA-F77BCDD7C534}.Debug|x86.Build.0 = Debug|Any CPU
- {72D902C9-8BE5-4408-B0EA-F77BCDD7C534}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {72D902C9-8BE5-4408-B0EA-F77BCDD7C534}.Release|Any CPU.Build.0 = Release|Any CPU
- {72D902C9-8BE5-4408-B0EA-F77BCDD7C534}.Release|x64.ActiveCfg = Release|Any CPU
- {72D902C9-8BE5-4408-B0EA-F77BCDD7C534}.Release|x64.Build.0 = Release|Any CPU
- {72D902C9-8BE5-4408-B0EA-F77BCDD7C534}.Release|x86.ActiveCfg = Release|Any CPU
- {72D902C9-8BE5-4408-B0EA-F77BCDD7C534}.Release|x86.Build.0 = Release|Any CPU
- {2DEDE88D-E43A-4FBC-A2E2-0AE5AD2945B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2DEDE88D-E43A-4FBC-A2E2-0AE5AD2945B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2DEDE88D-E43A-4FBC-A2E2-0AE5AD2945B5}.Debug|x64.ActiveCfg = Debug|Any CPU
- {2DEDE88D-E43A-4FBC-A2E2-0AE5AD2945B5}.Debug|x64.Build.0 = Debug|Any CPU
- {2DEDE88D-E43A-4FBC-A2E2-0AE5AD2945B5}.Debug|x86.ActiveCfg = Debug|Any CPU
- {2DEDE88D-E43A-4FBC-A2E2-0AE5AD2945B5}.Debug|x86.Build.0 = Debug|Any CPU
- {2DEDE88D-E43A-4FBC-A2E2-0AE5AD2945B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2DEDE88D-E43A-4FBC-A2E2-0AE5AD2945B5}.Release|Any CPU.Build.0 = Release|Any CPU
- {2DEDE88D-E43A-4FBC-A2E2-0AE5AD2945B5}.Release|x64.ActiveCfg = Release|Any CPU
- {2DEDE88D-E43A-4FBC-A2E2-0AE5AD2945B5}.Release|x64.Build.0 = Release|Any CPU
- {2DEDE88D-E43A-4FBC-A2E2-0AE5AD2945B5}.Release|x86.ActiveCfg = Release|Any CPU
- {2DEDE88D-E43A-4FBC-A2E2-0AE5AD2945B5}.Release|x86.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {A88D25D0-3D4D-48FE-A194-7ADA4CDC7086} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
- {3830631E-32C3-4DF6-8846-4B811D476AFE} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
- {3EEEC36F-EF22-4958-BDDC-F4EB7D2C1700} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
- {38C552F8-6C6C-419E-BDE8-13ECB1EB97F6} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
- {685D6187-D16F-4231-A59D-111A4C832F55} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
- {72D902C9-8BE5-4408-B0EA-F77BCDD7C534} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
- {2DEDE88D-E43A-4FBC-A2E2-0AE5AD2945B5} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
- EndGlobalSection
-EndGlobal
diff --git a/Atm.slnx b/Atm.slnx
new file mode 100644
index 0000000..a6b4f61
--- /dev/null
+++ b/Atm.slnx
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
index fd2910d..4989915 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,7 +1,42 @@
-
- true
- true
- latest
-
+
+ net10.0
+ disable
+ enable
+
+
+
+ latest
+ true
+ true
+ true
+ All
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..bd8297e
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,46 @@
+
+
+ true
+ false
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 00a6463..bbf9391 100644
--- a/README.md
+++ b/README.md
@@ -1,116 +1,123 @@
-# ATM [Hexagonal Architecture]
+# ATM Web API
-Implementation of an ATM system with a Web API interface using ASP.NET Controllers and in-memory storage.
+An ATM training project built with .NET 9, ASP.NET Core Controllers, and hexagonal architecture.
-## Functionality
+Database stage 1 is currently in progress. Microstages 13.1 through 13.3 are implemented; runtime repositories are still temporary in-memory adapters until the EF Core repositories replace them in microstage 13.9.
-- creating a user session using AccountNumber + PIN
-- creating an administrator session using SystemPassword + AccountNumber
-- creating an account, available only through an admin session
-- viewing the account balance
-- depositing money into an account
-- withdrawing money
-- viewing transaction history
+## Current functionality
-Any account-related operation creates a record in the transaction history.
+- create expiring user and administrator sessions;
+- create an account through an administrator session;
+- view an account balance;
+- deposit and withdraw money;
+- view the legacy in-memory operation history.
-## Architecture
+The domain model now also contains persistent shapes for users, canonical friendships, accounts, sessions, bank transactions, commission snapshots, and immutable account history entries. Transactional persistence, idempotency enforcement, commissions in runtime use cases, and transfers are deliberately implemented in later microstages.
-The project follows the hexagonal architecture approach:
+## Architecture
-- **Domain** — domain model and value objects
-- **Application** — business logic / use cases through ports
-- **Application.Abstractions** — abstractions such as repositories, providers, and similar components
-- **Application.Contracts** — request/response contracts and operation results
-- **Infrastructure** — implementations of abstractions, including in-memory repositories, and DI registration
-- **Presentation** — ASP.NET Web API Controllers and HTTP-to-port mapping
+- **Atm.Domain** — entities, value objects, invariants, and domain operation results;
+- **Atm.Application** — use-case orchestration;
+- **Atm.Application.Abstractions** — repository, clock, security, and provider ports;
+- **Atm.Application.Contracts** — input/output ports and application results;
+- **Atm.Infrastructure** — temporary repositories plus infrastructure adapters;
+- **Atm.Presentation** — HTTP controllers and composition root.
-The layers are connected using `Microsoft.Extensions.DependencyInjection`.
+Domain and Application have no EF Core or PostgreSQL dependency.
-Each module provides extension methods for dependency registration:
+## Validation and result style
-- `builder.Services.AddApplication();`
-- `builder.Services.AddInfrastructure();`
+Small value objects such as `AccountNumber`, `UserLogin`, `Money`, and `PinCode` validate themselves in constructors and reject invalid values with `ArgumentException`.
-## System Password Configuration
+Business operations that can normally be accepted or rejected use a dedicated result type with nested `Success` and `Failure` records. The project intentionally does not introduce a universal `Result` or one result hierarchy per primitive field.
-The system password is read through `ISystemPasswordProvider`, implemented in `Atm.Presentation.Common.SystemPasswordProvider`, and is taken from the configuration.
+## Money rules
-`appsettings.json`:
+Amounts use .NET `decimal` and allow at most four fractional digits. There is no artificial `numeric(19,4)` business ceiling: for example, values above `1_000_000_000_000_000` are valid.
-```json
-{
- "SystemAuth": {
- "SystemPassword": "your_password_here"
- }
-}
-```
+This does not mean infinite precision. Values must still fit in `decimal`, and arithmetic is rejected when it would overflow or silently lose a small amount near the representable boundary. PostgreSQL mappings use `numeric` without a precision declaration. Check constraints enforce at most four fractional digits and a coefficient representable by .NET decimal, including for external writes.
-## Running the API
-
-The HTTP launch profile uses:
+Commission snapshots retain the applied rate, fixed component, and calculated commission. The formula is:
```text
-http://localhost:5015
+round(amount * rate + fixed, 4, MidpointRounding.ToEven)
```
-Swagger UI is available at:
+Runtime deposit and withdrawal use cases still have their legacy no-commission behavior until microstage 13.7 wires the new transaction model into one database transaction.
-```text
-http://localhost:5015/swagger
-```
+## PIN security
-## API Use Cases
+The account entity stores `PinHash`, never `PinCode`. `IPinHasher` is implemented using the versioned Microsoft ASP.NET Core `PasswordHasher`, which generates a unique salt and provides the matching verification routine.
-### 1. Create an admin session
+Do not log request bodies containing PINs. JWT, cookies, and ASP.NET Core Identity user management are outside this stage.
-Use this when an administrator needs to perform admin-only actions, such as creating a new account.
+## Clock and sessions
-`POST /api/sessions/admin`
+`IClock` supplies the current UTC time. Production uses `SystemClock`; tests use a controllable `FixedClock`. This keeps time-dependent domain behavior deterministic.
+
+A user session references the stable account UUID. A system administrator session is not associated with an account. Sessions have UTC creation, expiration, and optional revocation timestamps. They are active from creation inclusively until expiration exclusively, unless revoked first.
+
+Session lifetimes are validated at startup:
```json
{
- "systemPassword": "your_password_here",
- "accountNumber": "acc-1"
+ "Sessions": {
+ "UserLifetimeMinutes": 30,
+ "AdminLifetimeMinutes": 15
+ }
}
```
-Successful response:
+Both values must be positive.
+
+## System password configuration
+
+The administrator password comes from configuration through `ISystemPasswordProvider`:
```json
{
- "sessionId": "admin-session-guid"
+ "SystemAuth": {
+ "SystemPassword": "use-a-secret-provider"
+ }
}
```
-The returned `sessionId` is used as `adminSessionId` in admin-only requests.
+Do not commit a real password. Override the development placeholder with user secrets or an environment variable.
-### 2. Create an account
+## API examples
-Requires an admin session.
+The local HTTP profile uses `http://localhost:5015`; Swagger is available at `http://localhost:5015/swagger` in Development.
-`POST /api/accounts/create`
+### Create an administrator session
+
+`POST /api/sessions/admin`
```json
{
- "adminSessionId": "admin-session-guid",
- "accountNumber": "acc-1",
- "pin": "1234",
- "initialBalance": 100
+ "systemPassword": "use-a-secret-provider",
+ "accountNumber": "legacy-admin-context"
}
```
-Successful response:
+`accountNumber` remains required only for backward compatibility with the existing HTTP contract. A system administrator session does not store or authorize against this number.
+
+### Create an account
+
+`POST /api/accounts/create`
```json
{
- "message": "Account created"
+ "adminSessionId": "00000000-0000-0000-0000-000000000001",
+ "accountNumber": "acc-1",
+ "pin": "1234",
+ "initialBalance": 100,
+ "ownerUserId": "00000000-0000-0000-0000-000000000002"
}
```
-### 3. Create a user session
+`ownerUserId` is a breaking addition to the old contract. In the current transitional in-memory implementation only a non-empty UUID is checked. Existence is enforced by the user repository and PostgreSQL foreign key in later persistence microstages. There is intentionally no user/profile HTTP CRUD endpoint in stage 1.
-Use this after an account has been created. The user signs in with the account number and PIN.
+### Create a user session
`POST /api/sessions/user`
@@ -121,103 +128,71 @@ Use this after an account has been created. The user signs in with the account n
}
```
-Successful response:
+The returned `sessionId` authorizes balance, deposit, withdrawal, and history operations.
-```json
-{
- "sessionId": "user-session-guid"
-}
-```
+### Account operations
-The returned `sessionId` is used as the user session identifier for balance, deposit, withdrawal, and history operations.
+- `GET /api/accounts/balance?sessionId={user-session-id}`
+- `POST /api/accounts/deposit` with `userSessionId` and `amount`
+- `POST /api/accounts/withdraw` with `userSessionId` and `amount`
+- `GET /api/accounts/history?sessionId={user-session-id}`
-### 4. View balance
+## Build and test
-Requires a user session.
-
-`GET /api/accounts/balance?sessionId=user-session-guid`
-
-Successful response:
-
-```json
-{
- "balance": 100
-}
+```powershell
+dotnet restore Atm.sln --locked-mode
+dotnet build Atm.sln -c Release --no-restore
+dotnet test Atm.sln -c Release --no-build
```
-### 5. Deposit money
+Tests are separated by architectural layer:
-Requires a user session.
+- `Atm.Domain.Tests` validates value objects, entities, invariants, commissions, and history calculations;
+- `Atm.Application.Tests` validates use cases, authorization, and application orchestration through ports;
+- `Atm.Infrastructure.Tests` validates concrete infrastructure adapters, EF metadata, PostgreSQL SQL generation, materialization, change tracking, financial write guards, and the design-time factory.
-`POST /api/accounts/deposit`
+Each test project references only the production layer it is intended to test. Unit tests create mocks or focused test objects directly and do not build the production DI container or start a database.
-```json
-{
- "userSessionId": "user-session-guid",
- "amount": 50
-}
-```
+## EF model and design-time tools (13.3)
-Successful response:
+`AtmDbContext` and six separate configurations are in `src/Atm.Infrastructure/Persistence`. The model maps the six tables from the specification in the `public` schema with explicit snake_case names, primary keys, foreign keys, checks, and indexes.
-```json
-{
- "balance": 150
-}
-```
+`UserLogin`, `AccountNumber`, and `CommissionSnapshot` use owned mappings within their owners' tables. Normalization is computed by the existing domain constructors, with unique indexes and PostgreSQL `C` collation on the normalized columns. `Money`, `Amount`, and `PinHash` use value conversions; monetary objects have value comparers for change tracking. The shared account-number mapping also includes the account version concurrency column.
-### 6. Withdraw money
+`SaveChanges` and `SaveChangesAsync` reject tracked updates or deletes of persisted transactions, commission snapshots, and history, as well as physical deletion of accounts. These guards apply to tracked saves; future repository APIs and database permissions must also prevent bulk SQL from bypassing the financial write policy.
-Requires a user session.
+The design-time factory reads `ConnectionStrings__AtmDatabase` from the environment. Supply Host, Database, and any required authentication through your local environment or secret tooling. The factory has no fallback credentials and does not include connection values in validation errors. It creates a context without opening a connection.
-`POST /api/accounts/withdraw`
+After configuring that variable, inspect the model without applying a migration:
-```json
-{
- "userSessionId": "user-session-guid",
- "amount": 30
-}
+```powershell
+dotnet tool restore
+dotnet ef dbcontext list --project src/Atm.Infrastructure --startup-project src/Atm.Presentation
+dotnet ef dbcontext script --project src/Atm.Infrastructure --startup-project src/Atm.Presentation
```
-Successful response:
+The script command generates SQL only. It is not a migration or a deployment step. Model tests exercise the actual Npgsql provider without executing SQL against a database; applying constraints and round-tripping data on PostgreSQL remain part of the migration and integration-test microstages.
-```json
-{
- "balance": 120
-}
-```
+## Review of 13.2
-### 7. View transaction history
+The review accompanying 13.3 checked the domain entities, UUIDs, owner references, normalization, session lifecycle, monetary invariants, commission snapshots, and history shape against the specification.
-Requires a user session.
+Two defects were fixed with domain regression tests:
-`GET /api/accounts/history?sessionId=user-session-guid`
+- an absent transaction side now requires `null`; `Guid.Empty` is rejected for the source of a deposit or target of a withdrawal;
+- commission multiplication detects unrepresentable intermediate results before the explicit four-place rounding, including `decimal.MaxValue * 0.5m`.
-Successful response:
+The domain remains independent of EF Core. User existence, request-ID uniqueness, atomic operations, and concurrent database writes require the later persistence/use-case stages; the review does not claim those features already work in the temporary runtime.
-```json
-{
- "transactions": [
- {
- "type": "Deposit",
- "amount": 50,
- "balanceAfter": 150
- },
- {
- "type": "Withdraw",
- "amount": 30,
- "balanceAfter": 120
- }
- ]
-}
-```
+## Transitional limitations
-## Authentication and Authorization Model
+The following stage-1 work is not yet complete:
-The project currently uses application-level sessions instead of ASP.NET Core JWT or cookie authentication.
+- initial migration and PostgreSQL repositories;
+- asynchronous persistence ports and cancellation tokens;
+- atomic balance/transaction/history commits;
+- database-backed idempotency and concurrency handling;
+- runtime commission policies and transfers;
+- PostgreSQL integration tests, production DI replacement, and CI environment.
-- Admin authentication is performed with `SystemPassword`.
-- User authentication is performed with `AccountNumber` and `PIN`.
-- Authorization is performed by checking the session type: `Admin` or `User`.
-- Admin-only operations require an admin session.
-- Account operations require a user session.
\ No newline at end of file
+Do not treat the current in-memory repositories as production persistence.
diff --git a/src/Atm.Api/Atm.Api.csproj b/src/Atm.Api/Atm.Api.csproj
new file mode 100644
index 0000000..dee25f9
--- /dev/null
+++ b/src/Atm.Api/Atm.Api.csproj
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Atm.Api/Program.cs b/src/Atm.Api/Program.cs
new file mode 100644
index 0000000..5dade1d
--- /dev/null
+++ b/src/Atm.Api/Program.cs
@@ -0,0 +1,29 @@
+using Atm.Application;
+using Atm.Infrastructure.Persistence;
+using Atm.Presentation;
+using SessionOptions = Atm.Application.Options.SessionOptions;
+
+WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
+
+builder.Services
+ .AddApplication()
+ .AddInfrastructure()
+ .AddPresentationHttp();
+
+builder.Services.AddSwaggerGen().AddEndpointsApiExplorer();
+
+builder.Services
+ .AddOptions()
+ .Bind(builder.Configuration.GetRequiredSection("Session"))
+ .ValidateDataAnnotations()
+ .ValidateOnStart();
+
+WebApplication app = builder.Build();
+
+app.UseSwagger();
+app.UseSwaggerUI();
+
+app.UseRouting();
+app.UsePresentationHttp();
+
+await app.RunAsync();
\ No newline at end of file
diff --git a/src/Atm.Presentation/Properties/launchSettings.json b/src/Atm.Api/Properties/launchSettings.json
similarity index 80%
rename from src/Atm.Presentation/Properties/launchSettings.json
rename to src/Atm.Api/Properties/launchSettings.json
index 1ec1e3a..808eff1 100644
--- a/src/Atm.Presentation/Properties/launchSettings.json
+++ b/src/Atm.Api/Properties/launchSettings.json
@@ -5,7 +5,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
- "applicationUrl": "http://localhost:5015",
+ "applicationUrl": "http://localhost:5140",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
@@ -14,7 +14,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
- "applicationUrl": "https://localhost:7083;http://localhost:5015",
+ "applicationUrl": "https://localhost:7204;http://localhost:5140",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
diff --git a/src/Atm.Presentation/appsettings.json b/src/Atm.Api/appsettings.Development.json
similarity index 59%
rename from src/Atm.Presentation/appsettings.json
rename to src/Atm.Api/appsettings.Development.json
index 52a8f53..8110c73 100644
--- a/src/Atm.Presentation/appsettings.json
+++ b/src/Atm.Api/appsettings.Development.json
@@ -1,12 +1,11 @@
{
- "SystemAuth": {
- "SystemPassword": "change_me"
- },
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
- "AllowedHosts": "*"
-}
+ "Session": {
+ "SystemPassword": "admin"
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Presentation/appsettings.Development.json b/src/Atm.Api/appsettings.json
similarity index 80%
rename from src/Atm.Presentation/appsettings.Development.json
rename to src/Atm.Api/appsettings.json
index 0c208ae..10f68b8 100644
--- a/src/Atm.Presentation/appsettings.Development.json
+++ b/src/Atm.Api/appsettings.json
@@ -4,5 +4,6 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
- }
+ },
+ "AllowedHosts": "*"
}
diff --git a/src/Atm.Application.Abstractions/Atm.Application.Abstractions.csproj b/src/Atm.Application.Abstractions/Atm.Application.Abstractions.csproj
index 2d25bf4..b799d9c 100644
--- a/src/Atm.Application.Abstractions/Atm.Application.Abstractions.csproj
+++ b/src/Atm.Application.Abstractions/Atm.Application.Abstractions.csproj
@@ -1,13 +1,9 @@
-
-
+
+
+
+
+
-
-
- net9.0
- enable
- enable
-
-
diff --git a/src/Atm.Application.Abstractions/IPersistenceContext.cs b/src/Atm.Application.Abstractions/IPersistenceContext.cs
new file mode 100644
index 0000000..09973b6
--- /dev/null
+++ b/src/Atm.Application.Abstractions/IPersistenceContext.cs
@@ -0,0 +1,14 @@
+using Atm.Application.Abstractions.Repositories;
+
+namespace Atm.Application.Abstractions;
+
+public interface IPersistenceContext
+{
+ IAccountRepository AccountRepository { get; }
+
+ IAdminSessionRepository AdminSessionRepository { get; }
+
+ IUserSessionRepository UserSessionRepository { get; }
+
+ IOperationHistoryRepository OperationHistoryRepository { get; }
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Abstractions/Repositories/IAccountRepository.cs b/src/Atm.Application.Abstractions/Repositories/IAccountRepository.cs
index 25bc8b8..de0a2f9 100644
--- a/src/Atm.Application.Abstractions/Repositories/IAccountRepository.cs
+++ b/src/Atm.Application.Abstractions/Repositories/IAccountRepository.cs
@@ -1,13 +1,23 @@
-using Atm.Domain.Accounts;
-using Atm.Domain.ValueObjects;
+using Atm.Application.Models.Accounts;
+using Atm.Application.Models.Accounts.ValueObjects;
namespace Atm.Application.Abstractions.Repositories;
public interface IAccountRepository
{
- Account? GetByNumber(AccountNumber accountNumber);
+ Task AddAccountAsync(
+ Account account,
+ CancellationToken cancellationToken);
- void Add(Account account);
+ Task FindAccountByIdAsync(
+ AccountId accountId,
+ CancellationToken cancellationToken);
- void Update(Account account);
+ Task FindAccountByNumberAsync(
+ AccountNumber accountNumber,
+ CancellationToken cancellationToken);
+
+ Task UpdateAccountAsync(
+ Account account,
+ CancellationToken cancellationToken);
}
\ No newline at end of file
diff --git a/src/Atm.Application.Abstractions/Repositories/IAdminSessionRepository.cs b/src/Atm.Application.Abstractions/Repositories/IAdminSessionRepository.cs
new file mode 100644
index 0000000..e74faf8
--- /dev/null
+++ b/src/Atm.Application.Abstractions/Repositories/IAdminSessionRepository.cs
@@ -0,0 +1,14 @@
+using Atm.Application.Models.Sessions;
+
+namespace Atm.Application.Abstractions.Repositories;
+
+public interface IAdminSessionRepository
+{
+ Task AddSessionAsync(
+ AdminSession sessions,
+ CancellationToken cancellationToken);
+
+ Task FindAdminSessionByIdAsync(
+ Guid sessionId,
+ CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Abstractions/Repositories/IOperationHistoryRepository.cs b/src/Atm.Application.Abstractions/Repositories/IOperationHistoryRepository.cs
new file mode 100644
index 0000000..984bcfb
--- /dev/null
+++ b/src/Atm.Application.Abstractions/Repositories/IOperationHistoryRepository.cs
@@ -0,0 +1,15 @@
+using Atm.Application.Models.Accounts.ValueObjects;
+using Atm.Application.Models.Operations;
+
+namespace Atm.Application.Abstractions.Repositories;
+
+public interface IOperationHistoryRepository
+{
+ Task AddOperationAsync(
+ Operation operation,
+ CancellationToken cancellationToken);
+
+ IAsyncEnumerable FindByAccountIdAsync(
+ AccountId accountId,
+ CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Abstractions/Repositories/IOperationRepository.cs b/src/Atm.Application.Abstractions/Repositories/IOperationRepository.cs
deleted file mode 100644
index 71dd687..0000000
--- a/src/Atm.Application.Abstractions/Repositories/IOperationRepository.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Atm.Domain.Operations;
-using Atm.Domain.ValueObjects;
-
-namespace Atm.Application.Abstractions.Repositories;
-
-public interface IOperationRepository
-{
- void Add(OperationRecord operation);
-
- IReadOnlyList GetByAccountNumber(AccountNumber accountNumber);
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Abstractions/Repositories/ISessionRepository.cs b/src/Atm.Application.Abstractions/Repositories/ISessionRepository.cs
deleted file mode 100644
index bff121b..0000000
--- a/src/Atm.Application.Abstractions/Repositories/ISessionRepository.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using Atm.Domain.Sessions;
-
-namespace Atm.Application.Abstractions.Repositories;
-
-public interface ISessionRepository
-{
- Session? GetById(Guid id);
-
- void Add(Session session);
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Abstractions/Repositories/ISystemPasswordProvider.cs b/src/Atm.Application.Abstractions/Repositories/ISystemPasswordProvider.cs
deleted file mode 100644
index 24c1635..0000000
--- a/src/Atm.Application.Abstractions/Repositories/ISystemPasswordProvider.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Atm.Application.Abstractions.Repositories;
-
-public interface ISystemPasswordProvider
-{
- string GetSystemPassword();
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Abstractions/Repositories/IUserSessionRepository.cs b/src/Atm.Application.Abstractions/Repositories/IUserSessionRepository.cs
new file mode 100644
index 0000000..69e12c7
--- /dev/null
+++ b/src/Atm.Application.Abstractions/Repositories/IUserSessionRepository.cs
@@ -0,0 +1,14 @@
+using Atm.Application.Models.Sessions;
+
+namespace Atm.Application.Abstractions.Repositories;
+
+public interface IUserSessionRepository
+{
+ Task AddSessionAsync(
+ UserSession session,
+ CancellationToken cancellationToken);
+
+ Task FindUserSessionByIdAsync(
+ Guid sessionId,
+ CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Abstractions/Transactions/IPersistenceTransaction.cs b/src/Atm.Application.Abstractions/Transactions/IPersistenceTransaction.cs
new file mode 100644
index 0000000..19fbc57
--- /dev/null
+++ b/src/Atm.Application.Abstractions/Transactions/IPersistenceTransaction.cs
@@ -0,0 +1,8 @@
+namespace Atm.Application.Abstractions.Transactions;
+
+public interface IPersistenceTransaction : IAsyncDisposable
+{
+ ValueTask CommitAsync(CancellationToken cancellationToken);
+
+ ValueTask RollbackAsync(CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Abstractions/Transactions/IPersistenceTransactionProvider.cs b/src/Atm.Application.Abstractions/Transactions/IPersistenceTransactionProvider.cs
new file mode 100644
index 0000000..05a088c
--- /dev/null
+++ b/src/Atm.Application.Abstractions/Transactions/IPersistenceTransactionProvider.cs
@@ -0,0 +1,10 @@
+using System.Data;
+
+namespace Atm.Application.Abstractions.Transactions;
+
+public interface IPersistenceTransactionProvider
+{
+ ValueTask BeginTransactionAsync(
+ IsolationLevel isolationLevel,
+ CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Abstractions/packages.lock.json b/src/Atm.Application.Abstractions/packages.lock.json
deleted file mode 100644
index ba655ce..0000000
--- a/src/Atm.Application.Abstractions/packages.lock.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "version": 1,
- "dependencies": {
- "net9.0": {
- "atm.domain": {
- "type": "Project"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Accounts/CreateAccount/CreateAccountRequest.cs b/src/Atm.Application.Contracts/Accounts/CreateAccount/CreateAccountRequest.cs
deleted file mode 100644
index 5df84dc..0000000
--- a/src/Atm.Application.Contracts/Accounts/CreateAccount/CreateAccountRequest.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Atm.Application.Contracts.Accounts.CreateAccount;
-
-public sealed record CreateAccountRequest(
- Guid AdminSessionId,
- string AccountNumber,
- string Pin,
- decimal InitialBalance);
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Accounts/CreateAccount/CreateAccountResult.cs b/src/Atm.Application.Contracts/Accounts/CreateAccount/CreateAccountResult.cs
deleted file mode 100644
index a8cebda..0000000
--- a/src/Atm.Application.Contracts/Accounts/CreateAccount/CreateAccountResult.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Atm.Application.Contracts.Accounts.CreateAccount;
-
-public abstract record CreateAccountResult
-{
- private CreateAccountResult() { }
-
- public sealed record Success : CreateAccountResult;
-
- public sealed record Unauthorized(string ErrorMessage) : CreateAccountResult;
-
- public sealed record Failure(string ErrorMessage) : CreateAccountResult;
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Accounts/CreateAccount/ICreateAccountPort.cs b/src/Atm.Application.Contracts/Accounts/CreateAccount/ICreateAccountPort.cs
deleted file mode 100644
index 74fb23f..0000000
--- a/src/Atm.Application.Contracts/Accounts/CreateAccount/ICreateAccountPort.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Atm.Application.Contracts.Accounts.CreateAccount;
-
-public interface ICreateAccountPort
-{
- CreateAccountResult Execute(CreateAccountRequest request);
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Accounts/GetBalance/GetBalanceRequest.cs b/src/Atm.Application.Contracts/Accounts/GetBalance/GetBalanceRequest.cs
deleted file mode 100644
index 07ee626..0000000
--- a/src/Atm.Application.Contracts/Accounts/GetBalance/GetBalanceRequest.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace Atm.Application.Contracts.Accounts.GetBalance;
-
-public sealed record GetBalanceRequest(Guid UserSessionId);
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Accounts/GetBalance/GetBalanceResult.cs b/src/Atm.Application.Contracts/Accounts/GetBalance/GetBalanceResult.cs
deleted file mode 100644
index be6ed8c..0000000
--- a/src/Atm.Application.Contracts/Accounts/GetBalance/GetBalanceResult.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Atm.Application.Contracts.Accounts.GetBalance;
-
-public abstract record GetBalanceResult
-{
- private GetBalanceResult() { }
-
- public sealed record Success(decimal Balance) : GetBalanceResult;
-
- public sealed record Unauthorized(string ErrorMessage) : GetBalanceResult;
-
- public sealed record Failure(string ErrorMessage) : GetBalanceResult;
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Accounts/GetBalance/IGetBalancePort.cs b/src/Atm.Application.Contracts/Accounts/GetBalance/IGetBalancePort.cs
deleted file mode 100644
index 3cf75f4..0000000
--- a/src/Atm.Application.Contracts/Accounts/GetBalance/IGetBalancePort.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Atm.Application.Contracts.Accounts.GetBalance;
-
-public interface IGetBalancePort
-{
- GetBalanceResult Execute(GetBalanceRequest request);
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Accounts/IAccountService.cs b/src/Atm.Application.Contracts/Accounts/IAccountService.cs
new file mode 100644
index 0000000..41a54b6
--- /dev/null
+++ b/src/Atm.Application.Contracts/Accounts/IAccountService.cs
@@ -0,0 +1,22 @@
+using Atm.Application.Contracts.Accounts.Operations;
+
+namespace Atm.Application.Contracts.Accounts;
+
+public interface IAccountService
+{
+ Task GetAccountBalanceAsync(
+ GetBalance.Request request,
+ CancellationToken cancellationToken);
+
+ Task WithdrawBalanceAsync(
+ WithdrawBalance.Request request,
+ CancellationToken cancellationToken);
+
+ Task DepositBalanceAsync(
+ DepositBalance.Request request,
+ CancellationToken cancellationToken);
+
+ Task GetOperationHistoryAsync(
+ GetOperationHistory.Request request,
+ CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Accounts/Models/BalanceDto.cs b/src/Atm.Application.Contracts/Accounts/Models/BalanceDto.cs
new file mode 100644
index 0000000..1954500
--- /dev/null
+++ b/src/Atm.Application.Contracts/Accounts/Models/BalanceDto.cs
@@ -0,0 +1,3 @@
+namespace Atm.Application.Contracts.Accounts.Models;
+
+public sealed record BalanceDto(decimal Balance);
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Accounts/Models/OperationHistoryDto.cs b/src/Atm.Application.Contracts/Accounts/Models/OperationHistoryDto.cs
new file mode 100644
index 0000000..a4cadbd
--- /dev/null
+++ b/src/Atm.Application.Contracts/Accounts/Models/OperationHistoryDto.cs
@@ -0,0 +1,9 @@
+namespace Atm.Application.Contracts.Accounts.Models;
+
+public sealed record OperationHistoryDto(
+ long OperationId,
+ long AccountId,
+ DateTimeOffset Time,
+ string OperationType,
+ decimal Amount,
+ decimal BalanceAfter);
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Accounts/Operations/DepositBalance.cs b/src/Atm.Application.Contracts/Accounts/Operations/DepositBalance.cs
new file mode 100644
index 0000000..c3874de
--- /dev/null
+++ b/src/Atm.Application.Contracts/Accounts/Operations/DepositBalance.cs
@@ -0,0 +1,15 @@
+using Atm.Application.Contracts.Accounts.Models;
+
+namespace Atm.Application.Contracts.Accounts.Operations;
+
+public static class DepositBalance
+{
+ public readonly record struct Request(Guid SessionId, decimal Money);
+
+ public abstract record Response
+ {
+ public sealed record Success(BalanceDto Balance) : Response;
+
+ public sealed record Failure(string ErrorMessage) : Response;
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Accounts/Operations/GetBalance.cs b/src/Atm.Application.Contracts/Accounts/Operations/GetBalance.cs
new file mode 100644
index 0000000..c77f2e6
--- /dev/null
+++ b/src/Atm.Application.Contracts/Accounts/Operations/GetBalance.cs
@@ -0,0 +1,17 @@
+using Atm.Application.Contracts.Accounts.Models;
+
+namespace Atm.Application.Contracts.Accounts.Operations;
+
+public static class GetBalance
+{
+ public readonly record struct Request(Guid SessionId);
+
+ public abstract record Response
+ {
+ private Response() { }
+
+ public sealed record Success(BalanceDto Balance) : Response;
+
+ public sealed record Failure(string ErrorMessage) : Response;
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Accounts/Operations/GetOperationHistory.cs b/src/Atm.Application.Contracts/Accounts/Operations/GetOperationHistory.cs
new file mode 100644
index 0000000..2616adb
--- /dev/null
+++ b/src/Atm.Application.Contracts/Accounts/Operations/GetOperationHistory.cs
@@ -0,0 +1,18 @@
+using Atm.Application.Contracts.Accounts.Models;
+
+namespace Atm.Application.Contracts.Accounts.Operations;
+
+public static class GetOperationHistory
+{
+ public readonly record struct Request(Guid SessionId);
+
+ public abstract record Response
+ {
+ private Response() { }
+
+ public sealed record Success(
+ IReadOnlyCollection OperationHistory) : Response;
+
+ public sealed record Failure(string ErrorMessage) : Response;
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Accounts/Operations/WithdrawBalance.cs b/src/Atm.Application.Contracts/Accounts/Operations/WithdrawBalance.cs
new file mode 100644
index 0000000..6603e26
--- /dev/null
+++ b/src/Atm.Application.Contracts/Accounts/Operations/WithdrawBalance.cs
@@ -0,0 +1,15 @@
+using Atm.Application.Contracts.Accounts.Models;
+
+namespace Atm.Application.Contracts.Accounts.Operations;
+
+public static class WithdrawBalance
+{
+ public readonly record struct Request(Guid SessionId, decimal Money);
+
+ public abstract record Response
+ {
+ public sealed record Success(BalanceDto Balance) : Response;
+
+ public sealed record Failure(string ErrorMessage) : Response;
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Admins/IAdminService.cs b/src/Atm.Application.Contracts/Admins/IAdminService.cs
new file mode 100644
index 0000000..802bf00
--- /dev/null
+++ b/src/Atm.Application.Contracts/Admins/IAdminService.cs
@@ -0,0 +1,10 @@
+using Atm.Application.Contracts.Admins.Models;
+
+namespace Atm.Application.Contracts.Admins;
+
+public interface IAdminService
+{
+ Task CreateAccountAsync(
+ CreateAccount.Request request,
+ CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Admins/Models/CreateAccount.cs b/src/Atm.Application.Contracts/Admins/Models/CreateAccount.cs
new file mode 100644
index 0000000..d0c6242
--- /dev/null
+++ b/src/Atm.Application.Contracts/Admins/Models/CreateAccount.cs
@@ -0,0 +1,20 @@
+using Atm.Application.Contracts.Admins.Operations;
+
+namespace Atm.Application.Contracts.Admins.Models;
+
+public static class CreateAccount
+{
+ public readonly record struct Request(
+ Guid SessionId,
+ string AccountNumber,
+ string PinCode);
+
+ public abstract record Response
+ {
+ private Response() { }
+
+ public sealed record Success(AccountDto Account) : Response;
+
+ public sealed record Failure(string ErrorMessage) : Response;
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Admins/Operations/AccountDto.cs b/src/Atm.Application.Contracts/Admins/Operations/AccountDto.cs
new file mode 100644
index 0000000..7532678
--- /dev/null
+++ b/src/Atm.Application.Contracts/Admins/Operations/AccountDto.cs
@@ -0,0 +1,3 @@
+namespace Atm.Application.Contracts.Admins.Operations;
+
+public sealed record AccountDto(string AccountNumber);
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Atm.Application.Contracts.csproj b/src/Atm.Application.Contracts/Atm.Application.Contracts.csproj
index eed35a8..10fb849 100644
--- a/src/Atm.Application.Contracts/Atm.Application.Contracts.csproj
+++ b/src/Atm.Application.Contracts/Atm.Application.Contracts.csproj
@@ -1,8 +1,8 @@
- net9.0
- enable
- enable
+ Atm.Application.Contracts
-
+
+
+
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Operations/Deposit/DepositRequest.cs b/src/Atm.Application.Contracts/Operations/Deposit/DepositRequest.cs
deleted file mode 100644
index b225633..0000000
--- a/src/Atm.Application.Contracts/Operations/Deposit/DepositRequest.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace Atm.Application.Contracts.Operations.Deposit;
-
-public sealed record DepositRequest(Guid UserSessionId, decimal Amount);
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Operations/Deposit/DepositResult.cs b/src/Atm.Application.Contracts/Operations/Deposit/DepositResult.cs
deleted file mode 100644
index fb9cfa8..0000000
--- a/src/Atm.Application.Contracts/Operations/Deposit/DepositResult.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Atm.Application.Contracts.Operations.Deposit;
-
-public abstract record DepositResult
-{
- private DepositResult() { }
-
- public sealed record Success(decimal Balance) : DepositResult;
-
- public sealed record Unauthorized(string ErrorMessage) : DepositResult;
-
- public sealed record Failure(string ErrorMessage) : DepositResult;
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Operations/Deposit/IDepositPort.cs b/src/Atm.Application.Contracts/Operations/Deposit/IDepositPort.cs
deleted file mode 100644
index 5c4c6a4..0000000
--- a/src/Atm.Application.Contracts/Operations/Deposit/IDepositPort.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Atm.Application.Contracts.Operations.Deposit;
-
-public interface IDepositPort
-{
- DepositResult Execute(DepositRequest request);
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Operations/GetTransactionHistory/GetTransactionHistoryRequest.cs b/src/Atm.Application.Contracts/Operations/GetTransactionHistory/GetTransactionHistoryRequest.cs
deleted file mode 100644
index 9773fe2..0000000
--- a/src/Atm.Application.Contracts/Operations/GetTransactionHistory/GetTransactionHistoryRequest.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace Atm.Application.Contracts.Operations.GetTransactionHistory;
-
-public sealed record GetTransactionHistoryRequest(Guid UserSessionId);
diff --git a/src/Atm.Application.Contracts/Operations/GetTransactionHistory/GetTransactionHistoryResult.cs b/src/Atm.Application.Contracts/Operations/GetTransactionHistory/GetTransactionHistoryResult.cs
deleted file mode 100644
index 51d800a..0000000
--- a/src/Atm.Application.Contracts/Operations/GetTransactionHistory/GetTransactionHistoryResult.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Atm.Application.Contracts.Operations.GetTransactionHistory;
-
-public abstract record GetTransactionHistoryResult
-{
- private GetTransactionHistoryResult() { }
-
- public sealed record Success(IReadOnlyList Transactions) : GetTransactionHistoryResult;
-
- public sealed record Unauthorized(string ErrorMessage) : GetTransactionHistoryResult;
-
- public sealed record Failure(string ErrorMessage) : GetTransactionHistoryResult;
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Operations/GetTransactionHistory/IGetTransactionHistoryPort.cs b/src/Atm.Application.Contracts/Operations/GetTransactionHistory/IGetTransactionHistoryPort.cs
deleted file mode 100644
index e8e637f..0000000
--- a/src/Atm.Application.Contracts/Operations/GetTransactionHistory/IGetTransactionHistoryPort.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Atm.Application.Contracts.Operations.GetTransactionHistory;
-
-public interface IGetTransactionHistoryPort
-{
- GetTransactionHistoryResult Execute(GetTransactionHistoryRequest request);
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Operations/GetTransactionHistory/TransactionDto.cs b/src/Atm.Application.Contracts/Operations/GetTransactionHistory/TransactionDto.cs
deleted file mode 100644
index 1b5f98b..0000000
--- a/src/Atm.Application.Contracts/Operations/GetTransactionHistory/TransactionDto.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Atm.Application.Contracts.Operations.GetTransactionHistory;
-
-public record TransactionDto(
- string Type,
- decimal Amount,
- decimal BalanceAfter);
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Operations/Withdraw/IWithdrawPort.cs b/src/Atm.Application.Contracts/Operations/Withdraw/IWithdrawPort.cs
deleted file mode 100644
index caf6e1b..0000000
--- a/src/Atm.Application.Contracts/Operations/Withdraw/IWithdrawPort.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Atm.Application.Contracts.Operations.Withdraw;
-
-public interface IWithdrawPort
-{
- WithdrawResult Execute(WithdrawRequest request);
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Operations/Withdraw/WithdrawRequest.cs b/src/Atm.Application.Contracts/Operations/Withdraw/WithdrawRequest.cs
deleted file mode 100644
index 6fda83d..0000000
--- a/src/Atm.Application.Contracts/Operations/Withdraw/WithdrawRequest.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace Atm.Application.Contracts.Operations.Withdraw;
-
-public sealed record WithdrawRequest(Guid UserSessionId, decimal Amount);
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Operations/Withdraw/WithdrawResult.cs b/src/Atm.Application.Contracts/Operations/Withdraw/WithdrawResult.cs
deleted file mode 100644
index ac3c741..0000000
--- a/src/Atm.Application.Contracts/Operations/Withdraw/WithdrawResult.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Atm.Application.Contracts.Operations.Withdraw;
-
-public abstract record WithdrawResult
-{
- private WithdrawResult() { }
-
- public sealed record Success(decimal Balance) : WithdrawResult;
-
- public sealed record Unauthorized(string ErrorMessage) : WithdrawResult;
-
- public sealed record Failure(string ErrorMessage) : WithdrawResult;
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Sessions/CreateAdminSession/CreateAdminSessionRequest.cs b/src/Atm.Application.Contracts/Sessions/CreateAdminSession/CreateAdminSessionRequest.cs
deleted file mode 100644
index b38b6f6..0000000
--- a/src/Atm.Application.Contracts/Sessions/CreateAdminSession/CreateAdminSessionRequest.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace Atm.Application.Contracts.Sessions.CreateAdminSession;
-
-public sealed record CreateAdminSessionRequest(string SystemPassword, string AccountNumber);
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Sessions/CreateAdminSession/CreateAdminSessionResult.cs b/src/Atm.Application.Contracts/Sessions/CreateAdminSession/CreateAdminSessionResult.cs
deleted file mode 100644
index 3a93be8..0000000
--- a/src/Atm.Application.Contracts/Sessions/CreateAdminSession/CreateAdminSessionResult.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Atm.Application.Contracts.Sessions.CreateAdminSession;
-
-public abstract record CreateAdminSessionResult
-{
- private CreateAdminSessionResult() { }
-
- public sealed record Success(Guid SessionId) : CreateAdminSessionResult;
-
- public sealed record Unauthorized(string ErrorMessage) : CreateAdminSessionResult;
-
- public sealed record Failure(string ErrorMessage) : CreateAdminSessionResult;
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Sessions/CreateAdminSession/ICreateAdminSessionPort.cs b/src/Atm.Application.Contracts/Sessions/CreateAdminSession/ICreateAdminSessionPort.cs
deleted file mode 100644
index cc21408..0000000
--- a/src/Atm.Application.Contracts/Sessions/CreateAdminSession/ICreateAdminSessionPort.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Atm.Application.Contracts.Sessions.CreateAdminSession;
-
-public interface ICreateAdminSessionPort
-{
- CreateAdminSessionResult Execute(CreateAdminSessionRequest request);
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Sessions/CreateUserSession/CreateUserSessionRequest.cs b/src/Atm.Application.Contracts/Sessions/CreateUserSession/CreateUserSessionRequest.cs
deleted file mode 100644
index 0e4043c..0000000
--- a/src/Atm.Application.Contracts/Sessions/CreateUserSession/CreateUserSessionRequest.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace Atm.Application.Contracts.Sessions.CreateUserSession;
-
-public sealed record CreateUserSessionRequest(string AccountNumber, string Pin);
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Sessions/CreateUserSession/CreateUserSessionResult.cs b/src/Atm.Application.Contracts/Sessions/CreateUserSession/CreateUserSessionResult.cs
deleted file mode 100644
index e8fbe97..0000000
--- a/src/Atm.Application.Contracts/Sessions/CreateUserSession/CreateUserSessionResult.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Atm.Application.Contracts.Sessions.CreateUserSession;
-
-public abstract record CreateUserSessionResult
-{
- private CreateUserSessionResult() { }
-
- public sealed record Success(Guid SessionId) : CreateUserSessionResult;
-
- public sealed record Unauthorized(string ErrorMessage) : CreateUserSessionResult;
-
- public sealed record Failure(string ErrorMessage) : CreateUserSessionResult;
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Sessions/CreateUserSession/ICreateUserSessionPort.cs b/src/Atm.Application.Contracts/Sessions/CreateUserSession/ICreateUserSessionPort.cs
deleted file mode 100644
index ec22b60..0000000
--- a/src/Atm.Application.Contracts/Sessions/CreateUserSession/ICreateUserSessionPort.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Atm.Application.Contracts.Sessions.CreateUserSession;
-
-public interface ICreateUserSessionPort
-{
- CreateUserSessionResult Execute(CreateUserSessionRequest request);
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Sessions/ISessionService.cs b/src/Atm.Application.Contracts/Sessions/ISessionService.cs
new file mode 100644
index 0000000..1a0422f
--- /dev/null
+++ b/src/Atm.Application.Contracts/Sessions/ISessionService.cs
@@ -0,0 +1,14 @@
+using Atm.Application.Contracts.Sessions.Operations;
+
+namespace Atm.Application.Contracts.Sessions;
+
+public interface ISessionService
+{
+ Task CreateUserSessionAsync(
+ CreateUserSession.Request request,
+ CancellationToken cancellationToken);
+
+ Task CreateAdminSessionAsync(
+ CreateAdminSession.Request request,
+ CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Sessions/Models/SessionDto.cs b/src/Atm.Application.Contracts/Sessions/Models/SessionDto.cs
new file mode 100644
index 0000000..d1e0fa5
--- /dev/null
+++ b/src/Atm.Application.Contracts/Sessions/Models/SessionDto.cs
@@ -0,0 +1,3 @@
+namespace Atm.Application.Contracts.Sessions.Models;
+
+public sealed record SessionDto(Guid SessionId);
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Sessions/Operations/CreateAdminSession.cs b/src/Atm.Application.Contracts/Sessions/Operations/CreateAdminSession.cs
new file mode 100644
index 0000000..a50d07d
--- /dev/null
+++ b/src/Atm.Application.Contracts/Sessions/Operations/CreateAdminSession.cs
@@ -0,0 +1,17 @@
+using Atm.Application.Contracts.Sessions.Models;
+
+namespace Atm.Application.Contracts.Sessions.Operations;
+
+public static class CreateAdminSession
+{
+ public readonly record struct Request(string SystemPassword);
+
+ public abstract record Response
+ {
+ private Response() { }
+
+ public sealed record Success(SessionDto Session) : Response;
+
+ public sealed record Failure(string ErrorMessage) : Response;
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/Sessions/Operations/CreateUserSession.cs b/src/Atm.Application.Contracts/Sessions/Operations/CreateUserSession.cs
new file mode 100644
index 0000000..be0b6ad
--- /dev/null
+++ b/src/Atm.Application.Contracts/Sessions/Operations/CreateUserSession.cs
@@ -0,0 +1,19 @@
+using Atm.Application.Contracts.Sessions.Models;
+
+namespace Atm.Application.Contracts.Sessions.Operations;
+
+public static class CreateUserSession
+{
+ public readonly record struct Request(
+ string AccountNumber,
+ string Pincode);
+
+ public abstract record Response
+ {
+ private Response() { }
+
+ public sealed record Success(SessionDto Session) : Response;
+
+ public sealed record Failure(string ErrorMessage) : Response;
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Contracts/packages.lock.json b/src/Atm.Application.Contracts/packages.lock.json
deleted file mode 100644
index ba655ce..0000000
--- a/src/Atm.Application.Contracts/packages.lock.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "version": 1,
- "dependencies": {
- "net9.0": {
- "atm.domain": {
- "type": "Project"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Application.Models/Accounts/Account.cs b/src/Atm.Application.Models/Accounts/Account.cs
new file mode 100644
index 0000000..3b81617
--- /dev/null
+++ b/src/Atm.Application.Models/Accounts/Account.cs
@@ -0,0 +1,66 @@
+using Atm.Application.Models.Accounts.Results;
+using Atm.Application.Models.Accounts.ValueObjects;
+
+namespace Atm.Application.Models.Accounts;
+
+public sealed class Account
+{
+ private Account(
+ AccountId id,
+ AccountNumber number,
+ Pincode pincode,
+ Money balance)
+ {
+ Id = id;
+ Number = number;
+ Pincode = pincode;
+ Balance = balance;
+ }
+
+ public static Account CreateFromService(
+ AccountNumber number,
+ Pincode pincode)
+ {
+ return new Account(
+ AccountId.Default(),
+ number,
+ pincode,
+ Money.Create(0));
+ }
+
+ public static Account CreateFromDb(
+ AccountId id,
+ AccountNumber number,
+ Pincode pincode,
+ Money balance)
+ {
+ return new Account(id, number, pincode, balance);
+ }
+
+ public AccountId Id { get; }
+
+ public AccountNumber Number { get; }
+
+ public Pincode Pincode { get; }
+
+ public Money Balance { get; private set; }
+
+ public DepositResult Deposit(Money money)
+ {
+ Balance += money;
+
+ return new DepositResult.Success();
+ }
+
+ public WithdrawResult Withdraw(Money money)
+ {
+ if (money > Balance)
+ {
+ return new WithdrawResult.Failure("Insufficient funds");
+ }
+
+ Balance -= money;
+
+ return new WithdrawResult.Success();
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Models/Accounts/Results/DepositResult.cs b/src/Atm.Application.Models/Accounts/Results/DepositResult.cs
new file mode 100644
index 0000000..a2e65b3
--- /dev/null
+++ b/src/Atm.Application.Models/Accounts/Results/DepositResult.cs
@@ -0,0 +1,8 @@
+namespace Atm.Application.Models.Accounts.Results;
+
+public abstract record DepositResult
+{
+ private DepositResult() { }
+
+ public sealed record Success : DepositResult;
+}
\ No newline at end of file
diff --git a/src/Atm.Domain/Accounts/Results/WithdrawResult.cs b/src/Atm.Application.Models/Accounts/Results/WithdrawResult.cs
similarity index 50%
rename from src/Atm.Domain/Accounts/Results/WithdrawResult.cs
rename to src/Atm.Application.Models/Accounts/Results/WithdrawResult.cs
index 165333f..234137f 100644
--- a/src/Atm.Domain/Accounts/Results/WithdrawResult.cs
+++ b/src/Atm.Application.Models/Accounts/Results/WithdrawResult.cs
@@ -1,12 +1,10 @@
-using Atm.Domain.ValueObjects;
-
-namespace Atm.Domain.Accounts.Results;
+namespace Atm.Application.Models.Accounts.Results;
public abstract record WithdrawResult
{
private WithdrawResult() { }
- public sealed record Success(Money BalanceAfter) : WithdrawResult;
+ public sealed record Success : WithdrawResult;
public sealed record Failure(string ErrorMessage) : WithdrawResult;
}
\ No newline at end of file
diff --git a/src/Atm.Application.Models/Accounts/ValueObjects/AccountId.cs b/src/Atm.Application.Models/Accounts/ValueObjects/AccountId.cs
new file mode 100644
index 0000000..78d1fec
--- /dev/null
+++ b/src/Atm.Application.Models/Accounts/ValueObjects/AccountId.cs
@@ -0,0 +1,25 @@
+namespace Atm.Application.Models.Accounts.ValueObjects;
+
+public readonly record struct AccountId
+{
+ public long Value { get; }
+
+ private AccountId(long value)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(value);
+
+ Value = value;
+ }
+
+ public static AccountId Create(long value)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value);
+
+ return new AccountId(value);
+ }
+
+ public static AccountId Default()
+ {
+ return new AccountId(0);
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Models/Accounts/ValueObjects/AccountNumber.cs b/src/Atm.Application.Models/Accounts/ValueObjects/AccountNumber.cs
new file mode 100644
index 0000000..56aadde
--- /dev/null
+++ b/src/Atm.Application.Models/Accounts/ValueObjects/AccountNumber.cs
@@ -0,0 +1,18 @@
+namespace Atm.Application.Models.Accounts.ValueObjects;
+
+public readonly record struct AccountNumber
+{
+ public string Value { get; }
+
+ private AccountNumber(string value)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(value);
+
+ Value = value;
+ }
+
+ public static AccountNumber Create(string value)
+ {
+ return new AccountNumber(value);
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Models/Accounts/ValueObjects/Money.cs b/src/Atm.Application.Models/Accounts/ValueObjects/Money.cs
new file mode 100644
index 0000000..c09f8a9
--- /dev/null
+++ b/src/Atm.Application.Models/Accounts/ValueObjects/Money.cs
@@ -0,0 +1,28 @@
+namespace Atm.Application.Models.Accounts.ValueObjects;
+
+public readonly record struct Money
+{
+ public decimal Value { get; }
+
+ public static Money Zero => Create(0);
+
+ private Money(decimal value)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(value);
+
+ Value = value;
+ }
+
+ public static Money Create(decimal value)
+ {
+ return new Money(value);
+ }
+
+ public static Money operator +(Money left, Money right) => Create(left.Value + right.Value);
+
+ public static Money operator -(Money left, Money right) => Create(left.Value - right.Value);
+
+ public static bool operator >(Money left, Money right) => left.Value > right.Value;
+
+ public static bool operator <(Money left, Money right) => left.Value < right.Value;
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Models/Accounts/ValueObjects/Pincode.cs b/src/Atm.Application.Models/Accounts/ValueObjects/Pincode.cs
new file mode 100644
index 0000000..7e38c0d
--- /dev/null
+++ b/src/Atm.Application.Models/Accounts/ValueObjects/Pincode.cs
@@ -0,0 +1,18 @@
+namespace Atm.Application.Models.Accounts.ValueObjects;
+
+public readonly record struct Pincode
+{
+ public string Value { get; }
+
+ private Pincode(string value)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(value);
+
+ Value = value;
+ }
+
+ public static Pincode Create(string value)
+ {
+ return new Pincode(value);
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Models/Atm.Application.Models.csproj b/src/Atm.Application.Models/Atm.Application.Models.csproj
new file mode 100644
index 0000000..a696898
--- /dev/null
+++ b/src/Atm.Application.Models/Atm.Application.Models.csproj
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/Atm.Application.Models/Operations/Operation.cs b/src/Atm.Application.Models/Operations/Operation.cs
new file mode 100644
index 0000000..6c44e62
--- /dev/null
+++ b/src/Atm.Application.Models/Operations/Operation.cs
@@ -0,0 +1,68 @@
+using Atm.Application.Models.Accounts.ValueObjects;
+using Atm.Application.Models.Operations.ValueObjects;
+
+namespace Atm.Application.Models.Operations;
+
+public sealed class Operation
+{
+ private Operation(
+ OperationId operationId,
+ AccountId accountId,
+ DateTimeOffset time,
+ OperationType operationType,
+ Money amount,
+ Money balanceAfter)
+ {
+ OperationId = operationId;
+ AccountId = accountId;
+ Time = time;
+ OperationType = operationType;
+ Amount = amount;
+ BalanceAfter = balanceAfter;
+ }
+
+ public static Operation CreateFromService(
+ AccountId accountId,
+ DateTimeOffset time,
+ OperationType operationType,
+ Money amount,
+ Money balanceAfter)
+ {
+ return new Operation(
+ OperationId.Default(),
+ accountId,
+ time,
+ operationType,
+ amount,
+ balanceAfter);
+ }
+
+ public static Operation CreateFromDb(
+ OperationId operationId,
+ AccountId accountId,
+ DateTimeOffset time,
+ OperationType operationType,
+ Money amount,
+ Money balanceAfter)
+ {
+ return new Operation(
+ operationId,
+ accountId,
+ time,
+ operationType,
+ amount,
+ balanceAfter);
+ }
+
+ public OperationId OperationId { get; }
+
+ public AccountId AccountId { get; }
+
+ public DateTimeOffset Time { get; }
+
+ public OperationType OperationType { get; }
+
+ public Money Amount { get; }
+
+ public Money BalanceAfter { get; }
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Models/Operations/OperationType.cs b/src/Atm.Application.Models/Operations/OperationType.cs
new file mode 100644
index 0000000..d15460c
--- /dev/null
+++ b/src/Atm.Application.Models/Operations/OperationType.cs
@@ -0,0 +1,8 @@
+namespace Atm.Application.Models.Operations;
+
+public enum OperationType
+{
+ Deposit,
+ Withdraw,
+ ShowBalance,
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Models/Operations/ValueObjects/OperationId.cs b/src/Atm.Application.Models/Operations/ValueObjects/OperationId.cs
new file mode 100644
index 0000000..49cd23f
--- /dev/null
+++ b/src/Atm.Application.Models/Operations/ValueObjects/OperationId.cs
@@ -0,0 +1,25 @@
+namespace Atm.Application.Models.Operations.ValueObjects;
+
+public readonly record struct OperationId
+{
+ public long Value { get; }
+
+ private OperationId(long value)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(value);
+
+ Value = value;
+ }
+
+ public static OperationId Create(long value)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value);
+
+ return new OperationId(value);
+ }
+
+ public static OperationId Default()
+ {
+ return new OperationId(0);
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Models/Sessions/AdminSession.cs b/src/Atm.Application.Models/Sessions/AdminSession.cs
new file mode 100644
index 0000000..491a038
--- /dev/null
+++ b/src/Atm.Application.Models/Sessions/AdminSession.cs
@@ -0,0 +1,11 @@
+namespace Atm.Application.Models.Sessions;
+
+public sealed class AdminSession
+{
+ public Guid SessionId { get; }
+
+ public AdminSession(Guid sessionId)
+ {
+ SessionId = sessionId;
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application.Models/Sessions/UserSession.cs b/src/Atm.Application.Models/Sessions/UserSession.cs
new file mode 100644
index 0000000..8cdabc9
--- /dev/null
+++ b/src/Atm.Application.Models/Sessions/UserSession.cs
@@ -0,0 +1,18 @@
+using Atm.Application.Models.Accounts.ValueObjects;
+
+namespace Atm.Application.Models.Sessions;
+
+public sealed class UserSession
+{
+ public Guid SessionId { get; }
+
+ public AccountId AccountId { get; }
+
+ public UserSession(
+ Guid sessionId,
+ AccountId accountId)
+ {
+ SessionId = sessionId;
+ AccountId = accountId;
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application/Accounts/CreateAccountUseCase.cs b/src/Atm.Application/Accounts/CreateAccountUseCase.cs
deleted file mode 100644
index 4fe3ce3..0000000
--- a/src/Atm.Application/Accounts/CreateAccountUseCase.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using Atm.Application.Abstractions.Repositories;
-using Atm.Application.Common;
-using Atm.Application.Contracts.Accounts.CreateAccount;
-using Atm.Domain.Accounts;
-using Atm.Domain.ValueObjects;
-
-namespace Atm.Application.Accounts;
-
-internal sealed class CreateAccountUseCase : ICreateAccountPort
-{
- private readonly IAccountRepository _accounts;
- private readonly SessionAuthorizer _auth;
-
- public CreateAccountUseCase(IAccountRepository accounts, ISessionRepository sessions)
- {
- _accounts = accounts;
- _auth = new SessionAuthorizer(sessions);
- }
-
- public CreateAccountResult Execute(CreateAccountRequest request)
- {
- AuthorizationResult auth = _auth.AuthorizeAdmin(request.AdminSessionId);
-
- if (auth is AuthorizationResult.Unauthorized u)
- {
- return new CreateAccountResult.Unauthorized(u.ErrorMessage);
- }
-
- try
- {
- var number = new AccountNumber(request.AccountNumber);
- var pin = new PinCode(request.Pin);
- var balance = new Money(request.InitialBalance);
-
- Account? existing = _accounts.GetByNumber(number);
-
- if (existing is not null)
- {
- return new CreateAccountResult.Failure("Account number already exists");
- }
-
- var account = new Account(number, pin, balance);
- _accounts.Add(account);
-
- return new CreateAccountResult.Success();
- }
- catch (Exception e)
- {
- return new CreateAccountResult.Failure(e.Message);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Application/Accounts/GetBalanceUseCase.cs b/src/Atm.Application/Accounts/GetBalanceUseCase.cs
deleted file mode 100644
index 7d6ed8a..0000000
--- a/src/Atm.Application/Accounts/GetBalanceUseCase.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using Atm.Application.Abstractions.Repositories;
-using Atm.Application.Common;
-using Atm.Application.Contracts.Accounts.GetBalance;
-
-namespace Atm.Application.Accounts;
-
-internal sealed class GetBalanceUseCase : IGetBalancePort
-{
- private readonly IAccountRepository _accounts;
- private readonly SessionAuthorizer _auth;
-
- public GetBalanceUseCase(IAccountRepository accounts, SessionAuthorizer auth)
- {
- _accounts = accounts;
- _auth = auth;
- }
-
- public GetBalanceResult Execute(GetBalanceRequest request)
- {
- AuthorizationResult auth = _auth.AuthorizeUser(request.UserSessionId);
-
- if (auth is AuthorizationResult.Unauthorized u)
- {
- return new GetBalanceResult.Unauthorized(u.ErrorMessage);
- }
-
- var user = (AuthorizationResult.UserSuccess)auth;
- Domain.Accounts.Account? acc = _accounts.GetByNumber(user.AccountNumber);
-
- if (acc is null)
- {
- return new GetBalanceResult.Failure("Account not found");
- }
-
- return new GetBalanceResult.Success(acc.Balance.Value);
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Application/Atm.Application.csproj b/src/Atm.Application/Atm.Application.csproj
index d64769e..579ae02 100644
--- a/src/Atm.Application/Atm.Application.csproj
+++ b/src/Atm.Application/Atm.Application.csproj
@@ -1,19 +1,16 @@
-
+
+ Atm.Application
+
-
-
+
-
+
+
+
-
-
- net9.0
- enable
- enable
-
-
+
\ No newline at end of file
diff --git a/src/Atm.Application/Common/AuthorizationResult.cs b/src/Atm.Application/Common/AuthorizationResult.cs
deleted file mode 100644
index 6587986..0000000
--- a/src/Atm.Application/Common/AuthorizationResult.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using Atm.Domain.ValueObjects;
-
-namespace Atm.Application.Common;
-
-public abstract record AuthorizationResult
-{
- private AuthorizationResult() { }
-
- public sealed record UserSuccess(AccountNumber AccountNumber) : AuthorizationResult;
-
- public sealed record AdminSuccess : AuthorizationResult;
-
- public sealed record Unauthorized(string ErrorMessage) : AuthorizationResult;
-}
\ No newline at end of file
diff --git a/src/Atm.Application/Common/SessionAuthorizer.cs b/src/Atm.Application/Common/SessionAuthorizer.cs
deleted file mode 100644
index 2e8d145..0000000
--- a/src/Atm.Application/Common/SessionAuthorizer.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using Atm.Application.Abstractions.Repositories;
-using Atm.Domain.Sessions;
-
-namespace Atm.Application.Common;
-
-public sealed class SessionAuthorizer
-{
- private readonly ISessionRepository _sessions;
-
- public SessionAuthorizer(ISessionRepository sessions)
- {
- _sessions = sessions;
- }
-
- public AuthorizationResult AuthorizeUser(Guid sessionId)
- {
- Session? s = _sessions.GetById(sessionId);
-
- if (s is null)
- {
- return new AuthorizationResult.Unauthorized("Session not found");
- }
-
- if (s.Type != SessionType.User)
- {
- return new AuthorizationResult.Unauthorized("Session type not supported [user session required]");
- }
-
- if (s.AccountNumber is null)
- {
- return new AuthorizationResult.Unauthorized("Account number not set");
- }
-
- return new AuthorizationResult.UserSuccess(s.AccountNumber);
- }
-
- public AuthorizationResult AuthorizeAdmin(Guid sessionId)
- {
- Session? s = _sessions.GetById(sessionId);
-
- if (s is null)
- {
- return new AuthorizationResult.Unauthorized("Session not found");
- }
-
- if (s.Type != SessionType.Admin)
- {
- return new AuthorizationResult.Unauthorized("Session type not supported [admin session required]");
- }
-
- return new AuthorizationResult.AdminSuccess();
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Application/Mapping/AccountMappingExtensions.cs b/src/Atm.Application/Mapping/AccountMappingExtensions.cs
new file mode 100644
index 0000000..4d133e2
--- /dev/null
+++ b/src/Atm.Application/Mapping/AccountMappingExtensions.cs
@@ -0,0 +1,13 @@
+using Atm.Application.Contracts.Admins.Operations;
+using Atm.Application.Models.Accounts;
+
+namespace Atm.Application.Mapping;
+
+public static class AccountMappingExtensions
+{
+ public static AccountDto AccountMapToDto(
+ this Account account)
+ {
+ return new AccountDto(account.Number.Value);
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application/Mapping/BalanceMappingExtensions.cs b/src/Atm.Application/Mapping/BalanceMappingExtensions.cs
new file mode 100644
index 0000000..444278c
--- /dev/null
+++ b/src/Atm.Application/Mapping/BalanceMappingExtensions.cs
@@ -0,0 +1,13 @@
+using Atm.Application.Contracts.Accounts.Models;
+using Atm.Application.Models.Accounts.ValueObjects;
+
+namespace Atm.Application.Mapping;
+
+public static class BalanceMappingExtensions
+{
+ public static BalanceDto BalanceToDto(
+ this Money balance)
+ {
+ return new BalanceDto(balance.Value);
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application/Mapping/OperationsMappingExtensions.cs b/src/Atm.Application/Mapping/OperationsMappingExtensions.cs
new file mode 100644
index 0000000..c0d917f
--- /dev/null
+++ b/src/Atm.Application/Mapping/OperationsMappingExtensions.cs
@@ -0,0 +1,23 @@
+using Atm.Application.Contracts.Accounts.Models;
+using Atm.Application.Models.Operations;
+
+namespace Atm.Application.Mapping;
+
+public static class OperationsMappingExtensions
+{
+ public static IReadOnlyCollection HistoryToDto(
+ this IEnumerable operations)
+ {
+ return
+ [
+ .. operations
+ .Select(operation => new OperationHistoryDto(
+ operation.OperationId.Value,
+ operation.AccountId.Value,
+ operation.Time,
+ operation.OperationType.ToString(),
+ operation.Amount.Value,
+ operation.BalanceAfter.Value))
+ ];
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application/Mapping/SessionMappingExtensions.cs b/src/Atm.Application/Mapping/SessionMappingExtensions.cs
new file mode 100644
index 0000000..68e4ee9
--- /dev/null
+++ b/src/Atm.Application/Mapping/SessionMappingExtensions.cs
@@ -0,0 +1,19 @@
+using Atm.Application.Contracts.Sessions.Models;
+using Atm.Application.Models.Sessions;
+
+namespace Atm.Application.Mapping;
+
+public static class SessionMappingExtensions
+{
+ public static SessionDto UserSessionMapToDto(
+ this UserSession userSession)
+ {
+ return new SessionDto(userSession.SessionId);
+ }
+
+ public static SessionDto AdminSessionMapToDto(
+ this AdminSession adminSession)
+ {
+ return new SessionDto(adminSession.SessionId);
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application/Operations/DepositUseCase.cs b/src/Atm.Application/Operations/DepositUseCase.cs
deleted file mode 100644
index 47273db..0000000
--- a/src/Atm.Application/Operations/DepositUseCase.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using Atm.Application.Abstractions.Repositories;
-using Atm.Application.Common;
-using Atm.Application.Contracts.Operations.Deposit;
-using Atm.Domain.Operations;
-using Atm.Domain.ValueObjects;
-
-namespace Atm.Application.Operations;
-
-public sealed class DepositUseCase : IDepositPort
-{
- private readonly IAccountRepository _accounts;
- private readonly IOperationRepository _operations;
- private readonly SessionAuthorizer _auth;
-
- public DepositUseCase(IAccountRepository accounts, IOperationRepository operations, SessionAuthorizer auth)
- {
- _accounts = accounts;
- _operations = operations;
- _auth = auth;
- }
-
- public DepositResult Execute(DepositRequest request)
- {
- AuthorizationResult auth = _auth.AuthorizeUser(request.UserSessionId);
-
- if (auth is AuthorizationResult.Unauthorized u)
- {
- return new DepositResult.Unauthorized(u.ErrorMessage);
- }
-
- var user = (AuthorizationResult.UserSuccess)auth;
- Domain.Accounts.Account? acc = _accounts.GetByNumber(user.AccountNumber);
-
- if (acc is null)
- {
- return new DepositResult.Failure("Account not found");
- }
-
- Amount amount;
- try
- {
- amount = new Amount(request.Amount);
- }
- catch (Exception e)
- {
- return new DepositResult.Failure(e.Message);
- }
-
- Domain.Accounts.Results.DepositResult domainResult = acc.Deposit(amount);
-
- if (domainResult is Domain.Accounts.Results.DepositResult.Failure f)
- {
- return new DepositResult.Failure(f.ErrorMessage);
- }
-
- _accounts.Update(acc);
- _operations.Add(new OperationRecord(Guid.NewGuid(), user.AccountNumber, OperationType.Deposit, amount, acc.Balance));
-
- return new DepositResult.Success(acc.Balance.Value);
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Application/Operations/GetTransactionHistoryUseCase.cs b/src/Atm.Application/Operations/GetTransactionHistoryUseCase.cs
deleted file mode 100644
index 78dc825..0000000
--- a/src/Atm.Application/Operations/GetTransactionHistoryUseCase.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using Atm.Application.Abstractions.Repositories;
-using Atm.Application.Common;
-using Atm.Application.Contracts.Operations.GetTransactionHistory;
-
-namespace Atm.Application.Operations;
-
-internal sealed class GetTransactionHistoryUseCase : IGetTransactionHistoryPort
-{
- private readonly IOperationRepository _operations;
- private readonly SessionAuthorizer _auth;
-
- public GetTransactionHistoryUseCase(IOperationRepository operations, SessionAuthorizer auth)
- {
- _operations = operations;
- _auth = auth;
- }
-
- public GetTransactionHistoryResult Execute(GetTransactionHistoryRequest request)
- {
- AuthorizationResult auth = _auth.AuthorizeUser(request.UserSessionId);
-
- if (auth is AuthorizationResult.Unauthorized u)
- {
- return new GetTransactionHistoryResult.Unauthorized(u.ErrorMessage);
- }
-
- var user = (AuthorizationResult.UserSuccess)auth;
- IReadOnlyList ops = _operations.GetByAccountNumber(user.AccountNumber);
- var list = new List(ops.Count);
-
- foreach (Domain.Operations.OperationRecord op in ops)
- {
- list.Add(new TransactionDto(op.Type.ToString(), op.Amount.Value, op.BalanceAfter.Value));
- }
-
- return new GetTransactionHistoryResult.Success(list);
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Application/Operations/WithdrawUseCase.cs b/src/Atm.Application/Operations/WithdrawUseCase.cs
deleted file mode 100644
index da1c2b9..0000000
--- a/src/Atm.Application/Operations/WithdrawUseCase.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using Atm.Application.Abstractions.Repositories;
-using Atm.Application.Common;
-using Atm.Application.Contracts.Operations.Withdraw;
-using Atm.Domain.Operations;
-using Atm.Domain.ValueObjects;
-
-namespace Atm.Application.Operations;
-
-public sealed class WithdrawUseCase : IWithdrawPort
-{
- private readonly IAccountRepository _accounts;
- private readonly IOperationRepository _operations;
- private readonly SessionAuthorizer _auth;
-
- public WithdrawUseCase(IAccountRepository accounts, IOperationRepository operations, SessionAuthorizer auth)
- {
- _accounts = accounts;
- _operations = operations;
- _auth = auth;
- }
-
- public WithdrawResult Execute(WithdrawRequest request)
- {
- AuthorizationResult auth = _auth.AuthorizeUser(request.UserSessionId);
-
- if (auth is AuthorizationResult.Unauthorized u)
- {
- return new WithdrawResult.Unauthorized(u.ErrorMessage);
- }
-
- var user = (AuthorizationResult.UserSuccess)auth;
- Domain.Accounts.Account? acc = _accounts.GetByNumber(user.AccountNumber);
-
- if (acc is null)
- {
- return new WithdrawResult.Failure("Account not found");
- }
-
- Amount amount;
- try
- {
- amount = new Amount(request.Amount);
- }
- catch (Exception e)
- {
- return new WithdrawResult.Failure(e.Message);
- }
-
- Domain.Accounts.Results.WithdrawResult domainResult = acc.Withdraw(amount);
- if (domainResult is Domain.Accounts.Results.WithdrawResult.Failure result)
- {
- return new WithdrawResult.Failure(result.ErrorMessage);
- }
-
- _accounts.Update(acc);
- _operations.Add(new OperationRecord(Guid.NewGuid(), user.AccountNumber, OperationType.Withdraw, amount, acc.Balance));
-
- return new WithdrawResult.Success(acc.Balance.Value);
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Application/Options/SessionOptions.cs b/src/Atm.Application/Options/SessionOptions.cs
new file mode 100644
index 0000000..fb0c3a4
--- /dev/null
+++ b/src/Atm.Application/Options/SessionOptions.cs
@@ -0,0 +1,10 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Atm.Application.Options;
+
+public sealed class SessionOptions
+{
+ [Required]
+ [MinLength(5, ErrorMessage = "System password must be at least 5 characters long.")]
+ public required string SystemPassword { get; init; }
+}
\ No newline at end of file
diff --git a/src/Atm.Application/ServiceCollectionExtensions.cs b/src/Atm.Application/ServiceCollectionExtensions.cs
index 381b342..4e5fb6f 100644
--- a/src/Atm.Application/ServiceCollectionExtensions.cs
+++ b/src/Atm.Application/ServiceCollectionExtensions.cs
@@ -1,34 +1,20 @@
-using Atm.Application.Accounts;
-using Atm.Application.Common;
-using Atm.Application.Contracts.Accounts.CreateAccount;
-using Atm.Application.Contracts.Accounts.GetBalance;
-using Atm.Application.Contracts.Operations.Deposit;
-using Atm.Application.Contracts.Operations.GetTransactionHistory;
-using Atm.Application.Contracts.Operations.Withdraw;
-using Atm.Application.Contracts.Sessions.CreateAdminSession;
-using Atm.Application.Contracts.Sessions.CreateUserSession;
-using Atm.Application.Operations;
-using Atm.Application.Sessions;
+using Atm.Application.Contracts.Accounts;
+using Atm.Application.Contracts.Admins;
+using Atm.Application.Contracts.Sessions;
+using Atm.Application.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Atm.Application;
public static class ServiceCollectionExtensions
{
- public static IServiceCollection AddApplication(this IServiceCollection services)
+ public static IServiceCollection AddApplication(
+ this IServiceCollection collection)
{
- services.AddSingleton();
+ collection.AddScoped();
+ collection.AddScoped();
+ collection.AddScoped();
- services.AddTransient();
- services.AddTransient();
-
- services.AddTransient();
- services.AddTransient();
-
- services.AddTransient();
- services.AddTransient();
- services.AddTransient();
-
- return services;
+ return collection;
}
}
\ No newline at end of file
diff --git a/src/Atm.Application/Services/AccountService.cs b/src/Atm.Application/Services/AccountService.cs
new file mode 100644
index 0000000..1fa2f00
--- /dev/null
+++ b/src/Atm.Application/Services/AccountService.cs
@@ -0,0 +1,206 @@
+using Atm.Application.Abstractions;
+using Atm.Application.Contracts.Accounts;
+using Atm.Application.Contracts.Accounts.Operations;
+using Atm.Application.Mapping;
+using Atm.Application.Models.Accounts;
+using Atm.Application.Models.Accounts.Results;
+using Atm.Application.Models.Accounts.ValueObjects;
+using Atm.Application.Models.Operations;
+using Atm.Application.Models.Sessions;
+
+namespace Atm.Application.Services;
+
+public sealed class AccountService : IAccountService
+{
+ private readonly IPersistenceContext _context;
+
+ public AccountService(IPersistenceContext context)
+ {
+ _context = context;
+ }
+
+ public async Task GetAccountBalanceAsync(
+ GetBalance.Request request,
+ CancellationToken cancellationToken)
+ {
+ UserSession? session = await _context.UserSessionRepository
+ .FindUserSessionByIdAsync(
+ request.SessionId,
+ cancellationToken);
+
+ if (session is null)
+ {
+ return new GetBalance.Response.Failure("UserSession does not exist.");
+ }
+
+ Account? account = await _context.AccountRepository
+ .FindAccountByIdAsync(
+ session.AccountId,
+ cancellationToken);
+
+ if (account is null)
+ {
+ return new GetBalance.Response.Failure("Account does not exist.");
+ }
+
+ var operation = Operation.CreateFromService(
+ account.Id,
+ DateTimeOffset.Now,
+ OperationType.ShowBalance,
+ Money.Zero,
+ account.Balance);
+
+ await _context.OperationHistoryRepository
+ .AddOperationAsync(
+ operation,
+ cancellationToken);
+
+ return new GetBalance.Response.Success(account.Balance.BalanceToDto());
+ }
+
+ public async Task WithdrawBalanceAsync(
+ WithdrawBalance.Request request,
+ CancellationToken cancellationToken)
+ {
+ UserSession? session = await _context.UserSessionRepository
+ .FindUserSessionByIdAsync(
+ request.SessionId,
+ cancellationToken);
+
+ if (session is null)
+ {
+ return new WithdrawBalance.Response.Failure("UserSession does not exist.");
+ }
+
+ Account? account = await _context.AccountRepository
+ .FindAccountByIdAsync(
+ session.AccountId,
+ cancellationToken);
+
+ if (account is null)
+ {
+ return new WithdrawBalance.Response.Failure("Account does not exist.");
+ }
+
+ if (request.Money <= 0)
+ {
+ return new WithdrawBalance.Response.Failure("Invalid money.");
+ }
+
+ var amount = Money.Create(request.Money);
+
+ WithdrawResult result = account.Withdraw(amount);
+
+ if (result is WithdrawResult.Failure failure)
+ {
+ return new WithdrawBalance.Response.Failure(failure.ErrorMessage);
+ }
+
+ await _context.AccountRepository
+ .UpdateAccountAsync(
+ account,
+ cancellationToken);
+
+ var operation = Operation.CreateFromService(
+ account.Id,
+ DateTimeOffset.Now,
+ OperationType.Withdraw,
+ amount,
+ account.Balance);
+
+ await _context.OperationHistoryRepository
+ .AddOperationAsync(
+ operation,
+ cancellationToken);
+
+ return new WithdrawBalance.Response.Success(account.Balance.BalanceToDto());
+ }
+
+ public async Task DepositBalanceAsync(
+ DepositBalance.Request request,
+ CancellationToken cancellationToken)
+ {
+ UserSession? session = await _context.UserSessionRepository
+ .FindUserSessionByIdAsync(
+ request.SessionId,
+ cancellationToken);
+
+ if (session is null)
+ {
+ return new DepositBalance.Response.Failure("UserSession does not exist.");
+ }
+
+ Account? account = await _context.AccountRepository
+ .FindAccountByIdAsync(
+ session.AccountId,
+ cancellationToken);
+
+ if (account is null)
+ {
+ return new DepositBalance.Response.Failure("Account does not exist.");
+ }
+
+ if (request.Money <= 0)
+ {
+ return new DepositBalance.Response.Failure("Invalid money.");
+ }
+
+ var amount = Money.Create(request.Money);
+
+ account.Deposit(amount);
+
+ await _context.AccountRepository.UpdateAccountAsync(
+ account,
+ cancellationToken);
+
+ var operation = Operation.CreateFromService(
+ account.Id,
+ DateTimeOffset.Now,
+ OperationType.Deposit,
+ amount,
+ account.Balance);
+
+ await _context.OperationHistoryRepository
+ .AddOperationAsync(
+ operation,
+ cancellationToken);
+
+ return new DepositBalance.Response.Success(account.Balance.BalanceToDto());
+ }
+
+ public async Task GetOperationHistoryAsync(
+ GetOperationHistory.Request request,
+ CancellationToken cancellationToken)
+ {
+ UserSession? session = await _context.UserSessionRepository
+ .FindUserSessionByIdAsync(
+ request.SessionId,
+ cancellationToken);
+
+ if (session is null)
+ {
+ return new GetOperationHistory.Response.Failure("UserSession does not exist.");
+ }
+
+ Account? account = await _context.AccountRepository
+ .FindAccountByIdAsync(
+ session.AccountId,
+ cancellationToken);
+
+ if (account is null)
+ {
+ return new GetOperationHistory.Response.Failure("Account does not exist.");
+ }
+
+ var operations = new List();
+
+ await foreach (Operation operation in _context.OperationHistoryRepository
+ .FindByAccountIdAsync(account.Id, cancellationToken)
+ .WithCancellation(cancellationToken))
+ {
+ operations.Add(operation);
+ }
+
+ return new GetOperationHistory.Response.Success(operations.HistoryToDto());
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application/Services/AdminService.cs b/src/Atm.Application/Services/AdminService.cs
new file mode 100644
index 0000000..394ee89
--- /dev/null
+++ b/src/Atm.Application/Services/AdminService.cs
@@ -0,0 +1,52 @@
+using Atm.Application.Abstractions;
+using Atm.Application.Contracts.Admins;
+using Atm.Application.Contracts.Admins.Models;
+using Atm.Application.Mapping;
+using Atm.Application.Models.Accounts;
+using Atm.Application.Models.Accounts.ValueObjects;
+using Atm.Application.Models.Sessions;
+
+namespace Atm.Application.Services;
+
+public sealed class AdminService : IAdminService
+{
+ private readonly IPersistenceContext _context;
+
+ public AdminService(IPersistenceContext context)
+ {
+ _context = context;
+ }
+
+ public async Task CreateAccountAsync(
+ CreateAccount.Request request,
+ CancellationToken cancellationToken)
+ {
+ AdminSession? adminSession = await _context.AdminSessionRepository
+ .FindAdminSessionByIdAsync(request.SessionId, cancellationToken);
+
+ if (adminSession is null)
+ {
+ return new CreateAccount.Response.Failure("Admin session was not found.");
+ }
+
+ var accountNumber = AccountNumber.Create(request.AccountNumber);
+ var pincode = Pincode.Create(request.PinCode);
+
+ Account? account = await _context.AccountRepository
+ .FindAccountByNumberAsync(accountNumber, cancellationToken);
+
+ if (account is not null)
+ {
+ return new CreateAccount.Response.Failure("Account with this AccountNumber already exists.");
+ }
+
+ var newAccount = Account.CreateFromService(
+ accountNumber,
+ pincode);
+
+ Account createdAccount = await _context.AccountRepository
+ .AddAccountAsync(newAccount, cancellationToken);
+
+ return new CreateAccount.Response.Success(createdAccount.AccountMapToDto());
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application/Services/SessionService.cs b/src/Atm.Application/Services/SessionService.cs
new file mode 100644
index 0000000..1b7c8b2
--- /dev/null
+++ b/src/Atm.Application/Services/SessionService.cs
@@ -0,0 +1,72 @@
+using Atm.Application.Abstractions;
+using Atm.Application.Contracts.Sessions;
+using Atm.Application.Contracts.Sessions.Operations;
+using Atm.Application.Mapping;
+using Atm.Application.Models.Accounts;
+using Atm.Application.Models.Accounts.ValueObjects;
+using Atm.Application.Models.Sessions;
+using Atm.Application.Options;
+using Microsoft.Extensions.Options;
+
+namespace Atm.Application.Services;
+
+public sealed class SessionService : ISessionService
+{
+ private readonly IPersistenceContext _context;
+ private readonly string _systemPassword;
+
+ public SessionService(
+ IPersistenceContext context,
+ IOptions options)
+ {
+ _context = context;
+ _systemPassword = options.Value.SystemPassword;
+ }
+
+ public async Task CreateUserSessionAsync(
+ CreateUserSession.Request request,
+ CancellationToken cancellationToken)
+ {
+ var accountNumber = AccountNumber.Create(request.AccountNumber);
+ var pincode = Pincode.Create(request.Pincode);
+
+ Account? account = await _context.AccountRepository
+ .FindAccountByNumberAsync(accountNumber, cancellationToken);
+
+ if (account is null)
+ {
+ return new CreateUserSession.Response.Failure("Account was not found.");
+ }
+
+ if (pincode != account.Pincode)
+ {
+ return new CreateUserSession.Response.Failure("Wrong pincode.");
+ }
+
+ var session = new UserSession(Guid.NewGuid(), account.Id);
+
+ await _context.UserSessionRepository.AddSessionAsync(
+ session,
+ cancellationToken);
+
+ return new CreateUserSession.Response.Success(session.UserSessionMapToDto());
+ }
+
+ public async Task CreateAdminSessionAsync(
+ CreateAdminSession.Request request,
+ CancellationToken cancellationToken)
+ {
+ if (request.SystemPassword != _systemPassword)
+ {
+ return new CreateAdminSession.Response.Failure("Wrong password.");
+ }
+
+ var session = new AdminSession(Guid.NewGuid());
+
+ await _context.AdminSessionRepository.AddSessionAsync(
+ session,
+ cancellationToken);
+
+ return new CreateAdminSession.Response.Success(session.AdminSessionMapToDto());
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Application/Sessions/CreateAdminSessionUseCase.cs b/src/Atm.Application/Sessions/CreateAdminSessionUseCase.cs
deleted file mode 100644
index ce90160..0000000
--- a/src/Atm.Application/Sessions/CreateAdminSessionUseCase.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using Atm.Application.Abstractions.Repositories;
-using Atm.Application.Contracts.Sessions.CreateAdminSession;
-using Atm.Domain.Sessions;
-using Atm.Domain.ValueObjects;
-
-namespace Atm.Application.Sessions;
-
-internal sealed class CreateAdminSessionUseCase : ICreateAdminSessionPort
-{
- private readonly ISessionRepository _sessions;
- private readonly ISystemPasswordProvider _passwords;
-
- public CreateAdminSessionUseCase(ISessionRepository sessions, ISystemPasswordProvider passwords)
- {
- _sessions = sessions;
- _passwords = passwords;
- }
-
- public CreateAdminSessionResult Execute(CreateAdminSessionRequest request)
- {
- string expected = _passwords.GetSystemPassword();
-
- if (string.IsNullOrWhiteSpace(expected))
- {
- return new CreateAdminSessionResult.Failure("System password is not configured");
- }
-
- if (request.SystemPassword != expected)
- {
- return new CreateAdminSessionResult.Unauthorized("Invalid system password");
- }
-
- try
- {
- var accountNumber = new AccountNumber(request.AccountNumber);
- var id = Guid.NewGuid();
-
- _sessions.Add(new Session(id, SessionType.Admin, accountNumber));
-
- return new CreateAdminSessionResult.Success(id);
- }
- catch(Exception ex)
- {
- return new CreateAdminSessionResult.Failure(ex.Message);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Application/Sessions/CreateUserSessionUseCase.cs b/src/Atm.Application/Sessions/CreateUserSessionUseCase.cs
deleted file mode 100644
index 6712e64..0000000
--- a/src/Atm.Application/Sessions/CreateUserSessionUseCase.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using Atm.Application.Abstractions.Repositories;
-using Atm.Application.Contracts.Sessions.CreateUserSession;
-using Atm.Domain.Sessions;
-using Atm.Domain.ValueObjects;
-
-namespace Atm.Application.Sessions;
-
-internal sealed class CreateUserSessionUseCase : ICreateUserSessionPort
-{
- private readonly ISessionRepository _sessions;
- private readonly IAccountRepository _accounts;
-
- public CreateUserSessionUseCase(ISessionRepository sessions, IAccountRepository accounts)
- {
- _sessions = sessions;
- _accounts = accounts;
- }
-
- public CreateUserSessionResult Execute(CreateUserSessionRequest request)
- {
- try
- {
- var number = new AccountNumber(request.AccountNumber);
- var pin = new PinCode(request.Pin);
- Domain.Accounts.Account? acc = _accounts.GetByNumber(number);
-
- if (acc is null || acc.Pin.Value != pin.Value)
- {
- return new CreateUserSessionResult.Unauthorized("Invalid credentials");
- }
-
- var id = Guid.NewGuid();
- var session = new Session(id, SessionType.User, number);
-
- _sessions.Add(session);
-
- return new CreateUserSessionResult.Success(id);
- }
- catch (Exception ex)
- {
- return new CreateUserSessionResult.Failure(ex.Message);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Application/packages.lock.json b/src/Atm.Application/packages.lock.json
deleted file mode 100644
index 60a845a..0000000
--- a/src/Atm.Application/packages.lock.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "version": 1,
- "dependencies": {
- "net9.0": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": {
- "type": "Direct",
- "requested": "[10.0.2, )",
- "resolved": "10.0.2",
- "contentHash": "zOIurr59+kUf9vNcsUkCvKWZv+fPosUZXURZesYkJCvl0EzTc9F7maAO4Cd2WEV7ZJJ0AZrFQvuH6Npph9wdBw=="
- },
- "atm.application.abstractions": {
- "type": "Project",
- "dependencies": {
- "Atm.Domain": "[1.0.0, )"
- }
- },
- "atm.application.contracts": {
- "type": "Project"
- },
- "atm.domain": {
- "type": "Project"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Domain/Accounts/Account.cs b/src/Atm.Domain/Accounts/Account.cs
deleted file mode 100644
index 0fba7e4..0000000
--- a/src/Atm.Domain/Accounts/Account.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using Atm.Domain.Accounts.Results;
-using Atm.Domain.ValueObjects;
-
-namespace Atm.Domain.Accounts;
-
-public sealed class Account
-{
- public Account(AccountNumber number, PinCode pin, Money balance)
- {
- Number = number;
- Pin = pin;
- Balance = balance;
- }
-
- public AccountNumber Number { get; }
-
- public PinCode Pin { get; }
-
- public Money Balance { get; private set; }
-
- public DepositResult Deposit(Amount amount)
- {
- Balance = new Money(Balance.Value + amount.Value);
- return new DepositResult.Success(Balance);
- }
-
- public WithdrawResult Withdraw(Amount amount)
- {
- if (Balance.Value < amount.Value)
- {
- return new WithdrawResult.Failure("Not enough balance");
- }
-
- Balance = new Money(Balance.Value - amount.Value);
- return new WithdrawResult.Success(Balance);
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Domain/Accounts/Results/DepositResult.cs b/src/Atm.Domain/Accounts/Results/DepositResult.cs
deleted file mode 100644
index 94fcde4..0000000
--- a/src/Atm.Domain/Accounts/Results/DepositResult.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Atm.Domain.ValueObjects;
-
-namespace Atm.Domain.Accounts.Results;
-
-public abstract record DepositResult
-{
- private DepositResult() { }
-
- public sealed record Success(Money BalanceAfter) : DepositResult;
-
- public sealed record Failure(string ErrorMessage) : DepositResult;
-}
\ No newline at end of file
diff --git a/src/Atm.Domain/Atm.Domain.csproj b/src/Atm.Domain/Atm.Domain.csproj
deleted file mode 100644
index 125f4c9..0000000
--- a/src/Atm.Domain/Atm.Domain.csproj
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
- net9.0
- enable
- enable
-
-
-
diff --git a/src/Atm.Domain/Operations/OperationRecord.cs b/src/Atm.Domain/Operations/OperationRecord.cs
deleted file mode 100644
index 6de51e9..0000000
--- a/src/Atm.Domain/Operations/OperationRecord.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using Atm.Domain.ValueObjects;
-
-namespace Atm.Domain.Operations;
-
-public sealed class OperationRecord
-{
- public OperationRecord(
- Guid id,
- AccountNumber accountNumber,
- OperationType type,
- Amount amount,
- Money balanceAfter)
- {
- Id = id;
- AccountNumber = accountNumber;
- Type = type;
- Amount = amount;
- BalanceAfter = balanceAfter;
- }
-
- public Guid Id { get; }
-
- public AccountNumber AccountNumber { get; }
-
- public OperationType Type { get; }
-
- public Amount Amount { get; }
-
- public Money BalanceAfter { get; }
-}
\ No newline at end of file
diff --git a/src/Atm.Domain/Operations/OperationType.cs b/src/Atm.Domain/Operations/OperationType.cs
deleted file mode 100644
index 480a149..0000000
--- a/src/Atm.Domain/Operations/OperationType.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Atm.Domain.Operations;
-
-public enum OperationType
-{
- Withdraw,
- Deposit,
-}
\ No newline at end of file
diff --git a/src/Atm.Domain/Sessions/Session.cs b/src/Atm.Domain/Sessions/Session.cs
deleted file mode 100644
index 8284340..0000000
--- a/src/Atm.Domain/Sessions/Session.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using Atm.Domain.ValueObjects;
-
-namespace Atm.Domain.Sessions;
-
-public sealed class Session
-{
- public Session(Guid id, SessionType type, AccountNumber? accountNumber)
- {
- if (id == Guid.Empty)
- {
- throw new ArgumentException("Session ID cannot be empty");
- }
-
- if (type == SessionType.User && accountNumber is null)
- {
- throw new ArgumentException("User session must contain account number");
- }
-
- if (type == SessionType.Admin && accountNumber is null)
- {
- throw new ArgumentException("Admin session must contain account number");
- }
-
- Id = id;
- Type = type;
- AccountNumber = accountNumber;
- }
-
- public Guid Id { get; }
-
- public SessionType Type { get; }
-
- public AccountNumber? AccountNumber { get; }
-}
\ No newline at end of file
diff --git a/src/Atm.Domain/Sessions/SessionType.cs b/src/Atm.Domain/Sessions/SessionType.cs
deleted file mode 100644
index 77ee136..0000000
--- a/src/Atm.Domain/Sessions/SessionType.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Atm.Domain.Sessions;
-
-public enum SessionType
-{
- User = 1,
- Admin = 2,
-}
\ No newline at end of file
diff --git a/src/Atm.Domain/ValueObjects/AccountNumber.cs b/src/Atm.Domain/ValueObjects/AccountNumber.cs
deleted file mode 100644
index 2535538..0000000
--- a/src/Atm.Domain/ValueObjects/AccountNumber.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace Atm.Domain.ValueObjects;
-
-public sealed class AccountNumber
-{
- public AccountNumber(string value)
- {
- if (string.IsNullOrEmpty(value))
- {
- throw new ArgumentException("Value cannot be null or empty.", nameof(value));
- }
-
- Value = value.Trim();
- }
-
- public string Value { get; }
-}
\ No newline at end of file
diff --git a/src/Atm.Domain/ValueObjects/Amount.cs b/src/Atm.Domain/ValueObjects/Amount.cs
deleted file mode 100644
index 166e4b6..0000000
--- a/src/Atm.Domain/ValueObjects/Amount.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace Atm.Domain.ValueObjects;
-
-public sealed class Amount
-{
- public Amount(decimal value)
- {
- ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value);
-
- Value = value;
- }
-
- public decimal Value { get; }
-}
\ No newline at end of file
diff --git a/src/Atm.Domain/ValueObjects/Money.cs b/src/Atm.Domain/ValueObjects/Money.cs
deleted file mode 100644
index 6ac8e22..0000000
--- a/src/Atm.Domain/ValueObjects/Money.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-namespace Atm.Domain.ValueObjects;
-
-public sealed class Money
-{
- public Money(decimal value)
- {
- if (value < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(value), value, "Value must be greater than zero.");
- }
-
- Value = value;
- }
-
- public decimal Value { get; }
-
- public Money Add(Amount amount)
- {
- return new Money(Value + amount.Value);
- }
-
- public Money Subtract(Amount amount)
- {
- if (Value < amount.Value)
- {
- throw new InvalidOperationException("Cannot subtract from non-overflow.");
- }
-
- return new Money(Value - amount.Value);
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Domain/ValueObjects/PinCode.cs b/src/Atm.Domain/ValueObjects/PinCode.cs
deleted file mode 100644
index 088b9de..0000000
--- a/src/Atm.Domain/ValueObjects/PinCode.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-namespace Atm.Domain.ValueObjects;
-
-public sealed class PinCode
-{
- public PinCode(string value)
- {
- if (value.Length != 4)
- {
- throw new ArgumentOutOfRangeException(nameof(value));
- }
-
- if (value.Any(c => !char.IsDigit(c)))
- {
- throw new ArgumentException("Pin code must contain only digits");
- }
-
- Value = value;
- }
-
- public string Value { get; }
-}
\ No newline at end of file
diff --git a/src/Atm.Domain/packages.lock.json b/src/Atm.Domain/packages.lock.json
deleted file mode 100644
index a07e21a..0000000
--- a/src/Atm.Domain/packages.lock.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "version": 1,
- "dependencies": {
- "net9.0": {}
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Infrastructure.Persistence/Atm.Infrastructure.Persistence.csproj b/src/Atm.Infrastructure.Persistence/Atm.Infrastructure.Persistence.csproj
new file mode 100644
index 0000000..38e93a9
--- /dev/null
+++ b/src/Atm.Infrastructure.Persistence/Atm.Infrastructure.Persistence.csproj
@@ -0,0 +1,16 @@
+
+
+ Atm.Infrastructure.Persistence
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Atm.Infrastructure.Persistence/PersistenceContext.cs b/src/Atm.Infrastructure.Persistence/PersistenceContext.cs
new file mode 100644
index 0000000..1fb95dd
--- /dev/null
+++ b/src/Atm.Infrastructure.Persistence/PersistenceContext.cs
@@ -0,0 +1,27 @@
+using Atm.Application.Abstractions;
+using Atm.Application.Abstractions.Repositories;
+
+namespace Atm.Infrastructure.Persistence;
+
+public sealed class PersistenceContext : IPersistenceContext
+{
+ public IAccountRepository AccountRepository { get; }
+
+ public IAdminSessionRepository AdminSessionRepository { get; }
+
+ public IUserSessionRepository UserSessionRepository { get; }
+
+ public IOperationHistoryRepository OperationHistoryRepository { get; }
+
+ public PersistenceContext(
+ IAccountRepository accountRepository,
+ IAdminSessionRepository adminSessionRepository,
+ IUserSessionRepository userSessionRepository,
+ IOperationHistoryRepository operationHistoryRepository)
+ {
+ AccountRepository = accountRepository;
+ AdminSessionRepository = adminSessionRepository;
+ UserSessionRepository = userSessionRepository;
+ OperationHistoryRepository = operationHistoryRepository;
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Infrastructure.Persistence/Repositories/AccountRepository.cs b/src/Atm.Infrastructure.Persistence/Repositories/AccountRepository.cs
new file mode 100644
index 0000000..b8af788
--- /dev/null
+++ b/src/Atm.Infrastructure.Persistence/Repositories/AccountRepository.cs
@@ -0,0 +1,98 @@
+using Atm.Application.Abstractions.Repositories;
+using Atm.Application.Models.Accounts;
+using Atm.Application.Models.Accounts.ValueObjects;
+
+namespace Atm.Infrastructure.Persistence.Repositories;
+
+public sealed class AccountRepository : IAccountRepository
+{
+ private readonly Dictionary _values = [];
+ private readonly Lock _sync = new();
+ private long _lastId;
+
+ public Task AddAccountAsync(
+ Account account,
+ CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ lock (_sync)
+ {
+ if (_values.Values.Any(x => x.Number == account.Number))
+ {
+ throw new InvalidOperationException(
+ "Account number already exists");
+ }
+
+ var savedAccount = Account.CreateFromDb(
+ AccountId.Create(++_lastId),
+ account.Number,
+ account.Pincode,
+ account.Balance);
+
+ _values.Add(savedAccount.Id, Copy(savedAccount));
+
+ return Task.FromResult(savedAccount);
+ }
+ }
+
+ public Task FindAccountByIdAsync(
+ AccountId accountId,
+ CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ lock (_sync)
+ {
+ Account? result = _values.TryGetValue(accountId, out Account? account)
+ ? Copy(account)
+ : null;
+
+ return Task.FromResult(result);
+ }
+ }
+
+ public Task FindAccountByNumberAsync(
+ AccountNumber accountNumber,
+ CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ lock (_sync)
+ {
+ Account? account = _values.Values
+ .FirstOrDefault(x => x.Number == accountNumber);
+
+ return Task.FromResult(
+ account is null ? null : Copy(account));
+ }
+ }
+
+ public Task UpdateAccountAsync(
+ Account account,
+ CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ lock (_sync)
+ {
+ if (!_values.ContainsKey(account.Id))
+ {
+ throw new InvalidOperationException("Account not found");
+ }
+
+ _values[account.Id] = Copy(account);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private static Account Copy(Account account)
+ {
+ return Account.CreateFromDb(
+ account.Id,
+ account.Number,
+ account.Pincode,
+ account.Balance);
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Infrastructure.Persistence/Repositories/AdminSessionRepository.cs b/src/Atm.Infrastructure.Persistence/Repositories/AdminSessionRepository.cs
new file mode 100644
index 0000000..526120a
--- /dev/null
+++ b/src/Atm.Infrastructure.Persistence/Repositories/AdminSessionRepository.cs
@@ -0,0 +1,35 @@
+using Atm.Application.Abstractions.Repositories;
+using Atm.Application.Models.Sessions;
+using System.Collections.Concurrent;
+
+namespace Atm.Infrastructure.Persistence.Repositories;
+
+public sealed class AdminSessionRepository : IAdminSessionRepository
+{
+ private readonly ConcurrentDictionary _values = new();
+
+ public Task AddSessionAsync(
+ AdminSession sessions,
+ CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (_values.TryAdd(sessions.SessionId, sessions) is false)
+ {
+ throw new InvalidOperationException("Session already exists");
+ }
+
+ return Task.FromResult(sessions);
+ }
+
+ public Task FindAdminSessionByIdAsync(
+ Guid sessionId,
+ CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ _values.TryGetValue(sessionId, out AdminSession? session);
+
+ return Task.FromResult(session);
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Infrastructure.Persistence/Repositories/OperationHistoryRepository.cs b/src/Atm.Infrastructure.Persistence/Repositories/OperationHistoryRepository.cs
new file mode 100644
index 0000000..624f183
--- /dev/null
+++ b/src/Atm.Infrastructure.Persistence/Repositories/OperationHistoryRepository.cs
@@ -0,0 +1,64 @@
+using Atm.Application.Abstractions.Repositories;
+using Atm.Application.Models.Accounts.ValueObjects;
+using Atm.Application.Models.Operations;
+using Atm.Application.Models.Operations.ValueObjects;
+using System.Runtime.CompilerServices;
+
+namespace Atm.Infrastructure.Persistence.Repositories;
+
+public sealed class OperationHistoryRepository : IOperationHistoryRepository
+{
+ private readonly Dictionary _values = [];
+ private readonly Lock _sync = new();
+ private long _lastId;
+
+ public Task AddOperationAsync(
+ Operation operation,
+ CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ lock (_sync)
+ {
+ var savedOperation = Operation.CreateFromDb(
+ OperationId.Create(++_lastId),
+ operation.AccountId,
+ operation.Time,
+ operation.OperationType,
+ operation.Amount,
+ operation.BalanceAfter);
+
+ _values.Add(savedOperation.OperationId, savedOperation);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public async IAsyncEnumerable FindByAccountIdAsync(
+ AccountId accountId,
+ [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ Operation[] operations;
+
+ lock (_sync)
+ {
+ operations =
+ [
+ .. _values.Values
+ .Where(x => x.AccountId == accountId)
+ .OrderByDescending(x => x.Time)
+ .ThenByDescending(x => x.OperationId.Value)
+ ];
+ }
+
+ await Task.CompletedTask;
+
+ foreach (Operation operation in operations)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ yield return operation;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Infrastructure.Persistence/Repositories/UserSessionRepository.cs b/src/Atm.Infrastructure.Persistence/Repositories/UserSessionRepository.cs
new file mode 100644
index 0000000..a5135fc
--- /dev/null
+++ b/src/Atm.Infrastructure.Persistence/Repositories/UserSessionRepository.cs
@@ -0,0 +1,35 @@
+using Atm.Application.Abstractions.Repositories;
+using Atm.Application.Models.Sessions;
+using System.Collections.Concurrent;
+
+namespace Atm.Infrastructure.Persistence.Repositories;
+
+public sealed class UserSessionRepository : IUserSessionRepository
+{
+ private readonly ConcurrentDictionary _values = new();
+
+ public Task AddSessionAsync(
+ UserSession session,
+ CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (_values.TryAdd(session.SessionId, session) is false)
+ {
+ throw new InvalidOperationException("Session is already exists.");
+ }
+
+ return Task.FromResult(session);
+ }
+
+ public Task FindUserSessionByIdAsync(
+ Guid sessionId,
+ CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ _values.TryGetValue(sessionId, out UserSession? session);
+
+ return Task.FromResult(session);
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Infrastructure.Persistence/ServiceCollectionExtensions.cs b/src/Atm.Infrastructure.Persistence/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..a61bab9
--- /dev/null
+++ b/src/Atm.Infrastructure.Persistence/ServiceCollectionExtensions.cs
@@ -0,0 +1,22 @@
+using Atm.Application.Abstractions;
+using Atm.Application.Abstractions.Repositories;
+using Atm.Infrastructure.Persistence.Repositories;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Atm.Infrastructure.Persistence;
+
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddInfrastructure(
+ this IServiceCollection collection)
+ {
+ collection.AddSingleton();
+ collection.AddSingleton();
+ collection.AddSingleton();
+ collection.AddSingleton();
+
+ collection.AddScoped();
+
+ return collection;
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Infrastructure/Atm.Infrastructure.csproj b/src/Atm.Infrastructure/Atm.Infrastructure.csproj
deleted file mode 100644
index e759e49..0000000
--- a/src/Atm.Infrastructure/Atm.Infrastructure.csproj
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- net9.0
- enable
- enable
-
-
-
diff --git a/src/Atm.Infrastructure/Repositories/AccountRepository.cs b/src/Atm.Infrastructure/Repositories/AccountRepository.cs
deleted file mode 100644
index 44b8616..0000000
--- a/src/Atm.Infrastructure/Repositories/AccountRepository.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using Atm.Application.Abstractions.Repositories;
-using Atm.Domain.Accounts;
-using Atm.Domain.ValueObjects;
-
-namespace Atm.Infrastructure.Repositories;
-
-public sealed class AccountRepository : IAccountRepository
-{
- private readonly Dictionary _accounts = new();
-
- public Account? GetByNumber(AccountNumber accountNumber)
- {
- return _accounts.GetValueOrDefault(accountNumber.Value);
- }
-
- public void Add(Account account)
- {
- _accounts[account.Number.Value] = account;
- }
-
- public void Update(Account account)
- {
- _accounts[account.Number.Value] = account;
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Infrastructure/Repositories/OperationRepository.cs b/src/Atm.Infrastructure/Repositories/OperationRepository.cs
deleted file mode 100644
index 340d8a8..0000000
--- a/src/Atm.Infrastructure/Repositories/OperationRepository.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using Atm.Application.Abstractions.Repositories;
-using Atm.Domain.Operations;
-using Atm.Domain.ValueObjects;
-
-namespace Atm.Infrastructure.Repositories;
-
-public sealed class OperationRepository : IOperationRepository
-{
- private readonly List _operations = new();
-
- public void Add(OperationRecord operation)
- {
- _operations.Add(operation);
- }
-
- public IReadOnlyList GetByAccountNumber(AccountNumber accountNumber)
- {
- var result = new List();
-
- foreach (OperationRecord operation in _operations)
- {
- if (operation.AccountNumber.Value == accountNumber.Value)
- {
- result.Add(operation);
- }
- }
-
- return result;
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Infrastructure/Repositories/SessionRepository.cs b/src/Atm.Infrastructure/Repositories/SessionRepository.cs
deleted file mode 100644
index fcd3232..0000000
--- a/src/Atm.Infrastructure/Repositories/SessionRepository.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using Atm.Application.Abstractions.Repositories;
-using Atm.Domain.Sessions;
-
-namespace Atm.Infrastructure.Repositories;
-
-public sealed class SessionRepository : ISessionRepository
-{
- private readonly Dictionary _sessions = new();
-
- public Session? GetById(Guid id)
- {
- if (_sessions.TryGetValue(id, out Session? session))
- {
- return session;
- }
-
- return null;
- }
-
- public void Add(Session session)
- {
- _sessions.Add(session.Id, session);
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Infrastructure/ServiceCollectionExtensions.cs b/src/Atm.Infrastructure/ServiceCollectionExtensions.cs
deleted file mode 100644
index afe63f4..0000000
--- a/src/Atm.Infrastructure/ServiceCollectionExtensions.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using Atm.Infrastructure.Repositories;
-using Atm.Application.Abstractions.Repositories;
-using Microsoft.Extensions.DependencyInjection;
-
-namespace Atm.Infrastructure;
-
-public static class ServiceCollectionExtensions
-{
- public static IServiceCollection AddInfrastructure(this IServiceCollection services)
- {
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
-
- return services;
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Infrastructure/packages.lock.json b/src/Atm.Infrastructure/packages.lock.json
deleted file mode 100644
index 9fadaef..0000000
--- a/src/Atm.Infrastructure/packages.lock.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "version": 1,
- "dependencies": {
- "net9.0": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": {
- "type": "Direct",
- "requested": "[10.0.2, )",
- "resolved": "10.0.2",
- "contentHash": "zOIurr59+kUf9vNcsUkCvKWZv+fPosUZXURZesYkJCvl0EzTc9F7maAO4Cd2WEV7ZJJ0AZrFQvuH6Npph9wdBw=="
- },
- "atm.application.abstractions": {
- "type": "Project",
- "dependencies": {
- "Atm.Domain": "[1.0.0, )"
- }
- },
- "atm.domain": {
- "type": "Project"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Presentation/Atm.Presentation.csproj b/src/Atm.Presentation/Atm.Presentation.csproj
index 04059c3..35c268b 100644
--- a/src/Atm.Presentation/Atm.Presentation.csproj
+++ b/src/Atm.Presentation/Atm.Presentation.csproj
@@ -1,19 +1,14 @@
-
+
- net9.0
- enable
- enable
+ Library
-
-
+
-
-
+
-
-
+
\ No newline at end of file
diff --git a/src/Atm.Presentation/Common/SystemPasswordProvider.cs b/src/Atm.Presentation/Common/SystemPasswordProvider.cs
deleted file mode 100644
index 7667a0b..0000000
--- a/src/Atm.Presentation/Common/SystemPasswordProvider.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using Atm.Application.Abstractions.Repositories;
-
-namespace Atm.Presentation.Common;
-
-public sealed class SystemPasswordProvider : ISystemPasswordProvider
-{
- private readonly IConfiguration _config;
-
- public SystemPasswordProvider(IConfiguration config)
- {
- _config = config;
- }
-
- public string GetSystemPassword()
- {
- IConfigurationSection section = _config.GetSection("SystemAuth");
- string? password = section["SystemPassword"];
-
- if (password is null)
- {
- return string.Empty;
- }
-
- return password;
- }
-}
\ No newline at end of file
diff --git a/src/Atm.Presentation/Contracts/ApiRequests.cs b/src/Atm.Presentation/Contracts/ApiRequests.cs
deleted file mode 100644
index ce79d66..0000000
--- a/src/Atm.Presentation/Contracts/ApiRequests.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.Text.Json.Serialization;
-
-namespace Atm.Presentation.Contracts;
-
-public sealed record CreateAccountApiRequest(
- [property: JsonPropertyName("adminSessionId")] Guid AdminSessionId,
- [property: JsonPropertyName("accountNumber")] string AccountNumber,
- [property: JsonPropertyName("pin")] string Pin,
- [property: JsonPropertyName("initialBalance")] decimal InitialBalance);
-
-public sealed record AmountApiRequest(
- [property: JsonPropertyName("userSessionId")] Guid UserSessionId,
- [property: JsonPropertyName("amount")] decimal Amount);
-
-public sealed record CreateUserSessionApiRequest(
- [property: JsonPropertyName("accountNumber")] string AccountNumber,
- [property: JsonPropertyName("pin")] string Pin);
-
-public sealed record CreateAdminSessionApiRequest(
- [property: JsonPropertyName("systemPassword")] string SystemPassword,
- [property: JsonPropertyName("accountNumber")] string AccountNumber);
diff --git a/src/Atm.Presentation/Contracts/ApiResponses.cs b/src/Atm.Presentation/Contracts/ApiResponses.cs
deleted file mode 100644
index 7cbdf07..0000000
--- a/src/Atm.Presentation/Contracts/ApiResponses.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using Atm.Application.Contracts.Operations.GetTransactionHistory;
-using System.Text.Json.Serialization;
-
-namespace Atm.Presentation.Contracts;
-
-public sealed record ErrorResponse(
- [property: JsonPropertyName("error")] string Error);
-
-public sealed record SessionResponse(
- [property: JsonPropertyName("sessionId")] Guid SessionId);
-
-public sealed record BalanceResponse(
- [property: JsonPropertyName("balance")] decimal Balance);
-
-public sealed record MessageResponse(
- [property: JsonPropertyName("message")] string Message);
-
-public sealed record TransactionsResponse(
- [property: JsonPropertyName("transactions")] IReadOnlyList Transactions);
-
\ No newline at end of file
diff --git a/src/Atm.Presentation/Controllers/AccountController.cs b/src/Atm.Presentation/Controllers/AccountController.cs
new file mode 100644
index 0000000..1720ccc
--- /dev/null
+++ b/src/Atm.Presentation/Controllers/AccountController.cs
@@ -0,0 +1,98 @@
+using Atm.Application.Contracts.Accounts;
+using Atm.Application.Contracts.Accounts.Models;
+using Atm.Application.Contracts.Accounts.Operations;
+using Atm.Presentation.Models;
+using Microsoft.AspNetCore.Mvc;
+using System.Diagnostics;
+
+namespace Atm.Presentation.Controllers;
+
+[ApiController]
+[Route("api/accounts")]
+public sealed class AccountController : ControllerBase
+{
+ private readonly IAccountService _accountService;
+
+ public AccountController(IAccountService accountService)
+ {
+ _accountService = accountService;
+ }
+
+ [HttpGet("balance")]
+ public async Task> GetAccountBalanceAsync(
+ [FromQuery] BalanceRequest httpRequest,
+ CancellationToken cancellationToken)
+ {
+ var request = new GetBalance.Request(
+ httpRequest.SessionId);
+
+ GetBalance.Response response = await _accountService
+ .GetAccountBalanceAsync(request, cancellationToken);
+
+ return response switch
+ {
+ GetBalance.Response.Success success => Ok(success.Balance),
+ GetBalance.Response.Failure failure => BadRequest(failure.ErrorMessage),
+ _ => throw new UnreachableException(),
+ };
+ }
+
+ [HttpPost("withdraw")]
+ public async Task> WithdrawBalanceAsync(
+ [FromBody] WithdrawRequest httpRequest,
+ CancellationToken cancellationToken)
+ {
+ var request = new WithdrawBalance.Request(
+ httpRequest.SessionId,
+ httpRequest.Money);
+
+ WithdrawBalance.Response response = await _accountService
+ .WithdrawBalanceAsync(request, cancellationToken);
+
+ return response switch
+ {
+ WithdrawBalance.Response.Success success => Ok(success.Balance),
+ WithdrawBalance.Response.Failure failure => BadRequest(failure.ErrorMessage),
+ _ => throw new UnreachableException(),
+ };
+ }
+
+ [HttpPost("deposit")]
+ public async Task> DepositBalanceAsync(
+ [FromBody] DepositRequest httpRequest,
+ CancellationToken cancellationToken)
+ {
+ var request = new DepositBalance.Request(
+ httpRequest.SessionId,
+ httpRequest.Money);
+
+ DepositBalance.Response response = await _accountService
+ .DepositBalanceAsync(request, cancellationToken);
+
+ return response switch
+ {
+ DepositBalance.Response.Success success => Ok(success.Balance),
+ DepositBalance.Response.Failure failure => BadRequest(failure.ErrorMessage),
+ _ => throw new UnreachableException(),
+ };
+ }
+
+ [HttpGet("history")]
+ public async Task>> GetOperationHistoryAsync(
+ [FromQuery] OperationHistoryRequest httpRequest,
+ CancellationToken cancellationToken)
+ {
+ var request = new GetOperationHistory.Request(
+ httpRequest.SessionId);
+
+ GetOperationHistory.Response response = await _accountService
+ .GetOperationHistoryAsync(request, cancellationToken);
+
+ return response switch
+ {
+ GetOperationHistory.Response.Success success => Ok(success.OperationHistory),
+ GetOperationHistory.Response.Failure failure => BadRequest(failure.ErrorMessage),
+ _ => throw new UnreachableException(),
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Presentation/Controllers/AccountsController.cs b/src/Atm.Presentation/Controllers/AccountsController.cs
deleted file mode 100644
index debec63..0000000
--- a/src/Atm.Presentation/Controllers/AccountsController.cs
+++ /dev/null
@@ -1,128 +0,0 @@
-using Atm.Application.Contracts.Accounts.CreateAccount;
-using Atm.Application.Contracts.Accounts.GetBalance;
-using Atm.Application.Contracts.Operations.Deposit;
-using Atm.Application.Contracts.Operations.GetTransactionHistory;
-using Atm.Application.Contracts.Operations.Withdraw;
-using Atm.Presentation.Contracts;
-using Microsoft.AspNetCore.Mvc;
-
-namespace Atm.Presentation.Controllers;
-
-[ApiController]
-[Route("api/accounts")]
-public sealed class AccountsController : ControllerBase
-{
- private readonly ICreateAccountPort _createAccount;
- private readonly IGetBalancePort _getBalance;
- private readonly IDepositPort _deposit;
- private readonly IWithdrawPort _withdraw;
- private readonly IGetTransactionHistoryPort _history;
-
- public AccountsController(
- ICreateAccountPort createAccount,
- IGetBalancePort getBalance,
- IDepositPort deposit,
- IWithdrawPort withdraw,
- IGetTransactionHistoryPort history)
- {
- _createAccount = createAccount;
- _getBalance = getBalance;
- _deposit = deposit;
- _withdraw = withdraw;
- _history = history;
- }
-
- [HttpPost("create")]
- [ProducesResponseType(typeof(MessageResponse), StatusCodes.Status200OK)]
- [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
- [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status401Unauthorized)]
- public IActionResult Create([FromBody] CreateAccountApiRequest request)
- {
- CreateAccountResult result = _createAccount.Execute(
- new CreateAccountRequest(
- request.AdminSessionId,
- request.AccountNumber,
- request.Pin,
- request.InitialBalance));
-
- return result switch
- {
- CreateAccountResult.Success => Ok(new MessageResponse("Account created")),
- CreateAccountResult.Unauthorized u => Unauthorized(new ErrorResponse(u.ErrorMessage)),
- CreateAccountResult.Failure f => BadRequest(new ErrorResponse(f.ErrorMessage)),
- _ => BadRequest(new ErrorResponse("Unknown error")),
- };
- }
-
- [HttpGet("balance")]
- [ProducesResponseType(typeof(BalanceResponse), StatusCodes.Status200OK)]
- [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
- [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status401Unauthorized)]
- public IActionResult Balance([FromQuery(Name = "sessionId")] Guid userSessionId)
- {
- GetBalanceResult result = _getBalance.Execute(
- new GetBalanceRequest(userSessionId));
-
- return result switch
- {
- GetBalanceResult.Success s => Ok(new BalanceResponse(s.Balance)),
- GetBalanceResult.Unauthorized u => Unauthorized(new ErrorResponse(u.ErrorMessage)),
- GetBalanceResult.Failure f => BadRequest(new ErrorResponse(f.ErrorMessage)),
- _ => BadRequest(new ErrorResponse("Unknown error")),
- };
- }
-
- [HttpPost("deposit")]
- [ProducesResponseType(typeof(BalanceResponse), StatusCodes.Status200OK)]
- [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
- [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status401Unauthorized)]
- public IActionResult Deposit([FromBody] AmountApiRequest request)
- {
- DepositResult result = _deposit.Execute(
- new DepositRequest(request.UserSessionId, request.Amount));
-
- return result switch
- {
- DepositResult.Success s => Ok(new BalanceResponse(s.Balance)),
- DepositResult.Unauthorized u => Unauthorized(new ErrorResponse(u.ErrorMessage)),
- DepositResult.Failure f => BadRequest(new ErrorResponse(f.ErrorMessage)),
- _ => BadRequest(new ErrorResponse("Unknown error")),
- };
- }
-
- [HttpPost("withdraw")]
- [ProducesResponseType(typeof(BalanceResponse), StatusCodes.Status200OK)]
- [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
- [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status401Unauthorized)]
- public IActionResult Withdraw([FromBody] AmountApiRequest request)
- {
- WithdrawResult result = _withdraw.Execute(
- new WithdrawRequest(request.UserSessionId, request.Amount));
-
- return result switch
- {
- WithdrawResult.Success s => Ok(new BalanceResponse(s.Balance)),
- WithdrawResult.Unauthorized u => Unauthorized(new ErrorResponse(u.ErrorMessage)),
- WithdrawResult.Failure f => BadRequest(new ErrorResponse(f.ErrorMessage)),
- _ => BadRequest(new ErrorResponse("Unknown error")),
- };
- }
-
- [HttpGet("history")]
- [ProducesResponseType(typeof(TransactionsResponse), StatusCodes.Status200OK)]
- [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
- [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status401Unauthorized)]
- public IActionResult History([FromQuery(Name = "sessionId")] Guid userSessionId)
- {
- GetTransactionHistoryResult result = _history.Execute(
- new GetTransactionHistoryRequest(userSessionId));
-
- return result switch
- {
- GetTransactionHistoryResult.Success s => Ok(new TransactionsResponse(s.Transactions)),
- GetTransactionHistoryResult.Unauthorized u => Unauthorized(new ErrorResponse(u.ErrorMessage)),
- GetTransactionHistoryResult.Failure f => BadRequest(new ErrorResponse(f.ErrorMessage)),
- _ => BadRequest(new ErrorResponse("Unknown error")),
- };
- }
-}
diff --git a/src/Atm.Presentation/Controllers/AdminController.cs b/src/Atm.Presentation/Controllers/AdminController.cs
new file mode 100644
index 0000000..edb3e9f
--- /dev/null
+++ b/src/Atm.Presentation/Controllers/AdminController.cs
@@ -0,0 +1,39 @@
+using Atm.Application.Contracts.Admins;
+using Atm.Application.Contracts.Admins.Models;
+using Atm.Application.Contracts.Admins.Operations;
+using Atm.Presentation.Models;
+using Microsoft.AspNetCore.Mvc;
+using System.Diagnostics;
+
+namespace Atm.Presentation.Controllers;
+
+[ApiController]
+[Route("/api/admin")]
+public sealed class AdminController : ControllerBase
+{
+ private readonly IAdminService _adminService;
+
+ public AdminController(IAdminService adminService)
+ {
+ _adminService = adminService;
+ }
+
+ [HttpPost("accounts")]
+ public async Task> CreateAccountAsync(
+ CreateAccountRequest httpRequest,
+ CancellationToken cancellationToken)
+ {
+ var request = new CreateAccount.Request(
+ httpRequest.SessionId,
+ httpRequest.AccountNumber,
+ httpRequest.PinCode);
+
+ CreateAccount.Response response = await _adminService.CreateAccountAsync(request, cancellationToken);
+ return response switch
+ {
+ CreateAccount.Response.Success success => Ok(success.Account),
+ CreateAccount.Response.Failure failure => BadRequest(failure.ErrorMessage),
+ _ => throw new UnreachableException(),
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Presentation/Controllers/SessionController.cs b/src/Atm.Presentation/Controllers/SessionController.cs
new file mode 100644
index 0000000..8c4539d
--- /dev/null
+++ b/src/Atm.Presentation/Controllers/SessionController.cs
@@ -0,0 +1,63 @@
+using Atm.Application.Contracts.Sessions;
+using Atm.Application.Contracts.Sessions.Models;
+using Atm.Application.Contracts.Sessions.Operations;
+using Atm.Presentation.Models;
+using Microsoft.AspNetCore.Mvc;
+using System.Diagnostics;
+
+namespace Atm.Presentation.Controllers;
+
+[ApiController]
+[Route("/api/sessions")]
+public sealed class SessionController : ControllerBase
+{
+ private readonly ISessionService _sessionService;
+
+ public SessionController(ISessionService sessionService)
+ {
+ _sessionService = sessionService;
+ }
+
+ [HttpPost("user")]
+ public async Task> CreateUserSessionAsync(
+ [FromBody] CreateUserSessionRequest httpRequest,
+ CancellationToken cancellationToken)
+ {
+ var request = new CreateUserSession.Request(
+ httpRequest.AccountNumber,
+ httpRequest.PinCode);
+
+ CreateUserSession.Response response = await _sessionService
+ .CreateUserSessionAsync(
+ request,
+ cancellationToken);
+
+ return response switch
+ {
+ CreateUserSession.Response.Success success => Ok(success.Session),
+ CreateUserSession.Response.Failure failure => Unauthorized(failure.ErrorMessage),
+ _ => throw new UnreachableException(),
+ };
+ }
+
+ [HttpPost("admin")]
+ public async Task> CreateAdminSessionAsync(
+ [FromBody] CreateAdminSessionRequest httpRequest,
+ CancellationToken cancellationToken)
+ {
+ var request = new CreateAdminSession.Request(
+ httpRequest.SystemPassword);
+
+ CreateAdminSession.Response response = await _sessionService
+ .CreateAdminSessionAsync(
+ request,
+ cancellationToken);
+
+ return response switch
+ {
+ CreateAdminSession.Response.Success success => Ok(success.Session),
+ CreateAdminSession.Response.Failure failure => Unauthorized(failure.ErrorMessage),
+ _ => throw new UnreachableException(),
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Presentation/Controllers/SessionsController.cs b/src/Atm.Presentation/Controllers/SessionsController.cs
deleted file mode 100644
index 984bb69..0000000
--- a/src/Atm.Presentation/Controllers/SessionsController.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-using Atm.Application.Contracts.Sessions.CreateAdminSession;
-using Atm.Application.Contracts.Sessions.CreateUserSession;
-using Atm.Presentation.Contracts;
-using Microsoft.AspNetCore.Mvc;
-
-namespace Atm.Presentation.Controllers;
-
-[ApiController]
-[Route("api/sessions")]
-public sealed class SessionsController : ControllerBase
-{
- private readonly ICreateUserSessionPort _createUserSession;
- private readonly ICreateAdminSessionPort _createAdminSession;
-
- public SessionsController(
- ICreateUserSessionPort createUserSession,
- ICreateAdminSessionPort createAdminSession)
- {
- _createUserSession = createUserSession;
- _createAdminSession = createAdminSession;
- }
-
- [HttpPost("user")]
- [ProducesResponseType(typeof(SessionResponse), StatusCodes.Status200OK)]
- [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
- [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status401Unauthorized)]
- public IActionResult CreateUser([FromBody] CreateUserSessionApiRequest request)
- {
- if (string.IsNullOrWhiteSpace(request.AccountNumber) || string.IsNullOrWhiteSpace(request.Pin))
- {
- return BadRequest(new ErrorResponse("AccountNumber and Pin are required"));
- }
-
- CreateUserSessionResult result = _createUserSession.Execute(
- new CreateUserSessionRequest(request.AccountNumber, request.Pin));
-
- return result switch
- {
- CreateUserSessionResult.Success s => Ok(new SessionResponse(s.SessionId)),
- CreateUserSessionResult.Unauthorized u => Unauthorized(new ErrorResponse(u.ErrorMessage)),
- CreateUserSessionResult.Failure f => BadRequest(new ErrorResponse(f.ErrorMessage)),
- _ => BadRequest(new ErrorResponse("Unknown error")),
- };
- }
-
- [HttpPost("admin")]
- [ProducesResponseType(typeof(SessionResponse), StatusCodes.Status200OK)]
- [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
- [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status401Unauthorized)]
- public IActionResult CreateAdmin([FromBody] CreateAdminSessionApiRequest request)
- {
- if (string.IsNullOrWhiteSpace(request.SystemPassword) || string.IsNullOrWhiteSpace(request.AccountNumber))
- {
- return BadRequest(new ErrorResponse("SystemPassword and AccountNumber are required"));
- }
-
- CreateAdminSessionResult result = _createAdminSession.Execute(
- new CreateAdminSessionRequest(request.SystemPassword, request.AccountNumber));
-
- return result switch
- {
- CreateAdminSessionResult.Success s => Ok(new SessionResponse(s.SessionId)),
- CreateAdminSessionResult.Unauthorized u => Unauthorized(new ErrorResponse(u.ErrorMessage)),
- CreateAdminSessionResult.Failure f => BadRequest(new ErrorResponse(f.ErrorMessage)),
- _ => BadRequest(new ErrorResponse("Unknown result")),
- };
- }
-}
diff --git a/src/Atm.Presentation/Models/BalanceRequest.cs b/src/Atm.Presentation/Models/BalanceRequest.cs
new file mode 100644
index 0000000..1328d78
--- /dev/null
+++ b/src/Atm.Presentation/Models/BalanceRequest.cs
@@ -0,0 +1,6 @@
+namespace Atm.Presentation.Models;
+
+public sealed class BalanceRequest
+{
+ public required Guid SessionId { get; init; }
+}
\ No newline at end of file
diff --git a/src/Atm.Presentation/Models/CreateAccountRequest.cs b/src/Atm.Presentation/Models/CreateAccountRequest.cs
new file mode 100644
index 0000000..db284c6
--- /dev/null
+++ b/src/Atm.Presentation/Models/CreateAccountRequest.cs
@@ -0,0 +1,16 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Atm.Presentation.Models;
+
+public sealed class CreateAccountRequest
+{
+ public required Guid SessionId { get; init; }
+
+ [Required]
+ [RegularExpression(@"^\d{20}$", ErrorMessage = "Account number must be exactly 20 digits long.")]
+ public required string AccountNumber { get; init; }
+
+ [Required]
+ [RegularExpression(@"^\d{4}$", ErrorMessage = "Pincode must be exactly 4 digits long.")]
+ public required string PinCode { get; init; }
+}
\ No newline at end of file
diff --git a/src/Atm.Presentation/Models/CreateAdminSessionRequest.cs b/src/Atm.Presentation/Models/CreateAdminSessionRequest.cs
new file mode 100644
index 0000000..13758c9
--- /dev/null
+++ b/src/Atm.Presentation/Models/CreateAdminSessionRequest.cs
@@ -0,0 +1,10 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Atm.Presentation.Models;
+
+public sealed class CreateAdminSessionRequest
+{
+ [Required]
+ [MinLength(5, ErrorMessage = "System password must be at least 5 characters long.")]
+ public required string SystemPassword { get; init; }
+}
\ No newline at end of file
diff --git a/src/Atm.Presentation/Models/CreateUserSessionRequest.cs b/src/Atm.Presentation/Models/CreateUserSessionRequest.cs
new file mode 100644
index 0000000..72c5f4c
--- /dev/null
+++ b/src/Atm.Presentation/Models/CreateUserSessionRequest.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Atm.Presentation.Models;
+
+public sealed class CreateUserSessionRequest
+{
+ [Required]
+ [RegularExpression(@"^\d{20}$", ErrorMessage = "Account number must be exactly 20 digits long.")]
+ public required string AccountNumber { get; init; }
+
+ [Required]
+ [RegularExpression(@"^\d{4}$", ErrorMessage = "Pincode must be exactly 4 digits long.")]
+ public required string PinCode { get; init; }
+}
\ No newline at end of file
diff --git a/src/Atm.Presentation/Models/DepositRequest.cs b/src/Atm.Presentation/Models/DepositRequest.cs
new file mode 100644
index 0000000..826d712
--- /dev/null
+++ b/src/Atm.Presentation/Models/DepositRequest.cs
@@ -0,0 +1,8 @@
+namespace Atm.Presentation.Models;
+
+public sealed class DepositRequest
+{
+ public required Guid SessionId { get; init; }
+
+ public required decimal Money { get; init; }
+}
\ No newline at end of file
diff --git a/src/Atm.Presentation/Models/OperationHistoryRequest.cs b/src/Atm.Presentation/Models/OperationHistoryRequest.cs
new file mode 100644
index 0000000..9998d37
--- /dev/null
+++ b/src/Atm.Presentation/Models/OperationHistoryRequest.cs
@@ -0,0 +1,6 @@
+namespace Atm.Presentation.Models;
+
+public sealed class OperationHistoryRequest
+{
+ public required Guid SessionId { get; init; }
+}
\ No newline at end of file
diff --git a/src/Atm.Presentation/Models/WithdrawRequest.cs b/src/Atm.Presentation/Models/WithdrawRequest.cs
new file mode 100644
index 0000000..697cc61
--- /dev/null
+++ b/src/Atm.Presentation/Models/WithdrawRequest.cs
@@ -0,0 +1,8 @@
+namespace Atm.Presentation.Models;
+
+public sealed class WithdrawRequest
+{
+ public required Guid SessionId { get; init; }
+
+ public required decimal Money { get; init; }
+}
\ No newline at end of file
diff --git a/src/Atm.Presentation/Program.cs b/src/Atm.Presentation/Program.cs
deleted file mode 100644
index 8cf0616..0000000
--- a/src/Atm.Presentation/Program.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using Atm.Application;
-using Atm.Application.Abstractions.Repositories;
-using Atm.Infrastructure;
-using Atm.Presentation.Common;
-
-WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
-
-builder.Services.AddControllers().AddJsonOptions(options =>
-{
- options.JsonSerializerOptions.PropertyNamingPolicy = null;
- options.JsonSerializerOptions.ReadCommentHandling = System.Text.Json.JsonCommentHandling.Skip;
- options.JsonSerializerOptions.AllowTrailingCommas = true;
-});
-
-builder.Services.AddEndpointsApiExplorer();
-builder.Services.AddSwaggerGen();
-
-builder.Services.AddApplication();
-builder.Services.AddInfrastructure();
-
-builder.Services.AddSingleton();
-
-WebApplication app = builder.Build();
-
-if (app.Environment.IsDevelopment())
-{
- app.UseSwagger();
- app.UseSwaggerUI();
-}
-
-app.UseHttpsRedirection();
-app.MapControllers();
-
-app.Run();
\ No newline at end of file
diff --git a/src/Atm.Presentation/ServiceCollectionExtensions.cs b/src/Atm.Presentation/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..0e77c03
--- /dev/null
+++ b/src/Atm.Presentation/ServiceCollectionExtensions.cs
@@ -0,0 +1,21 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Atm.Presentation;
+
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddPresentationHttp(
+ this IServiceCollection collection)
+ {
+ collection.AddControllers();
+ return collection;
+ }
+
+ public static WebApplication UsePresentationHttp(
+ this WebApplication application)
+ {
+ application.MapControllers();
+ return application;
+ }
+}
\ No newline at end of file
diff --git a/src/Atm.Presentation/packages.lock.json b/src/Atm.Presentation/packages.lock.json
deleted file mode 100644
index dfd4ccc..0000000
--- a/src/Atm.Presentation/packages.lock.json
+++ /dev/null
@@ -1,84 +0,0 @@
-{
- "version": 1,
- "dependencies": {
- "net9.0": {
- "Swashbuckle.AspNetCore": {
- "type": "Direct",
- "requested": "[6.6.2, )",
- "resolved": "6.6.2",
- "contentHash": "+NB4UYVYN6AhDSjW0IJAd1AGD8V33gemFNLPaxKTtPkHB+HaKAKf9MGAEUPivEWvqeQfcKIw8lJaHq6LHljRuw==",
- "dependencies": {
- "Microsoft.Extensions.ApiDescription.Server": "6.0.5",
- "Swashbuckle.AspNetCore.Swagger": "6.6.2",
- "Swashbuckle.AspNetCore.SwaggerGen": "6.6.2",
- "Swashbuckle.AspNetCore.SwaggerUI": "6.6.2"
- }
- },
- "Microsoft.Extensions.ApiDescription.Server": {
- "type": "Transitive",
- "resolved": "6.0.5",
- "contentHash": "Ckb5EDBUNJdFWyajfXzUIMRkhf52fHZOQuuZg/oiu8y7zDCVwD0iHhew6MnThjHmevanpxL3f5ci2TtHQEN6bw=="
- },
- "Microsoft.Extensions.DependencyInjection.Abstractions": {
- "type": "Transitive",
- "resolved": "10.0.2",
- "contentHash": "zOIurr59+kUf9vNcsUkCvKWZv+fPosUZXURZesYkJCvl0EzTc9F7maAO4Cd2WEV7ZJJ0AZrFQvuH6Npph9wdBw=="
- },
- "Microsoft.OpenApi": {
- "type": "Transitive",
- "resolved": "1.6.14",
- "contentHash": "tTaBT8qjk3xINfESyOPE2rIellPvB7qpVqiWiyA/lACVvz+xOGiXhFUfohcx82NLbi5avzLW0lx+s6oAqQijfw=="
- },
- "Swashbuckle.AspNetCore.Swagger": {
- "type": "Transitive",
- "resolved": "6.6.2",
- "contentHash": "ovgPTSYX83UrQUWiS5vzDcJ8TEX1MAxBgDFMK45rC24MorHEPQlZAHlaXj/yth4Zf6xcktpUgTEBvffRQVwDKA==",
- "dependencies": {
- "Microsoft.OpenApi": "1.6.14"
- }
- },
- "Swashbuckle.AspNetCore.SwaggerGen": {
- "type": "Transitive",
- "resolved": "6.6.2",
- "contentHash": "zv4ikn4AT1VYuOsDCpktLq4QDq08e7Utzbir86M5/ZkRaLXbCPF11E1/vTmOiDzRTl0zTZINQU2qLKwTcHgfrA==",
- "dependencies": {
- "Swashbuckle.AspNetCore.Swagger": "6.6.2"
- }
- },
- "Swashbuckle.AspNetCore.SwaggerUI": {
- "type": "Transitive",
- "resolved": "6.6.2",
- "contentHash": "mBBb+/8Hm2Q3Wygag+hu2jj69tZW5psuv0vMRXY07Wy+Rrj40vRP8ZTbKBhs91r45/HXT4aY4z0iSBYx1h6JvA=="
- },
- "atm.application": {
- "type": "Project",
- "dependencies": {
- "Atm.Application.Abstractions": "[1.0.0, )",
- "Atm.Application.Contracts": "[1.0.0, )",
- "Atm.Domain": "[1.0.0, )",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.2, )"
- }
- },
- "atm.application.abstractions": {
- "type": "Project",
- "dependencies": {
- "Atm.Domain": "[1.0.0, )"
- }
- },
- "atm.application.contracts": {
- "type": "Project"
- },
- "atm.domain": {
- "type": "Project"
- },
- "atm.infrastructure": {
- "type": "Project",
- "dependencies": {
- "Atm.Application.Abstractions": "[1.0.0, )",
- "Atm.Domain": "[1.0.0, )",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.2, )"
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/tests/Atm.Application.Tests/Atm.Application.Tests.csproj b/tests/Atm.Application.Tests/Atm.Application.Tests.csproj
deleted file mode 100644
index 6ba4c8d..0000000
--- a/tests/Atm.Application.Tests/Atm.Application.Tests.csproj
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
- net9.0
- enable
- enable
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/tests/Atm.Application.Tests/DepositUseCaseTests.cs b/tests/Atm.Application.Tests/DepositUseCaseTests.cs
deleted file mode 100644
index 565bb54..0000000
--- a/tests/Atm.Application.Tests/DepositUseCaseTests.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using Atm.Application.Abstractions.Repositories;
-using Atm.Application.Common;
-using Atm.Application.Contracts.Operations.Deposit;
-using Atm.Application.Operations;
-using Atm.Domain.Accounts;
-using Atm.Domain.Operations;
-using Atm.Domain.Sessions;
-using Atm.Domain.ValueObjects;
-using NSubstitute;
-
-namespace Atm.Application.Tests;
-
-public sealed class DepositUseCaseTests
-{
- [Fact]
- public void DepositWhenValidIncreaseBalanceAndSavesOperation()
- {
- IAccountRepository accounts = Substitute.For();
- ISessionRepository sessions = Substitute.For();
- IOperationRepository operations = Substitute.For();
-
- var accountNumber = new AccountNumber("aboba");
- var sessionId = Guid.NewGuid();
-
- sessions.GetById(sessionId).Returns(new Session(sessionId, SessionType.User, accountNumber));
-
- var account = new Account(accountNumber, new PinCode("1234"), new Money(100m));
- accounts.GetByNumber(accountNumber).Returns(account);
-
- var auth = new SessionAuthorizer(sessions);
- var useCase = new DepositUseCase(accounts, operations, auth);
-
- DepositResult result = useCase.Execute(new DepositRequest(sessionId, 50m));
-
- Assert.IsType(result);
- Assert.Equal(150m, ((DepositResult.Success)result).Balance);
-
- accounts.Received(1).Update(Arg.Is(a => a.Balance.Value == 150m));
-
- operations.Received(1).Add(Arg.Is(op =>
- op.AccountNumber == accountNumber &&
- op.Type == OperationType.Deposit &&
- op.Amount.Value == 50m &&
- op.BalanceAfter.Value == 150m));
- }
-}
\ No newline at end of file
diff --git a/tests/Atm.Application.Tests/WithdrawCaseTests.cs b/tests/Atm.Application.Tests/WithdrawCaseTests.cs
deleted file mode 100644
index 0b53806..0000000
--- a/tests/Atm.Application.Tests/WithdrawCaseTests.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using Atm.Application.Abstractions.Repositories;
-using Atm.Application.Common;
-using Atm.Application.Contracts.Operations.Withdraw;
-using Atm.Application.Operations;
-using Atm.Domain.Accounts;
-using Atm.Domain.Operations;
-using Atm.Domain.Sessions;
-using Atm.Domain.ValueObjects;
-using NSubstitute;
-
-namespace Atm.Application.Tests;
-
-public sealed class WithdrawCaseTests
-{
- [Fact]
- public void WithdrawWhenEnoughBalanceDecreasesBalanceAndSavesOperation()
- {
- IAccountRepository accounts = Substitute.For();
- ISessionRepository sessions = Substitute.For();
- IOperationRepository operations = Substitute.For();
-
- var accountNumber = new AccountNumber("aboba-2");
- var sessionId = Guid.NewGuid();
-
- sessions.GetById(sessionId).Returns(new Session(sessionId, SessionType.User, accountNumber));
-
- var account = new Account(accountNumber, new PinCode("6666"), new Money(100m));
- accounts.GetByNumber(accountNumber).Returns(account);
-
- var auth = new SessionAuthorizer(sessions);
- var useCase = new WithdrawUseCase(accounts, operations, auth);
-
- WithdrawResult result = useCase.Execute(new WithdrawRequest(sessionId, 40m));
-
- Assert.IsType(result);
- Assert.Equal(60m, ((WithdrawResult.Success)result).Balance);
-
- accounts.Received(1).Update(Arg.Is(a => a.Balance.Value == 60m));
-
- operations.Received(1).Add(Arg.Is(op =>
- op.AccountNumber == accountNumber &&
- op.Type == OperationType.Withdraw &&
- op.Amount.Value == 40m &&
- op.BalanceAfter.Value == 60m));
- }
-
- [Fact]
- public void WithdrawWhenNotEnoughBalanceReturnsFailureAndDoesNotSave()
- {
- IAccountRepository accounts = Substitute.For();
- ISessionRepository sessions = Substitute.For();
- IOperationRepository operations = Substitute.For();
-
- var accountNumber = new AccountNumber("aboba-666");
- var sessionId = Guid.NewGuid();
-
- sessions.GetById(sessionId).Returns(new Session(sessionId, SessionType.User, accountNumber));
-
- var account = new Account(accountNumber, new PinCode("9999"), new Money(10m));
- accounts.GetByNumber(accountNumber).Returns(account);
-
- var auth = new SessionAuthorizer(sessions);
- var useCase = new WithdrawUseCase(accounts, operations, auth);
-
- WithdrawResult result = useCase.Execute(new WithdrawRequest(sessionId, 20m));
-
- Assert.IsType(result);
- accounts.DidNotReceive().Update(Arg.Any());
- operations.DidNotReceive().Add(Arg.Any());
- }
-}
\ No newline at end of file
diff --git a/tests/Atm.Application.Tests/packages.lock.json b/tests/Atm.Application.Tests/packages.lock.json
deleted file mode 100644
index d8d50bb..0000000
--- a/tests/Atm.Application.Tests/packages.lock.json
+++ /dev/null
@@ -1,160 +0,0 @@
-{
- "version": 1,
- "dependencies": {
- "net9.0": {
- "coverlet.collector": {
- "type": "Direct",
- "requested": "[6.0.2, )",
- "resolved": "6.0.2",
- "contentHash": "bJShQ6uWRTQ100ZeyiMqcFlhP7WJ+bCuabUs885dJiBEzMsJMSFr7BOyeCw4rgvQokteGi5rKQTlkhfQPUXg2A=="
- },
- "Microsoft.NET.Test.Sdk": {
- "type": "Direct",
- "requested": "[17.12.0, )",
- "resolved": "17.12.0",
- "contentHash": "kt/PKBZ91rFCWxVIJZSgVLk+YR+4KxTuHf799ho8WNiK5ZQpJNAEZCAWX86vcKrs+DiYjiibpYKdGZP6+/N17w==",
- "dependencies": {
- "Microsoft.CodeCoverage": "17.12.0",
- "Microsoft.TestPlatform.TestHost": "17.12.0"
- }
- },
- "NSubstitute": {
- "type": "Direct",
- "requested": "[5.3.0, )",
- "resolved": "5.3.0",
- "contentHash": "lJ47Cps5Qzr86N99lcwd+OUvQma7+fBgr8+Mn+aOC0WrlqMNkdivaYD9IvnZ5Mqo6Ky3LS7ZI+tUq1/s9ERd0Q==",
- "dependencies": {
- "Castle.Core": "5.1.1"
- }
- },
- "xunit": {
- "type": "Direct",
- "requested": "[2.9.2, )",
- "resolved": "2.9.2",
- "contentHash": "7LhFS2N9Z6Xgg8aE5lY95cneYivRMfRI8v+4PATa4S64D5Z/Plkg0qa8dTRHSiGRgVZ/CL2gEfJDE5AUhOX+2Q==",
- "dependencies": {
- "xunit.analyzers": "1.16.0",
- "xunit.assert": "2.9.2",
- "xunit.core": "[2.9.2]"
- }
- },
- "xunit.runner.visualstudio": {
- "type": "Direct",
- "requested": "[2.8.2, )",
- "resolved": "2.8.2",
- "contentHash": "vm1tbfXhFmjFMUmS4M0J0ASXz3/U5XvXBa6DOQUL3fEz4Vt6YPhv+ESCarx6M6D+9kJkJYZKCNvJMas1+nVfmQ=="
- },
- "Castle.Core": {
- "type": "Transitive",
- "resolved": "5.1.1",
- "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==",
- "dependencies": {
- "System.Diagnostics.EventLog": "6.0.0"
- }
- },
- "Microsoft.CodeCoverage": {
- "type": "Transitive",
- "resolved": "17.12.0",
- "contentHash": "4svMznBd5JM21JIG2xZKGNanAHNXplxf/kQDFfLHXQ3OnpJkayRK/TjacFjA+EYmoyuNXHo/sOETEfcYtAzIrA=="
- },
- "Microsoft.Extensions.DependencyInjection.Abstractions": {
- "type": "Transitive",
- "resolved": "10.0.2",
- "contentHash": "zOIurr59+kUf9vNcsUkCvKWZv+fPosUZXURZesYkJCvl0EzTc9F7maAO4Cd2WEV7ZJJ0AZrFQvuH6Npph9wdBw=="
- },
- "Microsoft.TestPlatform.ObjectModel": {
- "type": "Transitive",
- "resolved": "17.12.0",
- "contentHash": "TDqkTKLfQuAaPcEb3pDDWnh7b3SyZF+/W9OZvWFp6eJCIiiYFdSB6taE2I6tWrFw5ywhzOb6sreoGJTI6m3rSQ==",
- "dependencies": {
- "System.Reflection.Metadata": "1.6.0"
- }
- },
- "Microsoft.TestPlatform.TestHost": {
- "type": "Transitive",
- "resolved": "17.12.0",
- "contentHash": "MiPEJQNyADfwZ4pJNpQex+t9/jOClBGMiCiVVFuELCMSX2nmNfvUor3uFVxNNCg30uxDP8JDYfPnMXQzsfzYyg==",
- "dependencies": {
- "Microsoft.TestPlatform.ObjectModel": "17.12.0",
- "Newtonsoft.Json": "13.0.1"
- }
- },
- "Newtonsoft.Json": {
- "type": "Transitive",
- "resolved": "13.0.1",
- "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A=="
- },
- "System.Diagnostics.EventLog": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw=="
- },
- "System.Reflection.Metadata": {
- "type": "Transitive",
- "resolved": "1.6.0",
- "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ=="
- },
- "xunit.abstractions": {
- "type": "Transitive",
- "resolved": "2.0.3",
- "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg=="
- },
- "xunit.analyzers": {
- "type": "Transitive",
- "resolved": "1.16.0",
- "contentHash": "hptYM7vGr46GUIgZt21YHO4rfuBAQS2eINbFo16CV/Dqq+24Tp+P5gDCACu1AbFfW4Sp/WRfDPSK8fmUUb8s0Q=="
- },
- "xunit.assert": {
- "type": "Transitive",
- "resolved": "2.9.2",
- "contentHash": "QkNBAQG4pa66cholm28AxijBjrmki98/vsEh4Sx5iplzotvPgpiotcxqJQMRC8d7RV7nIT8ozh97957hDnZwsQ=="
- },
- "xunit.core": {
- "type": "Transitive",
- "resolved": "2.9.2",
- "contentHash": "O6RrNSdmZ0xgEn5kT927PNwog5vxTtKrWMihhhrT0Sg9jQ7iBDciYOwzBgP2krBEk5/GBXI18R1lKvmnxGcb4w==",
- "dependencies": {
- "xunit.extensibility.core": "[2.9.2]",
- "xunit.extensibility.execution": "[2.9.2]"
- }
- },
- "xunit.extensibility.core": {
- "type": "Transitive",
- "resolved": "2.9.2",
- "contentHash": "Ol+KlBJz1x8BrdnhN2DeOuLrr1I/cTwtHCggL9BvYqFuVd/TUSzxNT5O0NxCIXth30bsKxgMfdqLTcORtM52yQ==",
- "dependencies": {
- "xunit.abstractions": "2.0.3"
- }
- },
- "xunit.extensibility.execution": {
- "type": "Transitive",
- "resolved": "2.9.2",
- "contentHash": "rKMpq4GsIUIJibXuZoZ8lYp5EpROlnYaRpwu9Zr0sRZXE7JqJfEEbCsUriZqB+ByXCLFBJyjkTRULMdC+U566g==",
- "dependencies": {
- "xunit.extensibility.core": "[2.9.2]"
- }
- },
- "atm.application": {
- "type": "Project",
- "dependencies": {
- "Atm.Application.Abstractions": "[1.0.0, )",
- "Atm.Application.Contracts": "[1.0.0, )",
- "Atm.Domain": "[1.0.0, )",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.2, )"
- }
- },
- "atm.application.abstractions": {
- "type": "Project",
- "dependencies": {
- "Atm.Domain": "[1.0.0, )"
- }
- },
- "atm.application.contracts": {
- "type": "Project"
- },
- "atm.domain": {
- "type": "Project"
- }
- }
- }
-}
\ No newline at end of file