feat(api): complete unified v2 mobile surface
This commit is contained in:
64
backend/unified-api/scripts/ensure-v2-demo-users.mjs
Normal file
64
backend/unified-api/scripts/ensure-v2-demo-users.mjs
Normal file
@@ -0,0 +1,64 @@
|
||||
import { signInWithPassword, signUpWithPassword } from '../src/services/identity-toolkit.js';
|
||||
|
||||
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!';
|
||||
|
||||
async function ensureUser({ email, password, displayName }) {
|
||||
try {
|
||||
const signedIn = await signInWithPassword({ email, password });
|
||||
return {
|
||||
uid: signedIn.localId,
|
||||
email,
|
||||
password,
|
||||
created: false,
|
||||
displayName,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error?.message || '';
|
||||
if (!message.includes('INVALID_LOGIN_CREDENTIALS') && !message.includes('EMAIL_NOT_FOUND')) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const signedUp = await signUpWithPassword({ email, password });
|
||||
return {
|
||||
uid: signedUp.localId,
|
||||
email,
|
||||
password,
|
||||
created: true,
|
||||
displayName,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error?.message || '';
|
||||
if (message.includes('EMAIL_EXISTS')) {
|
||||
throw new Error(`Firebase user ${email} exists but password does not match expected demo password.`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const owner = await ensureUser({
|
||||
email: ownerEmail,
|
||||
password: ownerPassword,
|
||||
displayName: 'Legendary Demo Owner V2',
|
||||
});
|
||||
|
||||
const staff = await ensureUser({
|
||||
email: staffEmail,
|
||||
password: staffPassword,
|
||||
displayName: 'Ana Barista V2',
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(JSON.stringify({ owner, staff }, null, 2));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
971
backend/unified-api/scripts/live-smoke-v2-unified.mjs
Normal file
971
backend/unified-api/scripts/live-smoke-v2-unified.mjs
Normal file
@@ -0,0 +1,971 @@
|
||||
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,
|
||||
} = {}) {
|
||||
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 (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 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 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));
|
||||
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 hubs = await apiCall('/client/hubs', {
|
||||
token: ownerSession.sessionToken,
|
||||
});
|
||||
assert.ok(hubs.items.some((hub) => hub.hubId === fixture.clockPoint.id));
|
||||
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 viewedOrders = await apiCall(`/client/orders/view?${reportWindow}`, {
|
||||
token: ownerSession.sessionToken,
|
||||
});
|
||||
assert.ok(Array.isArray(viewedOrders.items));
|
||||
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/${fixture.orders.completed.id}/edit`, {
|
||||
method: 'POST',
|
||||
token: ownerSession.sessionToken,
|
||||
idempotencyKey: uniqueKey('order-edit'),
|
||||
body: {
|
||||
eventName: `Edited Copy ${Date.now()}`,
|
||||
},
|
||||
});
|
||||
assert.ok(editedOrderCopy.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',
|
||||
},
|
||||
});
|
||||
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 staffDashboard = await apiCall('/staff/dashboard', {
|
||||
token: staffAuth.idToken,
|
||||
});
|
||||
assert.ok(Array.isArray(staffDashboard.recommendedShifts));
|
||||
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));
|
||||
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,
|
||||
});
|
||||
assert.ok(openShifts.items.some((shift) => shift.shiftId === fixture.shifts.available.id));
|
||||
logStep('staff.shifts.open.ok', { count: openShifts.items.length });
|
||||
|
||||
const pendingShifts = await apiCall('/staff/shifts/pending', {
|
||||
token: staffAuth.idToken,
|
||||
});
|
||||
assert.ok(pendingShifts.items.some((item) => item.shiftId === fixture.shifts.assigned.id));
|
||||
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));
|
||||
logStep('staff.shifts.completed.ok', { count: completedShifts.items.length });
|
||||
|
||||
const shiftDetail = await apiCall(`/staff/shifts/${fixture.shifts.available.id}`, {
|
||||
token: staffAuth.idToken,
|
||||
});
|
||||
assert.equal(shiftDetail.shiftId, fixture.shifts.available.id);
|
||||
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));
|
||||
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 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/${fixture.shifts.available.id}/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: 'NFC',
|
||||
nfcTagId: fixture.clockPoint.nfcTagUid,
|
||||
deviceId: 'smoke-iphone-15-pro',
|
||||
latitude: fixture.clockPoint.latitude,
|
||||
longitude: fixture.clockPoint.longitude,
|
||||
accuracyMeters: 8,
|
||||
capturedAt: isoTimestamp(0),
|
||||
},
|
||||
});
|
||||
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 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,
|
||||
breakMinutes: 30,
|
||||
capturedAt: isoTimestamp(1),
|
||||
},
|
||||
});
|
||||
logStep('staff.clock-out.ok', clockOut);
|
||||
|
||||
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 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 uploadFile(`/staff/profile/documents/${fixture.documents.governmentId.id}/upload`, staffAuth.idToken, {
|
||||
filename: 'government-id.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
content: Buffer.from('fake-government-id'),
|
||||
});
|
||||
assert.equal(uploadedGovId.documentId, fixture.documents.governmentId.id);
|
||||
logStep('staff.profile.document.upload.ok', uploadedGovId);
|
||||
|
||||
const uploadedAttire = await uploadFile(`/staff/profile/attire/${fixture.documents.attireBlackShirt.id}/upload`, staffAuth.idToken, {
|
||||
filename: 'black-shirt.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
content: Buffer.from('fake-black-shirt'),
|
||||
});
|
||||
assert.equal(uploadedAttire.documentId, fixture.documents.attireBlackShirt.id);
|
||||
logStep('staff.profile.attire.upload.ok', uploadedAttire);
|
||||
|
||||
const certificateType = `ALCOHOL_SERVICE_${Date.now()}`;
|
||||
const uploadedCertificate = await uploadFile('/staff/profile/certificates', staffAuth.idToken, {
|
||||
filename: 'certificate.pdf',
|
||||
contentType: 'application/pdf',
|
||||
content: Buffer.from('fake-certificate'),
|
||||
fields: {
|
||||
certificateType,
|
||||
name: 'Alcohol Service Permit',
|
||||
issuer: 'Demo Issuer',
|
||||
},
|
||||
});
|
||||
assert.equal(uploadedCertificate.certificateType, certificateType);
|
||||
logStep('staff.profile.certificate.upload.ok', uploadedCertificate);
|
||||
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user