feat(backend): implement v2 domain slice and live smoke
This commit is contained in:
348
backend/command-api/scripts/live-smoke-v2.mjs
Normal file
348
backend/command-api/scripts/live-smoke-v2.mjs
Normal 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);
|
||||
});
|
||||
@@ -3,11 +3,11 @@ import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Pool } from 'pg';
|
||||
|
||||
const databaseUrl = process.env.IDEMPOTENCY_DATABASE_URL;
|
||||
const databaseUrl = process.env.IDEMPOTENCY_DATABASE_URL || process.env.DATABASE_URL;
|
||||
|
||||
if (!databaseUrl) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('IDEMPOTENCY_DATABASE_URL is required');
|
||||
console.error('IDEMPOTENCY_DATABASE_URL or DATABASE_URL is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
69
backend/command-api/scripts/migrate-v2-schema.mjs
Normal file
69
backend/command-api/scripts/migrate-v2-schema.mjs
Normal file
@@ -0,0 +1,69 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Pool } from 'pg';
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
|
||||
if (!databaseUrl) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('DATABASE_URL is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const scriptDir = resolve(fileURLToPath(new URL('.', import.meta.url)));
|
||||
const migrationsDir = resolve(scriptDir, '../sql/v2');
|
||||
|
||||
const migrationFiles = readdirSync(migrationsDir)
|
||||
.filter((file) => file.endsWith('.sql'))
|
||||
.sort();
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: databaseUrl,
|
||||
max: Number.parseInt(process.env.DB_POOL_MAX || '5', 10),
|
||||
});
|
||||
|
||||
async function ensureMigrationTable(client) {
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version TEXT PRIMARY KEY,
|
||||
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
try {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await ensureMigrationTable(client);
|
||||
|
||||
for (const file of migrationFiles) {
|
||||
const alreadyApplied = await client.query(
|
||||
'SELECT 1 FROM schema_migrations WHERE version = $1',
|
||||
[file]
|
||||
);
|
||||
if (alreadyApplied.rowCount > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sql = readFileSync(resolve(migrationsDir, file), 'utf8');
|
||||
await client.query(sql);
|
||||
await client.query(
|
||||
'INSERT INTO schema_migrations (version) VALUES ($1)',
|
||||
[file]
|
||||
);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Applied migration ${file}`);
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
600
backend/command-api/scripts/seed-v2-demo-data.mjs
Normal file
600
backend/command-api/scripts/seed-v2-demo-data.mjs
Normal file
@@ -0,0 +1,600 @@
|
||||
import { Pool } from 'pg';
|
||||
import { resolveDatabasePoolConfig } from '../src/services/db.js';
|
||||
import { V2DemoFixture as fixture } from './v2-demo-fixture.mjs';
|
||||
|
||||
const poolConfig = resolveDatabasePoolConfig();
|
||||
|
||||
if (!poolConfig) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Database connection settings are required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pool = new Pool(poolConfig);
|
||||
|
||||
function hoursFromNow(hours) {
|
||||
return new Date(Date.now() + (hours * 60 * 60 * 1000)).toISOString();
|
||||
}
|
||||
|
||||
async function upsertUser(client, user) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO users (id, email, display_name, status, metadata)
|
||||
VALUES ($1, $2, $3, 'ACTIVE', '{}'::jsonb)
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET email = EXCLUDED.email,
|
||||
display_name = EXCLUDED.display_name,
|
||||
status = 'ACTIVE',
|
||||
updated_at = NOW()
|
||||
`,
|
||||
[user.id, user.email || null, user.displayName || null]
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
await client.query('DELETE FROM tenants WHERE id = $1', [fixture.tenant.id]);
|
||||
|
||||
const openStartsAt = hoursFromNow(4);
|
||||
const openEndsAt = hoursFromNow(12);
|
||||
const completedStartsAt = hoursFromNow(-28);
|
||||
const completedEndsAt = hoursFromNow(-20);
|
||||
const checkedInAt = hoursFromNow(-27.5);
|
||||
const checkedOutAt = hoursFromNow(-20.25);
|
||||
const invoiceDueAt = hoursFromNow(72);
|
||||
|
||||
await upsertUser(client, fixture.users.businessOwner);
|
||||
await upsertUser(client, fixture.users.operationsManager);
|
||||
await upsertUser(client, fixture.users.vendorManager);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO tenants (id, slug, name, status, metadata)
|
||||
VALUES ($1, $2, $3, 'ACTIVE', $4::jsonb)
|
||||
`,
|
||||
[fixture.tenant.id, fixture.tenant.slug, fixture.tenant.name, JSON.stringify({ seededBy: 'seed-v2-demo-data' })]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO tenant_memberships (tenant_id, user_id, membership_status, base_role, metadata)
|
||||
VALUES
|
||||
($1, $2, 'ACTIVE', 'admin', '{"persona":"business_owner"}'::jsonb),
|
||||
($1, $3, 'ACTIVE', 'manager', '{"persona":"ops_manager"}'::jsonb),
|
||||
($1, $4, 'ACTIVE', 'manager', '{"persona":"vendor_manager"}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.tenant.id,
|
||||
fixture.users.businessOwner.id,
|
||||
fixture.users.operationsManager.id,
|
||||
fixture.users.vendorManager.id,
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO businesses (
|
||||
id, tenant_id, slug, business_name, status, contact_name, contact_email, contact_phone, metadata
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, 'ACTIVE', $5, $6, $7, $8::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.business.id,
|
||||
fixture.tenant.id,
|
||||
fixture.business.slug,
|
||||
fixture.business.name,
|
||||
'Legendary Client Manager',
|
||||
fixture.users.businessOwner.email,
|
||||
'+15550001001',
|
||||
JSON.stringify({ segment: 'buyer', seeded: true }),
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO business_memberships (
|
||||
tenant_id, business_id, user_id, membership_status, business_role, metadata
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, 'ACTIVE', 'owner', '{"persona":"client_owner"}'::jsonb),
|
||||
($1, $2, $4, 'ACTIVE', 'manager', '{"persona":"client_ops"}'::jsonb)
|
||||
`,
|
||||
[fixture.tenant.id, fixture.business.id, fixture.users.businessOwner.id, fixture.users.operationsManager.id]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO vendors (
|
||||
id, tenant_id, slug, company_name, status, contact_name, contact_email, contact_phone, metadata
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, 'ACTIVE', $5, $6, $7, $8::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.vendor.id,
|
||||
fixture.tenant.id,
|
||||
fixture.vendor.slug,
|
||||
fixture.vendor.name,
|
||||
'Vendor Manager',
|
||||
fixture.users.vendorManager.email,
|
||||
'+15550001002',
|
||||
JSON.stringify({ kind: 'internal_pool', seeded: true }),
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO vendor_memberships (
|
||||
tenant_id, vendor_id, user_id, membership_status, vendor_role, metadata
|
||||
)
|
||||
VALUES ($1, $2, $3, 'ACTIVE', 'owner', '{"persona":"vendor_owner"}'::jsonb)
|
||||
`,
|
||||
[fixture.tenant.id, fixture.vendor.id, fixture.users.vendorManager.id]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO roles_catalog (id, tenant_id, code, name, status, metadata)
|
||||
VALUES
|
||||
($1, $3, $4, $5, 'ACTIVE', '{}'::jsonb),
|
||||
($2, $3, $6, $7, 'ACTIVE', '{}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.roles.barista.id,
|
||||
fixture.roles.captain.id,
|
||||
fixture.tenant.id,
|
||||
fixture.roles.barista.code,
|
||||
fixture.roles.barista.name,
|
||||
fixture.roles.captain.code,
|
||||
fixture.roles.captain.name,
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO staffs (
|
||||
id, tenant_id, user_id, full_name, email, phone, status, primary_role, onboarding_status,
|
||||
average_rating, rating_count, metadata
|
||||
)
|
||||
VALUES ($1, $2, NULL, $3, $4, $5, 'ACTIVE', $6, 'COMPLETED', 4.50, 1, $7::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.staff.ana.id,
|
||||
fixture.tenant.id,
|
||||
fixture.staff.ana.fullName,
|
||||
fixture.staff.ana.email,
|
||||
fixture.staff.ana.phone,
|
||||
fixture.staff.ana.primaryRole,
|
||||
JSON.stringify({ favoriteCandidate: true, seeded: true }),
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO staff_roles (staff_id, role_id, is_primary)
|
||||
VALUES ($1, $2, TRUE)
|
||||
`,
|
||||
[fixture.staff.ana.id, fixture.roles.barista.id]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO workforce (id, tenant_id, vendor_id, staff_id, workforce_number, employment_type, status, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, 'TEMP', 'ACTIVE', $6::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.workforce.ana.id,
|
||||
fixture.tenant.id,
|
||||
fixture.vendor.id,
|
||||
fixture.staff.ana.id,
|
||||
fixture.workforce.ana.workforceNumber,
|
||||
JSON.stringify({ source: 'seed-v2-demo' }),
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO clock_points (
|
||||
id, tenant_id, business_id, label, address, latitude, longitude, geofence_radius_meters, nfc_tag_uid, status, metadata
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'ACTIVE', '{}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.clockPoint.id,
|
||||
fixture.tenant.id,
|
||||
fixture.business.id,
|
||||
fixture.clockPoint.label,
|
||||
fixture.clockPoint.address,
|
||||
fixture.clockPoint.latitude,
|
||||
fixture.clockPoint.longitude,
|
||||
fixture.clockPoint.geofenceRadiusMeters,
|
||||
fixture.clockPoint.nfcTagUid,
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO orders (
|
||||
id, tenant_id, business_id, vendor_id, order_number, title, description, status, service_type,
|
||||
starts_at, ends_at, location_name, location_address, latitude, longitude, notes, created_by_user_id, metadata
|
||||
)
|
||||
VALUES
|
||||
($1, $3, $4, $5, $6, $7, 'Open order for live v2 commands', 'OPEN', 'EVENT', $8, $9, 'Google Cafe', $10, $11, $12, 'Use this order for live smoke and frontend reads', $13, '{"slice":"open"}'::jsonb),
|
||||
($2, $3, $4, $5, $14, $15, 'Completed order for favorites, reviews, invoices, and attendance history', 'COMPLETED', 'CATERING', $16, $17, 'Google Catering', $10, $11, $12, 'Completed historical example', $13, '{"slice":"completed"}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.orders.open.id,
|
||||
fixture.orders.completed.id,
|
||||
fixture.tenant.id,
|
||||
fixture.business.id,
|
||||
fixture.vendor.id,
|
||||
fixture.orders.open.number,
|
||||
fixture.orders.open.title,
|
||||
openStartsAt,
|
||||
openEndsAt,
|
||||
fixture.clockPoint.address,
|
||||
fixture.clockPoint.latitude,
|
||||
fixture.clockPoint.longitude,
|
||||
fixture.users.businessOwner.id,
|
||||
fixture.orders.completed.number,
|
||||
fixture.orders.completed.title,
|
||||
completedStartsAt,
|
||||
completedEndsAt,
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO shifts (
|
||||
id, tenant_id, order_id, business_id, vendor_id, clock_point_id, shift_code, title, status, starts_at, ends_at, timezone,
|
||||
location_name, location_address, latitude, longitude, geofence_radius_meters, required_workers, assigned_workers, notes, metadata
|
||||
)
|
||||
VALUES
|
||||
($1, $3, $5, $7, $9, $11, $13, $15, 'OPEN', $17, $18, 'America/Los_Angeles', 'Google Cafe', $19, $21, $22, $23, 1, 0, 'Open staffing need', '{"slice":"open"}'::jsonb),
|
||||
($2, $4, $6, $8, $10, $12, $14, $16, 'COMPLETED', $20, $24, 'America/Los_Angeles', 'Google Catering', $19, $21, $22, $23, 1, 1, 'Completed staffed shift', '{"slice":"completed"}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.shifts.open.id,
|
||||
fixture.shifts.completed.id,
|
||||
fixture.tenant.id,
|
||||
fixture.tenant.id,
|
||||
fixture.orders.open.id,
|
||||
fixture.orders.completed.id,
|
||||
fixture.business.id,
|
||||
fixture.business.id,
|
||||
fixture.vendor.id,
|
||||
fixture.vendor.id,
|
||||
fixture.clockPoint.id,
|
||||
fixture.clockPoint.id,
|
||||
fixture.shifts.open.code,
|
||||
fixture.shifts.completed.code,
|
||||
fixture.shifts.open.title,
|
||||
fixture.shifts.completed.title,
|
||||
openStartsAt,
|
||||
openEndsAt,
|
||||
fixture.clockPoint.address,
|
||||
completedStartsAt,
|
||||
fixture.clockPoint.latitude,
|
||||
fixture.clockPoint.longitude,
|
||||
fixture.clockPoint.geofenceRadiusMeters,
|
||||
completedEndsAt,
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO shift_roles (
|
||||
id, shift_id, role_id, role_code, role_name, workers_needed, assigned_count, pay_rate_cents, bill_rate_cents, metadata
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, 1, 0, 2200, 3500, '{"slice":"open"}'::jsonb),
|
||||
($6, $7, $3, $4, $5, 1, 1, 2200, 3500, '{"slice":"completed"}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.shiftRoles.openBarista.id,
|
||||
fixture.shifts.open.id,
|
||||
fixture.roles.barista.id,
|
||||
fixture.roles.barista.code,
|
||||
fixture.roles.barista.name,
|
||||
fixture.shiftRoles.completedBarista.id,
|
||||
fixture.shifts.completed.id,
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO applications (
|
||||
id, tenant_id, shift_id, shift_role_id, staff_id, status, origin, applied_at, metadata
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, 'PENDING', 'STAFF', NOW(), '{"slice":"open"}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.applications.openAna.id,
|
||||
fixture.tenant.id,
|
||||
fixture.shifts.open.id,
|
||||
fixture.shiftRoles.openBarista.id,
|
||||
fixture.staff.ana.id,
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO assignments (
|
||||
id, tenant_id, business_id, vendor_id, shift_id, shift_role_id, workforce_id, staff_id, status,
|
||||
assigned_at, accepted_at, checked_in_at, checked_out_at, metadata
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'COMPLETED', $9, $10, $11, $12, '{"slice":"completed"}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.assignments.completedAna.id,
|
||||
fixture.tenant.id,
|
||||
fixture.business.id,
|
||||
fixture.vendor.id,
|
||||
fixture.shifts.completed.id,
|
||||
fixture.shiftRoles.completedBarista.id,
|
||||
fixture.workforce.ana.id,
|
||||
fixture.staff.ana.id,
|
||||
completedStartsAt,
|
||||
completedStartsAt,
|
||||
checkedInAt,
|
||||
checkedOutAt,
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO attendance_events (
|
||||
tenant_id, assignment_id, shift_id, staff_id, clock_point_id, event_type, source_type, source_reference,
|
||||
nfc_tag_uid, device_id, latitude, longitude, accuracy_meters, distance_to_clock_point_meters, within_geofence,
|
||||
validation_status, validation_reason, captured_at, raw_payload
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, 'CLOCK_IN', 'NFC', 'seed', $6, 'seed-device', $7, $8, 5, 0, TRUE, 'ACCEPTED', NULL, $9, '{"seeded":true}'::jsonb),
|
||||
($1, $2, $3, $4, $5, 'CLOCK_OUT', 'NFC', 'seed', $6, 'seed-device', $7, $8, 5, 0, TRUE, 'ACCEPTED', NULL, $10, '{"seeded":true}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.tenant.id,
|
||||
fixture.assignments.completedAna.id,
|
||||
fixture.shifts.completed.id,
|
||||
fixture.staff.ana.id,
|
||||
fixture.clockPoint.id,
|
||||
fixture.clockPoint.nfcTagUid,
|
||||
fixture.clockPoint.latitude,
|
||||
fixture.clockPoint.longitude,
|
||||
checkedInAt,
|
||||
checkedOutAt,
|
||||
]
|
||||
);
|
||||
|
||||
const attendanceEvents = await client.query(
|
||||
`
|
||||
SELECT id, event_type
|
||||
FROM attendance_events
|
||||
WHERE assignment_id = $1
|
||||
ORDER BY captured_at ASC
|
||||
`,
|
||||
[fixture.assignments.completedAna.id]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO attendance_sessions (
|
||||
id, tenant_id, assignment_id, staff_id, clock_in_event_id, clock_out_event_id, status,
|
||||
check_in_at, check_out_at, worked_minutes, metadata
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'CLOSED', $7, $8, 435, '{"seeded":true}'::jsonb)
|
||||
`,
|
||||
[
|
||||
'95f6017c-256c-4eb5-8033-eb942f018001',
|
||||
fixture.tenant.id,
|
||||
fixture.assignments.completedAna.id,
|
||||
fixture.staff.ana.id,
|
||||
attendanceEvents.rows.find((row) => row.event_type === 'CLOCK_IN')?.id,
|
||||
attendanceEvents.rows.find((row) => row.event_type === 'CLOCK_OUT')?.id,
|
||||
checkedInAt,
|
||||
checkedOutAt,
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO timesheets (
|
||||
id, tenant_id, assignment_id, staff_id, status, regular_minutes, overtime_minutes, break_minutes, gross_pay_cents, metadata
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, 'APPROVED', 420, 15, 30, 15950, '{"seeded":true}'::jsonb)
|
||||
`,
|
||||
[fixture.timesheets.completedAna.id, fixture.tenant.id, fixture.assignments.completedAna.id, fixture.staff.ana.id]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO documents (id, tenant_id, document_type, name, required_for_role_code, metadata)
|
||||
VALUES ($1, $2, 'CERTIFICATION', $3, $4, '{"seeded":true}'::jsonb)
|
||||
`,
|
||||
[fixture.documents.foodSafety.id, fixture.tenant.id, fixture.documents.foodSafety.name, fixture.roles.barista.code]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO staff_documents (id, tenant_id, staff_id, document_id, file_uri, status, expires_at, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, 'VERIFIED', $6, '{"seeded":true}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.staffDocuments.foodSafety.id,
|
||||
fixture.tenant.id,
|
||||
fixture.staff.ana.id,
|
||||
fixture.documents.foodSafety.id,
|
||||
`gs://krow-workforce-dev-v2-private/uploads/${fixture.staff.ana.id}/food-handler-card.pdf`,
|
||||
hoursFromNow(24 * 180),
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO certificates (id, tenant_id, staff_id, certificate_type, certificate_number, issued_at, expires_at, status, metadata)
|
||||
VALUES ($1, $2, $3, 'FOOD_SAFETY', 'FH-ANA-2026', $4, $5, 'VERIFIED', '{"seeded":true}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.certificates.foodSafety.id,
|
||||
fixture.tenant.id,
|
||||
fixture.staff.ana.id,
|
||||
hoursFromNow(-24 * 30),
|
||||
hoursFromNow(24 * 180),
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO verification_jobs (
|
||||
tenant_id, staff_id, document_id, type, file_uri, status, idempotency_key,
|
||||
provider_name, provider_reference, confidence, reasons, extracted, review, metadata
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, 'certification', $4, 'APPROVED', 'seed-certification-job',
|
||||
'seed', 'seed-certification-provider', 0.980, '["Verified by seed"]'::jsonb,
|
||||
'{"certificateType":"FOOD_SAFETY"}'::jsonb, '{"decision":"APPROVED"}'::jsonb, '{"seeded":true}'::jsonb
|
||||
)
|
||||
`,
|
||||
[
|
||||
fixture.tenant.id,
|
||||
fixture.staff.ana.id,
|
||||
fixture.documents.foodSafety.id,
|
||||
`gs://krow-workforce-dev-v2-private/uploads/${fixture.staff.ana.id}/food-handler-card.pdf`,
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO accounts (
|
||||
id, tenant_id, owner_type, owner_business_id, owner_vendor_id, owner_staff_id,
|
||||
provider_name, provider_reference, last4, is_primary, metadata
|
||||
)
|
||||
VALUES
|
||||
($1, $3, 'BUSINESS', $4, NULL, NULL, 'stripe', 'ba_business_demo', '6789', TRUE, '{"seeded":true}'::jsonb),
|
||||
($2, $3, 'STAFF', NULL, NULL, $5, 'stripe', 'ba_staff_demo', '4321', TRUE, '{"seeded":true}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.accounts.businessPrimary.id,
|
||||
fixture.accounts.staffPrimary.id,
|
||||
fixture.tenant.id,
|
||||
fixture.business.id,
|
||||
fixture.staff.ana.id,
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO invoices (
|
||||
id, tenant_id, order_id, business_id, vendor_id, invoice_number, status, currency_code,
|
||||
subtotal_cents, tax_cents, total_cents, due_at, metadata
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'PENDING_REVIEW', 'USD', 15250, 700, 15950, $7, '{"seeded":true}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.invoices.completed.id,
|
||||
fixture.tenant.id,
|
||||
fixture.orders.completed.id,
|
||||
fixture.business.id,
|
||||
fixture.vendor.id,
|
||||
fixture.invoices.completed.number,
|
||||
invoiceDueAt,
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO recent_payments (
|
||||
id, tenant_id, invoice_id, assignment_id, staff_id, status, amount_cents, process_date, metadata
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, 'PENDING', 15950, NULL, '{"seeded":true}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.recentPayments.completed.id,
|
||||
fixture.tenant.id,
|
||||
fixture.invoices.completed.id,
|
||||
fixture.assignments.completedAna.id,
|
||||
fixture.staff.ana.id,
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO staff_favorites (id, tenant_id, business_id, staff_id, created_by_user_id, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, NOW())
|
||||
`,
|
||||
[
|
||||
fixture.favorites.ana.id,
|
||||
fixture.tenant.id,
|
||||
fixture.business.id,
|
||||
fixture.staff.ana.id,
|
||||
fixture.users.businessOwner.id,
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO staff_reviews (
|
||||
id, tenant_id, business_id, staff_id, assignment_id, reviewer_user_id, rating, review_text, tags, created_at, updated_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 5, 'Reliable, on time, and client friendly.', '["reliable","favorite"]'::jsonb, NOW(), NOW())
|
||||
`,
|
||||
[
|
||||
fixture.reviews.anaCompleted.id,
|
||||
fixture.tenant.id,
|
||||
fixture.business.id,
|
||||
fixture.staff.ana.id,
|
||||
fixture.assignments.completedAna.id,
|
||||
fixture.users.businessOwner.id,
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO domain_events (tenant_id, aggregate_type, aggregate_id, sequence, event_type, actor_user_id, payload)
|
||||
VALUES
|
||||
($1, 'order', $2, 1, 'ORDER_CREATED', $3, '{"seeded":true}'::jsonb),
|
||||
($1, 'assignment', $4, 1, 'STAFF_ASSIGNED', $3, '{"seeded":true}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.tenant.id,
|
||||
fixture.orders.completed.id,
|
||||
fixture.users.businessOwner.id,
|
||||
fixture.assignments.completedAna.id,
|
||||
]
|
||||
);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(JSON.stringify({
|
||||
tenantId: fixture.tenant.id,
|
||||
businessId: fixture.business.id,
|
||||
vendorId: fixture.vendor.id,
|
||||
staffId: fixture.staff.ana.id,
|
||||
workforceId: fixture.workforce.ana.id,
|
||||
openOrderId: fixture.orders.open.id,
|
||||
openShiftId: fixture.shifts.open.id,
|
||||
openShiftRoleId: fixture.shiftRoles.openBarista.id,
|
||||
openApplicationId: fixture.applications.openAna.id,
|
||||
completedOrderId: fixture.orders.completed.id,
|
||||
completedAssignmentId: fixture.assignments.completedAna.id,
|
||||
clockPointId: fixture.clockPoint.id,
|
||||
nfcTagUid: fixture.clockPoint.nfcTagUid,
|
||||
businessOwnerUid: fixture.users.businessOwner.id,
|
||||
}, null, 2));
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
162
backend/command-api/scripts/v2-demo-fixture.mjs
Normal file
162
backend/command-api/scripts/v2-demo-fixture.mjs
Normal file
@@ -0,0 +1,162 @@
|
||||
export const V2DemoFixture = {
|
||||
tenant: {
|
||||
id: '6d5fa42c-1f38-49be-8895-8aeb0e731001',
|
||||
slug: 'legendary-event-staffing',
|
||||
name: 'Legendary Event Staffing and Entertainment',
|
||||
},
|
||||
users: {
|
||||
businessOwner: {
|
||||
id: process.env.V2_DEMO_OWNER_UID || 'dvpWnaBjT6UksS5lo04hfMTyq1q1',
|
||||
email: process.env.V2_DEMO_OWNER_EMAIL || 'legendary@krowd.com',
|
||||
displayName: 'Legendary Demo Owner',
|
||||
},
|
||||
operationsManager: {
|
||||
id: 'demo-ops-manager',
|
||||
email: 'ops+v2@krowd.com',
|
||||
displayName: 'Wil Ops Lead',
|
||||
},
|
||||
vendorManager: {
|
||||
id: 'demo-vendor-manager',
|
||||
email: 'vendor+v2@krowd.com',
|
||||
displayName: 'Vendor Manager',
|
||||
},
|
||||
},
|
||||
business: {
|
||||
id: '14f4fcfb-f21f-4ba9-9328-90f794a56001',
|
||||
slug: 'google-mv-cafes',
|
||||
name: 'Google Mountain View Cafes',
|
||||
},
|
||||
vendor: {
|
||||
id: '80f8c8d3-9da8-4892-908f-4d4982af7001',
|
||||
slug: 'legendary-pool-a',
|
||||
name: 'Legendary Staffing Pool A',
|
||||
},
|
||||
roles: {
|
||||
barista: {
|
||||
id: '67c5010e-85f0-4f6b-99b7-167c9afdf001',
|
||||
code: 'BARISTA',
|
||||
name: 'Barista',
|
||||
},
|
||||
captain: {
|
||||
id: '67c5010e-85f0-4f6b-99b7-167c9afdf002',
|
||||
code: 'CAPTAIN',
|
||||
name: 'Captain',
|
||||
},
|
||||
},
|
||||
staff: {
|
||||
ana: {
|
||||
id: '4b7dff1a-1856-4d59-b450-5a6736461001',
|
||||
fullName: 'Ana Barista',
|
||||
email: 'ana.barista+v2@krowd.com',
|
||||
phone: '+15557654321',
|
||||
primaryRole: 'BARISTA',
|
||||
},
|
||||
},
|
||||
workforce: {
|
||||
ana: {
|
||||
id: '4cc1d34a-87c3-4426-8ee0-a24c8bcfa001',
|
||||
workforceNumber: 'WF-V2-ANA-001',
|
||||
},
|
||||
},
|
||||
clockPoint: {
|
||||
id: 'efb80ccf-3361-49c8-bc74-ff8cd4d2e001',
|
||||
label: 'Google MV Cafe Clock Point',
|
||||
address: '1600 Amphitheatre Pkwy, Mountain View, CA',
|
||||
latitude: 37.4221,
|
||||
longitude: -122.0841,
|
||||
geofenceRadiusMeters: 120,
|
||||
nfcTagUid: 'NFC-DEMO-ANA-001',
|
||||
},
|
||||
orders: {
|
||||
open: {
|
||||
id: 'b6132d7a-45c3-4879-b349-46b2fd518001',
|
||||
number: 'ORD-V2-OPEN-1001',
|
||||
title: 'Morning cafe staffing',
|
||||
},
|
||||
completed: {
|
||||
id: 'b6132d7a-45c3-4879-b349-46b2fd518002',
|
||||
number: 'ORD-V2-COMP-1002',
|
||||
title: 'Completed catering shift',
|
||||
},
|
||||
},
|
||||
shifts: {
|
||||
open: {
|
||||
id: '6e7dadad-99e4-45bb-b0da-7bb617954001',
|
||||
code: 'SHIFT-V2-OPEN-1',
|
||||
title: 'Open breakfast shift',
|
||||
},
|
||||
completed: {
|
||||
id: '6e7dadad-99e4-45bb-b0da-7bb617954002',
|
||||
code: 'SHIFT-V2-COMP-1',
|
||||
title: 'Completed catering shift',
|
||||
},
|
||||
},
|
||||
shiftRoles: {
|
||||
openBarista: {
|
||||
id: '4dd35b2b-4aaf-4c28-a91f-7bda05e2b001',
|
||||
},
|
||||
completedBarista: {
|
||||
id: '4dd35b2b-4aaf-4c28-a91f-7bda05e2b002',
|
||||
},
|
||||
},
|
||||
applications: {
|
||||
openAna: {
|
||||
id: 'd70d6441-6d0c-4fdb-9a29-c9d9e0c34001',
|
||||
},
|
||||
},
|
||||
assignments: {
|
||||
completedAna: {
|
||||
id: 'f1d3f738-a132-4863-b222-4f9cb25aa001',
|
||||
},
|
||||
},
|
||||
timesheets: {
|
||||
completedAna: {
|
||||
id: '41ea4057-0c55-4907-b525-07315b2b6001',
|
||||
},
|
||||
},
|
||||
invoices: {
|
||||
completed: {
|
||||
id: '1455e15b-77f9-4c66-b2a8-dce35f7ac001',
|
||||
number: 'INV-V2-2001',
|
||||
},
|
||||
},
|
||||
recentPayments: {
|
||||
completed: {
|
||||
id: 'be6f736b-e945-4676-a73d-2912c7575001',
|
||||
},
|
||||
},
|
||||
favorites: {
|
||||
ana: {
|
||||
id: 'ba5cb8fa-0be9-4ef4-a9fb-e60a8a48e001',
|
||||
},
|
||||
},
|
||||
reviews: {
|
||||
anaCompleted: {
|
||||
id: '9b6bc737-fd69-4855-b425-6f0c2c4fd001',
|
||||
},
|
||||
},
|
||||
documents: {
|
||||
foodSafety: {
|
||||
id: 'e6fd0183-34d9-4c23-9a9a-bf98da995001',
|
||||
name: 'Food Handler Card',
|
||||
},
|
||||
},
|
||||
staffDocuments: {
|
||||
foodSafety: {
|
||||
id: '4b157236-a4b0-4c44-b199-7d4ea1f95001',
|
||||
},
|
||||
},
|
||||
certificates: {
|
||||
foodSafety: {
|
||||
id: 'df6452dc-4ec7-4d54-876d-26bf8ce5b001',
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
businessPrimary: {
|
||||
id: '5d98e0ba-8e89-4ffb-aafd-df6bbe2fe001',
|
||||
},
|
||||
staffPrimary: {
|
||||
id: '5d98e0ba-8e89-4ffb-aafd-df6bbe2fe002',
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user