feat(api): add staff order detail and compliance eligibility
This commit is contained in:
39
backend/command-api/src/lib/staff-order-eligibility.js
Normal file
39
backend/command-api/src/lib/staff-order-eligibility.js
Normal file
@@ -0,0 +1,39 @@
|
||||
function dedupeStrings(values = []) {
|
||||
return [...new Set(
|
||||
values
|
||||
.filter((value) => typeof value === 'string')
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
)];
|
||||
}
|
||||
|
||||
export function dedupeDocumentNames(values = []) {
|
||||
return dedupeStrings(values);
|
||||
}
|
||||
|
||||
export function buildStaffOrderEligibilityBlockers({
|
||||
hasActiveWorkforce = true,
|
||||
businessBlockReason = null,
|
||||
hasExistingParticipation = false,
|
||||
missingDocumentNames = [],
|
||||
} = {}) {
|
||||
const blockers = [];
|
||||
|
||||
if (!hasActiveWorkforce) {
|
||||
blockers.push('Workforce profile is not active');
|
||||
}
|
||||
|
||||
if (businessBlockReason !== null && businessBlockReason !== undefined) {
|
||||
blockers.push(businessBlockReason
|
||||
? `You are blocked from working for this client: ${businessBlockReason}`
|
||||
: 'You are blocked from working for this client');
|
||||
}
|
||||
|
||||
if (hasExistingParticipation) {
|
||||
blockers.push('You already applied to or booked this order');
|
||||
}
|
||||
|
||||
blockers.push(...dedupeDocumentNames(missingDocumentNames).map((name) => `Missing required document: ${name}`));
|
||||
|
||||
return dedupeStrings(blockers);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { AppError } from '../lib/errors.js';
|
||||
import { buildStaffOrderEligibilityBlockers, dedupeDocumentNames } from '../lib/staff-order-eligibility.js';
|
||||
import { query, withTransaction } from './db.js';
|
||||
import { loadActorContext, requireClientContext, requireStaffContext } from './actor-context.js';
|
||||
import { recordGeofenceIncident } from './attendance-monitoring.js';
|
||||
@@ -89,6 +90,53 @@ async function ensureStaffNotBlockedByBusiness(client, { tenantId, businessId, s
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMissingRequiredDocuments(client, { tenantId, roleCode, staffId }) {
|
||||
if (!roleCode) return [];
|
||||
|
||||
const result = await client.query(
|
||||
`
|
||||
SELECT d.name
|
||||
FROM documents d
|
||||
WHERE d.tenant_id = $1
|
||||
AND d.required_for_role_code = $2
|
||||
AND d.document_type <> 'ATTIRE'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM staff_documents sd
|
||||
WHERE sd.tenant_id = d.tenant_id
|
||||
AND sd.staff_id = $3
|
||||
AND sd.document_id = d.id
|
||||
AND sd.status = 'VERIFIED'
|
||||
)
|
||||
ORDER BY d.name ASC
|
||||
`,
|
||||
[tenantId, roleCode, staffId]
|
||||
);
|
||||
|
||||
return dedupeDocumentNames(result.rows.map((row) => row.name));
|
||||
}
|
||||
|
||||
function buildMissingDocumentErrorDetails({
|
||||
roleCode,
|
||||
orderId = null,
|
||||
shiftId = null,
|
||||
roleId = null,
|
||||
missingDocumentNames = [],
|
||||
}) {
|
||||
const blockers = buildStaffOrderEligibilityBlockers({
|
||||
missingDocumentNames,
|
||||
});
|
||||
|
||||
return {
|
||||
orderId,
|
||||
shiftId,
|
||||
roleId,
|
||||
roleCode: roleCode || null,
|
||||
blockers,
|
||||
missingDocuments: dedupeDocumentNames(missingDocumentNames),
|
||||
};
|
||||
}
|
||||
|
||||
function buildAssignmentReferencePayload(assignment) {
|
||||
return {
|
||||
assignmentId: assignment.id,
|
||||
@@ -3024,6 +3072,20 @@ export async function bookOrder(actor, payload) {
|
||||
staffId: staff.id,
|
||||
});
|
||||
|
||||
const missingRequiredDocuments = await loadMissingRequiredDocuments(client, {
|
||||
tenantId: context.tenant.tenantId,
|
||||
roleCode: selectedRole.code,
|
||||
staffId: staff.id,
|
||||
});
|
||||
if (missingRequiredDocuments.length > 0) {
|
||||
throw new AppError('UNPROCESSABLE_ENTITY', 'Staff is missing required documents for this role', 422, buildMissingDocumentErrorDetails({
|
||||
orderId: payload.orderId,
|
||||
roleId: payload.roleId,
|
||||
roleCode: selectedRole.code,
|
||||
missingDocumentNames: missingRequiredDocuments,
|
||||
}));
|
||||
}
|
||||
|
||||
const bookingId = crypto.randomUUID();
|
||||
const assignedShifts = [];
|
||||
|
||||
|
||||
14
backend/command-api/test/staff-order-eligibility.test.js
Normal file
14
backend/command-api/test/staff-order-eligibility.test.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { buildStaffOrderEligibilityBlockers } from '../src/lib/staff-order-eligibility.js';
|
||||
|
||||
test('buildStaffOrderEligibilityBlockers formats missing document blockers for command flows', () => {
|
||||
const blockers = buildStaffOrderEligibilityBlockers({
|
||||
missingDocumentNames: ['Food Handler Card', 'Food Handler Card', ' Responsible Beverage Service '],
|
||||
});
|
||||
|
||||
assert.deepEqual(blockers, [
|
||||
'Missing required document: Food Handler Card',
|
||||
'Missing required document: Responsible Beverage Service',
|
||||
]);
|
||||
});
|
||||
Reference in New Issue
Block a user