openapi: 3.1.0 info: title: Krona Staff API version: 0.3.2 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: Недостаточно прав } /sync/devices: post: summary: Регистрация нового устройства синхронизации operationId: registerSyncDevice tags: [Sync] security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: type: object required: [device_code, name] properties: device_code: { type: string, minLength: 1, maxLength: 64 } name: { type: string, minLength: 1, maxLength: 255 } org_unit_id: { $ref: '#/components/schemas/Ulid' } allowed_scopes: type: array items: { type: string } responses: '201': description: Устройство зарегистрировано, выдан токен content: application/json: schema: type: object required: [device, token] properties: device: type: object required: [ id, tenant_id, device_code, name, token_hint, status, allowed_scopes, registered_at, created_by, ] properties: id: { $ref: '#/components/schemas/Ulid' } tenant_id: { $ref: '#/components/schemas/Ulid' } device_code: { type: string } name: { type: string } token_hint: { type: string } status: { type: string, enum: [active, revoked, pending] } allowed_scopes: { type: array, items: { type: string } } org_unit_id: oneOf: - { $ref: '#/components/schemas/Ulid' } - { type: 'null' } registered_at: { type: string, format: date-time } created_by: { $ref: '#/components/schemas/Ulid' } token: { type: string } '400': { $ref: '#/components/responses/BadRequest' } '401': { description: Требуется авторизация руководителя } '403': { description: Недостаточно прав } /sync/devices/{deviceId}/revoke: post: summary: Отозвать токен и доступ устройства operationId: revokeSyncDevice tags: [Sync] security: [{ bearerAuth: [] }] parameters: - in: path name: deviceId required: true schema: { $ref: '#/components/schemas/Ulid' } responses: '200': description: Устройство отозвано content: application/json: schema: type: object required: [id, status, revoked_at] properties: id: { $ref: '#/components/schemas/Ulid' } status: { type: string, enum: [revoked] } revoked_at: { type: string, format: date-time } '401': { description: Требуется авторизация руководителя } '403': { description: Недостаточно прав } '404': { description: Устройство не найдено } /sync/status: get: summary: Получить статус сервера синхронизации и чекпоинта устройства operationId: getSyncStatus tags: [Sync] security: [{ bearerAuth: [] }] responses: '200': description: Текущее состояние синхронизации content: application/json: schema: type: object required: [server_time_utc, latest_cursor, schema_version] properties: server_time_utc: { type: string, format: date-time } latest_cursor: { type: string } device_checkpoint: oneOf: - type: object properties: last_acked_cursor: oneOf: - { type: string } - { type: 'null' } acknowledged_at: oneOf: - { type: string, format: date-time } - { type: 'null' } - { type: 'null' } schema_version: { type: integer } '401': { description: Недействительный токен устройства } '403': { description: Устройство отозвано или заблокировано } /sync/pull: post: summary: Получить изменения сущностей после заданного курсора или начальный снимок operationId: pullSyncChanges tags: [Sync] security: [{ bearerAuth: [] }] requestBody: required: false content: application/json: schema: type: object properties: cursor: oneOf: - { type: string } - { type: 'null' } limit: { type: integer, minimum: 1, maximum: 100, default: 100 } entity_types: type: array items: { type: string } responses: '200': description: Пакет изменений или начальный снимок content: application/json: schema: type: object required: [changes, next_cursor, has_more] properties: changes: type: array items: type: object required: [cursor, entity_type, entity_id, operation, revision, payload, created_at] properties: cursor: { type: string } entity_type: { type: string } entity_id: { type: string } operation: { type: string, enum: [upsert, delete] } revision: { type: integer } payload: { type: object } created_at: { type: string, format: date-time } next_cursor: { type: string } has_more: { type: boolean } '400': { $ref: '#/components/responses/BadRequest' } '401': { description: Недействительный токен устройства } '403': { description: Устройство отозвано или заблокировано } /sync/push: post: summary: Пакетная отправка команд от локального устройства operationId: pushSyncCommands tags: [Sync] security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: type: object required: [commands] properties: commands: type: array minItems: 1 maxItems: 100 items: type: object required: [ command_type, schema_version, idempotency_key, correlation_id, created_at, payload, ] properties: command_type: { type: string } schema_version: { type: integer } idempotency_key: { type: string, minLength: 1, maxLength: 128 } correlation_id: { type: string } causation_id: oneOf: - { type: string } - { type: 'null' } created_at: { type: string, format: date-time } payload: { type: object } responses: '200': description: Результаты применения команд content: application/json: schema: type: object required: [results] properties: results: type: array items: type: object required: [idempotency_key, command_type, status] properties: idempotency_key: { type: string } command_type: { type: string } status: { type: string, enum: [applied, rejected, conflict] } result: oneOf: - { type: object } - { type: 'null' } conflict: oneOf: - type: object required: [ conflict_code, entity_type, entity_id, expected_revision, actual_revision, safe_description, client_action, ] properties: conflict_code: { type: string, enum: [REVISION_MISMATCH] } entity_type: { type: string } entity_id: { type: string } expected_revision: { type: integer } actual_revision: { type: integer } safe_description: { type: string } client_action: { type: string, enum: [FETCH_LATEST_AND_RETRY] } - { type: 'null' } error_code: description: > Код ошибки, если status = rejected. Команды `upsert_work_session` и `upsert_timesheet_entry` отклоняются с кодом COMMAND_NOT_SUPPORTED. oneOf: - { type: string } - { type: 'null' } error_message: oneOf: - { type: string } - { type: 'null' } '400': { $ref: '#/components/responses/BadRequest' } '401': { description: Недействительный токен устройства } '403': { description: Устройство отозвано или заблокировано } /sync/ack: post: summary: Подтвердить получение и обработку изменений до курсора operationId: ackSyncCheckpoint tags: [Sync] security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: type: object required: [cursor] properties: cursor: { type: string, minLength: 5 } responses: '200': description: Чекпоинт зафиксирован content: application/json: schema: type: object required: [acknowledged, cursor, acknowledged_at] properties: acknowledged: { type: boolean } cursor: { type: string } acknowledged_at: { type: string, format: date-time } '400': { $ref: '#/components/responses/BadRequest' } '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 }