@lilaquadrat/studio 12.2.0 → 12.3.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 (56) hide show
  1. package/lib/src/helpers/shiftPlanAllocation.d.ts +53 -0
  2. package/lib/src/helpers/shiftPlanAllocation.js +110 -0
  3. package/lib/src/helpers/shiftPlanAllocation.js.map +1 -0
  4. package/lib/src/helpers/shiftPlanCoverage.d.ts +2 -0
  5. package/lib/src/helpers/shiftPlanCoverage.js +4 -0
  6. package/lib/src/helpers/shiftPlanCoverage.js.map +1 -0
  7. package/lib/src/helpers/shiftPlanningTime.js +26 -2
  8. package/lib/src/helpers/shiftPlanningTime.js.map +1 -1
  9. package/lib/src/helpers/shiftPlanningValidation.d.ts +8 -2
  10. package/lib/src/helpers/shiftPlanningValidation.js +16 -0
  11. package/lib/src/helpers/shiftPlanningValidation.js.map +1 -1
  12. package/lib/src/helpers/structureMatch.js +3 -1
  13. package/lib/src/helpers/structureMatch.js.map +1 -1
  14. package/lib/src/interfaces/ListPlanningParticipant.d.ts +17 -0
  15. package/lib/src/interfaces/ListPlanningParticipant.js +2 -0
  16. package/lib/src/interfaces/ListPlanningParticipant.js.map +1 -0
  17. package/lib/src/interfaces/PlanCoveragePreparation.d.ts +17 -0
  18. package/lib/src/interfaces/PlanCoveragePreparation.js +2 -0
  19. package/lib/src/interfaces/PlanCoveragePreparation.js.map +1 -0
  20. package/lib/src/interfaces/PlanningParticipantsResult.d.ts +12 -0
  21. package/lib/src/interfaces/PlanningParticipantsResult.js +2 -0
  22. package/lib/src/interfaces/PlanningParticipantsResult.js.map +1 -0
  23. package/lib/src/interfaces/ShiftAutofillResult.d.ts +6 -0
  24. package/lib/src/interfaces/ShiftAutofillResult.js +2 -0
  25. package/lib/src/interfaces/ShiftAutofillResult.js.map +1 -0
  26. package/lib/src/interfaces/ShiftReportContextOptions.d.ts +9 -0
  27. package/lib/src/interfaces/ShiftReportContextOptions.js +2 -0
  28. package/lib/src/interfaces/ShiftReportContextOptions.js.map +1 -0
  29. package/lib/src/models/lists.model.js +2 -1
  30. package/lib/src/models/lists.model.js.map +1 -1
  31. package/lib/src/services/customers.service.d.ts +2 -2
  32. package/lib/src/services/customers.service.js +2 -2
  33. package/lib/src/services/customers.service.js.map +1 -1
  34. package/lib/src/services/listParticipants.service.d.ts +45 -8
  35. package/lib/src/services/listParticipants.service.js +320 -39
  36. package/lib/src/services/listParticipants.service.js.map +1 -1
  37. package/lib/src/services/lists.service.js +24 -1
  38. package/lib/src/services/lists.service.js.map +1 -1
  39. package/lib/src/services/shifts.service.d.ts +18 -1
  40. package/lib/src/services/shifts.service.js +171 -26
  41. package/lib/src/services/shifts.service.js.map +1 -1
  42. package/lib/tests/listParticipantsPlanning.spec.js +406 -10
  43. package/lib/tests/listParticipantsPlanning.spec.js.map +1 -1
  44. package/lib/tests/listsServiceCreateExternalProducts.spec.js +18 -0
  45. package/lib/tests/listsServiceCreateExternalProducts.spec.js.map +1 -1
  46. package/lib/tests/shiftPlanCoverage.spec.d.ts +1 -0
  47. package/lib/tests/shiftPlanCoverage.spec.js +189 -0
  48. package/lib/tests/shiftPlanCoverage.spec.js.map +1 -0
  49. package/lib/tests/shiftPlanning.spec.js +327 -0
  50. package/lib/tests/shiftPlanning.spec.js.map +1 -1
  51. package/lib/tests/shiftPlanningTime.spec.d.ts +1 -0
  52. package/lib/tests/shiftPlanningTime.spec.js +90 -0
  53. package/lib/tests/shiftPlanningTime.spec.js.map +1 -0
  54. package/lib/tests/structureMatch.spec.js +42 -0
  55. package/lib/tests/structureMatch.spec.js.map +1 -1
  56. package/package.json +2 -2
@@ -61,6 +61,24 @@ describe('ListsService.createExternalProducts', () => {
61
61
  expect(data.categories?.[0].externalId).toBe('prod_existing');
62
62
  expect(data.categories?.[0]).not.toHaveProperty('createExternalProduct');
63
63
  });
64
+ it('reports the missing price fields as validation errors on the category', async () => {
65
+ const byType = jest.spyOn(paymentProvidersService, 'byType');
66
+ const data = list([
67
+ category({ createExternalProduct: 'stripe', price: undefined }),
68
+ category({ createExternalProduct: 'stripe', price: { amount: 10, currency: '' } }),
69
+ category({ createExternalProduct: 'stripe', price: { amount: undefined, currency: 'eur' } }),
70
+ ]);
71
+ await expect(listsService.createExternalProducts(data, options)).rejects.toMatchObject({
72
+ message: 'VALIDATION_FAILED',
73
+ errors: [
74
+ expect.objectContaining({ instancePath: '/categories/0', keyword: 'required', params: { missingProperty: 'price' } }),
75
+ expect.objectContaining({ instancePath: '/categories/1/price', keyword: 'required', params: { missingProperty: 'currency' } }),
76
+ expect.objectContaining({ instancePath: '/categories/2/price', keyword: 'required', params: { missingProperty: 'amount' } }),
77
+ ],
78
+ });
79
+ expect(byType).not.toHaveBeenCalled();
80
+ expect(productsCreate).not.toHaveBeenCalled();
81
+ });
64
82
  it('throws when no active provider of the requested type exists', async () => {
65
83
  jest.spyOn(paymentProvidersService, 'byType').mockResolvedValue(null);
66
84
  await expect(listsService.createExternalProducts(list([category({ createExternalProduct: 'stripe' })]), options)).rejects.toThrow('NO_PAYMENT_PROVIDER');
@@ -1 +1 @@
1
- {"version":3,"file":"listsServiceCreateExternalProducts.spec.js","sourceRoot":"","sources":["../../tests/listsServiceCreateExternalProducts.spec.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"listsServiceCreateExternalProducts.spec.js","sourceRoot":"","sources":["../../tests/listsServiceCreateExternalProducts.spec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,189 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+ import { ObjectId } from 'mongodb';
3
+ import { allocateShiftCoverage } from '../src/helpers/shiftPlanCoverage.js';
4
+ const company = 'company';
5
+ const project = 'project';
6
+ const planId = new ObjectId();
7
+ const dayId = new ObjectId();
8
+ const areaId = new ObjectId();
9
+ const policy = () => ({
10
+ daytime: { start: '06:00', end: '22:00' }, nighttime: { start: '22:00', end: '06:00' },
11
+ maximumConsecutiveWorkingDays: { enabled: false, days: 6 },
12
+ maximumDailyWorkMinutes: { enabled: false, minutes: 480 },
13
+ minimumRest: { enabled: false, minutes: 660 },
14
+ breaks: { enabled: false, maximumContinuousWorkMinutes: 360, minimumQualifyingBreakMinutes: 15,
15
+ requirements: [{ afterWorkMinutes: 360, minimumTotalBreakMinutes: 30 }] },
16
+ absenceEnforcement: false,
17
+ });
18
+ const shift = (overrides) => ({
19
+ company, project, plan: planId, day: dayId, area: areaId, title: 'Shift',
20
+ start: '2026-01-01T08:00:00.000Z', end: '2026-01-01T16:00:00.000Z', assignments: [],
21
+ ...overrides,
22
+ });
23
+ const context = () => ({
24
+ plans: [{ _id: planId, company, project, name: 'Plan', timeZone: 'Europe/Berlin', settingsOverrides: {} }],
25
+ settings: policy(),
26
+ absences: [],
27
+ participants: [],
28
+ participantDetails: [],
29
+ customers: [],
30
+ });
31
+ const candidate = (participant, customer, matching = true) => ({ participant, customer, matching });
32
+ const target = (entry, candidates) => ({ shift: entry, conditions: [], candidates });
33
+ describe('shift plan coverage', () => {
34
+ it('fills a shift to its required people', () => {
35
+ const entry = shift({ _id: new ObjectId(), requiredPeople: 2 });
36
+ const coverage = allocateShiftCoverage([entry], [target(entry, [candidate(new ObjectId(), new ObjectId()), candidate(new ObjectId(), new ObjectId())])], context(), false, 'staffing').coverage;
37
+ expect(coverage).toHaveLength(1);
38
+ expect(coverage[0].assigned).toBe(2);
39
+ expect(coverage[0].fillable).toBe(true);
40
+ });
41
+ it('does not book one person into two overlapping shifts', () => {
42
+ const participant = new ObjectId();
43
+ const customer = new ObjectId();
44
+ const first = shift({ _id: new ObjectId(), requiredPeople: 1, start: '2026-01-01T08:00:00.000Z', end: '2026-01-01T16:00:00.000Z' });
45
+ const second = shift({ _id: new ObjectId(), requiredPeople: 1, start: '2026-01-01T12:00:00.000Z', end: '2026-01-01T20:00:00.000Z' });
46
+ const coverage = allocateShiftCoverage([first, second], [
47
+ target(first, [candidate(participant, customer)]),
48
+ target(second, [candidate(participant, customer)]),
49
+ ], context(), false, 'staffing').coverage;
50
+ expect(coverage.find((entry) => entry.shift.equals(first._id)).fillable).toBe(true);
51
+ expect(coverage.find((entry) => entry.shift.equals(second._id)).fillable).toBe(false);
52
+ });
53
+ it('reuses a person across shifts that do not overlap', () => {
54
+ const participant = new ObjectId();
55
+ const customer = new ObjectId();
56
+ const first = shift({ _id: new ObjectId(), requiredPeople: 1, start: '2026-01-01T08:00:00.000Z', end: '2026-01-01T16:00:00.000Z' });
57
+ const second = shift({ _id: new ObjectId(), requiredPeople: 1, start: '2026-01-01T17:00:00.000Z', end: '2026-01-01T20:00:00.000Z' });
58
+ const coverage = allocateShiftCoverage([first, second], [
59
+ target(first, [candidate(participant, customer)]),
60
+ target(second, [candidate(participant, customer)]),
61
+ ], context(), false, 'staffing').coverage;
62
+ expect(coverage.find((entry) => entry.shift.equals(first._id)).fillable).toBe(true);
63
+ expect(coverage.find((entry) => entry.shift.equals(second._id)).fillable).toBe(true);
64
+ });
65
+ it('stops at the first unfillable shift and reports the summary', () => {
66
+ const empty = shift({ _id: new ObjectId(), requiredPeople: 1, start: '2026-01-01T08:00:00.000Z', end: '2026-01-01T16:00:00.000Z' });
67
+ const later = shift({ _id: new ObjectId(), requiredPeople: 1, start: '2026-01-01T17:00:00.000Z', end: '2026-01-01T20:00:00.000Z' });
68
+ const allocation = allocateShiftCoverage([empty, later], [
69
+ target(empty, []),
70
+ target(later, [candidate(new ObjectId(), new ObjectId())]),
71
+ ], context(), true, 'staffing');
72
+ expect(allocation.fillable).toBe(false);
73
+ expect(allocation.complete).toBe(false);
74
+ expect(allocation.coverage).toHaveLength(1);
75
+ });
76
+ it('requires the matching minimum even when enough people exist', () => {
77
+ const entry = shift({ _id: new ObjectId(), requiredPeople: 2, matchingPeople: 2 });
78
+ const coverage = allocateShiftCoverage([entry], [target(entry, [candidate(new ObjectId(), new ObjectId()), candidate(new ObjectId(), new ObjectId(), false)])], context()).coverage;
79
+ expect(coverage[0].assigned).toBe(1);
80
+ expect(coverage[0].fillable).toBe(false);
81
+ });
82
+ it.each([true, false])('checks qualifications on a fully staffed shift (qualified: %s)', (qualified) => {
83
+ const participant = new ObjectId();
84
+ const customer = new ObjectId();
85
+ const structure = new ObjectId();
86
+ const entry = shift({ _id: new ObjectId(), requiredPeople: 1, matchingPeople: 1,
87
+ assignments: [{ participant, customer, breaks: [] }] });
88
+ const reportContext = context();
89
+ reportContext.participantDetails = [{
90
+ _id: participant, 's-boolean-1': qualified,
91
+ structures: [{ _id: structure, id: 's-boolean-1', type: 'boolean' }],
92
+ }];
93
+ const allocation = allocateShiftCoverage([entry], [{ shift: entry,
94
+ conditions: [{ structure: structure.toHexString(), constraint: 'required', value: true }],
95
+ candidates: [],
96
+ }], reportContext);
97
+ expect(allocation.coverage[0].assigned).toBe(1);
98
+ expect(allocation.coverage[0].matching).toBe(qualified ? 1 : 0);
99
+ expect(allocation.coverage[0].fillable).toBe(qualified);
100
+ expect(allocation.fillable).toBe(qualified);
101
+ expect(entry.assignments).toEqual([{ participant, customer, breaks: [] }]);
102
+ });
103
+ it('accepts a fully staffed shift without a qualification minimum', () => {
104
+ const entry = shift({ _id: new ObjectId(), requiredPeople: 1,
105
+ assignments: [{ participant: new ObjectId(), customer: new ObjectId(), breaks: [] }] });
106
+ const allocation = allocateShiftCoverage([entry], [target(entry, [])], context());
107
+ expect(allocation.coverage[0].assigned).toBe(1);
108
+ expect(allocation.coverage[0].fillable).toBe(true);
109
+ expect(allocation.fillable).toBe(true);
110
+ });
111
+ it('does not treat missing qualification evidence as a fulfilled minimum', () => {
112
+ const entry = shift({ _id: new ObjectId(), requiredPeople: 1, matchingPeople: 1,
113
+ assignments: [{ participant: new ObjectId(), customer: new ObjectId(), breaks: [] }] });
114
+ const allocation = allocateShiftCoverage([entry], [target(entry, [])], context());
115
+ expect(allocation.coverage[0].assigned).toBe(1);
116
+ expect(allocation.coverage[0].fillable).toBe(false);
117
+ });
118
+ it('separates staffing coverage from list matching coverage for a 4/4 shift with matchingPeople 10', () => {
119
+ const entry = shift({ _id: new ObjectId(), requiredPeople: 4, matchingPeople: 10,
120
+ assignments: Array.from({ length: 4 }, () => ({ participant: new ObjectId(), customer: new ObjectId(), breaks: [] })) });
121
+ const targets = [target(entry, [])];
122
+ const staffing = allocateShiftCoverage([entry], targets, context(), false, 'staffing');
123
+ const matching = allocateShiftCoverage([entry], targets, context(), false, 'matching');
124
+ expect(staffing.coverage[0]).toMatchObject({ assigned: 4, requiredPeople: 4, matchingPeople: 10, fillable: true });
125
+ expect(staffing.fillable).toBe(true);
126
+ expect(matching.coverage[0]).toMatchObject({ assigned: 4, requiredPeople: 4, matchingPeople: 10, fillable: false });
127
+ expect(matching.fillable).toBe(false);
128
+ expect(entry.assignments).toHaveLength(4);
129
+ });
130
+ it('fills remaining staffing places without imposing the matching minimum', () => {
131
+ const entry = shift({ _id: new ObjectId(), requiredPeople: 1, matchingPeople: 10 });
132
+ const targets = [target(entry, [candidate(new ObjectId(), new ObjectId(), false)])];
133
+ const staffing = allocateShiftCoverage([entry], targets, context(), true, 'staffing');
134
+ const matching = allocateShiftCoverage([entry], targets, context(), true, 'matching');
135
+ expect(staffing.coverage[0]).toMatchObject({ assigned: 1, fillable: true });
136
+ expect(matching.coverage[0]).toMatchObject({ assigned: 0, fillable: false });
137
+ expect(entry.assignments).toEqual([]);
138
+ });
139
+ it('continues matching allocation after staffing headcount is already reached', () => {
140
+ const assignments = Array.from({ length: 4 }, () => ({ participant: new ObjectId(), customer: new ObjectId(), breaks: [] }));
141
+ const entry = shift({ _id: new ObjectId(), requiredPeople: 4, matchingPeople: 10, assignments });
142
+ const reportContext = context();
143
+ reportContext.participantDetails = assignments.map((assignment) => ({ _id: assignment.participant }));
144
+ const targets = [target(entry, Array.from({ length: 6 }, () => candidate(new ObjectId(), new ObjectId())))];
145
+ const allocation = allocateShiftCoverage([entry], targets, reportContext);
146
+ expect(allocation.coverage[0]).toMatchObject({ assigned: 10, matching: 10, matchingPeople: 10, requiredPeople: 4, fillable: true });
147
+ expect(allocation.fillable).toBe(true);
148
+ expect(entry.assignments).toHaveLength(4);
149
+ });
150
+ it('does not require staffing headcount once the list matching target is reached', () => {
151
+ const entry = shift({ _id: new ObjectId(), requiredPeople: 10, matchingPeople: 2 });
152
+ const targets = [target(entry, [candidate(new ObjectId(), new ObjectId()), candidate(new ObjectId(), new ObjectId())])];
153
+ const allocation = allocateShiftCoverage([entry], targets, context());
154
+ expect(allocation.coverage[0]).toMatchObject({ assigned: 2, matching: 2, matchingPeople: 2, requiredPeople: 10, fillable: true });
155
+ expect(allocation.fillable).toBe(true);
156
+ });
157
+ it('does not mistake retained non-matching assignments for matching people', () => {
158
+ const participant = new ObjectId();
159
+ const customer = new ObjectId();
160
+ const structure = new ObjectId();
161
+ const entry = shift({ _id: new ObjectId(), requiredPeople: 1, matchingPeople: 2, assignments: [{ participant, customer, breaks: [] }] });
162
+ const reportContext = context();
163
+ reportContext.participantDetails = [{ _id: participant, 's-boolean-1': false,
164
+ structures: [{ _id: structure, id: 's-boolean-1', type: 'boolean' }],
165
+ }];
166
+ const allocation = allocateShiftCoverage([entry], [{ shift: entry,
167
+ conditions: [{ structure: structure.toHexString(), constraint: 'required', value: true }],
168
+ candidates: [candidate(new ObjectId(), new ObjectId()), candidate(new ObjectId(), new ObjectId())],
169
+ }], reportContext);
170
+ expect(allocation.coverage[0]).toMatchObject({ assigned: 3, matching: 2, matchingPeople: 2, fillable: true });
171
+ expect(entry.assignments).toEqual([{ participant, customer, breaks: [] }]);
172
+ });
173
+ it('counts matching people independently of the staffing target in overlapping shifts', () => {
174
+ const participant = new ObjectId();
175
+ const customer = new ObjectId();
176
+ const first = shift({ _id: new ObjectId(), requiredPeople: 0, matchingPeople: 1, start: '2026-01-01T08:00:00.000Z', end: '2026-01-01T16:00:00.000Z' });
177
+ const second = shift({ _id: new ObjectId(), requiredPeople: 0, matchingPeople: 1, start: '2026-01-01T12:00:00.000Z', end: '2026-01-01T20:00:00.000Z' });
178
+ const allocation = allocateShiftCoverage([first, second], [target(first, [candidate(participant, customer)]), target(second, [candidate(participant, customer)])], context());
179
+ expect(allocation.coverage[0]).toMatchObject({ matching: 1, fillable: true });
180
+ expect(allocation.coverage[1]).toMatchObject({ matching: 0, fillable: false });
181
+ expect(allocation.fillable).toBe(false);
182
+ });
183
+ it('does not impose a staffing target on list coverage without a matching target', () => {
184
+ const entry = shift({ _id: new ObjectId(), requiredPeople: 10 });
185
+ const allocation = allocateShiftCoverage([entry], [target(entry, [])], context());
186
+ expect(allocation.coverage[0]).toMatchObject({ assigned: 0, matching: 0, matchingPeople: 0, fillable: true });
187
+ });
188
+ });
189
+ //# sourceMappingURL=shiftPlanCoverage.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shiftPlanCoverage.spec.js","sourceRoot":"","sources":["../../tests/shiftPlanCoverage.spec.ts"],"names":[],"mappings":"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"}
@@ -19,6 +19,7 @@ import ParticipantsModel from '../src/models/listsParticipants.model.js';
19
19
  import CustomersModel from '../src/models/customers.model.js';
20
20
  import StructureModel from '../src/models/structure.model.js';
21
21
  import HistoryModel from '../src/models/history.model.js';
22
+ import participantsService from '../src/services/listParticipants.service.js';
22
23
  const options = { company: 'hotel', project: 'hotel-project', user: 'planner', app: 'studio' };
23
24
  const listId = new ObjectId();
24
25
  const categoryId = new ObjectId();
@@ -831,5 +832,331 @@ describe('shift planning', () => {
831
832
  }] }, options);
832
833
  expect(await HistoryModel.db.countDocuments({ 'history.parent': shift._id })).toBe(count);
833
834
  });
835
+ describe('read-only shift autofill', () => {
836
+ async function setupAutofill(changes = {}) {
837
+ const plan = await setupPlan();
838
+ const structureId = new ObjectId();
839
+ await StructureModel.db.insertOne({ _id: structureId, company: options.company, project: options.project,
840
+ model: 'listsParticipants', id: 's-boolean-preferred', type: 'boolean', question: 'Preferred' });
841
+ await ParticipantsModel.db.updateOne({ _id: secondParticipantId }, { $set: { 's-boolean-preferred': true } });
842
+ const shift = await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, {
843
+ assignments: [], requiredPeople: 1,
844
+ structures: [{ structure: structureId.toHexString(), grade: 'very-positive' }], ...changes,
845
+ }), options);
846
+ return { plan, shift, structureId };
847
+ }
848
+ it('ranks suggestions deterministically, batches rule reads and changes no stored documents', async () => {
849
+ const { plan, shift } = await setupAutofill({ requiredPeople: 2 });
850
+ const before = await Promise.all(models.map(async (model) => JSON.stringify(await model.db.find({}).sort({ _id: 1 }).toArray())));
851
+ const settingsRead = jest.spyOn(settingsService, 'singleSettings');
852
+ const absencesRead = jest.spyOn(absencesService, 'absencesForPlanning');
853
+ try {
854
+ const expected = { shift: shift._id, users: [secondParticipantId, participantId] };
855
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual(expected);
856
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual(expected);
857
+ expect(settingsRead).toHaveBeenCalledTimes(2);
858
+ expect(absencesRead).toHaveBeenCalledTimes(2);
859
+ const after = await Promise.all(models.map(async (model) => JSON.stringify(await model.db.find({}).sort({ _id: 1 }).toArray())));
860
+ expect(after).toEqual(before);
861
+ }
862
+ finally {
863
+ settingsRead.mockRestore();
864
+ absencesRead.mockRestore();
865
+ }
866
+ });
867
+ it('keeps current assignments and only fills the remaining places', async () => {
868
+ const { plan, shift } = await setupAutofill({ requiredPeople: 2, assignments: [{ participant: participantId }] });
869
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [participantId, secondParticipantId] });
870
+ expect((await ShiftModel.db.findOne({ _id: shift._id })).assignments).toEqual(shift.assignments);
871
+ });
872
+ it('retains existing attention items without allowing new violations', async () => {
873
+ const { plan, shift } = await setupAutofill({ requiredPeople: 2, assignments: [{ participant: participantId }] });
874
+ await ParticipantsModel.db.updateOne({ _id: participantId }, { $set: { state: 'canceled' } });
875
+ await absencesService.createAbsence({ customer: customerId, type: 'sickness', start: `${day}T00:00:00+02:00`, end: `${nextDay}T00:00:00+02:00` }, options);
876
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [participantId, secondParticipantId] });
877
+ });
878
+ it.each([undefined, 0, 1])('keeps an already assigned shift unchanged with target %p', async (requiredPeople) => {
879
+ const { plan, shift } = await setupAutofill({ requiredPeople, assignments: [{ participant: participantId }] });
880
+ const candidateRead = jest.spyOn(participantsService, 'planningParticipants');
881
+ try {
882
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [participantId] });
883
+ expect(candidateRead).not.toHaveBeenCalled();
884
+ }
885
+ finally {
886
+ candidateRead.mockRestore();
887
+ }
888
+ });
889
+ it('selects one list entry per person and returns a partial fill when the pool runs out', async () => {
890
+ const { plan, shift } = await setupAutofill({ requiredPeople: 3 });
891
+ const duplicateParticipantId = new ObjectId();
892
+ await ParticipantsModel.db.insertOne({ ...participant, _id: duplicateParticipantId, 's-boolean-preferred': true });
893
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [secondParticipantId, duplicateParticipantId] });
894
+ });
895
+ it.each(['blocked', 'unchecked', 'notuseful', 'canceled'])('excludes a high-scoring %s person', async (state) => {
896
+ const { plan, shift } = await setupAutofill();
897
+ await ParticipantsModel.db.updateOne({ _id: secondParticipantId }, { $set: { state } });
898
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [participantId] });
899
+ });
900
+ it('ignores entries without a person in the company', async () => {
901
+ const { plan, shift } = await setupAutofill();
902
+ await CustomersModel.db.updateOne({ _id: secondCustomerId }, { $set: { company: 'foreign' } });
903
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [participantId] });
904
+ });
905
+ it('applies the plan default state and area-category binding together', async () => {
906
+ const { plan, shift } = await setupAutofill({ requiredPeople: 3 });
907
+ const eventsCategoryId = new ObjectId();
908
+ const reservedCustomerId = new ObjectId();
909
+ const reservedParticipantId = new ObjectId();
910
+ await ListsModel.db.updateOne({ _id: listId }, { $set: { categories: [
911
+ ...list.categories, { _id: eventsCategoryId, id: 'events', name: 'Events' },
912
+ ] } });
913
+ await ParticipantsModel.db.updateOne({ _id: secondParticipantId }, { $set: { state: 'reserved', category: 'events' } });
914
+ await CustomersModel.db.insertOne({ ...customer, _id: reservedCustomerId, id: 'reserved', accounts: [] });
915
+ await ParticipantsModel.db.insertOne({ ...participant, _id: reservedParticipantId, user: reservedCustomerId, state: 'reserved' });
916
+ await plansService.changePlan(plan._id, planInput(listId, { defaultParticipantState: 'reserved',
917
+ workAreaCategories: [{ area: housekeepingAreaId, categoryIds: [categoryId] }],
918
+ }), options);
919
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [reservedParticipantId] });
920
+ });
921
+ it('ignores matchingPeople and fills only to requiredPeople', async () => {
922
+ const { plan, shift, structureId } = await setupAutofill({ requiredPeople: 2, matchingPeople: 2 });
923
+ await ShiftModel.db.updateOne({ _id: shift._id }, { $set: { structures: [
924
+ { structure: structureId.toHexString(), constraint: 'required', value: true, grade: 'very-negative' },
925
+ ] } });
926
+ // matchingPeople is a coverage concern only; autofill uses the ranked pool up to requiredPeople.
927
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id,
928
+ users: [secondParticipantId, participantId],
929
+ });
930
+ });
931
+ it('counts a retained assignment outside the candidate filter', async () => {
932
+ const { plan, shift } = await setupAutofill({ requiredPeople: 2, assignments: [{ participant: secondParticipantId }] });
933
+ await ParticipantsModel.db.updateOne({ _id: participantId }, { $set: { state: 'reserved' } });
934
+ await plansService.changePlan(plan._id, planInput(listId, { defaultParticipantState: 'reserved' }), options);
935
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [secondParticipantId, participantId] });
936
+ });
937
+ it('does not reject matchingPeople above requiredPeople', async () => {
938
+ const { plan, shift } = await setupAutofill({ requiredPeople: 1, matchingPeople: 2 });
939
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [secondParticipantId] });
940
+ });
941
+ it('skips an overlapping high-scoring person instead of double booking', async () => {
942
+ const { plan, shift } = await setupAutofill();
943
+ await shiftsService.createShift(plan._id, shiftInput(plan, receptionAreaId, { assignments: [{ participant: secondParticipantId }] }), options);
944
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [participantId] });
945
+ });
946
+ it.each([true, false])('respects absence enforcement with plan override %p', async (absenceEnforcement) => {
947
+ const { plan, shift } = await setupAutofill();
948
+ await absencesService.createAbsence({ customer: secondCustomerId, type: 'sickness', start: `${day}T00:00:00+02:00`, end: `${nextDay}T00:00:00+02:00` }, options);
949
+ await plansService.changePlan(plan._id, planInput(listId, { settingsOverrides: { absenceEnforcement } }), options);
950
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id,
951
+ users: [absenceEnforcement ? participantId : secondParticipantId],
952
+ });
953
+ });
954
+ it.each(['rest', 'dailyHours', 'consecutiveDays'])('enforces the general %s rule', async (rule) => {
955
+ const { plan, shift } = await setupAutofill(rule === 'consecutiveDays'
956
+ ? { day: nextDay, start: `${nextDay}T08:00:00+02:00`, end: `${nextDay}T12:00:00+02:00` } : {});
957
+ await shiftsService.createShift(plan._id, shiftInput(plan, receptionAreaId, {
958
+ assignments: [{ participant: secondParticipantId }],
959
+ ...(rule === 'consecutiveDays' ? {} : { start: `${day}T00:00:00+02:00`, end: `${day}T04:00:00+02:00` }),
960
+ }), options);
961
+ const settings = policy();
962
+ if (rule === 'rest')
963
+ settings.minimumRest = { enabled: true, minutes: 660 };
964
+ if (rule === 'dailyHours')
965
+ settings.maximumDailyWorkMinutes = { enabled: true, minutes: 420 };
966
+ if (rule === 'consecutiveDays')
967
+ settings.maximumConsecutiveWorkingDays = { enabled: true, days: 1 };
968
+ await settingsService.changeSettings({ settings }, options);
969
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [participantId] });
970
+ });
971
+ it('uses a plan override to relax the general daily hours limit', async () => {
972
+ const { plan, shift } = await setupAutofill();
973
+ await shiftsService.createShift(plan._id, shiftInput(plan, receptionAreaId, {
974
+ assignments: [{ participant: secondParticipantId }], start: `${day}T00:00:00+02:00`, end: `${day}T04:00:00+02:00`,
975
+ }), options);
976
+ const settings = policy();
977
+ settings.maximumDailyWorkMinutes = { enabled: true, minutes: 420 };
978
+ await settingsService.changeSettings({ settings }, options);
979
+ await plansService.changePlan(plan._id, planInput(listId, { settingsOverrides: { maximumDailyWorkMinutes: { enabled: false, minutes: 420 } } }), options);
980
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [secondParticipantId] });
981
+ });
982
+ it('protects a future shift governed by another project plan rest override', async () => {
983
+ const { plan, shift } = await setupAutofill({ start: `${day}T20:00:00+02:00`, end: `${day}T23:00:00+02:00` });
984
+ const otherListId = new ObjectId();
985
+ const otherParticipantId = new ObjectId();
986
+ const otherOptions = { ...options, project: 'other-project' };
987
+ await settingsService.changeSettings({ settings: policy() }, otherOptions);
988
+ await ListsModel.db.insertOne({ ...list, _id: otherListId, project: otherOptions.project });
989
+ await ParticipantsModel.db.insertOne({ ...participant, _id: otherParticipantId, user: secondCustomerId, project: otherOptions.project, list: otherListId });
990
+ const otherPlan = await otherProjectPlan(otherListId, otherOptions, { minimumRest: { enabled: true, minutes: 660 } });
991
+ await shiftsService.createShift(otherPlan._id, shiftInput(otherPlan, planAreas(otherPlan)[0].area, {
992
+ day: nextDay, assignments: [{ participant: otherParticipantId }],
993
+ }), otherOptions);
994
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [participantId] });
995
+ });
996
+ it('requires a valid shared break template when break rules demand it', async () => {
997
+ const { plan, shift } = await setupAutofill({ end: `${day}T16:00:00+02:00` });
998
+ const settings = policy();
999
+ settings.breaks.enabled = true;
1000
+ await settingsService.changeSettings({ settings }, options);
1001
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [] });
1002
+ const breaks = [{ start: `${day}T12:00:00+02:00`, end: `${day}T12:30:00+02:00` }];
1003
+ const originalBreaks = JSON.stringify(breaks);
1004
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options, breaks)).toStrictEqual({ shift: shift._id, users: [secondParticipantId] });
1005
+ expect(JSON.stringify(breaks)).toBe(originalBreaks);
1006
+ expect((await ShiftModel.db.findOne({ _id: shift._id })).assignments).toEqual([]);
1007
+ await expect(shiftsService.autofillShift(plan._id, shift._id, options, [{ start: `${day}T07:00:00+02:00`, end: `${day}T08:00:00+02:00` }])).rejects.toThrow('VALIDATION_FAILED');
1008
+ });
1009
+ it('requires effective settings but accepts complete plan overrides without general settings', async () => {
1010
+ const created = await plansService.createPlan(planInput(), options);
1011
+ await createDays(created._id, options, [{ date: day, areas: areaLinks() }]);
1012
+ const plan = (await plansService.singlePlan(created._id, options));
1013
+ const shift = await shiftsService.createShift(plan._id, {
1014
+ day: dayId(plan, day), area: housekeepingAreaId, title: 'Empty', start: `${day}T08:00:00+02:00`, end: `${day}T12:00:00+02:00`,
1015
+ requiredPeople: 1, assignments: [],
1016
+ }, options);
1017
+ await expect(shiftsService.autofillShift(plan._id, shift._id, options)).rejects.toThrow('SHIFT_PLANNING_SETTINGS_NOT_CONFIGURED');
1018
+ await plansService.changePlan(plan._id, planInput(listId, { settingsOverrides: policy() }), options);
1019
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [participantId] });
1020
+ });
1021
+ it('uses the current plan list rather than a stale shift list', async () => {
1022
+ const { plan, shift } = await setupAutofill();
1023
+ await plansService.changePlan(plan._id, { ...planInput(), list: undefined }, options);
1024
+ expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [] });
1025
+ });
1026
+ it('does not read a foreign tenant or another plan shift', async () => {
1027
+ const { plan, shift } = await setupAutofill();
1028
+ await expect(shiftsService.autofillShift(plan._id, shift._id, { ...options, company: 'foreign' })).rejects.toThrow('SHIFT_PLAN_NOT_FOUND');
1029
+ await expect(shiftsService.autofillShift(plan._id, shift._id, { ...options, project: 'foreign' })).rejects.toThrow('SHIFT_PLAN_NOT_FOUND');
1030
+ await expect(shiftsService.autofillShift(plan._id, new ObjectId(), options)).rejects.toThrow('SHIFT_NOT_FOUND');
1031
+ const sibling = await setupPlan();
1032
+ await expect(shiftsService.autofillShift(sibling._id, shift._id, options)).rejects.toThrow('SHIFT_NOT_FOUND');
1033
+ });
1034
+ it('returns the resolved suggestion as a report without persisting it', async () => {
1035
+ const { plan, shift } = await setupAutofill({ requiredPeople: 2 });
1036
+ const before = await ShiftModel.db.findOne({ _id: shift._id });
1037
+ const report = await shiftsService.autofillShiftReport(plan._id, shift._id, options);
1038
+ const suggested = [secondParticipantId.toHexString(), participantId.toHexString()];
1039
+ expect(report._id.equals(shift._id)).toBe(true);
1040
+ expect((report.assignments || []).map((assignment) => assignment.participant.toHexString())).toEqual(suggested);
1041
+ expect(report.participants.map((participant) => participant.participant.toHexString())).toEqual(suggested);
1042
+ // all scheduling rules run, so a clean suggestion has no violations
1043
+ expect(report.violations).toEqual([]);
1044
+ expect(await ShiftModel.db.findOne({ _id: shift._id })).toEqual(before);
1045
+ });
1046
+ });
1047
+ describe('scope shift autofill', () => {
1048
+ it('reuses preparation and reads the company conflict window only once', async () => {
1049
+ const plan = await setupPlan();
1050
+ await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, { assignments: [], requiredPeople: 1 }), options);
1051
+ const shiftReads = jest.spyOn(ShiftModel.db, 'find');
1052
+ const settingsReads = jest.spyOn(settingsService, 'singleSettings');
1053
+ const participantReads = jest.spyOn(ParticipantsModel.db, 'find');
1054
+ const customerReads = jest.spyOn(CustomersModel.db, 'find');
1055
+ const structureReads = jest.spyOn(StructureModel.db, 'find');
1056
+ const reports = await shiftsService.autofill({ level: 'plan', plan: plan._id }, options);
1057
+ expect(reports[0].assignments).toHaveLength(1);
1058
+ expect(shiftReads.mock.calls.filter(([query]) => query && 'assignments.customer' in query)).toHaveLength(1);
1059
+ expect(settingsReads).toHaveBeenCalledTimes(1);
1060
+ expect(participantReads).toHaveBeenCalledTimes(2);
1061
+ expect(customerReads).toHaveBeenCalledTimes(2);
1062
+ expect(structureReads).toHaveBeenCalledTimes(2);
1063
+ });
1064
+ it('does not expand the conflict window around distant neighbours', async () => {
1065
+ const plan = await setupPlan();
1066
+ const target = await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, { assignments: [], requiredPeople: 1 }), options);
1067
+ const storedPlan = (await ShiftPlanModel.db.findOne({ _id: plan._id }));
1068
+ const storedShift = (await ShiftModel.db.findOne({ _id: target._id }));
1069
+ const neighbourPlan = new ObjectId();
1070
+ const distantPlan = new ObjectId();
1071
+ await ShiftPlanModel.db.insertMany([{ ...storedPlan, _id: neighbourPlan }, { ...storedPlan, _id: distantPlan }]);
1072
+ await ShiftModel.db.insertMany([
1073
+ { ...storedShift, _id: new ObjectId(), plan: neighbourPlan, start: '2027-08-01T08:00:00.000Z', end: '2027-08-01T12:00:00.000Z',
1074
+ assignments: [{ participant: secondParticipantId, customer: secondCustomerId, breaks: [] }] },
1075
+ { ...storedShift, _id: new ObjectId(), plan: distantPlan, start: '2028-05-01T08:00:00.000Z', end: '2028-05-01T12:00:00.000Z',
1076
+ assignments: [{ participant: secondParticipantId, customer: secondCustomerId, breaks: [] }] },
1077
+ ]);
1078
+ const planReads = jest.spyOn(plansService, 'plansForCompany');
1079
+ const reports = await shiftsService.autofill({ level: 'plan', plan: plan._id }, options);
1080
+ expect(reports[0].assignments).toHaveLength(1);
1081
+ expect(reports[0].violations).toEqual([]);
1082
+ expect(planReads).toHaveBeenCalledTimes(1);
1083
+ expect(planReads.mock.calls[0][0].map((id) => id.toHexString())).toEqual([neighbourPlan.toHexString()]);
1084
+ });
1085
+ it('still protects a future shift governed by another project rest override', async () => {
1086
+ const plan = await setupPlan();
1087
+ const target = await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, {
1088
+ assignments: [], requiredPeople: 2, start: `${day}T20:00:00+02:00`, end: `${day}T23:00:00+02:00`,
1089
+ }), options);
1090
+ const otherListId = new ObjectId();
1091
+ const otherParticipantId = new ObjectId();
1092
+ const otherOptions = { ...options, project: 'other-project' };
1093
+ await settingsService.changeSettings({ settings: policy() }, otherOptions);
1094
+ await ListsModel.db.insertOne({ ...list, _id: otherListId, project: otherOptions.project });
1095
+ await ParticipantsModel.db.insertOne({ ...participant, _id: otherParticipantId, user: secondCustomerId, project: otherOptions.project, list: otherListId });
1096
+ const otherPlan = await otherProjectPlan(otherListId, otherOptions, { minimumRest: { enabled: true, minutes: 660 } });
1097
+ await shiftsService.createShift(otherPlan._id, shiftInput(otherPlan, planAreas(otherPlan)[0].area, {
1098
+ day: nextDay, assignments: [{ participant: otherParticipantId }],
1099
+ }), otherOptions);
1100
+ const reports = await shiftsService.autofill({ level: 'shift', plan: plan._id, shift: target._id }, options);
1101
+ expect(reports[0].assignments.map((assignment) => assignment.participant.toHexString())).toEqual([participantId.toHexString()]);
1102
+ expect(reports[0].violations).toEqual([]);
1103
+ });
1104
+ it('fills every under-staffed shift of a plan without double booking a person', async () => {
1105
+ const plan = await setupPlan();
1106
+ const morning = await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, {
1107
+ assignments: [], requiredPeople: 1, start: `${day}T08:00:00+02:00`, end: `${day}T16:00:00+02:00`,
1108
+ }), options);
1109
+ const evening = await shiftsService.createShift(plan._id, shiftInput(plan, receptionAreaId, {
1110
+ assignments: [], requiredPeople: 1, start: `${day}T12:00:00+02:00`, end: `${day}T20:00:00+02:00`,
1111
+ }), options);
1112
+ const reports = await shiftsService.autofill({ level: 'plan', plan: plan._id }, options);
1113
+ const byId = new Map(reports.map((report) => [report._id.toHexString(), report]));
1114
+ expect([...byId.keys()].sort()).toEqual([morning._id.toHexString(), evening._id.toHexString()].sort());
1115
+ const morningAssignee = byId.get(morning._id.toHexString()).assignments.map((assignment) => assignment.participant.toHexString());
1116
+ const eveningAssignee = byId.get(evening._id.toHexString()).assignments.map((assignment) => assignment.participant.toHexString());
1117
+ expect(morningAssignee).toHaveLength(1);
1118
+ expect(eveningAssignee).toHaveLength(1);
1119
+ // The shifts overlap, so the shared allocation cannot reuse the same person.
1120
+ expect(morningAssignee[0]).not.toBe(eveningAssignee[0]);
1121
+ });
1122
+ it('restricts a day scope to that day only', async () => {
1123
+ const plan = await setupPlan();
1124
+ const today = await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, { assignments: [], requiredPeople: 1 }), options);
1125
+ const tomorrow = await shiftsService.createShift(plan._id, shiftInput(plan, receptionAreaId, {
1126
+ assignments: [], requiredPeople: 1, day: nextDay, start: `${nextDay}T08:00:00+02:00`, end: `${nextDay}T16:00:00+02:00`,
1127
+ }), options);
1128
+ const reports = await shiftsService.autofill({ level: 'day', plan: plan._id, day: dayId(plan, day) }, options);
1129
+ expect(reports.map((report) => report._id.toHexString())).toEqual([today._id.toHexString()]);
1130
+ expect((await ShiftModel.db.findOne({ _id: tomorrow._id })).assignments).toEqual([]);
1131
+ });
1132
+ it('restricts a work area scope to that area only', async () => {
1133
+ const plan = await setupPlan();
1134
+ const housekeeping = await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, { assignments: [], requiredPeople: 1 }), options);
1135
+ const reception = await shiftsService.createShift(plan._id, shiftInput(plan, receptionAreaId, { assignments: [], requiredPeople: 1 }), options);
1136
+ const reports = await shiftsService.autofill({ level: 'workArea', plan: plan._id, workArea: housekeepingAreaId }, options);
1137
+ expect(reports.map((report) => report._id.toHexString())).toEqual([housekeeping._id.toHexString()]);
1138
+ expect((await ShiftModel.db.findOne({ _id: reception._id })).assignments).toEqual([]);
1139
+ });
1140
+ it('fills a single shift for a shift scope without persisting it', async () => {
1141
+ const plan = await setupPlan();
1142
+ const shift = await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, { assignments: [], requiredPeople: 1 }), options);
1143
+ const reports = await shiftsService.autofill({ level: 'shift', plan: plan._id, shift: shift._id }, options);
1144
+ expect(reports).toHaveLength(1);
1145
+ expect(reports[0]._id.equals(shift._id)).toBe(true);
1146
+ expect(reports[0].assignments).toHaveLength(1);
1147
+ expect(reports[0].violations).toEqual([]);
1148
+ expect((await ShiftModel.db.findOne({ _id: shift._id })).assignments).toEqual([]);
1149
+ });
1150
+ it('treats an overlapping shift in another plan as a conflict neighbour', async () => {
1151
+ const plan = await setupPlan();
1152
+ const shift = await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, { assignments: [], requiredPeople: 1 }), options);
1153
+ const sibling = await setupPlan();
1154
+ await shiftsService.createShift(sibling._id, shiftInput(sibling, housekeepingAreaId, {
1155
+ assignments: [{ participant: secondParticipantId }], start: `${day}T08:00:00+02:00`, end: `${day}T16:00:00+02:00`,
1156
+ }), options);
1157
+ const reports = await shiftsService.autofill({ level: 'shift', plan: plan._id, shift: shift._id }, options);
1158
+ expect(reports[0].assignments.map((assignment) => assignment.participant.toHexString())).toEqual([participantId.toHexString()]);
1159
+ });
1160
+ });
834
1161
  });
835
1162
  //# sourceMappingURL=shiftPlanning.spec.js.map