openapi: 3.1.0 info: title: Krona Staff API version: 0.3.1 description: API посещаемости Krona Staff для кабинета сотрудника, терминала и табеля руководителя. license: name: Proprietary url: https://git.ochenstarik.ru/ochenstarik/krona-staff/src/branch/main/LICENSE servers: - url: /api/v1/staff security: [] paths: /live: get: summary: Check process liveness operationId: getLiveness tags: [Operations] responses: '200': description: Process is alive content: application/json: schema: { $ref: '#/components/schemas/Health' } '400': { $ref: '#/components/responses/BadRequest' } /ready: get: summary: Check dependency readiness operationId: getReadiness tags: [Operations] responses: '200': description: Required dependencies are ready content: application/json: schema: { $ref: '#/components/schemas/Health' } '503': { description: A required dependency is unavailable } '400': { $ref: '#/components/responses/BadRequest' } /version: get: summary: Read runtime build identity operationId: getVersion tags: [Operations] responses: '200': description: Runtime build identity content: application/json: schema: { $ref: '#/components/schemas/Version' } '400': { $ref: '#/components/responses/BadRequest' } /session: post: summary: Открыть защищённую сессию администратора operationId: createStaffSession tags: [Authentication] requestBody: required: true content: application/json: schema: type: object additionalProperties: false required: [access_token] properties: access_token: { type: string, minLength: 32, maxLength: 4096, writeOnly: true } responses: '200': description: Сессия открыта; токен перенесён в HttpOnly Secure cookie '401': { description: Ключ недействителен } '403': { description: Ключ не имеет прав администратора } delete: summary: Завершить сессию администратора operationId: deleteStaffSession tags: [Authentication] responses: '204': { description: Сессия завершена } '403': { description: Запрос с постороннего сайта отклонён } /portal: get: summary: Получить доступную пользователю сводку посещаемости и табеля operationId: getStaffPortal tags: [Attendance] security: [{ bearerAuth: [] }] responses: '200': description: Сводка с автоматическим ограничением данными текущего сотрудника content: application/json: schema: { type: object } '403': { description: Недостаточно прав } /employees: post: summary: Добавить сотрудника operationId: createStaffEmployee tags: [Employees] security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/EmployeeCreate' responses: '201': description: Сотрудник создан content: application/json: schema: { $ref: '#/components/schemas/Employee' } '403': { description: Недостаточно прав } '409': { description: Табельный номер уже используется } /professions: get: summary: Получить кадровый справочник профессий operationId: listStaffProfessions tags: [Employees] security: [{ bearerAuth: [] }] responses: '200': { description: Tenant profession catalogue } '401': { description: Требуется авторизация } post: summary: Добавить профессию в кадровый справочник operationId: createStaffProfession tags: [Employees] security: [{ bearerAuth: [] }] responses: '201': { description: Profession created } '403': { description: Недостаточно прав } '409': { description: Профессия уже существует } /professions/{professionId}: parameters: - in: path name: professionId required: true schema: { type: string, pattern: '^[0-9A-HJKMNP-TV-Z]{26}$' } patch: summary: Изменить профессию или её статус operationId: updateStaffProfession tags: [Employees] security: [{ bearerAuth: [] }] responses: '200': { description: Profession updated } '403': { description: Недостаточно прав } '404': { description: Профессия не найдена } /employees/{employeeId}: patch: summary: Изменить карточку или статус сотрудника operationId: updateStaffEmployee tags: [Employees] security: [{ bearerAuth: [] }] parameters: - in: path name: employeeId required: true schema: { $ref: '#/components/schemas/Ulid' } requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/EmployeeUpdate' responses: '200': description: Сотрудник изменён content: application/json: schema: { $ref: '#/components/schemas/Employee' } '403': { description: Недостаточно прав } '404': { description: Сотрудник не найден } /attendance/events: post: summary: Принять идемпотентную отметку терминала operationId: recordAttendanceEvent tags: [Attendance] security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: type: object additionalProperties: false required: [employee_id, event_type, occurred_at, source_event_id] properties: event_id: { $ref: '#/components/schemas/Ulid' } employee_id: { $ref: '#/components/schemas/Ulid' } event_type: { enum: [check_in, check_out] } occurred_at: { type: string, format: date-time } source_event_id: { type: string, minLength: 1, maxLength: 200 } device_id: { $ref: '#/components/schemas/Ulid' } org_unit_id: { $ref: '#/components/schemas/Ulid' } responses: '201': { description: Отметка принята } '200': { description: Повтор уже принятой отметки } '403': { description: Недостаточно прав } /work-sessions/reconcile: post: summary: Сверить отметки терминала и сформировать смены за период operationId: reconcileStaffWorkSessions tags: [Work Sessions] security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: type: object additionalProperties: false required: [date_from, date_to] properties: date_from: { type: string, format: date } date_to: { type: string, format: date } business_day_start_hour: { type: integer, minimum: 0, maximum: 23, default: 4 } automatic_close_hour: { type: integer, minimum: 0, maximum: 23, default: 2 } open_shift_threshold_minutes: { type: integer, minimum: 60, maximum: 2880, default: 840 } responses: '200': description: Итоги сверки, включая события без назначенного подразделения content: application/json: schema: type: object required: [ groups, sessions_created_or_updated, confirmed_sessions_skipped, anomalous_sessions, unresolved_events, ] properties: groups: { type: integer, minimum: 0 } sessions_created_or_updated: { type: integer, minimum: 0 } confirmed_sessions_skipped: { type: integer, minimum: 0 } anomalous_sessions: { type: integer, minimum: 0 } unresolved_events: { type: integer, minimum: 0 } '403': { description: Недостаточно прав } /work-sessions/{sessionId}: patch: summary: Подтвердить, отклонить или скорректировать рабочую смену operationId: reviewStaffWorkSession tags: [Work Sessions] security: [{ bearerAuth: [] }] parameters: - in: path name: sessionId required: true schema: { $ref: '#/components/schemas/Ulid' } requestBody: required: true content: application/json: schema: type: object additionalProperties: false required: [status] properties: status: { enum: [confirmed, rejected] } worked_minutes: { type: integer, minimum: 0, maximum: 2880 } started_at: oneOf: [{ type: string, format: date-time }, { type: 'null' }] ended_at: oneOf: [{ type: string, format: date-time }, { type: 'null' }] correction_note: { type: string, maxLength: 1000 } responses: '200': { description: Смена проверена; подтверждённые часы перенесены в табель } '400': { description: Для корректировки не указана причина или интервал некорректен } '403': { description: Недостаточно прав } '404': { description: Смена не найдена } /timesheets/{employeeId}/{workDate}: put: summary: Сохранить строку табеля operationId: saveTimesheetEntry tags: [Timesheet] security: [{ bearerAuth: [] }] parameters: - in: path name: employeeId required: true schema: { $ref: '#/components/schemas/Ulid' } - in: path name: workDate required: true schema: { type: string, format: date } requestBody: required: true content: application/json: schema: type: object additionalProperties: false required: [minutes_worked, status] properties: minutes_worked: { type: integer, minimum: 0, maximum: 1440 } status: { enum: [draft, submitted, approved, rejected] } note: { type: string, maxLength: 1000 } responses: '200': { description: Строка табеля сохранена } '403': { description: Недостаточно прав } /employees/{employeeId}/access-token: post: summary: Выпустить новый одноразово показываемый ключ личного кабинета сотрудника operationId: rotateEmployeeAccessToken tags: [Employees] security: [{ bearerAuth: [] }] parameters: - in: path name: employeeId required: true schema: { $ref: '#/components/schemas/Ulid' } responses: '200': { description: Ключ выпущен; на сервере сохранён только его SHA-256 отпечаток } '403': { description: Недостаточно прав } '404': { description: Сотрудник не найден } /employees/{employeeId}/pay-assignment: get: summary: Получить зарплатный профиль сотрудника operationId: getEmployeePayAssignment tags: [Payroll] security: [{ bearerAuth: [] }] parameters: - in: path name: employeeId required: true schema: { $ref: '#/components/schemas/Ulid' } responses: '200': description: Зарплатный профиль сотрудника content: application/json: schema: { $ref: '#/components/schemas/PayAssignment' } '403': { description: Недостаточно прав } '404': { description: Сотрудник или зарплатный профиль не найден } put: summary: Сохранить зарплатный профиль сотрудника operationId: saveEmployeePayAssignment tags: [Payroll] security: [{ bearerAuth: [] }] parameters: - in: path name: employeeId required: true schema: { $ref: '#/components/schemas/Ulid' } requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/PayAssignmentSave' } responses: '200': description: Зарплатный профиль сохранён content: application/json: schema: { $ref: '#/components/schemas/PayAssignment' } '403': { description: Недостаточно прав } '404': { description: 'Сотрудник, точка или схема не найдены' } /pay-rules: get: summary: Получить список схем расчёта зарплаты operationId: listStaffPayRules tags: [Payroll] security: [{ bearerAuth: [] }] responses: '200': description: Список доступных схем расчёта content: application/json: schema: type: object required: [pay_rules] properties: pay_rules: type: array items: { type: object } '403': { description: Недостаточно прав } /org-units: get: summary: Получить доступные торговые точки operationId: listStaffOrgUnits tags: [Organization] security: [{ bearerAuth: [] }] responses: '200': { description: Список доступных торговых точек } '403': { description: Недостаточно прав } /finance: get: summary: Получить доходы, расходы и дневную динамику за период operationId: getStaffFinance tags: [Finance] security: [{ bearerAuth: [] }] parameters: - { in: query, name: from, required: true, schema: { type: string, format: date } } - { in: query, name: to, required: true, schema: { type: string, format: date } } responses: '200': { description: Финансовые операции и агрегаты } '403': { description: Недостаточно прав } /finance/{orgUnitId}/{businessDate}/{metricKey}: put: summary: Сохранить сумму по финансовой категории за день operationId: saveStaffFinanceMetric tags: [Finance] security: [{ bearerAuth: [] }] parameters: - { in: path, name: orgUnitId, required: true, schema: { $ref: '#/components/schemas/Ulid' }, } - { in: path, name: businessDate, required: true, schema: { type: string, format: date } } - { in: path, name: metricKey, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: type: object required: [value_minor] properties: value_minor: { type: integer, minimum: -9000000000000000, maximum: 9000000000000000 } currency_code: { type: string, pattern: '^[A-Z]{3}$' } responses: '200': { description: Сумма сохранена } '403': { description: Недостаточно прав } /schedule: get: summary: Получить плановые смены и фактические отметки сканера operationId: getStaffSchedule tags: [Schedule] security: [{ bearerAuth: [] }] parameters: - { in: query, name: from, required: true, schema: { type: string, format: date } } - { in: query, name: to, required: true, schema: { type: string, format: date } } responses: '200': { description: Календарь плановых и фактических смен } '403': { description: Недостаточно прав } /schedule/{employeeId}/{businessDate}: put: summary: Запланировать или удалить смену сотрудника operationId: saveStaffPlannedShift tags: [Schedule] security: [{ bearerAuth: [] }] parameters: - { in: path, name: employeeId, required: true, schema: { $ref: '#/components/schemas/Ulid' }, } - { in: path, name: businessDate, required: true, schema: { type: string, format: date } } requestBody: required: true content: application/json: schema: type: object required: [org_unit_id, planned_minutes] properties: org_unit_id: { $ref: '#/components/schemas/Ulid' } planned_minutes: { type: integer, minimum: 0, maximum: 1440 } start_time: { type: string } end_time: { type: string } note: { type: string, maxLength: 500 } responses: '200': { description: План смены сохранён } '403': { description: Недостаточно прав } /payroll: get: summary: Получить зарплатные итоги и расчётный лист за период operationId: getStaffPayroll tags: [Payroll] security: [{ bearerAuth: [] }] parameters: - { in: query, name: from, required: true, schema: { type: string, format: date } } - { in: query, name: to, required: true, schema: { type: string, format: date } } responses: '200': { description: 'Начисления, выплаты, остаток и подтверждённые часы' } '403': { description: Недостаточно прав } /payroll/calculate: post: summary: Рассчитать зарплату за период operationId: calculateStaffPayroll tags: [Payroll] security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: type: object properties: month: { type: string, pattern: '^\\d{4}-\\d{2}$' } from: { type: string, format: date } to: { type: string, format: date } org_unit_id: { $ref: '#/components/schemas/Ulid' } responses: '200': description: Предварительный результат расчёта content: application/json: schema: type: object required: [run_id, status, period_start, period_end, total_accrued_minor, totals, warnings] properties: run_id: { $ref: '#/components/schemas/Ulid' } status: { const: calculated } period_start: { type: string, format: date } period_end: { type: string, format: date } total_accrued_minor: { type: string } totals: { type: array, items: { type: object } } warnings: { type: array, items: { type: object } } '400': { description: Некорректный период } '403': { description: Недостаточно прав } '409': { description: Расчёт за этот период уже утверждён } /payroll/runs/{runId}/approve: post: summary: Утвердить расчёт зарплаты и записать начисления в реестр operationId: approveStaffPayrollRun tags: [Payroll] security: [{ bearerAuth: [] }] parameters: - in: path name: runId required: true schema: { $ref: '#/components/schemas/Ulid' } responses: '200': description: Расчёт утверждён и проведён в реестр content: application/json: schema: type: object required: [run_id, status, approved_by, approved_at, total_accrued_minor, entries_posted] properties: run_id: { $ref: '#/components/schemas/Ulid' } status: { const: approved } approved_by: { $ref: '#/components/schemas/Ulid' } approved_at: { type: string, format: date-time } total_accrued_minor: { type: string } entries_posted: { type: integer } '400': { description: Расчёт не в статусе calculated } '403': { description: Недостаточно прав } '404': { description: Расчёт не найден } /payroll/runs/{runId}: get: summary: Получить подробности расчёта зарплаты operationId: getStaffPayrollRun tags: [Payroll] security: [{ bearerAuth: [] }] parameters: - in: path name: runId required: true schema: { $ref: '#/components/schemas/Ulid' } responses: '200': description: Детали расчёта со строками content: application/json: schema: type: object required: [run, lines] properties: run: { type: object } lines: { type: array, items: { type: object } } '403': { description: Недостаточно прав } '404': { description: Расчёт не найден } /imports/owner-workbook: post: summary: Идемпотентно импортировать локально разобранную книгу владельца operationId: importStaffOwnerWorkbook tags: [Import] security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: type: object required: [source_name] properties: source_name: { type: string, maxLength: 240 } units: { type: array, items: { type: object } } financial_entries: { type: array, items: { type: object } } employees: { type: array, items: { type: object } } schedule_items: { type: array, items: { type: object } } payroll_entries: { type: array, items: { type: object } } responses: '200': { description: Пакет импортирован повторяемо без дублирования } '403': { description: Недостаточно прав } /push/public-key: get: summary: Получить публичный VAPID-ключ для WebPush operationId: getPushPublicKey tags: [PushNotifications] responses: '200': description: Публичный VAPID-ключ content: application/json: schema: type: object required: [public_key] properties: public_key: { type: string } '400': { $ref: '#/components/responses/BadRequest' } /push/subscriptions: post: summary: Сохранить WebPush-подписку текущего сотрудника operationId: createPushSubscription tags: [PushNotifications] security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: type: object required: [endpoint, keys] properties: endpoint: { type: string, minLength: 10, maxLength: 2000 } keys: type: object required: [p256dh, auth] properties: p256dh: { type: string, minLength: 10, maxLength: 250 } auth: { type: string, minLength: 10, maxLength: 100 } user_agent: { type: string, maxLength: 500 } responses: '201': description: Подписка сохранена content: application/json: schema: type: object required: [id, endpoint] properties: id: { type: string } endpoint: { type: string } '401': { description: Требуется авторизация } '403': { description: Недостаточно прав } '404': { description: Сотрудник не найден } delete: summary: Удалить WebPush-подписку operationId: deletePushSubscription tags: [PushNotifications] security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: type: object required: [endpoint] properties: endpoint: { type: string, minLength: 10, maxLength: 2000 } responses: '204': { description: Подписка удалена } '401': { description: Требуется авторизация } '403': { description: Недостаточно прав } components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT responses: BadRequest: description: Invalid request content: application/json: schema: { $ref: '#/components/schemas/Problem' } schemas: Ulid: type: string pattern: '^[0-9A-HJKMNP-TV-Z]{26}$' examples: [01J00000000000000000000000] Health: type: object additionalProperties: false required: [status] properties: status: { type: string } Employee: type: object additionalProperties: false required: [id, personnel_number, display_name, position, gender, status] properties: id: { $ref: '#/components/schemas/Ulid' } core_user_id: oneOf: - { $ref: '#/components/schemas/Ulid' } - { type: 'null' } personnel_number: { type: string } display_name: { type: string } position: { type: string } gender: { enum: ['', male, female] } status: { enum: [active, blocked, dismissed] } EmployeeCreate: type: object additionalProperties: false required: [personnel_number, display_name] properties: personnel_number: { type: string, minLength: 1, maxLength: 80 } display_name: { type: string, minLength: 1, maxLength: 240 } position: { type: string, maxLength: 160 } gender: { enum: ['', male, female] } EmployeeUpdate: type: object additionalProperties: false minProperties: 1 properties: personnel_number: { type: string, minLength: 1, maxLength: 80 } display_name: { type: string, minLength: 1, maxLength: 240 } position: { type: string, maxLength: 160 } gender: { enum: ['', male, female] } status: { enum: [active, blocked, dismissed] } Problem: type: object additionalProperties: false required: [type, title, status, trace_id] properties: type: { type: string, format: uri-reference } title: { type: string } status: { type: integer, minimum: 400, maximum: 599 } trace_id: { $ref: '#/components/schemas/Ulid' } Version: type: object additionalProperties: false required: [name, version, mode] properties: name: { const: krona-staff } version: { type: string } mode: { enum: [standalone, platform] } PayAssignment: type: object additionalProperties: false required: [ id, employee_id, pay_rule_id, pay_rule_code, pay_rule_name, rate_minor, percent_bp, guarantee_minor, pool_weight_bp, effective_from, ] properties: id: { $ref: '#/components/schemas/Ulid' } employee_id: { $ref: '#/components/schemas/Ulid' } org_unit_id: oneOf: - { $ref: '#/components/schemas/Ulid' } - { type: 'null' } pay_rule_id: { $ref: '#/components/schemas/Ulid' } pay_rule_code: { type: string } pay_rule_name: { type: string } rate_minor: { type: integer, minimum: 0 } percent_bp: { type: integer, minimum: 0 } guarantee_minor: { type: integer, minimum: 0 } pool_weight_bp: { type: integer, minimum: 0 } effective_from: { type: string, format: date } effective_to: oneOf: - { type: string, format: date } - { type: 'null' } params: { type: object } note: { type: string } PayAssignmentSave: type: object additionalProperties: false required: [pay_rule_id] properties: org_unit_id: oneOf: - { $ref: '#/components/schemas/Ulid' } - { type: 'null' } pay_rule_id: { $ref: '#/components/schemas/Ulid' } rate_minor: { type: integer, minimum: 0 } percent_bp: { type: integer, minimum: 0, maximum: 10000 } guarantee_minor: { type: integer, minimum: 0 } pool_weight_bp: { type: integer, minimum: 0, maximum: 20000 } effective_from: { type: string, format: date } note: { type: string, maxLength: 500 }