feat(api): add unified v2 gateway and mobile read slice
This commit is contained in:
@@ -5,6 +5,7 @@ import { requestContext } from './middleware/request-context.js';
|
||||
import { errorHandler, notFoundHandler } from './middleware/error-handler.js';
|
||||
import { healthRouter } from './routes/health.js';
|
||||
import { createQueryRouter } from './routes/query.js';
|
||||
import { createMobileQueryRouter } from './routes/mobile.js';
|
||||
|
||||
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
|
||||
|
||||
@@ -22,6 +23,7 @@ export function createApp(options = {}) {
|
||||
|
||||
app.use(healthRouter);
|
||||
app.use('/query', createQueryRouter(options.queryService));
|
||||
app.use('/query', createMobileQueryRouter(options.mobileQueryService));
|
||||
|
||||
app.use(notFoundHandler);
|
||||
app.use(errorHandler);
|
||||
|
||||
464
backend/query-api/src/routes/mobile.js
Normal file
464
backend/query-api/src/routes/mobile.js
Normal file
@@ -0,0 +1,464 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth, requirePolicy } from '../middleware/auth.js';
|
||||
import {
|
||||
getClientDashboard,
|
||||
getClientSession,
|
||||
getCoverageStats,
|
||||
getCurrentAttendanceStatus,
|
||||
getCurrentBill,
|
||||
getPaymentChart,
|
||||
getPaymentsSummary,
|
||||
getPersonalInfo,
|
||||
getProfileSectionsStatus,
|
||||
getSavings,
|
||||
getStaffDashboard,
|
||||
getStaffProfileCompletion,
|
||||
getStaffSession,
|
||||
getStaffShiftDetail,
|
||||
listAssignedShifts,
|
||||
listBusinessAccounts,
|
||||
listCancelledShifts,
|
||||
listCertificates,
|
||||
listCostCenters,
|
||||
listCoverageByDate,
|
||||
listCompletedShifts,
|
||||
listHubManagers,
|
||||
listHubs,
|
||||
listIndustries,
|
||||
listInvoiceHistory,
|
||||
listOpenShifts,
|
||||
listOrderItemsByDateRange,
|
||||
listPaymentsHistory,
|
||||
listPendingAssignments,
|
||||
listPendingInvoices,
|
||||
listProfileDocuments,
|
||||
listRecentReorders,
|
||||
listSkills,
|
||||
listStaffAvailability,
|
||||
listStaffBankAccounts,
|
||||
listStaffBenefits,
|
||||
listTodayShifts,
|
||||
listVendorRoles,
|
||||
listVendors,
|
||||
getSpendBreakdown,
|
||||
} from '../services/mobile-query-service.js';
|
||||
|
||||
const defaultQueryService = {
|
||||
getClientDashboard,
|
||||
getClientSession,
|
||||
getCoverageStats,
|
||||
getCurrentAttendanceStatus,
|
||||
getCurrentBill,
|
||||
getPaymentChart,
|
||||
getPaymentsSummary,
|
||||
getPersonalInfo,
|
||||
getProfileSectionsStatus,
|
||||
getSavings,
|
||||
getSpendBreakdown,
|
||||
getStaffDashboard,
|
||||
getStaffProfileCompletion,
|
||||
getStaffSession,
|
||||
getStaffShiftDetail,
|
||||
listAssignedShifts,
|
||||
listBusinessAccounts,
|
||||
listCancelledShifts,
|
||||
listCertificates,
|
||||
listCostCenters,
|
||||
listCoverageByDate,
|
||||
listCompletedShifts,
|
||||
listHubManagers,
|
||||
listHubs,
|
||||
listIndustries,
|
||||
listInvoiceHistory,
|
||||
listOpenShifts,
|
||||
listOrderItemsByDateRange,
|
||||
listPaymentsHistory,
|
||||
listPendingAssignments,
|
||||
listPendingInvoices,
|
||||
listProfileDocuments,
|
||||
listRecentReorders,
|
||||
listSkills,
|
||||
listStaffAvailability,
|
||||
listStaffBankAccounts,
|
||||
listStaffBenefits,
|
||||
listTodayShifts,
|
||||
listVendorRoles,
|
||||
listVendors,
|
||||
};
|
||||
|
||||
function requireQueryParam(name, value) {
|
||||
if (!value) {
|
||||
const error = new Error(`${name} is required`);
|
||||
error.code = 'VALIDATION_ERROR';
|
||||
error.status = 400;
|
||||
error.details = { field: name };
|
||||
throw error;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function createMobileQueryRouter(queryService = defaultQueryService) {
|
||||
const router = Router();
|
||||
|
||||
router.get('/client/session', requireAuth, requirePolicy('client.session.read', 'session'), async (req, res, next) => {
|
||||
try {
|
||||
const data = await queryService.getClientSession(req.actor.uid);
|
||||
return res.status(200).json({ ...data, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client/dashboard', requireAuth, requirePolicy('client.dashboard.read', 'dashboard'), async (req, res, next) => {
|
||||
try {
|
||||
const data = await queryService.getClientDashboard(req.actor.uid);
|
||||
return res.status(200).json({ ...data, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client/reorders', requireAuth, requirePolicy('orders.reorder.read', 'order'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listRecentReorders(req.actor.uid, req.query.limit);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client/billing/accounts', requireAuth, requirePolicy('billing.accounts.read', 'billing'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listBusinessAccounts(req.actor.uid);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client/billing/invoices/pending', requireAuth, requirePolicy('billing.invoices.read', 'billing'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listPendingInvoices(req.actor.uid);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client/billing/invoices/history', requireAuth, requirePolicy('billing.invoices.read', 'billing'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listInvoiceHistory(req.actor.uid);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client/billing/current-bill', requireAuth, requirePolicy('billing.summary.read', 'billing'), async (req, res, next) => {
|
||||
try {
|
||||
const data = await queryService.getCurrentBill(req.actor.uid);
|
||||
return res.status(200).json({ ...data, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client/billing/savings', requireAuth, requirePolicy('billing.summary.read', 'billing'), async (req, res, next) => {
|
||||
try {
|
||||
const data = await queryService.getSavings(req.actor.uid);
|
||||
return res.status(200).json({ ...data, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client/billing/spend-breakdown', requireAuth, requirePolicy('billing.summary.read', 'billing'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.getSpendBreakdown(req.actor.uid, req.query);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client/coverage', requireAuth, requirePolicy('coverage.read', 'coverage'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listCoverageByDate(req.actor.uid, { date: requireQueryParam('date', req.query.date) });
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client/coverage/stats', requireAuth, requirePolicy('coverage.read', 'coverage'), async (req, res, next) => {
|
||||
try {
|
||||
const data = await queryService.getCoverageStats(req.actor.uid, { date: requireQueryParam('date', req.query.date) });
|
||||
return res.status(200).json({ ...data, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client/hubs', requireAuth, requirePolicy('hubs.read', 'hub'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listHubs(req.actor.uid);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client/cost-centers', requireAuth, requirePolicy('hubs.read', 'hub'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listCostCenters(req.actor.uid);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client/vendors', requireAuth, requirePolicy('vendors.read', 'vendor'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listVendors(req.actor.uid);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client/vendors/:vendorId/roles', requireAuth, requirePolicy('vendors.read', 'vendor'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listVendorRoles(req.actor.uid, req.params.vendorId);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client/hubs/:hubId/managers', requireAuth, requirePolicy('hubs.read', 'hub'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listHubManagers(req.actor.uid, req.params.hubId);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client/orders/view', requireAuth, requirePolicy('orders.read', 'order'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listOrderItemsByDateRange(req.actor.uid, req.query);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/session', requireAuth, requirePolicy('staff.session.read', 'session'), async (req, res, next) => {
|
||||
try {
|
||||
const data = await queryService.getStaffSession(req.actor.uid);
|
||||
return res.status(200).json({ ...data, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/dashboard', requireAuth, requirePolicy('staff.dashboard.read', 'dashboard'), async (req, res, next) => {
|
||||
try {
|
||||
const data = await queryService.getStaffDashboard(req.actor.uid);
|
||||
return res.status(200).json({ ...data, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/profile-completion', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => {
|
||||
try {
|
||||
const data = await queryService.getStaffProfileCompletion(req.actor.uid);
|
||||
return res.status(200).json({ ...data, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/availability', requireAuth, requirePolicy('staff.availability.read', 'staff'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listStaffAvailability(req.actor.uid, req.query);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/clock-in/shifts/today', requireAuth, requirePolicy('attendance.read', 'attendance'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listTodayShifts(req.actor.uid);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/clock-in/status', requireAuth, requirePolicy('attendance.read', 'attendance'), async (req, res, next) => {
|
||||
try {
|
||||
const data = await queryService.getCurrentAttendanceStatus(req.actor.uid);
|
||||
return res.status(200).json({ ...data, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/payments/summary', requireAuth, requirePolicy('payments.read', 'payment'), async (req, res, next) => {
|
||||
try {
|
||||
const data = await queryService.getPaymentsSummary(req.actor.uid, req.query);
|
||||
return res.status(200).json({ ...data, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/payments/history', requireAuth, requirePolicy('payments.read', 'payment'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listPaymentsHistory(req.actor.uid, req.query);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/payments/chart', requireAuth, requirePolicy('payments.read', 'payment'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.getPaymentChart(req.actor.uid, req.query);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/shifts/assigned', requireAuth, requirePolicy('shifts.read', 'shift'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listAssignedShifts(req.actor.uid, req.query);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/shifts/open', requireAuth, requirePolicy('shifts.read', 'shift'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listOpenShifts(req.actor.uid, req.query);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/shifts/pending', requireAuth, requirePolicy('shifts.read', 'shift'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listPendingAssignments(req.actor.uid);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/shifts/cancelled', requireAuth, requirePolicy('shifts.read', 'shift'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listCancelledShifts(req.actor.uid);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/shifts/completed', requireAuth, requirePolicy('shifts.read', 'shift'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listCompletedShifts(req.actor.uid);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/shifts/:shiftId', requireAuth, requirePolicy('shifts.read', 'shift'), async (req, res, next) => {
|
||||
try {
|
||||
const data = await queryService.getStaffShiftDetail(req.actor.uid, req.params.shiftId);
|
||||
return res.status(200).json({ ...data, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/profile/sections', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => {
|
||||
try {
|
||||
const data = await queryService.getProfileSectionsStatus(req.actor.uid);
|
||||
return res.status(200).json({ ...data, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/profile/personal-info', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => {
|
||||
try {
|
||||
const data = await queryService.getPersonalInfo(req.actor.uid);
|
||||
return res.status(200).json({ ...data, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/profile/industries', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listIndustries(req.actor.uid);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/profile/skills', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listSkills(req.actor.uid);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/profile/documents', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listProfileDocuments(req.actor.uid);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/profile/certificates', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listCertificates(req.actor.uid);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/profile/bank-accounts', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listStaffBankAccounts(req.actor.uid);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/profile/benefits', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listStaffBenefits(req.actor.uid);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
111
backend/query-api/src/services/actor-context.js
Normal file
111
backend/query-api/src/services/actor-context.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import { AppError } from '../lib/errors.js';
|
||||
import { query } from './db.js';
|
||||
|
||||
export async function loadActorContext(uid) {
|
||||
const [userResult, tenantResult, businessResult, vendorResult, staffResult] = await Promise.all([
|
||||
query(
|
||||
`
|
||||
SELECT id AS "userId", email, display_name AS "displayName", phone, status
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
`,
|
||||
[uid]
|
||||
),
|
||||
query(
|
||||
`
|
||||
SELECT tm.id AS "membershipId",
|
||||
tm.tenant_id AS "tenantId",
|
||||
tm.base_role AS role,
|
||||
t.name AS "tenantName",
|
||||
t.slug AS "tenantSlug"
|
||||
FROM tenant_memberships tm
|
||||
JOIN tenants t ON t.id = tm.tenant_id
|
||||
WHERE tm.user_id = $1
|
||||
AND tm.membership_status = 'ACTIVE'
|
||||
ORDER BY tm.created_at ASC
|
||||
LIMIT 1
|
||||
`,
|
||||
[uid]
|
||||
),
|
||||
query(
|
||||
`
|
||||
SELECT bm.id AS "membershipId",
|
||||
bm.business_id AS "businessId",
|
||||
bm.business_role AS role,
|
||||
b.business_name AS "businessName",
|
||||
b.slug AS "businessSlug",
|
||||
bm.tenant_id AS "tenantId"
|
||||
FROM business_memberships bm
|
||||
JOIN businesses b ON b.id = bm.business_id
|
||||
WHERE bm.user_id = $1
|
||||
AND bm.membership_status = 'ACTIVE'
|
||||
ORDER BY bm.created_at ASC
|
||||
LIMIT 1
|
||||
`,
|
||||
[uid]
|
||||
),
|
||||
query(
|
||||
`
|
||||
SELECT vm.id AS "membershipId",
|
||||
vm.vendor_id AS "vendorId",
|
||||
vm.vendor_role AS role,
|
||||
v.company_name AS "vendorName",
|
||||
v.slug AS "vendorSlug",
|
||||
vm.tenant_id AS "tenantId"
|
||||
FROM vendor_memberships vm
|
||||
JOIN vendors v ON v.id = vm.vendor_id
|
||||
WHERE vm.user_id = $1
|
||||
AND vm.membership_status = 'ACTIVE'
|
||||
ORDER BY vm.created_at ASC
|
||||
LIMIT 1
|
||||
`,
|
||||
[uid]
|
||||
),
|
||||
query(
|
||||
`
|
||||
SELECT s.id AS "staffId",
|
||||
s.tenant_id AS "tenantId",
|
||||
s.full_name AS "fullName",
|
||||
s.email,
|
||||
s.phone,
|
||||
s.primary_role AS "primaryRole",
|
||||
s.onboarding_status AS "onboardingStatus",
|
||||
s.status,
|
||||
s.metadata,
|
||||
w.id AS "workforceId",
|
||||
w.vendor_id AS "vendorId",
|
||||
w.workforce_number AS "workforceNumber"
|
||||
FROM staffs s
|
||||
LEFT JOIN workforce w ON w.staff_id = s.id
|
||||
WHERE s.user_id = $1
|
||||
ORDER BY s.created_at ASC
|
||||
LIMIT 1
|
||||
`,
|
||||
[uid]
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
user: userResult.rows[0] || null,
|
||||
tenant: tenantResult.rows[0] || null,
|
||||
business: businessResult.rows[0] || null,
|
||||
vendor: vendorResult.rows[0] || null,
|
||||
staff: staffResult.rows[0] || null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function requireClientContext(uid) {
|
||||
const context = await loadActorContext(uid);
|
||||
if (!context.user || !context.tenant || !context.business) {
|
||||
throw new AppError('FORBIDDEN', 'Client business context is required for this route', 403, { uid });
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export async function requireStaffContext(uid) {
|
||||
const context = await loadActorContext(uid);
|
||||
if (!context.user || !context.tenant || !context.staff) {
|
||||
throw new AppError('FORBIDDEN', 'Staff context is required for this route', 403, { uid });
|
||||
}
|
||||
return context;
|
||||
}
|
||||
1071
backend/query-api/src/services/mobile-query-service.js
Normal file
1071
backend/query-api/src/services/mobile-query-service.js
Normal file
File diff suppressed because it is too large
Load Diff
91
backend/query-api/test/mobile-routes.test.js
Normal file
91
backend/query-api/test/mobile-routes.test.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import request from 'supertest';
|
||||
import { createApp } from '../src/app.js';
|
||||
|
||||
process.env.AUTH_BYPASS = 'true';
|
||||
|
||||
function createMobileQueryService() {
|
||||
return {
|
||||
getClientDashboard: async () => ({ businessName: 'Google Cafes' }),
|
||||
getClientSession: async () => ({ business: { businessId: 'b1' } }),
|
||||
getCoverageStats: async () => ({ totalCoveragePercentage: 100 }),
|
||||
getCurrentAttendanceStatus: async () => ({ attendanceStatus: 'NOT_CLOCKED_IN' }),
|
||||
getCurrentBill: async () => ({ currentBillCents: 1000 }),
|
||||
getPaymentChart: async () => ([{ amountCents: 100 }]),
|
||||
getPaymentsSummary: async () => ({ totalEarningsCents: 500 }),
|
||||
getPersonalInfo: async () => ({ firstName: 'Ana' }),
|
||||
getProfileSectionsStatus: async () => ({ personalInfoCompleted: true }),
|
||||
getSavings: async () => ({ savingsCents: 200 }),
|
||||
getSpendBreakdown: async () => ([{ category: 'Barista', amountCents: 1000 }]),
|
||||
getStaffDashboard: async () => ({ staffName: 'Ana Barista' }),
|
||||
getStaffProfileCompletion: async () => ({ completed: true }),
|
||||
getStaffSession: async () => ({ staff: { staffId: 's1' } }),
|
||||
getStaffShiftDetail: async () => ({ shiftId: 'shift-1' }),
|
||||
listAssignedShifts: async () => ([{ shiftId: 'assigned-1' }]),
|
||||
listBusinessAccounts: async () => ([{ accountId: 'acc-1' }]),
|
||||
listCancelledShifts: async () => ([{ shiftId: 'cancelled-1' }]),
|
||||
listCertificates: async () => ([{ certificateId: 'cert-1' }]),
|
||||
listCostCenters: async () => ([{ costCenterId: 'cc-1' }]),
|
||||
listCoverageByDate: async () => ([{ shiftId: 'coverage-1' }]),
|
||||
listCompletedShifts: async () => ([{ shiftId: 'completed-1' }]),
|
||||
listHubManagers: async () => ([{ managerId: 'm1' }]),
|
||||
listHubs: async () => ([{ hubId: 'hub-1' }]),
|
||||
listIndustries: async () => (['CATERING']),
|
||||
listInvoiceHistory: async () => ([{ invoiceId: 'inv-1' }]),
|
||||
listOpenShifts: async () => ([{ shiftId: 'open-1' }]),
|
||||
listOrderItemsByDateRange: async () => ([{ itemId: 'item-1' }]),
|
||||
listPaymentsHistory: async () => ([{ paymentId: 'pay-1' }]),
|
||||
listPendingAssignments: async () => ([{ assignmentId: 'asg-1' }]),
|
||||
listPendingInvoices: async () => ([{ invoiceId: 'pending-1' }]),
|
||||
listProfileDocuments: async () => ([{ staffDocumentId: 'doc-1' }]),
|
||||
listRecentReorders: async () => ([{ id: 'order-1' }]),
|
||||
listSkills: async () => (['BARISTA']),
|
||||
listStaffAvailability: async () => ([{ dayOfWeek: 1 }]),
|
||||
listStaffBankAccounts: async () => ([{ accountId: 'acc-2' }]),
|
||||
listStaffBenefits: async () => ([{ benefitId: 'benefit-1' }]),
|
||||
listTodayShifts: async () => ([{ shiftId: 'today-1' }]),
|
||||
listVendorRoles: async () => ([{ roleId: 'role-1' }]),
|
||||
listVendors: async () => ([{ vendorId: 'vendor-1' }]),
|
||||
};
|
||||
}
|
||||
|
||||
test('GET /query/client/session returns injected client session', async () => {
|
||||
const app = createApp({ mobileQueryService: createMobileQueryService() });
|
||||
const res = await request(app)
|
||||
.get('/query/client/session')
|
||||
.set('Authorization', 'Bearer test-token');
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.body.business.businessId, 'b1');
|
||||
});
|
||||
|
||||
test('GET /query/client/coverage validates date query param', async () => {
|
||||
const app = createApp({ mobileQueryService: createMobileQueryService() });
|
||||
const res = await request(app)
|
||||
.get('/query/client/coverage')
|
||||
.set('Authorization', 'Bearer test-token');
|
||||
|
||||
assert.equal(res.status, 400);
|
||||
assert.equal(res.body.code, 'VALIDATION_ERROR');
|
||||
});
|
||||
|
||||
test('GET /query/staff/dashboard returns injected dashboard', async () => {
|
||||
const app = createApp({ mobileQueryService: createMobileQueryService() });
|
||||
const res = await request(app)
|
||||
.get('/query/staff/dashboard')
|
||||
.set('Authorization', 'Bearer test-token');
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.body.staffName, 'Ana Barista');
|
||||
});
|
||||
|
||||
test('GET /query/staff/shifts/:shiftId returns injected shift detail', async () => {
|
||||
const app = createApp({ mobileQueryService: createMobileQueryService() });
|
||||
const res = await request(app)
|
||||
.get('/query/staff/shifts/shift-1')
|
||||
.set('Authorization', 'Bearer test-token');
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.body.shiftId, 'shift-1');
|
||||
});
|
||||
Reference in New Issue
Block a user