feat(api): complete unified v2 mobile surface
This commit is contained in:
233
backend/command-api/test/mobile-routes.test.js
Normal file
233
backend/command-api/test/mobile-routes.test.js
Normal file
@@ -0,0 +1,233 @@
|
||||
import test, { beforeEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import request from 'supertest';
|
||||
import { createApp } from '../src/app.js';
|
||||
import { __resetIdempotencyStoreForTests } from '../src/services/idempotency-store.js';
|
||||
|
||||
process.env.AUTH_BYPASS = 'true';
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.IDEMPOTENCY_STORE = 'memory';
|
||||
delete process.env.IDEMPOTENCY_DATABASE_URL;
|
||||
delete process.env.DATABASE_URL;
|
||||
__resetIdempotencyStoreForTests();
|
||||
});
|
||||
|
||||
function createMobileHandlers() {
|
||||
return {
|
||||
createClientOneTimeOrder: async (_actor, payload) => ({
|
||||
orderId: 'order-1',
|
||||
orderType: 'ONE_TIME',
|
||||
eventName: payload.eventName,
|
||||
}),
|
||||
createClientRecurringOrder: async (_actor, payload) => ({
|
||||
orderId: 'order-2',
|
||||
orderType: 'RECURRING',
|
||||
recurrenceDays: payload.recurrenceDays,
|
||||
}),
|
||||
createClientPermanentOrder: async (_actor, payload) => ({
|
||||
orderId: 'order-3',
|
||||
orderType: 'PERMANENT',
|
||||
horizonDays: payload.horizonDays || 28,
|
||||
}),
|
||||
createEditedOrderCopy: async (_actor, payload) => ({
|
||||
sourceOrderId: payload.orderId,
|
||||
orderId: 'order-4',
|
||||
cloned: true,
|
||||
}),
|
||||
cancelClientOrder: async (_actor, payload) => ({
|
||||
orderId: payload.orderId,
|
||||
status: 'CANCELLED',
|
||||
}),
|
||||
createHub: async (_actor, payload) => ({
|
||||
hubId: 'hub-1',
|
||||
name: payload.name,
|
||||
costCenterId: payload.costCenterId,
|
||||
}),
|
||||
approveInvoice: async (_actor, payload) => ({
|
||||
invoiceId: payload.invoiceId,
|
||||
status: 'APPROVED',
|
||||
}),
|
||||
applyForShift: async (_actor, payload) => ({
|
||||
shiftId: payload.shiftId,
|
||||
status: 'APPLIED',
|
||||
}),
|
||||
staffClockIn: async (_actor, payload) => ({
|
||||
assignmentId: payload.assignmentId || 'assignment-1',
|
||||
status: 'CLOCK_IN',
|
||||
}),
|
||||
staffClockOut: async (_actor, payload) => ({
|
||||
assignmentId: payload.assignmentId || 'assignment-1',
|
||||
status: 'CLOCK_OUT',
|
||||
}),
|
||||
saveTaxFormDraft: async (_actor, payload) => ({
|
||||
formType: payload.formType,
|
||||
status: 'DRAFT',
|
||||
}),
|
||||
addStaffBankAccount: async (_actor, payload) => ({
|
||||
accountType: payload.accountType,
|
||||
last4: payload.accountNumber.slice(-4),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
test('POST /commands/client/orders/one-time forwards one-time order payload', async () => {
|
||||
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
|
||||
const res = await request(app)
|
||||
.post('/commands/client/orders/one-time')
|
||||
.set('Authorization', 'Bearer test-token')
|
||||
.set('Idempotency-Key', 'client-order-1')
|
||||
.send({
|
||||
hubId: '11111111-1111-4111-8111-111111111111',
|
||||
vendorId: '22222222-2222-4222-8222-222222222222',
|
||||
eventName: 'Google Cafe Coverage',
|
||||
orderDate: '2026-03-20',
|
||||
positions: [
|
||||
{
|
||||
roleId: '33333333-3333-4333-8333-333333333333',
|
||||
startTime: '09:00',
|
||||
endTime: '17:00',
|
||||
workerCount: 2,
|
||||
hourlyRateCents: 2800,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.body.orderId, 'order-1');
|
||||
assert.equal(res.body.orderType, 'ONE_TIME');
|
||||
assert.equal(res.body.eventName, 'Google Cafe Coverage');
|
||||
});
|
||||
|
||||
test('POST /commands/client/orders/:orderId/edit injects order id from params', async () => {
|
||||
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
|
||||
const res = await request(app)
|
||||
.post('/commands/client/orders/44444444-4444-4444-8444-444444444444/edit')
|
||||
.set('Authorization', 'Bearer test-token')
|
||||
.set('Idempotency-Key', 'client-order-edit-1')
|
||||
.send({
|
||||
eventName: 'Edited Order Copy',
|
||||
});
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.body.sourceOrderId, '44444444-4444-4444-8444-444444444444');
|
||||
assert.equal(res.body.cloned, true);
|
||||
});
|
||||
|
||||
test('POST /commands/client/hubs returns injected hub response', async () => {
|
||||
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
|
||||
const res = await request(app)
|
||||
.post('/commands/client/hubs')
|
||||
.set('Authorization', 'Bearer test-token')
|
||||
.set('Idempotency-Key', 'hub-create-1')
|
||||
.send({
|
||||
tenantId: '11111111-1111-4111-8111-111111111111',
|
||||
businessId: '22222222-2222-4222-8222-222222222222',
|
||||
name: 'Google North Hub',
|
||||
locationName: 'North Campus',
|
||||
timezone: 'America/Los_Angeles',
|
||||
latitude: 37.422,
|
||||
longitude: -122.084,
|
||||
geofenceRadiusMeters: 100,
|
||||
costCenterId: '44444444-4444-4444-8444-444444444444',
|
||||
});
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.body.hubId, 'hub-1');
|
||||
assert.equal(res.body.name, 'Google North Hub');
|
||||
});
|
||||
|
||||
test('POST /commands/client/billing/invoices/:invoiceId/approve injects invoice id from params', async () => {
|
||||
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
|
||||
const res = await request(app)
|
||||
.post('/commands/client/billing/invoices/55555555-5555-4555-8555-555555555555/approve')
|
||||
.set('Authorization', 'Bearer test-token')
|
||||
.set('Idempotency-Key', 'invoice-approve-1')
|
||||
.send({});
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.body.invoiceId, '55555555-5555-4555-8555-555555555555');
|
||||
assert.equal(res.body.status, 'APPROVED');
|
||||
});
|
||||
|
||||
test('POST /commands/staff/shifts/:shiftId/apply injects shift id from params', async () => {
|
||||
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
|
||||
const res = await request(app)
|
||||
.post('/commands/staff/shifts/66666666-6666-4666-8666-666666666666/apply')
|
||||
.set('Authorization', 'Bearer test-token')
|
||||
.set('Idempotency-Key', 'shift-apply-1')
|
||||
.send({
|
||||
note: 'Available tonight',
|
||||
});
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.body.shiftId, '66666666-6666-4666-8666-666666666666');
|
||||
assert.equal(res.body.status, 'APPLIED');
|
||||
});
|
||||
|
||||
test('POST /commands/staff/clock-in accepts shift-based payload', async () => {
|
||||
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
|
||||
const res = await request(app)
|
||||
.post('/commands/staff/clock-in')
|
||||
.set('Authorization', 'Bearer test-token')
|
||||
.set('Idempotency-Key', 'clock-in-1')
|
||||
.send({
|
||||
shiftId: '77777777-7777-4777-8777-777777777777',
|
||||
sourceType: 'GEO',
|
||||
latitude: 37.422,
|
||||
longitude: -122.084,
|
||||
});
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.body.status, 'CLOCK_IN');
|
||||
});
|
||||
|
||||
test('POST /commands/staff/clock-out accepts assignment-based payload', async () => {
|
||||
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
|
||||
const res = await request(app)
|
||||
.post('/commands/staff/clock-out')
|
||||
.set('Authorization', 'Bearer test-token')
|
||||
.set('Idempotency-Key', 'clock-out-1')
|
||||
.send({
|
||||
assignmentId: '88888888-8888-4888-8888-888888888888',
|
||||
breakMinutes: 30,
|
||||
});
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.body.status, 'CLOCK_OUT');
|
||||
});
|
||||
|
||||
test('PUT /commands/staff/profile/tax-forms/:formType uppercases form type', async () => {
|
||||
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
|
||||
const res = await request(app)
|
||||
.put('/commands/staff/profile/tax-forms/w4')
|
||||
.set('Authorization', 'Bearer test-token')
|
||||
.set('Idempotency-Key', 'tax-form-1')
|
||||
.send({
|
||||
fields: {
|
||||
filingStatus: 'single',
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.body.formType, 'W4');
|
||||
assert.equal(res.body.status, 'DRAFT');
|
||||
});
|
||||
|
||||
test('POST /commands/staff/profile/bank-accounts uppercases account type', async () => {
|
||||
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
|
||||
const res = await request(app)
|
||||
.post('/commands/staff/profile/bank-accounts')
|
||||
.set('Authorization', 'Bearer test-token')
|
||||
.set('Idempotency-Key', 'bank-account-1')
|
||||
.send({
|
||||
bankName: 'Demo Credit Union',
|
||||
accountNumber: '1234567890',
|
||||
routingNumber: '021000021',
|
||||
accountType: 'checking',
|
||||
});
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.body.accountType, 'CHECKING');
|
||||
assert.equal(res.body.last4, '7890');
|
||||
});
|
||||
Reference in New Issue
Block a user