Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "9.0.19",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}
30 changes: 30 additions & 0 deletions Atm.sln
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atm.Application.Tests", "tests\Atm.Application.Tests\Atm.Application.Tests.csproj", "{2DEDE88D-E43A-4FBC-A2E2-0AE5AD2945B5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atm.Domain.Tests", "tests\Atm.Domain.Tests\Atm.Domain.Tests.csproj", "{D9ACF0B4-7830-4D65-96E2-49EC72B8DD3A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atm.Infrastructure.Tests", "tests\Atm.Infrastructure.Tests\Atm.Infrastructure.Tests.csproj", "{F2E5698E-1037-4F49-A653-1A7D60B909E4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -115,6 +119,30 @@ Global
{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
{D9ACF0B4-7830-4D65-96E2-49EC72B8DD3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D9ACF0B4-7830-4D65-96E2-49EC72B8DD3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D9ACF0B4-7830-4D65-96E2-49EC72B8DD3A}.Debug|x64.ActiveCfg = Debug|Any CPU
{D9ACF0B4-7830-4D65-96E2-49EC72B8DD3A}.Debug|x64.Build.0 = Debug|Any CPU
{D9ACF0B4-7830-4D65-96E2-49EC72B8DD3A}.Debug|x86.ActiveCfg = Debug|Any CPU
{D9ACF0B4-7830-4D65-96E2-49EC72B8DD3A}.Debug|x86.Build.0 = Debug|Any CPU
{D9ACF0B4-7830-4D65-96E2-49EC72B8DD3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D9ACF0B4-7830-4D65-96E2-49EC72B8DD3A}.Release|Any CPU.Build.0 = Release|Any CPU
{D9ACF0B4-7830-4D65-96E2-49EC72B8DD3A}.Release|x64.ActiveCfg = Release|Any CPU
{D9ACF0B4-7830-4D65-96E2-49EC72B8DD3A}.Release|x64.Build.0 = Release|Any CPU
{D9ACF0B4-7830-4D65-96E2-49EC72B8DD3A}.Release|x86.ActiveCfg = Release|Any CPU
{D9ACF0B4-7830-4D65-96E2-49EC72B8DD3A}.Release|x86.Build.0 = Release|Any CPU
{F2E5698E-1037-4F49-A653-1A7D60B909E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F2E5698E-1037-4F49-A653-1A7D60B909E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2E5698E-1037-4F49-A653-1A7D60B909E4}.Debug|x64.ActiveCfg = Debug|Any CPU
{F2E5698E-1037-4F49-A653-1A7D60B909E4}.Debug|x64.Build.0 = Debug|Any CPU
{F2E5698E-1037-4F49-A653-1A7D60B909E4}.Debug|x86.ActiveCfg = Debug|Any CPU
{F2E5698E-1037-4F49-A653-1A7D60B909E4}.Debug|x86.Build.0 = Debug|Any CPU
{F2E5698E-1037-4F49-A653-1A7D60B909E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F2E5698E-1037-4F49-A653-1A7D60B909E4}.Release|Any CPU.Build.0 = Release|Any CPU
{F2E5698E-1037-4F49-A653-1A7D60B909E4}.Release|x64.ActiveCfg = Release|Any CPU
{F2E5698E-1037-4F49-A653-1A7D60B909E4}.Release|x64.Build.0 = Release|Any CPU
{F2E5698E-1037-4F49-A653-1A7D60B909E4}.Release|x86.ActiveCfg = Release|Any CPU
{F2E5698E-1037-4F49-A653-1A7D60B909E4}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -127,5 +155,7 @@ Global
{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}
{D9ACF0B4-7830-4D65-96E2-49EC72B8DD3A} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{F2E5698E-1037-4F49-A653-1A7D60B909E4} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
1 change: 1 addition & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<AnalysisLevel>latest</AnalysisLevel>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>
234 changes: 89 additions & 145 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,223 +1,167 @@
# 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 and 13.2 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 project follows the hexagonal architecture approach:
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.

- **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
## Architecture

The layers are connected using `Microsoft.Extensions.DependencyInjection`.
- **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.

Each module provides extension methods for dependency registration:
Domain and Application have no EF Core or PostgreSQL dependency.

- `builder.Services.AddApplication();`
- `builder.Services.AddInfrastructure();`
## Validation and result style

## System Password Configuration
Small value objects such as `AccountNumber`, `UserLogin`, `Money`, and `PinCode` validate themselves in constructors and reject invalid values with `ArgumentException`.

The system password is read through `ISystemPasswordProvider`, implemented in `Atm.Presentation.Common.SystemPasswordProvider`, and is taken from the 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<T>` or one result hierarchy per primitive field.

`appsettings.json`:
## Money rules

```json
{
"SystemAuth": {
"SystemPassword": "your_password_here"
}
}
```
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.

## Running the API
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. Future PostgreSQL mappings use unconstrained `numeric` with scale checks instead of `numeric(19,4)`.

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.

```json
{
"systemPassword": "your_password_here",
"accountNumber": "acc-1"
}
```
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.

Successful response:
Session lifetimes are validated at startup:

```json
{
"sessionId": "admin-session-guid"
"Sessions": {
"UserLifetimeMinutes": 30,
"AdminLifetimeMinutes": 15
}
}
```

The returned `sessionId` is used as `adminSessionId` in admin-only requests.

### 2. Create an account

Requires an admin session.

`POST /api/accounts/create`
Both values must be positive.

```json
{
"adminSessionId": "admin-session-guid",
"accountNumber": "acc-1",
"pin": "1234",
"initialBalance": 100
}
```
## System password configuration

Successful response:
The administrator password comes from configuration through `ISystemPasswordProvider`:

```json
{
"message": "Account created"
"SystemAuth": {
"SystemPassword": "use-a-secret-provider"
}
}
```

### 3. Create a user session
Do not commit a real password. Override the development placeholder with user secrets or an environment variable.

Use this after an account has been created. The user signs in with the account number and PIN.
## API examples

`POST /api/sessions/user`
The local HTTP profile uses `http://localhost:5015`; Swagger is available at `http://localhost:5015/swagger` in Development.

```json
{
"accountNumber": "acc-1",
"pin": "1234"
}
```
### Create an administrator session

Successful response:
`POST /api/sessions/admin`

```json
{
"sessionId": "user-session-guid"
"systemPassword": "use-a-secret-provider",
"accountNumber": "legacy-admin-context"
}
```

The returned `sessionId` is used as the user session identifier for balance, deposit, withdrawal, and history operations.

### 4. View balance
`accountNumber` remains required only for backward compatibility with the existing HTTP contract. A system administrator session does not store or authorize against this number.

Requires a user session.
### Create an account

`GET /api/accounts/balance?sessionId=user-session-guid`

Successful response:
`POST /api/accounts/create`

```json
{
"balance": 100
"adminSessionId": "00000000-0000-0000-0000-000000000001",
"accountNumber": "acc-1",
"pin": "1234",
"initialBalance": 100,
"ownerUserId": "00000000-0000-0000-0000-000000000002"
}
```

### 5. Deposit money

Requires a user session.

`POST /api/accounts/deposit`
`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.

```json
{
"userSessionId": "user-session-guid",
"amount": 50
}
```
### Create a user session

Successful response:
`POST /api/sessions/user`

```json
{
"balance": 150
"accountNumber": "acc-1",
"pin": "1234"
}
```

### 6. Withdraw money

Requires a user session.
The returned `sessionId` authorizes balance, deposit, withdrawal, and history operations.

`POST /api/accounts/withdraw`
### Account operations

```json
{
"userSessionId": "user-session-guid",
"amount": 30
}
```
- `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}`

Successful response:
## Build and test

```json
{
"balance": 120
}
```powershell
dotnet restore Atm.sln --locked-mode
dotnet build Atm.sln -c Release --no-restore
dotnet test Atm.sln -c Release --no-build
```

### 7. View transaction history

Requires a user session.
Tests are separated by architectural layer:

`GET /api/accounts/history?sessionId=user-session-guid`
- `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 such as `PinHasher`.

Successful response:
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
{
"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.
- `AtmDbContext`, mappings, 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.
Do not treat the current in-memory repositories as production persistence.
7 changes: 7 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"sdk": {
"version": "9.0.100",
"rollForward": "latestFeature",
"allowPrerelease": false
}
}
Loading
Loading