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:
+ Я решил написать данное приложение с использованием луковой(чистой) архитектуры. Как известно, всё начинается с доменных сущностей:
+
+ 
+ (Все слои и взаимодействия можно увидеть в структкуре проекта.)
+
+ Не буду тянуть. Когда все сущности, модели, контекст, репозитории, интерфейсы, сервисы и контроллеры были написаны, я подключил базу данных PostgreSQL и поднял её в Docker-контейнере.
+ 
+ 
+ ___
+ Можно запускать проект. При запуске у нас открывается страница Swagger'a со следующими http-запросами:
+ 
+ ___
+ Проверим их работоспособность, и первым делом создадим заказ на перевозку груза:
+ 
+ 
+ Видим код "200", радуемся жизни и отправляем GET-запрос и получаем наши заказы, дабы убедиться, что всё не зря:
+ 
+ Перед нами два заказа, один из которых не несёт в себе никакой содержательной информации. Удалим его и проверим работоспособность DELETE-запроса. Копируем id заказа, который хотим удалить и вставляем в поле для заполнения.
+
+
+___
+Теперь представим, что экспедитору попался ненадёжный заказчик и ему нужно подкорректировать детали заказа. Корректируем данные, отправляем UPDATE-запрос, а затем GET-запрос, чтобы убедиться, что внесённые изменения сохранены.
+
+
+
+___
+Наконец, убедимся, что в базе данных всё также сохраняется:
+
+___
+
+## Frontend:
+Главная страница выглядит следующим образом. Мы можем увидеть кнопку для формирования нового заказа и форму для просмотра уже существующих заказов (пока что она пуста):
+
+
+
+Убедимся, что наша база данных также пуста и перейдём к ручному тестированию функционала страницы.
+
+
+___
+Создадим новый заказ, нажимаем кнопку "Отправить". Возращаемся на главную станицу и видим, что заказ отобразился.
+
+
+
+Проверим его наличие в БД:
+
+
+Видим, что заказ отобразился и все данные совпадают с тем, что отражено на странице.
+___
+Перейдём к тестированию кнопок "Просмотр заказа", "Редактировать", "Удалить":
+
+Просмотр осуществляется в режиме чтения. Открываем заказ, попробуем стереть введённые данные:
+
+
+Нажимаем кнопку закрыть и всё возвращается на свои места:
+
+___
+
+Теперь отредактируем наш заказ:
+
+
+Заказ отобразился, при этом заметим, что номер заказа у нас _не редактируется и не меняется_, то есть с точки зрения бизнес-логики это тот же самый заказ, в котором изменились некоторые поля.
+
+
+В базе данных изменения также отображаются:
+
+___
+
+Давайте теперь удалим наш заказ. Для этого воспользуемся кнопкой "Удалить".
+
+
+Из базы данных запись удаляется соответственно:
+
+___
+Теперь проверим синхронизацию бэкенда и фронтенда. Создадим какой-нибудь заказ через Swagger UI.
+
+Обновим страницу и увидим заказ на нашей страничке:
+
+
+___
+Теперь в обратную сторону, проверим как работают http-запросы с заказами, созданными через Razor Pages.
+
+
+
+Выполняем GET-запрос в Swagger UI и он нам возвращает наши заказы.
+
+
+Остальные запросы выполнять не будем, так как очевидно, что они будут работать.
+___
+Подведём итоги.
+Технические требования к реализации:
+
+- [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