1369 lines
47 KiB
JavaScript
1369 lines
47 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { signInWithPassword } from '../src/services/identity-toolkit.js';
|
|
import { V2DemoFixture as fixture } from '../../command-api/scripts/v2-demo-fixture.mjs';
|
|
|
|
const unifiedBaseUrl = process.env.UNIFIED_API_BASE_URL || 'https://krow-api-v2-e3g6witsvq-uc.a.run.app';
|
|
const ownerEmail = process.env.V2_DEMO_OWNER_EMAIL || 'legendary.owner+v2@krowd.com';
|
|
const staffEmail = process.env.V2_DEMO_STAFF_EMAIL || 'ana.barista+v2@krowd.com';
|
|
const ownerPassword = process.env.V2_DEMO_OWNER_PASSWORD || 'Demo2026!';
|
|
const staffPassword = process.env.V2_DEMO_STAFF_PASSWORD || 'Demo2026!';
|
|
|
|
function uniqueKey(prefix) {
|
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
}
|
|
|
|
function isoDate(offsetDays = 0) {
|
|
const value = new Date(Date.now() + (offsetDays * 24 * 60 * 60 * 1000));
|
|
return value.toISOString().slice(0, 10);
|
|
}
|
|
|
|
function isoTimestamp(offsetHours = 0) {
|
|
return new Date(Date.now() + (offsetHours * 60 * 60 * 1000)).toISOString();
|
|
}
|
|
|
|
function logStep(step, payload) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(`[unified-smoke-v2] ${step}: ${JSON.stringify(payload)}`);
|
|
}
|
|
|
|
async function readJson(response) {
|
|
const text = await response.text();
|
|
return text ? JSON.parse(text) : {};
|
|
}
|
|
|
|
async function apiCall(path, {
|
|
method = 'GET',
|
|
token,
|
|
idempotencyKey,
|
|
body,
|
|
expectedStatus = 200,
|
|
allowFailure = false,
|
|
} = {}) {
|
|
const headers = {};
|
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
|
|
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
|
|
const response = await fetch(`${unifiedBaseUrl}${path}`, {
|
|
method,
|
|
headers,
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
});
|
|
const payload = await readJson(response);
|
|
if (allowFailure) {
|
|
return {
|
|
statusCode: response.status,
|
|
body: payload,
|
|
};
|
|
}
|
|
if (response.status !== expectedStatus) {
|
|
throw new Error(`${method} ${path} expected ${expectedStatus}, got ${response.status}: ${JSON.stringify(payload)}`);
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
async function uploadFile(path, token, {
|
|
filename,
|
|
contentType,
|
|
content,
|
|
fields = {},
|
|
expectedStatus = 200,
|
|
}) {
|
|
const form = new FormData();
|
|
for (const [key, value] of Object.entries(fields)) {
|
|
form.set(key, value);
|
|
}
|
|
form.set(
|
|
'file',
|
|
new File([content], filename, {
|
|
type: contentType,
|
|
})
|
|
);
|
|
|
|
const response = await fetch(`${unifiedBaseUrl}${path}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: form,
|
|
});
|
|
const payload = await readJson(response);
|
|
if (response.status !== expectedStatus) {
|
|
throw new Error(`POST ${path} expected ${expectedStatus}, got ${response.status}: ${JSON.stringify(payload)}`);
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
async function finalizeVerifiedUpload({
|
|
token,
|
|
uploadCategory,
|
|
filename,
|
|
contentType,
|
|
content,
|
|
finalizePath,
|
|
finalizeMethod = 'PUT',
|
|
verificationType,
|
|
subjectId,
|
|
rules = {},
|
|
finalizeBody = {},
|
|
}) {
|
|
const uploaded = await uploadFile('/upload-file', token, {
|
|
filename,
|
|
contentType,
|
|
content,
|
|
fields: {
|
|
visibility: 'private',
|
|
category: uploadCategory,
|
|
},
|
|
});
|
|
|
|
const signed = await apiCall('/create-signed-url', {
|
|
method: 'POST',
|
|
token,
|
|
body: {
|
|
fileUri: uploaded.fileUri,
|
|
expiresInSeconds: 300,
|
|
},
|
|
});
|
|
|
|
const verification = await apiCall('/verifications', {
|
|
method: 'POST',
|
|
token,
|
|
body: {
|
|
type: verificationType,
|
|
subjectType: 'worker',
|
|
subjectId,
|
|
fileUri: uploaded.fileUri,
|
|
rules,
|
|
},
|
|
expectedStatus: 202,
|
|
});
|
|
|
|
const finalized = await apiCall(finalizePath, {
|
|
method: finalizeMethod,
|
|
token,
|
|
body: {
|
|
...finalizeBody,
|
|
verificationId: verification.verificationId,
|
|
fileUri: signed.signedUrl,
|
|
photoUrl: signed.signedUrl,
|
|
},
|
|
});
|
|
|
|
return {
|
|
uploaded,
|
|
signed,
|
|
verification,
|
|
finalized,
|
|
};
|
|
}
|
|
|
|
async function signInClient() {
|
|
return apiCall('/auth/client/sign-in', {
|
|
method: 'POST',
|
|
body: {
|
|
email: ownerEmail,
|
|
password: ownerPassword,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function signInStaff() {
|
|
return signInWithPassword({
|
|
email: staffEmail,
|
|
password: staffPassword,
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
const reportWindow = `startDate=${encodeURIComponent(isoTimestamp(-24 * 14))}&endDate=${encodeURIComponent(isoTimestamp(24 * 14))}`;
|
|
const ownerSession = await signInClient();
|
|
const staffAuth = await signInStaff();
|
|
|
|
assert.ok(ownerSession.sessionToken);
|
|
assert.ok(staffAuth.idToken);
|
|
assert.equal(ownerSession.business.businessId, fixture.business.id);
|
|
logStep('auth.client.sign-in.ok', {
|
|
tenantId: ownerSession.tenant.tenantId,
|
|
businessId: ownerSession.business.businessId,
|
|
});
|
|
logStep('auth.staff.password-sign-in.ok', {
|
|
uid: staffAuth.localId,
|
|
email: staffEmail,
|
|
});
|
|
|
|
const authSession = await apiCall('/auth/session', {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.equal(authSession.business.businessId, fixture.business.id);
|
|
logStep('auth.session.ok', authSession);
|
|
|
|
const staffPhoneStart = await apiCall('/auth/staff/phone/start', {
|
|
method: 'POST',
|
|
body: { phoneNumber: fixture.staff.ana.phone },
|
|
});
|
|
assert.equal(staffPhoneStart.mode, 'CLIENT_FIREBASE_SDK');
|
|
logStep('auth.staff.phone-start.ok', staffPhoneStart);
|
|
|
|
const staffPhoneVerify = await apiCall('/auth/staff/phone/verify', {
|
|
method: 'POST',
|
|
body: {
|
|
mode: 'sign-in',
|
|
idToken: staffAuth.idToken,
|
|
},
|
|
});
|
|
assert.equal(staffPhoneVerify.staff.staffId, fixture.staff.ana.id);
|
|
logStep('auth.staff.phone-verify.ok', {
|
|
staffId: staffPhoneVerify.staff.staffId,
|
|
requiresProfileSetup: staffPhoneVerify.requiresProfileSetup,
|
|
});
|
|
|
|
const clientSession = await apiCall('/client/session', {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.equal(clientSession.business.businessId, fixture.business.id);
|
|
logStep('client.session.ok', clientSession);
|
|
|
|
const clientPushTokenPrimary = await apiCall('/client/devices/push-tokens', {
|
|
method: 'POST',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('client-push-token-primary'),
|
|
body: {
|
|
provider: 'FCM',
|
|
platform: 'IOS',
|
|
pushToken: `smoke-client-primary-${Date.now()}-abcdefghijklmnop`,
|
|
deviceId: 'smoke-client-iphone-15-pro',
|
|
appVersion: '2.0.0-smoke',
|
|
appBuild: '2000',
|
|
locale: 'en-US',
|
|
timezone: 'America/Los_Angeles',
|
|
},
|
|
});
|
|
assert.ok(clientPushTokenPrimary.tokenId);
|
|
logStep('client.push-token.register-primary.ok', clientPushTokenPrimary);
|
|
|
|
const clientPushTokenCleanup = await apiCall('/client/devices/push-tokens', {
|
|
method: 'POST',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('client-push-token-cleanup'),
|
|
body: {
|
|
provider: 'FCM',
|
|
platform: 'ANDROID',
|
|
pushToken: `smoke-client-cleanup-${Date.now()}-abcdefghijklmnop`,
|
|
deviceId: 'smoke-client-pixel-9',
|
|
appVersion: '2.0.0-smoke',
|
|
appBuild: '2001',
|
|
locale: 'en-US',
|
|
timezone: 'America/Los_Angeles',
|
|
},
|
|
});
|
|
assert.ok(clientPushTokenCleanup.tokenId);
|
|
logStep('client.push-token.register-cleanup.ok', clientPushTokenCleanup);
|
|
|
|
const clientPushTokenDeleted = await apiCall(`/client/devices/push-tokens?tokenId=${encodeURIComponent(clientPushTokenCleanup.tokenId)}&reason=SMOKE_CLEANUP`, {
|
|
method: 'DELETE',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('client-push-token-delete'),
|
|
});
|
|
assert.equal(clientPushTokenDeleted.removedCount, 1);
|
|
logStep('client.push-token.delete.ok', clientPushTokenDeleted);
|
|
|
|
const clientDashboard = await apiCall('/client/dashboard', {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.equal(clientDashboard.businessId, fixture.business.id);
|
|
logStep('client.dashboard.ok', {
|
|
weeklySpendCents: clientDashboard.spending.weeklySpendCents,
|
|
openPositionsToday: clientDashboard.coverage.openPositionsToday,
|
|
});
|
|
|
|
const clientReorders = await apiCall('/client/reorders', {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.ok(Array.isArray(clientReorders.items));
|
|
if (clientReorders.items[0]) {
|
|
assert.equal(typeof clientReorders.items[0].hourlyRateCents, 'number');
|
|
assert.equal(typeof clientReorders.items[0].totalPriceCents, 'number');
|
|
}
|
|
logStep('client.reorders.ok', { count: clientReorders.items.length });
|
|
|
|
const billingAccounts = await apiCall('/client/billing/accounts', {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.ok(Array.isArray(billingAccounts.items));
|
|
logStep('client.billing.accounts.ok', { count: billingAccounts.items.length });
|
|
|
|
const pendingInvoices = await apiCall('/client/billing/invoices/pending', {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.ok(pendingInvoices.items.length >= 1);
|
|
const invoiceId = pendingInvoices.items[0].invoiceId;
|
|
logStep('client.billing.pending-invoices.ok', { count: pendingInvoices.items.length, invoiceId });
|
|
|
|
const invoiceHistory = await apiCall('/client/billing/invoices/history', {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.ok(Array.isArray(invoiceHistory.items));
|
|
logStep('client.billing.invoice-history.ok', { count: invoiceHistory.items.length });
|
|
|
|
const currentBill = await apiCall('/client/billing/current-bill', {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.ok(typeof currentBill.currentBillCents === 'number');
|
|
logStep('client.billing.current-bill.ok', currentBill);
|
|
|
|
const savings = await apiCall('/client/billing/savings', {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.ok(typeof savings.savingsCents === 'number');
|
|
logStep('client.billing.savings.ok', savings);
|
|
|
|
const spendBreakdown = await apiCall('/client/billing/spend-breakdown?period=month', {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.ok(Array.isArray(spendBreakdown.items));
|
|
logStep('client.billing.spend-breakdown.ok', { count: spendBreakdown.items.length });
|
|
|
|
const coverage = await apiCall(`/client/coverage?date=${isoDate(0)}`, {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.ok(Array.isArray(coverage.items));
|
|
logStep('client.coverage.ok', { count: coverage.items.length });
|
|
|
|
const coverageStats = await apiCall(`/client/coverage/stats?date=${isoDate(0)}`, {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.ok(typeof coverageStats.totalCoveragePercentage === 'number');
|
|
logStep('client.coverage.stats.ok', coverageStats);
|
|
|
|
const coreTeam = await apiCall('/client/coverage/core-team', {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
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,
|
|
});
|
|
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', {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.ok(costCenters.items.length >= 1);
|
|
logStep('client.cost-centers.ok', { count: costCenters.items.length });
|
|
|
|
const vendors = await apiCall('/client/vendors', {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.ok(vendors.items.length >= 1);
|
|
logStep('client.vendors.ok', { count: vendors.items.length });
|
|
|
|
const vendorRoles = await apiCall(`/client/vendors/${fixture.vendor.id}/roles`, {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.ok(vendorRoles.items.length >= 1);
|
|
logStep('client.vendor-roles.ok', { count: vendorRoles.items.length });
|
|
|
|
const hubManagers = await apiCall(`/client/hubs/${fixture.clockPoint.id}/managers`, {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.ok(Array.isArray(hubManagers.items));
|
|
logStep('client.hub-managers.ok', { count: hubManagers.items.length });
|
|
|
|
const teamMembers = await apiCall('/client/team-members', {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.ok(Array.isArray(teamMembers.items));
|
|
logStep('client.team-members.ok', { count: teamMembers.items.length });
|
|
|
|
const createdShiftManager = await apiCall('/client/shift-managers', {
|
|
method: 'POST',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('create-shift-manager'),
|
|
body: {
|
|
hubId: fixture.clockPoint.id,
|
|
email: `smoke.manager.${Date.now()}@krowd.com`,
|
|
firstName: 'Smoke',
|
|
lastName: 'Manager',
|
|
phone: '+15550009999',
|
|
},
|
|
});
|
|
assert.ok(createdShiftManager.businessMembershipId);
|
|
assert.equal(createdShiftManager.membershipStatus, 'INVITED');
|
|
assert.ok(createdShiftManager.managerAssignmentId);
|
|
logStep('client.shift-manager.create.ok', createdShiftManager);
|
|
|
|
const teamMembersAfterCreate = await apiCall('/client/team-members', {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.ok(teamMembersAfterCreate.items.some((item) => item.businessMembershipId === createdShiftManager.businessMembershipId));
|
|
logStep('client.team-members.after-create.ok', { count: teamMembersAfterCreate.items.length });
|
|
|
|
const viewedOrders = await apiCall(`/client/orders/view?${reportWindow}`, {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.ok(Array.isArray(viewedOrders.items));
|
|
if (viewedOrders.items[0]) {
|
|
assert.ok(viewedOrders.items[0].clientName);
|
|
assert.equal(typeof viewedOrders.items[0].hourlyRate, 'number');
|
|
}
|
|
logStep('client.orders.view.ok', { count: viewedOrders.items.length });
|
|
|
|
const reorderPreview = await apiCall(`/client/orders/${fixture.orders.completed.id}/reorder-preview`, {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.equal(reorderPreview.orderId, fixture.orders.completed.id);
|
|
logStep('client.orders.reorder-preview.ok', reorderPreview);
|
|
|
|
const reportSummary = await apiCall(`/client/reports/summary?${reportWindow}`, {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.ok(typeof reportSummary.totalShifts === 'number');
|
|
logStep('client.reports.summary.ok', reportSummary);
|
|
|
|
const dailyOps = await apiCall(`/client/reports/daily-ops?date=${isoDate(0)}`, {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
logStep('client.reports.daily-ops.ok', dailyOps);
|
|
|
|
const spendReport = await apiCall(`/client/reports/spend?${reportWindow}`, {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
logStep('client.reports.spend.ok', spendReport);
|
|
|
|
const coverageReport = await apiCall(`/client/reports/coverage?${reportWindow}`, {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
logStep('client.reports.coverage.ok', coverageReport);
|
|
|
|
const forecastReport = await apiCall(`/client/reports/forecast?${reportWindow}`, {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
logStep('client.reports.forecast.ok', forecastReport);
|
|
|
|
const performanceReport = await apiCall(`/client/reports/performance?${reportWindow}`, {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
logStep('client.reports.performance.ok', performanceReport);
|
|
|
|
const noShowReport = await apiCall(`/client/reports/no-show?${reportWindow}`, {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
logStep('client.reports.no-show.ok', noShowReport);
|
|
|
|
const createdHub = await apiCall('/client/hubs', {
|
|
method: 'POST',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('create-hub'),
|
|
body: {
|
|
name: `Smoke Hub ${Date.now()}`,
|
|
fullAddress: '500 Castro Street, Mountain View, CA',
|
|
latitude: 37.3925,
|
|
longitude: -122.0782,
|
|
city: 'Mountain View',
|
|
state: 'CA',
|
|
country: 'US',
|
|
zipCode: '94041',
|
|
costCenterId: fixture.costCenters.cafeOps.id,
|
|
geofenceRadiusMeters: 100,
|
|
},
|
|
});
|
|
assert.ok(createdHub.hubId);
|
|
logStep('client.hubs.create.ok', createdHub);
|
|
|
|
const updatedHub = await apiCall(`/client/hubs/${createdHub.hubId}`, {
|
|
method: 'PUT',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('update-hub'),
|
|
body: {
|
|
name: `${createdHub.name || 'Smoke Hub'} Updated`,
|
|
geofenceRadiusMeters: 140,
|
|
},
|
|
});
|
|
logStep('client.hubs.update.ok', updatedHub);
|
|
|
|
const assignedHubManager = await apiCall(`/client/hubs/${createdHub.hubId}/managers`, {
|
|
method: 'POST',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('assign-hub-manager'),
|
|
body: {
|
|
managerUserId: fixture.users.operationsManager.id,
|
|
},
|
|
});
|
|
logStep('client.hubs.assign-manager.ok', assignedHubManager);
|
|
|
|
const assignedNfc = await apiCall(`/client/hubs/${createdHub.hubId}/assign-nfc`, {
|
|
method: 'POST',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('assign-hub-nfc'),
|
|
body: {
|
|
nfcTagId: `NFC-SMOKE-${Date.now()}`,
|
|
},
|
|
});
|
|
logStep('client.hubs.assign-nfc.ok', assignedNfc);
|
|
|
|
const deletedHub = await apiCall(`/client/hubs/${createdHub.hubId}`, {
|
|
method: 'DELETE',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('delete-hub'),
|
|
body: {
|
|
reason: 'smoke cleanup',
|
|
},
|
|
});
|
|
logStep('client.hubs.delete.ok', deletedHub);
|
|
|
|
const disputedInvoice = await apiCall(`/client/billing/invoices/${invoiceId}/dispute`, {
|
|
method: 'POST',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('invoice-dispute'),
|
|
body: {
|
|
reason: 'Smoke dispute before approval',
|
|
},
|
|
});
|
|
logStep('client.billing.invoice-dispute.ok', disputedInvoice);
|
|
|
|
const approvedInvoice = await apiCall(`/client/billing/invoices/${invoiceId}/approve`, {
|
|
method: 'POST',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('invoice-approve'),
|
|
body: {},
|
|
});
|
|
logStep('client.billing.invoice-approve.ok', approvedInvoice);
|
|
|
|
const createdOneTimeOrder = await apiCall('/client/orders/one-time', {
|
|
method: 'POST',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('order-one-time'),
|
|
body: {
|
|
hubId: fixture.clockPoint.id,
|
|
vendorId: fixture.vendor.id,
|
|
eventName: `Smoke One-Time ${Date.now()}`,
|
|
orderDate: isoDate(3),
|
|
serviceType: 'RESTAURANT',
|
|
positions: [
|
|
{
|
|
roleId: fixture.roles.barista.id,
|
|
startTime: '08:00',
|
|
endTime: '16:00',
|
|
workerCount: 1,
|
|
payRateCents: 2200,
|
|
billRateCents: 3500,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
assert.ok(createdOneTimeOrder.orderId);
|
|
logStep('client.orders.create-one-time.ok', createdOneTimeOrder);
|
|
|
|
const createdRecurringOrder = await apiCall('/client/orders/recurring', {
|
|
method: 'POST',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('order-recurring'),
|
|
body: {
|
|
hubId: fixture.clockPoint.id,
|
|
vendorId: fixture.vendor.id,
|
|
eventName: `Smoke Recurring ${Date.now()}`,
|
|
startDate: isoDate(5),
|
|
endDate: isoDate(10),
|
|
recurrenceDays: [1, 3, 5],
|
|
serviceType: 'RESTAURANT',
|
|
positions: [
|
|
{
|
|
roleId: fixture.roles.barista.id,
|
|
startTime: '09:00',
|
|
endTime: '15:00',
|
|
workersNeeded: 1,
|
|
payRateCents: 2300,
|
|
billRateCents: 3600,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
assert.ok(createdRecurringOrder.orderId);
|
|
logStep('client.orders.create-recurring.ok', createdRecurringOrder);
|
|
|
|
const createdPermanentOrder = await apiCall('/client/orders/permanent', {
|
|
method: 'POST',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('order-permanent'),
|
|
body: {
|
|
hubId: fixture.clockPoint.id,
|
|
vendorId: fixture.vendor.id,
|
|
eventName: `Smoke Permanent ${Date.now()}`,
|
|
startDate: isoDate(7),
|
|
daysOfWeek: [1, 2, 3, 4, 5],
|
|
horizonDays: 14,
|
|
serviceType: 'RESTAURANT',
|
|
positions: [
|
|
{
|
|
roleId: fixture.roles.barista.id,
|
|
startTime: '07:00',
|
|
endTime: '13:00',
|
|
workersNeeded: 1,
|
|
payRateCents: 2200,
|
|
billRateCents: 3500,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
assert.ok(createdPermanentOrder.orderId);
|
|
logStep('client.orders.create-permanent.ok', createdPermanentOrder);
|
|
|
|
const editedOrderCopy = await apiCall(`/client/orders/${createdRecurringOrder.orderId}/edit`, {
|
|
method: 'POST',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('order-edit'),
|
|
body: {
|
|
eventName: `Edited Copy ${Date.now()}`,
|
|
},
|
|
});
|
|
assert.ok(editedOrderCopy.orderId);
|
|
assert.notEqual(editedOrderCopy.orderId, createdRecurringOrder.orderId);
|
|
logStep('client.orders.edit-copy.ok', editedOrderCopy);
|
|
|
|
const cancelledOrder = await apiCall(`/client/orders/${createdOneTimeOrder.orderId}/cancel`, {
|
|
method: 'POST',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('order-cancel'),
|
|
body: {
|
|
reason: 'Smoke cancel validation',
|
|
},
|
|
});
|
|
assert.equal(cancelledOrder.futureOnly, true);
|
|
logStep('client.orders.cancel.ok', cancelledOrder);
|
|
|
|
const coverageReview = await apiCall('/client/coverage/reviews', {
|
|
method: 'POST',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('coverage-review'),
|
|
body: {
|
|
staffId: fixture.staff.ana.id,
|
|
assignmentId: fixture.assignments.completedAna.id,
|
|
rating: 5,
|
|
markAsFavorite: true,
|
|
issueFlags: [],
|
|
feedback: 'Smoke review',
|
|
},
|
|
});
|
|
logStep('client.coverage.review.ok', coverageReview);
|
|
|
|
const staffSession = await apiCall('/staff/session', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.equal(staffSession.staff.staffId, fixture.staff.ana.id);
|
|
logStep('staff.session.ok', staffSession);
|
|
|
|
const staffPushTokenPrimary = await apiCall('/staff/devices/push-tokens', {
|
|
method: 'POST',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-push-token-primary'),
|
|
body: {
|
|
provider: 'FCM',
|
|
platform: 'IOS',
|
|
pushToken: `smoke-staff-primary-${Date.now()}-abcdefghijklmnop`,
|
|
deviceId: 'smoke-staff-iphone-15-pro',
|
|
appVersion: '2.0.0-smoke',
|
|
appBuild: '2000',
|
|
locale: 'en-US',
|
|
timezone: 'America/Los_Angeles',
|
|
},
|
|
});
|
|
assert.ok(staffPushTokenPrimary.tokenId);
|
|
logStep('staff.push-token.register-primary.ok', staffPushTokenPrimary);
|
|
|
|
const staffPushTokenCleanup = await apiCall('/staff/devices/push-tokens', {
|
|
method: 'POST',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-push-token-cleanup'),
|
|
body: {
|
|
provider: 'FCM',
|
|
platform: 'ANDROID',
|
|
pushToken: `smoke-staff-cleanup-${Date.now()}-abcdefghijklmnop`,
|
|
deviceId: 'smoke-staff-pixel-9',
|
|
appVersion: '2.0.0-smoke',
|
|
appBuild: '2001',
|
|
locale: 'en-US',
|
|
timezone: 'America/Los_Angeles',
|
|
},
|
|
});
|
|
assert.ok(staffPushTokenCleanup.tokenId);
|
|
logStep('staff.push-token.register-cleanup.ok', staffPushTokenCleanup);
|
|
|
|
const staffPushTokenDeleted = await apiCall(`/staff/devices/push-tokens?tokenId=${encodeURIComponent(staffPushTokenCleanup.tokenId)}&reason=SMOKE_CLEANUP`, {
|
|
method: 'DELETE',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-push-token-delete'),
|
|
});
|
|
assert.equal(staffPushTokenDeleted.removedCount, 1);
|
|
logStep('staff.push-token.delete.ok', staffPushTokenDeleted);
|
|
|
|
const staffDashboard = await apiCall('/staff/dashboard', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(Array.isArray(staffDashboard.recommendedShifts));
|
|
if (staffDashboard.todaysShifts[0]) {
|
|
assert.ok(staffDashboard.todaysShifts[0].clientName);
|
|
assert.equal(typeof staffDashboard.todaysShifts[0].totalRate, 'number');
|
|
}
|
|
if (staffDashboard.recommendedShifts[0]) {
|
|
assert.ok(staffDashboard.recommendedShifts[0].clientName);
|
|
assert.equal(typeof staffDashboard.recommendedShifts[0].totalRate, 'number');
|
|
}
|
|
logStep('staff.dashboard.ok', {
|
|
todaysShifts: staffDashboard.todaysShifts.length,
|
|
recommendedShifts: staffDashboard.recommendedShifts.length,
|
|
});
|
|
|
|
const staffProfileCompletion = await apiCall('/staff/profile-completion', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
logStep('staff.profile-completion.ok', staffProfileCompletion);
|
|
|
|
const staffAvailability = await apiCall('/staff/availability', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(Array.isArray(staffAvailability.items));
|
|
logStep('staff.availability.ok', { count: staffAvailability.items.length });
|
|
|
|
const todaysShifts = await apiCall('/staff/clock-in/shifts/today', {
|
|
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.clientName, fixture.business.name);
|
|
assert.equal(typeof assignedTodayShift.hourlyRate, 'number');
|
|
assert.equal(typeof assignedTodayShift.latitude, 'number');
|
|
assert.equal(typeof assignedTodayShift.longitude, 'number');
|
|
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', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
logStep('staff.clock-in.status-before.ok', attendanceStatusBefore);
|
|
|
|
const paymentsSummary = await apiCall('/staff/payments/summary?period=month', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
logStep('staff.payments.summary.ok', paymentsSummary);
|
|
|
|
const paymentsHistory = await apiCall('/staff/payments/history?period=month', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(Array.isArray(paymentsHistory.items));
|
|
logStep('staff.payments.history.ok', { count: paymentsHistory.items.length });
|
|
|
|
const paymentsChart = await apiCall('/staff/payments/chart?period=month', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(Array.isArray(paymentsChart.items));
|
|
logStep('staff.payments.chart.ok', { count: paymentsChart.items.length });
|
|
|
|
const assignedShifts = await apiCall(`/staff/shifts/assigned?${reportWindow}`, {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(Array.isArray(assignedShifts.items));
|
|
logStep('staff.shifts.assigned.ok', { count: assignedShifts.items.length });
|
|
|
|
const openShifts = await apiCall('/staff/shifts/open', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
const openShift = openShifts.items.find((shift) => shift.shiftId === fixture.shifts.available.id)
|
|
|| openShifts.items[0];
|
|
const blockedApplyCandidate = openShifts.items.find((shift) => shift.shiftId !== openShift.shiftId);
|
|
assert.ok(openShift);
|
|
assert.ok(blockedApplyCandidate);
|
|
logStep('staff.shifts.open.ok', { count: openShifts.items.length });
|
|
|
|
const pendingShifts = await apiCall('/staff/shifts/pending', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
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', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(Array.isArray(cancelledShifts.items));
|
|
logStep('staff.shifts.cancelled.ok', { count: cancelledShifts.items.length });
|
|
|
|
const completedShifts = await apiCall('/staff/shifts/completed', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(Array.isArray(completedShifts.items));
|
|
if (completedShifts.items[0]) {
|
|
assert.ok(completedShifts.items[0].clientName);
|
|
assert.ok(completedShifts.items[0].date);
|
|
assert.ok(completedShifts.items[0].startTime);
|
|
assert.ok(completedShifts.items[0].endTime);
|
|
assert.equal(typeof completedShifts.items[0].hourlyRate, 'number');
|
|
assert.equal(typeof completedShifts.items[0].totalRate, 'number');
|
|
}
|
|
logStep('staff.shifts.completed.ok', { count: completedShifts.items.length });
|
|
|
|
const shiftDetail = await apiCall(`/staff/shifts/${openShift.shiftId}`, {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.equal(shiftDetail.shiftId, openShift.shiftId);
|
|
assert.equal(typeof shiftDetail.latitude, 'number');
|
|
assert.equal(typeof shiftDetail.longitude, 'number');
|
|
logStep('staff.shifts.detail.ok', shiftDetail);
|
|
|
|
const profileSections = await apiCall('/staff/profile/sections', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
logStep('staff.profile.sections.ok', profileSections);
|
|
|
|
const personalInfo = await apiCall('/staff/profile/personal-info', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
logStep('staff.profile.personal-info.ok', personalInfo);
|
|
|
|
const industries = await apiCall('/staff/profile/industries', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(Array.isArray(industries.items));
|
|
logStep('staff.profile.industries.ok', industries);
|
|
|
|
const skills = await apiCall('/staff/profile/skills', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(Array.isArray(skills.items));
|
|
logStep('staff.profile.skills.ok', skills);
|
|
|
|
const profileDocumentsBefore = await apiCall('/staff/profile/documents', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(Array.isArray(profileDocumentsBefore.items));
|
|
assert.ok(profileDocumentsBefore.items.every((item) => item.documentType !== 'ATTIRE'));
|
|
logStep('staff.profile.documents-before.ok', { count: profileDocumentsBefore.items.length });
|
|
|
|
const attireChecklistBefore = await apiCall('/staff/profile/attire', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(Array.isArray(attireChecklistBefore.items));
|
|
logStep('staff.profile.attire-before.ok', { count: attireChecklistBefore.items.length });
|
|
|
|
const taxForms = await apiCall('/staff/profile/tax-forms', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(Array.isArray(taxForms.items));
|
|
logStep('staff.profile.tax-forms.ok', { count: taxForms.items.length });
|
|
|
|
const emergencyContactsBefore = await apiCall('/staff/profile/emergency-contacts', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(Array.isArray(emergencyContactsBefore.items));
|
|
logStep('staff.profile.emergency-contacts-before.ok', { count: emergencyContactsBefore.items.length });
|
|
|
|
const certificatesBefore = await apiCall('/staff/profile/certificates', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(Array.isArray(certificatesBefore.items));
|
|
logStep('staff.profile.certificates-before.ok', { count: certificatesBefore.items.length });
|
|
|
|
const bankAccountsBefore = await apiCall('/staff/profile/bank-accounts', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(Array.isArray(bankAccountsBefore.items));
|
|
logStep('staff.profile.bank-accounts-before.ok', { count: bankAccountsBefore.items.length });
|
|
|
|
const benefits = await apiCall('/staff/profile/benefits', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(Array.isArray(benefits.items));
|
|
logStep('staff.profile.benefits.ok', { count: benefits.items.length });
|
|
|
|
const benefitHistory = await apiCall('/staff/profile/benefits/history?limit=10', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(Array.isArray(benefitHistory.items));
|
|
assert.ok(benefitHistory.items.length >= 1);
|
|
logStep('staff.profile.benefits.history.ok', { count: benefitHistory.items.length });
|
|
|
|
const timeCard = await apiCall(`/staff/profile/time-card?month=${new Date().getUTCMonth() + 1}&year=${new Date().getUTCFullYear()}`, {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(Array.isArray(timeCard.items));
|
|
logStep('staff.profile.time-card.ok', { count: timeCard.items.length });
|
|
|
|
const privacyBefore = await apiCall('/staff/profile/privacy', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
logStep('staff.profile.privacy-before.ok', privacyBefore);
|
|
|
|
const faqs = await apiCall('/staff/faqs');
|
|
assert.ok(Array.isArray(faqs.items));
|
|
logStep('staff.faqs.ok', { count: faqs.items.length });
|
|
|
|
const faqSearch = await apiCall('/staff/faqs/search?q=payments');
|
|
assert.ok(Array.isArray(faqSearch.items));
|
|
logStep('staff.faqs.search.ok', { count: faqSearch.items.length });
|
|
|
|
const updatedAvailability = await apiCall('/staff/availability', {
|
|
method: 'PUT',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-availability'),
|
|
body: {
|
|
dayOfWeek: 2,
|
|
availabilityStatus: 'PARTIAL',
|
|
slots: [{ start: '10:00', end: '18:00' }],
|
|
},
|
|
});
|
|
logStep('staff.availability.update.ok', updatedAvailability);
|
|
|
|
const quickSetAvailability = await apiCall('/staff/availability/quick-set', {
|
|
method: 'POST',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-availability-quick-set'),
|
|
body: {
|
|
quickSetType: 'weekdays',
|
|
startDate: isoTimestamp(0),
|
|
endDate: isoTimestamp(24 * 7),
|
|
},
|
|
});
|
|
logStep('staff.availability.quick-set.ok', quickSetAvailability);
|
|
|
|
const personalInfoUpdate = await apiCall('/staff/profile/personal-info', {
|
|
method: 'PUT',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-personal-info'),
|
|
body: {
|
|
firstName: 'Ana',
|
|
lastName: 'Barista',
|
|
bio: 'Smoke-tested staff bio',
|
|
preferredLocations: [
|
|
{
|
|
label: 'Mountain View',
|
|
city: 'Mountain View',
|
|
latitude: fixture.clockPoint.latitude,
|
|
longitude: fixture.clockPoint.longitude,
|
|
radiusMiles: 20,
|
|
},
|
|
],
|
|
phone: fixture.staff.ana.phone,
|
|
email: staffEmail,
|
|
},
|
|
});
|
|
logStep('staff.profile.personal-info.update.ok', personalInfoUpdate);
|
|
|
|
const experienceUpdate = await apiCall('/staff/profile/experience', {
|
|
method: 'PUT',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-experience'),
|
|
body: {
|
|
industries: ['CATERING', 'CAFE'],
|
|
skills: ['BARISTA', 'CUSTOMER_SERVICE'],
|
|
primaryRole: 'BARISTA',
|
|
},
|
|
});
|
|
logStep('staff.profile.experience.update.ok', experienceUpdate);
|
|
|
|
const locationUpdate = await apiCall('/staff/profile/locations', {
|
|
method: 'PUT',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-locations'),
|
|
body: {
|
|
preferredLocations: [
|
|
{
|
|
label: 'Mountain View',
|
|
city: 'Mountain View',
|
|
latitude: fixture.clockPoint.latitude,
|
|
longitude: fixture.clockPoint.longitude,
|
|
radiusMiles: 25,
|
|
},
|
|
],
|
|
maxDistanceMiles: 25,
|
|
},
|
|
});
|
|
logStep('staff.profile.locations.update.ok', locationUpdate);
|
|
|
|
const createdEmergencyContact = await apiCall('/staff/profile/emergency-contacts', {
|
|
method: 'POST',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-emergency-create'),
|
|
body: {
|
|
fullName: 'Smoke Contact',
|
|
phone: '+15550009999',
|
|
relationshipType: 'Sibling',
|
|
isPrimary: false,
|
|
},
|
|
});
|
|
assert.ok(createdEmergencyContact.contactId);
|
|
logStep('staff.profile.emergency-contact.create.ok', createdEmergencyContact);
|
|
|
|
const updatedEmergencyContact = await apiCall(`/staff/profile/emergency-contacts/${createdEmergencyContact.contactId}`, {
|
|
method: 'PUT',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-emergency-update'),
|
|
body: {
|
|
fullName: 'Smoke Contact Updated',
|
|
relationshipType: 'Brother',
|
|
},
|
|
});
|
|
logStep('staff.profile.emergency-contact.update.ok', updatedEmergencyContact);
|
|
|
|
const savedW4Draft = await apiCall('/staff/profile/tax-forms/w4', {
|
|
method: 'PUT',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-tax-w4-draft'),
|
|
body: {
|
|
fields: {
|
|
filingStatus: 'single',
|
|
},
|
|
},
|
|
});
|
|
logStep('staff.profile.tax-form.w4-draft.ok', savedW4Draft);
|
|
|
|
const submittedI9 = await apiCall('/staff/profile/tax-forms/i9/submit', {
|
|
method: 'POST',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-tax-i9-submit'),
|
|
body: {
|
|
fields: {
|
|
section1Complete: true,
|
|
},
|
|
},
|
|
});
|
|
logStep('staff.profile.tax-form.i9-submit.ok', submittedI9);
|
|
|
|
const addedBankAccount = await apiCall('/staff/profile/bank-accounts', {
|
|
method: 'POST',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-bank-account'),
|
|
body: {
|
|
bankName: 'Demo Credit Union',
|
|
accountNumber: '1234567890',
|
|
routingNumber: '021000021',
|
|
accountType: 'checking',
|
|
},
|
|
});
|
|
logStep('staff.profile.bank-account.add.ok', addedBankAccount);
|
|
|
|
const updatedPrivacy = await apiCall('/staff/profile/privacy', {
|
|
method: 'PUT',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-privacy'),
|
|
body: {
|
|
profileVisible: true,
|
|
},
|
|
});
|
|
logStep('staff.profile.privacy.update.ok', updatedPrivacy);
|
|
|
|
const appliedShift = await apiCall(`/staff/shifts/${openShift.shiftId}/apply`, {
|
|
method: 'POST',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-shift-apply'),
|
|
body: {
|
|
roleId: fixture.shiftRoles.availableBarista.id,
|
|
},
|
|
});
|
|
logStep('staff.shifts.apply.ok', appliedShift);
|
|
|
|
const acceptedShift = await apiCall(`/staff/shifts/${fixture.shifts.assigned.id}/accept`, {
|
|
method: 'POST',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-shift-accept'),
|
|
body: {},
|
|
});
|
|
logStep('staff.shifts.accept.ok', acceptedShift);
|
|
|
|
const clockIn = await apiCall('/staff/clock-in', {
|
|
method: 'POST',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-clock-in'),
|
|
body: {
|
|
shiftId: fixture.shifts.assigned.id,
|
|
sourceType: 'GEO',
|
|
deviceId: 'smoke-iphone-15-pro',
|
|
latitude: fixture.clockPoint.latitude + 0.0075,
|
|
longitude: fixture.clockPoint.longitude + 0.0075,
|
|
accuracyMeters: 8,
|
|
proofNonce: uniqueKey('geo-proof-clock-in'),
|
|
proofTimestamp: isoTimestamp(0),
|
|
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);
|
|
assert.ok(clockIn.securityProofId);
|
|
logStep('staff.clock-in.ok', clockIn);
|
|
|
|
const attendanceStatusAfterClockIn = await apiCall('/staff/clock-in/status', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
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,
|
|
idempotencyKey: uniqueKey('staff-clock-out'),
|
|
body: {
|
|
shiftId: fixture.shifts.assigned.id,
|
|
sourceType: 'GEO',
|
|
deviceId: 'smoke-iphone-15-pro',
|
|
latitude: fixture.clockPoint.latitude,
|
|
longitude: fixture.clockPoint.longitude,
|
|
accuracyMeters: 10,
|
|
proofNonce: uniqueKey('geo-proof-clock-out'),
|
|
proofTimestamp: isoTimestamp(1),
|
|
breakMinutes: 30,
|
|
capturedAt: isoTimestamp(1),
|
|
},
|
|
});
|
|
assert.ok(clockOut.securityProofId);
|
|
logStep('staff.clock-out.ok', clockOut);
|
|
|
|
const submittedCompletedShift = await apiCall(`/staff/shifts/${fixture.shifts.assigned.id}/submit-for-approval`, {
|
|
method: 'POST',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-shift-submit-approval'),
|
|
body: {
|
|
note: 'Smoke approval submission',
|
|
},
|
|
});
|
|
assert.equal(submittedCompletedShift.submitted, true);
|
|
logStep('staff.shifts.submit-for-approval.ok', submittedCompletedShift);
|
|
|
|
const requestedSwap = await apiCall(`/staff/shifts/${fixture.shifts.assigned.id}/request-swap`, {
|
|
method: 'POST',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-shift-swap'),
|
|
body: {
|
|
reason: 'Smoke swap request',
|
|
},
|
|
});
|
|
logStep('staff.shifts.request-swap.ok', requestedSwap);
|
|
|
|
const blockedReview = await apiCall('/client/coverage/reviews', {
|
|
method: 'POST',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('coverage-block'),
|
|
body: {
|
|
staffId: fixture.staff.ana.id,
|
|
assignmentId: fixture.assignments.completedAna.id,
|
|
rating: 2,
|
|
markAsBlocked: true,
|
|
markAsFavorite: false,
|
|
issueFlags: ['LATE_CLOCK_IN'],
|
|
feedback: 'Smoke blocked staff test',
|
|
},
|
|
});
|
|
assert.equal(blockedReview.markAsBlocked, true);
|
|
logStep('client.coverage.block.ok', blockedReview);
|
|
|
|
const blockedStaff = await apiCall('/client/coverage/blocked-staff', {
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
assert.ok(blockedStaff.items.some((item) => item.staffId === fixture.staff.ana.id));
|
|
logStep('client.coverage.blocked-staff.ok', { count: blockedStaff.items.length });
|
|
|
|
const blockedApplyAttempt = await apiCall(`/staff/shifts/${blockedApplyCandidate.shiftId}/apply`, {
|
|
method: 'POST',
|
|
token: staffAuth.idToken,
|
|
idempotencyKey: uniqueKey('staff-shift-apply-blocked'),
|
|
body: {
|
|
roleId: blockedApplyCandidate.roleId,
|
|
},
|
|
allowFailure: true,
|
|
});
|
|
assert.equal(blockedApplyAttempt.statusCode, 409);
|
|
assert.equal(blockedApplyAttempt.body?.code, 'STAFF_BLOCKED');
|
|
logStep('staff.shifts.apply-blocked.ok', blockedApplyAttempt.body);
|
|
|
|
const unblockedReview = await apiCall('/client/coverage/reviews', {
|
|
method: 'POST',
|
|
token: ownerSession.sessionToken,
|
|
idempotencyKey: uniqueKey('coverage-unblock'),
|
|
body: {
|
|
staffId: fixture.staff.ana.id,
|
|
assignmentId: fixture.assignments.completedAna.id,
|
|
rating: 5,
|
|
markAsBlocked: false,
|
|
markAsFavorite: true,
|
|
issueFlags: [],
|
|
feedback: 'Smoke unblock cleanup',
|
|
},
|
|
});
|
|
assert.equal(unblockedReview.markAsBlocked, false);
|
|
logStep('client.coverage.unblock.ok', unblockedReview);
|
|
|
|
const uploadedProfilePhoto = await uploadFile('/staff/profile/photo', staffAuth.idToken, {
|
|
filename: 'profile-photo.jpg',
|
|
contentType: 'image/jpeg',
|
|
content: Buffer.from('fake-profile-photo'),
|
|
});
|
|
assert.ok(uploadedProfilePhoto.fileUri);
|
|
logStep('staff.profile.photo.upload.ok', uploadedProfilePhoto);
|
|
|
|
const uploadedGovId = await finalizeVerifiedUpload({
|
|
token: staffAuth.idToken,
|
|
uploadCategory: 'staff-document',
|
|
filename: 'government-id.jpg',
|
|
contentType: 'image/jpeg',
|
|
content: Buffer.from('fake-government-id'),
|
|
finalizePath: `/staff/profile/documents/${fixture.documents.governmentId.id}/upload`,
|
|
finalizeMethod: 'PUT',
|
|
verificationType: 'government_id',
|
|
subjectId: fixture.documents.governmentId.id,
|
|
rules: {
|
|
documentId: fixture.documents.governmentId.id,
|
|
},
|
|
});
|
|
assert.equal(uploadedGovId.finalized.documentId, fixture.documents.governmentId.id);
|
|
logStep('staff.profile.document.upload.ok', uploadedGovId.finalized);
|
|
|
|
const uploadedAttire = await finalizeVerifiedUpload({
|
|
token: staffAuth.idToken,
|
|
uploadCategory: 'staff-attire',
|
|
filename: 'black-shirt.jpg',
|
|
contentType: 'image/jpeg',
|
|
content: Buffer.from('fake-black-shirt'),
|
|
finalizePath: `/staff/profile/attire/${fixture.documents.attireBlackShirt.id}/upload`,
|
|
finalizeMethod: 'PUT',
|
|
verificationType: 'attire',
|
|
subjectId: fixture.documents.attireBlackShirt.id,
|
|
rules: {
|
|
dressCode: 'Black shirt',
|
|
},
|
|
});
|
|
assert.equal(uploadedAttire.finalized.documentId, fixture.documents.attireBlackShirt.id);
|
|
logStep('staff.profile.attire.upload.ok', uploadedAttire.finalized);
|
|
|
|
const certificateType = `ALCOHOL_SERVICE_${Date.now()}`;
|
|
const uploadedCertificate = await finalizeVerifiedUpload({
|
|
token: staffAuth.idToken,
|
|
uploadCategory: 'staff-certificate',
|
|
filename: 'certificate.pdf',
|
|
contentType: 'application/pdf',
|
|
content: Buffer.from('fake-certificate'),
|
|
finalizePath: '/staff/profile/certificates',
|
|
finalizeMethod: 'POST',
|
|
verificationType: 'certification',
|
|
subjectId: certificateType,
|
|
rules: {
|
|
certificateName: 'Alcohol Service Permit',
|
|
certificateIssuer: 'Demo Issuer',
|
|
},
|
|
finalizeBody: {
|
|
certificateType,
|
|
name: 'Alcohol Service Permit',
|
|
issuer: 'Demo Issuer',
|
|
},
|
|
});
|
|
assert.equal(uploadedCertificate.finalized.certificateType, certificateType);
|
|
logStep('staff.profile.certificate.upload.ok', uploadedCertificate.finalized);
|
|
|
|
const profileDocumentsAfter = await apiCall('/staff/profile/documents', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(profileDocumentsAfter.items.some((item) => item.documentId === fixture.documents.governmentId.id));
|
|
logStep('staff.profile.documents-after.ok', { count: profileDocumentsAfter.items.length });
|
|
|
|
const certificatesAfter = await apiCall('/staff/profile/certificates', {
|
|
token: staffAuth.idToken,
|
|
});
|
|
assert.ok(certificatesAfter.items.some((item) => item.certificateType === certificateType));
|
|
logStep('staff.profile.certificates-after.ok', { count: certificatesAfter.items.length });
|
|
|
|
const deletedCertificate = await apiCall(`/staff/profile/certificates/${certificateType}`, {
|
|
method: 'DELETE',
|
|
token: staffAuth.idToken,
|
|
expectedStatus: 200,
|
|
});
|
|
logStep('staff.profile.certificate.delete.ok', deletedCertificate);
|
|
|
|
const clientSignOut = await apiCall('/auth/client/sign-out', {
|
|
method: 'POST',
|
|
token: ownerSession.sessionToken,
|
|
});
|
|
logStep('auth.client.sign-out.ok', clientSignOut);
|
|
|
|
const staffSignOut = await apiCall('/auth/staff/sign-out', {
|
|
method: 'POST',
|
|
token: staffAuth.idToken,
|
|
});
|
|
logStep('auth.staff.sign-out.ok', staffSignOut);
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log('LIVE_SMOKE_V2_UNIFIED_OK');
|
|
}
|
|
|
|
main().catch((error) => {
|
|
// eslint-disable-next-line no-console
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|