import {z} from 'zod' export type GetApiShipmentArgs = {shipmentNumber: string, sealine?: string | undefined, shipmentType?: ('CT' | 'BL' | 'BK') | undefined, route?: boolean | undefined, ais?: boolean | undefined}; export type GetApiShipmentOptions = {baseUrl?: string | undefined, ApiKey?: string | undefined, fetch?: typeof fetch | undefined}; export const metadata = z.object({shipmentType: z.enum(["CT", "BL", "BK", "DC"]).optional(), shipmentNumber: z.string().optional(), sealine: z.string().optional(), sealineName: z.string().optional(), shippingStatus: z.enum(["PLANNED", "IN_TRANSIT", "DELIVERED", "UNKNOWN"]).optional(), updatedAt: z.string().optional(), warnings: z.array(z.enum(["OBSOLETE_DATA", "NO_POL", "NO_POD", "MISSING_DATE", "STATUS_MISMATCH", "NO_EVENTS", "NO_CONTAINERS", "SPLIT_BL"])).optional()}); export const coordinates = z.object({lat: z.number().optional(), lng: z.number().optional()}); export const location = z.object({name: z.string().optional(), state: z.string().optional(), country: z.string().optional(), countryCode: z.string().optional(), locode: z.string().optional(), coordinates: coordinates.optional(), timezone: z.string().optional()}); export const routeInfo = z.object({location: location.optional(), date: z.string().optional(), actual: z.boolean().optional(), predictiveEta: z.string().optional()}); export const route = z.object({prepol: routeInfo.optional(), pol: routeInfo.optional(), pod: routeInfo.optional(), postpod: routeInfo.optional()}); export const vessel = z.object({name: z.string().optional(), imo: z.number().int().optional(), callSign: z.string().optional(), mmsi: z.number().int().optional(), flag: z.string().optional()}); export const facility = z.object({name: z.string().optional(), countryCode: z.string().optional(), locode: z.string().optional(), bicCode: z.string().optional(), smdgCode: z.string().optional(), coordinates: coordinates.optional()}); export const event = z.object({location: location.optional(), facility: facility.optional(), description: z.string().optional(), eventType: z.enum(["SHIPMENT", "TRANSPORT", "EQUIPMENT"]).optional(), eventCode: z.enum(["ARRI", "DEPA", "LOAD", "DISC", "GTIN", "GTOT", "STUF", "STRP", "PICK", "AVPU", "DROP", "AVDO", "INSP", "RSEA", "RMVD", "CUSS", "CUSI", "CUSR", "CROS", "RECE", "DRFT", "PENA", "PENU", "PENC", "CONF", "REJE", "APPR", "ISSU", "SURR", "SUBM", "VOID", "REQS", "CMPL", "HOLD", "RELS", "CANC"]).optional(), status: z.enum(["CEP", "CPS", "CGI", "CLL", "VDL", "VAT", "CDT", "TSD", "CLT", "VDT", "VAD", "CDD", "CGO", "CDC", "CER", "LTS", "BTS", "UNK"]).optional(), date: z.string().optional(), isActual: z.boolean().optional(), isAdditionalEvent: z.boolean().optional(), routeType: z.enum(["SEA", "LAND"]).optional(), transportType: z.enum(["VESSEL", "BARGE", "FEEDER", "TRUCK", "RAIL", "AIR"]).optional(), vessel: vessel.optional(), voyage: z.string().optional()}); export const container = z.object({number: z.string().optional(), isoCode: z.string().optional(), sizeType: z.string().optional(), status: z.enum(["PLANNED", "IN_TRANSIT", "DELIVERED", "UNKNOWN"]).optional(), events: z.array(event).optional()}); export const routeSegment = z.object({path: z.array(coordinates).optional(), routeType: z.enum(["SEA", "LAND"]).optional()}); export const aisDataEvent = z.object({description: z.string().optional(), date: z.string().optional(), voyage: z.string().optional()}); export const aisDataPort = z.object({name: z.string().optional(), countryCode: z.string().optional(), code: z.string().optional(), date: z.string().optional(), dateLabel: z.enum(["ETA", "ATA", "ETD", "ATD"]).optional()}); export const aisCoordinates = z.object({lat: z.number().optional(), lng: z.number().optional(), updatedAt: z.string().optional()}); export const aisDataPortWithoutName = z.object({countryCode: z.string().optional(), code: z.string().optional(), date: z.string().optional(), dateLabel: z.enum(["ETA", "ATA", "ETD", "ATD"]).optional()}); export const aisData = z.object({lastEvent: aisDataEvent.optional(), dischargePort: aisDataPort.optional(), vessel: vessel.optional(), lastVesselPosition: aisCoordinates.optional(), departurePort: aisDataPortWithoutName.optional(), arrivalPort: aisDataPortWithoutName.optional(), updatedAt: z.string().optional()}); export const ais = z.object({status: z.enum(["OK", "NOT_ON_BOARD", "NO_AIS_DATA"]).optional(), data: aisData.optional()}); export const routeData = z.object({routeSegments: z.array(routeSegment).optional(), coordinates: coordinates.optional(), ais: ais.optional()}); export const shipmentDetails = z.object({metadata: metadata.optional(), locations: z.array(location).optional(), route: route.optional(), vessels: z.array(vessel).optional(), facilities: z.array(facility).optional(), containers: z.array(container).optional(), routeData: routeData.optional()}); export const validateResponse = async ( schema: { readonly '~standard': { readonly validate: (value: unknown) => ({ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }) | Promise<{ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }> } }, value: unknown ): Promise => { const result = await schema['~standard'].validate(value) if (result.issues !== undefined) { const detail = result.issues .map(({ message, path }) => path?.length ? `${path.map((segment) => String(typeof segment === 'object' ? segment.key : segment)).join('.')}: ${message}` : message ) .join('; ') throw new Error(`Response does not match the schema: ${detail}`, { cause: { issues: result.issues } }) } return result.value }; /** * GET /shipment */ export const getApiShipment = async (args: GetApiShipmentArgs, options?: GetApiShipmentOptions) => { const {baseUrl = "https://api.sinay.ai/container-tracking/api/v2", fetch: fetchFn = fetch, ApiKey} = options ?? {} const query = new URLSearchParams() const headers = new Headers() headers.set('accept', "application/json") if (args.shipmentNumber != null) query.append("shipmentNumber", String(args.shipmentNumber)) if (args.sealine != null) query.append("sealine", String(args.sealine)) if (args.shipmentType != null) query.append("shipmentType", String(args.shipmentType)) if (args.route != null) query.append("route", String(args.route)) if (args.ais != null) query.append("ais", String(args.ais)) if (ApiKey !== undefined) headers.set("API_KEY", ApiKey) const url = `${baseUrl.replace(/\/+$/, '')}/shipment${query.size > 0 ? `?${query}` : ''}` const res = await fetchFn(url, {method: "GET", headers}) if (!res.ok) throw new Error(`Request failed with status ${res.status}`, {cause: {status: res.status, body: await res.text()}}) return validateResponse(shipmentDetails, await res.json()) };