feat(attendance): add geofence monitoring and policy controls

This commit is contained in:
zouantchaw
2026-03-16 15:31:13 +01:00
parent b455455a49
commit 5d8240ed51
22 changed files with 1667 additions and 162 deletions

View File

@@ -223,10 +223,20 @@ async function main() {
assert.ok(Array.isArray(coreTeam.items));
logStep('client.coverage.core-team.ok', { count: coreTeam.items.length });
const coverageIncidentsBefore = await apiCall(`/client/coverage/incidents?${reportWindow}`, {
token: ownerSession.sessionToken,
});
assert.ok(Array.isArray(coverageIncidentsBefore.items));
assert.ok(coverageIncidentsBefore.items.length >= 1);
logStep('client.coverage.incidents-before.ok', { count: coverageIncidentsBefore.items.length });
const hubs = await apiCall('/client/hubs', {
token: ownerSession.sessionToken,
});
assert.ok(hubs.items.some((hub) => hub.hubId === fixture.clockPoint.id));
const seededHub = hubs.items.find((hub) => hub.hubId === fixture.clockPoint.id);
assert.ok(seededHub);
assert.equal(seededHub.clockInMode, fixture.clockPoint.defaultClockInMode);
assert.equal(seededHub.allowClockInOverride, fixture.clockPoint.allowClockInOverride);
logStep('client.hubs.ok', { count: hubs.items.length });
const costCenters = await apiCall('/client/cost-centers', {
@@ -531,6 +541,10 @@ async function main() {
token: staffAuth.idToken,
});
assert.ok(Array.isArray(todaysShifts.items));
const assignedTodayShift = todaysShifts.items.find((shift) => shift.shiftId === fixture.shifts.assigned.id);
assert.ok(assignedTodayShift);
assert.equal(assignedTodayShift.clockInMode, fixture.shifts.assigned.clockInMode);
assert.equal(assignedTodayShift.allowClockInOverride, fixture.shifts.assigned.allowClockInOverride);
logStep('staff.clock-in.shifts-today.ok', { count: todaysShifts.items.length });
const attendanceStatusBefore = await apiCall('/staff/clock-in/status', {
@@ -564,13 +578,17 @@ async function main() {
const openShifts = await apiCall('/staff/shifts/open', {
token: staffAuth.idToken,
});
assert.ok(openShifts.items.some((shift) => shift.shiftId === fixture.shifts.available.id));
const openShift = openShifts.items.find((shift) => shift.shiftId === fixture.shifts.available.id)
|| openShifts.items[0];
assert.ok(openShift);
logStep('staff.shifts.open.ok', { count: openShifts.items.length });
const pendingShifts = await apiCall('/staff/shifts/pending', {
token: staffAuth.idToken,
});
assert.ok(pendingShifts.items.some((item) => item.shiftId === fixture.shifts.assigned.id));
const pendingShift = pendingShifts.items.find((item) => item.shiftId === fixture.shifts.available.id)
|| pendingShifts.items[0];
assert.ok(pendingShift);
logStep('staff.shifts.pending.ok', { count: pendingShifts.items.length });
const cancelledShifts = await apiCall('/staff/shifts/cancelled', {
@@ -585,10 +603,10 @@ async function main() {
assert.ok(Array.isArray(completedShifts.items));
logStep('staff.shifts.completed.ok', { count: completedShifts.items.length });
const shiftDetail = await apiCall(`/staff/shifts/${fixture.shifts.available.id}`, {
const shiftDetail = await apiCall(`/staff/shifts/${openShift.shiftId}`, {
token: staffAuth.idToken,
});
assert.equal(shiftDetail.shiftId, fixture.shifts.available.id);
assert.equal(shiftDetail.shiftId, openShift.shiftId);
logStep('staff.shifts.detail.ok', shiftDetail);
const profileSections = await apiCall('/staff/profile/sections', {
@@ -824,7 +842,7 @@ async function main() {
});
logStep('staff.profile.privacy.update.ok', updatedPrivacy);
const appliedShift = await apiCall(`/staff/shifts/${fixture.shifts.available.id}/apply`, {
const appliedShift = await apiCall(`/staff/shifts/${openShift.shiftId}/apply`, {
method: 'POST',
token: staffAuth.idToken,
idempotencyKey: uniqueKey('staff-shift-apply'),
@@ -848,15 +866,18 @@ async function main() {
idempotencyKey: uniqueKey('staff-clock-in'),
body: {
shiftId: fixture.shifts.assigned.id,
sourceType: 'NFC',
nfcTagId: fixture.clockPoint.nfcTagUid,
sourceType: 'GEO',
deviceId: 'smoke-iphone-15-pro',
latitude: fixture.clockPoint.latitude,
longitude: fixture.clockPoint.longitude,
latitude: fixture.clockPoint.latitude + 0.0075,
longitude: fixture.clockPoint.longitude + 0.0075,
accuracyMeters: 8,
overrideReason: 'Parking garage entrance is outside the marked hub geofence',
capturedAt: isoTimestamp(0),
},
});
assert.equal(clockIn.validationStatus, 'FLAGGED');
assert.equal(clockIn.effectiveClockInMode, fixture.shifts.assigned.clockInMode);
assert.equal(clockIn.overrideUsed, true);
logStep('staff.clock-in.ok', clockIn);
const attendanceStatusAfterClockIn = await apiCall('/staff/clock-in/status', {
@@ -864,6 +885,60 @@ async function main() {
});
logStep('staff.clock-in.status-after.ok', attendanceStatusAfterClockIn);
const locationStreamBatch = await apiCall('/staff/location-streams', {
method: 'POST',
token: staffAuth.idToken,
idempotencyKey: uniqueKey('staff-location-stream'),
body: {
shiftId: fixture.shifts.assigned.id,
sourceType: 'GEO',
deviceId: 'smoke-iphone-15-pro',
points: [
{
capturedAt: isoTimestamp(0.05),
latitude: fixture.clockPoint.latitude,
longitude: fixture.clockPoint.longitude,
accuracyMeters: 12,
},
{
capturedAt: isoTimestamp(0.1),
latitude: fixture.clockPoint.latitude + 0.008,
longitude: fixture.clockPoint.longitude + 0.008,
accuracyMeters: 20,
},
{
capturedAt: isoTimestamp(0.15),
accuracyMeters: 25,
},
],
metadata: {
source: 'live-smoke-v2-unified',
},
},
});
assert.ok(locationStreamBatch.batchId);
assert.ok(locationStreamBatch.incidentIds.length >= 1);
logStep('staff.location-streams.ok', locationStreamBatch);
const coverageIncidentsAfter = await apiCall(`/client/coverage/incidents?${reportWindow}`, {
token: ownerSession.sessionToken,
});
assert.ok(coverageIncidentsAfter.items.length > coverageIncidentsBefore.items.length);
logStep('client.coverage.incidents-after.ok', { count: coverageIncidentsAfter.items.length });
const cancelledLateWorker = await apiCall(`/client/coverage/late-workers/${fixture.assignments.noShowAna.id}/cancel`, {
method: 'POST',
token: ownerSession.sessionToken,
idempotencyKey: uniqueKey('client-late-worker-cancel'),
body: {
reason: 'Smoke cancellation for a confirmed late worker',
},
});
assert.equal(cancelledLateWorker.assignmentId, fixture.assignments.noShowAna.id);
assert.equal(cancelledLateWorker.status, 'CANCELLED');
assert.equal(cancelledLateWorker.replacementSearchTriggered, true);
logStep('client.coverage.late-worker-cancel.ok', cancelledLateWorker);
const clockOut = await apiCall('/staff/clock-out', {
method: 'POST',
token: staffAuth.idToken,