MotorSignal-App/website/lib/create-group-ride.ts

79 lines
2.9 KiB
TypeScript

import { buildWebGroupRidePayload, type WebGroupRideInput } from './group-ride-payload';
export type CreateRideResult =
| { ok: true; rideId: string; payload: Record<string, unknown> }
| { ok: false; reason: 'unauthenticated' }
| { ok: false; reason: 'unavailable' }
| { ok: false; reason: 'commit-failed'; error: unknown };
export interface FirestoreBatchLike {
// Loose signatures so the real firebase WriteBatch is assignable here (its set() is
// generic over DocumentReference and returns WriteBatch). Tests pass a simple fake.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
set: (ref: any, data: any) => unknown;
commit: () => Promise<void>;
}
export interface CommitGroupRideDeps {
uid?: string | null;
firestore: unknown | null;
leadDisplayName: string;
leadHandle: string;
/** Injected Firestore primitives so this is unit-testable without the firebase SDK. */
writeBatch: (firestore: any) => FirestoreBatchLike;
docRef: (firestore: any, ...segments: string[]) => unknown;
now?: Date;
}
/**
* Persists a web-created group ride and reports the outcome. It performs the backend write
* and resolves ONLY after the batch commit succeeds, so callers can insert into UI state
* strictly after confirmation (no phantom rides). It never fabricates coordinates (see
* buildWebGroupRidePayload) and the returned rideId is the real Firestore document id used
* as the join code.
*
* Returns a discriminated result instead of throwing, so every failure path is explicit:
* - unauthenticated: no uid (an unauthenticated user cannot create a persisted ride)
* - unavailable: no Firestore handle
* - commit-failed: the write was attempted but rejected (nothing persisted)
*/
export async function commitGroupRide(
newRide: WebGroupRideInput,
deps: CommitGroupRideDeps
): Promise<CreateRideResult> {
const uid = deps.uid;
if (!uid) return { ok: false, reason: 'unauthenticated' };
if (!deps.firestore) return { ok: false, reason: 'unavailable' };
const now = deps.now ?? new Date();
const rideId = `ride_${now.getTime()}_${uid.substring(0, Math.min(6, uid.length))}`;
const payload = buildWebGroupRidePayload(newRide, {
rideId,
uid,
leadHandle: deps.leadHandle,
leadDisplayName: deps.leadDisplayName,
now,
});
const organizerParticipant = {
userId: uid,
role: 'organizer',
status: 'accepted',
riderHandle: deps.leadHandle,
riderDisplayName: deps.leadDisplayName,
createdAt: now.toISOString(),
updatedAt: now.toISOString(),
};
try {
const batch = deps.writeBatch(deps.firestore);
batch.set(deps.docRef(deps.firestore, 'group_rides', rideId), payload);
batch.set(deps.docRef(deps.firestore, 'group_rides', rideId, 'participants', uid), organizerParticipant);
await batch.commit();
return { ok: true, rideId, payload };
} catch (error) {
return { ok: false, reason: 'commit-failed', error };
}
}