feat(SW-66, SW-348): search functionality and ui
This commit is contained in:
@@ -18,7 +18,7 @@ export const userMutationRouter = router({
|
||||
"api.user.creditCard.add start",
|
||||
JSON.stringify({ query: { language: input.language } })
|
||||
)
|
||||
const apiResponse = await api.post(api.endpoints.v1.intiateSaveCard, {
|
||||
const apiResponse = await api.post(api.endpoints.v1.initiateSaveCard, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
|
||||
@@ -1,40 +1,54 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { countriesMap } from "@/components/TempDesignSystem/Form/Country/countries"
|
||||
import { getMembership } from "@/utils/user"
|
||||
|
||||
export const getUserSchema = z.object({
|
||||
address: z.object({
|
||||
city: z.string().optional(),
|
||||
country: z.string().optional(),
|
||||
countryCode: z.nativeEnum(countriesMap).optional(),
|
||||
streetAddress: z.string().optional(),
|
||||
zipCode: z.string().optional(),
|
||||
}),
|
||||
dateOfBirth: z.string().optional().default("1900-01-01"),
|
||||
email: z.string().email(),
|
||||
firstName: z.string(),
|
||||
language: z.string().optional(),
|
||||
lastName: z.string(),
|
||||
memberships: z.array(
|
||||
z.object({
|
||||
currentPoints: z.number(),
|
||||
expirationDate: z.string(),
|
||||
membershipNumber: z.string(),
|
||||
membershipLevel: z.string().optional(),
|
||||
memberSince: z.string(),
|
||||
membershipType: z.string(),
|
||||
nextLevel: z.string().optional(),
|
||||
nightsToTopTier: z.number().optional(),
|
||||
pointsExpiryDate: z.string().optional(),
|
||||
pointsRequiredToNextlevel: z.number().optional(),
|
||||
pointsToExpire: z.number().optional(),
|
||||
tierExpirationDate: z.string().optional(),
|
||||
})
|
||||
),
|
||||
phoneNumber: z.string().optional(),
|
||||
profileId: z.string(),
|
||||
export const membershipSchema = z.object({
|
||||
currentPoints: z.number(),
|
||||
expirationDate: z.string(),
|
||||
membershipNumber: z.string(),
|
||||
membershipLevel: z.string().optional(),
|
||||
memberSince: z.string(),
|
||||
membershipType: z.string(),
|
||||
nextLevel: z.string().optional(),
|
||||
nightsToTopTier: z.number().optional(),
|
||||
pointsExpiryDate: z.string().optional(),
|
||||
pointsRequiredToNextlevel: z.number().optional(),
|
||||
pointsToExpire: z.number().optional(),
|
||||
tierExpirationDate: z.string().optional(),
|
||||
})
|
||||
|
||||
export const getUserSchema = z
|
||||
.object({
|
||||
data: z.object({
|
||||
attributes: z.object({
|
||||
address: z.object({
|
||||
city: z.string().optional(),
|
||||
country: z.string().optional(),
|
||||
countryCode: z.nativeEnum(countriesMap).optional(),
|
||||
streetAddress: z.string().optional(),
|
||||
zipCode: z.string().optional(),
|
||||
}),
|
||||
dateOfBirth: z.string().optional().default("1900-01-01"),
|
||||
email: z.string().email(),
|
||||
firstName: z.string(),
|
||||
language: z.string().optional(),
|
||||
lastName: z.string(),
|
||||
memberships: z.array(membershipSchema),
|
||||
phoneNumber: z.string().optional(),
|
||||
profileId: z.string(),
|
||||
}),
|
||||
type: z.string(),
|
||||
}),
|
||||
})
|
||||
.transform((apiResponse) => {
|
||||
return {
|
||||
...apiResponse.data.attributes,
|
||||
membership: getMembership(apiResponse.data.attributes.memberships),
|
||||
name: `${apiResponse.data.attributes.firstName} ${apiResponse.data.attributes.lastName}`,
|
||||
}
|
||||
})
|
||||
|
||||
// Schema is the same for upcoming and previous stays endpoints
|
||||
export const getStaysSchema = z.object({
|
||||
data: z.array(
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { env } from "@/env/server"
|
||||
import * as api from "@/lib/api"
|
||||
import {
|
||||
protectedProcedure,
|
||||
@@ -13,26 +11,24 @@ import { countries } from "@/components/TempDesignSystem/Form/Country/countries"
|
||||
import * as maskValue from "@/utils/maskValue"
|
||||
import { getMembership, getMembershipCards } from "@/utils/user"
|
||||
|
||||
import encryptValue from "../utils/encryptValue"
|
||||
import { friendTransactionsInput, staysInput } from "./input"
|
||||
import {
|
||||
creditCardsSchema,
|
||||
FriendTransaction,
|
||||
getFriendTransactionsSchema,
|
||||
getMembershipCardsSchema,
|
||||
getStaysSchema,
|
||||
getUserSchema,
|
||||
Stay,
|
||||
} from "./output"
|
||||
import { benefits, extendedUser, nextLevelPerks } from "./temp"
|
||||
import { updateStaysBookingUrl } from "./utils"
|
||||
|
||||
import type { Session } from "next-auth"
|
||||
|
||||
import { RewardTransactionTypes } from "@/types/components/myPages/myPage/enums"
|
||||
import type {
|
||||
LoginType,
|
||||
TrackingSDKUserData,
|
||||
} from "@/types/components/tracking"
|
||||
import { BlocksEnums } from "@/types/enums/blocks"
|
||||
import { Transactions } from "@/types/enums/transactions"
|
||||
import type { MembershipLevel } from "@/constants/membershipLevels"
|
||||
|
||||
// OpenTelemetry metrics: User
|
||||
@@ -93,7 +89,6 @@ export async function getVerifiedUser({ session }: { session: Session }) {
|
||||
getVerifiedUserCounter.add(1)
|
||||
console.info("api.user.profile getVerifiedUser start", JSON.stringify({}))
|
||||
const apiResponse = await api.get(api.endpoints.v1.profile, {
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.token.access_token}`,
|
||||
},
|
||||
@@ -147,7 +142,7 @@ export async function getVerifiedUser({ session }: { session: Session }) {
|
||||
return null
|
||||
}
|
||||
|
||||
const verifiedData = getUserSchema.safeParse(apiJson.data.attributes)
|
||||
const verifiedData = getUserSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
getVerifiedUserFailCounter.add(1, {
|
||||
error_type: "validation_error",
|
||||
@@ -166,109 +161,6 @@ export async function getVerifiedUser({ session }: { session: Session }) {
|
||||
return verifiedData
|
||||
}
|
||||
|
||||
function fakingRequest<T>(payload: T): Promise<T> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(payload)
|
||||
}, 1500)
|
||||
})
|
||||
}
|
||||
|
||||
async function updateStaysBookingUrl(
|
||||
data: Stay[],
|
||||
token: string,
|
||||
lang: Lang
|
||||
): Promise<Stay[]>
|
||||
|
||||
async function updateStaysBookingUrl(
|
||||
data: FriendTransaction[],
|
||||
token: string,
|
||||
lang: Lang
|
||||
): Promise<FriendTransaction[]>
|
||||
|
||||
async function updateStaysBookingUrl(
|
||||
data: Stay[] | FriendTransaction[],
|
||||
token: string,
|
||||
lang: Lang
|
||||
) {
|
||||
// Tenporary API call needed till we have user name in ctx session data
|
||||
getProfileCounter.add(1)
|
||||
console.info("api.user.profile updatebookingurl start", JSON.stringify({}))
|
||||
const apiResponse = await api.get(api.endpoints.v1.profile, {
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
// Temporary Url, domain and lang support for current web
|
||||
let localeDomain = env.PUBLIC_URL
|
||||
let fullBookingUrl = localeDomain + "/hotelreservation/my-booking"
|
||||
switch (lang) {
|
||||
case Lang.sv:
|
||||
localeDomain = localeDomain?.replace(".com", ".se")
|
||||
fullBookingUrl = localeDomain + "/hotelreservation/din-bokning"
|
||||
break
|
||||
case Lang.no:
|
||||
localeDomain = localeDomain?.replace(".com", ".no")
|
||||
fullBookingUrl = localeDomain + "/hotelreservation/my-booking"
|
||||
break
|
||||
case Lang.da:
|
||||
localeDomain = localeDomain?.replace(".com", ".dk")
|
||||
fullBookingUrl = localeDomain + "/hotelreservation/min-booking"
|
||||
break
|
||||
case Lang.fi:
|
||||
localeDomain = localeDomain?.replace(".com", ".fi")
|
||||
fullBookingUrl = localeDomain + "/varaa-hotelli/varauksesi"
|
||||
break
|
||||
case Lang.de:
|
||||
localeDomain = localeDomain?.replace(".com", ".de")
|
||||
fullBookingUrl = localeDomain + "/hotelreservation/my-booking"
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if (apiResponse.ok) {
|
||||
getProfileSuccessCounter.add(1)
|
||||
console.info(
|
||||
"api.user.profile updatebookingurl success",
|
||||
JSON.stringify({})
|
||||
)
|
||||
const apiJson = await apiResponse.json()
|
||||
if (apiJson.data?.attributes) {
|
||||
return data.map((d) => {
|
||||
const originalString =
|
||||
d.attributes.confirmationNumber.toString() +
|
||||
"," +
|
||||
apiJson.data.attributes.lastName
|
||||
const encryptedBookingValue = encryptValue(originalString)
|
||||
const bookingUrl = !!encryptedBookingValue
|
||||
? fullBookingUrl + "?RefId=" + encryptedBookingValue
|
||||
: fullBookingUrl +
|
||||
"?lastName=" +
|
||||
apiJson.data.attributes.lastName +
|
||||
"&bookingId=" +
|
||||
d.attributes.confirmationNumber
|
||||
return {
|
||||
...d,
|
||||
attributes: {
|
||||
...d.attributes,
|
||||
bookingUrl: bookingUrl,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
getProfileFailCounter.add(1, { error: JSON.stringify(apiResponse) })
|
||||
console.info(
|
||||
"api.user.profile updatebookingurl error",
|
||||
JSON.stringify({ error: apiResponse })
|
||||
)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export const userQueryRouter = router({
|
||||
get: protectedProcedure
|
||||
.use(async function (opts) {
|
||||
@@ -299,7 +191,6 @@ export const userQueryRouter = router({
|
||||
)
|
||||
|
||||
const user = {
|
||||
...extendedUser,
|
||||
address: {
|
||||
city: verifiedData.data.address.city,
|
||||
country: country?.name ?? "",
|
||||
@@ -312,6 +203,7 @@ export const userQueryRouter = router({
|
||||
firstName: verifiedData.data.firstName,
|
||||
language: verifiedData.data.language,
|
||||
lastName: verifiedData.data.lastName,
|
||||
membership: getMembership(verifiedData.data.memberships),
|
||||
memberships: verifiedData.data.memberships,
|
||||
name: `${verifiedData.data.firstName} ${verifiedData.data.lastName}`,
|
||||
phoneNumber: verifiedData.data.phoneNumber,
|
||||
@@ -460,16 +352,6 @@ export const userQueryRouter = router({
|
||||
}
|
||||
return loggedInUserTrackingData
|
||||
}),
|
||||
benefits: router({
|
||||
current: protectedProcedure.query(async function (opts) {
|
||||
// TODO: Make request to get user data from Scandic API
|
||||
return await fakingRequest<typeof benefits>(benefits)
|
||||
}),
|
||||
nextLevel: protectedProcedure.query(async function (opts) {
|
||||
// TODO: Make request to get user data from Scandic API
|
||||
return await fakingRequest<typeof nextLevelPerks>(nextLevelPerks)
|
||||
}),
|
||||
}),
|
||||
|
||||
stays: router({
|
||||
previous: protectedProcedure
|
||||
@@ -735,7 +617,7 @@ export const userQueryRouter = router({
|
||||
)
|
||||
|
||||
const pageData = updatedData
|
||||
.filter((t) => t.type !== RewardTransactionTypes.expired)
|
||||
.filter((t) => t.type !== Transactions.rewardType.expired)
|
||||
.sort((a, b) => {
|
||||
// 'BALFWD' are transactions from Opera migration that happended in May 2021
|
||||
if (a.attributes.confirmationNumber === "BALFWD") return 1
|
||||
@@ -787,7 +669,6 @@ export const userQueryRouter = router({
|
||||
getCreditCardsCounter.add(1)
|
||||
console.info("api.profile.creditCards start", JSON.stringify({}))
|
||||
const apiResponse = await api.get(api.endpoints.v1.creditCards, {
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
export const benefits = [
|
||||
{
|
||||
id: 1,
|
||||
value: "€5 voucher",
|
||||
explanation: "to spend in bar & restaurant for each night",
|
||||
subtitle:
|
||||
"Lorem ipsum dolor sit amet consectetur. Pharetra lectus sagittis turpis blandit feugiat amet enim massa.",
|
||||
href: "#",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
value: "Breakfast to go",
|
||||
explanation: "for early birds, when staying",
|
||||
subtitle:
|
||||
"Lorem ipsum dolor sit amet consectetur. Pharetra lectus sagittis turpis blandit feugiat amet enim massa.",
|
||||
href: "#",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
value: "15% discount",
|
||||
explanation: "in the restaurant & the bar",
|
||||
subtitle:
|
||||
"Lorem ipsum dolor sit amet consectetur. Pharetra lectus sagittis turpis blandit feugiat amet enim massa.",
|
||||
href: "#",
|
||||
},
|
||||
]
|
||||
|
||||
export const challenges = {
|
||||
journeys: [
|
||||
{
|
||||
tag: "After work queen",
|
||||
title: "Try 3 Hotel Bars, Pocket 200 Points",
|
||||
},
|
||||
{
|
||||
tag: "Dine & Shine",
|
||||
title: "Visit 3 scandic Restaurants, Earn 150 Points",
|
||||
},
|
||||
],
|
||||
victories: [
|
||||
{
|
||||
tag: "Capital Explorer",
|
||||
title: "Stay in 3 scandic hotels, in three Capitals, Gain 2000 Points",
|
||||
},
|
||||
{
|
||||
tag: "Friends Feast",
|
||||
title: "Dine with 3 Buddies, Snag a Free Breakfast",
|
||||
},
|
||||
{
|
||||
tag: "Eco Warrior",
|
||||
title: "Choose Green, Get 500 Points",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const nextLevelPerks = {
|
||||
nextLevel: "Close Friend",
|
||||
perks: [
|
||||
{
|
||||
id: 1,
|
||||
|
||||
explanation: "Free soft drink voucher for the kids when staying",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
|
||||
explanation: "Free early check in",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
explanation: "25% extra bonus points on each stay",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
explanation: "25% extra bonus points on each stay",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const shortcuts = [
|
||||
{
|
||||
href: "#",
|
||||
title: "My Benefit",
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Program overview",
|
||||
},
|
||||
// {
|
||||
// href: "#",
|
||||
// title: "Scandic Friends shop",
|
||||
// },
|
||||
// {
|
||||
// href: "#",
|
||||
// title: "Fire and safety",
|
||||
// },
|
||||
// {
|
||||
// href: "#",
|
||||
// title: "Our sustainability work",
|
||||
// },
|
||||
// {
|
||||
// href: "#",
|
||||
// title: "How you earn points",
|
||||
// },
|
||||
// {
|
||||
// href: "#",
|
||||
// title: "How you use points",
|
||||
// },
|
||||
// {
|
||||
// href: "#",
|
||||
// title: "Missing points",
|
||||
// },
|
||||
// {
|
||||
// href: "#",
|
||||
// title: "Our term and conditions",
|
||||
// },
|
||||
]
|
||||
|
||||
export const extendedUser = {
|
||||
journeys: challenges.journeys,
|
||||
nights: 14,
|
||||
shortcuts,
|
||||
victories: challenges.victories,
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { env } from "@/env/server"
|
||||
import * as api from "@/lib/api"
|
||||
|
||||
import encryptValue from "../utils/encryptValue"
|
||||
|
||||
import type { FriendTransaction, Stay } from "./output"
|
||||
|
||||
const meter = metrics.getMeter("trpc.user")
|
||||
const getProfileCounter = meter.createCounter("trpc.user.profile")
|
||||
const getProfileSuccessCounter = meter.createCounter("trpc.user.profile-success")
|
||||
const getProfileFailCounter = meter.createCounter("trpc.user.profile-fail")
|
||||
|
||||
async function updateStaysBookingUrl(
|
||||
data: Stay[],
|
||||
token: string,
|
||||
lang: Lang
|
||||
): Promise<Stay[]>
|
||||
|
||||
async function updateStaysBookingUrl(
|
||||
data: FriendTransaction[],
|
||||
token: string,
|
||||
lang: Lang
|
||||
): Promise<FriendTransaction[]>
|
||||
|
||||
async function updateStaysBookingUrl(
|
||||
data: Stay[] | FriendTransaction[],
|
||||
token: string,
|
||||
lang: Lang
|
||||
) {
|
||||
// Temporary API call needed till we have user name in ctx session data
|
||||
getProfileCounter.add(1)
|
||||
console.info("api.user.profile updatebookingurl start", JSON.stringify({}))
|
||||
const apiResponse = await api.get(api.endpoints.v1.profile, {
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
// Temporary Url, domain and lang support for current web
|
||||
const bookingUrl = new URL(
|
||||
"/hotelreservation/my-booking",
|
||||
env.PUBLIC_URL ?? ""
|
||||
)
|
||||
switch (lang) {
|
||||
case Lang.sv:
|
||||
bookingUrl.host = bookingUrl.host.replace(".com", ".se")
|
||||
bookingUrl.pathname = "/hotelreservation/din-bokning"
|
||||
break
|
||||
case Lang.no:
|
||||
bookingUrl.host = bookingUrl.host.replace(".com", ".no")
|
||||
bookingUrl.pathname = "/hotelreservation/my-booking"
|
||||
break
|
||||
case Lang.da:
|
||||
bookingUrl.host = bookingUrl.host.replace(".com", ".dk")
|
||||
bookingUrl.pathname = "/hotelreservation/min-booking"
|
||||
break
|
||||
case Lang.fi:
|
||||
bookingUrl.host = bookingUrl.host.replace(".com", ".fi")
|
||||
bookingUrl.pathname = "/varaa-hotelli/varauksesi"
|
||||
break
|
||||
case Lang.de:
|
||||
bookingUrl.host = bookingUrl.host.replace(".com", ".de")
|
||||
bookingUrl.pathname = "/hotelreservation/my-booking"
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
getProfileFailCounter.add(1, { error: JSON.stringify(apiResponse) })
|
||||
console.info(
|
||||
"api.user.profile updatebookingurl error",
|
||||
JSON.stringify({ error: apiResponse })
|
||||
)
|
||||
return data
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
if (!apiJson.data?.attributes) {
|
||||
return data
|
||||
}
|
||||
|
||||
getProfileSuccessCounter.add(1)
|
||||
console.info(
|
||||
"api.user.profile updatebookingurl success",
|
||||
JSON.stringify({})
|
||||
)
|
||||
|
||||
return data.map((d) => {
|
||||
const originalString =
|
||||
d.attributes.confirmationNumber.toString() +
|
||||
"," +
|
||||
apiJson.data.attributes.lastName
|
||||
const encryptedBookingValue = encryptValue(originalString)
|
||||
if (!!encryptedBookingValue) {
|
||||
bookingUrl.searchParams.set("RefId", encryptedBookingValue)
|
||||
} else {
|
||||
bookingUrl.searchParams.set("lastName", apiJson.data.attributes.lastName)
|
||||
bookingUrl.searchParams.set(
|
||||
"bookingId",
|
||||
d.attributes.confirmationNumber
|
||||
)
|
||||
}
|
||||
return {
|
||||
...d,
|
||||
attributes: {
|
||||
...d.attributes,
|
||||
bookingUrl: bookingUrl.toString(),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export { updateStaysBookingUrl }
|
||||
Reference in New Issue
Block a user