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 ────────────────────────────────────────────────────────────
///
/// Запрос на создание заказа.
///
public record CreateOrderRequest
{
/// Идентификатор заказчика.
public Guid CustomerId { get; init; }
/// ФИО заказчика.
public string CustomerName { get; init; } = default!;
/// Идентификатор услуги.
public Guid ServiceId { get; init; }
/// Название услуги.
public string ServiceTitle { get; init; } = default!;
/// Сумма услуги.
public decimal PriceAmount { get; init; }
/// Тип цены (0=Фикс, 1=Почасовая, 2=Договорная).
public PriceType PriceType { get; init; }
/// Тип заказа: Direct (прямой) или PublicJob (публичный).
public OrderType Type { get; init; }
/// Адрес выполнения заказа (текстовый).
public string Address { get; init; } = default!;
/// Широта места выполнения.
public double Latitude { get; init; }
/// Долгота места выполнения.
public double Longitude { get; init; }
/// Крайний срок выполнения (необязательно).
public DateTime? Deadline { get; init; }
/// Идентификатор исполнителя (обязателен для прямого заказа).
public Guid? PerformerId { get; init; }
/// Имя исполнителя (обязателен для прямого заказа).
public string? PerformerName { get; init; }
public CreateOrderRequest() { }
}
///
/// Запрос принятия заказа исполнителем (Пустой, так как ID берется из токена).
///
public record AcceptOrderRequest();
///
/// Запрос завершения заказа.
///
public record CompleteOrderRequest
{
/// Идентификатор пользователя (заказчик или исполнитель).
public Guid RequesterId { get; init; }
public CompleteOrderRequest() { }
}
///
/// Запрос отмены заказа.
///
public record CancelOrderRequest
{
/// Идентификатор пользователя, инициирующего отмену.
public Guid RequesterId { get; init; }
/// Причина отмены (необязательно).
public string? Reason { get; init; }
public CancelOrderRequest() { }
}
///
/// Запрос добавления отклика на заказ.
///
public record AddApplicationRequest
{
/// Идентификатор исполнителя.
public Guid PerformerId { get; init; }
/// Предлагаемая сумма.
public decimal Amount { get; init; }
/// Валюта (например, RUB).
public string Currency { get; init; } = "RUB";
/// Комментарий к отклику.
public string Comment { get; init; } = default!;
public AddApplicationRequest() { }
}
///
/// Запрос выбора исполнителя.
///
public record SelectPerformerRequest
{
/// Идентификатор выбранного исполнителя.
public Guid PerformerId { get; init; }
public SelectPerformerRequest() { }
}
///
/// Запрос пометки выполнения заказа.
///
public record FinishOrderRequest(Guid PerformerId);
///
/// Запрос подтверждения выполнения.
///
public record ConfirmOrderCompletionRequest(Guid CustomerId);
///
/// Запрос открытия спора.
///
public record OpenDisputeRequest(
Guid CustomerId,
DisputeReason Reason,
string Description,
string ProposedSolution,
List Evidence);
///
/// Запрос ответа на спор.
///
public record RespondToDisputeRequest(
Guid PerformerId,
string Position,
List Evidence,
string? CounterSolution);
///
/// Запрос на возражение по спору.
///
public record RebutDisputeRequest(
Guid CustomerId,
string Comment,
List Evidence,
string? CounterSolution);
///
/// Запрос на принятие условий спора.
///
public record AcceptDisputeTermsRequest(Guid CustomerId);