diff --git a/backend/query-api/src/services/mobile-query-service.js b/backend/query-api/src/services/mobile-query-service.js index 4cbb7e53..cad050e9 100644 --- a/backend/query-api/src/services/mobile-query-service.js +++ b/backend/query-api/src/services/mobile-query-service.js @@ -630,8 +630,14 @@ export async function listTodayShifts(actorUid) { SELECT a.id AS "assignmentId", s.id AS "shiftId", + COALESCE(s.title, sr.role_name || ' shift') AS title, + b.business_name AS "clientName", + ROUND(COALESCE(sr.pay_rate_cents, 0)::numeric / 100, 2) AS "hourlyRate", sr.role_name AS "roleName", COALESCE(cp.label, s.location_name) AS location, + COALESCE(s.location_address, cp.address) AS "locationAddress", + COALESCE(s.latitude, cp.latitude) AS latitude, + COALESCE(s.longitude, cp.longitude) AS longitude, s.starts_at AS "startTime", s.ends_at AS "endTime", COALESCE(s.clock_in_mode, cp.default_clock_in_mode, 'EITHER') AS "clockInMode", @@ -643,6 +649,7 @@ export async function listTodayShifts(actorUid) { FROM assignments a JOIN shifts s ON s.id = a.shift_id JOIN shift_roles sr ON sr.id = a.shift_role_id + JOIN businesses b ON b.id = s.business_id LEFT JOIN clock_points cp ON cp.id = s.clock_point_id LEFT JOIN attendance_sessions ON attendance_sessions.assignment_id = a.id WHERE a.tenant_id = $1 diff --git a/backend/unified-api/scripts/ensure-v2-demo-users.mjs b/backend/unified-api/scripts/ensure-v2-demo-users.mjs index d5ea6cc1..1a1ff5a6 100644 --- a/backend/unified-api/scripts/ensure-v2-demo-users.mjs +++ b/backend/unified-api/scripts/ensure-v2-demo-users.mjs @@ -1,10 +1,24 @@ import { signInWithPassword, signUpWithPassword } from '../src/services/identity-toolkit.js'; +import { applicationDefault, getApps, initializeApp } from 'firebase-admin/app'; +import { getAuth } from 'firebase-admin/auth'; 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 staffPhone = process.env.V2_DEMO_STAFF_PHONE || '+15557654321'; const ownerPassword = process.env.V2_DEMO_OWNER_PASSWORD || 'Demo2026!'; const staffPassword = process.env.V2_DEMO_STAFF_PASSWORD || 'Demo2026!'; +function ensureAdminApp() { + if (getApps().length === 0) { + initializeApp({ credential: applicationDefault() }); + } +} + +function getAdminAuth() { + ensureAdminApp(); + return getAuth(); +} + async function ensureUser({ email, password, displayName }) { try { const signedIn = await signInWithPassword({ email, password }); @@ -40,6 +54,44 @@ async function ensureUser({ email, password, displayName }) { } } +async function getUserByPhoneNumber(phoneNumber) { + try { + return await getAdminAuth().getUserByPhoneNumber(phoneNumber); + } catch (error) { + if (error?.code === 'auth/user-not-found') return null; + throw error; + } +} + +async function reconcileStaffPhoneIdentity({ uid, email, displayName, phoneNumber }) { + const auth = getAdminAuth(); + const current = await auth.getUser(uid); + const existingPhoneUser = await getUserByPhoneNumber(phoneNumber); + let deletedConflictingUid = null; + + if (existingPhoneUser && existingPhoneUser.uid !== uid) { + deletedConflictingUid = existingPhoneUser.uid; + await auth.deleteUser(existingPhoneUser.uid); + } + + const updatePayload = {}; + if (current.displayName !== displayName) updatePayload.displayName = displayName; + if (current.email !== email) updatePayload.email = email; + if (current.phoneNumber !== phoneNumber) updatePayload.phoneNumber = phoneNumber; + + if (Object.keys(updatePayload).length > 0) { + await auth.updateUser(uid, updatePayload); + } + + const reconciled = await auth.getUser(uid); + return { + uid: reconciled.uid, + email: reconciled.email, + phoneNumber: reconciled.phoneNumber, + deletedConflictingUid, + }; +} + async function main() { const owner = await ensureUser({ email: ownerEmail, @@ -53,8 +105,15 @@ async function main() { displayName: 'Ana Barista V2', }); + const reconciledStaff = await reconcileStaffPhoneIdentity({ + uid: staff.uid, + email: staff.email, + displayName: staff.displayName, + phoneNumber: staffPhone, + }); + // eslint-disable-next-line no-console - console.log(JSON.stringify({ owner, staff }, null, 2)); + console.log(JSON.stringify({ owner, staff: { ...staff, ...reconciledStaff } }, null, 2)); } main().catch((error) => { diff --git a/backend/unified-api/scripts/live-smoke-v2-unified.mjs b/backend/unified-api/scripts/live-smoke-v2-unified.mjs index b6cd2402..61be8e53 100644 --- a/backend/unified-api/scripts/live-smoke-v2-unified.mjs +++ b/backend/unified-api/scripts/live-smoke-v2-unified.mjs @@ -631,6 +631,10 @@ async function main() { assert.ok(Array.isArray(todaysShifts.items)); const assignedTodayShift = todaysShifts.items.find((shift) => shift.shiftId === fixture.shifts.assigned.id); assert.ok(assignedTodayShift); + assert.equal(assignedTodayShift.clientName, fixture.business.name); + assert.equal(typeof assignedTodayShift.hourlyRate, 'number'); + assert.equal(typeof assignedTodayShift.latitude, 'number'); + assert.equal(typeof assignedTodayShift.longitude, 'number'); assert.equal(assignedTodayShift.clockInMode, fixture.shifts.assigned.clockInMode); assert.equal(assignedTodayShift.allowClockInOverride, fixture.shifts.assigned.allowClockInOverride); logStep('staff.clock-in.shifts-today.ok', { count: todaysShifts.items.length }); diff --git a/docs/BACKEND/API_GUIDES/V2/authentication.md b/docs/BACKEND/API_GUIDES/V2/authentication.md index d90e585a..c45cf5de 100644 --- a/docs/BACKEND/API_GUIDES/V2/authentication.md +++ b/docs/BACKEND/API_GUIDES/V2/authentication.md @@ -182,6 +182,13 @@ Possible response A: This is the normal mobile path when frontend does **not** send recaptcha or integrity tokens. +Current dev demo worker: + +- phone number: `+15557654321` +- email: `ana.barista+v2@krowd.com` + +Those two now resolve to the same Firebase user and the same seeded staff profile in v2. + Possible response B: ```json diff --git a/docs/BACKEND/API_GUIDES/V2/unified-api.md b/docs/BACKEND/API_GUIDES/V2/unified-api.md index c9778dea..aea858ec 100644 --- a/docs/BACKEND/API_GUIDES/V2/unified-api.md +++ b/docs/BACKEND/API_GUIDES/V2/unified-api.md @@ -114,6 +114,31 @@ Full auth behavior, including staff phone flow and refresh rules, is documented - `GET /staff/faqs` - `GET /staff/faqs/search` +Example `GET /staff/clock-in/shifts/today` item: + +```json +{ + "assignmentId": "uuid", + "shiftId": "uuid", + "title": "Assigned espresso shift", + "clientName": "Google Mountain View Cafes", + "hourlyRate": 23, + "roleName": "Barista", + "location": "Google MV Cafe Clock Point", + "locationAddress": "1600 Amphitheatre Pkwy, Mountain View, CA", + "latitude": 37.4221, + "longitude": -122.0841, + "startTime": "2026-03-17T13:48:23.482Z", + "endTime": "2026-03-17T21:48:23.482Z", + "clockInMode": "GEO_REQUIRED", + "allowClockInOverride": true, + "geofenceRadiusMeters": 120, + "nfcTagId": "NFC-DEMO-ANA-001", + "attendanceStatus": "NOT_CLOCKED_IN", + "clockInAt": null +} +``` + ### Staff writes - `POST /staff/profile/setup`