feat(api): add unified v2 gateway and mobile read slice
This commit is contained in:
@@ -49,6 +49,7 @@ async function main() {
|
||||
await upsertUser(client, fixture.users.businessOwner);
|
||||
await upsertUser(client, fixture.users.operationsManager);
|
||||
await upsertUser(client, fixture.users.vendorManager);
|
||||
await upsertUser(client, fixture.users.staffAna);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
@@ -64,13 +65,15 @@ async function main() {
|
||||
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)
|
||||
($1, $4, 'ACTIVE', 'manager', '{"persona":"vendor_manager"}'::jsonb),
|
||||
($1, $5, 'ACTIVE', 'member', '{"persona":"staff"}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.tenant.id,
|
||||
fixture.users.businessOwner.id,
|
||||
fixture.users.operationsManager.id,
|
||||
fixture.users.vendorManager.id,
|
||||
fixture.users.staffAna.id,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -134,6 +137,14 @@ async function main() {
|
||||
[fixture.tenant.id, fixture.vendor.id, fixture.users.vendorManager.id]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO cost_centers (id, tenant_id, business_id, code, name, status, metadata)
|
||||
VALUES ($1, $2, $3, 'CAFE_OPS', $4, 'ACTIVE', '{"seeded":true}'::jsonb)
|
||||
`,
|
||||
[fixture.costCenters.cafeOps.id, fixture.tenant.id, fixture.business.id, fixture.costCenters.cafeOps.name]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO roles_catalog (id, tenant_id, code, name, status, metadata)
|
||||
@@ -158,16 +169,37 @@ async function main() {
|
||||
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)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'ACTIVE', $7, 'COMPLETED', 4.50, 1, $8::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.staff.ana.id,
|
||||
fixture.tenant.id,
|
||||
fixture.users.staffAna.id,
|
||||
fixture.staff.ana.fullName,
|
||||
fixture.staff.ana.email,
|
||||
fixture.staff.ana.phone,
|
||||
fixture.staff.ana.primaryRole,
|
||||
JSON.stringify({ favoriteCandidate: true, seeded: true }),
|
||||
JSON.stringify({
|
||||
favoriteCandidate: true,
|
||||
seeded: true,
|
||||
firstName: 'Ana',
|
||||
lastName: 'Barista',
|
||||
bio: 'Experienced barista and event staffing professional.',
|
||||
preferredLocations: [
|
||||
{
|
||||
city: 'Mountain View',
|
||||
latitude: fixture.clockPoint.latitude,
|
||||
longitude: fixture.clockPoint.longitude,
|
||||
},
|
||||
],
|
||||
maxDistanceMiles: 20,
|
||||
industries: ['CATERING', 'CAFE'],
|
||||
skills: ['BARISTA', 'CUSTOMER_SERVICE'],
|
||||
emergencyContact: {
|
||||
name: 'Maria Barista',
|
||||
phone: '+15550007777',
|
||||
},
|
||||
}),
|
||||
]
|
||||
);
|
||||
|
||||
@@ -196,21 +228,63 @@ async function main() {
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO clock_points (
|
||||
id, tenant_id, business_id, label, address, latitude, longitude, geofence_radius_meters, nfc_tag_uid, status, metadata
|
||||
INSERT INTO staff_availability (
|
||||
id, tenant_id, staff_id, day_of_week, availability_status, time_slots, metadata
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'ACTIVE', '{}'::jsonb)
|
||||
VALUES
|
||||
($1, $3, $4, 1, 'PARTIAL', '[{"start":"08:00","end":"18:00"}]'::jsonb, '{"seeded":true}'::jsonb),
|
||||
($2, $3, $4, 5, 'PARTIAL', '[{"start":"09:00","end":"17:00"}]'::jsonb, '{"seeded":true}'::jsonb)
|
||||
`,
|
||||
[fixture.availability.monday.id, fixture.availability.friday.id, fixture.tenant.id, fixture.staff.ana.id]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO staff_benefits (
|
||||
id, tenant_id, staff_id, benefit_type, title, status, tracked_hours, target_hours, metadata
|
||||
)
|
||||
VALUES ($1, $2, $3, 'COMMUTER', $4, 'ACTIVE', 32, 40, '{"description":"Commuter stipend unlocked after 40 hours"}'::jsonb)
|
||||
`,
|
||||
[fixture.benefits.commuter.id, fixture.tenant.id, fixture.staff.ana.id, fixture.benefits.commuter.title]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO clock_points (
|
||||
id, tenant_id, business_id, cost_center_id, label, address, latitude, longitude,
|
||||
geofence_radius_meters, nfc_tag_uid, status, metadata
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'ACTIVE', $11::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.clockPoint.id,
|
||||
fixture.tenant.id,
|
||||
fixture.business.id,
|
||||
fixture.costCenters.cafeOps.id,
|
||||
fixture.clockPoint.label,
|
||||
fixture.clockPoint.address,
|
||||
fixture.clockPoint.latitude,
|
||||
fixture.clockPoint.longitude,
|
||||
fixture.clockPoint.geofenceRadiusMeters,
|
||||
fixture.clockPoint.nfcTagUid,
|
||||
JSON.stringify({ city: 'Mountain View', state: 'CA', zipCode: '94043', seeded: true }),
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO hub_managers (id, tenant_id, hub_id, business_membership_id)
|
||||
SELECT $1, $2, $3, bm.id
|
||||
FROM business_memberships bm
|
||||
WHERE bm.business_id = $4
|
||||
AND bm.user_id = $5
|
||||
`,
|
||||
[
|
||||
fixture.hubManagers.opsLead.id,
|
||||
fixture.tenant.id,
|
||||
fixture.clockPoint.id,
|
||||
fixture.business.id,
|
||||
fixture.users.operationsManager.id,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -221,8 +295,8 @@ async function main() {
|
||||
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)
|
||||
($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","orderType":"ONE_TIME"}'::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","orderType":"ONE_TIME"}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.orders.open.id,
|
||||
@@ -411,15 +485,30 @@ async function main() {
|
||||
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)
|
||||
VALUES
|
||||
($1, $2, 'CERTIFICATION', $3, $6, '{"seeded":true}'::jsonb),
|
||||
($4, $2, 'ATTIRE', $5, $6, '{"seeded":true}'::jsonb),
|
||||
($7, $2, 'TAX_FORM', $8, $6, '{"seeded":true}'::jsonb)
|
||||
`,
|
||||
[fixture.documents.foodSafety.id, fixture.tenant.id, fixture.documents.foodSafety.name, fixture.roles.barista.code]
|
||||
[
|
||||
fixture.documents.foodSafety.id,
|
||||
fixture.tenant.id,
|
||||
fixture.documents.foodSafety.name,
|
||||
fixture.documents.attireBlackShirt.id,
|
||||
fixture.documents.attireBlackShirt.name,
|
||||
fixture.roles.barista.code,
|
||||
fixture.documents.taxFormW9.id,
|
||||
fixture.documents.taxFormW9.name,
|
||||
]
|
||||
);
|
||||
|
||||
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)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, 'VERIFIED', $6, '{"seeded":true}'::jsonb),
|
||||
($7, $2, $3, $8, $9, 'VERIFIED', NULL, '{"seeded":true}'::jsonb),
|
||||
($10, $2, $3, $11, $12, 'VERIFIED', NULL, '{"seeded":true}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.staffDocuments.foodSafety.id,
|
||||
@@ -428,6 +517,12 @@ async function main() {
|
||||
fixture.documents.foodSafety.id,
|
||||
`gs://krow-workforce-dev-v2-private/uploads/${fixture.staff.ana.id}/food-handler-card.pdf`,
|
||||
hoursFromNow(24 * 180),
|
||||
fixture.staffDocuments.attireBlackShirt.id,
|
||||
fixture.documents.attireBlackShirt.id,
|
||||
`gs://krow-workforce-dev-v2-private/uploads/${fixture.staff.ana.id}/black-shirt.jpg`,
|
||||
fixture.staffDocuments.taxFormW9.id,
|
||||
fixture.documents.taxFormW9.id,
|
||||
`gs://krow-workforce-dev-v2-private/uploads/${fixture.staff.ana.id}/w9-form.pdf`,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -472,8 +567,8 @@ async function main() {
|
||||
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)
|
||||
($1, $3, 'BUSINESS', $4, NULL, NULL, 'stripe', 'ba_business_demo', '6789', TRUE, '{"seeded":true,"accountType":"CHECKING","routingNumberMasked":"*****0001"}'::jsonb),
|
||||
($2, $3, 'STAFF', NULL, NULL, $5, 'stripe', 'ba_staff_demo', '4321', TRUE, '{"seeded":true,"accountType":"CHECKING","routingNumberMasked":"*****0002"}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.accounts.businessPrimary.id,
|
||||
@@ -490,7 +585,7 @@ async function main() {
|
||||
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)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'PENDING_REVIEW', 'USD', 15250, 700, 15950, $7, '{"seeded":true,"savingsCents":1250}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.invoices.completed.id,
|
||||
@@ -573,6 +668,7 @@ async function main() {
|
||||
businessId: fixture.business.id,
|
||||
vendorId: fixture.vendor.id,
|
||||
staffId: fixture.staff.ana.id,
|
||||
staffUserId: fixture.users.staffAna.id,
|
||||
workforceId: fixture.workforce.ana.id,
|
||||
openOrderId: fixture.orders.open.id,
|
||||
openShiftId: fixture.shifts.open.id,
|
||||
|
||||
@@ -20,6 +20,11 @@ export const V2DemoFixture = {
|
||||
email: 'vendor+v2@krowd.com',
|
||||
displayName: 'Vendor Manager',
|
||||
},
|
||||
staffAna: {
|
||||
id: process.env.V2_DEMO_STAFF_UID || 'demo-staff-ana',
|
||||
email: process.env.V2_DEMO_STAFF_EMAIL || 'ana.barista+v2@krowd.com',
|
||||
displayName: 'Ana Barista',
|
||||
},
|
||||
},
|
||||
business: {
|
||||
id: '14f4fcfb-f21f-4ba9-9328-90f794a56001',
|
||||
@@ -31,6 +36,12 @@ export const V2DemoFixture = {
|
||||
slug: 'legendary-pool-a',
|
||||
name: 'Legendary Staffing Pool A',
|
||||
},
|
||||
costCenters: {
|
||||
cafeOps: {
|
||||
id: '31db54dd-9b32-4504-9056-9c71a9f73001',
|
||||
name: 'Cafe Operations',
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
barista: {
|
||||
id: '67c5010e-85f0-4f6b-99b7-167c9afdf001',
|
||||
@@ -67,6 +78,25 @@ export const V2DemoFixture = {
|
||||
geofenceRadiusMeters: 120,
|
||||
nfcTagUid: 'NFC-DEMO-ANA-001',
|
||||
},
|
||||
hubManagers: {
|
||||
opsLead: {
|
||||
id: '3f2dfd17-e6b4-4fe4-9fea-3c91c7ca8001',
|
||||
},
|
||||
},
|
||||
availability: {
|
||||
monday: {
|
||||
id: '887bc357-c3e0-4b2c-a174-bf27d6902001',
|
||||
},
|
||||
friday: {
|
||||
id: '887bc357-c3e0-4b2c-a174-bf27d6902002',
|
||||
},
|
||||
},
|
||||
benefits: {
|
||||
commuter: {
|
||||
id: 'dbd28438-66b0-452f-a5fc-dd0f3ea61001',
|
||||
title: 'Commuter Support',
|
||||
},
|
||||
},
|
||||
orders: {
|
||||
open: {
|
||||
id: 'b6132d7a-45c3-4879-b349-46b2fd518001',
|
||||
@@ -140,11 +170,25 @@ export const V2DemoFixture = {
|
||||
id: 'e6fd0183-34d9-4c23-9a9a-bf98da995001',
|
||||
name: 'Food Handler Card',
|
||||
},
|
||||
attireBlackShirt: {
|
||||
id: 'e6fd0183-34d9-4c23-9a9a-bf98da995002',
|
||||
name: 'Black Shirt',
|
||||
},
|
||||
taxFormW9: {
|
||||
id: 'e6fd0183-34d9-4c23-9a9a-bf98da995003',
|
||||
name: 'W-9 Tax Form',
|
||||
},
|
||||
},
|
||||
staffDocuments: {
|
||||
foodSafety: {
|
||||
id: '4b157236-a4b0-4c44-b199-7d4ea1f95001',
|
||||
},
|
||||
attireBlackShirt: {
|
||||
id: '4b157236-a4b0-4c44-b199-7d4ea1f95002',
|
||||
},
|
||||
taxFormW9: {
|
||||
id: '4b157236-a4b0-4c44-b199-7d4ea1f95003',
|
||||
},
|
||||
},
|
||||
certificates: {
|
||||
foodSafety: {
|
||||
|
||||
64
backend/command-api/sql/v2/002_v2_mobile_support.sql
Normal file
64
backend/command-api/sql/v2/002_v2_mobile_support.sql
Normal file
@@ -0,0 +1,64 @@
|
||||
CREATE TABLE IF NOT EXISTS cost_centers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
business_id UUID NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
|
||||
code TEXT,
|
||||
name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'ACTIVE'
|
||||
CHECK (status IN ('ACTIVE', 'INACTIVE', 'ARCHIVED')),
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_cost_centers_business_name
|
||||
ON cost_centers (business_id, name);
|
||||
|
||||
ALTER TABLE clock_points
|
||||
ADD COLUMN IF NOT EXISTS cost_center_id UUID REFERENCES cost_centers(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hub_managers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
hub_id UUID NOT NULL REFERENCES clock_points(id) ON DELETE CASCADE,
|
||||
business_membership_id UUID NOT NULL REFERENCES business_memberships(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_hub_managers_hub_membership
|
||||
ON hub_managers (hub_id, business_membership_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS staff_availability (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
staff_id UUID NOT NULL REFERENCES staffs(id) ON DELETE CASCADE,
|
||||
day_of_week SMALLINT NOT NULL CHECK (day_of_week BETWEEN 0 AND 6),
|
||||
availability_status TEXT NOT NULL DEFAULT 'UNAVAILABLE'
|
||||
CHECK (availability_status IN ('AVAILABLE', 'UNAVAILABLE', 'PARTIAL')),
|
||||
time_slots JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_staff_availability_staff_day
|
||||
ON staff_availability (staff_id, day_of_week);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS staff_benefits (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
staff_id UUID NOT NULL REFERENCES staffs(id) ON DELETE CASCADE,
|
||||
benefit_type TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'ACTIVE'
|
||||
CHECK (status IN ('ACTIVE', 'INACTIVE', 'PENDING')),
|
||||
tracked_hours INTEGER NOT NULL DEFAULT 0 CHECK (tracked_hours >= 0),
|
||||
target_hours INTEGER NOT NULL DEFAULT 0 CHECK (target_hours >= 0),
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_staff_benefits_staff_type
|
||||
ON staff_benefits (staff_id, benefit_type);
|
||||
Reference in New Issue
Block a user