349 lines
11 KiB
JavaScript
349 lines
11 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { V2DemoFixture as fixture } from './v2-demo-fixture.mjs';
|
|
|
|
const firebaseApiKey = process.env.FIREBASE_API_KEY || 'AIzaSyBqRtZPMGU-Sz5x5UnRrunKu5NSWYyPRn8';
|
|
const demoEmail = process.env.V2_SMOKE_EMAIL || fixture.users.businessOwner.email;
|
|
const demoPassword = process.env.V2_SMOKE_PASSWORD || 'Demo2026!';
|
|
const commandBaseUrl = process.env.COMMAND_API_BASE_URL || 'https://krow-command-api-v2-e3g6witsvq-uc.a.run.app';
|
|
const queryBaseUrl = process.env.QUERY_API_BASE_URL || 'https://krow-query-api-v2-e3g6witsvq-uc.a.run.app';
|
|
|
|
async function signInWithPassword() {
|
|
const response = await fetch(
|
|
`https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=${firebaseApiKey}`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
email: demoEmail,
|
|
password: demoPassword,
|
|
returnSecureToken: true,
|
|
}),
|
|
}
|
|
);
|
|
|
|
const payload = await response.json();
|
|
if (!response.ok) {
|
|
throw new Error(`Firebase sign-in failed: ${JSON.stringify(payload)}`);
|
|
}
|
|
|
|
return {
|
|
idToken: payload.idToken,
|
|
localId: payload.localId,
|
|
};
|
|
}
|
|
|
|
async function apiCall(baseUrl, 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(`${baseUrl}${path}`, {
|
|
method,
|
|
headers,
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
});
|
|
|
|
const text = await response.text();
|
|
const payload = text ? JSON.parse(text) : {};
|
|
|
|
if (response.status !== expectedStatus) {
|
|
throw new Error(`${method} ${path} expected ${expectedStatus}, got ${response.status}: ${text}`);
|
|
}
|
|
|
|
return payload;
|
|
}
|
|
|
|
function uniqueKey(prefix) {
|
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
}
|
|
|
|
function logStep(step, payload) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(`[live-smoke-v2] ${step}: ${JSON.stringify(payload)}`);
|
|
}
|
|
|
|
async function main() {
|
|
const auth = await signInWithPassword();
|
|
assert.equal(auth.localId, fixture.users.businessOwner.id);
|
|
logStep('auth.ok', { uid: auth.localId, email: demoEmail });
|
|
|
|
const listOrders = await apiCall(
|
|
queryBaseUrl,
|
|
`/query/tenants/${fixture.tenant.id}/orders`,
|
|
{ token: auth.idToken }
|
|
);
|
|
assert.ok(Array.isArray(listOrders.items));
|
|
assert.ok(listOrders.items.some((item) => item.id === fixture.orders.open.id));
|
|
logStep('orders.list.ok', { count: listOrders.items.length });
|
|
|
|
const openOrderDetail = await apiCall(
|
|
queryBaseUrl,
|
|
`/query/tenants/${fixture.tenant.id}/orders/${fixture.orders.open.id}`,
|
|
{ token: auth.idToken }
|
|
);
|
|
assert.equal(openOrderDetail.id, fixture.orders.open.id);
|
|
assert.equal(openOrderDetail.shifts[0].id, fixture.shifts.open.id);
|
|
logStep('orders.detail.ok', { orderId: openOrderDetail.id, shiftCount: openOrderDetail.shifts.length });
|
|
|
|
const favoriteResult = await apiCall(
|
|
commandBaseUrl,
|
|
`/commands/businesses/${fixture.business.id}/favorite-staff`,
|
|
{
|
|
method: 'POST',
|
|
token: auth.idToken,
|
|
idempotencyKey: uniqueKey('favorite'),
|
|
body: {
|
|
tenantId: fixture.tenant.id,
|
|
staffId: fixture.staff.ana.id,
|
|
},
|
|
}
|
|
);
|
|
assert.equal(favoriteResult.staffId, fixture.staff.ana.id);
|
|
logStep('favorites.add.ok', favoriteResult);
|
|
|
|
const favoriteList = await apiCall(
|
|
queryBaseUrl,
|
|
`/query/tenants/${fixture.tenant.id}/businesses/${fixture.business.id}/favorite-staff`,
|
|
{ token: auth.idToken }
|
|
);
|
|
assert.ok(favoriteList.items.some((item) => item.staffId === fixture.staff.ana.id));
|
|
logStep('favorites.list.ok', { count: favoriteList.items.length });
|
|
|
|
const reviewResult = await apiCall(
|
|
commandBaseUrl,
|
|
`/commands/assignments/${fixture.assignments.completedAna.id}/reviews`,
|
|
{
|
|
method: 'POST',
|
|
token: auth.idToken,
|
|
idempotencyKey: uniqueKey('review'),
|
|
body: {
|
|
tenantId: fixture.tenant.id,
|
|
businessId: fixture.business.id,
|
|
staffId: fixture.staff.ana.id,
|
|
rating: 5,
|
|
reviewText: 'Live smoke review',
|
|
tags: ['smoke', 'reliable'],
|
|
},
|
|
}
|
|
);
|
|
assert.equal(reviewResult.staffId, fixture.staff.ana.id);
|
|
logStep('reviews.create.ok', reviewResult);
|
|
|
|
const reviewSummary = await apiCall(
|
|
queryBaseUrl,
|
|
`/query/tenants/${fixture.tenant.id}/staff/${fixture.staff.ana.id}/review-summary`,
|
|
{ token: auth.idToken }
|
|
);
|
|
assert.equal(reviewSummary.staffId, fixture.staff.ana.id);
|
|
assert.ok(reviewSummary.ratingCount >= 1);
|
|
logStep('reviews.summary.ok', { ratingCount: reviewSummary.ratingCount, averageRating: reviewSummary.averageRating });
|
|
|
|
const assigned = await apiCall(
|
|
commandBaseUrl,
|
|
`/commands/shifts/${fixture.shifts.open.id}/assign-staff`,
|
|
{
|
|
method: 'POST',
|
|
token: auth.idToken,
|
|
idempotencyKey: uniqueKey('assign'),
|
|
body: {
|
|
tenantId: fixture.tenant.id,
|
|
shiftRoleId: fixture.shiftRoles.openBarista.id,
|
|
workforceId: fixture.workforce.ana.id,
|
|
applicationId: fixture.applications.openAna.id,
|
|
},
|
|
}
|
|
);
|
|
assert.equal(assigned.shiftId, fixture.shifts.open.id);
|
|
logStep('assign.ok', assigned);
|
|
|
|
const accepted = await apiCall(
|
|
commandBaseUrl,
|
|
`/commands/shifts/${fixture.shifts.open.id}/accept`,
|
|
{
|
|
method: 'POST',
|
|
token: auth.idToken,
|
|
idempotencyKey: uniqueKey('accept'),
|
|
body: {
|
|
shiftRoleId: fixture.shiftRoles.openBarista.id,
|
|
workforceId: fixture.workforce.ana.id,
|
|
},
|
|
}
|
|
);
|
|
assert.ok(['ASSIGNED', 'ACCEPTED', 'CHECKED_IN', 'CHECKED_OUT', 'COMPLETED'].includes(accepted.status));
|
|
const liveAssignmentId = accepted.assignmentId || assigned.assignmentId;
|
|
logStep('accept.ok', accepted);
|
|
|
|
const clockIn = await apiCall(
|
|
commandBaseUrl,
|
|
'/commands/attendance/clock-in',
|
|
{
|
|
method: 'POST',
|
|
token: auth.idToken,
|
|
idempotencyKey: uniqueKey('clockin'),
|
|
body: {
|
|
assignmentId: liveAssignmentId,
|
|
sourceType: 'NFC',
|
|
sourceReference: 'smoke',
|
|
nfcTagUid: fixture.clockPoint.nfcTagUid,
|
|
deviceId: 'smoke-device',
|
|
latitude: fixture.clockPoint.latitude,
|
|
longitude: fixture.clockPoint.longitude,
|
|
accuracyMeters: 5,
|
|
},
|
|
}
|
|
);
|
|
assert.equal(clockIn.assignmentId, liveAssignmentId);
|
|
logStep('attendance.clockin.ok', clockIn);
|
|
|
|
const clockOut = await apiCall(
|
|
commandBaseUrl,
|
|
'/commands/attendance/clock-out',
|
|
{
|
|
method: 'POST',
|
|
token: auth.idToken,
|
|
idempotencyKey: uniqueKey('clockout'),
|
|
body: {
|
|
assignmentId: liveAssignmentId,
|
|
sourceType: 'NFC',
|
|
sourceReference: 'smoke',
|
|
nfcTagUid: fixture.clockPoint.nfcTagUid,
|
|
deviceId: 'smoke-device',
|
|
latitude: fixture.clockPoint.latitude,
|
|
longitude: fixture.clockPoint.longitude,
|
|
accuracyMeters: 5,
|
|
},
|
|
}
|
|
);
|
|
assert.equal(clockOut.assignmentId, liveAssignmentId);
|
|
logStep('attendance.clockout.ok', clockOut);
|
|
|
|
const attendance = await apiCall(
|
|
queryBaseUrl,
|
|
`/query/tenants/${fixture.tenant.id}/assignments/${liveAssignmentId}/attendance`,
|
|
{ token: auth.idToken }
|
|
);
|
|
assert.ok(Array.isArray(attendance.events));
|
|
assert.ok(attendance.events.length >= 2);
|
|
logStep('attendance.query.ok', { eventCount: attendance.events.length, sessionStatus: attendance.sessionStatus });
|
|
|
|
const orderNumber = `ORD-V2-SMOKE-${Date.now()}`;
|
|
const createdOrder = await apiCall(
|
|
commandBaseUrl,
|
|
'/commands/orders/create',
|
|
{
|
|
method: 'POST',
|
|
token: auth.idToken,
|
|
idempotencyKey: uniqueKey('order-create'),
|
|
body: {
|
|
tenantId: fixture.tenant.id,
|
|
businessId: fixture.business.id,
|
|
vendorId: fixture.vendor.id,
|
|
orderNumber,
|
|
title: 'Smoke created order',
|
|
serviceType: 'EVENT',
|
|
shifts: [
|
|
{
|
|
shiftCode: `SHIFT-${Date.now()}`,
|
|
title: 'Smoke shift',
|
|
startsAt: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(),
|
|
endsAt: new Date(Date.now() + 6 * 60 * 60 * 1000).toISOString(),
|
|
requiredWorkers: 1,
|
|
clockPointId: fixture.clockPoint.id,
|
|
roles: [
|
|
{
|
|
roleCode: fixture.roles.barista.code,
|
|
roleName: fixture.roles.barista.name,
|
|
workersNeeded: 1,
|
|
payRateCents: 2200,
|
|
billRateCents: 3500,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
}
|
|
);
|
|
assert.equal(createdOrder.orderNumber, orderNumber);
|
|
logStep('orders.create.ok', createdOrder);
|
|
|
|
const updatedOrder = await apiCall(
|
|
commandBaseUrl,
|
|
`/commands/orders/${createdOrder.orderId}/update`,
|
|
{
|
|
method: 'POST',
|
|
token: auth.idToken,
|
|
idempotencyKey: uniqueKey('order-update'),
|
|
body: {
|
|
tenantId: fixture.tenant.id,
|
|
title: 'Smoke updated order',
|
|
notes: 'updated during live smoke',
|
|
},
|
|
}
|
|
);
|
|
assert.equal(updatedOrder.orderId, createdOrder.orderId);
|
|
logStep('orders.update.ok', updatedOrder);
|
|
|
|
const changedShift = await apiCall(
|
|
commandBaseUrl,
|
|
`/commands/shifts/${createdOrder.shiftIds[0]}/change-status`,
|
|
{
|
|
method: 'POST',
|
|
token: auth.idToken,
|
|
idempotencyKey: uniqueKey('shift-status'),
|
|
body: {
|
|
tenantId: fixture.tenant.id,
|
|
status: 'PENDING_CONFIRMATION',
|
|
reason: 'live smoke transition',
|
|
},
|
|
}
|
|
);
|
|
assert.equal(changedShift.status, 'PENDING_CONFIRMATION');
|
|
logStep('shift.status.ok', changedShift);
|
|
|
|
const cancelledOrder = await apiCall(
|
|
commandBaseUrl,
|
|
`/commands/orders/${createdOrder.orderId}/cancel`,
|
|
{
|
|
method: 'POST',
|
|
token: auth.idToken,
|
|
idempotencyKey: uniqueKey('order-cancel'),
|
|
body: {
|
|
tenantId: fixture.tenant.id,
|
|
reason: 'live smoke cleanup',
|
|
},
|
|
}
|
|
);
|
|
assert.equal(cancelledOrder.status, 'CANCELLED');
|
|
logStep('orders.cancel.ok', cancelledOrder);
|
|
|
|
const cancelledOrderDetail = await apiCall(
|
|
queryBaseUrl,
|
|
`/query/tenants/${fixture.tenant.id}/orders/${createdOrder.orderId}`,
|
|
{ token: auth.idToken }
|
|
);
|
|
assert.equal(cancelledOrderDetail.status, 'CANCELLED');
|
|
logStep('orders.cancel.verify.ok', { orderId: cancelledOrderDetail.id, status: cancelledOrderDetail.status });
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log('LIVE_SMOKE_V2_OK');
|
|
}
|
|
|
|
main().catch((error) => {
|
|
// eslint-disable-next-line no-console
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|