import {z} from 'zod' export type StoreId = string; export type GetApiV1MenusArgs = {'X-Store-Id': StoreId}; export type GetApiV1MenusOptions = {baseUrl?: string | undefined, OAuth20?: string | undefined, fetch?: typeof fetch | undefined}; export const photo = z.object({url: z.string(), fileName: z.string(), id: z.string(), contentType: z.string()}); export const exposedThirdPartyInfo = z.object({externalId: z.string(), externalServiceSlug: z.string()}); export const category = z.object({id: z.string(), name: z.string(), description: z.string().optional(), itemIds: z.array(z.string()).optional(), photoIds: z.array(z.string()).optional(), exposedThirdPartyInfos: z.array(exposedThirdPartyInfo).optional()}); export const defaultModifierSelection = z.object({itemId: z.string(), selectionQuantity: z.number().int().gte(0)}); export const defaultModifierSelectionData = z.object({defaultModifierSelections: z.array(defaultModifierSelection)}); export const modifierGroup = z.object({id: z.string(), name: z.string(), minimumSelections: z.number().int().gte(0).optional(), maximumSelections: z.number().int().gte(0).optional(), maxPerModifierSelectionQuantity: z.number().int().gte(0).optional(), defaultModifierSelectionData: defaultModifierSelectionData.optional(), itemIds: z.array(z.string()).optional(), description: z.string().optional(), type: z.string().optional(), exposedThirdPartyInfos: z.array(exposedThirdPartyInfo).optional()}); export const hourInterval = z.object({day: z.enum(["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"]).optional(), fromHour: z.number().int().gte(0).lte(23).optional(), fromMinute: z.number().int().gte(0).lte(59).optional(), toHour: z.number().int().gte(0).lte(23).optional(), toMinute: z.number().int().gte(0).lte(59).optional()}); export const hours = z.object({intervals: z.array(hourInterval)}); export const money = z.object({currencyCode: z.string().min(3).max(3), amount: z.number().gte(0)}); export const percentageValue = z.object({decimalValue: z.number().gte(0)}); export const unitPriceAndCount = z.object({unitPrice: money, count: z.number().int().gte(1)}); export const additionalCharge = z.object({chargeType: z.enum(["PACKAGING_CHARGE", "CONTAINER_DEPOSIT"]), flatCharge: money.optional(), percentageCharge: percentageValue.optional(), unitPriceAndCount: unitPriceAndCount.optional()}); export const menuPOS = z.object({id: z.string(), name: z.string(), categoryIds: z.array(z.string()).optional(), fulfillmentModes: z.array(z.enum(["DELIVERY", "PICK_UP", "DINE_IN"])).optional(), description: z.string().optional(), hours: hours.optional(), additionalCharges: z.array(additionalCharge).nullable().optional()}); export const itemStatus = z.object({saleStatus: z.enum(["FOR_SALE", "INDEFINITELY_NOT_FOR_SALE", "TEMPORARILY_NOT_FOR_SALE"]), suspendedUntil: z.string().nullable().optional()}); export const overrideRule = z.object({externalServiceSlug: z.string().min(1).optional(), entityIds: z.array(z.string()).optional()}); export const itemPriceOverride = z.object({rules: z.array(overrideRule), currencyCode: z.string().min(3).max(3), amount: z.number()}); export const skuBarcode = z.object({barcodeType: z.enum(["UPC", "GTIN", "ASIN", "INTERNAL"]).optional(), value: z.string().optional()}); export const productClassification = z.object({id: z.string(), namePath: z.array(z.string())}); export const dietaryClassification = z.object({tag: z.enum(["VEGAN", "VEGETARIAN", "NON_VEGETARIAN", "EGG", "KOSHER", "HALAL", "PLANT_BASED", "PALEO", "KETO"]).optional()}); export const allergenClassification = z.object({tag: z.enum(["GLUTEN", "DAIRY", "WHEAT", "RYE", "BARLEY", "OAT", "SPELT", "KAMUT", "CRUSTACEAN", "MOLLUSK", "EGG", "FISH", "PEANUT", "SOYBEAN", "NUT", "ALMOND", "HAZELNUT", "WALNUT", "CASHEW", "PECAN", "BRAZIL_NUT", "PISTACHIO", "MACADAMIA_NUT", "QUEENSLAND_NUT", "LUPIN", "CELERY", "MUSTARD", "SESAME_SEED", "SULFITE", "CAFFEINE", "PHENYLALANINE"]).optional(), containsAllergen: z.boolean().optional()}); export const storageRequirement = z.object({tag: z.enum(["FROZEN", "COLD", "DRY", "ROOM_TEMPERATURE", "AVOID_SUNLIGHT"]).optional()}); export const energyKcal = z.object({low: z.number().optional(), high: z.number().optional()}); export const nutritionContent = z.object({servingSizeInGrams: z.number().optional(), servingSizeInMilliliters: z.number().optional(), fats: z.number().optional(), saturatedFats: z.number().optional(), monoUnsaturatedFats: z.number().optional(), polyUnsaturatedFats: z.number().optional(), carbohydrates: z.number().optional(), sugar: z.number().optional(), polyols: z.number().optional(), starch: z.number().optional(), protein: z.number().optional(), salt: z.number().optional(), sodium: z.number().optional(), fibres: z.number().optional(), vitaminC: z.number().optional(), calcium: z.number().optional(), magnesium: z.number().optional(), chloride: z.number().optional(), fluoride: z.number().optional(), potassium: z.number().optional(), caffeine: z.number().optional(), energy: z.number().optional()}); export const nutritionalInfo = z.object({energyKcal: energyKcal.optional(), nutritionContent: nutritionContent.optional()}); export const servings = z.object({min: z.number().int().optional(), max: z.number().int().optional()}); export const skuDetails = z.object({skuSlug: z.string().optional(), skuId: z.string().optional(), barcodes: z.array(skuBarcode).optional(), productClassificationIds: z.array(z.string()).optional(), productClassifications: z.array(productClassification).optional(), unitOfMeasure: z.string().optional(), individualWrapStatus: z.enum(["UNKNOWN", "NO", "YES"]).optional(), dietaryClassifications: z.array(dietaryClassification).optional(), allergenClassifications: z.array(allergenClassification).optional(), storageRequirements: z.array(storageRequirement).optional(), additives: z.array(z.string()).optional(), countryOfOriginIso2: z.string().min(2).max(2).optional(), containsAlcohol: z.boolean().optional(), nutritionalInfo: nutritionalInfo.optional(), servings: servings.optional(), producerInformation: z.string().optional(), distributorInformation: z.string().optional()}); export const itemTax = z.object({percentageValue: percentageValue, isValueAddedTax: z.boolean()}); export const menuItemPOS = z.object({id: z.string().min(1), name: z.string(), photoIds: z.array(z.string()), price: money, status: itemStatus, description: z.string().optional(), modifierGroupIds: z.array(z.string()).optional(), priceOverrides: z.array(itemPriceOverride).nullable().optional(), skuDetails: skuDetails.optional(), additionalCharges: z.array(additionalCharge).nullable().optional(), tax: itemTax.optional()}); export const menus = z.object({photos: z.record(z.string(), photo), categories: z.record(z.string(), category), modifierGroups: z.record(z.string(), modifierGroup), menus: z.record(z.string(), menuPOS).optional(), items: z.record(z.string(), menuItemPOS).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 the menus for a store * * GET /v1/menus */ export const getApiV1Menus = async (args: GetApiV1MenusArgs, options?: GetApiV1MenusOptions) => { const {baseUrl = "https://partners.tryotter.com", fetch: fetchFn = fetch, OAuth20} = options ?? {} const query = new URLSearchParams() const headers = new Headers() headers.set('accept', "application/json") if (args['X-Store-Id'] != null) headers.set("X-Store-Id", String(args['X-Store-Id'])) if (OAuth20 !== undefined) headers.set('authorization', `Bearer ${OAuth20}`) const url = `${baseUrl.replace(/\/+$/, '')}/v1/menus${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(menus, await res.json()) };