From 6a9a9b7c996d00b3809a815deddfb6460bea2a2e Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sat, 29 Aug 2026 20:06:39 +0300 Subject: [PATCH 001/103] chore(persistence): add ef core postgres dependencies --- .config/dotnet-tools.json | 13 + Directory.Build.props | 1 + global.json | 7 + .../packages.lock.json | 6 +- src/Atm.Application/Atm.Application.csproj | 2 +- src/Atm.Application/packages.lock.json | 6 +- .../Atm.Infrastructure.csproj | 12 +- src/Atm.Infrastructure/packages.lock.json | 326 +++++++++++++++++- src/Atm.Presentation/packages.lock.json | 129 ++++++- .../Atm.Application.Tests/packages.lock.json | 6 +- 10 files changed, 488 insertions(+), 20 deletions(-) create mode 100644 .config/dotnet-tools.json create mode 100644 global.json diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..e5d8851 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "9.0.19", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index fd2910d..85cf783 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,5 +3,6 @@ true true latest + true \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 0000000..bf1f678 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "9.0.100", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} \ No newline at end of file diff --git a/src/Atm.Application.Contracts/packages.lock.json b/src/Atm.Application.Contracts/packages.lock.json index ba655ce..a07e21a 100644 --- a/src/Atm.Application.Contracts/packages.lock.json +++ b/src/Atm.Application.Contracts/packages.lock.json @@ -1,10 +1,6 @@ { "version": 1, "dependencies": { - "net9.0": { - "atm.domain": { - "type": "Project" - } - } + "net9.0": {} } } \ No newline at end of file diff --git a/src/Atm.Application/Atm.Application.csproj b/src/Atm.Application/Atm.Application.csproj index d64769e..2424e34 100644 --- a/src/Atm.Application/Atm.Application.csproj +++ b/src/Atm.Application/Atm.Application.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Atm.Application/packages.lock.json b/src/Atm.Application/packages.lock.json index 60a845a..e4e5f9e 100644 --- a/src/Atm.Application/packages.lock.json +++ b/src/Atm.Application/packages.lock.json @@ -4,9 +4,9 @@ "net9.0": { "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Direct", - "requested": "[10.0.2, )", - "resolved": "10.0.2", - "contentHash": "zOIurr59+kUf9vNcsUkCvKWZv+fPosUZXURZesYkJCvl0EzTc9F7maAO4Cd2WEV7ZJJ0AZrFQvuH6Npph9wdBw==" + "requested": "[9.0.19, )", + "resolved": "9.0.19", + "contentHash": "XgQ0aVWClYjYSBqbYrKC/xyZ1KHf+VVwNF3+d54jcaqnst30Q//ugs/FJwUztLZiph0gp85g7i/eB0556twO0Q==" }, "atm.application.abstractions": { "type": "Project", diff --git a/src/Atm.Infrastructure/Atm.Infrastructure.csproj b/src/Atm.Infrastructure/Atm.Infrastructure.csproj index e759e49..6200b48 100644 --- a/src/Atm.Infrastructure/Atm.Infrastructure.csproj +++ b/src/Atm.Infrastructure/Atm.Infrastructure.csproj @@ -6,7 +6,17 @@ - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/src/Atm.Infrastructure/packages.lock.json b/src/Atm.Infrastructure/packages.lock.json index 9fadaef..b27447b 100644 --- a/src/Atm.Infrastructure/packages.lock.json +++ b/src/Atm.Infrastructure/packages.lock.json @@ -2,11 +2,331 @@ "version": 1, "dependencies": { "net9.0": { + "Microsoft.EntityFrameworkCore": { + "type": "Direct", + "requested": "[9.0.19, )", + "resolved": "9.0.19", + "contentHash": "qWqqxrfEDfi3C/z0S/ACIA2gUnreRPHX74WE/dVnesbztNesYO4NLy76lE6FV/zDP4vH/pkvDr8h3Mq9+cFv/w==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "9.0.19", + "Microsoft.EntityFrameworkCore.Analyzers": "9.0.19", + "Microsoft.Extensions.Caching.Memory": "9.0.19", + "Microsoft.Extensions.Logging": "9.0.19" + } + }, + "Microsoft.EntityFrameworkCore.Design": { + "type": "Direct", + "requested": "[9.0.19, )", + "resolved": "9.0.19", + "contentHash": "O4o69Xne6dJ7+u21YPxevGt8F2wpxGz2bBiSa/SSyqb2/64OZtcbIOB264fuViRj2XgwzhSGErNP75fRVCk8Zw==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "17.8.43", + "Microsoft.Build.Locator": "1.7.8", + "Microsoft.CodeAnalysis.CSharp": "4.8.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "4.8.0", + "Microsoft.CodeAnalysis.Workspaces.MSBuild": "4.8.0", + "Microsoft.EntityFrameworkCore.Relational": "9.0.19", + "Microsoft.Extensions.Caching.Memory": "9.0.19", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.19", + "Microsoft.Extensions.DependencyModel": "9.0.19", + "Microsoft.Extensions.Logging": "9.0.19", + "Mono.TextTemplating": "3.0.0", + "System.Text.Json": "9.0.19" + } + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Direct", + "requested": "[9.0.19, )", + "resolved": "9.0.19", + "contentHash": "KAfYGG4LWRNn1G/vMRydxgOjuZoWUvdd5raUcIia/HPg1jUjlQzS386nUx5gt3YfKxpFJaCGxdsQ1nCZWuYR8A==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "9.0.19", + "Microsoft.Extensions.Caching.Memory": "9.0.19", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.19", + "Microsoft.Extensions.Logging": "9.0.19" + } + }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Direct", - "requested": "[10.0.2, )", - "resolved": "10.0.2", - "contentHash": "zOIurr59+kUf9vNcsUkCvKWZv+fPosUZXURZesYkJCvl0EzTc9F7maAO4Cd2WEV7ZJJ0AZrFQvuH6Npph9wdBw==" + "requested": "[9.0.19, )", + "resolved": "9.0.19", + "contentHash": "XgQ0aVWClYjYSBqbYrKC/xyZ1KHf+VVwNF3+d54jcaqnst30Q//ugs/FJwUztLZiph0gp85g7i/eB0556twO0Q==" + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)", + "Npgsql": "9.0.3" + } + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "3aeMZ1N0lJoSyzqiP03hqemtb1BijhsJADdobn/4nsMJ8V1H+CrpuduUe4hlRdx+ikBQju1VGjMD1GJ3Sk05Eg==" + }, + "Microsoft.Build.Framework": { + "type": "Transitive", + "resolved": "17.8.43", + "contentHash": "TA6fCbd9qFSyhIKcUn5BhDN3/JrqpspO9KYsZKUvHyQpQrHVdzy4Hk6egjjBP3T0L562TLZuHEzQwJziyRBEkw==" + }, + "Microsoft.Build.Locator": { + "type": "Transitive", + "resolved": "1.7.8", + "contentHash": "sPy10x527Ph16S2u0yGME4S6ohBKJ69WfjeGG/bvELYeZVmJdKjxgnlL8cJJJLGV/cZIRqSfB12UDB8ICakOog==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.4", + "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.4", + "System.Collections.Immutable": "7.0.0", + "System.Reflection.Metadata": "7.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "+3+qfdb/aaGD8PZRCrsdobbzGs1m9u119SkkJt8e/mk3xLJz/udLtS2T6nY27OTXxBBw10HzAbC8Z9w08VyP/g==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[4.8.0]" + } + }, + "Microsoft.CodeAnalysis.CSharp.Workspaces": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "3amm4tq4Lo8/BGvg9p3BJh3S9nKq2wqCXfS7138i69TUpo/bD+XvD0hNurpEBtcNZhi1FyutiomKJqVF39ugYA==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.CSharp": "[4.8.0]", + "Microsoft.CodeAnalysis.Common": "[4.8.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "LXyV+MJKsKRu3FGJA3OmSk40OUIa/dQCFLOnm5X8MNcujx7hzGu8o+zjXlb/cy5xUdZK2UKYb9YaQ2E8m9QehQ==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Bcl.AsyncInterfaces": "7.0.0", + "Microsoft.CodeAnalysis.Common": "[4.8.0]", + "System.Composition": "7.0.0", + "System.IO.Pipelines": "7.0.0", + "System.Threading.Channels": "7.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.MSBuild": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "IEYreI82QZKklp54yPHxZNG9EKSK6nHEkeuf+0Asie9llgS1gp0V1hw7ODG+QyoB7MuAnNQHmeV1Per/ECpv6A==", + "dependencies": { + "Microsoft.Build.Framework": "16.10.0", + "Microsoft.CodeAnalysis.Common": "[4.8.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]", + "System.Text.Json": "7.0.3" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "igPnmVL3DU2ZH/4DNeQqWttNrQpwwg0q6MWj5peZxtl0QVMDSeFtnt2yhhdSffklZKYsAFko1sh/va3z4y7S6A==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "yI5dQuvigJ6ICQECOlJDtrsUYcuDryv3qsZK1i6ZA/IzMzZ/VPkDzjE4r2LivuyVSGnkDRkH2cp4LmrhXARRKw==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "qe/6m6wxRG+N5xTf4Z3lDBAiVu6oBCUyADJ5OzlFgukD9Vg/6zB3MNnNEFA7amZg8hjcS6F4OZBRmB07A+gmHQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.19" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "Y0w5LkBqWkDh5vYTKM+QKlzxpqegEtP86oDhFMXgx4tDkWL2t+rl4h2vxBHkvBtM/gM6PoLK9QCrnoclXtHtVQ==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "9.0.19", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19", + "Microsoft.Extensions.Logging.Abstractions": "9.0.19", + "Microsoft.Extensions.Options": "9.0.19", + "Microsoft.Extensions.Primitives": "9.0.19" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "M6h5lX6QWSSn7FDeRUHePZen/xvxPsuhcX1VpBfefafqavn2BnfxheR7BH5D5AIZGM7Nah0xJKnq2DublD4doQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.19" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "o/XFYSiV4a7eiCkf7W1GFAviVhHQAPY5bHrQRsNWAPT3GhJ3Af63vcFQjHivIrTNZITlPPaGWhBy7jykKYYyaQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19" + } + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "Jpe5up2VLMkjKavOXZzWRXBvy8SUPoFCRuVkq1wYrtVwtzuFEc7saw02dltASs5T8DXp6Ff/DPzmU95mDOiw6g==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "3cnWzJ/FLEhLiW+TJTWtTjVj6fwlsP8OrbJqgOP4J9feAKjmT77vmClakHlej+jnzBgpnVU0+SZwtEIOr+KS6A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "9.0.19", + "Microsoft.Extensions.Logging.Abstractions": "9.0.19", + "Microsoft.Extensions.Options": "9.0.19" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "RpqMVoZ+NECJxLOZRjWOfjST0mULs4wb848mxG/CS9SMkVL6yi1sSIda4dGcsB01J2jUFd8hNwsaYl1gplU+Cw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "FdPZy4kUSwFlIVZ3EkK9fdHw2RfblUwzflSb4kjVeGf9v2lX9IH+q2RGUovYMR7CVfKMc2y2Db/bZ2b4yiMfOg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19", + "Microsoft.Extensions.Primitives": "9.0.19" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "9hIg8PQiMnpVFIsEHm25Wi1gBrPa2pS1G75uveyDUVLXrNKqBap9WGwQIgO0s6LUgRAOdsSYnbKbNC5b1G5Pcg==" + }, + "Mono.TextTemplating": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "YqueG52R/Xej4VVbKuRIodjiAhV0HR/XVbLbNrJhCZnzjnSjgMJ/dCdV0akQQxavX6hp/LC6rqLGLcXeQYU7XA==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, + "Npgsql": { + "type": "Transitive", + "resolved": "9.0.3", + "contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.2" + } + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==" + }, + "System.Composition": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "tRwgcAkDd85O8Aq6zHDANzQaq380cek9lbMg5Qma46u5BZXq/G+XvIYmu+UI+BIIZ9zssXLYrkTykEqxxvhcmg==", + "dependencies": { + "System.Composition.AttributedModel": "7.0.0", + "System.Composition.Convention": "7.0.0", + "System.Composition.Hosting": "7.0.0", + "System.Composition.Runtime": "7.0.0", + "System.Composition.TypedParts": "7.0.0" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "2QzClqjElKxgI1jK1Jztnq44/8DmSuTSGGahXqQ4TdEV0h9s2KikQZIgcEqVzR7OuWDFPGLHIprBJGQEPr8fAQ==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "IMhTlpCs4HmlD8B+J8/kWfwX7vrBBOs6xyjSTzBlYSs7W4OET4tlkR/Sg9NG8jkdJH9Mymq0qGdYS1VPqRTBnQ==", + "dependencies": { + "System.Composition.AttributedModel": "7.0.0" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "eB6gwN9S+54jCTBJ5bpwMOVerKeUfGGTYCzz3QgDr1P55Gg/Wb27ShfPIhLMjmZ3MoAKu8uUSv6fcCdYJTN7Bg==", + "dependencies": { + "System.Composition.Runtime": "7.0.0" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "aZJ1Zr5Txe925rbo4742XifEyW0MIni1eiUebmcrP3HwLXZ3IbXUj4MFMUH/RmnJOAQiS401leg/2Sz1MkApDw==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "ZK0KNPfbtxVceTwh+oHNGUOYV2WNOHReX2AXipuvkURC7s/jPwoWfsu3SnDBDgofqbiWr96geofdQ2erm/KTHg==", + "dependencies": { + "System.Composition.AttributedModel": "7.0.0", + "System.Composition.Hosting": "7.0.0", + "System.Composition.Runtime": "7.0.0" + } + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==", + "dependencies": { + "System.Collections.Immutable": "7.0.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "DjVVcIY/dS/rjb0b42nciHjUc0voQfUtrTos+wC0sgdx0VemTpYhx1jdeHowwFghygJ6QdaytcMHwA8jZ6MHFg==" + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" }, "atm.application.abstractions": { "type": "Project", diff --git a/src/Atm.Presentation/packages.lock.json b/src/Atm.Presentation/packages.lock.json index dfd4ccc..dbf801e 100644 --- a/src/Atm.Presentation/packages.lock.json +++ b/src/Atm.Presentation/packages.lock.json @@ -14,21 +14,139 @@ "Swashbuckle.AspNetCore.SwaggerUI": "6.6.2" } }, + "Microsoft.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "qWqqxrfEDfi3C/z0S/ACIA2gUnreRPHX74WE/dVnesbztNesYO4NLy76lE6FV/zDP4vH/pkvDr8h3Mq9+cFv/w==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "9.0.19", + "Microsoft.EntityFrameworkCore.Analyzers": "9.0.19", + "Microsoft.Extensions.Caching.Memory": "9.0.19", + "Microsoft.Extensions.Logging": "9.0.19" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "igPnmVL3DU2ZH/4DNeQqWttNrQpwwg0q6MWj5peZxtl0QVMDSeFtnt2yhhdSffklZKYsAFko1sh/va3z4y7S6A==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "yI5dQuvigJ6ICQECOlJDtrsUYcuDryv3qsZK1i6ZA/IzMzZ/VPkDzjE4r2LivuyVSGnkDRkH2cp4LmrhXARRKw==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "KAfYGG4LWRNn1G/vMRydxgOjuZoWUvdd5raUcIia/HPg1jUjlQzS386nUx5gt3YfKxpFJaCGxdsQ1nCZWuYR8A==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "9.0.19", + "Microsoft.Extensions.Caching.Memory": "9.0.19", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.19", + "Microsoft.Extensions.Logging": "9.0.19" + } + }, "Microsoft.Extensions.ApiDescription.Server": { "type": "Transitive", "resolved": "6.0.5", "contentHash": "Ckb5EDBUNJdFWyajfXzUIMRkhf52fHZOQuuZg/oiu8y7zDCVwD0iHhew6MnThjHmevanpxL3f5ci2TtHQEN6bw==" }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "qe/6m6wxRG+N5xTf4Z3lDBAiVu6oBCUyADJ5OzlFgukD9Vg/6zB3MNnNEFA7amZg8hjcS6F4OZBRmB07A+gmHQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.19" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "Y0w5LkBqWkDh5vYTKM+QKlzxpqegEtP86oDhFMXgx4tDkWL2t+rl4h2vxBHkvBtM/gM6PoLK9QCrnoclXtHtVQ==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "9.0.19", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19", + "Microsoft.Extensions.Logging.Abstractions": "9.0.19", + "Microsoft.Extensions.Options": "9.0.19", + "Microsoft.Extensions.Primitives": "9.0.19" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "M6h5lX6QWSSn7FDeRUHePZen/xvxPsuhcX1VpBfefafqavn2BnfxheR7BH5D5AIZGM7Nah0xJKnq2DublD4doQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.19" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "o/XFYSiV4a7eiCkf7W1GFAviVhHQAPY5bHrQRsNWAPT3GhJ3Af63vcFQjHivIrTNZITlPPaGWhBy7jykKYYyaQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19" + } + }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "10.0.2", - "contentHash": "zOIurr59+kUf9vNcsUkCvKWZv+fPosUZXURZesYkJCvl0EzTc9F7maAO4Cd2WEV7ZJJ0AZrFQvuH6Npph9wdBw==" + "resolved": "9.0.19", + "contentHash": "XgQ0aVWClYjYSBqbYrKC/xyZ1KHf+VVwNF3+d54jcaqnst30Q//ugs/FJwUztLZiph0gp85g7i/eB0556twO0Q==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "3cnWzJ/FLEhLiW+TJTWtTjVj6fwlsP8OrbJqgOP4J9feAKjmT77vmClakHlej+jnzBgpnVU0+SZwtEIOr+KS6A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "9.0.19", + "Microsoft.Extensions.Logging.Abstractions": "9.0.19", + "Microsoft.Extensions.Options": "9.0.19" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "RpqMVoZ+NECJxLOZRjWOfjST0mULs4wb848mxG/CS9SMkVL6yi1sSIda4dGcsB01J2jUFd8hNwsaYl1gplU+Cw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "FdPZy4kUSwFlIVZ3EkK9fdHw2RfblUwzflSb4kjVeGf9v2lX9IH+q2RGUovYMR7CVfKMc2y2Db/bZ2b4yiMfOg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19", + "Microsoft.Extensions.Primitives": "9.0.19" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "9hIg8PQiMnpVFIsEHm25Wi1gBrPa2pS1G75uveyDUVLXrNKqBap9WGwQIgO0s6LUgRAOdsSYnbKbNC5b1G5Pcg==" }, "Microsoft.OpenApi": { "type": "Transitive", "resolved": "1.6.14", "contentHash": "tTaBT8qjk3xINfESyOPE2rIellPvB7qpVqiWiyA/lACVvz+xOGiXhFUfohcx82NLbi5avzLW0lx+s6oAqQijfw==" }, + "Npgsql": { + "type": "Transitive", + "resolved": "9.0.3", + "contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.2" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)", + "Npgsql": "9.0.3" + } + }, "Swashbuckle.AspNetCore.Swagger": { "type": "Transitive", "resolved": "6.6.2", @@ -56,7 +174,7 @@ "Atm.Application.Abstractions": "[1.0.0, )", "Atm.Application.Contracts": "[1.0.0, )", "Atm.Domain": "[1.0.0, )", - "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.2, )" + "Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.19, )" } }, "atm.application.abstractions": { @@ -76,7 +194,10 @@ "dependencies": { "Atm.Application.Abstractions": "[1.0.0, )", "Atm.Domain": "[1.0.0, )", - "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.2, )" + "Microsoft.EntityFrameworkCore": "[9.0.19, )", + "Microsoft.EntityFrameworkCore.Relational": "[9.0.19, )", + "Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.19, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" } } } diff --git a/tests/Atm.Application.Tests/packages.lock.json b/tests/Atm.Application.Tests/packages.lock.json index d8d50bb..93bcc28 100644 --- a/tests/Atm.Application.Tests/packages.lock.json +++ b/tests/Atm.Application.Tests/packages.lock.json @@ -59,8 +59,8 @@ }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "10.0.2", - "contentHash": "zOIurr59+kUf9vNcsUkCvKWZv+fPosUZXURZesYkJCvl0EzTc9F7maAO4Cd2WEV7ZJJ0AZrFQvuH6Npph9wdBw==" + "resolved": "9.0.19", + "contentHash": "XgQ0aVWClYjYSBqbYrKC/xyZ1KHf+VVwNF3+d54jcaqnst30Q//ugs/FJwUztLZiph0gp85g7i/eB0556twO0Q==" }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", @@ -140,7 +140,7 @@ "Atm.Application.Abstractions": "[1.0.0, )", "Atm.Application.Contracts": "[1.0.0, )", "Atm.Domain": "[1.0.0, )", - "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.2, )" + "Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.19, )" } }, "atm.application.abstractions": { From 0887c0b5a037740098c0d041d9796492f1bf7b9a Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Wed, 2 Sep 2026 20:13:21 +0300 Subject: [PATCH 002/103] refactor(domain): simplify validation and remove money ceiling --- src/Atm.Domain/Common/DomainRules.cs | 30 +++++++ src/Atm.Domain/ValueObjects/AccountNumber.cs | 16 ++-- src/Atm.Domain/ValueObjects/Amount.cs | 9 ++- src/Atm.Domain/ValueObjects/Money.cs | 21 +++-- src/Atm.Domain/ValueObjects/PinCode.cs | 16 +--- src/Atm.Domain/ValueObjects/UserLogin.cs | 20 +++++ .../Atm.Application.Tests/ValueObjectTests.cs | 80 +++++++++++++++++++ 7 files changed, 159 insertions(+), 33 deletions(-) create mode 100644 src/Atm.Domain/Common/DomainRules.cs create mode 100644 src/Atm.Domain/ValueObjects/UserLogin.cs create mode 100644 tests/Atm.Application.Tests/ValueObjectTests.cs diff --git a/src/Atm.Domain/Common/DomainRules.cs b/src/Atm.Domain/Common/DomainRules.cs new file mode 100644 index 0000000..e5ffa74 --- /dev/null +++ b/src/Atm.Domain/Common/DomainRules.cs @@ -0,0 +1,30 @@ +namespace Atm.Domain.Common; + +internal static class DomainRules +{ + public static bool IsUtc(DateTimeOffset value) + { + return value.Offset == TimeSpan.Zero; + } + + public static bool HasScale(decimal value, int scale) + { + return decimal.Round(value, scale) == value; + } + + public static decimal AddExact(decimal left, decimal right) + { + decimal result = checked(left + right); + if (result - left != right || result - right != left) + throw new OverflowException("The amount cannot be represented exactly as decimal."); + return result; + } + + public static decimal SubtractExact(decimal left, decimal right) + { + decimal result = checked(left - right); + if (left - result != right || result + right != left) + throw new OverflowException("The amount cannot be represented exactly as decimal."); + return result; + } +} diff --git a/src/Atm.Domain/ValueObjects/AccountNumber.cs b/src/Atm.Domain/ValueObjects/AccountNumber.cs index 2535538..3587f1c 100644 --- a/src/Atm.Domain/ValueObjects/AccountNumber.cs +++ b/src/Atm.Domain/ValueObjects/AccountNumber.cs @@ -1,16 +1,20 @@ -namespace Atm.Domain.ValueObjects; +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)); - } + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("Account number is required.", nameof(value)); Value = value.Trim(); + NormalizedValue = Value.ToUpperInvariant(); + + if (Value.Length > 64 || NormalizedValue.Length > 64) + throw new ArgumentException("Account number cannot exceed 64 characters.", nameof(value)); } public string Value { get; } -} \ No newline at end of file + + public string NormalizedValue { get; } +} diff --git a/src/Atm.Domain/ValueObjects/Amount.cs b/src/Atm.Domain/ValueObjects/Amount.cs index 166e4b6..a7f3fd2 100644 --- a/src/Atm.Domain/ValueObjects/Amount.cs +++ b/src/Atm.Domain/ValueObjects/Amount.cs @@ -1,13 +1,16 @@ -namespace Atm.Domain.ValueObjects; +using Atm.Domain.Common; + +namespace Atm.Domain.ValueObjects; public sealed class Amount { public Amount(decimal value) { - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); + if (value <= 0m || !DomainRules.HasScale(value, 4)) + throw new ArgumentException("Amount must be positive with at most four decimal places.", nameof(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 index 6ac8e22..6095ea7 100644 --- a/src/Atm.Domain/ValueObjects/Money.cs +++ b/src/Atm.Domain/ValueObjects/Money.cs @@ -1,13 +1,13 @@ -namespace Atm.Domain.ValueObjects; +using Atm.Domain.Common; + +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."); - } + if (value < 0m || !DomainRules.HasScale(value, 4)) + throw new ArgumentException("Money must be non-negative with at most four decimal places.", nameof(value)); Value = value; } @@ -16,16 +16,13 @@ public Money(decimal value) public Money Add(Amount amount) { - return new Money(Value + amount.Value); + return new Money(DomainRules.AddExact(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); + throw new InvalidOperationException("Not enough balance."); + return new Money(DomainRules.SubtractExact(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 index 088b9de..62dcfa2 100644 --- a/src/Atm.Domain/ValueObjects/PinCode.cs +++ b/src/Atm.Domain/ValueObjects/PinCode.cs @@ -1,21 +1,13 @@ -namespace Atm.Domain.ValueObjects; +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"); - } - + if (value is null || value.Length != 4 || value.Any(character => character is < '0' or > '9')) + throw new ArgumentException("PIN must contain exactly four ASCII digits.", nameof(value)); Value = value; } public string Value { get; } -} \ No newline at end of file +} diff --git a/src/Atm.Domain/ValueObjects/UserLogin.cs b/src/Atm.Domain/ValueObjects/UserLogin.cs new file mode 100644 index 0000000..3a90359 --- /dev/null +++ b/src/Atm.Domain/ValueObjects/UserLogin.cs @@ -0,0 +1,20 @@ +namespace Atm.Domain.ValueObjects; + +public sealed class UserLogin +{ + public UserLogin(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("Login is required.", nameof(value)); + + Value = value.Trim(); + NormalizedValue = Value.ToUpperInvariant(); + + if (Value.Length > 64 || NormalizedValue.Length > 64) + throw new ArgumentException("Login cannot exceed 64 characters.", nameof(value)); + } + + public string Value { get; } + + public string NormalizedValue { get; } +} diff --git a/tests/Atm.Application.Tests/ValueObjectTests.cs b/tests/Atm.Application.Tests/ValueObjectTests.cs new file mode 100644 index 0000000..8fc1dcb --- /dev/null +++ b/tests/Atm.Application.Tests/ValueObjectTests.cs @@ -0,0 +1,80 @@ +using Atm.Domain.ValueObjects; +using System.Globalization; + +namespace Atm.Application.Tests; + +public sealed class ValueObjectTests +{ + [Fact] + public void NormalizationIsCultureIndependent() + { + CultureInfo original = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("tr-TR"); + var number = new AccountNumber(" idi-01 "); + var login = new UserLogin(" idi "); + Assert.Equal("idi-01", number.Value); + Assert.Equal("IDI-01", number.NormalizedValue); + Assert.Equal("IDI", login.NormalizedValue); + } + finally + { + CultureInfo.CurrentCulture = original; + } + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void EmptyIdentifiersAreRejected(string value) + { + Assert.Throws(() => new AccountNumber(value)); + Assert.Throws(() => new UserLogin(value)); + } + + [Fact] + public void LongIdentifiersAreRejected() + { + string value = new string('a', 65); + Assert.Throws(() => new AccountNumber(value)); + Assert.Throws(() => new UserLogin(value)); + } + + [Fact] + public void MoneyHasNoNumeric19BusinessCeiling() + { + var money = new Money(1000000000000000m); + Assert.Equal(1000000000000001m, money.Add(new Amount(1m)).Value); + Assert.Equal(decimal.MaxValue, new Money(decimal.MaxValue).Value); + } + + [Fact] + public void InvalidAmountsAndPrecisionAreRejected() + { + Assert.Throws(() => new Amount(0m)); + Assert.Throws(() => new Amount(-1m)); + Assert.Throws(() => new Amount(1.00001m)); + Assert.Throws(() => new Money(-1m)); + Assert.Throws(() => new Money(1.00001m)); + } + + [Fact] + public void ArithmeticDoesNotSilentlyLoseSmallAmounts() + { + var money = new Money(decimal.MaxValue); + Assert.Throws(() => money.Add(new Amount(1m))); + Assert.Throws(() => money.Add(new Amount(0.0001m))); + Assert.Throws(() => money.Subtract(new Amount(0.0001m))); + } + + [Theory] + [InlineData("123")] + [InlineData("abcd")] + [InlineData("1234")] + public void InvalidPinIsRejectedWithoutEchoingInput(string pin) + { + ArgumentException exception = Assert.Throws(() => new PinCode(pin)); + Assert.DoesNotContain(pin, exception.Message, StringComparison.Ordinal); + } +} From 2d15b97227093f4fbf313826e116bcdfe8df2982 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Wed, 2 Sep 2026 20:20:10 +0300 Subject: [PATCH 003/103] feat(domain): add users and canonical friendships --- src/Atm.Domain/Users/Friendship.cs | 33 ++++++++ src/Atm.Domain/Users/HairColor.cs | 13 ++++ .../Users/Results/FriendshipCreationResult.cs | 10 +++ .../Users/Results/ProfileUpdateResult.cs | 10 +++ .../Users/Results/UserCreationResult.cs | 10 +++ src/Atm.Domain/Users/User.cs | 77 +++++++++++++++++++ src/Atm.Domain/Users/UserGender.cs | 9 +++ tests/Atm.Application.Tests/UserTests.cs | 75 ++++++++++++++++++ 8 files changed, 237 insertions(+) create mode 100644 src/Atm.Domain/Users/Friendship.cs create mode 100644 src/Atm.Domain/Users/HairColor.cs create mode 100644 src/Atm.Domain/Users/Results/FriendshipCreationResult.cs create mode 100644 src/Atm.Domain/Users/Results/ProfileUpdateResult.cs create mode 100644 src/Atm.Domain/Users/Results/UserCreationResult.cs create mode 100644 src/Atm.Domain/Users/User.cs create mode 100644 src/Atm.Domain/Users/UserGender.cs create mode 100644 tests/Atm.Application.Tests/UserTests.cs diff --git a/src/Atm.Domain/Users/Friendship.cs b/src/Atm.Domain/Users/Friendship.cs new file mode 100644 index 0000000..13b6c38 --- /dev/null +++ b/src/Atm.Domain/Users/Friendship.cs @@ -0,0 +1,33 @@ +using Atm.Domain.Common; +using Atm.Domain.Users.Results; + +namespace Atm.Domain.Users; + +public sealed class Friendship +{ + private Friendship() { } + + public Guid UserId { get; private set; } + public Guid FriendUserId { get; private set; } + public DateTimeOffset CreatedAtUtc { get; private set; } + + public static FriendshipCreationResult Create( + Guid firstUserId, Guid secondUserId, DateTimeOffset createdAtUtc) + { + if (firstUserId == Guid.Empty || secondUserId == Guid.Empty) + return new FriendshipCreationResult.Failure("User IDs cannot be empty."); + if (firstUserId == secondUserId) + return new FriendshipCreationResult.Failure("A user cannot befriend themselves."); + if (!DomainRules.IsUtc(createdAtUtc)) + return new FriendshipCreationResult.Failure("Created timestamp must be UTC."); + + // Canonical hexadecimal order matches UUID byte order, not Guid.ToByteArray's little-endian layout. + bool firstIsSmaller = string.CompareOrdinal(firstUserId.ToString("N"), secondUserId.ToString("N")) < 0; + return new FriendshipCreationResult.Success(new Friendship + { + UserId = firstIsSmaller ? firstUserId : secondUserId, + FriendUserId = firstIsSmaller ? secondUserId : firstUserId, + CreatedAtUtc = createdAtUtc, + }); + } +} diff --git a/src/Atm.Domain/Users/HairColor.cs b/src/Atm.Domain/Users/HairColor.cs new file mode 100644 index 0000000..4e08a88 --- /dev/null +++ b/src/Atm.Domain/Users/HairColor.cs @@ -0,0 +1,13 @@ +namespace Atm.Domain.Users; + +public enum HairColor +{ + Unspecified = 0, + Black = 1, + Brown = 2, + Blond = 3, + Red = 4, + Gray = 5, + White = 6, + Other = 7, +} diff --git a/src/Atm.Domain/Users/Results/FriendshipCreationResult.cs b/src/Atm.Domain/Users/Results/FriendshipCreationResult.cs new file mode 100644 index 0000000..e0d9ec9 --- /dev/null +++ b/src/Atm.Domain/Users/Results/FriendshipCreationResult.cs @@ -0,0 +1,10 @@ +namespace Atm.Domain.Users.Results; + +public abstract record FriendshipCreationResult +{ + private FriendshipCreationResult() { } + + public sealed record Success(Friendship Friendship) : FriendshipCreationResult; + + public sealed record Failure(string ErrorMessage) : FriendshipCreationResult; +} diff --git a/src/Atm.Domain/Users/Results/ProfileUpdateResult.cs b/src/Atm.Domain/Users/Results/ProfileUpdateResult.cs new file mode 100644 index 0000000..57fb3eb --- /dev/null +++ b/src/Atm.Domain/Users/Results/ProfileUpdateResult.cs @@ -0,0 +1,10 @@ +namespace Atm.Domain.Users.Results; + +public abstract record ProfileUpdateResult +{ + private ProfileUpdateResult() { } + + public sealed record Success : ProfileUpdateResult; + + public sealed record Failure(string ErrorMessage) : ProfileUpdateResult; +} diff --git a/src/Atm.Domain/Users/Results/UserCreationResult.cs b/src/Atm.Domain/Users/Results/UserCreationResult.cs new file mode 100644 index 0000000..346f398 --- /dev/null +++ b/src/Atm.Domain/Users/Results/UserCreationResult.cs @@ -0,0 +1,10 @@ +namespace Atm.Domain.Users.Results; + +public abstract record UserCreationResult +{ + private UserCreationResult() { } + + public sealed record Success(User User) : UserCreationResult; + + public sealed record Failure(string ErrorMessage) : UserCreationResult; +} diff --git a/src/Atm.Domain/Users/User.cs b/src/Atm.Domain/Users/User.cs new file mode 100644 index 0000000..28146dd --- /dev/null +++ b/src/Atm.Domain/Users/User.cs @@ -0,0 +1,77 @@ +using Atm.Domain.Common; +using Atm.Domain.Users.Results; +using Atm.Domain.ValueObjects; + +namespace Atm.Domain.Users; + +public sealed class User +{ + // Public construction is validated by Create; private setters also allow ORM materialization. + private User() { } + + public Guid Id { get; private set; } + public UserLogin Login { get; private set; } = null!; + public string Name { get; private set; } = null!; + public short Age { get; private set; } + public UserGender Gender { get; private set; } + public HairColor HairColor { get; private set; } + public DateTimeOffset CreatedAtUtc { get; private set; } + public DateTimeOffset UpdatedAtUtc { get; private set; } + + public static UserCreationResult Create( + Guid id, UserLogin? login, string? name, short age, + UserGender gender, HairColor hairColor, DateTimeOffset createdAtUtc) + { + if (id == Guid.Empty || login is null) + return new UserCreationResult.Failure("User ID and login are required."); + + string? error = ValidateProfile(name, age, gender, hairColor); + if (error is not null) + return new UserCreationResult.Failure(error); + + if (!DomainRules.IsUtc(createdAtUtc)) + return new UserCreationResult.Failure("Created timestamp must be UTC."); + + return new UserCreationResult.Success(new User + { + Id = id, + Login = login, + Name = name!.Trim(), + Age = age, + Gender = gender, + HairColor = hairColor, + CreatedAtUtc = createdAtUtc, + UpdatedAtUtc = createdAtUtc, + }); + } + + public ProfileUpdateResult UpdateProfile( + string? name, short age, UserGender gender, + HairColor hairColor, DateTimeOffset updatedAtUtc) + { + string? error = ValidateProfile(name, age, gender, hairColor); + if (error is not null) + return new ProfileUpdateResult.Failure(error); + + if (!DomainRules.IsUtc(updatedAtUtc) || updatedAtUtc < UpdatedAtUtc) + return new ProfileUpdateResult.Failure("Updated timestamp must be UTC and cannot move backwards."); + + Name = name!.Trim(); + Age = age; + Gender = gender; + HairColor = hairColor; + UpdatedAtUtc = updatedAtUtc; + return new ProfileUpdateResult.Success(); + } + + private static string? ValidateProfile(string? name, short age, UserGender gender, HairColor hairColor) + { + if (string.IsNullOrWhiteSpace(name) || name.Trim().Length > 200) + return "Name must be non-empty and cannot exceed 200 characters."; + if (age is < 0 or > 150) + return "Age must be between zero and 150."; + if (!Enum.IsDefined(gender) || !Enum.IsDefined(hairColor)) + return "Unsupported gender or hair color."; + return null; + } +} diff --git a/src/Atm.Domain/Users/UserGender.cs b/src/Atm.Domain/Users/UserGender.cs new file mode 100644 index 0000000..5e61048 --- /dev/null +++ b/src/Atm.Domain/Users/UserGender.cs @@ -0,0 +1,9 @@ +namespace Atm.Domain.Users; + +public enum UserGender +{ + Unspecified = 0, + Male = 1, + Female = 2, + Other = 3, +} diff --git a/tests/Atm.Application.Tests/UserTests.cs b/tests/Atm.Application.Tests/UserTests.cs new file mode 100644 index 0000000..2ee6957 --- /dev/null +++ b/tests/Atm.Application.Tests/UserTests.cs @@ -0,0 +1,75 @@ +using Atm.Domain.Users; +using Atm.Domain.Users.Results; +using Atm.Domain.ValueObjects; + +namespace Atm.Application.Tests; + +public sealed class UserTests +{ + private static readonly DateTimeOffset Now = new DateTimeOffset(2026, 9, 2, 12, 0, 0, TimeSpan.Zero); + + [Fact] + public void ValidProfileIsCreatedAndUpdated() + { + User user = CreateUser(); + Assert.Equal("Test User", user.Name); + Assert.IsType( + user.UpdateProfile(" Changed ", 30, UserGender.Other, HairColor.Black, Now.AddMinutes(1))); + Assert.Equal("Changed", user.Name); + Assert.Equal(Now.AddMinutes(1), user.UpdatedAtUtc); + Assert.Equal(Now, user.CreatedAtUtc); + } + + [Theory] + [InlineData(-1)] + [InlineData(151)] + public void InvalidProfileDoesNotPartiallyMutateState(short age) + { + User user = CreateUser(); + Assert.IsType( + user.UpdateProfile("Changed", age, UserGender.Other, HairColor.Black, Now.AddMinutes(1))); + Assert.Equal("Test User", user.Name); + Assert.Equal((short)25, user.Age); + Assert.Equal(HairColor.Unspecified, user.HairColor); + Assert.Equal(Now, user.UpdatedAtUtc); + } + + [Fact] + public void InvalidIdentifiersEnumsAndTimestampsAreRejected() + { + var login = new UserLogin("user"); + Assert.IsType( + User.Create(Guid.Empty, login, "Name", 25, UserGender.Unspecified, HairColor.Unspecified, Now)); + Assert.IsType( + User.Create(Guid.NewGuid(), login, "Name", 25, (UserGender)99, HairColor.Unspecified, Now)); + User user = CreateUser(); + Assert.IsType( + user.UpdateProfile("Name", 25, UserGender.Unspecified, HairColor.Unspecified, Now.AddSeconds(-1))); + Assert.IsType( + user.UpdateProfile("Name", 25, UserGender.Unspecified, HairColor.Unspecified, Now.ToOffset(TimeSpan.FromHours(3)))); + } + + [Fact] + public void FriendshipIsCanonicalAndRejectsSelfReference() + { + var first = Guid.Parse("00000100-0000-0000-0000-000000000001"); + var second = Guid.Parse("000000ff-ffff-ffff-ffff-ffffffffffff"); + Friendship forward = Assert.IsType( + Friendship.Create(first, second, Now)).Friendship; + Friendship reverse = Assert.IsType( + Friendship.Create(second, first, Now)).Friendship; + Assert.Equal(second, forward.UserId); + Assert.Equal(first, forward.FriendUserId); + Assert.Equal(forward.UserId, reverse.UserId); + Assert.Equal(forward.FriendUserId, reverse.FriendUserId); + Assert.IsType(Friendship.Create(first, first, Now)); + Assert.IsType(Friendship.Create(first, Guid.Empty, Now)); + } + + private static User CreateUser() + { + return Assert.IsType( + User.Create(Guid.NewGuid(), new UserLogin("user"), " Test User ", 25, + UserGender.Unspecified, HairColor.Unspecified, Now)).User; + } +} From 2bd666d17a3bb6f9aa92300e155391e47ee719ca Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Wed, 2 Sep 2026 20:23:25 +0300 Subject: [PATCH 004/103] feat(security): add pin hashing and injectable clock --- .../Security/IPinHasher.cs | 10 ++ .../Time/IClock.cs | 6 + src/Atm.Domain/ValueObjects/PinHash.cs | 16 ++ .../Atm.Infrastructure.csproj | 1 + src/Atm.Infrastructure/Security/PinHasher.cs | 29 ++++ .../ServiceCollectionExtensions.cs | 10 +- src/Atm.Infrastructure/Time/SystemClock.cs | 8 + src/Atm.Infrastructure/packages.lock.json | 24 +++ src/Atm.Presentation/packages.lock.json | 24 +++ .../Atm.Application.Tests.csproj | 1 + tests/Atm.Application.Tests/PinHasherTests.cs | 34 ++++ .../Atm.Application.Tests/packages.lock.json | 153 ++++++++++++++++++ 12 files changed, 314 insertions(+), 2 deletions(-) create mode 100644 src/Atm.Application.Abstractions/Security/IPinHasher.cs create mode 100644 src/Atm.Application.Abstractions/Time/IClock.cs create mode 100644 src/Atm.Domain/ValueObjects/PinHash.cs create mode 100644 src/Atm.Infrastructure/Security/PinHasher.cs create mode 100644 src/Atm.Infrastructure/Time/SystemClock.cs create mode 100644 tests/Atm.Application.Tests/PinHasherTests.cs diff --git a/src/Atm.Application.Abstractions/Security/IPinHasher.cs b/src/Atm.Application.Abstractions/Security/IPinHasher.cs new file mode 100644 index 0000000..0131331 --- /dev/null +++ b/src/Atm.Application.Abstractions/Security/IPinHasher.cs @@ -0,0 +1,10 @@ +using Atm.Domain.ValueObjects; + +namespace Atm.Application.Abstractions.Security; + +public interface IPinHasher +{ + PinHash Hash(PinCode pin); + + bool Verify(PinHash hash, PinCode pin); +} diff --git a/src/Atm.Application.Abstractions/Time/IClock.cs b/src/Atm.Application.Abstractions/Time/IClock.cs new file mode 100644 index 0000000..c9b9680 --- /dev/null +++ b/src/Atm.Application.Abstractions/Time/IClock.cs @@ -0,0 +1,6 @@ +namespace Atm.Application.Abstractions.Time; + +public interface IClock +{ + DateTimeOffset UtcNow { get; } +} diff --git a/src/Atm.Domain/ValueObjects/PinHash.cs b/src/Atm.Domain/ValueObjects/PinHash.cs new file mode 100644 index 0000000..b3674e0 --- /dev/null +++ b/src/Atm.Domain/ValueObjects/PinHash.cs @@ -0,0 +1,16 @@ +namespace Atm.Domain.ValueObjects; + +public sealed record PinHash +{ + public PinHash(string value) + { + if (string.IsNullOrWhiteSpace(value) || value.Length > 512) + throw new ArgumentException("PIN hash must contain between 1 and 512 characters.", nameof(value)); + + Value = value; + } + + public string Value { get; } + + public override string ToString() => "[REDACTED]"; +} diff --git a/src/Atm.Infrastructure/Atm.Infrastructure.csproj b/src/Atm.Infrastructure/Atm.Infrastructure.csproj index 6200b48..4e2e946 100644 --- a/src/Atm.Infrastructure/Atm.Infrastructure.csproj +++ b/src/Atm.Infrastructure/Atm.Infrastructure.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Atm.Infrastructure/Security/PinHasher.cs b/src/Atm.Infrastructure/Security/PinHasher.cs new file mode 100644 index 0000000..8a85026 --- /dev/null +++ b/src/Atm.Infrastructure/Security/PinHasher.cs @@ -0,0 +1,29 @@ +using Atm.Application.Abstractions.Security; +using Atm.Domain.ValueObjects; +using Microsoft.AspNetCore.Identity; + +namespace Atm.Infrastructure.Security; + +public sealed class PinHasher : IPinHasher +{ + private readonly PasswordHasher _hasher = new(); + + public PinHash Hash(PinCode pin) + { + return new PinHash(_hasher.HashPassword(this, pin.Value)); + } + + public bool Verify(PinHash hash, PinCode pin) + { + try + { + // Both Success and SuccessRehashNeeded mean the PIN is valid. + return _hasher.VerifyHashedPassword(this, hash.Value, pin.Value) + != PasswordVerificationResult.Failed; + } + catch (FormatException) + { + return false; + } + } +} diff --git a/src/Atm.Infrastructure/ServiceCollectionExtensions.cs b/src/Atm.Infrastructure/ServiceCollectionExtensions.cs index afe63f4..0c2d7ba 100644 --- a/src/Atm.Infrastructure/ServiceCollectionExtensions.cs +++ b/src/Atm.Infrastructure/ServiceCollectionExtensions.cs @@ -1,5 +1,9 @@ -using Atm.Infrastructure.Repositories; using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Security; +using Atm.Application.Abstractions.Time; +using Atm.Infrastructure.Repositories; +using Atm.Infrastructure.Security; +using Atm.Infrastructure.Time; using Microsoft.Extensions.DependencyInjection; namespace Atm.Infrastructure; @@ -11,7 +15,9 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); return services; } -} \ No newline at end of file +} diff --git a/src/Atm.Infrastructure/Time/SystemClock.cs b/src/Atm.Infrastructure/Time/SystemClock.cs new file mode 100644 index 0000000..a06421a --- /dev/null +++ b/src/Atm.Infrastructure/Time/SystemClock.cs @@ -0,0 +1,8 @@ +using Atm.Application.Abstractions.Time; + +namespace Atm.Infrastructure.Time; + +public sealed class SystemClock : IClock +{ + public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; +} diff --git a/src/Atm.Infrastructure/packages.lock.json b/src/Atm.Infrastructure/packages.lock.json index b27447b..a16a968 100644 --- a/src/Atm.Infrastructure/packages.lock.json +++ b/src/Atm.Infrastructure/packages.lock.json @@ -53,6 +53,17 @@ "resolved": "9.0.19", "contentHash": "XgQ0aVWClYjYSBqbYrKC/xyZ1KHf+VVwNF3+d54jcaqnst30Q//ugs/FJwUztLZiph0gp85g7i/eB0556twO0Q==" }, + "Microsoft.Extensions.Identity.Core": { + "type": "Direct", + "requested": "[9.0.19, )", + "resolved": "9.0.19", + "contentHash": "6P2/iMA35j8RrjsJXhqaOy4Sturcu+NXzXSlKguGTzjBEVRf6atWPrKkUuZew9K5S5NbNh8gdYip2dXm8t5fLg==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.19", + "Microsoft.Extensions.Logging": "9.0.19", + "Microsoft.Extensions.Options": "9.0.19" + } + }, "Npgsql.EntityFrameworkCore.PostgreSQL": { "type": "Direct", "requested": "[9.0.4, )", @@ -69,6 +80,19 @@ "resolved": "2.14.1", "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" }, + "Microsoft.AspNetCore.Cryptography.Internal": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "zqiWdZ8Jw4/sfTBSFHchU3Iajm8EqLPUq/cUIuUjARwNGJ9hrKR7Yf0bs4WlF+GdPX6Bj7kmaf99aY0OTbLCOg==" + }, + "Microsoft.AspNetCore.Cryptography.KeyDerivation": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "w0ZZWnsz7U2dh5l9eytJYYHG3/HwafewFyZT+NsnBuQAt90cw2hmiX9J9OU1mrtn7EiIJqhRxjzY5kAbmhD2DA==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.Internal": "9.0.19" + } + }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "7.0.0", diff --git a/src/Atm.Presentation/packages.lock.json b/src/Atm.Presentation/packages.lock.json index dbf801e..013d951 100644 --- a/src/Atm.Presentation/packages.lock.json +++ b/src/Atm.Presentation/packages.lock.json @@ -14,6 +14,19 @@ "Swashbuckle.AspNetCore.SwaggerUI": "6.6.2" } }, + "Microsoft.AspNetCore.Cryptography.Internal": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "zqiWdZ8Jw4/sfTBSFHchU3Iajm8EqLPUq/cUIuUjARwNGJ9hrKR7Yf0bs4WlF+GdPX6Bj7kmaf99aY0OTbLCOg==" + }, + "Microsoft.AspNetCore.Cryptography.KeyDerivation": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "w0ZZWnsz7U2dh5l9eytJYYHG3/HwafewFyZT+NsnBuQAt90cw2hmiX9J9OU1mrtn7EiIJqhRxjzY5kAbmhD2DA==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.Internal": "9.0.19" + } + }, "Microsoft.EntityFrameworkCore": { "type": "Transitive", "resolved": "9.0.19", @@ -92,6 +105,16 @@ "resolved": "9.0.19", "contentHash": "XgQ0aVWClYjYSBqbYrKC/xyZ1KHf+VVwNF3+d54jcaqnst30Q//ugs/FJwUztLZiph0gp85g7i/eB0556twO0Q==" }, + "Microsoft.Extensions.Identity.Core": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "6P2/iMA35j8RrjsJXhqaOy4Sturcu+NXzXSlKguGTzjBEVRf6atWPrKkUuZew9K5S5NbNh8gdYip2dXm8t5fLg==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.19", + "Microsoft.Extensions.Logging": "9.0.19", + "Microsoft.Extensions.Options": "9.0.19" + } + }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "9.0.19", @@ -197,6 +220,7 @@ "Microsoft.EntityFrameworkCore": "[9.0.19, )", "Microsoft.EntityFrameworkCore.Relational": "[9.0.19, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.19, )", + "Microsoft.Extensions.Identity.Core": "[9.0.19, )", "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" } } diff --git a/tests/Atm.Application.Tests/Atm.Application.Tests.csproj b/tests/Atm.Application.Tests/Atm.Application.Tests.csproj index 6ba4c8d..76d3475 100644 --- a/tests/Atm.Application.Tests/Atm.Application.Tests.csproj +++ b/tests/Atm.Application.Tests/Atm.Application.Tests.csproj @@ -21,6 +21,7 @@ + diff --git a/tests/Atm.Application.Tests/PinHasherTests.cs b/tests/Atm.Application.Tests/PinHasherTests.cs new file mode 100644 index 0000000..da10d6c --- /dev/null +++ b/tests/Atm.Application.Tests/PinHasherTests.cs @@ -0,0 +1,34 @@ +using Atm.Domain.ValueObjects; +using Atm.Infrastructure.Security; + +namespace Atm.Application.Tests; + +public class PinHasherTests +{ + [Fact] + public void HashUsesDifferentSaltsAndVerifiesThePin() + { + var hasher = new PinHasher(); + var pin = new PinCode("1234"); + + PinHash first = hasher.Hash(pin); + PinHash second = hasher.Hash(pin); + + Assert.NotEqual(pin.Value, first.Value); + Assert.NotEqual(first.Value, second.Value); + Assert.True(hasher.Verify(first, pin)); + Assert.True(hasher.Verify(second, pin)); + Assert.False(hasher.Verify(first, new PinCode("4321"))); + Assert.Equal("[REDACTED]", first.ToString()); + } + + [Theory] + [InlineData("not-base64")] + [InlineData("AA==")] + public void VerifyInvalidStoredHashReturnsFalse(string value) + { + var hasher = new PinHasher(); + + Assert.False(hasher.Verify(new PinHash(value), new PinCode("1234"))); + } +} diff --git a/tests/Atm.Application.Tests/packages.lock.json b/tests/Atm.Application.Tests/packages.lock.json index 93bcc28..3080a22 100644 --- a/tests/Atm.Application.Tests/packages.lock.json +++ b/tests/Atm.Application.Tests/packages.lock.json @@ -52,16 +52,139 @@ "System.Diagnostics.EventLog": "6.0.0" } }, + "Microsoft.AspNetCore.Cryptography.Internal": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "zqiWdZ8Jw4/sfTBSFHchU3Iajm8EqLPUq/cUIuUjARwNGJ9hrKR7Yf0bs4WlF+GdPX6Bj7kmaf99aY0OTbLCOg==" + }, + "Microsoft.AspNetCore.Cryptography.KeyDerivation": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "w0ZZWnsz7U2dh5l9eytJYYHG3/HwafewFyZT+NsnBuQAt90cw2hmiX9J9OU1mrtn7EiIJqhRxjzY5kAbmhD2DA==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.Internal": "9.0.19" + } + }, "Microsoft.CodeCoverage": { "type": "Transitive", "resolved": "17.12.0", "contentHash": "4svMznBd5JM21JIG2xZKGNanAHNXplxf/kQDFfLHXQ3OnpJkayRK/TjacFjA+EYmoyuNXHo/sOETEfcYtAzIrA==" }, + "Microsoft.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "qWqqxrfEDfi3C/z0S/ACIA2gUnreRPHX74WE/dVnesbztNesYO4NLy76lE6FV/zDP4vH/pkvDr8h3Mq9+cFv/w==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "9.0.19", + "Microsoft.EntityFrameworkCore.Analyzers": "9.0.19", + "Microsoft.Extensions.Caching.Memory": "9.0.19", + "Microsoft.Extensions.Logging": "9.0.19" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "igPnmVL3DU2ZH/4DNeQqWttNrQpwwg0q6MWj5peZxtl0QVMDSeFtnt2yhhdSffklZKYsAFko1sh/va3z4y7S6A==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "yI5dQuvigJ6ICQECOlJDtrsUYcuDryv3qsZK1i6ZA/IzMzZ/VPkDzjE4r2LivuyVSGnkDRkH2cp4LmrhXARRKw==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "KAfYGG4LWRNn1G/vMRydxgOjuZoWUvdd5raUcIia/HPg1jUjlQzS386nUx5gt3YfKxpFJaCGxdsQ1nCZWuYR8A==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "9.0.19", + "Microsoft.Extensions.Caching.Memory": "9.0.19", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.19", + "Microsoft.Extensions.Logging": "9.0.19" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "qe/6m6wxRG+N5xTf4Z3lDBAiVu6oBCUyADJ5OzlFgukD9Vg/6zB3MNnNEFA7amZg8hjcS6F4OZBRmB07A+gmHQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.19" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "Y0w5LkBqWkDh5vYTKM+QKlzxpqegEtP86oDhFMXgx4tDkWL2t+rl4h2vxBHkvBtM/gM6PoLK9QCrnoclXtHtVQ==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "9.0.19", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19", + "Microsoft.Extensions.Logging.Abstractions": "9.0.19", + "Microsoft.Extensions.Options": "9.0.19", + "Microsoft.Extensions.Primitives": "9.0.19" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "M6h5lX6QWSSn7FDeRUHePZen/xvxPsuhcX1VpBfefafqavn2BnfxheR7BH5D5AIZGM7Nah0xJKnq2DublD4doQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.19" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "o/XFYSiV4a7eiCkf7W1GFAviVhHQAPY5bHrQRsNWAPT3GhJ3Af63vcFQjHivIrTNZITlPPaGWhBy7jykKYYyaQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19" + } + }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "9.0.19", "contentHash": "XgQ0aVWClYjYSBqbYrKC/xyZ1KHf+VVwNF3+d54jcaqnst30Q//ugs/FJwUztLZiph0gp85g7i/eB0556twO0Q==" }, + "Microsoft.Extensions.Identity.Core": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "6P2/iMA35j8RrjsJXhqaOy4Sturcu+NXzXSlKguGTzjBEVRf6atWPrKkUuZew9K5S5NbNh8gdYip2dXm8t5fLg==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.19", + "Microsoft.Extensions.Logging": "9.0.19", + "Microsoft.Extensions.Options": "9.0.19" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "3cnWzJ/FLEhLiW+TJTWtTjVj6fwlsP8OrbJqgOP4J9feAKjmT77vmClakHlej+jnzBgpnVU0+SZwtEIOr+KS6A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "9.0.19", + "Microsoft.Extensions.Logging.Abstractions": "9.0.19", + "Microsoft.Extensions.Options": "9.0.19" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "RpqMVoZ+NECJxLOZRjWOfjST0mULs4wb848mxG/CS9SMkVL6yi1sSIda4dGcsB01J2jUFd8hNwsaYl1gplU+Cw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "FdPZy4kUSwFlIVZ3EkK9fdHw2RfblUwzflSb4kjVeGf9v2lX9IH+q2RGUovYMR7CVfKMc2y2Db/bZ2b4yiMfOg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19", + "Microsoft.Extensions.Primitives": "9.0.19" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "9hIg8PQiMnpVFIsEHm25Wi1gBrPa2pS1G75uveyDUVLXrNKqBap9WGwQIgO0s6LUgRAOdsSYnbKbNC5b1G5Pcg==" + }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", "resolved": "17.12.0", @@ -84,6 +207,24 @@ "resolved": "13.0.1", "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" }, + "Npgsql": { + "type": "Transitive", + "resolved": "9.0.3", + "contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.2" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)", + "Npgsql": "9.0.3" + } + }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "6.0.0", @@ -154,6 +295,18 @@ }, "atm.domain": { "type": "Project" + }, + "atm.infrastructure": { + "type": "Project", + "dependencies": { + "Atm.Application.Abstractions": "[1.0.0, )", + "Atm.Domain": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[9.0.19, )", + "Microsoft.EntityFrameworkCore.Relational": "[9.0.19, )", + "Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.19, )", + "Microsoft.Extensions.Identity.Core": "[9.0.19, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" + } } } } From 8d1c7773d0972a04ae9a26894a87599bfb4f5dd5 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Wed, 2 Sep 2026 20:26:00 +0300 Subject: [PATCH 005/103] feat(accounts): add owners hashes and balance versions --- .../Repositories/IAccountRepository.cs | 6 +- .../CreateAccount/CreateAccountRequest.cs | 5 +- .../Accounts/CreateAccountUseCase.cs | 62 +++++++---- .../Operations/DepositUseCase.cs | 13 ++- .../Operations/WithdrawUseCase.cs | 13 ++- .../Sessions/CreateUserSessionUseCase.cs | 13 ++- src/Atm.Domain/Accounts/Account.cs | 104 +++++++++++++++--- .../Accounts/Results/AccountCreationResult.cs | 10 ++ .../Repositories/AccountRepository.cs | 18 ++- src/Atm.Presentation/Contracts/ApiRequests.cs | 3 +- .../Controllers/AccountsController.cs | 9 +- tests/Atm.Application.Tests/AccountTests.cs | 63 +++++++++++ .../Atm.Application.Tests/BankingTestData.cs | 15 +++ .../DepositUseCaseTests.cs | 8 +- tests/Atm.Application.Tests/FixedClock.cs | 8 ++ .../WithdrawCaseTests.cs | 12 +- 16 files changed, 281 insertions(+), 81 deletions(-) create mode 100644 src/Atm.Domain/Accounts/Results/AccountCreationResult.cs create mode 100644 tests/Atm.Application.Tests/AccountTests.cs create mode 100644 tests/Atm.Application.Tests/BankingTestData.cs create mode 100644 tests/Atm.Application.Tests/FixedClock.cs diff --git a/src/Atm.Application.Abstractions/Repositories/IAccountRepository.cs b/src/Atm.Application.Abstractions/Repositories/IAccountRepository.cs index 25bc8b8..4770a4b 100644 --- a/src/Atm.Application.Abstractions/Repositories/IAccountRepository.cs +++ b/src/Atm.Application.Abstractions/Repositories/IAccountRepository.cs @@ -1,13 +1,15 @@ -using Atm.Domain.Accounts; +using Atm.Domain.Accounts; using Atm.Domain.ValueObjects; namespace Atm.Application.Abstractions.Repositories; public interface IAccountRepository { + Account? GetById(Guid id); + Account? GetByNumber(AccountNumber accountNumber); void Add(Account account); void Update(Account account); -} \ 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 index 5df84dc..60b1fac 100644 --- a/src/Atm.Application.Contracts/Accounts/CreateAccount/CreateAccountRequest.cs +++ b/src/Atm.Application.Contracts/Accounts/CreateAccount/CreateAccountRequest.cs @@ -1,7 +1,8 @@ -namespace Atm.Application.Contracts.Accounts.CreateAccount; +namespace Atm.Application.Contracts.Accounts.CreateAccount; public sealed record CreateAccountRequest( Guid AdminSessionId, string AccountNumber, string Pin, - decimal InitialBalance); \ No newline at end of file + decimal InitialBalance, + Guid OwnerUserId); diff --git a/src/Atm.Application/Accounts/CreateAccountUseCase.cs b/src/Atm.Application/Accounts/CreateAccountUseCase.cs index 4fe3ce3..ceeab30 100644 --- a/src/Atm.Application/Accounts/CreateAccountUseCase.cs +++ b/src/Atm.Application/Accounts/CreateAccountUseCase.cs @@ -1,7 +1,10 @@ -using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Security; +using Atm.Application.Abstractions.Time; using Atm.Application.Common; using Atm.Application.Contracts.Accounts.CreateAccount; using Atm.Domain.Accounts; +using Atm.Domain.Accounts.Results; using Atm.Domain.ValueObjects; namespace Atm.Application.Accounts; @@ -10,43 +13,54 @@ internal sealed class CreateAccountUseCase : ICreateAccountPort { private readonly IAccountRepository _accounts; private readonly SessionAuthorizer _auth; + private readonly IPinHasher _pinHasher; + private readonly IClock _clock; - public CreateAccountUseCase(IAccountRepository accounts, ISessionRepository sessions) + public CreateAccountUseCase( + IAccountRepository accounts, + SessionAuthorizer auth, + IPinHasher pinHasher, + IClock clock) { _accounts = accounts; - _auth = new SessionAuthorizer(sessions); + _auth = auth; + _pinHasher = pinHasher; + _clock = clock; } public CreateAccountResult Execute(CreateAccountRequest request) { AuthorizationResult auth = _auth.AuthorizeAdmin(request.AdminSessionId); + if (auth is AuthorizationResult.Unauthorized unauthorized) + return new CreateAccountResult.Unauthorized(unauthorized.ErrorMessage); - if (auth is AuthorizationResult.Unauthorized u) + AccountNumber number; + PinCode pin; + Money balance; + try { - return new CreateAccountResult.Unauthorized(u.ErrorMessage); + number = new AccountNumber(request.AccountNumber); + pin = new PinCode(request.Pin); + balance = new Money(request.InitialBalance); } - - try + catch (ArgumentException exception) { - var number = new AccountNumber(request.AccountNumber); - var pin = new PinCode(request.Pin); - var balance = new Money(request.InitialBalance); + return new CreateAccountResult.Failure(exception.Message); + } - Account? existing = _accounts.GetByNumber(number); + if (request.OwnerUserId == Guid.Empty) + return new CreateAccountResult.Failure("Owner ID must not be empty."); + if (_accounts.GetByNumber(number) is not null) + return new CreateAccountResult.Failure("Account number already exists."); - if (existing is not null) - { - return new CreateAccountResult.Failure("Account number already exists"); - } + AccountCreationResult result = Account.Create( + Guid.NewGuid(), number, request.OwnerUserId, + _pinHasher.Hash(pin), balance, _clock.UtcNow); - var account = new Account(number, pin, balance); - _accounts.Add(account); + if (result is AccountCreationResult.Failure failure) + return new CreateAccountResult.Failure(failure.ErrorMessage); - return new CreateAccountResult.Success(); - } - catch (Exception e) - { - return new CreateAccountResult.Failure(e.Message); - } + _accounts.Add(((AccountCreationResult.Success)result).Account); + return new CreateAccountResult.Success(); } -} \ No newline at end of file +} diff --git a/src/Atm.Application/Operations/DepositUseCase.cs b/src/Atm.Application/Operations/DepositUseCase.cs index 47273db..50bf22b 100644 --- a/src/Atm.Application/Operations/DepositUseCase.cs +++ b/src/Atm.Application/Operations/DepositUseCase.cs @@ -1,4 +1,5 @@ -using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Time; using Atm.Application.Common; using Atm.Application.Contracts.Operations.Deposit; using Atm.Domain.Operations; @@ -11,12 +12,14 @@ public sealed class DepositUseCase : IDepositPort private readonly IAccountRepository _accounts; private readonly IOperationRepository _operations; private readonly SessionAuthorizer _auth; + private readonly IClock _clock; - public DepositUseCase(IAccountRepository accounts, IOperationRepository operations, SessionAuthorizer auth) + public DepositUseCase(IAccountRepository accounts, IOperationRepository operations, SessionAuthorizer auth, IClock clock) { _accounts = accounts; _operations = operations; _auth = auth; + _clock = clock; } public DepositResult Execute(DepositRequest request) @@ -41,12 +44,12 @@ public DepositResult Execute(DepositRequest request) { amount = new Amount(request.Amount); } - catch (Exception e) + catch (ArgumentException e) { return new DepositResult.Failure(e.Message); } - Domain.Accounts.Results.DepositResult domainResult = acc.Deposit(amount); + Domain.Accounts.Results.DepositResult domainResult = acc.Deposit(amount, _clock.UtcNow); if (domainResult is Domain.Accounts.Results.DepositResult.Failure f) { @@ -58,4 +61,4 @@ public DepositResult Execute(DepositRequest request) return new DepositResult.Success(acc.Balance.Value); } -} \ No newline at end of file +} diff --git a/src/Atm.Application/Operations/WithdrawUseCase.cs b/src/Atm.Application/Operations/WithdrawUseCase.cs index da1c2b9..a2158d5 100644 --- a/src/Atm.Application/Operations/WithdrawUseCase.cs +++ b/src/Atm.Application/Operations/WithdrawUseCase.cs @@ -1,4 +1,5 @@ -using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Time; using Atm.Application.Common; using Atm.Application.Contracts.Operations.Withdraw; using Atm.Domain.Operations; @@ -11,12 +12,14 @@ public sealed class WithdrawUseCase : IWithdrawPort private readonly IAccountRepository _accounts; private readonly IOperationRepository _operations; private readonly SessionAuthorizer _auth; + private readonly IClock _clock; - public WithdrawUseCase(IAccountRepository accounts, IOperationRepository operations, SessionAuthorizer auth) + public WithdrawUseCase(IAccountRepository accounts, IOperationRepository operations, SessionAuthorizer auth, IClock clock) { _accounts = accounts; _operations = operations; _auth = auth; + _clock = clock; } public WithdrawResult Execute(WithdrawRequest request) @@ -41,12 +44,12 @@ public WithdrawResult Execute(WithdrawRequest request) { amount = new Amount(request.Amount); } - catch (Exception e) + catch (ArgumentException e) { return new WithdrawResult.Failure(e.Message); } - Domain.Accounts.Results.WithdrawResult domainResult = acc.Withdraw(amount); + Domain.Accounts.Results.WithdrawResult domainResult = acc.Withdraw(amount, _clock.UtcNow); if (domainResult is Domain.Accounts.Results.WithdrawResult.Failure result) { return new WithdrawResult.Failure(result.ErrorMessage); @@ -57,4 +60,4 @@ public WithdrawResult Execute(WithdrawRequest request) return new WithdrawResult.Success(acc.Balance.Value); } -} \ No newline at end of file +} diff --git a/src/Atm.Application/Sessions/CreateUserSessionUseCase.cs b/src/Atm.Application/Sessions/CreateUserSessionUseCase.cs index 6712e64..049dc05 100644 --- a/src/Atm.Application/Sessions/CreateUserSessionUseCase.cs +++ b/src/Atm.Application/Sessions/CreateUserSessionUseCase.cs @@ -1,4 +1,5 @@ -using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Security; using Atm.Application.Contracts.Sessions.CreateUserSession; using Atm.Domain.Sessions; using Atm.Domain.ValueObjects; @@ -9,11 +10,13 @@ internal sealed class CreateUserSessionUseCase : ICreateUserSessionPort { private readonly ISessionRepository _sessions; private readonly IAccountRepository _accounts; + private readonly IPinHasher _pinHasher; - public CreateUserSessionUseCase(ISessionRepository sessions, IAccountRepository accounts) + public CreateUserSessionUseCase(ISessionRepository sessions, IAccountRepository accounts, IPinHasher pinHasher) { _sessions = sessions; _accounts = accounts; + _pinHasher = pinHasher; } public CreateUserSessionResult Execute(CreateUserSessionRequest request) @@ -24,7 +27,7 @@ public CreateUserSessionResult Execute(CreateUserSessionRequest request) var pin = new PinCode(request.Pin); Domain.Accounts.Account? acc = _accounts.GetByNumber(number); - if (acc is null || acc.Pin.Value != pin.Value) + if (acc is null || !_pinHasher.Verify(acc.PinHash, pin)) { return new CreateUserSessionResult.Unauthorized("Invalid credentials"); } @@ -36,9 +39,9 @@ public CreateUserSessionResult Execute(CreateUserSessionRequest request) return new CreateUserSessionResult.Success(id); } - catch (Exception ex) + catch (ArgumentException ex) { return new CreateUserSessionResult.Failure(ex.Message); } } -} \ No newline at end of file +} diff --git a/src/Atm.Domain/Accounts/Account.cs b/src/Atm.Domain/Accounts/Account.cs index 0fba7e4..120defa 100644 --- a/src/Atm.Domain/Accounts/Account.cs +++ b/src/Atm.Domain/Accounts/Account.cs @@ -1,37 +1,107 @@ -using Atm.Domain.Accounts.Results; +using Atm.Domain.Accounts.Results; +using Atm.Domain.Common; 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; - } + private Account() { } - public AccountNumber Number { get; } + public Guid Id { get; private set; } + public AccountNumber Number { get; private set; } = null!; + public Guid OwnerUserId { get; private set; } + public PinHash PinHash { get; private set; } = null!; + public Money Balance { get; private set; } = null!; + public long Version { get; private set; } + public DateTimeOffset CreatedAtUtc { get; private set; } + public DateTimeOffset UpdatedAtUtc { get; private set; } - public PinCode Pin { get; } + public static AccountCreationResult Create( + Guid id, + AccountNumber? number, + Guid ownerUserId, + PinHash? pinHash, + Money? balance, + DateTimeOffset createdAtUtc) + { + if (id == Guid.Empty || ownerUserId == Guid.Empty) + return new AccountCreationResult.Failure("Account and owner IDs must not be empty."); + if (number is null || pinHash is null || balance is null) + return new AccountCreationResult.Failure("Number, PIN hash and balance are required."); + if (!DomainRules.IsUtc(createdAtUtc)) + return new AccountCreationResult.Failure("Account timestamps must be UTC."); - public Money Balance { get; private set; } + return new AccountCreationResult.Success(new Account + { + Id = id, + Number = number, + OwnerUserId = ownerUserId, + PinHash = pinHash, + Balance = balance, + Version = 0, + CreatedAtUtc = createdAtUtc, + UpdatedAtUtc = createdAtUtc, + }); + } - public DepositResult Deposit(Amount amount) + public DepositResult Deposit(Amount? amount, DateTimeOffset updatedAtUtc) { - Balance = new Money(Balance.Value + amount.Value); + string? error = ValidateChange(amount, updatedAtUtc); + if (error is not null) + return new DepositResult.Failure(error); + + Money balance; + try + { + balance = Balance.Add(amount!); + } + catch (OverflowException) + { + return new DepositResult.Failure("The balance cannot be represented exactly as decimal."); + } + + ApplyBalance(balance, updatedAtUtc); return new DepositResult.Success(Balance); } - public WithdrawResult Withdraw(Amount amount) + public WithdrawResult Withdraw(Amount? amount, DateTimeOffset updatedAtUtc) { - if (Balance.Value < amount.Value) + string? error = ValidateChange(amount, updatedAtUtc); + if (error is not null) + return new WithdrawResult.Failure(error); + if (Balance.Value < amount!.Value) + return new WithdrawResult.Failure("Not enough balance."); + + Money balance; + try + { + balance = Balance.Subtract(amount); + } + catch (OverflowException) { - return new WithdrawResult.Failure("Not enough balance"); + return new WithdrawResult.Failure("The balance cannot be represented exactly as decimal."); } - Balance = new Money(Balance.Value - amount.Value); + ApplyBalance(balance, updatedAtUtc); return new WithdrawResult.Success(Balance); } -} \ No newline at end of file + + private string? ValidateChange(Amount? amount, DateTimeOffset updatedAtUtc) + { + if (amount is null) + return "Amount is required."; + if (!DomainRules.IsUtc(updatedAtUtc) || updatedAtUtc < UpdatedAtUtc) + return "Update time must be UTC and must not precede the previous update."; + if (Version == long.MaxValue) + return "Account version is exhausted."; + return null; + } + + private void ApplyBalance(Money balance, DateTimeOffset updatedAtUtc) + { + Balance = balance; + Version++; + UpdatedAtUtc = updatedAtUtc; + } +} diff --git a/src/Atm.Domain/Accounts/Results/AccountCreationResult.cs b/src/Atm.Domain/Accounts/Results/AccountCreationResult.cs new file mode 100644 index 0000000..e23b0c9 --- /dev/null +++ b/src/Atm.Domain/Accounts/Results/AccountCreationResult.cs @@ -0,0 +1,10 @@ +namespace Atm.Domain.Accounts.Results; + +public abstract record AccountCreationResult +{ + private AccountCreationResult() { } + + public sealed record Success(Account Account) : AccountCreationResult; + + public sealed record Failure(string ErrorMessage) : AccountCreationResult; +} diff --git a/src/Atm.Infrastructure/Repositories/AccountRepository.cs b/src/Atm.Infrastructure/Repositories/AccountRepository.cs index 44b8616..8a7f0e0 100644 --- a/src/Atm.Infrastructure/Repositories/AccountRepository.cs +++ b/src/Atm.Infrastructure/Repositories/AccountRepository.cs @@ -1,4 +1,4 @@ -using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Repositories; using Atm.Domain.Accounts; using Atm.Domain.ValueObjects; @@ -6,20 +6,26 @@ namespace Atm.Infrastructure.Repositories; public sealed class AccountRepository : IAccountRepository { - private readonly Dictionary _accounts = new(); + private readonly Dictionary _accounts = new(); + + public Account? GetById(Guid id) + { + return _accounts.GetValueOrDefault(id); + } public Account? GetByNumber(AccountNumber accountNumber) { - return _accounts.GetValueOrDefault(accountNumber.Value); + return _accounts.Values.FirstOrDefault( + account => account.Number.NormalizedValue == accountNumber.NormalizedValue); } public void Add(Account account) { - _accounts[account.Number.Value] = account; + _accounts.Add(account.Id, account); } public void Update(Account account) { - _accounts[account.Number.Value] = account; + _accounts[account.Id] = account; } -} \ No newline at end of file +} diff --git a/src/Atm.Presentation/Contracts/ApiRequests.cs b/src/Atm.Presentation/Contracts/ApiRequests.cs index ce79d66..cb4f71e 100644 --- a/src/Atm.Presentation/Contracts/ApiRequests.cs +++ b/src/Atm.Presentation/Contracts/ApiRequests.cs @@ -6,7 +6,8 @@ public sealed record CreateAccountApiRequest( [property: JsonPropertyName("adminSessionId")] Guid AdminSessionId, [property: JsonPropertyName("accountNumber")] string AccountNumber, [property: JsonPropertyName("pin")] string Pin, - [property: JsonPropertyName("initialBalance")] decimal InitialBalance); + [property: JsonPropertyName("initialBalance")] decimal InitialBalance, + [property: JsonPropertyName("ownerUserId")] Guid OwnerUserId); public sealed record AmountApiRequest( [property: JsonPropertyName("userSessionId")] Guid UserSessionId, diff --git a/src/Atm.Presentation/Controllers/AccountsController.cs b/src/Atm.Presentation/Controllers/AccountsController.cs index debec63..2b550f9 100644 --- a/src/Atm.Presentation/Controllers/AccountsController.cs +++ b/src/Atm.Presentation/Controllers/AccountsController.cs @@ -1,4 +1,4 @@ -using Atm.Application.Contracts.Accounts.CreateAccount; +using Atm.Application.Contracts.Accounts.CreateAccount; using Atm.Application.Contracts.Accounts.GetBalance; using Atm.Application.Contracts.Operations.Deposit; using Atm.Application.Contracts.Operations.GetTransactionHistory; @@ -43,7 +43,8 @@ public IActionResult Create([FromBody] CreateAccountApiRequest request) request.AdminSessionId, request.AccountNumber, request.Pin, - request.InitialBalance)); + request.InitialBalance, + request.OwnerUserId)); return result switch { @@ -62,7 +63,7 @@ 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)), @@ -116,7 +117,7 @@ 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)), diff --git a/tests/Atm.Application.Tests/AccountTests.cs b/tests/Atm.Application.Tests/AccountTests.cs new file mode 100644 index 0000000..c9e2a0c --- /dev/null +++ b/tests/Atm.Application.Tests/AccountTests.cs @@ -0,0 +1,63 @@ +using Atm.Domain.Accounts; +using Atm.Domain.Accounts.Results; +using Atm.Domain.ValueObjects; + +namespace Atm.Application.Tests; + +public class AccountTests +{ + [Fact] + public void CreateRequiresAnOwnerAndUtcTime() + { + var number = new AccountNumber("test"); + var hash = new PinHash("test-only-hash"); + var clock = new FixedClock(); + + Assert.IsType(Account.Create( + Guid.NewGuid(), number, Guid.Empty, hash, new Money(0m), clock.UtcNow)); + Assert.IsType(Account.Create( + Guid.NewGuid(), number, Guid.NewGuid(), hash, new Money(0m), clock.UtcNow.ToOffset(TimeSpan.FromHours(3)))); + } + + [Fact] + public void SuccessfulChangesAdvanceTheVersionAndTimestamp() + { + Account account = BankingTestData.CreateAccount(new AccountNumber("test"), 100m); + DateTimeOffset now = account.CreatedAtUtc.AddMinutes(1); + + Assert.IsType(account.Deposit(new Amount(10m), now)); + Assert.Equal(1, account.Version); + Assert.Equal(now, account.UpdatedAtUtc); + Assert.IsType(account.Withdraw(new Amount(20m), now.AddMinutes(1))); + Assert.Equal(2, account.Version); + Assert.Equal(90m, account.Balance.Value); + } + + [Fact] + public void FailedChangesDoNotMutateTheAccount() + { + Account account = BankingTestData.CreateAccount(new AccountNumber("test"), 100m); + DateTimeOffset now = account.CreatedAtUtc; + + Assert.IsType(account.Withdraw(new Amount(101m), now)); + Assert.IsType(account.Deposit(new Amount(1m), now.AddTicks(-1))); + Assert.Equal(100m, account.Balance.Value); + Assert.Equal(0, account.Version); + Assert.Equal(now, account.UpdatedAtUtc); + } + + [Fact] + public void LargeBalancesHaveNoBusinessCeilingButCannotLosePrecision() + { + Account account = BankingTestData.CreateAccount(new AccountNumber("large"), 1000000000000000m); + Assert.IsType(account.Deposit(new Amount(1m), account.CreatedAtUtc)); + Assert.Equal(1000000000000001m, account.Balance.Value); + + Account largest = BankingTestData.CreateAccount(new AccountNumber("largest"), decimal.MaxValue); + Assert.IsType(largest.Deposit(new Amount(1m), largest.CreatedAtUtc)); + Assert.IsType(largest.Deposit(new Amount(0.0001m), largest.CreatedAtUtc)); + Assert.IsType(largest.Withdraw(new Amount(0.0001m), largest.CreatedAtUtc)); + Assert.Equal(decimal.MaxValue, largest.Balance.Value); + Assert.Equal(0, largest.Version); + } +} diff --git a/tests/Atm.Application.Tests/BankingTestData.cs b/tests/Atm.Application.Tests/BankingTestData.cs new file mode 100644 index 0000000..8bda6a6 --- /dev/null +++ b/tests/Atm.Application.Tests/BankingTestData.cs @@ -0,0 +1,15 @@ +using Atm.Domain.Accounts; +using Atm.Domain.Accounts.Results; +using Atm.Domain.ValueObjects; + +namespace Atm.Application.Tests; + +internal static class BankingTestData +{ + public static Account CreateAccount(AccountNumber number, decimal balance) + { + return Assert.IsType(Account.Create( + Guid.NewGuid(), number, Guid.NewGuid(), + new PinHash("test-only-hash"), new Money(balance), new FixedClock().UtcNow)).Account; + } +} diff --git a/tests/Atm.Application.Tests/DepositUseCaseTests.cs b/tests/Atm.Application.Tests/DepositUseCaseTests.cs index 565bb54..7d43cac 100644 --- a/tests/Atm.Application.Tests/DepositUseCaseTests.cs +++ b/tests/Atm.Application.Tests/DepositUseCaseTests.cs @@ -1,4 +1,4 @@ -using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Repositories; using Atm.Application.Common; using Atm.Application.Contracts.Operations.Deposit; using Atm.Application.Operations; @@ -24,11 +24,11 @@ public void DepositWhenValidIncreaseBalanceAndSavesOperation() sessions.GetById(sessionId).Returns(new Session(sessionId, SessionType.User, accountNumber)); - var account = new Account(accountNumber, new PinCode("1234"), new Money(100m)); + Account account = BankingTestData.CreateAccount(accountNumber, 100m); accounts.GetByNumber(accountNumber).Returns(account); var auth = new SessionAuthorizer(sessions); - var useCase = new DepositUseCase(accounts, operations, auth); + var useCase = new DepositUseCase(accounts, operations, auth, new FixedClock()); DepositResult result = useCase.Execute(new DepositRequest(sessionId, 50m)); @@ -43,4 +43,4 @@ public void DepositWhenValidIncreaseBalanceAndSavesOperation() op.Amount.Value == 50m && op.BalanceAfter.Value == 150m)); } -} \ No newline at end of file +} diff --git a/tests/Atm.Application.Tests/FixedClock.cs b/tests/Atm.Application.Tests/FixedClock.cs new file mode 100644 index 0000000..af39324 --- /dev/null +++ b/tests/Atm.Application.Tests/FixedClock.cs @@ -0,0 +1,8 @@ +using Atm.Application.Abstractions.Time; + +namespace Atm.Application.Tests; + +internal sealed class FixedClock : IClock +{ + public DateTimeOffset UtcNow { get; set; } = new(2026, 9, 2, 12, 0, 0, TimeSpan.Zero); +} diff --git a/tests/Atm.Application.Tests/WithdrawCaseTests.cs b/tests/Atm.Application.Tests/WithdrawCaseTests.cs index 0b53806..09a5015 100644 --- a/tests/Atm.Application.Tests/WithdrawCaseTests.cs +++ b/tests/Atm.Application.Tests/WithdrawCaseTests.cs @@ -1,4 +1,4 @@ -using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Repositories; using Atm.Application.Common; using Atm.Application.Contracts.Operations.Withdraw; using Atm.Application.Operations; @@ -24,11 +24,11 @@ public void WithdrawWhenEnoughBalanceDecreasesBalanceAndSavesOperation() sessions.GetById(sessionId).Returns(new Session(sessionId, SessionType.User, accountNumber)); - var account = new Account(accountNumber, new PinCode("6666"), new Money(100m)); + Account account = BankingTestData.CreateAccount(accountNumber, 100m); accounts.GetByNumber(accountNumber).Returns(account); var auth = new SessionAuthorizer(sessions); - var useCase = new WithdrawUseCase(accounts, operations, auth); + var useCase = new WithdrawUseCase(accounts, operations, auth, new FixedClock()); WithdrawResult result = useCase.Execute(new WithdrawRequest(sessionId, 40m)); @@ -56,11 +56,11 @@ public void WithdrawWhenNotEnoughBalanceReturnsFailureAndDoesNotSave() sessions.GetById(sessionId).Returns(new Session(sessionId, SessionType.User, accountNumber)); - var account = new Account(accountNumber, new PinCode("9999"), new Money(10m)); + Account account = BankingTestData.CreateAccount(accountNumber, 10m); accounts.GetByNumber(accountNumber).Returns(account); var auth = new SessionAuthorizer(sessions); - var useCase = new WithdrawUseCase(accounts, operations, auth); + var useCase = new WithdrawUseCase(accounts, operations, auth, new FixedClock()); WithdrawResult result = useCase.Execute(new WithdrawRequest(sessionId, 20m)); @@ -68,4 +68,4 @@ public void WithdrawWhenNotEnoughBalanceReturnsFailureAndDoesNotSave() accounts.DidNotReceive().Update(Arg.Any()); operations.DidNotReceive().Add(Arg.Any()); } -} \ No newline at end of file +} From b5c6bf3005d68c3d19145f44aa90ebaf00fe3916 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Fri, 4 Sep 2026 20:21:11 +0300 Subject: [PATCH 006/103] feat(sessions): add expiry revocation and account identity --- .../Accounts/GetBalanceUseCase.cs | 20 ++--- src/Atm.Application/Atm.Application.csproj | 1 + .../Common/AuthorizationResult.cs | 6 +- .../Common/SessionAuthorizer.cs | 46 ++++------- .../Operations/DepositUseCase.cs | 4 +- .../GetTransactionHistoryUseCase.cs | 37 +++++---- .../Operations/WithdrawUseCase.cs | 4 +- .../Sessions/CreateAdminSessionUseCase.cs | 54 ++++++++----- .../Sessions/CreateUserSessionUseCase.cs | 54 +++++++++---- .../Sessions/SessionOptions.cs | 10 +++ src/Atm.Application/packages.lock.json | 15 ++++ .../Sessions/Results/SessionCreationResult.cs | 10 +++ .../Results/SessionRevocationResult.cs | 10 +++ src/Atm.Domain/Sessions/Session.cs | 78 +++++++++++++----- .../Contracts/ApiResponses.cs | 5 +- src/Atm.Presentation/Program.cs | 11 ++- src/Atm.Presentation/appsettings.json | 4 + src/Atm.Presentation/packages.lock.json | 3 +- .../Atm.Application.Tests/BankingTestData.cs | 14 ++++ .../DepositUseCaseTests.cs | 28 +++---- tests/Atm.Application.Tests/SessionTests.cs | 81 +++++++++++++++++++ .../WithdrawCaseTests.cs | 41 +++++----- .../Atm.Application.Tests/packages.lock.json | 3 +- 23 files changed, 372 insertions(+), 167 deletions(-) create mode 100644 src/Atm.Application/Sessions/SessionOptions.cs create mode 100644 src/Atm.Domain/Sessions/Results/SessionCreationResult.cs create mode 100644 src/Atm.Domain/Sessions/Results/SessionRevocationResult.cs create mode 100644 tests/Atm.Application.Tests/SessionTests.cs diff --git a/src/Atm.Application/Accounts/GetBalanceUseCase.cs b/src/Atm.Application/Accounts/GetBalanceUseCase.cs index 7d6ed8a..d229c0f 100644 --- a/src/Atm.Application/Accounts/GetBalanceUseCase.cs +++ b/src/Atm.Application/Accounts/GetBalanceUseCase.cs @@ -1,4 +1,4 @@ -using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Repositories; using Atm.Application.Common; using Atm.Application.Contracts.Accounts.GetBalance; @@ -18,20 +18,14 @@ public GetBalanceUseCase(IAccountRepository accounts, SessionAuthorizer auth) public GetBalanceResult Execute(GetBalanceRequest request) { AuthorizationResult auth = _auth.AuthorizeUser(request.UserSessionId); - - if (auth is AuthorizationResult.Unauthorized u) - { - return new GetBalanceResult.Unauthorized(u.ErrorMessage); - } + if (auth is AuthorizationResult.Unauthorized unauthorized) + return new GetBalanceResult.Unauthorized(unauthorized.ErrorMessage); var user = (AuthorizationResult.UserSuccess)auth; - Domain.Accounts.Account? acc = _accounts.GetByNumber(user.AccountNumber); - - if (acc is null) - { + Domain.Accounts.Account? account = _accounts.GetById(user.AccountId); + if (account is null) return new GetBalanceResult.Failure("Account not found"); - } - return new GetBalanceResult.Success(acc.Balance.Value); + return new GetBalanceResult.Success(account.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 2424e34..88aff46 100644 --- a/src/Atm.Application/Atm.Application.csproj +++ b/src/Atm.Application/Atm.Application.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Atm.Application/Common/AuthorizationResult.cs b/src/Atm.Application/Common/AuthorizationResult.cs index 6587986..a7501ec 100644 --- a/src/Atm.Application/Common/AuthorizationResult.cs +++ b/src/Atm.Application/Common/AuthorizationResult.cs @@ -1,14 +1,12 @@ -using Atm.Domain.ValueObjects; - namespace Atm.Application.Common; public abstract record AuthorizationResult { private AuthorizationResult() { } - public sealed record UserSuccess(AccountNumber AccountNumber) : AuthorizationResult; + public sealed record UserSuccess(Guid AccountId) : 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 index 2e8d145..9a06463 100644 --- a/src/Atm.Application/Common/SessionAuthorizer.cs +++ b/src/Atm.Application/Common/SessionAuthorizer.cs @@ -1,4 +1,5 @@ -using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Time; using Atm.Domain.Sessions; namespace Atm.Application.Common; @@ -6,48 +7,35 @@ namespace Atm.Application.Common; public sealed class SessionAuthorizer { private readonly ISessionRepository _sessions; + private readonly IClock _clock; - public SessionAuthorizer(ISessionRepository sessions) + public SessionAuthorizer(ISessionRepository sessions, IClock clock) { _sessions = sessions; + _clock = clock; } 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) - { + Session? session = _sessions.GetById(sessionId); + if (session is null || !session.IsActive(_clock.UtcNow)) + return new AuthorizationResult.Unauthorized("Session is missing, expired or revoked."); + if (session.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"); - } + if (session.AccountId is not Guid accountId) + return new AuthorizationResult.Unauthorized("Account is not set."); - return new AuthorizationResult.UserSuccess(s.AccountNumber); + return new AuthorizationResult.UserSuccess(accountId); } 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) - { + Session? session = _sessions.GetById(sessionId); + if (session is null || !session.IsActive(_clock.UtcNow)) + return new AuthorizationResult.Unauthorized("Session is missing, expired or revoked."); + if (session.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/Operations/DepositUseCase.cs b/src/Atm.Application/Operations/DepositUseCase.cs index 50bf22b..c290f07 100644 --- a/src/Atm.Application/Operations/DepositUseCase.cs +++ b/src/Atm.Application/Operations/DepositUseCase.cs @@ -32,7 +32,7 @@ public DepositResult Execute(DepositRequest request) } var user = (AuthorizationResult.UserSuccess)auth; - Domain.Accounts.Account? acc = _accounts.GetByNumber(user.AccountNumber); + Domain.Accounts.Account? acc = _accounts.GetById(user.AccountId); if (acc is null) { @@ -57,7 +57,7 @@ public DepositResult Execute(DepositRequest request) } _accounts.Update(acc); - _operations.Add(new OperationRecord(Guid.NewGuid(), user.AccountNumber, OperationType.Deposit, amount, acc.Balance)); + _operations.Add(new OperationRecord(Guid.NewGuid(), acc.Number, OperationType.Deposit, amount, acc.Balance)); return new DepositResult.Success(acc.Balance.Value); } diff --git a/src/Atm.Application/Operations/GetTransactionHistoryUseCase.cs b/src/Atm.Application/Operations/GetTransactionHistoryUseCase.cs index 78dc825..34ebf12 100644 --- a/src/Atm.Application/Operations/GetTransactionHistoryUseCase.cs +++ b/src/Atm.Application/Operations/GetTransactionHistoryUseCase.cs @@ -1,4 +1,4 @@ -using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Repositories; using Atm.Application.Common; using Atm.Application.Contracts.Operations.GetTransactionHistory; @@ -7,32 +7,41 @@ namespace Atm.Application.Operations; internal sealed class GetTransactionHistoryUseCase : IGetTransactionHistoryPort { private readonly IOperationRepository _operations; + private readonly IAccountRepository _accounts; private readonly SessionAuthorizer _auth; - public GetTransactionHistoryUseCase(IOperationRepository operations, SessionAuthorizer auth) + public GetTransactionHistoryUseCase( + IOperationRepository operations, + IAccountRepository accounts, + SessionAuthorizer auth) { _operations = operations; + _accounts = accounts; _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); - } + if (auth is AuthorizationResult.Unauthorized unauthorized) + return new GetTransactionHistoryResult.Unauthorized(unauthorized.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) + Domain.Accounts.Account? account = _accounts.GetById(user.AccountId); + if (account is null) + return new GetTransactionHistoryResult.Failure("Account not found"); + + IReadOnlyList operations = + _operations.GetByAccountNumber(account.Number); + var transactions = new List(operations.Count); + foreach (Domain.Operations.OperationRecord operation in operations) { - list.Add(new TransactionDto(op.Type.ToString(), op.Amount.Value, op.BalanceAfter.Value)); + transactions.Add(new TransactionDto( + operation.Type.ToString(), + operation.Amount.Value, + operation.BalanceAfter.Value)); } - return new GetTransactionHistoryResult.Success(list); + return new GetTransactionHistoryResult.Success(transactions); } -} \ No newline at end of file +} diff --git a/src/Atm.Application/Operations/WithdrawUseCase.cs b/src/Atm.Application/Operations/WithdrawUseCase.cs index a2158d5..607c44c 100644 --- a/src/Atm.Application/Operations/WithdrawUseCase.cs +++ b/src/Atm.Application/Operations/WithdrawUseCase.cs @@ -32,7 +32,7 @@ public WithdrawResult Execute(WithdrawRequest request) } var user = (AuthorizationResult.UserSuccess)auth; - Domain.Accounts.Account? acc = _accounts.GetByNumber(user.AccountNumber); + Domain.Accounts.Account? acc = _accounts.GetById(user.AccountId); if (acc is null) { @@ -56,7 +56,7 @@ public WithdrawResult Execute(WithdrawRequest request) } _accounts.Update(acc); - _operations.Add(new OperationRecord(Guid.NewGuid(), user.AccountNumber, OperationType.Withdraw, amount, acc.Balance)); + _operations.Add(new OperationRecord(Guid.NewGuid(), acc.Number, OperationType.Withdraw, amount, acc.Balance)); return new WithdrawResult.Success(acc.Balance.Value); } diff --git a/src/Atm.Application/Sessions/CreateAdminSessionUseCase.cs b/src/Atm.Application/Sessions/CreateAdminSessionUseCase.cs index ce90160..84d6dea 100644 --- a/src/Atm.Application/Sessions/CreateAdminSessionUseCase.cs +++ b/src/Atm.Application/Sessions/CreateAdminSessionUseCase.cs @@ -1,7 +1,10 @@ -using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Time; using Atm.Application.Contracts.Sessions.CreateAdminSession; using Atm.Domain.Sessions; +using Atm.Domain.Sessions.Results; using Atm.Domain.ValueObjects; +using Microsoft.Extensions.Options; namespace Atm.Application.Sessions; @@ -9,39 +12,52 @@ internal sealed class CreateAdminSessionUseCase : ICreateAdminSessionPort { private readonly ISessionRepository _sessions; private readonly ISystemPasswordProvider _passwords; - - public CreateAdminSessionUseCase(ISessionRepository sessions, ISystemPasswordProvider passwords) + private readonly IClock _clock; + private readonly SessionOptions _options; + + public CreateAdminSessionUseCase( + ISessionRepository sessions, + ISystemPasswordProvider passwords, + IClock clock, + IOptions options) { _sessions = sessions; _passwords = passwords; + _clock = clock; + _options = options.Value; } 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) - { + if (!string.Equals(request.SystemPassword, expected, StringComparison.Ordinal)) 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); + // Kept only for backward compatibility with the current HTTP contract. + _ = new AccountNumber(request.AccountNumber); } - catch(Exception ex) + catch (ArgumentException exception) { - return new CreateAdminSessionResult.Failure(ex.Message); + return new CreateAdminSessionResult.Failure(exception.Message); } + + DateTimeOffset createdAtUtc = _clock.UtcNow; + SessionCreationResult result = Session.Create( + Guid.NewGuid(), + SessionType.Admin, + null, + createdAtUtc, + createdAtUtc.AddMinutes(_options.AdminLifetimeMinutes)); + + if (result is SessionCreationResult.Failure failure) + return new CreateAdminSessionResult.Failure(failure.ErrorMessage); + + Session session = ((SessionCreationResult.Success)result).Session; + _sessions.Add(session); + return new CreateAdminSessionResult.Success(session.Id); } -} \ No newline at end of file +} diff --git a/src/Atm.Application/Sessions/CreateUserSessionUseCase.cs b/src/Atm.Application/Sessions/CreateUserSessionUseCase.cs index 049dc05..68937a3 100644 --- a/src/Atm.Application/Sessions/CreateUserSessionUseCase.cs +++ b/src/Atm.Application/Sessions/CreateUserSessionUseCase.cs @@ -1,8 +1,11 @@ using Atm.Application.Abstractions.Repositories; using Atm.Application.Abstractions.Security; +using Atm.Application.Abstractions.Time; using Atm.Application.Contracts.Sessions.CreateUserSession; using Atm.Domain.Sessions; +using Atm.Domain.Sessions.Results; using Atm.Domain.ValueObjects; +using Microsoft.Extensions.Options; namespace Atm.Application.Sessions; @@ -11,37 +14,54 @@ internal sealed class CreateUserSessionUseCase : ICreateUserSessionPort private readonly ISessionRepository _sessions; private readonly IAccountRepository _accounts; private readonly IPinHasher _pinHasher; + private readonly IClock _clock; + private readonly SessionOptions _options; - public CreateUserSessionUseCase(ISessionRepository sessions, IAccountRepository accounts, IPinHasher pinHasher) + public CreateUserSessionUseCase( + ISessionRepository sessions, + IAccountRepository accounts, + IPinHasher pinHasher, + IClock clock, + IOptions options) { _sessions = sessions; _accounts = accounts; _pinHasher = pinHasher; + _clock = clock; + _options = options.Value; } public CreateUserSessionResult Execute(CreateUserSessionRequest request) { + AccountNumber number; + PinCode pin; try { - var number = new AccountNumber(request.AccountNumber); - var pin = new PinCode(request.Pin); - Domain.Accounts.Account? acc = _accounts.GetByNumber(number); + number = new AccountNumber(request.AccountNumber); + pin = new PinCode(request.Pin); + } + catch (ArgumentException exception) + { + return new CreateUserSessionResult.Failure(exception.Message); + } - if (acc is null || !_pinHasher.Verify(acc.PinHash, pin)) - { - return new CreateUserSessionResult.Unauthorized("Invalid credentials"); - } + Domain.Accounts.Account? account = _accounts.GetByNumber(number); + if (account is null || !_pinHasher.Verify(account.PinHash, pin)) + return new CreateUserSessionResult.Unauthorized("Invalid credentials"); - var id = Guid.NewGuid(); - var session = new Session(id, SessionType.User, number); + DateTimeOffset createdAtUtc = _clock.UtcNow; + SessionCreationResult result = Session.Create( + Guid.NewGuid(), + SessionType.User, + account.Id, + createdAtUtc, + createdAtUtc.AddMinutes(_options.UserLifetimeMinutes)); - _sessions.Add(session); + if (result is SessionCreationResult.Failure failure) + return new CreateUserSessionResult.Failure(failure.ErrorMessage); - return new CreateUserSessionResult.Success(id); - } - catch (ArgumentException ex) - { - return new CreateUserSessionResult.Failure(ex.Message); - } + Session session = ((SessionCreationResult.Success)result).Session; + _sessions.Add(session); + return new CreateUserSessionResult.Success(session.Id); } } diff --git a/src/Atm.Application/Sessions/SessionOptions.cs b/src/Atm.Application/Sessions/SessionOptions.cs new file mode 100644 index 0000000..1ed0139 --- /dev/null +++ b/src/Atm.Application/Sessions/SessionOptions.cs @@ -0,0 +1,10 @@ +namespace Atm.Application.Sessions; + +public sealed class SessionOptions +{ + public const string SectionName = "Sessions"; + + public int UserLifetimeMinutes { get; set; } + + public int AdminLifetimeMinutes { get; set; } +} diff --git a/src/Atm.Application/packages.lock.json b/src/Atm.Application/packages.lock.json index e4e5f9e..b7879a5 100644 --- a/src/Atm.Application/packages.lock.json +++ b/src/Atm.Application/packages.lock.json @@ -8,6 +8,21 @@ "resolved": "9.0.19", "contentHash": "XgQ0aVWClYjYSBqbYrKC/xyZ1KHf+VVwNF3+d54jcaqnst30Q//ugs/FJwUztLZiph0gp85g7i/eB0556twO0Q==" }, + "Microsoft.Extensions.Options": { + "type": "Direct", + "requested": "[9.0.19, )", + "resolved": "9.0.19", + "contentHash": "FdPZy4kUSwFlIVZ3EkK9fdHw2RfblUwzflSb4kjVeGf9v2lX9IH+q2RGUovYMR7CVfKMc2y2Db/bZ2b4yiMfOg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19", + "Microsoft.Extensions.Primitives": "9.0.19" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "9hIg8PQiMnpVFIsEHm25Wi1gBrPa2pS1G75uveyDUVLXrNKqBap9WGwQIgO0s6LUgRAOdsSYnbKbNC5b1G5Pcg==" + }, "atm.application.abstractions": { "type": "Project", "dependencies": { diff --git a/src/Atm.Domain/Sessions/Results/SessionCreationResult.cs b/src/Atm.Domain/Sessions/Results/SessionCreationResult.cs new file mode 100644 index 0000000..ac4269d --- /dev/null +++ b/src/Atm.Domain/Sessions/Results/SessionCreationResult.cs @@ -0,0 +1,10 @@ +namespace Atm.Domain.Sessions.Results; + +public abstract record SessionCreationResult +{ + private SessionCreationResult() { } + + public sealed record Success(Session Session) : SessionCreationResult; + + public sealed record Failure(string ErrorMessage) : SessionCreationResult; +} diff --git a/src/Atm.Domain/Sessions/Results/SessionRevocationResult.cs b/src/Atm.Domain/Sessions/Results/SessionRevocationResult.cs new file mode 100644 index 0000000..c71e1b0 --- /dev/null +++ b/src/Atm.Domain/Sessions/Results/SessionRevocationResult.cs @@ -0,0 +1,10 @@ +namespace Atm.Domain.Sessions.Results; + +public abstract record SessionRevocationResult +{ + private SessionRevocationResult() { } + + public sealed record Success(DateTimeOffset RevokedAtUtc) : SessionRevocationResult; + + public sealed record Failure(string ErrorMessage) : SessionRevocationResult; +} diff --git a/src/Atm.Domain/Sessions/Session.cs b/src/Atm.Domain/Sessions/Session.cs index 8284340..6582ace 100644 --- a/src/Atm.Domain/Sessions/Session.cs +++ b/src/Atm.Domain/Sessions/Session.cs @@ -1,34 +1,70 @@ -using Atm.Domain.ValueObjects; +using Atm.Domain.Common; +using Atm.Domain.Sessions.Results; namespace Atm.Domain.Sessions; public sealed class Session { - public Session(Guid id, SessionType type, AccountNumber? accountNumber) + private Session() { } + + public Guid Id { get; private set; } + + public SessionType Type { get; private set; } + + public Guid? AccountId { get; private set; } + + public DateTimeOffset CreatedAtUtc { get; private set; } + + public DateTimeOffset ExpiresAtUtc { get; private set; } + + public DateTimeOffset? RevokedAtUtc { get; private set; } + + public static SessionCreationResult Create( + Guid id, + SessionType type, + Guid? accountId, + DateTimeOffset createdAtUtc, + DateTimeOffset expiresAtUtc) { if (id == Guid.Empty) - { - throw new ArgumentException("Session ID cannot be empty"); - } + return new SessionCreationResult.Failure("Session ID must not be empty."); + if (!Enum.IsDefined(type)) + return new SessionCreationResult.Failure("Session type is not supported."); + if (type == SessionType.User && (accountId is null || accountId == Guid.Empty)) + return new SessionCreationResult.Failure("User session must reference an account."); + if (type == SessionType.Admin && accountId is not null) + return new SessionCreationResult.Failure("System admin session must not reference an account."); + if (!DomainRules.IsUtc(createdAtUtc) || !DomainRules.IsUtc(expiresAtUtc)) + return new SessionCreationResult.Failure("Session timestamps must be UTC."); + if (expiresAtUtc <= createdAtUtc) + return new SessionCreationResult.Failure("Session expiration must follow its creation time."); - if (type == SessionType.User && accountNumber is null) + return new SessionCreationResult.Success(new Session { - 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; + Id = id, + Type = type, + AccountId = accountId, + CreatedAtUtc = createdAtUtc, + ExpiresAtUtc = expiresAtUtc, + }); } - public Guid Id { get; } + public bool IsActive(DateTimeOffset utcNow) + { + return DomainRules.IsUtc(utcNow) + && utcNow >= CreatedAtUtc + && utcNow < ExpiresAtUtc + && RevokedAtUtc is null; + } - public SessionType Type { get; } + public SessionRevocationResult Revoke(DateTimeOffset revokedAtUtc) + { + if (RevokedAtUtc is DateTimeOffset existing) + return new SessionRevocationResult.Success(existing); + if (!DomainRules.IsUtc(revokedAtUtc) || revokedAtUtc < CreatedAtUtc) + return new SessionRevocationResult.Failure("Revocation time must be UTC and must not precede session creation."); - public AccountNumber? AccountNumber { get; } -} \ No newline at end of file + RevokedAtUtc = revokedAtUtc; + return new SessionRevocationResult.Success(revokedAtUtc); + } +} diff --git a/src/Atm.Presentation/Contracts/ApiResponses.cs b/src/Atm.Presentation/Contracts/ApiResponses.cs index 7cbdf07..c2aaf64 100644 --- a/src/Atm.Presentation/Contracts/ApiResponses.cs +++ b/src/Atm.Presentation/Contracts/ApiResponses.cs @@ -5,10 +5,10 @@ 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); @@ -17,4 +17,3 @@ public sealed record MessageResponse( public sealed record TransactionsResponse( [property: JsonPropertyName("transactions")] IReadOnlyList Transactions); - \ No newline at end of file diff --git a/src/Atm.Presentation/Program.cs b/src/Atm.Presentation/Program.cs index 8cf0616..74a7068 100644 --- a/src/Atm.Presentation/Program.cs +++ b/src/Atm.Presentation/Program.cs @@ -1,7 +1,8 @@ -using Atm.Application; +using Atm.Application; using Atm.Application.Abstractions.Repositories; using Atm.Infrastructure; using Atm.Presentation.Common; +using ApplicationSessionOptions = Atm.Application.Sessions.SessionOptions; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -18,6 +19,12 @@ builder.Services.AddApplication(); builder.Services.AddInfrastructure(); +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection(ApplicationSessionOptions.SectionName)) + .Validate(options => options.UserLifetimeMinutes > 0, "User session lifetime must be positive.") + .Validate(options => options.AdminLifetimeMinutes > 0, "Admin session lifetime must be positive.") + .ValidateOnStart(); + builder.Services.AddSingleton(); WebApplication app = builder.Build(); @@ -31,4 +38,4 @@ app.UseHttpsRedirection(); app.MapControllers(); -app.Run(); \ No newline at end of file +app.Run(); diff --git a/src/Atm.Presentation/appsettings.json b/src/Atm.Presentation/appsettings.json index 52a8f53..27151bb 100644 --- a/src/Atm.Presentation/appsettings.json +++ b/src/Atm.Presentation/appsettings.json @@ -2,6 +2,10 @@ "SystemAuth": { "SystemPassword": "change_me" }, + "Sessions": { + "UserLifetimeMinutes": 30, + "AdminLifetimeMinutes": 15 + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/src/Atm.Presentation/packages.lock.json b/src/Atm.Presentation/packages.lock.json index 013d951..00449bf 100644 --- a/src/Atm.Presentation/packages.lock.json +++ b/src/Atm.Presentation/packages.lock.json @@ -197,7 +197,8 @@ "Atm.Application.Abstractions": "[1.0.0, )", "Atm.Application.Contracts": "[1.0.0, )", "Atm.Domain": "[1.0.0, )", - "Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.19, )" + "Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.19, )", + "Microsoft.Extensions.Options": "[9.0.19, )" } }, "atm.application.abstractions": { diff --git a/tests/Atm.Application.Tests/BankingTestData.cs b/tests/Atm.Application.Tests/BankingTestData.cs index 8bda6a6..3c3f56e 100644 --- a/tests/Atm.Application.Tests/BankingTestData.cs +++ b/tests/Atm.Application.Tests/BankingTestData.cs @@ -1,5 +1,7 @@ using Atm.Domain.Accounts; using Atm.Domain.Accounts.Results; +using Atm.Domain.Sessions; +using Atm.Domain.Sessions.Results; using Atm.Domain.ValueObjects; namespace Atm.Application.Tests; @@ -12,4 +14,16 @@ public static Account CreateAccount(AccountNumber number, decimal balance) Guid.NewGuid(), number, Guid.NewGuid(), new PinHash("test-only-hash"), new Money(balance), new FixedClock().UtcNow)).Account; } + + public static Session CreateUserSession(Guid id, Account account, FixedClock clock) + { + return Assert.IsType(Session.Create( + id, SessionType.User, account.Id, clock.UtcNow, clock.UtcNow.AddMinutes(30))).Session; + } + + public static Session CreateAdminSession(Guid id, FixedClock clock) + { + return Assert.IsType(Session.Create( + id, SessionType.Admin, null, clock.UtcNow, clock.UtcNow.AddMinutes(15))).Session; + } } diff --git a/tests/Atm.Application.Tests/DepositUseCaseTests.cs b/tests/Atm.Application.Tests/DepositUseCaseTests.cs index 7d43cac..6edd8e5 100644 --- a/tests/Atm.Application.Tests/DepositUseCaseTests.cs +++ b/tests/Atm.Application.Tests/DepositUseCaseTests.cs @@ -4,7 +4,6 @@ using Atm.Application.Operations; using Atm.Domain.Accounts; using Atm.Domain.Operations; -using Atm.Domain.Sessions; using Atm.Domain.ValueObjects; using NSubstitute; @@ -18,29 +17,26 @@ public void DepositWhenValidIncreaseBalanceAndSavesOperation() IAccountRepository accounts = Substitute.For(); ISessionRepository sessions = Substitute.For(); IOperationRepository operations = Substitute.For(); - + var clock = new FixedClock(); var accountNumber = new AccountNumber("aboba"); var sessionId = Guid.NewGuid(); - - sessions.GetById(sessionId).Returns(new Session(sessionId, SessionType.User, accountNumber)); - Account account = BankingTestData.CreateAccount(accountNumber, 100m); - accounts.GetByNumber(accountNumber).Returns(account); - var auth = new SessionAuthorizer(sessions); - var useCase = new DepositUseCase(accounts, operations, auth, new FixedClock()); + sessions.GetById(sessionId).Returns(BankingTestData.CreateUserSession(sessionId, account, clock)); + accounts.GetById(account.Id).Returns(account); + + var auth = new SessionAuthorizer(sessions, clock); + var useCase = new DepositUseCase(accounts, operations, auth, clock); 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)); + accounts.Received(1).Update(Arg.Is(item => item.Balance.Value == 150m)); + operations.Received(1).Add(Arg.Is(operation => + operation.AccountNumber == accountNumber && + operation.Type == OperationType.Deposit && + operation.Amount.Value == 50m && + operation.BalanceAfter.Value == 150m)); } } diff --git a/tests/Atm.Application.Tests/SessionTests.cs b/tests/Atm.Application.Tests/SessionTests.cs new file mode 100644 index 0000000..576f04e --- /dev/null +++ b/tests/Atm.Application.Tests/SessionTests.cs @@ -0,0 +1,81 @@ +using Atm.Application.Abstractions.Repositories; +using Atm.Application.Common; +using Atm.Domain.Accounts; +using Atm.Domain.Sessions; +using Atm.Domain.Sessions.Results; +using Atm.Domain.ValueObjects; +using NSubstitute; + +namespace Atm.Application.Tests; + +public sealed class SessionTests +{ + [Fact] + public void CreateEnforcesIdentityShapeAndUtcLifetime() + { + var clock = new FixedClock(); + + Assert.IsType(Session.Create( + Guid.Empty, SessionType.Admin, null, clock.UtcNow, clock.UtcNow.AddMinutes(1))); + Assert.IsType(Session.Create( + Guid.NewGuid(), SessionType.User, null, clock.UtcNow, clock.UtcNow.AddMinutes(1))); + Assert.IsType(Session.Create( + Guid.NewGuid(), SessionType.Admin, Guid.NewGuid(), clock.UtcNow, clock.UtcNow.AddMinutes(1))); + Assert.IsType(Session.Create( + Guid.NewGuid(), SessionType.Admin, null, clock.UtcNow, clock.UtcNow)); + Assert.IsType(Session.Create( + Guid.NewGuid(), SessionType.Admin, null, + clock.UtcNow.ToOffset(TimeSpan.FromHours(3)), clock.UtcNow.AddMinutes(1))); + } + + [Fact] + public void IsActiveUsesInclusiveStartAndExclusiveExpiration() + { + var clock = new FixedClock(); + Session session = BankingTestData.CreateAdminSession(Guid.NewGuid(), clock); + + Assert.True(session.IsActive(clock.UtcNow)); + Assert.True(session.IsActive(session.ExpiresAtUtc.AddTicks(-1))); + Assert.False(session.IsActive(session.ExpiresAtUtc)); + Assert.False(session.IsActive(clock.UtcNow.AddTicks(-1))); + Assert.False(session.IsActive(clock.UtcNow.ToOffset(TimeSpan.FromHours(3)))); + } + + [Fact] + public void RevokeIsIdempotentAndKeepsTheFirstTimestamp() + { + var clock = new FixedClock(); + Session session = BankingTestData.CreateAdminSession(Guid.NewGuid(), clock); + DateTimeOffset first = clock.UtcNow.AddMinutes(1); + + Assert.IsType(session.Revoke(clock.UtcNow.AddTicks(-1))); + Assert.IsType(session.Revoke(first)); + SessionRevocationResult.Success repeated = Assert.IsType( + session.Revoke(first.AddMinutes(1))); + + Assert.Equal(first, repeated.RevokedAtUtc); + Assert.Equal(first, session.RevokedAtUtc); + Assert.False(session.IsActive(first)); + } + + [Fact] + public void AuthorizerRejectsExpiredAndRevokedSessions() + { + ISessionRepository sessions = Substitute.For(); + var clock = new FixedClock(); + Account account = BankingTestData.CreateAccount(new AccountNumber("session-test"), 0m); + var sessionId = Guid.NewGuid(); + Session session = BankingTestData.CreateUserSession(sessionId, account, clock); + sessions.GetById(sessionId).Returns(session); + var authorizer = new SessionAuthorizer(sessions, clock); + + Assert.IsType(authorizer.AuthorizeUser(sessionId)); + + clock.UtcNow = session.ExpiresAtUtc; + Assert.IsType(authorizer.AuthorizeUser(sessionId)); + + clock.UtcNow = session.CreatedAtUtc; + Assert.IsType(session.Revoke(clock.UtcNow)); + Assert.IsType(authorizer.AuthorizeUser(sessionId)); + } +} diff --git a/tests/Atm.Application.Tests/WithdrawCaseTests.cs b/tests/Atm.Application.Tests/WithdrawCaseTests.cs index 09a5015..f9711d7 100644 --- a/tests/Atm.Application.Tests/WithdrawCaseTests.cs +++ b/tests/Atm.Application.Tests/WithdrawCaseTests.cs @@ -4,7 +4,6 @@ using Atm.Application.Operations; using Atm.Domain.Accounts; using Atm.Domain.Operations; -using Atm.Domain.Sessions; using Atm.Domain.ValueObjects; using NSubstitute; @@ -18,30 +17,27 @@ public void WithdrawWhenEnoughBalanceDecreasesBalanceAndSavesOperation() IAccountRepository accounts = Substitute.For(); ISessionRepository sessions = Substitute.For(); IOperationRepository operations = Substitute.For(); - + var clock = new FixedClock(); var accountNumber = new AccountNumber("aboba-2"); var sessionId = Guid.NewGuid(); - - sessions.GetById(sessionId).Returns(new Session(sessionId, SessionType.User, accountNumber)); - Account account = BankingTestData.CreateAccount(accountNumber, 100m); - accounts.GetByNumber(accountNumber).Returns(account); - var auth = new SessionAuthorizer(sessions); - var useCase = new WithdrawUseCase(accounts, operations, auth, new FixedClock()); + sessions.GetById(sessionId).Returns(BankingTestData.CreateUserSession(sessionId, account, clock)); + accounts.GetById(account.Id).Returns(account); + + var auth = new SessionAuthorizer(sessions, clock); + var useCase = new WithdrawUseCase(accounts, operations, auth, clock); 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)); + accounts.Received(1).Update(Arg.Is(item => item.Balance.Value == 60m)); + operations.Received(1).Add(Arg.Is(operation => + operation.AccountNumber == accountNumber && + operation.Type == OperationType.Withdraw && + operation.Amount.Value == 40m && + operation.BalanceAfter.Value == 60m)); } [Fact] @@ -50,17 +46,16 @@ public void WithdrawWhenNotEnoughBalanceReturnsFailureAndDoesNotSave() IAccountRepository accounts = Substitute.For(); ISessionRepository sessions = Substitute.For(); IOperationRepository operations = Substitute.For(); - + var clock = new FixedClock(); var accountNumber = new AccountNumber("aboba-666"); var sessionId = Guid.NewGuid(); - - sessions.GetById(sessionId).Returns(new Session(sessionId, SessionType.User, accountNumber)); - Account account = BankingTestData.CreateAccount(accountNumber, 10m); - accounts.GetByNumber(accountNumber).Returns(account); - var auth = new SessionAuthorizer(sessions); - var useCase = new WithdrawUseCase(accounts, operations, auth, new FixedClock()); + sessions.GetById(sessionId).Returns(BankingTestData.CreateUserSession(sessionId, account, clock)); + accounts.GetById(account.Id).Returns(account); + + var auth = new SessionAuthorizer(sessions, clock); + var useCase = new WithdrawUseCase(accounts, operations, auth, clock); WithdrawResult result = useCase.Execute(new WithdrawRequest(sessionId, 20m)); diff --git a/tests/Atm.Application.Tests/packages.lock.json b/tests/Atm.Application.Tests/packages.lock.json index 3080a22..3376167 100644 --- a/tests/Atm.Application.Tests/packages.lock.json +++ b/tests/Atm.Application.Tests/packages.lock.json @@ -281,7 +281,8 @@ "Atm.Application.Abstractions": "[1.0.0, )", "Atm.Application.Contracts": "[1.0.0, )", "Atm.Domain": "[1.0.0, )", - "Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.19, )" + "Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.19, )", + "Microsoft.Extensions.Options": "[9.0.19, )" } }, "atm.application.abstractions": { From 56927ed4415de791ef3c04553877d960c825402a Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Fri, 4 Sep 2026 20:24:21 +0300 Subject: [PATCH 007/103] feat(domain): add banking transactions and immutable history --- .../Operations/AccountHistoryEntry.cs | 84 ++++++++++ .../Operations/AccountHistoryEntryType.cs | 9 ++ src/Atm.Domain/Operations/BankTransaction.cs | 123 ++++++++++++++ .../Operations/BankTransactionType.cs | 8 + .../Operations/CommissionSnapshot.cs | 45 ++++++ .../AccountHistoryEntryCreationResult.cs | 10 ++ .../Results/BankTransactionCreationResult.cs | 10 ++ .../BankTransactionTests.cs | 153 ++++++++++++++++++ 8 files changed, 442 insertions(+) create mode 100644 src/Atm.Domain/Operations/AccountHistoryEntry.cs create mode 100644 src/Atm.Domain/Operations/AccountHistoryEntryType.cs create mode 100644 src/Atm.Domain/Operations/BankTransaction.cs create mode 100644 src/Atm.Domain/Operations/BankTransactionType.cs create mode 100644 src/Atm.Domain/Operations/CommissionSnapshot.cs create mode 100644 src/Atm.Domain/Operations/Results/AccountHistoryEntryCreationResult.cs create mode 100644 src/Atm.Domain/Operations/Results/BankTransactionCreationResult.cs create mode 100644 tests/Atm.Application.Tests/BankTransactionTests.cs diff --git a/src/Atm.Domain/Operations/AccountHistoryEntry.cs b/src/Atm.Domain/Operations/AccountHistoryEntry.cs new file mode 100644 index 0000000..d033777 --- /dev/null +++ b/src/Atm.Domain/Operations/AccountHistoryEntry.cs @@ -0,0 +1,84 @@ +using Atm.Domain.Operations.Results; +using Atm.Domain.ValueObjects; + +namespace Atm.Domain.Operations; + +public sealed class AccountHistoryEntry +{ + private AccountHistoryEntry() { } + + public Guid Id { get; private set; } + + public Guid TransactionId { get; private set; } + + public Guid AccountId { get; private set; } + + public AccountHistoryEntryType EntryType { get; private set; } + + public Money BalanceBefore { get; private set; } = null!; + + public Money BalanceAfter { get; private set; } = null!; + + public DateTimeOffset CreatedAtUtc { get; private set; } + + public static AccountHistoryEntryCreationResult Create( + Guid id, + BankTransaction? transaction, + Guid accountId, + Money? balanceBefore) + { + if (id == Guid.Empty || accountId == Guid.Empty) + return new AccountHistoryEntryCreationResult.Failure("History entry and account IDs must not be empty."); + if (transaction is null || balanceBefore is null) + return new AccountHistoryEntryCreationResult.Failure("Transaction and previous balance are required."); + + AccountHistoryEntryType entryType; + Money balanceAfter; + try + { + if (transaction.Type == BankTransactionType.Deposit && transaction.TargetAccountId == accountId) + { + entryType = AccountHistoryEntryType.Deposit; + balanceAfter = balanceBefore.Add(transaction.GetDepositCreditAmount()); + } + else if (transaction.Type == BankTransactionType.Withdrawal && transaction.SourceAccountId == accountId) + { + entryType = AccountHistoryEntryType.Withdrawal; + balanceAfter = balanceBefore.Subtract(transaction.GetSourceDebitAmount()); + } + else if (transaction.Type == BankTransactionType.Transfer && transaction.SourceAccountId == accountId) + { + entryType = AccountHistoryEntryType.TransferDebit; + balanceAfter = balanceBefore.Subtract(transaction.GetSourceDebitAmount()); + } + else if (transaction.Type == BankTransactionType.Transfer && transaction.TargetAccountId == accountId) + { + entryType = AccountHistoryEntryType.TransferCredit; + balanceAfter = balanceBefore.Add(transaction.Amount); + } + else + { + return new AccountHistoryEntryCreationResult.Failure("Account does not participate in the transaction."); + } + } + catch (InvalidOperationException exception) + { + return new AccountHistoryEntryCreationResult.Failure(exception.Message); + } + catch (OverflowException) + { + return new AccountHistoryEntryCreationResult.Failure("Resulting balance cannot be represented exactly as decimal."); + } + + return new AccountHistoryEntryCreationResult.Success(new AccountHistoryEntry + { + Id = id, + TransactionId = transaction.Id, + AccountId = accountId, + EntryType = entryType, + BalanceBefore = balanceBefore, + BalanceAfter = balanceAfter, + CreatedAtUtc = transaction.CreatedAtUtc, + }); + } +} diff --git a/src/Atm.Domain/Operations/AccountHistoryEntryType.cs b/src/Atm.Domain/Operations/AccountHistoryEntryType.cs new file mode 100644 index 0000000..aa94227 --- /dev/null +++ b/src/Atm.Domain/Operations/AccountHistoryEntryType.cs @@ -0,0 +1,9 @@ +namespace Atm.Domain.Operations; + +public enum AccountHistoryEntryType +{ + Deposit = 1, + Withdrawal = 2, + TransferDebit = 3, + TransferCredit = 4, +} diff --git a/src/Atm.Domain/Operations/BankTransaction.cs b/src/Atm.Domain/Operations/BankTransaction.cs new file mode 100644 index 0000000..b897fda --- /dev/null +++ b/src/Atm.Domain/Operations/BankTransaction.cs @@ -0,0 +1,123 @@ +using Atm.Domain.Common; +using Atm.Domain.Operations.Results; +using Atm.Domain.ValueObjects; + +namespace Atm.Domain.Operations; + +public sealed class BankTransaction +{ + private BankTransaction() { } + + public Guid Id { get; private set; } + + public Guid RequestId { get; private set; } + + public BankTransactionType Type { get; private set; } + + public Guid? SourceAccountId { get; private set; } + + public Guid? TargetAccountId { get; private set; } + + public Amount Amount { get; private set; } = null!; + + public CommissionSnapshot Commission { get; private set; } = null!; + + public DateTimeOffset CreatedAtUtc { get; private set; } + + public static BankTransactionCreationResult Create( + Guid id, + Guid requestId, + BankTransactionType type, + Guid? sourceAccountId, + Guid? targetAccountId, + Amount? amount, + CommissionSnapshot? commission, + DateTimeOffset createdAtUtc) + { + if (id == Guid.Empty || requestId == Guid.Empty) + return new BankTransactionCreationResult.Failure("Transaction and request IDs must not be empty."); + if (!Enum.IsDefined(type)) + return new BankTransactionCreationResult.Failure("Transaction type is not supported."); + if (amount is null || commission is null) + return new BankTransactionCreationResult.Failure("Amount and commission snapshot are required."); + if (!commission.IsCalculatedFor(amount)) + return new BankTransactionCreationResult.Failure("Commission snapshot does not match the transaction amount."); + if (!DomainRules.IsUtc(createdAtUtc)) + return new BankTransactionCreationResult.Failure("Transaction timestamp must be UTC."); + + string? shapeError = ValidateAccountShape(type, sourceAccountId, targetAccountId); + if (shapeError is not null) + return new BankTransactionCreationResult.Failure(shapeError); + + try + { + if (type == BankTransactionType.Deposit) + _ = CalculateDepositCredit(amount, commission); + else + _ = CalculateSourceDebit(amount, commission); + } + catch (InvalidOperationException exception) + { + return new BankTransactionCreationResult.Failure(exception.Message); + } + catch (OverflowException) + { + return new BankTransactionCreationResult.Failure("The transaction amount cannot be represented exactly as decimal."); + } + + return new BankTransactionCreationResult.Success(new BankTransaction + { + Id = id, + RequestId = requestId, + Type = type, + SourceAccountId = sourceAccountId, + TargetAccountId = targetAccountId, + Amount = amount, + Commission = commission, + CreatedAtUtc = createdAtUtc, + }); + } + + public Amount GetDepositCreditAmount() + { + if (Type != BankTransactionType.Deposit) + throw new InvalidOperationException("Only a deposit has a deposit credit amount."); + return CalculateDepositCredit(Amount, Commission); + } + + public Amount GetSourceDebitAmount() + { + if (Type == BankTransactionType.Deposit) + throw new InvalidOperationException("A deposit has no source debit amount."); + return CalculateSourceDebit(Amount, Commission); + } + + private static string? ValidateAccountShape( + BankTransactionType type, + Guid? sourceAccountId, + Guid? targetAccountId) + { + bool hasSource = sourceAccountId is Guid source && source != Guid.Empty; + bool hasTarget = targetAccountId is Guid target && target != Guid.Empty; + + return type switch + { + BankTransactionType.Deposit when !hasSource && hasTarget => null, + BankTransactionType.Withdrawal when hasSource && !hasTarget => null, + BankTransactionType.Transfer when hasSource && hasTarget && sourceAccountId != targetAccountId => null, + _ => "Source and target accounts do not match the transaction type.", + }; + } + + private static Amount CalculateDepositCredit(Amount amount, CommissionSnapshot commission) + { + if (amount.Value <= commission.CommissionAmount.Value) + throw new InvalidOperationException("Deposit amount must exceed its commission."); + return new Amount(DomainRules.SubtractExact(amount.Value, commission.CommissionAmount.Value)); + } + + private static Amount CalculateSourceDebit(Amount amount, CommissionSnapshot commission) + { + return new Amount(DomainRules.AddExact(amount.Value, commission.CommissionAmount.Value)); + } +} diff --git a/src/Atm.Domain/Operations/BankTransactionType.cs b/src/Atm.Domain/Operations/BankTransactionType.cs new file mode 100644 index 0000000..dcd858b --- /dev/null +++ b/src/Atm.Domain/Operations/BankTransactionType.cs @@ -0,0 +1,8 @@ +namespace Atm.Domain.Operations; + +public enum BankTransactionType +{ + Deposit = 1, + Withdrawal = 2, + Transfer = 3, +} diff --git a/src/Atm.Domain/Operations/CommissionSnapshot.cs b/src/Atm.Domain/Operations/CommissionSnapshot.cs new file mode 100644 index 0000000..d0740ab --- /dev/null +++ b/src/Atm.Domain/Operations/CommissionSnapshot.cs @@ -0,0 +1,45 @@ +using Atm.Domain.Common; +using Atm.Domain.ValueObjects; + +namespace Atm.Domain.Operations; + +public sealed class CommissionSnapshot +{ + private CommissionSnapshot() { } + + public CommissionSnapshot(Amount operationAmount, decimal rate, decimal fixedAmount) + { + ArgumentNullException.ThrowIfNull(operationAmount); + if (rate < 0m || rate > 1m || !DomainRules.HasScale(rate, 6)) + throw new ArgumentException("Commission rate must be between zero and one with at most six decimal places.", nameof(rate)); + + var fixedMoney = new Money(fixedAmount); + decimal proportional = checked(operationAmount.Value * rate); + decimal unrounded = DomainRules.AddExact(proportional, fixedMoney.Value); + decimal rounded = decimal.Round(unrounded, 4, MidpointRounding.ToEven); + + Rate = rate; + FixedAmount = fixedMoney; + CommissionAmount = new Money(rounded); + } + + public decimal Rate { get; private set; } + + public Money FixedAmount { get; private set; } = null!; + + public Money CommissionAmount { get; private set; } = null!; + + public bool IsCalculatedFor(Amount operationAmount) + { + ArgumentNullException.ThrowIfNull(operationAmount); + try + { + var expected = new CommissionSnapshot(operationAmount, Rate, FixedAmount.Value); + return expected.CommissionAmount.Value == CommissionAmount.Value; + } + catch (OverflowException) + { + return false; + } + } +} diff --git a/src/Atm.Domain/Operations/Results/AccountHistoryEntryCreationResult.cs b/src/Atm.Domain/Operations/Results/AccountHistoryEntryCreationResult.cs new file mode 100644 index 0000000..3b87e6f --- /dev/null +++ b/src/Atm.Domain/Operations/Results/AccountHistoryEntryCreationResult.cs @@ -0,0 +1,10 @@ +namespace Atm.Domain.Operations.Results; + +public abstract record AccountHistoryEntryCreationResult +{ + private AccountHistoryEntryCreationResult() { } + + public sealed record Success(AccountHistoryEntry Entry) : AccountHistoryEntryCreationResult; + + public sealed record Failure(string ErrorMessage) : AccountHistoryEntryCreationResult; +} diff --git a/src/Atm.Domain/Operations/Results/BankTransactionCreationResult.cs b/src/Atm.Domain/Operations/Results/BankTransactionCreationResult.cs new file mode 100644 index 0000000..aa5023f --- /dev/null +++ b/src/Atm.Domain/Operations/Results/BankTransactionCreationResult.cs @@ -0,0 +1,10 @@ +namespace Atm.Domain.Operations.Results; + +public abstract record BankTransactionCreationResult +{ + private BankTransactionCreationResult() { } + + public sealed record Success(BankTransaction Transaction) : BankTransactionCreationResult; + + public sealed record Failure(string ErrorMessage) : BankTransactionCreationResult; +} diff --git a/tests/Atm.Application.Tests/BankTransactionTests.cs b/tests/Atm.Application.Tests/BankTransactionTests.cs new file mode 100644 index 0000000..5f91d95 --- /dev/null +++ b/tests/Atm.Application.Tests/BankTransactionTests.cs @@ -0,0 +1,153 @@ +using Atm.Domain.Operations; +using Atm.Domain.Operations.Results; +using Atm.Domain.ValueObjects; + +namespace Atm.Application.Tests; + +public sealed class BankTransactionTests +{ + private static readonly DateTimeOffset Now = new(2026, 9, 4, 12, 0, 0, TimeSpan.Zero); + + [Fact] + public void CommissionSnapshotUsesBankersRoundingAndStoresTheTariff() + { + var roundDown = new CommissionSnapshot(new Amount(1.0001m), 0.5m, 0m); + var roundUp = new CommissionSnapshot(new Amount(1.0003m), 0.5m, 0m); + + Assert.Equal(0.5m, roundDown.Rate); + Assert.Equal(0m, roundDown.FixedAmount.Value); + Assert.Equal(0.5000m, roundDown.CommissionAmount.Value); + Assert.Equal(0.5002m, roundUp.CommissionAmount.Value); + } + + [Theory] + [InlineData(-0.000001)] + [InlineData(1.000001)] + [InlineData(0.1234567)] + public void CommissionSnapshotRejectsInvalidRate(decimal rate) + { + Assert.Throws(() => new CommissionSnapshot(new Amount(10m), rate, 0m)); + } + + [Fact] + public void CommissionSnapshotRejectsInvalidFixedAmountAndPrecisionLoss() + { + Assert.Throws(() => new CommissionSnapshot(new Amount(10m), 0m, -1m)); + Assert.Throws(() => new CommissionSnapshot(new Amount(10m), 0m, 0.00001m)); + Assert.Throws(() => + new CommissionSnapshot(new Amount(decimal.MaxValue), 1m, 0.0001m)); + } + + [Fact] + public void CreateEnforcesAccountShapeRequestIdAndUtc() + { + var source = Guid.NewGuid(); + var target = Guid.NewGuid(); + var amount = new Amount(10m); + var commission = new CommissionSnapshot(amount, 0m, 0m); + + Assert.IsType(BankTransaction.Create( + Guid.NewGuid(), Guid.Empty, BankTransactionType.Deposit, null, target, amount, commission, Now)); + Assert.IsType(BankTransaction.Create( + Guid.NewGuid(), Guid.NewGuid(), BankTransactionType.Deposit, source, target, amount, commission, Now)); + Assert.IsType(BankTransaction.Create( + Guid.NewGuid(), Guid.NewGuid(), BankTransactionType.Withdrawal, source, target, amount, commission, Now)); + Assert.IsType(BankTransaction.Create( + Guid.NewGuid(), Guid.NewGuid(), BankTransactionType.Transfer, source, source, amount, commission, Now)); + Assert.IsType(BankTransaction.Create( + Guid.NewGuid(), Guid.NewGuid(), BankTransactionType.Deposit, null, target, amount, commission, + Now.ToOffset(TimeSpan.FromHours(3)))); + } + + [Fact] + public void CreateRejectsMismatchedOrEconomicallyInvalidCommission() + { + var target = Guid.NewGuid(); + var originalAmount = new Amount(100m); + var mismatchedAmount = new Amount(101m); + var commission = new CommissionSnapshot(originalAmount, 0.1m, 0m); + + Assert.IsType(BankTransaction.Create( + Guid.NewGuid(), Guid.NewGuid(), BankTransactionType.Deposit, + null, target, mismatchedAmount, commission, Now)); + + var smallAmount = new Amount(1m); + var fullCommission = new CommissionSnapshot(smallAmount, 0m, 1m); + Assert.IsType(BankTransaction.Create( + Guid.NewGuid(), Guid.NewGuid(), BankTransactionType.Deposit, + null, target, smallAmount, fullCommission, Now)); + } + + [Fact] + public void HistoryEntryCalculatesDepositAndWithdrawalBalances() + { + var accountId = Guid.NewGuid(); + BankTransaction deposit = CreateTransaction( + BankTransactionType.Deposit, null, accountId, 100m, 0m, 10m); + AccountHistoryEntry depositEntry = Assert.IsType( + AccountHistoryEntry.Create(Guid.NewGuid(), deposit, accountId, new Money(10m))).Entry; + + Assert.Equal(AccountHistoryEntryType.Deposit, depositEntry.EntryType); + Assert.Equal(10m, depositEntry.BalanceBefore.Value); + Assert.Equal(100m, depositEntry.BalanceAfter.Value); + Assert.Equal(deposit.Id, depositEntry.TransactionId); + Assert.Equal(deposit.CreatedAtUtc, depositEntry.CreatedAtUtc); + + BankTransaction withdrawal = CreateTransaction( + BankTransactionType.Withdrawal, accountId, null, 20m, 0m, 2m); + AccountHistoryEntry withdrawalEntry = Assert.IsType( + AccountHistoryEntry.Create(Guid.NewGuid(), withdrawal, accountId, new Money(100m))).Entry; + + Assert.Equal(AccountHistoryEntryType.Withdrawal, withdrawalEntry.EntryType); + Assert.Equal(78m, withdrawalEntry.BalanceAfter.Value); + } + + [Fact] + public void HistoryEntryProducesTwoSidesOfATransfer() + { + var source = Guid.NewGuid(); + var target = Guid.NewGuid(); + BankTransaction transaction = CreateTransaction( + BankTransactionType.Transfer, source, target, 10m, 0m, 1m); + + AccountHistoryEntry debit = Assert.IsType( + AccountHistoryEntry.Create(Guid.NewGuid(), transaction, source, new Money(100m))).Entry; + AccountHistoryEntry credit = Assert.IsType( + AccountHistoryEntry.Create(Guid.NewGuid(), transaction, target, new Money(50m))).Entry; + + Assert.Equal(AccountHistoryEntryType.TransferDebit, debit.EntryType); + Assert.Equal(89m, debit.BalanceAfter.Value); + Assert.Equal(AccountHistoryEntryType.TransferCredit, credit.EntryType); + Assert.Equal(60m, credit.BalanceAfter.Value); + Assert.Equal(debit.TransactionId, credit.TransactionId); + } + + [Fact] + public void HistoryEntryFailureDoesNotChangeTheProvidedBalance() + { + var source = Guid.NewGuid(); + BankTransaction transaction = CreateTransaction( + BankTransactionType.Withdrawal, source, null, 10m, 0m, 1m); + var balance = new Money(10m); + + Assert.IsType( + AccountHistoryEntry.Create(Guid.NewGuid(), transaction, source, balance)); + Assert.Equal(10m, balance.Value); + Assert.IsType( + AccountHistoryEntry.Create(Guid.NewGuid(), transaction, Guid.NewGuid(), new Money(100m))); + } + + private static BankTransaction CreateTransaction( + BankTransactionType type, + Guid? source, + Guid? target, + decimal amountValue, + decimal rate, + decimal fixedAmount) + { + var amount = new Amount(amountValue); + var commission = new CommissionSnapshot(amount, rate, fixedAmount); + return Assert.IsType(BankTransaction.Create( + Guid.NewGuid(), Guid.NewGuid(), type, source, target, amount, commission, Now)).Transaction; + } +} From efb0eb1a011a4aa2f7920825ea596cd7daf5cae9 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Fri, 4 Sep 2026 20:28:40 +0300 Subject: [PATCH 008/103] test(domain): verify banking flows and document stage changes --- README.md | 232 +++++++----------- .../Properties/AssemblyInfo.cs | 3 + .../AccountAuthenticationFlowTests.cs | 97 ++++++++ 3 files changed, 185 insertions(+), 147 deletions(-) create mode 100644 src/Atm.Application/Properties/AssemblyInfo.cs create mode 100644 tests/Atm.Application.Tests/AccountAuthenticationFlowTests.cs diff --git a/README.md b/README.md index 00a6463..7da6005 100644 --- a/README.md +++ b/README.md @@ -1,223 +1,161 @@ -# 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` 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. +Both values must be positive. -### 2. Create an account +## System password configuration -Requires an admin session. - -`POST /api/accounts/create` - -```json -{ - "adminSessionId": "admin-session-guid", - "accountNumber": "acc-1", - "pin": "1234", - "initialBalance": 100 -} -``` - -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 +`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. -Requires a user session. +### Create a user session -`POST /api/accounts/deposit` - -```json -{ - "userSessionId": "user-session-guid", - "amount": 50 -} -``` - -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. - -`GET /api/accounts/history?sessionId=user-session-guid` - -Successful response: +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. \ No newline at end of file +Do not treat the current in-memory repositories as production persistence. diff --git a/src/Atm.Application/Properties/AssemblyInfo.cs b/src/Atm.Application/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a1a4d7e --- /dev/null +++ b/src/Atm.Application/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Atm.Application.Tests")] diff --git a/tests/Atm.Application.Tests/AccountAuthenticationFlowTests.cs b/tests/Atm.Application.Tests/AccountAuthenticationFlowTests.cs new file mode 100644 index 0000000..a928a45 --- /dev/null +++ b/tests/Atm.Application.Tests/AccountAuthenticationFlowTests.cs @@ -0,0 +1,97 @@ +using Atm.Application.Abstractions.Repositories; +using Atm.Application.Accounts; +using Atm.Application.Common; +using Atm.Application.Contracts.Accounts.CreateAccount; +using Atm.Application.Contracts.Sessions.CreateUserSession; +using Atm.Application.Sessions; +using Atm.Domain.Accounts; +using Atm.Domain.Sessions; +using Atm.Domain.ValueObjects; +using Atm.Infrastructure.Security; +using Microsoft.Extensions.Options; + +namespace Atm.Application.Tests; + +public sealed class AccountAuthenticationFlowTests +{ + [Fact] + public void CreatedAccountStoresOnlyHashAndAcceptsOnlyTheCorrectPin() + { + var accounts = new TestAccountRepository(); + var sessions = new TestSessionRepository(); + var clock = new FixedClock(); + var hasher = new PinHasher(); + var adminSessionId = Guid.NewGuid(); + var ownerUserId = Guid.NewGuid(); + sessions.Add(BankingTestData.CreateAdminSession(adminSessionId, clock)); + + var authorizer = new SessionAuthorizer(sessions, clock); + var createAccount = new CreateAccountUseCase(accounts, authorizer, hasher, clock); + CreateAccountResult created = createAccount.Execute(new CreateAccountRequest( + adminSessionId, " account-01 ", "1234", 1_000_000_000_000_000m, ownerUserId)); + + Assert.IsType(created); + Account storedAccount = Assert.IsType(accounts.Account); + Assert.Equal("ACCOUNT-01", storedAccount.Number.NormalizedValue); + Assert.Equal(ownerUserId, storedAccount.OwnerUserId); + Assert.NotEqual("1234", storedAccount.PinHash.Value); + Assert.True(hasher.Verify(storedAccount.PinHash, new PinCode("1234"))); + + IOptions options = Options.Create(new SessionOptions + { + UserLifetimeMinutes = 30, + AdminLifetimeMinutes = 15, + }); + var createSession = new CreateUserSessionUseCase(sessions, accounts, hasher, clock, options); + + Assert.IsType(createSession.Execute( + new CreateUserSessionRequest("account-01", "4321"))); + CreateUserSessionResult.Success signedIn = Assert.IsType( + createSession.Execute(new CreateUserSessionRequest("account-01", "1234"))); + + Session userSession = Assert.IsType(sessions.GetById(signedIn.SessionId)); + Assert.Equal(storedAccount.Id, userSession.AccountId); + Assert.True(userSession.IsActive(clock.UtcNow)); + Assert.Equal(clock.UtcNow.AddMinutes(30), userSession.ExpiresAtUtc); + } + + private sealed class TestAccountRepository : IAccountRepository + { + public Account? Account { get; private set; } + + public Account? GetById(Guid id) + { + return Account?.Id == id ? Account : null; + } + + public Account? GetByNumber(AccountNumber accountNumber) + { + return Account?.Number.NormalizedValue == accountNumber.NormalizedValue ? Account : null; + } + + public void Add(Account account) + { + Account = account; + } + + public void Update(Account account) + { + Account = account; + } + } + + private sealed class TestSessionRepository : ISessionRepository + { + private readonly Dictionary _sessions = new(); + + public Session? GetById(Guid id) + { + return _sessions.GetValueOrDefault(id); + } + + public void Add(Session session) + { + _sessions.Add(session.Id, session); + } + } +} From c84d2eb09d4073c20328a4a28373545cf985f608 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Fri, 4 Sep 2026 20:37:30 +0300 Subject: [PATCH 009/103] style: remove inline code comments --- src/Atm.Application/Sessions/CreateAdminSessionUseCase.cs | 1 - src/Atm.Domain/Users/Friendship.cs | 1 - src/Atm.Domain/Users/User.cs | 1 - src/Atm.Infrastructure/Security/PinHasher.cs | 1 - 4 files changed, 4 deletions(-) diff --git a/src/Atm.Application/Sessions/CreateAdminSessionUseCase.cs b/src/Atm.Application/Sessions/CreateAdminSessionUseCase.cs index 84d6dea..a8768df 100644 --- a/src/Atm.Application/Sessions/CreateAdminSessionUseCase.cs +++ b/src/Atm.Application/Sessions/CreateAdminSessionUseCase.cs @@ -37,7 +37,6 @@ public CreateAdminSessionResult Execute(CreateAdminSessionRequest request) try { - // Kept only for backward compatibility with the current HTTP contract. _ = new AccountNumber(request.AccountNumber); } catch (ArgumentException exception) diff --git a/src/Atm.Domain/Users/Friendship.cs b/src/Atm.Domain/Users/Friendship.cs index 13b6c38..092f1d9 100644 --- a/src/Atm.Domain/Users/Friendship.cs +++ b/src/Atm.Domain/Users/Friendship.cs @@ -21,7 +21,6 @@ public static FriendshipCreationResult Create( if (!DomainRules.IsUtc(createdAtUtc)) return new FriendshipCreationResult.Failure("Created timestamp must be UTC."); - // Canonical hexadecimal order matches UUID byte order, not Guid.ToByteArray's little-endian layout. bool firstIsSmaller = string.CompareOrdinal(firstUserId.ToString("N"), secondUserId.ToString("N")) < 0; return new FriendshipCreationResult.Success(new Friendship { diff --git a/src/Atm.Domain/Users/User.cs b/src/Atm.Domain/Users/User.cs index 28146dd..bd172e1 100644 --- a/src/Atm.Domain/Users/User.cs +++ b/src/Atm.Domain/Users/User.cs @@ -6,7 +6,6 @@ namespace Atm.Domain.Users; public sealed class User { - // Public construction is validated by Create; private setters also allow ORM materialization. private User() { } public Guid Id { get; private set; } diff --git a/src/Atm.Infrastructure/Security/PinHasher.cs b/src/Atm.Infrastructure/Security/PinHasher.cs index 8a85026..3e39ce9 100644 --- a/src/Atm.Infrastructure/Security/PinHasher.cs +++ b/src/Atm.Infrastructure/Security/PinHasher.cs @@ -17,7 +17,6 @@ public bool Verify(PinHash hash, PinCode pin) { try { - // Both Success and SuccessRehashNeeded mean the PIN is valid. return _hasher.VerifyHashedPassword(this, hash.Value, pin.Value) != PasswordVerificationResult.Failed; } From 218a00c6de9a1ba1aa94d238f805c2acfbab523b Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Fri, 4 Sep 2026 20:41:53 +0300 Subject: [PATCH 010/103] test: separate projects by architectural layer --- Atm.sln | 30 ++++ README.md | 8 +- .../AccountAuthenticationFlowTests.cs | 21 ++- .../Atm.Application.Tests.csproj | 1 - .../SessionAuthorizerTests.cs | 33 +++++ tests/Atm.Application.Tests/SessionTests.cs | 81 ---------- .../Atm.Application.Tests/packages.lock.json | 139 ------------------ .../AccountTests.cs | 16 +- .../Atm.Domain.Tests/Atm.Domain.Tests.csproj | 25 ++++ .../BankTransactionTests.cs | 2 +- tests/Atm.Domain.Tests/DomainTestData.cs | 34 +++++ tests/Atm.Domain.Tests/SessionTests.cs | 58 ++++++++ .../UserTests.cs | 2 +- .../ValueObjectTests.cs | 2 +- .../Atm.Infrastructure.Tests.csproj | 25 ++++ .../PinHasherTests.cs | 2 +- 16 files changed, 243 insertions(+), 236 deletions(-) create mode 100644 tests/Atm.Application.Tests/SessionAuthorizerTests.cs delete mode 100644 tests/Atm.Application.Tests/SessionTests.cs rename tests/{Atm.Application.Tests => Atm.Domain.Tests}/AccountTests.cs (80%) create mode 100644 tests/Atm.Domain.Tests/Atm.Domain.Tests.csproj rename tests/{Atm.Application.Tests => Atm.Domain.Tests}/BankTransactionTests.cs (99%) create mode 100644 tests/Atm.Domain.Tests/DomainTestData.cs create mode 100644 tests/Atm.Domain.Tests/SessionTests.cs rename tests/{Atm.Application.Tests => Atm.Domain.Tests}/UserTests.cs (98%) rename tests/{Atm.Application.Tests => Atm.Domain.Tests}/ValueObjectTests.cs (98%) create mode 100644 tests/Atm.Infrastructure.Tests/Atm.Infrastructure.Tests.csproj rename tests/{Atm.Application.Tests => Atm.Infrastructure.Tests}/PinHasherTests.cs (96%) diff --git a/Atm.sln b/Atm.sln index fee2692..d015d3a 100644 --- a/Atm.sln +++ b/Atm.sln @@ -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 @@ -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 @@ -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 diff --git a/README.md b/README.md index 7da6005..263cc31 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,13 @@ dotnet build Atm.sln -c Release --no-restore dotnet test Atm.sln -c Release --no-build ``` -Unit tests create mocks or focused test objects directly and do not build the production DI container or start a database. +Tests are separated by architectural layer: + +- `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`. + +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. ## Transitional limitations diff --git a/tests/Atm.Application.Tests/AccountAuthenticationFlowTests.cs b/tests/Atm.Application.Tests/AccountAuthenticationFlowTests.cs index a928a45..2790a13 100644 --- a/tests/Atm.Application.Tests/AccountAuthenticationFlowTests.cs +++ b/tests/Atm.Application.Tests/AccountAuthenticationFlowTests.cs @@ -1,4 +1,5 @@ using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions.Security; using Atm.Application.Accounts; using Atm.Application.Common; using Atm.Application.Contracts.Accounts.CreateAccount; @@ -7,7 +8,6 @@ using Atm.Domain.Accounts; using Atm.Domain.Sessions; using Atm.Domain.ValueObjects; -using Atm.Infrastructure.Security; using Microsoft.Extensions.Options; namespace Atm.Application.Tests; @@ -20,7 +20,7 @@ public void CreatedAccountStoresOnlyHashAndAcceptsOnlyTheCorrectPin() var accounts = new TestAccountRepository(); var sessions = new TestSessionRepository(); var clock = new FixedClock(); - var hasher = new PinHasher(); + var hasher = new TestPinHasher(); var adminSessionId = Guid.NewGuid(); var ownerUserId = Guid.NewGuid(); sessions.Add(BankingTestData.CreateAdminSession(adminSessionId, clock)); @@ -94,4 +94,21 @@ public void Add(Session session) _sessions.Add(session.Id, session); } } + + private sealed class TestPinHasher : IPinHasher + { + private readonly Dictionary _pinsByHash = new(); + + public PinHash Hash(PinCode pin) + { + var hash = new PinHash($"test-hash-{Guid.NewGuid():N}"); + _pinsByHash.Add(hash.Value, pin.Value); + return hash; + } + + public bool Verify(PinHash hash, PinCode pin) + { + return _pinsByHash.GetValueOrDefault(hash.Value) == pin.Value; + } + } } diff --git a/tests/Atm.Application.Tests/Atm.Application.Tests.csproj b/tests/Atm.Application.Tests/Atm.Application.Tests.csproj index 76d3475..6ba4c8d 100644 --- a/tests/Atm.Application.Tests/Atm.Application.Tests.csproj +++ b/tests/Atm.Application.Tests/Atm.Application.Tests.csproj @@ -21,7 +21,6 @@ - diff --git a/tests/Atm.Application.Tests/SessionAuthorizerTests.cs b/tests/Atm.Application.Tests/SessionAuthorizerTests.cs new file mode 100644 index 0000000..8d3466d --- /dev/null +++ b/tests/Atm.Application.Tests/SessionAuthorizerTests.cs @@ -0,0 +1,33 @@ +using Atm.Application.Abstractions.Repositories; +using Atm.Application.Common; +using Atm.Domain.Accounts; +using Atm.Domain.Sessions; +using Atm.Domain.Sessions.Results; +using Atm.Domain.ValueObjects; +using NSubstitute; + +namespace Atm.Application.Tests; + +public sealed class SessionAuthorizerTests +{ + [Fact] + public void AuthorizerRejectsExpiredAndRevokedSessions() + { + ISessionRepository sessions = Substitute.For(); + var clock = new FixedClock(); + Account account = BankingTestData.CreateAccount(new AccountNumber("session-test"), 0m); + var sessionId = Guid.NewGuid(); + Session session = BankingTestData.CreateUserSession(sessionId, account, clock); + sessions.GetById(sessionId).Returns(session); + var authorizer = new SessionAuthorizer(sessions, clock); + + Assert.IsType(authorizer.AuthorizeUser(sessionId)); + + clock.UtcNow = session.ExpiresAtUtc; + Assert.IsType(authorizer.AuthorizeUser(sessionId)); + + clock.UtcNow = session.CreatedAtUtc; + Assert.IsType(session.Revoke(clock.UtcNow)); + Assert.IsType(authorizer.AuthorizeUser(sessionId)); + } +} diff --git a/tests/Atm.Application.Tests/SessionTests.cs b/tests/Atm.Application.Tests/SessionTests.cs deleted file mode 100644 index 576f04e..0000000 --- a/tests/Atm.Application.Tests/SessionTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -using Atm.Application.Abstractions.Repositories; -using Atm.Application.Common; -using Atm.Domain.Accounts; -using Atm.Domain.Sessions; -using Atm.Domain.Sessions.Results; -using Atm.Domain.ValueObjects; -using NSubstitute; - -namespace Atm.Application.Tests; - -public sealed class SessionTests -{ - [Fact] - public void CreateEnforcesIdentityShapeAndUtcLifetime() - { - var clock = new FixedClock(); - - Assert.IsType(Session.Create( - Guid.Empty, SessionType.Admin, null, clock.UtcNow, clock.UtcNow.AddMinutes(1))); - Assert.IsType(Session.Create( - Guid.NewGuid(), SessionType.User, null, clock.UtcNow, clock.UtcNow.AddMinutes(1))); - Assert.IsType(Session.Create( - Guid.NewGuid(), SessionType.Admin, Guid.NewGuid(), clock.UtcNow, clock.UtcNow.AddMinutes(1))); - Assert.IsType(Session.Create( - Guid.NewGuid(), SessionType.Admin, null, clock.UtcNow, clock.UtcNow)); - Assert.IsType(Session.Create( - Guid.NewGuid(), SessionType.Admin, null, - clock.UtcNow.ToOffset(TimeSpan.FromHours(3)), clock.UtcNow.AddMinutes(1))); - } - - [Fact] - public void IsActiveUsesInclusiveStartAndExclusiveExpiration() - { - var clock = new FixedClock(); - Session session = BankingTestData.CreateAdminSession(Guid.NewGuid(), clock); - - Assert.True(session.IsActive(clock.UtcNow)); - Assert.True(session.IsActive(session.ExpiresAtUtc.AddTicks(-1))); - Assert.False(session.IsActive(session.ExpiresAtUtc)); - Assert.False(session.IsActive(clock.UtcNow.AddTicks(-1))); - Assert.False(session.IsActive(clock.UtcNow.ToOffset(TimeSpan.FromHours(3)))); - } - - [Fact] - public void RevokeIsIdempotentAndKeepsTheFirstTimestamp() - { - var clock = new FixedClock(); - Session session = BankingTestData.CreateAdminSession(Guid.NewGuid(), clock); - DateTimeOffset first = clock.UtcNow.AddMinutes(1); - - Assert.IsType(session.Revoke(clock.UtcNow.AddTicks(-1))); - Assert.IsType(session.Revoke(first)); - SessionRevocationResult.Success repeated = Assert.IsType( - session.Revoke(first.AddMinutes(1))); - - Assert.Equal(first, repeated.RevokedAtUtc); - Assert.Equal(first, session.RevokedAtUtc); - Assert.False(session.IsActive(first)); - } - - [Fact] - public void AuthorizerRejectsExpiredAndRevokedSessions() - { - ISessionRepository sessions = Substitute.For(); - var clock = new FixedClock(); - Account account = BankingTestData.CreateAccount(new AccountNumber("session-test"), 0m); - var sessionId = Guid.NewGuid(); - Session session = BankingTestData.CreateUserSession(sessionId, account, clock); - sessions.GetById(sessionId).Returns(session); - var authorizer = new SessionAuthorizer(sessions, clock); - - Assert.IsType(authorizer.AuthorizeUser(sessionId)); - - clock.UtcNow = session.ExpiresAtUtc; - Assert.IsType(authorizer.AuthorizeUser(sessionId)); - - clock.UtcNow = session.CreatedAtUtc; - Assert.IsType(session.Revoke(clock.UtcNow)); - Assert.IsType(authorizer.AuthorizeUser(sessionId)); - } -} diff --git a/tests/Atm.Application.Tests/packages.lock.json b/tests/Atm.Application.Tests/packages.lock.json index 3376167..efaa490 100644 --- a/tests/Atm.Application.Tests/packages.lock.json +++ b/tests/Atm.Application.Tests/packages.lock.json @@ -52,125 +52,16 @@ "System.Diagnostics.EventLog": "6.0.0" } }, - "Microsoft.AspNetCore.Cryptography.Internal": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "zqiWdZ8Jw4/sfTBSFHchU3Iajm8EqLPUq/cUIuUjARwNGJ9hrKR7Yf0bs4WlF+GdPX6Bj7kmaf99aY0OTbLCOg==" - }, - "Microsoft.AspNetCore.Cryptography.KeyDerivation": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "w0ZZWnsz7U2dh5l9eytJYYHG3/HwafewFyZT+NsnBuQAt90cw2hmiX9J9OU1mrtn7EiIJqhRxjzY5kAbmhD2DA==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.Internal": "9.0.19" - } - }, "Microsoft.CodeCoverage": { "type": "Transitive", "resolved": "17.12.0", "contentHash": "4svMznBd5JM21JIG2xZKGNanAHNXplxf/kQDFfLHXQ3OnpJkayRK/TjacFjA+EYmoyuNXHo/sOETEfcYtAzIrA==" }, - "Microsoft.EntityFrameworkCore": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "qWqqxrfEDfi3C/z0S/ACIA2gUnreRPHX74WE/dVnesbztNesYO4NLy76lE6FV/zDP4vH/pkvDr8h3Mq9+cFv/w==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "9.0.19", - "Microsoft.EntityFrameworkCore.Analyzers": "9.0.19", - "Microsoft.Extensions.Caching.Memory": "9.0.19", - "Microsoft.Extensions.Logging": "9.0.19" - } - }, - "Microsoft.EntityFrameworkCore.Abstractions": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "igPnmVL3DU2ZH/4DNeQqWttNrQpwwg0q6MWj5peZxtl0QVMDSeFtnt2yhhdSffklZKYsAFko1sh/va3z4y7S6A==" - }, - "Microsoft.EntityFrameworkCore.Analyzers": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "yI5dQuvigJ6ICQECOlJDtrsUYcuDryv3qsZK1i6ZA/IzMzZ/VPkDzjE4r2LivuyVSGnkDRkH2cp4LmrhXARRKw==" - }, - "Microsoft.EntityFrameworkCore.Relational": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "KAfYGG4LWRNn1G/vMRydxgOjuZoWUvdd5raUcIia/HPg1jUjlQzS386nUx5gt3YfKxpFJaCGxdsQ1nCZWuYR8A==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "9.0.19", - "Microsoft.Extensions.Caching.Memory": "9.0.19", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.19", - "Microsoft.Extensions.Logging": "9.0.19" - } - }, - "Microsoft.Extensions.Caching.Abstractions": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "qe/6m6wxRG+N5xTf4Z3lDBAiVu6oBCUyADJ5OzlFgukD9Vg/6zB3MNnNEFA7amZg8hjcS6F4OZBRmB07A+gmHQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.19" - } - }, - "Microsoft.Extensions.Caching.Memory": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "Y0w5LkBqWkDh5vYTKM+QKlzxpqegEtP86oDhFMXgx4tDkWL2t+rl4h2vxBHkvBtM/gM6PoLK9QCrnoclXtHtVQ==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "9.0.19", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19", - "Microsoft.Extensions.Logging.Abstractions": "9.0.19", - "Microsoft.Extensions.Options": "9.0.19", - "Microsoft.Extensions.Primitives": "9.0.19" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "M6h5lX6QWSSn7FDeRUHePZen/xvxPsuhcX1VpBfefafqavn2BnfxheR7BH5D5AIZGM7Nah0xJKnq2DublD4doQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.19" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "o/XFYSiV4a7eiCkf7W1GFAviVhHQAPY5bHrQRsNWAPT3GhJ3Af63vcFQjHivIrTNZITlPPaGWhBy7jykKYYyaQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19" - } - }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "9.0.19", "contentHash": "XgQ0aVWClYjYSBqbYrKC/xyZ1KHf+VVwNF3+d54jcaqnst30Q//ugs/FJwUztLZiph0gp85g7i/eB0556twO0Q==" }, - "Microsoft.Extensions.Identity.Core": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "6P2/iMA35j8RrjsJXhqaOy4Sturcu+NXzXSlKguGTzjBEVRf6atWPrKkUuZew9K5S5NbNh8gdYip2dXm8t5fLg==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.19", - "Microsoft.Extensions.Logging": "9.0.19", - "Microsoft.Extensions.Options": "9.0.19" - } - }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "3cnWzJ/FLEhLiW+TJTWtTjVj6fwlsP8OrbJqgOP4J9feAKjmT77vmClakHlej+jnzBgpnVU0+SZwtEIOr+KS6A==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "9.0.19", - "Microsoft.Extensions.Logging.Abstractions": "9.0.19", - "Microsoft.Extensions.Options": "9.0.19" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "RpqMVoZ+NECJxLOZRjWOfjST0mULs4wb848mxG/CS9SMkVL6yi1sSIda4dGcsB01J2jUFd8hNwsaYl1gplU+Cw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19" - } - }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "9.0.19", @@ -207,24 +98,6 @@ "resolved": "13.0.1", "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" }, - "Npgsql": { - "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.2" - } - }, - "Npgsql.EntityFrameworkCore.PostgreSQL": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)", - "Npgsql": "9.0.3" - } - }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "6.0.0", @@ -296,18 +169,6 @@ }, "atm.domain": { "type": "Project" - }, - "atm.infrastructure": { - "type": "Project", - "dependencies": { - "Atm.Application.Abstractions": "[1.0.0, )", - "Atm.Domain": "[1.0.0, )", - "Microsoft.EntityFrameworkCore": "[9.0.19, )", - "Microsoft.EntityFrameworkCore.Relational": "[9.0.19, )", - "Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.19, )", - "Microsoft.Extensions.Identity.Core": "[9.0.19, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" - } } } } diff --git a/tests/Atm.Application.Tests/AccountTests.cs b/tests/Atm.Domain.Tests/AccountTests.cs similarity index 80% rename from tests/Atm.Application.Tests/AccountTests.cs rename to tests/Atm.Domain.Tests/AccountTests.cs index c9e2a0c..58a5e74 100644 --- a/tests/Atm.Application.Tests/AccountTests.cs +++ b/tests/Atm.Domain.Tests/AccountTests.cs @@ -2,7 +2,7 @@ using Atm.Domain.Accounts.Results; using Atm.Domain.ValueObjects; -namespace Atm.Application.Tests; +namespace Atm.Domain.Tests; public class AccountTests { @@ -11,18 +11,18 @@ public void CreateRequiresAnOwnerAndUtcTime() { var number = new AccountNumber("test"); var hash = new PinHash("test-only-hash"); - var clock = new FixedClock(); + DateTimeOffset now = DomainTestData.UtcNow; Assert.IsType(Account.Create( - Guid.NewGuid(), number, Guid.Empty, hash, new Money(0m), clock.UtcNow)); + Guid.NewGuid(), number, Guid.Empty, hash, new Money(0m), now)); Assert.IsType(Account.Create( - Guid.NewGuid(), number, Guid.NewGuid(), hash, new Money(0m), clock.UtcNow.ToOffset(TimeSpan.FromHours(3)))); + Guid.NewGuid(), number, Guid.NewGuid(), hash, new Money(0m), now.ToOffset(TimeSpan.FromHours(3)))); } [Fact] public void SuccessfulChangesAdvanceTheVersionAndTimestamp() { - Account account = BankingTestData.CreateAccount(new AccountNumber("test"), 100m); + Account account = DomainTestData.CreateAccount(new AccountNumber("test"), 100m); DateTimeOffset now = account.CreatedAtUtc.AddMinutes(1); Assert.IsType(account.Deposit(new Amount(10m), now)); @@ -36,7 +36,7 @@ public void SuccessfulChangesAdvanceTheVersionAndTimestamp() [Fact] public void FailedChangesDoNotMutateTheAccount() { - Account account = BankingTestData.CreateAccount(new AccountNumber("test"), 100m); + Account account = DomainTestData.CreateAccount(new AccountNumber("test"), 100m); DateTimeOffset now = account.CreatedAtUtc; Assert.IsType(account.Withdraw(new Amount(101m), now)); @@ -49,11 +49,11 @@ public void FailedChangesDoNotMutateTheAccount() [Fact] public void LargeBalancesHaveNoBusinessCeilingButCannotLosePrecision() { - Account account = BankingTestData.CreateAccount(new AccountNumber("large"), 1000000000000000m); + Account account = DomainTestData.CreateAccount(new AccountNumber("large"), 1000000000000000m); Assert.IsType(account.Deposit(new Amount(1m), account.CreatedAtUtc)); Assert.Equal(1000000000000001m, account.Balance.Value); - Account largest = BankingTestData.CreateAccount(new AccountNumber("largest"), decimal.MaxValue); + Account largest = DomainTestData.CreateAccount(new AccountNumber("largest"), decimal.MaxValue); Assert.IsType(largest.Deposit(new Amount(1m), largest.CreatedAtUtc)); Assert.IsType(largest.Deposit(new Amount(0.0001m), largest.CreatedAtUtc)); Assert.IsType(largest.Withdraw(new Amount(0.0001m), largest.CreatedAtUtc)); diff --git a/tests/Atm.Domain.Tests/Atm.Domain.Tests.csproj b/tests/Atm.Domain.Tests/Atm.Domain.Tests.csproj new file mode 100644 index 0000000..69d6dd0 --- /dev/null +++ b/tests/Atm.Domain.Tests/Atm.Domain.Tests.csproj @@ -0,0 +1,25 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + diff --git a/tests/Atm.Application.Tests/BankTransactionTests.cs b/tests/Atm.Domain.Tests/BankTransactionTests.cs similarity index 99% rename from tests/Atm.Application.Tests/BankTransactionTests.cs rename to tests/Atm.Domain.Tests/BankTransactionTests.cs index 5f91d95..c6fa6a3 100644 --- a/tests/Atm.Application.Tests/BankTransactionTests.cs +++ b/tests/Atm.Domain.Tests/BankTransactionTests.cs @@ -2,7 +2,7 @@ using Atm.Domain.Operations.Results; using Atm.Domain.ValueObjects; -namespace Atm.Application.Tests; +namespace Atm.Domain.Tests; public sealed class BankTransactionTests { diff --git a/tests/Atm.Domain.Tests/DomainTestData.cs b/tests/Atm.Domain.Tests/DomainTestData.cs new file mode 100644 index 0000000..e5e84a4 --- /dev/null +++ b/tests/Atm.Domain.Tests/DomainTestData.cs @@ -0,0 +1,34 @@ +using Atm.Domain.Accounts; +using Atm.Domain.Accounts.Results; +using Atm.Domain.Sessions; +using Atm.Domain.Sessions.Results; +using Atm.Domain.ValueObjects; + +namespace Atm.Domain.Tests; + +internal static class DomainTestData +{ + public static DateTimeOffset UtcNow { get; } = + new(2026, 9, 2, 12, 0, 0, TimeSpan.Zero); + + public static Account CreateAccount(AccountNumber number, decimal balance) + { + return Assert.IsType(Account.Create( + Guid.NewGuid(), + number, + Guid.NewGuid(), + new PinHash("test-only-hash"), + new Money(balance), + UtcNow)).Account; + } + + public static Session CreateAdminSession(Guid id) + { + return Assert.IsType(Session.Create( + id, + SessionType.Admin, + null, + UtcNow, + UtcNow.AddMinutes(15))).Session; + } +} diff --git a/tests/Atm.Domain.Tests/SessionTests.cs b/tests/Atm.Domain.Tests/SessionTests.cs new file mode 100644 index 0000000..595e57e --- /dev/null +++ b/tests/Atm.Domain.Tests/SessionTests.cs @@ -0,0 +1,58 @@ +using Atm.Domain.Sessions; +using Atm.Domain.Sessions.Results; + +namespace Atm.Domain.Tests; + +public sealed class SessionTests +{ + [Fact] + public void CreateEnforcesIdentityShapeAndUtcLifetime() + { + DateTimeOffset now = DomainTestData.UtcNow; + + Assert.IsType(Session.Create( + Guid.Empty, SessionType.Admin, null, now, now.AddMinutes(1))); + Assert.IsType(Session.Create( + Guid.NewGuid(), SessionType.User, null, now, now.AddMinutes(1))); + Assert.IsType(Session.Create( + Guid.NewGuid(), SessionType.Admin, Guid.NewGuid(), now, now.AddMinutes(1))); + Assert.IsType(Session.Create( + Guid.NewGuid(), SessionType.Admin, null, now, now)); + Assert.IsType(Session.Create( + Guid.NewGuid(), + SessionType.Admin, + null, + now.ToOffset(TimeSpan.FromHours(3)), + now.AddMinutes(1))); + } + + [Fact] + public void IsActiveUsesInclusiveStartAndExclusiveExpiration() + { + DateTimeOffset now = DomainTestData.UtcNow; + Session session = DomainTestData.CreateAdminSession(Guid.NewGuid()); + + Assert.True(session.IsActive(now)); + Assert.True(session.IsActive(session.ExpiresAtUtc.AddTicks(-1))); + Assert.False(session.IsActive(session.ExpiresAtUtc)); + Assert.False(session.IsActive(now.AddTicks(-1))); + Assert.False(session.IsActive(now.ToOffset(TimeSpan.FromHours(3)))); + } + + [Fact] + public void RevokeIsIdempotentAndKeepsTheFirstTimestamp() + { + DateTimeOffset now = DomainTestData.UtcNow; + Session session = DomainTestData.CreateAdminSession(Guid.NewGuid()); + DateTimeOffset first = now.AddMinutes(1); + + Assert.IsType(session.Revoke(now.AddTicks(-1))); + Assert.IsType(session.Revoke(first)); + SessionRevocationResult.Success repeated = Assert.IsType( + session.Revoke(first.AddMinutes(1))); + + Assert.Equal(first, repeated.RevokedAtUtc); + Assert.Equal(first, session.RevokedAtUtc); + Assert.False(session.IsActive(first)); + } +} diff --git a/tests/Atm.Application.Tests/UserTests.cs b/tests/Atm.Domain.Tests/UserTests.cs similarity index 98% rename from tests/Atm.Application.Tests/UserTests.cs rename to tests/Atm.Domain.Tests/UserTests.cs index 2ee6957..9b6bff3 100644 --- a/tests/Atm.Application.Tests/UserTests.cs +++ b/tests/Atm.Domain.Tests/UserTests.cs @@ -2,7 +2,7 @@ using Atm.Domain.Users.Results; using Atm.Domain.ValueObjects; -namespace Atm.Application.Tests; +namespace Atm.Domain.Tests; public sealed class UserTests { diff --git a/tests/Atm.Application.Tests/ValueObjectTests.cs b/tests/Atm.Domain.Tests/ValueObjectTests.cs similarity index 98% rename from tests/Atm.Application.Tests/ValueObjectTests.cs rename to tests/Atm.Domain.Tests/ValueObjectTests.cs index 8fc1dcb..2baa8af 100644 --- a/tests/Atm.Application.Tests/ValueObjectTests.cs +++ b/tests/Atm.Domain.Tests/ValueObjectTests.cs @@ -1,7 +1,7 @@ using Atm.Domain.ValueObjects; using System.Globalization; -namespace Atm.Application.Tests; +namespace Atm.Domain.Tests; public sealed class ValueObjectTests { diff --git a/tests/Atm.Infrastructure.Tests/Atm.Infrastructure.Tests.csproj b/tests/Atm.Infrastructure.Tests/Atm.Infrastructure.Tests.csproj new file mode 100644 index 0000000..f207203 --- /dev/null +++ b/tests/Atm.Infrastructure.Tests/Atm.Infrastructure.Tests.csproj @@ -0,0 +1,25 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + diff --git a/tests/Atm.Application.Tests/PinHasherTests.cs b/tests/Atm.Infrastructure.Tests/PinHasherTests.cs similarity index 96% rename from tests/Atm.Application.Tests/PinHasherTests.cs rename to tests/Atm.Infrastructure.Tests/PinHasherTests.cs index da10d6c..528b558 100644 --- a/tests/Atm.Application.Tests/PinHasherTests.cs +++ b/tests/Atm.Infrastructure.Tests/PinHasherTests.cs @@ -1,7 +1,7 @@ using Atm.Domain.ValueObjects; using Atm.Infrastructure.Security; -namespace Atm.Application.Tests; +namespace Atm.Infrastructure.Tests; public class PinHasherTests { From a63f57034e7bdff8d62668c026f0ca117cece14e Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Fri, 4 Sep 2026 20:42:51 +0300 Subject: [PATCH 011/103] chore(test): lock new test project dependencies --- tests/Atm.Domain.Tests/packages.lock.json | 115 ++++++++ .../packages.lock.json | 279 ++++++++++++++++++ 2 files changed, 394 insertions(+) create mode 100644 tests/Atm.Domain.Tests/packages.lock.json create mode 100644 tests/Atm.Infrastructure.Tests/packages.lock.json diff --git a/tests/Atm.Domain.Tests/packages.lock.json b/tests/Atm.Domain.Tests/packages.lock.json new file mode 100644 index 0000000..04bcc42 --- /dev/null +++ b/tests/Atm.Domain.Tests/packages.lock.json @@ -0,0 +1,115 @@ +{ + "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" + } + }, + "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==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.12.0", + "contentHash": "4svMznBd5JM21JIG2xZKGNanAHNXplxf/kQDFfLHXQ3OnpJkayRK/TjacFjA+EYmoyuNXHo/sOETEfcYtAzIrA==" + }, + "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.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.domain": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/tests/Atm.Infrastructure.Tests/packages.lock.json b/tests/Atm.Infrastructure.Tests/packages.lock.json new file mode 100644 index 0000000..43b0953 --- /dev/null +++ b/tests/Atm.Infrastructure.Tests/packages.lock.json @@ -0,0 +1,279 @@ +{ + "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" + } + }, + "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==" + }, + "Microsoft.AspNetCore.Cryptography.Internal": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "zqiWdZ8Jw4/sfTBSFHchU3Iajm8EqLPUq/cUIuUjARwNGJ9hrKR7Yf0bs4WlF+GdPX6Bj7kmaf99aY0OTbLCOg==" + }, + "Microsoft.AspNetCore.Cryptography.KeyDerivation": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "w0ZZWnsz7U2dh5l9eytJYYHG3/HwafewFyZT+NsnBuQAt90cw2hmiX9J9OU1mrtn7EiIJqhRxjzY5kAbmhD2DA==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.Internal": "9.0.19" + } + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.12.0", + "contentHash": "4svMznBd5JM21JIG2xZKGNanAHNXplxf/kQDFfLHXQ3OnpJkayRK/TjacFjA+EYmoyuNXHo/sOETEfcYtAzIrA==" + }, + "Microsoft.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "qWqqxrfEDfi3C/z0S/ACIA2gUnreRPHX74WE/dVnesbztNesYO4NLy76lE6FV/zDP4vH/pkvDr8h3Mq9+cFv/w==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "9.0.19", + "Microsoft.EntityFrameworkCore.Analyzers": "9.0.19", + "Microsoft.Extensions.Caching.Memory": "9.0.19", + "Microsoft.Extensions.Logging": "9.0.19" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "igPnmVL3DU2ZH/4DNeQqWttNrQpwwg0q6MWj5peZxtl0QVMDSeFtnt2yhhdSffklZKYsAFko1sh/va3z4y7S6A==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "yI5dQuvigJ6ICQECOlJDtrsUYcuDryv3qsZK1i6ZA/IzMzZ/VPkDzjE4r2LivuyVSGnkDRkH2cp4LmrhXARRKw==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "KAfYGG4LWRNn1G/vMRydxgOjuZoWUvdd5raUcIia/HPg1jUjlQzS386nUx5gt3YfKxpFJaCGxdsQ1nCZWuYR8A==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "9.0.19", + "Microsoft.Extensions.Caching.Memory": "9.0.19", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.19", + "Microsoft.Extensions.Logging": "9.0.19" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "qe/6m6wxRG+N5xTf4Z3lDBAiVu6oBCUyADJ5OzlFgukD9Vg/6zB3MNnNEFA7amZg8hjcS6F4OZBRmB07A+gmHQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.19" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "Y0w5LkBqWkDh5vYTKM+QKlzxpqegEtP86oDhFMXgx4tDkWL2t+rl4h2vxBHkvBtM/gM6PoLK9QCrnoclXtHtVQ==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "9.0.19", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19", + "Microsoft.Extensions.Logging.Abstractions": "9.0.19", + "Microsoft.Extensions.Options": "9.0.19", + "Microsoft.Extensions.Primitives": "9.0.19" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "M6h5lX6QWSSn7FDeRUHePZen/xvxPsuhcX1VpBfefafqavn2BnfxheR7BH5D5AIZGM7Nah0xJKnq2DublD4doQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.19" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "o/XFYSiV4a7eiCkf7W1GFAviVhHQAPY5bHrQRsNWAPT3GhJ3Af63vcFQjHivIrTNZITlPPaGWhBy7jykKYYyaQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "XgQ0aVWClYjYSBqbYrKC/xyZ1KHf+VVwNF3+d54jcaqnst30Q//ugs/FJwUztLZiph0gp85g7i/eB0556twO0Q==" + }, + "Microsoft.Extensions.Identity.Core": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "6P2/iMA35j8RrjsJXhqaOy4Sturcu+NXzXSlKguGTzjBEVRf6atWPrKkUuZew9K5S5NbNh8gdYip2dXm8t5fLg==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.19", + "Microsoft.Extensions.Logging": "9.0.19", + "Microsoft.Extensions.Options": "9.0.19" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "3cnWzJ/FLEhLiW+TJTWtTjVj6fwlsP8OrbJqgOP4J9feAKjmT77vmClakHlej+jnzBgpnVU0+SZwtEIOr+KS6A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "9.0.19", + "Microsoft.Extensions.Logging.Abstractions": "9.0.19", + "Microsoft.Extensions.Options": "9.0.19" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "RpqMVoZ+NECJxLOZRjWOfjST0mULs4wb848mxG/CS9SMkVL6yi1sSIda4dGcsB01J2jUFd8hNwsaYl1gplU+Cw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "FdPZy4kUSwFlIVZ3EkK9fdHw2RfblUwzflSb4kjVeGf9v2lX9IH+q2RGUovYMR7CVfKMc2y2Db/bZ2b4yiMfOg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19", + "Microsoft.Extensions.Primitives": "9.0.19" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "9hIg8PQiMnpVFIsEHm25Wi1gBrPa2pS1G75uveyDUVLXrNKqBap9WGwQIgO0s6LUgRAOdsSYnbKbNC5b1G5Pcg==" + }, + "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==" + }, + "Npgsql": { + "type": "Transitive", + "resolved": "9.0.3", + "contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.2" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)", + "Npgsql": "9.0.3" + } + }, + "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.abstractions": { + "type": "Project", + "dependencies": { + "Atm.Domain": "[1.0.0, )" + } + }, + "atm.domain": { + "type": "Project" + }, + "atm.infrastructure": { + "type": "Project", + "dependencies": { + "Atm.Application.Abstractions": "[1.0.0, )", + "Atm.Domain": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[9.0.19, )", + "Microsoft.EntityFrameworkCore.Relational": "[9.0.19, )", + "Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.19, )", + "Microsoft.Extensions.Identity.Core": "[9.0.19, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" + } + } + } + } +} \ No newline at end of file From 41e2f718fc7acfa1a8680f196bce5bea4d276ce0 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sun, 6 Sep 2026 17:34:46 +0300 Subject: [PATCH 012/103] fix(domain): enforce transaction shape and exact commission math --- src/Atm.Domain/Operations/BankTransaction.cs | 4 ++-- .../Operations/CommissionSnapshot.cs | 7 +++++- .../Atm.Domain.Tests/BankTransactionTests.cs | 24 +++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/Atm.Domain/Operations/BankTransaction.cs b/src/Atm.Domain/Operations/BankTransaction.cs index b897fda..47ff961 100644 --- a/src/Atm.Domain/Operations/BankTransaction.cs +++ b/src/Atm.Domain/Operations/BankTransaction.cs @@ -102,8 +102,8 @@ public Amount GetSourceDebitAmount() return type switch { - BankTransactionType.Deposit when !hasSource && hasTarget => null, - BankTransactionType.Withdrawal when hasSource && !hasTarget => null, + BankTransactionType.Deposit when sourceAccountId is null && hasTarget => null, + BankTransactionType.Withdrawal when hasSource && targetAccountId is null => null, BankTransactionType.Transfer when hasSource && hasTarget && sourceAccountId != targetAccountId => null, _ => "Source and target accounts do not match the transaction type.", }; diff --git a/src/Atm.Domain/Operations/CommissionSnapshot.cs b/src/Atm.Domain/Operations/CommissionSnapshot.cs index d0740ab..b98be5d 100644 --- a/src/Atm.Domain/Operations/CommissionSnapshot.cs +++ b/src/Atm.Domain/Operations/CommissionSnapshot.cs @@ -14,7 +14,12 @@ public CommissionSnapshot(Amount operationAmount, decimal rate, decimal fixedAmo throw new ArgumentException("Commission rate must be between zero and one with at most six decimal places.", nameof(rate)); var fixedMoney = new Money(fixedAmount); - decimal proportional = checked(operationAmount.Value * rate); + decimal rateUnits = rate * 1_000_000m; + decimal whole = decimal.Truncate(operationAmount.Value / 1_000_000m); + decimal remainder = operationAmount.Value % 1_000_000m; + decimal proportional = DomainRules.AddExact( + checked(whole * rateUnits), + remainder * rateUnits / 1_000_000m); decimal unrounded = DomainRules.AddExact(proportional, fixedMoney.Value); decimal rounded = decimal.Round(unrounded, 4, MidpointRounding.ToEven); diff --git a/tests/Atm.Domain.Tests/BankTransactionTests.cs b/tests/Atm.Domain.Tests/BankTransactionTests.cs index c6fa6a3..f31d403 100644 --- a/tests/Atm.Domain.Tests/BankTransactionTests.cs +++ b/tests/Atm.Domain.Tests/BankTransactionTests.cs @@ -137,6 +137,30 @@ public void HistoryEntryFailureDoesNotChangeTheProvidedBalance() AccountHistoryEntry.Create(Guid.NewGuid(), transaction, Guid.NewGuid(), new Money(100m))); } + [Theory] + [InlineData(BankTransactionType.Deposit)] + [InlineData(BankTransactionType.Withdrawal)] + public void AbsentAccountMustBeNull(BankTransactionType type) + { + var amount = new Amount(10m); + var commission = new CommissionSnapshot(amount, 0m, 0m); + Guid? source = type == BankTransactionType.Deposit ? Guid.Empty : Guid.NewGuid(); + Guid? target = type == BankTransactionType.Withdrawal ? Guid.Empty : Guid.NewGuid(); + + Assert.IsType(BankTransaction.Create( + Guid.NewGuid(), Guid.NewGuid(), type, source, target, amount, commission, Now)); + } + + [Fact] + public void CommissionRejectsPrecisionLossBeforeExplicitRounding() + { + Assert.Throws(() => + new CommissionSnapshot(new Amount(decimal.MaxValue), 0.5m, 0m)); + var snapshot = new CommissionSnapshot(new Amount(1_234_567.1234m), 0.123456m, 0.01m); + Assert.Equal(decimal.Round((1_234_567.1234m * 0.123456m) + 0.01m, 4, + MidpointRounding.ToEven), snapshot.CommissionAmount.Value); + } + private static BankTransaction CreateTransaction( BankTransactionType type, Guid? source, From f2a0689ff104cf41cbba6c0cbfadf2b7a669f79d Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sun, 6 Sep 2026 19:05:12 +0300 Subject: [PATCH 013/103] feat(persistence): configure atm database context --- README.md | 39 +++- .../Atm.Infrastructure.csproj | 2 +- .../Persistence/AtmDbContext.cs | 55 +++++ .../Configurations/AccountConfiguration.cs | 47 +++++ .../AccountHistoryEntryConfiguration.cs | 41 ++++ .../BankTransactionConfiguration.cs | 52 +++++ .../Persistence/Configurations/ColumnRules.cs | 40 ++++ .../Configurations/FriendshipConfiguration.cs | 26 +++ .../Configurations/SessionConfiguration.cs | 32 +++ .../Configurations/UserConfiguration.cs | 40 ++++ .../DesignTime/AtmDbContextFactory.cs | 35 +++ src/Atm.Presentation/Atm.Presentation.csproj | 4 + src/Atm.Presentation/packages.lock.json | 199 ++++++++++++++++++ .../Persistence/AtmModelTests.cs | 117 ++++++++++ .../Persistence/DesignTimeFactoryTests.cs | 52 +++++ .../Persistence/FinancialWriteGuardTests.cs | 82 ++++++++ .../Persistence/MaterializationTests.cs | 72 +++++++ .../Persistence/ModelTestContext.cs | 14 ++ 18 files changed, 944 insertions(+), 5 deletions(-) create mode 100644 src/Atm.Infrastructure/Persistence/AtmDbContext.cs create mode 100644 src/Atm.Infrastructure/Persistence/Configurations/AccountConfiguration.cs create mode 100644 src/Atm.Infrastructure/Persistence/Configurations/AccountHistoryEntryConfiguration.cs create mode 100644 src/Atm.Infrastructure/Persistence/Configurations/BankTransactionConfiguration.cs create mode 100644 src/Atm.Infrastructure/Persistence/Configurations/ColumnRules.cs create mode 100644 src/Atm.Infrastructure/Persistence/Configurations/FriendshipConfiguration.cs create mode 100644 src/Atm.Infrastructure/Persistence/Configurations/SessionConfiguration.cs create mode 100644 src/Atm.Infrastructure/Persistence/Configurations/UserConfiguration.cs create mode 100644 src/Atm.Infrastructure/Persistence/DesignTime/AtmDbContextFactory.cs create mode 100644 tests/Atm.Infrastructure.Tests/Persistence/AtmModelTests.cs create mode 100644 tests/Atm.Infrastructure.Tests/Persistence/DesignTimeFactoryTests.cs create mode 100644 tests/Atm.Infrastructure.Tests/Persistence/FinancialWriteGuardTests.cs create mode 100644 tests/Atm.Infrastructure.Tests/Persistence/MaterializationTests.cs create mode 100644 tests/Atm.Infrastructure.Tests/Persistence/ModelTestContext.cs diff --git a/README.md b/README.md index 263cc31..bbf9391 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ An ATM training project built with .NET 9, ASP.NET Core Controllers, and hexagonal architecture. -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. +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. ## Current functionality @@ -35,7 +35,7 @@ Business operations that can normally be accepted or rejected use a dedicated re 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. -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)`. +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. Commission snapshots retain the applied rate, fixed component, and calculated commission. The formula is: @@ -149,15 +149,46 @@ Tests are separated by architectural layer: - `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`. +- `Atm.Infrastructure.Tests` validates concrete infrastructure adapters, EF metadata, PostgreSQL SQL generation, materialization, change tracking, financial write guards, and the design-time factory. 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. +## EF model and design-time tools (13.3) + +`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. + +`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. + +`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. + +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. + +After configuring that variable, inspect the model without applying a migration: + +```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 +``` + +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. + +## Review of 13.2 + +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. + +Two defects were fixed with domain regression tests: + +- 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`. + +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. + ## Transitional limitations The following stage-1 work is not yet complete: -- `AtmDbContext`, mappings, migration, and PostgreSQL repositories; +- initial migration and PostgreSQL repositories; - asynchronous persistence ports and cancellation tokens; - atomic balance/transaction/history commits; - database-backed idempotency and concurrency handling; diff --git a/src/Atm.Infrastructure/Atm.Infrastructure.csproj b/src/Atm.Infrastructure/Atm.Infrastructure.csproj index 4e2e946..befd5ab 100644 --- a/src/Atm.Infrastructure/Atm.Infrastructure.csproj +++ b/src/Atm.Infrastructure/Atm.Infrastructure.csproj @@ -11,7 +11,7 @@ all - runtime; build; native; contentfiles; analyzers; buildtransitive + runtime; compile; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Atm.Infrastructure/Persistence/AtmDbContext.cs b/src/Atm.Infrastructure/Persistence/AtmDbContext.cs new file mode 100644 index 0000000..d2ba3c5 --- /dev/null +++ b/src/Atm.Infrastructure/Persistence/AtmDbContext.cs @@ -0,0 +1,55 @@ +using Atm.Domain.Accounts; +using Atm.Domain.Operations; +using Atm.Domain.Sessions; +using Atm.Domain.Users; +using Microsoft.EntityFrameworkCore; + +namespace Atm.Infrastructure.Persistence; + +public sealed class AtmDbContext : DbContext +{ + public AtmDbContext(DbContextOptions options) : base(options) { } + + public DbSet Users => Set(); + public DbSet Friendships => Set(); + public DbSet Accounts => Set(); + public DbSet Sessions => Set(); + public DbSet BankTransactions => Set(); + public DbSet AccountHistoryEntries => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.HasDefaultSchema("public"); + modelBuilder.ApplyConfigurationsFromAssembly(typeof(AtmDbContext).Assembly); + } + + public override int SaveChanges(bool acceptAllChangesOnSuccess) + { + ValidateFinancialChanges(); + return base.SaveChanges(acceptAllChangesOnSuccess); + } + + public override Task SaveChangesAsync( + bool acceptAllChangesOnSuccess, + CancellationToken cancellationToken = default) + { + ValidateFinancialChanges(); + return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + } + + private void ValidateFinancialChanges() + { + ChangeTracker.DetectChanges(); + foreach (Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry entry in ChangeTracker.Entries()) + { + if (entry.Entity is Account && entry.State == EntityState.Deleted) + throw new InvalidOperationException("Physical account deletion is not supported."); + if (entry.Entity is BankTransaction or AccountHistoryEntry or CommissionSnapshot + && entry.State is EntityState.Modified or EntityState.Deleted) + { + throw new InvalidOperationException("Persisted financial records are immutable."); + } + } + } +} diff --git a/src/Atm.Infrastructure/Persistence/Configurations/AccountConfiguration.cs b/src/Atm.Infrastructure/Persistence/Configurations/AccountConfiguration.cs new file mode 100644 index 0000000..b85bae9 --- /dev/null +++ b/src/Atm.Infrastructure/Persistence/Configurations/AccountConfiguration.cs @@ -0,0 +1,47 @@ +using Atm.Domain.Accounts; +using Atm.Domain.Users; +using Atm.Domain.ValueObjects; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Atm.Infrastructure.Persistence.Configurations; + +public sealed class AccountConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("accounts", table => + { + table.HasCheckConstraint("ck_accounts_id", ColumnRules.NonEmptyUuid("id")); + table.HasCheckConstraint("ck_accounts_balance", ColumnRules.MonetaryCheck("balance")); + table.HasCheckConstraint("ck_accounts_number", "length(btrim(number)) > 0 AND length(normalized_number) > 0"); + table.HasCheckConstraint("ck_accounts_pin_hash", "length(btrim(pin_hash)) > 0"); + table.HasCheckConstraint("ck_accounts_version", "version >= 0"); + table.HasCheckConstraint("ck_accounts_timestamps", "updated_at_utc >= created_at_utc"); + }); + builder.HasKey(account => account.Id).HasName("pk_accounts"); + builder.Property(account => account.Id).HasColumnName("id").HasColumnType("uuid").ValueGeneratedNever(); + builder.Property(account => account.OwnerUserId).HasColumnName("owner_user_id").HasColumnType("uuid"); + ColumnRules.Money(builder.Property(account => account.Balance), "balance"); + builder.Property(account => account.PinHash).HasColumnName("pin_hash") + .HasConversion(value => value.Value, value => new PinHash(value)).HasMaxLength(512).IsRequired(); + builder.Property(account => account.Version).HasColumnName("version").HasColumnType("bigint") + .IsConcurrencyToken().ValueGeneratedNever(); + builder.Property(account => account.CreatedAtUtc).HasColumnName("created_at_utc").HasColumnType("timestamp with time zone"); + builder.Property(account => account.UpdatedAtUtc).HasColumnName("updated_at_utc").HasColumnType("timestamp with time zone"); + builder.HasOne().WithMany().HasForeignKey(account => account.OwnerUserId) + .OnDelete(DeleteBehavior.Restrict).HasConstraintName("fk_accounts_users_owner_user_id"); + builder.HasIndex(account => account.OwnerUserId).HasDatabaseName("ix_accounts_owner_user_id"); + builder.OwnsOne(account => account.Number, number => + { + number.WithOwner().HasForeignKey("AccountId"); + number.Property("AccountId").HasColumnName("id").ValueGeneratedNever(); + number.HasKey("AccountId").HasName("pk_accounts"); + number.Property(value => value.Value).HasColumnName("number").HasMaxLength(64).IsRequired(); + number.Property(value => value.NormalizedValue).HasColumnName("normalized_number").HasMaxLength(64).UseCollation("C").IsRequired(); + number.Property("Version").HasColumnName("version").IsConcurrencyToken().ValueGeneratedNever(); + number.HasIndex(value => value.NormalizedValue).IsUnique().HasDatabaseName("ux_accounts_normalized_number"); + }); + builder.Navigation(account => account.Number).IsRequired(); + } +} diff --git a/src/Atm.Infrastructure/Persistence/Configurations/AccountHistoryEntryConfiguration.cs b/src/Atm.Infrastructure/Persistence/Configurations/AccountHistoryEntryConfiguration.cs new file mode 100644 index 0000000..ab9372c --- /dev/null +++ b/src/Atm.Infrastructure/Persistence/Configurations/AccountHistoryEntryConfiguration.cs @@ -0,0 +1,41 @@ +using Atm.Domain.Accounts; +using Atm.Domain.Operations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Atm.Infrastructure.Persistence.Configurations; + +public sealed class AccountHistoryEntryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("account_history_entries", table => + { + table.HasCheckConstraint("ck_account_history_entries_id", ColumnRules.NonEmptyUuid("id")); + table.HasCheckConstraint("ck_account_history_entries_balance_before", ColumnRules.MonetaryCheck("balance_before")); + table.HasCheckConstraint("ck_account_history_entries_balance_after", ColumnRules.MonetaryCheck("balance_after")); + table.HasCheckConstraint("ck_account_history_entries_type", + "entry_type IN ('Deposit', 'Withdrawal', 'TransferDebit', 'TransferCredit')"); + table.HasCheckConstraint("ck_account_history_entries_direction", + "(entry_type IN ('Deposit', 'TransferCredit') AND balance_after > balance_before) OR " + + "(entry_type IN ('Withdrawal', 'TransferDebit') AND balance_after < balance_before)"); + }); + builder.HasKey(entry => entry.Id).HasName("pk_account_history_entries"); + builder.Property(entry => entry.Id).HasColumnName("id").HasColumnType("uuid").ValueGeneratedNever(); + builder.Property(entry => entry.TransactionId).HasColumnName("transaction_id").HasColumnType("uuid"); + builder.Property(entry => entry.AccountId).HasColumnName("account_id").HasColumnType("uuid"); + builder.Property(entry => entry.EntryType).HasColumnName("entry_type").HasConversion().HasMaxLength(32); + ColumnRules.Money(builder.Property(entry => entry.BalanceBefore), "balance_before"); + ColumnRules.Money(builder.Property(entry => entry.BalanceAfter), "balance_after"); + builder.Property(entry => entry.CreatedAtUtc).HasColumnName("created_at_utc").HasColumnType("timestamp with time zone"); + builder.HasOne().WithMany().HasForeignKey(entry => entry.AccountId) + .OnDelete(DeleteBehavior.Restrict).HasConstraintName("fk_account_history_entries_accounts_account_id"); + builder.HasOne().WithMany().HasForeignKey(entry => entry.TransactionId) + .OnDelete(DeleteBehavior.Restrict).HasConstraintName("fk_account_history_entries_bank_transactions_transaction_id"); + builder.HasIndex(entry => new { entry.AccountId, entry.CreatedAtUtc, entry.Id }) + .IsDescending(false, true, true).HasDatabaseName("ix_account_history_entries_account_timeline"); + builder.HasIndex(entry => entry.TransactionId).HasDatabaseName("ix_account_history_entries_transaction_id"); + builder.HasAlternateKey(entry => new { entry.AccountId, entry.TransactionId, entry.EntryType }) + .HasName("ak_account_history_entries_account_transaction_type"); + } +} diff --git a/src/Atm.Infrastructure/Persistence/Configurations/BankTransactionConfiguration.cs b/src/Atm.Infrastructure/Persistence/Configurations/BankTransactionConfiguration.cs new file mode 100644 index 0000000..db66647 --- /dev/null +++ b/src/Atm.Infrastructure/Persistence/Configurations/BankTransactionConfiguration.cs @@ -0,0 +1,52 @@ +using Atm.Domain.Accounts; +using Atm.Domain.Operations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Atm.Infrastructure.Persistence.Configurations; + +public sealed class BankTransactionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("bank_transactions", table => + { + table.HasCheckConstraint("ck_bank_transactions_id", ColumnRules.NonEmptyUuid("id")); + table.HasCheckConstraint("ck_bank_transactions_request_id", ColumnRules.NonEmptyUuid("request_id")); + table.HasCheckConstraint("ck_bank_transactions_amount", ColumnRules.MonetaryCheck("amount", true)); + table.HasCheckConstraint("ck_bank_transactions_commission_rate", "commission_rate BETWEEN 0 AND 1"); + table.HasCheckConstraint("ck_bank_transactions_commission_fixed", ColumnRules.MonetaryCheck("commission_fixed")); + table.HasCheckConstraint("ck_bank_transactions_commission_amount", ColumnRules.MonetaryCheck("commission_amount")); + table.HasCheckConstraint("ck_bank_transactions_account_shape", + "(type = 'Deposit' AND source_account_id IS NULL AND target_account_id IS NOT NULL) OR " + + "(type = 'Withdrawal' AND source_account_id IS NOT NULL AND target_account_id IS NULL) OR " + + "(type = 'Transfer' AND source_account_id IS NOT NULL AND target_account_id IS NOT NULL AND source_account_id <> target_account_id)"); + table.HasCheckConstraint("ck_bank_transactions_deposit_credit", "type <> 'Deposit' OR amount > commission_amount"); + }); + builder.HasKey(transaction => transaction.Id).HasName("pk_bank_transactions"); + builder.Property(transaction => transaction.Id).HasColumnName("id").HasColumnType("uuid").ValueGeneratedNever(); + builder.Property(transaction => transaction.RequestId).HasColumnName("request_id").HasColumnType("uuid").ValueGeneratedNever(); + builder.Property(transaction => transaction.Type).HasColumnName("type").HasConversion().HasMaxLength(32); + builder.Property(transaction => transaction.SourceAccountId).HasColumnName("source_account_id").HasColumnType("uuid"); + builder.Property(transaction => transaction.TargetAccountId).HasColumnName("target_account_id").HasColumnType("uuid"); + ColumnRules.Amount(builder.Property(transaction => transaction.Amount), "amount"); + builder.Property(transaction => transaction.CreatedAtUtc).HasColumnName("created_at_utc").HasColumnType("timestamp with time zone"); + builder.HasIndex(transaction => transaction.RequestId).IsUnique().HasDatabaseName("ux_bank_transactions_request_id"); + builder.HasIndex(transaction => transaction.SourceAccountId).HasDatabaseName("ix_bank_transactions_source_account_id"); + builder.HasIndex(transaction => transaction.TargetAccountId).HasDatabaseName("ix_bank_transactions_target_account_id"); + builder.HasOne().WithMany().HasForeignKey(transaction => transaction.SourceAccountId) + .OnDelete(DeleteBehavior.Restrict).HasConstraintName("fk_bank_transactions_accounts_source_account_id"); + builder.HasOne().WithMany().HasForeignKey(transaction => transaction.TargetAccountId) + .OnDelete(DeleteBehavior.Restrict).HasConstraintName("fk_bank_transactions_accounts_target_account_id"); + builder.OwnsOne(transaction => transaction.Commission, commission => + { + commission.WithOwner().HasForeignKey("TransactionId"); + commission.Property("TransactionId").HasColumnName("id").ValueGeneratedNever(); + commission.HasKey("TransactionId").HasName("pk_bank_transactions"); + commission.Property(value => value.Rate).HasColumnName("commission_rate").HasPrecision(9, 6); + ColumnRules.Money(commission.Property(value => value.FixedAmount), "commission_fixed"); + ColumnRules.Money(commission.Property(value => value.CommissionAmount), "commission_amount"); + }); + builder.Navigation(transaction => transaction.Commission).IsRequired(); + } +} diff --git a/src/Atm.Infrastructure/Persistence/Configurations/ColumnRules.cs b/src/Atm.Infrastructure/Persistence/Configurations/ColumnRules.cs new file mode 100644 index 0000000..19c117d --- /dev/null +++ b/src/Atm.Infrastructure/Persistence/Configurations/ColumnRules.cs @@ -0,0 +1,40 @@ +using Atm.Domain.ValueObjects; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Atm.Infrastructure.Persistence.Configurations; + +internal static class ColumnRules +{ + public static PropertyBuilder Money(PropertyBuilder property, string column) + { + property.HasConversion(value => value.Value, value => new Money(value)); + property.Metadata.SetValueComparer(new ValueComparer( + (left, right) => left != null && right != null ? left.Value == right.Value : left == right, + value => value.Value.GetHashCode(), value => value)); + return property.HasColumnName(column).HasColumnType("numeric").IsRequired(); + } + + public static PropertyBuilder Amount(PropertyBuilder property, string column) + { + property.HasConversion(value => value.Value, value => new Amount(value)); + property.Metadata.SetValueComparer(new ValueComparer( + (left, right) => left != null && right != null ? left.Value == right.Value : left == right, + value => value.Value.GetHashCode(), value => value)); + return property.HasColumnName(column).HasColumnType("numeric").IsRequired(); + } + + public static string MonetaryCheck(string column, bool positive = false) + { + string comparison = positive ? ">" : ">="; + return $"{column} {comparison} 0 AND {column} <= 79228162514264337593543950335 " + + $"AND {column} = trunc({column}, 4) " + + $"AND {column} * power(10::numeric, scale(trim_scale({column}))) <= 79228162514264337593543950335"; + } + + public static string NonEmptyUuid(string column) + { + return $"{column} <> '00000000-0000-0000-0000-000000000000'::uuid"; + } +} diff --git a/src/Atm.Infrastructure/Persistence/Configurations/FriendshipConfiguration.cs b/src/Atm.Infrastructure/Persistence/Configurations/FriendshipConfiguration.cs new file mode 100644 index 0000000..ca7fe53 --- /dev/null +++ b/src/Atm.Infrastructure/Persistence/Configurations/FriendshipConfiguration.cs @@ -0,0 +1,26 @@ +using Atm.Domain.Users; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Atm.Infrastructure.Persistence.Configurations; + +public sealed class FriendshipConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("friendships", table => + { + table.HasCheckConstraint("ck_friendships_distinct_users", "user_id <> friend_user_id"); + table.HasCheckConstraint("ck_friendships_canonical_order", "user_id < friend_user_id"); + }); + builder.HasKey(friendship => new { friendship.UserId, friendship.FriendUserId }).HasName("pk_friendships"); + builder.Property(friendship => friendship.UserId).HasColumnName("user_id").HasColumnType("uuid").ValueGeneratedNever(); + builder.Property(friendship => friendship.FriendUserId).HasColumnName("friend_user_id").HasColumnType("uuid").ValueGeneratedNever(); + builder.Property(friendship => friendship.CreatedAtUtc).HasColumnName("created_at_utc").HasColumnType("timestamp with time zone"); + builder.HasOne().WithMany().HasForeignKey(friendship => friendship.UserId) + .OnDelete(DeleteBehavior.Cascade).HasConstraintName("fk_friendships_users_user_id"); + builder.HasOne().WithMany().HasForeignKey(friendship => friendship.FriendUserId) + .OnDelete(DeleteBehavior.Cascade).HasConstraintName("fk_friendships_users_friend_user_id"); + builder.HasIndex(friendship => friendship.FriendUserId).HasDatabaseName("ix_friendships_friend_user_id"); + } +} diff --git a/src/Atm.Infrastructure/Persistence/Configurations/SessionConfiguration.cs b/src/Atm.Infrastructure/Persistence/Configurations/SessionConfiguration.cs new file mode 100644 index 0000000..6dc911a --- /dev/null +++ b/src/Atm.Infrastructure/Persistence/Configurations/SessionConfiguration.cs @@ -0,0 +1,32 @@ +using Atm.Domain.Accounts; +using Atm.Domain.Sessions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Atm.Infrastructure.Persistence.Configurations; + +public sealed class SessionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("sessions", table => + { + table.HasCheckConstraint("ck_sessions_id", ColumnRules.NonEmptyUuid("id")); + table.HasCheckConstraint("ck_sessions_account_type", + "(type = 'User' AND account_id IS NOT NULL) OR (type = 'Admin' AND account_id IS NULL)"); + table.HasCheckConstraint("ck_sessions_expiration", "expires_at_utc > created_at_utc"); + table.HasCheckConstraint("ck_sessions_revocation", "revoked_at_utc IS NULL OR revoked_at_utc >= created_at_utc"); + }); + builder.HasKey(session => session.Id).HasName("pk_sessions"); + builder.Property(session => session.Id).HasColumnName("id").HasColumnType("uuid").ValueGeneratedNever(); + builder.Property(session => session.Type).HasColumnName("type").HasConversion().HasMaxLength(16); + builder.Property(session => session.AccountId).HasColumnName("account_id").HasColumnType("uuid"); + builder.Property(session => session.CreatedAtUtc).HasColumnName("created_at_utc").HasColumnType("timestamp with time zone"); + builder.Property(session => session.ExpiresAtUtc).HasColumnName("expires_at_utc").HasColumnType("timestamp with time zone"); + builder.Property(session => session.RevokedAtUtc).HasColumnName("revoked_at_utc").HasColumnType("timestamp with time zone"); + builder.HasOne().WithMany().HasForeignKey(session => session.AccountId) + .OnDelete(DeleteBehavior.Restrict).HasConstraintName("fk_sessions_accounts_account_id"); + builder.HasIndex(session => session.AccountId).HasDatabaseName("ix_sessions_account_id"); + builder.HasIndex(session => session.ExpiresAtUtc).HasDatabaseName("ix_sessions_expires_at_utc"); + } +} diff --git a/src/Atm.Infrastructure/Persistence/Configurations/UserConfiguration.cs b/src/Atm.Infrastructure/Persistence/Configurations/UserConfiguration.cs new file mode 100644 index 0000000..527e84f --- /dev/null +++ b/src/Atm.Infrastructure/Persistence/Configurations/UserConfiguration.cs @@ -0,0 +1,40 @@ +using Atm.Domain.Users; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Atm.Infrastructure.Persistence.Configurations; + +public sealed class UserConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("users", table => + { + table.HasCheckConstraint("ck_users_id", ColumnRules.NonEmptyUuid("id")); + table.HasCheckConstraint("ck_users_name", "length(btrim(name)) > 0"); + table.HasCheckConstraint("ck_users_login", "length(btrim(login)) > 0 AND length(normalized_login) > 0"); + table.HasCheckConstraint("ck_users_age", "age BETWEEN 0 AND 150"); + table.HasCheckConstraint("ck_users_gender", "gender IN ('Unspecified', 'Male', 'Female', 'Other')"); + table.HasCheckConstraint("ck_users_hair_color", "hair_color IN ('Unspecified', 'Black', 'Brown', 'Blond', 'Red', 'Gray', 'White', 'Other')"); + table.HasCheckConstraint("ck_users_timestamps", "updated_at_utc >= created_at_utc"); + }); + builder.HasKey(user => user.Id).HasName("pk_users"); + builder.Property(user => user.Id).HasColumnName("id").HasColumnType("uuid").ValueGeneratedNever(); + builder.Property(user => user.Name).HasColumnName("name").HasMaxLength(200).IsRequired(); + builder.Property(user => user.Age).HasColumnName("age").HasColumnType("smallint"); + builder.Property(user => user.Gender).HasColumnName("gender").HasConversion().HasMaxLength(32); + builder.Property(user => user.HairColor).HasColumnName("hair_color").HasConversion().HasMaxLength(32); + builder.Property(user => user.CreatedAtUtc).HasColumnName("created_at_utc").HasColumnType("timestamp with time zone"); + builder.Property(user => user.UpdatedAtUtc).HasColumnName("updated_at_utc").HasColumnType("timestamp with time zone"); + builder.OwnsOne(user => user.Login, login => + { + login.WithOwner().HasForeignKey("UserId"); + login.Property("UserId").HasColumnName("id").ValueGeneratedNever(); + login.HasKey("UserId").HasName("pk_users"); + login.Property(value => value.Value).HasColumnName("login").HasMaxLength(64).IsRequired(); + login.Property(value => value.NormalizedValue).HasColumnName("normalized_login").HasMaxLength(64).UseCollation("C").IsRequired(); + login.HasIndex(value => value.NormalizedValue).IsUnique().HasDatabaseName("ux_users_normalized_login"); + }); + builder.Navigation(user => user.Login).IsRequired(); + } +} diff --git a/src/Atm.Infrastructure/Persistence/DesignTime/AtmDbContextFactory.cs b/src/Atm.Infrastructure/Persistence/DesignTime/AtmDbContextFactory.cs new file mode 100644 index 0000000..2ae24a3 --- /dev/null +++ b/src/Atm.Infrastructure/Persistence/DesignTime/AtmDbContextFactory.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Npgsql; + +namespace Atm.Infrastructure.Persistence.DesignTime; + +public sealed class AtmDbContextFactory : IDesignTimeDbContextFactory +{ + public const string ConnectionStringVariable = "ConnectionStrings__AtmDatabase"; + + public AtmDbContext CreateDbContext(string[] args) + { + string? connectionString = Environment.GetEnvironmentVariable(ConnectionStringVariable); + if (string.IsNullOrWhiteSpace(connectionString)) + throw new InvalidOperationException($"Set {ConnectionStringVariable} before running EF tools."); + + NpgsqlConnectionStringBuilder settings; + try + { + settings = new NpgsqlConnectionStringBuilder(connectionString); + } + catch (ArgumentException) + { + throw new InvalidOperationException("The database connection configuration is invalid."); + } + + if (string.IsNullOrWhiteSpace(settings.Host) || string.IsNullOrWhiteSpace(settings.Database)) + throw new InvalidOperationException("The database connection must specify Host and Database."); + + DbContextOptions options = new DbContextOptionsBuilder() + .UseNpgsql(settings.ConnectionString) + .Options; + return new AtmDbContext(options); + } +} diff --git a/src/Atm.Presentation/Atm.Presentation.csproj b/src/Atm.Presentation/Atm.Presentation.csproj index 04059c3..d09df39 100644 --- a/src/Atm.Presentation/Atm.Presentation.csproj +++ b/src/Atm.Presentation/Atm.Presentation.csproj @@ -8,6 +8,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Atm.Presentation/packages.lock.json b/src/Atm.Presentation/packages.lock.json index 00449bf..12bea76 100644 --- a/src/Atm.Presentation/packages.lock.json +++ b/src/Atm.Presentation/packages.lock.json @@ -2,6 +2,27 @@ "version": 1, "dependencies": { "net9.0": { + "Microsoft.EntityFrameworkCore.Design": { + "type": "Direct", + "requested": "[9.0.19, )", + "resolved": "9.0.19", + "contentHash": "O4o69Xne6dJ7+u21YPxevGt8F2wpxGz2bBiSa/SSyqb2/64OZtcbIOB264fuViRj2XgwzhSGErNP75fRVCk8Zw==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "17.8.43", + "Microsoft.Build.Locator": "1.7.8", + "Microsoft.CodeAnalysis.CSharp": "4.8.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "4.8.0", + "Microsoft.CodeAnalysis.Workspaces.MSBuild": "4.8.0", + "Microsoft.EntityFrameworkCore.Relational": "9.0.19", + "Microsoft.Extensions.Caching.Memory": "9.0.19", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.19", + "Microsoft.Extensions.DependencyModel": "9.0.19", + "Microsoft.Extensions.Logging": "9.0.19", + "Mono.TextTemplating": "3.0.0", + "System.Text.Json": "9.0.19" + } + }, "Swashbuckle.AspNetCore": { "type": "Direct", "requested": "[6.6.2, )", @@ -14,6 +35,11 @@ "Swashbuckle.AspNetCore.SwaggerUI": "6.6.2" } }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, "Microsoft.AspNetCore.Cryptography.Internal": { "type": "Transitive", "resolved": "9.0.19", @@ -27,6 +53,80 @@ "Microsoft.AspNetCore.Cryptography.Internal": "9.0.19" } }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "3aeMZ1N0lJoSyzqiP03hqemtb1BijhsJADdobn/4nsMJ8V1H+CrpuduUe4hlRdx+ikBQju1VGjMD1GJ3Sk05Eg==" + }, + "Microsoft.Build.Framework": { + "type": "Transitive", + "resolved": "17.8.43", + "contentHash": "TA6fCbd9qFSyhIKcUn5BhDN3/JrqpspO9KYsZKUvHyQpQrHVdzy4Hk6egjjBP3T0L562TLZuHEzQwJziyRBEkw==" + }, + "Microsoft.Build.Locator": { + "type": "Transitive", + "resolved": "1.7.8", + "contentHash": "sPy10x527Ph16S2u0yGME4S6ohBKJ69WfjeGG/bvELYeZVmJdKjxgnlL8cJJJLGV/cZIRqSfB12UDB8ICakOog==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.4", + "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.4", + "System.Collections.Immutable": "7.0.0", + "System.Reflection.Metadata": "7.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "+3+qfdb/aaGD8PZRCrsdobbzGs1m9u119SkkJt8e/mk3xLJz/udLtS2T6nY27OTXxBBw10HzAbC8Z9w08VyP/g==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[4.8.0]" + } + }, + "Microsoft.CodeAnalysis.CSharp.Workspaces": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "3amm4tq4Lo8/BGvg9p3BJh3S9nKq2wqCXfS7138i69TUpo/bD+XvD0hNurpEBtcNZhi1FyutiomKJqVF39ugYA==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.CSharp": "[4.8.0]", + "Microsoft.CodeAnalysis.Common": "[4.8.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "LXyV+MJKsKRu3FGJA3OmSk40OUIa/dQCFLOnm5X8MNcujx7hzGu8o+zjXlb/cy5xUdZK2UKYb9YaQ2E8m9QehQ==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Bcl.AsyncInterfaces": "7.0.0", + "Microsoft.CodeAnalysis.Common": "[4.8.0]", + "System.Composition": "7.0.0", + "System.IO.Pipelines": "7.0.0", + "System.Threading.Channels": "7.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.MSBuild": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "IEYreI82QZKklp54yPHxZNG9EKSK6nHEkeuf+0Asie9llgS1gp0V1hw7ODG+QyoB7MuAnNQHmeV1Per/ECpv6A==", + "dependencies": { + "Microsoft.Build.Framework": "16.10.0", + "Microsoft.CodeAnalysis.Common": "[4.8.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]", + "System.Text.Json": "7.0.3" + } + }, "Microsoft.EntityFrameworkCore": { "type": "Transitive", "resolved": "9.0.19", @@ -105,6 +205,11 @@ "resolved": "9.0.19", "contentHash": "XgQ0aVWClYjYSBqbYrKC/xyZ1KHf+VVwNF3+d54jcaqnst30Q//ugs/FJwUztLZiph0gp85g7i/eB0556twO0Q==" }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "Jpe5up2VLMkjKavOXZzWRXBvy8SUPoFCRuVkq1wYrtVwtzuFEc7saw02dltASs5T8DXp6Ff/DPzmU95mDOiw6g==" + }, "Microsoft.Extensions.Identity.Core": { "type": "Transitive", "resolved": "9.0.19", @@ -152,6 +257,14 @@ "resolved": "1.6.14", "contentHash": "tTaBT8qjk3xINfESyOPE2rIellPvB7qpVqiWiyA/lACVvz+xOGiXhFUfohcx82NLbi5avzLW0lx+s6oAqQijfw==" }, + "Mono.TextTemplating": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "YqueG52R/Xej4VVbKuRIodjiAhV0HR/XVbLbNrJhCZnzjnSjgMJ/dCdV0akQQxavX6hp/LC6rqLGLcXeQYU7XA==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, "Npgsql": { "type": "Transitive", "resolved": "9.0.3", @@ -191,6 +304,92 @@ "resolved": "6.6.2", "contentHash": "mBBb+/8Hm2Q3Wygag+hu2jj69tZW5psuv0vMRXY07Wy+Rrj40vRP8ZTbKBhs91r45/HXT4aY4z0iSBYx1h6JvA==" }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==" + }, + "System.Composition": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "tRwgcAkDd85O8Aq6zHDANzQaq380cek9lbMg5Qma46u5BZXq/G+XvIYmu+UI+BIIZ9zssXLYrkTykEqxxvhcmg==", + "dependencies": { + "System.Composition.AttributedModel": "7.0.0", + "System.Composition.Convention": "7.0.0", + "System.Composition.Hosting": "7.0.0", + "System.Composition.Runtime": "7.0.0", + "System.Composition.TypedParts": "7.0.0" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "2QzClqjElKxgI1jK1Jztnq44/8DmSuTSGGahXqQ4TdEV0h9s2KikQZIgcEqVzR7OuWDFPGLHIprBJGQEPr8fAQ==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "IMhTlpCs4HmlD8B+J8/kWfwX7vrBBOs6xyjSTzBlYSs7W4OET4tlkR/Sg9NG8jkdJH9Mymq0qGdYS1VPqRTBnQ==", + "dependencies": { + "System.Composition.AttributedModel": "7.0.0" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "eB6gwN9S+54jCTBJ5bpwMOVerKeUfGGTYCzz3QgDr1P55Gg/Wb27ShfPIhLMjmZ3MoAKu8uUSv6fcCdYJTN7Bg==", + "dependencies": { + "System.Composition.Runtime": "7.0.0" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "aZJ1Zr5Txe925rbo4742XifEyW0MIni1eiUebmcrP3HwLXZ3IbXUj4MFMUH/RmnJOAQiS401leg/2Sz1MkApDw==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "ZK0KNPfbtxVceTwh+oHNGUOYV2WNOHReX2AXipuvkURC7s/jPwoWfsu3SnDBDgofqbiWr96geofdQ2erm/KTHg==", + "dependencies": { + "System.Composition.AttributedModel": "7.0.0", + "System.Composition.Hosting": "7.0.0", + "System.Composition.Runtime": "7.0.0" + } + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==", + "dependencies": { + "System.Collections.Immutable": "7.0.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "9.0.19", + "contentHash": "DjVVcIY/dS/rjb0b42nciHjUc0voQfUtrTos+wC0sgdx0VemTpYhx1jdeHowwFghygJ6QdaytcMHwA8jZ6MHFg==" + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" + }, "atm.application": { "type": "Project", "dependencies": { diff --git a/tests/Atm.Infrastructure.Tests/Persistence/AtmModelTests.cs b/tests/Atm.Infrastructure.Tests/Persistence/AtmModelTests.cs new file mode 100644 index 0000000..92564b4 --- /dev/null +++ b/tests/Atm.Infrastructure.Tests/Persistence/AtmModelTests.cs @@ -0,0 +1,117 @@ +using Atm.Domain.Accounts; +using Atm.Domain.Operations; +using Atm.Domain.ValueObjects; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Atm.Infrastructure.Tests.Persistence; + +public sealed class AtmModelTests +{ + [Fact] + public void ModelContainsExactlyTheRequiredTables() + { + using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); + IModel model = context.GetService().Model; + string[] tables = model.GetRelationalModel().Tables.Select(table => table.Name).Order().ToArray(); + Assert.Equal(new[] + { + "account_history_entries", "accounts", "bank_transactions", "friendships", "sessions", "users", + }, tables); + Assert.Null(model.FindEntityType(typeof(OperationRecord))); + Assert.All(model.GetRelationalModel().Tables, table => Assert.Equal("public", table.Schema)); + Assert.All(model.GetEntityTypes().Where(entity => !entity.IsOwned()), entity => + { + Assert.NotNull(entity.FindPrimaryKey()); + Assert.All(entity.FindPrimaryKey()!.Properties, property => + Assert.Equal(ValueGenerated.Never, property.ValueGenerated)); + }); + } + + [Fact] + public void SchemaScriptContainsConstraintsAndNoMoneyPrecisionCeiling() + { + using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); + string sql = context.Database.GenerateCreateScript(); + Assert.Contains("CREATE TABLE public.users", sql, StringComparison.Ordinal); + Assert.Contains("user_id < friend_user_id", sql, StringComparison.Ordinal); + Assert.Contains("ON DELETE RESTRICT", sql, StringComparison.Ordinal); + Assert.Contains("ON DELETE CASCADE", sql, StringComparison.Ordinal); + Assert.Contains("balance numeric NOT NULL", sql, StringComparison.Ordinal); + Assert.Contains("commission_rate numeric(9,6) NOT NULL", sql, StringComparison.Ordinal); + Assert.DoesNotContain("numeric(19,4)", sql, StringComparison.Ordinal); + Assert.Contains("trunc(balance, 4)", sql, StringComparison.Ordinal); + Assert.Contains("normalized_login", sql, StringComparison.Ordinal); + Assert.Contains("normalized_number", sql, StringComparison.Ordinal); + } + + [Fact] + public void VersionIndexesAndDeletionRulesMatchTheSpecification() + { + using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); + IModel model = context.GetService().Model; + IEntityType account = model.FindEntityType(typeof(Account))!; + Assert.True(account.FindProperty(nameof(Account.Version))!.IsConcurrencyToken); + Assert.All(model.GetEntityTypes().Where(entity => !entity.IsOwned()) + .SelectMany(entity => entity.GetForeignKeys()) + .Where(key => key.DeclaringEntityType.GetTableName() != "friendships"), + key => Assert.Equal(DeleteBehavior.Restrict, key.DeleteBehavior)); + + IIndex timeline = model.FindEntityType(typeof(AccountHistoryEntry))!.GetIndexes() + .Single(index => index.GetDatabaseName() == "ix_account_history_entries_account_timeline"); + Assert.Equal(new[] { false, true, true }, timeline.IsDescending); + Assert.Equal(new[] { "AccountId", "CreatedAtUtc", "Id" }, timeline.Properties.Select(property => property.Name)); + Assert.True(model.FindEntityType(typeof(BankTransaction))!.GetIndexes() + .Single(index => index.Properties.Single().Name == "RequestId").IsUnique); + Assert.Equal(2, model.FindEntityType(typeof(AccountHistoryEntry))!.GetKeys().Count()); + } + + [Fact] + public void ValueConversionsRoundTripAndCompareByValue() + { + using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); + IProperty balance = context.Model.FindEntityType(typeof(Account))!.FindProperty(nameof(Account.Balance))!; + var original = new Money(1_000_000_000_000_001.0001m); + Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter converter = balance.GetTypeMapping().Converter!; + object? databaseValue = converter.ConvertToProvider(original); + Money restored = Assert.IsType(converter.ConvertFromProvider(databaseValue)); + Assert.Equal(original.Value, restored.Value); + Assert.True(balance.GetValueComparer()!.Equals(original, restored)); + Assert.False(balance.GetValueComparer()!.Equals(original, new Money(1m))); + + IProperty amount = context.Model.FindEntityType(typeof(BankTransaction))!.FindProperty("Amount")!; + Amount restoredAmount = Assert.IsType( + amount.GetTypeMapping().Converter!.ConvertFromProvider(0.0001m)); + Assert.Equal(0.0001m, restoredAmount.Value); + } + + [Fact] + public void NormalizedLookupsAndHistoryPaginationTranslateToSql() + { + using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); + string users = context.Users.Where(user => user.Login.NormalizedValue == "USER").ToQueryString(); + string accounts = context.Accounts.Where(account => account.Number.NormalizedValue == "ACCOUNT").ToQueryString(); + string history = context.AccountHistoryEntries.Where(entry => entry.AccountId == Guid.Empty) + .OrderByDescending(entry => entry.CreatedAtUtc).ThenByDescending(entry => entry.Id) + .Take(20).ToQueryString(); + Assert.Contains("normalized_login", users, StringComparison.Ordinal); + Assert.Contains("normalized_number", accounts, StringComparison.Ordinal); + Assert.Contains("DESC", history, StringComparison.Ordinal); + Assert.Contains("LIMIT", history, StringComparison.Ordinal); + } + + [Theory] + [InlineData("users", "normalized_login", "ux_users_normalized_login")] + [InlineData("accounts", "normalized_number", "ux_accounts_normalized_number")] + [InlineData("bank_transactions", "request_id", "ux_bank_transactions_request_id")] + public void BusinessKeysHaveRequiredUniqueIndexes(string tableName, string columnName, string indexName) + { + using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); + ITable table = context.GetService().Model.GetRelationalModel().FindTable(tableName, "public")!; + Assert.False(table.FindColumn(columnName)!.IsNullable); + ITableIndex index = table.Indexes.Single(value => value.Name == indexName); + Assert.True(index.IsUnique); + Assert.Equal(columnName, Assert.Single(index.Columns).Name); + } +} diff --git a/tests/Atm.Infrastructure.Tests/Persistence/DesignTimeFactoryTests.cs b/tests/Atm.Infrastructure.Tests/Persistence/DesignTimeFactoryTests.cs new file mode 100644 index 0000000..dc3d718 --- /dev/null +++ b/tests/Atm.Infrastructure.Tests/Persistence/DesignTimeFactoryTests.cs @@ -0,0 +1,52 @@ +using Atm.Infrastructure.Persistence.DesignTime; +using Microsoft.EntityFrameworkCore; +using Npgsql; + +namespace Atm.Infrastructure.Tests.Persistence; + +[Collection("Environment configuration")] +public sealed class DesignTimeFactoryTests +{ + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("UnknownOption=secret-value")] + public void InvalidConfigurationFailsWithoutExposingValues(string? value) + { + string? previous = Environment.GetEnvironmentVariable(AtmDbContextFactory.ConnectionStringVariable); + try + { + Environment.SetEnvironmentVariable(AtmDbContextFactory.ConnectionStringVariable, value); + var factory = new AtmDbContextFactory(); + InvalidOperationException error = Assert.Throws(() => factory.CreateDbContext([])); + Assert.DoesNotContain("secret-value", error.ToString(), StringComparison.Ordinal); + Assert.Null(error.InnerException); + } + finally + { + Environment.SetEnvironmentVariable(AtmDbContextFactory.ConnectionStringVariable, previous); + } + } + + [Fact] + public void FactoryCreatesNpgsqlContextWithoutOpeningAConnection() + { + string? previous = Environment.GetEnvironmentVariable(AtmDbContextFactory.ConnectionStringVariable); + try + { + var settings = new NpgsqlConnectionStringBuilder { Host = "localhost", Database = "model_tests" }; + Environment.SetEnvironmentVariable(AtmDbContextFactory.ConnectionStringVariable, settings.ConnectionString); + using Infrastructure.Persistence.AtmDbContext context = new AtmDbContextFactory().CreateDbContext([]); + Assert.Equal("Npgsql.EntityFrameworkCore.PostgreSQL", context.Database.ProviderName); + Assert.Equal(System.Data.ConnectionState.Closed, context.Database.GetDbConnection().State); + Assert.NotEmpty(context.Model.GetEntityTypes()); + } + finally + { + Environment.SetEnvironmentVariable(AtmDbContextFactory.ConnectionStringVariable, previous); + } + } +} + +[CollectionDefinition("Environment configuration", DisableParallelization = true)] +public sealed class EnvironmentConfigurationCollection; diff --git a/tests/Atm.Infrastructure.Tests/Persistence/FinancialWriteGuardTests.cs b/tests/Atm.Infrastructure.Tests/Persistence/FinancialWriteGuardTests.cs new file mode 100644 index 0000000..5aea0d7 --- /dev/null +++ b/tests/Atm.Infrastructure.Tests/Persistence/FinancialWriteGuardTests.cs @@ -0,0 +1,82 @@ +using Atm.Domain.Accounts; +using Atm.Domain.Accounts.Results; +using Atm.Domain.Operations; +using Atm.Domain.Operations.Results; +using Atm.Domain.ValueObjects; + +namespace Atm.Infrastructure.Tests.Persistence; + +public sealed class FinancialWriteGuardTests +{ + private static readonly DateTimeOffset Now = new(2026, 9, 6, 12, 0, 0, TimeSpan.Zero); + + [Fact] + public void DeletingAnAccountFailsBeforeDatabaseAccess() + { + using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); + Account account = CreateAccount(); + context.Attach(account); + context.Remove(account); + InvalidOperationException error = Assert.Throws(() => context.SaveChanges()); + Assert.Equal("Physical account deletion is not supported.", error.Message); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdatingOrDeletingHistoryFailsBeforeDatabaseAccess(bool delete) + { + using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); + BankTransaction transaction = CreateTransaction(); + AccountHistoryEntry history = Assert.IsType( + AccountHistoryEntry.Create(Guid.NewGuid(), transaction, transaction.TargetAccountId!.Value, new Money(10m))).Entry; + context.Attach(history); + if (delete) + context.Remove(history); + else + context.Entry(history).Property(entry => entry.BalanceAfter).CurrentValue = new Money(20m); + + InvalidOperationException error = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); + Assert.Equal("Persisted financial records are immutable.", error.Message); + } + + [Fact] + public void ChangingTheCommissionSnapshotFailsBeforeDatabaseAccess() + { + using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); + BankTransaction transaction = CreateTransaction(); + context.Attach(transaction); + context.Entry(transaction).Reference(value => value.Commission).TargetEntry! + .Property(nameof(CommissionSnapshot.Rate)).CurrentValue = 0.5m; + InvalidOperationException error = Assert.Throws(() => context.SaveChanges()); + Assert.Equal("Persisted financial records are immutable.", error.Message); + } + + [Fact] + public void ChangingBalanceTracksTheVersionAndConvertedValue() + { + using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); + Account account = CreateAccount(); + context.Attach(account); + account.Deposit(new Amount(2m), Now.AddMinutes(1)); + context.ChangeTracker.DetectChanges(); + Assert.True(context.Entry(account).Property(value => value.Balance).IsModified); + Assert.Equal(0, context.Entry(account).Property(value => value.Version).OriginalValue); + Assert.Equal(1, context.Entry(account).Property(value => value.Version).CurrentValue); + } + + private static Account CreateAccount() + { + return Assert.IsType(Account.Create( + Guid.NewGuid(), new AccountNumber("guard"), Guid.NewGuid(), new PinHash("test-hash"), + new Money(10m), Now)).Account; + } + + private static BankTransaction CreateTransaction() + { + var amount = new Amount(1m); + return Assert.IsType(BankTransaction.Create( + Guid.NewGuid(), Guid.NewGuid(), BankTransactionType.Deposit, null, Guid.NewGuid(), + amount, new CommissionSnapshot(amount, 0m, 0m), Now)).Transaction; + } +} diff --git a/tests/Atm.Infrastructure.Tests/Persistence/MaterializationTests.cs b/tests/Atm.Infrastructure.Tests/Persistence/MaterializationTests.cs new file mode 100644 index 0000000..a096382 --- /dev/null +++ b/tests/Atm.Infrastructure.Tests/Persistence/MaterializationTests.cs @@ -0,0 +1,72 @@ +using Atm.Domain.Accounts; +using Atm.Domain.Accounts.Results; +using Atm.Domain.Operations; +using Atm.Domain.Operations.Results; +using Atm.Domain.Sessions; +using Atm.Domain.Sessions.Results; +using Atm.Domain.Users; +using Atm.Domain.Users.Results; +using Atm.Domain.ValueObjects; +using Atm.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Atm.Infrastructure.Tests.Persistence; + +public sealed class MaterializationTests +{ + [Fact] + public void EfCanRestorePrivateConstructorsAndImmutableValueObjects() + { + using AtmDbContext context = ModelTestContext.Create(); + AddSampleEntities(context); + VerifyMaterialization(context); + } + + private static void AddSampleEntities(AtmDbContext context) + { + var now = new DateTimeOffset(2026, 9, 6, 12, 0, 0, TimeSpan.Zero); + User user = Assert.IsType(User.Create( + Guid.NewGuid(), new UserLogin("Alice"), "Alice", 30, + UserGender.Unspecified, HairColor.Unspecified, now)).User; + Account account = Assert.IsType(Account.Create( + Guid.NewGuid(), new AccountNumber("account-1"), user.Id, + new PinHash("test-hash"), new Money(10m), now)).Account; + var amount = new Amount(1m); + BankTransaction transaction = Assert.IsType(BankTransaction.Create( + Guid.NewGuid(), Guid.NewGuid(), BankTransactionType.Deposit, null, account.Id, + amount, new CommissionSnapshot(amount, 0m, 0m), now)).Transaction; + context.Add(user); + context.Add(account); + context.Add(transaction); + context.Add(Assert.IsType( + Friendship.Create(user.Id, Guid.NewGuid(), now)).Friendship); + context.Add(Assert.IsType( + Session.Create(Guid.NewGuid(), SessionType.User, account.Id, now, now.AddMinutes(30))).Session); + context.Add(Assert.IsType( + AccountHistoryEntry.Create(Guid.NewGuid(), transaction, account.Id, account.Balance)).Entry); + context.ChangeTracker.DetectChanges(); + } + + private static void VerifyMaterialization(AtmDbContext context) + { + IEntityMaterializerSource materializer = context.GetService(); + foreach (Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry entry in context.ChangeTracker.Entries()) + { + Microsoft.EntityFrameworkCore.Metadata.IProperty[] properties = entry.Metadata.GetProperties().ToArray(); + object[] values = new object[properties.Length]; + foreach (Microsoft.EntityFrameworkCore.Metadata.IProperty? property in properties) + values[property.GetIndex()] = entry.Property(property.Name).CurrentValue!; + + object restored = materializer.GetMaterializer(entry.Metadata)( + new MaterializationContext(new ValueBuffer(values), context)); + Assert.Equal(entry.Entity.GetType(), restored.GetType()); + foreach (Microsoft.EntityFrameworkCore.Metadata.IProperty? property in properties.Where(property => !property.IsShadowProperty())) + { + object? value = property.PropertyInfo!.GetValue(restored); + Assert.True(property.GetValueComparer()!.Equals(entry.Property(property.Name).CurrentValue, value)); + } + } + } +} diff --git a/tests/Atm.Infrastructure.Tests/Persistence/ModelTestContext.cs b/tests/Atm.Infrastructure.Tests/Persistence/ModelTestContext.cs new file mode 100644 index 0000000..1e2227a --- /dev/null +++ b/tests/Atm.Infrastructure.Tests/Persistence/ModelTestContext.cs @@ -0,0 +1,14 @@ +using Atm.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace Atm.Infrastructure.Tests.Persistence; + +internal static class ModelTestContext +{ + public static AtmDbContext Create() + { + return new AtmDbContext(new DbContextOptionsBuilder() + .UseNpgsql() + .Options); + } +} From 4ce7ffce42d1cbecc766f8c9027fd1c3a6c763f1 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 8 Oct 2026 16:19:26 +0300 Subject: [PATCH 014/103] chore: configure solution and projects --- .config/dotnet-tools.json | 13 - .editorconfig | 9 +- .gitattributes | 7 + .githooks/pre-commit | 22 + .gitignore | 4 +- Atm.sln | 161 ------- Atm.slnx | 24 + Directory.Build.props | 46 +- Directory.Packages.props | 36 ++ global.json | 7 - src/Atm.Api/Atm.Api.csproj | 2 + src/Atm.Api/Program.cs | 8 + .../Properties/launchSettings.json | 4 +- .../appsettings.Development.json | 0 .../appsettings.json | 7 - .../Atm.Application.Abstractions.csproj | 17 +- .../Repositories/IAccountRepository.cs | 15 - .../Repositories/IOperationRepository.cs | 11 - .../Repositories/ISessionRepository.cs | 10 - .../Repositories/ISystemPasswordProvider.cs | 6 - .../Security/IPinHasher.cs | 10 - .../Time/IClock.cs | 6 - .../packages.lock.json | 10 - .../CreateAccount/CreateAccountRequest.cs | 8 - .../CreateAccount/CreateAccountResult.cs | 12 - .../CreateAccount/ICreateAccountPort.cs | 6 - .../Accounts/GetBalance/GetBalanceRequest.cs | 3 - .../Accounts/GetBalance/GetBalanceResult.cs | 12 - .../Accounts/GetBalance/IGetBalancePort.cs | 6 - .../Atm.Application.Contracts.csproj | 8 +- .../Operations/Deposit/DepositRequest.cs | 3 - .../Operations/Deposit/DepositResult.cs | 12 - .../Operations/Deposit/IDepositPort.cs | 6 - .../GetTransactionHistoryRequest.cs | 3 - .../GetTransactionHistoryResult.cs | 12 - .../IGetTransactionHistoryPort.cs | 6 - .../GetTransactionHistory/TransactionDto.cs | 6 - .../Operations/Withdraw/IWithdrawPort.cs | 6 - .../Operations/Withdraw/WithdrawRequest.cs | 3 - .../Operations/Withdraw/WithdrawResult.cs | 12 - .../CreateAdminSessionRequest.cs | 3 - .../CreateAdminSessionResult.cs | 12 - .../ICreateAdminSessionPort.cs | 6 - .../CreateUserSessionRequest.cs | 3 - .../CreateUserSessionResult.cs | 12 - .../ICreateUserSessionPort.cs | 6 - .../packages.lock.json | 6 - .../Atm.Application.Models.csproj | 1 + .../Accounts/CreateAccountUseCase.cs | 66 --- .../Accounts/GetBalanceUseCase.cs | 31 -- src/Atm.Application/Atm.Application.csproj | 18 +- .../Common/AuthorizationResult.cs | 12 - .../Common/SessionAuthorizer.cs | 41 -- .../Operations/DepositUseCase.cs | 64 --- .../GetTransactionHistoryUseCase.cs | 47 -- .../Operations/WithdrawUseCase.cs | 63 --- .../Properties/AssemblyInfo.cs | 3 - .../ServiceCollectionExtensions.cs | 34 -- .../Sessions/CreateAdminSessionUseCase.cs | 62 --- .../Sessions/CreateUserSessionUseCase.cs | 67 --- .../Sessions/SessionOptions.cs | 10 - src/Atm.Application/packages.lock.json | 40 -- src/Atm.Domain/Accounts/Account.cs | 107 ----- .../Accounts/Results/AccountCreationResult.cs | 10 - .../Accounts/Results/DepositResult.cs | 12 - .../Accounts/Results/WithdrawResult.cs | 12 - src/Atm.Domain/Atm.Domain.csproj | 9 - src/Atm.Domain/Common/DomainRules.cs | 30 -- .../Operations/AccountHistoryEntry.cs | 84 ---- .../Operations/AccountHistoryEntryType.cs | 9 - src/Atm.Domain/Operations/BankTransaction.cs | 123 ----- .../Operations/BankTransactionType.cs | 8 - .../Operations/CommissionSnapshot.cs | 50 -- src/Atm.Domain/Operations/OperationRecord.cs | 30 -- src/Atm.Domain/Operations/OperationType.cs | 7 - .../AccountHistoryEntryCreationResult.cs | 10 - .../Results/BankTransactionCreationResult.cs | 10 - .../Sessions/Results/SessionCreationResult.cs | 10 - .../Results/SessionRevocationResult.cs | 10 - src/Atm.Domain/Sessions/Session.cs | 70 --- src/Atm.Domain/Sessions/SessionType.cs | 7 - src/Atm.Domain/Users/Friendship.cs | 32 -- src/Atm.Domain/Users/HairColor.cs | 13 - .../Users/Results/FriendshipCreationResult.cs | 10 - .../Users/Results/ProfileUpdateResult.cs | 10 - .../Users/Results/UserCreationResult.cs | 10 - src/Atm.Domain/Users/User.cs | 76 ---- src/Atm.Domain/Users/UserGender.cs | 9 - src/Atm.Domain/ValueObjects/AccountNumber.cs | 20 - src/Atm.Domain/ValueObjects/Amount.cs | 16 - src/Atm.Domain/ValueObjects/Money.cs | 28 -- src/Atm.Domain/ValueObjects/PinCode.cs | 13 - src/Atm.Domain/ValueObjects/PinHash.cs | 16 - src/Atm.Domain/ValueObjects/UserLogin.cs | 20 - src/Atm.Domain/packages.lock.json | 6 - .../Atm.Infrastructure.Persistence.csproj | 16 + .../Atm.Infrastructure.csproj | 29 -- .../Persistence/AtmDbContext.cs | 55 --- .../Configurations/AccountConfiguration.cs | 47 -- .../AccountHistoryEntryConfiguration.cs | 41 -- .../BankTransactionConfiguration.cs | 52 --- .../Persistence/Configurations/ColumnRules.cs | 40 -- .../Configurations/FriendshipConfiguration.cs | 26 -- .../Configurations/SessionConfiguration.cs | 32 -- .../Configurations/UserConfiguration.cs | 40 -- .../DesignTime/AtmDbContextFactory.cs | 35 -- .../Repositories/AccountRepository.cs | 31 -- .../Repositories/OperationRepository.cs | 30 -- .../Repositories/SessionRepository.cs | 24 - src/Atm.Infrastructure/Security/PinHasher.cs | 28 -- .../ServiceCollectionExtensions.cs | 23 - src/Atm.Infrastructure/Time/SystemClock.cs | 8 - src/Atm.Infrastructure/packages.lock.json | 366 --------------- src/Atm.Presentation/Atm.Presentation.csproj | 22 +- .../Common/SystemPasswordProvider.cs | 26 -- src/Atm.Presentation/Contracts/ApiRequests.cs | 22 - .../Contracts/ApiResponses.cs | 19 - .../Controllers/AccountsController.cs | 129 ------ .../Controllers/SessionsController.cs | 68 --- src/Atm.Presentation/Program.cs | 41 -- src/Atm.Presentation/packages.lock.json | 429 ------------------ .../AccountAuthenticationFlowTests.cs | 114 ----- .../Atm.Application.Tests.csproj | 26 -- .../Atm.Application.Tests/BankingTestData.cs | 29 -- .../DepositUseCaseTests.cs | 42 -- tests/Atm.Application.Tests/FixedClock.cs | 8 - .../SessionAuthorizerTests.cs | 33 -- .../WithdrawCaseTests.cs | 66 --- .../Atm.Application.Tests/packages.lock.json | 175 ------- tests/Atm.Domain.Tests/AccountTests.cs | 63 --- .../Atm.Domain.Tests/Atm.Domain.Tests.csproj | 25 - .../Atm.Domain.Tests/BankTransactionTests.cs | 177 -------- tests/Atm.Domain.Tests/DomainTestData.cs | 34 -- tests/Atm.Domain.Tests/SessionTests.cs | 58 --- tests/Atm.Domain.Tests/UserTests.cs | 75 --- tests/Atm.Domain.Tests/ValueObjectTests.cs | 80 ---- tests/Atm.Domain.Tests/packages.lock.json | 115 ----- .../Atm.Infrastructure.Tests.csproj | 25 - .../Persistence/AtmModelTests.cs | 117 ----- .../Persistence/DesignTimeFactoryTests.cs | 52 --- .../Persistence/FinancialWriteGuardTests.cs | 82 ---- .../Persistence/MaterializationTests.cs | 72 --- .../Persistence/ModelTestContext.cs | 14 - .../PinHasherTests.cs | 34 -- .../packages.lock.json | 279 ------------ 145 files changed, 189 insertions(+), 5338 deletions(-) delete mode 100644 .config/dotnet-tools.json create mode 100644 .gitattributes create mode 100644 .githooks/pre-commit delete mode 100644 Atm.sln create mode 100644 Atm.slnx create mode 100644 Directory.Packages.props delete mode 100644 global.json create mode 100644 src/Atm.Api/Atm.Api.csproj create mode 100644 src/Atm.Api/Program.cs rename src/{Atm.Presentation => Atm.Api}/Properties/launchSettings.json (80%) rename src/{Atm.Presentation => Atm.Api}/appsettings.Development.json (100%) rename src/{Atm.Presentation => Atm.Api}/appsettings.json (50%) delete mode 100644 src/Atm.Application.Abstractions/Repositories/IAccountRepository.cs delete mode 100644 src/Atm.Application.Abstractions/Repositories/IOperationRepository.cs delete mode 100644 src/Atm.Application.Abstractions/Repositories/ISessionRepository.cs delete mode 100644 src/Atm.Application.Abstractions/Repositories/ISystemPasswordProvider.cs delete mode 100644 src/Atm.Application.Abstractions/Security/IPinHasher.cs delete mode 100644 src/Atm.Application.Abstractions/Time/IClock.cs delete mode 100644 src/Atm.Application.Abstractions/packages.lock.json delete mode 100644 src/Atm.Application.Contracts/Accounts/CreateAccount/CreateAccountRequest.cs delete mode 100644 src/Atm.Application.Contracts/Accounts/CreateAccount/CreateAccountResult.cs delete mode 100644 src/Atm.Application.Contracts/Accounts/CreateAccount/ICreateAccountPort.cs delete mode 100644 src/Atm.Application.Contracts/Accounts/GetBalance/GetBalanceRequest.cs delete mode 100644 src/Atm.Application.Contracts/Accounts/GetBalance/GetBalanceResult.cs delete mode 100644 src/Atm.Application.Contracts/Accounts/GetBalance/IGetBalancePort.cs delete mode 100644 src/Atm.Application.Contracts/Operations/Deposit/DepositRequest.cs delete mode 100644 src/Atm.Application.Contracts/Operations/Deposit/DepositResult.cs delete mode 100644 src/Atm.Application.Contracts/Operations/Deposit/IDepositPort.cs delete mode 100644 src/Atm.Application.Contracts/Operations/GetTransactionHistory/GetTransactionHistoryRequest.cs delete mode 100644 src/Atm.Application.Contracts/Operations/GetTransactionHistory/GetTransactionHistoryResult.cs delete mode 100644 src/Atm.Application.Contracts/Operations/GetTransactionHistory/IGetTransactionHistoryPort.cs delete mode 100644 src/Atm.Application.Contracts/Operations/GetTransactionHistory/TransactionDto.cs delete mode 100644 src/Atm.Application.Contracts/Operations/Withdraw/IWithdrawPort.cs delete mode 100644 src/Atm.Application.Contracts/Operations/Withdraw/WithdrawRequest.cs delete mode 100644 src/Atm.Application.Contracts/Operations/Withdraw/WithdrawResult.cs delete mode 100644 src/Atm.Application.Contracts/Sessions/CreateAdminSession/CreateAdminSessionRequest.cs delete mode 100644 src/Atm.Application.Contracts/Sessions/CreateAdminSession/CreateAdminSessionResult.cs delete mode 100644 src/Atm.Application.Contracts/Sessions/CreateAdminSession/ICreateAdminSessionPort.cs delete mode 100644 src/Atm.Application.Contracts/Sessions/CreateUserSession/CreateUserSessionRequest.cs delete mode 100644 src/Atm.Application.Contracts/Sessions/CreateUserSession/CreateUserSessionResult.cs delete mode 100644 src/Atm.Application.Contracts/Sessions/CreateUserSession/ICreateUserSessionPort.cs delete mode 100644 src/Atm.Application.Contracts/packages.lock.json create mode 100644 src/Atm.Application.Models/Atm.Application.Models.csproj delete mode 100644 src/Atm.Application/Accounts/CreateAccountUseCase.cs delete mode 100644 src/Atm.Application/Accounts/GetBalanceUseCase.cs delete mode 100644 src/Atm.Application/Common/AuthorizationResult.cs delete mode 100644 src/Atm.Application/Common/SessionAuthorizer.cs delete mode 100644 src/Atm.Application/Operations/DepositUseCase.cs delete mode 100644 src/Atm.Application/Operations/GetTransactionHistoryUseCase.cs delete mode 100644 src/Atm.Application/Operations/WithdrawUseCase.cs delete mode 100644 src/Atm.Application/Properties/AssemblyInfo.cs delete mode 100644 src/Atm.Application/ServiceCollectionExtensions.cs delete mode 100644 src/Atm.Application/Sessions/CreateAdminSessionUseCase.cs delete mode 100644 src/Atm.Application/Sessions/CreateUserSessionUseCase.cs delete mode 100644 src/Atm.Application/Sessions/SessionOptions.cs delete mode 100644 src/Atm.Application/packages.lock.json delete mode 100644 src/Atm.Domain/Accounts/Account.cs delete mode 100644 src/Atm.Domain/Accounts/Results/AccountCreationResult.cs delete mode 100644 src/Atm.Domain/Accounts/Results/DepositResult.cs delete mode 100644 src/Atm.Domain/Accounts/Results/WithdrawResult.cs delete mode 100644 src/Atm.Domain/Atm.Domain.csproj delete mode 100644 src/Atm.Domain/Common/DomainRules.cs delete mode 100644 src/Atm.Domain/Operations/AccountHistoryEntry.cs delete mode 100644 src/Atm.Domain/Operations/AccountHistoryEntryType.cs delete mode 100644 src/Atm.Domain/Operations/BankTransaction.cs delete mode 100644 src/Atm.Domain/Operations/BankTransactionType.cs delete mode 100644 src/Atm.Domain/Operations/CommissionSnapshot.cs delete mode 100644 src/Atm.Domain/Operations/OperationRecord.cs delete mode 100644 src/Atm.Domain/Operations/OperationType.cs delete mode 100644 src/Atm.Domain/Operations/Results/AccountHistoryEntryCreationResult.cs delete mode 100644 src/Atm.Domain/Operations/Results/BankTransactionCreationResult.cs delete mode 100644 src/Atm.Domain/Sessions/Results/SessionCreationResult.cs delete mode 100644 src/Atm.Domain/Sessions/Results/SessionRevocationResult.cs delete mode 100644 src/Atm.Domain/Sessions/Session.cs delete mode 100644 src/Atm.Domain/Sessions/SessionType.cs delete mode 100644 src/Atm.Domain/Users/Friendship.cs delete mode 100644 src/Atm.Domain/Users/HairColor.cs delete mode 100644 src/Atm.Domain/Users/Results/FriendshipCreationResult.cs delete mode 100644 src/Atm.Domain/Users/Results/ProfileUpdateResult.cs delete mode 100644 src/Atm.Domain/Users/Results/UserCreationResult.cs delete mode 100644 src/Atm.Domain/Users/User.cs delete mode 100644 src/Atm.Domain/Users/UserGender.cs delete mode 100644 src/Atm.Domain/ValueObjects/AccountNumber.cs delete mode 100644 src/Atm.Domain/ValueObjects/Amount.cs delete mode 100644 src/Atm.Domain/ValueObjects/Money.cs delete mode 100644 src/Atm.Domain/ValueObjects/PinCode.cs delete mode 100644 src/Atm.Domain/ValueObjects/PinHash.cs delete mode 100644 src/Atm.Domain/ValueObjects/UserLogin.cs delete mode 100644 src/Atm.Domain/packages.lock.json create mode 100644 src/Atm.Infrastructure.Persistence/Atm.Infrastructure.Persistence.csproj delete mode 100644 src/Atm.Infrastructure/Atm.Infrastructure.csproj delete mode 100644 src/Atm.Infrastructure/Persistence/AtmDbContext.cs delete mode 100644 src/Atm.Infrastructure/Persistence/Configurations/AccountConfiguration.cs delete mode 100644 src/Atm.Infrastructure/Persistence/Configurations/AccountHistoryEntryConfiguration.cs delete mode 100644 src/Atm.Infrastructure/Persistence/Configurations/BankTransactionConfiguration.cs delete mode 100644 src/Atm.Infrastructure/Persistence/Configurations/ColumnRules.cs delete mode 100644 src/Atm.Infrastructure/Persistence/Configurations/FriendshipConfiguration.cs delete mode 100644 src/Atm.Infrastructure/Persistence/Configurations/SessionConfiguration.cs delete mode 100644 src/Atm.Infrastructure/Persistence/Configurations/UserConfiguration.cs delete mode 100644 src/Atm.Infrastructure/Persistence/DesignTime/AtmDbContextFactory.cs delete mode 100644 src/Atm.Infrastructure/Repositories/AccountRepository.cs delete mode 100644 src/Atm.Infrastructure/Repositories/OperationRepository.cs delete mode 100644 src/Atm.Infrastructure/Repositories/SessionRepository.cs delete mode 100644 src/Atm.Infrastructure/Security/PinHasher.cs delete mode 100644 src/Atm.Infrastructure/ServiceCollectionExtensions.cs delete mode 100644 src/Atm.Infrastructure/Time/SystemClock.cs delete mode 100644 src/Atm.Infrastructure/packages.lock.json delete mode 100644 src/Atm.Presentation/Common/SystemPasswordProvider.cs delete mode 100644 src/Atm.Presentation/Contracts/ApiRequests.cs delete mode 100644 src/Atm.Presentation/Contracts/ApiResponses.cs delete mode 100644 src/Atm.Presentation/Controllers/AccountsController.cs delete mode 100644 src/Atm.Presentation/Controllers/SessionsController.cs delete mode 100644 src/Atm.Presentation/packages.lock.json delete mode 100644 tests/Atm.Application.Tests/AccountAuthenticationFlowTests.cs delete mode 100644 tests/Atm.Application.Tests/Atm.Application.Tests.csproj delete mode 100644 tests/Atm.Application.Tests/BankingTestData.cs delete mode 100644 tests/Atm.Application.Tests/DepositUseCaseTests.cs delete mode 100644 tests/Atm.Application.Tests/FixedClock.cs delete mode 100644 tests/Atm.Application.Tests/SessionAuthorizerTests.cs delete mode 100644 tests/Atm.Application.Tests/WithdrawCaseTests.cs delete mode 100644 tests/Atm.Application.Tests/packages.lock.json delete mode 100644 tests/Atm.Domain.Tests/AccountTests.cs delete mode 100644 tests/Atm.Domain.Tests/Atm.Domain.Tests.csproj delete mode 100644 tests/Atm.Domain.Tests/BankTransactionTests.cs delete mode 100644 tests/Atm.Domain.Tests/DomainTestData.cs delete mode 100644 tests/Atm.Domain.Tests/SessionTests.cs delete mode 100644 tests/Atm.Domain.Tests/UserTests.cs delete mode 100644 tests/Atm.Domain.Tests/ValueObjectTests.cs delete mode 100644 tests/Atm.Domain.Tests/packages.lock.json delete mode 100644 tests/Atm.Infrastructure.Tests/Atm.Infrastructure.Tests.csproj delete mode 100644 tests/Atm.Infrastructure.Tests/Persistence/AtmModelTests.cs delete mode 100644 tests/Atm.Infrastructure.Tests/Persistence/DesignTimeFactoryTests.cs delete mode 100644 tests/Atm.Infrastructure.Tests/Persistence/FinancialWriteGuardTests.cs delete mode 100644 tests/Atm.Infrastructure.Tests/Persistence/MaterializationTests.cs delete mode 100644 tests/Atm.Infrastructure.Tests/Persistence/ModelTestContext.cs delete mode 100644 tests/Atm.Infrastructure.Tests/PinHasherTests.cs delete mode 100644 tests/Atm.Infrastructure.Tests/packages.lock.json diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json deleted file mode 100644 index e5d8851..0000000 --- a/.config/dotnet-tools.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "dotnet-ef": { - "version": "9.0.19", - "commands": [ - "dotnet-ef" - ], - "rollForward": false - } - } -} \ No newline at end of file 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 d015d3a..0000000 --- a/Atm.sln +++ /dev/null @@ -1,161 +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 -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 - 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 - {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 - 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} - {D9ACF0B4-7830-4D65-96E2-49EC72B8DD3A} = {0AB3BF05-4346-4AA6-1389-037BE0695223} - {F2E5698E-1037-4F49-A653-1A7D60B909E4} = {0AB3BF05-4346-4AA6-1389-037BE0695223} - EndGlobalSection -EndGlobal diff --git a/Atm.slnx b/Atm.slnx new file mode 100644 index 0000000..bc767d3 --- /dev/null +++ b/Atm.slnx @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 85cf783..4989915 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,8 +1,42 @@ - - true - true - latest - true - + + 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..671bea8 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,36 @@ + + + true + false + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/global.json b/global.json deleted file mode 100644 index bf1f678..0000000 --- a/global.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "sdk": { - "version": "9.0.100", - "rollForward": "latestFeature", - "allowPrerelease": false - } -} \ No newline at end of file diff --git a/src/Atm.Api/Atm.Api.csproj b/src/Atm.Api/Atm.Api.csproj new file mode 100644 index 0000000..edbd018 --- /dev/null +++ b/src/Atm.Api/Atm.Api.csproj @@ -0,0 +1,2 @@ + + \ 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..c9baf2f --- /dev/null +++ b/src/Atm.Api/Program.cs @@ -0,0 +1,8 @@ +namespace Atm.Api; + +public class Program +{ + public static void Main() + { + } +} \ 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.Development.json b/src/Atm.Api/appsettings.Development.json similarity index 100% rename from src/Atm.Presentation/appsettings.Development.json rename to src/Atm.Api/appsettings.Development.json diff --git a/src/Atm.Presentation/appsettings.json b/src/Atm.Api/appsettings.json similarity index 50% rename from src/Atm.Presentation/appsettings.json rename to src/Atm.Api/appsettings.json index 27151bb..10f68b8 100644 --- a/src/Atm.Presentation/appsettings.json +++ b/src/Atm.Api/appsettings.json @@ -1,11 +1,4 @@ { - "SystemAuth": { - "SystemPassword": "change_me" - }, - "Sessions": { - "UserLifetimeMinutes": 30, - "AdminLifetimeMinutes": 15 - }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/src/Atm.Application.Abstractions/Atm.Application.Abstractions.csproj b/src/Atm.Application.Abstractions/Atm.Application.Abstractions.csproj index 2d25bf4..670ff57 100644 --- a/src/Atm.Application.Abstractions/Atm.Application.Abstractions.csproj +++ b/src/Atm.Application.Abstractions/Atm.Application.Abstractions.csproj @@ -1,13 +1,12 @@  - - - - - - net9.0 - enable - enable + Atm.Application.Abstractions - + + + + + + + diff --git a/src/Atm.Application.Abstractions/Repositories/IAccountRepository.cs b/src/Atm.Application.Abstractions/Repositories/IAccountRepository.cs deleted file mode 100644 index 4770a4b..0000000 --- a/src/Atm.Application.Abstractions/Repositories/IAccountRepository.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Atm.Domain.Accounts; -using Atm.Domain.ValueObjects; - -namespace Atm.Application.Abstractions.Repositories; - -public interface IAccountRepository -{ - Account? GetById(Guid id); - - Account? GetByNumber(AccountNumber accountNumber); - - void Add(Account account); - - void Update(Account account); -} 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/Security/IPinHasher.cs b/src/Atm.Application.Abstractions/Security/IPinHasher.cs deleted file mode 100644 index 0131331..0000000 --- a/src/Atm.Application.Abstractions/Security/IPinHasher.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Atm.Domain.ValueObjects; - -namespace Atm.Application.Abstractions.Security; - -public interface IPinHasher -{ - PinHash Hash(PinCode pin); - - bool Verify(PinHash hash, PinCode pin); -} diff --git a/src/Atm.Application.Abstractions/Time/IClock.cs b/src/Atm.Application.Abstractions/Time/IClock.cs deleted file mode 100644 index c9b9680..0000000 --- a/src/Atm.Application.Abstractions/Time/IClock.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Atm.Application.Abstractions.Time; - -public interface IClock -{ - DateTimeOffset UtcNow { get; } -} 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 60b1fac..0000000 --- a/src/Atm.Application.Contracts/Accounts/CreateAccount/CreateAccountRequest.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Atm.Application.Contracts.Accounts.CreateAccount; - -public sealed record CreateAccountRequest( - Guid AdminSessionId, - string AccountNumber, - string Pin, - decimal InitialBalance, - Guid OwnerUserId); 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/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/packages.lock.json b/src/Atm.Application.Contracts/packages.lock.json deleted file mode 100644 index a07e21a..0000000 --- a/src/Atm.Application.Contracts/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.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/Accounts/CreateAccountUseCase.cs b/src/Atm.Application/Accounts/CreateAccountUseCase.cs deleted file mode 100644 index ceeab30..0000000 --- a/src/Atm.Application/Accounts/CreateAccountUseCase.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Atm.Application.Abstractions.Repositories; -using Atm.Application.Abstractions.Security; -using Atm.Application.Abstractions.Time; -using Atm.Application.Common; -using Atm.Application.Contracts.Accounts.CreateAccount; -using Atm.Domain.Accounts; -using Atm.Domain.Accounts.Results; -using Atm.Domain.ValueObjects; - -namespace Atm.Application.Accounts; - -internal sealed class CreateAccountUseCase : ICreateAccountPort -{ - private readonly IAccountRepository _accounts; - private readonly SessionAuthorizer _auth; - private readonly IPinHasher _pinHasher; - private readonly IClock _clock; - - public CreateAccountUseCase( - IAccountRepository accounts, - SessionAuthorizer auth, - IPinHasher pinHasher, - IClock clock) - { - _accounts = accounts; - _auth = auth; - _pinHasher = pinHasher; - _clock = clock; - } - - public CreateAccountResult Execute(CreateAccountRequest request) - { - AuthorizationResult auth = _auth.AuthorizeAdmin(request.AdminSessionId); - if (auth is AuthorizationResult.Unauthorized unauthorized) - return new CreateAccountResult.Unauthorized(unauthorized.ErrorMessage); - - AccountNumber number; - PinCode pin; - Money balance; - try - { - number = new AccountNumber(request.AccountNumber); - pin = new PinCode(request.Pin); - balance = new Money(request.InitialBalance); - } - catch (ArgumentException exception) - { - return new CreateAccountResult.Failure(exception.Message); - } - - if (request.OwnerUserId == Guid.Empty) - return new CreateAccountResult.Failure("Owner ID must not be empty."); - if (_accounts.GetByNumber(number) is not null) - return new CreateAccountResult.Failure("Account number already exists."); - - AccountCreationResult result = Account.Create( - Guid.NewGuid(), number, request.OwnerUserId, - _pinHasher.Hash(pin), balance, _clock.UtcNow); - - if (result is AccountCreationResult.Failure failure) - return new CreateAccountResult.Failure(failure.ErrorMessage); - - _accounts.Add(((AccountCreationResult.Success)result).Account); - return new CreateAccountResult.Success(); - } -} diff --git a/src/Atm.Application/Accounts/GetBalanceUseCase.cs b/src/Atm.Application/Accounts/GetBalanceUseCase.cs deleted file mode 100644 index d229c0f..0000000 --- a/src/Atm.Application/Accounts/GetBalanceUseCase.cs +++ /dev/null @@ -1,31 +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 unauthorized) - return new GetBalanceResult.Unauthorized(unauthorized.ErrorMessage); - - var user = (AuthorizationResult.UserSuccess)auth; - Domain.Accounts.Account? account = _accounts.GetById(user.AccountId); - if (account is null) - return new GetBalanceResult.Failure("Account not found"); - - return new GetBalanceResult.Success(account.Balance.Value); - } -} diff --git a/src/Atm.Application/Atm.Application.csproj b/src/Atm.Application/Atm.Application.csproj index 88aff46..77a6187 100644 --- a/src/Atm.Application/Atm.Application.csproj +++ b/src/Atm.Application/Atm.Application.csproj @@ -1,20 +1,14 @@  - + + Atm.HighLoad.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 a7501ec..0000000 --- a/src/Atm.Application/Common/AuthorizationResult.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Atm.Application.Common; - -public abstract record AuthorizationResult -{ - private AuthorizationResult() { } - - public sealed record UserSuccess(Guid AccountId) : AuthorizationResult; - - public sealed record AdminSuccess : AuthorizationResult; - - public sealed record Unauthorized(string ErrorMessage) : AuthorizationResult; -} diff --git a/src/Atm.Application/Common/SessionAuthorizer.cs b/src/Atm.Application/Common/SessionAuthorizer.cs deleted file mode 100644 index 9a06463..0000000 --- a/src/Atm.Application/Common/SessionAuthorizer.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Atm.Application.Abstractions.Repositories; -using Atm.Application.Abstractions.Time; -using Atm.Domain.Sessions; - -namespace Atm.Application.Common; - -public sealed class SessionAuthorizer -{ - private readonly ISessionRepository _sessions; - private readonly IClock _clock; - - public SessionAuthorizer(ISessionRepository sessions, IClock clock) - { - _sessions = sessions; - _clock = clock; - } - - public AuthorizationResult AuthorizeUser(Guid sessionId) - { - Session? session = _sessions.GetById(sessionId); - if (session is null || !session.IsActive(_clock.UtcNow)) - return new AuthorizationResult.Unauthorized("Session is missing, expired or revoked."); - if (session.Type != SessionType.User) - return new AuthorizationResult.Unauthorized("Session type not supported [user session required]"); - if (session.AccountId is not Guid accountId) - return new AuthorizationResult.Unauthorized("Account is not set."); - - return new AuthorizationResult.UserSuccess(accountId); - } - - public AuthorizationResult AuthorizeAdmin(Guid sessionId) - { - Session? session = _sessions.GetById(sessionId); - if (session is null || !session.IsActive(_clock.UtcNow)) - return new AuthorizationResult.Unauthorized("Session is missing, expired or revoked."); - if (session.Type != SessionType.Admin) - return new AuthorizationResult.Unauthorized("Session type not supported [admin session required]"); - - return new AuthorizationResult.AdminSuccess(); - } -} diff --git a/src/Atm.Application/Operations/DepositUseCase.cs b/src/Atm.Application/Operations/DepositUseCase.cs deleted file mode 100644 index c290f07..0000000 --- a/src/Atm.Application/Operations/DepositUseCase.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Atm.Application.Abstractions.Repositories; -using Atm.Application.Abstractions.Time; -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; - private readonly IClock _clock; - - public DepositUseCase(IAccountRepository accounts, IOperationRepository operations, SessionAuthorizer auth, IClock clock) - { - _accounts = accounts; - _operations = operations; - _auth = auth; - _clock = clock; - } - - 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.GetById(user.AccountId); - - if (acc is null) - { - return new DepositResult.Failure("Account not found"); - } - - Amount amount; - try - { - amount = new Amount(request.Amount); - } - catch (ArgumentException e) - { - return new DepositResult.Failure(e.Message); - } - - Domain.Accounts.Results.DepositResult domainResult = acc.Deposit(amount, _clock.UtcNow); - - if (domainResult is Domain.Accounts.Results.DepositResult.Failure f) - { - return new DepositResult.Failure(f.ErrorMessage); - } - - _accounts.Update(acc); - _operations.Add(new OperationRecord(Guid.NewGuid(), acc.Number, OperationType.Deposit, amount, acc.Balance)); - - return new DepositResult.Success(acc.Balance.Value); - } -} diff --git a/src/Atm.Application/Operations/GetTransactionHistoryUseCase.cs b/src/Atm.Application/Operations/GetTransactionHistoryUseCase.cs deleted file mode 100644 index 34ebf12..0000000 --- a/src/Atm.Application/Operations/GetTransactionHistoryUseCase.cs +++ /dev/null @@ -1,47 +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 IAccountRepository _accounts; - private readonly SessionAuthorizer _auth; - - public GetTransactionHistoryUseCase( - IOperationRepository operations, - IAccountRepository accounts, - SessionAuthorizer auth) - { - _operations = operations; - _accounts = accounts; - _auth = auth; - } - - public GetTransactionHistoryResult Execute(GetTransactionHistoryRequest request) - { - AuthorizationResult auth = _auth.AuthorizeUser(request.UserSessionId); - if (auth is AuthorizationResult.Unauthorized unauthorized) - return new GetTransactionHistoryResult.Unauthorized(unauthorized.ErrorMessage); - - var user = (AuthorizationResult.UserSuccess)auth; - Domain.Accounts.Account? account = _accounts.GetById(user.AccountId); - if (account is null) - return new GetTransactionHistoryResult.Failure("Account not found"); - - IReadOnlyList operations = - _operations.GetByAccountNumber(account.Number); - var transactions = new List(operations.Count); - foreach (Domain.Operations.OperationRecord operation in operations) - { - transactions.Add(new TransactionDto( - operation.Type.ToString(), - operation.Amount.Value, - operation.BalanceAfter.Value)); - } - - return new GetTransactionHistoryResult.Success(transactions); - } -} diff --git a/src/Atm.Application/Operations/WithdrawUseCase.cs b/src/Atm.Application/Operations/WithdrawUseCase.cs deleted file mode 100644 index 607c44c..0000000 --- a/src/Atm.Application/Operations/WithdrawUseCase.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Atm.Application.Abstractions.Repositories; -using Atm.Application.Abstractions.Time; -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; - private readonly IClock _clock; - - public WithdrawUseCase(IAccountRepository accounts, IOperationRepository operations, SessionAuthorizer auth, IClock clock) - { - _accounts = accounts; - _operations = operations; - _auth = auth; - _clock = clock; - } - - 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.GetById(user.AccountId); - - if (acc is null) - { - return new WithdrawResult.Failure("Account not found"); - } - - Amount amount; - try - { - amount = new Amount(request.Amount); - } - catch (ArgumentException e) - { - return new WithdrawResult.Failure(e.Message); - } - - Domain.Accounts.Results.WithdrawResult domainResult = acc.Withdraw(amount, _clock.UtcNow); - if (domainResult is Domain.Accounts.Results.WithdrawResult.Failure result) - { - return new WithdrawResult.Failure(result.ErrorMessage); - } - - _accounts.Update(acc); - _operations.Add(new OperationRecord(Guid.NewGuid(), acc.Number, OperationType.Withdraw, amount, acc.Balance)); - - return new WithdrawResult.Success(acc.Balance.Value); - } -} diff --git a/src/Atm.Application/Properties/AssemblyInfo.cs b/src/Atm.Application/Properties/AssemblyInfo.cs deleted file mode 100644 index a1a4d7e..0000000 --- a/src/Atm.Application/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,3 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Atm.Application.Tests")] diff --git a/src/Atm.Application/ServiceCollectionExtensions.cs b/src/Atm.Application/ServiceCollectionExtensions.cs deleted file mode 100644 index 381b342..0000000 --- a/src/Atm.Application/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -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 Microsoft.Extensions.DependencyInjection; - -namespace Atm.Application; - -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddApplication(this IServiceCollection services) - { - services.AddSingleton(); - - services.AddTransient(); - services.AddTransient(); - - services.AddTransient(); - services.AddTransient(); - - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - return services; - } -} \ 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 a8768df..0000000 --- a/src/Atm.Application/Sessions/CreateAdminSessionUseCase.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Atm.Application.Abstractions.Repositories; -using Atm.Application.Abstractions.Time; -using Atm.Application.Contracts.Sessions.CreateAdminSession; -using Atm.Domain.Sessions; -using Atm.Domain.Sessions.Results; -using Atm.Domain.ValueObjects; -using Microsoft.Extensions.Options; - -namespace Atm.Application.Sessions; - -internal sealed class CreateAdminSessionUseCase : ICreateAdminSessionPort -{ - private readonly ISessionRepository _sessions; - private readonly ISystemPasswordProvider _passwords; - private readonly IClock _clock; - private readonly SessionOptions _options; - - public CreateAdminSessionUseCase( - ISessionRepository sessions, - ISystemPasswordProvider passwords, - IClock clock, - IOptions options) - { - _sessions = sessions; - _passwords = passwords; - _clock = clock; - _options = options.Value; - } - - public CreateAdminSessionResult Execute(CreateAdminSessionRequest request) - { - string expected = _passwords.GetSystemPassword(); - if (string.IsNullOrWhiteSpace(expected)) - return new CreateAdminSessionResult.Failure("System password is not configured"); - if (!string.Equals(request.SystemPassword, expected, StringComparison.Ordinal)) - return new CreateAdminSessionResult.Unauthorized("Invalid system password"); - - try - { - _ = new AccountNumber(request.AccountNumber); - } - catch (ArgumentException exception) - { - return new CreateAdminSessionResult.Failure(exception.Message); - } - - DateTimeOffset createdAtUtc = _clock.UtcNow; - SessionCreationResult result = Session.Create( - Guid.NewGuid(), - SessionType.Admin, - null, - createdAtUtc, - createdAtUtc.AddMinutes(_options.AdminLifetimeMinutes)); - - if (result is SessionCreationResult.Failure failure) - return new CreateAdminSessionResult.Failure(failure.ErrorMessage); - - Session session = ((SessionCreationResult.Success)result).Session; - _sessions.Add(session); - return new CreateAdminSessionResult.Success(session.Id); - } -} diff --git a/src/Atm.Application/Sessions/CreateUserSessionUseCase.cs b/src/Atm.Application/Sessions/CreateUserSessionUseCase.cs deleted file mode 100644 index 68937a3..0000000 --- a/src/Atm.Application/Sessions/CreateUserSessionUseCase.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Atm.Application.Abstractions.Repositories; -using Atm.Application.Abstractions.Security; -using Atm.Application.Abstractions.Time; -using Atm.Application.Contracts.Sessions.CreateUserSession; -using Atm.Domain.Sessions; -using Atm.Domain.Sessions.Results; -using Atm.Domain.ValueObjects; -using Microsoft.Extensions.Options; - -namespace Atm.Application.Sessions; - -internal sealed class CreateUserSessionUseCase : ICreateUserSessionPort -{ - private readonly ISessionRepository _sessions; - private readonly IAccountRepository _accounts; - private readonly IPinHasher _pinHasher; - private readonly IClock _clock; - private readonly SessionOptions _options; - - public CreateUserSessionUseCase( - ISessionRepository sessions, - IAccountRepository accounts, - IPinHasher pinHasher, - IClock clock, - IOptions options) - { - _sessions = sessions; - _accounts = accounts; - _pinHasher = pinHasher; - _clock = clock; - _options = options.Value; - } - - public CreateUserSessionResult Execute(CreateUserSessionRequest request) - { - AccountNumber number; - PinCode pin; - try - { - number = new AccountNumber(request.AccountNumber); - pin = new PinCode(request.Pin); - } - catch (ArgumentException exception) - { - return new CreateUserSessionResult.Failure(exception.Message); - } - - Domain.Accounts.Account? account = _accounts.GetByNumber(number); - if (account is null || !_pinHasher.Verify(account.PinHash, pin)) - return new CreateUserSessionResult.Unauthorized("Invalid credentials"); - - DateTimeOffset createdAtUtc = _clock.UtcNow; - SessionCreationResult result = Session.Create( - Guid.NewGuid(), - SessionType.User, - account.Id, - createdAtUtc, - createdAtUtc.AddMinutes(_options.UserLifetimeMinutes)); - - if (result is SessionCreationResult.Failure failure) - return new CreateUserSessionResult.Failure(failure.ErrorMessage); - - Session session = ((SessionCreationResult.Success)result).Session; - _sessions.Add(session); - return new CreateUserSessionResult.Success(session.Id); - } -} diff --git a/src/Atm.Application/Sessions/SessionOptions.cs b/src/Atm.Application/Sessions/SessionOptions.cs deleted file mode 100644 index 1ed0139..0000000 --- a/src/Atm.Application/Sessions/SessionOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Atm.Application.Sessions; - -public sealed class SessionOptions -{ - public const string SectionName = "Sessions"; - - public int UserLifetimeMinutes { get; set; } - - public int AdminLifetimeMinutes { get; set; } -} diff --git a/src/Atm.Application/packages.lock.json b/src/Atm.Application/packages.lock.json deleted file mode 100644 index b7879a5..0000000 --- a/src/Atm.Application/packages.lock.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "version": 1, - "dependencies": { - "net9.0": { - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Direct", - "requested": "[9.0.19, )", - "resolved": "9.0.19", - "contentHash": "XgQ0aVWClYjYSBqbYrKC/xyZ1KHf+VVwNF3+d54jcaqnst30Q//ugs/FJwUztLZiph0gp85g7i/eB0556twO0Q==" - }, - "Microsoft.Extensions.Options": { - "type": "Direct", - "requested": "[9.0.19, )", - "resolved": "9.0.19", - "contentHash": "FdPZy4kUSwFlIVZ3EkK9fdHw2RfblUwzflSb4kjVeGf9v2lX9IH+q2RGUovYMR7CVfKMc2y2Db/bZ2b4yiMfOg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19", - "Microsoft.Extensions.Primitives": "9.0.19" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "9hIg8PQiMnpVFIsEHm25Wi1gBrPa2pS1G75uveyDUVLXrNKqBap9WGwQIgO0s6LUgRAOdsSYnbKbNC5b1G5Pcg==" - }, - "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 120defa..0000000 --- a/src/Atm.Domain/Accounts/Account.cs +++ /dev/null @@ -1,107 +0,0 @@ -using Atm.Domain.Accounts.Results; -using Atm.Domain.Common; -using Atm.Domain.ValueObjects; - -namespace Atm.Domain.Accounts; - -public sealed class Account -{ - private Account() { } - - public Guid Id { get; private set; } - public AccountNumber Number { get; private set; } = null!; - public Guid OwnerUserId { get; private set; } - public PinHash PinHash { get; private set; } = null!; - public Money Balance { get; private set; } = null!; - public long Version { get; private set; } - public DateTimeOffset CreatedAtUtc { get; private set; } - public DateTimeOffset UpdatedAtUtc { get; private set; } - - public static AccountCreationResult Create( - Guid id, - AccountNumber? number, - Guid ownerUserId, - PinHash? pinHash, - Money? balance, - DateTimeOffset createdAtUtc) - { - if (id == Guid.Empty || ownerUserId == Guid.Empty) - return new AccountCreationResult.Failure("Account and owner IDs must not be empty."); - if (number is null || pinHash is null || balance is null) - return new AccountCreationResult.Failure("Number, PIN hash and balance are required."); - if (!DomainRules.IsUtc(createdAtUtc)) - return new AccountCreationResult.Failure("Account timestamps must be UTC."); - - return new AccountCreationResult.Success(new Account - { - Id = id, - Number = number, - OwnerUserId = ownerUserId, - PinHash = pinHash, - Balance = balance, - Version = 0, - CreatedAtUtc = createdAtUtc, - UpdatedAtUtc = createdAtUtc, - }); - } - - public DepositResult Deposit(Amount? amount, DateTimeOffset updatedAtUtc) - { - string? error = ValidateChange(amount, updatedAtUtc); - if (error is not null) - return new DepositResult.Failure(error); - - Money balance; - try - { - balance = Balance.Add(amount!); - } - catch (OverflowException) - { - return new DepositResult.Failure("The balance cannot be represented exactly as decimal."); - } - - ApplyBalance(balance, updatedAtUtc); - return new DepositResult.Success(Balance); - } - - public WithdrawResult Withdraw(Amount? amount, DateTimeOffset updatedAtUtc) - { - string? error = ValidateChange(amount, updatedAtUtc); - if (error is not null) - return new WithdrawResult.Failure(error); - if (Balance.Value < amount!.Value) - return new WithdrawResult.Failure("Not enough balance."); - - Money balance; - try - { - balance = Balance.Subtract(amount); - } - catch (OverflowException) - { - return new WithdrawResult.Failure("The balance cannot be represented exactly as decimal."); - } - - ApplyBalance(balance, updatedAtUtc); - return new WithdrawResult.Success(Balance); - } - - private string? ValidateChange(Amount? amount, DateTimeOffset updatedAtUtc) - { - if (amount is null) - return "Amount is required."; - if (!DomainRules.IsUtc(updatedAtUtc) || updatedAtUtc < UpdatedAtUtc) - return "Update time must be UTC and must not precede the previous update."; - if (Version == long.MaxValue) - return "Account version is exhausted."; - return null; - } - - private void ApplyBalance(Money balance, DateTimeOffset updatedAtUtc) - { - Balance = balance; - Version++; - UpdatedAtUtc = updatedAtUtc; - } -} diff --git a/src/Atm.Domain/Accounts/Results/AccountCreationResult.cs b/src/Atm.Domain/Accounts/Results/AccountCreationResult.cs deleted file mode 100644 index e23b0c9..0000000 --- a/src/Atm.Domain/Accounts/Results/AccountCreationResult.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Atm.Domain.Accounts.Results; - -public abstract record AccountCreationResult -{ - private AccountCreationResult() { } - - public sealed record Success(Account Account) : AccountCreationResult; - - public sealed record Failure(string ErrorMessage) : AccountCreationResult; -} 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/Accounts/Results/WithdrawResult.cs b/src/Atm.Domain/Accounts/Results/WithdrawResult.cs deleted file mode 100644 index 165333f..0000000 --- a/src/Atm.Domain/Accounts/Results/WithdrawResult.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Atm.Domain.ValueObjects; - -namespace Atm.Domain.Accounts.Results; - -public abstract record WithdrawResult -{ - private WithdrawResult() { } - - public sealed record Success(Money BalanceAfter) : WithdrawResult; - - public sealed record Failure(string ErrorMessage) : WithdrawResult; -} \ 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/Common/DomainRules.cs b/src/Atm.Domain/Common/DomainRules.cs deleted file mode 100644 index e5ffa74..0000000 --- a/src/Atm.Domain/Common/DomainRules.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Atm.Domain.Common; - -internal static class DomainRules -{ - public static bool IsUtc(DateTimeOffset value) - { - return value.Offset == TimeSpan.Zero; - } - - public static bool HasScale(decimal value, int scale) - { - return decimal.Round(value, scale) == value; - } - - public static decimal AddExact(decimal left, decimal right) - { - decimal result = checked(left + right); - if (result - left != right || result - right != left) - throw new OverflowException("The amount cannot be represented exactly as decimal."); - return result; - } - - public static decimal SubtractExact(decimal left, decimal right) - { - decimal result = checked(left - right); - if (left - result != right || result + right != left) - throw new OverflowException("The amount cannot be represented exactly as decimal."); - return result; - } -} diff --git a/src/Atm.Domain/Operations/AccountHistoryEntry.cs b/src/Atm.Domain/Operations/AccountHistoryEntry.cs deleted file mode 100644 index d033777..0000000 --- a/src/Atm.Domain/Operations/AccountHistoryEntry.cs +++ /dev/null @@ -1,84 +0,0 @@ -using Atm.Domain.Operations.Results; -using Atm.Domain.ValueObjects; - -namespace Atm.Domain.Operations; - -public sealed class AccountHistoryEntry -{ - private AccountHistoryEntry() { } - - public Guid Id { get; private set; } - - public Guid TransactionId { get; private set; } - - public Guid AccountId { get; private set; } - - public AccountHistoryEntryType EntryType { get; private set; } - - public Money BalanceBefore { get; private set; } = null!; - - public Money BalanceAfter { get; private set; } = null!; - - public DateTimeOffset CreatedAtUtc { get; private set; } - - public static AccountHistoryEntryCreationResult Create( - Guid id, - BankTransaction? transaction, - Guid accountId, - Money? balanceBefore) - { - if (id == Guid.Empty || accountId == Guid.Empty) - return new AccountHistoryEntryCreationResult.Failure("History entry and account IDs must not be empty."); - if (transaction is null || balanceBefore is null) - return new AccountHistoryEntryCreationResult.Failure("Transaction and previous balance are required."); - - AccountHistoryEntryType entryType; - Money balanceAfter; - try - { - if (transaction.Type == BankTransactionType.Deposit && transaction.TargetAccountId == accountId) - { - entryType = AccountHistoryEntryType.Deposit; - balanceAfter = balanceBefore.Add(transaction.GetDepositCreditAmount()); - } - else if (transaction.Type == BankTransactionType.Withdrawal && transaction.SourceAccountId == accountId) - { - entryType = AccountHistoryEntryType.Withdrawal; - balanceAfter = balanceBefore.Subtract(transaction.GetSourceDebitAmount()); - } - else if (transaction.Type == BankTransactionType.Transfer && transaction.SourceAccountId == accountId) - { - entryType = AccountHistoryEntryType.TransferDebit; - balanceAfter = balanceBefore.Subtract(transaction.GetSourceDebitAmount()); - } - else if (transaction.Type == BankTransactionType.Transfer && transaction.TargetAccountId == accountId) - { - entryType = AccountHistoryEntryType.TransferCredit; - balanceAfter = balanceBefore.Add(transaction.Amount); - } - else - { - return new AccountHistoryEntryCreationResult.Failure("Account does not participate in the transaction."); - } - } - catch (InvalidOperationException exception) - { - return new AccountHistoryEntryCreationResult.Failure(exception.Message); - } - catch (OverflowException) - { - return new AccountHistoryEntryCreationResult.Failure("Resulting balance cannot be represented exactly as decimal."); - } - - return new AccountHistoryEntryCreationResult.Success(new AccountHistoryEntry - { - Id = id, - TransactionId = transaction.Id, - AccountId = accountId, - EntryType = entryType, - BalanceBefore = balanceBefore, - BalanceAfter = balanceAfter, - CreatedAtUtc = transaction.CreatedAtUtc, - }); - } -} diff --git a/src/Atm.Domain/Operations/AccountHistoryEntryType.cs b/src/Atm.Domain/Operations/AccountHistoryEntryType.cs deleted file mode 100644 index aa94227..0000000 --- a/src/Atm.Domain/Operations/AccountHistoryEntryType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Atm.Domain.Operations; - -public enum AccountHistoryEntryType -{ - Deposit = 1, - Withdrawal = 2, - TransferDebit = 3, - TransferCredit = 4, -} diff --git a/src/Atm.Domain/Operations/BankTransaction.cs b/src/Atm.Domain/Operations/BankTransaction.cs deleted file mode 100644 index 47ff961..0000000 --- a/src/Atm.Domain/Operations/BankTransaction.cs +++ /dev/null @@ -1,123 +0,0 @@ -using Atm.Domain.Common; -using Atm.Domain.Operations.Results; -using Atm.Domain.ValueObjects; - -namespace Atm.Domain.Operations; - -public sealed class BankTransaction -{ - private BankTransaction() { } - - public Guid Id { get; private set; } - - public Guid RequestId { get; private set; } - - public BankTransactionType Type { get; private set; } - - public Guid? SourceAccountId { get; private set; } - - public Guid? TargetAccountId { get; private set; } - - public Amount Amount { get; private set; } = null!; - - public CommissionSnapshot Commission { get; private set; } = null!; - - public DateTimeOffset CreatedAtUtc { get; private set; } - - public static BankTransactionCreationResult Create( - Guid id, - Guid requestId, - BankTransactionType type, - Guid? sourceAccountId, - Guid? targetAccountId, - Amount? amount, - CommissionSnapshot? commission, - DateTimeOffset createdAtUtc) - { - if (id == Guid.Empty || requestId == Guid.Empty) - return new BankTransactionCreationResult.Failure("Transaction and request IDs must not be empty."); - if (!Enum.IsDefined(type)) - return new BankTransactionCreationResult.Failure("Transaction type is not supported."); - if (amount is null || commission is null) - return new BankTransactionCreationResult.Failure("Amount and commission snapshot are required."); - if (!commission.IsCalculatedFor(amount)) - return new BankTransactionCreationResult.Failure("Commission snapshot does not match the transaction amount."); - if (!DomainRules.IsUtc(createdAtUtc)) - return new BankTransactionCreationResult.Failure("Transaction timestamp must be UTC."); - - string? shapeError = ValidateAccountShape(type, sourceAccountId, targetAccountId); - if (shapeError is not null) - return new BankTransactionCreationResult.Failure(shapeError); - - try - { - if (type == BankTransactionType.Deposit) - _ = CalculateDepositCredit(amount, commission); - else - _ = CalculateSourceDebit(amount, commission); - } - catch (InvalidOperationException exception) - { - return new BankTransactionCreationResult.Failure(exception.Message); - } - catch (OverflowException) - { - return new BankTransactionCreationResult.Failure("The transaction amount cannot be represented exactly as decimal."); - } - - return new BankTransactionCreationResult.Success(new BankTransaction - { - Id = id, - RequestId = requestId, - Type = type, - SourceAccountId = sourceAccountId, - TargetAccountId = targetAccountId, - Amount = amount, - Commission = commission, - CreatedAtUtc = createdAtUtc, - }); - } - - public Amount GetDepositCreditAmount() - { - if (Type != BankTransactionType.Deposit) - throw new InvalidOperationException("Only a deposit has a deposit credit amount."); - return CalculateDepositCredit(Amount, Commission); - } - - public Amount GetSourceDebitAmount() - { - if (Type == BankTransactionType.Deposit) - throw new InvalidOperationException("A deposit has no source debit amount."); - return CalculateSourceDebit(Amount, Commission); - } - - private static string? ValidateAccountShape( - BankTransactionType type, - Guid? sourceAccountId, - Guid? targetAccountId) - { - bool hasSource = sourceAccountId is Guid source && source != Guid.Empty; - bool hasTarget = targetAccountId is Guid target && target != Guid.Empty; - - return type switch - { - BankTransactionType.Deposit when sourceAccountId is null && hasTarget => null, - BankTransactionType.Withdrawal when hasSource && targetAccountId is null => null, - BankTransactionType.Transfer when hasSource && hasTarget && sourceAccountId != targetAccountId => null, - _ => "Source and target accounts do not match the transaction type.", - }; - } - - private static Amount CalculateDepositCredit(Amount amount, CommissionSnapshot commission) - { - if (amount.Value <= commission.CommissionAmount.Value) - throw new InvalidOperationException("Deposit amount must exceed its commission."); - return new Amount(DomainRules.SubtractExact(amount.Value, commission.CommissionAmount.Value)); - } - - private static Amount CalculateSourceDebit(Amount amount, CommissionSnapshot commission) - { - return new Amount(DomainRules.AddExact(amount.Value, commission.CommissionAmount.Value)); - } -} diff --git a/src/Atm.Domain/Operations/BankTransactionType.cs b/src/Atm.Domain/Operations/BankTransactionType.cs deleted file mode 100644 index dcd858b..0000000 --- a/src/Atm.Domain/Operations/BankTransactionType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Atm.Domain.Operations; - -public enum BankTransactionType -{ - Deposit = 1, - Withdrawal = 2, - Transfer = 3, -} diff --git a/src/Atm.Domain/Operations/CommissionSnapshot.cs b/src/Atm.Domain/Operations/CommissionSnapshot.cs deleted file mode 100644 index b98be5d..0000000 --- a/src/Atm.Domain/Operations/CommissionSnapshot.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Atm.Domain.Common; -using Atm.Domain.ValueObjects; - -namespace Atm.Domain.Operations; - -public sealed class CommissionSnapshot -{ - private CommissionSnapshot() { } - - public CommissionSnapshot(Amount operationAmount, decimal rate, decimal fixedAmount) - { - ArgumentNullException.ThrowIfNull(operationAmount); - if (rate < 0m || rate > 1m || !DomainRules.HasScale(rate, 6)) - throw new ArgumentException("Commission rate must be between zero and one with at most six decimal places.", nameof(rate)); - - var fixedMoney = new Money(fixedAmount); - decimal rateUnits = rate * 1_000_000m; - decimal whole = decimal.Truncate(operationAmount.Value / 1_000_000m); - decimal remainder = operationAmount.Value % 1_000_000m; - decimal proportional = DomainRules.AddExact( - checked(whole * rateUnits), - remainder * rateUnits / 1_000_000m); - decimal unrounded = DomainRules.AddExact(proportional, fixedMoney.Value); - decimal rounded = decimal.Round(unrounded, 4, MidpointRounding.ToEven); - - Rate = rate; - FixedAmount = fixedMoney; - CommissionAmount = new Money(rounded); - } - - public decimal Rate { get; private set; } - - public Money FixedAmount { get; private set; } = null!; - - public Money CommissionAmount { get; private set; } = null!; - - public bool IsCalculatedFor(Amount operationAmount) - { - ArgumentNullException.ThrowIfNull(operationAmount); - try - { - var expected = new CommissionSnapshot(operationAmount, Rate, FixedAmount.Value); - return expected.CommissionAmount.Value == CommissionAmount.Value; - } - catch (OverflowException) - { - return false; - } - } -} 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/Operations/Results/AccountHistoryEntryCreationResult.cs b/src/Atm.Domain/Operations/Results/AccountHistoryEntryCreationResult.cs deleted file mode 100644 index 3b87e6f..0000000 --- a/src/Atm.Domain/Operations/Results/AccountHistoryEntryCreationResult.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Atm.Domain.Operations.Results; - -public abstract record AccountHistoryEntryCreationResult -{ - private AccountHistoryEntryCreationResult() { } - - public sealed record Success(AccountHistoryEntry Entry) : AccountHistoryEntryCreationResult; - - public sealed record Failure(string ErrorMessage) : AccountHistoryEntryCreationResult; -} diff --git a/src/Atm.Domain/Operations/Results/BankTransactionCreationResult.cs b/src/Atm.Domain/Operations/Results/BankTransactionCreationResult.cs deleted file mode 100644 index aa5023f..0000000 --- a/src/Atm.Domain/Operations/Results/BankTransactionCreationResult.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Atm.Domain.Operations.Results; - -public abstract record BankTransactionCreationResult -{ - private BankTransactionCreationResult() { } - - public sealed record Success(BankTransaction Transaction) : BankTransactionCreationResult; - - public sealed record Failure(string ErrorMessage) : BankTransactionCreationResult; -} diff --git a/src/Atm.Domain/Sessions/Results/SessionCreationResult.cs b/src/Atm.Domain/Sessions/Results/SessionCreationResult.cs deleted file mode 100644 index ac4269d..0000000 --- a/src/Atm.Domain/Sessions/Results/SessionCreationResult.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Atm.Domain.Sessions.Results; - -public abstract record SessionCreationResult -{ - private SessionCreationResult() { } - - public sealed record Success(Session Session) : SessionCreationResult; - - public sealed record Failure(string ErrorMessage) : SessionCreationResult; -} diff --git a/src/Atm.Domain/Sessions/Results/SessionRevocationResult.cs b/src/Atm.Domain/Sessions/Results/SessionRevocationResult.cs deleted file mode 100644 index c71e1b0..0000000 --- a/src/Atm.Domain/Sessions/Results/SessionRevocationResult.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Atm.Domain.Sessions.Results; - -public abstract record SessionRevocationResult -{ - private SessionRevocationResult() { } - - public sealed record Success(DateTimeOffset RevokedAtUtc) : SessionRevocationResult; - - public sealed record Failure(string ErrorMessage) : SessionRevocationResult; -} diff --git a/src/Atm.Domain/Sessions/Session.cs b/src/Atm.Domain/Sessions/Session.cs deleted file mode 100644 index 6582ace..0000000 --- a/src/Atm.Domain/Sessions/Session.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Atm.Domain.Common; -using Atm.Domain.Sessions.Results; - -namespace Atm.Domain.Sessions; - -public sealed class Session -{ - private Session() { } - - public Guid Id { get; private set; } - - public SessionType Type { get; private set; } - - public Guid? AccountId { get; private set; } - - public DateTimeOffset CreatedAtUtc { get; private set; } - - public DateTimeOffset ExpiresAtUtc { get; private set; } - - public DateTimeOffset? RevokedAtUtc { get; private set; } - - public static SessionCreationResult Create( - Guid id, - SessionType type, - Guid? accountId, - DateTimeOffset createdAtUtc, - DateTimeOffset expiresAtUtc) - { - if (id == Guid.Empty) - return new SessionCreationResult.Failure("Session ID must not be empty."); - if (!Enum.IsDefined(type)) - return new SessionCreationResult.Failure("Session type is not supported."); - if (type == SessionType.User && (accountId is null || accountId == Guid.Empty)) - return new SessionCreationResult.Failure("User session must reference an account."); - if (type == SessionType.Admin && accountId is not null) - return new SessionCreationResult.Failure("System admin session must not reference an account."); - if (!DomainRules.IsUtc(createdAtUtc) || !DomainRules.IsUtc(expiresAtUtc)) - return new SessionCreationResult.Failure("Session timestamps must be UTC."); - if (expiresAtUtc <= createdAtUtc) - return new SessionCreationResult.Failure("Session expiration must follow its creation time."); - - return new SessionCreationResult.Success(new Session - { - Id = id, - Type = type, - AccountId = accountId, - CreatedAtUtc = createdAtUtc, - ExpiresAtUtc = expiresAtUtc, - }); - } - - public bool IsActive(DateTimeOffset utcNow) - { - return DomainRules.IsUtc(utcNow) - && utcNow >= CreatedAtUtc - && utcNow < ExpiresAtUtc - && RevokedAtUtc is null; - } - - public SessionRevocationResult Revoke(DateTimeOffset revokedAtUtc) - { - if (RevokedAtUtc is DateTimeOffset existing) - return new SessionRevocationResult.Success(existing); - if (!DomainRules.IsUtc(revokedAtUtc) || revokedAtUtc < CreatedAtUtc) - return new SessionRevocationResult.Failure("Revocation time must be UTC and must not precede session creation."); - - RevokedAtUtc = revokedAtUtc; - return new SessionRevocationResult.Success(revokedAtUtc); - } -} 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/Users/Friendship.cs b/src/Atm.Domain/Users/Friendship.cs deleted file mode 100644 index 092f1d9..0000000 --- a/src/Atm.Domain/Users/Friendship.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Atm.Domain.Common; -using Atm.Domain.Users.Results; - -namespace Atm.Domain.Users; - -public sealed class Friendship -{ - private Friendship() { } - - public Guid UserId { get; private set; } - public Guid FriendUserId { get; private set; } - public DateTimeOffset CreatedAtUtc { get; private set; } - - public static FriendshipCreationResult Create( - Guid firstUserId, Guid secondUserId, DateTimeOffset createdAtUtc) - { - if (firstUserId == Guid.Empty || secondUserId == Guid.Empty) - return new FriendshipCreationResult.Failure("User IDs cannot be empty."); - if (firstUserId == secondUserId) - return new FriendshipCreationResult.Failure("A user cannot befriend themselves."); - if (!DomainRules.IsUtc(createdAtUtc)) - return new FriendshipCreationResult.Failure("Created timestamp must be UTC."); - - bool firstIsSmaller = string.CompareOrdinal(firstUserId.ToString("N"), secondUserId.ToString("N")) < 0; - return new FriendshipCreationResult.Success(new Friendship - { - UserId = firstIsSmaller ? firstUserId : secondUserId, - FriendUserId = firstIsSmaller ? secondUserId : firstUserId, - CreatedAtUtc = createdAtUtc, - }); - } -} diff --git a/src/Atm.Domain/Users/HairColor.cs b/src/Atm.Domain/Users/HairColor.cs deleted file mode 100644 index 4e08a88..0000000 --- a/src/Atm.Domain/Users/HairColor.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Atm.Domain.Users; - -public enum HairColor -{ - Unspecified = 0, - Black = 1, - Brown = 2, - Blond = 3, - Red = 4, - Gray = 5, - White = 6, - Other = 7, -} diff --git a/src/Atm.Domain/Users/Results/FriendshipCreationResult.cs b/src/Atm.Domain/Users/Results/FriendshipCreationResult.cs deleted file mode 100644 index e0d9ec9..0000000 --- a/src/Atm.Domain/Users/Results/FriendshipCreationResult.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Atm.Domain.Users.Results; - -public abstract record FriendshipCreationResult -{ - private FriendshipCreationResult() { } - - public sealed record Success(Friendship Friendship) : FriendshipCreationResult; - - public sealed record Failure(string ErrorMessage) : FriendshipCreationResult; -} diff --git a/src/Atm.Domain/Users/Results/ProfileUpdateResult.cs b/src/Atm.Domain/Users/Results/ProfileUpdateResult.cs deleted file mode 100644 index 57fb3eb..0000000 --- a/src/Atm.Domain/Users/Results/ProfileUpdateResult.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Atm.Domain.Users.Results; - -public abstract record ProfileUpdateResult -{ - private ProfileUpdateResult() { } - - public sealed record Success : ProfileUpdateResult; - - public sealed record Failure(string ErrorMessage) : ProfileUpdateResult; -} diff --git a/src/Atm.Domain/Users/Results/UserCreationResult.cs b/src/Atm.Domain/Users/Results/UserCreationResult.cs deleted file mode 100644 index 346f398..0000000 --- a/src/Atm.Domain/Users/Results/UserCreationResult.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Atm.Domain.Users.Results; - -public abstract record UserCreationResult -{ - private UserCreationResult() { } - - public sealed record Success(User User) : UserCreationResult; - - public sealed record Failure(string ErrorMessage) : UserCreationResult; -} diff --git a/src/Atm.Domain/Users/User.cs b/src/Atm.Domain/Users/User.cs deleted file mode 100644 index bd172e1..0000000 --- a/src/Atm.Domain/Users/User.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Atm.Domain.Common; -using Atm.Domain.Users.Results; -using Atm.Domain.ValueObjects; - -namespace Atm.Domain.Users; - -public sealed class User -{ - private User() { } - - public Guid Id { get; private set; } - public UserLogin Login { get; private set; } = null!; - public string Name { get; private set; } = null!; - public short Age { get; private set; } - public UserGender Gender { get; private set; } - public HairColor HairColor { get; private set; } - public DateTimeOffset CreatedAtUtc { get; private set; } - public DateTimeOffset UpdatedAtUtc { get; private set; } - - public static UserCreationResult Create( - Guid id, UserLogin? login, string? name, short age, - UserGender gender, HairColor hairColor, DateTimeOffset createdAtUtc) - { - if (id == Guid.Empty || login is null) - return new UserCreationResult.Failure("User ID and login are required."); - - string? error = ValidateProfile(name, age, gender, hairColor); - if (error is not null) - return new UserCreationResult.Failure(error); - - if (!DomainRules.IsUtc(createdAtUtc)) - return new UserCreationResult.Failure("Created timestamp must be UTC."); - - return new UserCreationResult.Success(new User - { - Id = id, - Login = login, - Name = name!.Trim(), - Age = age, - Gender = gender, - HairColor = hairColor, - CreatedAtUtc = createdAtUtc, - UpdatedAtUtc = createdAtUtc, - }); - } - - public ProfileUpdateResult UpdateProfile( - string? name, short age, UserGender gender, - HairColor hairColor, DateTimeOffset updatedAtUtc) - { - string? error = ValidateProfile(name, age, gender, hairColor); - if (error is not null) - return new ProfileUpdateResult.Failure(error); - - if (!DomainRules.IsUtc(updatedAtUtc) || updatedAtUtc < UpdatedAtUtc) - return new ProfileUpdateResult.Failure("Updated timestamp must be UTC and cannot move backwards."); - - Name = name!.Trim(); - Age = age; - Gender = gender; - HairColor = hairColor; - UpdatedAtUtc = updatedAtUtc; - return new ProfileUpdateResult.Success(); - } - - private static string? ValidateProfile(string? name, short age, UserGender gender, HairColor hairColor) - { - if (string.IsNullOrWhiteSpace(name) || name.Trim().Length > 200) - return "Name must be non-empty and cannot exceed 200 characters."; - if (age is < 0 or > 150) - return "Age must be between zero and 150."; - if (!Enum.IsDefined(gender) || !Enum.IsDefined(hairColor)) - return "Unsupported gender or hair color."; - return null; - } -} diff --git a/src/Atm.Domain/Users/UserGender.cs b/src/Atm.Domain/Users/UserGender.cs deleted file mode 100644 index 5e61048..0000000 --- a/src/Atm.Domain/Users/UserGender.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Atm.Domain.Users; - -public enum UserGender -{ - Unspecified = 0, - Male = 1, - Female = 2, - Other = 3, -} diff --git a/src/Atm.Domain/ValueObjects/AccountNumber.cs b/src/Atm.Domain/ValueObjects/AccountNumber.cs deleted file mode 100644 index 3587f1c..0000000 --- a/src/Atm.Domain/ValueObjects/AccountNumber.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Atm.Domain.ValueObjects; - -public sealed class AccountNumber -{ - public AccountNumber(string value) - { - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentException("Account number is required.", nameof(value)); - - Value = value.Trim(); - NormalizedValue = Value.ToUpperInvariant(); - - if (Value.Length > 64 || NormalizedValue.Length > 64) - throw new ArgumentException("Account number cannot exceed 64 characters.", nameof(value)); - } - - public string Value { get; } - - public string NormalizedValue { get; } -} diff --git a/src/Atm.Domain/ValueObjects/Amount.cs b/src/Atm.Domain/ValueObjects/Amount.cs deleted file mode 100644 index a7f3fd2..0000000 --- a/src/Atm.Domain/ValueObjects/Amount.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Atm.Domain.Common; - -namespace Atm.Domain.ValueObjects; - -public sealed class Amount -{ - public Amount(decimal value) - { - if (value <= 0m || !DomainRules.HasScale(value, 4)) - throw new ArgumentException("Amount must be positive with at most four decimal places.", nameof(value)); - - Value = value; - } - - public decimal Value { get; } -} diff --git a/src/Atm.Domain/ValueObjects/Money.cs b/src/Atm.Domain/ValueObjects/Money.cs deleted file mode 100644 index 6095ea7..0000000 --- a/src/Atm.Domain/ValueObjects/Money.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Atm.Domain.Common; - -namespace Atm.Domain.ValueObjects; - -public sealed class Money -{ - public Money(decimal value) - { - if (value < 0m || !DomainRules.HasScale(value, 4)) - throw new ArgumentException("Money must be non-negative with at most four decimal places.", nameof(value)); - - Value = value; - } - - public decimal Value { get; } - - public Money Add(Amount amount) - { - return new Money(DomainRules.AddExact(Value, amount.Value)); - } - - public Money Subtract(Amount amount) - { - if (Value < amount.Value) - throw new InvalidOperationException("Not enough balance."); - return new Money(DomainRules.SubtractExact(Value, amount.Value)); - } -} diff --git a/src/Atm.Domain/ValueObjects/PinCode.cs b/src/Atm.Domain/ValueObjects/PinCode.cs deleted file mode 100644 index 62dcfa2..0000000 --- a/src/Atm.Domain/ValueObjects/PinCode.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Atm.Domain.ValueObjects; - -public sealed class PinCode -{ - public PinCode(string value) - { - if (value is null || value.Length != 4 || value.Any(character => character is < '0' or > '9')) - throw new ArgumentException("PIN must contain exactly four ASCII digits.", nameof(value)); - Value = value; - } - - public string Value { get; } -} diff --git a/src/Atm.Domain/ValueObjects/PinHash.cs b/src/Atm.Domain/ValueObjects/PinHash.cs deleted file mode 100644 index b3674e0..0000000 --- a/src/Atm.Domain/ValueObjects/PinHash.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Atm.Domain.ValueObjects; - -public sealed record PinHash -{ - public PinHash(string value) - { - if (string.IsNullOrWhiteSpace(value) || value.Length > 512) - throw new ArgumentException("PIN hash must contain between 1 and 512 characters.", nameof(value)); - - Value = value; - } - - public string Value { get; } - - public override string ToString() => "[REDACTED]"; -} diff --git a/src/Atm.Domain/ValueObjects/UserLogin.cs b/src/Atm.Domain/ValueObjects/UserLogin.cs deleted file mode 100644 index 3a90359..0000000 --- a/src/Atm.Domain/ValueObjects/UserLogin.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Atm.Domain.ValueObjects; - -public sealed class UserLogin -{ - public UserLogin(string value) - { - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentException("Login is required.", nameof(value)); - - Value = value.Trim(); - NormalizedValue = Value.ToUpperInvariant(); - - if (Value.Length > 64 || NormalizedValue.Length > 64) - throw new ArgumentException("Login cannot exceed 64 characters.", nameof(value)); - } - - public string Value { get; } - - public string NormalizedValue { get; } -} 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..1019c63 --- /dev/null +++ b/src/Atm.Infrastructure.Persistence/Atm.Infrastructure.Persistence.csproj @@ -0,0 +1,16 @@ + + + Atm.Infrastructure.Persistence + + + + + + + + + + + + + diff --git a/src/Atm.Infrastructure/Atm.Infrastructure.csproj b/src/Atm.Infrastructure/Atm.Infrastructure.csproj deleted file mode 100644 index befd5ab..0000000 --- a/src/Atm.Infrastructure/Atm.Infrastructure.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - all - runtime; compile; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - net9.0 - enable - enable - - - diff --git a/src/Atm.Infrastructure/Persistence/AtmDbContext.cs b/src/Atm.Infrastructure/Persistence/AtmDbContext.cs deleted file mode 100644 index d2ba3c5..0000000 --- a/src/Atm.Infrastructure/Persistence/AtmDbContext.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Atm.Domain.Accounts; -using Atm.Domain.Operations; -using Atm.Domain.Sessions; -using Atm.Domain.Users; -using Microsoft.EntityFrameworkCore; - -namespace Atm.Infrastructure.Persistence; - -public sealed class AtmDbContext : DbContext -{ - public AtmDbContext(DbContextOptions options) : base(options) { } - - public DbSet Users => Set(); - public DbSet Friendships => Set(); - public DbSet Accounts => Set(); - public DbSet Sessions => Set(); - public DbSet BankTransactions => Set(); - public DbSet AccountHistoryEntries => Set(); - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - modelBuilder.HasDefaultSchema("public"); - modelBuilder.ApplyConfigurationsFromAssembly(typeof(AtmDbContext).Assembly); - } - - public override int SaveChanges(bool acceptAllChangesOnSuccess) - { - ValidateFinancialChanges(); - return base.SaveChanges(acceptAllChangesOnSuccess); - } - - public override Task SaveChangesAsync( - bool acceptAllChangesOnSuccess, - CancellationToken cancellationToken = default) - { - ValidateFinancialChanges(); - return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); - } - - private void ValidateFinancialChanges() - { - ChangeTracker.DetectChanges(); - foreach (Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry entry in ChangeTracker.Entries()) - { - if (entry.Entity is Account && entry.State == EntityState.Deleted) - throw new InvalidOperationException("Physical account deletion is not supported."); - if (entry.Entity is BankTransaction or AccountHistoryEntry or CommissionSnapshot - && entry.State is EntityState.Modified or EntityState.Deleted) - { - throw new InvalidOperationException("Persisted financial records are immutable."); - } - } - } -} diff --git a/src/Atm.Infrastructure/Persistence/Configurations/AccountConfiguration.cs b/src/Atm.Infrastructure/Persistence/Configurations/AccountConfiguration.cs deleted file mode 100644 index b85bae9..0000000 --- a/src/Atm.Infrastructure/Persistence/Configurations/AccountConfiguration.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Atm.Domain.Accounts; -using Atm.Domain.Users; -using Atm.Domain.ValueObjects; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace Atm.Infrastructure.Persistence.Configurations; - -public sealed class AccountConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("accounts", table => - { - table.HasCheckConstraint("ck_accounts_id", ColumnRules.NonEmptyUuid("id")); - table.HasCheckConstraint("ck_accounts_balance", ColumnRules.MonetaryCheck("balance")); - table.HasCheckConstraint("ck_accounts_number", "length(btrim(number)) > 0 AND length(normalized_number) > 0"); - table.HasCheckConstraint("ck_accounts_pin_hash", "length(btrim(pin_hash)) > 0"); - table.HasCheckConstraint("ck_accounts_version", "version >= 0"); - table.HasCheckConstraint("ck_accounts_timestamps", "updated_at_utc >= created_at_utc"); - }); - builder.HasKey(account => account.Id).HasName("pk_accounts"); - builder.Property(account => account.Id).HasColumnName("id").HasColumnType("uuid").ValueGeneratedNever(); - builder.Property(account => account.OwnerUserId).HasColumnName("owner_user_id").HasColumnType("uuid"); - ColumnRules.Money(builder.Property(account => account.Balance), "balance"); - builder.Property(account => account.PinHash).HasColumnName("pin_hash") - .HasConversion(value => value.Value, value => new PinHash(value)).HasMaxLength(512).IsRequired(); - builder.Property(account => account.Version).HasColumnName("version").HasColumnType("bigint") - .IsConcurrencyToken().ValueGeneratedNever(); - builder.Property(account => account.CreatedAtUtc).HasColumnName("created_at_utc").HasColumnType("timestamp with time zone"); - builder.Property(account => account.UpdatedAtUtc).HasColumnName("updated_at_utc").HasColumnType("timestamp with time zone"); - builder.HasOne().WithMany().HasForeignKey(account => account.OwnerUserId) - .OnDelete(DeleteBehavior.Restrict).HasConstraintName("fk_accounts_users_owner_user_id"); - builder.HasIndex(account => account.OwnerUserId).HasDatabaseName("ix_accounts_owner_user_id"); - builder.OwnsOne(account => account.Number, number => - { - number.WithOwner().HasForeignKey("AccountId"); - number.Property("AccountId").HasColumnName("id").ValueGeneratedNever(); - number.HasKey("AccountId").HasName("pk_accounts"); - number.Property(value => value.Value).HasColumnName("number").HasMaxLength(64).IsRequired(); - number.Property(value => value.NormalizedValue).HasColumnName("normalized_number").HasMaxLength(64).UseCollation("C").IsRequired(); - number.Property("Version").HasColumnName("version").IsConcurrencyToken().ValueGeneratedNever(); - number.HasIndex(value => value.NormalizedValue).IsUnique().HasDatabaseName("ux_accounts_normalized_number"); - }); - builder.Navigation(account => account.Number).IsRequired(); - } -} diff --git a/src/Atm.Infrastructure/Persistence/Configurations/AccountHistoryEntryConfiguration.cs b/src/Atm.Infrastructure/Persistence/Configurations/AccountHistoryEntryConfiguration.cs deleted file mode 100644 index ab9372c..0000000 --- a/src/Atm.Infrastructure/Persistence/Configurations/AccountHistoryEntryConfiguration.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Atm.Domain.Accounts; -using Atm.Domain.Operations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace Atm.Infrastructure.Persistence.Configurations; - -public sealed class AccountHistoryEntryConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("account_history_entries", table => - { - table.HasCheckConstraint("ck_account_history_entries_id", ColumnRules.NonEmptyUuid("id")); - table.HasCheckConstraint("ck_account_history_entries_balance_before", ColumnRules.MonetaryCheck("balance_before")); - table.HasCheckConstraint("ck_account_history_entries_balance_after", ColumnRules.MonetaryCheck("balance_after")); - table.HasCheckConstraint("ck_account_history_entries_type", - "entry_type IN ('Deposit', 'Withdrawal', 'TransferDebit', 'TransferCredit')"); - table.HasCheckConstraint("ck_account_history_entries_direction", - "(entry_type IN ('Deposit', 'TransferCredit') AND balance_after > balance_before) OR " - + "(entry_type IN ('Withdrawal', 'TransferDebit') AND balance_after < balance_before)"); - }); - builder.HasKey(entry => entry.Id).HasName("pk_account_history_entries"); - builder.Property(entry => entry.Id).HasColumnName("id").HasColumnType("uuid").ValueGeneratedNever(); - builder.Property(entry => entry.TransactionId).HasColumnName("transaction_id").HasColumnType("uuid"); - builder.Property(entry => entry.AccountId).HasColumnName("account_id").HasColumnType("uuid"); - builder.Property(entry => entry.EntryType).HasColumnName("entry_type").HasConversion().HasMaxLength(32); - ColumnRules.Money(builder.Property(entry => entry.BalanceBefore), "balance_before"); - ColumnRules.Money(builder.Property(entry => entry.BalanceAfter), "balance_after"); - builder.Property(entry => entry.CreatedAtUtc).HasColumnName("created_at_utc").HasColumnType("timestamp with time zone"); - builder.HasOne().WithMany().HasForeignKey(entry => entry.AccountId) - .OnDelete(DeleteBehavior.Restrict).HasConstraintName("fk_account_history_entries_accounts_account_id"); - builder.HasOne().WithMany().HasForeignKey(entry => entry.TransactionId) - .OnDelete(DeleteBehavior.Restrict).HasConstraintName("fk_account_history_entries_bank_transactions_transaction_id"); - builder.HasIndex(entry => new { entry.AccountId, entry.CreatedAtUtc, entry.Id }) - .IsDescending(false, true, true).HasDatabaseName("ix_account_history_entries_account_timeline"); - builder.HasIndex(entry => entry.TransactionId).HasDatabaseName("ix_account_history_entries_transaction_id"); - builder.HasAlternateKey(entry => new { entry.AccountId, entry.TransactionId, entry.EntryType }) - .HasName("ak_account_history_entries_account_transaction_type"); - } -} diff --git a/src/Atm.Infrastructure/Persistence/Configurations/BankTransactionConfiguration.cs b/src/Atm.Infrastructure/Persistence/Configurations/BankTransactionConfiguration.cs deleted file mode 100644 index db66647..0000000 --- a/src/Atm.Infrastructure/Persistence/Configurations/BankTransactionConfiguration.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Atm.Domain.Accounts; -using Atm.Domain.Operations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace Atm.Infrastructure.Persistence.Configurations; - -public sealed class BankTransactionConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("bank_transactions", table => - { - table.HasCheckConstraint("ck_bank_transactions_id", ColumnRules.NonEmptyUuid("id")); - table.HasCheckConstraint("ck_bank_transactions_request_id", ColumnRules.NonEmptyUuid("request_id")); - table.HasCheckConstraint("ck_bank_transactions_amount", ColumnRules.MonetaryCheck("amount", true)); - table.HasCheckConstraint("ck_bank_transactions_commission_rate", "commission_rate BETWEEN 0 AND 1"); - table.HasCheckConstraint("ck_bank_transactions_commission_fixed", ColumnRules.MonetaryCheck("commission_fixed")); - table.HasCheckConstraint("ck_bank_transactions_commission_amount", ColumnRules.MonetaryCheck("commission_amount")); - table.HasCheckConstraint("ck_bank_transactions_account_shape", - "(type = 'Deposit' AND source_account_id IS NULL AND target_account_id IS NOT NULL) OR " - + "(type = 'Withdrawal' AND source_account_id IS NOT NULL AND target_account_id IS NULL) OR " - + "(type = 'Transfer' AND source_account_id IS NOT NULL AND target_account_id IS NOT NULL AND source_account_id <> target_account_id)"); - table.HasCheckConstraint("ck_bank_transactions_deposit_credit", "type <> 'Deposit' OR amount > commission_amount"); - }); - builder.HasKey(transaction => transaction.Id).HasName("pk_bank_transactions"); - builder.Property(transaction => transaction.Id).HasColumnName("id").HasColumnType("uuid").ValueGeneratedNever(); - builder.Property(transaction => transaction.RequestId).HasColumnName("request_id").HasColumnType("uuid").ValueGeneratedNever(); - builder.Property(transaction => transaction.Type).HasColumnName("type").HasConversion().HasMaxLength(32); - builder.Property(transaction => transaction.SourceAccountId).HasColumnName("source_account_id").HasColumnType("uuid"); - builder.Property(transaction => transaction.TargetAccountId).HasColumnName("target_account_id").HasColumnType("uuid"); - ColumnRules.Amount(builder.Property(transaction => transaction.Amount), "amount"); - builder.Property(transaction => transaction.CreatedAtUtc).HasColumnName("created_at_utc").HasColumnType("timestamp with time zone"); - builder.HasIndex(transaction => transaction.RequestId).IsUnique().HasDatabaseName("ux_bank_transactions_request_id"); - builder.HasIndex(transaction => transaction.SourceAccountId).HasDatabaseName("ix_bank_transactions_source_account_id"); - builder.HasIndex(transaction => transaction.TargetAccountId).HasDatabaseName("ix_bank_transactions_target_account_id"); - builder.HasOne().WithMany().HasForeignKey(transaction => transaction.SourceAccountId) - .OnDelete(DeleteBehavior.Restrict).HasConstraintName("fk_bank_transactions_accounts_source_account_id"); - builder.HasOne().WithMany().HasForeignKey(transaction => transaction.TargetAccountId) - .OnDelete(DeleteBehavior.Restrict).HasConstraintName("fk_bank_transactions_accounts_target_account_id"); - builder.OwnsOne(transaction => transaction.Commission, commission => - { - commission.WithOwner().HasForeignKey("TransactionId"); - commission.Property("TransactionId").HasColumnName("id").ValueGeneratedNever(); - commission.HasKey("TransactionId").HasName("pk_bank_transactions"); - commission.Property(value => value.Rate).HasColumnName("commission_rate").HasPrecision(9, 6); - ColumnRules.Money(commission.Property(value => value.FixedAmount), "commission_fixed"); - ColumnRules.Money(commission.Property(value => value.CommissionAmount), "commission_amount"); - }); - builder.Navigation(transaction => transaction.Commission).IsRequired(); - } -} diff --git a/src/Atm.Infrastructure/Persistence/Configurations/ColumnRules.cs b/src/Atm.Infrastructure/Persistence/Configurations/ColumnRules.cs deleted file mode 100644 index 19c117d..0000000 --- a/src/Atm.Infrastructure/Persistence/Configurations/ColumnRules.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Atm.Domain.ValueObjects; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.ChangeTracking; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace Atm.Infrastructure.Persistence.Configurations; - -internal static class ColumnRules -{ - public static PropertyBuilder Money(PropertyBuilder property, string column) - { - property.HasConversion(value => value.Value, value => new Money(value)); - property.Metadata.SetValueComparer(new ValueComparer( - (left, right) => left != null && right != null ? left.Value == right.Value : left == right, - value => value.Value.GetHashCode(), value => value)); - return property.HasColumnName(column).HasColumnType("numeric").IsRequired(); - } - - public static PropertyBuilder Amount(PropertyBuilder property, string column) - { - property.HasConversion(value => value.Value, value => new Amount(value)); - property.Metadata.SetValueComparer(new ValueComparer( - (left, right) => left != null && right != null ? left.Value == right.Value : left == right, - value => value.Value.GetHashCode(), value => value)); - return property.HasColumnName(column).HasColumnType("numeric").IsRequired(); - } - - public static string MonetaryCheck(string column, bool positive = false) - { - string comparison = positive ? ">" : ">="; - return $"{column} {comparison} 0 AND {column} <= 79228162514264337593543950335 " - + $"AND {column} = trunc({column}, 4) " - + $"AND {column} * power(10::numeric, scale(trim_scale({column}))) <= 79228162514264337593543950335"; - } - - public static string NonEmptyUuid(string column) - { - return $"{column} <> '00000000-0000-0000-0000-000000000000'::uuid"; - } -} diff --git a/src/Atm.Infrastructure/Persistence/Configurations/FriendshipConfiguration.cs b/src/Atm.Infrastructure/Persistence/Configurations/FriendshipConfiguration.cs deleted file mode 100644 index ca7fe53..0000000 --- a/src/Atm.Infrastructure/Persistence/Configurations/FriendshipConfiguration.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Atm.Domain.Users; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace Atm.Infrastructure.Persistence.Configurations; - -public sealed class FriendshipConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("friendships", table => - { - table.HasCheckConstraint("ck_friendships_distinct_users", "user_id <> friend_user_id"); - table.HasCheckConstraint("ck_friendships_canonical_order", "user_id < friend_user_id"); - }); - builder.HasKey(friendship => new { friendship.UserId, friendship.FriendUserId }).HasName("pk_friendships"); - builder.Property(friendship => friendship.UserId).HasColumnName("user_id").HasColumnType("uuid").ValueGeneratedNever(); - builder.Property(friendship => friendship.FriendUserId).HasColumnName("friend_user_id").HasColumnType("uuid").ValueGeneratedNever(); - builder.Property(friendship => friendship.CreatedAtUtc).HasColumnName("created_at_utc").HasColumnType("timestamp with time zone"); - builder.HasOne().WithMany().HasForeignKey(friendship => friendship.UserId) - .OnDelete(DeleteBehavior.Cascade).HasConstraintName("fk_friendships_users_user_id"); - builder.HasOne().WithMany().HasForeignKey(friendship => friendship.FriendUserId) - .OnDelete(DeleteBehavior.Cascade).HasConstraintName("fk_friendships_users_friend_user_id"); - builder.HasIndex(friendship => friendship.FriendUserId).HasDatabaseName("ix_friendships_friend_user_id"); - } -} diff --git a/src/Atm.Infrastructure/Persistence/Configurations/SessionConfiguration.cs b/src/Atm.Infrastructure/Persistence/Configurations/SessionConfiguration.cs deleted file mode 100644 index 6dc911a..0000000 --- a/src/Atm.Infrastructure/Persistence/Configurations/SessionConfiguration.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Atm.Domain.Accounts; -using Atm.Domain.Sessions; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace Atm.Infrastructure.Persistence.Configurations; - -public sealed class SessionConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("sessions", table => - { - table.HasCheckConstraint("ck_sessions_id", ColumnRules.NonEmptyUuid("id")); - table.HasCheckConstraint("ck_sessions_account_type", - "(type = 'User' AND account_id IS NOT NULL) OR (type = 'Admin' AND account_id IS NULL)"); - table.HasCheckConstraint("ck_sessions_expiration", "expires_at_utc > created_at_utc"); - table.HasCheckConstraint("ck_sessions_revocation", "revoked_at_utc IS NULL OR revoked_at_utc >= created_at_utc"); - }); - builder.HasKey(session => session.Id).HasName("pk_sessions"); - builder.Property(session => session.Id).HasColumnName("id").HasColumnType("uuid").ValueGeneratedNever(); - builder.Property(session => session.Type).HasColumnName("type").HasConversion().HasMaxLength(16); - builder.Property(session => session.AccountId).HasColumnName("account_id").HasColumnType("uuid"); - builder.Property(session => session.CreatedAtUtc).HasColumnName("created_at_utc").HasColumnType("timestamp with time zone"); - builder.Property(session => session.ExpiresAtUtc).HasColumnName("expires_at_utc").HasColumnType("timestamp with time zone"); - builder.Property(session => session.RevokedAtUtc).HasColumnName("revoked_at_utc").HasColumnType("timestamp with time zone"); - builder.HasOne().WithMany().HasForeignKey(session => session.AccountId) - .OnDelete(DeleteBehavior.Restrict).HasConstraintName("fk_sessions_accounts_account_id"); - builder.HasIndex(session => session.AccountId).HasDatabaseName("ix_sessions_account_id"); - builder.HasIndex(session => session.ExpiresAtUtc).HasDatabaseName("ix_sessions_expires_at_utc"); - } -} diff --git a/src/Atm.Infrastructure/Persistence/Configurations/UserConfiguration.cs b/src/Atm.Infrastructure/Persistence/Configurations/UserConfiguration.cs deleted file mode 100644 index 527e84f..0000000 --- a/src/Atm.Infrastructure/Persistence/Configurations/UserConfiguration.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Atm.Domain.Users; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace Atm.Infrastructure.Persistence.Configurations; - -public sealed class UserConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("users", table => - { - table.HasCheckConstraint("ck_users_id", ColumnRules.NonEmptyUuid("id")); - table.HasCheckConstraint("ck_users_name", "length(btrim(name)) > 0"); - table.HasCheckConstraint("ck_users_login", "length(btrim(login)) > 0 AND length(normalized_login) > 0"); - table.HasCheckConstraint("ck_users_age", "age BETWEEN 0 AND 150"); - table.HasCheckConstraint("ck_users_gender", "gender IN ('Unspecified', 'Male', 'Female', 'Other')"); - table.HasCheckConstraint("ck_users_hair_color", "hair_color IN ('Unspecified', 'Black', 'Brown', 'Blond', 'Red', 'Gray', 'White', 'Other')"); - table.HasCheckConstraint("ck_users_timestamps", "updated_at_utc >= created_at_utc"); - }); - builder.HasKey(user => user.Id).HasName("pk_users"); - builder.Property(user => user.Id).HasColumnName("id").HasColumnType("uuid").ValueGeneratedNever(); - builder.Property(user => user.Name).HasColumnName("name").HasMaxLength(200).IsRequired(); - builder.Property(user => user.Age).HasColumnName("age").HasColumnType("smallint"); - builder.Property(user => user.Gender).HasColumnName("gender").HasConversion().HasMaxLength(32); - builder.Property(user => user.HairColor).HasColumnName("hair_color").HasConversion().HasMaxLength(32); - builder.Property(user => user.CreatedAtUtc).HasColumnName("created_at_utc").HasColumnType("timestamp with time zone"); - builder.Property(user => user.UpdatedAtUtc).HasColumnName("updated_at_utc").HasColumnType("timestamp with time zone"); - builder.OwnsOne(user => user.Login, login => - { - login.WithOwner().HasForeignKey("UserId"); - login.Property("UserId").HasColumnName("id").ValueGeneratedNever(); - login.HasKey("UserId").HasName("pk_users"); - login.Property(value => value.Value).HasColumnName("login").HasMaxLength(64).IsRequired(); - login.Property(value => value.NormalizedValue).HasColumnName("normalized_login").HasMaxLength(64).UseCollation("C").IsRequired(); - login.HasIndex(value => value.NormalizedValue).IsUnique().HasDatabaseName("ux_users_normalized_login"); - }); - builder.Navigation(user => user.Login).IsRequired(); - } -} diff --git a/src/Atm.Infrastructure/Persistence/DesignTime/AtmDbContextFactory.cs b/src/Atm.Infrastructure/Persistence/DesignTime/AtmDbContextFactory.cs deleted file mode 100644 index 2ae24a3..0000000 --- a/src/Atm.Infrastructure/Persistence/DesignTime/AtmDbContextFactory.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; -using Npgsql; - -namespace Atm.Infrastructure.Persistence.DesignTime; - -public sealed class AtmDbContextFactory : IDesignTimeDbContextFactory -{ - public const string ConnectionStringVariable = "ConnectionStrings__AtmDatabase"; - - public AtmDbContext CreateDbContext(string[] args) - { - string? connectionString = Environment.GetEnvironmentVariable(ConnectionStringVariable); - if (string.IsNullOrWhiteSpace(connectionString)) - throw new InvalidOperationException($"Set {ConnectionStringVariable} before running EF tools."); - - NpgsqlConnectionStringBuilder settings; - try - { - settings = new NpgsqlConnectionStringBuilder(connectionString); - } - catch (ArgumentException) - { - throw new InvalidOperationException("The database connection configuration is invalid."); - } - - if (string.IsNullOrWhiteSpace(settings.Host) || string.IsNullOrWhiteSpace(settings.Database)) - throw new InvalidOperationException("The database connection must specify Host and Database."); - - DbContextOptions options = new DbContextOptionsBuilder() - .UseNpgsql(settings.ConnectionString) - .Options; - return new AtmDbContext(options); - } -} diff --git a/src/Atm.Infrastructure/Repositories/AccountRepository.cs b/src/Atm.Infrastructure/Repositories/AccountRepository.cs deleted file mode 100644 index 8a7f0e0..0000000 --- a/src/Atm.Infrastructure/Repositories/AccountRepository.cs +++ /dev/null @@ -1,31 +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? GetById(Guid id) - { - return _accounts.GetValueOrDefault(id); - } - - public Account? GetByNumber(AccountNumber accountNumber) - { - return _accounts.Values.FirstOrDefault( - account => account.Number.NormalizedValue == accountNumber.NormalizedValue); - } - - public void Add(Account account) - { - _accounts.Add(account.Id, account); - } - - public void Update(Account account) - { - _accounts[account.Id] = account; - } -} 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/Security/PinHasher.cs b/src/Atm.Infrastructure/Security/PinHasher.cs deleted file mode 100644 index 3e39ce9..0000000 --- a/src/Atm.Infrastructure/Security/PinHasher.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Atm.Application.Abstractions.Security; -using Atm.Domain.ValueObjects; -using Microsoft.AspNetCore.Identity; - -namespace Atm.Infrastructure.Security; - -public sealed class PinHasher : IPinHasher -{ - private readonly PasswordHasher _hasher = new(); - - public PinHash Hash(PinCode pin) - { - return new PinHash(_hasher.HashPassword(this, pin.Value)); - } - - public bool Verify(PinHash hash, PinCode pin) - { - try - { - return _hasher.VerifyHashedPassword(this, hash.Value, pin.Value) - != PasswordVerificationResult.Failed; - } - catch (FormatException) - { - return false; - } - } -} diff --git a/src/Atm.Infrastructure/ServiceCollectionExtensions.cs b/src/Atm.Infrastructure/ServiceCollectionExtensions.cs deleted file mode 100644 index 0c2d7ba..0000000 --- a/src/Atm.Infrastructure/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Atm.Application.Abstractions.Repositories; -using Atm.Application.Abstractions.Security; -using Atm.Application.Abstractions.Time; -using Atm.Infrastructure.Repositories; -using Atm.Infrastructure.Security; -using Atm.Infrastructure.Time; -using Microsoft.Extensions.DependencyInjection; - -namespace Atm.Infrastructure; - -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddInfrastructure(this IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - return services; - } -} diff --git a/src/Atm.Infrastructure/Time/SystemClock.cs b/src/Atm.Infrastructure/Time/SystemClock.cs deleted file mode 100644 index a06421a..0000000 --- a/src/Atm.Infrastructure/Time/SystemClock.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Atm.Application.Abstractions.Time; - -namespace Atm.Infrastructure.Time; - -public sealed class SystemClock : IClock -{ - public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; -} diff --git a/src/Atm.Infrastructure/packages.lock.json b/src/Atm.Infrastructure/packages.lock.json deleted file mode 100644 index a16a968..0000000 --- a/src/Atm.Infrastructure/packages.lock.json +++ /dev/null @@ -1,366 +0,0 @@ -{ - "version": 1, - "dependencies": { - "net9.0": { - "Microsoft.EntityFrameworkCore": { - "type": "Direct", - "requested": "[9.0.19, )", - "resolved": "9.0.19", - "contentHash": "qWqqxrfEDfi3C/z0S/ACIA2gUnreRPHX74WE/dVnesbztNesYO4NLy76lE6FV/zDP4vH/pkvDr8h3Mq9+cFv/w==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "9.0.19", - "Microsoft.EntityFrameworkCore.Analyzers": "9.0.19", - "Microsoft.Extensions.Caching.Memory": "9.0.19", - "Microsoft.Extensions.Logging": "9.0.19" - } - }, - "Microsoft.EntityFrameworkCore.Design": { - "type": "Direct", - "requested": "[9.0.19, )", - "resolved": "9.0.19", - "contentHash": "O4o69Xne6dJ7+u21YPxevGt8F2wpxGz2bBiSa/SSyqb2/64OZtcbIOB264fuViRj2XgwzhSGErNP75fRVCk8Zw==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.Build.Framework": "17.8.43", - "Microsoft.Build.Locator": "1.7.8", - "Microsoft.CodeAnalysis.CSharp": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Workspaces": "4.8.0", - "Microsoft.CodeAnalysis.Workspaces.MSBuild": "4.8.0", - "Microsoft.EntityFrameworkCore.Relational": "9.0.19", - "Microsoft.Extensions.Caching.Memory": "9.0.19", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.19", - "Microsoft.Extensions.DependencyModel": "9.0.19", - "Microsoft.Extensions.Logging": "9.0.19", - "Mono.TextTemplating": "3.0.0", - "System.Text.Json": "9.0.19" - } - }, - "Microsoft.EntityFrameworkCore.Relational": { - "type": "Direct", - "requested": "[9.0.19, )", - "resolved": "9.0.19", - "contentHash": "KAfYGG4LWRNn1G/vMRydxgOjuZoWUvdd5raUcIia/HPg1jUjlQzS386nUx5gt3YfKxpFJaCGxdsQ1nCZWuYR8A==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "9.0.19", - "Microsoft.Extensions.Caching.Memory": "9.0.19", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.19", - "Microsoft.Extensions.Logging": "9.0.19" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Direct", - "requested": "[9.0.19, )", - "resolved": "9.0.19", - "contentHash": "XgQ0aVWClYjYSBqbYrKC/xyZ1KHf+VVwNF3+d54jcaqnst30Q//ugs/FJwUztLZiph0gp85g7i/eB0556twO0Q==" - }, - "Microsoft.Extensions.Identity.Core": { - "type": "Direct", - "requested": "[9.0.19, )", - "resolved": "9.0.19", - "contentHash": "6P2/iMA35j8RrjsJXhqaOy4Sturcu+NXzXSlKguGTzjBEVRf6atWPrKkUuZew9K5S5NbNh8gdYip2dXm8t5fLg==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.19", - "Microsoft.Extensions.Logging": "9.0.19", - "Microsoft.Extensions.Options": "9.0.19" - } - }, - "Npgsql.EntityFrameworkCore.PostgreSQL": { - "type": "Direct", - "requested": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)", - "Npgsql": "9.0.3" - } - }, - "Humanizer.Core": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" - }, - "Microsoft.AspNetCore.Cryptography.Internal": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "zqiWdZ8Jw4/sfTBSFHchU3Iajm8EqLPUq/cUIuUjARwNGJ9hrKR7Yf0bs4WlF+GdPX6Bj7kmaf99aY0OTbLCOg==" - }, - "Microsoft.AspNetCore.Cryptography.KeyDerivation": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "w0ZZWnsz7U2dh5l9eytJYYHG3/HwafewFyZT+NsnBuQAt90cw2hmiX9J9OU1mrtn7EiIJqhRxjzY5kAbmhD2DA==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.Internal": "9.0.19" - } - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "3aeMZ1N0lJoSyzqiP03hqemtb1BijhsJADdobn/4nsMJ8V1H+CrpuduUe4hlRdx+ikBQju1VGjMD1GJ3Sk05Eg==" - }, - "Microsoft.Build.Framework": { - "type": "Transitive", - "resolved": "17.8.43", - "contentHash": "TA6fCbd9qFSyhIKcUn5BhDN3/JrqpspO9KYsZKUvHyQpQrHVdzy4Hk6egjjBP3T0L562TLZuHEzQwJziyRBEkw==" - }, - "Microsoft.Build.Locator": { - "type": "Transitive", - "resolved": "1.7.8", - "contentHash": "sPy10x527Ph16S2u0yGME4S6ohBKJ69WfjeGG/bvELYeZVmJdKjxgnlL8cJJJLGV/cZIRqSfB12UDB8ICakOog==" - }, - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Transitive", - "resolved": "3.3.4", - "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" - }, - "Microsoft.CodeAnalysis.Common": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.3.4", - "System.Collections.Immutable": "7.0.0", - "System.Reflection.Metadata": "7.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "Microsoft.CodeAnalysis.CSharp": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "+3+qfdb/aaGD8PZRCrsdobbzGs1m9u119SkkJt8e/mk3xLJz/udLtS2T6nY27OTXxBBw10HzAbC8Z9w08VyP/g==", - "dependencies": { - "Microsoft.CodeAnalysis.Common": "[4.8.0]" - } - }, - "Microsoft.CodeAnalysis.CSharp.Workspaces": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "3amm4tq4Lo8/BGvg9p3BJh3S9nKq2wqCXfS7138i69TUpo/bD+XvD0hNurpEBtcNZhi1FyutiomKJqVF39ugYA==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.CodeAnalysis.CSharp": "[4.8.0]", - "Microsoft.CodeAnalysis.Common": "[4.8.0]", - "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]" - } - }, - "Microsoft.CodeAnalysis.Workspaces.Common": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "LXyV+MJKsKRu3FGJA3OmSk40OUIa/dQCFLOnm5X8MNcujx7hzGu8o+zjXlb/cy5xUdZK2UKYb9YaQ2E8m9QehQ==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.Bcl.AsyncInterfaces": "7.0.0", - "Microsoft.CodeAnalysis.Common": "[4.8.0]", - "System.Composition": "7.0.0", - "System.IO.Pipelines": "7.0.0", - "System.Threading.Channels": "7.0.0" - } - }, - "Microsoft.CodeAnalysis.Workspaces.MSBuild": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "IEYreI82QZKklp54yPHxZNG9EKSK6nHEkeuf+0Asie9llgS1gp0V1hw7ODG+QyoB7MuAnNQHmeV1Per/ECpv6A==", - "dependencies": { - "Microsoft.Build.Framework": "16.10.0", - "Microsoft.CodeAnalysis.Common": "[4.8.0]", - "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]", - "System.Text.Json": "7.0.3" - } - }, - "Microsoft.EntityFrameworkCore.Abstractions": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "igPnmVL3DU2ZH/4DNeQqWttNrQpwwg0q6MWj5peZxtl0QVMDSeFtnt2yhhdSffklZKYsAFko1sh/va3z4y7S6A==" - }, - "Microsoft.EntityFrameworkCore.Analyzers": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "yI5dQuvigJ6ICQECOlJDtrsUYcuDryv3qsZK1i6ZA/IzMzZ/VPkDzjE4r2LivuyVSGnkDRkH2cp4LmrhXARRKw==" - }, - "Microsoft.Extensions.Caching.Abstractions": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "qe/6m6wxRG+N5xTf4Z3lDBAiVu6oBCUyADJ5OzlFgukD9Vg/6zB3MNnNEFA7amZg8hjcS6F4OZBRmB07A+gmHQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.19" - } - }, - "Microsoft.Extensions.Caching.Memory": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "Y0w5LkBqWkDh5vYTKM+QKlzxpqegEtP86oDhFMXgx4tDkWL2t+rl4h2vxBHkvBtM/gM6PoLK9QCrnoclXtHtVQ==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "9.0.19", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19", - "Microsoft.Extensions.Logging.Abstractions": "9.0.19", - "Microsoft.Extensions.Options": "9.0.19", - "Microsoft.Extensions.Primitives": "9.0.19" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "M6h5lX6QWSSn7FDeRUHePZen/xvxPsuhcX1VpBfefafqavn2BnfxheR7BH5D5AIZGM7Nah0xJKnq2DublD4doQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.19" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "o/XFYSiV4a7eiCkf7W1GFAviVhHQAPY5bHrQRsNWAPT3GhJ3Af63vcFQjHivIrTNZITlPPaGWhBy7jykKYYyaQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19" - } - }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "Jpe5up2VLMkjKavOXZzWRXBvy8SUPoFCRuVkq1wYrtVwtzuFEc7saw02dltASs5T8DXp6Ff/DPzmU95mDOiw6g==" - }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "3cnWzJ/FLEhLiW+TJTWtTjVj6fwlsP8OrbJqgOP4J9feAKjmT77vmClakHlej+jnzBgpnVU0+SZwtEIOr+KS6A==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "9.0.19", - "Microsoft.Extensions.Logging.Abstractions": "9.0.19", - "Microsoft.Extensions.Options": "9.0.19" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "RpqMVoZ+NECJxLOZRjWOfjST0mULs4wb848mxG/CS9SMkVL6yi1sSIda4dGcsB01J2jUFd8hNwsaYl1gplU+Cw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19" - } - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "FdPZy4kUSwFlIVZ3EkK9fdHw2RfblUwzflSb4kjVeGf9v2lX9IH+q2RGUovYMR7CVfKMc2y2Db/bZ2b4yiMfOg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19", - "Microsoft.Extensions.Primitives": "9.0.19" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "9hIg8PQiMnpVFIsEHm25Wi1gBrPa2pS1G75uveyDUVLXrNKqBap9WGwQIgO0s6LUgRAOdsSYnbKbNC5b1G5Pcg==" - }, - "Mono.TextTemplating": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "YqueG52R/Xej4VVbKuRIodjiAhV0HR/XVbLbNrJhCZnzjnSjgMJ/dCdV0akQQxavX6hp/LC6rqLGLcXeQYU7XA==", - "dependencies": { - "System.CodeDom": "6.0.0" - } - }, - "Npgsql": { - "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.2" - } - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==" - }, - "System.Composition": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "tRwgcAkDd85O8Aq6zHDANzQaq380cek9lbMg5Qma46u5BZXq/G+XvIYmu+UI+BIIZ9zssXLYrkTykEqxxvhcmg==", - "dependencies": { - "System.Composition.AttributedModel": "7.0.0", - "System.Composition.Convention": "7.0.0", - "System.Composition.Hosting": "7.0.0", - "System.Composition.Runtime": "7.0.0", - "System.Composition.TypedParts": "7.0.0" - } - }, - "System.Composition.AttributedModel": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "2QzClqjElKxgI1jK1Jztnq44/8DmSuTSGGahXqQ4TdEV0h9s2KikQZIgcEqVzR7OuWDFPGLHIprBJGQEPr8fAQ==" - }, - "System.Composition.Convention": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "IMhTlpCs4HmlD8B+J8/kWfwX7vrBBOs6xyjSTzBlYSs7W4OET4tlkR/Sg9NG8jkdJH9Mymq0qGdYS1VPqRTBnQ==", - "dependencies": { - "System.Composition.AttributedModel": "7.0.0" - } - }, - "System.Composition.Hosting": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "eB6gwN9S+54jCTBJ5bpwMOVerKeUfGGTYCzz3QgDr1P55Gg/Wb27ShfPIhLMjmZ3MoAKu8uUSv6fcCdYJTN7Bg==", - "dependencies": { - "System.Composition.Runtime": "7.0.0" - } - }, - "System.Composition.Runtime": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "aZJ1Zr5Txe925rbo4742XifEyW0MIni1eiUebmcrP3HwLXZ3IbXUj4MFMUH/RmnJOAQiS401leg/2Sz1MkApDw==" - }, - "System.Composition.TypedParts": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "ZK0KNPfbtxVceTwh+oHNGUOYV2WNOHReX2AXipuvkURC7s/jPwoWfsu3SnDBDgofqbiWr96geofdQ2erm/KTHg==", - "dependencies": { - "System.Composition.AttributedModel": "7.0.0", - "System.Composition.Hosting": "7.0.0", - "System.Composition.Runtime": "7.0.0" - } - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg==" - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==", - "dependencies": { - "System.Collections.Immutable": "7.0.0" - } - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" - }, - "System.Text.Json": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "DjVVcIY/dS/rjb0b42nciHjUc0voQfUtrTos+wC0sgdx0VemTpYhx1jdeHowwFghygJ6QdaytcMHwA8jZ6MHFg==" - }, - "System.Threading.Channels": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" - }, - "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 d09df39..845329d 100644 --- a/src/Atm.Presentation/Atm.Presentation.csproj +++ b/src/Atm.Presentation/Atm.Presentation.csproj @@ -1,23 +1,11 @@ - + - net9.0 - enable - enable + Library - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - - - - - - - + \ 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 cb4f71e..0000000 --- a/src/Atm.Presentation/Contracts/ApiRequests.cs +++ /dev/null @@ -1,22 +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, - [property: JsonPropertyName("ownerUserId")] Guid OwnerUserId); - -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 c2aaf64..0000000 --- a/src/Atm.Presentation/Contracts/ApiResponses.cs +++ /dev/null @@ -1,19 +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); diff --git a/src/Atm.Presentation/Controllers/AccountsController.cs b/src/Atm.Presentation/Controllers/AccountsController.cs deleted file mode 100644 index 2b550f9..0000000 --- a/src/Atm.Presentation/Controllers/AccountsController.cs +++ /dev/null @@ -1,129 +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, - request.OwnerUserId)); - - 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/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/Program.cs b/src/Atm.Presentation/Program.cs index 74a7068..e69de29 100644 --- a/src/Atm.Presentation/Program.cs +++ b/src/Atm.Presentation/Program.cs @@ -1,41 +0,0 @@ -using Atm.Application; -using Atm.Application.Abstractions.Repositories; -using Atm.Infrastructure; -using Atm.Presentation.Common; -using ApplicationSessionOptions = Atm.Application.Sessions.SessionOptions; - -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.AddOptions() - .Bind(builder.Configuration.GetSection(ApplicationSessionOptions.SectionName)) - .Validate(options => options.UserLifetimeMinutes > 0, "User session lifetime must be positive.") - .Validate(options => options.AdminLifetimeMinutes > 0, "Admin session lifetime must be positive.") - .ValidateOnStart(); - -builder.Services.AddSingleton(); - -WebApplication app = builder.Build(); - -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - -app.UseHttpsRedirection(); -app.MapControllers(); - -app.Run(); diff --git a/src/Atm.Presentation/packages.lock.json b/src/Atm.Presentation/packages.lock.json deleted file mode 100644 index 12bea76..0000000 --- a/src/Atm.Presentation/packages.lock.json +++ /dev/null @@ -1,429 +0,0 @@ -{ - "version": 1, - "dependencies": { - "net9.0": { - "Microsoft.EntityFrameworkCore.Design": { - "type": "Direct", - "requested": "[9.0.19, )", - "resolved": "9.0.19", - "contentHash": "O4o69Xne6dJ7+u21YPxevGt8F2wpxGz2bBiSa/SSyqb2/64OZtcbIOB264fuViRj2XgwzhSGErNP75fRVCk8Zw==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.Build.Framework": "17.8.43", - "Microsoft.Build.Locator": "1.7.8", - "Microsoft.CodeAnalysis.CSharp": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Workspaces": "4.8.0", - "Microsoft.CodeAnalysis.Workspaces.MSBuild": "4.8.0", - "Microsoft.EntityFrameworkCore.Relational": "9.0.19", - "Microsoft.Extensions.Caching.Memory": "9.0.19", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.19", - "Microsoft.Extensions.DependencyModel": "9.0.19", - "Microsoft.Extensions.Logging": "9.0.19", - "Mono.TextTemplating": "3.0.0", - "System.Text.Json": "9.0.19" - } - }, - "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" - } - }, - "Humanizer.Core": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" - }, - "Microsoft.AspNetCore.Cryptography.Internal": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "zqiWdZ8Jw4/sfTBSFHchU3Iajm8EqLPUq/cUIuUjARwNGJ9hrKR7Yf0bs4WlF+GdPX6Bj7kmaf99aY0OTbLCOg==" - }, - "Microsoft.AspNetCore.Cryptography.KeyDerivation": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "w0ZZWnsz7U2dh5l9eytJYYHG3/HwafewFyZT+NsnBuQAt90cw2hmiX9J9OU1mrtn7EiIJqhRxjzY5kAbmhD2DA==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.Internal": "9.0.19" - } - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "3aeMZ1N0lJoSyzqiP03hqemtb1BijhsJADdobn/4nsMJ8V1H+CrpuduUe4hlRdx+ikBQju1VGjMD1GJ3Sk05Eg==" - }, - "Microsoft.Build.Framework": { - "type": "Transitive", - "resolved": "17.8.43", - "contentHash": "TA6fCbd9qFSyhIKcUn5BhDN3/JrqpspO9KYsZKUvHyQpQrHVdzy4Hk6egjjBP3T0L562TLZuHEzQwJziyRBEkw==" - }, - "Microsoft.Build.Locator": { - "type": "Transitive", - "resolved": "1.7.8", - "contentHash": "sPy10x527Ph16S2u0yGME4S6ohBKJ69WfjeGG/bvELYeZVmJdKjxgnlL8cJJJLGV/cZIRqSfB12UDB8ICakOog==" - }, - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Transitive", - "resolved": "3.3.4", - "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" - }, - "Microsoft.CodeAnalysis.Common": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.3.4", - "System.Collections.Immutable": "7.0.0", - "System.Reflection.Metadata": "7.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "Microsoft.CodeAnalysis.CSharp": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "+3+qfdb/aaGD8PZRCrsdobbzGs1m9u119SkkJt8e/mk3xLJz/udLtS2T6nY27OTXxBBw10HzAbC8Z9w08VyP/g==", - "dependencies": { - "Microsoft.CodeAnalysis.Common": "[4.8.0]" - } - }, - "Microsoft.CodeAnalysis.CSharp.Workspaces": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "3amm4tq4Lo8/BGvg9p3BJh3S9nKq2wqCXfS7138i69TUpo/bD+XvD0hNurpEBtcNZhi1FyutiomKJqVF39ugYA==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.CodeAnalysis.CSharp": "[4.8.0]", - "Microsoft.CodeAnalysis.Common": "[4.8.0]", - "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]" - } - }, - "Microsoft.CodeAnalysis.Workspaces.Common": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "LXyV+MJKsKRu3FGJA3OmSk40OUIa/dQCFLOnm5X8MNcujx7hzGu8o+zjXlb/cy5xUdZK2UKYb9YaQ2E8m9QehQ==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.Bcl.AsyncInterfaces": "7.0.0", - "Microsoft.CodeAnalysis.Common": "[4.8.0]", - "System.Composition": "7.0.0", - "System.IO.Pipelines": "7.0.0", - "System.Threading.Channels": "7.0.0" - } - }, - "Microsoft.CodeAnalysis.Workspaces.MSBuild": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "IEYreI82QZKklp54yPHxZNG9EKSK6nHEkeuf+0Asie9llgS1gp0V1hw7ODG+QyoB7MuAnNQHmeV1Per/ECpv6A==", - "dependencies": { - "Microsoft.Build.Framework": "16.10.0", - "Microsoft.CodeAnalysis.Common": "[4.8.0]", - "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]", - "System.Text.Json": "7.0.3" - } - }, - "Microsoft.EntityFrameworkCore": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "qWqqxrfEDfi3C/z0S/ACIA2gUnreRPHX74WE/dVnesbztNesYO4NLy76lE6FV/zDP4vH/pkvDr8h3Mq9+cFv/w==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "9.0.19", - "Microsoft.EntityFrameworkCore.Analyzers": "9.0.19", - "Microsoft.Extensions.Caching.Memory": "9.0.19", - "Microsoft.Extensions.Logging": "9.0.19" - } - }, - "Microsoft.EntityFrameworkCore.Abstractions": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "igPnmVL3DU2ZH/4DNeQqWttNrQpwwg0q6MWj5peZxtl0QVMDSeFtnt2yhhdSffklZKYsAFko1sh/va3z4y7S6A==" - }, - "Microsoft.EntityFrameworkCore.Analyzers": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "yI5dQuvigJ6ICQECOlJDtrsUYcuDryv3qsZK1i6ZA/IzMzZ/VPkDzjE4r2LivuyVSGnkDRkH2cp4LmrhXARRKw==" - }, - "Microsoft.EntityFrameworkCore.Relational": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "KAfYGG4LWRNn1G/vMRydxgOjuZoWUvdd5raUcIia/HPg1jUjlQzS386nUx5gt3YfKxpFJaCGxdsQ1nCZWuYR8A==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "9.0.19", - "Microsoft.Extensions.Caching.Memory": "9.0.19", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.19", - "Microsoft.Extensions.Logging": "9.0.19" - } - }, - "Microsoft.Extensions.ApiDescription.Server": { - "type": "Transitive", - "resolved": "6.0.5", - "contentHash": "Ckb5EDBUNJdFWyajfXzUIMRkhf52fHZOQuuZg/oiu8y7zDCVwD0iHhew6MnThjHmevanpxL3f5ci2TtHQEN6bw==" - }, - "Microsoft.Extensions.Caching.Abstractions": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "qe/6m6wxRG+N5xTf4Z3lDBAiVu6oBCUyADJ5OzlFgukD9Vg/6zB3MNnNEFA7amZg8hjcS6F4OZBRmB07A+gmHQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.19" - } - }, - "Microsoft.Extensions.Caching.Memory": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "Y0w5LkBqWkDh5vYTKM+QKlzxpqegEtP86oDhFMXgx4tDkWL2t+rl4h2vxBHkvBtM/gM6PoLK9QCrnoclXtHtVQ==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "9.0.19", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19", - "Microsoft.Extensions.Logging.Abstractions": "9.0.19", - "Microsoft.Extensions.Options": "9.0.19", - "Microsoft.Extensions.Primitives": "9.0.19" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "M6h5lX6QWSSn7FDeRUHePZen/xvxPsuhcX1VpBfefafqavn2BnfxheR7BH5D5AIZGM7Nah0xJKnq2DublD4doQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.19" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "o/XFYSiV4a7eiCkf7W1GFAviVhHQAPY5bHrQRsNWAPT3GhJ3Af63vcFQjHivIrTNZITlPPaGWhBy7jykKYYyaQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "XgQ0aVWClYjYSBqbYrKC/xyZ1KHf+VVwNF3+d54jcaqnst30Q//ugs/FJwUztLZiph0gp85g7i/eB0556twO0Q==" - }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "Jpe5up2VLMkjKavOXZzWRXBvy8SUPoFCRuVkq1wYrtVwtzuFEc7saw02dltASs5T8DXp6Ff/DPzmU95mDOiw6g==" - }, - "Microsoft.Extensions.Identity.Core": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "6P2/iMA35j8RrjsJXhqaOy4Sturcu+NXzXSlKguGTzjBEVRf6atWPrKkUuZew9K5S5NbNh8gdYip2dXm8t5fLg==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.19", - "Microsoft.Extensions.Logging": "9.0.19", - "Microsoft.Extensions.Options": "9.0.19" - } - }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "3cnWzJ/FLEhLiW+TJTWtTjVj6fwlsP8OrbJqgOP4J9feAKjmT77vmClakHlej+jnzBgpnVU0+SZwtEIOr+KS6A==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "9.0.19", - "Microsoft.Extensions.Logging.Abstractions": "9.0.19", - "Microsoft.Extensions.Options": "9.0.19" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "RpqMVoZ+NECJxLOZRjWOfjST0mULs4wb848mxG/CS9SMkVL6yi1sSIda4dGcsB01J2jUFd8hNwsaYl1gplU+Cw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19" - } - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "FdPZy4kUSwFlIVZ3EkK9fdHw2RfblUwzflSb4kjVeGf9v2lX9IH+q2RGUovYMR7CVfKMc2y2Db/bZ2b4yiMfOg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19", - "Microsoft.Extensions.Primitives": "9.0.19" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "9hIg8PQiMnpVFIsEHm25Wi1gBrPa2pS1G75uveyDUVLXrNKqBap9WGwQIgO0s6LUgRAOdsSYnbKbNC5b1G5Pcg==" - }, - "Microsoft.OpenApi": { - "type": "Transitive", - "resolved": "1.6.14", - "contentHash": "tTaBT8qjk3xINfESyOPE2rIellPvB7qpVqiWiyA/lACVvz+xOGiXhFUfohcx82NLbi5avzLW0lx+s6oAqQijfw==" - }, - "Mono.TextTemplating": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "YqueG52R/Xej4VVbKuRIodjiAhV0HR/XVbLbNrJhCZnzjnSjgMJ/dCdV0akQQxavX6hp/LC6rqLGLcXeQYU7XA==", - "dependencies": { - "System.CodeDom": "6.0.0" - } - }, - "Npgsql": { - "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.2" - } - }, - "Npgsql.EntityFrameworkCore.PostgreSQL": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)", - "Npgsql": "9.0.3" - } - }, - "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==" - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==" - }, - "System.Composition": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "tRwgcAkDd85O8Aq6zHDANzQaq380cek9lbMg5Qma46u5BZXq/G+XvIYmu+UI+BIIZ9zssXLYrkTykEqxxvhcmg==", - "dependencies": { - "System.Composition.AttributedModel": "7.0.0", - "System.Composition.Convention": "7.0.0", - "System.Composition.Hosting": "7.0.0", - "System.Composition.Runtime": "7.0.0", - "System.Composition.TypedParts": "7.0.0" - } - }, - "System.Composition.AttributedModel": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "2QzClqjElKxgI1jK1Jztnq44/8DmSuTSGGahXqQ4TdEV0h9s2KikQZIgcEqVzR7OuWDFPGLHIprBJGQEPr8fAQ==" - }, - "System.Composition.Convention": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "IMhTlpCs4HmlD8B+J8/kWfwX7vrBBOs6xyjSTzBlYSs7W4OET4tlkR/Sg9NG8jkdJH9Mymq0qGdYS1VPqRTBnQ==", - "dependencies": { - "System.Composition.AttributedModel": "7.0.0" - } - }, - "System.Composition.Hosting": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "eB6gwN9S+54jCTBJ5bpwMOVerKeUfGGTYCzz3QgDr1P55Gg/Wb27ShfPIhLMjmZ3MoAKu8uUSv6fcCdYJTN7Bg==", - "dependencies": { - "System.Composition.Runtime": "7.0.0" - } - }, - "System.Composition.Runtime": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "aZJ1Zr5Txe925rbo4742XifEyW0MIni1eiUebmcrP3HwLXZ3IbXUj4MFMUH/RmnJOAQiS401leg/2Sz1MkApDw==" - }, - "System.Composition.TypedParts": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "ZK0KNPfbtxVceTwh+oHNGUOYV2WNOHReX2AXipuvkURC7s/jPwoWfsu3SnDBDgofqbiWr96geofdQ2erm/KTHg==", - "dependencies": { - "System.Composition.AttributedModel": "7.0.0", - "System.Composition.Hosting": "7.0.0", - "System.Composition.Runtime": "7.0.0" - } - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg==" - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==", - "dependencies": { - "System.Collections.Immutable": "7.0.0" - } - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" - }, - "System.Text.Json": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "DjVVcIY/dS/rjb0b42nciHjUc0voQfUtrTos+wC0sgdx0VemTpYhx1jdeHowwFghygJ6QdaytcMHwA8jZ6MHFg==" - }, - "System.Threading.Channels": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" - }, - "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": "[9.0.19, )", - "Microsoft.Extensions.Options": "[9.0.19, )" - } - }, - "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.EntityFrameworkCore": "[9.0.19, )", - "Microsoft.EntityFrameworkCore.Relational": "[9.0.19, )", - "Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.19, )", - "Microsoft.Extensions.Identity.Core": "[9.0.19, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" - } - } - } - } -} \ No newline at end of file diff --git a/tests/Atm.Application.Tests/AccountAuthenticationFlowTests.cs b/tests/Atm.Application.Tests/AccountAuthenticationFlowTests.cs deleted file mode 100644 index 2790a13..0000000 --- a/tests/Atm.Application.Tests/AccountAuthenticationFlowTests.cs +++ /dev/null @@ -1,114 +0,0 @@ -using Atm.Application.Abstractions.Repositories; -using Atm.Application.Abstractions.Security; -using Atm.Application.Accounts; -using Atm.Application.Common; -using Atm.Application.Contracts.Accounts.CreateAccount; -using Atm.Application.Contracts.Sessions.CreateUserSession; -using Atm.Application.Sessions; -using Atm.Domain.Accounts; -using Atm.Domain.Sessions; -using Atm.Domain.ValueObjects; -using Microsoft.Extensions.Options; - -namespace Atm.Application.Tests; - -public sealed class AccountAuthenticationFlowTests -{ - [Fact] - public void CreatedAccountStoresOnlyHashAndAcceptsOnlyTheCorrectPin() - { - var accounts = new TestAccountRepository(); - var sessions = new TestSessionRepository(); - var clock = new FixedClock(); - var hasher = new TestPinHasher(); - var adminSessionId = Guid.NewGuid(); - var ownerUserId = Guid.NewGuid(); - sessions.Add(BankingTestData.CreateAdminSession(adminSessionId, clock)); - - var authorizer = new SessionAuthorizer(sessions, clock); - var createAccount = new CreateAccountUseCase(accounts, authorizer, hasher, clock); - CreateAccountResult created = createAccount.Execute(new CreateAccountRequest( - adminSessionId, " account-01 ", "1234", 1_000_000_000_000_000m, ownerUserId)); - - Assert.IsType(created); - Account storedAccount = Assert.IsType(accounts.Account); - Assert.Equal("ACCOUNT-01", storedAccount.Number.NormalizedValue); - Assert.Equal(ownerUserId, storedAccount.OwnerUserId); - Assert.NotEqual("1234", storedAccount.PinHash.Value); - Assert.True(hasher.Verify(storedAccount.PinHash, new PinCode("1234"))); - - IOptions options = Options.Create(new SessionOptions - { - UserLifetimeMinutes = 30, - AdminLifetimeMinutes = 15, - }); - var createSession = new CreateUserSessionUseCase(sessions, accounts, hasher, clock, options); - - Assert.IsType(createSession.Execute( - new CreateUserSessionRequest("account-01", "4321"))); - CreateUserSessionResult.Success signedIn = Assert.IsType( - createSession.Execute(new CreateUserSessionRequest("account-01", "1234"))); - - Session userSession = Assert.IsType(sessions.GetById(signedIn.SessionId)); - Assert.Equal(storedAccount.Id, userSession.AccountId); - Assert.True(userSession.IsActive(clock.UtcNow)); - Assert.Equal(clock.UtcNow.AddMinutes(30), userSession.ExpiresAtUtc); - } - - private sealed class TestAccountRepository : IAccountRepository - { - public Account? Account { get; private set; } - - public Account? GetById(Guid id) - { - return Account?.Id == id ? Account : null; - } - - public Account? GetByNumber(AccountNumber accountNumber) - { - return Account?.Number.NormalizedValue == accountNumber.NormalizedValue ? Account : null; - } - - public void Add(Account account) - { - Account = account; - } - - public void Update(Account account) - { - Account = account; - } - } - - private sealed class TestSessionRepository : ISessionRepository - { - private readonly Dictionary _sessions = new(); - - public Session? GetById(Guid id) - { - return _sessions.GetValueOrDefault(id); - } - - public void Add(Session session) - { - _sessions.Add(session.Id, session); - } - } - - private sealed class TestPinHasher : IPinHasher - { - private readonly Dictionary _pinsByHash = new(); - - public PinHash Hash(PinCode pin) - { - var hash = new PinHash($"test-hash-{Guid.NewGuid():N}"); - _pinsByHash.Add(hash.Value, pin.Value); - return hash; - } - - public bool Verify(PinHash hash, PinCode pin) - { - return _pinsByHash.GetValueOrDefault(hash.Value) == pin.Value; - } - } -} 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/BankingTestData.cs b/tests/Atm.Application.Tests/BankingTestData.cs deleted file mode 100644 index 3c3f56e..0000000 --- a/tests/Atm.Application.Tests/BankingTestData.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Atm.Domain.Accounts; -using Atm.Domain.Accounts.Results; -using Atm.Domain.Sessions; -using Atm.Domain.Sessions.Results; -using Atm.Domain.ValueObjects; - -namespace Atm.Application.Tests; - -internal static class BankingTestData -{ - public static Account CreateAccount(AccountNumber number, decimal balance) - { - return Assert.IsType(Account.Create( - Guid.NewGuid(), number, Guid.NewGuid(), - new PinHash("test-only-hash"), new Money(balance), new FixedClock().UtcNow)).Account; - } - - public static Session CreateUserSession(Guid id, Account account, FixedClock clock) - { - return Assert.IsType(Session.Create( - id, SessionType.User, account.Id, clock.UtcNow, clock.UtcNow.AddMinutes(30))).Session; - } - - public static Session CreateAdminSession(Guid id, FixedClock clock) - { - return Assert.IsType(Session.Create( - id, SessionType.Admin, null, clock.UtcNow, clock.UtcNow.AddMinutes(15))).Session; - } -} diff --git a/tests/Atm.Application.Tests/DepositUseCaseTests.cs b/tests/Atm.Application.Tests/DepositUseCaseTests.cs deleted file mode 100644 index 6edd8e5..0000000 --- a/tests/Atm.Application.Tests/DepositUseCaseTests.cs +++ /dev/null @@ -1,42 +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.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 clock = new FixedClock(); - var accountNumber = new AccountNumber("aboba"); - var sessionId = Guid.NewGuid(); - Account account = BankingTestData.CreateAccount(accountNumber, 100m); - - sessions.GetById(sessionId).Returns(BankingTestData.CreateUserSession(sessionId, account, clock)); - accounts.GetById(account.Id).Returns(account); - - var auth = new SessionAuthorizer(sessions, clock); - var useCase = new DepositUseCase(accounts, operations, auth, clock); - - DepositResult result = useCase.Execute(new DepositRequest(sessionId, 50m)); - - Assert.IsType(result); - Assert.Equal(150m, ((DepositResult.Success)result).Balance); - accounts.Received(1).Update(Arg.Is(item => item.Balance.Value == 150m)); - operations.Received(1).Add(Arg.Is(operation => - operation.AccountNumber == accountNumber && - operation.Type == OperationType.Deposit && - operation.Amount.Value == 50m && - operation.BalanceAfter.Value == 150m)); - } -} diff --git a/tests/Atm.Application.Tests/FixedClock.cs b/tests/Atm.Application.Tests/FixedClock.cs deleted file mode 100644 index af39324..0000000 --- a/tests/Atm.Application.Tests/FixedClock.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Atm.Application.Abstractions.Time; - -namespace Atm.Application.Tests; - -internal sealed class FixedClock : IClock -{ - public DateTimeOffset UtcNow { get; set; } = new(2026, 9, 2, 12, 0, 0, TimeSpan.Zero); -} diff --git a/tests/Atm.Application.Tests/SessionAuthorizerTests.cs b/tests/Atm.Application.Tests/SessionAuthorizerTests.cs deleted file mode 100644 index 8d3466d..0000000 --- a/tests/Atm.Application.Tests/SessionAuthorizerTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Atm.Application.Abstractions.Repositories; -using Atm.Application.Common; -using Atm.Domain.Accounts; -using Atm.Domain.Sessions; -using Atm.Domain.Sessions.Results; -using Atm.Domain.ValueObjects; -using NSubstitute; - -namespace Atm.Application.Tests; - -public sealed class SessionAuthorizerTests -{ - [Fact] - public void AuthorizerRejectsExpiredAndRevokedSessions() - { - ISessionRepository sessions = Substitute.For(); - var clock = new FixedClock(); - Account account = BankingTestData.CreateAccount(new AccountNumber("session-test"), 0m); - var sessionId = Guid.NewGuid(); - Session session = BankingTestData.CreateUserSession(sessionId, account, clock); - sessions.GetById(sessionId).Returns(session); - var authorizer = new SessionAuthorizer(sessions, clock); - - Assert.IsType(authorizer.AuthorizeUser(sessionId)); - - clock.UtcNow = session.ExpiresAtUtc; - Assert.IsType(authorizer.AuthorizeUser(sessionId)); - - clock.UtcNow = session.CreatedAtUtc; - Assert.IsType(session.Revoke(clock.UtcNow)); - Assert.IsType(authorizer.AuthorizeUser(sessionId)); - } -} diff --git a/tests/Atm.Application.Tests/WithdrawCaseTests.cs b/tests/Atm.Application.Tests/WithdrawCaseTests.cs deleted file mode 100644 index f9711d7..0000000 --- a/tests/Atm.Application.Tests/WithdrawCaseTests.cs +++ /dev/null @@ -1,66 +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.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 clock = new FixedClock(); - var accountNumber = new AccountNumber("aboba-2"); - var sessionId = Guid.NewGuid(); - Account account = BankingTestData.CreateAccount(accountNumber, 100m); - - sessions.GetById(sessionId).Returns(BankingTestData.CreateUserSession(sessionId, account, clock)); - accounts.GetById(account.Id).Returns(account); - - var auth = new SessionAuthorizer(sessions, clock); - var useCase = new WithdrawUseCase(accounts, operations, auth, clock); - - WithdrawResult result = useCase.Execute(new WithdrawRequest(sessionId, 40m)); - - Assert.IsType(result); - Assert.Equal(60m, ((WithdrawResult.Success)result).Balance); - accounts.Received(1).Update(Arg.Is(item => item.Balance.Value == 60m)); - operations.Received(1).Add(Arg.Is(operation => - operation.AccountNumber == accountNumber && - operation.Type == OperationType.Withdraw && - operation.Amount.Value == 40m && - operation.BalanceAfter.Value == 60m)); - } - - [Fact] - public void WithdrawWhenNotEnoughBalanceReturnsFailureAndDoesNotSave() - { - IAccountRepository accounts = Substitute.For(); - ISessionRepository sessions = Substitute.For(); - IOperationRepository operations = Substitute.For(); - var clock = new FixedClock(); - var accountNumber = new AccountNumber("aboba-666"); - var sessionId = Guid.NewGuid(); - Account account = BankingTestData.CreateAccount(accountNumber, 10m); - - sessions.GetById(sessionId).Returns(BankingTestData.CreateUserSession(sessionId, account, clock)); - accounts.GetById(account.Id).Returns(account); - - var auth = new SessionAuthorizer(sessions, clock); - var useCase = new WithdrawUseCase(accounts, operations, auth, clock); - - WithdrawResult result = useCase.Execute(new WithdrawRequest(sessionId, 20m)); - - Assert.IsType(result); - accounts.DidNotReceive().Update(Arg.Any()); - operations.DidNotReceive().Add(Arg.Any()); - } -} diff --git a/tests/Atm.Application.Tests/packages.lock.json b/tests/Atm.Application.Tests/packages.lock.json deleted file mode 100644 index efaa490..0000000 --- a/tests/Atm.Application.Tests/packages.lock.json +++ /dev/null @@ -1,175 +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": "9.0.19", - "contentHash": "XgQ0aVWClYjYSBqbYrKC/xyZ1KHf+VVwNF3+d54jcaqnst30Q//ugs/FJwUztLZiph0gp85g7i/eB0556twO0Q==" - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "FdPZy4kUSwFlIVZ3EkK9fdHw2RfblUwzflSb4kjVeGf9v2lX9IH+q2RGUovYMR7CVfKMc2y2Db/bZ2b4yiMfOg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19", - "Microsoft.Extensions.Primitives": "9.0.19" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "9hIg8PQiMnpVFIsEHm25Wi1gBrPa2pS1G75uveyDUVLXrNKqBap9WGwQIgO0s6LUgRAOdsSYnbKbNC5b1G5Pcg==" - }, - "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": "[9.0.19, )", - "Microsoft.Extensions.Options": "[9.0.19, )" - } - }, - "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/tests/Atm.Domain.Tests/AccountTests.cs b/tests/Atm.Domain.Tests/AccountTests.cs deleted file mode 100644 index 58a5e74..0000000 --- a/tests/Atm.Domain.Tests/AccountTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Atm.Domain.Accounts; -using Atm.Domain.Accounts.Results; -using Atm.Domain.ValueObjects; - -namespace Atm.Domain.Tests; - -public class AccountTests -{ - [Fact] - public void CreateRequiresAnOwnerAndUtcTime() - { - var number = new AccountNumber("test"); - var hash = new PinHash("test-only-hash"); - DateTimeOffset now = DomainTestData.UtcNow; - - Assert.IsType(Account.Create( - Guid.NewGuid(), number, Guid.Empty, hash, new Money(0m), now)); - Assert.IsType(Account.Create( - Guid.NewGuid(), number, Guid.NewGuid(), hash, new Money(0m), now.ToOffset(TimeSpan.FromHours(3)))); - } - - [Fact] - public void SuccessfulChangesAdvanceTheVersionAndTimestamp() - { - Account account = DomainTestData.CreateAccount(new AccountNumber("test"), 100m); - DateTimeOffset now = account.CreatedAtUtc.AddMinutes(1); - - Assert.IsType(account.Deposit(new Amount(10m), now)); - Assert.Equal(1, account.Version); - Assert.Equal(now, account.UpdatedAtUtc); - Assert.IsType(account.Withdraw(new Amount(20m), now.AddMinutes(1))); - Assert.Equal(2, account.Version); - Assert.Equal(90m, account.Balance.Value); - } - - [Fact] - public void FailedChangesDoNotMutateTheAccount() - { - Account account = DomainTestData.CreateAccount(new AccountNumber("test"), 100m); - DateTimeOffset now = account.CreatedAtUtc; - - Assert.IsType(account.Withdraw(new Amount(101m), now)); - Assert.IsType(account.Deposit(new Amount(1m), now.AddTicks(-1))); - Assert.Equal(100m, account.Balance.Value); - Assert.Equal(0, account.Version); - Assert.Equal(now, account.UpdatedAtUtc); - } - - [Fact] - public void LargeBalancesHaveNoBusinessCeilingButCannotLosePrecision() - { - Account account = DomainTestData.CreateAccount(new AccountNumber("large"), 1000000000000000m); - Assert.IsType(account.Deposit(new Amount(1m), account.CreatedAtUtc)); - Assert.Equal(1000000000000001m, account.Balance.Value); - - Account largest = DomainTestData.CreateAccount(new AccountNumber("largest"), decimal.MaxValue); - Assert.IsType(largest.Deposit(new Amount(1m), largest.CreatedAtUtc)); - Assert.IsType(largest.Deposit(new Amount(0.0001m), largest.CreatedAtUtc)); - Assert.IsType(largest.Withdraw(new Amount(0.0001m), largest.CreatedAtUtc)); - Assert.Equal(decimal.MaxValue, largest.Balance.Value); - Assert.Equal(0, largest.Version); - } -} diff --git a/tests/Atm.Domain.Tests/Atm.Domain.Tests.csproj b/tests/Atm.Domain.Tests/Atm.Domain.Tests.csproj deleted file mode 100644 index 69d6dd0..0000000 --- a/tests/Atm.Domain.Tests/Atm.Domain.Tests.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - net9.0 - enable - enable - false - - - - - - - - - - - - - - - - - - diff --git a/tests/Atm.Domain.Tests/BankTransactionTests.cs b/tests/Atm.Domain.Tests/BankTransactionTests.cs deleted file mode 100644 index f31d403..0000000 --- a/tests/Atm.Domain.Tests/BankTransactionTests.cs +++ /dev/null @@ -1,177 +0,0 @@ -using Atm.Domain.Operations; -using Atm.Domain.Operations.Results; -using Atm.Domain.ValueObjects; - -namespace Atm.Domain.Tests; - -public sealed class BankTransactionTests -{ - private static readonly DateTimeOffset Now = new(2026, 9, 4, 12, 0, 0, TimeSpan.Zero); - - [Fact] - public void CommissionSnapshotUsesBankersRoundingAndStoresTheTariff() - { - var roundDown = new CommissionSnapshot(new Amount(1.0001m), 0.5m, 0m); - var roundUp = new CommissionSnapshot(new Amount(1.0003m), 0.5m, 0m); - - Assert.Equal(0.5m, roundDown.Rate); - Assert.Equal(0m, roundDown.FixedAmount.Value); - Assert.Equal(0.5000m, roundDown.CommissionAmount.Value); - Assert.Equal(0.5002m, roundUp.CommissionAmount.Value); - } - - [Theory] - [InlineData(-0.000001)] - [InlineData(1.000001)] - [InlineData(0.1234567)] - public void CommissionSnapshotRejectsInvalidRate(decimal rate) - { - Assert.Throws(() => new CommissionSnapshot(new Amount(10m), rate, 0m)); - } - - [Fact] - public void CommissionSnapshotRejectsInvalidFixedAmountAndPrecisionLoss() - { - Assert.Throws(() => new CommissionSnapshot(new Amount(10m), 0m, -1m)); - Assert.Throws(() => new CommissionSnapshot(new Amount(10m), 0m, 0.00001m)); - Assert.Throws(() => - new CommissionSnapshot(new Amount(decimal.MaxValue), 1m, 0.0001m)); - } - - [Fact] - public void CreateEnforcesAccountShapeRequestIdAndUtc() - { - var source = Guid.NewGuid(); - var target = Guid.NewGuid(); - var amount = new Amount(10m); - var commission = new CommissionSnapshot(amount, 0m, 0m); - - Assert.IsType(BankTransaction.Create( - Guid.NewGuid(), Guid.Empty, BankTransactionType.Deposit, null, target, amount, commission, Now)); - Assert.IsType(BankTransaction.Create( - Guid.NewGuid(), Guid.NewGuid(), BankTransactionType.Deposit, source, target, amount, commission, Now)); - Assert.IsType(BankTransaction.Create( - Guid.NewGuid(), Guid.NewGuid(), BankTransactionType.Withdrawal, source, target, amount, commission, Now)); - Assert.IsType(BankTransaction.Create( - Guid.NewGuid(), Guid.NewGuid(), BankTransactionType.Transfer, source, source, amount, commission, Now)); - Assert.IsType(BankTransaction.Create( - Guid.NewGuid(), Guid.NewGuid(), BankTransactionType.Deposit, null, target, amount, commission, - Now.ToOffset(TimeSpan.FromHours(3)))); - } - - [Fact] - public void CreateRejectsMismatchedOrEconomicallyInvalidCommission() - { - var target = Guid.NewGuid(); - var originalAmount = new Amount(100m); - var mismatchedAmount = new Amount(101m); - var commission = new CommissionSnapshot(originalAmount, 0.1m, 0m); - - Assert.IsType(BankTransaction.Create( - Guid.NewGuid(), Guid.NewGuid(), BankTransactionType.Deposit, - null, target, mismatchedAmount, commission, Now)); - - var smallAmount = new Amount(1m); - var fullCommission = new CommissionSnapshot(smallAmount, 0m, 1m); - Assert.IsType(BankTransaction.Create( - Guid.NewGuid(), Guid.NewGuid(), BankTransactionType.Deposit, - null, target, smallAmount, fullCommission, Now)); - } - - [Fact] - public void HistoryEntryCalculatesDepositAndWithdrawalBalances() - { - var accountId = Guid.NewGuid(); - BankTransaction deposit = CreateTransaction( - BankTransactionType.Deposit, null, accountId, 100m, 0m, 10m); - AccountHistoryEntry depositEntry = Assert.IsType( - AccountHistoryEntry.Create(Guid.NewGuid(), deposit, accountId, new Money(10m))).Entry; - - Assert.Equal(AccountHistoryEntryType.Deposit, depositEntry.EntryType); - Assert.Equal(10m, depositEntry.BalanceBefore.Value); - Assert.Equal(100m, depositEntry.BalanceAfter.Value); - Assert.Equal(deposit.Id, depositEntry.TransactionId); - Assert.Equal(deposit.CreatedAtUtc, depositEntry.CreatedAtUtc); - - BankTransaction withdrawal = CreateTransaction( - BankTransactionType.Withdrawal, accountId, null, 20m, 0m, 2m); - AccountHistoryEntry withdrawalEntry = Assert.IsType( - AccountHistoryEntry.Create(Guid.NewGuid(), withdrawal, accountId, new Money(100m))).Entry; - - Assert.Equal(AccountHistoryEntryType.Withdrawal, withdrawalEntry.EntryType); - Assert.Equal(78m, withdrawalEntry.BalanceAfter.Value); - } - - [Fact] - public void HistoryEntryProducesTwoSidesOfATransfer() - { - var source = Guid.NewGuid(); - var target = Guid.NewGuid(); - BankTransaction transaction = CreateTransaction( - BankTransactionType.Transfer, source, target, 10m, 0m, 1m); - - AccountHistoryEntry debit = Assert.IsType( - AccountHistoryEntry.Create(Guid.NewGuid(), transaction, source, new Money(100m))).Entry; - AccountHistoryEntry credit = Assert.IsType( - AccountHistoryEntry.Create(Guid.NewGuid(), transaction, target, new Money(50m))).Entry; - - Assert.Equal(AccountHistoryEntryType.TransferDebit, debit.EntryType); - Assert.Equal(89m, debit.BalanceAfter.Value); - Assert.Equal(AccountHistoryEntryType.TransferCredit, credit.EntryType); - Assert.Equal(60m, credit.BalanceAfter.Value); - Assert.Equal(debit.TransactionId, credit.TransactionId); - } - - [Fact] - public void HistoryEntryFailureDoesNotChangeTheProvidedBalance() - { - var source = Guid.NewGuid(); - BankTransaction transaction = CreateTransaction( - BankTransactionType.Withdrawal, source, null, 10m, 0m, 1m); - var balance = new Money(10m); - - Assert.IsType( - AccountHistoryEntry.Create(Guid.NewGuid(), transaction, source, balance)); - Assert.Equal(10m, balance.Value); - Assert.IsType( - AccountHistoryEntry.Create(Guid.NewGuid(), transaction, Guid.NewGuid(), new Money(100m))); - } - - [Theory] - [InlineData(BankTransactionType.Deposit)] - [InlineData(BankTransactionType.Withdrawal)] - public void AbsentAccountMustBeNull(BankTransactionType type) - { - var amount = new Amount(10m); - var commission = new CommissionSnapshot(amount, 0m, 0m); - Guid? source = type == BankTransactionType.Deposit ? Guid.Empty : Guid.NewGuid(); - Guid? target = type == BankTransactionType.Withdrawal ? Guid.Empty : Guid.NewGuid(); - - Assert.IsType(BankTransaction.Create( - Guid.NewGuid(), Guid.NewGuid(), type, source, target, amount, commission, Now)); - } - - [Fact] - public void CommissionRejectsPrecisionLossBeforeExplicitRounding() - { - Assert.Throws(() => - new CommissionSnapshot(new Amount(decimal.MaxValue), 0.5m, 0m)); - var snapshot = new CommissionSnapshot(new Amount(1_234_567.1234m), 0.123456m, 0.01m); - Assert.Equal(decimal.Round((1_234_567.1234m * 0.123456m) + 0.01m, 4, - MidpointRounding.ToEven), snapshot.CommissionAmount.Value); - } - - private static BankTransaction CreateTransaction( - BankTransactionType type, - Guid? source, - Guid? target, - decimal amountValue, - decimal rate, - decimal fixedAmount) - { - var amount = new Amount(amountValue); - var commission = new CommissionSnapshot(amount, rate, fixedAmount); - return Assert.IsType(BankTransaction.Create( - Guid.NewGuid(), Guid.NewGuid(), type, source, target, amount, commission, Now)).Transaction; - } -} diff --git a/tests/Atm.Domain.Tests/DomainTestData.cs b/tests/Atm.Domain.Tests/DomainTestData.cs deleted file mode 100644 index e5e84a4..0000000 --- a/tests/Atm.Domain.Tests/DomainTestData.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Atm.Domain.Accounts; -using Atm.Domain.Accounts.Results; -using Atm.Domain.Sessions; -using Atm.Domain.Sessions.Results; -using Atm.Domain.ValueObjects; - -namespace Atm.Domain.Tests; - -internal static class DomainTestData -{ - public static DateTimeOffset UtcNow { get; } = - new(2026, 9, 2, 12, 0, 0, TimeSpan.Zero); - - public static Account CreateAccount(AccountNumber number, decimal balance) - { - return Assert.IsType(Account.Create( - Guid.NewGuid(), - number, - Guid.NewGuid(), - new PinHash("test-only-hash"), - new Money(balance), - UtcNow)).Account; - } - - public static Session CreateAdminSession(Guid id) - { - return Assert.IsType(Session.Create( - id, - SessionType.Admin, - null, - UtcNow, - UtcNow.AddMinutes(15))).Session; - } -} diff --git a/tests/Atm.Domain.Tests/SessionTests.cs b/tests/Atm.Domain.Tests/SessionTests.cs deleted file mode 100644 index 595e57e..0000000 --- a/tests/Atm.Domain.Tests/SessionTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Atm.Domain.Sessions; -using Atm.Domain.Sessions.Results; - -namespace Atm.Domain.Tests; - -public sealed class SessionTests -{ - [Fact] - public void CreateEnforcesIdentityShapeAndUtcLifetime() - { - DateTimeOffset now = DomainTestData.UtcNow; - - Assert.IsType(Session.Create( - Guid.Empty, SessionType.Admin, null, now, now.AddMinutes(1))); - Assert.IsType(Session.Create( - Guid.NewGuid(), SessionType.User, null, now, now.AddMinutes(1))); - Assert.IsType(Session.Create( - Guid.NewGuid(), SessionType.Admin, Guid.NewGuid(), now, now.AddMinutes(1))); - Assert.IsType(Session.Create( - Guid.NewGuid(), SessionType.Admin, null, now, now)); - Assert.IsType(Session.Create( - Guid.NewGuid(), - SessionType.Admin, - null, - now.ToOffset(TimeSpan.FromHours(3)), - now.AddMinutes(1))); - } - - [Fact] - public void IsActiveUsesInclusiveStartAndExclusiveExpiration() - { - DateTimeOffset now = DomainTestData.UtcNow; - Session session = DomainTestData.CreateAdminSession(Guid.NewGuid()); - - Assert.True(session.IsActive(now)); - Assert.True(session.IsActive(session.ExpiresAtUtc.AddTicks(-1))); - Assert.False(session.IsActive(session.ExpiresAtUtc)); - Assert.False(session.IsActive(now.AddTicks(-1))); - Assert.False(session.IsActive(now.ToOffset(TimeSpan.FromHours(3)))); - } - - [Fact] - public void RevokeIsIdempotentAndKeepsTheFirstTimestamp() - { - DateTimeOffset now = DomainTestData.UtcNow; - Session session = DomainTestData.CreateAdminSession(Guid.NewGuid()); - DateTimeOffset first = now.AddMinutes(1); - - Assert.IsType(session.Revoke(now.AddTicks(-1))); - Assert.IsType(session.Revoke(first)); - SessionRevocationResult.Success repeated = Assert.IsType( - session.Revoke(first.AddMinutes(1))); - - Assert.Equal(first, repeated.RevokedAtUtc); - Assert.Equal(first, session.RevokedAtUtc); - Assert.False(session.IsActive(first)); - } -} diff --git a/tests/Atm.Domain.Tests/UserTests.cs b/tests/Atm.Domain.Tests/UserTests.cs deleted file mode 100644 index 9b6bff3..0000000 --- a/tests/Atm.Domain.Tests/UserTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Atm.Domain.Users; -using Atm.Domain.Users.Results; -using Atm.Domain.ValueObjects; - -namespace Atm.Domain.Tests; - -public sealed class UserTests -{ - private static readonly DateTimeOffset Now = new DateTimeOffset(2026, 9, 2, 12, 0, 0, TimeSpan.Zero); - - [Fact] - public void ValidProfileIsCreatedAndUpdated() - { - User user = CreateUser(); - Assert.Equal("Test User", user.Name); - Assert.IsType( - user.UpdateProfile(" Changed ", 30, UserGender.Other, HairColor.Black, Now.AddMinutes(1))); - Assert.Equal("Changed", user.Name); - Assert.Equal(Now.AddMinutes(1), user.UpdatedAtUtc); - Assert.Equal(Now, user.CreatedAtUtc); - } - - [Theory] - [InlineData(-1)] - [InlineData(151)] - public void InvalidProfileDoesNotPartiallyMutateState(short age) - { - User user = CreateUser(); - Assert.IsType( - user.UpdateProfile("Changed", age, UserGender.Other, HairColor.Black, Now.AddMinutes(1))); - Assert.Equal("Test User", user.Name); - Assert.Equal((short)25, user.Age); - Assert.Equal(HairColor.Unspecified, user.HairColor); - Assert.Equal(Now, user.UpdatedAtUtc); - } - - [Fact] - public void InvalidIdentifiersEnumsAndTimestampsAreRejected() - { - var login = new UserLogin("user"); - Assert.IsType( - User.Create(Guid.Empty, login, "Name", 25, UserGender.Unspecified, HairColor.Unspecified, Now)); - Assert.IsType( - User.Create(Guid.NewGuid(), login, "Name", 25, (UserGender)99, HairColor.Unspecified, Now)); - User user = CreateUser(); - Assert.IsType( - user.UpdateProfile("Name", 25, UserGender.Unspecified, HairColor.Unspecified, Now.AddSeconds(-1))); - Assert.IsType( - user.UpdateProfile("Name", 25, UserGender.Unspecified, HairColor.Unspecified, Now.ToOffset(TimeSpan.FromHours(3)))); - } - - [Fact] - public void FriendshipIsCanonicalAndRejectsSelfReference() - { - var first = Guid.Parse("00000100-0000-0000-0000-000000000001"); - var second = Guid.Parse("000000ff-ffff-ffff-ffff-ffffffffffff"); - Friendship forward = Assert.IsType( - Friendship.Create(first, second, Now)).Friendship; - Friendship reverse = Assert.IsType( - Friendship.Create(second, first, Now)).Friendship; - Assert.Equal(second, forward.UserId); - Assert.Equal(first, forward.FriendUserId); - Assert.Equal(forward.UserId, reverse.UserId); - Assert.Equal(forward.FriendUserId, reverse.FriendUserId); - Assert.IsType(Friendship.Create(first, first, Now)); - Assert.IsType(Friendship.Create(first, Guid.Empty, Now)); - } - - private static User CreateUser() - { - return Assert.IsType( - User.Create(Guid.NewGuid(), new UserLogin("user"), " Test User ", 25, - UserGender.Unspecified, HairColor.Unspecified, Now)).User; - } -} diff --git a/tests/Atm.Domain.Tests/ValueObjectTests.cs b/tests/Atm.Domain.Tests/ValueObjectTests.cs deleted file mode 100644 index 2baa8af..0000000 --- a/tests/Atm.Domain.Tests/ValueObjectTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Atm.Domain.ValueObjects; -using System.Globalization; - -namespace Atm.Domain.Tests; - -public sealed class ValueObjectTests -{ - [Fact] - public void NormalizationIsCultureIndependent() - { - CultureInfo original = CultureInfo.CurrentCulture; - try - { - CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("tr-TR"); - var number = new AccountNumber(" idi-01 "); - var login = new UserLogin(" idi "); - Assert.Equal("idi-01", number.Value); - Assert.Equal("IDI-01", number.NormalizedValue); - Assert.Equal("IDI", login.NormalizedValue); - } - finally - { - CultureInfo.CurrentCulture = original; - } - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - public void EmptyIdentifiersAreRejected(string value) - { - Assert.Throws(() => new AccountNumber(value)); - Assert.Throws(() => new UserLogin(value)); - } - - [Fact] - public void LongIdentifiersAreRejected() - { - string value = new string('a', 65); - Assert.Throws(() => new AccountNumber(value)); - Assert.Throws(() => new UserLogin(value)); - } - - [Fact] - public void MoneyHasNoNumeric19BusinessCeiling() - { - var money = new Money(1000000000000000m); - Assert.Equal(1000000000000001m, money.Add(new Amount(1m)).Value); - Assert.Equal(decimal.MaxValue, new Money(decimal.MaxValue).Value); - } - - [Fact] - public void InvalidAmountsAndPrecisionAreRejected() - { - Assert.Throws(() => new Amount(0m)); - Assert.Throws(() => new Amount(-1m)); - Assert.Throws(() => new Amount(1.00001m)); - Assert.Throws(() => new Money(-1m)); - Assert.Throws(() => new Money(1.00001m)); - } - - [Fact] - public void ArithmeticDoesNotSilentlyLoseSmallAmounts() - { - var money = new Money(decimal.MaxValue); - Assert.Throws(() => money.Add(new Amount(1m))); - Assert.Throws(() => money.Add(new Amount(0.0001m))); - Assert.Throws(() => money.Subtract(new Amount(0.0001m))); - } - - [Theory] - [InlineData("123")] - [InlineData("abcd")] - [InlineData("1234")] - public void InvalidPinIsRejectedWithoutEchoingInput(string pin) - { - ArgumentException exception = Assert.Throws(() => new PinCode(pin)); - Assert.DoesNotContain(pin, exception.Message, StringComparison.Ordinal); - } -} diff --git a/tests/Atm.Domain.Tests/packages.lock.json b/tests/Atm.Domain.Tests/packages.lock.json deleted file mode 100644 index 04bcc42..0000000 --- a/tests/Atm.Domain.Tests/packages.lock.json +++ /dev/null @@ -1,115 +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" - } - }, - "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==" - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "17.12.0", - "contentHash": "4svMznBd5JM21JIG2xZKGNanAHNXplxf/kQDFfLHXQ3OnpJkayRK/TjacFjA+EYmoyuNXHo/sOETEfcYtAzIrA==" - }, - "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.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.domain": { - "type": "Project" - } - } - } -} \ No newline at end of file diff --git a/tests/Atm.Infrastructure.Tests/Atm.Infrastructure.Tests.csproj b/tests/Atm.Infrastructure.Tests/Atm.Infrastructure.Tests.csproj deleted file mode 100644 index f207203..0000000 --- a/tests/Atm.Infrastructure.Tests/Atm.Infrastructure.Tests.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - net9.0 - enable - enable - false - - - - - - - - - - - - - - - - - - diff --git a/tests/Atm.Infrastructure.Tests/Persistence/AtmModelTests.cs b/tests/Atm.Infrastructure.Tests/Persistence/AtmModelTests.cs deleted file mode 100644 index 92564b4..0000000 --- a/tests/Atm.Infrastructure.Tests/Persistence/AtmModelTests.cs +++ /dev/null @@ -1,117 +0,0 @@ -using Atm.Domain.Accounts; -using Atm.Domain.Operations; -using Atm.Domain.ValueObjects; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; - -namespace Atm.Infrastructure.Tests.Persistence; - -public sealed class AtmModelTests -{ - [Fact] - public void ModelContainsExactlyTheRequiredTables() - { - using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); - IModel model = context.GetService().Model; - string[] tables = model.GetRelationalModel().Tables.Select(table => table.Name).Order().ToArray(); - Assert.Equal(new[] - { - "account_history_entries", "accounts", "bank_transactions", "friendships", "sessions", "users", - }, tables); - Assert.Null(model.FindEntityType(typeof(OperationRecord))); - Assert.All(model.GetRelationalModel().Tables, table => Assert.Equal("public", table.Schema)); - Assert.All(model.GetEntityTypes().Where(entity => !entity.IsOwned()), entity => - { - Assert.NotNull(entity.FindPrimaryKey()); - Assert.All(entity.FindPrimaryKey()!.Properties, property => - Assert.Equal(ValueGenerated.Never, property.ValueGenerated)); - }); - } - - [Fact] - public void SchemaScriptContainsConstraintsAndNoMoneyPrecisionCeiling() - { - using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); - string sql = context.Database.GenerateCreateScript(); - Assert.Contains("CREATE TABLE public.users", sql, StringComparison.Ordinal); - Assert.Contains("user_id < friend_user_id", sql, StringComparison.Ordinal); - Assert.Contains("ON DELETE RESTRICT", sql, StringComparison.Ordinal); - Assert.Contains("ON DELETE CASCADE", sql, StringComparison.Ordinal); - Assert.Contains("balance numeric NOT NULL", sql, StringComparison.Ordinal); - Assert.Contains("commission_rate numeric(9,6) NOT NULL", sql, StringComparison.Ordinal); - Assert.DoesNotContain("numeric(19,4)", sql, StringComparison.Ordinal); - Assert.Contains("trunc(balance, 4)", sql, StringComparison.Ordinal); - Assert.Contains("normalized_login", sql, StringComparison.Ordinal); - Assert.Contains("normalized_number", sql, StringComparison.Ordinal); - } - - [Fact] - public void VersionIndexesAndDeletionRulesMatchTheSpecification() - { - using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); - IModel model = context.GetService().Model; - IEntityType account = model.FindEntityType(typeof(Account))!; - Assert.True(account.FindProperty(nameof(Account.Version))!.IsConcurrencyToken); - Assert.All(model.GetEntityTypes().Where(entity => !entity.IsOwned()) - .SelectMany(entity => entity.GetForeignKeys()) - .Where(key => key.DeclaringEntityType.GetTableName() != "friendships"), - key => Assert.Equal(DeleteBehavior.Restrict, key.DeleteBehavior)); - - IIndex timeline = model.FindEntityType(typeof(AccountHistoryEntry))!.GetIndexes() - .Single(index => index.GetDatabaseName() == "ix_account_history_entries_account_timeline"); - Assert.Equal(new[] { false, true, true }, timeline.IsDescending); - Assert.Equal(new[] { "AccountId", "CreatedAtUtc", "Id" }, timeline.Properties.Select(property => property.Name)); - Assert.True(model.FindEntityType(typeof(BankTransaction))!.GetIndexes() - .Single(index => index.Properties.Single().Name == "RequestId").IsUnique); - Assert.Equal(2, model.FindEntityType(typeof(AccountHistoryEntry))!.GetKeys().Count()); - } - - [Fact] - public void ValueConversionsRoundTripAndCompareByValue() - { - using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); - IProperty balance = context.Model.FindEntityType(typeof(Account))!.FindProperty(nameof(Account.Balance))!; - var original = new Money(1_000_000_000_000_001.0001m); - Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter converter = balance.GetTypeMapping().Converter!; - object? databaseValue = converter.ConvertToProvider(original); - Money restored = Assert.IsType(converter.ConvertFromProvider(databaseValue)); - Assert.Equal(original.Value, restored.Value); - Assert.True(balance.GetValueComparer()!.Equals(original, restored)); - Assert.False(balance.GetValueComparer()!.Equals(original, new Money(1m))); - - IProperty amount = context.Model.FindEntityType(typeof(BankTransaction))!.FindProperty("Amount")!; - Amount restoredAmount = Assert.IsType( - amount.GetTypeMapping().Converter!.ConvertFromProvider(0.0001m)); - Assert.Equal(0.0001m, restoredAmount.Value); - } - - [Fact] - public void NormalizedLookupsAndHistoryPaginationTranslateToSql() - { - using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); - string users = context.Users.Where(user => user.Login.NormalizedValue == "USER").ToQueryString(); - string accounts = context.Accounts.Where(account => account.Number.NormalizedValue == "ACCOUNT").ToQueryString(); - string history = context.AccountHistoryEntries.Where(entry => entry.AccountId == Guid.Empty) - .OrderByDescending(entry => entry.CreatedAtUtc).ThenByDescending(entry => entry.Id) - .Take(20).ToQueryString(); - Assert.Contains("normalized_login", users, StringComparison.Ordinal); - Assert.Contains("normalized_number", accounts, StringComparison.Ordinal); - Assert.Contains("DESC", history, StringComparison.Ordinal); - Assert.Contains("LIMIT", history, StringComparison.Ordinal); - } - - [Theory] - [InlineData("users", "normalized_login", "ux_users_normalized_login")] - [InlineData("accounts", "normalized_number", "ux_accounts_normalized_number")] - [InlineData("bank_transactions", "request_id", "ux_bank_transactions_request_id")] - public void BusinessKeysHaveRequiredUniqueIndexes(string tableName, string columnName, string indexName) - { - using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); - ITable table = context.GetService().Model.GetRelationalModel().FindTable(tableName, "public")!; - Assert.False(table.FindColumn(columnName)!.IsNullable); - ITableIndex index = table.Indexes.Single(value => value.Name == indexName); - Assert.True(index.IsUnique); - Assert.Equal(columnName, Assert.Single(index.Columns).Name); - } -} diff --git a/tests/Atm.Infrastructure.Tests/Persistence/DesignTimeFactoryTests.cs b/tests/Atm.Infrastructure.Tests/Persistence/DesignTimeFactoryTests.cs deleted file mode 100644 index dc3d718..0000000 --- a/tests/Atm.Infrastructure.Tests/Persistence/DesignTimeFactoryTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Atm.Infrastructure.Persistence.DesignTime; -using Microsoft.EntityFrameworkCore; -using Npgsql; - -namespace Atm.Infrastructure.Tests.Persistence; - -[Collection("Environment configuration")] -public sealed class DesignTimeFactoryTests -{ - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("UnknownOption=secret-value")] - public void InvalidConfigurationFailsWithoutExposingValues(string? value) - { - string? previous = Environment.GetEnvironmentVariable(AtmDbContextFactory.ConnectionStringVariable); - try - { - Environment.SetEnvironmentVariable(AtmDbContextFactory.ConnectionStringVariable, value); - var factory = new AtmDbContextFactory(); - InvalidOperationException error = Assert.Throws(() => factory.CreateDbContext([])); - Assert.DoesNotContain("secret-value", error.ToString(), StringComparison.Ordinal); - Assert.Null(error.InnerException); - } - finally - { - Environment.SetEnvironmentVariable(AtmDbContextFactory.ConnectionStringVariable, previous); - } - } - - [Fact] - public void FactoryCreatesNpgsqlContextWithoutOpeningAConnection() - { - string? previous = Environment.GetEnvironmentVariable(AtmDbContextFactory.ConnectionStringVariable); - try - { - var settings = new NpgsqlConnectionStringBuilder { Host = "localhost", Database = "model_tests" }; - Environment.SetEnvironmentVariable(AtmDbContextFactory.ConnectionStringVariable, settings.ConnectionString); - using Infrastructure.Persistence.AtmDbContext context = new AtmDbContextFactory().CreateDbContext([]); - Assert.Equal("Npgsql.EntityFrameworkCore.PostgreSQL", context.Database.ProviderName); - Assert.Equal(System.Data.ConnectionState.Closed, context.Database.GetDbConnection().State); - Assert.NotEmpty(context.Model.GetEntityTypes()); - } - finally - { - Environment.SetEnvironmentVariable(AtmDbContextFactory.ConnectionStringVariable, previous); - } - } -} - -[CollectionDefinition("Environment configuration", DisableParallelization = true)] -public sealed class EnvironmentConfigurationCollection; diff --git a/tests/Atm.Infrastructure.Tests/Persistence/FinancialWriteGuardTests.cs b/tests/Atm.Infrastructure.Tests/Persistence/FinancialWriteGuardTests.cs deleted file mode 100644 index 5aea0d7..0000000 --- a/tests/Atm.Infrastructure.Tests/Persistence/FinancialWriteGuardTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Atm.Domain.Accounts; -using Atm.Domain.Accounts.Results; -using Atm.Domain.Operations; -using Atm.Domain.Operations.Results; -using Atm.Domain.ValueObjects; - -namespace Atm.Infrastructure.Tests.Persistence; - -public sealed class FinancialWriteGuardTests -{ - private static readonly DateTimeOffset Now = new(2026, 9, 6, 12, 0, 0, TimeSpan.Zero); - - [Fact] - public void DeletingAnAccountFailsBeforeDatabaseAccess() - { - using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); - Account account = CreateAccount(); - context.Attach(account); - context.Remove(account); - InvalidOperationException error = Assert.Throws(() => context.SaveChanges()); - Assert.Equal("Physical account deletion is not supported.", error.Message); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task UpdatingOrDeletingHistoryFailsBeforeDatabaseAccess(bool delete) - { - using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); - BankTransaction transaction = CreateTransaction(); - AccountHistoryEntry history = Assert.IsType( - AccountHistoryEntry.Create(Guid.NewGuid(), transaction, transaction.TargetAccountId!.Value, new Money(10m))).Entry; - context.Attach(history); - if (delete) - context.Remove(history); - else - context.Entry(history).Property(entry => entry.BalanceAfter).CurrentValue = new Money(20m); - - InvalidOperationException error = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); - Assert.Equal("Persisted financial records are immutable.", error.Message); - } - - [Fact] - public void ChangingTheCommissionSnapshotFailsBeforeDatabaseAccess() - { - using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); - BankTransaction transaction = CreateTransaction(); - context.Attach(transaction); - context.Entry(transaction).Reference(value => value.Commission).TargetEntry! - .Property(nameof(CommissionSnapshot.Rate)).CurrentValue = 0.5m; - InvalidOperationException error = Assert.Throws(() => context.SaveChanges()); - Assert.Equal("Persisted financial records are immutable.", error.Message); - } - - [Fact] - public void ChangingBalanceTracksTheVersionAndConvertedValue() - { - using Infrastructure.Persistence.AtmDbContext context = ModelTestContext.Create(); - Account account = CreateAccount(); - context.Attach(account); - account.Deposit(new Amount(2m), Now.AddMinutes(1)); - context.ChangeTracker.DetectChanges(); - Assert.True(context.Entry(account).Property(value => value.Balance).IsModified); - Assert.Equal(0, context.Entry(account).Property(value => value.Version).OriginalValue); - Assert.Equal(1, context.Entry(account).Property(value => value.Version).CurrentValue); - } - - private static Account CreateAccount() - { - return Assert.IsType(Account.Create( - Guid.NewGuid(), new AccountNumber("guard"), Guid.NewGuid(), new PinHash("test-hash"), - new Money(10m), Now)).Account; - } - - private static BankTransaction CreateTransaction() - { - var amount = new Amount(1m); - return Assert.IsType(BankTransaction.Create( - Guid.NewGuid(), Guid.NewGuid(), BankTransactionType.Deposit, null, Guid.NewGuid(), - amount, new CommissionSnapshot(amount, 0m, 0m), Now)).Transaction; - } -} diff --git a/tests/Atm.Infrastructure.Tests/Persistence/MaterializationTests.cs b/tests/Atm.Infrastructure.Tests/Persistence/MaterializationTests.cs deleted file mode 100644 index a096382..0000000 --- a/tests/Atm.Infrastructure.Tests/Persistence/MaterializationTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Atm.Domain.Accounts; -using Atm.Domain.Accounts.Results; -using Atm.Domain.Operations; -using Atm.Domain.Operations.Results; -using Atm.Domain.Sessions; -using Atm.Domain.Sessions.Results; -using Atm.Domain.Users; -using Atm.Domain.Users.Results; -using Atm.Domain.ValueObjects; -using Atm.Infrastructure.Persistence; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.Storage; - -namespace Atm.Infrastructure.Tests.Persistence; - -public sealed class MaterializationTests -{ - [Fact] - public void EfCanRestorePrivateConstructorsAndImmutableValueObjects() - { - using AtmDbContext context = ModelTestContext.Create(); - AddSampleEntities(context); - VerifyMaterialization(context); - } - - private static void AddSampleEntities(AtmDbContext context) - { - var now = new DateTimeOffset(2026, 9, 6, 12, 0, 0, TimeSpan.Zero); - User user = Assert.IsType(User.Create( - Guid.NewGuid(), new UserLogin("Alice"), "Alice", 30, - UserGender.Unspecified, HairColor.Unspecified, now)).User; - Account account = Assert.IsType(Account.Create( - Guid.NewGuid(), new AccountNumber("account-1"), user.Id, - new PinHash("test-hash"), new Money(10m), now)).Account; - var amount = new Amount(1m); - BankTransaction transaction = Assert.IsType(BankTransaction.Create( - Guid.NewGuid(), Guid.NewGuid(), BankTransactionType.Deposit, null, account.Id, - amount, new CommissionSnapshot(amount, 0m, 0m), now)).Transaction; - context.Add(user); - context.Add(account); - context.Add(transaction); - context.Add(Assert.IsType( - Friendship.Create(user.Id, Guid.NewGuid(), now)).Friendship); - context.Add(Assert.IsType( - Session.Create(Guid.NewGuid(), SessionType.User, account.Id, now, now.AddMinutes(30))).Session); - context.Add(Assert.IsType( - AccountHistoryEntry.Create(Guid.NewGuid(), transaction, account.Id, account.Balance)).Entry); - context.ChangeTracker.DetectChanges(); - } - - private static void VerifyMaterialization(AtmDbContext context) - { - IEntityMaterializerSource materializer = context.GetService(); - foreach (Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry entry in context.ChangeTracker.Entries()) - { - Microsoft.EntityFrameworkCore.Metadata.IProperty[] properties = entry.Metadata.GetProperties().ToArray(); - object[] values = new object[properties.Length]; - foreach (Microsoft.EntityFrameworkCore.Metadata.IProperty? property in properties) - values[property.GetIndex()] = entry.Property(property.Name).CurrentValue!; - - object restored = materializer.GetMaterializer(entry.Metadata)( - new MaterializationContext(new ValueBuffer(values), context)); - Assert.Equal(entry.Entity.GetType(), restored.GetType()); - foreach (Microsoft.EntityFrameworkCore.Metadata.IProperty? property in properties.Where(property => !property.IsShadowProperty())) - { - object? value = property.PropertyInfo!.GetValue(restored); - Assert.True(property.GetValueComparer()!.Equals(entry.Property(property.Name).CurrentValue, value)); - } - } - } -} diff --git a/tests/Atm.Infrastructure.Tests/Persistence/ModelTestContext.cs b/tests/Atm.Infrastructure.Tests/Persistence/ModelTestContext.cs deleted file mode 100644 index 1e2227a..0000000 --- a/tests/Atm.Infrastructure.Tests/Persistence/ModelTestContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Atm.Infrastructure.Persistence; -using Microsoft.EntityFrameworkCore; - -namespace Atm.Infrastructure.Tests.Persistence; - -internal static class ModelTestContext -{ - public static AtmDbContext Create() - { - return new AtmDbContext(new DbContextOptionsBuilder() - .UseNpgsql() - .Options); - } -} diff --git a/tests/Atm.Infrastructure.Tests/PinHasherTests.cs b/tests/Atm.Infrastructure.Tests/PinHasherTests.cs deleted file mode 100644 index 528b558..0000000 --- a/tests/Atm.Infrastructure.Tests/PinHasherTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Atm.Domain.ValueObjects; -using Atm.Infrastructure.Security; - -namespace Atm.Infrastructure.Tests; - -public class PinHasherTests -{ - [Fact] - public void HashUsesDifferentSaltsAndVerifiesThePin() - { - var hasher = new PinHasher(); - var pin = new PinCode("1234"); - - PinHash first = hasher.Hash(pin); - PinHash second = hasher.Hash(pin); - - Assert.NotEqual(pin.Value, first.Value); - Assert.NotEqual(first.Value, second.Value); - Assert.True(hasher.Verify(first, pin)); - Assert.True(hasher.Verify(second, pin)); - Assert.False(hasher.Verify(first, new PinCode("4321"))); - Assert.Equal("[REDACTED]", first.ToString()); - } - - [Theory] - [InlineData("not-base64")] - [InlineData("AA==")] - public void VerifyInvalidStoredHashReturnsFalse(string value) - { - var hasher = new PinHasher(); - - Assert.False(hasher.Verify(new PinHash(value), new PinCode("1234"))); - } -} diff --git a/tests/Atm.Infrastructure.Tests/packages.lock.json b/tests/Atm.Infrastructure.Tests/packages.lock.json deleted file mode 100644 index 43b0953..0000000 --- a/tests/Atm.Infrastructure.Tests/packages.lock.json +++ /dev/null @@ -1,279 +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" - } - }, - "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==" - }, - "Microsoft.AspNetCore.Cryptography.Internal": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "zqiWdZ8Jw4/sfTBSFHchU3Iajm8EqLPUq/cUIuUjARwNGJ9hrKR7Yf0bs4WlF+GdPX6Bj7kmaf99aY0OTbLCOg==" - }, - "Microsoft.AspNetCore.Cryptography.KeyDerivation": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "w0ZZWnsz7U2dh5l9eytJYYHG3/HwafewFyZT+NsnBuQAt90cw2hmiX9J9OU1mrtn7EiIJqhRxjzY5kAbmhD2DA==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.Internal": "9.0.19" - } - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "17.12.0", - "contentHash": "4svMznBd5JM21JIG2xZKGNanAHNXplxf/kQDFfLHXQ3OnpJkayRK/TjacFjA+EYmoyuNXHo/sOETEfcYtAzIrA==" - }, - "Microsoft.EntityFrameworkCore": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "qWqqxrfEDfi3C/z0S/ACIA2gUnreRPHX74WE/dVnesbztNesYO4NLy76lE6FV/zDP4vH/pkvDr8h3Mq9+cFv/w==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "9.0.19", - "Microsoft.EntityFrameworkCore.Analyzers": "9.0.19", - "Microsoft.Extensions.Caching.Memory": "9.0.19", - "Microsoft.Extensions.Logging": "9.0.19" - } - }, - "Microsoft.EntityFrameworkCore.Abstractions": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "igPnmVL3DU2ZH/4DNeQqWttNrQpwwg0q6MWj5peZxtl0QVMDSeFtnt2yhhdSffklZKYsAFko1sh/va3z4y7S6A==" - }, - "Microsoft.EntityFrameworkCore.Analyzers": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "yI5dQuvigJ6ICQECOlJDtrsUYcuDryv3qsZK1i6ZA/IzMzZ/VPkDzjE4r2LivuyVSGnkDRkH2cp4LmrhXARRKw==" - }, - "Microsoft.EntityFrameworkCore.Relational": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "KAfYGG4LWRNn1G/vMRydxgOjuZoWUvdd5raUcIia/HPg1jUjlQzS386nUx5gt3YfKxpFJaCGxdsQ1nCZWuYR8A==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "9.0.19", - "Microsoft.Extensions.Caching.Memory": "9.0.19", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.19", - "Microsoft.Extensions.Logging": "9.0.19" - } - }, - "Microsoft.Extensions.Caching.Abstractions": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "qe/6m6wxRG+N5xTf4Z3lDBAiVu6oBCUyADJ5OzlFgukD9Vg/6zB3MNnNEFA7amZg8hjcS6F4OZBRmB07A+gmHQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.19" - } - }, - "Microsoft.Extensions.Caching.Memory": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "Y0w5LkBqWkDh5vYTKM+QKlzxpqegEtP86oDhFMXgx4tDkWL2t+rl4h2vxBHkvBtM/gM6PoLK9QCrnoclXtHtVQ==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "9.0.19", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19", - "Microsoft.Extensions.Logging.Abstractions": "9.0.19", - "Microsoft.Extensions.Options": "9.0.19", - "Microsoft.Extensions.Primitives": "9.0.19" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "M6h5lX6QWSSn7FDeRUHePZen/xvxPsuhcX1VpBfefafqavn2BnfxheR7BH5D5AIZGM7Nah0xJKnq2DublD4doQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.19" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "o/XFYSiV4a7eiCkf7W1GFAviVhHQAPY5bHrQRsNWAPT3GhJ3Af63vcFQjHivIrTNZITlPPaGWhBy7jykKYYyaQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "XgQ0aVWClYjYSBqbYrKC/xyZ1KHf+VVwNF3+d54jcaqnst30Q//ugs/FJwUztLZiph0gp85g7i/eB0556twO0Q==" - }, - "Microsoft.Extensions.Identity.Core": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "6P2/iMA35j8RrjsJXhqaOy4Sturcu+NXzXSlKguGTzjBEVRf6atWPrKkUuZew9K5S5NbNh8gdYip2dXm8t5fLg==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.19", - "Microsoft.Extensions.Logging": "9.0.19", - "Microsoft.Extensions.Options": "9.0.19" - } - }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "3cnWzJ/FLEhLiW+TJTWtTjVj6fwlsP8OrbJqgOP4J9feAKjmT77vmClakHlej+jnzBgpnVU0+SZwtEIOr+KS6A==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "9.0.19", - "Microsoft.Extensions.Logging.Abstractions": "9.0.19", - "Microsoft.Extensions.Options": "9.0.19" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "RpqMVoZ+NECJxLOZRjWOfjST0mULs4wb848mxG/CS9SMkVL6yi1sSIda4dGcsB01J2jUFd8hNwsaYl1gplU+Cw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19" - } - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "FdPZy4kUSwFlIVZ3EkK9fdHw2RfblUwzflSb4kjVeGf9v2lX9IH+q2RGUovYMR7CVfKMc2y2Db/bZ2b4yiMfOg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.19", - "Microsoft.Extensions.Primitives": "9.0.19" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "9.0.19", - "contentHash": "9hIg8PQiMnpVFIsEHm25Wi1gBrPa2pS1G75uveyDUVLXrNKqBap9WGwQIgO0s6LUgRAOdsSYnbKbNC5b1G5Pcg==" - }, - "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==" - }, - "Npgsql": { - "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.2" - } - }, - "Npgsql.EntityFrameworkCore.PostgreSQL": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)", - "Npgsql": "9.0.3" - } - }, - "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.abstractions": { - "type": "Project", - "dependencies": { - "Atm.Domain": "[1.0.0, )" - } - }, - "atm.domain": { - "type": "Project" - }, - "atm.infrastructure": { - "type": "Project", - "dependencies": { - "Atm.Application.Abstractions": "[1.0.0, )", - "Atm.Domain": "[1.0.0, )", - "Microsoft.EntityFrameworkCore": "[9.0.19, )", - "Microsoft.EntityFrameworkCore.Relational": "[9.0.19, )", - "Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.19, )", - "Microsoft.Extensions.Identity.Core": "[9.0.19, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" - } - } - } - } -} \ No newline at end of file From 50003ea81a75838a1fde494e49d3537b393bdc78 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 8 Oct 2026 16:21:46 +0300 Subject: [PATCH 015/103] fix: changed dependencies in .csprojs --- .../Atm.Infrastructure.Persistence.csproj | 2 +- src/Atm.Presentation/Atm.Presentation.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Atm.Infrastructure.Persistence/Atm.Infrastructure.Persistence.csproj b/src/Atm.Infrastructure.Persistence/Atm.Infrastructure.Persistence.csproj index 1019c63..38e93a9 100644 --- a/src/Atm.Infrastructure.Persistence/Atm.Infrastructure.Persistence.csproj +++ b/src/Atm.Infrastructure.Persistence/Atm.Infrastructure.Persistence.csproj @@ -11,6 +11,6 @@ - + diff --git a/src/Atm.Presentation/Atm.Presentation.csproj b/src/Atm.Presentation/Atm.Presentation.csproj index 845329d..4986e6e 100644 --- a/src/Atm.Presentation/Atm.Presentation.csproj +++ b/src/Atm.Presentation/Atm.Presentation.csproj @@ -5,7 +5,7 @@ - - + + \ No newline at end of file From f659ec6307252ccdbfdbd36fa0a6dbe3b3e52b52 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 12 Oct 2026 02:06:57 +0300 Subject: [PATCH 016/103] feat: add ValueObjects for Account model --- .../Accounts/ValueObjects/AccountId.cs | 18 ++++++++++++ .../Accounts/ValueObjects/AccountNumber.cs | 18 ++++++++++++ .../Accounts/ValueObjects/Money.cs | 28 +++++++++++++++++++ .../Accounts/ValueObjects/Pincode.cs | 18 ++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 src/Atm.Application.Models/Accounts/ValueObjects/AccountId.cs create mode 100644 src/Atm.Application.Models/Accounts/ValueObjects/AccountNumber.cs create mode 100644 src/Atm.Application.Models/Accounts/ValueObjects/Money.cs create mode 100644 src/Atm.Application.Models/Accounts/ValueObjects/Pincode.cs 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..8e0838f --- /dev/null +++ b/src/Atm.Application.Models/Accounts/ValueObjects/AccountId.cs @@ -0,0 +1,18 @@ +namespace Atm.Application.Models.Accounts.ValueObjects; + +public readonly record struct AccountId +{ + public long Value { get; } + + private AccountId(long value) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); + + Value = value; + } + + public static AccountId Create(long value) + { + return new AccountId(value); + } +} \ 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 From 6304d913fd0b65f8cf51e6d5dcac2135b25ddc6d Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 12 Oct 2026 02:07:09 +0300 Subject: [PATCH 017/103] feat: add ResultTypes for Account operations --- .../Accounts/Results/DepositResult.cs | 8 ++++++++ .../Accounts/Results/WithdrawResult.cs | 10 ++++++++++ 2 files changed, 18 insertions(+) create mode 100644 src/Atm.Application.Models/Accounts/Results/DepositResult.cs create mode 100644 src/Atm.Application.Models/Accounts/Results/WithdrawResult.cs 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.Application.Models/Accounts/Results/WithdrawResult.cs b/src/Atm.Application.Models/Accounts/Results/WithdrawResult.cs new file mode 100644 index 0000000..234137f --- /dev/null +++ b/src/Atm.Application.Models/Accounts/Results/WithdrawResult.cs @@ -0,0 +1,10 @@ +namespace Atm.Application.Models.Accounts.Results; + +public abstract record WithdrawResult +{ + private WithdrawResult() { } + + public sealed record Success : WithdrawResult; + + public sealed record Failure(string ErrorMessage) : WithdrawResult; +} \ No newline at end of file From 24a7fc6e0a80dc726eae56ab8c6e0b568f67e556 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 12 Oct 2026 02:07:21 +0300 Subject: [PATCH 018/103] feat: add Account model --- .../Accounts/Account.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/Atm.Application.Models/Accounts/Account.cs diff --git a/src/Atm.Application.Models/Accounts/Account.cs b/src/Atm.Application.Models/Accounts/Account.cs new file mode 100644 index 0000000..b0791fd --- /dev/null +++ b/src/Atm.Application.Models/Accounts/Account.cs @@ -0,0 +1,46 @@ +using Atm.Application.Models.Accounts.Results; +using Atm.Application.Models.Accounts.ValueObjects; + +namespace Atm.Application.Models.Accounts; + +public sealed class Account +{ + private Money _balance; + + public AccountId Id { get; } + + public AccountNumber Number { get; } + + public Pincode Pincode { get; } + + public Account( + AccountId id, + AccountNumber number, + Pincode pincode, + Money balance) + { + Id = id; + Number = number; + Pincode = pincode; + _balance = balance; + } + + 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 From 409045d5b7da169d77fec547ce7d8cded2d76b20 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 12 Oct 2026 08:31:16 +0300 Subject: [PATCH 019/103] feat: impl AdminSession.cs --- src/Atm.Application.Models/Sessions/AdminSession.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/Atm.Application.Models/Sessions/AdminSession.cs diff --git a/src/Atm.Application.Models/Sessions/AdminSession.cs b/src/Atm.Application.Models/Sessions/AdminSession.cs new file mode 100644 index 0000000..2d3b8ce --- /dev/null +++ b/src/Atm.Application.Models/Sessions/AdminSession.cs @@ -0,0 +1,11 @@ +namespace Atm.Application.Models.Sessions; + +public sealed class AdminSession +{ + public AdminSession(Guid sessionId) + { + SessionId = sessionId; + } + + public Guid SessionId { get; } +} \ No newline at end of file From f404655c769a2df51e510b24ffb18c36c6f0bfdb Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 12 Oct 2026 08:31:25 +0300 Subject: [PATCH 020/103] feat: impl UserSession.cs --- .../Sessions/UserSession.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/Atm.Application.Models/Sessions/UserSession.cs diff --git a/src/Atm.Application.Models/Sessions/UserSession.cs b/src/Atm.Application.Models/Sessions/UserSession.cs new file mode 100644 index 0000000..09ab3d5 --- /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 UserSession( + Guid sessionId, + AccountId accountId) + { + SessionId = sessionId; + AccountId = accountId; + } + + public Guid SessionId { get; } + + public AccountId AccountId { get; } +} \ No newline at end of file From 5a7cf58d28a12466a35255bbcb1b6c297562e8a3 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 12 Oct 2026 08:34:09 +0300 Subject: [PATCH 021/103] fix: formatted property places --- src/Atm.Application.Models/Sessions/AdminSession.cs | 4 ++-- src/Atm.Application.Models/Sessions/UserSession.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Atm.Application.Models/Sessions/AdminSession.cs b/src/Atm.Application.Models/Sessions/AdminSession.cs index 2d3b8ce..491a038 100644 --- a/src/Atm.Application.Models/Sessions/AdminSession.cs +++ b/src/Atm.Application.Models/Sessions/AdminSession.cs @@ -2,10 +2,10 @@ public sealed class AdminSession { + public Guid SessionId { get; } + public AdminSession(Guid sessionId) { SessionId = sessionId; } - - public Guid SessionId { get; } } \ No newline at end of file diff --git a/src/Atm.Application.Models/Sessions/UserSession.cs b/src/Atm.Application.Models/Sessions/UserSession.cs index 09ab3d5..8cdabc9 100644 --- a/src/Atm.Application.Models/Sessions/UserSession.cs +++ b/src/Atm.Application.Models/Sessions/UserSession.cs @@ -4,6 +4,10 @@ namespace Atm.Application.Models.Sessions; public sealed class UserSession { + public Guid SessionId { get; } + + public AccountId AccountId { get; } + public UserSession( Guid sessionId, AccountId accountId) @@ -11,8 +15,4 @@ public UserSession( SessionId = sessionId; AccountId = accountId; } - - public Guid SessionId { get; } - - public AccountId AccountId { get; } } \ No newline at end of file From 3906e02fcb77b6c4cb1b15ac7cbba24c39782fe2 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 12 Oct 2026 08:39:11 +0300 Subject: [PATCH 022/103] fix: init BankOperations --- .../Operations/Operation.cs | 31 +++++++++++++++++++ .../Operations/OperationType.cs | 8 +++++ .../Operations/ValueObjects/OperationId.cs | 18 +++++++++++ 3 files changed, 57 insertions(+) create mode 100644 src/Atm.Application.Models/Operations/Operation.cs create mode 100644 src/Atm.Application.Models/Operations/OperationType.cs create mode 100644 src/Atm.Application.Models/Operations/ValueObjects/OperationId.cs diff --git a/src/Atm.Application.Models/Operations/Operation.cs b/src/Atm.Application.Models/Operations/Operation.cs new file mode 100644 index 0000000..291b812 --- /dev/null +++ b/src/Atm.Application.Models/Operations/Operation.cs @@ -0,0 +1,31 @@ +using Atm.Application.Models.Accounts.ValueObjects; +using Atm.Application.Models.Operations.ValueObjects; + +namespace Atm.Application.Models.Operations; + +public sealed class Operation +{ + public OperationId OperationId { get; } + + public AccountId AccountId { get; } + + public DateTimeOffset Time { get; } + + public OperationType OperationType { get; } + + public Money Balance { get; } + + public Operation( + OperationId operationId, + AccountId accountId, + DateTimeOffset time, + OperationType operationType, + Money balance) + { + OperationId = operationId; + AccountId = accountId; + Time = time; + OperationType = operationType; + Balance = balance; + } +} \ 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..60fe67f --- /dev/null +++ b/src/Atm.Application.Models/Operations/ValueObjects/OperationId.cs @@ -0,0 +1,18 @@ +namespace Atm.Application.Models.Operations.ValueObjects; + +public readonly record struct OperationId +{ + public long Value { get; } + + private OperationId(long value) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); + + Value = value; + } + + public static OperationId Create(long value) + { + return new OperationId(value); + } +} \ No newline at end of file From 714010d59c702408b3deda64483a35eb0e8f9761 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 12 Oct 2026 11:09:38 +0300 Subject: [PATCH 023/103] feat: create Sessions contracts --- .../Sessions/Models/SessionDto.cs | 3 +++ .../Sessions/Operations/CreateAdminSession.cs | 17 ++++++++++++++++ .../Sessions/Operations/CreateUserSession.cs | 20 +++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 src/Atm.Application.Contracts/Sessions/Models/SessionDto.cs create mode 100644 src/Atm.Application.Contracts/Sessions/Operations/CreateAdminSession.cs create mode 100644 src/Atm.Application.Contracts/Sessions/Operations/CreateUserSession.cs 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..6e31b93 --- /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 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..fe38f10 --- /dev/null +++ b/src/Atm.Application.Contracts/Sessions/Operations/CreateUserSession.cs @@ -0,0 +1,20 @@ +using Atm.Application.Contracts.Sessions.Models; +using Atm.Application.Models.Accounts.ValueObjects; + +namespace Atm.Application.Contracts.Sessions.Operations; + +public static class CreateUserSession +{ + public readonly record struct Request( + AccountNumber AccountNumber, + Pincode 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 From 18ae8344a50fa03b3c7fdbad4b155f1f42c5d781 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 12 Oct 2026 11:09:51 +0300 Subject: [PATCH 024/103] feat: init ISessionService.cs interface --- .../Sessions/ISessionService.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/Atm.Application.Contracts/Sessions/ISessionService.cs 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 From 5e6ba8e39cc12cfd4c5c027b648fdb6bc5253d73 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 12 Oct 2026 11:11:47 +0300 Subject: [PATCH 025/103] fix: add static to class --- .../Sessions/Operations/CreateAdminSession.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Atm.Application.Contracts/Sessions/Operations/CreateAdminSession.cs b/src/Atm.Application.Contracts/Sessions/Operations/CreateAdminSession.cs index 6e31b93..a50d07d 100644 --- a/src/Atm.Application.Contracts/Sessions/Operations/CreateAdminSession.cs +++ b/src/Atm.Application.Contracts/Sessions/Operations/CreateAdminSession.cs @@ -2,7 +2,7 @@ namespace Atm.Application.Contracts.Sessions.Operations; -public class CreateAdminSession +public static class CreateAdminSession { public readonly record struct Request(string SystemPassword); From de2af2c6e048da61bf6bde7fc5ed84c8b1d9621f Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 12 Oct 2026 12:51:37 +0300 Subject: [PATCH 026/103] fix: init AccountDto.cs --- src/Atm.Application.Contracts/Accounts/Models/AccountDto.cs | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/Atm.Application.Contracts/Accounts/Models/AccountDto.cs diff --git a/src/Atm.Application.Contracts/Accounts/Models/AccountDto.cs b/src/Atm.Application.Contracts/Accounts/Models/AccountDto.cs new file mode 100644 index 0000000..4109f79 --- /dev/null +++ b/src/Atm.Application.Contracts/Accounts/Models/AccountDto.cs @@ -0,0 +1,3 @@ +namespace Atm.Application.Contracts.Accounts.Models; + +public sealed record AccountDto(string AccountNumber); \ No newline at end of file From 768ee2d59ca48e9b1caa761ba786e865fea25035 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Wed, 14 Oct 2026 23:46:10 +0300 Subject: [PATCH 027/103] feat: init BalanceDto.cs --- src/Atm.Application.Contracts/Accounts/Models/BalanceDto.cs | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/Atm.Application.Contracts/Accounts/Models/BalanceDto.cs 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 From 186451569847519662e35e5e8bc52daa7b11013c Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Wed, 14 Oct 2026 23:46:26 +0300 Subject: [PATCH 028/103] feat: add contracts for IAccountService.cs --- .../Accounts/Operations/CreateAccount.cs | 21 +++++++++++++++++++ .../Accounts/Operations/DepositBalance.cs | 15 +++++++++++++ .../Accounts/Operations/GetBalance.cs | 17 +++++++++++++++ .../Accounts/Operations/WithdrawBalance.cs | 15 +++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 src/Atm.Application.Contracts/Accounts/Operations/CreateAccount.cs create mode 100644 src/Atm.Application.Contracts/Accounts/Operations/DepositBalance.cs create mode 100644 src/Atm.Application.Contracts/Accounts/Operations/GetBalance.cs create mode 100644 src/Atm.Application.Contracts/Accounts/Operations/WithdrawBalance.cs diff --git a/src/Atm.Application.Contracts/Accounts/Operations/CreateAccount.cs b/src/Atm.Application.Contracts/Accounts/Operations/CreateAccount.cs new file mode 100644 index 0000000..e156e40 --- /dev/null +++ b/src/Atm.Application.Contracts/Accounts/Operations/CreateAccount.cs @@ -0,0 +1,21 @@ +using Atm.Application.Contracts.Accounts.Models; +using Atm.Application.Models.Accounts.ValueObjects; + +namespace Atm.Application.Contracts.Accounts.Operations; + +public static class CreateAccount +{ + public readonly record struct Request( + Guid SessionId, + AccountNumber AccountNumber, + Pincode Pincode); + + public abstract record Response + { + private Response() { } + + public sealed record Success(AccountDto AccountNumber) : Response; + + public sealed record Failure(string ErrorMessage) : Response; + } +} \ 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/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 From 6c7e9a2a27a4ce97ee6a61650cc7e487512b4829 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Wed, 14 Oct 2026 23:46:39 +0300 Subject: [PATCH 029/103] feat: implemented interface of AccountService --- .../Accounts/IAccountService.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Atm.Application.Contracts/Accounts/IAccountService.cs diff --git a/src/Atm.Application.Contracts/Accounts/IAccountService.cs b/src/Atm.Application.Contracts/Accounts/IAccountService.cs new file mode 100644 index 0000000..5710802 --- /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 CreateAccountAsync( + CreateAccount.Request request, + CancellationToken cancellationToken); + + Task GetAccountAsync( + GetBalance.Request request, + CancellationToken cancellationToken); + + Task WithdrawBalanceAsync( + WithdrawBalance.Request request, + CancellationToken cancellationToken); + + Task DepositBalanceAsync( + WithdrawBalance.Request request, + CancellationToken cancellationToken); +} \ No newline at end of file From c16311a165a6ad5a86734f4edfa876e5a2da6db5 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Wed, 14 Oct 2026 23:57:35 +0300 Subject: [PATCH 030/103] style: renamed the Application.Abstractions.csproj --- Atm.slnx | 2 +- .../Atm.Application.Abstractions.Persistence.csproj} | 3 --- src/Atm.Application/Atm.Application.csproj | 2 +- .../Atm.Infrastructure.Persistence.csproj | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) rename src/{Atm.Application.Abstractions/Atm.Application.Abstractions.csproj => Atm.Application.Abstractions.Persistence/Atm.Application.Abstractions.Persistence.csproj} (76%) diff --git a/Atm.slnx b/Atm.slnx index bc767d3..68c2590 100644 --- a/Atm.slnx +++ b/Atm.slnx @@ -6,7 +6,7 @@ - + diff --git a/src/Atm.Application.Abstractions/Atm.Application.Abstractions.csproj b/src/Atm.Application.Abstractions.Persistence/Atm.Application.Abstractions.Persistence.csproj similarity index 76% rename from src/Atm.Application.Abstractions/Atm.Application.Abstractions.csproj rename to src/Atm.Application.Abstractions.Persistence/Atm.Application.Abstractions.Persistence.csproj index 670ff57..b799d9c 100644 --- a/src/Atm.Application.Abstractions/Atm.Application.Abstractions.csproj +++ b/src/Atm.Application.Abstractions.Persistence/Atm.Application.Abstractions.Persistence.csproj @@ -1,7 +1,4 @@  - - Atm.Application.Abstractions - diff --git a/src/Atm.Application/Atm.Application.csproj b/src/Atm.Application/Atm.Application.csproj index 77a6187..7d96306 100644 --- a/src/Atm.Application/Atm.Application.csproj +++ b/src/Atm.Application/Atm.Application.csproj @@ -4,7 +4,7 @@ - + diff --git a/src/Atm.Infrastructure.Persistence/Atm.Infrastructure.Persistence.csproj b/src/Atm.Infrastructure.Persistence/Atm.Infrastructure.Persistence.csproj index 38e93a9..f7e331e 100644 --- a/src/Atm.Infrastructure.Persistence/Atm.Infrastructure.Persistence.csproj +++ b/src/Atm.Infrastructure.Persistence/Atm.Infrastructure.Persistence.csproj @@ -11,6 +11,6 @@ - + From ddaa66c11b80f7d670f314e0df5a78885b68f953 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 14:23:15 +0300 Subject: [PATCH 031/103] fix: removed creatin account from AccountService into AdminService --- src/Atm.Application.Contracts/Accounts/IAccountService.cs | 4 ---- src/Atm.Application.Contracts/Accounts/Models/AccountDto.cs | 3 --- src/Atm.Application.Contracts/Admins/Operations/AccountDto.cs | 3 +++ 3 files changed, 3 insertions(+), 7 deletions(-) delete mode 100644 src/Atm.Application.Contracts/Accounts/Models/AccountDto.cs create mode 100644 src/Atm.Application.Contracts/Admins/Operations/AccountDto.cs diff --git a/src/Atm.Application.Contracts/Accounts/IAccountService.cs b/src/Atm.Application.Contracts/Accounts/IAccountService.cs index 5710802..4e21a6e 100644 --- a/src/Atm.Application.Contracts/Accounts/IAccountService.cs +++ b/src/Atm.Application.Contracts/Accounts/IAccountService.cs @@ -4,10 +4,6 @@ namespace Atm.Application.Contracts.Accounts; public interface IAccountService { - Task CreateAccountAsync( - CreateAccount.Request request, - CancellationToken cancellationToken); - Task GetAccountAsync( GetBalance.Request request, CancellationToken cancellationToken); diff --git a/src/Atm.Application.Contracts/Accounts/Models/AccountDto.cs b/src/Atm.Application.Contracts/Accounts/Models/AccountDto.cs deleted file mode 100644 index 4109f79..0000000 --- a/src/Atm.Application.Contracts/Accounts/Models/AccountDto.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Atm.Application.Contracts.Accounts.Models; - -public sealed record AccountDto(string AccountNumber); \ 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 From cabe675848d2bcbfd1443bd46d18e02ecfe81839 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 14:25:03 +0300 Subject: [PATCH 032/103] fix: CreateAccount now in IAdminService.cs --- src/Atm.Application.Contracts/Admins/IAdminService.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/Atm.Application.Contracts/Admins/IAdminService.cs 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 From ea6224d01d3edd27fe32710e6a25983b64eec63f Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 14:37:26 +0300 Subject: [PATCH 033/103] feat: init GetOperationHistory.cs --- .../Accounts/IAccountService.cs | 4 ++++ .../Accounts/Models/OperationHistoryDto.cs | 8 ++++++++ .../Accounts/Models/OperationType.cs | 8 ++++++++ .../Accounts/Operations/GetOperationHistory.cs | 18 ++++++++++++++++++ 4 files changed, 38 insertions(+) create mode 100644 src/Atm.Application.Contracts/Accounts/Models/OperationHistoryDto.cs create mode 100644 src/Atm.Application.Contracts/Accounts/Models/OperationType.cs create mode 100644 src/Atm.Application.Contracts/Accounts/Operations/GetOperationHistory.cs diff --git a/src/Atm.Application.Contracts/Accounts/IAccountService.cs b/src/Atm.Application.Contracts/Accounts/IAccountService.cs index 4e21a6e..e47c01b 100644 --- a/src/Atm.Application.Contracts/Accounts/IAccountService.cs +++ b/src/Atm.Application.Contracts/Accounts/IAccountService.cs @@ -15,4 +15,8 @@ public interface IAccountService Task DepositBalanceAsync( WithdrawBalance.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/OperationHistoryDto.cs b/src/Atm.Application.Contracts/Accounts/Models/OperationHistoryDto.cs new file mode 100644 index 0000000..c2a4c62 --- /dev/null +++ b/src/Atm.Application.Contracts/Accounts/Models/OperationHistoryDto.cs @@ -0,0 +1,8 @@ +namespace Atm.Application.Contracts.Accounts.Models; + +public sealed record OperationHistoryDto( + Guid OperationId, + Guid AccountId, + OperationType OperationType, + decimal Amount, + decimal BalanceAfter); \ No newline at end of file diff --git a/src/Atm.Application.Contracts/Accounts/Models/OperationType.cs b/src/Atm.Application.Contracts/Accounts/Models/OperationType.cs new file mode 100644 index 0000000..cfc4d7b --- /dev/null +++ b/src/Atm.Application.Contracts/Accounts/Models/OperationType.cs @@ -0,0 +1,8 @@ +namespace Atm.Application.Contracts.Accounts.Models; + +public enum OperationType +{ + Withdraw, + Deposit, + ShowBalance, +} \ 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..155ae5e --- /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( + OperationHistoryDto OperationHistory) : Response; + + public sealed record Failure(string ErrorMessage) : Response; + } +} \ No newline at end of file From cd28a0b51b304057e9102441a9ef7e6f3eeb94a3 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 14:41:02 +0300 Subject: [PATCH 034/103] feat: add interface of operationHistory repository --- .../Repositories/IOperationHistoryRepository.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs b/src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs new file mode 100644 index 0000000..a9d7807 --- /dev/null +++ b/src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs @@ -0,0 +1,11 @@ +using Atm.Application.Models.Accounts.ValueObjects; +using Atm.Application.Models.Operations; + +namespace Atm.Application.Abstractions.Persistence.Repositories; + +public interface IOperationHistoryRepository +{ + IAsyncEnumerable FindByAccountIdAsync( + AccountId accountId, + CancellationToken cancellationToken); +} \ No newline at end of file From 9025659017d80709ec29c6041d53f2cd496d8a06 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 14:43:31 +0300 Subject: [PATCH 035/103] feat: add Update method in AccountRepository --- .../Repositories/IAccountRepository.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs b/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs new file mode 100644 index 0000000..3ccff95 --- /dev/null +++ b/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs @@ -0,0 +1,23 @@ +using Atm.Application.Models.Accounts; +using Atm.Application.Models.Accounts.ValueObjects; + +namespace Atm.Application.Abstractions.Persistence.Repositories; + +public interface IAccountRepository +{ + Task AddAccounts( + IReadOnlyCollection accounts, + CancellationToken cancellationToken); + + Task GetAccountById( + AccountId accountId, + CancellationToken cancellationToken); + + Task GetAccountByNumber( + AccountNumber accountNumber, + CancellationToken cancellationToken); + + Task UpdateAccount( + Account account, + CancellationToken cancellationToken); +} \ No newline at end of file From 9aa32a40ed0bdeb0bc0e8c746d9c5c2db860a0e2 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 14:44:41 +0300 Subject: [PATCH 036/103] style: renamed the methods with async --- .../Repositories/IAccountRepository.cs | 8 ++++---- .../Repositories/IAdminSessionRepository.cs | 14 ++++++++++++++ .../Repositories/IUserSessionRepository.cs | 14 ++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs create mode 100644 src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs b/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs index 3ccff95..7cccb29 100644 --- a/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs +++ b/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs @@ -5,19 +5,19 @@ namespace Atm.Application.Abstractions.Persistence.Repositories; public interface IAccountRepository { - Task AddAccounts( + Task AddAccountsAsync( IReadOnlyCollection accounts, CancellationToken cancellationToken); - Task GetAccountById( + Task GetAccountByIdAsync( AccountId accountId, CancellationToken cancellationToken); - Task GetAccountByNumber( + Task GetAccountByNumberAsync( AccountNumber accountNumber, CancellationToken cancellationToken); - Task UpdateAccount( + Task UpdateAccountAsync( Account account, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs b/src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs new file mode 100644 index 0000000..42df2ad --- /dev/null +++ b/src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs @@ -0,0 +1,14 @@ +using Atm.Application.Models.Sessions; + +namespace Atm.Application.Abstractions.Persistence.Repositories; + +public interface IAdminSessionRepository +{ + Task AddSessionsAsync( + IReadOnlyCollection sessions, + CancellationToken cancellationToken); + + Task GetAdminSessionByIdAsync( + Guid sessionId, + CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs b/src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs new file mode 100644 index 0000000..582d964 --- /dev/null +++ b/src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs @@ -0,0 +1,14 @@ +using Atm.Application.Models.Sessions; + +namespace Atm.Application.Abstractions.Persistence.Repositories; + +public interface IUserSessionRepository +{ + Task AddSessionsAsync( + IReadOnlyCollection sessions, + CancellationToken cancellationToken); + + Task GetUserSessionByIdAsync( + Guid sessionId, + CancellationToken cancellationToken); +} \ No newline at end of file From 72cf460a5707756ba145ac3da803667c4837ae87 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 14:45:59 +0300 Subject: [PATCH 037/103] feat: add facade for repositories --- .../IPersistenceContext.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/Atm.Application.Abstractions.Persistence/IPersistenceContext.cs diff --git a/src/Atm.Application.Abstractions.Persistence/IPersistenceContext.cs b/src/Atm.Application.Abstractions.Persistence/IPersistenceContext.cs new file mode 100644 index 0000000..d2e779f --- /dev/null +++ b/src/Atm.Application.Abstractions.Persistence/IPersistenceContext.cs @@ -0,0 +1,14 @@ +using Atm.Application.Abstractions.Persistence.Repositories; + +namespace Atm.Application.Abstractions.Persistence; + +public interface IPersistenceContext +{ + IAccountRepository AccountRepository { get; } + + IAdminSessionRepository AdminSessionRepository { get; } + + IUserSessionRepository UserSessionRepository { get; } + + IOperationHistoryRepository OperationHistoryRepository { get; } +} \ No newline at end of file From 3ea281dcd989e6763c577b1d8083962565b6dc38 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 14:49:46 +0300 Subject: [PATCH 038/103] style: renamed a few methods in repositories --- .../Repositories/IAccountRepository.cs | 4 ++-- .../Repositories/IAdminSessionRepository.cs | 2 +- .../Repositories/IOperationHistoryRepository.cs | 4 ++++ .../Repositories/IUserSessionRepository.cs | 4 ++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs b/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs index 7cccb29..1e4e594 100644 --- a/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs +++ b/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs @@ -9,11 +9,11 @@ Task AddAccountsAsync( IReadOnlyCollection accounts, CancellationToken cancellationToken); - Task GetAccountByIdAsync( + Task FindAccountByIdAsync( AccountId accountId, CancellationToken cancellationToken); - Task GetAccountByNumberAsync( + Task FindAccountByNumberAsync( AccountNumber accountNumber, CancellationToken cancellationToken); diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs b/src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs index 42df2ad..2a8b19e 100644 --- a/src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs +++ b/src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs @@ -8,7 +8,7 @@ Task AddSessionsAsync( IReadOnlyCollection sessions, CancellationToken cancellationToken); - Task GetAdminSessionByIdAsync( + Task FindAdminSessionByIdAsync( Guid sessionId, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs b/src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs index a9d7807..618c3c1 100644 --- a/src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs +++ b/src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs @@ -5,6 +5,10 @@ namespace Atm.Application.Abstractions.Persistence.Repositories; public interface IOperationHistoryRepository { + Task AddOperationAsync( + Operation operation, + CancellationToken cancellationToken); + IAsyncEnumerable FindByAccountIdAsync( AccountId accountId, CancellationToken cancellationToken); diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs b/src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs index 582d964..911ad85 100644 --- a/src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs +++ b/src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs @@ -5,10 +5,10 @@ namespace Atm.Application.Abstractions.Persistence.Repositories; public interface IUserSessionRepository { Task AddSessionsAsync( - IReadOnlyCollection sessions, + UserSession session, CancellationToken cancellationToken); - Task GetUserSessionByIdAsync( + Task FindUserSessionByIdAsync( Guid sessionId, CancellationToken cancellationToken); } \ No newline at end of file From c81721af0307140353d5ace1d6e3ea8129cb91aa Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 14:53:46 +0300 Subject: [PATCH 039/103] fix: GetOperationHistory returns a list of operations now --- .../Accounts/Operations/GetOperationHistory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Atm.Application.Contracts/Accounts/Operations/GetOperationHistory.cs b/src/Atm.Application.Contracts/Accounts/Operations/GetOperationHistory.cs index 155ae5e..2616adb 100644 --- a/src/Atm.Application.Contracts/Accounts/Operations/GetOperationHistory.cs +++ b/src/Atm.Application.Contracts/Accounts/Operations/GetOperationHistory.cs @@ -11,7 +11,7 @@ public abstract record Response private Response() { } public sealed record Success( - OperationHistoryDto OperationHistory) : Response; + IReadOnlyCollection OperationHistory) : Response; public sealed record Failure(string ErrorMessage) : Response; } From f724c3371d6cfea61066db821ad2ab541177a00d Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 14:55:01 +0300 Subject: [PATCH 040/103] feat: now account creates in AdminService --- .../Accounts/Operations/CreateAccount.cs | 21 ------------------- .../Admins/Models/CreateAccount.cs | 20 ++++++++++++++++++ 2 files changed, 20 insertions(+), 21 deletions(-) delete mode 100644 src/Atm.Application.Contracts/Accounts/Operations/CreateAccount.cs create mode 100644 src/Atm.Application.Contracts/Admins/Models/CreateAccount.cs diff --git a/src/Atm.Application.Contracts/Accounts/Operations/CreateAccount.cs b/src/Atm.Application.Contracts/Accounts/Operations/CreateAccount.cs deleted file mode 100644 index e156e40..0000000 --- a/src/Atm.Application.Contracts/Accounts/Operations/CreateAccount.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Atm.Application.Contracts.Accounts.Models; -using Atm.Application.Models.Accounts.ValueObjects; - -namespace Atm.Application.Contracts.Accounts.Operations; - -public static class CreateAccount -{ - public readonly record struct Request( - Guid SessionId, - AccountNumber AccountNumber, - Pincode Pincode); - - public abstract record Response - { - private Response() { } - - public sealed record Success(AccountDto AccountNumber) : Response; - - public sealed record Failure(string ErrorMessage) : Response; - } -} \ 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 From 024370cac8624dfadc32a1c541a01102b923829b Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 14:55:16 +0300 Subject: [PATCH 041/103] feat: add directory --- src/Atm.Application/Atm.Application.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Atm.Application/Atm.Application.csproj b/src/Atm.Application/Atm.Application.csproj index 7d96306..bd66533 100644 --- a/src/Atm.Application/Atm.Application.csproj +++ b/src/Atm.Application/Atm.Application.csproj @@ -10,5 +10,9 @@ + + + + \ No newline at end of file From 7cd75fb9eed473a2eea65e74835dd1a420eb713e Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 15:01:11 +0300 Subject: [PATCH 042/103] feat: impl the blueprint of AdminService.cs to continue it later --- src/Atm.Application/Services/AdminService.cs | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/Atm.Application/Services/AdminService.cs diff --git a/src/Atm.Application/Services/AdminService.cs b/src/Atm.Application/Services/AdminService.cs new file mode 100644 index 0000000..9b43fad --- /dev/null +++ b/src/Atm.Application/Services/AdminService.cs @@ -0,0 +1,23 @@ +using Atm.Application.Abstractions.Persistence; +using Atm.Application.Contracts.Admins; +using Atm.Application.Contracts.Admins.Models; + +namespace Atm.HighLoad.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) + { + throw new NotImplementedException(); + } +} \ No newline at end of file From 251c01a5fadd277c931d0746c2668083e96ffce4 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 15:51:16 +0300 Subject: [PATCH 043/103] feat: add nullable reference in find methods --- .../Repositories/IAccountRepository.cs | 4 ++-- .../Repositories/IAdminSessionRepository.cs | 2 +- .../Repositories/IOperationHistoryRepository.cs | 2 +- .../Repositories/IUserSessionRepository.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs b/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs index 1e4e594..03b7be4 100644 --- a/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs +++ b/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs @@ -9,11 +9,11 @@ Task AddAccountsAsync( IReadOnlyCollection accounts, CancellationToken cancellationToken); - Task FindAccountByIdAsync( + Task FindAccountByIdAsync( AccountId accountId, CancellationToken cancellationToken); - Task FindAccountByNumberAsync( + Task FindAccountByNumberAsync( AccountNumber accountNumber, CancellationToken cancellationToken); diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs b/src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs index 2a8b19e..087f1e4 100644 --- a/src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs +++ b/src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs @@ -8,7 +8,7 @@ Task AddSessionsAsync( IReadOnlyCollection sessions, CancellationToken cancellationToken); - Task FindAdminSessionByIdAsync( + Task FindAdminSessionByIdAsync( Guid sessionId, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs b/src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs index 618c3c1..5cd1c77 100644 --- a/src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs +++ b/src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs @@ -9,7 +9,7 @@ Task AddOperationAsync( Operation operation, CancellationToken cancellationToken); - IAsyncEnumerable FindByAccountIdAsync( + IAsyncEnumerable FindByAccountIdAsync( AccountId accountId, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs b/src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs index 911ad85..4df71c1 100644 --- a/src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs +++ b/src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs @@ -8,7 +8,7 @@ Task AddSessionsAsync( UserSession session, CancellationToken cancellationToken); - Task FindUserSessionByIdAsync( + Task FindUserSessionByIdAsync( Guid sessionId, CancellationToken cancellationToken); } \ No newline at end of file From 00bbbf8fe75626ccd2201c8f2f36ae6a42f2e6fb Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 16:11:39 +0300 Subject: [PATCH 044/103] fix: now users would be able to create one account/session each time (will improve it later) --- .../Repositories/IAccountRepository.cs | 4 ++-- .../Repositories/IAdminSessionRepository.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs b/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs index 03b7be4..2c6ad71 100644 --- a/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs +++ b/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs @@ -5,8 +5,8 @@ namespace Atm.Application.Abstractions.Persistence.Repositories; public interface IAccountRepository { - Task AddAccountsAsync( - IReadOnlyCollection accounts, + Task AddAccountAsync( + Account account, CancellationToken cancellationToken); Task FindAccountByIdAsync( diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs b/src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs index 087f1e4..8ad294e 100644 --- a/src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs +++ b/src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs @@ -4,8 +4,8 @@ namespace Atm.Application.Abstractions.Persistence.Repositories; public interface IAdminSessionRepository { - Task AddSessionsAsync( - IReadOnlyCollection sessions, + Task AddSessionAsync( + AdminSession sessions, CancellationToken cancellationToken); Task FindAdminSessionByIdAsync( From 6b7a54526ec4c7c170ce95fd8a0b8cacc1e2eff1 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 16:12:03 +0300 Subject: [PATCH 045/103] fix: now users would be able to create one account/session each time (will improve it later) --- .../Repositories/IUserSessionRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs b/src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs index 4df71c1..503c2ce 100644 --- a/src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs +++ b/src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs @@ -4,7 +4,7 @@ namespace Atm.Application.Abstractions.Persistence.Repositories; public interface IUserSessionRepository { - Task AddSessionsAsync( + Task AddSessionAsync( UserSession session, CancellationToken cancellationToken); From 02f90512bef3341d20fc7ae598f36b617cbbe976 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 16:35:25 +0300 Subject: [PATCH 046/103] feat: separate acccount creation and rehydration --- .../Accounts/Account.cs | 34 +++++++++++++++---- .../Accounts/ValueObjects/AccountId.cs | 5 +++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/Atm.Application.Models/Accounts/Account.cs b/src/Atm.Application.Models/Accounts/Account.cs index b0791fd..0a4df9a 100644 --- a/src/Atm.Application.Models/Accounts/Account.cs +++ b/src/Atm.Application.Models/Accounts/Account.cs @@ -7,13 +7,7 @@ public sealed class Account { private Money _balance; - public AccountId Id { get; } - - public AccountNumber Number { get; } - - public Pincode Pincode { get; } - - public Account( + private Account( AccountId id, AccountNumber number, Pincode pincode, @@ -25,6 +19,32 @@ public Account( _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 DepositResult Deposit(Money money) { _balance += money; diff --git a/src/Atm.Application.Models/Accounts/ValueObjects/AccountId.cs b/src/Atm.Application.Models/Accounts/ValueObjects/AccountId.cs index 8e0838f..40d8fd9 100644 --- a/src/Atm.Application.Models/Accounts/ValueObjects/AccountId.cs +++ b/src/Atm.Application.Models/Accounts/ValueObjects/AccountId.cs @@ -15,4 +15,9 @@ public static AccountId Create(long value) { return new AccountId(value); } + + public static AccountId Default() + { + return new AccountId(0); + } } \ No newline at end of file From a8d94d7ef8c99fe4b8f5c0b6c213aff11ebd6f5a Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 16:37:09 +0300 Subject: [PATCH 047/103] feat: add mapping to dto (account) --- .../Mapping/AccountMappingExtensions.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/Atm.Application/Mapping/AccountMappingExtensions.cs diff --git a/src/Atm.Application/Mapping/AccountMappingExtensions.cs b/src/Atm.Application/Mapping/AccountMappingExtensions.cs new file mode 100644 index 0000000..5268af9 --- /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.HighLoad.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 From b3967adffe27ae82f81ba2c550bc4d063d2a295b Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 16:37:51 +0300 Subject: [PATCH 048/103] feat: wrote AdminService with the method of creating an account --- src/Atm.Application/Services/AdminService.cs | 33 ++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/Atm.Application/Services/AdminService.cs b/src/Atm.Application/Services/AdminService.cs index 9b43fad..4cfcac3 100644 --- a/src/Atm.Application/Services/AdminService.cs +++ b/src/Atm.Application/Services/AdminService.cs @@ -1,6 +1,10 @@ using Atm.Application.Abstractions.Persistence; using Atm.Application.Contracts.Admins; using Atm.Application.Contracts.Admins.Models; +using Atm.Application.Models.Accounts; +using Atm.Application.Models.Accounts.ValueObjects; +using Atm.Application.Models.Sessions; +using Atm.HighLoad.Application.Mapping; namespace Atm.HighLoad.Application.Services; @@ -13,11 +17,36 @@ public AdminService(IPersistenceContext context) _context = context; } - public async Task CreateAccountAsync( CreateAccount.Request request, CancellationToken cancellationToken) { - throw new NotImplementedException(); + 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 From 64bbcfc92fd31b609488455cc4eea8928edb54ac Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 16:38:15 +0300 Subject: [PATCH 049/103] fix: delete dependency --- src/Atm.Application/Atm.Application.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Atm.Application/Atm.Application.csproj b/src/Atm.Application/Atm.Application.csproj index bd66533..7d96306 100644 --- a/src/Atm.Application/Atm.Application.csproj +++ b/src/Atm.Application/Atm.Application.csproj @@ -10,9 +10,5 @@ - - - - \ No newline at end of file From 59b97aebf2c0632cb16cfbbbef85aab9286ddda4 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 16:54:38 +0300 Subject: [PATCH 050/103] feat: add SystemPassword for IOptions --- src/Atm.Application/Options/SessionOptions.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/Atm.Application/Options/SessionOptions.cs diff --git a/src/Atm.Application/Options/SessionOptions.cs b/src/Atm.Application/Options/SessionOptions.cs new file mode 100644 index 0000000..2fdfbb2 --- /dev/null +++ b/src/Atm.Application/Options/SessionOptions.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace Atm.HighLoad.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 From 1671a536f689449e4682e95717f31a6e00ae6dc5 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 21:09:01 +0300 Subject: [PATCH 051/103] feat: implemented CreateAdminSession in SessionService.cs --- .../Services/SessionService.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/Atm.Application/Services/SessionService.cs diff --git a/src/Atm.Application/Services/SessionService.cs b/src/Atm.Application/Services/SessionService.cs new file mode 100644 index 0000000..64edaa8 --- /dev/null +++ b/src/Atm.Application/Services/SessionService.cs @@ -0,0 +1,48 @@ +using Atm.Application.Abstractions.Persistence; +using Atm.Application.Contracts.Sessions; +using Atm.Application.Contracts.Sessions.Operations; +using Atm.Application.Models.Sessions; +using Atm.HighLoad.Application.Mapping; +using Atm.HighLoad.Application.Options; +using Microsoft.Extensions.Options; + +namespace Atm.HighLoad.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) + { + + } + + 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 From 823ad3ed08c60993607b27416af7cb32821eaa47 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 21:09:22 +0300 Subject: [PATCH 052/103] feat: add mappers for Sessions --- .../Mapping/SessionMappingExtensions.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/Atm.Application/Mapping/SessionMappingExtensions.cs diff --git a/src/Atm.Application/Mapping/SessionMappingExtensions.cs b/src/Atm.Application/Mapping/SessionMappingExtensions.cs new file mode 100644 index 0000000..4fac1d0 --- /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.HighLoad.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 From 35ed6bc17c5d01dfd6c94214ab62c9c67b5804bf Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 21:09:37 +0300 Subject: [PATCH 053/103] feat: add dependencies for IOption --- src/Atm.Application/Atm.Application.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Atm.Application/Atm.Application.csproj b/src/Atm.Application/Atm.Application.csproj index 7d96306..6dc4b91 100644 --- a/src/Atm.Application/Atm.Application.csproj +++ b/src/Atm.Application/Atm.Application.csproj @@ -9,6 +9,8 @@ + + \ No newline at end of file From 7c79b87e4971ad4cc41202427f04e35bab63dc39 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Thu, 15 Oct 2026 21:18:56 +0300 Subject: [PATCH 054/103] fix: deleted domain models from contracts --- .../Sessions/Operations/CreateUserSession.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Atm.Application.Contracts/Sessions/Operations/CreateUserSession.cs b/src/Atm.Application.Contracts/Sessions/Operations/CreateUserSession.cs index fe38f10..be0b6ad 100644 --- a/src/Atm.Application.Contracts/Sessions/Operations/CreateUserSession.cs +++ b/src/Atm.Application.Contracts/Sessions/Operations/CreateUserSession.cs @@ -1,13 +1,12 @@ using Atm.Application.Contracts.Sessions.Models; -using Atm.Application.Models.Accounts.ValueObjects; namespace Atm.Application.Contracts.Sessions.Operations; public static class CreateUserSession { public readonly record struct Request( - AccountNumber AccountNumber, - Pincode Pincode); + string AccountNumber, + string Pincode); public abstract record Response { From f6852dc975dbcbcaa0f8e755728397e2e7810ac1 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sat, 17 Oct 2026 16:15:04 +0300 Subject: [PATCH 055/103] feat: impl SessionService.cs --- .../Services/SessionService.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Atm.Application/Services/SessionService.cs b/src/Atm.Application/Services/SessionService.cs index 64edaa8..054fb1f 100644 --- a/src/Atm.Application/Services/SessionService.cs +++ b/src/Atm.Application/Services/SessionService.cs @@ -1,6 +1,8 @@ using Atm.Application.Abstractions.Persistence; using Atm.Application.Contracts.Sessions; using Atm.Application.Contracts.Sessions.Operations; +using Atm.Application.Models.Accounts; +using Atm.Application.Models.Accounts.ValueObjects; using Atm.Application.Models.Sessions; using Atm.HighLoad.Application.Mapping; using Atm.HighLoad.Application.Options; @@ -25,7 +27,29 @@ public SessionService( 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( @@ -34,7 +58,7 @@ public SessionService( { if (request.SystemPassword != _systemPassword) { - return new CreateAdminSession.Response.Failure("wrong password."); + return new CreateAdminSession.Response.Failure("Wrong password."); } var session = new AdminSession(Guid.NewGuid()); From 38b6558670b4be46d9725691fc8c2c990ed85e0c Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sat, 17 Oct 2026 16:35:48 +0300 Subject: [PATCH 056/103] feat: add interfacecs of transactions to use them in services --- .../Transactions/IPersistenceTransaction.cs | 8 ++++++++ .../Transactions/IPersistenceTransactionProvider.cs | 10 ++++++++++ 2 files changed, 18 insertions(+) create mode 100644 src/Atm.Application.Abstractions.Persistence/Transactions/IPersistenceTransaction.cs create mode 100644 src/Atm.Application.Abstractions.Persistence/Transactions/IPersistenceTransactionProvider.cs diff --git a/src/Atm.Application.Abstractions.Persistence/Transactions/IPersistenceTransaction.cs b/src/Atm.Application.Abstractions.Persistence/Transactions/IPersistenceTransaction.cs new file mode 100644 index 0000000..6c56d82 --- /dev/null +++ b/src/Atm.Application.Abstractions.Persistence/Transactions/IPersistenceTransaction.cs @@ -0,0 +1,8 @@ +namespace Atm.Application.Abstractions.Persistence.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.Persistence/Transactions/IPersistenceTransactionProvider.cs b/src/Atm.Application.Abstractions.Persistence/Transactions/IPersistenceTransactionProvider.cs new file mode 100644 index 0000000..1c4a0c9 --- /dev/null +++ b/src/Atm.Application.Abstractions.Persistence/Transactions/IPersistenceTransactionProvider.cs @@ -0,0 +1,10 @@ +using System.Data; + +namespace Atm.Application.Abstractions.Persistence.Transactions; + +public interface IPersistenceTransactionProvider +{ + ValueTask BeginTransactionAsync( + IsolationLevel isolationLevel, + CancellationToken cancellationToken); +} \ No newline at end of file From 83dbb3b10ea39ff9eaed77b9cc8682ad984d11d0 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sun, 18 Oct 2026 16:54:19 +0300 Subject: [PATCH 057/103] fix: changed the contracts of OperationHistoryDto.cs now id's are long --- .../Accounts/Models/OperationHistoryDto.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Atm.Application.Contracts/Accounts/Models/OperationHistoryDto.cs b/src/Atm.Application.Contracts/Accounts/Models/OperationHistoryDto.cs index c2a4c62..2483bf7 100644 --- a/src/Atm.Application.Contracts/Accounts/Models/OperationHistoryDto.cs +++ b/src/Atm.Application.Contracts/Accounts/Models/OperationHistoryDto.cs @@ -1,8 +1,9 @@ namespace Atm.Application.Contracts.Accounts.Models; public sealed record OperationHistoryDto( - Guid OperationId, - Guid AccountId, + long OperationId, + long AccountId, + DateTimeOffset Time, OperationType OperationType, decimal Amount, decimal BalanceAfter); \ No newline at end of file From 186bf1c3f2c933a8cbc9b2e819985f4a98123225 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sun, 18 Oct 2026 16:59:07 +0300 Subject: [PATCH 058/103] fix: removed OperationType.cs in dto --- .../Accounts/Models/OperationHistoryDto.cs | 2 +- .../Accounts/Models/OperationType.cs | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 src/Atm.Application.Contracts/Accounts/Models/OperationType.cs diff --git a/src/Atm.Application.Contracts/Accounts/Models/OperationHistoryDto.cs b/src/Atm.Application.Contracts/Accounts/Models/OperationHistoryDto.cs index 2483bf7..a4cadbd 100644 --- a/src/Atm.Application.Contracts/Accounts/Models/OperationHistoryDto.cs +++ b/src/Atm.Application.Contracts/Accounts/Models/OperationHistoryDto.cs @@ -4,6 +4,6 @@ public sealed record OperationHistoryDto( long OperationId, long AccountId, DateTimeOffset Time, - OperationType OperationType, + string OperationType, decimal Amount, decimal BalanceAfter); \ No newline at end of file diff --git a/src/Atm.Application.Contracts/Accounts/Models/OperationType.cs b/src/Atm.Application.Contracts/Accounts/Models/OperationType.cs deleted file mode 100644 index cfc4d7b..0000000 --- a/src/Atm.Application.Contracts/Accounts/Models/OperationType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Atm.Application.Contracts.Accounts.Models; - -public enum OperationType -{ - Withdraw, - Deposit, - ShowBalance, -} \ No newline at end of file From a5c8a072f954e5e5ef862e06b8e41e3334b3c3fc Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sun, 18 Oct 2026 16:59:46 +0300 Subject: [PATCH 059/103] style: fix the naming of method --- src/Atm.Application.Contracts/Accounts/IAccountService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Atm.Application.Contracts/Accounts/IAccountService.cs b/src/Atm.Application.Contracts/Accounts/IAccountService.cs index e47c01b..4e6b5f7 100644 --- a/src/Atm.Application.Contracts/Accounts/IAccountService.cs +++ b/src/Atm.Application.Contracts/Accounts/IAccountService.cs @@ -4,7 +4,7 @@ namespace Atm.Application.Contracts.Accounts; public interface IAccountService { - Task GetAccountAsync( + Task GetAccountBalanceAsync( GetBalance.Request request, CancellationToken cancellationToken); From 4ee6edc8003edfdd5385a37c80830319409a887a Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sun, 18 Oct 2026 17:00:04 +0300 Subject: [PATCH 060/103] feat: balance mapping to dto --- .../Mapping/BalanceMappingExtensions.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/Atm.Application/Mapping/BalanceMappingExtensions.cs diff --git a/src/Atm.Application/Mapping/BalanceMappingExtensions.cs b/src/Atm.Application/Mapping/BalanceMappingExtensions.cs new file mode 100644 index 0000000..b539122 --- /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.HighLoad.Application.Mapping; + +public static class BalanceMappingExtensions +{ + public static BalanceDto BalanceToDto( + this Money balance) + { + return new BalanceDto(balance.Value); + } +} \ No newline at end of file From 3c94c6934b81d4b0c705d6179d38e027bbe94574 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sun, 18 Oct 2026 17:00:24 +0300 Subject: [PATCH 061/103] fix: now money is a public property --- src/Atm.Application.Models/Accounts/Account.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Atm.Application.Models/Accounts/Account.cs b/src/Atm.Application.Models/Accounts/Account.cs index 0a4df9a..3b81617 100644 --- a/src/Atm.Application.Models/Accounts/Account.cs +++ b/src/Atm.Application.Models/Accounts/Account.cs @@ -5,8 +5,6 @@ namespace Atm.Application.Models.Accounts; public sealed class Account { - private Money _balance; - private Account( AccountId id, AccountNumber number, @@ -16,7 +14,7 @@ private Account( Id = id; Number = number; Pincode = pincode; - _balance = balance; + Balance = balance; } public static Account CreateFromService( @@ -45,21 +43,23 @@ public static Account CreateFromDb( public Pincode Pincode { get; } + public Money Balance { get; private set; } + public DepositResult Deposit(Money money) { - _balance += money; + Balance += money; return new DepositResult.Success(); } public WithdrawResult Withdraw(Money money) { - if (money > _balance) + if (money > Balance) { return new WithdrawResult.Failure("Insufficient funds"); } - _balance -= money; + Balance -= money; return new WithdrawResult.Success(); } From afc0bf93fa60226f079f172a53efc002391e357c Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sun, 18 Oct 2026 22:05:04 +0300 Subject: [PATCH 062/103] feat: separate operation creation and rehydration --- .../Operations/Operation.cs | 63 +++++++++++++++---- .../Operations/ValueObjects/OperationId.cs | 5 ++ 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/Atm.Application.Models/Operations/Operation.cs b/src/Atm.Application.Models/Operations/Operation.cs index 291b812..6c44e62 100644 --- a/src/Atm.Application.Models/Operations/Operation.cs +++ b/src/Atm.Application.Models/Operations/Operation.cs @@ -5,27 +5,64 @@ namespace Atm.Application.Models.Operations; public sealed class Operation { - public OperationId OperationId { get; } - - public AccountId AccountId { get; } - - public DateTimeOffset Time { get; } - - public OperationType OperationType { get; } - - public Money Balance { get; } - - public Operation( + private Operation( OperationId operationId, AccountId accountId, DateTimeOffset time, OperationType operationType, - Money balance) + Money amount, + Money balanceAfter) { OperationId = operationId; AccountId = accountId; Time = time; OperationType = operationType; - Balance = balance; + 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/ValueObjects/OperationId.cs b/src/Atm.Application.Models/Operations/ValueObjects/OperationId.cs index 60fe67f..799d77a 100644 --- a/src/Atm.Application.Models/Operations/ValueObjects/OperationId.cs +++ b/src/Atm.Application.Models/Operations/ValueObjects/OperationId.cs @@ -15,4 +15,9 @@ public static OperationId Create(long value) { return new OperationId(value); } + + public static OperationId Default() + { + return new OperationId(0); + } } \ No newline at end of file From 57b94b7191baf5bee545009d4ffcaa587bcd7170 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sun, 18 Oct 2026 22:16:24 +0300 Subject: [PATCH 063/103] feat: add OperationHistory mapper --- .../Mapping/OperationsMappingExtensions.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/Atm.Application/Mapping/OperationsMappingExtensions.cs diff --git a/src/Atm.Application/Mapping/OperationsMappingExtensions.cs b/src/Atm.Application/Mapping/OperationsMappingExtensions.cs new file mode 100644 index 0000000..de59348 --- /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.HighLoad.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 From 2ae2353c60e8e541387442d16720fdb10a27c35a Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sun, 18 Oct 2026 22:25:22 +0300 Subject: [PATCH 064/103] fix: deleted nullable reference in find method --- .../Repositories/IOperationHistoryRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs b/src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs index 5cd1c77..618c3c1 100644 --- a/src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs +++ b/src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs @@ -9,7 +9,7 @@ Task AddOperationAsync( Operation operation, CancellationToken cancellationToken); - IAsyncEnumerable FindByAccountIdAsync( + IAsyncEnumerable FindByAccountIdAsync( AccountId accountId, CancellationToken cancellationToken); } \ No newline at end of file From b1448ee207d7c13eb7fb1c7789fd21da45f40f2f Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sun, 18 Oct 2026 22:26:37 +0300 Subject: [PATCH 065/103] feat: add AccountService.cs --- .../Services/AccountService.cs | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 src/Atm.Application/Services/AccountService.cs diff --git a/src/Atm.Application/Services/AccountService.cs b/src/Atm.Application/Services/AccountService.cs new file mode 100644 index 0000000..6d95462 --- /dev/null +++ b/src/Atm.Application/Services/AccountService.cs @@ -0,0 +1,191 @@ +using Atm.Application.Abstractions.Persistence; +using Atm.Application.Contracts.Accounts; +using Atm.Application.Contracts.Accounts.Operations; +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; +using Atm.HighLoad.Application.Mapping; + +namespace Atm.HighLoad.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."); + } + + var amount = Money.Create(request.Money); + + WithdrawResult result = account.Withdraw(amount); + + 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( + WithdrawBalance.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."); + } + + var amount = Money.Create(request.Money); + + DepositResult result = 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 From 4aa15466ca65b672b6daca608b9032e411c55c24 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sun, 18 Oct 2026 22:43:34 +0300 Subject: [PATCH 066/103] fix: allow default IDs for new accounts and operations --- src/Atm.Application.Models/Accounts/ValueObjects/AccountId.cs | 4 +++- .../Operations/ValueObjects/OperationId.cs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Atm.Application.Models/Accounts/ValueObjects/AccountId.cs b/src/Atm.Application.Models/Accounts/ValueObjects/AccountId.cs index 40d8fd9..78d1fec 100644 --- a/src/Atm.Application.Models/Accounts/ValueObjects/AccountId.cs +++ b/src/Atm.Application.Models/Accounts/ValueObjects/AccountId.cs @@ -6,13 +6,15 @@ public readonly record struct AccountId private AccountId(long value) { - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); + ArgumentOutOfRangeException.ThrowIfNegative(value); Value = value; } public static AccountId Create(long value) { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); + return new AccountId(value); } diff --git a/src/Atm.Application.Models/Operations/ValueObjects/OperationId.cs b/src/Atm.Application.Models/Operations/ValueObjects/OperationId.cs index 799d77a..49cd23f 100644 --- a/src/Atm.Application.Models/Operations/ValueObjects/OperationId.cs +++ b/src/Atm.Application.Models/Operations/ValueObjects/OperationId.cs @@ -6,13 +6,15 @@ public readonly record struct OperationId private OperationId(long value) { - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); + ArgumentOutOfRangeException.ThrowIfNegative(value); Value = value; } public static OperationId Create(long value) { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); + return new OperationId(value); } From 97652e825bdf0742e8a808fb34a29234c283264c Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sun, 18 Oct 2026 22:45:54 +0300 Subject: [PATCH 067/103] fix: handle withdrawal failure before persisting changes --- src/Atm.Application/Services/AccountService.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Atm.Application/Services/AccountService.cs b/src/Atm.Application/Services/AccountService.cs index 6d95462..c19f4b2 100644 --- a/src/Atm.Application/Services/AccountService.cs +++ b/src/Atm.Application/Services/AccountService.cs @@ -86,6 +86,11 @@ await _context.OperationHistoryRepository WithdrawResult result = account.Withdraw(amount); + if (result is WithdrawResult.Failure failure) + { + return new WithdrawBalance.Response.Failure(failure.ErrorMessage); + } + await _context.AccountRepository .UpdateAccountAsync( account, From 6065d68c5868926d4a577d18d394c57886f86018 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sun, 18 Oct 2026 22:50:24 +0300 Subject: [PATCH 068/103] fix: reject non-positive deposit and withdrawal amounts --- src/Atm.Application/Services/AccountService.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Atm.Application/Services/AccountService.cs b/src/Atm.Application/Services/AccountService.cs index c19f4b2..7871deb 100644 --- a/src/Atm.Application/Services/AccountService.cs +++ b/src/Atm.Application/Services/AccountService.cs @@ -82,6 +82,11 @@ await _context.OperationHistoryRepository 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); @@ -137,7 +142,7 @@ await _context.OperationHistoryRepository var amount = Money.Create(request.Money); - DepositResult result = account.Deposit(amount); + account.Deposit(amount); await _context.AccountRepository.UpdateAccountAsync( account, From 488eb91991859564a777aebbb2772e24a81a9d04 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sun, 18 Oct 2026 22:53:47 +0300 Subject: [PATCH 069/103] fix: use deposit request type for balance deposit --- src/Atm.Application.Contracts/Accounts/IAccountService.cs | 2 +- src/Atm.Application/Services/AccountService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Atm.Application.Contracts/Accounts/IAccountService.cs b/src/Atm.Application.Contracts/Accounts/IAccountService.cs index 4e6b5f7..41a54b6 100644 --- a/src/Atm.Application.Contracts/Accounts/IAccountService.cs +++ b/src/Atm.Application.Contracts/Accounts/IAccountService.cs @@ -13,7 +13,7 @@ public interface IAccountService CancellationToken cancellationToken); Task DepositBalanceAsync( - WithdrawBalance.Request request, + DepositBalance.Request request, CancellationToken cancellationToken); Task GetOperationHistoryAsync( diff --git a/src/Atm.Application/Services/AccountService.cs b/src/Atm.Application/Services/AccountService.cs index 7871deb..690290e 100644 --- a/src/Atm.Application/Services/AccountService.cs +++ b/src/Atm.Application/Services/AccountService.cs @@ -117,7 +117,7 @@ await _context.OperationHistoryRepository } public async Task DepositBalanceAsync( - WithdrawBalance.Request request, + DepositBalance.Request request, CancellationToken cancellationToken) { UserSession? session = await _context.UserSessionRepository From 50280afb5edc79e700d4ac30c92115617da922f7 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Sun, 18 Oct 2026 23:05:02 +0300 Subject: [PATCH 070/103] style: renamed Application.Abstractions --- Atm.slnx | 2 +- .../Atm.Application.Abstractions.Persistence.csproj | 9 --------- .../IPersistenceContext.cs | 4 ++-- .../Repositories/IAccountRepository.cs | 2 +- .../Repositories/IAdminSessionRepository.cs | 2 +- .../Repositories/IOperationHistoryRepository.cs | 2 +- .../Repositories/IUserSessionRepository.cs | 2 +- .../Transactions/IPersistenceTransaction.cs | 2 +- .../Transactions/IPersistenceTransactionProvider.cs | 2 +- src/Atm.Application/Atm.Application.csproj | 2 +- src/Atm.Application/Services/AccountService.cs | 2 +- src/Atm.Application/Services/AdminService.cs | 2 +- src/Atm.Application/Services/SessionService.cs | 2 +- .../Atm.Infrastructure.Persistence.csproj | 2 +- 14 files changed, 14 insertions(+), 23 deletions(-) delete mode 100644 src/Atm.Application.Abstractions.Persistence/Atm.Application.Abstractions.Persistence.csproj rename src/{Atm.Application.Abstractions.Persistence => Atm.Application.Abstractions}/IPersistenceContext.cs (70%) rename src/{Atm.Application.Abstractions.Persistence => Atm.Application.Abstractions}/Repositories/IAccountRepository.cs (90%) rename src/{Atm.Application.Abstractions.Persistence => Atm.Application.Abstractions}/Repositories/IAdminSessionRepository.cs (83%) rename src/{Atm.Application.Abstractions.Persistence => Atm.Application.Abstractions}/Repositories/IOperationHistoryRepository.cs (85%) rename src/{Atm.Application.Abstractions.Persistence => Atm.Application.Abstractions}/Repositories/IUserSessionRepository.cs (83%) rename src/{Atm.Application.Abstractions.Persistence => Atm.Application.Abstractions}/Transactions/IPersistenceTransaction.cs (74%) rename src/{Atm.Application.Abstractions.Persistence => Atm.Application.Abstractions}/Transactions/IPersistenceTransactionProvider.cs (77%) diff --git a/Atm.slnx b/Atm.slnx index 68c2590..bc767d3 100644 --- a/Atm.slnx +++ b/Atm.slnx @@ -6,7 +6,7 @@ - + diff --git a/src/Atm.Application.Abstractions.Persistence/Atm.Application.Abstractions.Persistence.csproj b/src/Atm.Application.Abstractions.Persistence/Atm.Application.Abstractions.Persistence.csproj deleted file mode 100644 index b799d9c..0000000 --- a/src/Atm.Application.Abstractions.Persistence/Atm.Application.Abstractions.Persistence.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/Atm.Application.Abstractions.Persistence/IPersistenceContext.cs b/src/Atm.Application.Abstractions/IPersistenceContext.cs similarity index 70% rename from src/Atm.Application.Abstractions.Persistence/IPersistenceContext.cs rename to src/Atm.Application.Abstractions/IPersistenceContext.cs index d2e779f..09973b6 100644 --- a/src/Atm.Application.Abstractions.Persistence/IPersistenceContext.cs +++ b/src/Atm.Application.Abstractions/IPersistenceContext.cs @@ -1,6 +1,6 @@ -using Atm.Application.Abstractions.Persistence.Repositories; +using Atm.Application.Abstractions.Repositories; -namespace Atm.Application.Abstractions.Persistence; +namespace Atm.Application.Abstractions; public interface IPersistenceContext { diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs b/src/Atm.Application.Abstractions/Repositories/IAccountRepository.cs similarity index 90% rename from src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs rename to src/Atm.Application.Abstractions/Repositories/IAccountRepository.cs index 2c6ad71..de0a2f9 100644 --- a/src/Atm.Application.Abstractions.Persistence/Repositories/IAccountRepository.cs +++ b/src/Atm.Application.Abstractions/Repositories/IAccountRepository.cs @@ -1,7 +1,7 @@ using Atm.Application.Models.Accounts; using Atm.Application.Models.Accounts.ValueObjects; -namespace Atm.Application.Abstractions.Persistence.Repositories; +namespace Atm.Application.Abstractions.Repositories; public interface IAccountRepository { diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs b/src/Atm.Application.Abstractions/Repositories/IAdminSessionRepository.cs similarity index 83% rename from src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs rename to src/Atm.Application.Abstractions/Repositories/IAdminSessionRepository.cs index 8ad294e..e74faf8 100644 --- a/src/Atm.Application.Abstractions.Persistence/Repositories/IAdminSessionRepository.cs +++ b/src/Atm.Application.Abstractions/Repositories/IAdminSessionRepository.cs @@ -1,6 +1,6 @@ using Atm.Application.Models.Sessions; -namespace Atm.Application.Abstractions.Persistence.Repositories; +namespace Atm.Application.Abstractions.Repositories; public interface IAdminSessionRepository { diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs b/src/Atm.Application.Abstractions/Repositories/IOperationHistoryRepository.cs similarity index 85% rename from src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs rename to src/Atm.Application.Abstractions/Repositories/IOperationHistoryRepository.cs index 618c3c1..984bcfb 100644 --- a/src/Atm.Application.Abstractions.Persistence/Repositories/IOperationHistoryRepository.cs +++ b/src/Atm.Application.Abstractions/Repositories/IOperationHistoryRepository.cs @@ -1,7 +1,7 @@ using Atm.Application.Models.Accounts.ValueObjects; using Atm.Application.Models.Operations; -namespace Atm.Application.Abstractions.Persistence.Repositories; +namespace Atm.Application.Abstractions.Repositories; public interface IOperationHistoryRepository { diff --git a/src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs b/src/Atm.Application.Abstractions/Repositories/IUserSessionRepository.cs similarity index 83% rename from src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs rename to src/Atm.Application.Abstractions/Repositories/IUserSessionRepository.cs index 503c2ce..69e12c7 100644 --- a/src/Atm.Application.Abstractions.Persistence/Repositories/IUserSessionRepository.cs +++ b/src/Atm.Application.Abstractions/Repositories/IUserSessionRepository.cs @@ -1,6 +1,6 @@ using Atm.Application.Models.Sessions; -namespace Atm.Application.Abstractions.Persistence.Repositories; +namespace Atm.Application.Abstractions.Repositories; public interface IUserSessionRepository { diff --git a/src/Atm.Application.Abstractions.Persistence/Transactions/IPersistenceTransaction.cs b/src/Atm.Application.Abstractions/Transactions/IPersistenceTransaction.cs similarity index 74% rename from src/Atm.Application.Abstractions.Persistence/Transactions/IPersistenceTransaction.cs rename to src/Atm.Application.Abstractions/Transactions/IPersistenceTransaction.cs index 6c56d82..19fbc57 100644 --- a/src/Atm.Application.Abstractions.Persistence/Transactions/IPersistenceTransaction.cs +++ b/src/Atm.Application.Abstractions/Transactions/IPersistenceTransaction.cs @@ -1,4 +1,4 @@ -namespace Atm.Application.Abstractions.Persistence.Transactions; +namespace Atm.Application.Abstractions.Transactions; public interface IPersistenceTransaction : IAsyncDisposable { diff --git a/src/Atm.Application.Abstractions.Persistence/Transactions/IPersistenceTransactionProvider.cs b/src/Atm.Application.Abstractions/Transactions/IPersistenceTransactionProvider.cs similarity index 77% rename from src/Atm.Application.Abstractions.Persistence/Transactions/IPersistenceTransactionProvider.cs rename to src/Atm.Application.Abstractions/Transactions/IPersistenceTransactionProvider.cs index 1c4a0c9..05a088c 100644 --- a/src/Atm.Application.Abstractions.Persistence/Transactions/IPersistenceTransactionProvider.cs +++ b/src/Atm.Application.Abstractions/Transactions/IPersistenceTransactionProvider.cs @@ -1,6 +1,6 @@ using System.Data; -namespace Atm.Application.Abstractions.Persistence.Transactions; +namespace Atm.Application.Abstractions.Transactions; public interface IPersistenceTransactionProvider { diff --git a/src/Atm.Application/Atm.Application.csproj b/src/Atm.Application/Atm.Application.csproj index 6dc4b91..67b2696 100644 --- a/src/Atm.Application/Atm.Application.csproj +++ b/src/Atm.Application/Atm.Application.csproj @@ -4,7 +4,7 @@ - + diff --git a/src/Atm.Application/Services/AccountService.cs b/src/Atm.Application/Services/AccountService.cs index 690290e..0baacad 100644 --- a/src/Atm.Application/Services/AccountService.cs +++ b/src/Atm.Application/Services/AccountService.cs @@ -1,4 +1,4 @@ -using Atm.Application.Abstractions.Persistence; +using Atm.Application.Abstractions; using Atm.Application.Contracts.Accounts; using Atm.Application.Contracts.Accounts.Operations; using Atm.Application.Models.Accounts; diff --git a/src/Atm.Application/Services/AdminService.cs b/src/Atm.Application/Services/AdminService.cs index 4cfcac3..52805b5 100644 --- a/src/Atm.Application/Services/AdminService.cs +++ b/src/Atm.Application/Services/AdminService.cs @@ -1,4 +1,4 @@ -using Atm.Application.Abstractions.Persistence; +using Atm.Application.Abstractions; using Atm.Application.Contracts.Admins; using Atm.Application.Contracts.Admins.Models; using Atm.Application.Models.Accounts; diff --git a/src/Atm.Application/Services/SessionService.cs b/src/Atm.Application/Services/SessionService.cs index 054fb1f..f1c8c0c 100644 --- a/src/Atm.Application/Services/SessionService.cs +++ b/src/Atm.Application/Services/SessionService.cs @@ -1,4 +1,4 @@ -using Atm.Application.Abstractions.Persistence; +using Atm.Application.Abstractions; using Atm.Application.Contracts.Sessions; using Atm.Application.Contracts.Sessions.Operations; using Atm.Application.Models.Accounts; diff --git a/src/Atm.Infrastructure.Persistence/Atm.Infrastructure.Persistence.csproj b/src/Atm.Infrastructure.Persistence/Atm.Infrastructure.Persistence.csproj index f7e331e..38e93a9 100644 --- a/src/Atm.Infrastructure.Persistence/Atm.Infrastructure.Persistence.csproj +++ b/src/Atm.Infrastructure.Persistence/Atm.Infrastructure.Persistence.csproj @@ -11,6 +11,6 @@ - + From fe62447da6ba4bfba75a333daf2dbc60168e211d Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 08:47:46 +0300 Subject: [PATCH 071/103] fix: renamed the .csproj --- .../Atm.Application.Abstractions.csproj | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/Atm.Application.Abstractions/Atm.Application.Abstractions.csproj diff --git a/src/Atm.Application.Abstractions/Atm.Application.Abstractions.csproj b/src/Atm.Application.Abstractions/Atm.Application.Abstractions.csproj new file mode 100644 index 0000000..b799d9c --- /dev/null +++ b/src/Atm.Application.Abstractions/Atm.Application.Abstractions.csproj @@ -0,0 +1,9 @@ + + + + + + + + + From cb42df90c63ae75be42346db8a47369b870eafb0 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 09:06:49 +0300 Subject: [PATCH 072/103] feat: add inmemory AccountRepository.cs with sync methods:) --- .../Repositories/AccountRepository.cs | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/Atm.Infrastructure.Persistence/Repositories/AccountRepository.cs diff --git a/src/Atm.Infrastructure.Persistence/Repositories/AccountRepository.cs b/src/Atm.Infrastructure.Persistence/Repositories/AccountRepository.cs new file mode 100644 index 0000000..e138d48 --- /dev/null +++ b/src/Atm.Infrastructure.Persistence/Repositories/AccountRepository.cs @@ -0,0 +1,99 @@ +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(account)); + + 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 From 306e64c1f0741b7dd054dec3a2511776ae998416 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 09:14:55 +0300 Subject: [PATCH 073/103] feat: add inmemory SessionRepositories with concurrent dictionary --- .../Repositories/AdminSessionRepository.cs | 35 +++++++++++++++++++ .../Repositories/UserSessionRepository.cs | 35 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 src/Atm.Infrastructure.Persistence/Repositories/AdminSessionRepository.cs create mode 100644 src/Atm.Infrastructure.Persistence/Repositories/UserSessionRepository.cs diff --git a/src/Atm.Infrastructure.Persistence/Repositories/AdminSessionRepository.cs b/src/Atm.Infrastructure.Persistence/Repositories/AdminSessionRepository.cs new file mode 100644 index 0000000..75b1a6d --- /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 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/UserSessionRepository.cs b/src/Atm.Infrastructure.Persistence/Repositories/UserSessionRepository.cs new file mode 100644 index 0000000..cb1ea72 --- /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 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 From ee073492fb386259c73a456b884645db82404c43 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 09:15:22 +0300 Subject: [PATCH 074/103] fix: add sealed to repositories --- .../Repositories/AdminSessionRepository.cs | 2 +- .../Repositories/UserSessionRepository.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Atm.Infrastructure.Persistence/Repositories/AdminSessionRepository.cs b/src/Atm.Infrastructure.Persistence/Repositories/AdminSessionRepository.cs index 75b1a6d..526120a 100644 --- a/src/Atm.Infrastructure.Persistence/Repositories/AdminSessionRepository.cs +++ b/src/Atm.Infrastructure.Persistence/Repositories/AdminSessionRepository.cs @@ -4,7 +4,7 @@ namespace Atm.Infrastructure.Persistence.Repositories; -public class AdminSessionRepository : IAdminSessionRepository +public sealed class AdminSessionRepository : IAdminSessionRepository { private readonly ConcurrentDictionary _values = new(); diff --git a/src/Atm.Infrastructure.Persistence/Repositories/UserSessionRepository.cs b/src/Atm.Infrastructure.Persistence/Repositories/UserSessionRepository.cs index cb1ea72..a5135fc 100644 --- a/src/Atm.Infrastructure.Persistence/Repositories/UserSessionRepository.cs +++ b/src/Atm.Infrastructure.Persistence/Repositories/UserSessionRepository.cs @@ -4,7 +4,7 @@ namespace Atm.Infrastructure.Persistence.Repositories; -public class UserSessionRepository : IUserSessionRepository +public sealed class UserSessionRepository : IUserSessionRepository { private readonly ConcurrentDictionary _values = new(); From 61c70bf6c3c0d775db483d64d0e7ab916e5ea09e Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 10:08:45 +0300 Subject: [PATCH 075/103] feat: add inmemory repository for operation history --- .../OperationHistoryRepository.cs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/Atm.Infrastructure.Persistence/Repositories/OperationHistoryRepository.cs 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 From bf94496311f7164a30063c2bcba8311cfb1a3187 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 10:10:27 +0300 Subject: [PATCH 076/103] feat: implemented PersistenceContext in infrastructure --- .../PersistenceContext.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/Atm.Infrastructure.Persistence/PersistenceContext.cs 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 From 9e8a2e18169d33552eb0ff5d52af86892b37b628 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 10:20:41 +0300 Subject: [PATCH 077/103] feat: add services and repositories into DI --- .../ServiceCollectionExtensions.cs | 20 +++++++++++++++++++ .../ServiceCollectionExtensions.cs | 19 ++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/Atm.Application/ServiceCollectionExtensions.cs create mode 100644 src/Atm.Infrastructure.Persistence/ServiceCollectionExtensions.cs diff --git a/src/Atm.Application/ServiceCollectionExtensions.cs b/src/Atm.Application/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..4e5fb6f --- /dev/null +++ b/src/Atm.Application/ServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +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 collection) + { + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + + return collection; + } +} \ 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..39e8363 --- /dev/null +++ b/src/Atm.Infrastructure.Persistence/ServiceCollectionExtensions.cs @@ -0,0 +1,19 @@ +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.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + + return collection; + } +} \ No newline at end of file From ced474a98671949caa0da664dfccbd2fb75de92b Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 10:21:08 +0300 Subject: [PATCH 078/103] fix: changed the root namespace of Atm.Application.csproj --- src/Atm.Application/Atm.Application.csproj | 2 +- src/Atm.Application/Mapping/AccountMappingExtensions.cs | 2 +- src/Atm.Application/Mapping/BalanceMappingExtensions.cs | 2 +- src/Atm.Application/Mapping/OperationsMappingExtensions.cs | 2 +- src/Atm.Application/Mapping/SessionMappingExtensions.cs | 2 +- src/Atm.Application/Options/SessionOptions.cs | 2 +- src/Atm.Application/Services/AccountService.cs | 4 ++-- src/Atm.Application/Services/AdminService.cs | 4 ++-- src/Atm.Application/Services/SessionService.cs | 6 +++--- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Atm.Application/Atm.Application.csproj b/src/Atm.Application/Atm.Application.csproj index 67b2696..579ae02 100644 --- a/src/Atm.Application/Atm.Application.csproj +++ b/src/Atm.Application/Atm.Application.csproj @@ -1,6 +1,6 @@  - Atm.HighLoad.Application + Atm.Application diff --git a/src/Atm.Application/Mapping/AccountMappingExtensions.cs b/src/Atm.Application/Mapping/AccountMappingExtensions.cs index 5268af9..4d133e2 100644 --- a/src/Atm.Application/Mapping/AccountMappingExtensions.cs +++ b/src/Atm.Application/Mapping/AccountMappingExtensions.cs @@ -1,7 +1,7 @@ using Atm.Application.Contracts.Admins.Operations; using Atm.Application.Models.Accounts; -namespace Atm.HighLoad.Application.Mapping; +namespace Atm.Application.Mapping; public static class AccountMappingExtensions { diff --git a/src/Atm.Application/Mapping/BalanceMappingExtensions.cs b/src/Atm.Application/Mapping/BalanceMappingExtensions.cs index b539122..444278c 100644 --- a/src/Atm.Application/Mapping/BalanceMappingExtensions.cs +++ b/src/Atm.Application/Mapping/BalanceMappingExtensions.cs @@ -1,7 +1,7 @@ using Atm.Application.Contracts.Accounts.Models; using Atm.Application.Models.Accounts.ValueObjects; -namespace Atm.HighLoad.Application.Mapping; +namespace Atm.Application.Mapping; public static class BalanceMappingExtensions { diff --git a/src/Atm.Application/Mapping/OperationsMappingExtensions.cs b/src/Atm.Application/Mapping/OperationsMappingExtensions.cs index de59348..c0d917f 100644 --- a/src/Atm.Application/Mapping/OperationsMappingExtensions.cs +++ b/src/Atm.Application/Mapping/OperationsMappingExtensions.cs @@ -1,7 +1,7 @@ using Atm.Application.Contracts.Accounts.Models; using Atm.Application.Models.Operations; -namespace Atm.HighLoad.Application.Mapping; +namespace Atm.Application.Mapping; public static class OperationsMappingExtensions { diff --git a/src/Atm.Application/Mapping/SessionMappingExtensions.cs b/src/Atm.Application/Mapping/SessionMappingExtensions.cs index 4fac1d0..68e4ee9 100644 --- a/src/Atm.Application/Mapping/SessionMappingExtensions.cs +++ b/src/Atm.Application/Mapping/SessionMappingExtensions.cs @@ -1,7 +1,7 @@ using Atm.Application.Contracts.Sessions.Models; using Atm.Application.Models.Sessions; -namespace Atm.HighLoad.Application.Mapping; +namespace Atm.Application.Mapping; public static class SessionMappingExtensions { diff --git a/src/Atm.Application/Options/SessionOptions.cs b/src/Atm.Application/Options/SessionOptions.cs index 2fdfbb2..fb0c3a4 100644 --- a/src/Atm.Application/Options/SessionOptions.cs +++ b/src/Atm.Application/Options/SessionOptions.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Atm.HighLoad.Application.Options; +namespace Atm.Application.Options; public sealed class SessionOptions { diff --git a/src/Atm.Application/Services/AccountService.cs b/src/Atm.Application/Services/AccountService.cs index 0baacad..20502a5 100644 --- a/src/Atm.Application/Services/AccountService.cs +++ b/src/Atm.Application/Services/AccountService.cs @@ -6,9 +6,9 @@ using Atm.Application.Models.Accounts.ValueObjects; using Atm.Application.Models.Operations; using Atm.Application.Models.Sessions; -using Atm.HighLoad.Application.Mapping; +using Atm.Application.Mapping; -namespace Atm.HighLoad.Application.Services; +namespace Atm.Application.Services; public sealed class AccountService : IAccountService { diff --git a/src/Atm.Application/Services/AdminService.cs b/src/Atm.Application/Services/AdminService.cs index 52805b5..0012b93 100644 --- a/src/Atm.Application/Services/AdminService.cs +++ b/src/Atm.Application/Services/AdminService.cs @@ -4,9 +4,9 @@ using Atm.Application.Models.Accounts; using Atm.Application.Models.Accounts.ValueObjects; using Atm.Application.Models.Sessions; -using Atm.HighLoad.Application.Mapping; +using Atm.Application.Mapping; -namespace Atm.HighLoad.Application.Services; +namespace Atm.Application.Services; public sealed class AdminService : IAdminService { diff --git a/src/Atm.Application/Services/SessionService.cs b/src/Atm.Application/Services/SessionService.cs index f1c8c0c..210438b 100644 --- a/src/Atm.Application/Services/SessionService.cs +++ b/src/Atm.Application/Services/SessionService.cs @@ -4,11 +4,11 @@ using Atm.Application.Models.Accounts; using Atm.Application.Models.Accounts.ValueObjects; using Atm.Application.Models.Sessions; -using Atm.HighLoad.Application.Mapping; -using Atm.HighLoad.Application.Options; +using Atm.Application.Mapping; +using Atm.Application.Options; using Microsoft.Extensions.Options; -namespace Atm.HighLoad.Application.Services; +namespace Atm.Application.Services; public sealed class SessionService : ISessionService { From 056f253d88a6a5d26f9e854675bd579a66b51ead Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 10:22:36 +0300 Subject: [PATCH 079/103] fix: alphabetely ordered usings --- src/Atm.Application/Services/SessionService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Atm.Application/Services/SessionService.cs b/src/Atm.Application/Services/SessionService.cs index 210438b..1b7c8b2 100644 --- a/src/Atm.Application/Services/SessionService.cs +++ b/src/Atm.Application/Services/SessionService.cs @@ -1,10 +1,10 @@ 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.Mapping; using Atm.Application.Options; using Microsoft.Extensions.Options; From bf33d52feecb885d369c08feeeb3ea4c9ad61489 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 12:29:40 +0300 Subject: [PATCH 080/103] chore: order usings in services --- src/Atm.Application/Services/AccountService.cs | 2 +- src/Atm.Application/Services/AdminService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Atm.Application/Services/AccountService.cs b/src/Atm.Application/Services/AccountService.cs index 20502a5..2672103 100644 --- a/src/Atm.Application/Services/AccountService.cs +++ b/src/Atm.Application/Services/AccountService.cs @@ -1,12 +1,12 @@ 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; -using Atm.Application.Mapping; namespace Atm.Application.Services; diff --git a/src/Atm.Application/Services/AdminService.cs b/src/Atm.Application/Services/AdminService.cs index 0012b93..394ee89 100644 --- a/src/Atm.Application/Services/AdminService.cs +++ b/src/Atm.Application/Services/AdminService.cs @@ -1,10 +1,10 @@ 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; -using Atm.Application.Mapping; namespace Atm.Application.Services; From fe5d5acfe0fb87d45d7ea4a7dce19a81060f14bd Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 12:30:32 +0300 Subject: [PATCH 081/103] chore: update central package versions --- Directory.Packages.props | 77 ++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 671bea8..cab85e2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,36 +1,45 @@ - - true - false - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + true + false + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From fb5c87c4bf2bd84944bc7b6b8249e708bb958abc Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 12:30:56 +0300 Subject: [PATCH 082/103] feat: add Microsoft.AspnetCore into Presentation layer --- src/Atm.Presentation/Atm.Presentation.csproj | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Atm.Presentation/Atm.Presentation.csproj b/src/Atm.Presentation/Atm.Presentation.csproj index 4986e6e..59e27dd 100644 --- a/src/Atm.Presentation/Atm.Presentation.csproj +++ b/src/Atm.Presentation/Atm.Presentation.csproj @@ -5,7 +5,14 @@ - + + + + + + + + \ No newline at end of file From 86174dea76f85d68a3f2c09741bca7776bc002ac Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 12:43:46 +0300 Subject: [PATCH 083/103] feat: add AccountController with CreatingAccount method --- .../Controllers/AccountController.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/Atm.Presentation/Controllers/AccountController.cs diff --git a/src/Atm.Presentation/Controllers/AccountController.cs b/src/Atm.Presentation/Controllers/AccountController.cs new file mode 100644 index 0000000..134ad46 --- /dev/null +++ b/src/Atm.Presentation/Controllers/AccountController.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 AccountController : ControllerBase +{ + private readonly IAdminService _adminService; + + public AccountController(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 From d0397750c3d579b72fa7c4481d6a8f5be5e1d830 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 12:56:04 +0300 Subject: [PATCH 084/103] style: add slash before api --- src/Atm.Presentation/Controllers/AccountController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Atm.Presentation/Controllers/AccountController.cs b/src/Atm.Presentation/Controllers/AccountController.cs index 134ad46..cfd10a4 100644 --- a/src/Atm.Presentation/Controllers/AccountController.cs +++ b/src/Atm.Presentation/Controllers/AccountController.cs @@ -8,7 +8,7 @@ namespace Atm.Presentation.Controllers; [ApiController] -[Route("api/admin")] +[Route("/api/admin")] public sealed class AccountController : ControllerBase { private readonly IAdminService _adminService; From d2eaa89382a1e51b3f1ea4655f8dec665028a76b Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 23:42:16 +0300 Subject: [PATCH 085/103] feat: add dependencies --- src/Atm.Presentation/Atm.Presentation.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Atm.Presentation/Atm.Presentation.csproj b/src/Atm.Presentation/Atm.Presentation.csproj index 59e27dd..35c268b 100644 --- a/src/Atm.Presentation/Atm.Presentation.csproj +++ b/src/Atm.Presentation/Atm.Presentation.csproj @@ -8,10 +8,6 @@ - - - - From b7bbba31040d2cbe733270d6c6cfd3830b80d137 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 23:42:44 +0300 Subject: [PATCH 086/103] feat: add http requests models for SessionController.cs --- .../Models/CreateAdminSessionRequest.cs | 10 ++++++++++ .../Models/CreateUserSessionRequest.cs | 14 ++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 src/Atm.Presentation/Models/CreateAdminSessionRequest.cs create mode 100644 src/Atm.Presentation/Models/CreateUserSessionRequest.cs 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 From b9fc0fa803fadf9a1528e9b73f2691c81988d125 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 23:43:05 +0300 Subject: [PATCH 087/103] feat: add http requests models for AccountController.cs --- .../Models/CreateAccountRequest.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/Atm.Presentation/Models/CreateAccountRequest.cs 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 From c124f5722730b055afd3be68901624c562fc4264 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 23:43:13 +0300 Subject: [PATCH 088/103] feat: implemented SessionController.cs --- .../Controllers/SessionController.cs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/Atm.Presentation/Controllers/SessionController.cs diff --git a/src/Atm.Presentation/Controllers/SessionController.cs b/src/Atm.Presentation/Controllers/SessionController.cs new file mode 100644 index 0000000..8319817 --- /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/session")] +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 From c540067881917d290daae3f7a3e1165a1d30dbc3 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 23:44:10 +0300 Subject: [PATCH 089/103] chore: renamed the AdminController.cs --- .../Controllers/{AccountController.cs => AdminController.cs} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/Atm.Presentation/Controllers/{AccountController.cs => AdminController.cs} (91%) diff --git a/src/Atm.Presentation/Controllers/AccountController.cs b/src/Atm.Presentation/Controllers/AdminController.cs similarity index 91% rename from src/Atm.Presentation/Controllers/AccountController.cs rename to src/Atm.Presentation/Controllers/AdminController.cs index cfd10a4..edb3e9f 100644 --- a/src/Atm.Presentation/Controllers/AccountController.cs +++ b/src/Atm.Presentation/Controllers/AdminController.cs @@ -9,11 +9,11 @@ namespace Atm.Presentation.Controllers; [ApiController] [Route("/api/admin")] -public sealed class AccountController : ControllerBase +public sealed class AdminController : ControllerBase { private readonly IAdminService _adminService; - public AccountController(IAdminService adminService) + public AdminController(IAdminService adminService) { _adminService = adminService; } From a213576927c3881421535fd3be0a6706b24be720 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 19 Oct 2026 23:50:37 +0300 Subject: [PATCH 090/103] chore: renamed the route of SessionController.cs --- src/Atm.Presentation/Controllers/SessionController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Atm.Presentation/Controllers/SessionController.cs b/src/Atm.Presentation/Controllers/SessionController.cs index 8319817..8c4539d 100644 --- a/src/Atm.Presentation/Controllers/SessionController.cs +++ b/src/Atm.Presentation/Controllers/SessionController.cs @@ -8,7 +8,7 @@ namespace Atm.Presentation.Controllers; [ApiController] -[Route("/api/session")] +[Route("/api/sessions")] public sealed class SessionController : ControllerBase { private readonly ISessionService _sessionService; From f3a0a84f7c4fdb5632e7d0a434fa4fb4f263d3cb Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Tue, 20 Oct 2026 00:07:15 +0300 Subject: [PATCH 091/103] feat: add http request models for AccountController.cs --- src/Atm.Presentation/Models/BalanceRequest.cs | 6 ++++++ src/Atm.Presentation/Models/DepositRequest.cs | 8 ++++++++ src/Atm.Presentation/Models/OperationHistoryRequest.cs | 6 ++++++ src/Atm.Presentation/Models/WithdrawRequest.cs | 8 ++++++++ 4 files changed, 28 insertions(+) create mode 100644 src/Atm.Presentation/Models/BalanceRequest.cs create mode 100644 src/Atm.Presentation/Models/DepositRequest.cs create mode 100644 src/Atm.Presentation/Models/OperationHistoryRequest.cs create mode 100644 src/Atm.Presentation/Models/WithdrawRequest.cs 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/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 From 3c6b8dd53b5bac9be6f799366b911ecc2a08460c Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Tue, 20 Oct 2026 00:07:24 +0300 Subject: [PATCH 092/103] feat: implemented AccountController.cs --- .../Controllers/AccountController.cs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/Atm.Presentation/Controllers/AccountController.cs diff --git a/src/Atm.Presentation/Controllers/AccountController.cs b/src/Atm.Presentation/Controllers/AccountController.cs new file mode 100644 index 0000000..183baeb --- /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( + [FromBody] 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 From 739c694963724208d9331644c94455fa177de970 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Tue, 20 Oct 2026 00:12:44 +0300 Subject: [PATCH 093/103] feat: add HttpControllers into DI --- .../ServiceCollectionExtensions.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/Atm.Presentation/ServiceCollectionExtensions.cs 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 From 3c462e9b6801f1af46e476298a665723217983b5 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Tue, 20 Oct 2026 00:23:14 +0300 Subject: [PATCH 094/103] feat: app entry point of the project --- src/Atm.Api/Atm.Api.csproj | 10 ++++++++++ src/Atm.Api/Program.cs | 30 ++++++++++++++++++++++-------- src/Atm.Presentation/Program.cs | 0 3 files changed, 32 insertions(+), 8 deletions(-) delete mode 100644 src/Atm.Presentation/Program.cs diff --git a/src/Atm.Api/Atm.Api.csproj b/src/Atm.Api/Atm.Api.csproj index edbd018..3dfce4c 100644 --- a/src/Atm.Api/Atm.Api.csproj +++ b/src/Atm.Api/Atm.Api.csproj @@ -1,2 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Atm.Api/Program.cs b/src/Atm.Api/Program.cs index c9baf2f..1b44adf 100644 --- a/src/Atm.Api/Program.cs +++ b/src/Atm.Api/Program.cs @@ -1,8 +1,22 @@ -namespace Atm.Api; - -public class Program -{ - public static void Main() - { - } -} \ No newline at end of file +using Atm.Application; +using Atm.Infrastructure.Persistence; +using Atm.Presentation; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddApplication() + .AddInfrastructure() + .AddPresentationHttp(); + +builder.Services.AddSwaggerGen().AddEndpointsApiExplorer(); + +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/Program.cs b/src/Atm.Presentation/Program.cs deleted file mode 100644 index e69de29..0000000 From aa7763d382cb411aee716ffb532e6fa5bb1394bd Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Tue, 20 Oct 2026 00:24:38 +0300 Subject: [PATCH 095/103] fix: formatting whitespaces --- .../Repositories/AccountRepository.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Atm.Infrastructure.Persistence/Repositories/AccountRepository.cs b/src/Atm.Infrastructure.Persistence/Repositories/AccountRepository.cs index e138d48..42ff8c3 100644 --- a/src/Atm.Infrastructure.Persistence/Repositories/AccountRepository.cs +++ b/src/Atm.Infrastructure.Persistence/Repositories/AccountRepository.cs @@ -10,7 +10,6 @@ public sealed class AccountRepository : IAccountRepository private readonly Lock _sync = new(); private long _lastId; - public Task AddAccountAsync( Account account, CancellationToken cancellationToken) @@ -84,7 +83,7 @@ public Task UpdateAccountAsync( _values[account.Id] = Copy(account); } - + return Task.CompletedTask; } From eb580c3782c646a4a210b8562760ab05eaddb6d3 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Tue, 20 Oct 2026 00:25:05 +0300 Subject: [PATCH 096/103] chore: deleted solution folder of tests --- Atm.slnx | 1 - 1 file changed, 1 deletion(-) diff --git a/Atm.slnx b/Atm.slnx index bc767d3..a6b4f61 100644 --- a/Atm.slnx +++ b/Atm.slnx @@ -20,5 +20,4 @@ - \ No newline at end of file From f559f411cb6fdfc3bfb2194c43f1d798929459cb Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Tue, 20 Oct 2026 00:27:18 +0300 Subject: [PATCH 097/103] fix: upgrade Microsoft.OpenApi to patched version --- Directory.Packages.props | 1 + src/Atm.Api/Atm.Api.csproj | 1 + 2 files changed, 2 insertions(+) diff --git a/Directory.Packages.props b/Directory.Packages.props index cab85e2..bd8297e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -41,5 +41,6 @@ + \ No newline at end of file diff --git a/src/Atm.Api/Atm.Api.csproj b/src/Atm.Api/Atm.Api.csproj index 3dfce4c..dee25f9 100644 --- a/src/Atm.Api/Atm.Api.csproj +++ b/src/Atm.Api/Atm.Api.csproj @@ -8,5 +8,6 @@ + \ No newline at end of file From 0d43e96e2eb3bc3b22410ac2d02825f82faee356 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Tue, 20 Oct 2026 00:56:46 +0300 Subject: [PATCH 098/103] feat: add validation for deposit so it won't throw 500:) --- src/Atm.Application/Services/AccountService.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Atm.Application/Services/AccountService.cs b/src/Atm.Application/Services/AccountService.cs index 2672103..1fa2f00 100644 --- a/src/Atm.Application/Services/AccountService.cs +++ b/src/Atm.Application/Services/AccountService.cs @@ -140,6 +140,11 @@ await _context.OperationHistoryRepository 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); From c05d261a4c4619c4f5a8c0038ba634829bbf8e6a Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Tue, 20 Oct 2026 00:57:17 +0300 Subject: [PATCH 099/103] feat: GetOperationHistory now returns Collection of operations --- src/Atm.Presentation/Controllers/AccountController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Atm.Presentation/Controllers/AccountController.cs b/src/Atm.Presentation/Controllers/AccountController.cs index 183baeb..1720ccc 100644 --- a/src/Atm.Presentation/Controllers/AccountController.cs +++ b/src/Atm.Presentation/Controllers/AccountController.cs @@ -20,7 +20,7 @@ public AccountController(IAccountService accountService) [HttpGet("balance")] public async Task> GetAccountBalanceAsync( - [FromBody] BalanceRequest httpRequest, + [FromQuery] BalanceRequest httpRequest, CancellationToken cancellationToken) { var request = new GetBalance.Request( @@ -78,7 +78,7 @@ public async Task> DepositBalanceAsync( } [HttpGet("history")] - public async Task> GetOperationHistoryAsync( + public async Task>> GetOperationHistoryAsync( [FromQuery] OperationHistoryRequest httpRequest, CancellationToken cancellationToken) { From 164ec44eb08b58d106a8b0268b72cbbe3a5614be Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Tue, 20 Oct 2026 00:57:41 +0300 Subject: [PATCH 100/103] fix: refactor copy in method --- .../Repositories/AccountRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Atm.Infrastructure.Persistence/Repositories/AccountRepository.cs b/src/Atm.Infrastructure.Persistence/Repositories/AccountRepository.cs index 42ff8c3..b8af788 100644 --- a/src/Atm.Infrastructure.Persistence/Repositories/AccountRepository.cs +++ b/src/Atm.Infrastructure.Persistence/Repositories/AccountRepository.cs @@ -30,7 +30,7 @@ public Task AddAccountAsync( account.Pincode, account.Balance); - _values.Add(savedAccount.Id, Copy(account)); + _values.Add(savedAccount.Id, Copy(savedAccount)); return Task.FromResult(savedAccount); } From 6d364036907bb4b8d2ff4754a96451e2955cc639 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Tue, 20 Oct 2026 00:58:02 +0300 Subject: [PATCH 101/103] fix: add singleton inmemory repository instead of scoped --- .../ServiceCollectionExtensions.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Atm.Infrastructure.Persistence/ServiceCollectionExtensions.cs b/src/Atm.Infrastructure.Persistence/ServiceCollectionExtensions.cs index 39e8363..a61bab9 100644 --- a/src/Atm.Infrastructure.Persistence/ServiceCollectionExtensions.cs +++ b/src/Atm.Infrastructure.Persistence/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using Atm.Application.Abstractions.Repositories; +using Atm.Application.Abstractions; +using Atm.Application.Abstractions.Repositories; using Atm.Infrastructure.Persistence.Repositories; using Microsoft.Extensions.DependencyInjection; @@ -9,10 +10,12 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddInfrastructure( this IServiceCollection collection) { - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(); + + collection.AddScoped(); return collection; } From f2f97b87b51df5bde55f0d2b1e72b7925fe8ab17 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Tue, 20 Oct 2026 00:58:23 +0300 Subject: [PATCH 102/103] fix: add Options for system password --- src/Atm.Api/Program.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Atm.Api/Program.cs b/src/Atm.Api/Program.cs index 1b44adf..5dade1d 100644 --- a/src/Atm.Api/Program.cs +++ b/src/Atm.Api/Program.cs @@ -1,6 +1,7 @@ using Atm.Application; using Atm.Infrastructure.Persistence; using Atm.Presentation; +using SessionOptions = Atm.Application.Options.SessionOptions; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -11,6 +12,12 @@ builder.Services.AddSwaggerGen().AddEndpointsApiExplorer(); +builder.Services + .AddOptions() + .Bind(builder.Configuration.GetRequiredSection("Session")) + .ValidateDataAnnotations() + .ValidateOnStart(); + WebApplication app = builder.Build(); app.UseSwagger(); From ed4f08681913556fdb179478f84b2d147f4c0064 Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Tue, 20 Oct 2026 00:58:27 +0300 Subject: [PATCH 103/103] fix: add Options for system password --- src/Atm.Api/appsettings.Development.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Atm.Api/appsettings.Development.json b/src/Atm.Api/appsettings.Development.json index 0c208ae..8110c73 100644 --- a/src/Atm.Api/appsettings.Development.json +++ b/src/Atm.Api/appsettings.Development.json @@ -4,5 +4,8 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "Session": { + "SystemPassword": "admin" } -} +} \ No newline at end of file