import { z } from "zod" import { countriesMap } from "../../constants/countries" import { getFriendsMembership } from "./helpers" const scandicFriendsTier = z.enum(["L1", "L2", "L3", "L4", "L5", "L6", "L7"]) const sasEurobonusTier = z.enum(["EBB", "EBS", "EBG", "EBD", "EBP"]) const commonMembershipSchema = z.object({ membershipNumber: z.string(), tierExpires: z.string().nullish().default(null), memberSince: z.string().nullish(), }) // This prevents validation errors if the API returns an unhandled membership type const otherMembershipSchema = z .object({ // This ensures that `type` won't widen into "string", losing the literal types, when used in a union type: z.string().refine((val): val is string & {} => true), }) .merge(commonMembershipSchema) export const sasMembershipSchema = z .object({ type: z.literal("SAS_EB"), tier: sasEurobonusTier, nextTier: sasEurobonusTier.nullish(), spendablePoints: z.number().nullish(), boostedByScandic: z.boolean().nullish(), boostedTier: sasEurobonusTier.nullish(), boostedTierExpires: z.string().nullish().default(null), }) .merge(commonMembershipSchema) .transform((response) => { return { ...response, tierExpires: // SAS API returns 1900-01-01 for non-expiring tiers response.tierExpires === "1900-01-01" ? null : response.tierExpires, } }) export const friendsMembershipSchema = z .object({ type: z.literal("SCANDIC_NATIVE"), tier: scandicFriendsTier, nextTier: scandicFriendsTier.nullish(), pointsToNextTier: z.number().nullish(), nightsToTopTier: z.number().nullish(), }) .merge(commonMembershipSchema) export const membershipSchema = z.union([ friendsMembershipSchema, sasMembershipSchema, otherMembershipSchema, ]) const pointExpirationSchema = z.object({ points: z.number().int(), expires: z.string(), }) export const userLoyaltySchema = z.object({ memberships: z.array(membershipSchema), points: z.object({ spendable: z.number().int(), earned: z.number().int(), spent: z.number().int(), }), tier: scandicFriendsTier, tierExpires: z.string(), tierBoostedBy: z.string().nullish(), pointExpirations: z.array(pointExpirationSchema), }) export const getUserSchema = z .object({ data: z.object({ attributes: z.object({ dateOfBirth: z.string().optional().default("1900-01-01"), email: z.string().email(), firstName: z.string(), language: z .string() // Preserve Profile v1 formatting for now so it matches ApiLang enum .transform((s) => s.charAt(0).toUpperCase() + s.slice(1)) .optional(), lastName: z.string(), phoneNumber: z.string().optional(), profileId: z.string(), membershipNumber: z.string(), address: z .object({ city: z.string().optional(), country: z.string().optional(), countryCode: z.nativeEnum(countriesMap).optional(), streetAddress: z.string().optional(), zipCode: z.string().optional(), }) .optional() .nullable(), loyalty: userLoyaltySchema.optional(), }), type: z.string(), }), }) .transform((apiResponse) => { return { ...apiResponse.data.attributes, membership: apiResponse.data.attributes.loyalty ? getFriendsMembership(apiResponse.data.attributes.loyalty) : null, name: `${apiResponse.data.attributes.firstName} ${apiResponse.data.attributes.lastName}`, } }) export const creditCardSchema = z .object({ attribute: z.object({ cardName: z.string().optional(), alias: z.string(), truncatedNumber: z.string().transform((s) => s.slice(-4)), expirationDate: z.string(), cardType: z .string() .transform((s) => s.charAt(0).toLowerCase() + s.slice(1)), }), id: z.string(), type: z.string(), }) .transform((apiResponse) => { return { id: apiResponse.id, type: apiResponse.attribute.cardType, truncatedNumber: apiResponse.attribute.truncatedNumber, alias: apiResponse.attribute.alias, expirationDate: apiResponse.attribute.expirationDate, cardType: apiResponse.attribute.cardType, } }) export const creditCardsSchema = z.object({ data: z.array(creditCardSchema), })