390 lines
17 KiB
C#
390 lines
17 KiB
C#
using System.Security.Claims;
|
||
using MediatR;
|
||
using Microsoft.AspNetCore.Authorization;
|
||
using Microsoft.AspNetCore.Builder;
|
||
using Microsoft.AspNetCore.Http;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Microsoft.AspNetCore.Routing;
|
||
using Nashel.Modules.Order.Application.Commands;
|
||
using Nashel.Modules.Order.Application.Queries;
|
||
using Nashel.Modules.Order.Domain.Entities;
|
||
using Nashel.Modules.Order.Domain.Enums;
|
||
|
||
namespace Nashel.Modules.Order.Presentation;
|
||
|
||
public static class OrderEndpoints
|
||
{
|
||
public static void MapOrderEndpoints(this IEndpointRouteBuilder app)
|
||
{
|
||
// ─── Создание заказа (временно без авторизации) ─────────────────────────────
|
||
// AllowAnonymous не перекрывает RequireAuthorization на группе в Minimal API,
|
||
// поэтому создание вынесено в отдельную группу.
|
||
app.MapPost("/api/orders", async ([FromBody] CreateOrderRequest request, ISender sender) =>
|
||
{
|
||
var command = new CreateOrderCommand(
|
||
request.CustomerId,
|
||
request.CustomerName,
|
||
request.ServiceId,
|
||
request.ServiceTitle,
|
||
request.PriceAmount,
|
||
request.PriceType,
|
||
request.Type,
|
||
request.Address,
|
||
request.Latitude,
|
||
request.Longitude,
|
||
request.Deadline,
|
||
request.PerformerId,
|
||
request.PerformerName
|
||
);
|
||
var orderId = await sender.Send(command);
|
||
return Results.Ok(orderId);
|
||
})
|
||
.WithTags("Заказы")
|
||
.WithName("CreateOrder")
|
||
.WithOpenApi(op => new(op)
|
||
{
|
||
Summary = "Создать заказ",
|
||
Description = "Создаёт прямой заказ (конкретному мастеру) или публичную заявку."
|
||
})
|
||
.AllowAnonymous();
|
||
|
||
var group = app.MapGroup("/api/orders")
|
||
.WithTags("Заказы")
|
||
.RequireAuthorization();
|
||
|
||
// ─── Получить мои заказы (как заказчик) ─────────────────────────────────────
|
||
group.MapGet("/as-customer/{userId:guid}", async (Guid userId, ISender sender) =>
|
||
{
|
||
var result = await sender.Send(new GetMyOrdersAsCustomerQuery(userId));
|
||
return Results.Ok(result);
|
||
})
|
||
.WithName("GetMyOrdersAsCustomer")
|
||
.WithOpenApi(op => new(op)
|
||
{
|
||
Summary = "Мои заказы (роль: Заказчик)",
|
||
Description = "Возвращает список всех заказов, где текущий пользователь является заказчиком, отсортированных по дате создания."
|
||
});
|
||
|
||
// ─── Получить мои заказы (как исполнитель) ───────────────────────────────────
|
||
group.MapGet("/as-performer/{userId:guid}", async (Guid userId, ISender sender) =>
|
||
{
|
||
var result = await sender.Send(new GetMyOrdersAsPerformerQuery(userId));
|
||
return Results.Ok(result);
|
||
})
|
||
.WithName("GetMyOrdersAsPerformer")
|
||
.WithOpenApi(op => new(op)
|
||
{
|
||
Summary = "Мои заказы (роль: Исполнитель)",
|
||
Description = "Возвращает список всех заказов, где текущий пользователь является назначенным исполнителем."
|
||
});
|
||
|
||
// ─── Принять заказ (мастер) ──────────────────────────────────────────────────
|
||
group.MapPost("/{id:guid}/accept", async (Guid id, ISender sender) =>
|
||
{
|
||
var command = new AcceptOrderCommand(id);
|
||
await sender.Send(command);
|
||
return Results.Ok();
|
||
})
|
||
.WithName("AcceptOrder")
|
||
.WithOpenApi(op => new(op)
|
||
{
|
||
Summary = "Принять заказ",
|
||
Description = "Исполнитель принимает заказ. Переводит заказ в статус «В работе»."
|
||
});
|
||
|
||
// ─── Пометить выполненным (мастер) ──────────────────────────────────────────
|
||
group.MapPost("/{id:guid}/finish", async (Guid id, FinishOrderRequest request, ISender sender) =>
|
||
{
|
||
var command = new FinishOrderCommand(id, request.PerformerId);
|
||
await sender.Send(command);
|
||
return Results.Ok();
|
||
})
|
||
.WithName("FinishOrder")
|
||
.WithOpenApi(op => new(op)
|
||
{
|
||
Summary = "Завершить работу (мастер)",
|
||
Description = "Исполнитель помечает работу как выполненную. Заказ переходит в статус подтверждения заказчиком."
|
||
});
|
||
|
||
// ─── Подтвердить выполнение (заказчик) ───────────────────────────────────────
|
||
group.MapPost("/{id:guid}/confirm", async (Guid id, ConfirmOrderCompletionRequest request, ISender sender) =>
|
||
{
|
||
var command = new ConfirmOrderCompletionCommand(id, request.CustomerId);
|
||
await sender.Send(command);
|
||
return Results.Ok();
|
||
})
|
||
.WithName("ConfirmOrderCompletion")
|
||
.WithOpenApi(op => new(op)
|
||
{
|
||
Summary = "Подтвердить выполнение (заказчик)",
|
||
Description = "Заказчик подтверждает, что работа выполнена успешно. Статус заказа становится «Завершён»."
|
||
});
|
||
|
||
// ─── Открыть спор (заказчик) ──────────────────────────────────────────────────
|
||
group.MapPost("/{id:guid}/dispute", async (Guid id, OpenDisputeRequest request, ISender sender) =>
|
||
{
|
||
var command = new OpenOrderDisputeCommand(
|
||
id,
|
||
request.CustomerId,
|
||
request.Reason,
|
||
request.Description,
|
||
request.ProposedSolution,
|
||
request.Evidence);
|
||
await sender.Send(command);
|
||
return Results.Ok();
|
||
})
|
||
.WithName("OpenOrderDispute")
|
||
.WithOpenApi(op => new(op)
|
||
{
|
||
Summary = "Открыть спор",
|
||
Description = "Заказчик открывает спор, если работа выполнена некачественно или не в полном объеме."
|
||
});
|
||
|
||
// ─── Ответить на спор (мастер) ───────────────────────────────────────────────
|
||
group.MapPost("/{id:guid}/dispute/respond", async (Guid id, RespondToDisputeRequest request, ISender sender) =>
|
||
{
|
||
var command = new RespondToOrderDisputeCommand(
|
||
id,
|
||
request.PerformerId,
|
||
request.Position,
|
||
request.Evidence,
|
||
request.CounterSolution);
|
||
await sender.Send(command);
|
||
return Results.Ok();
|
||
})
|
||
.WithName("RespondToDispute")
|
||
.WithOpenApi(op => new(op)
|
||
{
|
||
Summary = "Ответить на спор",
|
||
Description = "Исполнитель предоставляет свою позицию и доказательства в рамках открытого спора."
|
||
});
|
||
|
||
// ─── Возразить на ответ исполнителя (заказчик) ──────────────────────────────
|
||
group.MapPost("/{id:guid}/dispute/rebut", async (Guid id, RebutDisputeRequest request, ISender sender) =>
|
||
{
|
||
var command = new RebutDisputeCommand(
|
||
id,
|
||
request.CustomerId,
|
||
request.Comment,
|
||
request.Evidence,
|
||
request.CounterSolution);
|
||
await sender.Send(command);
|
||
return Results.Ok();
|
||
})
|
||
.WithName("RebutDispute")
|
||
.WithOpenApi(op => new(op)
|
||
{
|
||
Summary = "Возразить на ответ исполнителя",
|
||
Description = "Заказчик добавляет новые доказательства или требования в ответ на позицию исполнителя."
|
||
});
|
||
|
||
// ─── Принять условия спора (заказчик) ──────────────────────────────────────
|
||
group.MapPost("/{id:guid}/dispute/accept", async (Guid id, AcceptDisputeTermsRequest request, ISender sender) =>
|
||
{
|
||
var command = new AcceptDisputeTermsCommand(id, request.CustomerId);
|
||
await sender.Send(command);
|
||
return Results.Ok();
|
||
})
|
||
.WithName("AcceptDisputeTerms")
|
||
.WithOpenApi(op => new(op)
|
||
{
|
||
Summary = "Принять условия спора",
|
||
Description = "Заказчик принимает предложенное исполнителем решение. Заказ завершается."
|
||
});
|
||
|
||
// ─── Отменить заказ ──────────────────────────────────────────────────────────
|
||
group.MapPost("/{id:guid}/cancel", async (Guid id, CancelOrderRequest request, ISender sender) =>
|
||
{
|
||
var command = new CancelOrderCommand(id, request.RequesterId, request.Reason);
|
||
await sender.Send(command);
|
||
return Results.Ok();
|
||
})
|
||
.WithName("CancelOrder")
|
||
.WithOpenApi(op => new(op)
|
||
{
|
||
Summary = "Отменить заказ",
|
||
Description = "Заказчик или исполнитель отменяет заказ с указанием причины."
|
||
});
|
||
|
||
// ─── Добавить отклик (мастер на публичный заказ) ─────────────────────────────
|
||
group.MapPost("/{id:guid}/applications", async (Guid id, AddApplicationRequest request, ISender sender) =>
|
||
{
|
||
var command = new AddApplicationCommand(id, request.PerformerId, request.Amount, request.Currency, request.Comment);
|
||
await sender.Send(command);
|
||
return Results.Ok();
|
||
})
|
||
.WithName("AddOrderApplication")
|
||
.WithOpenApi(op => new(op)
|
||
{
|
||
Summary = "Откликнуться на заказ",
|
||
Description = "Исполнитель отправляет отклик на публичный заказ с указанием цены и комментария."
|
||
});
|
||
|
||
// ─── Выбрать исполнителя из откликов (заказчик) ─────────────────────────────
|
||
group.MapPost("/{id:guid}/select-performer", async (Guid id, SelectPerformerRequest request, ISender sender) =>
|
||
{
|
||
var command = new SelectPerformerCommand(id, request.PerformerId);
|
||
await sender.Send(command);
|
||
return Results.Ok();
|
||
})
|
||
.WithName("SelectPerformer")
|
||
.WithOpenApi(op => new(op)
|
||
{
|
||
Summary = "Выбрать исполнителя",
|
||
Description = "Заказчик выбирает конкретного исполнителя из откликнувшихся. Запускает SLA-таймер 60 минут."
|
||
});
|
||
}
|
||
}
|
||
|
||
// ─── Request DTOs ────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Запрос на создание заказа.
|
||
/// </summary>
|
||
public record CreateOrderRequest
|
||
{
|
||
/// <summary>Идентификатор заказчика.</summary>
|
||
public Guid CustomerId { get; init; }
|
||
|
||
/// <summary>ФИО заказчика.</summary>
|
||
public string CustomerName { get; init; } = default!;
|
||
|
||
/// <summary>Идентификатор услуги.</summary>
|
||
public Guid ServiceId { get; init; }
|
||
|
||
/// <summary>Название услуги.</summary>
|
||
public string ServiceTitle { get; init; } = default!;
|
||
|
||
/// <summary>Сумма услуги.</summary>
|
||
public decimal PriceAmount { get; init; }
|
||
|
||
/// <summary>Тип цены (0=Фикс, 1=Почасовая, 2=Договорная).</summary>
|
||
public PriceType PriceType { get; init; }
|
||
|
||
/// <summary>Тип заказа: Direct (прямой) или PublicJob (публичный).</summary>
|
||
public OrderType Type { get; init; }
|
||
|
||
/// <summary>Адрес выполнения заказа (текстовый).</summary>
|
||
public string Address { get; init; } = default!;
|
||
|
||
/// <summary>Широта места выполнения.</summary>
|
||
public double Latitude { get; init; }
|
||
|
||
/// <summary>Долгота места выполнения.</summary>
|
||
public double Longitude { get; init; }
|
||
|
||
/// <summary>Крайний срок выполнения (необязательно).</summary>
|
||
public DateTime? Deadline { get; init; }
|
||
|
||
/// <summary>Идентификатор исполнителя (обязателен для прямого заказа).</summary>
|
||
public Guid? PerformerId { get; init; }
|
||
|
||
/// <summary>Имя исполнителя (обязателен для прямого заказа).</summary>
|
||
public string? PerformerName { get; init; }
|
||
|
||
public CreateOrderRequest() { }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Запрос принятия заказа исполнителем (Пустой, так как ID берется из токена).
|
||
/// </summary>
|
||
public record AcceptOrderRequest();
|
||
|
||
/// <summary>
|
||
/// Запрос завершения заказа.
|
||
/// </summary>
|
||
public record CompleteOrderRequest
|
||
{
|
||
/// <summary>Идентификатор пользователя (заказчик или исполнитель).</summary>
|
||
public Guid RequesterId { get; init; }
|
||
|
||
public CompleteOrderRequest() { }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Запрос отмены заказа.
|
||
/// </summary>
|
||
public record CancelOrderRequest
|
||
{
|
||
/// <summary>Идентификатор пользователя, инициирующего отмену.</summary>
|
||
public Guid RequesterId { get; init; }
|
||
|
||
/// <summary>Причина отмены (необязательно).</summary>
|
||
public string? Reason { get; init; }
|
||
|
||
public CancelOrderRequest() { }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Запрос добавления отклика на заказ.
|
||
/// </summary>
|
||
public record AddApplicationRequest
|
||
{
|
||
/// <summary>Идентификатор исполнителя.</summary>
|
||
public Guid PerformerId { get; init; }
|
||
|
||
/// <summary>Предлагаемая сумма.</summary>
|
||
public decimal Amount { get; init; }
|
||
|
||
/// <summary>Валюта (например, RUB).</summary>
|
||
public string Currency { get; init; } = "RUB";
|
||
|
||
/// <summary>Комментарий к отклику.</summary>
|
||
public string Comment { get; init; } = default!;
|
||
|
||
public AddApplicationRequest() { }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Запрос выбора исполнителя.
|
||
/// </summary>
|
||
public record SelectPerformerRequest
|
||
{
|
||
/// <summary>Идентификатор выбранного исполнителя.</summary>
|
||
public Guid PerformerId { get; init; }
|
||
|
||
public SelectPerformerRequest() { }
|
||
}
|
||
/// <summary>
|
||
/// Запрос пометки выполнения заказа.
|
||
/// </summary>
|
||
public record FinishOrderRequest(Guid PerformerId);
|
||
|
||
/// <summary>
|
||
/// Запрос подтверждения выполнения.
|
||
/// </summary>
|
||
public record ConfirmOrderCompletionRequest(Guid CustomerId);
|
||
|
||
/// <summary>
|
||
/// Запрос открытия спора.
|
||
/// </summary>
|
||
public record OpenDisputeRequest(
|
||
Guid CustomerId,
|
||
DisputeReason Reason,
|
||
string Description,
|
||
string ProposedSolution,
|
||
List<string> Evidence);
|
||
|
||
/// <summary>
|
||
/// Запрос ответа на спор.
|
||
/// </summary>
|
||
public record RespondToDisputeRequest(
|
||
Guid PerformerId,
|
||
string Position,
|
||
List<string> Evidence,
|
||
string? CounterSolution);
|
||
|
||
/// <summary>
|
||
/// Запрос на возражение по спору.
|
||
/// </summary>
|
||
public record RebutDisputeRequest(
|
||
Guid CustomerId,
|
||
string Comment,
|
||
List<string> Evidence,
|
||
string? CounterSolution);
|
||
|
||
/// <summary>
|
||
/// Запрос на принятие условий спора.
|
||
/// </summary>
|
||
public record AcceptDisputeTermsRequest(Guid CustomerId);
|