v0.1.4: fix silent failure on zones that reject ParkingEstimateMulti
All checks were successful
build-apk / build (push) Successful in 14m27s
All checks were successful
build-apk / build (push) Successful in 14m27s
Some zones (e.g. short-term "DL" spaces at the downtown lot) return
Response.Status:Error "Unable to process your request" for ParkingEstimateMulti
even though the single-duration ParkingEstimate works. The screen swallowed that
error -> empty duration list, disabled Pay button, no message ("nothing happens").
- Fall back to a single-estimate ladder across the zone's Min/Max time so these
zones are purchasable (verified live: flat ~$0.10 up to ~33 min).
- Surface estimate errors with a message + Try again, instead of a blank screen.
- Log the estimate fallback/failure to the on-device diagnostics log.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
30f32bc494
commit
f8d9e2970c
2 changed files with 94 additions and 9 deletions
|
|
@ -34,6 +34,45 @@ function fmtDuration(min?: number): string {
|
|||
}
|
||||
const money = (v?: string | number) => `$${Number(v ?? 0).toFixed(2)}`;
|
||||
|
||||
/**
|
||||
* Some zones (e.g. short-term "DL" spaces) reject ParkingEstimateMulti with
|
||||
* "Unable to process your request" even though the single-duration estimate
|
||||
* works. Build a price ladder from ParkingEstimate (single) across the zone's
|
||||
* Min/Max time so those zones stay purchasable instead of showing a blank screen.
|
||||
*/
|
||||
async function buildSingleLadder(base: {
|
||||
zoneId: number;
|
||||
spaceId: number;
|
||||
customerId: number;
|
||||
vehicleId: number;
|
||||
}): Promise<ParkingDetail[]> {
|
||||
const probe = await ps.getParkingEstimateSingle({ ...base, durationInMinutes: 30, creditCardId: 0 });
|
||||
if ((probe as any)?.Response?.Status === 'Error') {
|
||||
throw new Error((probe as any).Response?.Message || 'Estimate unavailable for this space.');
|
||||
}
|
||||
const min = Math.max(5, Number(probe.MinTime) || 5);
|
||||
const max = Math.max(min, Number(probe.MaxTime) || min);
|
||||
const span = max - min;
|
||||
const step = span > 180 ? 30 : span > 60 ? 15 : span > 20 ? 10 : 5;
|
||||
const durs = new Set<number>();
|
||||
for (let d = min; d < max; d += step) durs.add(d);
|
||||
durs.add(max);
|
||||
const rungs = await Promise.all(
|
||||
[...durs].map(async (d) => {
|
||||
try {
|
||||
const r = await ps.getParkingEstimateSingle({ ...base, durationInMinutes: d, creditCardId: 0 });
|
||||
const p = r.ParkingDetail;
|
||||
if (!p || (r as any)?.Response?.Status === 'Error') return null;
|
||||
// The single endpoint often echoes Minutes:0; trust the requested duration.
|
||||
return { ...p, Minutes: d } as ParkingDetail;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
return rungs.filter((r): r is ParkingDetail => r != null);
|
||||
}
|
||||
|
||||
/** Parse the API's "MM-DD-YYYY hh:mm AM" end-time string into a Date for reminders. */
|
||||
function parseApiTime(s?: string): Date | null {
|
||||
if (!s) return null;
|
||||
|
|
@ -67,6 +106,7 @@ export function StartSessionScreen() {
|
|||
const [selIdx, setSelIdx] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [paying, setPaying] = useState(false);
|
||||
const [ladderError, setLadderError] = useState<string | null>(null);
|
||||
|
||||
// Load the account's vehicles + cards and pick the defaults.
|
||||
useEffect(() => {
|
||||
|
|
@ -88,16 +128,41 @@ export function StartSessionScreen() {
|
|||
const loadLadder = useCallback(async () => {
|
||||
if (vehicleId == null || space?.SpaceId == null) return;
|
||||
setLoading(true);
|
||||
setLadderError(null);
|
||||
const base = {
|
||||
zoneId: zone.ZoneId!,
|
||||
spaceId: space.SpaceId!,
|
||||
customerId: zone.CustomerId!,
|
||||
vehicleId,
|
||||
};
|
||||
try {
|
||||
const est = await ps.getParkingEstimateMulti({
|
||||
zoneId: zone.ZoneId!,
|
||||
spaceId: space.SpaceId!,
|
||||
customerId: zone.CustomerId!,
|
||||
vehicleId,
|
||||
});
|
||||
const details = est.ParkingDetail ?? [];
|
||||
let details: ParkingDetail[] = [];
|
||||
let multiErrored = false;
|
||||
try {
|
||||
const est = await ps.getParkingEstimateMulti(base);
|
||||
if ((est as any)?.Response?.Status === 'Error') multiErrored = true;
|
||||
else details = est.ParkingDetail ?? [];
|
||||
} catch {
|
||||
multiErrored = true;
|
||||
}
|
||||
// Multi is unsupported or empty for some zones — fall back to single estimates.
|
||||
if (!details.length) {
|
||||
logLine(
|
||||
`[ESTIMATE] multi ${multiErrored ? 'errored' : 'empty'} for zone=${base.zoneId} ` +
|
||||
`space=${base.spaceId}; falling back to single`,
|
||||
);
|
||||
details = await buildSingleLadder(base);
|
||||
}
|
||||
if (!details.length) {
|
||||
throw new Error('No parking options are available for this space right now.');
|
||||
}
|
||||
setLadder(details);
|
||||
setSelIdx((i) => Math.min(i, Math.max(0, details.length - 1)));
|
||||
} catch (e: any) {
|
||||
const msg = e?.serverMessage ?? e?.message ?? 'Could not load parking options.';
|
||||
setLadder([]);
|
||||
setLadderError(msg);
|
||||
logLine(`[ESTIMATE] failed zone=${base.zoneId} space=${base.spaceId}: ${msg}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -218,6 +283,26 @@ export function StartSessionScreen() {
|
|||
);
|
||||
}
|
||||
|
||||
if (ladderError && !ladder.length) {
|
||||
return (
|
||||
<View style={[styles.center, { backgroundColor: colors.bg, padding: 24 }]}>
|
||||
<Text style={[styles.zone, { color: colors.text }]}>{zone.ZoneName}</Text>
|
||||
<View style={[styles.freeBanner, { backgroundColor: '#fdecea', marginTop: 16, alignSelf: 'stretch' }]}>
|
||||
<Text style={{ color: '#c0392b', fontWeight: '700', fontSize: 16 }}>
|
||||
Couldn’t load parking options
|
||||
</Text>
|
||||
<Text style={{ color: colors.subtext, marginTop: 6 }}>{ladderError}</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.payBtn, { backgroundColor: colors.primary, marginTop: 20, alignSelf: 'stretch' }]}
|
||||
onPress={() => void loadLadder()}
|
||||
>
|
||||
<Text style={styles.payText}>Try again</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const Chip = ({
|
||||
active,
|
||||
label,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue