feat(attendance): add geofence monitoring and policy controls
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user