diff --git a/apps/delivery-api/src/config/env.ts b/apps/delivery-api/src/config/env.ts index 8a2a93a1..42424357 100644 --- a/apps/delivery-api/src/config/env.ts +++ b/apps/delivery-api/src/config/env.ts @@ -1,6 +1,3 @@ -import type { TokenEncryptionKey } from '../modules/security/token-encryption.js'; -import { loadTokenEncryptionKey } from '../modules/security/token-encryption.js'; - export type AppEnv = { nodeEnv: string; port: number; @@ -17,12 +14,6 @@ export function loadEnv(input: NodeJS.ProcessEnv = process.env): AppEnv { }; } -export function loadShopifyTokenEncryptionKey( - input: NodeJS.ProcessEnv = process.env -): TokenEncryptionKey { - return loadTokenEncryptionKey(input.SHOPIFY_TOKEN_ENCRYPTION_KEY); -} - function parsePort(value: string | undefined): number { if (value === undefined || value.trim() === '') { return DEFAULT_PORT; diff --git a/apps/delivery-api/src/modules/customer-email/customer-email.service.ts b/apps/delivery-api/src/modules/customer-email/customer-email.service.ts index c589b8a3..9f509f4c 100644 --- a/apps/delivery-api/src/modules/customer-email/customer-email.service.ts +++ b/apps/delivery-api/src/modules/customer-email/customer-email.service.ts @@ -1527,14 +1527,6 @@ function automaticSettingsVersion(settings: CustomerEmailSettings): string { return `v3:g${settings.globalVersion}:t${templateVersions}`; } -export function readCustomerEmailSettingsPayload(value: unknown): CustomerEmailSettings | null { - try { - return validateCustomerEmailSettingsPayload(value); - } catch { - return null; - } -} - export function readCustomerEmailGlobalSettingsPayload(value: unknown): { branding: Partial; expectedVersion: number; diff --git a/apps/delivery-api/src/modules/driver/driver-runtime-diagnostics.contract.ts b/apps/delivery-api/src/modules/driver/driver-runtime-diagnostics.contract.ts index 1fd66efa..53b80d75 100644 --- a/apps/delivery-api/src/modules/driver/driver-runtime-diagnostics.contract.ts +++ b/apps/delivery-api/src/modules/driver/driver-runtime-diagnostics.contract.ts @@ -421,7 +421,3 @@ export function parseDriverDiagnosticEnvelopeDetailed(value: unknown): ParsedDri rejectedRecords }; } - -export function parseDriverDiagnosticEnvelope(value: unknown): DriverDiagnosticEnvelope | null { - return parseDriverDiagnosticEnvelopeDetailed(value)?.envelope ?? null; -} diff --git a/apps/delivery-api/src/modules/driver/driver-token-verifier.ts b/apps/delivery-api/src/modules/driver/driver-token-verifier.ts index 81455f18..5ec5692a 100644 --- a/apps/delivery-api/src/modules/driver/driver-token-verifier.ts +++ b/apps/delivery-api/src/modules/driver/driver-token-verifier.ts @@ -1,7 +1,5 @@ import { createHmac, timingSafeEqual } from 'node:crypto'; -import { normalizeDriverCommerceDomain } from './driver-commerce-domain.js'; -const DRIVER_AUDIENCE = 'clever-delivery-driver'; const DRIVER_ACCOUNT_AUDIENCE = 'clever-driver-account'; const DRIVER_ROUTE_AUDIENCE = 'clever-delivery-driver-route'; const MIN_DRIVER_JWT_SECRET_CHARACTERS = 32; @@ -22,14 +20,6 @@ export type VerifiedDriverAccountToken = { tokenVersion: number; }; -export type VerifiedDriverToken = { - driverId: string; - issuedAt: Date; - shopDomain: string; - subject: string; - tokenVersion: number; -}; - export type VerifiedDriverRouteToken = { accountId: string; issuedAt: Date; @@ -43,14 +33,6 @@ export type VerifyDriverTokenOptions = { secret: string; }; -export type SignDriverTokenInput = { - driverId: string; - expiresInSeconds: number; - shopDomain: string; - subject: string; - tokenVersion?: number; -}; - export type SignDriverAccountTokenInput = { accountId: string; expiresInSeconds: number; @@ -116,35 +98,6 @@ export function verifyDriverAccountToken( }; } -export function verifyDriverToken( - token: string, - options: VerifyDriverTokenOptions -): VerifiedDriverToken { - const claims = verifyTokenEnvelope(token, options.secret); - const nowSeconds = Math.floor((options.now ?? new Date()).getTime() / 1000); - const audience = requireStringClaim(claims.aud, 'aud'); - const driverId = requireStringClaim(claims.driverId, 'driverId'); - const expiresAt = requireNumberClaim(claims.exp, 'exp'); - const issuedAtSeconds = requireNumberClaim(claims.iat, 'iat'); - const shopDomain = normalizeDriverCommerceDomain(requireStringClaim(claims.shopDomain, 'shopDomain')); - const subject = requireStringClaim(claims.sub, 'sub'); - const tokenVersion = readTokenVersionClaim(claims.tokenVersion); - - if (audience !== DRIVER_AUDIENCE) { - throw new Error('Driver token audience mismatch'); - } - - verifyTokenTimes(claims, expiresAt, nowSeconds); - - return { - driverId, - issuedAt: new Date(issuedAtSeconds * 1000), - shopDomain, - subject, - tokenVersion - }; -} - export function verifyDriverRouteToken( token: string, options: VerifyDriverTokenOptions @@ -185,19 +138,6 @@ export function signDriverAccountToken( }, input.expiresInSeconds, options); } -export function signDriverToken( - input: SignDriverTokenInput, - options: VerifyDriverTokenOptions -): SignDriverTokenResult { - return signToken({ - aud: DRIVER_AUDIENCE, - driverId: requireStringClaim(input.driverId, 'driverId'), - shopDomain: normalizeDriverCommerceDomain(input.shopDomain), - sub: requireStringClaim(input.subject, 'sub'), - tokenVersion: readTokenVersionClaim(input.tokenVersion) - }, input.expiresInSeconds, options); -} - export function signDriverRouteToken( input: SignDriverRouteTokenInput, options: VerifyDriverTokenOptions diff --git a/apps/delivery-api/src/modules/dsv/dsv-customer-account-invitations.service.ts b/apps/delivery-api/src/modules/dsv/dsv-customer-account-invitations.service.ts index 0d2f924a..ec9b595d 100644 --- a/apps/delivery-api/src/modules/dsv/dsv-customer-account-invitations.service.ts +++ b/apps/delivery-api/src/modules/dsv/dsv-customer-account-invitations.service.ts @@ -5,7 +5,6 @@ import { appScopedShopWhere } from '../shopify/shopify-app-scope.js'; import type { AdminStoreSettings } from '../commerce/admin-store-settings.service.js'; import { normalizeDsvOperationalSettings } from './dsv-operational-settings.js'; import type { DsvManualEmailService } from './dsv-manual-email.service.js'; -import { loadDsvWebPublicOrigin } from './dsv-web-public-origin.js'; export type DsvCustomerAccountInvitePurpose = 'SIGNUP' | 'PASSWORD_RESET'; export type DsvCustomerAccountApiStatus = 'INVITED' | 'ACTIVE' | 'DISABLED' | 'EXPIRED'; @@ -528,10 +527,6 @@ export class DsvCustomerAccountServiceError extends Error { } } -export function loadDsvCustomerAccountWebPublicOrigin(value: string | undefined): string | undefined { - return loadDsvWebPublicOrigin(value); -} - export function createCustomerSessionSubject(input: { accountId: string; activeSessionId: string }): string { if (!uuidPattern.test(input.accountId) || !uuidPattern.test(input.activeSessionId)) { throw new Error('Invalid DSV customer session subject'); diff --git a/apps/delivery-api/src/modules/dsv/dsv-driver-identity.ts b/apps/delivery-api/src/modules/dsv/dsv-driver-identity.ts index 8796c653..fc453ce9 100644 --- a/apps/delivery-api/src/modules/dsv/dsv-driver-identity.ts +++ b/apps/delivery-api/src/modules/dsv/dsv-driver-identity.ts @@ -1,7 +1,3 @@ -import { createHmac } from 'node:crypto'; - -const RESIDENT_NUMBER_FRONT_PATTERN = /^\d{7}$/u; - export function normalizeDsvDriverLoginId(value: string): string { return value.trim().toLowerCase(); } @@ -9,20 +5,3 @@ export function normalizeDsvDriverLoginId(value: string): string { export function normalizeDsvDriverPhone(value: string): string { return value.replace(/\D/gu, ''); } - -export function fingerprintResidentNumberFront( - residentNumberFront: string, - secret: string, -): string { - if (!RESIDENT_NUMBER_FRONT_PATTERN.test(residentNumberFront)) { - throw new Error('residentNumberFront must contain exactly seven digits'); - } - if (secret.length < 32) { - throw new Error('DSV driver identity secret must contain at least 32 characters'); - } - - return createHmac('sha256', secret) - .update('clever-dsv-driver-resident-front-v1\0', 'utf8') - .update(residentNumberFront, 'utf8') - .digest('hex'); -} diff --git a/apps/delivery-api/src/modules/dsv/dsv-principal.ts b/apps/delivery-api/src/modules/dsv/dsv-principal.ts index 27502605..39c904d6 100644 --- a/apps/delivery-api/src/modules/dsv/dsv-principal.ts +++ b/apps/delivery-api/src/modules/dsv/dsv-principal.ts @@ -171,24 +171,3 @@ export function requireDsvScopes(principal: DsvPrincipal, requiredScopes: readon if (requiredScopes.every((scope) => granted.has(scope))) return; throw new DsvForbiddenError({ principal, requiredScopes }); } - -export function requireCustomerDeliveryPrincipal(input: { - customerId?: string; - destinationId?: string; - principal: DsvPrincipal; -}): DsvCustomerUserPrincipal { - requireDsvScopes(input.principal, ['dsv:customer-deliveries:read']); - if (input.principal.principalType !== 'CUSTOMER_USER' || input.principal.customerId === '') { - throw new DsvForbiddenError({ - principal: input.principal, - requiredScopes: ['dsv:customer-deliveries:read'], - }); - } - if (input.customerId !== undefined && input.customerId !== input.principal.customerId) { - throw new DsvForbiddenError({ - principal: input.principal, - requiredScopes: ['dsv:customer-deliveries:read'], - }); - } - return input.principal; -} diff --git a/apps/delivery-api/src/modules/dsv/dsv-v1-read.dto.ts b/apps/delivery-api/src/modules/dsv/dsv-v1-read.dto.ts index 2359fe7b..b2cb6c70 100644 --- a/apps/delivery-api/src/modules/dsv/dsv-v1-read.dto.ts +++ b/apps/delivery-api/src/modules/dsv/dsv-v1-read.dto.ts @@ -24,31 +24,6 @@ export const dsvV1ErrorCodes = [ ] as const; export type DsvV1ErrorCode = typeof dsvV1ErrorCodes[number]; -export const dsvV1CustomerDeliveryRequiredFields = [ - 'sellerOrderId', - 'sellerOrderKey', - 'destinationId', - 'destinationDisplayName', - 'shippedBoxes', - 'deliveryStatus', - 'etaStatus', - 'eventSummary', - 'proofStatus', -] as const; - -export const dsvV1SellerOrderSummaryRequiredFields = [ - 'sellerOrderId', - 'sellerOrderKey', - 'deliveryStopId', - 'customerId', - 'destinationId', - 'assignmentStatus', - 'etaStatus', - 'eventSummary', -] as const; - -export const dsvV1SessionRequiredFields = ['csrfToken', 'principalType', 'shopId', 'scopes'] as const; - export type DsvV1PrincipalType = 'DSV_ADMIN' | 'CUSTOMER_USER' | 'DRIVER' | 'IMPORT_WORKER' | 'DEVICE'; export type DsvV1SuccessEnvelope = { diff --git a/apps/delivery-api/src/modules/geocoding/geocoding.service.ts b/apps/delivery-api/src/modules/geocoding/geocoding.service.ts index 3de8d4a5..130b8449 100644 --- a/apps/delivery-api/src/modules/geocoding/geocoding.service.ts +++ b/apps/delivery-api/src/modules/geocoding/geocoding.service.ts @@ -368,12 +368,6 @@ export function normalizeAddress(address: GeocodingAddress): string | null { return normalizeAddressParts(addressParts(address)); } -export function normalizeAddressQueries(address: GeocodingAddress): string[] { - return buildGeocodingQueries(address).flatMap((query) => - query.kind === 'freeform' ? [query.q] : [], - ); -} - export function buildGeocodingQueries(address: GeocodingAddress): GeocodingQuery[] { const full = normalizeAddress(address); if (full === null) return []; diff --git a/apps/delivery-api/src/modules/notifications/admin-notification.dependencies.ts b/apps/delivery-api/src/modules/notifications/admin-notification.dependencies.ts index 6b25fbe7..8fc1acb7 100644 --- a/apps/delivery-api/src/modules/notifications/admin-notification.dependencies.ts +++ b/apps/delivery-api/src/modules/notifications/admin-notification.dependencies.ts @@ -41,9 +41,3 @@ export function createAdminNotificationRuntime(input: { start: () => bridge?.start() ?? Promise.resolve(), }; } - -export function createAdminNotificationService(input: { - prisma: PrismaClient; -}): AdminNotificationService { - return createAdminNotificationRuntime(input).service; -} diff --git a/apps/delivery-api/src/modules/route-grouping/driver-push.provider.ts b/apps/delivery-api/src/modules/route-grouping/driver-push.provider.ts index 631211e2..dac31d8c 100644 --- a/apps/delivery-api/src/modules/route-grouping/driver-push.provider.ts +++ b/apps/delivery-api/src/modules/route-grouping/driver-push.provider.ts @@ -23,19 +23,6 @@ export type DriverPushProvider = { sendRouteNotification(message: DriverRoutePushMessage): Promise; }; -export class FakeDriverPushProvider implements DriverPushProvider { - readonly providerName = 'fake'; - readonly sentMessages: DriverRoutePushMessage[] = []; - - sendRouteNotification(message: DriverRoutePushMessage): Promise { - this.sentMessages.push(message); - return Promise.resolve({ - providerMessageId: `fake:${message.routeGroupingId ?? 'standalone'}:${message.childVersion ?? message.publicationVersion ?? 'current'}:${message.routePlanId}`, - status: 'SENT' - }); - } -} - export class DisabledDriverPushProvider implements DriverPushProvider { readonly providerName = 'disabled'; diff --git a/apps/delivery-api/src/modules/route-grouping/route-grouping.geometry.ts b/apps/delivery-api/src/modules/route-grouping/route-grouping.geometry.ts index 22c3c056..9bf43f39 100644 --- a/apps/delivery-api/src/modules/route-grouping/route-grouping.geometry.ts +++ b/apps/delivery-api/src/modules/route-grouping/route-grouping.geometry.ts @@ -70,19 +70,3 @@ export function coordinatesFromGeoJsonPolygon(value: unknown): PolygonRing { }) .filter((entry): entry is Coordinate => entry !== null); } - -export function geoJsonPolygonFromCoordinates(vertices: PolygonRing): { type: 'Polygon'; coordinates: number[][][] } { - const ring = vertices.map((vertex) => [vertex.longitude, vertex.latitude]); - const first = ring[0]; - const last = ring[ring.length - 1]; - const closed = - first !== undefined && - last !== undefined && - first[0] === last[0] && - first[1] === last[1] - ? ring - : first === undefined - ? ring - : [...ring, first]; - return { type: 'Polygon', coordinates: [closed] }; -} diff --git a/apps/delivery-api/src/modules/route-grouping/route-grouping.service.ts b/apps/delivery-api/src/modules/route-grouping/route-grouping.service.ts index 54992d35..69551883 100644 --- a/apps/delivery-api/src/modules/route-grouping/route-grouping.service.ts +++ b/apps/delivery-api/src/modules/route-grouping/route-grouping.service.ts @@ -169,31 +169,6 @@ type ChildRouteGeometrySnapshot = { routeStopPoints: RoutePlanRouteStopPoint[]; }; -export function currentRouteBindingAuthorityState( - childVersionId: string, - snapshotOrderIds: string[], - assignments: Array<{ orderId: string; order: { currentRouteVersionId: string | null } }> -): 'EXACT' | 'LEGACY_UNBOUND' | 'MISMATCH' { - const snapshotOrderIdSet = new Set(snapshotOrderIds); - if (snapshotOrderIdSet.size !== snapshotOrderIds.length) return 'MISMATCH'; - - const snapshotAssignments = assignments.filter(({ orderId }) => snapshotOrderIdSet.has(orderId)); - if (snapshotAssignments.length !== snapshotOrderIds.length) return 'MISMATCH'; - - const boundOrderIds = assignments - .filter(({ order }) => order.currentRouteVersionId === childVersionId) - .map(({ orderId }) => orderId) - .sort(); - const sortedSnapshotOrderIds = [...snapshotOrderIds].sort(); - const exactBinding = sortedSnapshotOrderIds.length === boundOrderIds.length - && sortedSnapshotOrderIds.every((orderId, index) => orderId === boundOrderIds[index]); - if (exactBinding) return 'EXACT'; - - const entirelyUnbound = boundOrderIds.length === 0 - && snapshotAssignments.every(({ order }) => order.currentRouteVersionId === null); - return entirelyUnbound ? 'LEGACY_UNBOUND' : 'MISMATCH'; -} - type OptimizedDraftRoute = { assignments: LoadedAssignment[]; routeResult: RoutePlanRouteResult; diff --git a/apps/delivery-api/src/modules/route-ops/route-scope-config.ts b/apps/delivery-api/src/modules/route-ops/route-scope-config.ts index bebe1132..1f1c077e 100644 --- a/apps/delivery-api/src/modules/route-ops/route-scope-config.ts +++ b/apps/delivery-api/src/modules/route-ops/route-scope-config.ts @@ -165,14 +165,6 @@ export function isActiveDeliverySession(config: RouteScopeConfigDto, value: unkn return isActiveRouteScopeValue(config.deliverySessions, value); } -export function buildActiveServiceTypes(config: RouteScopeConfigDto): RouteScopeValueDto[] { - return config.serviceTypes.filter((item) => item.enabled); -} - -export function buildActiveDeliverySessions(config: RouteScopeConfigDto): RouteScopeValueDto[] { - return config.deliverySessions.filter((item) => item.enabled); -} - function normalizeRouteScopeValues(input: { builtIns: readonly RouteScopeValueDto[]; raw: unknown; diff --git a/apps/delivery-api/src/modules/route-plans/route-execution-ownership.ts b/apps/delivery-api/src/modules/route-plans/route-execution-ownership.ts index a5cf0425..84298175 100644 --- a/apps/delivery-api/src/modules/route-plans/route-execution-ownership.ts +++ b/apps/delivery-api/src/modules/route-plans/route-execution-ownership.ts @@ -2,20 +2,6 @@ import { Prisma } from '@prisma/client'; type RouteExecutionOwnershipTx = { $queryRaw(query: TemplateStringsArray | Prisma.Sql, ...values: unknown[]): Promise; - routePlanStop: { - findFirst(args: { - select: { deliveryStopId: true; routePlanId: true }; - where: { - deliveryStopId: { in: string[] }; - routePlanId: { not: string }; - routePlan: { shopId: string; status: 'IN_PROGRESS' }; - }; - }): Promise<{ deliveryStopId: string; routePlanId: string } | null>; - findMany(args: { - select: { deliveryStopId: true }; - where: { routePlanId: string }; - }): Promise>; - }; }; export class RouteExecutionConflictError extends Error { @@ -40,7 +26,7 @@ export type RouteDispatchConflict = { }; export async function lockRouteExecutionStops( - tx: Pick, + tx: RouteExecutionOwnershipTx, deliveryStopIds: string[] ): Promise { for (const stopId of [...new Set(deliveryStopIds)].sort()) { @@ -51,7 +37,7 @@ export async function lockRouteExecutionStops( // Planning membership is many-to-many. Dispatch reserves execution, separately // from the order's legacy primary-route projection. No stop outcome is reset. export async function assertRouteDispatchOwnership( - tx: Pick, + tx: RouteExecutionOwnershipTx, input: { deliveryStopIds: string[]; routePlanId: string; shopId: string } ): Promise { const stopIds = [...new Set(input.deliveryStopIds)].sort(); @@ -100,7 +86,7 @@ export function withoutDispatchReservation(value: unknown): Prisma.InputJsonObje } export async function claimRouteExecutionProjection( - tx: Pick, + tx: RouteExecutionOwnershipTx, input: { routePlanId: string; shopId: string } ): Promise { // This singular field remains a compatibility projection for order/DSV @@ -117,51 +103,3 @@ export async function claimRouteExecutionProjection( AND s."routePlanId" = ${input.routePlanId}::uuid AND o."shopId" = ${input.shopId}::uuid `); } - -export async function assertRouteExecutionOwnership( - tx: RouteExecutionOwnershipTx, - input: { - createConflictError?: (conflict: { deliveryStopId: string; routePlanId: string }) => Error; - deliveryStopIds: string[]; - routePlanId: string; - shopId: string; - } -): Promise { - const deliveryStopIds = [...new Set(input.deliveryStopIds)].sort((left, right) => left.localeCompare(right)); - if (deliveryStopIds.length === 0) return; - - await lockRouteExecutionStops(tx, deliveryStopIds); - - const conflict = await tx.routePlanStop.findFirst({ - select: { deliveryStopId: true, routePlanId: true }, - where: { - deliveryStopId: { in: deliveryStopIds }, - routePlanId: { not: input.routePlanId }, - routePlan: { - shopId: input.shopId, - status: 'IN_PROGRESS' - } - } - }); - if (conflict !== null) { - throw input.createConflictError?.(conflict) ?? new RouteExecutionConflictError(conflict.routePlanId, conflict.deliveryStopId); - } -} - -export async function assertRoutePlanExecutionOwnership( - tx: RouteExecutionOwnershipTx, - input: { - createConflictError?: (conflict: { deliveryStopId: string; routePlanId: string }) => Error; - routePlanId: string; - shopId: string; - } -): Promise { - const stops = await tx.routePlanStop.findMany({ - select: { deliveryStopId: true }, - where: { routePlanId: input.routePlanId } - }); - await assertRouteExecutionOwnership(tx, { - ...input, - deliveryStopIds: stops.map((stop) => stop.deliveryStopId) - }); -} diff --git a/apps/delivery-api/src/modules/route-tracking/route-tracking.geometry.ts b/apps/delivery-api/src/modules/route-tracking/route-tracking.geometry.ts index 935d5bb0..2f24fb93 100644 --- a/apps/delivery-api/src/modules/route-tracking/route-tracking.geometry.ts +++ b/apps/delivery-api/src/modules/route-tracking/route-tracking.geometry.ts @@ -129,35 +129,6 @@ export async function persistRouteTrackingGeometryPosition( return document; } -export async function rebuildRouteTrackingGeometryForRoute( - prisma: RouteTrackingGeometryPrismaClient, - routePlanId: string, - now = new Date() -): Promise { - await prisma.$queryRaw( - Prisma.sql`SELECT TRUE AS "locked" FROM pg_advisory_xact_lock(hashtextextended(${routePlanId}, 0))` - ); - const retentionCutoff = new Date( - now.getTime() - ROUTE_TRACKING_GEOMETRY_RETENTION_DAYS * 24 * 60 * 60 * 1000 - ); - const eventWindow = await loadRouteTrackingEventWindow(prisma, routePlanId); - const document = buildRouteTrackingGeometryDocument( - await loadRouteTrackingPositions(prisma, routePlanId, retentionCutoff, eventWindow) - ); - if (document.coordinates.length === 0) return document; - const write = createRouteTrackingGeometryWrite(routePlanId, document); - await prisma.routeTrackingGeometry.upsert({ create: write, update: write, where: { routePlanId } }); - const lastSample = document.samples.at(-1)!; - if (prisma.routeTrackingRoadMatchJob !== undefined) { - await enqueueRouteTrackingRoadMatch(prisma as RouteTrackingGeometryPrismaClient & { routeTrackingRoadMatchJob: Prisma.TransactionClient['routeTrackingRoadMatchJob'] }, { - lastInputOccurredAt: new Date(lastSample.occurredAt), - routePlanId, - sourcePointCount: document.sourcePointCount, - }); - } - return document; -} - export function buildRouteTrackingGeometryDocument( positions: RouteTrackingGeometryPositionInput[] ): RouteTrackingGeometryDocumentV1 { diff --git a/apps/delivery-api/src/modules/route-tracking/route-tracking.road-match.ts b/apps/delivery-api/src/modules/route-tracking/route-tracking.road-match.ts index 056ca85b..034fdd40 100644 --- a/apps/delivery-api/src/modules/route-tracking/route-tracking.road-match.ts +++ b/apps/delivery-api/src/modules/route-tracking/route-tracking.road-match.ts @@ -9,7 +9,6 @@ import { } from '../route-plans/route-engine-coverage.js'; import { ROUTE_TRACKING_V1_POLICY } from './route-tracking.policy.js'; import { - readRouteTrackingGeometryDocument, type RouteTrackingGeometryDocumentV1, type RouteTrackingGeometryPositionInput, type RouteTrackingGeometryRecord, @@ -497,16 +496,6 @@ export function buildRouteTrackingRoadMatchedPath( }; } -export function shouldRefreshRouteTrackingRoadMatchedPath(record: RouteTrackingGeometryRecord | null | undefined): boolean { - if (record === null || record === undefined) return false; - const document = readRouteTrackingGeometryDocument(record); - if (document.coordinates.length < 2) return false; - if (record.roadMatchedSchemaVersion !== ROUTE_TRACKING_ROAD_MATCH_CACHE_VERSION) return true; - if ((record.roadMatchedSourcePointCount ?? 0) < record.sourcePointCount) return true; - if (record.roadMatchedLastInputOccurredAt === null || record.roadMatchedLastInputOccurredAt === undefined) return true; - return false; -} - export function buildRouteTrackingRoadMatchCacheWrite(path: RouteTrackingRoadMatchedPathV1): { roadMatchedCoverage: RouteEngineCoverage; roadMatchedGeometry: Prisma.JsonObject | typeof Prisma.JsonNull; diff --git a/apps/delivery-api/src/modules/shopify/order-sync.dependencies.ts b/apps/delivery-api/src/modules/shopify/order-sync.dependencies.ts index 6b160278..acdeb267 100644 --- a/apps/delivery-api/src/modules/shopify/order-sync.dependencies.ts +++ b/apps/delivery-api/src/modules/shopify/order-sync.dependencies.ts @@ -31,14 +31,6 @@ export type AdminOrdersRuntimeEnv = ShopifyAppCredentialsEnv & Partial>; -export function loadAdminOrdersDependencies(input: { - adminNotificationService?: AdminNotificationServiceApi | undefined; - env: AdminOrdersRuntimeEnv; - prisma: PrismaClient; -}): AdminOrdersDependencies | undefined { - return loadAdminOrdersRuntime(input)?.dependencies; -} - export function loadAdminOrdersRuntime(input: { adminNotificationService?: AdminNotificationServiceApi | undefined; env: AdminOrdersRuntimeEnv; diff --git a/apps/delivery-api/src/modules/shopify/webhook.dependencies.ts b/apps/delivery-api/src/modules/shopify/webhook.dependencies.ts index e63f73df..12ccedc5 100644 --- a/apps/delivery-api/src/modules/shopify/webhook.dependencies.ts +++ b/apps/delivery-api/src/modules/shopify/webhook.dependencies.ts @@ -27,12 +27,6 @@ type LoadShopifyWebhookDependenciesInput = { prisma: PrismaClient; }; -export function loadShopifyWebhookDependencies( - input: LoadShopifyWebhookDependenciesInput -): ShopifyWebhookDependencies | undefined { - return loadShopifyWebhookRuntime(input)?.dependencies; -} - export function loadShopifyWebhookRuntime( input: LoadShopifyWebhookDependenciesInput & { logger?: Pick } ): { dependencies: ShopifyWebhookDependencies; worker?: ShopifyOrderWebhookWorker } | undefined { diff --git a/apps/delivery-api/src/modules/woocommerce/woocommerce-order-sync.service.ts b/apps/delivery-api/src/modules/woocommerce/woocommerce-order-sync.service.ts index 6002299a..3b18bb54 100644 --- a/apps/delivery-api/src/modules/woocommerce/woocommerce-order-sync.service.ts +++ b/apps/delivery-api/src/modules/woocommerce/woocommerce-order-sync.service.ts @@ -54,14 +54,6 @@ export type WooCommerceSyncSingleOrderInput = { sourceOrderId: number | string; }; -export type WooCommerceSyncTier = 'hot' | 'cold'; - -export type WooCommerceSyncTierClassification = { - reason: 'today_or_future_delivery' | 'past_delivery' | 'missing_delivery_date'; - review: boolean; - tier: WooCommerceSyncTier; -}; - type Repository = { findCanonicalOrderById?(input: { orderId: string; shopDomain: string; shopId?: string | undefined }): Promise; listCanonicalOrdersBySourceIdentity(input: ListCanonicalOrdersBySourceIdentityInput): Promise; @@ -403,19 +395,6 @@ export function applyWooModifiedAfterOverlap( return new Date(modifiedAfter.getTime() - Math.floor(overlapWindowMs)); } -export function classifyWooCommerceSyncTier(input: { - deliveryDate: string | null; - today: string; -}): WooCommerceSyncTierClassification { - if (input.deliveryDate === null) { - return { reason: 'missing_delivery_date', review: true, tier: 'cold' }; - } - if (input.deliveryDate >= input.today) { - return { reason: 'today_or_future_delivery', review: false, tier: 'hot' }; - } - return { reason: 'past_delivery', review: false, tier: 'cold' }; -} - function withUpdatedDeliveryFact( synced: UpsertOrderWithDeliveryStopInput['synced'], geocodeStatus: NonNullable['geocodeStatus'], diff --git a/apps/delivery-api/src/routes/admin-ui-route-planning.ts b/apps/delivery-api/src/routes/admin-ui-route-planning.ts index 6936df18..afa11995 100644 --- a/apps/delivery-api/src/routes/admin-ui-route-planning.ts +++ b/apps/delivery-api/src/routes/admin-ui-route-planning.ts @@ -40,11 +40,6 @@ import { type SaveRoutePlanPayload, } from "../modules/route-plans/route-plan.types.js"; import { RouteExecutionConflictError } from "../modules/route-plans/route-execution-ownership.js"; -import type { - RouteOptimizationResult, - RouteOptimizationService, - RouteOptimizationStopSequence, -} from "../modules/route-plans/route-optimization.types.js"; import { readRouteEngineRegistrySummary } from "../modules/route-plans/route-engine-coverage.js"; const ADMIN_UI_APP_ROUTE_PLANS_PATH = "/admin/ui/app/routes"; @@ -1619,156 +1614,6 @@ export function readStopOrderLines( }); } -export type OptimizedStopOrder = { - missingCoordinateStops: number; - source: "clever_v1" | RouteOptimizationResult["source"]; - stops: RouteOptimizationStopSequence[]; -}; - -export async function buildOptimizedStopOrder(input: { - detail: RoutePlanDetail; - routeOptimizationService?: RouteOptimizationService | undefined; - shopDomain: string; -}): Promise { - if (input.routeOptimizationService !== undefined) { - try { - const optimized = await input.routeOptimizationService.optimizeStopOrder({ - detail: input.detail, - shopDomain: input.shopDomain, - }); - if (optimized !== null) { - return optimized; - } - } catch { - // Keep the operator workflow available when the internal solver is degraded. - } - } - - return buildCleverV1OptimizedStopOrder(input.detail); -} - -export function buildCleverV1OptimizedStopOrder( - detail: RoutePlanDetail, -): OptimizedStopOrder { - const sortableStops = detail.stops - .map((stop) => ({ coordinates: readStopCoordinates(stop), stop })) - .filter( - ( - entry, - ): entry is { - coordinates: { latitude: number; longitude: number }; - stop: RoutePlanDetail["stops"][number]; - } => entry.coordinates !== null, - ) - .sort( - (left, right) => - left.stop.sequence - right.stop.sequence || - left.stop.shopifyOrderGid.localeCompare(right.stop.shopifyOrderGid), - ); - const missingStops = detail.stops - .filter((stop) => readStopCoordinates(stop) === null) - .sort( - (left, right) => - left.sequence - right.sequence || - left.shopifyOrderGid.localeCompare(right.shopifyOrderGid), - ); - - const depot = readDepotCoordinates(detail.routePlan); - let origin = depot ?? sortableStops[0]?.coordinates ?? null; - const ordered: RoutePlanDetail["stops"][number][] = []; - const remaining = [...sortableStops]; - - while (remaining.length > 0) { - if (origin === null) { - ordered.push(...remaining.map((entry) => entry.stop)); - remaining.length = 0; - break; - } - let nearestIndex = 0; - let nearestDistance = Number.POSITIVE_INFINITY; - for (let index = 0; index < remaining.length; index += 1) { - const candidate = remaining[index]; - if (candidate === undefined) continue; - const distance = haversineMeters(origin, candidate.coordinates); - if ( - distance < nearestDistance || - (distance === nearestDistance && - candidate.stop.shopifyOrderGid.localeCompare( - remaining[nearestIndex]?.stop.shopifyOrderGid ?? "", - ) < 0) - ) { - nearestIndex = index; - nearestDistance = distance; - } - } - const [next] = remaining.splice(nearestIndex, 1); - if (next === undefined) break; - ordered.push(next.stop); - origin = next.coordinates; - } - - const stops = [...ordered, ...missingStops].map((stop, index) => ({ - deliveryStopId: stop.deliveryStopId, - sequence: index + 1, - shopifyOrderGid: stop.shopifyOrderGid, - })); - - return { - missingCoordinateStops: missingStops.length, - source: "clever_v1", - stops, - }; -} - -export function buildRouteOptimizeNotice( - optimized: OptimizedStopOrder, -): string { - const sourceLabel = optimized.source === "vroom" ? "VROOM" : "CLEVER v1"; - return optimized.missingCoordinateStops === 0 - ? `${sourceLabel} optimized sequence saved.` - : `${sourceLabel} optimized sequence saved; ${optimized.missingCoordinateStops} stop(s) without coordinates stayed at the end.`; -} - -export function readDepotCoordinates( - routePlan: RoutePlanSummary, -): { latitude: number; longitude: number } | null { - const latitude = routePlan.depot.latitude; - const longitude = routePlan.depot.longitude; - if (latitude === null || longitude === null) return null; - return { latitude, longitude }; -} - -export function readStopCoordinates( - stop: RoutePlanDetail["stops"][number], -): { latitude: number; longitude: number } | null { - const latitude = stop.coordinates.latitude; - const longitude = stop.coordinates.longitude; - if (latitude === null || longitude === null) return null; - return { latitude, longitude }; -} - -function haversineMeters( - left: { latitude: number; longitude: number }, - right: { latitude: number; longitude: number }, -): number { - const earthRadiusMeters = 6_371_000; - const leftLatitude = toRadians(left.latitude); - const rightLatitude = toRadians(right.latitude); - const deltaLatitude = toRadians(right.latitude - left.latitude); - const deltaLongitude = toRadians(right.longitude - left.longitude); - const a = - Math.sin(deltaLatitude / 2) * Math.sin(deltaLatitude / 2) + - Math.cos(leftLatitude) * - Math.cos(rightLatitude) * - Math.sin(deltaLongitude / 2) * - Math.sin(deltaLongitude / 2); - return 2 * earthRadiusMeters * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); -} - -function toRadians(value: number): number { - return (value * Math.PI) / 180; -} - export function normalizeOptionalDate( value: string | null | undefined, ): string | null { diff --git a/apps/delivery-api/tests/admin-active-route-reorder.integration.test.ts b/apps/delivery-api/tests/admin-active-route-reorder.integration.test.ts index 3d6a766e..9c1027e2 100644 --- a/apps/delivery-api/tests/admin-active-route-reorder.integration.test.ts +++ b/apps/delivery-api/tests/admin-active-route-reorder.integration.test.ts @@ -3,7 +3,7 @@ import { PrismaClient } from '@prisma/client'; import { afterAll, describe, expect, test } from 'vitest'; import { PrismaDriverAssignedRouteRepository } from '../src/modules/driver/driver-assigned-route.repository.js'; -import { FakeDriverPushProvider } from '../src/modules/route-grouping/driver-push.provider.js'; +import { FakeDriverPushProvider } from './support/fake-driver-push-provider.js'; import { PrismaRouteGroupingService } from '../src/modules/route-grouping/route-grouping.service.js'; import { PrismaRoutePlanRepository } from '../src/modules/route-plans/route-plan.repository.js'; diff --git a/apps/delivery-api/tests/admin-notification.dependencies.test.ts b/apps/delivery-api/tests/admin-notification.dependencies.test.ts index 2a34b2ca..973bc995 100644 --- a/apps/delivery-api/tests/admin-notification.dependencies.test.ts +++ b/apps/delivery-api/tests/admin-notification.dependencies.test.ts @@ -1,7 +1,7 @@ import type { PrismaClient } from '@prisma/client'; import { describe, expect, test, vi } from 'vitest'; -import { loadAdminOrdersDependencies } from '../src/modules/shopify/order-sync.dependencies.js'; +import { loadAdminOrdersRuntime } from '../src/modules/shopify/order-sync.dependencies.js'; import { loadWooCommerceWebhookDependencies } from '../src/modules/woocommerce/woocommerce.dependencies.js'; import { loadWordPressPluginDependencies } from '../src/modules/wordpress-plugin/wordpress-plugin.dependencies.js'; import type { AdminNotificationServiceApi } from '../src/modules/notifications/admin-notification.service.js'; @@ -64,11 +64,11 @@ type WordPressSyncCarrier = { describe('admin notification dependency wiring', () => { test('threads the shared notification service into admin Shopify order sync', () => { const service = notificationService(); - const dependencies = loadAdminOrdersDependencies({ + const dependencies = loadAdminOrdersRuntime({ adminNotificationService: service, env: { SHOPIFY_API_KEY: 'api-key', SHOPIFY_API_SECRET: 'api-secret' }, prisma: prisma(), - }); + })?.dependencies; const carrier = dependencies?.orderSyncService as unknown as RepositoryCarrier; expect(carrier.options.repository.options?.notificationService).toBe(service); diff --git a/apps/delivery-api/tests/admin-ui-route-planning.optimize.test.ts b/apps/delivery-api/tests/admin-ui-route-planning.optimize.test.ts index 126ff6b8..d644d8e3 100644 --- a/apps/delivery-api/tests/admin-ui-route-planning.optimize.test.ts +++ b/apps/delivery-api/tests/admin-ui-route-planning.optimize.test.ts @@ -1,12 +1,9 @@ import { describe, expect, test } from 'vitest'; import type { CanonicalOrderRow } from '../src/modules/shopify/order-sync.mapper.js'; -import { - buildRouteOptimizeNotice, - toRouteOpsOrderDto, -} from '../src/routes/admin-ui-route-planning.js'; +import { toRouteOpsOrderDto } from '../src/routes/admin-ui-route-planning.js'; -describe('buildRouteOptimizeNotice', () => { +describe('route planning DTO', () => { test('exposes order total and currency to the Route Ops UI DTO', () => { const dto = toRouteOpsOrderDto(canonicalOrderRow({ currencyCode: 'CAD', @@ -16,25 +13,6 @@ describe('buildRouteOptimizeNotice', () => { expect(dto.currencyCode).toBe('CAD'); expect(dto.totalPriceAmount).toBe('42.50'); }); - - test('labels VROOM optimizer results distinctly from clever fallback', () => { - expect( - buildRouteOptimizeNotice({ - missingCoordinateStops: 0, - source: 'vroom', - stops: [], - }), - ).toBe('VROOM optimized sequence saved.'); - expect( - buildRouteOptimizeNotice({ - missingCoordinateStops: 2, - source: 'vroom', - stops: [], - }), - ).toBe( - 'VROOM optimized sequence saved; 2 stop(s) without coordinates stayed at the end.', - ); - }); }); function canonicalOrderRow( diff --git a/apps/delivery-api/tests/driver-event-contract-v2.integration.test.ts b/apps/delivery-api/tests/driver-event-contract-v2.integration.test.ts index e59e2911..58c50266 100644 --- a/apps/delivery-api/tests/driver-event-contract-v2.integration.test.ts +++ b/apps/delivery-api/tests/driver-event-contract-v2.integration.test.ts @@ -16,7 +16,7 @@ import { PrismaDriverEventReceiptRepository } from '../src/modules/driver/driver-event-receipt.repository.js'; import { PrismaRouteGroupingService, replaceCurrentRouteGroupingChildVersion } from '../src/modules/route-grouping/route-grouping.service.js'; -import { FakeDriverPushProvider } from '../src/modules/route-grouping/driver-push.provider.js'; +import { FakeDriverPushProvider } from './support/fake-driver-push-provider.js'; import { PrismaRoutePlanRepository } from '../src/modules/route-plans/route-plan.repository.js'; import { PrismaDriverRouteCompletionReviewRepository } from '../src/modules/driver/driver-route-completion-review.repository.js'; diff --git a/apps/delivery-api/tests/driver-runtime-diagnostics.contract.test.ts b/apps/delivery-api/tests/driver-runtime-diagnostics.contract.test.ts index 03ecb7d2..03e9610c 100644 --- a/apps/delivery-api/tests/driver-runtime-diagnostics.contract.test.ts +++ b/apps/delivery-api/tests/driver-runtime-diagnostics.contract.test.ts @@ -2,7 +2,6 @@ import { describe, expect, test } from 'vitest'; import { DRIVER_DIAGNOSTIC_MAX_BATCH_BYTES, - parseDriverDiagnosticEnvelope, parseDriverDiagnosticEnvelopeDetailed, parseDriverDiagnosticSnapshot, type DriverDiagnosticBlocker, @@ -134,30 +133,30 @@ describe('driver diagnostic contract parser', () => { }] }); - const parsed = parseDriverDiagnosticEnvelope(unsafe); + const parsed = parseDriverDiagnosticEnvelopeDetailed(unsafe)?.envelope ?? null; expect(parsed).not.toBeNull(); const serialized = JSON.stringify(parsed); expect(serialized).not.toMatch(/authorization|Bearer|latitude|longitude|phone|errorMessage|customer|address|token/iu); }); test('rejects an unknown live reason instead of silently projecting healthy', () => { - expect(parseDriverDiagnosticEnvelope(envelope({ + expect(parseDriverDiagnosticEnvelopeDetailed(envelope({ liveSnapshot: snapshot({ blockers: [{ ...blocker(), reason: 'FREE_TEXT_FAILURE' as never }] }) - }))).toBeNull(); + }))?.envelope ?? null).toBeNull(); }); test('accepts a user-initiated support report without retaining free text', () => { const record = envelope().records[0]!; - const parsed = parseDriverDiagnosticEnvelope(envelope({ + const parsed = parseDriverDiagnosticEnvelopeDetailed(envelope({ records: [{ ...record, kind: 'USER_REPORT', message: 'customer address, PIN, and token must not persist', rawError: 'private stack trace' }] - })); + }))?.envelope ?? null; expect(parsed?.records[0]).toMatchObject({ diagnosticId: DIAGNOSTIC_ID, kind: 'USER_REPORT' }); expect(JSON.stringify(parsed?.records[0])).not.toMatch(/message|rawError|customer address|PIN|token|stack trace/iu); @@ -185,9 +184,9 @@ describe('driver diagnostic contract parser', () => { }); test('enforces record count and serialized 64 KiB limits before sanitization', () => { - expect(parseDriverDiagnosticEnvelope(envelope({ records: Array.from({ length: 51 }, () => envelope().records[0]) }))) + expect(parseDriverDiagnosticEnvelopeDetailed(envelope({ records: Array.from({ length: 51 }, () => envelope().records[0]) }))?.envelope ?? null) .toBeNull(); - expect(parseDriverDiagnosticEnvelope(envelope({ ignored: 'x'.repeat(DRIVER_DIAGNOSTIC_MAX_BATCH_BYTES) }))) + expect(parseDriverDiagnosticEnvelopeDetailed(envelope({ ignored: 'x'.repeat(DRIVER_DIAGNOSTIC_MAX_BATCH_BYTES) }))?.envelope ?? null) .toBeNull(); }); @@ -202,13 +201,13 @@ describe('driver diagnostic contract parser', () => { const clientEventId = `completion-assistance-${operation}:${REQUEST_ID}`; const payload = envelope(); const record = payload.records[0]!; - const parsed = parseDriverDiagnosticEnvelope(envelope({ + const parsed = parseDriverDiagnosticEnvelopeDetailed(envelope({ records: [{ ...record, identifiers: { clientEventId }, snapshot: snapshot({ blockers: [blocker({ clientEventId, reason: 'STORAGE_READ_FAILED', stage: 'STORAGE' })] }) }] - })); + }))?.envelope ?? null; expect(parsed?.records[0]?.identifiers?.clientEventId).toBe(clientEventId); expect(parsed?.records[0]?.snapshot.blockers?.[0]?.clientEventId).toBe(clientEventId); }); @@ -216,9 +215,9 @@ describe('driver diagnostic contract parser', () => { test('accepts a strict version-7 UUID completion assistance identifier', () => { const clientEventId = 'completion-assistance-read:018f47a2-4b5c-7def-8abc-0123456789ab'; const record = envelope().records[0]!; - const parsed = parseDriverDiagnosticEnvelope(envelope({ + const parsed = parseDriverDiagnosticEnvelopeDetailed(envelope({ records: [{ ...record, identifiers: { clientEventId } }] - })); + }))?.envelope ?? null; expect(parsed?.records[0]?.identifiers?.clientEventId).toBe(clientEventId); }); @@ -547,14 +546,14 @@ describe('driver runtime diagnostic projection', () => { describe('database integer wire bounds', () => { test('accepts the PostgreSQL integer boundary and rejects envelope overflow', () => { - expect(parseDriverDiagnosticEnvelope(envelope({ discardedRecordCount: 2_147_483_647 }))).not.toBeNull(); - expect(parseDriverDiagnosticEnvelope(envelope({ discardedRecordCount: 2_147_483_648 }))).toBeNull(); + expect(parseDriverDiagnosticEnvelopeDetailed(envelope({ discardedRecordCount: 2_147_483_647 }))?.envelope ?? null).not.toBeNull(); + expect(parseDriverDiagnosticEnvelopeDetailed(envelope({ discardedRecordCount: 2_147_483_648 }))?.envelope ?? null).toBeNull(); }); test('rejects only an overflowing record sequence with an explicit record rejection', () => { const payload = envelope(); const first = payload.records[0]!; - expect(parseDriverDiagnosticEnvelope(envelope({ records: [{ ...first, sequence: 2_147_483_647 }] }))).not.toBeNull(); + expect(parseDriverDiagnosticEnvelopeDetailed(envelope({ records: [{ ...first, sequence: 2_147_483_647 }] }))?.envelope ?? null).not.toBeNull(); expect(parseDriverDiagnosticEnvelopeDetailed(envelope({ records: [{ ...first, sequence: 2_147_483_648 }] }))) .toMatchObject({ envelope: { records: [] }, rejectedRecords: [{ diagnosticId: DIAGNOSTIC_ID, code: 'INVALID_RECORD' }] }); }); diff --git a/apps/delivery-api/tests/driver-token-verifier.test.ts b/apps/delivery-api/tests/driver-token-verifier.test.ts index 60e6aab4..f39e2e84 100644 --- a/apps/delivery-api/tests/driver-token-verifier.test.ts +++ b/apps/delivery-api/tests/driver-token-verifier.test.ts @@ -4,9 +4,7 @@ import { describe, expect, test } from 'vitest'; import { readDriverJwtSecret, signDriverRouteToken, - signDriverToken, verifyDriverRouteToken, - verifyDriverToken } from '../src/modules/driver/driver-token-verifier.js'; const secret = 'driver-secret'; @@ -25,7 +23,7 @@ describe('readDriverJwtSecret', () => { }); }); -describe('verifyDriverToken', () => { +describe('verifyDriverRouteToken', () => { test('signs route access with only the global account and assigned route scope', () => { const result = signDriverRouteToken( { @@ -49,90 +47,21 @@ describe('verifyDriverToken', () => { expect(decodePayload(result.token)).not.toHaveProperty('shopDomain'); }); - test('signs a short-lived driver JWT that the verifier accepts', () => { - const result = signDriverToken( - { - driverId: 'driver-id', - expiresInSeconds: 900, - shopDomain: 'Example.myshopify.com', - subject: 'driver:driver-id' - }, - { now, secret } - ); - - expect(result.expiresAt).toBe('2026-05-07T06:25:00.000Z'); - expect(verifyDriverToken(result.token, { now, secret })).toEqual({ - driverId: 'driver-id', - issuedAt: new Date('2026-05-07T06:10:00.000Z'), - shopDomain: 'example.myshopify.com', - subject: 'driver:driver-id', - tokenVersion: 0 - }); - }); - - test('signs and verifies driver JWTs for Woo/customer domains', () => { - const result = signDriverToken( - { - driverId: 'driver-id', - expiresInSeconds: 900, - shopDomain: 'Dev1.TomatonoFood.com', - subject: 'driver:driver-id', - tokenVersion: 4 - }, - { now, secret } - ); - - expect(verifyDriverToken(result.token, { now, secret })).toEqual({ - driverId: 'driver-id', - issuedAt: new Date('2026-05-07T06:10:00.000Z'), - shopDomain: 'dev1.tomatonofood.com', - subject: 'driver:driver-id', - tokenVersion: 4 - }); - }); - - test('accepts a server-issued driver JWT and returns driver context', () => { - const token = legacySignDriverToken({ - aud: 'clever-delivery-driver', - driverId: 'driver-id', - exp: Math.floor(now.getTime() / 1000) + 60, - iat: Math.floor(now.getTime() / 1000), - shopDomain: 'example.myshopify.com', - sub: 'driver-auth-subject', - tokenVersion: 3 - }); - - expect(verifyDriverToken(token, { now, secret })).toEqual({ - driverId: 'driver-id', - issuedAt: new Date('2026-05-07T06:10:00.000Z'), - shopDomain: 'example.myshopify.com', - subject: 'driver-auth-subject', - tokenVersion: 3 - }); - }); - - test('rejects invalid commerce domains in server-issued tokens', () => { - expect(() => signDriverToken( - { - driverId: 'driver-id', - expiresInSeconds: 900, - shopDomain: 'localhost', - subject: 'driver:driver-id' - }, - { now, secret } - )).toThrow('Commerce domain is not a valid customer domain'); + test.each([ + [{ aud: 'clever-delivery-driver' }, 'audience mismatch'], + [{ aud: 'clever-driver-account' }, 'audience mismatch'], + [{ exp: Math.floor(now.getTime() / 1000) }, 'has expired'], + [{ nbf: Math.floor(now.getTime() / 1000) + 1 }, 'not active yet'], + ])('rejects incompatible scope or time claims %j', (claims, message) => { + expect(() => verifyDriverRouteToken(signClaims(claims), { now, secret })).toThrow(message); }); test('rejects tokens with invalid signatures', () => { - const token = `${legacySignDriverToken({ - aud: 'clever-delivery-driver', - driverId: 'driver-id', - exp: Math.floor(now.getTime() / 1000) + 60, - shopDomain: 'example.myshopify.com', - sub: 'driver-auth-subject' - }).slice(0, -1)}x`; + const token = signClaims({}); + const [header, payload, signature] = token.split('.'); + const invalidSignature = `${signature?.[0] === 'x' ? 'y' : 'x'}${signature?.slice(1)}`; - expect(() => verifyDriverToken(token, { now, secret })).toThrow('Invalid driver token signature'); + expect(() => verifyDriverRouteToken(`${header}.${payload}.${invalidSignature}`, { now, secret })).toThrow('Invalid driver token signature'); }); }); @@ -142,7 +71,17 @@ function decodePayload(token: string): Record { return JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8')) as Record; } -function legacySignDriverToken(payload: Record): string { +function signClaims(overrides: Record): string { + const issuedAt = Math.floor(now.getTime() / 1000); + const payload = { + accountId: 'account-id', + aud: 'clever-delivery-driver-route', + exp: issuedAt + 900, + iat: issuedAt, + routePlanId: 'route-plan-id', + sub: 'driver-account:account-id', + ...overrides, + }; const header = { alg: 'HS256', typ: 'JWT' }; const encodedHeader = Buffer.from(JSON.stringify(header), 'utf8').toString('base64url'); const encodedPayload = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url'); diff --git a/apps/delivery-api/tests/dsv-assignment-command.integration.test.ts b/apps/delivery-api/tests/dsv-assignment-command.integration.test.ts index 3c099d83..5ced20e2 100644 --- a/apps/delivery-api/tests/dsv-assignment-command.integration.test.ts +++ b/apps/delivery-api/tests/dsv-assignment-command.integration.test.ts @@ -13,7 +13,7 @@ import type { RouteGroupingService, } from '../src/modules/route-grouping/route-grouping.types.js'; import type { DsvAssignmentTransactionClient } from '../src/modules/dsv/dsv-assignment-transaction-port.js'; -import { FakeDriverPushProvider } from '../src/modules/route-grouping/driver-push.provider.js'; +import { FakeDriverPushProvider } from './support/fake-driver-push-provider.js'; import { PrismaRouteGroupingService } from '../src/modules/route-grouping/route-grouping.service.js'; const legacySafeTargetClass = 'safe-local-g004-temp-cluster'; diff --git a/apps/delivery-api/tests/dsv-customer-account-invitations.service.test.ts b/apps/delivery-api/tests/dsv-customer-account-invitations.service.test.ts index be477c5d..f4da50e1 100644 --- a/apps/delivery-api/tests/dsv-customer-account-invitations.service.test.ts +++ b/apps/delivery-api/tests/dsv-customer-account-invitations.service.test.ts @@ -3,7 +3,6 @@ import { describe, expect, test, vi } from 'vitest'; import { DsvCustomerAccountServiceError, - loadDsvCustomerAccountWebPublicOrigin, PrismaDsvCustomerAccountService, } from '../src/modules/dsv/dsv-customer-account-invitations.service.js'; import { defaultDsvOperationalSettings } from '../src/modules/dsv/dsv-operational-settings.js'; @@ -17,13 +16,6 @@ const activeSessionId = 'dddddddd-dddd-4ddd-8ddd-dddddddddddd'; const token = 'valid_token_value_12345678901234567890'; describe('PrismaDsvCustomerAccountService', () => { - test('validates http(s) DSV web public origins only', () => { - expect(loadDsvCustomerAccountWebPublicOrigin('https://dsv.example.com')).toBe('https://dsv.example.com'); - expect(loadDsvCustomerAccountWebPublicOrigin('')).toBeUndefined(); - expect(() => loadDsvCustomerAccountWebPublicOrigin('https://dsv.example.com/path')).toThrow(/origin/u); - expect(() => loadDsvCustomerAccountWebPublicOrigin('javascript:alert(1)')).toThrow(/origin/u); - }); - test('returns null for invalid, consumed, revoked, or cross-boundary invites and identifies expiration', async () => { const harness = createHarness(); const service = harness.service; diff --git a/apps/delivery-api/tests/dsv-driver-identity.test.ts b/apps/delivery-api/tests/dsv-driver-identity.test.ts index 9fcac696..52717b77 100644 --- a/apps/delivery-api/tests/dsv-driver-identity.test.ts +++ b/apps/delivery-api/tests/dsv-driver-identity.test.ts @@ -1,7 +1,6 @@ import { describe, expect, test } from 'vitest'; import { - fingerprintResidentNumberFront, normalizeDsvDriverLoginId, normalizeDsvDriverPhone, } from '../src/modules/dsv/dsv-driver-identity.js'; @@ -11,15 +10,4 @@ describe('DSV driver identity helpers', () => { expect(normalizeDsvDriverLoginId(' Driver.One ')).toBe('driver.one'); expect(normalizeDsvDriverPhone('010-9000-0001')).toBe('01090000001'); }); - - test('creates a deterministic keyed fingerprint without retaining the seven digits', () => { - const first = fingerprintResidentNumberFront('9001011', 'identity-secret-a-that-is-long-enough'); - const same = fingerprintResidentNumberFront('9001011', 'identity-secret-a-that-is-long-enough'); - const otherSecret = fingerprintResidentNumberFront('9001011', 'identity-secret-b-that-is-long-enough'); - - expect(first).toBe(same); - expect(first).not.toBe(otherSecret); - expect(first).toMatch(/^[a-f0-9]{64}$/u); - expect(first).not.toContain('9001011'); - }); }); diff --git a/apps/delivery-api/tests/dsv-principal.test.ts b/apps/delivery-api/tests/dsv-principal.test.ts index 1795d50e..339f57f4 100644 --- a/apps/delivery-api/tests/dsv-principal.test.ts +++ b/apps/delivery-api/tests/dsv-principal.test.ts @@ -4,7 +4,6 @@ import { DsvForbiddenError, createDsvAdminPrincipal, createDsvCustomerUserPrincipalFromAccount, - requireCustomerDeliveryPrincipal, requireDsvScopes, } from '../src/modules/dsv/dsv-principal.js'; import type { DsvPrincipal } from '../src/modules/dsv/dsv-principal.js'; @@ -98,16 +97,6 @@ describe('DSV principal authorization', () => { }); expect(() => requireDsvScopes(principal, ['dsv:session:read'])).not.toThrow(); expect(() => requireDsvScopes(principal, ['dsv:customer-deliveries:read'])).not.toThrow(); - expect(requireCustomerDeliveryPrincipal({ - customerId: 'customer-a', - destinationId: 'shared-destination', - principal, - })).toBe(principal); - expect(() => requireCustomerDeliveryPrincipal({ - customerId: 'customer-b', - destinationId: 'shared-destination', - principal, - })).toThrow(DsvForbiddenError); }); test('missing, null, inactive, or empty CustomerAccount status cannot grant CUSTOMER_USER scope', () => { @@ -139,30 +128,4 @@ describe('DSV principal authorization', () => { }, })).toThrow(DsvForbiddenError); }); - - test('destination ID alone cannot authorize customer reads', () => { - const customerPrincipal = createDsvCustomerUserPrincipalFromAccount({ - account: { - customerId: 'customer-a', - shopId: '99999999-9999-4999-8999-999999999999', - status: 'ACTIVE', - }, - }); - const adminPrincipal = createDsvAdminPrincipal({ shopId: '99999999-9999-4999-8999-999999999999' }); - - expect(requireCustomerDeliveryPrincipal({ - customerId: 'customer-a', - destinationId: 'shared-destination', - principal: customerPrincipal, - })).toBe(customerPrincipal); - expect(() => requireCustomerDeliveryPrincipal({ - destinationId: 'shared-destination', - principal: adminPrincipal, - })).toThrow(DsvForbiddenError); - expect(() => requireCustomerDeliveryPrincipal({ - customerId: 'customer-b', - destinationId: 'shared-destination', - principal: customerPrincipal, - })).toThrow(DsvForbiddenError); - }); }); diff --git a/apps/delivery-api/tests/dsv-v1-contract-floor.test.ts b/apps/delivery-api/tests/dsv-v1-contract-floor.test.ts index 2d05c0e9..0b0d44c8 100644 --- a/apps/delivery-api/tests/dsv-v1-contract-floor.test.ts +++ b/apps/delivery-api/tests/dsv-v1-contract-floor.test.ts @@ -2,13 +2,10 @@ import { describe, expect, test } from 'vitest'; import { dsvV1ApiVersion, - dsvV1CustomerDeliveryRequiredFields, dsvV1EmittedProofStatuses, dsvV1ErrorCodes, dsvV1EtaStatuses, dsvV1ProofStatuses, - dsvV1SellerOrderSummaryRequiredFields, - dsvV1SessionRequiredFields, mapDsvV1CustomerDeliveryInquiryItem, mapDsvV1SellerOrderSummary, mapDsvV1SessionPrincipal, @@ -50,7 +47,6 @@ describe('G005 DSV v1 OpenAPI contract floor', () => { }); test('freezes session, ETA, proof, and emitted proof enum floors', () => { - expect(dsvV1SessionRequiredFields).toEqual(['csrfToken', 'principalType', 'shopId', 'scopes']); expect(dsvV1EtaStatuses).toEqual(['NOT_REQUIRED', 'PENDING', 'READY', 'FAILED', 'STALE']); expect(dsvV1ProofStatuses).toEqual(['NONE', 'AVAILABLE', 'REDACTED', 'EXPIRED']); expect(dsvV1EmittedProofStatuses).toEqual(['NONE', 'AVAILABLE', 'EXPIRED']); @@ -61,9 +57,12 @@ describe('G005 DSV v1 OpenAPI contract floor', () => { shopId: 'shop-1', }, 'csrf-1'); - for (const key of dsvV1SessionRequiredFields) { - expect(dto).toHaveProperty(key); - } + expect(dto).toEqual({ + csrfToken: 'csrf-1', + principalType: 'DSV_ADMIN', + scopes: ['dsv:session:read'], + shopId: 'shop-1', + }); }); test('freezes seller order summary required shape and ETA serialization floor', () => { @@ -80,19 +79,6 @@ describe('G005 DSV v1 OpenAPI contract floor', () => { sellerOrderKey: 'SO-001', }); - expect(dsvV1SellerOrderSummaryRequiredFields).toEqual([ - 'sellerOrderId', - 'sellerOrderKey', - 'deliveryStopId', - 'customerId', - 'destinationId', - 'assignmentStatus', - 'etaStatus', - 'eventSummary', - ]); - for (const key of dsvV1SellerOrderSummaryRequiredFields) { - expect(dto).toHaveProperty(key); - } expect(dto).toEqual({ assignmentStatus: 'ASSIGNED', customerId: 'customer-1', @@ -127,20 +113,6 @@ describe('G005 DSV v1 OpenAPI contract floor', () => { shippedBoxes: 3, }); - expect(dsvV1CustomerDeliveryRequiredFields).toEqual([ - 'sellerOrderId', - 'sellerOrderKey', - 'destinationId', - 'destinationDisplayName', - 'shippedBoxes', - 'deliveryStatus', - 'etaStatus', - 'eventSummary', - 'proofStatus', - ]); - for (const key of dsvV1CustomerDeliveryRequiredFields) { - expect(dto).toHaveProperty(key); - } expect(dto).toEqual({ deliveryStatus: 'DELIVERED', destinationDisplayName: 'Dock A', diff --git a/apps/delivery-api/tests/route-execution-ownership.test.ts b/apps/delivery-api/tests/route-execution-ownership.test.ts index 0aa572ab..1cea2262 100644 --- a/apps/delivery-api/tests/route-execution-ownership.test.ts +++ b/apps/delivery-api/tests/route-execution-ownership.test.ts @@ -1,65 +1,57 @@ import { describe, expect, test, vi } from 'vitest'; import { - assertRouteExecutionOwnership, + assertRouteDispatchOwnership, RouteExecutionConflictError } from '../src/modules/route-plans/route-execution-ownership.js'; describe('route execution ownership', () => { - test('locks unique stop ids in deterministic order before checking active overlap', async () => { + test('locks unique stop ids in deterministic order before rejecting an active dispatch overlap', async () => { const lockSql: string[] = []; + const lockedStopIds: unknown[] = []; const tx = { - $queryRaw: vi.fn((query: TemplateStringsArray | { strings: readonly string[] }) => { + $queryRaw: vi.fn((query: TemplateStringsArray | { strings: readonly string[]; values: readonly unknown[] }) => { const strings = 'strings' in query ? query.strings : query; - lockSql.push(Array.from(strings).join('?')); - return Promise.resolve([{ locked: true }]); - }), - routePlanStop: { - findFirst: vi.fn(() => Promise.resolve({ + const sql = Array.from(strings).join('?'); + if (sql.includes('SELECT DISTINCT')) return Promise.resolve([{ deliveryStopId: 'stop-a', - routePlanId: 'other-route' - })), - findMany: vi.fn(() => Promise.resolve([])) - } + orderId: 'order-a', + orderName: '#1001', + routePlanId: 'other-route', + routeName: 'Other route' + }]); + lockSql.push(sql); + if ('strings' in query) lockedStopIds.push(query.values[0]); + return Promise.resolve([{ locked: true }]); + }) }; - await expect(assertRouteExecutionOwnership(tx, { + await expect(assertRouteDispatchOwnership(tx, { deliveryStopIds: ['stop-b', 'stop-a', 'stop-b'], routePlanId: 'route-current', shopId: 'shop-id' })).rejects.toBeInstanceOf(RouteExecutionConflictError); - expect(tx.$queryRaw).toHaveBeenCalledTimes(2); + expect(tx.$queryRaw).toHaveBeenCalledTimes(3); expect(lockSql).toEqual([ 'SELECT TRUE AS "locked" FROM pg_advisory_xact_lock(710027, hashtext(?))', 'SELECT TRUE AS "locked" FROM pg_advisory_xact_lock(710027, hashtext(?))' ]); - expect(tx.routePlanStop.findFirst).toHaveBeenCalledWith({ - select: { deliveryStopId: true, routePlanId: true }, - where: { - deliveryStopId: { in: ['stop-a', 'stop-b'] }, - routePlanId: { not: 'route-current' }, - routePlan: { shopId: 'shop-id', status: 'IN_PROGRESS' } - } - }); + expect(lockedStopIds).toEqual(['stop-a', 'stop-b']); }); test('fails closed when the transaction lock cannot be acquired', async () => { const lockError = new Error('database lock unavailable'); const tx = { - $queryRaw: vi.fn(() => Promise.reject(lockError)), - routePlanStop: { - findFirst: vi.fn(() => Promise.resolve(null)), - findMany: vi.fn(() => Promise.resolve([])) - } + $queryRaw: vi.fn(() => Promise.reject(lockError)) }; - await expect(assertRouteExecutionOwnership(tx, { + await expect(assertRouteDispatchOwnership(tx, { deliveryStopIds: ['stop-a'], routePlanId: 'route-current', shopId: 'shop-id' })).rejects.toBe(lockError); - expect(tx.routePlanStop.findFirst).not.toHaveBeenCalled(); + expect(tx.$queryRaw).toHaveBeenCalledOnce(); }); }); diff --git a/apps/delivery-api/tests/route-group-copy.integration.test.ts b/apps/delivery-api/tests/route-group-copy.integration.test.ts index cfdfb689..9615ce04 100644 --- a/apps/delivery-api/tests/route-group-copy.integration.test.ts +++ b/apps/delivery-api/tests/route-group-copy.integration.test.ts @@ -1,7 +1,7 @@ import { PrismaClient } from '@prisma/client'; import { afterAll, beforeAll, describe, expect, test } from 'vitest'; -import { FakeDriverPushProvider } from '../src/modules/route-grouping/driver-push.provider.js'; +import { FakeDriverPushProvider } from './support/fake-driver-push-provider.js'; import { PrismaRouteGroupingService } from '../src/modules/route-grouping/route-grouping.service.js'; import { PrismaRoutePlanRepository } from '../src/modules/route-plans/route-plan.repository.js'; diff --git a/apps/delivery-api/tests/route-grouping-save.integration.test.ts b/apps/delivery-api/tests/route-grouping-save.integration.test.ts index 3819ac13..c7e0cf3e 100644 --- a/apps/delivery-api/tests/route-grouping-save.integration.test.ts +++ b/apps/delivery-api/tests/route-grouping-save.integration.test.ts @@ -13,7 +13,7 @@ import { } from '../src/modules/driver/driver-event.repository.js'; import { PrismaDriverAssignedRouteRepository } from '../src/modules/driver/driver-assigned-route.repository.js'; import { PrismaDriverRouteAccessRepository } from '../src/modules/driver/driver-route-access.repository.js'; -import { FakeDriverPushProvider } from '../src/modules/route-grouping/driver-push.provider.js'; +import { FakeDriverPushProvider } from './support/fake-driver-push-provider.js'; import { PrismaRouteGroupingService, rebindCurrentOrdersToRouteVersion } from '../src/modules/route-grouping/route-grouping.service.js'; import { PrismaRoutePlanRepository } from '../src/modules/route-plans/route-plan.repository.js'; import { PrismaOrderQueryRepository } from '../src/modules/shopify/order-query.repository.js'; diff --git a/apps/delivery-api/tests/route-grouping.service.test.ts b/apps/delivery-api/tests/route-grouping.service.test.ts index cb55e573..853a66a1 100644 --- a/apps/delivery-api/tests/route-grouping.service.test.ts +++ b/apps/delivery-api/tests/route-grouping.service.test.ts @@ -1,12 +1,11 @@ import { describe, expect, test, vi } from 'vitest'; import { buildApp } from '../src/app.js'; import { classifyCoordinateInPolygons } from '../src/modules/route-grouping/route-grouping.geometry.js'; -import { FakeDriverPushProvider } from '../src/modules/route-grouping/driver-push.provider.js'; +import { FakeDriverPushProvider } from './support/fake-driver-push-provider.js'; import { computeRouteShapeSignatureFromParts } from '../src/modules/route-plans/route-plan-geometry-cache.js'; import { assertDraftSchedulePlanDates, assertLockedRoutePlanSuccessorPolicy, - currentRouteBindingAuthorityState, deriveGroupingDisplayStatus, PrismaRouteGroupingService, newChildRouteName, @@ -1136,39 +1135,6 @@ describe('route grouping contracts', () => { })).rejects.toMatchObject({ code: 'ROUTE_GROUPING_STALE_WRITE' }); }); - test('classifies exact, legacy-unbound, and mismatched route binding authority', () => { - const assignments = (bindings: Array) => bindings.map((currentRouteVersionId, index) => ({ - order: { currentRouteVersionId }, - orderId: `order-${index + 1}` - })); - - expect(currentRouteBindingAuthorityState( - 'child-current', - ['order-1', 'order-2'], - assignments([null, null]) - )).toBe('LEGACY_UNBOUND'); - expect(currentRouteBindingAuthorityState( - 'child-current', - ['order-1', 'order-2'], - assignments(['child-current', 'child-current']) - )).toBe('EXACT'); - expect(currentRouteBindingAuthorityState( - 'child-current', - ['order-1', 'order-2'], - assignments(['child-current', null]) - )).toBe('MISMATCH'); - expect(currentRouteBindingAuthorityState( - 'child-current', - ['order-1', 'order-2'], - assignments(['child-foreign', null]) - )).toBe('MISMATCH'); - expect(currentRouteBindingAuthorityState( - 'child-current', - ['order-1', 'order-2'], - [...assignments([null, null]), { order: { currentRouteVersionId: 'child-current' }, orderId: 'order-extra' }] - )).toBe('MISMATCH'); - }); - test('rebinds current order ownership across every child-version replacement path', () => { const source = readFileSync(join(process.cwd(), 'src/modules/route-grouping/route-grouping.service.ts'), 'utf8'); const calls = source.match(/await replaceCurrentRouteGroupingChildVersion\(tx,/gu) ?? []; diff --git a/apps/delivery-api/tests/route-membership-mutation-authority.test.ts b/apps/delivery-api/tests/route-membership-mutation-authority.test.ts index c03e3368..42e9eb4e 100644 --- a/apps/delivery-api/tests/route-membership-mutation-authority.test.ts +++ b/apps/delivery-api/tests/route-membership-mutation-authority.test.ts @@ -145,18 +145,9 @@ describe('route membership mutation authority', () => { expect(assignmentAuthority).toContain("throw new RouteGroupingValidationError(['current route membership snapshot is malformed'])"); expect(assignmentAuthority).toContain("throw new RouteGroupingValidationError(['current route membership snapshot tuple does not match grouping authority'])"); expect(assignmentAuthority).toContain("throw new RouteGroupingValidationError(['current route membership snapshot does not match bound route authority'])"); - expect(assignmentAuthority).not.toContain('currentRouteBindingAuthorityState(child.id, snapshotOrderIds, group.orders)'); expect(assignmentAuthority).toContain("return resolveChildSnapshotAssignments(group, child, 'CURRENT')"); expect(assignmentAuthority).toContain("return resolveChildSnapshotAssignments(group, child, 'CURRENT_READ')"); expect(assignmentAuthority).not.toContain('.filter((assignment)'); - const bindingAuthority = source.slice( - source.indexOf('export function currentRouteBindingAuthorityState('), - source.indexOf('type OptimizedDraftRoute =') - ); - expect(bindingAuthority).toContain('order.currentRouteVersionId === childVersionId'); - expect(bindingAuthority).toContain('boundOrderIds.length === 0'); - expect(bindingAuthority).toContain('order.currentRouteVersionId === null'); - expect(bindingAuthority).toContain("return entirelyUnbound ? 'LEGACY_UNBOUND' : 'MISMATCH'"); // Draft partition discovery also reads membership; actual child mutations still require CURRENT authority. expect(source.match(/readCurrentChildAssignments\(/gu)).toHaveLength(5); const copy = source.slice(source.indexOf('async copyGrouping('), source.indexOf('async copyStandaloneRoutePlan(')); diff --git a/apps/delivery-api/tests/route-tracking.road-match.test.ts b/apps/delivery-api/tests/route-tracking.road-match.test.ts index 4364f3b5..723aee7f 100644 --- a/apps/delivery-api/tests/route-tracking.road-match.test.ts +++ b/apps/delivery-api/tests/route-tracking.road-match.test.ts @@ -4,7 +4,6 @@ import { OsrmRouteTrackingRoadMatchProvider, buildRouteTrackingRoadMatchCacheWrite, buildRouteTrackingRoadMatchedPath, - shouldRefreshRouteTrackingRoadMatchedPath, } from '../src/modules/route-tracking/route-tracking.road-match.js'; import type { RouteTrackingGeometryDocumentV1, @@ -1498,32 +1497,6 @@ describe('route tracking road matching', () => { expect(String((fetch.mock.calls as unknown as Array<[string]>)[0]![0])).not.toContain('-79.4'); }); - test('does not refresh from cache when the watermark already covers the latest input', () => { - const record = trackingRecord({ - roadMatchedLastInputOccurredAt: new Date('2026-07-21T00:02:00.000Z'), - roadMatchedSchemaVersion: 'route_tracking_road_match.v5', - roadMatchedSourcePointCount: 3, - roadMatchedWatermark: 'route_tracking_road_match.v1:korea:3:2:2026-07-21T00:02:00.000Z:abc', - sourcePointCount: 3, - }); - - expect(shouldRefreshRouteTrackingRoadMatchedPath(record)).toBe(false); - expect(shouldRefreshRouteTrackingRoadMatchedPath(trackingRecord({ - roadMatchedLastInputOccurredAt: new Date('2026-07-21T00:01:00.000Z'), - roadMatchedSchemaVersion: 'route_tracking_road_match.v5', - roadMatchedSourcePointCount: 2, - roadMatchedWatermark: 'route_tracking_road_match.v1:korea:2:2:2026-07-21T00:01:00.000Z:abc', - sourcePointCount: 3, - }))).toBe(true); - expect(shouldRefreshRouteTrackingRoadMatchedPath(trackingRecord({ - roadMatchedLastInputOccurredAt: new Date('2026-07-21T00:02:00.000Z'), - roadMatchedSchemaVersion: 'route_tracking_road_match.v4', - roadMatchedSourcePointCount: 3, - roadMatchedWatermark: 'route_tracking_road_match.v1:korea:3:2:2026-07-21T00:02:00.000Z:abc', - sourcePointCount: 3, - }))).toBe(true); - }); - test('serializes cached road-matched geometry into the snapshot contract', () => { const path = buildRouteTrackingRoadMatchedPath(trackingRecord({ roadMatchedCoverage: 'korea', diff --git a/apps/delivery-api/tests/shopify-webhook.dependencies.test.ts b/apps/delivery-api/tests/shopify-webhook.dependencies.test.ts index 7a1dbbd1..37fbae96 100644 --- a/apps/delivery-api/tests/shopify-webhook.dependencies.test.ts +++ b/apps/delivery-api/tests/shopify-webhook.dependencies.test.ts @@ -1,9 +1,9 @@ import type { PrismaClient } from '@prisma/client'; import { describe, expect, test } from 'vitest'; -import { loadShopifyWebhookBodyLimitBytes, loadShopifyWebhookDependencies } from '../src/modules/shopify/webhook.dependencies.js'; +import { loadShopifyWebhookBodyLimitBytes, loadShopifyWebhookRuntime } from '../src/modules/shopify/webhook.dependencies.js'; -describe('loadShopifyWebhookDependencies', () => { +describe('loadShopifyWebhookRuntime', () => { test('uses a bounded Shopify-compatible webhook body limit', () => { expect(loadShopifyWebhookBodyLimitBytes(undefined)).toBe(5 * 1024 * 1024); expect(loadShopifyWebhookBodyLimitBytes(String(6 * 1024 * 1024))).toBe(6 * 1024 * 1024); @@ -11,14 +11,14 @@ describe('loadShopifyWebhookDependencies', () => { expect(() => loadShopifyWebhookBodyLimitBytes(String(10 * 1024 * 1024 + 1))).toThrow('between 1048576 and 10485760'); }); test('stays disabled when no Shopify webhook secret material is configured', () => { - expect(loadShopifyWebhookDependencies({ env: {}, prisma: prisma() })).toBeUndefined(); + expect(loadShopifyWebhookRuntime({ env: {}, prisma: prisma() })).toBeUndefined(); }); test('uses the legacy Shopify API secret as a default webhook-only fallback', () => { - const dependencies = loadShopifyWebhookDependencies({ + const dependencies = loadShopifyWebhookRuntime({ env: { SHOPIFY_API_SECRET: 'shared-secret' }, prisma: prisma() - }); + })?.dependencies; expect(dependencies?.appCredentials).toEqual([ { appId: 'clever', clientSecret: 'shared-secret' } @@ -26,13 +26,13 @@ describe('loadShopifyWebhookDependencies', () => { }); test('loads app-specific webhook secrets from Shopify app credentials', () => { - const dependencies = loadShopifyWebhookDependencies({ + const dependencies = loadShopifyWebhookRuntime({ env: { SHOPIFY_DEV_API_KEY: 'dev-client-id', SHOPIFY_DEV_API_SECRET: 'dev-secret' }, prisma: prisma() - }); + })?.dependencies; expect(dependencies?.appCredentials).toEqual([ { appId: 'clever-route-dev', clientSecret: 'dev-secret' } @@ -40,13 +40,13 @@ describe('loadShopifyWebhookDependencies', () => { }); test('wires order webhook processing when token encryption is configured', () => { - const dependencies = loadShopifyWebhookDependencies({ + const dependencies = loadShopifyWebhookRuntime({ env: { SHOPIFY_API_SECRET: 'shared-secret', SHOPIFY_TOKEN_ENCRYPTION_KEY: 'base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' }, prisma: prisma() - }); + })?.dependencies; expect(dependencies?.orderWebhookProcessor?.canProcessTopic('orders/updated')).toBe(true); }); diff --git a/apps/delivery-api/tests/support/fake-driver-push-provider.ts b/apps/delivery-api/tests/support/fake-driver-push-provider.ts new file mode 100644 index 00000000..268976f3 --- /dev/null +++ b/apps/delivery-api/tests/support/fake-driver-push-provider.ts @@ -0,0 +1,18 @@ +import type { + DriverPushProvider, + DriverRoutePushMessage, + DriverRoutePushResult, +} from '../../src/modules/route-grouping/driver-push.provider.js'; + +export class FakeDriverPushProvider implements DriverPushProvider { + readonly providerName = 'fake'; + readonly sentMessages: DriverRoutePushMessage[] = []; + + sendRouteNotification(message: DriverRoutePushMessage): Promise { + this.sentMessages.push(message); + return Promise.resolve({ + providerMessageId: `fake:${message.routeGroupingId ?? 'standalone'}:${message.childVersion ?? message.publicationVersion ?? 'current'}:${message.routePlanId}`, + status: 'SENT', + }); + } +} diff --git a/apps/delivery-api/tests/woocommerce-order-sync.service.test.ts b/apps/delivery-api/tests/woocommerce-order-sync.service.test.ts index 026e392c..11cea713 100644 --- a/apps/delivery-api/tests/woocommerce-order-sync.service.test.ts +++ b/apps/delivery-api/tests/woocommerce-order-sync.service.test.ts @@ -5,7 +5,6 @@ import type { UpsertOrderWithDeliveryStopResult } from '../src/modules/shopify/o import { GeocodingService } from '../src/modules/geocoding/geocoding.service.js'; import type { GeocodingQuery } from '../src/modules/geocoding/geocoding.types.js'; import { - classifyWooCommerceSyncTier, prePersistGeocodingAddress, WooCommerceOrderSyncService } from '../src/modules/woocommerce/woocommerce-order-sync.service.js'; @@ -76,25 +75,6 @@ describe('WooCommerceOrderSyncService', () => { ); }); - test('classifies hot and cold fallback tiers from CLEVER normalized schedule dates', () => { - expect(classifyWooCommerceSyncTier({ - deliveryDate: '2026-06-05', - today: '2026-06-05' - })).toEqual({ reason: 'today_or_future_delivery', review: false, tier: 'hot' }); - expect(classifyWooCommerceSyncTier({ - deliveryDate: '2026-06-06', - today: '2026-06-05' - })).toEqual({ reason: 'today_or_future_delivery', review: false, tier: 'hot' }); - expect(classifyWooCommerceSyncTier({ - deliveryDate: '2026-06-04', - today: '2026-06-05' - })).toEqual({ reason: 'past_delivery', review: false, tier: 'cold' }); - expect(classifyWooCommerceSyncTier({ - deliveryDate: null, - today: '2026-06-05' - })).toEqual({ reason: 'missing_delivery_date', review: true, tier: 'cold' }); - }); - test('fetches and syncs a single WooCommerce order through the shared mapper pipeline', async () => { const wooOrder = order(11432); const client = { diff --git a/apps/route-ops-web/src/api.ts b/apps/route-ops-web/src/api.ts index aa0efcb9..0cba741b 100644 --- a/apps/route-ops-web/src/api.ts +++ b/apps/route-ops-web/src/api.ts @@ -1,9 +1,7 @@ import type { ApiEnvelope, BootstrapPayload, - BulkGeocodeOrdersResponse, DriversResponse, - GeocodeOrderResponse, GeocodeSettingsResponse, NotificationMutationResponse, NotificationsResponse, @@ -14,7 +12,6 @@ import type { OperationalHealthResponse, OrdersResponse, RouteDeleteResponse, - RouteOptimizationJobResponse, RouteGroupingDetailDto, RoutePlanDetailDto, RouteOperationalStateResponse, @@ -197,64 +194,6 @@ export async function patchOrderMetadata(input: { ); } -export async function patchOrderCoordinates(input: { - csrfToken: string; - latitude: number; - longitude: number; - orderId: string; - source: "manual" | "map_click"; -}): Promise { - return apiMutation( - `/admin/ui/app/api/orders/${encodeURIComponent(input.orderId)}/coordinates`, - "PATCH", - input.csrfToken, - { - latitude: input.latitude, - longitude: input.longitude, - source: input.source, - }, - ); -} - -export async function geocodeOrder(input: { - address?: Record; - csrfToken: string; - orderId: string; - save: boolean; -}): Promise { - return apiMutation( - `/admin/ui/app/api/orders/${encodeURIComponent(input.orderId)}/geocode`, - "POST", - input.csrfToken, - { - ...(input.address === undefined ? {} : { address: input.address }), - save: input.save, - }, - ); -} - -export async function bulkGeocodeOrders(input: { - csrfToken: string; - query: string; -}): Promise { - return apiMutation( - input.query === "" - ? "/admin/ui/app/api/orders/geocode" - : `/admin/ui/app/api/orders/geocode?${input.query}`, - "POST", - input.csrfToken, - {}, - ); -} - -export async function getBulkGeocodeJob( - jobId: string, -): Promise { - return apiGet( - `/admin/ui/app/api/orders/geocode/${encodeURIComponent(jobId)}`, - ); -} - export async function requestWooOrderSync(input: { csrfToken: string; pageSize?: number; @@ -277,10 +216,6 @@ export async function getWooOrderSyncRun( ); } -export async function getLatestWooOrderSync(): Promise { - return apiGet("/admin/ui/app/api/orders/sync/latest"); -} - export async function geocodeSettings(input: { csrfToken: string; defaultDepotAddress: string; @@ -363,19 +298,6 @@ export async function saveRouteGroupingPolygons(input: { ); } -export async function resolveRouteGroupingAssignments(input: { - assignments: Array<{ assignedDriverId: string; orderId: string }>; - csrfToken: string; - routeGroupId: string; -}): Promise<{ routeGroup: RouteGroupingDetailDto }> { - return apiMutation<{ routeGroup: RouteGroupingDetailDto }>( - `/admin/ui/app/api/route-groups/${encodeURIComponent(input.routeGroupId)}/assignments`, - "PATCH", - input.csrfToken, - { assignments: input.assignments }, - ); -} - export async function generateRouteGroupingChildRoutes(input: { confirmRisk?: boolean; csrfToken: string; @@ -398,24 +320,6 @@ export async function getRouteDetail( ); } -export async function createRoute(input: { - csrfToken: string; - depotAddress: string | null; - depotLatitude: number | null; - depotLongitude: number | null; - orderIds: string[]; - planDate: string; - routeName: string; - scope?: "history" | "planning"; -}): Promise<{ routePlan: { id: string } }> { - return apiMutation<{ routePlan: { id: string } }>( - appendRouteOpsScope("/admin/ui/app/api/routes", input.scope), - "POST", - input.csrfToken, - input, - ); -} - export async function deleteRoute( routePlanId: string, csrfToken: string, @@ -471,81 +375,6 @@ export async function publishRoute( ); } -export async function saveStopSequence( - routePlanId: string, - csrfToken: string, - stops: Array<{ deliveryStopId: string; sourceOrderId: string }>, -): Promise { - return apiMutation( - `/admin/ui/app/api/routes/${encodeURIComponent(routePlanId)}/stops`, - "PATCH", - csrfToken, - { stops }, - ); -} - -export async function saveRouteOptions( - routePlanId: string, - csrfToken: string, - routeEndMode: RoutePlanDetailDto["routePlan"]["routeEndMode"], -): Promise { - return apiMutation( - `/admin/ui/app/api/routes/${encodeURIComponent(routePlanId)}/options`, - "PATCH", - csrfToken, - { routeEndMode }, - ); -} - -export async function createRouteOptimizationJob( - routePlanId: string, - csrfToken: string, -): Promise { - return apiMutation( - `/admin/ui/app/api/routes/${encodeURIComponent(routePlanId)}/optimize-jobs`, - "POST", - csrfToken, - {}, - ); -} - -export async function getLatestRouteOptimizationJob( - routePlanId: string, -): Promise { - return apiGet( - `/admin/ui/app/api/routes/${encodeURIComponent(routePlanId)}/optimize-jobs/latest`, - ); -} - -export async function getRouteOptimizationJob( - routePlanId: string, - jobId: string, -): Promise { - return apiGet( - `/admin/ui/app/api/routes/${encodeURIComponent(routePlanId)}/optimize-jobs/${encodeURIComponent(jobId)}`, - ); -} - -export async function optimizeRoute( - routePlanId: string, - csrfToken: string, -): Promise { - return createRouteOptimizationJob(routePlanId, csrfToken); -} - -export async function assignDriver( - routePlanId: string, - csrfToken: string, - driverId: string | null, -): Promise { - return apiMutation( - `/admin/ui/app/api/routes/${encodeURIComponent(routePlanId)}/driver`, - "PATCH", - csrfToken, - { driverId }, - ); -} - export async function getDrivers(): Promise { return apiGet("/admin/ui/app/api/drivers"); } diff --git a/apps/route-ops-web/src/components/PageShell.tsx b/apps/route-ops-web/src/components/PageShell.tsx deleted file mode 100644 index 7ad5fd70..00000000 --- a/apps/route-ops-web/src/components/PageShell.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { ReactElement, ReactNode } from 'react'; - -export function PageShell({ actions, children, eyebrow, title }: { actions?: ReactNode; children: ReactNode; eyebrow?: string; title: string }): ReactElement { - return ( -
-
-
- {eyebrow === undefined ? null : {eyebrow}} -

{title}

-
- {actions === undefined ? null :
{actions}
} -
- {children} -
- ); -} diff --git a/apps/route-ops-web/src/components/primitives.tsx b/apps/route-ops-web/src/components/primitives.tsx index f2aaacc6..fa17356b 100644 --- a/apps/route-ops-web/src/components/primitives.tsx +++ b/apps/route-ops-web/src/components/primitives.tsx @@ -1,6 +1,5 @@ import type { ReactElement, ReactNode } from 'react'; -import type { CanonicalOrderDto } from '../types'; import type { OperationalPillModel, OperationalPillTone } from '../operationalStatus'; export function Kpi({ label, value }: { label: string; value: number }): ReactElement { @@ -32,12 +31,3 @@ export function OperationalPillGroup({ }): ReactElement { return
{pills.map((pill) => {pill.label})}
; } - -export function BlockerList({ blockers }: { blockers: string[] }): ReactElement { - return
Resolve before routing
    {blockers.map((blocker) =>
  • {blocker}
  • )}
; -} - -export function MiniOrderList({ empty, onSelect, orders }: { empty: string; onSelect?(order: CanonicalOrderDto): void; orders: CanonicalOrderDto[] }): ReactElement { - if (orders.length === 0) return

{empty}

; - return
    {orders.map((order) =>
  • {order.orderName}{order.blockerReasons.join(', ') || order.deliveryDate}{onSelect === undefined ? null : }
  • )}
; -} diff --git a/apps/route-ops-web/src/i18n.ts b/apps/route-ops-web/src/i18n.ts index cb89ede9..89fd1742 100644 --- a/apps/route-ops-web/src/i18n.ts +++ b/apps/route-ops-web/src/i18n.ts @@ -1329,9 +1329,6 @@ export const orderDetailLabelsByLocale = { } } as const; -export const orderDetailLabels = orderDetailLabelsByLocale['en-CA']; -export const orderBlockerLabels = orderDetailLabels.blockerReasons; - export const orderFieldLabelsByLocale = { 'en-CA': { ...orderDetailLabelsByLocale['en-CA'].diagnosticPaths, @@ -1365,8 +1362,6 @@ export const orderFieldLabelsByLocale = { } } as const; -export const orderFieldLabels = orderFieldLabelsByLocale['en-CA']; - export function getAppCopy(locale: string | null | undefined): (typeof appCopy)[AppLocale] { return appCopy[resolveLocale(locale)]; } @@ -1402,7 +1397,3 @@ export function getOrderDetailLabels(locale: string | null | undefined): (typeof export function getOrderFieldLabels(locale: string | null | undefined): (typeof orderFieldLabelsByLocale)[AppLocale] { return orderFieldLabelsByLocale[resolveLocale(locale)]; } - -export function getOrderBlockerLabels(locale: string | null | undefined): (typeof orderDetailLabelsByLocale)[AppLocale]['blockerReasons'] { - return getOrderDetailLabels(locale).blockerReasons; -} diff --git a/apps/route-ops-web/src/maps/provider.ts b/apps/route-ops-web/src/maps/provider.ts index 4d18d0fa..61bf651a 100644 --- a/apps/route-ops-web/src/maps/provider.ts +++ b/apps/route-ops-web/src/maps/provider.ts @@ -1,5 +1,4 @@ -import type { BootstrapPayload, MapProviderMode, MapProviderStatus } from '../types'; -import { settingsCopy, resolveLocale } from '../i18n'; +import type { MapProviderStatus } from '../types'; export type MapReadiness = 'interactive_map' | 'provider_not_configured'; @@ -7,74 +6,3 @@ export function mapReadiness(input: { mapStatus: MapProviderStatus }): MapReadin if (input.mapStatus === 'not_configured') return 'provider_not_configured'; return 'interactive_map'; } - -export function providerModeLabel(mode: MapProviderMode, locale: string | null | undefined = 'en-CA'): string { - if (resolveLocale(locale) === 'ko-KR') { - const t = settingsCopy['ko-KR']; - if (mode === 'self_hosted') return t.selfHosted; - if (mode === 'public_allowlisted') return t.publicAllowlisted; - return t.notConfigured; - } - if (mode === 'self_hosted') return 'Self-hosted map'; - if (mode === 'public_allowlisted') return 'Public map provider allowlisted'; - return 'Map provider not configured'; -} - -export function providerStatusLabel(mapConfig: BootstrapPayload['mapConfig'], locale: string | null | undefined = 'en-CA'): string { - if (mapConfig.status === 'not_configured') return mapConfig.disabledReason ?? 'not_configured'; - return providerModeLabel(mapConfig.providerMode, locale); -} - -export function extractStyleEndpointUrls(manifest: unknown): string[] { - if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) return []; - const record = manifest as Record; - const endpoints: string[] = []; - collectStringEndpoint(record.sprite, endpoints); - collectStringEndpoint(record.glyphs, endpoints); - const sources = record.sources; - if (sources !== null && typeof sources === 'object' && !Array.isArray(sources)) { - for (const source of Object.values(sources as Record)) { - if (source === null || typeof source !== 'object' || Array.isArray(source)) continue; - const sourceRecord = source as Record; - collectStringEndpoint(sourceRecord.url, endpoints); - const tiles = sourceRecord.tiles; - if (Array.isArray(tiles)) { - for (const tile of tiles) collectStringEndpoint(tile, endpoints); - } - } - } - return [...new Set(endpoints)]; -} - -export function auditStyleEndpoints(endpoints: string[], allowedHosts: readonly string[]): { - externalHosts: string[]; - isAllowed: boolean; - providerMode: 'public_allowlisted' | 'self_hosted'; -} { - const externalHosts = [...new Set(endpoints.map(endpointHost).filter((host): host is string => host !== null))].sort(); - return { - externalHosts, - isAllowed: externalHosts.every((host) => allowedHosts.includes(host)), - providerMode: externalHosts.length === 0 ? 'self_hosted' : 'public_allowlisted' - }; -} - -function collectStringEndpoint(value: unknown, endpoints: string[]): void { - if (typeof value !== 'string' || value.trim() === '') return; - const endpoint = value.trim(); - endpoints.push(endpoint); - if (endpoint.startsWith('pmtiles://')) { - const nested = endpoint.slice('pmtiles://'.length); - if (nested !== '') endpoints.push(nested); - } -} - -function endpointHost(endpoint: string): string | null { - const normalized = endpoint.startsWith('pmtiles://') ? endpoint.slice('pmtiles://'.length) : endpoint; - if (!/^https?:\/\//iu.test(normalized)) return null; - try { - return new URL(normalized).host.toLowerCase(); - } catch { - return null; - } -} diff --git a/apps/route-ops-web/src/pages/OrdersPage.tsx b/apps/route-ops-web/src/pages/OrdersPage.tsx index f1476e51..a6c6f8bb 100644 --- a/apps/route-ops-web/src/pages/OrdersPage.tsx +++ b/apps/route-ops-web/src/pages/OrdersPage.tsx @@ -26,7 +26,6 @@ import { formatOrderItemName, formatOrderItemOptions, getOrderItemDisplayKey, - getOrderItemSemanticDisplayKey, getOrderItems, } from "../orderItems"; import { @@ -810,59 +809,6 @@ export function RoutePlanPanel(input: { ); } -export type OrderInventoryRow = { - orderRefs: string[]; - product: string; - quantity: number; -}; - -export function buildOrderInventoryRows( - orders: CanonicalOrderDto[], -): OrderInventoryRow[] { - const rows = new Map< - string, - { orderQuantities: Map; product: string; quantity: number } - >(); - for (const order of orders) { - const orderRef = formatInventoryOrderRef(order); - for (const item of getOrderItems(order.items)) { - const key = getOrderItemSemanticDisplayKey(item); - const options = formatOrderItemOptions(item); - const product = - options.length === 0 - ? formatOrderItemName(item) - : `${formatOrderItemName(item)} (${options})`; - const row = rows.get(key) ?? { - orderQuantities: new Map(), - product, - quantity: 0, - }; - row.quantity += item.quantity; - row.orderQuantities.set( - orderRef, - (row.orderQuantities.get(orderRef) ?? 0) + item.quantity, - ); - rows.set(key, row); - } - } - return [...rows.values()] - .map((row) => ({ - orderRefs: [...row.orderQuantities.entries()] - .sort(([first], [second]) => - first.localeCompare(second, undefined, { numeric: true }), - ) - .map(([orderRef, quantity]) => `${quantity}x ${orderRef}`), - product: row.product, - quantity: row.quantity, - })) - .sort((first, second) => first.product.localeCompare(second.product)); -} - -function formatInventoryOrderRef(order: CanonicalOrderDto): string { - const source = order.sourceOrderNumber ?? order.orderName; - return source.startsWith('#') ? source : `#${source}`; -} - function FilterBar({ filterOptions, filters, @@ -1878,16 +1824,6 @@ function statusMeaningForOrder( return t.metadataReview; } -export function formatOrderReceivedLabel( - order: CanonicalOrderDto, - locale: string | null | undefined = "en-CA", -): string { - const label = formatOrderReceivedLabelParts(order, locale); - return label.updated === null - ? label.created - : `${label.created}\n${label.updated}`; -} - export function formatOrderReceivedLabelParts( order: CanonicalOrderDto, locale: string | null | undefined = "en-CA", @@ -1975,88 +1911,6 @@ function formatRouteLabel( return humanizeToken(order.planningStatus, locale); } -export function getRouteRepairPrompt( - order: CanonicalOrderDto, - locale: string | null | undefined = "en-CA", -): { - canGeocode: boolean; - routeDetail: string; - statusDetail: string | null; - statusLabel: string; -} { - const t = getOrdersCopy(locale); - if (order.routePlanId !== null || order.planningStatus !== "UNPLANNED") { - return { - canGeocode: false, - routeDetail: t.statusLabels.alreadyPlanned, - statusDetail: - order.routePlanName ?? humanizeToken(order.planningStatus, locale), - statusLabel: t.statusLabels.planned, - }; - } - if (isRoutePlanEligible(order)) { - return { - canGeocode: false, - routeDetail: t.statusLabels.routeEligible, - statusDetail: null, - statusLabel: t.statusLabels.ready, - }; - } - if (isAddressReviewRequired(order)) { - return { - canGeocode: false, - routeDetail: t.statusLabels.addressReview, - statusDetail: t.statusDetails.verifyAddress, - statusLabel: t.statusLabels.addressReview, - }; - } - if (isDeliveryDateReviewRequired(order)) { - return { - canGeocode: false, - routeDetail: t.statusLabels.deliveryDateReview, - statusDetail: t.statusDetails.verifyDeliveryDate, - statusLabel: t.statusLabels.deliveryDateReview, - }; - } - if (order.deliveryDate === null) { - return { - canGeocode: false, - routeDetail: t.statusLabels.missingDeliveryDate, - statusDetail: t.statusDetails.enterDeliveryDate, - statusLabel: t.statusLabels.missingDeliveryDate, - }; - } - if (order.metadataResolved !== true) { - return { - canGeocode: false, - routeDetail: t.statusLabels.needsMetadata, - statusDetail: geocodeDetail(order, locale), - statusLabel: t.statusLabels.metadataReview, - }; - } - if (!hasResolvedCoordinates(order)) { - const canGeocode = hasGeocodableAddress(order); - return { - canGeocode, - routeDetail: canGeocode - ? t.statusLabels.needCoordinates - : t.statusLabels.needAddress, - statusDetail: canGeocode - ? t.statusDetails.useBulkGeocode - : t.statusDetails.enterAddressOrCoordinates, - statusLabel: canGeocode - ? t.statusLabels.needCoordinates - : t.statusLabels.missingAddress, - }; - } - return { - canGeocode: false, - routeDetail: t.statusLabels.notRouteEligible, - statusDetail: t.statusDetails.reviewRouteConstraints, - statusLabel: t.statusLabels.notRouteEligible, - }; -} - function geocodeDetail( order: CanonicalOrderDto, locale: string | null | undefined = "en-CA", @@ -3013,54 +2867,10 @@ function formatAddressSummary( }; } -function formatCoordinateSummary( - order: CanonicalOrderDto, - locale: string | null | undefined = "en-CA", -): { - primary: string; - secondary: string | null; -} { - const t = getOrdersCopy(locale); - if (hasResolvedCoordinates(order)) { - return { - primary: `${order.coordinates.latitude?.toFixed(6)}, ${order.coordinates.longitude?.toFixed(6)}`, - secondary: t.coordinatesReady, - }; - } - if (isAddressReviewRequired(order)) { - return { - primary: t.statusLabels.addressReview, - secondary: t.statusDetails.verifyAddress, - }; - } - if (hasGeocodableAddress(order)) { - return { - primary: t.coordinatesNeeded, - secondary: t.useBulkGeocodeFromList, - }; - } - return { - primary: t.addressRequired, - secondary: t.enterAddressBeforeGeocoding, - }; -} - function sanitizeId(value: string): string { return value.replace(/[^a-zA-Z0-9_-]+/gu, "-"); } -export function filterOrdersByRoutePlan( - orders: CanonicalOrderDto[], - routePlanId: string | null, -): CanonicalOrderDto[] | null { - if (routePlanId === null) return null; - return orders.filter((order) => order.routePlanId === routePlanId); -} - -export function buildRouteDetailPath(routePlanId: string): string { - return `/admin/ui/app/routes/${encodeURIComponent(routePlanId)}`; -} - function orderSelectedOrdersByDraft( selectedOrders: CanonicalOrderDto[], selectedOrderIds: ReadonlySet, @@ -3208,29 +3018,6 @@ export function buildVisibleSelectedOrderIds( .map((order) => order.orderId); } -export function buildRouteDraftSelection( - orders: CanonicalOrderDto[], - requestedSelectedOrderIds: ReadonlySet, - locale: string | null | undefined = "en-CA", -): { - deliveryDate: string | null; - orderIds: string[]; - routeType: OrderRouteTypeFilter | null; - warning: string | null; -} { - const ordersById = new Map(orders.map((order) => [order.orderId, order])); - const requestedOrders = [...requestedSelectedOrderIds] - .map((orderId) => ordersById.get(orderId)) - .filter((order): order is CanonicalOrderDto => order !== undefined); - - return { - deliveryDate: getRouteDraftSingleDeliveryDate(requestedOrders), - orderIds: requestedOrders.map((order) => order.orderId), - routeType: getRouteDraftSingleType(requestedOrders), - warning: null, - }; -} - export function getRouteDraftCreateReasons( selectedOrders: CanonicalOrderDto[], locale: string | null | undefined = "en-CA", @@ -3255,13 +3042,6 @@ export function getRouteDraftCreateReasons( return reasons; } -export function getRouteDraftFirstCreateReason( - selectedOrders: CanonicalOrderDto[], - locale: string | null | undefined = "en-CA", -): string | null { - return getRouteDraftCreateReasons(selectedOrders, locale)[0] ?? null; -} - function getRouteDraftSingleDeliveryDate( orders: CanonicalOrderDto[], ): string | null { @@ -3314,15 +3094,3 @@ function formatRoutePlanningType( ): string { return humanizeToken(value, locale); } - -function isRoutePlanEligible(order: CanonicalOrderDto): boolean { - if (order.routePlanId !== null || order.planningStatus !== "UNPLANNED") { - return false; - } - return ( - order.routeEligible === true || - (order.routeEligible !== false && - order.blockerReasons.length === 0 && - order.planningStatus === "UNPLANNED") - ); -} diff --git a/apps/route-ops-web/src/routeScopeConfig.ts b/apps/route-ops-web/src/routeScopeConfig.ts index 041a90e9..c9156d55 100644 --- a/apps/route-ops-web/src/routeScopeConfig.ts +++ b/apps/route-ops-web/src/routeScopeConfig.ts @@ -1,4 +1,4 @@ -import type { RouteScopeConfigDto, RouteScopeValueDto } from './types'; +import type { RouteScopeConfigDto } from './types'; export function defaultRouteScopeConfig(): RouteScopeConfigDto { return { @@ -62,37 +62,3 @@ export function defaultRouteScopeConfig(): RouteScopeConfigDto { version: 1 }; } - -export function normalizeRouteScopeConfig(config: RouteScopeConfigDto | null | undefined): RouteScopeConfigDto { - if (config === null || config === undefined) return defaultRouteScopeConfig(); - return { - deliverySessions: normalizeValues(config.deliverySessions, defaultRouteScopeConfig().deliverySessions), - serviceTypes: normalizeValues(config.serviceTypes, defaultRouteScopeConfig().serviceTypes), - timeWindow: { - endExample: config.timeWindow?.endExample ?? '21:00', - helpText: config.timeWindow?.helpText ?? 'Use 24-hour HH:mm format.', - startExample: config.timeWindow?.startExample ?? '17:00' - }, - version: 1 - }; -} - -export function activeRouteScopeValues(values: RouteScopeValueDto[]): RouteScopeValueDto[] { - return values.filter((value) => value.enabled); -} - -export function routeScopeValueSummary(values: RouteScopeValueDto[]): string { - return activeRouteScopeValues(values) - .map((item) => item.value) - .join(', '); -} - -function normalizeValues(values: RouteScopeValueDto[] | undefined, defaults: RouteScopeValueDto[]): RouteScopeValueDto[] { - const byValue = new Map((values ?? []).map((item) => [item.value, item])); - const output = defaults.map((fallback) => ({ ...fallback, ...byValue.get(fallback.value), builtIn: true, enabled: true })); - for (const item of values ?? []) { - if (output.some((existing) => existing.value === item.value)) continue; - output.push({ ...item, builtIn: false }); - } - return output; -} diff --git a/apps/route-ops-web/src/state.ts b/apps/route-ops-web/src/state.ts index 61012b7e..fca5c699 100644 --- a/apps/route-ops-web/src/state.ts +++ b/apps/route-ops-web/src/state.ts @@ -1,8 +1,6 @@ import type { BootstrapPayload, CanonicalOrderDto, - MapProviderStatus, - RoutePlanDetailDto, RouteStopDto, StoreSettingsDto } from './types'; @@ -48,50 +46,6 @@ export type OrderWorksetContext = { today?: string; }; -export type OrderWorksetUnavailableReason = - | 'already_planned' - | 'address_review' - | 'completed_or_cancelled' - | 'delivery_date_review' - | 'different_delivery_date' - | 'different_route_scope' - | 'missing_address' - | 'missing_coordinates' - | 'missing_delivery_date' - | 'missing_route_scope' - | 'needs_review'; - -export type OrderWorksetReason = { - code: OrderWorksetUnavailableReason; - label: string; -}; - -export type OrderWorksetSummary = { - reasonLabels: string[]; - reasonsByCode: Record; - selectableCount: number; - selectableOrderIds: string[]; - selectedCount: number; - unavailableCount: number; -}; - -export const ORDER_WORKSET_REASON_LABELS: Record< - OrderWorksetUnavailableReason, - string -> = { - already_planned: 'Already planned', - address_review: 'Address Review', - completed_or_cancelled: 'Completed/cancelled', - delivery_date_review: 'Delivery date review', - different_delivery_date: 'Different delivery date', - different_route_scope: 'Different delivery session', - missing_address: 'Missing address', - missing_coordinates: 'Missing coordinates', - missing_delivery_date: 'Missing delivery date', - missing_route_scope: 'Missing route scope', - needs_review: 'Other metadata review' -}; - export function createDefaultOrderFilters(): OrderFilterState { return { deliveryArea: '', @@ -140,10 +94,6 @@ export function buildOrderFetchQuery(filters: OrderFilters): string { return buildOrderQuery(serverFilters); } -export function buildAreaOptionSourceFilters(filters: OrderFilters): OrderFilters { - return { ...filters, deliveryArea: '' }; -} - export function deriveAreaFilterOptions(orders: CanonicalOrderDto[]): string[] { return Array.from( new Set( @@ -644,125 +594,6 @@ export function matchesOrderTab( return !isPlannedOrder(order) && isRoutePlanEligibleForWorkset(order); } -export function getOrderWorksetUnavailableReasons( - order: CanonicalOrderDto, - context: OrderWorksetContext = {} -): OrderWorksetReason[] { - const reasons = new Set(); - if (isCompletedOrCancelledOrder(order)) reasons.add('completed_or_cancelled'); - if (isPlannedOrder(order)) reasons.add('already_planned'); - if (isDeliveryDateReviewRequired(order)) { - reasons.add('delivery_date_review'); - } else if (order.deliveryDate === null) { - reasons.add('missing_delivery_date'); - } - if (!hasRouteScope(order)) reasons.add('missing_route_scope'); - if (!hasResolvedCoordinates(order)) { - reasons.add(readCoordinateUnavailableReason(order)); - } - if (hasUnrepresentedNeedsReviewReason(order, reasons)) { - reasons.add('needs_review'); - } - if ( - context.routeDate !== undefined && - context.routeDate !== null && - order.deliveryDate !== null && - order.deliveryDate !== context.routeDate - ) { - reasons.add('different_delivery_date'); - } - const routeScopeKey = getRouteScopeKey(order); - if ( - context.routeScopeKey !== undefined && - context.routeScopeKey !== null && - routeScopeKey !== null && - routeScopeKey !== context.routeScopeKey - ) { - reasons.add('different_route_scope'); - } - return [...reasons].map((code) => ({ - code, - label: ORDER_WORKSET_REASON_LABELS[code] - })); -} - -export function formatOrderWorksetUnavailableReasons( - reasons: OrderWorksetReason[], - locale: string | null | undefined = 'en-CA' -): OrderWorksetReason[] { - const labels = getStateCopy(locale).worksetReasons; - return reasons.map((reason) => ({ ...reason, label: labels[reason.code] })); -} - -export function isOrderWorksetEligible( - order: CanonicalOrderDto, - context: OrderWorksetContext = {} -): boolean { - return ( - getOrderWorksetUnavailableReasons(order, context).length === 0 && - isRoutePlanEligibleForWorkset(order) - ); -} - -export function summarizeOrderWorkset( - orders: CanonicalOrderDto[], - selectedOrderIds: ReadonlySet, - context: OrderWorksetContext = {}, - locale: string | null | undefined = 'en-CA' -): OrderWorksetSummary { - const labels = getStateCopy(locale).worksetReasons; - const reasonsByCode = Object.fromEntries( - Object.keys(ORDER_WORKSET_REASON_LABELS).map((code) => [code, 0]) - ) as Record; - const selectableOrderIds: string[] = []; - for (const order of orders) { - const reasons = getOrderWorksetUnavailableReasons(order, context); - if (reasons.length === 0 && isRoutePlanEligibleForWorkset(order)) { - selectableOrderIds.push(order.orderId); - continue; - } - for (const reason of reasons) reasonsByCode[reason.code] += 1; - } - const reasonLabels = ( - Object.entries(reasonsByCode) as Array< - [OrderWorksetUnavailableReason, number] - > - ) - .filter(([, count]) => count > 0) - .map(([code, count]) => `${labels[code]} ${count}`); - return { - reasonLabels, - reasonsByCode, - selectableCount: selectableOrderIds.length, - selectableOrderIds, - selectedCount: selectableOrderIds.filter((orderId) => - selectedOrderIds.has(orderId) - ).length, - unavailableCount: orders.length - selectableOrderIds.length - }; -} - -export function summarizeSelection( - orders: CanonicalOrderDto[], - selectedOrderIds: ReadonlySet -): { - blockers: string[]; - readySelected: CanonicalOrderDto[]; -} { - const selected = orders.filter((order) => selectedOrderIds.has(order.orderId)); - const blockers = selected.flatMap((order) => - order.blockerReasons.map((reason) => `${order.orderName}: ${reason}`) - ); - return { - blockers, - readySelected: selected.filter( - (order) => - order.blockerReasons.length === 0 && - order.planningStatus === 'UNPLANNED' - ) - }; -} - function isRoutePlanEligibleForWorkset(order: CanonicalOrderDto): boolean { if (isPlannedOrder(order)) return false; return ( @@ -792,51 +623,6 @@ function isNeedsReviewOrder(order: CanonicalOrderDto): boolean { ); } -const BLOCKER_WORKSET_REASON_MAP: Partial< - Record -> = { - ambiguous_delivery_day: 'delivery_date_review', - delivery_date_weekday_mismatch: 'delivery_date_review', - delivery_date_weekday_unverified: 'delivery_date_review', - delivery_day_unparsed: 'delivery_date_review', - missing_address: 'missing_address', - missing_coordinates: 'missing_coordinates', - missing_delivery_date: 'missing_delivery_date', - missing_route_scope: 'missing_route_scope' -}; - -function hasUnrepresentedNeedsReviewReason( - order: CanonicalOrderDto, - reasons: ReadonlySet -): boolean { - const hasSpecificMetadataReason = - reasons.has('address_review') || - reasons.has('delivery_date_review') || - reasons.has('missing_address') || - reasons.has('missing_coordinates') || - reasons.has('missing_delivery_date') || - reasons.has('missing_route_scope'); - const hasUnrepresentedBlocker = order.blockerReasons.some((blocker) => { - if (blocker === 'missing_coordinates' && reasons.has('address_review')) { - return false; - } - if (blocker === 'missing_delivery_date' && reasons.has('delivery_date_review')) { - return false; - } - const mappedReason = BLOCKER_WORKSET_REASON_MAP[blocker]; - return mappedReason === undefined || !reasons.has(mappedReason); - }); - if (hasUnrepresentedBlocker) return true; - if ( - order.health === 'needs_review' || - order.metadataResolved === false || - order.routeEligible === false - ) { - return !hasSpecificMetadataReason; - } - return false; -} - function isCompletedOrCancelledOrder(order: CanonicalOrderDto): boolean { return ( order.deliveryStatus === 'completed' || @@ -849,17 +635,6 @@ function hasRouteScope(order: CanonicalOrderDto): boolean { return isPresent(order.serviceType) && isPresent(order.deliverySession); } -function getRouteScopeKey(order: CanonicalOrderDto): string | null { - if (!hasRouteScope(order) || order.deliveryDate === null) return null; - return [ - order.deliveryDate, - order.serviceType, - order.deliverySession, - order.timeWindowStart ?? '', - order.timeWindowEnd ?? '' - ].join('|'); -} - function hasResolvedCoordinates(order: CanonicalOrderDto): boolean { return ( typeof order.coordinates.latitude === 'number' && @@ -869,13 +644,6 @@ function hasResolvedCoordinates(order: CanonicalOrderDto): boolean { ); } -function readCoordinateUnavailableReason( - order: CanonicalOrderDto -): OrderWorksetUnavailableReason { - if (!hasGeocodableAddress(order)) return 'missing_address'; - return isAddressReviewRequired(order) ? 'address_review' : 'missing_coordinates'; -} - export function isAddressReviewRequired(order: CanonicalOrderDto): boolean { if (hasResolvedCoordinates(order)) return false; if (!hasGeocodableAddress(order)) return false; @@ -942,10 +710,6 @@ export function moveStop(stops: RouteStopDto[], deliveryStopId: string, directio } -export function moveStopBefore(stops: RouteStopDto[], draggedStopId: string, targetStopId: string): RouteStopDto[] { - return moveStopToDropPosition(stops, draggedStopId, targetStopId, 'before'); -} - export type StopDropPosition = 'before' | 'after'; export function moveStopToDropPosition( @@ -986,46 +750,6 @@ export function resequenceStops(stops: RouteStopDto[]): RouteStopDto[] { return stops.map((stop, index) => ({ ...stop, sequence: index + 1 })); } -export function deriveRouteStats(detail: RoutePlanDetailDto | null): { - attempted: number; - completed: number; - missingCoordinates: number; - stops: number; -} { - if (detail === null) return { attempted: 0, completed: 0, missingCoordinates: 0, stops: 0 }; - return detail.stops.reduce( - (stats, stop) => ({ - attempted: stats.attempted + (stop.status.toUpperCase() === 'ATTEMPTED' ? 1 : 0), - completed: stats.completed + (stop.status.toUpperCase() === 'COMPLETED' ? 1 : 0), - missingCoordinates: stats.missingCoordinates + (stop.coordinates.latitude === null || stop.coordinates.longitude === null ? 1 : 0), - stops: stats.stops + 1 - }), - { attempted: 0, completed: 0, missingCoordinates: 0, stops: 0 } - ); -} - -export function geometryLabel(detail: RoutePlanDetailDto | null, routerStatus: MapProviderStatus, locale: string | null | undefined = 'en-CA'): string { - const t = getStateCopy(locale).geometry; - if (detail === null) return t.noRouteSelected; - if (detail.routeGeometry !== null) return t.roadGeometry; - if (countRoutableRoutePoints(detail) < 2) return t.noCoordinates; - if (routerStatus === 'not_configured') return t.routerNotConfigured; - return t.roadGeometryUnavailable; -} - -function countRoutableRoutePoints(detail: RoutePlanDetailDto): number { - const depot = detail.routePlan.depot; - const points = [ - depot === null ? null : { latitude: depot.latitude, longitude: depot.longitude }, - ...detail.stops.map((stop) => stop.coordinates) - ]; - return points.filter((point): point is { latitude: number; longitude: number } => - point !== null && - Number.isFinite(point.latitude) && - Number.isFinite(point.longitude) - ).length; -} - export function hideSetupActions(bootstrap: BootstrapPayload): boolean { return bootstrap.mode === 'plugin'; } diff --git a/apps/route-ops-web/src/types.ts b/apps/route-ops-web/src/types.ts index 125e8224..9afe4e42 100644 --- a/apps/route-ops-web/src/types.ts +++ b/apps/route-ops-web/src/types.ts @@ -463,45 +463,6 @@ export type RouteSaveResponse = RoutePlanDetailDto & { saveOperations?: RouteSaveOperationDto[]; }; -export type RouteOptimizationJobStatus = - | "APPLIED" - | "CANCELLED" - | "FAILED" - | "QUEUED" - | "RUNNING" - | "TIMEOUT"; - -export type RouteOptimizationJobStep = - | "APPLYING_RESULT" - | "CALLING_ENGINE" - | "COMPLETED" - | "QUEUED"; - -export type RouteOptimizationJobDto = { - appliedAt: string | null; - createdAt: string; - createdBy: string | null; - currentStep: RouteOptimizationJobStep; - elapsedMs: number | null; - engineResultSequence: unknown; - errorCode: string | null; - errorMessage: string | null; - finishedAt: string | null; - id: string; - invalidatedReason: string | null; - routePlanId: string; - shopId: string; - startedAt: string | null; - status: RouteOptimizationJobStatus; - timeoutBudgetMs: number; - traceId: string; - updatedAt: string; -}; - -export type RouteOptimizationJobResponse = { - job: RouteOptimizationJobDto | null; -}; - export type DriverDto = { appLinked: boolean; authStatus: string; @@ -575,44 +536,6 @@ export type RouteScopeConfigDto = { version: 1; }; -export type GeocodeOrderResponse = { - geocode: { - cached: boolean; - ok: true; - result: { - addressLabel: string; - latitude: number; - longitude: number; - provider: string; - providerPlaceId: string | null; - rawLabel?: string | null; - }; - }; - order?: CanonicalOrderDto; -}; - -export type BulkGeocodeJobDto = { - completedAt: string | null; - counts: { - alreadyHasCoordinates: number; - attempted: number; - failed: number; - matched: number; - noAddress: number; - succeeded: number; - }; - createdAt: string; - error: string | null; - jobId: string; - results: Array>; - status: "accepted" | "completed" | "failed" | "running"; - updatedAt: string; -}; - -export type BulkGeocodeOrdersResponse = { - geocode: BulkGeocodeJobDto; -}; - export type WooSyncRunDto = { acceptedAt: string; completedAt: string | null; diff --git a/apps/route-ops-web/tests/drivers-routes.test.tsx b/apps/route-ops-web/tests/drivers-routes.test.tsx index 82f3be6b..b6b5b45b 100644 --- a/apps/route-ops-web/tests/drivers-routes.test.tsx +++ b/apps/route-ops-web/tests/drivers-routes.test.tsx @@ -5,14 +5,11 @@ import { afterEach, describe, expect, test, vi } from 'vitest'; import { acknowledgeNotification, ApiError, - createRouteOptimizationJob, deleteDriver, generateRouteGroupingChildRoutes, - getLatestRouteOptimizationJob, getNotifications, getOperationalHealth, getRouteOperationalState, - getRouteOptimizationJob, openNotificationChangeStream, markNotificationRead, publishRoute, @@ -52,7 +49,6 @@ import type { BootstrapPayload, DriverDto, RouteGroupingSummaryDto, - RouteOptimizationJobDto, RoutePlanDetailDto, RoutePlanSummaryDto, StoreSettingsDto, @@ -1052,53 +1048,6 @@ describe('Route Ops driver invite and route assignment UI helpers', () => { expect(html).not.toContain('route-optimize-button'); }); - test('route optimization job API helpers use the protected Route Ops endpoints', async () => { - const fetchMock = vi.fn((url: string) => - Promise.resolve( - new Response( - JSON.stringify({ data: { job: routeOptimizationJobFixture({ id: url.includes('latest') ? 'latest-job-id' : 'job-id' }) }, error: null }), - { - headers: { 'Content-Type': 'application/json' }, - status: 200, - }, - ), - ), - ); - vi.stubGlobal('fetch', fetchMock); - vi.stubGlobal('window', { location: { search: '?shopDomain=tenant-a.example.test' } }); - - await createRouteOptimizationJob('route/id', 'csrf-token'); - await getLatestRouteOptimizationJob('route/id'); - await getRouteOptimizationJob('route/id', 'job/id'); - - expect(fetchMock).toHaveBeenNthCalledWith( - 1, - '/admin/ui/app/api/routes/route%2Fid/optimize-jobs?shopDomain=tenant-a.example.test', - expect.objectContaining({ - body: '{}', - credentials: 'same-origin', - headers: expect.objectContaining({ 'X-CSRF-Token': 'csrf-token' }), - method: 'POST', - }), - ); - expect(fetchMock).toHaveBeenNthCalledWith( - 2, - '/admin/ui/app/api/routes/route%2Fid/optimize-jobs/latest?shopDomain=tenant-a.example.test', - expect.objectContaining({ - credentials: 'same-origin', - headers: { Accept: 'application/json' }, - }), - ); - expect(fetchMock).toHaveBeenNthCalledWith( - 3, - '/admin/ui/app/api/routes/route%2Fid/optimize-jobs/job%2Fid?shopDomain=tenant-a.example.test', - expect.objectContaining({ - credentials: 'same-origin', - headers: { Accept: 'application/json' }, - }), - ); - }); - test('route grouping child generation posts to the protected Route Ops API with CSRF', async () => { const fetchMock = vi.fn(() => Promise.resolve( @@ -1588,30 +1537,6 @@ function bootstrap(overrides: Partial = {}): BootstrapPayload }; } -function routeOptimizationJobFixture(overrides: Partial = {}): RouteOptimizationJobDto { - return { - appliedAt: null, - createdAt: '2026-06-10T07:00:00.000Z', - createdBy: 'web-operator', - currentStep: 'QUEUED', - elapsedMs: null, - engineResultSequence: null, - errorCode: null, - errorMessage: null, - finishedAt: null, - id: 'job-id', - invalidatedReason: null, - routePlanId: 'route-plan-id', - shopId: 'shop-id', - startedAt: null, - status: 'QUEUED', - timeoutBudgetMs: 180000, - traceId: 'route-opt:route-plan-id:test', - updatedAt: '2026-06-10T07:00:00.000Z', - ...overrides, - }; -} - function routePlanDetailFixture(overrides: Partial = {}): RoutePlanDetailDto { return { routeGeometry: null, diff --git a/apps/route-ops-web/tests/map-helpers.test.ts b/apps/route-ops-web/tests/map-helpers.test.ts index 12bd001c..1d965f54 100644 --- a/apps/route-ops-web/tests/map-helpers.test.ts +++ b/apps/route-ops-web/tests/map-helpers.test.ts @@ -3,9 +3,9 @@ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { buildOrdersMapFeatureCollection, buildRouteDropoffPointFeatureCollection, buildRouteGeometryFeature, buildRouteStopMarkerFeatureCollection, fitBoundsForPoints, getRouteDropoffPoints, getRouteFitPoints, getRouteMapPoints } from '../src/maps/geojson'; -import { auditStyleEndpoints, extractStyleEndpointUrls, mapReadiness, providerStatusLabel } from '../src/maps/provider'; +import { mapReadiness } from '../src/maps/provider'; import { installPmtilesProtocol } from '../src/maps/pmtiles'; -import type { BootstrapPayload, CanonicalOrderDto, RoutePlanDetailDto } from '../src/types'; +import type { CanonicalOrderDto, RoutePlanDetailDto } from '../src/types'; describe('route ops map helpers', () => { test('builds order GeoJSON with candidate/review pin classification and filters invalid coordinates', () => { @@ -259,25 +259,6 @@ describe('route ops map helpers', () => { ]); }); - test('extracts style manifest endpoints and classifies public vs self-hosted hosts', () => { - const endpoints = extractStyleEndpointUrls({ - glyphs: 'https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf', - sources: { - local: { tiles: ['/admin/ui/app/vendor/tiles/{z}/{x}/{y}.pbf'] }, - pmtiles: { url: 'pmtiles://https://example.test/world.pmtiles' } - }, - sprite: '/admin/ui/app/vendor/sprites/clever' - }); - expect(endpoints).toContain('https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf'); - expect(endpoints).toContain('https://example.test/world.pmtiles'); - expect(auditStyleEndpoints(endpoints, ['tiles.openfreemap.org']).isAllowed).toBe(false); - expect(auditStyleEndpoints(endpoints, ['tiles.openfreemap.org', 'example.test'])).toEqual({ - externalHosts: ['example.test', 'tiles.openfreemap.org'], - isAllowed: true, - providerMode: 'public_allowlisted' - }); - }); - test('does not double-install PMTiles protocol in one browser session', () => { const originalWindow = (globalThis as { window?: Window }).window; (globalThis as { window?: Partial }).window = { __cleverRouteOpsPmtilesProtocolInstalled: false }; @@ -292,11 +273,6 @@ describe('route ops map helpers', () => { } }); - test('labels configured and fallback provider states', () => { - expect(providerStatusLabel(bootstrap({ providerMode: 'public_allowlisted', status: 'configured' }))).toBe('Public map provider allowlisted'); - expect(providerStatusLabel(bootstrap({ disabledReason: 'public_provider_mode_not_enabled', providerMode: null, status: 'not_configured' }))).toBe('public_provider_mode_not_enabled'); - }); - test('keeps configured providers interactive before markers exist', () => { expect(mapReadiness({ mapStatus: 'not_configured' })).toBe('provider_not_configured'); expect(mapReadiness({ mapStatus: 'configured' })).toBe('interactive_map'); @@ -363,15 +339,3 @@ function routeDetail(): RoutePlanDetailDto { ] }; } - -function bootstrap(mapConfig: Partial): BootstrapPayload['mapConfig'] { - return { - allowedHosts: [], - attribution: null, - providerMode: null, - status: 'not_configured', - styleAudit: null, - styleUrl: null, - ...mapConfig - }; -} diff --git a/apps/route-ops-web/tests/orders-table.test.tsx b/apps/route-ops-web/tests/orders-table.test.tsx index e6d10a93..d280ea70 100644 --- a/apps/route-ops-web/tests/orders-table.test.tsx +++ b/apps/route-ops-web/tests/orders-table.test.tsx @@ -2,15 +2,10 @@ import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, test } from "vitest"; import { - buildRouteDetailPath, buildEditableMetadataFields, buildNextPlanSelection, - buildOrderInventoryRows, buildOrderMapMarkerStates, - buildRouteDraftSelection, buildVisibleSelectedOrderIds, - filterOrdersByRoutePlan, - getRouteDraftFirstCreateReason, getRouteDraftCreateReasons, normalizeOrderMetadataPatchForFields, OrderDetailChoiceDropdown, @@ -19,21 +14,17 @@ import { formatBlockerReason, formatMethodLabel, formatMethodStatusLabel, - formatOrderReceivedLabel, formatOperationalStatus, formatTableReadinessStatus, formatPaymentStatusLabel, - getRouteRepairPrompt, ORDERS_TABLE_COLUMN_COUNT, RoutePlanPanel, OrderTable, type OrderMetadataPatch, } from "../src/pages/OrdersPage"; import { - getOrderBlockerLabels, + getOrderDetailLabels, getOrderFieldLabels, - orderBlockerLabels, - orderFieldLabels, } from "../src/i18n"; import { createDefaultOrderFilters } from "../src/state"; import type { @@ -103,27 +94,6 @@ describe("Orders compact operations table", () => { expect(html).not.toContain("Route eligible"); }); - test("formats source-created order labels with localized update markers", () => { - expect(formatOrderReceivedLabel(orderFixture(), "en-CA")).toBe( - "2026-06-04 THU\nupdated 2026-06-05 FRI", - ); - expect(formatOrderReceivedLabel(orderFixture(), "ko-KR")).toBe( - "2026-06-04 목요일\n수정 2026-06-05 금요일", - ); - expect( - formatOrderReceivedLabel( - orderFixture({ sourceUpdatedDate: "2026-06-04" }), - "en-CA", - ), - ).toBe("2026-06-04 THU"); - expect( - formatOrderReceivedLabel( - orderFixture({ sourceCreatedDate: null, sourceUpdatedDate: null }), - "en-CA", - ), - ).toBe("—"); - }); - test("keeps the order table mounted during filter refreshes to avoid scroll jumps", () => { const html = renderOrderTable([orderFixture()], { loading: true, @@ -201,37 +171,6 @@ describe("Orders compact operations table", () => { }); - test("builds a compact printable inventory table from filtered order items", () => { - const rows = buildOrderInventoryRows([ - orderFixture({ - orderName: "#1001", - sourceOrderNumber: "1001", - items: [ - { name: "Soup", options: [], productId: 1, quantity: 1, sku: null, variationId: 0 }, - { name: "Kimchi", options: [{ key: "size", value: "Large" }], productId: 2, quantity: 2, sku: "K", variationId: 0 }, - ], - }), - orderFixture({ - orderName: "#1002", - sourceOrderNumber: "1002", - items: [ - { name: "Soup", options: [], productId: 1, quantity: 3, sku: null, variationId: 0 }, - ], - }), - ]); - - expect(rows).toContainEqual({ - orderRefs: ["1x #1001", "3x #1002"], - product: "Soup", - quantity: 4, - }); - expect(rows).toContainEqual({ - orderRefs: ["2x #1001"], - product: "Kimchi (size: Large)", - quantity: 2, - }); - }); - test("renders reliable order total in order detail only", () => { const html = renderOrderTable( [orderFixture({ currencyCode: "CAD", totalPriceAmount: "42.50" })], @@ -413,12 +352,6 @@ describe("Orders compact operations table", () => { expect(html).not.toContain("Geocode & add"); expect(html).not.toContain("Geocode and add order"); expect(html).not.toContain("Use bulk geocode"); - expect(getRouteRepairPrompt(missingCoordinates)).toEqual({ - canGeocode: true, - routeDetail: "Need coordinates", - statusDetail: "use bulk geocode", - statusLabel: "Need coordinates", - }); expect(formatOperationalStatus(missingCoordinates).label).toBe( "Need coordinates", ); @@ -452,12 +385,6 @@ describe("Orders compact operations table", () => { expect(html).toContain('name="address1"'); expect(html).not.toContain("use bulk geocode"); expect(html).not.toContain("Use Bulk geocode from the order list."); - expect(getRouteRepairPrompt(addressReview)).toEqual({ - canGeocode: false, - routeDetail: "Address Review", - statusDetail: "Verify address", - statusLabel: "Address Review", - }); expect(formatOperationalStatus(addressReview)).toEqual( expect.objectContaining({ detail: "Verify address", @@ -508,12 +435,6 @@ describe("Orders compact operations table", () => { expect(formatOperationalStatus(dateReview).meaning).toContain( "A delivery date hint exists", ); - expect(getRouteRepairPrompt(dateReview)).toEqual({ - canGeocode: false, - routeDetail: "Delivery date review", - statusDetail: "Verify delivery date", - statusLabel: "Delivery date review", - }); expect(html).not.toContain("Delivery date review"); expect(html).toContain("Verify delivery date"); expect(html).not.toContain("Warning meaning: A delivery date hint exists"); @@ -911,6 +832,8 @@ describe("Orders compact operations table", () => { }); test("centralizes order i18n labels and avoids raw fallback labels", () => { + const orderFieldLabels = getOrderFieldLabels("en-CA"); + const orderBlockerLabels = getOrderDetailLabels("en-CA").blockerReasons; expect(orderFieldLabels["meta_data._tomatono_delivery_day"]).toBe( "Delivery day", ); @@ -923,7 +846,7 @@ describe("Orders compact operations table", () => { ); expect(orderBlockerLabels.missing_coordinates).toBe("Need coordinates"); expect(getOrderFieldLabels("ko-KR").address1).toBe("도로명 주소"); - expect(getOrderBlockerLabels("ko-KR").missing_coordinates).toBe( + expect(getOrderDetailLabels("ko-KR").blockerReasons.missing_coordinates).toBe( "좌표 필요", ); expect(formatDiagnosticPathLabel("unknown.path")).toBe("Order metadata"); @@ -1054,7 +977,7 @@ describe("Orders compact operations table", () => { expect(html).not.toContain("Missing delivery date"); }); - test("route draft selection keeps every selected order and validates create separately", () => { + test("route draft validation reports mismatched delivery dates and types", () => { const first = orderFixture({ orderId: "first" }); const sameScope = orderFixture({ orderId: "same-scope", @@ -1074,21 +997,8 @@ describe("Orders compact operations table", () => { timeWindowStart: null, }); - const draft = buildRouteDraftSelection( - [first, sameScope, otherDate, otherSession], - new Set(["first", "same-scope", "other-date", "other-session"]), - ); - - expect(draft.deliveryDate).toBeNull(); - expect(draft.orderIds).toEqual([ - "first", - "same-scope", - "other-date", - "other-session", - ]); - expect(draft.warning).toBeNull(); - expect(getRouteDraftFirstCreateReason([first, sameScope])).toBeNull(); - expect(getRouteDraftFirstCreateReason([first])).toBeNull(); + expect(getRouteDraftCreateReasons([first, sameScope])).toEqual([]); + expect(getRouteDraftCreateReasons([first])).toEqual([]); expect( getRouteDraftCreateReasons([first, otherDate, otherSession]), ).toEqual([ @@ -1134,17 +1044,12 @@ describe("Orders compact operations table", () => { const third = orderFixture({ orderId: "third", orderName: "#11455" }); const filters = createDefaultOrderFilters(); - const clickedDraft = buildRouteDraftSelection( - [first, second, third], - new Set(["third", "first", "second"]), - ); - - expect(clickedDraft.orderIds).toEqual(["third", "first", "second"]); + const selectedOrderIds = new Set(["third", "first", "second"]); const clickedMarkers = buildOrderMapMarkerStates({ filters, orders: [first, second, third], - selectedOrderIds: new Set(clickedDraft.orderIds), + selectedOrderIds, worksetContext: { scope: "planning" }, }); @@ -1163,39 +1068,6 @@ describe("Orders compact operations table", () => { new Set(["delivery", "pickup"]), ), ).toEqual(["delivery"]); - expect( - buildRouteDraftSelection( - [delivery, pickup], - new Set(buildVisibleSelectedOrderIds([delivery], new Set(["delivery", "pickup"]))), - ), - ).toMatchObject({ - orderIds: ["delivery"], - routeType: "delivery", - }); - }); - - test("planned route helpers filter orders by route and build the route detail URL", () => { - const routeOrder = orderFixture({ - orderId: "route-order", - orderName: "#11460", - planningStatus: "PLANNED", - routePlanId: "route/id", - routePlanName: "Route 1", - }); - const otherOrder = orderFixture({ - orderId: "other-order", - orderName: "#11461", - }); - - expect( - filterOrdersByRoutePlan([routeOrder, otherOrder], "route/id")?.map( - (order) => order.orderId, - ), - ).toEqual(["route-order"]); - expect(filterOrdersByRoutePlan([routeOrder, otherOrder], null)).toBeNull(); - expect(buildRouteDetailPath("route/id")).toBe( - "/admin/ui/app/routes/route%2Fid", - ); }); test("planned route markers stay blue and unfaded even when subfilters differ", () => { diff --git a/apps/route-ops-web/tests/state.test.ts b/apps/route-ops-web/tests/state.test.ts index 1338cc69..daf0643a 100644 --- a/apps/route-ops-web/tests/state.test.ts +++ b/apps/route-ops-web/tests/state.test.ts @@ -11,15 +11,12 @@ import { } from '../src/settingsUi'; import { applyClientOrderFilters, - buildAreaOptionSourceFilters, + applyClientOrderFiltersExcept, buildOrderFetchQuery, buildOrderQuery, createDefaultOrderFilters, deriveAreaFilterOptions, deriveOrderFilterOptions, - deriveRouteStats, - geometryLabel, - getOrderWorksetUnavailableReasons, hasStopSequenceChanged, hideSetupActions, isAddressReviewRequired, @@ -28,7 +25,6 @@ import { matchesPlanningScope, mergeOrderListsById, moveStop, - moveStopBefore, moveStopToDropPosition, moveStopToSequence, pruneOrderFilters, @@ -37,11 +33,9 @@ import { selectOrdersForClientFilters, shouldLoadHistoryOrders, storeSettingsToDepotPoint, - summarizeOrderWorkset, - summarizeSelection, toggleWeekdayDeliveryDates } from '../src/state'; -import type { BootstrapPayload, CanonicalOrderDto, RoutePlanDetailDto, RouteStopDto } from '../src/types'; +import type { BootstrapPayload, CanonicalOrderDto, RouteStopDto } from '../src/types'; describe('route ops web state helpers', () => { test('manages route ops UI reminder and template helpers without rendering real order values', () => { @@ -250,7 +244,6 @@ describe('route ops web state helpers', () => { deliveryArea: 'Toronto West', deliveryDate: '2026-05-27' }; - const sourceFilters = buildAreaOptionSourceFilters(filters); const orders = [ order({ deliveryArea: 'Toronto West', deliveryDate: '2026-05-27', orderId: 'west' }), order({ deliveryArea: 'Toronto East', deliveryDate: '2026-05-27', orderId: 'east' }), @@ -259,8 +252,7 @@ describe('route ops web state helpers', () => { order({ deliveryArea: null, deliveryDate: '2026-05-27', orderId: 'missing-area' }), ]; - expect(sourceFilters).toEqual(expect.objectContaining({ deliveryArea: '', deliveryDate: '2026-05-27' })); - const sourceOrders = applyClientOrderFilters(orders, sourceFilters); + const sourceOrders = applyClientOrderFiltersExcept(orders, filters, 'deliveryArea'); expect(sourceOrders.map((item) => item.orderId)).toEqual(['west', 'east', 'blank', 'missing-area']); expect(deriveAreaFilterOptions(sourceOrders)).toEqual(['Toronto East', 'Toronto West']); @@ -280,11 +272,10 @@ describe('route ops web state helpers', () => { ); }); - test('classifies planning scope tabs and workset availability reasons', () => { + test('classifies planning scope tabs', () => { const ready = order({ deliveryDate: '2026-05-29', orderId: 'ready' }); const planned = order({ deliveryDate: '2026-05-29', orderId: 'planned', planningStatus: 'PLANNED', routePlanId: 'route-1' }); const missingDate = order({ blockerReasons: ['missing_delivery_date'], deliveryDate: null, metadataResolved: false, orderId: 'missing-date', routeEligible: false }); - const metadataReview = order({ blockerReasons: ['missing_delivery_area'], metadataResolved: false, orderId: 'metadata-review', routeEligible: false }); const completed = order({ deliveryDate: '2026-05-29', deliveryStatus: 'completed', orderId: 'completed' }); expect(matchesPlanningScope(ready, '2026-05-29')).toBe(true); @@ -292,20 +283,6 @@ describe('route ops web state helpers', () => { expect(matchesOrderTab(ready, 'unplanned', '2026-05-29')).toBe(true); expect(matchesOrderTab(planned, 'planned', '2026-05-29')).toBe(true); expect(matchesOrderTab(missingDate, 'needs_review', '2026-05-29')).toBe(true); - - expect(getOrderWorksetUnavailableReasons(planned, { scope: 'planning' }).map((reason) => reason.code)).toContain('already_planned'); - expect(getOrderWorksetUnavailableReasons(ready, { scope: 'history' })).toEqual([]); - expect(getOrderWorksetUnavailableReasons(missingDate, { scope: 'history' }).map((reason) => reason.code)).toEqual(['missing_delivery_date']); - expect(getOrderWorksetUnavailableReasons(metadataReview, { scope: 'history' }).map((reason) => reason.code)).toContain('needs_review'); - const summary = summarizeOrderWorkset([ready, planned, missingDate, metadataReview], new Set(['ready']), { scope: 'planning' }); - expect(summary).toEqual(expect.objectContaining({ selectableCount: 1, selectedCount: 1, unavailableCount: 3 })); - expect(summary.reasonLabels.join(' ')).toContain('Already planned'); - expect(summary.reasonLabels.join(' ')).toContain('Missing delivery date'); - expect(summary.reasonLabels.join(' ')).toContain('Other metadata review'); - const koreanSummary = summarizeOrderWorkset([ready, planned, missingDate, metadataReview], new Set(['ready']), { scope: 'planning' }, 'ko-KR'); - expect(koreanSummary.reasonLabels.join(' ')).toContain('이미 배정됨'); - expect(koreanSummary.reasonLabels.join(' ')).toContain('배송 날짜 누락'); - expect(koreanSummary.reasonLabels.join(' ')).toContain('기타 메타데이터 검토'); }); test('separates exhausted bulk geocode failures as address review', () => { @@ -328,13 +305,6 @@ describe('route ops web state helpers', () => { }); expect(isAddressReviewRequired(addressReview)).toBe(true); - expect( - getOrderWorksetUnavailableReasons(addressReview).map((reason) => reason.code) - ).toEqual(['address_review']); - const summary = summarizeOrderWorkset([addressReview], new Set()); - expect(summary.reasonLabels).toEqual(['Address Review 1']); - expect(summary.reasonsByCode.address_review).toBe(1); - const pendingCoordinates = order({ blockerReasons: ['missing_coordinates'], coordinates: { latitude: null, longitude: null }, @@ -352,9 +322,6 @@ describe('route ops web state helpers', () => { } }); expect(isAddressReviewRequired(pendingCoordinates)).toBe(false); - expect( - getOrderWorksetUnavailableReasons(pendingCoordinates).map((reason) => reason.code) - ).toEqual(['missing_coordinates']); }); test('separates missing delivery dates from delivery date review blockers', () => { @@ -375,20 +342,7 @@ describe('route ops web state helpers', () => { expect(isDeliveryDateReviewRequired(actuallyMissing)).toBe(false); expect(isDeliveryDateReviewRequired(reviewNeeded)).toBe(true); - expect( - getOrderWorksetUnavailableReasons(actuallyMissing).map((reason) => reason.code) - ).toEqual(['missing_delivery_date']); - expect( - getOrderWorksetUnavailableReasons(reviewNeeded).map((reason) => reason.code) - ).toEqual(['delivery_date_review']); - const summary = summarizeOrderWorkset([actuallyMissing, reviewNeeded], new Set()); - expect(summary.reasonLabels).toEqual([ - 'Delivery date review 1', - 'Missing delivery date 1' - ]); - expect(summary.reasonsByCode.delivery_date_review).toBe(1); - expect(summary.reasonsByCode.missing_delivery_date).toBe(1); }); @@ -403,13 +357,6 @@ describe('route ops web state helpers', () => { expect(withWorkspaceQuery('/admin/ui/app/api/bootstrap', '')).toBe('/admin/ui/app/api/bootstrap'); }); - test('summarizes selected ready orders and blockers', () => { - const orders = [order({ orderId: 'ready' }), order({ blockerReasons: ['missing_coordinates'], orderId: 'blocked', planningStatus: 'UNPLANNED' })]; - const result = summarizeSelection(orders, new Set(['ready', 'blocked'])); - expect(result.readySelected.map((item) => item.orderId)).toEqual(['ready']); - expect(result.blockers).toEqual(['#1001: missing_coordinates']); - }); - test('moves stops using keyboard-compatible reorder helper', () => { const stops = [stop('a', 1), stop('b', 2), stop('c', 3)]; expect(moveStop(stops, 'b', -1).map((item) => `${item.deliveryStopId}:${item.sequence}`)).toEqual(['b:1', 'a:2', 'c:3']); @@ -426,13 +373,6 @@ describe('route ops web state helpers', () => { - test('supports drag/drop stop insertion before a target stop', () => { - const stops = [stop('a', 1), stop('b', 2), stop('c', 3), stop('d', 4)]; - expect(moveStopBefore(stops, 'd', 'b').map((item) => `${item.deliveryStopId}:${item.sequence}`)).toEqual(['a:1', 'd:2', 'b:3', 'c:4']); - expect(moveStopBefore(stops, 'b', 'd').map((item) => `${item.deliveryStopId}:${item.sequence}`)).toEqual(['a:1', 'c:2', 'b:3', 'd:4']); - expect(moveStopBefore(stops, 'x', 'd')).toBe(stops); - }); - test('supports drag/drop stop insertion before or after a preview target', () => { const stops = [stop('a', 1), stop('b', 2), stop('c', 3), stop('d', 4)]; @@ -453,38 +393,6 @@ describe('route ops web state helpers', () => { expect(moveStopToSequence(stops, 'b', 5)).toBe(stops); }); - test('derives route stats and geometry labels honestly', () => { - const detail: RoutePlanDetailDto = { - routeGeometry: null, - routePlan: { - createdAt: '', - deliveryAreas: [], - deliveryDate: '2026-05-27', - depot: { latitude: 43.7, longitude: -79.4 }, - driverId: null, - id: 'route-1', - missingCoordinates: 1, - name: 'Route 1', - planDate: '2026-05-27', - routeEndMode: 'END_AT_LAST_STOP', - status: 'DRAFT', - stopsCount: 2, - updatedAt: '' - }, - routeStopPoints: [], - stops: [stop('a', 1, 'COMPLETED'), stop('b', 2, 'ATTEMPTED', null, null)] - }; - expect(deriveRouteStats(detail)).toEqual({ attempted: 1, completed: 1, missingCoordinates: 1, stops: 2 }); - expect(geometryLabel(null, 'configured')).toBe('No route selected'); - expect(geometryLabel(detail, 'not_configured')).toBe('Router not configured for ready stops'); - expect(geometryLabel(detail, 'not_configured', 'ko-KR')).toBe('표시된 정류지의 라우터가 설정되지 않음'); - expect(geometryLabel({ ...detail, routeGeometry: { coordinates: [[-79, 43], [-79.1, 43.1]], type: 'LineString' } }, 'configured')).toBe('Road path ready'); - expect(geometryLabel({ ...detail, routeGeometry: { coordinates: [[-79, 43], [-79.1, 43.1]], type: 'LineString' } }, 'configured', 'ko-KR')).toBe('도로 경로'); - expect(geometryLabel(detail, 'configured')).toBe('Road path not generated for ready stops'); - expect(geometryLabel({ ...detail, routePlan: { ...detail.routePlan, depot: { latitude: null, longitude: null } }, stops: [stop('a', 1, 'PENDING', null, null)] }, 'configured')).toBe('Need coordinates for road path'); - expect(geometryLabel({ ...detail, routePlan: { ...detail.routePlan, depot: { latitude: null, longitude: null } }, stops: [stop('a', 1, 'PENDING', null, null)] }, 'configured', 'ko-KR')).toBe('경로 선에 필요한 좌표 부족'); - }); - test('keeps map/provider states explicit and plugin mode hides setup actions', () => { expect(hideSetupActions(bootstrap('plugin'))).toBe(true); expect(hideSetupActions(bootstrap('internal-admin'))).toBe(false);