diff --git a/apps/scandic-web/actions/editProfile.ts b/apps/scandic-web/actions/editProfile.ts index 548a263f8..1b077ee58 100644 --- a/apps/scandic-web/actions/editProfile.ts +++ b/apps/scandic-web/actions/editProfile.ts @@ -2,6 +2,7 @@ import { z } from "zod" +import { phoneValidator } from "@scandic-hotels/common/utils/zod/phoneValidator" import * as api from "@scandic-hotels/trpc/api" import { ApiLang } from "@scandic-hotels/trpc/constants/apiLang" import { countriesMap } from "@scandic-hotels/trpc/constants/countries" @@ -11,7 +12,6 @@ import { protectedServerActionProcedure } from "@/server/trpc" import { editProfileSchema } from "@/components/Forms/Edit/Profile/schema" import { getIntl } from "@/i18n" -import { phoneValidator } from "@/utils/zod/phoneValidator" import { Status } from "@/types/components/myPages/myProfile/edit" diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/(contentTypes)/content_page/[uid]/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/(contentTypes)/content_page/[uid]/page.tsx index 7041755d0..f2ad77dd9 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/(contentTypes)/content_page/[uid]/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/(contentTypes)/content_page/[uid]/page.tsx @@ -2,8 +2,8 @@ import { headers } from "next/headers" import { notFound, redirect } from "next/navigation" import { overview } from "@scandic-hotels/common/constants/routes/myPages" +import { isSignupPage } from "@scandic-hotels/common/constants/routes/signup" -import { isSignupPage } from "@/constants/routes/signup" import { env } from "@/env/server" import ContentPage from "@/components/ContentType/StaticPages/ContentPage" diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/ClientTable/Row/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/ClientTable/Row/index.tsx index 0a6aae751..443832f85 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/ClientTable/Row/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/ClientTable/Row/index.tsx @@ -4,6 +4,7 @@ import { usePathname } from "next/navigation" import { useIntl } from "react-intl" import { dt } from "@scandic-hotels/common/dt" +import { Transactions } from "@scandic-hotels/trpc/enums/transactions" import { webviews } from "@/constants/routes/webviews" @@ -15,7 +16,6 @@ import useLang from "@/hooks/useLang" import AwardPoints from "../../../AwardPoints" import type { RowProps } from "@/types/components/myPages/myPage/earnAndBurn" -import { Transactions } from "@/types/enums/transactions" export default function Row({ transaction }: RowProps) { const intl = useIntl() diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/EmptyUpcomingStays/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/EmptyUpcomingStays/index.tsx index b9cc8281c..a82c99763 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/EmptyUpcomingStays/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/EmptyUpcomingStays/index.tsx @@ -1,3 +1,4 @@ +import { getCurrentWebUrl } from "@scandic-hotels/common/utils/url" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { env } from "@/env/server" @@ -6,7 +7,6 @@ import Link from "@/components/TempDesignSystem/Link" import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" -import { getCurrentWebUrl } from "@/utils/url" import styles from "./emptyUpcomingStays.module.css" diff --git a/apps/scandic-web/components/Current/Header/MainMenu/index.tsx b/apps/scandic-web/components/Current/Header/MainMenu/index.tsx index b4ff27ecf..9ece85894 100644 --- a/apps/scandic-web/components/Current/Header/MainMenu/index.tsx +++ b/apps/scandic-web/components/Current/Header/MainMenu/index.tsx @@ -6,6 +6,7 @@ import { useIntl } from "react-intl" import { findMyBookingCurrentWebPath } from "@scandic-hotels/common/constants/routes/findMyBooking" import { myPages } from "@scandic-hotels/common/constants/routes/myPages" +import { getCurrentWebUrl } from "@scandic-hotels/common/utils/url" import { logout } from "@/constants/routes/handleAuth" import { env } from "@/env/client" @@ -18,7 +19,6 @@ import SkeletonShimmer from "@/components/SkeletonShimmer" import Link from "@/components/TempDesignSystem/Link" import useLang from "@/hooks/useLang" import { trackClick } from "@/utils/tracking" -import { getCurrentWebUrl } from "@/utils/url" import BookingButton from "../BookingButton" diff --git a/apps/scandic-web/components/DigitalTeamMemberCard/EmployeeBenefits/AuthCard/index.tsx b/apps/scandic-web/components/DigitalTeamMemberCard/EmployeeBenefits/AuthCard/index.tsx index 0a5b5aa0d..f14467dd9 100644 --- a/apps/scandic-web/components/DigitalTeamMemberCard/EmployeeBenefits/AuthCard/index.tsx +++ b/apps/scandic-web/components/DigitalTeamMemberCard/EmployeeBenefits/AuthCard/index.tsx @@ -1,5 +1,6 @@ +import { signup } from "@scandic-hotels/common/constants/routes/signup" + import { login } from "@/constants/routes/handleAuth" -import { signup } from "@/constants/routes/signup" import Card from "@/components/TempDesignSystem/Card" import { getIntl } from "@/i18n" diff --git a/apps/scandic-web/components/DigitalTeamMemberCard/EmployeeBenefits/CallToActions/index.tsx b/apps/scandic-web/components/DigitalTeamMemberCard/EmployeeBenefits/CallToActions/index.tsx index 090acb092..cfa6e8faa 100644 --- a/apps/scandic-web/components/DigitalTeamMemberCard/EmployeeBenefits/CallToActions/index.tsx +++ b/apps/scandic-web/components/DigitalTeamMemberCard/EmployeeBenefits/CallToActions/index.tsx @@ -1,11 +1,11 @@ import React from "react" +import { signup } from "@scandic-hotels/common/constants/routes/signup" import { Typography } from "@scandic-hotels/design-system/Typography" import { isValidSession } from "@scandic-hotels/trpc/utils/session" import { dtmcLogin } from "@/constants/routes/dtmc" import { login } from "@/constants/routes/handleAuth" -import { signup } from "@/constants/routes/signup" import { auth } from "@/auth" import ButtonLink from "@/components/ButtonLink" diff --git a/apps/scandic-web/components/Forms/Edit/Profile/schema.ts b/apps/scandic-web/components/Forms/Edit/Profile/schema.ts index a0d4bcd43..a3720ac3f 100644 --- a/apps/scandic-web/components/Forms/Edit/Profile/schema.ts +++ b/apps/scandic-web/components/Forms/Edit/Profile/schema.ts @@ -1,7 +1,7 @@ import { z } from "zod" -import { passwordValidator } from "@/utils/zod/passwordValidator" -import { phoneValidator } from "@/utils/zod/phoneValidator" +import { passwordValidator } from "@scandic-hotels/common/utils/zod/passwordValidator" +import { phoneValidator } from "@scandic-hotels/common/utils/zod/phoneValidator" export const editProfileErrors = { COUNTRY_REQUIRED: "COUNTRY_REQUIRED", diff --git a/apps/scandic-web/components/Forms/Signup/index.tsx b/apps/scandic-web/components/Forms/Signup/index.tsx index 930bbed70..602de3ed6 100644 --- a/apps/scandic-web/components/Forms/Signup/index.tsx +++ b/apps/scandic-web/components/Forms/Signup/index.tsx @@ -8,6 +8,10 @@ import { useIntl } from "react-intl" import { Button } from "@scandic-hotels/design-system/Button" import { Typography } from "@scandic-hotels/design-system/Typography" +import { + type SignUpSchema, + signUpSchema, +} from "@scandic-hotels/trpc/routers/user/schemas" import { getDefaultCountryFromLang } from "@/constants/languages" import { @@ -28,8 +32,7 @@ import { useFormTracking } from "@/components/TrackingSDK/hooks" import useLang from "@/hooks/useLang" import { formatPhoneNumber } from "@/utils/phone" -import { type SignUpSchema, signUpSchema } from "./schema" - +// import { type SignUpSchema, signUpSchema } from "./schema" import styles from "./form.module.css" import type { SignUpFormProps } from "@/types/components/form/signupForm" diff --git a/apps/scandic-web/components/Header/MainMenu/MobileMenu/index.tsx b/apps/scandic-web/components/Header/MainMenu/MobileMenu/index.tsx index eda1f20f2..45a58936b 100644 --- a/apps/scandic-web/components/Header/MainMenu/MobileMenu/index.tsx +++ b/apps/scandic-web/components/Header/MainMenu/MobileMenu/index.tsx @@ -9,6 +9,7 @@ import { findMyBooking, findMyBookingCurrentWebPath, } from "@scandic-hotels/common/constants/routes/findMyBooking" +import { getCurrentWebUrl } from "@scandic-hotels/common/utils/url" import { customerService } from "@/constants/webHrefs" import { env } from "@/env/client" @@ -19,7 +20,6 @@ import LanguageSwitcher from "@/components/LanguageSwitcher" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" import { useIsLangLive } from "@/hooks/useIsLangLive" import useLang from "@/hooks/useLang" -import { getCurrentWebUrl } from "@/utils/url" import HeaderLink from "../../HeaderLink" import TopLink from "../../TopLink" diff --git a/apps/scandic-web/components/Header/TopMenu/index.tsx b/apps/scandic-web/components/Header/TopMenu/index.tsx index 9e27b5981..b19fc6eb9 100644 --- a/apps/scandic-web/components/Header/TopMenu/index.tsx +++ b/apps/scandic-web/components/Header/TopMenu/index.tsx @@ -2,6 +2,7 @@ import { findMyBooking, findMyBookingCurrentWebPath, } from "@scandic-hotels/common/constants/routes/findMyBooking" +import { getCurrentWebUrl } from "@scandic-hotels/common/utils/url" import { env } from "@/env/server" import { getHeader } from "@/lib/trpc/memoizedRequests" @@ -12,7 +13,6 @@ import SkeletonShimmer from "@/components/SkeletonShimmer" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" import { isLoggedInUser } from "@/utils/isLoggedInUser" -import { getCurrentWebUrl } from "@/utils/url" import HeaderLink from "../HeaderLink" import TopLink from "../TopLink" diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/Multiroom/schema.ts b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/Multiroom/schema.ts index 4db659b33..d6377a0da 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/Multiroom/schema.ts +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/Multiroom/schema.ts @@ -1,7 +1,8 @@ import { z } from "zod" +import { phoneValidator } from "@scandic-hotels/common/utils/zod/phoneValidator" + import { specialRequestSchema } from "@/components/HotelReservation/EnterDetails/Details/SpecialRequests/schema" -import { phoneValidator } from "@/utils/zod/phoneValidator" // stringMatcher regex is copied from current web as specified by requirements. const stringMatcher = diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/schema.ts b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/schema.ts index 706fd3a15..eb370f316 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/schema.ts +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/schema.ts @@ -1,9 +1,9 @@ import { z } from "zod" import { dt } from "@scandic-hotels/common/dt" +import { phoneValidator } from "@scandic-hotels/common/utils/zod/phoneValidator" import { specialRequestSchema } from "@/components/HotelReservation/EnterDetails/Details/SpecialRequests/schema" -import { phoneValidator } from "@/utils/zod/phoneValidator" // stringMatcher regex is copied from current web as specified by requirements. const stringMatcher = diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Receipt/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Receipt/index.tsx index 3db661b20..4a90286fd 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Receipt/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Receipt/index.tsx @@ -3,6 +3,7 @@ import { notFound } from "next/navigation" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import { dt } from "@scandic-hotels/common/dt" +import * as maskValue from "@scandic-hotels/common/utils/maskValue" import ScandicLogoIcon from "@scandic-hotels/design-system/Icons/ScandicLogoIcon" import { Typography } from "@scandic-hotels/design-system/Typography" import { parseRefId } from "@scandic-hotels/trpc/utils/refId" @@ -16,7 +17,6 @@ import { import { getIntl } from "@/i18n" import { isLoggedInUser } from "@/utils/isLoggedInUser" -import * as maskValue from "@/utils/maskValue" import AdditionalInfoForm from "../../FindMyBooking/AdditionalInfoForm" import accessBooking, { diff --git a/apps/scandic-web/components/HotelReservation/MyStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/index.tsx index b47394e1e..8229d346b 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/index.tsx @@ -2,6 +2,8 @@ import { cookies } from "next/headers" import { notFound } from "next/navigation" import { dt } from "@scandic-hotels/common/dt" +import * as maskValue from "@scandic-hotels/common/utils/maskValue" +import { getCurrentWebUrl } from "@scandic-hotels/common/utils/url" import { Typography } from "@scandic-hotels/design-system/Typography" import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast" import { parseRefId } from "@scandic-hotels/trpc/utils/refId" @@ -35,8 +37,6 @@ import Image from "@/components/Image" import { getIntl } from "@/i18n" import MyStayProvider from "@/providers/MyStay" import { isLoggedInUser } from "@/utils/isLoggedInUser" -import * as maskValue from "@/utils/maskValue" -import { getCurrentWebUrl } from "@/utils/url" import styles from "./index.module.css" diff --git a/apps/scandic-web/components/TempDesignSystem/Form/Checkbox/errors.ts b/apps/scandic-web/components/TempDesignSystem/Form/Checkbox/errors.ts index db3e4ee11..7c09842d5 100644 --- a/apps/scandic-web/components/TempDesignSystem/Form/Checkbox/errors.ts +++ b/apps/scandic-web/components/TempDesignSystem/Form/Checkbox/errors.ts @@ -1,4 +1,4 @@ -import { signupErrors } from "@/components/Forms/Signup/schema" +import { signupErrors } from "@scandic-hotels/trpc/routers/user/schemas" import type { IntlShape } from "react-intl" diff --git a/apps/scandic-web/components/TempDesignSystem/Form/Input/errors.ts b/apps/scandic-web/components/TempDesignSystem/Form/Input/errors.ts index bf2d4c4b3..6babc73a3 100644 --- a/apps/scandic-web/components/TempDesignSystem/Form/Input/errors.ts +++ b/apps/scandic-web/components/TempDesignSystem/Form/Input/errors.ts @@ -1,10 +1,11 @@ +import { phoneErrors } from "@scandic-hotels/common/utils/zod/phoneValidator" +import { signupErrors } from "@scandic-hotels/trpc/routers/user/schemas" + import { bookingWidgetErrors } from "@/components/Forms/BookingWidget/schema" import { editProfileErrors } from "@/components/Forms/Edit/Profile/schema" -import { signupErrors } from "@/components/Forms/Signup/schema" import { multiroomErrors } from "@/components/HotelReservation/EnterDetails/Details/Multiroom/schema" import { roomOneErrors } from "@/components/HotelReservation/EnterDetails/Details/RoomOne/schema" import { findMyBookingErrors } from "@/components/HotelReservation/FindMyBooking/schema" -import { phoneErrors } from "@/utils/zod/phoneValidator" import type { IntlShape } from "react-intl" diff --git a/apps/scandic-web/components/TempDesignSystem/Form/PasswordInput/index.tsx b/apps/scandic-web/components/TempDesignSystem/Form/PasswordInput/index.tsx index 85dbf4653..24c422c7b 100644 --- a/apps/scandic-web/components/TempDesignSystem/Form/PasswordInput/index.tsx +++ b/apps/scandic-web/components/TempDesignSystem/Form/PasswordInput/index.tsx @@ -5,12 +5,12 @@ import { Text, TextField } from "react-aria-components" import { Controller, useFormContext } from "react-hook-form" import { useIntl } from "react-intl" +import { passwordValidators } from "@scandic-hotels/common/utils/zod/passwordValidator" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { Input } from "@scandic-hotels/design-system/Input" import Button from "@/components/TempDesignSystem/Button" import Caption from "@/components/TempDesignSystem/Text/Caption" -import { passwordValidators } from "@/utils/zod/passwordValidator" import { getErrorMessage } from "../Input/errors" diff --git a/apps/scandic-web/constants/myBooking.ts b/apps/scandic-web/constants/myBooking.ts deleted file mode 100644 index d0fa7187d..000000000 --- a/apps/scandic-web/constants/myBooking.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { LangRoute } from "@scandic-hotels/common/constants/routes/langRoute" - -export const myBookingPath: LangRoute = { - da: "/hotelreservation/min-booking", - de: "/hotelreservation/my-booking", - en: "/hotelreservation/my-booking", - fi: "/varaa-hotelli/varauksesi", - no: "/hotelreservation/my-booking", - sv: "/hotelreservation/din-bokning", -} diff --git a/apps/scandic-web/lib/trpc/memoizedRequests/index.ts b/apps/scandic-web/lib/trpc/memoizedRequests/index.ts index 9623db3b7..c09532f46 100644 --- a/apps/scandic-web/lib/trpc/memoizedRequests/index.ts +++ b/apps/scandic-web/lib/trpc/memoizedRequests/index.ts @@ -10,6 +10,7 @@ import { serverClient } from "../server" import type { Lang } from "@scandic-hotels/common/constants/language" import type { GetHotelsByCSFilterInput } from "@scandic-hotels/trpc/routers/hotels/input" +import type { GetSavedPaymentCardsInput } from "@scandic-hotels/trpc/routers/user/input" import type { RoomsAvailabilityExtendedInputSchema } from "@scandic-hotels/trpc/types/availability" import type { Country } from "@scandic-hotels/trpc/types/country" import type { @@ -22,8 +23,6 @@ import type { PackagesInput, } from "@scandic-hotels/trpc/types/packages" -import type { GetSavedPaymentCardsInput } from "@/server/routers/user/input" - export const getLocations = cache(async function getMemoizedLocations() { const lang = await getLang() const caller = await serverClient() diff --git a/apps/scandic-web/server/index.ts b/apps/scandic-web/server/index.ts index 6c04a2945..d3553b3e8 100644 --- a/apps/scandic-web/server/index.ts +++ b/apps/scandic-web/server/index.ts @@ -6,8 +6,7 @@ import { contentstackRouter } from "@scandic-hotels/trpc/routers/contentstack" import { hotelsRouter } from "@scandic-hotels/trpc/routers/hotels" import { navigationRouter } from "@scandic-hotels/trpc/routers/navigation" import { partnerRouter } from "@scandic-hotels/trpc/routers/partners" - -import { userRouter } from "./routers/user" +import { userRouter } from "@scandic-hotels/trpc/routers/user" export const appRouter = router({ booking: bookingRouter, diff --git a/apps/scandic-web/server/routers/user/output.ts b/apps/scandic-web/server/routers/user/output.ts deleted file mode 100644 index 8bf0d3e5a..000000000 --- a/apps/scandic-web/server/routers/user/output.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { z } from "zod" - -import { imageSchema } from "@scandic-hotels/trpc/routers/hotels/schemas/image" - -// Schema is the same for upcoming and previous stays endpoints -export const getStaysSchema = z.object({ - data: z.array( - z.object({ - attributes: z.object({ - hotelOperaId: z.string(), - hotelInformation: z.object({ - hotelContent: z.object({ - images: imageSchema, - }), - hotelName: z.string(), - cityName: z.string().nullable(), - }), - confirmationNumber: z.string(), - checkinDate: z.string(), - checkoutDate: z.string(), - isWebAppOrigin: z.boolean(), - bookingUrl: z.string().default(""), - }), - relationships: z.object({ - hotel: z.object({ - links: z.object({ - related: z.string().nullable().optional(), - }), - data: z.object({ - id: z.string(), - type: z.string(), - }), - }), - }), - type: z.string(), - id: z.string(), - links: z.object({ - self: z.object({ - href: z.string(), - meta: z.object({ - method: z.string(), - }), - }), - }), - }) - ), - links: z - .object({ - self: z.string(), - offset: z.number(), - limit: z.number(), - totalCount: z.number(), - }) - .optional() - .nullable(), -}) - -type GetStaysData = z.infer - -export type Stay = GetStaysData["data"][number] - -export const getFriendTransactionsSchema = z.object({ - data: z.array( - z.object({ - attributes: z.object({ - awardPoints: z.number().default(0), - checkinDate: z.string().default(""), - checkoutDate: z.string().default(""), - confirmationNumber: z.string().default(""), - hotelOperaId: z.string().default(""), - nights: z.number().default(1), - pointsCalculated: z.boolean().default(true), - transactionDate: z.string().default(""), - bookingUrl: z.string().default(""), - hotelInformation: z - .object({ - city: z.string().default(""), - name: z.string().default(""), - hotelContent: z.object({ - images: imageSchema, - }), - }) - .optional(), - }), - relationships: z.object({ - booking: z.object({ - data: z.object({ - id: z.string().default(""), - type: z.string().default(""), - }), - links: z.object({ - related: z.string().default(""), - }), - }), - hotel: z - .object({ - data: z.object({ - id: z.string().default(""), - type: z.string().default(""), - }), - links: z.object({ - related: z.string().default(""), - }), - }) - .optional(), - }), - type: z.string().default(""), - }) - ), - links: z - .object({ - self: z.string(), - }) - .nullable(), -}) - -type GetFriendTransactionsData = z.infer - -export type FriendTransaction = GetFriendTransactionsData["data"][number] - -export const initiateSaveCardSchema = z.object({ - data: z.object({ - attribute: z.object({ - transactionId: z.string(), - link: z.string(), - mobileToken: z.string().optional(), - }), - type: z.string(), - }), -}) - -export const subscriberIdSchema = z.object({ - subscriberId: z.string(), -}) diff --git a/apps/scandic-web/server/routers/user/utils.ts b/apps/scandic-web/server/routers/user/utils.ts deleted file mode 100644 index d350be58d..000000000 --- a/apps/scandic-web/server/routers/user/utils.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { myStay } from "@scandic-hotels/common/constants/routes/myStay" -import { dt } from "@scandic-hotels/common/dt" -import { createCounter } from "@scandic-hotels/common/telemetry" -import * as api from "@scandic-hotels/trpc/api" -import { countries } from "@scandic-hotels/trpc/constants/countries" -import { getFriendsMembership } from "@scandic-hotels/trpc/routers/user/helpers" -import { creditCardsSchema } from "@scandic-hotels/trpc/routers/user/output" -import { getVerifiedUser } from "@scandic-hotels/trpc/routers/user/utils" -import { toApiLang } from "@scandic-hotels/trpc/utils" -import { encrypt } from "@scandic-hotels/trpc/utils/encryption" - -import { myBookingPath } from "@/constants/myBooking" -import { env } from "@/env/server" - -import { cache } from "@/utils/cache" -import * as maskValue from "@/utils/maskValue" -import { getCurrentWebUrl } from "@/utils/url" - -import { type FriendTransaction, getStaysSchema, type Stay } from "./output" - -import type { Lang } from "@scandic-hotels/common/constants/language" -import type { User } from "@scandic-hotels/trpc/types/user" -import type { Session } from "next-auth" - -export async function getPreviousStays( - accessToken: string, - limit: number = 10, - language: Lang, - cursor?: string -) { - const getPreviousStaysCounter = createCounter("user", "getPreviousStays") - const metricsGetPreviousStays = getPreviousStaysCounter.init({ - limit, - cursor, - language, - }) - - metricsGetPreviousStays.start() - - const params: Record = { - limit: String(limit), - language: toApiLang(language), - } - - if (cursor) { - params.offset = cursor - } - - const apiResponse = await api.get( - api.endpoints.v1.Booking.Stays.past, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - params - ) - - if (!apiResponse.ok) { - await metricsGetPreviousStays.httpError(apiResponse) - return null - } - - const apiJson = await apiResponse.json() - - const verifiedData = getStaysSchema.safeParse(apiJson) - if (!verifiedData.success) { - metricsGetPreviousStays.validationError(verifiedData.error) - return null - } - - metricsGetPreviousStays.success() - - return verifiedData.data -} - -export async function getUpcomingStays( - accessToken: string, - limit: number = 10, - language: Lang, - cursor?: string -) { - const getUpcomingStaysCounter = createCounter("user", "getUpcomingStays") - const metricsGetUpcomingStays = getUpcomingStaysCounter.init({ - limit, - cursor, - language, - }) - - metricsGetUpcomingStays.start() - - const params: Record = { - limit: String(limit), - language: toApiLang(language), - } - - if (cursor) { - params.offset = cursor - } - - const apiResponse = await api.get( - api.endpoints.v1.Booking.Stays.future, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - params - ) - - if (!apiResponse.ok) { - await metricsGetUpcomingStays.httpError(apiResponse) - return null - } - - const apiJson = await apiResponse.json() - - const verifiedData = getStaysSchema.safeParse(apiJson) - if (!verifiedData.success) { - metricsGetUpcomingStays.validationError(verifiedData.error) - return null - } - - metricsGetUpcomingStays.success() - - return verifiedData.data -} - -export function parsedUser(data: User, isMFA: boolean) { - const country = countries.find((c) => c.code === data.address?.countryCode) - - const user = { - address: { - city: data.address?.city, - country: country?.name ?? "", - countryCode: data.address?.countryCode, - streetAddress: data.address?.streetAddress, - zipCode: data.address?.zipCode, - }, - dateOfBirth: data.dateOfBirth, - email: data.email, - firstName: data.firstName, - language: data.language, - lastName: data.lastName, - membershipNumber: data.membershipNumber, - membership: data.loyalty ? getFriendsMembership(data.loyalty) : null, - loyalty: data.loyalty, - name: `${data.firstName} ${data.lastName}`, - phoneNumber: data.phoneNumber, - profileId: data.profileId, - } - - if (!isMFA) { - if (user.address.city) { - user.address.city = maskValue.text(user.address.city) - } - if (user.address.streetAddress) { - user.address.streetAddress = maskValue.text(user.address.streetAddress) - } - - user.address.zipCode = data.address?.zipCode - ? maskValue.text(data.address.zipCode) - : "" - - user.dateOfBirth = maskValue.all(user.dateOfBirth) - - user.email = maskValue.email(user.email) - - user.phoneNumber = user.phoneNumber ? maskValue.phone(user.phoneNumber) : "" - } - - return user -} - -export const getCreditCards = cache( - async ({ - session, - onlyNonExpired, - }: { - session: Session - onlyNonExpired?: boolean - }) => { - const getCreditCardsCounter = createCounter("user", "getCreditCards") - const metricsGetCreditCards = getCreditCardsCounter.init({ - onlyNonExpired, - }) - - metricsGetCreditCards.start() - - const apiResponse = await api.get(api.endpoints.v1.Profile.creditCards, { - headers: { - Authorization: `Bearer ${session.token.access_token}`, - }, - }) - - if (!apiResponse.ok) { - await metricsGetCreditCards.httpError(apiResponse) - return null - } - - const apiJson = await apiResponse.json() - const verifiedData = creditCardsSchema.safeParse(apiJson) - if (!verifiedData.success) { - metricsGetCreditCards.validationError(verifiedData.error) - return null - } - - const result = verifiedData.data.data.filter((card) => { - if (onlyNonExpired) { - try { - const expirationDate = dt(card.expirationDate).startOf("day") - const currentDate = dt().startOf("day") - return expirationDate > currentDate - } catch (_) { - return false - } - } - return true - }) - - metricsGetCreditCards.success() - - return result - } -) - -export async function updateStaysBookingUrl( - data: Stay[], - session: Session, - lang: Lang -): Promise - -export async function updateStaysBookingUrl( - data: FriendTransaction[], - session: Session, - lang: Lang -): Promise - -export async function updateStaysBookingUrl( - data: Stay[] | FriendTransaction[], - session: Session, - lang: Lang -) { - const user = await getVerifiedUser({ - session, - }) - - if (user && !("error" in user)) { - return data.map((d) => { - const originalString = - d.attributes.confirmationNumber.toString() + "," + user.data.lastName - const encryptedBookingValue = encrypt(originalString) - - // Get base URL with fallback for ephemeral environments (like deploy previews). - const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com" - - // Construct Booking URL. - const bookingUrl = !env.isLangLive(lang) - ? new URL( - getCurrentWebUrl({ - path: myBookingPath[lang], - lang, - baseUrl, - }) - ) - : new URL(myStay[lang], baseUrl) - - // Add search parameters. - if (encryptedBookingValue) { - bookingUrl.searchParams.set("RefId", encryptedBookingValue) - } else { - bookingUrl.searchParams.set("lastName", user.data.lastName) - bookingUrl.searchParams.set( - "bookingId", - d.attributes.confirmationNumber.toString() - ) - } - - return { - ...d, - attributes: { - ...d.attributes, - bookingUrl: bookingUrl.toString(), - }, - } - }) - } - - return data -} diff --git a/apps/scandic-web/types/components/form/newPassword.ts b/apps/scandic-web/types/components/form/newPassword.ts index a3cbe91da..72657352c 100644 --- a/apps/scandic-web/types/components/form/newPassword.ts +++ b/apps/scandic-web/types/components/form/newPassword.ts @@ -1,3 +1,3 @@ -import type { passwordValidators } from "@/utils/zod/passwordValidator" +import type { passwordValidators } from "@scandic-hotels/common/utils/zod/passwordValidator" export type PasswordValidatorKey = keyof typeof passwordValidators diff --git a/apps/scandic-web/types/components/myPages/stays/stayCard.ts b/apps/scandic-web/types/components/myPages/stays/stayCard.ts index 76ba43beb..314dee426 100644 --- a/apps/scandic-web/types/components/myPages/stays/stayCard.ts +++ b/apps/scandic-web/types/components/myPages/stays/stayCard.ts @@ -1,4 +1,4 @@ -import type { Stay } from "@/server/routers/user/output" +import type { Stay } from "@scandic-hotels/trpc/routers/user/output" export type StayCardProps = { stay: Stay diff --git a/apps/scandic-web/types/components/myPages/user.ts b/apps/scandic-web/types/components/myPages/user.ts index 25f6406ec..63eaa267f 100644 --- a/apps/scandic-web/types/components/myPages/user.ts +++ b/apps/scandic-web/types/components/myPages/user.ts @@ -1,7 +1,6 @@ +import type { userQueryRouter } from "@scandic-hotels/trpc/routers/user/query" import type { User } from "@scandic-hotels/trpc/types/user" -import type { userQueryRouter } from "@/server/routers/user/query" - export type UserQueryRouter = typeof userQueryRouter export interface UserProps { diff --git a/apps/scandic-web/utils/url.ts b/apps/scandic-web/utils/url.ts index 21388bc3e..602857c12 100644 --- a/apps/scandic-web/utils/url.ts +++ b/apps/scandic-web/utils/url.ts @@ -1,6 +1,5 @@ import { z } from "zod" -import { Lang } from "@scandic-hotels/common/constants/language" import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast" import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter" @@ -240,52 +239,3 @@ export function serializeBookingSearchParams( typeHints, }) } - -/** - * Returns the TLD (top-level domain) for a given language. - * @param lang - The language to get the TLD for - * @returns The TLD for the given language - */ -export function getTldForLanguage(lang: Lang): string { - switch (lang) { - case Lang.sv: - return "se" - case Lang.no: - return "no" - case Lang.da: - return "dk" - case Lang.fi: - return "fi" - case Lang.de: - return "de" - default: - return "com" - } -} - -/** - * Constructs a URL with the correct TLD (top-level domain) based on lang, for current web. - * @param params - Object containing path, lang, and baseUrl - * @param params.path - The path to append to the URL - * @param params.lang - The language to use for TLD - * @param params.baseUrl - The base URL to use (e.g. https://www.scandichotels.com) - * @returns The complete URL with language-specific TLD - */ -export function getCurrentWebUrl({ - path, - lang, - baseUrl = "https://www.scandichotels.com", // Fallback for ephemeral environments (e.g. deploy previews). -}: { - path: string - lang: Lang - baseUrl?: string -}): string { - const tld = getTldForLanguage(lang) - const url = new URL(path, baseUrl) - - if (tld !== "com") { - url.host = url.host.replace(".com", `.${tld}`) - } - - return url.toString() -} diff --git a/apps/scandic-web/utils/user.ts b/apps/scandic-web/utils/user.ts index 414bfcea6..aaa756214 100644 --- a/apps/scandic-web/utils/user.ts +++ b/apps/scandic-web/utils/user.ts @@ -2,23 +2,8 @@ import { type MembershipLevel, MembershipLevelEnum, } from "@scandic-hotels/common/constants/membershipLevels" -import { scandicMembershipTypes } from "@scandic-hotels/trpc/routers/user/helpers" -import type { User, UserLoyalty } from "@scandic-hotels/trpc/types/user" - -export function getMembershipCards(userLoyalty: UserLoyalty) { - return userLoyalty.memberships - .filter( - (membership) => membership.type !== scandicMembershipTypes.SCANDIC_NATIVE - ) - .map((membership) => ({ - currentPoints: 0, // We only have points for Friends so we can't set this for now - expirationDate: membership.tierExpires, - membershipNumber: membership.membershipNumber, - membershipType: membership.type, - memberSince: membership.memberSince, - })) -} +import type { User } from "@scandic-hotels/trpc/types/user" export function isHighestMembership( membershipLevel: MembershipLevel | undefined diff --git a/apps/scandic-web/constants/routes/signup.ts b/packages/common/constants/routes/signup.ts similarity index 100% rename from apps/scandic-web/constants/routes/signup.ts rename to packages/common/constants/routes/signup.ts diff --git a/packages/common/package.json b/packages/common/package.json index 2a9a9b843..156cc1f50 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -22,9 +22,8 @@ "./utils/languages": "./utils/languages.ts", "./utils/chunk": "./utils/chunk.ts", "./utils/isDefined": "./utils/isDefined.ts", - "./utils/zod/stringValidator": "./utils/zod/stringValidator.ts", - "./utils/zod/numberValidator": "./utils/zod/numberValidator.ts", - "./utils/zod/arrayValidator": "./utils/zod/arrayValidator.ts", + "./utils/maskValue": "./utils/maskValue.ts", + "./utils/zod/*": "./utils/zod/*.ts", "./constants/language": "./constants/language.ts", "./constants/membershipLevels": "./constants/membershipLevels.ts", "./constants/paymentMethod": "./constants/paymentMethod.ts", diff --git a/apps/scandic-web/utils/maskValue.ts b/packages/common/utils/maskValue.ts similarity index 100% rename from apps/scandic-web/utils/maskValue.ts rename to packages/common/utils/maskValue.ts diff --git a/apps/scandic-web/utils/maskvalue.test.ts b/packages/common/utils/maskvalue.test.ts similarity index 94% rename from apps/scandic-web/utils/maskvalue.test.ts rename to packages/common/utils/maskvalue.test.ts index 0b147286e..863d8c810 100644 --- a/apps/scandic-web/utils/maskvalue.test.ts +++ b/packages/common/utils/maskvalue.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "@jest/globals" +import { describe, expect, test } from "vitest" import { all, email, phone, text } from "./maskValue" diff --git a/packages/common/utils/url.ts b/packages/common/utils/url.ts index d9be2c491..c91bee003 100644 --- a/packages/common/utils/url.ts +++ b/packages/common/utils/url.ts @@ -1,3 +1,5 @@ +import { Lang } from "../constants/language" + export function removeMultipleSlashes(pathname: string) { return pathname.replaceAll(/\/\/+/g, "/") } @@ -9,3 +11,52 @@ export function removeTrailingSlash(pathname: string) { } return pathname } + +/** + * Returns the TLD (top-level domain) for a given language. + * @param lang - The language to get the TLD for + * @returns The TLD for the given language + */ +export function getTldForLanguage(lang: Lang): string { + switch (lang) { + case Lang.sv: + return "se" + case Lang.no: + return "no" + case Lang.da: + return "dk" + case Lang.fi: + return "fi" + case Lang.de: + return "de" + default: + return "com" + } +} + +/** + * Constructs a URL with the correct TLD (top-level domain) based on lang, for current web. + * @param params - Object containing path, lang, and baseUrl + * @param params.path - The path to append to the URL + * @param params.lang - The language to use for TLD + * @param params.baseUrl - The base URL to use (e.g. https://www.scandichotels.com) + * @returns The complete URL with language-specific TLD + */ +export function getCurrentWebUrl({ + path, + lang, + baseUrl = "https://www.scandichotels.com", // Fallback for ephemeral environments (e.g. deploy previews). +}: { + path: string + lang: Lang + baseUrl?: string +}): string { + const tld = getTldForLanguage(lang) + const url = new URL(path, baseUrl) + + if (tld !== "com") { + url.host = url.host.replace(".com", `.${tld}`) + } + + return url.toString() +} diff --git a/apps/scandic-web/utils/zod/passwordValidator.ts b/packages/common/utils/zod/passwordValidator.ts similarity index 100% rename from apps/scandic-web/utils/zod/passwordValidator.ts rename to packages/common/utils/zod/passwordValidator.ts diff --git a/apps/scandic-web/utils/zod/phoneValidator.ts b/packages/common/utils/zod/phoneValidator.ts similarity index 100% rename from apps/scandic-web/utils/zod/phoneValidator.ts rename to packages/common/utils/zod/phoneValidator.ts diff --git a/packages/trpc/env/server.ts b/packages/trpc/env/server.ts index e95471e67..5c3929bb5 100644 --- a/packages/trpc/env/server.ts +++ b/packages/trpc/env/server.ts @@ -1,13 +1,17 @@ import { createEnv } from "@t3-oss/env-nextjs" import { z } from "zod" +import { isLangLive } from "../lib/DUPLICATED/isLangLive" + +import type { Lang } from "@scandic-hotels/common/constants/language" + /* * ⚠️ Remember to also add environment variables to the corresponding config in sites that uses this package. ⚠️ */ const TWENTYFOUR_HOURS = 24 * 60 * 60 -export const env = createEnv({ +const _env = createEnv({ /** * Due to t3-env only checking typeof window === "undefined" * and Netlify running Deno, window is never "undefined" @@ -49,6 +53,18 @@ export const env = createEnv({ .refine((s) => s === "true" || s === "false") .transform((s) => s === "true") .default("false"), + /** + * Include the languages that should be hidden for the next release + * Should be in the format of "en,da,de,fi,no,sv" or empty + */ + NEW_SITE_LIVE_FOR_LANGS: z + .string() + .regex(/^([a-z]{2},)*([a-z]{2}){0,1}$/) + .transform((val) => { + return val.split(",") + }) + .default(""), + SALESFORCE_PREFERENCE_BASE_URL: z.string(), }, emptyStringAsUndefined: true, runtimeEnv: { @@ -70,5 +86,12 @@ export const env = createEnv({ SENTRY_ENVIRONMENT: process.env.SENTRY_ENVIRONMENT, PUBLIC_URL: process.env.NEXT_PUBLIC_PUBLIC_URL, PRINT_QUERY: process.env.PRINT_QUERY, + NEW_SITE_LIVE_FOR_LANGS: process.env.NEXT_PUBLIC_NEW_SITE_LIVE_FOR_LANGS, + SALESFORCE_PREFERENCE_BASE_URL: process.env.SALESFORCE_PREFERENCE_BASE_URL, }, }) + +export const env = { + ..._env, + isLangLive: (lang: Lang) => isLangLive(lang, _env.NEW_SITE_LIVE_FOR_LANGS), +} as const diff --git a/packages/trpc/lib/DUPLICATED/isLangLive.test.ts b/packages/trpc/lib/DUPLICATED/isLangLive.test.ts new file mode 100644 index 000000000..d25127b85 --- /dev/null +++ b/packages/trpc/lib/DUPLICATED/isLangLive.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest" + +import { Lang } from "@scandic-hotels/common/constants/language" + +import { isLangLive } from "./isLangLive" + +describe("hideForNextRelease", () => { + it("should return true if en is part of live languages", () => { + expect(isLangLive(Lang.en, ["en", "sv"])).toBe(true) + expect(isLangLive(Lang.en, ["en"])).toBe(true) + }) + + it("should return false if en is not part of live languages", () => { + expect(isLangLive(Lang.en, [])).toBe(false) + expect(isLangLive(Lang.en, ["sv"])).toBe(false) + expect(isLangLive(Lang.en, ["sv,fi"])).toBe(false) + }) +}) diff --git a/packages/trpc/lib/DUPLICATED/isLangLive.ts b/packages/trpc/lib/DUPLICATED/isLangLive.ts new file mode 100644 index 000000000..2a985f3fa --- /dev/null +++ b/packages/trpc/lib/DUPLICATED/isLangLive.ts @@ -0,0 +1,5 @@ +import type { Lang } from "@scandic-hotels/common/constants/language" + +export function isLangLive(lang: Lang, liveLangs: string[]): boolean { + return liveLangs.includes(lang) +} diff --git a/apps/scandic-web/types/enums/transactions.ts b/packages/trpc/lib/enums/transactions.ts similarity index 100% rename from apps/scandic-web/types/enums/transactions.ts rename to packages/trpc/lib/enums/transactions.ts diff --git a/packages/trpc/lib/routers/types.ts b/packages/trpc/lib/routers/types.ts index 70b26df4f..9f2d97c6a 100644 --- a/packages/trpc/lib/routers/types.ts +++ b/packages/trpc/lib/routers/types.ts @@ -1,4 +1,7 @@ import type { Lang } from "@scandic-hotels/common/constants/language" +import type { MembershipLevel } from "@scandic-hotels/common/constants/membershipLevels" + +import type { LoginType } from "../types/loginType" export type TrackingPageData = { pageId: string @@ -25,3 +28,19 @@ type TrackingSDKChannel = | "hotels" | "homepage" | "campaign-overview-page" + +export type TrackingUserData = + | { + loginStatus: "logged in" + loginType?: LoginType + memberId?: string + membershipNumber?: string + memberLevel?: MembershipLevel + noOfNightsStayed?: number + totalPointsAvailableToSpend?: number + loginAction?: "login success" + } + | { + loginStatus: "Non-logged in" + } + | { loginStatus: "Error" } diff --git a/packages/trpc/lib/routers/user/helpers.ts b/packages/trpc/lib/routers/user/helpers.ts index 23a04f874..bee4620df 100644 --- a/packages/trpc/lib/routers/user/helpers.ts +++ b/packages/trpc/lib/routers/user/helpers.ts @@ -56,3 +56,17 @@ function isEurobonusMembership( export function getEurobonusMembership(loyalty: UserLoyalty) { return loyalty.memberships?.find(isEurobonusMembership) } + +export function getMembershipCards(userLoyalty: UserLoyalty) { + return userLoyalty.memberships + .filter( + (membership) => membership.type !== scandicMembershipTypes.SCANDIC_NATIVE + ) + .map((membership) => ({ + currentPoints: 0, // We only have points for Friends so we can't set this for now + expirationDate: membership.tierExpires, + membershipNumber: membership.membershipNumber, + membershipType: membership.type, + memberSince: membership.memberSince, + })) +} diff --git a/apps/scandic-web/server/routers/user/index.ts b/packages/trpc/lib/routers/user/index.ts similarity index 100% rename from apps/scandic-web/server/routers/user/index.ts rename to packages/trpc/lib/routers/user/index.ts diff --git a/apps/scandic-web/server/routers/user/input.ts b/packages/trpc/lib/routers/user/input.ts similarity index 95% rename from apps/scandic-web/server/routers/user/input.ts rename to packages/trpc/lib/routers/user/input.ts index 50499eefc..915c4dbdf 100644 --- a/apps/scandic-web/server/routers/user/input.ts +++ b/packages/trpc/lib/routers/user/input.ts @@ -2,7 +2,7 @@ import { z } from "zod" import { Lang } from "@scandic-hotels/common/constants/language" -import { signUpSchema } from "@/components/Forms/Signup/schema" +import { signUpSchema } from "./schemas" // Query export const userTrackingInput = z.object({ diff --git a/apps/scandic-web/server/routers/user/mutation.ts b/packages/trpc/lib/routers/user/mutation.ts similarity index 92% rename from apps/scandic-web/server/routers/user/mutation.ts rename to packages/trpc/lib/routers/user/mutation.ts index e15ee6588..19fecc6b0 100644 --- a/apps/scandic-web/server/routers/user/mutation.ts +++ b/packages/trpc/lib/routers/user/mutation.ts @@ -1,25 +1,18 @@ +import { signupVerify } from "@scandic-hotels/common/constants/routes/signup" import { createCounter } from "@scandic-hotels/common/telemetry" -import { router } from "@scandic-hotels/trpc" -import * as api from "@scandic-hotels/trpc/api" -import { serverErrorByStatus } from "@scandic-hotels/trpc/errors" -import { - protectedProcedure, - serviceProcedure, -} from "@scandic-hotels/trpc/procedures" - -import { signupVerify } from "@/constants/routes/signup" -import { env } from "@/env/server" -import { - initiateSaveCardSchema, - subscriberIdSchema, -} from "@/server/routers/user/output" +import { env } from "../../../env/server" +import { router } from "../.." +import * as api from "../../api" +import { serverErrorByStatus } from "../../errors" +import { protectedProcedure, serviceProcedure } from "../../procedures" import { addCreditCardInput, deleteCreditCardInput, saveCreditCardInput, signupInput, } from "./input" +import { initiateSaveCardSchema, subscriberIdSchema } from "./output" export const userMutationRouter = router({ creditCard: router({ diff --git a/packages/trpc/lib/routers/user/output.ts b/packages/trpc/lib/routers/user/output.ts index 01569916b..d8920bdc0 100644 --- a/packages/trpc/lib/routers/user/output.ts +++ b/packages/trpc/lib/routers/user/output.ts @@ -1,6 +1,7 @@ import { z } from "zod" import { countriesMap } from "../../constants/countries" +import { imageSchema } from "../../routers/hotels/schemas/image" import { getFriendsMembership } from "./helpers" const scandicFriendsTier = z.enum(["L1", "L2", "L3", "L4", "L5", "L6", "L7"]) @@ -143,3 +144,134 @@ export const creditCardSchema = z export const creditCardsSchema = z.object({ data: z.array(creditCardSchema), }) + +// Schema is the same for upcoming and previous stays endpoints +export const getStaysSchema = z.object({ + data: z.array( + z.object({ + attributes: z.object({ + hotelOperaId: z.string(), + hotelInformation: z.object({ + hotelContent: z.object({ + images: imageSchema, + }), + hotelName: z.string(), + cityName: z.string().nullable(), + }), + confirmationNumber: z.string(), + checkinDate: z.string(), + checkoutDate: z.string(), + isWebAppOrigin: z.boolean(), + bookingUrl: z.string().default(""), + }), + relationships: z.object({ + hotel: z.object({ + links: z.object({ + related: z.string().nullable().optional(), + }), + data: z.object({ + id: z.string(), + type: z.string(), + }), + }), + }), + type: z.string(), + id: z.string(), + links: z.object({ + self: z.object({ + href: z.string(), + meta: z.object({ + method: z.string(), + }), + }), + }), + }) + ), + links: z + .object({ + self: z.string(), + offset: z.number(), + limit: z.number(), + totalCount: z.number(), + }) + .optional() + .nullable(), +}) + +type GetStaysData = z.infer + +export type Stay = GetStaysData["data"][number] + +export const getFriendTransactionsSchema = z.object({ + data: z.array( + z.object({ + attributes: z.object({ + awardPoints: z.number().default(0), + checkinDate: z.string().default(""), + checkoutDate: z.string().default(""), + confirmationNumber: z.string().default(""), + hotelOperaId: z.string().default(""), + nights: z.number().default(1), + pointsCalculated: z.boolean().default(true), + transactionDate: z.string().default(""), + bookingUrl: z.string().default(""), + hotelInformation: z + .object({ + city: z.string().default(""), + name: z.string().default(""), + hotelContent: z.object({ + images: imageSchema, + }), + }) + .optional(), + }), + relationships: z.object({ + booking: z.object({ + data: z.object({ + id: z.string().default(""), + type: z.string().default(""), + }), + links: z.object({ + related: z.string().default(""), + }), + }), + hotel: z + .object({ + data: z.object({ + id: z.string().default(""), + type: z.string().default(""), + }), + links: z.object({ + related: z.string().default(""), + }), + }) + .optional(), + }), + type: z.string().default(""), + }) + ), + links: z + .object({ + self: z.string(), + }) + .nullable(), +}) + +type GetFriendTransactionsData = z.infer + +export type FriendTransaction = GetFriendTransactionsData["data"][number] + +export const initiateSaveCardSchema = z.object({ + data: z.object({ + attribute: z.object({ + transactionId: z.string(), + link: z.string(), + mobileToken: z.string().optional(), + }), + type: z.string(), + }), +}) + +export const subscriberIdSchema = z.object({ + subscriberId: z.string(), +}) diff --git a/apps/scandic-web/server/routers/user/query.ts b/packages/trpc/lib/routers/user/query.ts similarity index 96% rename from apps/scandic-web/server/routers/user/query.ts rename to packages/trpc/lib/routers/user/query.ts index d732d1e12..024939b0b 100644 --- a/apps/scandic-web/server/routers/user/query.ts +++ b/packages/trpc/lib/routers/user/query.ts @@ -6,13 +6,15 @@ import { protectedProcedure, safeProtectedProcedure, } from "@scandic-hotels/trpc/procedures" -import { getFriendsMembership } from "@scandic-hotels/trpc/routers/user/helpers" +import { + getFriendsMembership, + getMembershipCards, +} from "@scandic-hotels/trpc/routers/user/helpers" import { getVerifiedUser } from "@scandic-hotels/trpc/routers/user/utils" import { toApiLang } from "@scandic-hotels/trpc/utils" import { isValidSession } from "@scandic-hotels/trpc/utils/session" -import { getMembershipCards } from "@/utils/user" - +import { Transactions } from "../../enums/transactions" import { friendTransactionsInput, getSavedPaymentCardsInput, @@ -30,11 +32,7 @@ import { import type { LoginType } from "@scandic-hotels/trpc/types/loginType" -import type { - // LoginType, - TrackingSDKUserData, -} from "@/types/components/tracking" -import { Transactions } from "@/types/enums/transactions" +import type { TrackingUserData } from "../types" export const userQueryRouter = router({ get: protectedProcedure @@ -148,7 +146,7 @@ export const userQueryRouter = router({ metricsUserTrackingInfo.start() - const notLoggedInUserTrackingData: TrackingSDKUserData = { + const notLoggedInUserTrackingData: TrackingUserData = { loginStatus: "Non-logged in", } @@ -190,7 +188,7 @@ export const userQueryRouter = router({ const membership = getFriendsMembership(verifiedUserData.data.loyalty) - const loggedInUserTrackingData: TrackingSDKUserData = { + const loggedInUserTrackingData: TrackingUserData = { loginStatus: "logged in", loginType: ctx.session.token.loginType as LoginType, memberId: verifiedUserData.data.profileId, diff --git a/apps/scandic-web/components/Forms/Signup/schema.ts b/packages/trpc/lib/routers/user/schemas.ts similarity index 91% rename from apps/scandic-web/components/Forms/Signup/schema.ts rename to packages/trpc/lib/routers/user/schemas.ts index d2ab3dfac..c72001e3a 100644 --- a/apps/scandic-web/components/Forms/Signup/schema.ts +++ b/packages/trpc/lib/routers/user/schemas.ts @@ -1,7 +1,7 @@ import { z } from "zod" -import { passwordValidator } from "@/utils/zod/passwordValidator" -import { phoneValidator } from "@/utils/zod/phoneValidator" +import { passwordValidator } from "@scandic-hotels/common/utils/zod/passwordValidator" +import { phoneValidator } from "@scandic-hotels/common/utils/zod/phoneValidator" export const signupErrors = { COUNTRY_REQUIRED: "COUNTRY_REQUIRED", diff --git a/apps/scandic-web/server/routers/user/tempFriendTransactions.json b/packages/trpc/lib/routers/user/tempFriendTransactions.json similarity index 100% rename from apps/scandic-web/server/routers/user/tempFriendTransactions.json rename to packages/trpc/lib/routers/user/tempFriendTransactions.json diff --git a/packages/trpc/lib/routers/user/utils.ts b/packages/trpc/lib/routers/user/utils.ts index 36dd3dd7e..2f5f95e41 100644 --- a/packages/trpc/lib/routers/user/utils.ts +++ b/packages/trpc/lib/routers/user/utils.ts @@ -1,12 +1,27 @@ +import { myStay } from "@scandic-hotels/common/constants/routes/myStay" +import { dt } from "@scandic-hotels/common/dt" import { createCounter } from "@scandic-hotels/common/telemetry" +import * as maskValue from "@scandic-hotels/common/utils/maskValue" +import { getCurrentWebUrl } from "@scandic-hotels/common/utils/url" +import { env } from "../../../env/server" import * as api from "../../api" +import { countries } from "../../constants/countries" import { cache } from "../../DUPLICATED/cache" +import { getFriendsMembership } from "../../routers/user/helpers" +import { creditCardsSchema } from "../../routers/user/output" +import { toApiLang } from "../../utils" +import { encrypt } from "../../utils/encryption" import { isValidSession } from "../../utils/session" import { getUserSchema } from "./output" +import { type FriendTransaction, getStaysSchema, type Stay } from "./output" +import type { Lang } from "@scandic-hotels/common/constants/language" +import type { LangRoute } from "@scandic-hotels/common/constants/routes/langRoute" import type { Session } from "next-auth" +import type { User } from "../../types/user" + export async function getMembershipNumber( session: Session | null ): Promise { @@ -91,3 +106,279 @@ export const getVerifiedUser = cache( return verifiedData } ) + +export async function getPreviousStays( + accessToken: string, + limit: number = 10, + language: Lang, + cursor?: string +) { + const getPreviousStaysCounter = createCounter("user", "getPreviousStays") + const metricsGetPreviousStays = getPreviousStaysCounter.init({ + limit, + cursor, + language, + }) + + metricsGetPreviousStays.start() + + const params: Record = { + limit: String(limit), + language: toApiLang(language), + } + + if (cursor) { + params.offset = cursor + } + + const apiResponse = await api.get( + api.endpoints.v1.Booking.Stays.past, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + params + ) + + if (!apiResponse.ok) { + await metricsGetPreviousStays.httpError(apiResponse) + return null + } + + const apiJson = await apiResponse.json() + + const verifiedData = getStaysSchema.safeParse(apiJson) + if (!verifiedData.success) { + metricsGetPreviousStays.validationError(verifiedData.error) + return null + } + + metricsGetPreviousStays.success() + + return verifiedData.data +} + +export async function getUpcomingStays( + accessToken: string, + limit: number = 10, + language: Lang, + cursor?: string +) { + const getUpcomingStaysCounter = createCounter("user", "getUpcomingStays") + const metricsGetUpcomingStays = getUpcomingStaysCounter.init({ + limit, + cursor, + language, + }) + + metricsGetUpcomingStays.start() + + const params: Record = { + limit: String(limit), + language: toApiLang(language), + } + + if (cursor) { + params.offset = cursor + } + + const apiResponse = await api.get( + api.endpoints.v1.Booking.Stays.future, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + params + ) + + if (!apiResponse.ok) { + await metricsGetUpcomingStays.httpError(apiResponse) + return null + } + + const apiJson = await apiResponse.json() + + const verifiedData = getStaysSchema.safeParse(apiJson) + if (!verifiedData.success) { + metricsGetUpcomingStays.validationError(verifiedData.error) + return null + } + + metricsGetUpcomingStays.success() + + return verifiedData.data +} + +export function parsedUser(data: User, isMFA: boolean) { + const country = countries.find((c) => c.code === data.address?.countryCode) + + const user = { + address: { + city: data.address?.city, + country: country?.name ?? "", + countryCode: data.address?.countryCode, + streetAddress: data.address?.streetAddress, + zipCode: data.address?.zipCode, + }, + dateOfBirth: data.dateOfBirth, + email: data.email, + firstName: data.firstName, + language: data.language, + lastName: data.lastName, + membershipNumber: data.membershipNumber, + membership: data.loyalty ? getFriendsMembership(data.loyalty) : null, + loyalty: data.loyalty, + name: `${data.firstName} ${data.lastName}`, + phoneNumber: data.phoneNumber, + profileId: data.profileId, + } + + if (!isMFA) { + if (user.address.city) { + user.address.city = maskValue.text(user.address.city) + } + if (user.address.streetAddress) { + user.address.streetAddress = maskValue.text(user.address.streetAddress) + } + + user.address.zipCode = data.address?.zipCode + ? maskValue.text(data.address.zipCode) + : "" + + user.dateOfBirth = maskValue.all(user.dateOfBirth) + + user.email = maskValue.email(user.email) + + user.phoneNumber = user.phoneNumber ? maskValue.phone(user.phoneNumber) : "" + } + + return user +} + +export const getCreditCards = cache( + async ({ + session, + onlyNonExpired, + }: { + session: Session + onlyNonExpired?: boolean + }) => { + const getCreditCardsCounter = createCounter("user", "getCreditCards") + const metricsGetCreditCards = getCreditCardsCounter.init({ + onlyNonExpired, + }) + + metricsGetCreditCards.start() + + const apiResponse = await api.get(api.endpoints.v1.Profile.creditCards, { + headers: { + Authorization: `Bearer ${session.token.access_token}`, + }, + }) + + if (!apiResponse.ok) { + await metricsGetCreditCards.httpError(apiResponse) + return null + } + + const apiJson = await apiResponse.json() + const verifiedData = creditCardsSchema.safeParse(apiJson) + if (!verifiedData.success) { + metricsGetCreditCards.validationError(verifiedData.error) + return null + } + + const result = verifiedData.data.data.filter((card) => { + if (onlyNonExpired) { + try { + const expirationDate = dt(card.expirationDate).startOf("day") + const currentDate = dt().startOf("day") + return expirationDate > currentDate + } catch (_) { + return false + } + } + return true + }) + + metricsGetCreditCards.success() + + return result + } +) + +export async function updateStaysBookingUrl( + data: Stay[], + session: Session, + lang: Lang +): Promise + +export async function updateStaysBookingUrl( + data: FriendTransaction[], + session: Session, + lang: Lang +): Promise + +export async function updateStaysBookingUrl( + data: Stay[] | FriendTransaction[], + session: Session, + lang: Lang +) { + const user = await getVerifiedUser({ + session, + }) + + if (user && !("error" in user)) { + return data.map((d) => { + const originalString = + d.attributes.confirmationNumber.toString() + "," + user.data.lastName + const encryptedBookingValue = encrypt(originalString) + + // Get base URL with fallback for ephemeral environments (like deploy previews). + const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com" + + // Construct Booking URL. + const bookingUrl = !env.isLangLive(lang) + ? new URL( + getCurrentWebUrl({ + path: myBookingPath[lang], + lang, + baseUrl, + }) + ) + : new URL(myStay[lang], baseUrl) + + // Add search parameters. + if (encryptedBookingValue) { + bookingUrl.searchParams.set("RefId", encryptedBookingValue) + } else { + bookingUrl.searchParams.set("lastName", user.data.lastName) + bookingUrl.searchParams.set( + "bookingId", + d.attributes.confirmationNumber.toString() + ) + } + + return { + ...d, + attributes: { + ...d.attributes, + bookingUrl: bookingUrl.toString(), + }, + } + }) + } + + return data +} + +export const myBookingPath: LangRoute = { + da: "/hotelreservation/min-booking", + de: "/hotelreservation/my-booking", + en: "/hotelreservation/my-booking", + fi: "/varaa-hotelli/varauksesi", + no: "/hotelreservation/my-booking", + sv: "/hotelreservation/din-bokning", +}