diff --git a/src/app/api/purchase/route.ts b/src/app/api/purchase/route.ts index 1039494..f7454a3 100644 --- a/src/app/api/purchase/route.ts +++ b/src/app/api/purchase/route.ts @@ -4,7 +4,7 @@ import { nanoid } from "nanoid"; import { FlightLib__factory, FlightOracle__factory, FlightProduct__factory, FlightUSD__factory } from "../../../contracts/flight"; import { IPolicyService__factory } from "../../../contracts/gif"; import { IBundleService__factory, IPoolService__factory } from "../../../contracts/gif/factories/pool"; -import { AirportBlacklistedError, AirportNotWhitelistedError, TransactionFailedException } from "../../../types/errors"; +import { AirportBlacklistedError, AirportNotWhitelistedError, FlightNotFoundError, InconsistentFlightDataError, TransactionFailedException } from "../../../types/errors"; import { Airport } from "../../../types/flightstats/airport"; import { ApplicationData, PermitData, PurchaseRequest } from "../../../types/purchase_request"; import { LOGGER } from "../../../utils/logger_backend"; @@ -60,7 +60,17 @@ export async function POST(request: Request) { return Response.json({ error: "BALANCE_ERROR" }, { status: 500 }); - } else { + } else if (err instanceof FlightNotFoundError) { + return Response.json({ + error: "NO_FLIGHT_FOUND", + message: err.message, + }, { status: 400 }); + } else if (err instanceof InconsistentFlightDataError) { + return Response.json({ + error: "INCONSISTENT_DATA", + message: err.message, + }, { status: 400 }); + } else { // @ts-expect-error unknown error LOGGER.error(`unexpected error: ${err.message}`); return Response.json({ @@ -94,19 +104,19 @@ async function validateFlightPlan(reqId: string, application: ApplicationData) { const response = await fetch(url); if (!response.ok) { - throw new Error(`[${reqId}] Flight not found on flightstats api`); + throw new FlightNotFoundError(`[${reqId}] Flight not found on flightstats api`); } const flightData = await response.json(); const scheduledFlights = flightData.scheduledFlights; if (scheduledFlights.length === 0) { - throw new Error(`[${reqId}] Flight not found (1)`); + throw new FlightNotFoundError(`[${reqId}] Flight not found (1)`); } const appendix = flightData.appendix; if (appendix.length === 0) { - throw new Error(`[${reqId}] Flight not found (2)`); + throw new FlightNotFoundError(`[${reqId}] Flight not found (2)`); } const airports = appendix.airports.map((airport: Airport) => airport.iata) as string[]; LOGGER.debug(`[${reqId}] airports in flightPlan: ${JSON.stringify(airports)}`); @@ -129,19 +139,22 @@ async function validateFlightPlan(reqId: string, application: ApplicationData) { const departureAirportIataFlightStats = appendix.airports.find((airport: Airport) => airport.fs === scheduledFlights[0].departureAirportFsCode)?.iata; if (departureAirportIataFlightStats !== application.departureAirport) { - throw new Error(`[${reqId}] Departure airport invalid`); + throw new InconsistentFlightDataError(`[${reqId}] Departure airport invalid`); } const arrivalAirportIataFlightStats = appendix.airports.find((airport: Airport) => airport.fs === scheduledFlights[0].arrivalAirportFsCode)?.iata; if (arrivalAirportIataFlightStats !== application.arrivalAirport) { - throw new Error(`[${reqId}] Arrival airport invalid`); + throw new InconsistentFlightDataError(`[${reqId}] Arrival airport invalid`); } LOGGER.debug(`[${reqId}] airports whitelisted`); } catch (err) { + if (err instanceof AirportBlacklistedError || err instanceof AirportNotWhitelistedError || err instanceof FlightNotFoundError || err instanceof InconsistentFlightDataError) { + throw err; + } // @ts-expect-error error has field message LOGGER.error(err.message); - throw new Error(`[${reqId}] Flight not found`); + throw new FlightNotFoundError(`[${reqId}] Flight not found`); } } diff --git a/src/hooks/api/use_local_api.tsx b/src/hooks/api/use_local_api.tsx index 054b70f..2ce3480 100644 --- a/src/hooks/api/use_local_api.tsx +++ b/src/hooks/api/use_local_api.tsx @@ -1,5 +1,6 @@ import { ApplicationData, PermitData } from "../../types/purchase_request"; import { PurchaseFailedError, PurchaseNotPossibleError } from "../../utils/error"; +import { Reason } from "../../types/errors"; // @ts-expect-error BigInt is not defined in the global scope BigInt.prototype.toJSON = function () { @@ -29,6 +30,16 @@ export function useLocalApi() { throw new PurchaseFailedError(result.transaction, result.decodedError); } else if (result.error === "BALANCE_ERROR") { throw new PurchaseNotPossibleError(); + } else if (result.error === "NO_FLIGHT_FOUND") { + const err = new Error(result.message); + // @ts-expect-error adding custom field + err.reason = Reason.NO_FLIGHT_FOUND; + throw err; + } else if (result.error === "INCONSISTENT_DATA") { + const err = new Error(result.message); + // @ts-expect-error adding custom field + err.reason = Reason.INCONSISTENT_DATA; + throw err; } else { throw new Error(`Error sending purchase protection request: ${result.statusText}`); } diff --git a/src/hooks/use_application.tsx b/src/hooks/use_application.tsx index 5fe98a5..9f985e9 100644 --- a/src/hooks/use_application.tsx +++ b/src/hooks/use_application.tsx @@ -14,7 +14,8 @@ import { useERC20Contract } from "./onchain/use_erc20_contract"; import { useFlightDelayProductContract } from "./onchain/use_flightdelay_product"; import { useWallet } from "./onchain/use_wallet"; import { setGeneralErrorMessage } from "../redux/slices/common"; -import { EVENT_API_ERROR, EVENT_BLACKLISTED_ARRIVAL_AIRPORT, EVENT_BLACKLISTED_DEPARTURE_AIRPORT, EVENT_INSUFFICIENT_BALANCE, EVENT_INVALID_CHAIN, EVENT_NON_WHITELISTED_AIRPORT, EVENT_PERMIT_SIGNED, EVENT_PURACHASE_SUCCESSFUL, EVENT_PURCHASE_FAILED_UNKNOWN_ERROR, EVENT_PURCHASE_NOT_POSSIBLE, EVENT_PURCHASE_STARTED, EVENT_RISKPOOL_FULL, EVENT_USER_REJECTED, useAnalytics } from "./use_analytics"; +import { EVENT_API_ERROR, EVENT_NO_FLIGHT_FOUND, EVENT_BLACKLISTED_ARRIVAL_AIRPORT, EVENT_BLACKLISTED_DEPARTURE_AIRPORT, EVENT_INSUFFICIENT_BALANCE, EVENT_INVALID_CHAIN, EVENT_NON_WHITELISTED_AIRPORT, EVENT_PERMIT_SIGNED, EVENT_PURACHASE_SUCCESSFUL, EVENT_PURCHASE_FAILED_UNKNOWN_ERROR, EVENT_PURCHASE_NOT_POSSIBLE, EVENT_PURCHASE_STARTED, EVENT_RISKPOOL_FULL, EVENT_USER_REJECTED, useAnalytics } from "./use_analytics"; +import { Reason } from "../types/errors"; export default function useApplication() { const { t } = useTranslation(); @@ -161,16 +162,19 @@ export default function useApplication() { console.log("purchase result", result); dispatch(setPolicy({policyNftId: result.policyNftId, riskId: result.riskId})); - } catch (err) { + } catch (err: unknown) { if (err instanceof PurchaseFailedError) { console.log("purchase failed", err); dispatch(setError({ message: `${t("error.purchase_failed")} (${err.decodedError?.reason || "unknown error"})`, level: "error" })); } else if (err instanceof PurchaseNotPossibleError) { dispatch(setError({ message: t("error.purchase_currently_not_possible"), level: "error" })); + } else if (err instanceof Error && 'reason' in err && err.reason === Reason.NO_FLIGHT_FOUND) { + dispatch(setError({ message: t("error.no_flight_found"), level: "error", reason: Reason.NO_FLIGHT_FOUND })); + trackEvent(EVENT_NO_FLIGHT_FOUND, { category: "purchase"}); + } else if (err instanceof Error && 'reason' in err && err.reason === Reason.INCONSISTENT_DATA) { + dispatch(setError({ message: t("error.inconsistent_data"), level: "error", reason: Reason.INCONSISTENT_DATA })); } else { - // @ts-expect-error code is custom field for metamask error - if (err.code !== undefined) { - // @ts-expect-error code is custom field for metamask error + if (err && typeof err === 'object' && 'code' in err) { if (err.code === "ACTION_REJECTED") { dispatch(setError({ message: t("error.user_rejected"), level: "error" })); trackEvent(EVENT_USER_REJECTED, { category: "purchase"}); diff --git a/src/redux/slices/flightData.ts b/src/redux/slices/flightData.ts index d7e34b9..1d5976a 100644 --- a/src/redux/slices/flightData.ts +++ b/src/redux/slices/flightData.ts @@ -95,9 +95,12 @@ export const flightDataSlice = createSlice({ state.flightNumber = action.payload.flightNumber; state.departureDate = action.payload.departureDate; }, - setError(state, action: PayloadAction<{message: string, level: string}>) { + setError(state, action: PayloadAction<{message: string, level: string, reason?: Reason}>) { state.errorMessage = action.payload.message; state.errorLevel = action.payload.level; + if (action.payload.reason !== undefined) { + state.errorReasonApi = action.payload.reason; + } }, resetFlightData(state) { // assign initial state diff --git a/src/types/errors.ts b/src/types/errors.ts index 97beb73..2aa2de3 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -35,3 +35,15 @@ export class AirportNotWhitelistedError extends Error { super(`Airport ${airport} is not whitelisted`); } } + +export class FlightNotFoundError extends Error { + constructor(msg?: string) { + super(msg || "Flight not found"); + } +} + +export class InconsistentFlightDataError extends Error { + constructor(msg?: string) { + super(msg || "Inconsistent flight data"); + } +}