feat(backend): implement v2 domain slice and live smoke

This commit is contained in:
zouantchaw
2026-03-11 18:23:55 +01:00
parent bc068373e9
commit fe43ff23cf
40 changed files with 5191 additions and 99 deletions

View File

@@ -0,0 +1,348 @@
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);
});