├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── NPay.rest ├── NPay.sln ├── README.md ├── docker-compose.yml └── src ├── Bootstrapper └── NPay.Bootstrapper │ ├── NPay.Bootstrapper.csproj │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Startup.cs │ ├── appsettings.development.json │ ├── appsettings.docker.json │ └── appsettings.json ├── Modules ├── Notifications │ ├── NPay.Modules.Notifications.Api │ │ ├── Extensions.cs │ │ ├── Handlers │ │ │ ├── Owners │ │ │ │ └── OwnerVerifiedHandler.cs │ │ │ ├── Users │ │ │ │ ├── UserCreatedHandler.cs │ │ │ │ └── UserVerifiedHandler.cs │ │ │ └── Wallets │ │ │ │ ├── FundsAddedHandler.cs │ │ │ │ ├── FundsTransferredHandler.cs │ │ │ │ └── WalletAddedHandler.cs │ │ ├── NPay.Modules.Notifications.Api.csproj │ │ └── Services │ │ │ ├── EmailResolver.cs │ │ │ ├── EmailSender.cs │ │ │ ├── IEmailResolver.cs │ │ │ ├── IEmailSender.cs │ │ │ └── NotificationsModuleApi.cs │ └── NPay.Modules.Notifications.Shared │ │ ├── INotificationsModuleApi.cs │ │ └── NPay.Modules.Notifications.Shared.csproj ├── Users │ ├── NPay.Modules.Users.Api │ │ ├── Controllers │ │ │ └── UsersController.cs │ │ ├── Extensions.cs │ │ └── NPay.Modules.Users.Api.csproj │ ├── NPay.Modules.Users.Core │ │ ├── DAL │ │ │ ├── Configurations │ │ │ │ └── UserConfiguration.cs │ │ │ ├── Migrations │ │ │ │ ├── 20211019160150_Users_Init.Designer.cs │ │ │ │ ├── 20211019160150_Users_Init.cs │ │ │ │ └── UsersDbContextModelSnapshot.cs │ │ │ └── UsersDbContext.cs │ │ ├── Entities │ │ │ └── User.cs │ │ ├── Exceptions │ │ │ ├── InvalidAddressException.cs │ │ │ ├── InvalidEmailException.cs │ │ │ ├── InvalidFullNameException.cs │ │ │ ├── UserAlreadyExistsException.cs │ │ │ ├── UserAlreadyVerifiedException.cs │ │ │ └── UserNotFoundException.cs │ │ ├── Extensions.cs │ │ ├── NPay.Modules.Users.Core.csproj │ │ └── Services │ │ │ ├── IUsersService.cs │ │ │ ├── UsersModuleApi.cs │ │ │ └── UsersService.cs │ ├── NPay.Modules.Users.Shared │ │ ├── DTO │ │ │ ├── UserDetailsDto.cs │ │ │ └── UserDto.cs │ │ ├── Events │ │ │ ├── UserCreated.cs │ │ │ └── UserVerified.cs │ │ ├── IUsersModuleApi.cs │ │ └── NPay.Modules.Users.Shared.csproj │ └── Users.rest └── Wallets │ ├── NPay.Modules.Wallets.Api │ ├── Controllers │ │ ├── FundsController.cs │ │ ├── OwnersController.cs │ │ └── WalletsController.cs │ ├── Extensions.cs │ └── NPay.Modules.Wallets.Api.csproj │ ├── NPay.Modules.Wallets.Application │ ├── Extensions.cs │ ├── NPay.Modules.Wallets.Application.csproj │ ├── Owners │ │ ├── Commands │ │ │ ├── AddOwner.cs │ │ │ └── Handlers │ │ │ │ └── AddOwnerHandler.cs │ │ ├── Exceptions │ │ │ ├── UserAlreadyExistsException.cs │ │ │ └── UserNotFoundException.cs │ │ └── ExternalEvents │ │ │ └── Handlers │ │ │ ├── UserCreatedHandler.cs │ │ │ └── UserVerifiedHandler.cs │ ├── Services │ │ └── WalletsModuleApi.cs │ └── Wallets │ │ ├── Commands │ │ ├── AddFunds.cs │ │ ├── AddWallet.cs │ │ ├── Handlers │ │ │ ├── AddFundsHandler.cs │ │ │ ├── AddWalletHandler.cs │ │ │ └── TransferFundsHandler.cs │ │ └── TransferFunds.cs │ │ ├── ExternalEvents │ │ └── Handlers │ │ │ └── UserVerifiedHandler.cs │ │ ├── Queries │ │ ├── GetWallet.cs │ │ └── Handlers │ │ │ ├── Extensions.cs │ │ │ └── GetWalletHandler.cs │ │ └── Storage │ │ ├── Extensions.cs │ │ └── IWalletStorage.cs │ ├── NPay.Modules.Wallets.Core │ ├── Extensions.cs │ ├── NPay.Modules.Wallets.Core.csproj │ ├── Owners │ │ ├── Aggregates │ │ │ └── Owner.cs │ │ ├── Exceptions │ │ │ ├── InvalidFullNameException.cs │ │ │ ├── InvalidNationalityException.cs │ │ │ ├── OwnerNotFoundException.cs │ │ │ └── UnsupportedNationalityException.cs │ │ ├── Repositories │ │ │ └── IOwnerRepository.cs │ │ └── ValueObjects │ │ │ ├── FullName.cs │ │ │ └── Nationality.cs │ ├── SharedKernel │ │ ├── AggregateRoot.cs │ │ └── OwnerId.cs │ └── Wallets │ │ ├── Aggregates │ │ └── Wallet.cs │ │ ├── Entities │ │ └── Transfer.cs │ │ ├── Exceptions │ │ ├── InsufficientWalletFundsException.cs │ │ ├── InvalidAmountException.cs │ │ ├── InvalidCurrencyException.cs │ │ ├── InvalidTransferAmountException.cs │ │ ├── UnsupportedCurrencyException.cs │ │ └── WalletNotFoundException.cs │ │ ├── Repositories │ │ └── IWalletRepository.cs │ │ ├── Services │ │ ├── CurrencyResolver.cs │ │ └── ICurrencyResolver.cs │ │ └── ValueObjects │ │ ├── Amount.cs │ │ ├── Currency.cs │ │ ├── TransferId.cs │ │ └── WalletId.cs │ ├── NPay.Modules.Wallets.Infrastructure │ ├── DAL │ │ ├── Configurations │ │ │ ├── OwnerConfiguration.cs │ │ │ ├── TransferConfiguration.cs │ │ │ └── WalletConfiguration.cs │ │ ├── Migrations │ │ │ ├── 20211020191405_Wallets_Init.Designer.cs │ │ │ ├── 20211020191405_Wallets_Init.cs │ │ │ └── WalletsDbContextModelSnapshot.cs │ │ ├── Repositories │ │ │ ├── OwnerRepository.cs │ │ │ └── WalletRepository.cs │ │ └── WalletsDbContext.cs │ ├── Extensions.cs │ ├── NPay.Modules.Wallets.Infrastructure.csproj │ └── Storage │ │ └── WalletStorage.cs │ ├── NPay.Modules.Wallets.Shared │ ├── DTO │ │ ├── TransferDto.cs │ │ └── WalletDto.cs │ ├── Events │ │ ├── FundsAdded.cs │ │ ├── FundsTransfered.cs │ │ ├── OwnerVerified.cs │ │ └── WalletAdded.cs │ ├── IWalletsModuleApi.cs │ └── NPay.Modules.Wallets.Shared.csproj │ └── Wallets.rest └── Shared └── NPay.Shared ├── Commands ├── CommandDispatcher.cs ├── Extensions.cs ├── ICommand.cs ├── ICommandDispatcher.cs └── ICommandHandler.cs ├── Database ├── DbContextAppInitializer.cs ├── Extensions.cs └── PostgresOptions.cs ├── Dispatchers ├── IDispatcher.cs └── InMemoryDispatcher.cs ├── Events ├── EventDispatcher.cs ├── Extensions.cs ├── IEvent.cs ├── IEventDispatcher.cs └── IEventHandler.cs ├── Exceptions ├── ErrorHandlerMiddleware.cs ├── ExceptionResponse.cs ├── ExceptionToResponseMapper.cs ├── Extensions.cs ├── IExceptionToResponseMapper.cs └── NPayException.cs ├── Extensions.cs ├── Messaging ├── AsyncEventDispatcher.cs ├── EventChannel.cs ├── EventDispatcherJob.cs ├── Extensions.cs ├── IAsyncEventDispatcher.cs ├── IEventChannel.cs ├── IMessageBroker.cs └── InMemoryMessageBroker.cs ├── NPay.Shared.csproj ├── Queries ├── Extensions.cs ├── IQuery.cs ├── IQueryDispatcher.cs ├── IQueryHandler.cs └── QueryDispatcher.cs └── Time ├── IClock.cs └── UtcClock.cs /.dockerignore: -------------------------------------------------------------------------------- 1 | **/[Oo]bj 2 | **/[Bb]in 3 | **/[Ll]ogs 4 | TestResults/ 5 | .nuget/ 6 | _ReSharper.*/ 7 | packages/ 8 | artifacts/ 9 | PublishProfiles/ 10 | *.user 11 | *.suo 12 | *.cache 13 | *.docstates 14 | _ReSharper.* 15 | nuget.exe 16 | *net45.csproj 17 | *k10.csproj 18 | *.psess 19 | *.vsp 20 | *.pidb 21 | *.userprefs 22 | *DS_Store 23 | *.ncrunchsolution 24 | *.*sdf 25 | *.ipch 26 | .vs/ 27 | project.lock.json 28 | bower_components/ 29 | node_modules/ 30 | tests/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | # **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.publishsettings 221 | orleans.codegen.cs 222 | 223 | # Including strong name files can present a security risk 224 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 225 | #*.snk 226 | 227 | # Since there are multiple workflows, uncomment next line to ignore bower_components 228 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 229 | #bower_components/ 230 | 231 | # RIA/Silverlight projects 232 | Generated_Code/ 233 | 234 | # Backup & report files from converting an old project file 235 | # to a newer Visual Studio version. Backup files are not needed, 236 | # because we have git ;-) 237 | _UpgradeReport_Files/ 238 | Backup*/ 239 | UpgradeLog*.XML 240 | UpgradeLog*.htm 241 | ServiceFabricBackup/ 242 | *.rptproj.bak 243 | 244 | # SQL Server files 245 | *.mdf 246 | *.ldf 247 | *.ndf 248 | 249 | # Business Intelligence projects 250 | *.rdl.data 251 | *.bim.layout 252 | *.bim_*.settings 253 | *.rptproj.rsuser 254 | 255 | # Microsoft Fakes 256 | FakesAssemblies/ 257 | 258 | # GhostDoc plugin setting file 259 | *.GhostDoc.xml 260 | 261 | # Node.js Tools for Visual Studio 262 | .ntvs_analysis.dat 263 | node_modules/ 264 | 265 | # Visual Studio 6 build log 266 | *.plg 267 | 268 | # Visual Studio 6 workspace options file 269 | *.opt 270 | 271 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 272 | *.vbw 273 | 274 | # Visual Studio LightSwitch build output 275 | **/*.HTMLClient/GeneratedArtifacts 276 | **/*.DesktopClient/GeneratedArtifacts 277 | **/*.DesktopClient/ModelManifest.xml 278 | **/*.Server/GeneratedArtifacts 279 | **/*.Server/ModelManifest.xml 280 | _Pvt_Extensions 281 | 282 | # Paket dependency manager 283 | .paket/paket.exe 284 | paket-files/ 285 | 286 | # FAKE - F# Make 287 | .fake/ 288 | 289 | # JetBrains Rider 290 | .idea/ 291 | *.sln.iml 292 | 293 | # CodeRush 294 | .cr/ 295 | 296 | # Python Tools for Visual Studio (PTVS) 297 | __pycache__/ 298 | *.pyc 299 | 300 | # Cake - Uncomment if you are using it 301 | # tools/** 302 | # !tools/packages.config 303 | 304 | # Tabs Studio 305 | *.tss 306 | 307 | # Telerik's JustMock configuration file 308 | *.jmconfig 309 | 310 | # BizTalk build output 311 | *.btp.cs 312 | *.btm.cs 313 | *.odx.cs 314 | *.xsd.cs 315 | 316 | # OpenCover UI analysis results 317 | OpenCover/ 318 | 319 | # Azure Stream Analytics local run output 320 | ASALocalRun/ 321 | 322 | # MSBuild Binary and Structured Log 323 | *.binlog 324 | 325 | # NVidia Nsight GPU debugger configuration file 326 | *.nvuser 327 | 328 | # MFractors (Xamarin productivity tool) working folder 329 | .mfractor/ 330 | 331 | logs/ 332 | 333 | .vscode/ 334 | 335 | .DS_Store -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build 2 | WORKDIR /app 3 | COPY . . 4 | RUN dotnet publish src/Bootstrapper/NPay.Bootstrapper -c release -o out 5 | 6 | FROM mcr.microsoft.com/dotnet/aspnet:5.0 7 | WORKDIR /app 8 | COPY --from=build /app/out . 9 | ENV ASPNETCORE_URLS http://*:80 10 | ENV ASPNETCORE_ENVIRONMENT docker 11 | ENTRYPOINT dotnet NPay.Bootstrapper.dll -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 DevMentors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NPay.rest: -------------------------------------------------------------------------------- 1 | @url = http://localhost:5000 2 | 3 | ### 4 | GET {{url}} -------------------------------------------------------------------------------- /NPay.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Bootstrapper", "Bootstrapper", "{48607D4E-25F8-467B-BF0B-C0638D8BDFDC}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{6D99FF9F-068C-43B2-8849-C49929B87109}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{2EBB0D23-40E6-4664-8C20-D6E37718D15E}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Users", "Users", "{3D538A52-DE28-4079-AEE1-F8A992753E38}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Notifications", "Notifications", "{4F23B6D4-C6A3-44E2-829C-E6EC01D130CF}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Wallets", "Wallets", "{6811A758-296C-49FF-A2DD-7A5EB3799E0D}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPay.Bootstrapper", "src\Bootstrapper\NPay.Bootstrapper\NPay.Bootstrapper.csproj", "{28B29BD4-2CD0-461D-ADE6-17284AB6936F}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPay.Modules.Notifications.Api", "src\Modules\Notifications\NPay.Modules.Notifications.Api\NPay.Modules.Notifications.Api.csproj", "{B2716F1E-2211-4673-891D-67E3F7D7D1F7}" 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPay.Modules.Users.Api", "src\Modules\Users\NPay.Modules.Users.Api\NPay.Modules.Users.Api.csproj", "{44FBAC9C-7F39-496F-9BD0-B88A18D3B0FF}" 23 | EndProject 24 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPay.Modules.Users.Core", "src\Modules\Users\NPay.Modules.Users.Core\NPay.Modules.Users.Core.csproj", "{A6CAE675-D96D-4C49-9360-3588DAE3DE8B}" 25 | EndProject 26 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPay.Modules.Users.Shared", "src\Modules\Users\NPay.Modules.Users.Shared\NPay.Modules.Users.Shared.csproj", "{61EDB434-2A83-4C5E-A675-80B91B251E01}" 27 | EndProject 28 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPay.Modules.Wallets.Api", "src\Modules\Wallets\NPay.Modules.Wallets.Api\NPay.Modules.Wallets.Api.csproj", "{ACB82BC8-761E-42CF-9F56-E0CB9B634AE5}" 29 | EndProject 30 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPay.Modules.Wallets.Application", "src\Modules\Wallets\NPay.Modules.Wallets.Application\NPay.Modules.Wallets.Application.csproj", "{D3E56B79-0400-40EA-B716-30549C078FCB}" 31 | EndProject 32 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPay.Modules.Wallets.Core", "src\Modules\Wallets\NPay.Modules.Wallets.Core\NPay.Modules.Wallets.Core.csproj", "{92E2A846-B990-4795-B8EA-EAC31E7FE205}" 33 | EndProject 34 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPay.Modules.Wallets.Infrastructure", "src\Modules\Wallets\NPay.Modules.Wallets.Infrastructure\NPay.Modules.Wallets.Infrastructure.csproj", "{8AD07D64-218C-48F8-9016-7EA8848F6C9A}" 35 | EndProject 36 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPay.Modules.Wallets.Shared", "src\Modules\Wallets\NPay.Modules.Wallets.Shared\NPay.Modules.Wallets.Shared.csproj", "{1F18212C-20D6-4AFD-A64B-5917BB37BCE9}" 37 | EndProject 38 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPay.Shared", "src\Shared\NPay.Shared\NPay.Shared.csproj", "{B3DD62E9-B9E2-4088-8700-415B011590A4}" 39 | EndProject 40 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPay.Modules.Notifications.Shared", "src\Modules\Notifications\NPay.Modules.Notifications.Shared\NPay.Modules.Notifications.Shared.csproj", "{EDFFD275-60CD-4AC3-B8DB-0CBDC833E0CF}" 41 | EndProject 42 | Global 43 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 44 | Debug|Any CPU = Debug|Any CPU 45 | Debug|x64 = Debug|x64 46 | Debug|x86 = Debug|x86 47 | Release|Any CPU = Release|Any CPU 48 | Release|x64 = Release|x64 49 | Release|x86 = Release|x86 50 | EndGlobalSection 51 | GlobalSection(SolutionProperties) = preSolution 52 | HideSolutionNode = FALSE 53 | EndGlobalSection 54 | GlobalSection(NestedProjects) = preSolution 55 | {3D538A52-DE28-4079-AEE1-F8A992753E38} = {6D99FF9F-068C-43B2-8849-C49929B87109} 56 | {4F23B6D4-C6A3-44E2-829C-E6EC01D130CF} = {6D99FF9F-068C-43B2-8849-C49929B87109} 57 | {6811A758-296C-49FF-A2DD-7A5EB3799E0D} = {6D99FF9F-068C-43B2-8849-C49929B87109} 58 | {28B29BD4-2CD0-461D-ADE6-17284AB6936F} = {48607D4E-25F8-467B-BF0B-C0638D8BDFDC} 59 | {B2716F1E-2211-4673-891D-67E3F7D7D1F7} = {4F23B6D4-C6A3-44E2-829C-E6EC01D130CF} 60 | {44FBAC9C-7F39-496F-9BD0-B88A18D3B0FF} = {3D538A52-DE28-4079-AEE1-F8A992753E38} 61 | {A6CAE675-D96D-4C49-9360-3588DAE3DE8B} = {3D538A52-DE28-4079-AEE1-F8A992753E38} 62 | {61EDB434-2A83-4C5E-A675-80B91B251E01} = {3D538A52-DE28-4079-AEE1-F8A992753E38} 63 | {ACB82BC8-761E-42CF-9F56-E0CB9B634AE5} = {6811A758-296C-49FF-A2DD-7A5EB3799E0D} 64 | {D3E56B79-0400-40EA-B716-30549C078FCB} = {6811A758-296C-49FF-A2DD-7A5EB3799E0D} 65 | {92E2A846-B990-4795-B8EA-EAC31E7FE205} = {6811A758-296C-49FF-A2DD-7A5EB3799E0D} 66 | {8AD07D64-218C-48F8-9016-7EA8848F6C9A} = {6811A758-296C-49FF-A2DD-7A5EB3799E0D} 67 | {1F18212C-20D6-4AFD-A64B-5917BB37BCE9} = {6811A758-296C-49FF-A2DD-7A5EB3799E0D} 68 | {B3DD62E9-B9E2-4088-8700-415B011590A4} = {2EBB0D23-40E6-4664-8C20-D6E37718D15E} 69 | {EDFFD275-60CD-4AC3-B8DB-0CBDC833E0CF} = {4F23B6D4-C6A3-44E2-829C-E6EC01D130CF} 70 | EndGlobalSection 71 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 72 | {28B29BD4-2CD0-461D-ADE6-17284AB6936F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 73 | {28B29BD4-2CD0-461D-ADE6-17284AB6936F}.Debug|Any CPU.Build.0 = Debug|Any CPU 74 | {28B29BD4-2CD0-461D-ADE6-17284AB6936F}.Debug|x64.ActiveCfg = Debug|Any CPU 75 | {28B29BD4-2CD0-461D-ADE6-17284AB6936F}.Debug|x64.Build.0 = Debug|Any CPU 76 | {28B29BD4-2CD0-461D-ADE6-17284AB6936F}.Debug|x86.ActiveCfg = Debug|Any CPU 77 | {28B29BD4-2CD0-461D-ADE6-17284AB6936F}.Debug|x86.Build.0 = Debug|Any CPU 78 | {28B29BD4-2CD0-461D-ADE6-17284AB6936F}.Release|Any CPU.ActiveCfg = Release|Any CPU 79 | {28B29BD4-2CD0-461D-ADE6-17284AB6936F}.Release|Any CPU.Build.0 = Release|Any CPU 80 | {28B29BD4-2CD0-461D-ADE6-17284AB6936F}.Release|x64.ActiveCfg = Release|Any CPU 81 | {28B29BD4-2CD0-461D-ADE6-17284AB6936F}.Release|x64.Build.0 = Release|Any CPU 82 | {28B29BD4-2CD0-461D-ADE6-17284AB6936F}.Release|x86.ActiveCfg = Release|Any CPU 83 | {28B29BD4-2CD0-461D-ADE6-17284AB6936F}.Release|x86.Build.0 = Release|Any CPU 84 | {B2716F1E-2211-4673-891D-67E3F7D7D1F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 85 | {B2716F1E-2211-4673-891D-67E3F7D7D1F7}.Debug|Any CPU.Build.0 = Debug|Any CPU 86 | {B2716F1E-2211-4673-891D-67E3F7D7D1F7}.Debug|x64.ActiveCfg = Debug|Any CPU 87 | {B2716F1E-2211-4673-891D-67E3F7D7D1F7}.Debug|x64.Build.0 = Debug|Any CPU 88 | {B2716F1E-2211-4673-891D-67E3F7D7D1F7}.Debug|x86.ActiveCfg = Debug|Any CPU 89 | {B2716F1E-2211-4673-891D-67E3F7D7D1F7}.Debug|x86.Build.0 = Debug|Any CPU 90 | {B2716F1E-2211-4673-891D-67E3F7D7D1F7}.Release|Any CPU.ActiveCfg = Release|Any CPU 91 | {B2716F1E-2211-4673-891D-67E3F7D7D1F7}.Release|Any CPU.Build.0 = Release|Any CPU 92 | {B2716F1E-2211-4673-891D-67E3F7D7D1F7}.Release|x64.ActiveCfg = Release|Any CPU 93 | {B2716F1E-2211-4673-891D-67E3F7D7D1F7}.Release|x64.Build.0 = Release|Any CPU 94 | {B2716F1E-2211-4673-891D-67E3F7D7D1F7}.Release|x86.ActiveCfg = Release|Any CPU 95 | {B2716F1E-2211-4673-891D-67E3F7D7D1F7}.Release|x86.Build.0 = Release|Any CPU 96 | {44FBAC9C-7F39-496F-9BD0-B88A18D3B0FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 97 | {44FBAC9C-7F39-496F-9BD0-B88A18D3B0FF}.Debug|Any CPU.Build.0 = Debug|Any CPU 98 | {44FBAC9C-7F39-496F-9BD0-B88A18D3B0FF}.Debug|x64.ActiveCfg = Debug|Any CPU 99 | {44FBAC9C-7F39-496F-9BD0-B88A18D3B0FF}.Debug|x64.Build.0 = Debug|Any CPU 100 | {44FBAC9C-7F39-496F-9BD0-B88A18D3B0FF}.Debug|x86.ActiveCfg = Debug|Any CPU 101 | {44FBAC9C-7F39-496F-9BD0-B88A18D3B0FF}.Debug|x86.Build.0 = Debug|Any CPU 102 | {44FBAC9C-7F39-496F-9BD0-B88A18D3B0FF}.Release|Any CPU.ActiveCfg = Release|Any CPU 103 | {44FBAC9C-7F39-496F-9BD0-B88A18D3B0FF}.Release|Any CPU.Build.0 = Release|Any CPU 104 | {44FBAC9C-7F39-496F-9BD0-B88A18D3B0FF}.Release|x64.ActiveCfg = Release|Any CPU 105 | {44FBAC9C-7F39-496F-9BD0-B88A18D3B0FF}.Release|x64.Build.0 = Release|Any CPU 106 | {44FBAC9C-7F39-496F-9BD0-B88A18D3B0FF}.Release|x86.ActiveCfg = Release|Any CPU 107 | {44FBAC9C-7F39-496F-9BD0-B88A18D3B0FF}.Release|x86.Build.0 = Release|Any CPU 108 | {A6CAE675-D96D-4C49-9360-3588DAE3DE8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 109 | {A6CAE675-D96D-4C49-9360-3588DAE3DE8B}.Debug|Any CPU.Build.0 = Debug|Any CPU 110 | {A6CAE675-D96D-4C49-9360-3588DAE3DE8B}.Debug|x64.ActiveCfg = Debug|Any CPU 111 | {A6CAE675-D96D-4C49-9360-3588DAE3DE8B}.Debug|x64.Build.0 = Debug|Any CPU 112 | {A6CAE675-D96D-4C49-9360-3588DAE3DE8B}.Debug|x86.ActiveCfg = Debug|Any CPU 113 | {A6CAE675-D96D-4C49-9360-3588DAE3DE8B}.Debug|x86.Build.0 = Debug|Any CPU 114 | {A6CAE675-D96D-4C49-9360-3588DAE3DE8B}.Release|Any CPU.ActiveCfg = Release|Any CPU 115 | {A6CAE675-D96D-4C49-9360-3588DAE3DE8B}.Release|Any CPU.Build.0 = Release|Any CPU 116 | {A6CAE675-D96D-4C49-9360-3588DAE3DE8B}.Release|x64.ActiveCfg = Release|Any CPU 117 | {A6CAE675-D96D-4C49-9360-3588DAE3DE8B}.Release|x64.Build.0 = Release|Any CPU 118 | {A6CAE675-D96D-4C49-9360-3588DAE3DE8B}.Release|x86.ActiveCfg = Release|Any CPU 119 | {A6CAE675-D96D-4C49-9360-3588DAE3DE8B}.Release|x86.Build.0 = Release|Any CPU 120 | {61EDB434-2A83-4C5E-A675-80B91B251E01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 121 | {61EDB434-2A83-4C5E-A675-80B91B251E01}.Debug|Any CPU.Build.0 = Debug|Any CPU 122 | {61EDB434-2A83-4C5E-A675-80B91B251E01}.Debug|x64.ActiveCfg = Debug|Any CPU 123 | {61EDB434-2A83-4C5E-A675-80B91B251E01}.Debug|x64.Build.0 = Debug|Any CPU 124 | {61EDB434-2A83-4C5E-A675-80B91B251E01}.Debug|x86.ActiveCfg = Debug|Any CPU 125 | {61EDB434-2A83-4C5E-A675-80B91B251E01}.Debug|x86.Build.0 = Debug|Any CPU 126 | {61EDB434-2A83-4C5E-A675-80B91B251E01}.Release|Any CPU.ActiveCfg = Release|Any CPU 127 | {61EDB434-2A83-4C5E-A675-80B91B251E01}.Release|Any CPU.Build.0 = Release|Any CPU 128 | {61EDB434-2A83-4C5E-A675-80B91B251E01}.Release|x64.ActiveCfg = Release|Any CPU 129 | {61EDB434-2A83-4C5E-A675-80B91B251E01}.Release|x64.Build.0 = Release|Any CPU 130 | {61EDB434-2A83-4C5E-A675-80B91B251E01}.Release|x86.ActiveCfg = Release|Any CPU 131 | {61EDB434-2A83-4C5E-A675-80B91B251E01}.Release|x86.Build.0 = Release|Any CPU 132 | {ACB82BC8-761E-42CF-9F56-E0CB9B634AE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 133 | {ACB82BC8-761E-42CF-9F56-E0CB9B634AE5}.Debug|Any CPU.Build.0 = Debug|Any CPU 134 | {ACB82BC8-761E-42CF-9F56-E0CB9B634AE5}.Debug|x64.ActiveCfg = Debug|Any CPU 135 | {ACB82BC8-761E-42CF-9F56-E0CB9B634AE5}.Debug|x64.Build.0 = Debug|Any CPU 136 | {ACB82BC8-761E-42CF-9F56-E0CB9B634AE5}.Debug|x86.ActiveCfg = Debug|Any CPU 137 | {ACB82BC8-761E-42CF-9F56-E0CB9B634AE5}.Debug|x86.Build.0 = Debug|Any CPU 138 | {ACB82BC8-761E-42CF-9F56-E0CB9B634AE5}.Release|Any CPU.ActiveCfg = Release|Any CPU 139 | {ACB82BC8-761E-42CF-9F56-E0CB9B634AE5}.Release|Any CPU.Build.0 = Release|Any CPU 140 | {ACB82BC8-761E-42CF-9F56-E0CB9B634AE5}.Release|x64.ActiveCfg = Release|Any CPU 141 | {ACB82BC8-761E-42CF-9F56-E0CB9B634AE5}.Release|x64.Build.0 = Release|Any CPU 142 | {ACB82BC8-761E-42CF-9F56-E0CB9B634AE5}.Release|x86.ActiveCfg = Release|Any CPU 143 | {ACB82BC8-761E-42CF-9F56-E0CB9B634AE5}.Release|x86.Build.0 = Release|Any CPU 144 | {D3E56B79-0400-40EA-B716-30549C078FCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 145 | {D3E56B79-0400-40EA-B716-30549C078FCB}.Debug|Any CPU.Build.0 = Debug|Any CPU 146 | {D3E56B79-0400-40EA-B716-30549C078FCB}.Debug|x64.ActiveCfg = Debug|Any CPU 147 | {D3E56B79-0400-40EA-B716-30549C078FCB}.Debug|x64.Build.0 = Debug|Any CPU 148 | {D3E56B79-0400-40EA-B716-30549C078FCB}.Debug|x86.ActiveCfg = Debug|Any CPU 149 | {D3E56B79-0400-40EA-B716-30549C078FCB}.Debug|x86.Build.0 = Debug|Any CPU 150 | {D3E56B79-0400-40EA-B716-30549C078FCB}.Release|Any CPU.ActiveCfg = Release|Any CPU 151 | {D3E56B79-0400-40EA-B716-30549C078FCB}.Release|Any CPU.Build.0 = Release|Any CPU 152 | {D3E56B79-0400-40EA-B716-30549C078FCB}.Release|x64.ActiveCfg = Release|Any CPU 153 | {D3E56B79-0400-40EA-B716-30549C078FCB}.Release|x64.Build.0 = Release|Any CPU 154 | {D3E56B79-0400-40EA-B716-30549C078FCB}.Release|x86.ActiveCfg = Release|Any CPU 155 | {D3E56B79-0400-40EA-B716-30549C078FCB}.Release|x86.Build.0 = Release|Any CPU 156 | {92E2A846-B990-4795-B8EA-EAC31E7FE205}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 157 | {92E2A846-B990-4795-B8EA-EAC31E7FE205}.Debug|Any CPU.Build.0 = Debug|Any CPU 158 | {92E2A846-B990-4795-B8EA-EAC31E7FE205}.Debug|x64.ActiveCfg = Debug|Any CPU 159 | {92E2A846-B990-4795-B8EA-EAC31E7FE205}.Debug|x64.Build.0 = Debug|Any CPU 160 | {92E2A846-B990-4795-B8EA-EAC31E7FE205}.Debug|x86.ActiveCfg = Debug|Any CPU 161 | {92E2A846-B990-4795-B8EA-EAC31E7FE205}.Debug|x86.Build.0 = Debug|Any CPU 162 | {92E2A846-B990-4795-B8EA-EAC31E7FE205}.Release|Any CPU.ActiveCfg = Release|Any CPU 163 | {92E2A846-B990-4795-B8EA-EAC31E7FE205}.Release|Any CPU.Build.0 = Release|Any CPU 164 | {92E2A846-B990-4795-B8EA-EAC31E7FE205}.Release|x64.ActiveCfg = Release|Any CPU 165 | {92E2A846-B990-4795-B8EA-EAC31E7FE205}.Release|x64.Build.0 = Release|Any CPU 166 | {92E2A846-B990-4795-B8EA-EAC31E7FE205}.Release|x86.ActiveCfg = Release|Any CPU 167 | {92E2A846-B990-4795-B8EA-EAC31E7FE205}.Release|x86.Build.0 = Release|Any CPU 168 | {8AD07D64-218C-48F8-9016-7EA8848F6C9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 169 | {8AD07D64-218C-48F8-9016-7EA8848F6C9A}.Debug|Any CPU.Build.0 = Debug|Any CPU 170 | {8AD07D64-218C-48F8-9016-7EA8848F6C9A}.Debug|x64.ActiveCfg = Debug|Any CPU 171 | {8AD07D64-218C-48F8-9016-7EA8848F6C9A}.Debug|x64.Build.0 = Debug|Any CPU 172 | {8AD07D64-218C-48F8-9016-7EA8848F6C9A}.Debug|x86.ActiveCfg = Debug|Any CPU 173 | {8AD07D64-218C-48F8-9016-7EA8848F6C9A}.Debug|x86.Build.0 = Debug|Any CPU 174 | {8AD07D64-218C-48F8-9016-7EA8848F6C9A}.Release|Any CPU.ActiveCfg = Release|Any CPU 175 | {8AD07D64-218C-48F8-9016-7EA8848F6C9A}.Release|Any CPU.Build.0 = Release|Any CPU 176 | {8AD07D64-218C-48F8-9016-7EA8848F6C9A}.Release|x64.ActiveCfg = Release|Any CPU 177 | {8AD07D64-218C-48F8-9016-7EA8848F6C9A}.Release|x64.Build.0 = Release|Any CPU 178 | {8AD07D64-218C-48F8-9016-7EA8848F6C9A}.Release|x86.ActiveCfg = Release|Any CPU 179 | {8AD07D64-218C-48F8-9016-7EA8848F6C9A}.Release|x86.Build.0 = Release|Any CPU 180 | {1F18212C-20D6-4AFD-A64B-5917BB37BCE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 181 | {1F18212C-20D6-4AFD-A64B-5917BB37BCE9}.Debug|Any CPU.Build.0 = Debug|Any CPU 182 | {1F18212C-20D6-4AFD-A64B-5917BB37BCE9}.Debug|x64.ActiveCfg = Debug|Any CPU 183 | {1F18212C-20D6-4AFD-A64B-5917BB37BCE9}.Debug|x64.Build.0 = Debug|Any CPU 184 | {1F18212C-20D6-4AFD-A64B-5917BB37BCE9}.Debug|x86.ActiveCfg = Debug|Any CPU 185 | {1F18212C-20D6-4AFD-A64B-5917BB37BCE9}.Debug|x86.Build.0 = Debug|Any CPU 186 | {1F18212C-20D6-4AFD-A64B-5917BB37BCE9}.Release|Any CPU.ActiveCfg = Release|Any CPU 187 | {1F18212C-20D6-4AFD-A64B-5917BB37BCE9}.Release|Any CPU.Build.0 = Release|Any CPU 188 | {1F18212C-20D6-4AFD-A64B-5917BB37BCE9}.Release|x64.ActiveCfg = Release|Any CPU 189 | {1F18212C-20D6-4AFD-A64B-5917BB37BCE9}.Release|x64.Build.0 = Release|Any CPU 190 | {1F18212C-20D6-4AFD-A64B-5917BB37BCE9}.Release|x86.ActiveCfg = Release|Any CPU 191 | {1F18212C-20D6-4AFD-A64B-5917BB37BCE9}.Release|x86.Build.0 = Release|Any CPU 192 | {B3DD62E9-B9E2-4088-8700-415B011590A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 193 | {B3DD62E9-B9E2-4088-8700-415B011590A4}.Debug|Any CPU.Build.0 = Debug|Any CPU 194 | {B3DD62E9-B9E2-4088-8700-415B011590A4}.Debug|x64.ActiveCfg = Debug|Any CPU 195 | {B3DD62E9-B9E2-4088-8700-415B011590A4}.Debug|x64.Build.0 = Debug|Any CPU 196 | {B3DD62E9-B9E2-4088-8700-415B011590A4}.Debug|x86.ActiveCfg = Debug|Any CPU 197 | {B3DD62E9-B9E2-4088-8700-415B011590A4}.Debug|x86.Build.0 = Debug|Any CPU 198 | {B3DD62E9-B9E2-4088-8700-415B011590A4}.Release|Any CPU.ActiveCfg = Release|Any CPU 199 | {B3DD62E9-B9E2-4088-8700-415B011590A4}.Release|Any CPU.Build.0 = Release|Any CPU 200 | {B3DD62E9-B9E2-4088-8700-415B011590A4}.Release|x64.ActiveCfg = Release|Any CPU 201 | {B3DD62E9-B9E2-4088-8700-415B011590A4}.Release|x64.Build.0 = Release|Any CPU 202 | {B3DD62E9-B9E2-4088-8700-415B011590A4}.Release|x86.ActiveCfg = Release|Any CPU 203 | {B3DD62E9-B9E2-4088-8700-415B011590A4}.Release|x86.Build.0 = Release|Any CPU 204 | {EDFFD275-60CD-4AC3-B8DB-0CBDC833E0CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 205 | {EDFFD275-60CD-4AC3-B8DB-0CBDC833E0CF}.Debug|Any CPU.Build.0 = Debug|Any CPU 206 | {EDFFD275-60CD-4AC3-B8DB-0CBDC833E0CF}.Debug|x64.ActiveCfg = Debug|Any CPU 207 | {EDFFD275-60CD-4AC3-B8DB-0CBDC833E0CF}.Debug|x64.Build.0 = Debug|Any CPU 208 | {EDFFD275-60CD-4AC3-B8DB-0CBDC833E0CF}.Debug|x86.ActiveCfg = Debug|Any CPU 209 | {EDFFD275-60CD-4AC3-B8DB-0CBDC833E0CF}.Debug|x86.Build.0 = Debug|Any CPU 210 | {EDFFD275-60CD-4AC3-B8DB-0CBDC833E0CF}.Release|Any CPU.ActiveCfg = Release|Any CPU 211 | {EDFFD275-60CD-4AC3-B8DB-0CBDC833E0CF}.Release|Any CPU.Build.0 = Release|Any CPU 212 | {EDFFD275-60CD-4AC3-B8DB-0CBDC833E0CF}.Release|x64.ActiveCfg = Release|Any CPU 213 | {EDFFD275-60CD-4AC3-B8DB-0CBDC833E0CF}.Release|x64.Build.0 = Release|Any CPU 214 | {EDFFD275-60CD-4AC3-B8DB-0CBDC833E0CF}.Release|x86.ActiveCfg = Release|Any CPU 215 | {EDFFD275-60CD-4AC3-B8DB-0CBDC833E0CF}.Release|x86.Build.0 = Release|Any CPU 216 | EndGlobalSection 217 | EndGlobal 218 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NPay 2 | 3 | ## About 4 | 5 | NPay is a simple virtual payments app built as a modular monolith. 6 | 7 | **How to start the solution?** 8 | ---------------- 9 | 10 | Start the infrastructure using [Docker](https://docs.docker.com/get-docker/): 11 | 12 | ``` 13 | docker-compose up -d 14 | ``` 15 | 16 | Start API located under Bootstrapper project: 17 | 18 | ``` 19 | cd src/Bootstrapper/NPay.Bootstrapper 20 | dotnet run 21 | ``` -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | postgres: 5 | image: postgres 6 | shm_size: '4gb' 7 | container_name: postgres 8 | restart: unless-stopped 9 | environment: 10 | - POSTGRES_HOST_AUTH_METHOD=trust 11 | ports: 12 | - 5432:5432 13 | volumes: 14 | - postgres:/var/lib/postgresql/data 15 | 16 | volumes: 17 | postgres: 18 | driver: local -------------------------------------------------------------------------------- /src/Bootstrapper/NPay.Bootstrapper/NPay.Bootstrapper.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Bootstrapper/NPay.Bootstrapper/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Http; 3 | using NPay.Modules.Notifications.Api; 4 | using NPay.Modules.Users.Api; 5 | using NPay.Modules.Wallets.Api; 6 | using NPay.Shared; 7 | 8 | var builder = WebApplication 9 | .CreateBuilder(args); 10 | 11 | builder.Services 12 | .AddNotificationsModule() 13 | .AddUsersModule() 14 | .AddWalletsModule() 15 | .AddSharedFramework(builder.Configuration); 16 | 17 | var app = builder.Build(); 18 | 19 | app.UseSharedFramework(); 20 | app.UseNotificationsModule(); 21 | app.UseUsersModule(); 22 | app.UseWalletsModule(); 23 | app.UseEndpoints(endpoints => 24 | { 25 | endpoints.MapControllers(); 26 | endpoints.MapGet("/", ctx => ctx.Response.WriteAsync("NPay API")); 27 | }); 28 | 29 | app.Run(); -------------------------------------------------------------------------------- /src/Bootstrapper/NPay.Bootstrapper/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:45389", 7 | "sslPort": 44304 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": false, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "development" 16 | } 17 | }, 18 | "NPay.Bootstrapper": { 19 | "commandName": "Project", 20 | "dotnetRunMessages": "true", 21 | "launchBrowser": false, 22 | "applicationUrl": "http://localhost:5000", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Bootstrapper/NPay.Bootstrapper/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using NPay.Modules.Notifications.Api; 7 | using NPay.Modules.Users.Api; 8 | using NPay.Modules.Wallets.Api; 9 | using NPay.Shared; 10 | 11 | namespace NPay.Bootstrapper; 12 | 13 | public class Startup 14 | { 15 | private readonly IConfiguration _configuration; 16 | 17 | public Startup(IConfiguration configuration) 18 | { 19 | _configuration = configuration; 20 | } 21 | 22 | public void ConfigureServices(IServiceCollection services) 23 | { 24 | services.AddNotificationsModule(); 25 | services.AddUsersModule(); 26 | services.AddWalletsModule(); 27 | services.AddSharedFramework(_configuration); 28 | } 29 | 30 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 31 | { 32 | app.UseSharedFramework(); 33 | app.UseNotificationsModule(); 34 | app.UseUsersModule(); 35 | app.UseWalletsModule(); 36 | app.UseEndpoints(endpoints => 37 | { 38 | endpoints.MapControllers(); 39 | endpoints.MapGet("/", ctx => ctx.Response.WriteAsync("NPay API")); 40 | }); 41 | } 42 | } -------------------------------------------------------------------------------- /src/Bootstrapper/NPay.Bootstrapper/appsettings.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "logging": { 3 | "logLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Information", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Bootstrapper/NPay.Bootstrapper/appsettings.docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "postgres": { 3 | "connectionString": "Host=postgres;Database=npay;Username=postgres;Password=" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/Bootstrapper/NPay.Bootstrapper/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "postgres": { 3 | "connectionString": "Host=localhost;Database=npay;Username=postgres;Password=" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/Modules/Notifications/NPay.Modules.Notifications.Api/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using NPay.Modules.Notifications.Api.Services; 4 | using NPay.Modules.Notifications.Shared; 5 | 6 | namespace NPay.Modules.Notifications.Api; 7 | 8 | public static class Extensions 9 | { 10 | public static IServiceCollection AddNotificationsModule(this IServiceCollection services) 11 | { 12 | services.AddTransient(); 13 | services.AddSingleton(); 14 | services.AddSingleton(); 15 | 16 | return services; 17 | } 18 | 19 | public static IApplicationBuilder UseNotificationsModule(this IApplicationBuilder app) 20 | { 21 | return app; 22 | } 23 | } -------------------------------------------------------------------------------- /src/Modules/Notifications/NPay.Modules.Notifications.Api/Handlers/Owners/OwnerVerifiedHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NPay.Modules.Notifications.Api.Services; 4 | using NPay.Modules.Wallets.Shared.Events; 5 | using NPay.Shared.Events; 6 | 7 | namespace NPay.Modules.Notifications.Api.Handlers.Owners; 8 | 9 | internal sealed class OwnerVerifiedHandler : IEventHandler 10 | { 11 | private readonly IEmailSender _emailSender; 12 | private readonly IEmailResolver _emailResolver; 13 | 14 | public OwnerVerifiedHandler(IEmailSender emailSender, IEmailResolver emailResolver) 15 | { 16 | _emailSender = emailSender; 17 | _emailResolver = emailResolver; 18 | } 19 | 20 | public Task HandleAsync(OwnerVerified @event, CancellationToken cancellationToken = default) 21 | => _emailSender.SendAsync(_emailResolver.GetForOwner(@event.OwnerId), "owner_verified"); 22 | } -------------------------------------------------------------------------------- /src/Modules/Notifications/NPay.Modules.Notifications.Api/Handlers/Users/UserCreatedHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NPay.Modules.Notifications.Api.Services; 4 | using NPay.Modules.Users.Shared.Events; 5 | using NPay.Shared.Events; 6 | 7 | namespace NPay.Modules.Notifications.Api.Handlers.Users; 8 | 9 | internal sealed class UserCreatedHandler : IEventHandler 10 | { 11 | private readonly IEmailSender _emailSender; 12 | 13 | public UserCreatedHandler(IEmailSender emailSender) 14 | { 15 | _emailSender = emailSender; 16 | } 17 | 18 | public Task HandleAsync(UserCreated @event, CancellationToken cancellationToken = default) 19 | => _emailSender.SendAsync(@event.Email, "account_created"); 20 | } -------------------------------------------------------------------------------- /src/Modules/Notifications/NPay.Modules.Notifications.Api/Handlers/Users/UserVerifiedHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NPay.Modules.Notifications.Api.Services; 4 | using NPay.Modules.Users.Shared.Events; 5 | using NPay.Shared.Events; 6 | 7 | namespace NPay.Modules.Notifications.Api.Handlers.Users; 8 | 9 | internal sealed class UserVerifiedHandler : IEventHandler 10 | { 11 | private readonly IEmailSender _emailSender; 12 | 13 | public UserVerifiedHandler(IEmailSender emailSender) 14 | { 15 | _emailSender = emailSender; 16 | } 17 | 18 | public Task HandleAsync(UserVerified @event, CancellationToken cancellationToken = default) 19 | => _emailSender.SendAsync(@event.Email, "account_verified"); 20 | } -------------------------------------------------------------------------------- /src/Modules/Notifications/NPay.Modules.Notifications.Api/Handlers/Wallets/FundsAddedHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NPay.Modules.Notifications.Api.Services; 4 | using NPay.Modules.Wallets.Shared.Events; 5 | using NPay.Shared.Events; 6 | 7 | namespace NPay.Modules.Notifications.Api.Handlers.Wallets; 8 | 9 | internal sealed class FundsAddedHandler : IEventHandler 10 | { 11 | private readonly IEmailSender _emailSender; 12 | private readonly IEmailResolver _emailResolver; 13 | 14 | public FundsAddedHandler(IEmailSender emailSender, IEmailResolver emailResolver) 15 | { 16 | _emailSender = emailSender; 17 | _emailResolver = emailResolver; 18 | } 19 | 20 | public Task HandleAsync(FundsAdded @event, CancellationToken cancellationToken = default) 21 | => _emailSender.SendAsync(_emailResolver.GetForOwner(@event.OwnerId), "funds_added"); 22 | } -------------------------------------------------------------------------------- /src/Modules/Notifications/NPay.Modules.Notifications.Api/Handlers/Wallets/FundsTransferredHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NPay.Modules.Notifications.Api.Services; 4 | using NPay.Modules.Wallets.Shared.Events; 5 | using NPay.Shared.Events; 6 | 7 | namespace NPay.Modules.Notifications.Api.Handlers.Wallets; 8 | 9 | internal sealed class FundsTransferredHandler : IEventHandler 10 | { 11 | private readonly IEmailSender _emailSender; 12 | private readonly IEmailResolver _emailResolver; 13 | 14 | public FundsTransferredHandler(IEmailSender emailSender, IEmailResolver emailResolver) 15 | { 16 | _emailSender = emailSender; 17 | _emailResolver = emailResolver; 18 | } 19 | 20 | public Task HandleAsync(FundsTransferred @event, CancellationToken cancellationToken = default) 21 | => Task.WhenAll(_emailSender.SendAsync(_emailResolver.GetForOwner(@event.FromWalletId), "funds_deducted"), 22 | _emailSender.SendAsync(_emailResolver.GetForOwner(@event.ToWalletId), "funds_added")); 23 | } -------------------------------------------------------------------------------- /src/Modules/Notifications/NPay.Modules.Notifications.Api/Handlers/Wallets/WalletAddedHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NPay.Modules.Notifications.Api.Services; 4 | using NPay.Modules.Wallets.Shared.Events; 5 | using NPay.Shared.Events; 6 | 7 | namespace NPay.Modules.Notifications.Api.Handlers.Wallets; 8 | 9 | internal sealed class WalletAddedHandler : IEventHandler 10 | { 11 | private readonly IEmailSender _emailSender; 12 | private readonly IEmailResolver _emailResolver; 13 | 14 | public WalletAddedHandler(IEmailSender emailSender, IEmailResolver emailResolver) 15 | { 16 | _emailSender = emailSender; 17 | _emailResolver = emailResolver; 18 | } 19 | 20 | public Task HandleAsync(WalletAdded @event, CancellationToken cancellationToken = default) 21 | => _emailSender.SendAsync(_emailResolver.GetForOwner(@event.OwnerId), "wallet_added"); 22 | } -------------------------------------------------------------------------------- /src/Modules/Notifications/NPay.Modules.Notifications.Api/NPay.Modules.Notifications.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Modules/Notifications/NPay.Modules.Notifications.Api/Services/EmailResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NPay.Modules.Notifications.Api.Services; 4 | 5 | internal sealed class EmailResolver : IEmailResolver 6 | { 7 | // TODO: Resolve the user/owner email address from other modules 8 | public string GetForOwner(Guid ownerId) => $"{ownerId:N}@npay.io"; 9 | } -------------------------------------------------------------------------------- /src/Modules/Notifications/NPay.Modules.Notifications.Api/Services/EmailSender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace NPay.Modules.Notifications.Api.Services; 5 | 6 | internal sealed class EmailSender : IEmailSender 7 | { 8 | private readonly ILogger _logger; 9 | 10 | public EmailSender(ILogger logger) 11 | { 12 | _logger = logger; 13 | } 14 | 15 | public Task SendAsync(string receiver, string template) 16 | { 17 | // TODO: Implement an email sender 18 | _logger.LogInformation($"Sending an email to: '{receiver}', template: '{template}'..."); 19 | return Task.CompletedTask; 20 | } 21 | } -------------------------------------------------------------------------------- /src/Modules/Notifications/NPay.Modules.Notifications.Api/Services/IEmailResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NPay.Modules.Notifications.Api.Services; 4 | 5 | internal interface IEmailResolver 6 | { 7 | string GetForOwner(Guid ownerId); 8 | } -------------------------------------------------------------------------------- /src/Modules/Notifications/NPay.Modules.Notifications.Api/Services/IEmailSender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace NPay.Modules.Notifications.Api.Services; 4 | 5 | internal interface IEmailSender 6 | { 7 | Task SendAsync(string receiver, string template); 8 | } -------------------------------------------------------------------------------- /src/Modules/Notifications/NPay.Modules.Notifications.Api/Services/NotificationsModuleApi.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using NPay.Modules.Notifications.Shared; 3 | 4 | namespace NPay.Modules.Notifications.Api.Services; 5 | 6 | internal sealed class NotificationsModuleApi : INotificationsModuleApi 7 | { 8 | private readonly IEmailSender _emailSender; 9 | 10 | public NotificationsModuleApi(IEmailSender emailSender) 11 | { 12 | _emailSender = emailSender; 13 | } 14 | 15 | public Task SendEmailAsync(string receiver, string template) 16 | => _emailSender.SendAsync(receiver, template); 17 | } -------------------------------------------------------------------------------- /src/Modules/Notifications/NPay.Modules.Notifications.Shared/INotificationsModuleApi.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace NPay.Modules.Notifications.Shared; 4 | 5 | public interface INotificationsModuleApi 6 | { 7 | Task SendEmailAsync(string receiver, string template); 8 | } -------------------------------------------------------------------------------- /src/Modules/Notifications/NPay.Modules.Notifications.Shared/NPay.Modules.Notifications.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Api/Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using NPay.Modules.Users.Core.Services; 7 | using NPay.Modules.Users.Shared.DTO; 8 | using Swashbuckle.AspNetCore.Annotations; 9 | 10 | namespace NPay.Modules.Users.Api.Controllers; 11 | 12 | [ApiController] 13 | [Route("[controller]")] 14 | public class UsersController : ControllerBase 15 | { 16 | private readonly IUsersService _usersService; 17 | 18 | public UsersController(IUsersService usersService) 19 | { 20 | _usersService = usersService; 21 | } 22 | 23 | [HttpGet] 24 | [SwaggerOperation("Browse users")] 25 | [ProducesResponseType(StatusCodes.Status200OK)] 26 | public async Task>> Get() 27 | => Ok(await _usersService.BrowseAsync()); 28 | 29 | [HttpGet("{userId:guid}")] 30 | [SwaggerOperation("Get user by ID")] 31 | [ProducesResponseType(StatusCodes.Status200OK)] 32 | [ProducesResponseType(StatusCodes.Status404NotFound)] 33 | public async Task> Get(Guid userId) 34 | { 35 | var user = await _usersService.GetAsync(userId); 36 | if (user is not null) 37 | { 38 | return Ok(user); 39 | } 40 | 41 | return NotFound(); 42 | } 43 | 44 | [HttpGet("by-email/{email}")] 45 | [SwaggerOperation("Get user by email")] 46 | [ProducesResponseType(StatusCodes.Status200OK)] 47 | [ProducesResponseType(StatusCodes.Status404NotFound)] 48 | public async Task> Get(string email) 49 | { 50 | var user = await _usersService.GetAsync(email); 51 | if (user is not null) 52 | { 53 | return Ok(user); 54 | } 55 | 56 | return NotFound(); 57 | } 58 | 59 | [HttpPost] 60 | [SwaggerOperation("Create user")] 61 | [ProducesResponseType(StatusCodes.Status201Created)] 62 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 63 | public async Task Post(UserDetailsDto user) 64 | { 65 | user.UserId = Guid.NewGuid(); 66 | await _usersService.AddAsync(user); 67 | return CreatedAtAction(nameof(Get), new { userId = user.UserId }, null); 68 | } 69 | 70 | [HttpPut("{userId:guid}/verify")] 71 | [SwaggerOperation("Verify user")] 72 | [ProducesResponseType(StatusCodes.Status204NoContent)] 73 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 74 | public async Task Post(Guid userId) 75 | { 76 | await _usersService.VerifyAsync(userId); 77 | return NoContent(); 78 | } 79 | } -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Api/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using NPay.Modules.Users.Core; 4 | 5 | namespace NPay.Modules.Users.Api; 6 | 7 | public static class Extensions 8 | { 9 | public static IServiceCollection AddUsersModule(this IServiceCollection services) 10 | { 11 | services.AddCoreLayer(); 12 | 13 | return services; 14 | } 15 | 16 | public static IApplicationBuilder UseUsersModule(this IApplicationBuilder app) 17 | { 18 | return app; 19 | } 20 | } -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Api/NPay.Modules.Users.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Core/DAL/Configurations/UserConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using NPay.Modules.Users.Core.Entities; 4 | 5 | namespace NPay.Modules.Users.Core.DAL.Configurations; 6 | 7 | internal sealed class UserConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.HasIndex(x => x.Email).IsUnique(); 12 | builder.Property(x => x.Email).IsRequired(); 13 | builder.Property(x => x.FullName).IsRequired().HasMaxLength(100); 14 | builder.Property(x => x.Address).IsRequired().HasMaxLength(200); 15 | builder.Property(x => x.Identity).IsRequired().HasMaxLength(40); 16 | builder.Property(x => x.Nationality).IsRequired().HasMaxLength(2); 17 | } 18 | } -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Core/DAL/Migrations/20211019160150_Users_Init.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using NPay.Modules.Users.Core.DAL; 8 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 9 | 10 | namespace NPay.Modules.Users.Core.DAL.Migrations 11 | { 12 | [DbContext(typeof(UsersDbContext))] 13 | [Migration("20211019160150_Users_Init")] 14 | partial class Users_Init 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasDefaultSchema("users") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 63) 22 | .HasAnnotation("ProductVersion", "5.0.11") 23 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); 24 | 25 | modelBuilder.Entity("NPay.Modules.Users.Core.Entities.User", b => 26 | { 27 | b.Property("Id") 28 | .ValueGeneratedOnAdd() 29 | .HasColumnType("uuid"); 30 | 31 | b.Property("Address") 32 | .IsRequired() 33 | .HasMaxLength(200) 34 | .HasColumnType("character varying(200)"); 35 | 36 | b.Property("CreatedAt") 37 | .HasColumnType("timestamp without time zone"); 38 | 39 | b.Property("Email") 40 | .IsRequired() 41 | .HasColumnType("text"); 42 | 43 | b.Property("FullName") 44 | .IsRequired() 45 | .HasMaxLength(100) 46 | .HasColumnType("character varying(100)"); 47 | 48 | b.Property("Identity") 49 | .IsRequired() 50 | .HasMaxLength(40) 51 | .HasColumnType("character varying(40)"); 52 | 53 | b.Property("Nationality") 54 | .IsRequired() 55 | .HasMaxLength(2) 56 | .HasColumnType("character varying(2)"); 57 | 58 | b.Property("VerifiedAt") 59 | .HasColumnType("timestamp without time zone"); 60 | 61 | b.HasKey("Id"); 62 | 63 | b.HasIndex("Email") 64 | .IsUnique(); 65 | 66 | b.ToTable("Users"); 67 | }); 68 | #pragma warning restore 612, 618 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Core/DAL/Migrations/20211019160150_Users_Init.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace NPay.Modules.Users.Core.DAL.Migrations 5 | { 6 | public partial class Users_Init : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.EnsureSchema( 11 | name: "users"); 12 | 13 | migrationBuilder.CreateTable( 14 | name: "Users", 15 | schema: "users", 16 | columns: table => new 17 | { 18 | Id = table.Column(type: "uuid", nullable: false), 19 | Email = table.Column(type: "text", nullable: false), 20 | FullName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), 21 | Address = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), 22 | Nationality = table.Column(type: "character varying(2)", maxLength: 2, nullable: false), 23 | Identity = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), 24 | CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), 25 | VerifiedAt = table.Column(type: "timestamp without time zone", nullable: true) 26 | }, 27 | constraints: table => 28 | { 29 | table.PrimaryKey("PK_Users", x => x.Id); 30 | }); 31 | 32 | migrationBuilder.CreateIndex( 33 | name: "IX_Users_Email", 34 | schema: "users", 35 | table: "Users", 36 | column: "Email", 37 | unique: true); 38 | } 39 | 40 | protected override void Down(MigrationBuilder migrationBuilder) 41 | { 42 | migrationBuilder.DropTable( 43 | name: "Users", 44 | schema: "users"); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Core/DAL/Migrations/UsersDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using NPay.Modules.Users.Core.DAL; 7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 8 | 9 | namespace NPay.Modules.Users.Core.DAL.Migrations 10 | { 11 | [DbContext(typeof(UsersDbContext))] 12 | partial class UsersDbContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasDefaultSchema("users") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 63) 20 | .HasAnnotation("ProductVersion", "5.0.11") 21 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); 22 | 23 | modelBuilder.Entity("NPay.Modules.Users.Core.Entities.User", b => 24 | { 25 | b.Property("Id") 26 | .ValueGeneratedOnAdd() 27 | .HasColumnType("uuid"); 28 | 29 | b.Property("Address") 30 | .IsRequired() 31 | .HasMaxLength(200) 32 | .HasColumnType("character varying(200)"); 33 | 34 | b.Property("CreatedAt") 35 | .HasColumnType("timestamp without time zone"); 36 | 37 | b.Property("Email") 38 | .IsRequired() 39 | .HasColumnType("text"); 40 | 41 | b.Property("FullName") 42 | .IsRequired() 43 | .HasMaxLength(100) 44 | .HasColumnType("character varying(100)"); 45 | 46 | b.Property("Identity") 47 | .IsRequired() 48 | .HasMaxLength(40) 49 | .HasColumnType("character varying(40)"); 50 | 51 | b.Property("Nationality") 52 | .IsRequired() 53 | .HasMaxLength(2) 54 | .HasColumnType("character varying(2)"); 55 | 56 | b.Property("VerifiedAt") 57 | .HasColumnType("timestamp without time zone"); 58 | 59 | b.HasKey("Id"); 60 | 61 | b.HasIndex("Email") 62 | .IsUnique(); 63 | 64 | b.ToTable("Users"); 65 | }); 66 | #pragma warning restore 612, 618 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Core/DAL/UsersDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using NPay.Modules.Users.Core.Entities; 3 | 4 | namespace NPay.Modules.Users.Core.DAL; 5 | 6 | internal sealed class UsersDbContext : DbContext 7 | { 8 | public DbSet Users { get; set; } 9 | 10 | public UsersDbContext(DbContextOptions options) : base(options) 11 | { 12 | } 13 | 14 | protected override void OnModelCreating(ModelBuilder modelBuilder) 15 | { 16 | modelBuilder.HasDefaultSchema("users"); 17 | modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Core/Entities/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Modules.Users.Core.Exceptions; 3 | 4 | namespace NPay.Modules.Users.Core.Entities; 5 | 6 | internal sealed class User 7 | { 8 | public Guid Id { get; private set; } 9 | public string Email { get; private set; } 10 | public string FullName { get; private set; } 11 | public string Address { get; private set; } 12 | public string Nationality { get; private set; } 13 | public string Identity { get; private set; } 14 | public DateTime CreatedAt { get; private set; } 15 | public DateTime? VerifiedAt { get; private set; } 16 | 17 | private User() 18 | { 19 | } 20 | 21 | public User(Guid id, string email, string fullName, string address, string nationality, string identity, 22 | DateTime createdAt) 23 | { 24 | Id = id; 25 | Email = email; 26 | FullName = fullName; 27 | Address = address; 28 | Nationality = nationality; 29 | Identity = identity; 30 | CreatedAt = createdAt; 31 | } 32 | 33 | public void Verify(DateTime verifiedAt) 34 | { 35 | if (VerifiedAt.HasValue) 36 | { 37 | throw new UserAlreadyVerifiedException(Id); 38 | } 39 | 40 | VerifiedAt = verifiedAt; 41 | } 42 | } -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Core/Exceptions/InvalidAddressException.cs: -------------------------------------------------------------------------------- 1 | using NPay.Shared.Exceptions; 2 | 3 | namespace NPay.Modules.Users.Core.Exceptions; 4 | 5 | internal sealed class InvalidAddressException : NPayException 6 | { 7 | public string Address { get; } 8 | 9 | public InvalidAddressException(string address) : base($"Address: '{address}' is invalid.") 10 | { 11 | Address = address; 12 | } 13 | } -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Core/Exceptions/InvalidEmailException.cs: -------------------------------------------------------------------------------- 1 | using NPay.Shared.Exceptions; 2 | 3 | namespace NPay.Modules.Users.Core.Exceptions; 4 | 5 | internal sealed class InvalidEmailException : NPayException 6 | { 7 | public string Email { get; } 8 | 9 | public InvalidEmailException(string email) : base($"Email: '{email}' is invalid.") 10 | { 11 | Email = email; 12 | } 13 | } -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Core/Exceptions/InvalidFullNameException.cs: -------------------------------------------------------------------------------- 1 | using NPay.Shared.Exceptions; 2 | 3 | namespace NPay.Modules.Users.Core.Exceptions; 4 | 5 | internal sealed class InvalidFullNameException : NPayException 6 | { 7 | public string FullName { get; } 8 | 9 | public InvalidFullNameException(string fullName) : base($"Full name: '{fullName}' is invalid.") 10 | { 11 | FullName = fullName; 12 | } 13 | } -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Core/Exceptions/UserAlreadyExistsException.cs: -------------------------------------------------------------------------------- 1 | using NPay.Shared.Exceptions; 2 | 3 | namespace NPay.Modules.Users.Core.Exceptions; 4 | 5 | internal sealed class UserAlreadyExistsException : NPayException 6 | { 7 | public string Email { get; } 8 | 9 | public UserAlreadyExistsException(string email) : base($"User with email: '{email}' already exists.") 10 | { 11 | Email = email; 12 | } 13 | } -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Core/Exceptions/UserAlreadyVerifiedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Shared.Exceptions; 3 | 4 | namespace NPay.Modules.Users.Core.Exceptions; 5 | 6 | internal sealed class UserAlreadyVerifiedException : NPayException 7 | { 8 | public Guid UserId { get; } 9 | 10 | public UserAlreadyVerifiedException(Guid userId) : base($"User with ID: '{userId}' is already verified.") 11 | { 12 | UserId = userId; 13 | } 14 | } -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Core/Exceptions/UserNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Shared.Exceptions; 3 | 4 | namespace NPay.Modules.Users.Core.Exceptions; 5 | 6 | internal sealed class UserNotFoundException : NPayException 7 | { 8 | public Guid UserId { get; } 9 | 10 | public UserNotFoundException(Guid userId) : base($"User with ID: '{userId}' was not found.") 11 | { 12 | UserId = userId; 13 | } 14 | } -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Core/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using NPay.Modules.Users.Core.DAL; 3 | using NPay.Modules.Users.Core.Services; 4 | using NPay.Modules.Users.Shared; 5 | using NPay.Shared.Database; 6 | 7 | namespace NPay.Modules.Users.Core; 8 | 9 | public static class Extensions 10 | { 11 | public static IServiceCollection AddCoreLayer(this IServiceCollection services) 12 | { 13 | services.AddPostgres(); 14 | services.AddTransient(); 15 | services.AddTransient(); 16 | 17 | return services; 18 | } 19 | } -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Core/NPay.Modules.Users.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Core/Services/IUsersService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using NPay.Modules.Users.Shared.DTO; 5 | 6 | namespace NPay.Modules.Users.Core.Services; 7 | 8 | public interface IUsersService 9 | { 10 | Task GetAsync(Guid userId); 11 | Task GetAsync(string email); 12 | Task> BrowseAsync(); 13 | Task AddAsync(UserDetailsDto dto); 14 | Task VerifyAsync(Guid userId); 15 | } -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Core/Services/UsersModuleApi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using NPay.Modules.Users.Shared; 4 | using NPay.Modules.Users.Shared.DTO; 5 | 6 | namespace NPay.Modules.Users.Core.Services; 7 | 8 | internal sealed class UsersModuleApi : IUsersModuleApi 9 | { 10 | private readonly IUsersService _usersService; 11 | 12 | public UsersModuleApi(IUsersService usersService) 13 | { 14 | _usersService = usersService; 15 | } 16 | 17 | public Task GetUserAsync(Guid userId) => _usersService.GetAsync(userId); 18 | 19 | public Task GetUserAsync(string email) => _usersService.GetAsync(email); 20 | } -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Core/Services/UsersService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.Logging; 7 | using NPay.Modules.Users.Core.DAL; 8 | using NPay.Modules.Users.Core.Entities; 9 | using NPay.Modules.Users.Core.Exceptions; 10 | using NPay.Modules.Users.Shared.DTO; 11 | using NPay.Modules.Users.Shared.Events; 12 | using NPay.Shared.Messaging; 13 | using NPay.Shared.Time; 14 | 15 | namespace NPay.Modules.Users.Core.Services; 16 | 17 | internal sealed class UsersService : IUsersService 18 | { 19 | private readonly UsersDbContext _dbContext; 20 | private readonly IMessageBroker _messageBroker; 21 | private readonly IClock _clock; 22 | private readonly ILogger _logger; 23 | 24 | public UsersService(UsersDbContext dbContext, IMessageBroker messageBroker, IClock clock, 25 | ILogger logger) 26 | { 27 | _dbContext = dbContext; 28 | _messageBroker = messageBroker; 29 | _clock = clock; 30 | _logger = logger; 31 | } 32 | 33 | public async Task GetAsync(Guid userId) 34 | { 35 | var user = await _dbContext.Users.SingleOrDefaultAsync(x => x.Id == userId); 36 | 37 | // TODO: Make use of custom mapping interface or AutoMapper, Mapster etc. 38 | 39 | return user is null ? null : MapToDetailsDto(user); 40 | } 41 | 42 | public async Task GetAsync(string email) 43 | { 44 | // TODO: Additional email validation 45 | email = email?.ToLowerInvariant(); 46 | if (string.IsNullOrWhiteSpace(email)) 47 | { 48 | throw new InvalidEmailException(email); 49 | } 50 | 51 | var user = await _dbContext.Users.SingleOrDefaultAsync(x => x.Email == email); 52 | 53 | return user is null ? null : MapToDetailsDto(user); 54 | } 55 | 56 | public async Task> BrowseAsync() 57 | { 58 | var users = await _dbContext.Users.ToListAsync(); 59 | 60 | // TODO: Implement pagination, sorting etc. 61 | 62 | return users.Select(MapToDto).ToList(); 63 | } 64 | 65 | public async Task AddAsync(UserDetailsDto dto) 66 | { 67 | var email = dto.Email.ToLowerInvariant(); 68 | if (await _dbContext.Users.AnyAsync(x => x.Email == email)) 69 | { 70 | throw new UserAlreadyExistsException(email); 71 | } 72 | 73 | // TODO: Additional validation for the remaining properties 74 | if (string.IsNullOrWhiteSpace(dto.FullName)) 75 | { 76 | throw new InvalidFullNameException(dto.FullName); 77 | } 78 | 79 | if (string.IsNullOrWhiteSpace(dto.Address)) 80 | { 81 | throw new InvalidAddressException(dto.Address); 82 | } 83 | 84 | var user = new User(dto.UserId, email, dto.FullName, dto.Address, 85 | dto.Nationality, dto.Identity, _clock.CurrentDate()); 86 | await _dbContext.Users.AddAsync(user); 87 | await _dbContext.SaveChangesAsync(); 88 | await _messageBroker.PublishAsync(new UserCreated(user.Id, user.Email, user.FullName, user.Nationality)); 89 | _logger.LogInformation($"Created the user with ID: '{dto.UserId}'."); 90 | } 91 | 92 | public async Task VerifyAsync(Guid userId) 93 | { 94 | var user = await _dbContext.Users.SingleOrDefaultAsync(x => x.Id == userId); 95 | if (user is null) 96 | { 97 | throw new UserNotFoundException(userId); 98 | } 99 | 100 | user.Verify(_clock.CurrentDate()); 101 | _dbContext.Users.Update(user); 102 | await _dbContext.SaveChangesAsync(); 103 | await _messageBroker.PublishAsync(new UserVerified(user.Id, user.Email, user.Nationality)); 104 | _logger.LogInformation($"Verified the user with ID: '{user.Id}'."); 105 | } 106 | 107 | // TODO: Extract to the dedicated "mapper" interfaces or extension methods 108 | 109 | private static UserDto MapToDto(User user)=> Map(user); 110 | 111 | private static UserDetailsDto MapToDetailsDto(User user) 112 | { 113 | var dto = Map(user); 114 | dto.Address = user.Address; 115 | dto.Identity = user.Identity; 116 | 117 | return dto; 118 | } 119 | 120 | private static T Map(User user) where T : UserDto, new() 121 | => new() 122 | { 123 | UserId = user.Id, 124 | Email = user.Email, 125 | FullName = user.FullName, 126 | Nationality = user.Nationality, 127 | State = user.VerifiedAt.HasValue ? "verified" : "unverified", 128 | CreatedAt = user.CreatedAt 129 | }; 130 | } -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Shared/DTO/UserDetailsDto.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace NPay.Modules.Users.Shared.DTO; 4 | 5 | public class UserDetailsDto : UserDto 6 | { 7 | [Required] 8 | public string Address { get; set; } 9 | 10 | [Required] 11 | public string Identity { get; set; } 12 | } -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Shared/DTO/UserDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace NPay.Modules.Users.Shared.DTO; 5 | 6 | public class UserDto 7 | { 8 | public Guid UserId { get; set; } 9 | 10 | [Required] 11 | [EmailAddress] 12 | public string Email { get; set; } 13 | 14 | [Required] 15 | public string FullName { get; set; } 16 | 17 | [Required] 18 | public string Nationality { get; set; } 19 | 20 | public string State { get; set; } 21 | public DateTime CreatedAt { get; set; } 22 | } -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Shared/Events/UserCreated.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Shared.Events; 3 | 4 | namespace NPay.Modules.Users.Shared.Events; 5 | 6 | public record UserCreated(Guid UserId, string Email, string FullName, string Nationality) : IEvent; -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Shared/Events/UserVerified.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Shared.Events; 3 | 4 | namespace NPay.Modules.Users.Shared.Events; 5 | 6 | public record UserVerified(Guid UserId, string Email, string Nationality) : IEvent; -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Shared/IUsersModuleApi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using NPay.Modules.Users.Shared.DTO; 4 | 5 | namespace NPay.Modules.Users.Shared; 6 | 7 | public interface IUsersModuleApi 8 | { 9 | Task GetUserAsync(Guid userId); 10 | Task GetUserAsync(string email); 11 | } -------------------------------------------------------------------------------- /src/Modules/Users/NPay.Modules.Users.Shared/NPay.Modules.Users.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Modules/Users/Users.rest: -------------------------------------------------------------------------------- 1 | @url = http://localhost:5000/users 2 | @email = user1@npay.io 3 | @userId = 00000000-0000-0000-0000-000000000000 4 | 5 | ### 6 | GET {{url}} 7 | 8 | ### 9 | GET {{url}}/{{userId}} 10 | 11 | ### 12 | GET {{url}}/by-email/{{email}} 13 | 14 | ### 15 | POST {{url}} 16 | Content-Type: application/json 17 | 18 | { 19 | "email": "{{email}}", 20 | "fullName": "John Doe", 21 | "address": "Test street 123", 22 | "nationality": "PL", 23 | "identity": "ID1234567890" 24 | } 25 | 26 | ### 27 | PUT {{url}}/{{userId}}/verify 28 | Content-Type: application/json 29 | 30 | { 31 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Api/Controllers/FundsController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | using NPay.Modules.Wallets.Application.Wallets.Commands; 5 | using NPay.Shared.Dispatchers; 6 | using Swashbuckle.AspNetCore.Annotations; 7 | 8 | namespace NPay.Modules.Wallets.Api.Controllers; 9 | 10 | [ApiController] 11 | [Route("[controller]")] 12 | public class FundsController : ControllerBase 13 | { 14 | private readonly IDispatcher _dispatcher; 15 | 16 | public FundsController(IDispatcher dispatcher) 17 | { 18 | _dispatcher = dispatcher; 19 | } 20 | 21 | [HttpPost] 22 | [SwaggerOperation("Add funds")] 23 | [ProducesResponseType(StatusCodes.Status204NoContent)] 24 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 25 | public async Task Post(AddFunds command) 26 | { 27 | await _dispatcher.SendAsync(command); 28 | return NoContent(); 29 | } 30 | 31 | [HttpPost("transfer")] 32 | [SwaggerOperation("Transfer funds")] 33 | [ProducesResponseType(StatusCodes.Status204NoContent)] 34 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 35 | public async Task Post(TransferFunds command) 36 | { 37 | await _dispatcher.SendAsync(command); 38 | return NoContent(); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Api/Controllers/OwnersController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | using NPay.Modules.Wallets.Application.Owners.Commands; 5 | using NPay.Shared.Dispatchers; 6 | using Swashbuckle.AspNetCore.Annotations; 7 | 8 | namespace NPay.Modules.Wallets.Api.Controllers; 9 | 10 | [ApiController] 11 | [Route("[controller]")] 12 | public class OwnersController : ControllerBase 13 | { 14 | private readonly IDispatcher _dispatcher; 15 | 16 | public OwnersController(IDispatcher dispatcher) 17 | { 18 | _dispatcher = dispatcher; 19 | } 20 | 21 | [HttpPost] 22 | [SwaggerOperation("Add owner")] 23 | [ProducesResponseType(StatusCodes.Status204NoContent)] 24 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 25 | public async Task Post(AddOwner command) 26 | { 27 | await _dispatcher.SendAsync(command); 28 | return NoContent(); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Api/Controllers/WalletsController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using NPay.Modules.Wallets.Application.Wallets.Commands; 6 | using NPay.Modules.Wallets.Application.Wallets.Queries; 7 | using NPay.Modules.Wallets.Shared.DTO; 8 | using NPay.Shared.Dispatchers; 9 | using Swashbuckle.AspNetCore.Annotations; 10 | 11 | namespace NPay.Modules.Wallets.Api.Controllers; 12 | 13 | [ApiController] 14 | [Route("[controller]")] 15 | public class WalletsController : ControllerBase 16 | { 17 | private readonly IDispatcher _dispatcher; 18 | 19 | public WalletsController(IDispatcher dispatcher) 20 | { 21 | _dispatcher = dispatcher; 22 | } 23 | 24 | [HttpGet("{walletId:guid}")] 25 | [SwaggerOperation("Get wallet")] 26 | [ProducesResponseType(StatusCodes.Status200OK)] 27 | [ProducesResponseType(StatusCodes.Status404NotFound)] 28 | public async Task> Get(Guid walletId) 29 | { 30 | var wallet = await _dispatcher.QueryAsync(new GetWallet { WalletId = walletId }); 31 | if (wallet is not null) 32 | { 33 | return Ok(wallet); 34 | } 35 | 36 | return NotFound(); 37 | } 38 | 39 | [HttpPost] 40 | [SwaggerOperation("Add wallet")] 41 | [ProducesResponseType(StatusCodes.Status201Created)] 42 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 43 | public async Task Post(AddWallet command) 44 | { 45 | await _dispatcher.SendAsync(command); 46 | return CreatedAtAction(nameof(Get), new { walletId = command.WalletId }, null); 47 | } 48 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Api/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using NPay.Modules.Wallets.Application; 4 | using NPay.Modules.Wallets.Core; 5 | using NPay.Modules.Wallets.Infrastructure; 6 | 7 | namespace NPay.Modules.Wallets.Api; 8 | 9 | public static class Extensions 10 | { 11 | public static IServiceCollection AddWalletsModule(this IServiceCollection services) 12 | { 13 | services.AddCoreLayer(); 14 | services.AddApplicationLayer(); 15 | services.AddInfrastructureLayer(); 16 | 17 | return services; 18 | } 19 | 20 | public static IApplicationBuilder UseWalletsModule(this IApplicationBuilder app) 21 | { 22 | return app; 23 | } 24 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Api/NPay.Modules.Wallets.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using NPay.Modules.Wallets.Application.Services; 4 | using NPay.Modules.Wallets.Shared; 5 | 6 | [assembly: InternalsVisibleTo("NPay.Modules.Wallets.Infrastructure")] 7 | namespace NPay.Modules.Wallets.Application; 8 | 9 | public static class Extensions 10 | { 11 | public static IServiceCollection AddApplicationLayer(this IServiceCollection services) 12 | { 13 | services.AddTransient(); 14 | 15 | return services; 16 | } 17 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/NPay.Modules.Wallets.Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Owners/Commands/AddOwner.cs: -------------------------------------------------------------------------------- 1 | using NPay.Shared.Commands; 2 | 3 | namespace NPay.Modules.Wallets.Application.Owners.Commands; 4 | 5 | public record AddOwner(string Email) : ICommand; -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Owners/Commands/Handlers/AddOwnerHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | using NPay.Modules.Users.Shared; 5 | using NPay.Modules.Wallets.Application.Owners.Exceptions; 6 | using NPay.Modules.Wallets.Core.Owners.Aggregates; 7 | using NPay.Modules.Wallets.Core.Owners.Repositories; 8 | using NPay.Shared.Commands; 9 | using NPay.Shared.Time; 10 | 11 | namespace NPay.Modules.Wallets.Application.Owners.Commands.Handlers; 12 | 13 | internal sealed class AddOwnerHandler : ICommandHandler 14 | { 15 | private readonly IOwnerRepository _ownerRepository; 16 | private readonly IUsersModuleApi _usersModuleApi; 17 | private readonly IClock _clock; 18 | private readonly ILogger _logger; 19 | 20 | public AddOwnerHandler(IOwnerRepository ownerRepository, IUsersModuleApi usersModuleApi, IClock clock, 21 | ILogger logger) 22 | { 23 | _ownerRepository = ownerRepository; 24 | _usersModuleApi = usersModuleApi; 25 | _clock = clock; 26 | _logger = logger; 27 | } 28 | 29 | public async Task HandleAsync(AddOwner command, CancellationToken cancellationToken = default) 30 | { 31 | var user = await _usersModuleApi.GetUserAsync(command.Email); 32 | if (user is null) 33 | { 34 | throw new UserNotFoundException(command.Email); 35 | } 36 | 37 | if (await _ownerRepository.GetAsync(user.UserId) is not null) 38 | { 39 | throw new OwnerAlreadyExistsException(command.Email); 40 | } 41 | 42 | var now = _clock.CurrentDate(); 43 | var owner = new Owner(user.UserId, user.FullName, user.Nationality, now); 44 | await _ownerRepository.AddAsync(owner); 45 | _logger.LogInformation($"Created an owner for the user with ID: '{user.UserId}'."); 46 | } 47 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Owners/Exceptions/UserAlreadyExistsException.cs: -------------------------------------------------------------------------------- 1 | using NPay.Shared.Exceptions; 2 | 3 | namespace NPay.Modules.Wallets.Application.Owners.Exceptions; 4 | 5 | internal sealed class OwnerAlreadyExistsException : NPayException 6 | { 7 | public string Email { get; } 8 | 9 | public OwnerAlreadyExistsException(string email) : base($"Owner with email: '{email}' already exists.") 10 | { 11 | Email = email; 12 | } 13 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Owners/Exceptions/UserNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using NPay.Shared.Exceptions; 2 | 3 | namespace NPay.Modules.Wallets.Application.Owners.Exceptions; 4 | 5 | internal sealed class UserNotFoundException : NPayException 6 | { 7 | public string Email { get; } 8 | 9 | public UserNotFoundException(string email) : base($"User with email: '{email}' was not found.") 10 | { 11 | Email = email; 12 | } 13 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Owners/ExternalEvents/Handlers/UserCreatedHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | using NPay.Modules.Users.Shared.Events; 5 | using NPay.Modules.Wallets.Application.Owners.Exceptions; 6 | using NPay.Modules.Wallets.Core.Owners.Aggregates; 7 | using NPay.Modules.Wallets.Core.Owners.Repositories; 8 | using NPay.Shared.Events; 9 | using NPay.Shared.Time; 10 | 11 | namespace NPay.Modules.Wallets.Application.Owners.ExternalEvents.Handlers; 12 | 13 | internal sealed class UserCreatedHandler : IEventHandler 14 | { 15 | private readonly IOwnerRepository _ownerRepository; 16 | private readonly IClock _clock; 17 | private readonly ILogger _logger; 18 | 19 | public UserCreatedHandler(IOwnerRepository ownerRepository, IClock clock, ILogger logger) 20 | { 21 | _ownerRepository = ownerRepository; 22 | _clock = clock; 23 | _logger = logger; 24 | } 25 | 26 | public async Task HandleAsync(UserCreated @event, CancellationToken cancellationToken = default) 27 | { 28 | if (await _ownerRepository.GetAsync(@event.UserId) is not null) 29 | { 30 | throw new OwnerAlreadyExistsException(@event.Email); 31 | } 32 | 33 | var now = _clock.CurrentDate(); 34 | var owner = new Owner(@event.UserId, @event.FullName, @event.Nationality, now); 35 | await _ownerRepository.AddAsync(owner); 36 | _logger.LogInformation($"Created an owner for the user with ID: '{@event.UserId}'."); 37 | } 38 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Owners/ExternalEvents/Handlers/UserVerifiedHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | using NPay.Modules.Users.Shared.Events; 5 | using NPay.Modules.Wallets.Core.Owners.Exceptions; 6 | using NPay.Modules.Wallets.Core.Owners.Repositories; 7 | using NPay.Modules.Wallets.Shared.Events; 8 | using NPay.Shared.Events; 9 | using NPay.Shared.Messaging; 10 | using NPay.Shared.Time; 11 | 12 | namespace NPay.Modules.Wallets.Application.Owners.ExternalEvents.Handlers; 13 | 14 | internal sealed class UserVerifiedHandler : IEventHandler 15 | { 16 | private readonly IOwnerRepository _ownerRepository; 17 | private readonly IMessageBroker _messageBroker; 18 | private readonly IClock _clock; 19 | private readonly ILogger _logger; 20 | 21 | public UserVerifiedHandler(IOwnerRepository ownerRepository, IMessageBroker messageBroker, IClock clock, 22 | ILogger logger) 23 | { 24 | _ownerRepository = ownerRepository; 25 | _messageBroker = messageBroker; 26 | _clock = clock; 27 | _logger = logger; 28 | } 29 | 30 | public async Task HandleAsync(UserVerified @event, CancellationToken cancellationToken = default) 31 | { 32 | var owner = await _ownerRepository.GetAsync(@event.UserId); 33 | if (owner is null) 34 | { 35 | throw new OwnerNotFoundException(@event.UserId); 36 | } 37 | 38 | var now = _clock.CurrentDate(); 39 | owner.Verify(now); 40 | await _ownerRepository.UpdateAsync(owner); 41 | await _messageBroker.PublishAsync(new OwnerVerified(owner.Id), cancellationToken); 42 | _logger.LogInformation($"Verified an owner for the user with ID: '{@event.UserId}'."); 43 | } 44 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Services/WalletsModuleApi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using NPay.Modules.Wallets.Application.Wallets.Queries; 4 | using NPay.Modules.Wallets.Shared; 5 | using NPay.Modules.Wallets.Shared.DTO; 6 | using NPay.Shared.Dispatchers; 7 | 8 | namespace NPay.Modules.Wallets.Application.Services; 9 | 10 | internal sealed class WalletsModuleApi : IWalletsModuleApi 11 | { 12 | private readonly IDispatcher _dispatcher; 13 | 14 | public WalletsModuleApi(IDispatcher dispatcher) 15 | { 16 | _dispatcher = dispatcher; 17 | } 18 | 19 | public Task GetWalletAsync(Guid walletId) 20 | => _dispatcher.QueryAsync(new GetWallet { WalletId = walletId }); 21 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Wallets/Commands/AddFunds.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Shared.Commands; 3 | 4 | namespace NPay.Modules.Wallets.Application.Wallets.Commands; 5 | 6 | public record AddFunds(Guid WalletId, decimal Amount) : ICommand; -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Wallets/Commands/AddWallet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Shared.Commands; 3 | 4 | namespace NPay.Modules.Wallets.Application.Wallets.Commands; 5 | 6 | public record AddWallet(Guid OwnerId, string Currency) : ICommand 7 | { 8 | public Guid WalletId { get; init; } = Guid.NewGuid(); 9 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Wallets/Commands/Handlers/AddFundsHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | using NPay.Modules.Wallets.Core.Wallets.Exceptions; 5 | using NPay.Modules.Wallets.Core.Wallets.Repositories; 6 | using NPay.Modules.Wallets.Core.Wallets.ValueObjects; 7 | using NPay.Modules.Wallets.Shared.Events; 8 | using NPay.Shared.Commands; 9 | using NPay.Shared.Messaging; 10 | using NPay.Shared.Time; 11 | 12 | namespace NPay.Modules.Wallets.Application.Wallets.Commands.Handlers; 13 | 14 | internal sealed class AddFundsHandler : ICommandHandler 15 | { 16 | private readonly IWalletRepository _walletRepository; 17 | private readonly IClock _clock; 18 | private readonly IMessageBroker _messageBroker; 19 | private readonly ILogger _logger; 20 | 21 | public AddFundsHandler(IWalletRepository walletRepository, IClock clock, IMessageBroker messageBroker, 22 | ILogger logger) 23 | 24 | { 25 | _walletRepository = walletRepository; 26 | _clock = clock; 27 | _messageBroker = messageBroker; 28 | _logger = logger; 29 | } 30 | 31 | public async Task HandleAsync(AddFunds command, CancellationToken cancellationToken = default) 32 | { 33 | var (walletId, amount) = command; 34 | var wallet = await _walletRepository.GetAsync(walletId); 35 | if (wallet is null) 36 | { 37 | throw new WalletNotFoundException(walletId); 38 | } 39 | 40 | var now = _clock.CurrentDate(); 41 | var transfer = wallet.AddFunds(new TransferId(), amount, now); 42 | await _walletRepository.UpdateAsync(wallet); 43 | await _messageBroker.PublishAsync(new FundsAdded(walletId, wallet.OwnerId, transfer.Currency, 44 | transfer.Amount), cancellationToken); 45 | _logger.LogInformation($"Added {transfer.Amount} {transfer.Currency} to the wallet: '{wallet.Id}'"); 46 | } 47 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Wallets/Commands/Handlers/AddWalletHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | using NPay.Modules.Wallets.Core.Wallets.Aggregates; 5 | using NPay.Modules.Wallets.Core.Wallets.Repositories; 6 | using NPay.Modules.Wallets.Shared.Events; 7 | using NPay.Shared.Commands; 8 | using NPay.Shared.Messaging; 9 | using NPay.Shared.Time; 10 | 11 | namespace NPay.Modules.Wallets.Application.Wallets.Commands.Handlers; 12 | 13 | internal sealed class AddWalletHandler : ICommandHandler 14 | { 15 | private readonly IWalletRepository _walletRepository; 16 | private readonly IClock _clock; 17 | private readonly IMessageBroker _messageBroker; 18 | private readonly ILogger _logger; 19 | 20 | public AddWalletHandler(IWalletRepository walletRepository, IClock clock, IMessageBroker messageBroker, 21 | ILogger logger) 22 | { 23 | _walletRepository = walletRepository; 24 | _clock = clock; 25 | _messageBroker = messageBroker; 26 | _logger = logger; 27 | } 28 | 29 | public async Task HandleAsync(AddWallet command, CancellationToken cancellationToken = default) 30 | { 31 | var now = _clock.CurrentDate(); 32 | var wallet = Wallet.Create(command.WalletId, command.OwnerId, command.Currency, now); 33 | await _walletRepository.AddAsync(wallet); 34 | await _messageBroker.PublishAsync(new WalletAdded(wallet.Id, wallet.OwnerId, wallet.Currency), 35 | cancellationToken); 36 | _logger.LogInformation($"Added the wallet with ID: '{wallet.Id}' for an owner: '{wallet.OwnerId}'."); 37 | } 38 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Wallets/Commands/Handlers/TransferFundsHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | using NPay.Modules.Wallets.Core.Wallets.Exceptions; 5 | using NPay.Modules.Wallets.Core.Wallets.Repositories; 6 | using NPay.Modules.Wallets.Shared.Events; 7 | using NPay.Shared.Commands; 8 | using NPay.Shared.Messaging; 9 | using NPay.Shared.Time; 10 | 11 | namespace NPay.Modules.Wallets.Application.Wallets.Commands.Handlers; 12 | 13 | internal sealed class TransferFundsHandler : ICommandHandler 14 | { 15 | private readonly IWalletRepository _walletRepository; 16 | private readonly IClock _clock; 17 | private readonly IMessageBroker _messageBroker; 18 | private readonly ILogger _logger; 19 | 20 | public TransferFundsHandler(IWalletRepository walletRepository, IClock clock, IMessageBroker messageBroker, 21 | ILogger logger) 22 | { 23 | _walletRepository = walletRepository; 24 | _clock = clock; 25 | _messageBroker = messageBroker; 26 | _logger = logger; 27 | } 28 | 29 | public async Task HandleAsync(TransferFunds command, CancellationToken cancellationToken = default) 30 | { 31 | var (fromWalletId, toWalletId, amount) = command; 32 | var fromWallet = await _walletRepository.GetAsync(fromWalletId); 33 | if (fromWallet is null) 34 | { 35 | throw new WalletNotFoundException(fromWalletId); 36 | } 37 | 38 | var toWallet = await _walletRepository.GetAsync(toWalletId); 39 | if (toWallet is null) 40 | { 41 | throw new WalletNotFoundException(toWalletId); 42 | } 43 | 44 | var now = _clock.CurrentDate(); 45 | var currency = fromWallet.Currency; 46 | fromWallet.TransferFunds(toWallet, amount, now); 47 | await _walletRepository.UpdateAsync(fromWallet); 48 | await _walletRepository.UpdateAsync(toWallet); 49 | await _messageBroker.PublishAsync(new FundsTransferred(fromWalletId, toWalletId, amount, currency), 50 | cancellationToken); 51 | _logger.LogInformation($"Transferred {amount} {currency} from: '{fromWallet.Id}' to: '{toWallet.Id}'."); 52 | } 53 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Wallets/Commands/TransferFunds.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Shared.Commands; 3 | 4 | namespace NPay.Modules.Wallets.Application.Wallets.Commands; 5 | 6 | public record TransferFunds(Guid FromWalletId, Guid ToWalletId, decimal Amount) : ICommand; -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Wallets/ExternalEvents/Handlers/UserVerifiedHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | using NPay.Modules.Users.Shared.Events; 5 | using NPay.Modules.Wallets.Core.Wallets.Aggregates; 6 | using NPay.Modules.Wallets.Core.Wallets.Repositories; 7 | using NPay.Modules.Wallets.Core.Wallets.Services; 8 | using NPay.Modules.Wallets.Shared.Events; 9 | using NPay.Shared.Events; 10 | using NPay.Shared.Messaging; 11 | using NPay.Shared.Time; 12 | 13 | namespace NPay.Modules.Wallets.Application.Wallets.ExternalEvents.Handlers; 14 | 15 | internal sealed class UserVerifiedHandler : IEventHandler 16 | { 17 | private readonly IWalletRepository _walletRepository; 18 | private readonly ICurrencyResolver _currencyResolver; 19 | private readonly IClock _clock; 20 | private readonly IMessageBroker _messageBroker; 21 | private readonly ILogger _logger; 22 | 23 | public UserVerifiedHandler(IWalletRepository walletRepository, ICurrencyResolver currencyResolver, 24 | IClock clock, IMessageBroker messageBroker, ILogger logger) 25 | { 26 | _walletRepository = walletRepository; 27 | _currencyResolver = currencyResolver; 28 | _clock = clock; 29 | _messageBroker = messageBroker; 30 | _logger = logger; 31 | } 32 | 33 | public async Task HandleAsync(UserVerified @event, CancellationToken cancellationToken = default) 34 | { 35 | var now = _clock.CurrentDate(); 36 | var currency = _currencyResolver.Resolve(@event.Nationality); 37 | var wallet = Wallet.Create(@event.UserId, currency, now); 38 | await _walletRepository.AddAsync(wallet); 39 | await _messageBroker.PublishAsync(new WalletAdded(wallet.Id, wallet.OwnerId, wallet.Currency), 40 | cancellationToken); 41 | _logger.LogInformation($"Added the wallet with ID: '{wallet.Id}' for an owner: '{wallet.OwnerId}'."); 42 | } 43 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Wallets/Queries/GetWallet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Modules.Wallets.Shared.DTO; 3 | using NPay.Shared.Queries; 4 | 5 | namespace NPay.Modules.Wallets.Application.Wallets.Queries; 6 | 7 | public class GetWallet : IQuery 8 | { 9 | public Guid WalletId { get; set; } 10 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Wallets/Queries/Handlers/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using NPay.Modules.Wallets.Core.Wallets.Aggregates; 3 | using NPay.Modules.Wallets.Core.Wallets.Entities; 4 | using NPay.Modules.Wallets.Shared.DTO; 5 | 6 | namespace NPay.Modules.Wallets.Application.Wallets.Queries.Handlers; 7 | 8 | internal static class Extensions 9 | { 10 | public static WalletDto AsDto(this Wallet wallet) 11 | => new() 12 | { 13 | WalletId = wallet.Id, 14 | OwnerId = wallet.OwnerId, 15 | Currency = wallet.Currency, 16 | CreatedAt = wallet.CreatedAt, 17 | Amount = wallet.CurrentAmount(), 18 | Transfers = wallet.Transfers.Select(x => x.AsDto()) 19 | .OrderByDescending(x => x.CreatedAt) 20 | .ToList() 21 | }; 22 | 23 | public static TransferDto AsDto(this Transfer transfer) 24 | => new() 25 | { 26 | TransferId = transfer.Id, 27 | ReferenceId = transfer.ReferenceId, 28 | WalletId = transfer.WalletId, 29 | Amount = transfer.Amount, 30 | Currency = transfer.Currency, 31 | Direction = transfer.Direction.ToString().ToLowerInvariant(), 32 | CreatedAt = transfer.CreatedAt 33 | }; 34 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Wallets/Queries/Handlers/GetWalletHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NPay.Modules.Wallets.Application.Wallets.Storage; 4 | using NPay.Modules.Wallets.Shared.DTO; 5 | using NPay.Shared.Queries; 6 | 7 | namespace NPay.Modules.Wallets.Application.Wallets.Queries.Handlers; 8 | 9 | internal sealed class GetWalletHandler : IQueryHandler 10 | { 11 | private readonly IWalletStorage _storage; 12 | 13 | public GetWalletHandler(IWalletStorage storage) 14 | { 15 | _storage = storage; 16 | } 17 | 18 | public async Task HandleAsync(GetWallet query, CancellationToken cancellationToken = default) 19 | { 20 | var wallet = await _storage.FindAsync(x => x.Id == query.WalletId); 21 | 22 | return wallet?.AsDto(); 23 | } 24 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Wallets/Storage/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | 5 | namespace NPay.Modules.Wallets.Application.Wallets.Storage; 6 | 7 | internal static class Extensions 8 | { 9 | public static Expression> And(this Expression> a, Expression> b) 10 | { 11 | var firstParam = a.Parameters[0]; 12 | var visitor = new SubstExpressionVisitor 13 | { 14 | Dictionary = 15 | { 16 | [b.Parameters[0]] = firstParam 17 | } 18 | }; 19 | 20 | var body = Expression.AndAlso(a.Body, visitor.Visit(b.Body)); 21 | return Expression.Lambda>(body, firstParam); 22 | } 23 | 24 | public static Expression> Or(this Expression> a, Expression> b) 25 | { 26 | var firstParam = a.Parameters[0]; 27 | var visitor = new SubstExpressionVisitor 28 | { 29 | Dictionary = 30 | { 31 | [b.Parameters[0]] = firstParam 32 | } 33 | }; 34 | 35 | var body = Expression.OrElse(a.Body, visitor.Visit(b.Body)); 36 | return Expression.Lambda>(body, firstParam); 37 | } 38 | 39 | 40 | private class SubstExpressionVisitor : ExpressionVisitor 41 | { 42 | public readonly Dictionary Dictionary = new(); 43 | 44 | protected override Expression VisitParameter(ParameterExpression node) 45 | => Dictionary.TryGetValue(node, out var newValue) ? newValue : node; 46 | } 47 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Application/Wallets/Storage/IWalletStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Threading.Tasks; 4 | using NPay.Modules.Wallets.Core.Wallets.Aggregates; 5 | 6 | namespace NPay.Modules.Wallets.Application.Wallets.Storage; 7 | 8 | internal interface IWalletStorage 9 | { 10 | Task FindAsync(Expression> expression); 11 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using NPay.Modules.Wallets.Core.Wallets.Services; 4 | 5 | [assembly: InternalsVisibleTo("NPay.Modules.Wallets.Application")] 6 | [assembly: InternalsVisibleTo("NPay.Modules.Wallets.Infrastructure")] 7 | namespace NPay.Modules.Wallets.Core; 8 | 9 | public static class Extensions 10 | { 11 | public static IServiceCollection AddCoreLayer(this IServiceCollection services) 12 | { 13 | services.AddSingleton(); 14 | 15 | return services; 16 | } 17 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/NPay.Modules.Wallets.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Owners/Aggregates/Owner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Modules.Wallets.Core.Owners.ValueObjects; 3 | using NPay.Modules.Wallets.Core.SharedKernel; 4 | 5 | namespace NPay.Modules.Wallets.Core.Owners.Aggregates; 6 | 7 | internal class Owner : AggregateRoot 8 | { 9 | public FullName FullName { get; private set; } 10 | public Nationality Nationality { get; private set; } 11 | public DateTime CreatedAt { get; private set; } 12 | public DateTime? VerifiedAt { get; private set; } 13 | public bool IsVerified => VerifiedAt.HasValue; 14 | 15 | protected Owner() 16 | { 17 | } 18 | 19 | public Owner(OwnerId id, FullName fullName, Nationality nationality, DateTime createdAt) 20 | { 21 | Id = id; 22 | FullName = fullName; 23 | Nationality = nationality; 24 | CreatedAt = createdAt; 25 | } 26 | 27 | public void Verify(DateTime verifiedAt) 28 | { 29 | if (IsVerified) 30 | { 31 | return; 32 | } 33 | 34 | VerifiedAt = verifiedAt; 35 | IncrementVersion(); 36 | } 37 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Owners/Exceptions/InvalidFullNameException.cs: -------------------------------------------------------------------------------- 1 | using NPay.Shared.Exceptions; 2 | 3 | namespace NPay.Modules.Wallets.Core.Owners.Exceptions; 4 | 5 | internal sealed class InvalidFullNameException : NPayException 6 | { 7 | public string FullName { get; } 8 | 9 | public InvalidFullNameException(string fullName) : base($"Full name: '{fullName}' is invalid.") 10 | { 11 | FullName = fullName; 12 | } 13 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Owners/Exceptions/InvalidNationalityException.cs: -------------------------------------------------------------------------------- 1 | using NPay.Shared.Exceptions; 2 | 3 | namespace NPay.Modules.Wallets.Core.Owners.Exceptions; 4 | 5 | internal sealed class InvalidNationalityException : NPayException 6 | { 7 | public string Nationality { get; } 8 | 9 | public InvalidNationalityException(string nationality) 10 | : base($"Nationality: '{nationality}' is invalid.") 11 | { 12 | Nationality = nationality; 13 | } 14 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Owners/Exceptions/OwnerNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Shared.Exceptions; 3 | 4 | namespace NPay.Modules.Wallets.Core.Owners.Exceptions; 5 | 6 | internal sealed class OwnerNotFoundException : NPayException 7 | { 8 | public Guid OwnerId { get; } 9 | 10 | public OwnerNotFoundException(Guid ownerId) : base($"Owner with ID: '{ownerId}' was not found.") 11 | { 12 | OwnerId = ownerId; 13 | } 14 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Owners/Exceptions/UnsupportedNationalityException.cs: -------------------------------------------------------------------------------- 1 | using NPay.Shared.Exceptions; 2 | 3 | namespace NPay.Modules.Wallets.Core.Owners.Exceptions; 4 | 5 | internal sealed class UnsupportedNationalityException : NPayException 6 | { 7 | public string Nationality { get; } 8 | 9 | public UnsupportedNationalityException(string nationality) 10 | : base($"Nationality: '{nationality}' is unsupported.") 11 | { 12 | Nationality = nationality; 13 | } 14 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Owners/Repositories/IOwnerRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using NPay.Modules.Wallets.Core.Owners.Aggregates; 3 | using NPay.Modules.Wallets.Core.SharedKernel; 4 | 5 | namespace NPay.Modules.Wallets.Core.Owners.Repositories; 6 | 7 | internal interface IOwnerRepository 8 | { 9 | Task GetAsync(OwnerId id); 10 | Task AddAsync(Owner owner); 11 | Task UpdateAsync(Owner owner); 12 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Owners/ValueObjects/FullName.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Modules.Wallets.Core.Owners.Exceptions; 3 | 4 | namespace NPay.Modules.Wallets.Core.Owners.ValueObjects; 5 | 6 | internal sealed class FullName : IEquatable 7 | { 8 | public string Value { get; } 9 | 10 | public FullName(string value) 11 | { 12 | if (string.IsNullOrWhiteSpace(value) || value.Length is > 100 or < 2) 13 | { 14 | throw new InvalidFullNameException(value); 15 | } 16 | 17 | Value = value; 18 | } 19 | 20 | public static implicit operator FullName(string value) => value is null ? null : new FullName(value); 21 | 22 | public static implicit operator string(FullName value) => value?.Value; 23 | 24 | public bool Equals(FullName other) 25 | { 26 | if (ReferenceEquals(null, other)) return false; 27 | if (ReferenceEquals(this, other)) return true; 28 | return Value == other.Value; 29 | } 30 | 31 | public override bool Equals(object obj) 32 | { 33 | if (ReferenceEquals(null, obj)) return false; 34 | if (ReferenceEquals(this, obj)) return true; 35 | return obj.GetType() == GetType() && Equals((FullName)obj); 36 | } 37 | 38 | public override int GetHashCode() => Value is not null ? Value.GetHashCode() : 0; 39 | 40 | public override string ToString() => Value; 41 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Owners/ValueObjects/Nationality.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using NPay.Modules.Wallets.Core.Owners.Exceptions; 4 | 5 | namespace NPay.Modules.Wallets.Core.Owners.ValueObjects; 6 | 7 | internal sealed class Nationality : IEquatable 8 | { 9 | private static readonly HashSet AllowedValues = new() 10 | { 11 | "PL", "DE", "FR", "ES", "GB" 12 | }; 13 | 14 | public string Value { get; } 15 | 16 | public Nationality(string value) 17 | { 18 | if (string.IsNullOrWhiteSpace(value) || value.Length != 2) 19 | { 20 | throw new InvalidNationalityException(value); 21 | } 22 | 23 | value = value.ToUpperInvariant(); 24 | if (!AllowedValues.Contains(value)) 25 | { 26 | throw new UnsupportedNationalityException(value); 27 | } 28 | 29 | Value = value; 30 | } 31 | 32 | public static implicit operator Nationality(string value) => value is null ? null : new Nationality(value); 33 | 34 | public static implicit operator string(Nationality value) => value?.Value; 35 | 36 | public bool Equals(Nationality other) 37 | { 38 | if (ReferenceEquals(null, other)) return false; 39 | if (ReferenceEquals(this, other)) return true; 40 | return Value == other.Value; 41 | } 42 | 43 | public override bool Equals(object obj) 44 | { 45 | if (ReferenceEquals(null, obj)) return false; 46 | if (ReferenceEquals(this, obj)) return true; 47 | return obj.GetType() == GetType() && Equals((Nationality)obj); 48 | } 49 | 50 | public override int GetHashCode() => Value is not null ? Value.GetHashCode() : 0; 51 | 52 | public override string ToString() => Value; 53 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/SharedKernel/AggregateRoot.cs: -------------------------------------------------------------------------------- 1 | namespace NPay.Modules.Wallets.Core.SharedKernel; 2 | 3 | public abstract class AggregateRoot 4 | { 5 | private bool _versionIncremented; 6 | public T Id { get; protected set; } 7 | public int Version { get; protected set; } = 1; 8 | 9 | protected void IncrementVersion() 10 | { 11 | if (_versionIncremented) 12 | { 13 | return; 14 | } 15 | 16 | Version++; 17 | _versionIncremented = true; 18 | } 19 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/SharedKernel/OwnerId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NPay.Modules.Wallets.Core.SharedKernel; 4 | 5 | internal sealed class OwnerId : IEquatable 6 | { 7 | public Guid Value { get; } 8 | 9 | public OwnerId(Guid value) 10 | { 11 | Value = value; 12 | } 13 | 14 | public bool Equals(OwnerId other) 15 | { 16 | if (ReferenceEquals(null, other)) return false; 17 | return ReferenceEquals(this, other) || Value.Equals(other.Value); 18 | } 19 | 20 | public override bool Equals(object obj) 21 | { 22 | if (ReferenceEquals(null, obj)) return false; 23 | if (ReferenceEquals(this, obj)) return true; 24 | return obj.GetType() == GetType() && Equals((OwnerId)obj); 25 | } 26 | 27 | public override int GetHashCode() => Value.GetHashCode(); 28 | 29 | public override string ToString() => Value.ToString(); 30 | 31 | public static implicit operator OwnerId(Guid value) => new(value); 32 | 33 | public static implicit operator Guid(OwnerId value) => value.Value; 34 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Wallets/Aggregates/Wallet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using NPay.Modules.Wallets.Core.SharedKernel; 5 | using NPay.Modules.Wallets.Core.Wallets.Entities; 6 | using NPay.Modules.Wallets.Core.Wallets.Exceptions; 7 | using NPay.Modules.Wallets.Core.Wallets.ValueObjects; 8 | 9 | namespace NPay.Modules.Wallets.Core.Wallets.Aggregates; 10 | 11 | internal class Wallet : AggregateRoot 12 | { 13 | private HashSet _transfers = new(); 14 | 15 | public OwnerId OwnerId { get; private set; } 16 | public Currency Currency { get; private set; } 17 | 18 | public IEnumerable Transfers 19 | { 20 | get => _transfers; 21 | set => _transfers = new HashSet(value); 22 | } 23 | 24 | public DateTime CreatedAt { get; private set; } 25 | 26 | private Wallet() 27 | { 28 | } 29 | 30 | private Wallet(WalletId id, OwnerId ownerId, Currency currency, DateTime createdAt) 31 | { 32 | Id = id; 33 | OwnerId = ownerId; 34 | Currency = currency; 35 | CreatedAt = createdAt; 36 | } 37 | 38 | public static Wallet Create(OwnerId ownerId, Currency currency, DateTime createdAt) 39 | => Create(new WalletId(), ownerId, currency, createdAt); 40 | 41 | public static Wallet Create(WalletId id, OwnerId ownerId, Currency currency, DateTime createdAt) 42 | => new Wallet(id, ownerId, currency, createdAt); 43 | 44 | public IReadOnlyCollection TransferFunds(Wallet receiver, Amount amount, DateTime createdAt) 45 | { 46 | var outTransferId = new TransferId(); 47 | var inTransferId = new TransferId(); 48 | 49 | var outTransfer = DeductFunds(outTransferId, amount, createdAt, inTransferId); 50 | var inTransfer = receiver.AddFunds(inTransferId, amount, createdAt, outTransferId); 51 | 52 | return new[] { outTransfer, inTransfer }; 53 | } 54 | 55 | public Transfer AddFunds(TransferId transferId, Amount amount, DateTime createdAt, 56 | TransferId referenceId = null) 57 | { 58 | if (amount <= 0) 59 | { 60 | throw new InvalidTransferAmountException(amount); 61 | } 62 | 63 | var transfer = Transfer.Incoming(transferId, Id, Currency, amount, createdAt, referenceId); 64 | _transfers.Add(transfer); 65 | IncrementVersion(); 66 | 67 | return transfer; 68 | } 69 | 70 | public Transfer DeductFunds(TransferId transferId, Amount amount, DateTime createdAt, 71 | TransferId referenceId = null) 72 | { 73 | if (amount <= 0) 74 | { 75 | throw new InvalidTransferAmountException(amount); 76 | } 77 | 78 | if (CurrentAmount() < amount) 79 | { 80 | throw new InsufficientWalletFundsException(Id); 81 | } 82 | 83 | var transfer = Transfer.Outgoing(transferId, Id, Currency, amount, createdAt, referenceId); 84 | _transfers.Add(transfer); 85 | IncrementVersion(); 86 | 87 | return transfer; 88 | } 89 | 90 | public Amount CurrentAmount() => SumIncomingAmount() - SumOutgoingAmount(); 91 | 92 | private Amount SumIncomingAmount() 93 | => _transfers.Where(x => x.Direction == Transfer.TransferDirection.In).Sum(x => x.Amount); 94 | 95 | private Amount SumOutgoingAmount() 96 | => _transfers.Where(x => x.Direction == Transfer.TransferDirection.Out).Sum(x => x.Amount); 97 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Wallets/Entities/Transfer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Modules.Wallets.Core.Wallets.ValueObjects; 3 | 4 | namespace NPay.Modules.Wallets.Core.Wallets.Entities; 5 | 6 | internal class Transfer 7 | { 8 | public TransferId Id { get; private set; } 9 | public TransferId ReferenceId { get; private set; } 10 | public WalletId WalletId { get; private set; } 11 | public Currency Currency { get; private set; } 12 | public Amount Amount { get; private set; } 13 | public TransferDirection Direction { get; private set; } 14 | public DateTime CreatedAt { get; private set; } 15 | 16 | private Transfer() 17 | { 18 | } 19 | 20 | private Transfer(TransferId id, WalletId walletId, Currency currency, Amount amount, 21 | TransferDirection direction, DateTime createdAt, TransferId referenceId = null) 22 | { 23 | Id = id; 24 | WalletId = walletId; 25 | Currency = currency; 26 | Amount = amount; 27 | Direction = direction; 28 | CreatedAt = createdAt; 29 | ReferenceId = referenceId; 30 | } 31 | 32 | public static Transfer Incoming(TransferId id, WalletId walletId, Currency currency, Amount amount, 33 | DateTime createdAt, TransferId referenceId = null) 34 | => new(id, walletId, currency, amount, TransferDirection.In, createdAt, referenceId); 35 | 36 | public static Transfer Outgoing(TransferId id, WalletId walletId, Currency currency, Amount amount, 37 | DateTime createdAt, TransferId referenceId = null) 38 | => new(id, walletId, currency, amount, TransferDirection.Out, createdAt, referenceId); 39 | 40 | internal enum TransferDirection 41 | { 42 | In, 43 | Out 44 | } 45 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Wallets/Exceptions/InsufficientWalletFundsException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Shared.Exceptions; 3 | 4 | namespace NPay.Modules.Wallets.Core.Wallets.Exceptions; 5 | 6 | internal sealed class InsufficientWalletFundsException : NPayException 7 | { 8 | public Guid WalletId { get; } 9 | 10 | public InsufficientWalletFundsException(Guid walletId) 11 | : base($"Insufficient funds for wallet with ID: '{walletId}'.") 12 | { 13 | WalletId = walletId; 14 | } 15 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Wallets/Exceptions/InvalidAmountException.cs: -------------------------------------------------------------------------------- 1 | using NPay.Shared.Exceptions; 2 | 3 | namespace NPay.Modules.Wallets.Core.Wallets.Exceptions; 4 | 5 | internal sealed class InvalidAmountException : NPayException 6 | { 7 | public decimal Amount { get; } 8 | 9 | public InvalidAmountException(decimal amount) : base($"Amount: '{amount}' is invalid.") 10 | { 11 | Amount = amount; 12 | } 13 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Wallets/Exceptions/InvalidCurrencyException.cs: -------------------------------------------------------------------------------- 1 | using NPay.Shared.Exceptions; 2 | 3 | namespace NPay.Modules.Wallets.Core.Wallets.Exceptions; 4 | 5 | internal sealed class InvalidCurrencyException : NPayException 6 | { 7 | public string Currency { get; } 8 | 9 | public InvalidCurrencyException(string currency) : base($"Currency: '{currency}' is invalid.") 10 | { 11 | Currency = currency; 12 | } 13 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Wallets/Exceptions/InvalidTransferAmountException.cs: -------------------------------------------------------------------------------- 1 | using NPay.Shared.Exceptions; 2 | 3 | namespace NPay.Modules.Wallets.Core.Wallets.Exceptions; 4 | 5 | internal sealed class InvalidTransferAmountException : NPayException 6 | { 7 | public decimal Amount { get; } 8 | 9 | public InvalidTransferAmountException(decimal amount) : base($"Transfer has invalid amount: '{amount}'.") 10 | { 11 | Amount = amount; 12 | } 13 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Wallets/Exceptions/UnsupportedCurrencyException.cs: -------------------------------------------------------------------------------- 1 | using NPay.Shared.Exceptions; 2 | 3 | namespace NPay.Modules.Wallets.Core.Wallets.Exceptions; 4 | 5 | internal sealed class UnsupportedCurrencyException : NPayException 6 | { 7 | public string Currency { get; } 8 | 9 | public UnsupportedCurrencyException(string currency) : base($"Currency: '{currency}' is unsupported.") 10 | { 11 | Currency = currency; 12 | } 13 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Wallets/Exceptions/WalletNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Shared.Exceptions; 3 | 4 | namespace NPay.Modules.Wallets.Core.Wallets.Exceptions; 5 | 6 | internal sealed class WalletNotFoundException : NPayException 7 | { 8 | public Guid OwnerId { get; } 9 | public string Currency { get; } 10 | public Guid WalletId { get; } 11 | 12 | public WalletNotFoundException(Guid walletId) : base($"Wallet with ID: '{walletId}' was not found.") 13 | { 14 | WalletId = walletId; 15 | } 16 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Wallets/Repositories/IWalletRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using NPay.Modules.Wallets.Core.SharedKernel; 3 | using NPay.Modules.Wallets.Core.Wallets.Aggregates; 4 | using NPay.Modules.Wallets.Core.Wallets.ValueObjects; 5 | 6 | namespace NPay.Modules.Wallets.Core.Wallets.Repositories; 7 | 8 | internal interface IWalletRepository 9 | { 10 | Task GetAsync(WalletId id); 11 | Task GetAsync(OwnerId ownerId, Currency currency); 12 | Task AddAsync(Wallet wallet); 13 | Task UpdateAsync(Wallet wallet); 14 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Wallets/Services/CurrencyResolver.cs: -------------------------------------------------------------------------------- 1 | using NPay.Modules.Wallets.Core.Owners.ValueObjects; 2 | using NPay.Modules.Wallets.Core.Wallets.ValueObjects; 3 | 4 | namespace NPay.Modules.Wallets.Core.Wallets.Services; 5 | 6 | internal sealed class CurrencyResolver : ICurrencyResolver 7 | { 8 | public Currency Resolve(Nationality nationality) 9 | => nationality.Value switch 10 | { 11 | "PL" => "PLN", 12 | "DE" => "EUR", 13 | "FR" => "EUR", 14 | "ES" => "EUR", 15 | "GB" => "GBP", 16 | _ => "EUR" 17 | }; 18 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Wallets/Services/ICurrencyResolver.cs: -------------------------------------------------------------------------------- 1 | using NPay.Modules.Wallets.Core.Owners.ValueObjects; 2 | using NPay.Modules.Wallets.Core.Wallets.ValueObjects; 3 | 4 | namespace NPay.Modules.Wallets.Core.Wallets.Services; 5 | 6 | internal interface ICurrencyResolver 7 | { 8 | Currency Resolve(Nationality nationality); 9 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Wallets/ValueObjects/Amount.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using NPay.Modules.Wallets.Core.Wallets.Exceptions; 4 | 5 | namespace NPay.Modules.Wallets.Core.Wallets.ValueObjects; 6 | 7 | internal sealed class Amount : IEquatable 8 | { 9 | public decimal Value { get; } 10 | 11 | public Amount(decimal value) 12 | { 13 | if (value is < 0 or > 1000000) 14 | { 15 | throw new InvalidAmountException(value); 16 | } 17 | 18 | Value = value; 19 | } 20 | 21 | public static Amount Zero => new(0); 22 | 23 | public static implicit operator Amount(decimal value) => new(value); 24 | 25 | public static implicit operator decimal(Amount value) => value.Value; 26 | 27 | public static bool operator ==(Amount a, Amount b) 28 | { 29 | if (ReferenceEquals(a, b)) 30 | { 31 | return true; 32 | } 33 | 34 | if (a is not null && b is not null) 35 | { 36 | return a.Value.Equals(b.Value); 37 | } 38 | 39 | return false; 40 | } 41 | 42 | public static bool operator !=(Amount a, Amount b) => !(a == b); 43 | 44 | public static bool operator >(Amount a, Amount b) => a.Value > b.Value; 45 | 46 | public static bool operator <(Amount a, Amount b) => a.Value < b.Value; 47 | 48 | public static bool operator >=(Amount a, Amount b) => a.Value >= b.Value; 49 | 50 | public static bool operator <=(Amount a, Amount b) => a.Value <= b.Value; 51 | 52 | public static Amount operator +(Amount a, Amount b) => a.Value + b.Value; 53 | 54 | public static Amount operator -(Amount a, Amount b) => a.Value - b.Value; 55 | 56 | public bool Equals(Amount other) 57 | { 58 | if (ReferenceEquals(null, other)) return false; 59 | if (ReferenceEquals(this, other)) return true; 60 | return Value == other.Value; 61 | } 62 | 63 | public override bool Equals(object obj) 64 | { 65 | if (ReferenceEquals(null, obj)) return false; 66 | if (ReferenceEquals(this, obj)) return true; 67 | return obj.GetType() == GetType() && Equals((Amount)obj); 68 | } 69 | 70 | public override int GetHashCode() => Value.GetHashCode(); 71 | 72 | public override string ToString() => Value.ToString(CultureInfo.InvariantCulture); 73 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Wallets/ValueObjects/Currency.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using NPay.Modules.Wallets.Core.Wallets.Exceptions; 4 | 5 | namespace NPay.Modules.Wallets.Core.Wallets.ValueObjects; 6 | 7 | internal sealed class Currency : IEquatable 8 | { 9 | private static readonly HashSet AllowedValues = new() 10 | { 11 | "PLN", "EUR", "GBP" 12 | }; 13 | 14 | public string Value { get; } 15 | 16 | public Currency(string value) 17 | { 18 | if (string.IsNullOrWhiteSpace(value) || value.Length != 3) 19 | { 20 | throw new InvalidCurrencyException(value); 21 | } 22 | 23 | value = value.ToUpperInvariant(); 24 | if (!AllowedValues.Contains(value)) 25 | { 26 | throw new UnsupportedCurrencyException(value); 27 | } 28 | 29 | Value = value; 30 | } 31 | 32 | public static implicit operator Currency(string value) => new(value); 33 | 34 | public static implicit operator string(Currency value) => value.Value; 35 | 36 | public static bool operator ==(Currency a, Currency b) 37 | { 38 | if (ReferenceEquals(a, b)) 39 | { 40 | return true; 41 | } 42 | 43 | if (a is not null && b is not null) 44 | { 45 | return a.Value.Equals(b.Value); 46 | } 47 | 48 | return false; 49 | } 50 | 51 | public static bool operator !=(Currency a, Currency b) => !(a == b); 52 | 53 | public bool Equals(Currency other) 54 | { 55 | if (ReferenceEquals(null, other)) return false; 56 | if (ReferenceEquals(this, other)) return true; 57 | return Value == other.Value; 58 | } 59 | 60 | public override bool Equals(object obj) 61 | { 62 | if (ReferenceEquals(null, obj)) return false; 63 | if (ReferenceEquals(this, obj)) return true; 64 | return obj.GetType() == GetType() && Equals((Currency)obj); 65 | } 66 | 67 | public override int GetHashCode() => Value is not null ? Value.GetHashCode() : 0; 68 | 69 | public override string ToString() => Value; 70 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Wallets/ValueObjects/TransferId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NPay.Modules.Wallets.Core.Wallets.ValueObjects; 4 | 5 | internal sealed class TransferId : IEquatable 6 | { 7 | public Guid Value { get; } 8 | 9 | public TransferId() : this(Guid.NewGuid()) 10 | { 11 | } 12 | 13 | public TransferId(Guid value) 14 | { 15 | Value = value; 16 | } 17 | 18 | public bool Equals(TransferId other) 19 | { 20 | if (ReferenceEquals(null, other)) return false; 21 | return ReferenceEquals(this, other) || Value.Equals(other.Value); 22 | } 23 | 24 | public override bool Equals(object obj) 25 | { 26 | if (ReferenceEquals(null, obj)) return false; 27 | if (ReferenceEquals(this, obj)) return true; 28 | return obj.GetType() == GetType() && Equals((TransferId)obj); 29 | } 30 | 31 | public override int GetHashCode() => Value.GetHashCode(); 32 | 33 | public override string ToString() => Value.ToString(); 34 | 35 | public static implicit operator TransferId(Guid value) => new(value); 36 | 37 | public static implicit operator Guid(TransferId value) => value.Value; 38 | 39 | public static implicit operator Guid?(TransferId value) => value?.Value; 40 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Core/Wallets/ValueObjects/WalletId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NPay.Modules.Wallets.Core.Wallets.ValueObjects; 4 | 5 | internal sealed class WalletId : IEquatable 6 | { 7 | public Guid Value { get; } 8 | 9 | public WalletId() : this(Guid.NewGuid()) 10 | { 11 | } 12 | 13 | public WalletId(Guid value) 14 | { 15 | Value = value; 16 | } 17 | 18 | public bool Equals(WalletId other) 19 | { 20 | if (ReferenceEquals(null, other)) return false; 21 | return ReferenceEquals(this, other) || Value.Equals(other.Value); 22 | } 23 | 24 | public override bool Equals(object obj) 25 | { 26 | if (ReferenceEquals(null, obj)) return false; 27 | if (ReferenceEquals(this, obj)) return true; 28 | return obj.GetType() == GetType() && Equals((WalletId)obj); 29 | } 30 | 31 | public override int GetHashCode() => Value.GetHashCode(); 32 | 33 | public override string ToString() => Value.ToString(); 34 | 35 | public static implicit operator WalletId(Guid value) => new(value); 36 | 37 | public static implicit operator Guid(WalletId value) => value.Value; 38 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Infrastructure/DAL/Configurations/OwnerConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using NPay.Modules.Wallets.Core.Owners.Aggregates; 4 | using NPay.Modules.Wallets.Core.Owners.ValueObjects; 5 | using NPay.Modules.Wallets.Core.SharedKernel; 6 | 7 | namespace NPay.Modules.Wallets.Infrastructure.DAL.Configurations; 8 | 9 | internal class OwnerConfiguration : IEntityTypeConfiguration 10 | { 11 | public void Configure(EntityTypeBuilder builder) 12 | { 13 | builder.ToTable("Owners"); 14 | 15 | builder.Property(x => x.Id) 16 | .HasConversion(x => x.Value, x => new OwnerId(x)); 17 | 18 | builder.Property(x => x.FullName) 19 | .IsRequired() 20 | .HasConversion(x => x.Value, x => new FullName(x)); 21 | 22 | builder.Property(x => x.Nationality) 23 | .IsRequired() 24 | .HasConversion(x => x.Value, x => new Nationality(x)); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Infrastructure/DAL/Configurations/TransferConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using NPay.Modules.Wallets.Core.Wallets.Entities; 4 | using NPay.Modules.Wallets.Core.Wallets.ValueObjects; 5 | 6 | namespace NPay.Modules.Wallets.Infrastructure.DAL.Configurations; 7 | 8 | internal class TransferConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.ToTable("Transfers"); 13 | 14 | builder.Property(x => x.Id) 15 | .HasConversion(x => x.Value, x => new TransferId(x)); 16 | 17 | builder.Property(x => x.ReferenceId) 18 | .HasConversion(x => x.Value, x => new TransferId(x)); 19 | 20 | builder.Property(x => x.WalletId) 21 | .IsRequired() 22 | .HasConversion(x => x.Value, x => new WalletId(x)); 23 | 24 | builder.Property(x => x.Currency) 25 | .IsRequired() 26 | .HasConversion(x => x.Value, x => new Currency(x)); 27 | 28 | builder.Property(x => x.Amount) 29 | .IsRequired() 30 | .HasConversion(x => x.Value, x => new Amount(x)); 31 | } 32 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Infrastructure/DAL/Configurations/WalletConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using NPay.Modules.Wallets.Core.Owners.Aggregates; 4 | using NPay.Modules.Wallets.Core.SharedKernel; 5 | using NPay.Modules.Wallets.Core.Wallets.Aggregates; 6 | using NPay.Modules.Wallets.Core.Wallets.ValueObjects; 7 | 8 | namespace NPay.Modules.Wallets.Infrastructure.DAL.Configurations; 9 | 10 | internal class WalletConfiguration : IEntityTypeConfiguration 11 | { 12 | public void Configure(EntityTypeBuilder builder) 13 | { 14 | builder.Property(x => x.Version).IsConcurrencyToken(); 15 | builder.HasOne().WithMany().HasForeignKey(x => x.OwnerId); 16 | 17 | builder.Property(x => x.Id) 18 | .HasConversion(x => x.Value, x => new WalletId(x)); 19 | 20 | builder.Property(x => x.OwnerId) 21 | .IsRequired() 22 | .HasConversion(x => x.Value, x => new OwnerId(x)); 23 | 24 | builder.Property(x => x.Currency) 25 | .IsRequired() 26 | .HasConversion(x => x.Value, x => new Currency(x)); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Infrastructure/DAL/Migrations/20211020191405_Wallets_Init.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using NPay.Modules.Wallets.Infrastructure.DAL; 8 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 9 | 10 | namespace NPay.Modules.Wallets.Infrastructure.DAL.Migrations 11 | { 12 | [DbContext(typeof(WalletsDbContext))] 13 | [Migration("20211020191405_Wallets_Init")] 14 | partial class Wallets_Init 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasDefaultSchema("wallets") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 63) 22 | .HasAnnotation("ProductVersion", "5.0.11") 23 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); 24 | 25 | modelBuilder.Entity("NPay.Modules.Wallets.Core.Owners.Aggregates.Owner", b => 26 | { 27 | b.Property("Id") 28 | .HasColumnType("uuid"); 29 | 30 | b.Property("CreatedAt") 31 | .HasColumnType("timestamp without time zone"); 32 | 33 | b.Property("FullName") 34 | .IsRequired() 35 | .HasColumnType("text"); 36 | 37 | b.Property("Nationality") 38 | .IsRequired() 39 | .HasColumnType("text"); 40 | 41 | b.Property("VerifiedAt") 42 | .HasColumnType("timestamp without time zone"); 43 | 44 | b.Property("Version") 45 | .HasColumnType("integer"); 46 | 47 | b.HasKey("Id"); 48 | 49 | b.ToTable("Owners"); 50 | }); 51 | 52 | modelBuilder.Entity("NPay.Modules.Wallets.Core.Wallets.Aggregates.Wallet", b => 53 | { 54 | b.Property("Id") 55 | .HasColumnType("uuid"); 56 | 57 | b.Property("CreatedAt") 58 | .HasColumnType("timestamp without time zone"); 59 | 60 | b.Property("Currency") 61 | .IsRequired() 62 | .HasColumnType("text"); 63 | 64 | b.Property("OwnerId") 65 | .HasColumnType("uuid"); 66 | 67 | b.Property("Version") 68 | .IsConcurrencyToken() 69 | .HasColumnType("integer"); 70 | 71 | b.HasKey("Id"); 72 | 73 | b.HasIndex("OwnerId"); 74 | 75 | b.ToTable("Wallets"); 76 | }); 77 | 78 | modelBuilder.Entity("NPay.Modules.Wallets.Core.Wallets.Entities.Transfer", b => 79 | { 80 | b.Property("Id") 81 | .HasColumnType("uuid"); 82 | 83 | b.Property("Amount") 84 | .HasColumnType("numeric"); 85 | 86 | b.Property("CreatedAt") 87 | .HasColumnType("timestamp without time zone"); 88 | 89 | b.Property("Currency") 90 | .IsRequired() 91 | .HasColumnType("text"); 92 | 93 | b.Property("Direction") 94 | .HasColumnType("integer"); 95 | 96 | b.Property("ReferenceId") 97 | .HasColumnType("uuid"); 98 | 99 | b.Property("WalletId") 100 | .HasColumnType("uuid"); 101 | 102 | b.HasKey("Id"); 103 | 104 | b.HasIndex("WalletId"); 105 | 106 | b.ToTable("Transfers"); 107 | }); 108 | 109 | modelBuilder.Entity("NPay.Modules.Wallets.Core.Wallets.Aggregates.Wallet", b => 110 | { 111 | b.HasOne("NPay.Modules.Wallets.Core.Owners.Aggregates.Owner", null) 112 | .WithMany() 113 | .HasForeignKey("OwnerId") 114 | .OnDelete(DeleteBehavior.Cascade) 115 | .IsRequired(); 116 | }); 117 | 118 | modelBuilder.Entity("NPay.Modules.Wallets.Core.Wallets.Entities.Transfer", b => 119 | { 120 | b.HasOne("NPay.Modules.Wallets.Core.Wallets.Aggregates.Wallet", null) 121 | .WithMany("Transfers") 122 | .HasForeignKey("WalletId") 123 | .OnDelete(DeleteBehavior.Cascade) 124 | .IsRequired(); 125 | }); 126 | 127 | modelBuilder.Entity("NPay.Modules.Wallets.Core.Wallets.Aggregates.Wallet", b => 128 | { 129 | b.Navigation("Transfers"); 130 | }); 131 | #pragma warning restore 612, 618 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Infrastructure/DAL/Migrations/20211020191405_Wallets_Init.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace NPay.Modules.Wallets.Infrastructure.DAL.Migrations 5 | { 6 | public partial class Wallets_Init : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.EnsureSchema( 11 | name: "wallets"); 12 | 13 | migrationBuilder.CreateTable( 14 | name: "Owners", 15 | schema: "wallets", 16 | columns: table => new 17 | { 18 | Id = table.Column(type: "uuid", nullable: false), 19 | FullName = table.Column(type: "text", nullable: false), 20 | Nationality = table.Column(type: "text", nullable: false), 21 | CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), 22 | VerifiedAt = table.Column(type: "timestamp without time zone", nullable: true), 23 | Version = table.Column(type: "integer", nullable: false) 24 | }, 25 | constraints: table => 26 | { 27 | table.PrimaryKey("PK_Owners", x => x.Id); 28 | }); 29 | 30 | migrationBuilder.CreateTable( 31 | name: "Wallets", 32 | schema: "wallets", 33 | columns: table => new 34 | { 35 | Id = table.Column(type: "uuid", nullable: false), 36 | OwnerId = table.Column(type: "uuid", nullable: false), 37 | Currency = table.Column(type: "text", nullable: false), 38 | CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), 39 | Version = table.Column(type: "integer", nullable: false) 40 | }, 41 | constraints: table => 42 | { 43 | table.PrimaryKey("PK_Wallets", x => x.Id); 44 | table.ForeignKey( 45 | name: "FK_Wallets_Owners_OwnerId", 46 | column: x => x.OwnerId, 47 | principalSchema: "wallets", 48 | principalTable: "Owners", 49 | principalColumn: "Id", 50 | onDelete: ReferentialAction.Cascade); 51 | }); 52 | 53 | migrationBuilder.CreateTable( 54 | name: "Transfers", 55 | schema: "wallets", 56 | columns: table => new 57 | { 58 | Id = table.Column(type: "uuid", nullable: false), 59 | ReferenceId = table.Column(type: "uuid", nullable: true), 60 | WalletId = table.Column(type: "uuid", nullable: false), 61 | Currency = table.Column(type: "text", nullable: false), 62 | Amount = table.Column(type: "numeric", nullable: false), 63 | Direction = table.Column(type: "integer", nullable: false), 64 | CreatedAt = table.Column(type: "timestamp without time zone", nullable: false) 65 | }, 66 | constraints: table => 67 | { 68 | table.PrimaryKey("PK_Transfers", x => x.Id); 69 | table.ForeignKey( 70 | name: "FK_Transfers_Wallets_WalletId", 71 | column: x => x.WalletId, 72 | principalSchema: "wallets", 73 | principalTable: "Wallets", 74 | principalColumn: "Id", 75 | onDelete: ReferentialAction.Cascade); 76 | }); 77 | 78 | migrationBuilder.CreateIndex( 79 | name: "IX_Transfers_WalletId", 80 | schema: "wallets", 81 | table: "Transfers", 82 | column: "WalletId"); 83 | 84 | migrationBuilder.CreateIndex( 85 | name: "IX_Wallets_OwnerId", 86 | schema: "wallets", 87 | table: "Wallets", 88 | column: "OwnerId"); 89 | } 90 | 91 | protected override void Down(MigrationBuilder migrationBuilder) 92 | { 93 | migrationBuilder.DropTable( 94 | name: "Transfers", 95 | schema: "wallets"); 96 | 97 | migrationBuilder.DropTable( 98 | name: "Wallets", 99 | schema: "wallets"); 100 | 101 | migrationBuilder.DropTable( 102 | name: "Owners", 103 | schema: "wallets"); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Infrastructure/DAL/Migrations/WalletsDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using NPay.Modules.Wallets.Infrastructure.DAL; 7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 8 | 9 | namespace NPay.Modules.Wallets.Infrastructure.DAL.Migrations 10 | { 11 | [DbContext(typeof(WalletsDbContext))] 12 | partial class WalletsDbContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasDefaultSchema("wallets") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 63) 20 | .HasAnnotation("ProductVersion", "5.0.11") 21 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); 22 | 23 | modelBuilder.Entity("NPay.Modules.Wallets.Core.Owners.Aggregates.Owner", b => 24 | { 25 | b.Property("Id") 26 | .HasColumnType("uuid"); 27 | 28 | b.Property("CreatedAt") 29 | .HasColumnType("timestamp without time zone"); 30 | 31 | b.Property("FullName") 32 | .IsRequired() 33 | .HasColumnType("text"); 34 | 35 | b.Property("Nationality") 36 | .IsRequired() 37 | .HasColumnType("text"); 38 | 39 | b.Property("VerifiedAt") 40 | .HasColumnType("timestamp without time zone"); 41 | 42 | b.Property("Version") 43 | .HasColumnType("integer"); 44 | 45 | b.HasKey("Id"); 46 | 47 | b.ToTable("Owners"); 48 | }); 49 | 50 | modelBuilder.Entity("NPay.Modules.Wallets.Core.Wallets.Aggregates.Wallet", b => 51 | { 52 | b.Property("Id") 53 | .HasColumnType("uuid"); 54 | 55 | b.Property("CreatedAt") 56 | .HasColumnType("timestamp without time zone"); 57 | 58 | b.Property("Currency") 59 | .IsRequired() 60 | .HasColumnType("text"); 61 | 62 | b.Property("OwnerId") 63 | .HasColumnType("uuid"); 64 | 65 | b.Property("Version") 66 | .IsConcurrencyToken() 67 | .HasColumnType("integer"); 68 | 69 | b.HasKey("Id"); 70 | 71 | b.HasIndex("OwnerId"); 72 | 73 | b.ToTable("Wallets"); 74 | }); 75 | 76 | modelBuilder.Entity("NPay.Modules.Wallets.Core.Wallets.Entities.Transfer", b => 77 | { 78 | b.Property("Id") 79 | .HasColumnType("uuid"); 80 | 81 | b.Property("Amount") 82 | .HasColumnType("numeric"); 83 | 84 | b.Property("CreatedAt") 85 | .HasColumnType("timestamp without time zone"); 86 | 87 | b.Property("Currency") 88 | .IsRequired() 89 | .HasColumnType("text"); 90 | 91 | b.Property("Direction") 92 | .HasColumnType("integer"); 93 | 94 | b.Property("ReferenceId") 95 | .HasColumnType("uuid"); 96 | 97 | b.Property("WalletId") 98 | .HasColumnType("uuid"); 99 | 100 | b.HasKey("Id"); 101 | 102 | b.HasIndex("WalletId"); 103 | 104 | b.ToTable("Transfers"); 105 | }); 106 | 107 | modelBuilder.Entity("NPay.Modules.Wallets.Core.Wallets.Aggregates.Wallet", b => 108 | { 109 | b.HasOne("NPay.Modules.Wallets.Core.Owners.Aggregates.Owner", null) 110 | .WithMany() 111 | .HasForeignKey("OwnerId") 112 | .OnDelete(DeleteBehavior.Cascade) 113 | .IsRequired(); 114 | }); 115 | 116 | modelBuilder.Entity("NPay.Modules.Wallets.Core.Wallets.Entities.Transfer", b => 117 | { 118 | b.HasOne("NPay.Modules.Wallets.Core.Wallets.Aggregates.Wallet", null) 119 | .WithMany("Transfers") 120 | .HasForeignKey("WalletId") 121 | .OnDelete(DeleteBehavior.Cascade) 122 | .IsRequired(); 123 | }); 124 | 125 | modelBuilder.Entity("NPay.Modules.Wallets.Core.Wallets.Aggregates.Wallet", b => 126 | { 127 | b.Navigation("Transfers"); 128 | }); 129 | #pragma warning restore 612, 618 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Infrastructure/DAL/Repositories/OwnerRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.EntityFrameworkCore; 3 | using NPay.Modules.Wallets.Core.Owners.Aggregates; 4 | using NPay.Modules.Wallets.Core.Owners.Repositories; 5 | using NPay.Modules.Wallets.Core.SharedKernel; 6 | 7 | namespace NPay.Modules.Wallets.Infrastructure.DAL.Repositories; 8 | 9 | internal class OwnerRepository : IOwnerRepository 10 | { 11 | private readonly WalletsDbContext _context; 12 | private readonly DbSet _owners; 13 | 14 | public OwnerRepository(WalletsDbContext context) 15 | { 16 | _context = context; 17 | _owners = _context.Owners; 18 | } 19 | 20 | public Task GetAsync(OwnerId id) 21 | => _owners.SingleOrDefaultAsync(x => x.Id.Equals(id)); 22 | 23 | public async Task AddAsync(Owner owner) 24 | { 25 | await _owners.AddAsync(owner); 26 | await _context.SaveChangesAsync(); 27 | } 28 | 29 | public async Task UpdateAsync(Owner owner) 30 | { 31 | _owners.Update(owner); 32 | await _context.SaveChangesAsync(); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Infrastructure/DAL/Repositories/WalletRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.EntityFrameworkCore; 3 | using NPay.Modules.Wallets.Core.SharedKernel; 4 | using NPay.Modules.Wallets.Core.Wallets.Aggregates; 5 | using NPay.Modules.Wallets.Core.Wallets.Repositories; 6 | using NPay.Modules.Wallets.Core.Wallets.ValueObjects; 7 | 8 | namespace NPay.Modules.Wallets.Infrastructure.DAL.Repositories; 9 | 10 | internal class WalletRepository : IWalletRepository 11 | { 12 | private readonly WalletsDbContext _context; 13 | private readonly DbSet _wallets; 14 | 15 | public WalletRepository(WalletsDbContext context) 16 | { 17 | _context = context; 18 | _wallets = _context.Wallets; 19 | } 20 | 21 | public Task GetAsync(WalletId id) 22 | => _wallets 23 | .Include(x => x.Transfers) 24 | .SingleOrDefaultAsync(x => x.Id.Equals(id)); 25 | 26 | public Task GetAsync(OwnerId ownerId, Currency currency) 27 | => _wallets 28 | .Include(x => x.Transfers) 29 | .SingleOrDefaultAsync(x => x.OwnerId.Equals(ownerId) && x.Currency.Value.Equals(currency.Value)); 30 | 31 | public async Task AddAsync(Wallet wallet) 32 | { 33 | await _wallets.AddAsync(wallet); 34 | await _context.SaveChangesAsync(); 35 | } 36 | 37 | public async Task UpdateAsync(Wallet wallet) 38 | { 39 | _wallets.Update(wallet); 40 | await _context.SaveChangesAsync(); 41 | } 42 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Infrastructure/DAL/WalletsDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using NPay.Modules.Wallets.Core.Owners.Aggregates; 3 | using NPay.Modules.Wallets.Core.Wallets.Aggregates; 4 | using NPay.Modules.Wallets.Core.Wallets.Entities; 5 | 6 | namespace NPay.Modules.Wallets.Infrastructure.DAL; 7 | 8 | internal class WalletsDbContext : DbContext 9 | { 10 | public DbSet Owners { get; set; } 11 | public DbSet Transfers { get; set; } 12 | public DbSet Wallets { get; set; } 13 | 14 | public WalletsDbContext(DbContextOptions options) : base(options) 15 | { 16 | } 17 | 18 | protected override void OnModelCreating(ModelBuilder modelBuilder) 19 | { 20 | modelBuilder.HasDefaultSchema("wallets"); 21 | modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Infrastructure/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using NPay.Modules.Wallets.Application.Wallets.Storage; 3 | using NPay.Modules.Wallets.Core.Owners.Repositories; 4 | using NPay.Modules.Wallets.Core.Wallets.Repositories; 5 | using NPay.Modules.Wallets.Infrastructure.DAL; 6 | using NPay.Modules.Wallets.Infrastructure.DAL.Repositories; 7 | using NPay.Modules.Wallets.Infrastructure.Storage; 8 | using NPay.Shared.Database; 9 | 10 | namespace NPay.Modules.Wallets.Infrastructure; 11 | 12 | public static class Extensions 13 | { 14 | public static IServiceCollection AddInfrastructureLayer(this IServiceCollection services) 15 | { 16 | services.AddPostgres(); 17 | services.AddScoped(); 18 | services.AddScoped(); 19 | services.AddScoped(); 20 | 21 | return services; 22 | } 23 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Infrastructure/NPay.Modules.Wallets.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Infrastructure/Storage/WalletStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | using System.Threading.Tasks; 5 | using Microsoft.EntityFrameworkCore; 6 | using NPay.Modules.Wallets.Application.Wallets.Storage; 7 | using NPay.Modules.Wallets.Core.Wallets.Aggregates; 8 | using NPay.Modules.Wallets.Infrastructure.DAL; 9 | 10 | namespace NPay.Modules.Wallets.Infrastructure.Storage; 11 | 12 | internal sealed class WalletStorage : IWalletStorage 13 | { 14 | private readonly DbSet _wallets; 15 | 16 | public WalletStorage(WalletsDbContext dbContext) 17 | { 18 | _wallets = dbContext.Wallets; 19 | } 20 | 21 | public Task FindAsync(Expression> expression) 22 | => _wallets 23 | .AsNoTracking() 24 | .AsQueryable() 25 | .Where(expression) 26 | .Include(x => x.Transfers) 27 | .SingleOrDefaultAsync(); 28 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Shared/DTO/TransferDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NPay.Modules.Wallets.Shared.DTO; 4 | 5 | public class TransferDto 6 | { 7 | public string Direction { get; set; } 8 | public Guid TransferId { get; set; } 9 | public Guid? ReferenceId { get; set; } 10 | public Guid WalletId { get; set; } 11 | public decimal Amount { get; set; } 12 | public string Currency { get; set; } 13 | public DateTime CreatedAt { get; set; } 14 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Shared/DTO/WalletDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace NPay.Modules.Wallets.Shared.DTO; 5 | 6 | public class WalletDto 7 | { 8 | public Guid WalletId { get; set; } 9 | public Guid OwnerId { get; set; } 10 | public string Currency { get; set; } 11 | public DateTime CreatedAt { get; set; } 12 | public decimal Amount { get; set; } 13 | public List Transfers { get; set; } 14 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Shared/Events/FundsAdded.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Shared.Events; 3 | 4 | namespace NPay.Modules.Wallets.Shared.Events; 5 | 6 | public record FundsAdded(Guid WalletId, Guid OwnerId, string Currency, decimal Amount) : IEvent; -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Shared/Events/FundsTransfered.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Shared.Events; 3 | 4 | namespace NPay.Modules.Wallets.Shared.Events; 5 | 6 | public record FundsTransferred(Guid FromWalletId, Guid ToWalletId, decimal Amount, string Currency) : IEvent; -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Shared/Events/OwnerVerified.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Shared.Events; 3 | 4 | namespace NPay.Modules.Wallets.Shared.Events; 5 | 6 | public record OwnerVerified(Guid OwnerId) : IEvent; -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Shared/Events/WalletAdded.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NPay.Shared.Events; 3 | 4 | namespace NPay.Modules.Wallets.Shared.Events; 5 | 6 | public record WalletAdded(Guid WalletId, Guid OwnerId, string Currency) : IEvent; -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Shared/IWalletsModuleApi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using NPay.Modules.Wallets.Shared.DTO; 4 | 5 | namespace NPay.Modules.Wallets.Shared; 6 | 7 | public interface IWalletsModuleApi 8 | { 9 | Task GetWalletAsync(Guid walletId); 10 | } -------------------------------------------------------------------------------- /src/Modules/Wallets/NPay.Modules.Wallets.Shared/NPay.Modules.Wallets.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Modules/Wallets/Wallets.rest: -------------------------------------------------------------------------------- 1 | @url = http://localhost:5000 2 | @email = user1@npay.io 3 | @ownerId = 00000000-0000-0000-0000-000000000000 4 | @fromWalletId = 00000000-0000-0000-0000-000000000000 5 | @toWalletId = 00000000-0000-0000-0000-000000000000 6 | 7 | ### 8 | POST {{url}}/owners 9 | Content-Type: application/json 10 | 11 | { 12 | "email": "{{email}}" 13 | } 14 | 15 | ### 16 | GET {{url}}/wallets/{{fromWalletId}} 17 | 18 | ### 19 | POST {{url}}/wallets 20 | Content-Type: application/json 21 | 22 | { 23 | "ownerId": "{{ownerId}}", 24 | "currency": "EUR" 25 | } 26 | 27 | ### 28 | POST {{url}}/funds 29 | Content-Type: application/json 30 | 31 | { 32 | "walletId": "{{fromWalletId}}", 33 | "amount": 1000 34 | } 35 | 36 | ### 37 | POST {{url}}/funds/transfer 38 | Content-Type: application/json 39 | 40 | { 41 | "fromWalletId": "{{fromWalletId}}", 42 | "toWalletId": "{{toWalletId}}", 43 | "amount": 100 44 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Commands/CommandDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace NPay.Shared.Commands; 7 | 8 | internal sealed class CommandDispatcher : ICommandDispatcher 9 | { 10 | private readonly IServiceProvider _serviceProvider; 11 | 12 | public CommandDispatcher(IServiceProvider serviceProvider) 13 | => _serviceProvider = serviceProvider; 14 | 15 | public async Task SendAsync(TCommand command, CancellationToken cancellationToken = default) where TCommand : class, ICommand 16 | { 17 | if (command is null) 18 | { 19 | return; 20 | } 21 | 22 | using var scope = _serviceProvider.CreateScope(); 23 | var handler = scope.ServiceProvider.GetRequiredService>(); 24 | await handler.HandleAsync(command, cancellationToken); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Commands/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace NPay.Shared.Commands; 5 | 6 | internal static class Extensions 7 | { 8 | public static IServiceCollection AddCommands(this IServiceCollection services) 9 | { 10 | services.AddSingleton(); 11 | services.Scan(s => s.FromAssemblies(AppDomain.CurrentDomain.GetAssemblies()) 12 | .AddClasses(c => c.AssignableTo(typeof(ICommandHandler<>))) 13 | .AsImplementedInterfaces() 14 | .WithScopedLifetime()); 15 | 16 | return services; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Commands/ICommand.cs: -------------------------------------------------------------------------------- 1 | namespace NPay.Shared.Commands; 2 | 3 | //Marker 4 | public interface ICommand 5 | { 6 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Commands/ICommandDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace NPay.Shared.Commands; 5 | 6 | public interface ICommandDispatcher 7 | { 8 | Task SendAsync(TCommand command, CancellationToken cancellationToken = default) where TCommand : class, ICommand; 9 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Commands/ICommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace NPay.Shared.Commands; 5 | 6 | public interface ICommandHandler where TCommand : class, ICommand 7 | { 8 | Task HandleAsync(TCommand command, CancellationToken cancellationToken = default); 9 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Database/DbContextAppInitializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace NPay.Shared.Database; 11 | 12 | internal sealed class DbContextAppInitializer : IHostedService 13 | { 14 | private readonly IServiceProvider _serviceProvider; 15 | private readonly ILogger _logger; 16 | 17 | public DbContextAppInitializer(IServiceProvider serviceProvider, ILogger logger) 18 | { 19 | _serviceProvider = serviceProvider; 20 | _logger = logger; 21 | } 22 | 23 | public async Task StartAsync(CancellationToken cancellationToken) 24 | { 25 | var dbContextTypes = AppDomain.CurrentDomain.GetAssemblies() 26 | .SelectMany(x => x.GetTypes()) 27 | .Where(x => typeof(DbContext).IsAssignableFrom(x) && !x.IsInterface && x != typeof(DbContext)); 28 | 29 | using var scope = _serviceProvider.CreateScope(); 30 | foreach (var dbContextType in dbContextTypes) 31 | { 32 | var dbContext = scope.ServiceProvider.GetService(dbContextType) as DbContext; 33 | if (dbContext is null) 34 | { 35 | continue; 36 | } 37 | 38 | _logger.LogInformation($"Running DB context: {dbContext.GetType().Name}..."); 39 | await dbContext.Database.MigrateAsync(cancellationToken); 40 | } 41 | } 42 | 43 | public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; 44 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Database/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace NPay.Shared.Database; 7 | 8 | public static class Extensions 9 | { 10 | private const string SectionName = "postgres"; 11 | 12 | internal static IServiceCollection AddPostgres(this IServiceCollection services, IConfiguration configuration) 13 | { 14 | services.Configure(configuration.GetSection(SectionName)); 15 | services.AddHostedService(); 16 | 17 | // Temporary fix for EF Core issue related to https://github.com/npgsql/efcore.pg/issues/2000 18 | AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); 19 | 20 | return services; 21 | } 22 | 23 | public static IServiceCollection AddPostgres(this IServiceCollection services) where T : DbContext 24 | { 25 | var configuration = services.BuildServiceProvider().GetRequiredService(); 26 | var connectionString = configuration[$"{SectionName}:{nameof(PostgresOptions.ConnectionString)}"]; 27 | services.AddDbContext(x => x.UseNpgsql(connectionString)); 28 | 29 | return services; 30 | } 31 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Database/PostgresOptions.cs: -------------------------------------------------------------------------------- 1 | namespace NPay.Shared.Database; 2 | 3 | internal sealed class PostgresOptions 4 | { 5 | public string ConnectionString { get; set; } 6 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Dispatchers/IDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NPay.Shared.Commands; 4 | using NPay.Shared.Events; 5 | using NPay.Shared.Queries; 6 | 7 | namespace NPay.Shared.Dispatchers; 8 | 9 | public interface IDispatcher 10 | { 11 | Task SendAsync(T command, CancellationToken cancellationToken = default) where T : class, ICommand; 12 | Task PublishAsync(T @event, CancellationToken cancellationToken = default) where T : class, IEvent; 13 | Task QueryAsync(IQuery query, CancellationToken cancellationToken = default); 14 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Dispatchers/InMemoryDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NPay.Shared.Commands; 4 | using NPay.Shared.Events; 5 | using NPay.Shared.Queries; 6 | 7 | namespace NPay.Shared.Dispatchers; 8 | 9 | internal sealed class InMemoryDispatcher : IDispatcher 10 | { 11 | private readonly ICommandDispatcher _commandDispatcher; 12 | private readonly IEventDispatcher _eventDispatcher; 13 | private readonly IQueryDispatcher _queryDispatcher; 14 | 15 | public InMemoryDispatcher(ICommandDispatcher commandDispatcher, IEventDispatcher eventDispatcher, 16 | IQueryDispatcher queryDispatcher) 17 | { 18 | _commandDispatcher = commandDispatcher; 19 | _eventDispatcher = eventDispatcher; 20 | _queryDispatcher = queryDispatcher; 21 | } 22 | 23 | public Task SendAsync(T command, CancellationToken cancellationToken = default) where T : class, ICommand 24 | => _commandDispatcher.SendAsync(command, cancellationToken); 25 | 26 | public Task PublishAsync(T @event, CancellationToken cancellationToken = default) where T : class, IEvent 27 | => _eventDispatcher.PublishAsync(@event, cancellationToken); 28 | 29 | public Task QueryAsync(IQuery query, CancellationToken cancellationToken = default) 30 | => _queryDispatcher.QueryAsync(query, cancellationToken); 31 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Events/EventDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace NPay.Shared.Events; 8 | 9 | internal sealed class EventDispatcher : IEventDispatcher 10 | { 11 | private readonly IServiceProvider _serviceProvider; 12 | 13 | public EventDispatcher(IServiceProvider serviceProvider) 14 | => _serviceProvider = serviceProvider; 15 | 16 | public async Task PublishAsync(TEvent @event, CancellationToken cancellationToken = default) 17 | where TEvent : class, IEvent 18 | { 19 | if (typeof(TEvent) == typeof(IEvent)) 20 | { 21 | await DispatchDynamicallyAsync(@event, cancellationToken); 22 | return; 23 | } 24 | 25 | using var scope = _serviceProvider.CreateScope(); 26 | var handlers = scope.ServiceProvider.GetServices>(); 27 | var tasks = handlers.Select(x => x.HandleAsync(@event, cancellationToken)); 28 | await Task.WhenAll(tasks); 29 | } 30 | 31 | private async Task DispatchDynamicallyAsync(IEvent @event, CancellationToken cancellationToken = default) 32 | { 33 | using var scope = _serviceProvider.CreateScope(); 34 | var handlerType = typeof(IEventHandler<>).MakeGenericType(@event.GetType()); 35 | var handlers = scope.ServiceProvider.GetServices(handlerType); 36 | var method = handlerType.GetMethod(nameof(IEventHandler.HandleAsync)); 37 | if (method is null) 38 | { 39 | throw new InvalidOperationException($"Event handler for '{@event.GetType().Name}' is invalid."); 40 | } 41 | 42 | var tasks = handlers.Select(x => (Task)method.Invoke(x, new object[] { @event, cancellationToken })); 43 | await Task.WhenAll(tasks); 44 | } 45 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Events/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace NPay.Shared.Events; 5 | 6 | internal static class Extensions 7 | { 8 | public static IServiceCollection AddEvents(this IServiceCollection services) 9 | { 10 | services.AddSingleton(); 11 | services.Scan(s => s.FromAssemblies(AppDomain.CurrentDomain.GetAssemblies()) 12 | .AddClasses(c => c.AssignableTo(typeof(IEventHandler<>))) 13 | .AsImplementedInterfaces() 14 | .WithScopedLifetime()); 15 | 16 | return services; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Events/IEvent.cs: -------------------------------------------------------------------------------- 1 | namespace NPay.Shared.Events; 2 | 3 | // Marker 4 | public interface IEvent 5 | { 6 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Events/IEventDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace NPay.Shared.Events; 5 | 6 | public interface IEventDispatcher 7 | { 8 | // Task PublishAsync(IEvent @event, CancellationToken cancellationToken = default); 9 | 10 | Task PublishAsync(TEvent @event, CancellationToken cancellationToken = default) 11 | where TEvent : class, IEvent; 12 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Events/IEventHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace NPay.Shared.Events; 5 | 6 | public interface IEventHandler where TEvent : class, IEvent 7 | { 8 | Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default); 9 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Exceptions/ErrorHandlerMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace NPay.Shared.Exceptions; 8 | 9 | internal sealed class ErrorHandlerMiddleware : IMiddleware 10 | { 11 | private readonly IExceptionToResponseMapper _exceptionToResponseMapper; 12 | private readonly ILogger _logger; 13 | 14 | public ErrorHandlerMiddleware(IExceptionToResponseMapper exceptionToResponseMapper, 15 | ILogger logger) 16 | { 17 | _exceptionToResponseMapper = exceptionToResponseMapper; 18 | _logger = logger; 19 | } 20 | 21 | public async Task InvokeAsync(HttpContext context, RequestDelegate next) 22 | { 23 | try 24 | { 25 | await next(context); 26 | } 27 | catch (Exception exception) 28 | { 29 | _logger.LogError(exception, exception.Message); 30 | await HandleErrorAsync(context, exception); 31 | } 32 | } 33 | 34 | private async Task HandleErrorAsync(HttpContext context, Exception exception) 35 | { 36 | var errorResponse = _exceptionToResponseMapper.Map(exception); 37 | context.Response.StatusCode = (int) (errorResponse?.StatusCode ?? HttpStatusCode.InternalServerError); 38 | var response = errorResponse?.Response; 39 | if (response is null) 40 | { 41 | return; 42 | } 43 | 44 | await context.Response.WriteAsJsonAsync(response); 45 | } 46 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Exceptions/ExceptionResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace NPay.Shared.Exceptions; 4 | 5 | public sealed record ExceptionResponse(object Response, HttpStatusCode StatusCode); -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Exceptions/ExceptionToResponseMapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Net; 4 | using Humanizer; 5 | 6 | namespace NPay.Shared.Exceptions; 7 | 8 | internal sealed class ExceptionToResponseMapper : IExceptionToResponseMapper 9 | { 10 | private static readonly ConcurrentDictionary Codes = new(); 11 | 12 | public ExceptionResponse Map(Exception exception) 13 | => exception switch 14 | { 15 | NPayException ex => new ExceptionResponse(new ErrorsResponse(new Error(GetErrorCode(ex), ex.Message)) 16 | , HttpStatusCode.BadRequest), 17 | _ => new ExceptionResponse(new ErrorsResponse(new Error("error", "There was an error.")), 18 | HttpStatusCode.InternalServerError) 19 | }; 20 | 21 | private record Error(string Code, string Message); 22 | 23 | private record ErrorsResponse(params Error[] Errors); 24 | 25 | private static string GetErrorCode(object exception) 26 | { 27 | var type = exception.GetType(); 28 | return Codes.GetOrAdd(type, type.Name.Underscore().Replace("_exception", string.Empty)); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Exceptions/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace NPay.Shared.Exceptions; 5 | 6 | internal static class Extensions 7 | { 8 | public static IServiceCollection AddErrorHandling(this IServiceCollection services) 9 | => services 10 | .AddScoped() 11 | .AddSingleton(); 12 | 13 | public static IApplicationBuilder UseErrorHandling(this IApplicationBuilder app) 14 | => app.UseMiddleware(); 15 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Exceptions/IExceptionToResponseMapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NPay.Shared.Exceptions; 4 | 5 | internal interface IExceptionToResponseMapper 6 | { 7 | ExceptionResponse Map(Exception exception); 8 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Exceptions/NPayException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NPay.Shared.Exceptions; 4 | 5 | public abstract class NPayException : Exception 6 | { 7 | protected NPayException(string message) : base(message) 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.OpenApi.Models; 6 | using NPay.Shared.Commands; 7 | using NPay.Shared.Database; 8 | using NPay.Shared.Dispatchers; 9 | using NPay.Shared.Events; 10 | using NPay.Shared.Exceptions; 11 | using NPay.Shared.Messaging; 12 | using NPay.Shared.Queries; 13 | using NPay.Shared.Time; 14 | 15 | namespace NPay.Shared; 16 | 17 | public static class Extensions 18 | { 19 | private const string ApiTitle = "NPay API"; 20 | private const string ApiVersion = "v1"; 21 | 22 | public static IServiceCollection AddSharedFramework(this IServiceCollection services, IConfiguration configuration) 23 | { 24 | services.AddErrorHandling(); 25 | services.AddCommands(); 26 | services.AddEvents(); 27 | services.AddQueries(); 28 | services.AddMessaging(); 29 | services.AddPostgres(configuration); 30 | services.AddControllers(); 31 | services.AddSingleton(); 32 | services.AddSingleton(); 33 | services.AddSingleton(); 34 | services.AddSwaggerGen(swagger => 35 | { 36 | swagger.EnableAnnotations(); 37 | swagger.CustomSchemaIds(x => x.FullName); 38 | swagger.SwaggerDoc(ApiVersion, new OpenApiInfo 39 | { 40 | Title = ApiTitle, 41 | Version = ApiVersion 42 | }); 43 | }); 44 | 45 | return services; 46 | } 47 | 48 | public static IApplicationBuilder UseSharedFramework(this IApplicationBuilder app) 49 | { 50 | app.UseErrorHandling(); 51 | app.UseSwagger(); 52 | app.UseReDoc(reDoc => 53 | { 54 | reDoc.RoutePrefix = "docs"; 55 | reDoc.SpecUrl($"/swagger/{ApiVersion}/swagger.json"); 56 | reDoc.DocumentTitle = ApiTitle; 57 | }); 58 | app.UseRouting(); 59 | 60 | return app; 61 | } 62 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Messaging/AsyncEventDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NPay.Shared.Events; 4 | 5 | namespace NPay.Shared.Messaging; 6 | 7 | internal sealed class AsyncEventDispatcher : IAsyncEventDispatcher 8 | { 9 | private readonly IEventChannel _channel; 10 | 11 | public AsyncEventDispatcher(IEventChannel channel) 12 | { 13 | _channel = channel; 14 | } 15 | 16 | public async Task PublishAsync(TEvent @event, CancellationToken cancellationToken = default) 17 | where TEvent : class, IEvent 18 | { 19 | await _channel.Writer.WriteAsync(@event, cancellationToken); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Messaging/EventChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Channels; 2 | using NPay.Shared.Events; 3 | 4 | namespace NPay.Shared.Messaging; 5 | 6 | internal sealed class EventChannel : IEventChannel 7 | { 8 | private readonly Channel _messages = Channel.CreateUnbounded(); 9 | 10 | public ChannelReader Reader => _messages.Reader; 11 | public ChannelWriter Writer => _messages.Writer; 12 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Messaging/EventDispatcherJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Hosting; 5 | using Microsoft.Extensions.Logging; 6 | using NPay.Shared.Events; 7 | 8 | namespace NPay.Shared.Messaging; 9 | 10 | internal sealed class EventDispatcherJob : BackgroundService 11 | { 12 | private readonly IEventChannel _eventChannel; 13 | private readonly IEventDispatcher _eventDispatcher; 14 | private readonly ILogger _logger; 15 | 16 | public EventDispatcherJob(IEventChannel eventChannel, IEventDispatcher eventDispatcher, 17 | ILogger logger) 18 | { 19 | _eventChannel = eventChannel; 20 | _eventDispatcher = eventDispatcher; 21 | _logger = logger; 22 | } 23 | 24 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 25 | { 26 | await foreach (var @event in _eventChannel.Reader.ReadAllAsync(stoppingToken)) 27 | { 28 | try 29 | { 30 | await _eventDispatcher.PublishAsync(@event, stoppingToken); 31 | } 32 | catch (Exception exception) 33 | { 34 | _logger.LogError(exception, exception.Message); 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Messaging/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace NPay.Shared.Messaging; 4 | 5 | internal static class Extensions 6 | { 7 | private const string SectionName = "messaging"; 8 | 9 | public static IServiceCollection AddMessaging(this IServiceCollection services) 10 | { 11 | services.AddTransient(); 12 | services.AddTransient(); 13 | services.AddSingleton(); 14 | services.AddHostedService(); 15 | 16 | return services; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Messaging/IAsyncEventDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NPay.Shared.Events; 4 | 5 | namespace NPay.Shared.Messaging; 6 | 7 | internal interface IAsyncEventDispatcher 8 | { 9 | Task PublishAsync(TEvent @event, CancellationToken cancellationToken = default) 10 | where TEvent : class, IEvent; 11 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Messaging/IEventChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Channels; 2 | using NPay.Shared.Events; 3 | 4 | namespace NPay.Shared.Messaging; 5 | 6 | internal interface IEventChannel 7 | { 8 | ChannelReader Reader { get; } 9 | ChannelWriter Writer { get; } 10 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Messaging/IMessageBroker.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NPay.Shared.Events; 4 | 5 | namespace NPay.Shared.Messaging; 6 | 7 | public interface IMessageBroker 8 | { 9 | Task PublishAsync(IEvent @event, CancellationToken cancellationToken = default); 10 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Messaging/InMemoryMessageBroker.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Humanizer; 4 | using Microsoft.Extensions.Logging; 5 | using NPay.Shared.Events; 6 | 7 | namespace NPay.Shared.Messaging; 8 | 9 | internal sealed class InMemoryMessageBroker : IMessageBroker 10 | { 11 | private readonly IAsyncEventDispatcher _asyncEventDispatcher; 12 | private readonly ILogger _logger; 13 | 14 | public InMemoryMessageBroker(IAsyncEventDispatcher asyncEventDispatcher, ILogger logger) 15 | { 16 | _asyncEventDispatcher = asyncEventDispatcher; 17 | _logger = logger; 18 | } 19 | 20 | public async Task PublishAsync(IEvent @event, CancellationToken cancellationToken = default) 21 | { 22 | var name = @event.GetType().Name.Underscore(); 23 | _logger.LogInformation("Publishing an event: {Name}...", name); 24 | await _asyncEventDispatcher.PublishAsync(@event, cancellationToken); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/NPay.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Queries/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace NPay.Shared.Queries; 5 | 6 | internal static class Extensions 7 | { 8 | public static IServiceCollection AddQueries(this IServiceCollection services) 9 | { 10 | services.AddSingleton(); 11 | services.Scan(s => s.FromAssemblies(AppDomain.CurrentDomain.GetAssemblies()) 12 | .AddClasses(c => c.AssignableTo(typeof(IQueryHandler<,>))) 13 | .AsImplementedInterfaces() 14 | .WithScopedLifetime()); 15 | 16 | return services; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Queries/IQuery.cs: -------------------------------------------------------------------------------- 1 | namespace NPay.Shared.Queries; 2 | 3 | //Marker 4 | public interface IQuery 5 | { 6 | } 7 | 8 | public interface IQuery : IQuery 9 | { 10 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Queries/IQueryDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace NPay.Shared.Queries; 5 | 6 | public interface IQueryDispatcher 7 | { 8 | Task QueryAsync(IQuery query, CancellationToken cancellationToken = default); 9 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Queries/IQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace NPay.Shared.Queries; 5 | 6 | public interface IQueryHandler where TQuery : class, IQuery 7 | { 8 | Task HandleAsync(TQuery query, CancellationToken cancellationToken = default); 9 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Queries/QueryDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace NPay.Shared.Queries; 7 | 8 | internal sealed class QueryDispatcher : IQueryDispatcher 9 | { 10 | private readonly IServiceProvider _serviceProvider; 11 | 12 | public QueryDispatcher(IServiceProvider serviceProvider) 13 | => _serviceProvider = serviceProvider; 14 | 15 | public async Task QueryAsync(IQuery query, CancellationToken cancellationToken = default) 16 | { 17 | using var scope = _serviceProvider.CreateScope(); 18 | var handlerType = typeof(IQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult)); 19 | var handler = scope.ServiceProvider.GetRequiredService(handlerType); 20 | var method = handlerType.GetMethod(nameof(IQueryHandler, TResult>.HandleAsync)); 21 | if (method is null) 22 | { 23 | throw new InvalidOperationException($"Query handler for '{typeof(TResult).Name}' is invalid."); 24 | } 25 | 26 | // ReSharper disable once PossibleNullReferenceException 27 | return await (Task)method.Invoke(handler, new object[] {query, cancellationToken}); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Time/IClock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NPay.Shared.Time; 4 | 5 | public interface IClock 6 | { 7 | DateTime CurrentDate(); 8 | } -------------------------------------------------------------------------------- /src/Shared/NPay.Shared/Time/UtcClock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NPay.Shared.Time; 4 | 5 | internal sealed class UtcClock : IClock 6 | { 7 | public DateTime CurrentDate() => DateTime.UtcNow; 8 | } --------------------------------------------------------------------------------