Merged in monorepo-step-1 (pull request #1080)
Migrate to a monorepo setup - step 1 * Move web to subfolder /apps/scandic-web * Yarn + transitive deps - Move to yarn - design-system package removed for now since yarn doesn't support the parameter for token (ie project currently broken) - Add missing transitive dependencies as Yarn otherwise prevents these imports - VS Code doesn't pick up TS path aliases unless you open /apps/scandic-web instead of root (will be fixed with monorepo) * Pin framer-motion to temporarily fix typing issue https://github.com/adobe/react-spectrum/issues/7494 * Pin zod to avoid typ error There seems to have been a breaking change in the types returned by zod where error is now returned as undefined instead of missing in the type. We should just handle this but to avoid merge conflicts just pin the dependency for now. * Pin react-intl version Pin version of react-intl to avoid tiny type issue where formatMessage does not accept a generic any more. This will be fixed in a future commit, but to avoid merge conflicts just pin for now. * Pin typescript version Temporarily pin version as newer versions as stricter and results in a type error. Will be fixed in future commit after merge. * Setup workspaces * Add design-system as a monorepo package * Remove unused env var DESIGN_SYSTEM_ACCESS_TOKEN * Fix husky for monorepo setup * Update netlify.toml * Add lint script to root package.json * Add stub readme * Fix react-intl formatMessage types * Test netlify.toml in root * Remove root toml * Update netlify.toml publish path * Remove package-lock.json * Update build for branch/preview builds Approved-by: Linus Flood
This commit is contained in:
committed by
Linus Flood
parent
667cab6fb6
commit
80100e7631
6
apps/scandic-web/server/routers/user/index.ts
Normal file
6
apps/scandic-web/server/routers/user/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { mergeRouters } from "@/server/trpc"
|
||||
|
||||
import { userMutationRouter } from "./mutation"
|
||||
import { userQueryRouter } from "./query"
|
||||
|
||||
export const userRouter = mergeRouters(userQueryRouter, userMutationRouter)
|
||||
65
apps/scandic-web/server/routers/user/input.ts
Normal file
65
apps/scandic-web/server/routers/user/input.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
|
||||
import { signUpSchema } from "@/components/Forms/Signup/schema"
|
||||
|
||||
// Query
|
||||
export const staysInput = z
|
||||
.object({
|
||||
cursor: z
|
||||
.number()
|
||||
.optional()
|
||||
.transform((num) => (num ? String(num) : undefined)),
|
||||
limit: z
|
||||
.number()
|
||||
.min(0)
|
||||
.default(6)
|
||||
.transform((num) => String(num)),
|
||||
})
|
||||
.default({})
|
||||
|
||||
export const friendTransactionsInput = z
|
||||
.object({
|
||||
limit: z.number().int().positive(),
|
||||
page: z.number().int().positive(),
|
||||
})
|
||||
.default({ limit: 5, page: 1 })
|
||||
|
||||
// Mutation
|
||||
export const addCreditCardInput = z.object({
|
||||
language: z.string(),
|
||||
})
|
||||
|
||||
export const deleteCreditCardInput = z.object({
|
||||
creditCardId: z.string(),
|
||||
})
|
||||
|
||||
export const saveCreditCardInput = z.object({
|
||||
transactionId: z.string(),
|
||||
merchantId: z.string().optional(),
|
||||
})
|
||||
|
||||
export const signupInput = signUpSchema
|
||||
.extend({
|
||||
language: z.nativeEnum(Lang),
|
||||
})
|
||||
.omit({ termsAccepted: true })
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
phoneNumber: data.phoneNumber.replace(/\s+/g, ""),
|
||||
address: {
|
||||
...data.address,
|
||||
city: "",
|
||||
country: "",
|
||||
streetAddress: "",
|
||||
},
|
||||
}))
|
||||
|
||||
export const getSavedPaymentCardsInput = z.object({
|
||||
supportedCards: z.array(z.string()),
|
||||
})
|
||||
|
||||
export type GetSavedPaymentCardsInput = z.input<
|
||||
typeof getSavedPaymentCardsInput
|
||||
>
|
||||
259
apps/scandic-web/server/routers/user/mutation.ts
Normal file
259
apps/scandic-web/server/routers/user/mutation.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
|
||||
import { signupVerify } from "@/constants/routes/signup"
|
||||
import { env } from "@/env/server"
|
||||
import * as api from "@/lib/api"
|
||||
import { serverErrorByStatus } from "@/server/errors/trpc"
|
||||
import {
|
||||
initiateSaveCardSchema,
|
||||
subscriberIdSchema,
|
||||
} from "@/server/routers/user/output"
|
||||
import { protectedProcedure, router, serviceProcedure } from "@/server/trpc"
|
||||
|
||||
import {
|
||||
addCreditCardInput,
|
||||
deleteCreditCardInput,
|
||||
saveCreditCardInput,
|
||||
signupInput,
|
||||
} from "./input"
|
||||
|
||||
const meter = metrics.getMeter("trpc.user")
|
||||
const generatePreferencesLinkCounter = meter.createCounter(
|
||||
"trpc.user.generatePreferencesLink"
|
||||
)
|
||||
const generatePreferencesLinkSuccessCounter = meter.createCounter(
|
||||
"trpc.user.generatePreferencesLink-success"
|
||||
)
|
||||
const generatePreferencesLinkFailCounter = meter.createCounter(
|
||||
"trpc.user.generatePreferencesLink-fail"
|
||||
)
|
||||
const signupCounter = meter.createCounter("trpc.user.signup")
|
||||
const signupSuccessCounter = meter.createCounter("trpc.user.signup-success")
|
||||
const signupFailCounter = meter.createCounter("trpc.user.signup-fail")
|
||||
|
||||
export const userMutationRouter = router({
|
||||
creditCard: router({
|
||||
add: protectedProcedure.input(addCreditCardInput).mutation(async function ({
|
||||
ctx,
|
||||
input,
|
||||
}) {
|
||||
console.info(
|
||||
"api.user.creditCard.add start",
|
||||
JSON.stringify({ query: { language: input.language } })
|
||||
)
|
||||
const apiResponse = await api.post(
|
||||
api.endpoints.v1.Profile.CreditCards.initiateSaveCard,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
body: {
|
||||
language: input.language,
|
||||
mobileToken: false,
|
||||
redirectUrl: `api/web/add-card-callback/${input.language}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
console.error(
|
||||
"api.user.creditCard.add error",
|
||||
JSON.stringify({
|
||||
query: { language: input.language },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
error: text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = initiateSaveCardSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
console.error(
|
||||
"api.user.creditCard.add validation error",
|
||||
JSON.stringify({
|
||||
query: { language: input.language },
|
||||
error: verifiedData.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
console.info(
|
||||
"api.user.creditCard.add success",
|
||||
JSON.stringify({ query: { language: input.language } })
|
||||
)
|
||||
return verifiedData.data.data
|
||||
}),
|
||||
save: protectedProcedure
|
||||
.input(saveCreditCardInput)
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
console.info("api.user.creditCard.save start", JSON.stringify({}))
|
||||
const apiResponse = await api.post(
|
||||
api.endpoints.v1.Profile.CreditCards.transaction(input.transactionId),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
console.error(
|
||||
"api.user.creditCard.save error",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return false
|
||||
}
|
||||
console.info("api.user.creditCard.save success", JSON.stringify({}))
|
||||
return true
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.input(deleteCreditCardInput)
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
console.info(
|
||||
"api.user.creditCard.delete start",
|
||||
JSON.stringify({ query: {} })
|
||||
)
|
||||
const apiResponse = await api.remove(
|
||||
api.endpoints.v1.Profile.CreditCards.deleteCreditCard(
|
||||
input.creditCardId
|
||||
),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
console.error(
|
||||
"api.user.creditCard.delete error",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
query: {},
|
||||
})
|
||||
)
|
||||
return false
|
||||
}
|
||||
console.info("api.user.creditCard.delete success", JSON.stringify({}))
|
||||
return true
|
||||
}),
|
||||
}),
|
||||
generatePreferencesLink: protectedProcedure.mutation(async function ({
|
||||
ctx,
|
||||
}) {
|
||||
generatePreferencesLinkCounter.add(1)
|
||||
const apiResponse = await api.get(api.endpoints.v1.Profile.subscriberId, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
generatePreferencesLinkFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.user.subscriberId error ",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await apiResponse.json()
|
||||
|
||||
const validatedData = subscriberIdSchema.safeParse(data)
|
||||
|
||||
if (!validatedData.success) {
|
||||
generatePreferencesLinkSuccessCounter.add(1, {
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.user.generatePreferencesLink validation error",
|
||||
JSON.stringify({
|
||||
error: validatedData.error,
|
||||
})
|
||||
)
|
||||
console.error(validatedData.error.format())
|
||||
|
||||
return null
|
||||
}
|
||||
const preferencesLink = new URL(env.SALESFORCE_PREFERENCE_BASE_URL)
|
||||
preferencesLink.searchParams.set("subKey", validatedData.data.subscriberId)
|
||||
|
||||
generatePreferencesLinkSuccessCounter.add(1)
|
||||
return preferencesLink.toString()
|
||||
}),
|
||||
signup: serviceProcedure.input(signupInput).mutation(async function ({
|
||||
ctx,
|
||||
input,
|
||||
}) {
|
||||
signupCounter.add(1)
|
||||
|
||||
const apiResponse = await api.post(api.endpoints.v1.Profile.profile, {
|
||||
body: input,
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
signupFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
error: text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.user.signup api error",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
error: text,
|
||||
},
|
||||
})
|
||||
)
|
||||
throw serverErrorByStatus(apiResponse.status, text)
|
||||
}
|
||||
signupSuccessCounter.add(1)
|
||||
console.info("api.user.signup success")
|
||||
return {
|
||||
success: true,
|
||||
redirectUrl: signupVerify[input.language],
|
||||
}
|
||||
}),
|
||||
})
|
||||
225
apps/scandic-web/server/routers/user/output.ts
Normal file
225
apps/scandic-web/server/routers/user/output.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { countriesMap } from "@/components/TempDesignSystem/Form/Country/countries"
|
||||
import { getMembership } from "@/utils/user"
|
||||
|
||||
import { imageSchema } from "../hotels/schemas/image"
|
||||
|
||||
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(),
|
||||
})
|
||||
.optional()
|
||||
.nullable(),
|
||||
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(
|
||||
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<typeof getStaysSchema>
|
||||
|
||||
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<typeof getFriendTransactionsSchema>
|
||||
|
||||
export type FriendTransaction = GetFriendTransactionsData["data"][number]
|
||||
|
||||
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),
|
||||
})
|
||||
|
||||
export const getMembershipCardsSchema = z.array(
|
||||
z.object({
|
||||
currentPoints: z.number(),
|
||||
expirationDate: z.string(),
|
||||
membershipNumber: z.string(),
|
||||
memberSince: z.string(),
|
||||
membershipType: z.string(),
|
||||
})
|
||||
)
|
||||
|
||||
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(),
|
||||
})
|
||||
809
apps/scandic-web/server/routers/user/query.ts
Normal file
809
apps/scandic-web/server/routers/user/query.ts
Normal file
@@ -0,0 +1,809 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
|
||||
import * as api from "@/lib/api"
|
||||
import { dt } from "@/lib/dt"
|
||||
import {
|
||||
protectedProcedure,
|
||||
router,
|
||||
safeProtectedProcedure,
|
||||
} from "@/server/trpc"
|
||||
|
||||
import { countries } from "@/components/TempDesignSystem/Form/Country/countries"
|
||||
import { cache } from "@/utils/cache"
|
||||
import * as maskValue from "@/utils/maskValue"
|
||||
import { getMembership, getMembershipCards } from "@/utils/user"
|
||||
|
||||
import {
|
||||
friendTransactionsInput,
|
||||
getSavedPaymentCardsInput,
|
||||
staysInput,
|
||||
} from "./input"
|
||||
import {
|
||||
creditCardsSchema,
|
||||
getFriendTransactionsSchema,
|
||||
getMembershipCardsSchema,
|
||||
getStaysSchema,
|
||||
getUserSchema,
|
||||
} from "./output"
|
||||
import { updateStaysBookingUrl } from "./utils"
|
||||
|
||||
import type { Session } from "next-auth"
|
||||
|
||||
import type {
|
||||
LoginType,
|
||||
TrackingSDKUserData,
|
||||
} from "@/types/components/tracking"
|
||||
import { Transactions } from "@/types/enums/transactions"
|
||||
import type { User } from "@/types/user"
|
||||
import type { MembershipLevel } from "@/constants/membershipLevels"
|
||||
|
||||
// OpenTelemetry metrics: User
|
||||
const meter = metrics.getMeter("trpc.user")
|
||||
const getVerifiedUserCounter = meter.createCounter("trpc.user.get")
|
||||
const getVerifiedUserSuccessCounter = meter.createCounter(
|
||||
"trpc.user.get-success"
|
||||
)
|
||||
const getVerifiedUserFailCounter = meter.createCounter("trpc.user.get-fail")
|
||||
const getProfileCounter = meter.createCounter("trpc.user.profile")
|
||||
const getProfileSuccessCounter = meter.createCounter(
|
||||
"trpc.user.profile-success"
|
||||
)
|
||||
const getProfileFailCounter = meter.createCounter("trpc.user.profile-fail")
|
||||
|
||||
// OpenTelemetry metrics: Stays
|
||||
const getPreviousStaysCounter = meter.createCounter("trpc.user.stays.previous")
|
||||
const getPreviousStaysSuccessCounter = meter.createCounter(
|
||||
"trpc.user.stays.previous-success"
|
||||
)
|
||||
const getPreviousStaysFailCounter = meter.createCounter(
|
||||
"trpc.user.stays.previous-fail"
|
||||
)
|
||||
const getUpcomingStaysCounter = meter.createCounter("trpc.user.stays.upcoming")
|
||||
const getUpcomingStaysSuccessCounter = meter.createCounter(
|
||||
"trpc.user.stays.upcoming-success"
|
||||
)
|
||||
const getUpcomingStaysFailCounter = meter.createCounter(
|
||||
"trpc.user.stays.upcoming-fail"
|
||||
)
|
||||
|
||||
// OpenTelemetry metrics: Transactions
|
||||
const getFriendTransactionsCounter = meter.createCounter(
|
||||
"trpc.user.transactions.friendTransactions"
|
||||
)
|
||||
const getFriendTransactionsSuccessCounter = meter.createCounter(
|
||||
"trpc.user.transactions.friendTransactions-success"
|
||||
)
|
||||
const getFriendTransactionsFailCounter = meter.createCounter(
|
||||
"trpc.user.transactions.friendTransactions-fail"
|
||||
)
|
||||
|
||||
// OpenTelemetry metrics: Credit Cards
|
||||
const getCreditCardsCounter = meter.createCounter("trpc.user.creditCards")
|
||||
const getCreditCardsSuccessCounter = meter.createCounter(
|
||||
"trpc.user.creditCards-success"
|
||||
)
|
||||
const getCreditCardsFailCounter = meter.createCounter(
|
||||
"trpc.user.creditCards-fail"
|
||||
)
|
||||
|
||||
export const getVerifiedUser = cache(
|
||||
async ({ session }: { session: Session }) => {
|
||||
const now = Date.now()
|
||||
if (session.token.expires_at && session.token.expires_at < now) {
|
||||
return { error: true, cause: "token_expired" } as const
|
||||
}
|
||||
getVerifiedUserCounter.add(1)
|
||||
console.info("api.user.profile getVerifiedUser start", JSON.stringify({}))
|
||||
const apiResponse = await api.get(api.endpoints.v1.Profile.profile, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.token.access_token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
getVerifiedUserFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.user.profile getVerifiedUser error",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
if (apiResponse.status === 401) {
|
||||
return { error: true, cause: "unauthorized" } as const
|
||||
} else if (apiResponse.status === 403) {
|
||||
return { error: true, cause: "forbidden" } as const
|
||||
} else if (apiResponse.status === 404) {
|
||||
return { error: true, cause: "notfound" } as const
|
||||
}
|
||||
return {
|
||||
error: true,
|
||||
cause: "unknown",
|
||||
status: apiResponse.status,
|
||||
} as const
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
if (!apiJson.data?.attributes) {
|
||||
getVerifiedUserFailCounter.add(1, {
|
||||
error_type: "data_error",
|
||||
})
|
||||
console.error(
|
||||
"api.user.profile getVerifiedUser data error",
|
||||
JSON.stringify({
|
||||
apiResponse: apiJson,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const verifiedData = getUserSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
getVerifiedUserFailCounter.add(1, {
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(verifiedData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.user.profile validation error",
|
||||
JSON.stringify({
|
||||
errors: verifiedData.error,
|
||||
apiResponse: apiJson,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
getVerifiedUserSuccessCounter.add(1)
|
||||
console.info("api.user.profile getVerifiedUser success", JSON.stringify({}))
|
||||
return verifiedData
|
||||
}
|
||||
)
|
||||
|
||||
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,
|
||||
membership: getMembership(data.memberships),
|
||||
memberships: data.memberships,
|
||||
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
|
||||
}
|
||||
|
||||
const getCreditCards = cache(
|
||||
async ({
|
||||
session,
|
||||
onlyNonExpired,
|
||||
}: {
|
||||
session: Session
|
||||
onlyNonExpired?: boolean
|
||||
}) => {
|
||||
getCreditCardsCounter.add(1)
|
||||
console.info("api.profile.creditCards start", JSON.stringify({}))
|
||||
const apiResponse = await api.get(api.endpoints.v1.Profile.creditCards, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.token.access_token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
getCreditCardsFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.profile.creditCards error ",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = creditCardsSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
getCreditCardsFailCounter.add(1, {
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(verifiedData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.profile.creditCards validation error ",
|
||||
JSON.stringify({ error: verifiedData.error })
|
||||
)
|
||||
return null
|
||||
}
|
||||
getCreditCardsSuccessCounter.add(1)
|
||||
console.info("api.profile.creditCards success", JSON.stringify({}))
|
||||
|
||||
return verifiedData.data.data.filter((card) => {
|
||||
if (onlyNonExpired) {
|
||||
try {
|
||||
const expirationDate = dt(card.expirationDate).startOf("day")
|
||||
const currentDate = dt().startOf("day")
|
||||
return expirationDate > currentDate
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export const userQueryRouter = router({
|
||||
get: protectedProcedure
|
||||
.use(async function (opts) {
|
||||
return opts.next({
|
||||
ctx: {
|
||||
...opts.ctx,
|
||||
isMFA:
|
||||
opts.ctx.session.token.mfa_scope &&
|
||||
opts.ctx.session.token.mfa_expires_at > Date.now(),
|
||||
},
|
||||
})
|
||||
})
|
||||
.query(async function getUser({ ctx }) {
|
||||
const data = await getVerifiedUser({ session: ctx.session })
|
||||
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
|
||||
if ("error" in data) {
|
||||
return data
|
||||
}
|
||||
|
||||
return parsedUser(data.data, ctx.isMFA)
|
||||
}),
|
||||
getSafely: safeProtectedProcedure.query(async function getUser({ ctx }) {
|
||||
if (!ctx.session) {
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await getVerifiedUser({ session: ctx.session })
|
||||
|
||||
if (!data || "error" in data) {
|
||||
return null
|
||||
}
|
||||
|
||||
return parsedUser(data.data, true)
|
||||
}),
|
||||
name: safeProtectedProcedure.query(async function ({ ctx }) {
|
||||
if (!ctx.session) {
|
||||
return null
|
||||
}
|
||||
const verifiedData = await getVerifiedUser({ session: ctx.session })
|
||||
|
||||
if (!verifiedData || "error" in verifiedData) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
firstName: verifiedData.data.firstName,
|
||||
lastName: verifiedData.data.lastName,
|
||||
}
|
||||
}),
|
||||
membershipLevel: protectedProcedure.query(async function ({ ctx }) {
|
||||
const verifiedData = await getVerifiedUser({ session: ctx.session })
|
||||
if (!verifiedData || "error" in verifiedData) {
|
||||
return null
|
||||
}
|
||||
|
||||
const membershipLevel = getMembership(verifiedData.data.memberships)
|
||||
return membershipLevel
|
||||
}),
|
||||
safeMembershipLevel: safeProtectedProcedure.query(async function ({ ctx }) {
|
||||
if (!ctx.session) {
|
||||
return null
|
||||
}
|
||||
const verifiedData = await getVerifiedUser({ session: ctx.session })
|
||||
|
||||
if (!verifiedData || "error" in verifiedData) {
|
||||
return null
|
||||
}
|
||||
|
||||
const membershipLevel = getMembership(verifiedData.data.memberships)
|
||||
return membershipLevel
|
||||
}),
|
||||
tracking: safeProtectedProcedure.query(async function ({ ctx }) {
|
||||
const notLoggedInUserTrackingData: TrackingSDKUserData = {
|
||||
loginStatus: "Non-logged in",
|
||||
}
|
||||
|
||||
if (!ctx.session) {
|
||||
return notLoggedInUserTrackingData
|
||||
}
|
||||
const verifiedUserData = await getVerifiedUser({ session: ctx.session })
|
||||
|
||||
if (!verifiedUserData || "error" in verifiedUserData) {
|
||||
return notLoggedInUserTrackingData
|
||||
}
|
||||
|
||||
const params = new URLSearchParams()
|
||||
params.set("limit", "1")
|
||||
getPreviousStaysCounter.add(1, { query: JSON.stringify({ params }) })
|
||||
console.info(
|
||||
"api.booking.stays.past start",
|
||||
JSON.stringify({ query: { params } })
|
||||
)
|
||||
const previousStaysResponse = await api.get(
|
||||
api.endpoints.v1.Booking.Stays.past,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!previousStaysResponse.ok) {
|
||||
getPreviousStaysFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: previousStaysResponse.status,
|
||||
statusText: previousStaysResponse.statusText,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.booking.stays.past error",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: previousStaysResponse.status,
|
||||
statusText: previousStaysResponse.statusText,
|
||||
},
|
||||
})
|
||||
)
|
||||
return notLoggedInUserTrackingData
|
||||
}
|
||||
|
||||
const previousStaysApiJson = await previousStaysResponse.json()
|
||||
const verifiedPreviousStaysData =
|
||||
getStaysSchema.safeParse(previousStaysApiJson)
|
||||
if (!verifiedPreviousStaysData.success) {
|
||||
getPreviousStaysFailCounter.add(1, {
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(verifiedPreviousStaysData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.booking.stays.past validation error, ",
|
||||
JSON.stringify({ error: verifiedPreviousStaysData.error })
|
||||
)
|
||||
return notLoggedInUserTrackingData
|
||||
}
|
||||
getPreviousStaysSuccessCounter.add(1)
|
||||
console.info("api.booking.stays.past success", JSON.stringify({}))
|
||||
|
||||
const membership = getMembership(verifiedUserData.data.memberships)
|
||||
|
||||
const loggedInUserTrackingData: TrackingSDKUserData = {
|
||||
loginStatus: "logged in",
|
||||
loginType: ctx.session.token.loginType as LoginType,
|
||||
memberId: verifiedUserData.data.profileId,
|
||||
membershipNumber: membership?.membershipNumber,
|
||||
memberLevel: membership?.membershipLevel as MembershipLevel,
|
||||
noOfNightsStayed: verifiedPreviousStaysData.data.links?.totalCount ?? 0,
|
||||
totalPointsAvailableToSpend: membership?.currentPoints,
|
||||
loginAction: "login success",
|
||||
}
|
||||
return loggedInUserTrackingData
|
||||
}),
|
||||
|
||||
stays: router({
|
||||
previous: protectedProcedure
|
||||
.input(staysInput)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { limit, cursor } = input
|
||||
const params: Record<string, string> = { limit }
|
||||
if (cursor) {
|
||||
params.offset = cursor
|
||||
}
|
||||
getPreviousStaysCounter.add(1, { query: JSON.stringify({ params }) })
|
||||
console.info(
|
||||
"api.booking.stays.past start",
|
||||
JSON.stringify({ query: { params } })
|
||||
)
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Booking.Stays.past,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
getPreviousStaysFailCounter.add(1, {
|
||||
query: JSON.stringify({ params }),
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.booking.stays.past error ",
|
||||
JSON.stringify({
|
||||
query: { params },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
|
||||
const verifiedData = getStaysSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
getPreviousStaysFailCounter.add(1, {
|
||||
query: JSON.stringify({ params }),
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(verifiedData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.booking.stays.past validation error ",
|
||||
JSON.stringify({
|
||||
query: { params },
|
||||
error: verifiedData.error,
|
||||
})
|
||||
)
|
||||
|
||||
return null
|
||||
}
|
||||
getPreviousStaysSuccessCounter.add(1, {
|
||||
query: JSON.stringify({ params }),
|
||||
})
|
||||
console.info(
|
||||
"api.booking.stays.past success",
|
||||
JSON.stringify({ query: { params } })
|
||||
)
|
||||
const nextCursor =
|
||||
verifiedData.data.links &&
|
||||
verifiedData.data.links.offset < verifiedData.data.links.totalCount
|
||||
? verifiedData.data.links.offset
|
||||
: undefined
|
||||
|
||||
const updatedData = await updateStaysBookingUrl(
|
||||
verifiedData.data.data,
|
||||
ctx.session.token.access_token,
|
||||
ctx.lang
|
||||
)
|
||||
|
||||
return {
|
||||
data: updatedData,
|
||||
nextCursor,
|
||||
}
|
||||
}),
|
||||
|
||||
upcoming: protectedProcedure
|
||||
.input(staysInput)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { limit, cursor } = input
|
||||
|
||||
const params: Record<string, string> = { limit }
|
||||
if (cursor) {
|
||||
params.offset = cursor
|
||||
}
|
||||
getUpcomingStaysCounter.add(1, {
|
||||
query: JSON.stringify({ params }),
|
||||
})
|
||||
console.info(
|
||||
"api.booking.stays.future start",
|
||||
JSON.stringify({ query: { params } })
|
||||
)
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Booking.Stays.future,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
getUpcomingStaysFailCounter.add(1, {
|
||||
query: JSON.stringify({ params }),
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.booking.stays.future error ",
|
||||
JSON.stringify({
|
||||
query: { params },
|
||||
error_type: "http_error",
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = getStaysSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
getUpcomingStaysFailCounter.add(1, {
|
||||
query: JSON.stringify({ params }),
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(verifiedData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.booking.stays.future validation error ",
|
||||
JSON.stringify({
|
||||
query: { params },
|
||||
error: verifiedData.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
getUpcomingStaysSuccessCounter.add(1, {
|
||||
query: JSON.stringify({ params }),
|
||||
})
|
||||
console.info("api.booking.stays.future success", {
|
||||
query: JSON.stringify({ params }),
|
||||
})
|
||||
const nextCursor =
|
||||
verifiedData.data.links &&
|
||||
verifiedData.data.links.offset < verifiedData.data.links.totalCount
|
||||
? verifiedData.data.links.offset
|
||||
: undefined
|
||||
|
||||
const updatedData = await updateStaysBookingUrl(
|
||||
verifiedData.data.data,
|
||||
ctx.session.token.access_token,
|
||||
ctx.lang
|
||||
)
|
||||
|
||||
return {
|
||||
data: updatedData,
|
||||
nextCursor,
|
||||
}
|
||||
}),
|
||||
}),
|
||||
transaction: router({
|
||||
friendTransactions: protectedProcedure
|
||||
.input(friendTransactionsInput)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { limit, page } = input
|
||||
getFriendTransactionsCounter.add(1)
|
||||
console.info(
|
||||
"api.transaction.friendTransactions start",
|
||||
JSON.stringify({})
|
||||
)
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Profile.Transaction.friendTransactions,
|
||||
{
|
||||
cache: undefined, // override defaultOptions
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
next: { revalidate: 30 * 60 * 1000 },
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
// switch (apiResponse.status) {
|
||||
// case 400:
|
||||
// throw badRequestError()
|
||||
// case 401:
|
||||
// throw unauthorizedError()
|
||||
// case 403:
|
||||
// throw forbiddenError()
|
||||
// default:
|
||||
// throw internalServerError()
|
||||
// }
|
||||
const text = await apiResponse.text()
|
||||
getFriendTransactionsFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.transaction.friendTransactions error ",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = getFriendTransactionsSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
getFriendTransactionsFailCounter.add(1, {
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(verifiedData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.transaction.friendTransactions validation error ",
|
||||
JSON.stringify({ error: verifiedData.error })
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
getFriendTransactionsSuccessCounter.add(1)
|
||||
console.info(
|
||||
"api.transaction.friendTransactions success",
|
||||
JSON.stringify({})
|
||||
)
|
||||
const updatedData = await updateStaysBookingUrl(
|
||||
verifiedData.data.data,
|
||||
ctx.session.token.access_token,
|
||||
ctx.lang
|
||||
)
|
||||
|
||||
const pageData = updatedData
|
||||
.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
|
||||
if (b.attributes.confirmationNumber === "BALFWD") return -1
|
||||
|
||||
const dateA = new Date(
|
||||
a.attributes.checkinDate
|
||||
? a.attributes.checkinDate
|
||||
: a.attributes.transactionDate
|
||||
)
|
||||
const dateB = new Date(
|
||||
b.attributes.checkinDate
|
||||
? b.attributes.checkinDate
|
||||
: b.attributes.transactionDate
|
||||
)
|
||||
|
||||
return dateA > dateB ? -1 : 1
|
||||
})
|
||||
|
||||
const slicedData = pageData.slice(limit * (page - 1), limit * page)
|
||||
|
||||
return {
|
||||
data: {
|
||||
transactions: slicedData.map(({ type, attributes }) => {
|
||||
return {
|
||||
type,
|
||||
awardPoints: attributes.awardPoints,
|
||||
checkinDate: attributes.checkinDate,
|
||||
checkoutDate: attributes.checkoutDate,
|
||||
city: attributes.hotelInformation?.city,
|
||||
confirmationNumber: attributes.confirmationNumber,
|
||||
hotelName: attributes.hotelInformation?.name,
|
||||
nights: attributes.nights,
|
||||
pointsCalculated: attributes.pointsCalculated,
|
||||
hotelId: attributes.hotelOperaId,
|
||||
transactionDate: attributes.transactionDate,
|
||||
bookingUrl: attributes.bookingUrl,
|
||||
}
|
||||
}),
|
||||
},
|
||||
meta: {
|
||||
totalPages: Math.ceil(pageData.length / limit),
|
||||
},
|
||||
}
|
||||
}),
|
||||
}),
|
||||
|
||||
creditCards: protectedProcedure.query(async function ({ ctx }) {
|
||||
return await getCreditCards({ session: ctx.session })
|
||||
}),
|
||||
safePaymentCards: safeProtectedProcedure
|
||||
.input(getSavedPaymentCardsInput)
|
||||
.query(async function ({ ctx, input }) {
|
||||
if (!ctx.session) {
|
||||
return null
|
||||
}
|
||||
|
||||
const savedCards = await getCreditCards({
|
||||
session: ctx.session,
|
||||
onlyNonExpired: true,
|
||||
})
|
||||
|
||||
if (!savedCards) {
|
||||
return null
|
||||
}
|
||||
|
||||
return savedCards.filter((card) =>
|
||||
input.supportedCards.includes(card.type)
|
||||
)
|
||||
}),
|
||||
|
||||
membershipCards: protectedProcedure.query(async function ({ ctx }) {
|
||||
const userData = await getVerifiedUser({ session: ctx.session })
|
||||
|
||||
if (!userData || "error" in userData) {
|
||||
return null
|
||||
}
|
||||
|
||||
const verifiedData = getMembershipCardsSchema.safeParse(
|
||||
userData.data.memberships
|
||||
)
|
||||
|
||||
if (!verifiedData.success) {
|
||||
getProfileFailCounter.add(1, {
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(verifiedData),
|
||||
})
|
||||
console.error(
|
||||
"api.profile validation error",
|
||||
JSON.stringify({ error: verifiedData })
|
||||
)
|
||||
return null
|
||||
}
|
||||
getProfileSuccessCounter.add(1)
|
||||
|
||||
return getMembershipCards(verifiedData.data)
|
||||
}),
|
||||
})
|
||||
116
apps/scandic-web/server/routers/user/tempFriendTransactions.json
Normal file
116
apps/scandic-web/server/routers/user/tempFriendTransactions.json
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"attributes": {
|
||||
"hotelOperaId": "216",
|
||||
"confirmationNumber": "991646189",
|
||||
"checkInDate": "2023-09-16",
|
||||
"checkOutDate": "2023-09-17",
|
||||
"transactionDate": "2023-04-18",
|
||||
"nights": 1,
|
||||
"awardPoints": 1863,
|
||||
"pointsCalculated": true,
|
||||
"hotelInformation": {
|
||||
"hotelName": "Scandic Landvetter",
|
||||
"city": "Stockholm",
|
||||
"hotelContent": {
|
||||
"images": {
|
||||
"metaData": {
|
||||
"title": "Lobby",
|
||||
"altText": "lobby at scandic landvetter in gothenburg",
|
||||
"altText_En": "lobby at scandic landvetter in gothenburg",
|
||||
"copyRight": "Werner Nystrand"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/1cz71gn106ej1mz7u4nr/Scandic-Landvetter-lobby-0013-2-vald.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/29ejr75mwp7riv63nz0x/Scandic-Landvetter-lobby-0013-2-vald.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/bldh2liyfddkv74szp9v/Scandic-Landvetter-lobby-0013-2-vald.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/kbmpmkb714o028ufcgu4/Scandic-Landvetter-lobby-0013-2-vald.jpg"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"relationships": {
|
||||
"hotel": {
|
||||
"links": {
|
||||
"related": "https://api-test.scandichotels.com/hotels/V0/fdea883a-8092-4604-8afb-032391a59009/hotels"
|
||||
},
|
||||
"data": {
|
||||
"id": "d98c7ab1-ebaa-4102-b351-758daf1ddf55",
|
||||
"type": "hotels"
|
||||
}
|
||||
},
|
||||
"booking": {
|
||||
"links": {
|
||||
"related": "https://api-test.scandichotels.com/booking/v1/bookings/991646189"
|
||||
},
|
||||
"data": {
|
||||
"id": "991646189",
|
||||
"type": "booking"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "stay"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"hotelOperaId": "216",
|
||||
"confirmationNumber": "991646190",
|
||||
"checkInDate": "2023-09-16",
|
||||
"checkOutDate": "2023-09-17",
|
||||
"transactionDate": "2023-04-18",
|
||||
"nights": 1,
|
||||
"awardPoints": 1863,
|
||||
"pointsCalculated": true,
|
||||
"hotelInformation": {
|
||||
"hotelName": "Scandic Landvetter",
|
||||
"city": "Stockholm",
|
||||
"hotelContent": {
|
||||
"images": {
|
||||
"metaData": {
|
||||
"title": "Lobby",
|
||||
"altText": "lobby at scandic landvetter in gothenburg",
|
||||
"altText_En": "lobby at scandic landvetter in gothenburg",
|
||||
"copyRight": "Werner Nystrand"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/1cz71gn106ej1mz7u4nr/Scandic-Landvetter-lobby-0013-2-vald.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/29ejr75mwp7riv63nz0x/Scandic-Landvetter-lobby-0013-2-vald.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/bldh2liyfddkv74szp9v/Scandic-Landvetter-lobby-0013-2-vald.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/kbmpmkb714o028ufcgu4/Scandic-Landvetter-lobby-0013-2-vald.jpg"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"relationships": {
|
||||
"hotel": {
|
||||
"links": {
|
||||
"related": "https://api-test.scandichotels.com/hotels/V0/fdea883a-8092-4604-8afb-032391a59009/hotels"
|
||||
},
|
||||
"data": {
|
||||
"id": "d98c7ab1-ebaa-4102-b351-758daf1ddf55",
|
||||
"type": "hotels"
|
||||
}
|
||||
},
|
||||
"booking": {
|
||||
"links": {
|
||||
"related": "https://api-test.scandichotels.com/booking/v1/bookings/991646189"
|
||||
},
|
||||
"data": {
|
||||
"id": "991646189",
|
||||
"type": "booking"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "stay"
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"self": "https://api-test.scandichotels.com/profile/v1/transaction/friendTransactions?language=en&offset=1&limit=20",
|
||||
"offset": 2,
|
||||
"limit": 20,
|
||||
"totalCount": 40
|
||||
}
|
||||
}
|
||||
113
apps/scandic-web/server/routers/user/utils.ts
Normal file
113
apps/scandic-web/server/routers/user/utils.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
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.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 || "https://www.scandichotels.com" // fallback to production for ephemeral envs (like deploy previews)
|
||||
)
|
||||
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