< Summary

Information
Class: OzonOrdersWeb.Areas.PartnerOffice.Controllers.PartnersController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/PartnerOffice/Controllers/PartnersController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 105
Coverable lines: 105
Total lines: 221
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 66
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
get_OwnerId()0%4260%
Index()100%210%
Create()100%210%
Create()0%2040%
Edit()0%620%
UpdateMain()0%4260%
SendInvite()0%620%
SetPermissions()0%7280%
SaveRules()0%2040%
PreviewRules()0%2040%
Delete()0%620%
Audit()0%110100%
AuditGrid()0%4260%
AuditGridFacets()0%4260%
AuditSource(...)0%2040%
Back(...)0%2040%
LoadLookupsAsync()100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/PartnerOffice/Controllers/PartnersController.cs

#LineLine coverage
 1using System.Security.Claims;
 2using Microsoft.AspNetCore.Authorization;
 3using Microsoft.AspNetCore.Mvc;
 4using Microsoft.EntityFrameworkCore;
 5using OzonDomains;
 6using OzonDomains.Models.Authorization.Enums;
 7using OzonDomains.Models.Partners;
 8using OzonOrdersWeb.Areas.PartnerOffice.Models;
 9using OzonOrdersWeb.Authorization;
 10using OzonOrdersWeb.WebServices.Grid;
 11using OzonOrdersWeb.WebServices.Journals;
 12using OzonOrdersWeb.WebServices.Partners;
 13using OzonRepositories.Context;
 14using Services.Authorization;
 15using Services.PartnerServices;
 16
 17namespace OzonOrdersWeb.Areas.PartnerOffice.Controllers;
 18
 19/// <summary>
 20/// Кабинет владельца: создание и настройка партнёров. Всё управление живёт в Studio2,
 21/// сервис PartnerOffice нужен только самим партнёрам.
 22/// </summary>
 23[Area("PartnerOffice")]
 24[Authorize(Roles = AuthorizationConstants.SuperAdminRoleName + ",Admin")]
 25[Route("PartnerOffice/[controller]/[action]/{id?}")]
 026public sealed class PartnersController(
 027    IPartnerManagementService management,
 028    IPartnerAuthService auth,
 029    IPartnerOrdersService orders,
 030    ApplicationDbContext db) : Controller
 31{
 032    private string OwnerId => User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.Identity?.Name ?? "owner";
 33
 34    [HttpGet]
 35    [HttpGet("~/PartnerOffice/Partners")]
 36    public async Task<IActionResult> Index(CancellationToken ct) =>
 037        View(PartnerGridRows.Build(await management.ListAsync(ct)));
 38
 39    [HttpGet]
 040    public IActionResult Create() => View(new PartnerCreateForm());
 41
 42    [HttpPost, ValidateAntiForgeryToken]
 43    public async Task<IActionResult> Create(PartnerCreateForm form, CancellationToken ct)
 44    {
 045        var result = await management.CreateAsync(
 046            new CreatePartnerRequest(form.Name, form.Email, form.GoogleEmail), OwnerId, ct);
 047        if (!result.Ok)
 48        {
 049            ModelState.AddModelError(string.Empty, result.Error!);
 050            return View(form);
 51        }
 52
 053        var invite = await auth.SendInviteAsync(result.Value!.Id, OwnerId, ct);
 054        if (invite.Ok)
 055            TempData["Success"] = $"Партнёр создан, приглашение отправлено на {result.Value.Email}. Теперь задайте прави
 56        else
 057            TempData["Error"] = $"Партнёр создан, но приглашение не ушло: {invite.Error}";
 058        return RedirectToAction(nameof(Edit), new { id = result.Value.Id });
 059    }
 60
 61    [HttpGet]
 62    public async Task<IActionResult> Edit(long id, CancellationToken ct)
 63    {
 064        var partner = await management.GetAsync(id, ct);
 065        if (partner is null) return NotFound();
 066        return View(new PartnerEditViewModel(partner, await LoadLookupsAsync(ct)));
 067    }
 68
 69    [HttpPost, ValidateAntiForgeryToken]
 70    public async Task<IActionResult> UpdateMain(long id, PartnerMainForm form, CancellationToken ct)
 71    {
 072        var channels = PartnerNotificationChannels.None;
 073        if (form.NotifyEmail) channels |= PartnerNotificationChannels.Email;
 074        if (form.NotifyTelegram) channels |= PartnerNotificationChannels.Telegram;
 75
 076        var result = await management.UpdateAsync(id,
 077            new UpdatePartnerRequest(form.Name, form.Email, form.GoogleEmail, form.IsActive, channels, form.Notification
 078            OwnerId, ct);
 079        return Back(id, result.Ok ? "Сохранено." : null, result.Error);
 080    }
 81
 82    /// <summary>Повторно отправляет приглашение со ссылкой и новым кодом. Прежнее приглашение перестаёт действовать.</s
 83    [HttpPost, ValidateAntiForgeryToken]
 84    public async Task<IActionResult> SendInvite(long id, CancellationToken ct)
 85    {
 086        var result = await auth.SendInviteAsync(id, OwnerId, ct);
 087        return Back(id, result.Ok ? "Приглашение отправлено." : null, result.Error);
 088    }
 89
 90    [HttpPost, ValidateAntiForgeryToken]
 91    public async Task<IActionResult> SetPermissions(long id, PartnerPermissionsForm form, CancellationToken ct)
 92    {
 093        var codes = new List<PartnerPermissionCode>();
 094        if (form.LabelPrint) codes.Add(PartnerPermissionCode.LabelPrint);
 095        if (form.Assemble) codes.Add(PartnerPermissionCode.Assemble);
 096        if (form.TrackNumber) codes.Add(PartnerPermissionCode.TrackNumber);
 97
 098        var result = await management.SetPermissionsAsync(id, codes, OwnerId, ct);
 099        return Back(id, result.Ok ? "Возможности сохранены." : null, result.Error);
 0100    }
 101
 102    /// <summary>Сохраняет правила целиком. Принимает JSON из редактора правил.</summary>
 103    [HttpPost, ValidateAntiForgeryToken]
 104    public async Task<IActionResult> SaveRules(long id, [FromBody] SaveRulesBody body, CancellationToken ct)
 105    {
 0106        var result = await management.SaveRulesAsync(id, body.Rules ?? [], OwnerId, ct);
 0107        return result.Ok
 0108            ? Json(new { ok = true, rules = result.Value!.Rules })
 0109            : BadRequest(new { ok = false, error = result.Error });
 0110    }
 111
 112    /// <summary>Сколько заказов подойдёт под правила прямо сейчас и id первых для ссылки на таблицу заказов. Ничего не 
 113    [HttpPost, ValidateAntiForgeryToken]
 114    public async Task<IActionResult> PreviewRules([FromBody] SaveRulesBody body, CancellationToken ct)
 115    {
 0116        var rules = body.Rules ?? [];
 0117        var error = PartnerManagementService.ValidateRules(rules);
 0118        if (error is not null) return BadRequest(new { ok = false, error });
 0119        var preview = await orders.PreviewAsync(rules, ct: ct);
 0120        return Json(new { ok = true, count = preview.Count, ids = preview.Ids, truncated = preview.Truncated, limit = pr
 0121    }
 122
 123    [HttpPost, ValidateAntiForgeryToken]
 124    public async Task<IActionResult> Delete(long id, CancellationToken ct)
 125    {
 0126        var result = await management.DeleteAsync(id, OwnerId, ct);
 0127        if (!result.Ok)
 128        {
 0129            TempData["Error"] = result.Error;
 0130            return RedirectToAction(nameof(Index));
 131        }
 132
 0133        TempData["Success"] = "Партнёр удалён.";
 0134        return RedirectToAction(nameof(Index));
 0135    }
 136
 137    private const string AuditPageCode = "PartnerOffice.Partners.Audit";
 138
 139    /// <summary>Каркас страницы журнала: строки грид подгружает блоками через <see cref="AuditGrid"/>.</summary>
 140    [HttpGet]
 141    public async Task<IActionResult> Audit(long? partnerId, CancellationToken ct)
 142    {
 0143        var partners = await management.ListAsync(ct);
 0144        ViewBag.PartnerId = partnerId;
 0145        ViewBag.PartnerName = partnerId is { } id ? partners.FirstOrDefault(p => p.Id == id)?.Name : null;
 0146        ViewBag.TotalCount = await db.PartnerAuditLogs.CountAsync(ct);
 0147        return View();
 0148    }
 149
 150    [HttpPost("~/PartnerOffice/Partners/Audit/grid")]
 151    [ValidateAntiForgeryToken]
 152    [PageAuthorize(AuditPageCode, AccessType.View)]
 153    public async Task<IActionResult> AuditGrid([FromBody] ServerGridRequest? request, CancellationToken ct)
 154    {
 0155        var error = ServerGridQuery.Validate(request);
 0156        if (error != null) return BadRequest(new { success = false, error });
 157
 0158        var def = JournalGridDefinitions.PartnerAudit;
 0159        var filtered = ServerGridQuery.ApplyFilters(AuditSource(request!), def, request!);
 0160        var total = await filtered.CountAsync(ct);
 0161        var start = Math.Max(0, request!.StartRow);
 0162        var take = Math.Clamp(request.EndRow - start, 1, ServerGridQuery.MaxBlockSize);
 0163        var page = start >= total
 0164            ? []
 0165            : await ServerGridQuery.ApplySort(filtered, def, request.SortModel).Skip(start).Take(take).ToListAsync(ct);
 166
 0167        var ids = page.Where(r => r.PartnerId != null).Select(r => r.PartnerId!.Value).Distinct().ToList();
 0168        var names = await db.Partners.AsNoTracking().Where(p => ids.Contains(p.Id)).ToDictionaryAsync(p => p.Id, p => p.
 0169        var rows = PartnerGridRows.BuildAudit(page, names);
 0170        var lastRow = start + rows.Count >= total ? total : -1;
 0171        return Ok(new { success = true, rows, totalCount = total, lastRow });
 0172    }
 173
 174    [HttpPost("~/PartnerOffice/Partners/Audit/grid/facets")]
 175    [ValidateAntiForgeryToken]
 176    [PageAuthorize(AuditPageCode, AccessType.View)]
 177    public async Task<IActionResult> AuditGridFacets([FromBody] ServerGridRequest? request, CancellationToken ct)
 178    {
 0179        if (request is null || string.IsNullOrWhiteSpace(request.ColId)) return BadRequest(new { success = false, error 
 0180        request.StartRow = 0;
 0181        request.EndRow = 1;
 0182        var error = ServerGridQuery.Validate(request);
 0183        if (error != null) return BadRequest(new { success = false, error });
 0184        var values = await ServerGridQuery.FacetsAsync(AuditSource(request), JournalGridDefinitions.PartnerAudit, reques
 0185        return Ok(new { success = true, values });
 0186    }
 187
 188    /// <summary>Журнал одного партнёра, если страница открыта с ?partnerId=.</summary>
 189    private IQueryable<PartnerAuditLog> AuditSource(ServerGridRequest request)
 190    {
 0191        IQueryable<PartnerAuditLog> source = db.PartnerAuditLogs.AsNoTracking();
 0192        if (request.Params.TryGetValue("partnerId", out var raw) && long.TryParse(raw, out var partnerId))
 0193            source = source.Where(x => x.PartnerId == partnerId);
 0194        return source;
 195    }
 196
 197    private IActionResult Back(long id, string? success, string? error)
 198    {
 0199        if (success is not null) TempData["Success"] = success;
 0200        if (error is not null) TempData["Error"] = error;
 0201        return RedirectToAction(nameof(Edit), new { id });
 202    }
 203
 204    private async Task<RuleLookups> LoadLookupsAsync(CancellationToken ct)
 205    {
 0206        var statuses = await db.AppStatuses.AsNoTracking().OrderBy(x => x.Name).ToListAsync(ct);
 0207        var warehouses = await db.Warehouses.AsNoTracking().OrderBy(x => x.Name).ToListAsync(ct);
 0208        var clients = await db.OzonClients.AsNoTracking().OrderBy(x => x.Name).ToListAsync(ct);
 209
 0210        return new RuleLookups(
 0211            statuses.Select(x => new RuleLookupOption(x.Id.ToString(), x.Name ?? $"#{x.Id}")).ToList(),
 0212            warehouses.Select(x => new RuleLookupOption(x.Id.ToString(), x.Name ?? $"#{x.Id}")).ToList(),
 0213            clients.Select(x => new RuleLookupOption(x.Id.ToString(), x.Name)).ToList(),
 0214            [new RuleLookupOption(nameof(ClientType.OZON), "Ozon"), new RuleLookupOption(nameof(ClientType.YANDEX), "Янд
 0215    }
 216}
 217
 218public sealed class SaveRulesBody
 219{
 220    public List<PartnerRuleDto>? Rules { get; set; }
 221}