feat(attendance): add notification delivery and NFC security foundation

This commit is contained in:
zouantchaw
2026-03-16 17:06:17 +01:00
parent 5d8240ed51
commit 73287f42bd
21 changed files with 1734 additions and 36 deletions

View File

@@ -6,6 +6,7 @@ import { recordGeofenceIncident } from './attendance-monitoring.js';
import { distanceMeters, resolveEffectiveClockInPolicy } from './clock-in-policy.js';
import { uploadLocationBatch } from './location-log-storage.js';
import { enqueueHubManagerAlert, enqueueUserAlert } from './notification-outbox.js';
import { registerPushToken, unregisterPushToken } from './notification-device-tokens.js';
import {
cancelOrder as cancelOrderCommand,
clockIn as clockInCommand,
@@ -1614,8 +1615,13 @@ export async function staffClockIn(actor, payload) {
accuracyMeters: payload.accuracyMeters,
capturedAt: payload.capturedAt,
overrideReason: payload.overrideReason || null,
proofNonce: payload.proofNonce || null,
proofTimestamp: payload.proofTimestamp || null,
attestationProvider: payload.attestationProvider || null,
attestationToken: payload.attestationToken || null,
rawPayload: {
notes: payload.notes || null,
isMockLocation: payload.isMockLocation ?? null,
...(payload.rawPayload || {}),
},
});
@@ -1640,15 +1646,116 @@ export async function staffClockOut(actor, payload) {
accuracyMeters: payload.accuracyMeters,
capturedAt: payload.capturedAt,
overrideReason: payload.overrideReason || null,
proofNonce: payload.proofNonce || null,
proofTimestamp: payload.proofTimestamp || null,
attestationProvider: payload.attestationProvider || null,
attestationToken: payload.attestationToken || null,
rawPayload: {
notes: payload.notes || null,
breakMinutes: payload.breakMinutes ?? null,
applicationId: payload.applicationId || null,
isMockLocation: payload.isMockLocation ?? null,
...(payload.rawPayload || {}),
},
});
}
export async function registerClientPushToken(actor, payload) {
const context = await requireClientContext(actor.uid);
return withTransaction(async (client) => {
await ensureActorUser(client, actor);
const token = await registerPushToken(client, {
tenantId: context.tenant.tenantId,
userId: actor.uid,
businessMembershipId: context.business.membershipId,
provider: payload.provider,
platform: payload.platform,
pushToken: payload.pushToken,
deviceId: payload.deviceId || null,
appVersion: payload.appVersion || null,
appBuild: payload.appBuild || null,
locale: payload.locale || null,
timezone: payload.timezone || null,
notificationsEnabled: payload.notificationsEnabled ?? true,
metadata: payload.metadata || {},
});
return {
tokenId: token.id,
provider: token.provider,
platform: token.platform,
notificationsEnabled: token.notificationsEnabled,
};
});
}
export async function unregisterClientPushToken(actor, payload) {
const context = await requireClientContext(actor.uid);
return withTransaction(async (client) => {
await ensureActorUser(client, actor);
const removed = await unregisterPushToken(client, {
tenantId: context.tenant.tenantId,
userId: actor.uid,
tokenId: payload.tokenId || null,
pushToken: payload.pushToken || null,
reason: payload.reason || 'CLIENT_SIGN_OUT',
});
return {
removedCount: removed.length,
removed,
};
});
}
export async function registerStaffPushToken(actor, payload) {
const context = await requireStaffContext(actor.uid);
return withTransaction(async (client) => {
await ensureActorUser(client, actor);
const token = await registerPushToken(client, {
tenantId: context.tenant.tenantId,
userId: actor.uid,
staffId: context.staff.staffId,
provider: payload.provider,
platform: payload.platform,
pushToken: payload.pushToken,
deviceId: payload.deviceId || null,
appVersion: payload.appVersion || null,
appBuild: payload.appBuild || null,
locale: payload.locale || null,
timezone: payload.timezone || null,
notificationsEnabled: payload.notificationsEnabled ?? true,
metadata: payload.metadata || {},
});
return {
tokenId: token.id,
provider: token.provider,
platform: token.platform,
notificationsEnabled: token.notificationsEnabled,
};
});
}
export async function unregisterStaffPushToken(actor, payload) {
const context = await requireStaffContext(actor.uid);
return withTransaction(async (client) => {
await ensureActorUser(client, actor);
const removed = await unregisterPushToken(client, {
tenantId: context.tenant.tenantId,
userId: actor.uid,
tokenId: payload.tokenId || null,
pushToken: payload.pushToken || null,
reason: payload.reason || 'STAFF_SIGN_OUT',
});
return {
removedCount: removed.length,
removed,
};
});
}
function summarizeLocationPoints(points, assignment) {
let outOfGeofenceCount = 0;
let missingCoordinateCount = 0;