Files
edr-platform/apps/edr-hr-api/src/modules/job-positions/entities/job-position.entity.ts
2026-08-25 00:11:39 +03:00

66 lines
2.4 KiB
TypeScript

import { SoftDeleteAudit } from "@tria-plc/api-common/modules/typeorm/audit.entity";
import {
Check,
Column,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Unique,
} from "typeorm";
import { JobTitle } from "../../job-titles/entities/job-title.entity";
/**
* The headcount / vacancy layer over `iam.positions`.
*
* Stores NOTHING IAM already holds — not the unit, not the organization. Both
* come from `iam.positions` and are read by joining. A copy here would be a
* second answer that goes stale the moment a position is moved.
*
* It EXTENDS a position, never replaces it: `positionId` is a soft reference to
* the IAM slot, unique here so a position cannot acquire two competing headcount
* records. IAM keeps owning who holds the position; HR adds what it is graded as,
* how many bodies it is budgeted for, and whether it is open for recruitment
* (which 3.5 reads when creating a job opening).
*/
@Entity({ schema: "hr", name: "job_positions" })
@Unique("uq_job_positions_position_id", ["positionId"])
@Check("ck_job_positions_budgeted_count", `"budgeted_count" >= 0`)
@Check("ck_job_positions_current_count", `"current_count" >= 0`)
export class JobPosition extends SoftDeleteAudit {
@PrimaryGeneratedColumn("uuid")
id!: string;
/** Soft reference → `iam.positions.id`. */
@Column({ type: "uuid", name: "position_id" })
positionId!: string;
@Column({ type: "uuid", name: "job_title_id", nullable: true })
jobTitleId?: string | null;
@ManyToOne(() => JobTitle, { nullable: true, onDelete: "SET NULL" })
@JoinColumn({ name: "job_title_id" })
jobTitle?: JobTitle | null;
@Column({ type: "int", name: "budgeted_count", default: 1 })
budgetedCount!: number;
/**
* Cache of the current holder count in IAM. Refreshed from
* `IamDirectoryService.countCurrentPositionHolders` whenever the position is
* read or written through HR — IAM stays the source of truth, this column just
* makes "which posts are under-filled" answerable in one query.
*/
@Column({ type: "int", name: "current_count", default: 0 })
currentCount!: number;
/** Open for recruitment. Independent of vacancy: a post can be under-filled
* and still frozen, which is exactly what a hiring freeze is. */
@Column({ type: "boolean", name: "is_open", default: false })
isOpen!: boolean;
@Column({ type: "uuid", name: "created_by", nullable: true })
createdBy?: string | null;
}