food-market/src/food-market.domain/Organizations/Employee.cs
nns 6b959ac59d
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
feat(pos): быстрый вход кассира по PIN (grant_type=pin) + lockout
- Сервер: Employee.PinHash (PBKDF2, миграция Phase36); POST
  /api/organization/employees/{id}/pin — админ задаёт/снимает PIN (4–6 цифр,
  уникален среди активных сотрудников на пересекающихся точках). Кастомный
  OpenIddict-грант grant_type=pin (retail_point_id + pin): находит кассира,
  привязанного к точке, по PIN и выдаёт токены. Брутфорс-защита — in-memory по
  точке (5 неверных → блок 5 мин). Клиент-сидер досоздаёт gt:pin существующим
  клиентам (без пересоздания). Пароль остаётся запасным.
- Касса: PIN-пад на зарегистрированном устройстве (маскированный ввод, 0–9/⌫/
  Войти), переключение на вход по логину; AuthService.PinLoginAsync.
- Тесты: unit PinHasher (9), integration (3): вход по PIN выдаёт рабочий токен;
  5 неверных → блок; дубль PIN на точке → 409. + скриншот PIN-пада в галерее.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:38:36 +05:00

66 lines
3.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using foodmarket.Domain.Common;
namespace foodmarket.Domain.Organizations;
/// <summary>Сотрудник организации. Может быть привязан к учётной записи
/// (UserId), а может существовать без логина (например, кассир, которого
/// добавили в HR, но логин ещё не выдали).</summary>
public class Employee : TenantEntity
{
/// <summary>FK на Identity-юзера. Заполняется когда сотруднику выдан логин.
/// Null — запись без учётки.</summary>
public Guid? UserId { get; set; }
public string LastName { get; set; } = "";
public string FirstName { get; set; } = "";
public string? MiddleName { get; set; }
public string? Position { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
/// <summary>Оклад в валюте организации, опц.</summary>
public decimal? Salary { get; set; }
/// <summary>ИИН (12 цифр для физлица в РК), опционально.</summary>
public string? TaxNumber { get; set; }
/// <summary>Произвольное описание (комментарий HR'а).</summary>
public string? Description { get; set; }
/// <summary>Аватар сотрудника. URL до файла в общем images-стораж.</summary>
public string? ImageUrl { get; set; }
public Guid RoleId { get; set; }
public EmployeeRole Role { get; set; } = null!;
/// <summary>Активен ли сотрудник. False — заблокирован, не может логиниться.
/// Двухступенчатое удаление: сначала «Уволить» (IsActive=false + FiredAt),
/// затем «Удалить» (IsDeleted=true + DeletedAt). Физически не удаляем
/// никогда — у сотрудника есть FK из документов (продаж, поставок).</summary>
public bool IsActive { get; set; } = true;
public DateTime? FiredAt { get; set; }
/// <summary>Soft-delete-флаг. Ставится только из состояния «уволен» (IsActive=false).
/// В UI и связанных документах отображается «Иванов И.И. (удалён)».
/// В списках сотрудников по умолчанию скрыты, доступны через фильтр.</summary>
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
public ICollection<EmployeeRetailPointAssignment> RetailPointAssignments { get; set; }
= new List<EmployeeRetailPointAssignment>();
// ─ Быстрый вход кассира по PIN на зарегистрированном устройстве ─────
/// <summary>Хэш PIN-кода (PBKDF2 с солью, base64). null — PIN не задан,
/// вход только по паролю. Задаёт администратор в карточке сотрудника.
/// Брутфорс-защита (5 неверных → блок 5 мин) — на уровне точки (in-memory),
/// т.к. вход по PIN без логина не идентифицирует конкретного сотрудника.</summary>
public string? PinHash { get; set; }
}
/// <summary>Привязка сотрудника к кассе (для роли Кассир): к каким RetailPoint'ам
/// он может вставать. Если назначений нет — может ко всем (поведение по умолчанию).</summary>
public class EmployeeRetailPointAssignment : TenantEntity
{
public Guid EmployeeId { get; set; }
public Employee Employee { get; set; } = null!;
public Guid RetailPointId { get; set; }
}