88 lines
3.1 KiB
TypeScript
88 lines
3.1 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)
|
|
*/
|
|
let lastRideTimestamp = 0;
|
|
|
|
export function resetRideIdCounter(): void {
|
|
lastRideTimestamp = 0;
|
|
}
|
|
|
|
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 rawMs = now.getTime();
|
|
const timestamp = Math.max(rawMs, lastRideTimestamp + 1);
|
|
lastRideTimestamp = timestamp;
|
|
const rideId = `ride_${timestamp}_${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 };
|
|
}
|
|
}
|