diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2fbaa8d --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +################################################################################ +# Данный GITIGNORE-файл был автоматически создан Microsoft(R) Visual Studio. +################################################################################ + +/.vs/TestTaskForVersta/FileContentIndex +/.vs +/backend/CargoDelivery.API/bin/Debug/net8.0 +/backend/CargoDelivery.API/.vs +/backend/CargoDelivery.API/obj +/backend/CargoDelivery.Application/bin/Debug/net8.0 +/backend/CargoDelivery.Application/obj +/backend/CargoDelivery.Core/bin/Debug/net8.0 +/backend/CargoDelivery.Core/obj +/backend/CargoDelivery.DataAccess/bin/Debug/net8.0 +/backend/CargoDelivery.DataAccess/obj +/frontend/node_modules +/frontend diff --git a/README.md b/README.md index 45ef0f6..1797100 100644 --- a/README.md +++ b/README.md @@ -1 +1,151 @@ -# TestTaskForVersta \ No newline at end of file +# TestTaskForVersta +## Техническое задание: +*Реализовать простое Web приложение для приемки заказа на доставку со следующим функционалом:* + +*1. Форма создания нового заказа (все поля обязательны для заполнения):* +*Город отправителя +Адрес отправителя +Город получателя +Адрес получателя +Вес груза +Дата забора груза* + +*2. Форма отображения списка заказов: все созданные заказы должны отображаться в отдельной форме. Помимо полей, введенных пользователем при создании заказа, должен отображаться автоматически сгенерированный номер заказа.* + +*3. Форма просмотра созданного заказа в режиме чтения. Должна открываться при клике на заказ в списке заказов.* + +___ +## Стек технологий: + +* ASP.NET 8 на backend +* Razor Pages +* Entity Framework +* PostgreSQL +* Docker +___ +## Backend: + Я решил написать данное приложение с использованием луковой(чистой) архитектуры. Как известно, всё начинается с доменных сущностей: + + ![image](https://github.com/VladislavRkm/TestTaskForVersta/assets/113041279/4af54062-a543-42bd-bcbb-fc0799627fb9) + (Все слои и взаимодействия можно увидеть в структкуре проекта.) + + Не буду тянуть. Когда все сущности, модели, контекст, репозитории, интерфейсы, сервисы и контроллеры были написаны, я подключил базу данных PostgreSQL и поднял её в Docker-контейнере. + ![image](https://github.com/VladislavRkm/TestTaskForVersta/assets/113041279/0785811f-eac8-4ee0-a266-43d88a19e48d) + ![image](https://github.com/VladislavRkm/TestTaskForVersta/assets/113041279/608d291e-1f46-4b99-b3b5-e36904ec9453) + ___ + Можно запускать проект. При запуске у нас открывается страница Swagger'a со следующими http-запросами: + ![image](https://github.com/VladislavRkm/TestTaskForVersta/assets/113041279/b6f43d69-5cd5-4f92-9833-a6e7d51b3f41) + ___ + Проверим их работоспособность, и первым делом создадим заказ на перевозку груза: + ![image](https://github.com/VladislavRkm/TestTaskForVersta/assets/113041279/b6295f9b-87f5-4351-8108-d92a115f9e1b) + ![image](https://github.com/VladislavRkm/TestTaskForVersta/assets/113041279/13440220-455c-4132-ae4b-100dd13e67c1) + Видим код "200", радуемся жизни и отправляем GET-запрос и получаем наши заказы, дабы убедиться, что всё не зря: + ![image](https://github.com/VladislavRkm/TestTaskForVersta/assets/113041279/79abdfb1-084a-4aba-ae17-1e00c607851f) + Перед нами два заказа, один из которых не несёт в себе никакой содержательной информации. Удалим его и проверим работоспособность DELETE-запроса. Копируем id заказа, который хотим удалить и вставляем в поле для заполнения. +![image](https://github.com/VladislavRkm/TestTaskForVersta/assets/113041279/62a83acf-4280-4615-9302-679a4fb4e8b8) +![image](https://github.com/VladislavRkm/TestTaskForVersta/assets/113041279/81e87d2e-8e1c-4648-aeee-76e147cfe6de) +___ +Теперь представим, что экспедитору попался ненадёжный заказчик и ему нужно подкорректировать детали заказа. Корректируем данные, отправляем UPDATE-запрос, а затем GET-запрос, чтобы убедиться, что внесённые изменения сохранены. +![image](https://github.com/VladislavRkm/TestTaskForVersta/assets/113041279/b179ba10-056d-48a0-a1cd-4f87aea475da) +![image](https://github.com/VladislavRkm/TestTaskForVersta/assets/113041279/e9a901ac-846b-403a-a423-bca654235a71) +![image](https://github.com/VladislavRkm/TestTaskForVersta/assets/113041279/83d16269-7291-4858-955a-db6e0e3fc1f9) +___ +Наконец, убедимся, что в базе данных всё также сохраняется: +![image](https://github.com/VladislavRkm/TestTaskForVersta/assets/113041279/434803a5-0f7e-4220-b1a1-e545dc57aa44) +___ + +## Frontend: +Главная страница выглядит следующим образом. Мы можем увидеть кнопку для формирования нового заказа и форму для просмотра уже существующих заказов (пока что она пуста): + +![image](https://github.com/user-attachments/assets/997906dc-ab77-48f9-b29e-9870da7f7b07) + +Убедимся, что наша база данных также пуста и перейдём к ручному тестированию функционала страницы. + +![image](https://github.com/user-attachments/assets/5b2ec1cb-35a7-4e27-8d13-a933d150d421) +___ +Создадим новый заказ, нажимаем кнопку "Отправить". Возращаемся на главную станицу и видим, что заказ отобразился. +![image](https://github.com/user-attachments/assets/1cd30de6-62d4-43db-870e-eb0c99e0ba12) +![image](https://github.com/user-attachments/assets/ba0b8325-d65a-41b2-a635-03ffb5a38973) + +Проверим его наличие в БД: +![image](https://github.com/user-attachments/assets/224a4216-a08d-4151-a4bb-fa5fa5285177) + +Видим, что заказ отобразился и все данные совпадают с тем, что отражено на странице. +___ +Перейдём к тестированию кнопок "Просмотр заказа", "Редактировать", "Удалить": + +Просмотр осуществляется в режиме чтения. Открываем заказ, попробуем стереть введённые данные: +![image](https://github.com/user-attachments/assets/bf404dc5-5cee-4ffb-9ea8-9989e7076f4a) + +Нажимаем кнопку закрыть и всё возвращается на свои места: +![image](https://github.com/user-attachments/assets/f5cf7bea-d3f1-4273-944c-667c6b5081ec) +___ + +Теперь отредактируем наш заказ: +![image](https://github.com/user-attachments/assets/bb6d4ccb-7ead-4db0-84fd-f5c43e989c4c) + +Заказ отобразился, при этом заметим, что номер заказа у нас _не редактируется и не меняется_, то есть с точки зрения бизнес-логики это тот же самый заказ, в котором изменились некоторые поля. +![image](https://github.com/user-attachments/assets/ecbbc533-71b8-44b6-bd57-2e4f0ea95346) + +В базе данных изменения также отображаются: +![image](https://github.com/user-attachments/assets/07975d8e-81c7-4451-b370-fc715e1b9bd8) +___ + +Давайте теперь удалим наш заказ. Для этого воспользуемся кнопкой "Удалить". +![image](https://github.com/user-attachments/assets/feec350c-2542-4a53-b6b8-d4585a98dc51) + +Из базы данных запись удаляется соответственно: +![image](https://github.com/user-attachments/assets/2c8d6622-7766-4b8b-a012-65ff7f3dc977) +___ +Теперь проверим синхронизацию бэкенда и фронтенда. Создадим какой-нибудь заказ через Swagger UI. +![image](https://github.com/user-attachments/assets/2cbc660b-5a27-46a6-ae19-ec1f84619986) +Обновим страницу и увидим заказ на нашей страничке: + +![image](https://github.com/user-attachments/assets/21bf185a-d473-435b-8eca-c5408e37c556) +___ +Теперь в обратную сторону, проверим как работают http-запросы с заказами, созданными через Razor Pages. +![image](https://github.com/user-attachments/assets/37ecdbee-ee90-4d59-b253-8531e0474239) +![image](https://github.com/user-attachments/assets/424f9451-5918-47b9-9deb-8d256ead09a7) + +Выполняем GET-запрос в Swagger UI и он нам возвращает наши заказы. +![image](https://github.com/user-attachments/assets/caebeab5-1699-4ce3-a430-9eac87b128dc) + +Остальные запросы выполнять не будем, так как очевидно, что они будут работать. +___ +Подведём итоги. +Технические требования к реализации: + +- [x] Форма заказа с обязательными для заполнения полями +- [x] Форма для отображения сформированных заказов с автоматически сгенерированным номером для каждого заказа +- [x] Форма для просмотра заказа в режиме чтения + +Технологии: + +- [x] ASP.NET 8 на backend +- [ ] React.JS (предпочтительно) на front +- [x] Razor Pages на front +- [x] Entity Framework +- [x] Docker (дополнительно) +- [x] PostgreSQL + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/CargoDelivery.API/.dockerignore b/backend/CargoDelivery.API/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/backend/CargoDelivery.API/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/backend/CargoDelivery.API/CargoDelivery.API.csproj b/backend/CargoDelivery.API/CargoDelivery.API.csproj new file mode 100644 index 0000000..9453d09 --- /dev/null +++ b/backend/CargoDelivery.API/CargoDelivery.API.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + enable + enable + 70e9f40d-985e-4d30-b9f2-959dabf4dc9c + Linux + . + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/backend/CargoDelivery.API/CargoDelivery.API.csproj.user b/backend/CargoDelivery.API/CargoDelivery.API.csproj.user new file mode 100644 index 0000000..983ecfc --- /dev/null +++ b/backend/CargoDelivery.API/CargoDelivery.API.csproj.user @@ -0,0 +1,9 @@ + + + + http + + + ProjectDebugger + + \ No newline at end of file diff --git a/backend/CargoDelivery.API/CargoDelivery.API.http b/backend/CargoDelivery.API/CargoDelivery.API.http new file mode 100644 index 0000000..19fa1dd --- /dev/null +++ b/backend/CargoDelivery.API/CargoDelivery.API.http @@ -0,0 +1,6 @@ +@CargoDelivery.API_HostAddress = http://localhost:5298 + +GET {{CargoDelivery.API_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/backend/CargoDelivery.API/CargoDelivery.sln b/backend/CargoDelivery.API/CargoDelivery.sln new file mode 100644 index 0000000..ead9f5c --- /dev/null +++ b/backend/CargoDelivery.API/CargoDelivery.sln @@ -0,0 +1,47 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35013.160 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CargoDelivery.API", "CargoDelivery.API.csproj", "{539CE379-B35B-41B7-A20A-F581CEEDE422}" + ProjectSection(ProjectDependencies) = postProject + {60CC839E-94A5-4F41-88C8-EACFED9FC89D} = {60CC839E-94A5-4F41-88C8-EACFED9FC89D} + {D9E9F7C0-6649-4DF0-B236-57748F306AC9} = {D9E9F7C0-6649-4DF0-B236-57748F306AC9} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CargoDelivery.Core", "..\CargoDelivery.Core\CargoDelivery.Core.csproj", "{AB950833-2C0F-4E3C-AA4B-B57F44C631BD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CargoDelivery.DataAccess", "..\CargoDelivery.DataAccess\CargoDelivery.DataAccess.csproj", "{D9E9F7C0-6649-4DF0-B236-57748F306AC9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CargoDelivery.Application", "..\CargoDelivery.Application\CargoDelivery.Application.csproj", "{60CC839E-94A5-4F41-88C8-EACFED9FC89D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {539CE379-B35B-41B7-A20A-F581CEEDE422}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {539CE379-B35B-41B7-A20A-F581CEEDE422}.Debug|Any CPU.Build.0 = Debug|Any CPU + {539CE379-B35B-41B7-A20A-F581CEEDE422}.Release|Any CPU.ActiveCfg = Release|Any CPU + {539CE379-B35B-41B7-A20A-F581CEEDE422}.Release|Any CPU.Build.0 = Release|Any CPU + {AB950833-2C0F-4E3C-AA4B-B57F44C631BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB950833-2C0F-4E3C-AA4B-B57F44C631BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB950833-2C0F-4E3C-AA4B-B57F44C631BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB950833-2C0F-4E3C-AA4B-B57F44C631BD}.Release|Any CPU.Build.0 = Release|Any CPU + {D9E9F7C0-6649-4DF0-B236-57748F306AC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9E9F7C0-6649-4DF0-B236-57748F306AC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9E9F7C0-6649-4DF0-B236-57748F306AC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9E9F7C0-6649-4DF0-B236-57748F306AC9}.Release|Any CPU.Build.0 = Release|Any CPU + {60CC839E-94A5-4F41-88C8-EACFED9FC89D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60CC839E-94A5-4F41-88C8-EACFED9FC89D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60CC839E-94A5-4F41-88C8-EACFED9FC89D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60CC839E-94A5-4F41-88C8-EACFED9FC89D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BC1836CA-DD1B-40EF-8EFC-4F4B3101097D} + EndGlobalSection +EndGlobal diff --git a/backend/CargoDelivery.API/Contracts/OrderRequest.cs b/backend/CargoDelivery.API/Contracts/OrderRequest.cs new file mode 100644 index 0000000..4d7f8c4 --- /dev/null +++ b/backend/CargoDelivery.API/Contracts/OrderRequest.cs @@ -0,0 +1,11 @@ +namespace CargoDelivery.API.Contracts; +public record OrderRequest + ( + string SenderCity, + string SenderAddress, + string RecipientCity, + string RecipientAddress, + float Weight, + DateTime PickUpDate + ); + diff --git a/backend/CargoDelivery.API/Contracts/OrderResponse.cs b/backend/CargoDelivery.API/Contracts/OrderResponse.cs new file mode 100644 index 0000000..063c1c3 --- /dev/null +++ b/backend/CargoDelivery.API/Contracts/OrderResponse.cs @@ -0,0 +1,13 @@ +namespace CargoDelivery.API.Contracts; + +public record OrderResponse( + Guid id, + string OrderNumber, + string SenderCity, + string SenderAddress, + string RecipientCity, + string RecipientAddress, + float Weight, + DateTime PickUpDate + ); + diff --git a/backend/CargoDelivery.API/Controllers/OrdersController.cs b/backend/CargoDelivery.API/Controllers/OrdersController.cs new file mode 100644 index 0000000..5a7b6d8 --- /dev/null +++ b/backend/CargoDelivery.API/Controllers/OrdersController.cs @@ -0,0 +1,57 @@ +using CargoDelivery.API.Contracts; +using CargoDelivery.Application.Services; +using CargoDelivery.Core.Models; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace CargoDelivery.API.Controllers; + +[ApiController] +[Route("[controller]")] +public class OrdersController : ControllerBase +{ + private readonly IOrderService _orderService; + + public OrdersController(IOrderService orderService) + { + _orderService = orderService; + } + [HttpGet] + public async Task>> GetOrders() + { + var orders = await _orderService.GetAllOrders(); + + var response = orders.Select(o => new OrderResponse(o.Id, o.OrderNumber, o.SenderCity, o.SenderAddress, o.RecipientCity, o.RecipientAddress, o.Weight, o.PickUpDate)); + return Ok(response); + } + [HttpPost] + public async Task> CreateOrder([FromBody] OrderRequest orderRequest) + { + var (order, error) = Order.Create( + Guid.NewGuid(), + orderRequest.SenderCity, + orderRequest.SenderAddress, + orderRequest.RecipientCity, + orderRequest.RecipientAddress, + orderRequest.Weight, + orderRequest.PickUpDate); + + if (!string.IsNullOrEmpty(error)) + { + return BadRequest(error); + } + var orderId = await _orderService.CreateOrder(order); + return Ok(orderId); + } + [HttpPut("{id:guid}")] + public async Task> UpdateOrder(Guid id, string orderNumber, [FromBody] OrderRequest orderRequest) + { + var orderId = await _orderService.UpdateOrder(id, orderRequest.SenderCity, orderRequest.SenderAddress, orderRequest.RecipientCity, orderRequest.RecipientAddress, orderRequest.Weight, orderRequest.PickUpDate); + return Ok(orderId); + } + [HttpDelete("{id:guid}")] + public async Task> DeleteOrder(Guid id) + { + return Ok(await _orderService.DeleteOrder(id)); + } +} diff --git a/backend/CargoDelivery.API/Dockerfile b/backend/CargoDelivery.API/Dockerfile new file mode 100644 index 0000000..6c42998 --- /dev/null +++ b/backend/CargoDelivery.API/Dockerfile @@ -0,0 +1,29 @@ +#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +#Depending on the operating system of the host machines(s) that will build or run the containers, the image specified in the FROM statement may need to be changed. +#For more information, please see https://aka.ms/containercompat + +FROM mcr.microsoft.com/dotnet/aspnet:8.0-nanoserver-1809 AS base +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0-nanoserver-1809 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["CargoDelivery.API.csproj", "."] +COPY ["CargoDelivery.DataAccess/CargoDelivery.DataAccess.csproj", "CargoDelivery.DataAccess/"] +COPY ["CargoDelivery.Core/CargoDelivery.Core.csproj", "CargoDelivery.Core/"] +RUN dotnet restore "./CargoDelivery.API.csdocproj" +COPY . . +WORKDIR "/src/." +RUN dotnet build "./CargoDelivery.API.csproj" -c %BUILD_CONFIGURATION% -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./CargoDelivery.API.csproj" -c %BUILD_CONFIGURATION% -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "CargoDelivery.API.dll"] \ No newline at end of file diff --git a/backend/CargoDelivery.API/Program.cs b/backend/CargoDelivery.API/Program.cs new file mode 100644 index 0000000..d77fb16 --- /dev/null +++ b/backend/CargoDelivery.API/Program.cs @@ -0,0 +1,44 @@ +using CargoDelivery.Application.Services; +using CargoDelivery.Core.Abstractions; +using CargoDelivery.DataAccess; +using CargoDelivery.DataAccess.Repositories; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.AddDbContext( + options => + { + options.UseNpgsql(builder.Configuration.GetConnectionString(nameof(DatabaseContext))); + }); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +var app = builder.Build(); + + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); +app.UseCors(x => +{ + x.WithHeaders().AllowAnyHeader(); + x.WithOrigins("http://localhost:3000"); + x.WithMethods().AllowAnyMethod(); +}); + + +app.Run(); diff --git a/backend/CargoDelivery.API/Properties/launchSettings.json b/backend/CargoDelivery.API/Properties/launchSettings.json new file mode 100644 index 0000000..57ad3a8 --- /dev/null +++ b/backend/CargoDelivery.API/Properties/launchSettings.json @@ -0,0 +1,52 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5298" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7273;http://localhost:5298" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:13879", + "sslPort": 44308 + } + } +} \ No newline at end of file diff --git a/backend/CargoDelivery.API/appsettings.Development.json b/backend/CargoDelivery.API/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/backend/CargoDelivery.API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/backend/CargoDelivery.API/appsettings.json b/backend/CargoDelivery.API/appsettings.json new file mode 100644 index 0000000..73d8afd --- /dev/null +++ b/backend/CargoDelivery.API/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "DatabaseContext": "User ID=postgres; Password=123; Host=localhost;Port=5432;Database=CargoDelivery;" + } +} diff --git a/backend/CargoDelivery.API/docker-compose.yml b/backend/CargoDelivery.API/docker-compose.yml new file mode 100644 index 0000000..201a6d2 --- /dev/null +++ b/backend/CargoDelivery.API/docker-compose.yml @@ -0,0 +1,16 @@ +services: + postgres: + image: postgres:latest + container_name: postgres + environment: + POSTGRES_DB: "CargoDelivery" + POSTGRES_USER: "postgres" + POSTGRES_PASSWORD: "123" + volumes: + - postgres-data:/var/lib/postgresql/data + + ports: + - "5432:5432" + +volumes: + postgres-data: \ No newline at end of file diff --git a/backend/CargoDelivery.Application/CargoDelivery.Application.csproj b/backend/CargoDelivery.Application/CargoDelivery.Application.csproj new file mode 100644 index 0000000..2285c41 --- /dev/null +++ b/backend/CargoDelivery.Application/CargoDelivery.Application.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/backend/CargoDelivery.Application/Services/OrderService.cs b/backend/CargoDelivery.Application/Services/OrderService.cs new file mode 100644 index 0000000..7fef999 --- /dev/null +++ b/backend/CargoDelivery.Application/Services/OrderService.cs @@ -0,0 +1,30 @@ +using CargoDelivery.Core.Abstractions; +using CargoDelivery.Core.Models; + +namespace CargoDelivery.Application.Services; + +public class OrderService : IOrderService +{ + private readonly IOrderRepository _orderRepository; + public OrderService(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } + public async Task> GetAllOrders() + { + return await _orderRepository.GetOrders(); + } + + public async Task CreateOrder(Order order) + { + return await _orderRepository.Create(order); + } + public async Task UpdateOrder(Guid id, string SenderCity, string SenderAddress, string RecipientCity, string RecipientAddress, float Weight, DateTime PickUpDate) + { + return await (_orderRepository.Update(id, SenderCity, SenderAddress, RecipientCity, RecipientAddress, Weight, PickUpDate)); + } + public async Task DeleteOrder(Guid id) + { + return await _orderRepository.Delete(id); + } +} diff --git a/backend/CargoDelivery.Core/Abstractions/IOrderRepository.cs b/backend/CargoDelivery.Core/Abstractions/IOrderRepository.cs new file mode 100644 index 0000000..3313d89 --- /dev/null +++ b/backend/CargoDelivery.Core/Abstractions/IOrderRepository.cs @@ -0,0 +1,12 @@ +using CargoDelivery.Core.Models; + +namespace CargoDelivery.Core.Abstractions; + +public interface IOrderRepository +{ + Task Create(Order order); + Task Delete(Guid id); + Task> GetOrders(); + Task Update(Guid id, string SenderCity, string SenderAddress, string RecipientCity, string RecipientAddress, float Weight, DateTime PickUpDate); + +} diff --git a/backend/CargoDelivery.Core/Abstractions/IOrderService.cs b/backend/CargoDelivery.Core/Abstractions/IOrderService.cs new file mode 100644 index 0000000..22e1826 --- /dev/null +++ b/backend/CargoDelivery.Core/Abstractions/IOrderService.cs @@ -0,0 +1,12 @@ +using CargoDelivery.Core.Models; + +namespace CargoDelivery.Application.Services +{ + public interface IOrderService + { + Task CreateOrder(Order order); + Task DeleteOrder(Guid id); + Task> GetAllOrders(); + Task UpdateOrder(Guid id, string SenderCity, string SenderAddress, string RecipientCity, string RecipientAddress, float Weight, DateTime PickUpDate); + } +} \ No newline at end of file diff --git a/backend/CargoDelivery.Core/CargoDelivery.Core.csproj b/backend/CargoDelivery.Core/CargoDelivery.Core.csproj new file mode 100644 index 0000000..9f7bc45 --- /dev/null +++ b/backend/CargoDelivery.Core/CargoDelivery.Core.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/backend/CargoDelivery.Core/Models/Order.cs b/backend/CargoDelivery.Core/Models/Order.cs new file mode 100644 index 0000000..18b272f --- /dev/null +++ b/backend/CargoDelivery.Core/Models/Order.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +namespace CargoDelivery.Core.Models; + +public class Order +{ + + public const int MAX_GEO_LENGTH = 250; + public Order(Guid id, string senderCity, string senderAddress, string recipientCity, string recipientAddress, float weight, DateTime pickUpDate) + { + Id = id; + OrderNumber = GenerateOrderNumber(); + SenderCity = senderCity; + SenderAddress = senderAddress; + RecipientCity = recipientCity; + RecipientAddress = recipientAddress; + Weight = weight; + PickUpDate = pickUpDate; + } + + public Guid Id { get; } + public string OrderNumber { get; } = string.Empty; + + public string SenderCity { get; } = string.Empty; + + public string SenderAddress { get; } = string.Empty; + + public string RecipientCity { get; } = string.Empty; + + public string RecipientAddress { get; } = string.Empty; + + public float Weight { get; } + + public DateTime PickUpDate { get; } + + public static (Order Order, string Error) Create(Guid id, string senderCity, string senderAddress, string recipientCity, string recipientAddress, float weight, DateTime pickUpDate) + { + var error = string.Empty; + if (string.IsNullOrEmpty(senderCity) || senderCity.Length > MAX_GEO_LENGTH) + { + error = "Sender's city can not be empty or longer than 250 symbols"; + } + else if (string.IsNullOrEmpty(senderAddress) || senderAddress.Length > MAX_GEO_LENGTH) + { + error = "Sender's address can not be empty or longer than 250 symbols"; + } + else if (string.IsNullOrEmpty(recipientCity) || recipientCity.Length > MAX_GEO_LENGTH) + { + error = "Recipient's city can not be empty or longer than 250 symbols"; + } + else if (string.IsNullOrEmpty(recipientAddress) || recipientAddress.Length > MAX_GEO_LENGTH) + { + error = "Recipient's address can not be empty or longer than 250 symbols"; + } + var order = new Order(id, senderCity, senderAddress, recipientCity, recipientAddress, weight, pickUpDate); + return (order, error); + } + + Random rng = new Random(); + + public string GenerateOrderNumber() + { + long orderPart1 = rng.Next(100000, 9999999); + int orderPart2 = rng.Next(1000, 9999); + string letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + char firstLetter = letters[rng.Next(letters.Length)]; + char secondLetter = letters[rng.Next(letters.Length)]; + string orderNumber = $"{firstLetter}{secondLetter}-{orderPart1}-{orderPart2}"; + return orderNumber; + } + +} \ No newline at end of file diff --git a/backend/CargoDelivery.DataAccess/CargoDelivery.DataAccess.csproj b/backend/CargoDelivery.DataAccess/CargoDelivery.DataAccess.csproj new file mode 100644 index 0000000..41307b4 --- /dev/null +++ b/backend/CargoDelivery.DataAccess/CargoDelivery.DataAccess.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/backend/CargoDelivery.DataAccess/Configurations/OrderConfiguration.cs b/backend/CargoDelivery.DataAccess/Configurations/OrderConfiguration.cs new file mode 100644 index 0000000..4e66aad --- /dev/null +++ b/backend/CargoDelivery.DataAccess/Configurations/OrderConfiguration.cs @@ -0,0 +1,22 @@ +using CargoDelivery.Core.Models; +using CargoDelivery.DataAccess.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + + +namespace CargoDelivery.DataAccess.Configurations; + +public class OrderConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(k => k.Id); + builder.Property(o => o.SenderCity).HasMaxLength(Order.MAX_GEO_LENGTH).IsRequired(); + builder.Property(o => o.SenderAddress).HasMaxLength(Order.MAX_GEO_LENGTH).IsRequired(); + builder.Property(o => o.RecipientCity).HasMaxLength(Order.MAX_GEO_LENGTH).IsRequired(); + builder.Property(o => o.RecipientAddress).HasMaxLength(Order.MAX_GEO_LENGTH).IsRequired(); + builder.Property(o => o.Weight).IsRequired(); + builder.Property(o => o.PickUpDate).IsRequired(); + + } +} diff --git a/backend/CargoDelivery.DataAccess/DatabaseContext.cs b/backend/CargoDelivery.DataAccess/DatabaseContext.cs new file mode 100644 index 0000000..52fb2a2 --- /dev/null +++ b/backend/CargoDelivery.DataAccess/DatabaseContext.cs @@ -0,0 +1,14 @@ +using CargoDelivery.DataAccess.Entities; +using Microsoft.EntityFrameworkCore; + + +namespace CargoDelivery.DataAccess; + +public class DatabaseContext : DbContext +{ + public DatabaseContext(DbContextOptions options) : base(options) + { + } + + public DbSet Orders { get; set; } +} diff --git a/backend/CargoDelivery.DataAccess/Entities/OrderEntity.cs b/backend/CargoDelivery.DataAccess/Entities/OrderEntity.cs new file mode 100644 index 0000000..dbf7fa8 --- /dev/null +++ b/backend/CargoDelivery.DataAccess/Entities/OrderEntity.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CargoDelivery.DataAccess.Entities; + +public class OrderEntity +{ + public Guid Id { get; set; } + public string OrderNumber { get; set; } = string.Empty; + + public string SenderCity { get; set; } = string.Empty; + + public string SenderAddress { get; set; } = string.Empty; + + public string RecipientCity { get; set; } = string.Empty; + + public string RecipientAddress { get; set; } = string.Empty; + + public float Weight { get; set; } + + public DateTime PickUpDate { get; set; } +} diff --git a/backend/CargoDelivery.DataAccess/Migrations/20240706000137_init.Designer.cs b/backend/CargoDelivery.DataAccess/Migrations/20240706000137_init.Designer.cs new file mode 100644 index 0000000..f26c2cd --- /dev/null +++ b/backend/CargoDelivery.DataAccess/Migrations/20240706000137_init.Designer.cs @@ -0,0 +1,67 @@ +// +using System; +using CargoDelivery.DataAccess; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CargoDelivery.DataAccess.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20240706000137_init")] + partial class init + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CargoDelivery.DataAccess.Entities.OrderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("OrderNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("PickUpDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RecipientAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecipientCity") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderCity") + .IsRequired() + .HasColumnType("text"); + + b.Property("Weight") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.ToTable("Orders"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/CargoDelivery.DataAccess/Migrations/20240706000137_init.cs b/backend/CargoDelivery.DataAccess/Migrations/20240706000137_init.cs new file mode 100644 index 0000000..44285b2 --- /dev/null +++ b/backend/CargoDelivery.DataAccess/Migrations/20240706000137_init.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CargoDelivery.DataAccess.Migrations +{ + /// + public partial class init : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Orders", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrderNumber = table.Column(type: "text", nullable: false), + SenderCity = table.Column(type: "text", nullable: false), + SenderAddress = table.Column(type: "text", nullable: false), + RecipientCity = table.Column(type: "text", nullable: false), + RecipientAddress = table.Column(type: "text", nullable: false), + Weight = table.Column(type: "real", nullable: false), + PickUpDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Orders", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Orders"); + } + } +} diff --git a/backend/CargoDelivery.DataAccess/Migrations/DatabaseContextModelSnapshot.cs b/backend/CargoDelivery.DataAccess/Migrations/DatabaseContextModelSnapshot.cs new file mode 100644 index 0000000..07da2b4 --- /dev/null +++ b/backend/CargoDelivery.DataAccess/Migrations/DatabaseContextModelSnapshot.cs @@ -0,0 +1,64 @@ +// +using System; +using CargoDelivery.DataAccess; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CargoDelivery.DataAccess.Migrations +{ + [DbContext(typeof(DatabaseContext))] + partial class DatabaseContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CargoDelivery.DataAccess.Entities.OrderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("OrderNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("PickUpDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RecipientAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecipientCity") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderCity") + .IsRequired() + .HasColumnType("text"); + + b.Property("Weight") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.ToTable("Orders"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/CargoDelivery.DataAccess/Repositories/OrderRepository.cs b/backend/CargoDelivery.DataAccess/Repositories/OrderRepository.cs new file mode 100644 index 0000000..7f1f68f --- /dev/null +++ b/backend/CargoDelivery.DataAccess/Repositories/OrderRepository.cs @@ -0,0 +1,62 @@ +using CargoDelivery.Core.Abstractions; +using CargoDelivery.Core.Models; +using CargoDelivery.DataAccess.Entities; +using Microsoft.EntityFrameworkCore; +using System.Net.WebSockets; + +namespace CargoDelivery.DataAccess.Repositories; + +public class OrderRepository : IOrderRepository +{ + private readonly DatabaseContext _context; + public OrderRepository(DatabaseContext context) + { + _context = context; + } + public async Task> GetOrders() + { + var orderEntities = await _context.Orders.AsNoTracking().ToListAsync(); + var orders = orderEntities + .Select(o => Order.Create(o.Id, o.SenderCity, o.SenderAddress, o.RecipientCity, o.RecipientAddress, o.Weight, o.PickUpDate).Order) + .ToList(); + return orders; + } + public async Task Create(Order order) + { + var orderEntity = new OrderEntity() + { + Id = order.Id, + OrderNumber = order.OrderNumber, + SenderCity = order.SenderCity, + SenderAddress = order.SenderAddress, + RecipientCity = order.RecipientCity, + RecipientAddress = order.RecipientAddress, + Weight = order.Weight, + PickUpDate = order.PickUpDate, + }; + + await _context.Orders.AddAsync(orderEntity); + await _context.SaveChangesAsync(); + + return orderEntity.Id; + } + public async Task Update(Guid id, string SenderCity, string SenderAddress, string RecipientCity, string RecipientAddress, float Weight, DateTime PickUpDate) + { + await _context.Orders.Where(o => o.Id == id).ExecuteUpdateAsync(u => u + .SetProperty(o => o.SenderCity, o => SenderCity) + .SetProperty(o => o.SenderAddress, o => SenderAddress) + .SetProperty(o => o.RecipientCity, o => RecipientCity) + .SetProperty(o => o.RecipientAddress, o => RecipientAddress) + .SetProperty(o => o.Weight, o => Weight) + .SetProperty(o => o.PickUpDate, o => PickUpDate)); + + return id; + } + + public async Task Delete(Guid id) + { + await _context.Orders.Where(b => b.Id == id).ExecuteDeleteAsync(); + + return id; + } +} diff --git a/backend/CargoDelivry.DataAccess/obj/.EntityFrameworkCore.targets b/backend/CargoDelivry.DataAccess/obj/.EntityFrameworkCore.targets new file mode 100644 index 0000000..7d6485d --- /dev/null +++ b/backend/CargoDelivry.DataAccess/obj/.EntityFrameworkCore.targets @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend.rar b/frontend.rar new file mode 100644 index 0000000..8c4bfb0 Binary files /dev/null and b/frontend.rar differ