@kernhq/module-hr 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/contract/capabilities.d.ts.map +1 -1
  2. package/dist/contract/capabilities.js +31 -0
  3. package/dist/contract/capabilities.js.map +1 -1
  4. package/dist/contract/index.d.ts +1 -0
  5. package/dist/contract/index.d.ts.map +1 -1
  6. package/dist/contract/index.js +1 -0
  7. package/dist/contract/index.js.map +1 -1
  8. package/dist/contract/permissions.d.ts +23 -0
  9. package/dist/contract/permissions.d.ts.map +1 -1
  10. package/dist/contract/permissions.js +27 -0
  11. package/dist/contract/permissions.js.map +1 -1
  12. package/dist/contract/policies.d.ts +256 -0
  13. package/dist/contract/policies.d.ts.map +1 -0
  14. package/dist/contract/policies.js +175 -0
  15. package/dist/contract/policies.js.map +1 -0
  16. package/dist/contract/router.d.ts +707 -0
  17. package/dist/contract/router.d.ts.map +1 -1
  18. package/dist/contract/router.js +127 -0
  19. package/dist/contract/router.js.map +1 -1
  20. package/dist/policy/accrual.d.ts +127 -0
  21. package/dist/policy/accrual.d.ts.map +1 -0
  22. package/dist/policy/accrual.js +134 -0
  23. package/dist/policy/accrual.js.map +1 -0
  24. package/dist/server/jobs.d.ts.map +1 -1
  25. package/dist/server/jobs.js +186 -1
  26. package/dist/server/jobs.js.map +1 -1
  27. package/dist/server/router.d.ts +772 -0
  28. package/dist/server/router.d.ts.map +1 -1
  29. package/dist/server/router.js +462 -1
  30. package/dist/server/router.js.map +1 -1
  31. package/dist/server/schema.d.ts +612 -1
  32. package/dist/server/schema.d.ts.map +1 -1
  33. package/dist/server/schema.js +76 -0
  34. package/dist/server/schema.js.map +1 -1
  35. package/dist/server/services/policies.d.ts +53 -0
  36. package/dist/server/services/policies.d.ts.map +1 -0
  37. package/dist/server/services/policies.js +158 -0
  38. package/dist/server/services/policies.js.map +1 -0
  39. package/migrations/meta/_journal.json +7 -0
  40. package/package.json +1 -1
  41. package/src/contract/capabilities.ts +31 -0
  42. package/src/contract/index.ts +1 -0
  43. package/src/contract/permissions.ts +28 -0
  44. package/src/contract/router.ts +148 -0
@@ -53,6 +53,15 @@ import {
53
53
  TimeZone,
54
54
  WorkingWeek,
55
55
  } from './models.js'
56
+ import {
57
+ AccrualPreview,
58
+ Period,
59
+ Policy,
60
+ PolicyAssignment,
61
+ PolicyKind,
62
+ PolicySubjectKind,
63
+ ResolvedPolicy,
64
+ } from './policies.js'
56
65
 
57
66
  const ws = z.object({ workspaceId: WorkspaceId })
58
67
  const t = ['hr'] as const
@@ -602,6 +611,145 @@ export const hrContract = {
602
611
  .output(ok),
603
612
  },
604
613
 
614
+ // ---------------------------------------------------------------- policies
615
+ policies: {
616
+ list: baseContract
617
+ .route({ method: 'GET', path: '/policies', tags: t })
618
+ .input(ws.extend({ kind: PolicyKind.optional(), includeArchived: z.boolean().default(false) }))
619
+ .output(z.array(Policy.extend({ assignments: z.array(PolicyAssignment) }))),
620
+ get: baseContract
621
+ .route({ method: 'GET', path: '/policies/{policyId}', tags: t })
622
+ .input(ws.extend({ policyId: z.uuid() }))
623
+ .output(Policy.extend({ assignments: z.array(PolicyAssignment) })),
624
+ create: baseContract
625
+ .route({ method: 'POST', path: '/policies', tags: t })
626
+ .input(
627
+ ws.extend({
628
+ kind: PolicyKind,
629
+ name: z.string().min(1).max(120),
630
+ /** Validated against the schema for `kind` — a config for the wrong kind is refused. */
631
+ config: z.record(z.string(), z.unknown()),
632
+ effectiveFrom: IsoDate,
633
+ effectiveTo: IsoDate.nullish(),
634
+ }),
635
+ )
636
+ .output(Policy),
637
+ /**
638
+ * Edits a policy in place.
639
+ *
640
+ * A change that should apply from a date rather than retroactively is a **new** policy with a
641
+ * later `effectiveFrom`, not an edit — editing rewrites what was true in the past, and anything
642
+ * already derived from it becomes unexplainable.
643
+ */
644
+ update: baseContract
645
+ .route({ method: 'PATCH', path: '/policies/{policyId}', tags: t })
646
+ .input(
647
+ ws.extend({
648
+ policyId: z.uuid(),
649
+ name: z.string().min(1).max(120).optional(),
650
+ config: z.record(z.string(), z.unknown()).optional(),
651
+ effectiveTo: IsoDate.nullish(),
652
+ }),
653
+ )
654
+ .output(Policy),
655
+ archive: baseContract
656
+ .route({ method: 'DELETE', path: '/policies/{policyId}', tags: t })
657
+ .input(ws.extend({ policyId: z.uuid() }))
658
+ .output(ok),
659
+ assign: baseContract
660
+ .route({ method: 'POST', path: '/policies/{policyId}/assign', tags: t })
661
+ .input(
662
+ ws.extend({
663
+ policyId: z.uuid(),
664
+ subjectKind: PolicySubjectKind,
665
+ /** Null for `workspace`, which needs no id. */
666
+ subjectId: z.uuid().nullish(),
667
+ effectiveFrom: IsoDate,
668
+ effectiveTo: IsoDate.nullish(),
669
+ }),
670
+ )
671
+ .output(PolicyAssignment),
672
+ unassign: baseContract
673
+ .route({ method: 'DELETE', path: '/policies/assignments/{assignmentId}', tags: t })
674
+ .input(ws.extend({ assignmentId: z.uuid() }))
675
+ .output(ok),
676
+ /**
677
+ * Which policy of each kind applies to this person on this date, and which rung answered.
678
+ *
679
+ * "Why does she accrue differently from her team" is the question this module gets asked, and
680
+ * this is what answers it without a database session.
681
+ */
682
+ resolveFor: baseContract
683
+ .route({ method: 'GET', path: '/people/{personId}/policies', tags: t })
684
+ .input(ws.extend({ personId: z.uuid(), on: IsoDate.optional() }))
685
+ .output(z.array(ResolvedPolicy)),
686
+ },
687
+
688
+ // ---------------------------------------------------------------- accrual
689
+ accrual: {
690
+ /**
691
+ * What a run would credit, per person, before it writes anything.
692
+ *
693
+ * Runs the same code the run does. A preview computed differently from the thing it previews is
694
+ * a preview that eventually lies.
695
+ */
696
+ preview: baseContract
697
+ .route({ method: 'POST', path: '/accrual/preview', tags: t })
698
+ .input(ws.extend({ from: IsoDate, to: IsoDate, personId: z.uuid().optional() }))
699
+ .output(AccrualPreview),
700
+ /**
701
+ * Credits the ledger.
702
+ *
703
+ * Idempotent per person, per leave type, per period: a second run for the same window credits
704
+ * nothing, because an accrual job that double-credits when somebody clicks twice is worse than
705
+ * one that never ran.
706
+ */
707
+ run: baseContract
708
+ .route({ method: 'POST', path: '/accrual/run', tags: t })
709
+ .input(ws.extend({ from: IsoDate, to: IsoDate, personId: z.uuid().optional() }))
710
+ .output(
711
+ z.object({
712
+ credited: z.number().int(),
713
+ skipped: z.number().int(),
714
+ totalMinutes: z.number().int(),
715
+ }),
716
+ ),
717
+ },
718
+
719
+ // ---------------------------------------------------------------- periods
720
+ periods: {
721
+ list: baseContract
722
+ .route({ method: 'GET', path: '/periods', tags: t })
723
+ .input(ws.extend({ kind: Period.shape.kind.optional(), ...PageInput.shape }))
724
+ .output(page(Period)),
725
+ create: baseContract
726
+ .route({ method: 'POST', path: '/periods', tags: t })
727
+ .input(
728
+ ws.extend({
729
+ kind: Period.shape.kind.default('payroll'),
730
+ legalEntityId: z.uuid().nullish(),
731
+ startsOn: IsoDate,
732
+ endsOn: IsoDate,
733
+ }),
734
+ )
735
+ .output(Period),
736
+ /** Freezes the month. Every derived day inside it stops being recomputable. */
737
+ lock: baseContract
738
+ .route({ method: 'POST', path: '/periods/{periodId}/lock', tags: t })
739
+ .input(ws.extend({ periodId: z.uuid(), note: z.string().max(500).nullish() }))
740
+ .output(Period.extend({ lockedDays: z.number().int() })),
741
+ /**
742
+ * Reopens it.
743
+ *
744
+ * Dangerous on purpose: a payroll has usually been filed against a locked month, and reopening
745
+ * lets figures move underneath it. The response says how many days became recomputable again.
746
+ */
747
+ unlock: baseContract
748
+ .route({ method: 'POST', path: '/periods/{periodId}/unlock', tags: t })
749
+ .input(ws.extend({ periodId: z.uuid(), reason: z.string().min(1).max(500) }))
750
+ .output(Period.extend({ unlockedDays: z.number().int() })),
751
+ },
752
+
605
753
  // ---------------------------------------------------------------- attendance
606
754
  attendance: {
607
755
  /** Am I clocked in? The one call a clock widget makes. */