diff --git a/.gitignore b/.gitignore index 29eb907..f67dd76 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,88 @@ -bin/ -obj/ -/packages/ +# Build results +[Bb]in/ +[Oo]bj/ +[Dd]ebug/ +[Rr]elease/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ + +# .NET / NuGet +*.nupkg +*.snupkg +*.nuspec +packages/ +project.lock.json +project.assets.json +artifacts/ +TestResults/ +coverage/ +coverage.* +*.coverage +*.coveragexml + +# User-specific files +*.user +*.rsuser +*.suo +*.userosscache +*.sln.docstates +*.DotSettings.user +*.csproj.user + +# Visual Studio +.vs/ +*.vssscc +*.vspscc +_ReSharper*/ +_ReSharper.Caches/ +*.resharper + +# Rider / JetBrains +.idea/ +*.sln.iml +*.iml riderModule.iml -/_ReSharper.Caches/ -appsettings.Development.json -appsettings.json \ No newline at end of file + +# VS Code +.vscode/* +!.vscode/extensions.json +!.vscode/settings.example.json +!.vscode/launch.example.json +!.vscode/tasks.example.json + +# OS files +.DS_Store +Thumbs.db +ehthumbs.db +Desktop.ini + +# Logs +*.log +logs/ +Log/ + +# Environment and secrets +.env +.env.* +!.env.example +secrets.json +appsettings.json +appsettings.*.json +!appsettings.example.json +!appsettings.*.example.json + +# Local databases +*.db +*.sqlite +*.sqlite3 +*.mdf +*.ldf + +# Docker local overrides +docker-compose.override.yml + +# Publish output +publish/ +wwwroot/dist/ diff --git a/.idea/.idea.Reservation_System/.idea/.gitignore b/.idea/.idea.Reservation_System/.idea/.gitignore deleted file mode 100644 index a33dc03..0000000 --- a/.idea/.idea.Reservation_System/.idea/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Rider ignored files -/projectSettingsUpdater.xml -/modules.xml -/.idea.Reservation_System.iml -/contentModel.xml -# Ignored default folder with query files -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/.idea.Reservation_System/.idea/.name b/.idea/.idea.Reservation_System/.idea/.name deleted file mode 100644 index 4317306..0000000 --- a/.idea/.idea.Reservation_System/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -Reservation_System \ No newline at end of file diff --git a/.idea/.idea.Reservation_System/.idea/indexLayout.xml b/.idea/.idea.Reservation_System/.idea/indexLayout.xml deleted file mode 100644 index 7b08163..0000000 --- a/.idea/.idea.Reservation_System/.idea/indexLayout.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/.idea.Reservation_System/.idea/vcs.xml b/.idea/.idea.Reservation_System/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/.idea.Reservation_System/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/config/applicationhost.config b/.idea/config/applicationhost.config deleted file mode 100644 index 92370fd..0000000 --- a/.idea/config/applicationhost.config +++ /dev/null @@ -1,987 +0,0 @@ - - - - - - -
-
-
-
-
-
-
-
- - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
- -
-
-
-
-
-
- -
-
-
-
-
- -
-
-
- -
-
- -
-
- -
-
-
- - -
-
-
-
-
-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.vs/ProjectEvaluation/reservation_system.metadata.v9.bin b/.vs/ProjectEvaluation/reservation_system.metadata.v9.bin deleted file mode 100644 index 3d5ab46..0000000 Binary files a/.vs/ProjectEvaluation/reservation_system.metadata.v9.bin and /dev/null differ diff --git a/.vs/ProjectEvaluation/reservation_system.projects.v9.bin b/.vs/ProjectEvaluation/reservation_system.projects.v9.bin deleted file mode 100644 index b1e77a3..0000000 Binary files a/.vs/ProjectEvaluation/reservation_system.projects.v9.bin and /dev/null differ diff --git a/.vs/ProjectEvaluation/reservation_system.strings.v9.bin b/.vs/ProjectEvaluation/reservation_system.strings.v9.bin deleted file mode 100644 index 6a0aead..0000000 Binary files a/.vs/ProjectEvaluation/reservation_system.strings.v9.bin and /dev/null differ diff --git a/.vs/Reservation_System/DesignTimeBuild/.dtbcache.v2 b/.vs/Reservation_System/DesignTimeBuild/.dtbcache.v2 deleted file mode 100644 index 1e05e56..0000000 Binary files a/.vs/Reservation_System/DesignTimeBuild/.dtbcache.v2 and /dev/null differ diff --git a/.vs/Reservation_System/FileContentIndex/ae539560-ce46-4966-9da4-453ff7ce7541.vsidx b/.vs/Reservation_System/FileContentIndex/ae539560-ce46-4966-9da4-453ff7ce7541.vsidx deleted file mode 100644 index 8b25003..0000000 Binary files a/.vs/Reservation_System/FileContentIndex/ae539560-ce46-4966-9da4-453ff7ce7541.vsidx and /dev/null differ diff --git a/.vs/Reservation_System/v17/.futdcache.v2 b/.vs/Reservation_System/v17/.futdcache.v2 deleted file mode 100644 index b924ebf..0000000 Binary files a/.vs/Reservation_System/v17/.futdcache.v2 and /dev/null differ diff --git a/.vs/Reservation_System/v17/.suo b/.vs/Reservation_System/v17/.suo deleted file mode 100644 index c6e0ba6..0000000 Binary files a/.vs/Reservation_System/v17/.suo and /dev/null differ diff --git a/.vs/Reservation_System/v17/DocumentLayout.backup.json b/.vs/Reservation_System/v17/DocumentLayout.backup.json deleted file mode 100644 index bb1b617..0000000 --- a/.vs/Reservation_System/v17/DocumentLayout.backup.json +++ /dev/null @@ -1,173 +0,0 @@ -{ - "Version": 1, - "WorkspaceRootPath": "E:\\Code\\c#\\Reservation_System\\", - "Documents": [ - { - "AbsoluteMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|e:\\code\\c#\\reservation_system\\application\\services\\passwordhasherservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|solutionrelative:application\\services\\passwordhasherservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" - }, - { - "AbsoluteMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|e:\\code\\c#\\reservation_system\\application\\application.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}|", - "RelativeMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|solutionrelative:application\\application.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}|" - }, - { - "AbsoluteMoniker": "D:0:0:{50003437-236E-486B-87C3-F06DAFE51795}|api\\api.csproj|e:\\code\\c#\\reservation_system\\api\\api.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}|", - "RelativeMoniker": "D:0:0:{50003437-236E-486B-87C3-F06DAFE51795}|api\\api.csproj|solutionrelative:api\\api.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}|" - }, - { - "AbsoluteMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|e:\\code\\c#\\reservation_system\\application\\interfaces\\services\\iuserservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|solutionrelative:application\\interfaces\\services\\iuserservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" - }, - { - "AbsoluteMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|e:\\code\\c#\\reservation_system\\application\\services\\appointmentservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|solutionrelative:application\\services\\appointmentservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" - }, - { - "AbsoluteMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|e:\\code\\c#\\reservation_system\\application\\services\\userservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|solutionrelative:application\\services\\userservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" - }, - { - "AbsoluteMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|e:\\code\\c#\\reservation_system\\application\\services\\serviceappservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|solutionrelative:application\\services\\serviceappservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" - }, - { - "AbsoluteMoniker": "D:0:0:{99CD1C44-45C7-476B-8C80-0A823954BA56}|Domain\\Domain.csproj|e:\\code\\c#\\reservation_system\\domain\\service.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{99CD1C44-45C7-476B-8C80-0A823954BA56}|Domain\\Domain.csproj|solutionrelative:domain\\service.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" - }, - { - "AbsoluteMoniker": "D:0:0:{99CD1C44-45C7-476B-8C80-0A823954BA56}|Domain\\Domain.csproj|e:\\code\\c#\\reservation_system\\domain\\domain.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}|", - "RelativeMoniker": "D:0:0:{99CD1C44-45C7-476B-8C80-0A823954BA56}|Domain\\Domain.csproj|solutionrelative:domain\\domain.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}|" - } - ], - "DocumentGroupContainers": [ - { - "Orientation": 0, - "VerticalTabListWidth": 256, - "DocumentGroups": [ - { - "DockedWidth": 200, - "SelectedChildIndex": 4, - "Children": [ - { - "$type": "Document", - "DocumentIndex": 2, - "Title": "api.csproj", - "DocumentMoniker": "E:\\Code\\c#\\Reservation_System\\api\\api.csproj", - "RelativeDocumentMoniker": "api\\api.csproj", - "ToolTip": "E:\\Code\\c#\\Reservation_System\\api\\api.csproj", - "RelativeToolTip": "api\\api.csproj", - "ViewState": "AgIAAAEAAAAAAAAAAABBwBkAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000758|", - "WhenOpened": "2026-02-04T12:40:09.071Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 3, - "Title": "IUserService.cs", - "DocumentMoniker": "E:\\Code\\c#\\Reservation_System\\Application\\Interfaces\\Services\\IUserService.cs", - "RelativeDocumentMoniker": "Application\\Interfaces\\Services\\IUserService.cs", - "ToolTip": "E:\\Code\\c#\\Reservation_System\\Application\\Interfaces\\Services\\IUserService.cs", - "RelativeToolTip": "Application\\Interfaces\\Services\\IUserService.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2026-02-04T12:38:00.905Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 4, - "Title": "AppointmentService.cs", - "DocumentMoniker": "E:\\Code\\c#\\Reservation_System\\Application\\Services\\AppointmentService.cs", - "RelativeDocumentMoniker": "Application\\Services\\AppointmentService.cs", - "ToolTip": "E:\\Code\\c#\\Reservation_System\\Application\\Services\\AppointmentService.cs", - "RelativeToolTip": "Application\\Services\\AppointmentService.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2026-02-04T12:37:56.288Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 5, - "Title": "UserService.cs", - "DocumentMoniker": "E:\\Code\\c#\\Reservation_System\\Application\\Services\\UserService.cs", - "RelativeDocumentMoniker": "Application\\Services\\UserService.cs", - "ToolTip": "E:\\Code\\c#\\Reservation_System\\Application\\Services\\UserService.cs", - "RelativeToolTip": "Application\\Services\\UserService.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2026-02-04T12:37:54.491Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 0, - "Title": "PasswordHasherService.cs", - "DocumentMoniker": "E:\\Code\\c#\\Reservation_System\\Application\\Services\\PasswordHasherService.cs", - "RelativeDocumentMoniker": "Application\\Services\\PasswordHasherService.cs", - "ToolTip": "E:\\Code\\c#\\Reservation_System\\Application\\Services\\PasswordHasherService.cs*", - "RelativeToolTip": "Application\\Services\\PasswordHasherService.cs*", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2026-02-04T12:37:40.758Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 7, - "Title": "Service.cs", - "DocumentMoniker": "E:\\Code\\c#\\Reservation_System\\Domain\\Service.cs", - "RelativeDocumentMoniker": "Domain\\Service.cs", - "ToolTip": "E:\\Code\\c#\\Reservation_System\\Domain\\Service.cs", - "RelativeToolTip": "Domain\\Service.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2026-02-04T12:37:25.567Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 6, - "Title": "ServiceAppService.cs", - "DocumentMoniker": "E:\\Code\\c#\\Reservation_System\\Application\\Services\\ServiceAppService.cs", - "RelativeDocumentMoniker": "Application\\Services\\ServiceAppService.cs", - "ToolTip": "E:\\Code\\c#\\Reservation_System\\Application\\Services\\ServiceAppService.cs", - "RelativeToolTip": "Application\\Services\\ServiceAppService.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAACEAAAAbAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2026-02-04T12:33:48.359Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 8, - "Title": "Domain.csproj", - "DocumentMoniker": "E:\\Code\\c#\\Reservation_System\\Domain\\Domain.csproj", - "RelativeDocumentMoniker": "Domain\\Domain.csproj", - "ToolTip": "E:\\Code\\c#\\Reservation_System\\Domain\\Domain.csproj", - "RelativeToolTip": "Domain\\Domain.csproj", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000758|", - "WhenOpened": "2026-02-04T12:33:00.372Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 1, - "Title": "Application.csproj", - "DocumentMoniker": "E:\\Code\\c#\\Reservation_System\\Application\\Application.csproj", - "RelativeDocumentMoniker": "Application\\Application.csproj", - "ToolTip": "E:\\Code\\c#\\Reservation_System\\Application\\Application.csproj", - "RelativeToolTip": "Application\\Application.csproj", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAA0AAAAFAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000758|", - "WhenOpened": "2026-02-04T12:32:48.335Z", - "EditorCaption": "" - } - ] - } - ] - } - ] -} \ No newline at end of file diff --git a/.vs/Reservation_System/v17/DocumentLayout.json b/.vs/Reservation_System/v17/DocumentLayout.json deleted file mode 100644 index 6ddb446..0000000 --- a/.vs/Reservation_System/v17/DocumentLayout.json +++ /dev/null @@ -1,173 +0,0 @@ -{ - "Version": 1, - "WorkspaceRootPath": "E:\\Code\\c#\\Reservation_System\\", - "Documents": [ - { - "AbsoluteMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|e:\\code\\c#\\reservation_system\\application\\services\\passwordhasherservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|solutionrelative:application\\services\\passwordhasherservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" - }, - { - "AbsoluteMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|e:\\code\\c#\\reservation_system\\application\\application.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}|", - "RelativeMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|solutionrelative:application\\application.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}|" - }, - { - "AbsoluteMoniker": "D:0:0:{50003437-236E-486B-87C3-F06DAFE51795}|api\\api.csproj|e:\\code\\c#\\reservation_system\\api\\api.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}|", - "RelativeMoniker": "D:0:0:{50003437-236E-486B-87C3-F06DAFE51795}|api\\api.csproj|solutionrelative:api\\api.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}|" - }, - { - "AbsoluteMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|e:\\code\\c#\\reservation_system\\application\\interfaces\\services\\iuserservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|solutionrelative:application\\interfaces\\services\\iuserservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" - }, - { - "AbsoluteMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|e:\\code\\c#\\reservation_system\\application\\services\\appointmentservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|solutionrelative:application\\services\\appointmentservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" - }, - { - "AbsoluteMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|e:\\code\\c#\\reservation_system\\application\\services\\userservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|solutionrelative:application\\services\\userservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" - }, - { - "AbsoluteMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|e:\\code\\c#\\reservation_system\\application\\services\\serviceappservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{0BDB1E54-9822-4261-AF10-BE438DD36BDF}|Application\\Application.csproj|solutionrelative:application\\services\\serviceappservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" - }, - { - "AbsoluteMoniker": "D:0:0:{99CD1C44-45C7-476B-8C80-0A823954BA56}|Domain\\Domain.csproj|e:\\code\\c#\\reservation_system\\domain\\service.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{99CD1C44-45C7-476B-8C80-0A823954BA56}|Domain\\Domain.csproj|solutionrelative:domain\\service.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" - }, - { - "AbsoluteMoniker": "D:0:0:{99CD1C44-45C7-476B-8C80-0A823954BA56}|Domain\\Domain.csproj|e:\\code\\c#\\reservation_system\\domain\\domain.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}|", - "RelativeMoniker": "D:0:0:{99CD1C44-45C7-476B-8C80-0A823954BA56}|Domain\\Domain.csproj|solutionrelative:domain\\domain.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}|" - } - ], - "DocumentGroupContainers": [ - { - "Orientation": 0, - "VerticalTabListWidth": 256, - "DocumentGroups": [ - { - "DockedWidth": 200, - "SelectedChildIndex": 4, - "Children": [ - { - "$type": "Document", - "DocumentIndex": 2, - "Title": "api.csproj", - "DocumentMoniker": "E:\\Code\\c#\\Reservation_System\\api\\api.csproj", - "RelativeDocumentMoniker": "api\\api.csproj", - "ToolTip": "E:\\Code\\c#\\Reservation_System\\api\\api.csproj", - "RelativeToolTip": "api\\api.csproj", - "ViewState": "AgIAAAEAAAAAAAAAAABBwBkAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000758|", - "WhenOpened": "2026-02-04T12:40:09.071Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 3, - "Title": "IUserService.cs", - "DocumentMoniker": "E:\\Code\\c#\\Reservation_System\\Application\\Interfaces\\Services\\IUserService.cs", - "RelativeDocumentMoniker": "Application\\Interfaces\\Services\\IUserService.cs", - "ToolTip": "E:\\Code\\c#\\Reservation_System\\Application\\Interfaces\\Services\\IUserService.cs", - "RelativeToolTip": "Application\\Interfaces\\Services\\IUserService.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2026-02-04T12:38:00.905Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 4, - "Title": "AppointmentService.cs", - "DocumentMoniker": "E:\\Code\\c#\\Reservation_System\\Application\\Services\\AppointmentService.cs", - "RelativeDocumentMoniker": "Application\\Services\\AppointmentService.cs", - "ToolTip": "E:\\Code\\c#\\Reservation_System\\Application\\Services\\AppointmentService.cs", - "RelativeToolTip": "Application\\Services\\AppointmentService.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2026-02-04T12:37:56.288Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 5, - "Title": "UserService.cs", - "DocumentMoniker": "E:\\Code\\c#\\Reservation_System\\Application\\Services\\UserService.cs", - "RelativeDocumentMoniker": "Application\\Services\\UserService.cs", - "ToolTip": "E:\\Code\\c#\\Reservation_System\\Application\\Services\\UserService.cs", - "RelativeToolTip": "Application\\Services\\UserService.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2026-02-04T12:37:54.491Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 0, - "Title": "PasswordHasherService.cs", - "DocumentMoniker": "E:\\Code\\c#\\Reservation_System\\Application\\Services\\PasswordHasherService.cs", - "RelativeDocumentMoniker": "Application\\Services\\PasswordHasherService.cs", - "ToolTip": "E:\\Code\\c#\\Reservation_System\\Application\\Services\\PasswordHasherService.cs", - "RelativeToolTip": "Application\\Services\\PasswordHasherService.cs", - "ViewState": "AgIAAAMAAAAAAAAAAAAqwBMAAAAcAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2026-02-04T12:37:40.758Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 7, - "Title": "Service.cs", - "DocumentMoniker": "E:\\Code\\c#\\Reservation_System\\Domain\\Service.cs", - "RelativeDocumentMoniker": "Domain\\Service.cs", - "ToolTip": "E:\\Code\\c#\\Reservation_System\\Domain\\Service.cs", - "RelativeToolTip": "Domain\\Service.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2026-02-04T12:37:25.567Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 6, - "Title": "ServiceAppService.cs", - "DocumentMoniker": "E:\\Code\\c#\\Reservation_System\\Application\\Services\\ServiceAppService.cs", - "RelativeDocumentMoniker": "Application\\Services\\ServiceAppService.cs", - "ToolTip": "E:\\Code\\c#\\Reservation_System\\Application\\Services\\ServiceAppService.cs", - "RelativeToolTip": "Application\\Services\\ServiceAppService.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAACEAAAAbAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2026-02-04T12:33:48.359Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 8, - "Title": "Domain.csproj", - "DocumentMoniker": "E:\\Code\\c#\\Reservation_System\\Domain\\Domain.csproj", - "RelativeDocumentMoniker": "Domain\\Domain.csproj", - "ToolTip": "E:\\Code\\c#\\Reservation_System\\Domain\\Domain.csproj", - "RelativeToolTip": "Domain\\Domain.csproj", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000758|", - "WhenOpened": "2026-02-04T12:33:00.372Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 1, - "Title": "Application.csproj", - "DocumentMoniker": "E:\\Code\\c#\\Reservation_System\\Application\\Application.csproj", - "RelativeDocumentMoniker": "Application\\Application.csproj", - "ToolTip": "E:\\Code\\c#\\Reservation_System\\Application\\Application.csproj", - "RelativeToolTip": "Application\\Application.csproj", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAA0AAAAFAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000758|", - "WhenOpened": "2026-02-04T12:32:48.335Z", - "EditorCaption": "" - } - ] - } - ] - } - ] -} \ No newline at end of file diff --git a/Application/Features/Payment/DTOs/VerifyPaymentResult.cs b/Application/Features/Payment/DTOs/VerifyPaymentResult.cs new file mode 100644 index 0000000..fd8cb43 --- /dev/null +++ b/Application/Features/Payment/DTOs/VerifyPaymentResult.cs @@ -0,0 +1,25 @@ +namespace Application.Features.Payment.DTOs; + +public class VerifyPaymentResult +{ + public bool IsSuccess { get; set; } + public string? RefNumber { get; set; } + public string Message { get; set; } + + public static VerifyPaymentResult Success(string message) + { + return new VerifyPaymentResult + { + IsSuccess = true, + Message = message + }; + } + public static VerifyPaymentResult Failed(string message) + { + return new VerifyPaymentResult + { + IsSuccess = false, + Message = message + }; + } +} \ No newline at end of file diff --git a/Application/Features/Payment/DTOs/ZarinPal/CreateZarinPalPaymentDto.cs b/Application/Features/Payment/DTOs/ZarinPal/CreateZarinPalPaymentDto.cs index 7fbbec7..617f0fd 100644 --- a/Application/Features/Payment/DTOs/ZarinPal/CreateZarinPalPaymentDto.cs +++ b/Application/Features/Payment/DTOs/ZarinPal/CreateZarinPalPaymentDto.cs @@ -11,11 +11,11 @@ public class CreateZarinPalPaymentDto [JsonPropertyName("referrer_id")] public string? ReferrerId { get; set; } - [JsonPropertyName("matadata")] - public ZarinPalMataData ZarinPalMataData { get; set; } + [JsonPropertyName("metadata")] + public ZarinPalMetaData ZarinPalMetaData { get; set; } } -public class ZarinPalMataData +public class ZarinPalMetaData { [JsonPropertyName("mobile")] public string? Mobile { get; set; } @@ -23,4 +23,6 @@ public class ZarinPalMataData public string? Email { get; set; } [JsonPropertyName("order_id")] public string? OrderId { get; set; } + + [JsonPropertyName("auto_verify")] public bool AutoVerify { get; set; } } \ No newline at end of file diff --git a/Application/Features/Payment/DTOs/ZarinPal/ZarinPalVerifyResponse.cs b/Application/Features/Payment/DTOs/ZarinPal/ZarinPalVerifyResponse.cs index acc3f4e..6b914a5 100644 --- a/Application/Features/Payment/DTOs/ZarinPal/ZarinPalVerifyResponse.cs +++ b/Application/Features/Payment/DTOs/ZarinPal/ZarinPalVerifyResponse.cs @@ -4,6 +4,7 @@ namespace Application.Features.Payment.DTOs.ZarinPal; public class ZarinPalVerifyResponse { + [JsonPropertyName("data")] public ZarinPalVerifyData Data { get; set; } } diff --git a/Application/Features/Payment/Interfaces/PaymentGatewayProviderContract.cs b/Application/Features/Payment/Interfaces/PaymentGatewayProviderContract.cs index 714dd7f..7b8e042 100644 --- a/Application/Features/Payment/Interfaces/PaymentGatewayProviderContract.cs +++ b/Application/Features/Payment/Interfaces/PaymentGatewayProviderContract.cs @@ -10,5 +10,6 @@ public interface PaymentGatewayProviderContract Task RequestPaymentAsync(Domain.Entities.Payment payment, CreatePaymentDto dto); - Task HandleCallBackAsync(PaymentGateway gateway, SandBoxCallBackDto dto); + Task HandleCallBackAsync(SandBoxCallBackDto dto); + Task VerifyPaymentAsync(string verifyAuthority); } \ No newline at end of file diff --git a/Application/Features/Payment/Interfaces/PaymentServiceContract.cs b/Application/Features/Payment/Interfaces/PaymentServiceContract.cs index 776b744..71de5aa 100644 --- a/Application/Features/Payment/Interfaces/PaymentServiceContract.cs +++ b/Application/Features/Payment/Interfaces/PaymentServiceContract.cs @@ -8,6 +8,5 @@ namespace Application.Features.Payment.Interfaces; public interface PaymentServiceContract { Task CreatePaymentAsync(CreatePaymentDto dto); - Task HandleCallBackAsync(PaymentGateway gateway,SandBoxCallBackDto dto); - Task VerifyTransaction(string RefNum); + Task HandleCallBackAsync(PaymentGateway gateway,SandBoxCallBackDto dto); } \ No newline at end of file diff --git a/Application/Features/Payment/Services/PaymentService.cs b/Application/Features/Payment/Services/PaymentService.cs index 258a0c2..2ef6cd9 100644 --- a/Application/Features/Payment/Services/PaymentService.cs +++ b/Application/Features/Payment/Services/PaymentService.cs @@ -24,28 +24,9 @@ public PaymentService(PaymentRepositoryContract paymentRepository, IConfiguratio _httpClient = httpClient; _gatewayResolver = gatewayResolver; } - - // public async Task CreatePaymentAsync(CreatePaymentDto dto) - // { - // var payment = new Domain.Entities.Payment(dto.TenantId, dto.AppointmentId, dto.Amount, dto.Gateway); - // - // var provider = _gatewayResolver.Resolve(dto.Gateway); - // - // var requestResult=await provider.RequestPaymentAsync(payment,dto); - // - // if (!requestResult.IsSuccess) - // { - // payment.MarkAsFailed(); - // await _paymentRepository.SaveAsync(); - // - // throw new InvalidOperationException(requestResult.ErrorMessage); - // } - // await _paymentRepository.CreatePaymentAsync(payment); - // await _paymentRepository.SaveAsync(); - // return requestResult.PaymentUrl; public async Task CreatePaymentAsync(CreatePaymentDto dto) { - var payment = new Domain.Entities.Payment(dto.TenantId, dto.AppointmentId, dto.Amount, dto.Gateway); + var payment = new Domain.Entities.Payment(dto.TenantId, dto.AppointmentId, dto.Amount,dto.Description, dto.Gateway); var provider = _gatewayResolver.Resolve(dto.Gateway); @@ -64,30 +45,10 @@ public PaymentService(PaymentRepositoryContract paymentRepository, IConfiguratio return requestResult.PaymentUrl; } - public async Task HandleCallBackAsync(PaymentGateway gateway, SandBoxCallBackDto dto) + public async Task HandleCallBackAsync(PaymentGateway gateway, SandBoxCallBackDto dto) { var provider = _gatewayResolver.Resolve(gateway); - var result= await provider.HandleCallBackAsync(gateway, dto); - + var result= await provider.HandleCallBackAsync(dto); return result; } - - public async Task VerifyTransaction(string RefNum) - { - var request = new - { - refNum = RefNum, - TerminalNumber = _configuration["Payment:TerminalId"] - }; - var verifyUrl = _configuration["Payment:VerifyTransactionUrl"]; - var response = await _httpClient.PostAsJsonAsync(verifyUrl, request); - - if (!response.IsSuccessStatusCode) - return false; - - var result = await response.Content.ReadAsStringAsync(); - Console.WriteLine(result); - return result is not null; - } - } \ No newline at end of file diff --git a/Domain/Entities/Payment.cs b/Domain/Entities/Payment.cs index b887ac5..88b4592 100644 --- a/Domain/Entities/Payment.cs +++ b/Domain/Entities/Payment.cs @@ -32,7 +32,7 @@ public class Payment public DateTime? PaidAt { get; set; } - public Payment(Guid tenantId, Guid appointmentId, long amount, PaymentGateway gateway) + public Payment(Guid tenantId, Guid appointmentId, long amount,string description, PaymentGateway gateway) { Id = Guid.NewGuid(); TenantId = tenantId; @@ -40,6 +40,7 @@ public Payment(Guid tenantId, Guid appointmentId, long amount, PaymentGateway ga Amount = amount; Gateway=gateway; PaymentStatus = PaymentStatus.pending; + Description=description; CreatedAt = DateTime.UtcNow; } @@ -70,7 +71,11 @@ public void MarkAsFailed() } public void MarkAsSuccess() { + if (PaymentStatus == PaymentStatus.Success) + return; + PaymentStatus = PaymentStatus.Success; + PaidAt??=DateTime.UtcNow; } public void GenerateOrderNumber() diff --git a/Infrastructure/Services/Implement/SamanPaymentGatewayProvider.cs b/Infrastructure/Services/Implement/SamanPaymentGatewayProvider.cs index 491f565..9c586da 100644 --- a/Infrastructure/Services/Implement/SamanPaymentGatewayProvider.cs +++ b/Infrastructure/Services/Implement/SamanPaymentGatewayProvider.cs @@ -66,16 +66,15 @@ public async Task RequestPaymentAsync(Payment payme } var paymentUrl = $"{paymentPageUrl}?token={result.Token}"; - return PaymentGatewayRequestResult.Success(result.Token, paymentUrl); } - public async Task HandleCallBackAsync(PaymentGateway gateway, SandBoxCallBackDto dto) + public async Task HandleCallBackAsync(SandBoxCallBackDto dto) { - var payment=await _paymentRepositoryContract.GetPaymentByAuthorityAsync(dto.Authority); + var payment=await _paymentRepositoryContract.GetPaymentByResNumAsync(dto.ResNum); var request = new { - RefNum = payment.RefNum, + RefNum = dto.RefNum, TerminalNumber = _configuration["Payment:TerminalId"] }; var verifyUrl = _configuration["Payment:VerifyTransactionUrl"]; @@ -85,9 +84,13 @@ public async Task RequestPaymentAsync(Payment payme var result=await response.Content.ReadFromJsonAsync(); if (!response.IsSuccessStatusCode) - return "پرداخت ناموفق"; + return VerifyPaymentResult.Failed("تراکنش ناموفق بود"); - return result.ResultDescription; + return VerifyPaymentResult.Success(result.ResultDescription); } + public Task VerifyPaymentAsync(string verifyAuthority) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/Infrastructure/Services/Implement/ZarinPalPaymentGatewayProvider.cs b/Infrastructure/Services/Implement/ZarinPalPaymentGatewayProvider.cs index 35ff3dc..2722056 100644 --- a/Infrastructure/Services/Implement/ZarinPalPaymentGatewayProvider.cs +++ b/Infrastructure/Services/Implement/ZarinPalPaymentGatewayProvider.cs @@ -15,7 +15,8 @@ public class ZarinPalPaymentGatewayProvider : PaymentGatewayProviderContract private readonly HttpClient _httpClient; private readonly PaymentRepositoryContract _repositoryContract; - public ZarinPalPaymentGatewayProvider(IConfiguration config, HttpClient httpClient, PaymentRepositoryContract repositoryContract) + public ZarinPalPaymentGatewayProvider(IConfiguration config, HttpClient httpClient, + PaymentRepositoryContract repositoryContract) { _config = config; _httpClient = httpClient; @@ -32,11 +33,12 @@ public async Task RequestPaymentAsync(Payment payme Amount = dto.Amount, Description = dto.Description, ReferrerId = dto.ReferrerId, - ZarinPalMataData = new ZarinPalMataData + ZarinPalMetaData = new ZarinPalMetaData { Mobile = dto.Mobile, Email = dto.Email, - OrderId = payment.ResNum + OrderId = payment.ResNum, + AutoVerify = bool.Parse(_config["Payment:ZarinPal_AutoVerify"]) } }; var request = new @@ -45,7 +47,7 @@ public async Task RequestPaymentAsync(Payment payme amount = requestDto.Amount, description = requestDto.Description, callback_url = _config["Payment:ZarinPalCallBackUrl"], - matadata = requestDto.ZarinPalMataData + metadata = requestDto.ZarinPalMetaData }; using var response = @@ -61,7 +63,7 @@ public async Task RequestPaymentAsync(Payment payme } var result = JsonSerializer.Deserialize(raw); - if (result is null || string.IsNullOrWhiteSpace(result.Data.Authority)) + if (string.IsNullOrWhiteSpace(result?.Data?.Authority)) { return PaymentGatewayRequestResult.Failed( "INVALID_RESPONSE", @@ -72,28 +74,95 @@ public async Task RequestPaymentAsync(Payment payme return PaymentGatewayRequestResult.Success(result.Data.Authority, paymentUrl); } - public async Task HandleCallBackAsync(PaymentGateway gateway, SandBoxCallBackDto dto) + public async Task HandleCallBackAsync(SandBoxCallBackDto dto) { + var payment = await _repositoryContract.GetPaymentByAuthorityAsync(dto.Authority); + + if (payment is null) + return VerifyPaymentResult.Failed("تراکنش یافت نشد."); + if (dto.Status != "OK") { - return "پرداخت ناموفق"; + payment.Edit("NOK", null, null, 0); + payment.MarkAsFailed(); + await _repositoryContract.SaveAsync(); + + return VerifyPaymentResult.Failed("پرداخت توسط کاربر لغو شد."); + } + + if (payment.PaymentStatus == PaymentStatus.Success) + { + return VerifyPaymentResult.Success( + $"این تراکنش قبلاً تایید شده است. شماره پیگیری: {payment.RefNum}"); } - var payment=await _repositoryContract.GetPaymentByAuthorityAsync(dto.Authority); + + return await VerifyPaymentAsync(dto.Authority); + } + + + + public async Task VerifyPaymentAsync(string authority) + { + var payment = await _repositoryContract.GetPaymentByAuthorityAsync(authority); + + if (payment is null) + return VerifyPaymentResult.Failed("تراکنش یافت نشد"); + var verifyRequest = new { merchant_id = _config["Payment:MerchantIdZarinPal"], amount = payment.Amount, - authority = dto.Authority + authority = authority }; - var verifyResponse = - await _httpClient.PostAsJsonAsync("https://sandbox.zarinpal.com/pg/v4/payment/verify.json", verifyRequest); - var result = await verifyResponse.Content.ReadFromJsonAsync(); + try + { + var response = await _httpClient.PostAsJsonAsync( + _config["Payment:ZarinPalVerifyUrl"], + verifyRequest); + + var raw = await response.Content.ReadAsStringAsync(); - if (result.Data.Code != 100 && result.Data.Code != 101) - return "پرداخت ناموفق ."; - payment.Edit(dto.Status, result.Data.RefId, result.Data.CardPan, result.Data.Fee); - await _repositoryContract.SaveAsync(); - return "پرداخت با موفقیت انجام شد شماره پیگیری " + result.Data.RefId; + if (!response.IsSuccessStatusCode) + return VerifyPaymentResult.Failed($"خطا از درگاه: {raw}"); + + var result = JsonSerializer.Deserialize(raw); + + if (result?.Data is null) + return VerifyPaymentResult.Failed("پاسخ نامعتبر از درگاه زرین پال"); + + if (result.Data.Code == 100) + { + payment.Edit("OK", result.Data.RefId, result.Data.CardPan, result.Data.Fee); + payment.MarkAsSuccess(); + await _repositoryContract.SaveAsync(); + + return VerifyPaymentResult.Success( + $"تراکنش با شماره پیگیری {result.Data.RefId} انجام شد"); + } + + if (result.Data.Code == 101) + { + if (payment.PaymentStatus != PaymentStatus.Success) + { + payment.Edit("OK", result.Data.RefId, result.Data.CardPan, result.Data.Fee); + payment.MarkAsSuccess(); + await _repositoryContract.SaveAsync(); + } + + return VerifyPaymentResult.Success( + $"این تراکنش قبلاً تایید شده است. شماره پیگیری: {result.Data.RefId}"); + } + + payment.Edit("NOK", null, null, 0); + payment.MarkAsFailed(); + await _repositoryContract.SaveAsync(); + + return VerifyPaymentResult.Failed("تایید تراکنش توسط درگاه انجام نشد."); + } + catch (Exception) + { + return VerifyPaymentResult.Failed("خطای غیر منتظره در ارتباط با درگاه."); + } } } \ No newline at end of file diff --git a/README.md b/README.md index 3007258..5cf593c 100644 --- a/README.md +++ b/README.md @@ -1,206 +1,343 @@ +# Booking System API -# 📅 Appointment Reservation System API +یک API برای **سیستم رزرو نوبت چندمستاجری (Multi-Tenant)** که با **ASP.NET Core 8** پیاده‌سازی شده و قابلیت‌هایی مثل احراز هویت JWT، مدیریت کاربران و نقش‌ها، رزرو نوبت، مدیریت سرویس‌ها و اتصال به درگاه پرداخت را پوشش می‌دهد. -## 🎯 معرفی پروژه -این پروژه یک **API سیستم رزرو نوبت** است که با **ASP.NET Core** و بر اساس اصول **Clean Architecture** طراحی و پیاده‌سازی شده است. +> این پروژه برای نمایش معماری، طراحی API و پیاده‌سازی یک سیستم رزرو قابل توسعه آماده شده است. -تمرکز اصلی پروژه بر: -- ✅ امنیت مبتنی بر JWT -- ✅ طراحی RESTful واقعی -- ✅ جداسازی مسئولیت‌ها -- ✅ جلوگیری از ارسال و دستکاری شناسه کاربر -- ✅ اعمال Authorization در لایه Application +--- + +## فهرست مطالب -می‌باشد؛ به‌طوری که API برای استفاده در پروژه‌های واقعی و Production-ready مناسب است. +- [ویژگی‌ها](#ویژگیها) +- [تکنولوژی‌ها](#تکنولوژیها) +- [معماری پروژه](#معماری-پروژه) +- [ماژول‌ها](#ماژولها) +- [احراز هویت و سطح دسترسی](#احراز-هویت-و-سطح-دسترسی) +- [Multi-Tenancy](#multi-tenancy) +- [درگاه پرداخت](#درگاه-پرداخت) +- [راه‌اندازی پروژه](#راهاندازی-پروژه) +- [Endpointهای اصلی](#endpointهای-اصلی) +- [نکات امنیتی برای انتشار عمومی](#نکات-امنیتی-برای-انتشار-عمومی) +- [وضعیت پروژه](#وضعیت-پروژه) --- -## 🧱 معماری پروژه (Clean Architecture) +## ویژگی‌ها + +- پیاده‌سازی API رزرو نوبت با ASP.NET Core +- معماری چندلایه بر اساس اصول Clean Architecture +- پشتیبانی از Multi-Tenancy برای تفکیک داده‌ها بر اساس Tenant +- احراز هویت با JWT +- پشتیبانی از Refresh Token +- مدیریت نقش‌ها شامل `SuperAdmin`، `Admin`، `User` و `Customer` +- مدیریت کاربران، پروفایل و خروج از حساب کاربری +- مدیریت سرویس‌های قابل رزرو +- ایجاد، ویرایش، مشاهده و لغو نوبت +- اتصال به درگاه پرداخت با پشتیبانی از Saman و ZarinPal +- اعتبارسنجی ورودی‌ها با FluentValidation +- مستندسازی API با Swagger +- استفاده از Entity Framework Core و SQL Server -ساختار کلی پروژه: +--- +## تکنولوژی‌ها -``` -Api - └── Controllers - └── REST Endpoints +- .NET 8 +- ASP.NET Core Web API +- Entity Framework Core +- SQL Server +- JWT Bearer Authentication +- Refresh Token +- FluentValidation +- Swagger / Swashbuckle -Application - ├── Interfaces - │ └── IUserContext - ├── Features - │ ├── Appointment - │ ├── User - │ └── Service - ├── DTOs - └── Application Services +--- -Infrastructure - ├── Security - │ ├── JWT - │ └── UserContext - └── Persistence +## معماری پروژه + +پروژه به صورت چندلایه طراحی شده است: + +```text +Booking-System +├── api +│ ├── Controllers +│ ├── Middlewares +│ └── Program.cs +├── Application +│ ├── Common +│ ├── Features +│ └── Validators +├── Domain +│ ├── Entities +│ ├── Enums +│ ├── Exceptions +│ └── Interfaces +└── Infrastructure + ├── Persistence + ├── Repositories + ├── Security + └── Services ``` -✅ لایه `Application` مستقل از `Api` و `Infrastructure` است -✅ هیچ منطق امنیتی حیاتی صرفاً در Controllerها قرار ندارد +### توضیح لایه‌ها + +- `Domain`: شامل Entityها، Enumها، Exceptionها و قراردادهای اصلی دامنه +- `Application`: شامل Use Caseها، سرویس‌های اپلیکیشن، DTOها، Validatorها و Interfaceها +- `Infrastructure`: شامل EF Core، Repositoryها، JWT، Hashing و پیاده‌سازی سرویس‌های خارجی +- `api`: شامل Controllerها، Middlewareها، Swagger و پیکربندی اجرای برنامه --- -## 🔐 احراز هویت (JWT Authentication) +## ماژول‌ها + +### User / Auth + +- ثبت‌نام کاربر +- ورود با ایمیل و رمز عبور +- انتخاب Tenant هنگام وجود چند هویت برای یک ایمیل +- صدور Access Token و Refresh Token +- تمدید Token +- خروج از حساب کاربری +- مشاهده و ویرایش پروفایل +- مشاهده کاربران توسط `SuperAdmin` + +### Tenant + +- ثبت Tenant جدید +- اتصال کاربر به Tenant +- بررسی وضعیت اشتراک Tenant +- نگهداری `TenantId` در Context برنامه + +### Service + +- ایجاد سرویس قابل رزرو +- ویرایش سرویس +- حذف سرویس +- مشاهده سرویس‌ها + +### Appointment + +- ایجاد نوبت +- اتصال نوبت به یک یا چند سرویس +- ویرایش سرویس‌های نوبت +- لغو نوبت +- مشاهده نوبت‌ها +- بررسی مالکیت نوبت برای جلوگیری از دسترسی غیرمجاز -### JWT -- احراز هویت کاربران با **JSON Web Token** -- ارسال Token در Header درخواست: +### Payment + +- ایجاد پرداخت برای نوبت +- انتخاب درگاه پرداخت +- دریافت URL پرداخت +- مدیریت Callback درگاه +- ثبت وضعیت پرداخت +- ذخیره اطلاعاتی مثل `Authority`، `RefNum`، `RRN`، `TraceNo` و `SecurePan` + +--- + +## احراز هویت و سطح دسترسی + +احراز هویت با JWT انجام می‌شود. برای دسترسی به endpointهای محافظت‌شده باید Token در Header ارسال شود: + +```http +Authorization: Bearer ``` -Authorization: Bearer {token} + +### نقش‌ها + +```text +SuperAdmin +Admin +User +Customer ``` -### UserContext -برای حذف وابستگی به `userId` ارسالی از کلاینت: +برخی عملیات مثل مدیریت سرویس‌ها، مشاهده همه کاربران، مشاهده همه نوبت‌ها و ثبت Tenant فقط برای نقش‌های مدیریتی در نظر گرفته شده‌اند. -- Interface: `IUserContext` در لایه Application -- Implementation: `UserContext` در Infrastructure -- استخراج اطلاعات از JWT Claims: - - `UserId` ← `sub` یا `ClaimTypes.NameIdentifier` - - `Role` ← `ClaimTypes.Role` +--- -✅ تمام عملیات وابسته به کاربر، هویت را از JWT می‌گیرند، نه از ورودی کلاینت. +## Multi-Tenancy + +این پروژه با رویکرد Multi-Tenant طراحی شده است. موجودیت‌هایی مثل کاربر، سرویس، نوبت، Refresh Token و پرداخت به Tenant متصل هستند و داده‌ها بر اساس `TenantId` تفکیک می‌شوند. + +در حال حاضر Tenant از طریق Context و Claimهای JWT مدیریت می‌شود و Middleware اختصاصی برای بررسی وضعیت Tenant و اعتبار اشتراک در پروژه وجود دارد. --- -## 🛡️ Authorization و Role Management +## درگاه پرداخت -### EnsureAdmin -برای عملیات Admin-only، متد زیر در AppServiceها پیاده‌سازی شده است: +پروژه ساختار قابل توسعه برای اتصال به چند درگاه پرداخت دارد. -```csharp -EnsureAdmin(); -``` +درگاه‌های فعلی: + +- Saman +- ZarinPal -این متد: -- Role کاربر جاری را بررسی می‌کند -- در صورت عدم دسترسی، Exception پرتاب می‌کند +نمونه سناریوی پرداخت: -✅ حتی اگر Controller اشتباه پیکربندی شود، -لایه Application همچنان امنیت را حفظ می‌کند (**Defense in Depth**). +1. کاربر برای یک نوبت درخواست پرداخت ایجاد می‌کند. +2. سیستم بر اساس درگاه انتخاب‌شده URL پرداخت تولید می‌کند. +3. کاربر به صفحه پرداخت منتقل می‌شود. +4. درگاه، نتیجه پرداخت را به Callback API ارسال می‌کند. +5. وضعیت پرداخت در سیستم ثبت و بروزرسانی می‌شود. --- -## 👤 بررسی مالکیت (Ownership Check) +## راه‌اندازی پروژه -برای عملیات حساس کاربرمحور مانند: -- ویرایش نوبت -- لغو نوبت -- مشاهده نوبت‌های شخصی +### پیش‌نیازها + +- .NET SDK 8 +- SQL Server +- IDE یا Editor مثل Visual Studio، Rider یا VS Code + +### مراحل اجرا + +1. ریپازیتوری را Clone کنید: -مالکیت بررسی می‌شود: +```bash +git clone https://github.com//.git +cd +``` + +2. پکیج‌ها را Restore کنید: +```bash +dotnet restore ``` -appointment.UserId == CurrentUserId + +3. فایل تنظیمات نمونه را کپی کنید و مقدار Connection String، JWT و درگاه پرداخت را تنظیم کنید: + +```bash +cp api/appsettings.example.json api/appsettings.json ``` -✅ هیچ کاربری به منابع کاربران دیگر دسترسی ندارد. +برای پروژه‌های واقعی بهتر است مقادیر حساس را در Secret Manager یا Environment Variable نگهداری کنید: + +```json +{ + "ConnectionStrings": { + "DefaultConnection": "Server=.;Database=BookingDb;Trusted_Connection=True;TrustServerCertificate=True" + }, + "JwtSettings": { + "Issuer": "https://localhost:44328", + "Audience": "https://localhost:44328", + "SecretKey": "CHANGE_ME_TO_A_LONG_RANDOM_SECRET", + "AccessTokenMinutes": 10 + } +} +``` ---- +4. دیتابیس را ایجاد یا بروزرسانی کنید: -## 📅 Appointment API +```bash +dotnet ef database update --project Infrastructure --startup-project api +``` -### قابلیت‌ها -- ایجاد نوبت -- ویرایش جزئی نوبت (PATCH) -- لغو نوبت -- مشاهده نوبت‌های شخصی (JWT-Based) -- مشاهده تمام نوبت‌ها (Admin) +5. پروژه API را اجرا کنید: -### Endpointها +```bash +dotnet run --project api ``` -POST /api/appointment -PATCH /api/appointment/{id} -DELETE /api/appointment/{id} -GET /api/appointment/my -GET /api/appointment (Admin only) + +6. Swagger را در محیط Development باز کنید: + +```text +https://localhost:/swagger ``` --- -## 👥 User API +## Endpointهای اصلی -### امکانات -- ثبت‌نام و ورود -- دریافت پروفایل کاربر جاری -- ویرایش پروفایل -- تغییر رمز عبور -- حذف حساب کاربری -- مشاهده لیست کاربران (Admin) +### User -### Endpointها +```http +POST /api/User/Register +POST /api/User/login +POST /api/User/refresh +POST /api/User/Logout +GET /api/User/Profile +PATCH /api/User/Profile +GET /api/User/ViewUsers ``` -POST /api/user/register -POST /api/user/login -POST /api/user/refresh -GET /api/user/me -PATCH /api/user/me -POST /api/user/change-password -DELETE /api/user/me -GET /api/user (Admin only) + +### Auth + +```http +POST /api/Auth/Select-Tenant ``` ---- +### Tenant -## 🌐 اصول REST رعایت‌شده -- ✅ استفاده صحیح از HTTP Verbs -- ✅ Resource-based URL -- ✅ حذف Action-based Routeها -- ✅ استفاده از `me` به‌جای ارسال userId -- ✅ عدم دریافت اطلاعات هویتی حساس از کلاینت +```http +POST /api/Tenant +GET /api/Tenant +``` ---- +### Service -## 🧪 متدهای HTTP +```http +POST /api/Service +PATCH /api/Service/{serviceId} +DELETE /api/Service/{serviceId} +GET /api/Service/ViewServices +``` -| Method | کاربرد | -|------|------| -| GET | دریافت اطلاعات | -| POST | ایجاد Resource | -| PATCH | ویرایش جزئی | -| DELETE | حذف / لغو | +### Appointment ---- +```http +POST /api/Appointment +PATCH /api/Appointment/{appointmentId} +DELETE /api/Appointment/{appointmentId} +GET /api/Appointment/ViewAppointments +GET /api/Appointment/ViewMyAppointments +``` -## ⚙️ تکنولوژی‌ها -- ASP.NET Core -- JWT Authentication -- Clean Architecture -- Entity Framework Core -- FluentValidation +### Payment + +```http +POST /api/Payment/GetPaymentUrl +GET /api/Payment/CallBack/{gateway} +POST /api/Payment/CallBack/{gateway} +``` --- -## 🚀 اجرای پروژه +## نکات امنیتی برای انتشار عمومی + +قبل از Public کردن ریپازیتوری در GitHub: + +- مقدار واقعی `JwtSettings:SecretKey` را از فایل‌های تنظیمات حذف کنید. +- اطلاعات واقعی Super Admin، شماره موبایل، ایمیل و رمز عبور را در ریپازیتوری قرار ندهید. +- Merchant ID، Terminal ID، Callback URL و کلیدهای درگاه پرداخت را در Secret Manager، Environment Variable یا GitHub Secrets نگهداری کنید. +- اگر Secret واقعی قبلاً Commit شده، آن را Rotate کنید. +- فایل‌های `bin`، `obj`، لاگ‌ها و فایل‌های local development را Commit نکنید. +- برای Production مقدار `RequireHttpsMetadata`، تنظیمات CORS، مدیریت خطا و Logging را بازبینی کنید. + +نمونه استفاده از Secret Manager: + ```bash -dotnet restore -dotnet run +dotnet user-secrets init --project api +dotnet user-secrets set "JwtSettings:SecretKey" "YOUR_LONG_RANDOM_SECRET" --project api +dotnet user-secrets set "ConnectionStrings:DefaultConnection" "YOUR_CONNECTION_STRING" --project api ``` --- -## ⚠️ وضعیت توسعه و نکات مهم -این پروژه در حال حاضر **نسخه‌ی اولیه (v1)** محسوب می‌شود. +## وضعیت پروژه -با وجود اینکه: -- ساختار معماری به‌صورت اصولی (Clean Architecture) طراحی شده -- ملاحظات امنیتی (JWT، Authorization، Ownership Check) رعایت شده -- API از نظر RESTful و Business Logic پایدار است +این پروژه در حال توسعه است و ممکن است در آینده موارد زیر تغییر کند: -ممکن است: -- 🐞 باگ‌های منطقی یا لبه‌ای (Edge Case) وجود داشته باشد -- 🔄 برخی بخش‌ها در آینده Refactor یا بهینه‌سازی شوند -- ➕ قابلیت‌های جدید یا تغییر در Endpointها اضافه شود +- بهبود ساختار Multi-Tenancy +- تکمیل Password Recovery +- بهبود Error Handling و Exception Middleware +- اضافه شدن تست‌های واحد و یکپارچه +- بهبود مستندات Swagger +- بازبینی کامل تنظیمات Production -✅ پروژه به‌صورت **Active Development** نگهداری می‌شود و -در بازه‌های زمانی مختلف به‌روزرسانی خواهد شد. +--- -> هدف این نسخه، ارائه‌ی یک پایه‌ی تمیز، امن و قابل توسعه برای ادامه‌ی مسیر است. - +## مشارکت +Pull Requestها و پیشنهادها خوشحال‌کننده‌اند. قبل از ارسال تغییرات، لطفاً پروژه را Build کنید و مطمئن شوید تنظیمات حساس در Commitها قرار نگرفته‌اند. diff --git a/Reservation_System.sln.DotSettings.user b/Reservation_System.sln.DotSettings.user deleted file mode 100644 index 9a85fc2..0000000 --- a/Reservation_System.sln.DotSettings.user +++ /dev/null @@ -1,5 +0,0 @@ - - ForceIncluded - ForceIncluded - ForceIncluded - \ No newline at end of file diff --git a/api/api.csproj.user b/api/api.csproj.user deleted file mode 100644 index 9ff5820..0000000 --- a/api/api.csproj.user +++ /dev/null @@ -1,6 +0,0 @@ - - - - https - - \ No newline at end of file diff --git a/api/appsettings.example.json b/api/appsettings.example.json new file mode 100644 index 0000000..43d9311 --- /dev/null +++ b/api/appsettings.example.json @@ -0,0 +1,34 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=.;Database=BookingDb;Trusted_Connection=True;TrustServerCertificate=True" + }, + "JwtSettings": { + "Issuer": "https://localhost:44328", + "Audience": "https://localhost:44328", + "SecretKey": "CHANGE_ME_TO_A_LONG_RANDOM_SECRET_KEY", + "AccessTokenMinutes": 10 + }, + "SeedSuperAdmin": { + "FullName": "Super Admin", + "Email": "admin@example.com", + "PhoneNumber": "09120000000", + "Password": "CHANGE_ME" + }, + "Payment": { + "TerminalId": "CHANGE_ME", + "SamanRedirectUrl": "https://localhost:44328/api/Payment/CallBack/0", + "VerifyTransactionUrl": "https://sandbox.banktest.ir/saman/sep.shaparak.ir/verifyTxnRandomSessionkey/ipg/VerifyTransaction", + "MerchantIdZarinPal": "CHANGE_ME", + "ZarinPalCallBackUrl": "https://localhost:44328/api/Payment/CallBack/1", + "ZarinPalVerifyUrl": "https://sandbox.zarinpal.com/pg/v4/payment/verify.json", + "ZarinPal_AutoVerify": false, + "ZarinPalCurrency": "IRT" + } +} diff --git a/appsettings.Development.json b/appsettings.Development.json deleted file mode 100644 index 0c208ae..0000000 --- a/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/appsettings.json b/appsettings.json deleted file mode 100644 index 10f68b8..0000000 --- a/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -}