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

@@ -48,13 +48,30 @@ function createMobileHandlers() {
invoiceId: payload.invoiceId,
status: 'APPROVED',
}),
registerClientPushToken: async (_actor, payload) => ({
tokenId: 'push-token-client-1',
platform: payload.platform,
notificationsEnabled: payload.notificationsEnabled ?? true,
}),
unregisterClientPushToken: async () => ({
removedCount: 1,
}),
applyForShift: async (_actor, payload) => ({
shiftId: payload.shiftId,
status: 'APPLIED',
}),
registerStaffPushToken: async (_actor, payload) => ({
tokenId: 'push-token-staff-1',
platform: payload.platform,
notificationsEnabled: payload.notificationsEnabled ?? true,
}),
unregisterStaffPushToken: async () => ({
removedCount: 1,
}),
staffClockIn: async (_actor, payload) => ({
assignmentId: payload.assignmentId || 'assignment-1',
status: 'CLOCK_IN',
proofNonce: payload.proofNonce || null,
}),
staffClockOut: async (_actor, payload) => ({
assignmentId: payload.assignmentId || 'assignment-1',
@@ -157,6 +174,36 @@ test('POST /commands/client/billing/invoices/:invoiceId/approve injects invoice
assert.equal(res.body.status, 'APPROVED');
});
test('POST /commands/client/devices/push-tokens registers a client push token', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
.post('/commands/client/devices/push-tokens')
.set('Authorization', 'Bearer test-token')
.set('Idempotency-Key', 'client-push-token-1')
.send({
provider: 'FCM',
platform: 'IOS',
pushToken: 'f'.repeat(160),
deviceId: 'iphone-15-pro',
notificationsEnabled: true,
});
assert.equal(res.status, 200);
assert.equal(res.body.tokenId, 'push-token-client-1');
assert.equal(res.body.platform, 'IOS');
});
test('DELETE /commands/client/devices/push-tokens accepts tokenId from query params', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
.delete('/commands/client/devices/push-tokens?tokenId=11111111-1111-4111-8111-111111111111&reason=SMOKE_CLEANUP')
.set('Authorization', 'Bearer test-token')
.set('Idempotency-Key', 'client-push-token-delete-1');
assert.equal(res.status, 200);
assert.equal(res.body.removedCount, 1);
});
test('POST /commands/staff/shifts/:shiftId/apply injects shift id from params', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
@@ -183,11 +230,13 @@ test('POST /commands/staff/clock-in accepts shift-based payload', async () => {
sourceType: 'GEO',
latitude: 37.422,
longitude: -122.084,
proofNonce: 'nonce-12345678',
overrideReason: 'GPS timed out near the hub',
});
assert.equal(res.status, 200);
assert.equal(res.body.status, 'CLOCK_IN');
assert.equal(res.body.proofNonce, 'nonce-12345678');
});
test('POST /commands/staff/clock-out accepts assignment-based payload', async () => {
@@ -230,6 +279,35 @@ test('POST /commands/staff/location-streams accepts batched location payloads',
assert.equal(res.body.pointCount, 1);
});
test('POST /commands/staff/devices/push-tokens registers a staff push token', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
.post('/commands/staff/devices/push-tokens')
.set('Authorization', 'Bearer test-token')
.set('Idempotency-Key', 'staff-push-token-1')
.send({
provider: 'FCM',
platform: 'ANDROID',
pushToken: 'g'.repeat(170),
deviceId: 'pixel-9',
});
assert.equal(res.status, 200);
assert.equal(res.body.tokenId, 'push-token-staff-1');
assert.equal(res.body.platform, 'ANDROID');
});
test('DELETE /commands/staff/devices/push-tokens accepts tokenId from query params', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
.delete('/commands/staff/devices/push-tokens?tokenId=22222222-2222-4222-8222-222222222222&reason=SMOKE_CLEANUP')
.set('Authorization', 'Bearer test-token')
.set('Idempotency-Key', 'staff-push-token-delete-1');
assert.equal(res.status, 200);
assert.equal(res.body.removedCount, 1);
});
test('PUT /commands/staff/profile/tax-forms/:formType uppercases form type', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)