diff --git a/apps/scandic-web/app/api/web/auth/dtmc/route.ts b/apps/scandic-web/app/api/web/auth/dtmc/route.ts index 4be7198b1..221eca05b 100644 --- a/apps/scandic-web/app/api/web/auth/dtmc/route.ts +++ b/apps/scandic-web/app/api/web/auth/dtmc/route.ts @@ -1,6 +1,7 @@ import { type NextRequest, NextResponse } from "next/server" import { overview } from "@scandic-hotels/common/constants/routes/myPages" +import * as api from "@scandic-hotels/trpc/api" import { isValidSession } from "@scandic-hotels/trpc/utils/session" import { DTMC_SUCCESS_BANNER_KEY } from "@/constants/dtmc" @@ -12,15 +13,94 @@ import { auth } from "@/auth" import { auth as dtmcAuth } from "@/auth.dtmc" import { getLang } from "@/i18n/serverContext" -async function linkEmployeeToUser(employeeId: string) { +interface LinkEmployeeSuccessResult { + success: true +} + +interface LinkEmployeeErrorResult { + success: false + statusCode: number + queryParam?: string | null +} + +type LinkEmployeeResult = LinkEmployeeSuccessResult | LinkEmployeeErrorResult + +/** + * Links an employee to a user account via API call. + */ +async function linkEmployeeToUser( + employeeId: string, + accessToken: string +): Promise { + console.log(`[dtmc] Linking employee ID ${employeeId}`) + let response: Response try { - console.log(`[dtmc] Linking employee ID ${employeeId}`) - // TODO: Use the actual API once available. For now, return a mock success response. - return { success: true } - } catch (error) { - console.error("[dtmc] Error linking employee to user:", error) - throw error + response = await api.post( + api.endpoints.v2.Profile.teamMemberCard(employeeId), + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + } + ) + } catch (networkError) { + console.error("[dtmc] Network error during API request:", networkError) + return { + success: false, + statusCode: 0, + } } + + if (!response.ok) { + console.error(`[dtmc] API returned error status ${response.status}`) + try { + const errorResponse = await response.json() + console.error(`[dtmc] API error response:`, errorResponse) + } catch (parseError) { + console.warn(`[dtmc] Could not parse API error response:`, parseError) + try { + const errorText = await response.text() + console.error(`[dtmc] Raw error response:`, errorText) + } catch { + console.error(`[dtmc] Could not read error response body`) + } + } + + let queryParam: string | null = null + switch (response.status) { + case 400: + case 404: + queryParam = "unable_to_verify_employee_id" + break + case 401: + queryParam = "unauthorized" + break + case 403: + queryParam = "forbidden" + break + } + return { + success: false, + statusCode: response.status, + queryParam, + } + } + + console.log(`[dtmc] API call successful - Status: ${response.status}`) + console.log( + `[dtmc] Response headers:`, + Object.fromEntries(response.headers.entries()) + ) + try { + const responseBody = await response.json() + console.log(`[dtmc] Response body:`, responseBody) + } catch (parseError) { + console.warn(`[dtmc] Could not parse success response body:`, parseError) + } + + console.log(`[dtmc] Successfully linked employee ID ${employeeId}`) + return { success: true } } /** @@ -72,7 +152,16 @@ export async function GET(request: NextRequest) { "[dtmc] DTMC Callback handler - Calling linkEmployeeToUser with ID:", employeeId ) - const result = await linkEmployeeToUser(employeeId) + + const accessToken = session.token.access_token + if (!accessToken) { + console.error("[dtmc] DTMC Callback handler - No access token in session") + const errorUrl = new URL(linkEmploymentError[lang], baseUrl) + errorUrl.searchParams.set("error", "missing_access_token") + return NextResponse.redirect(errorUrl) + } + + const result = await linkEmployeeToUser(employeeId, accessToken) console.log( "[dtmc] DTMC Callback handler - linkEmployeeToUser result:", result @@ -80,10 +169,16 @@ export async function GET(request: NextRequest) { if (!result.success) { console.error( - "[dtmc] DTMC Callback handler - Failed to verify employment" + "[dtmc] DTMC Callback handler - Failed to verify employment:", + `Status: ${result.statusCode}, Error: ${result.queryParam}` ) + const errorUrl = new URL(linkEmploymentError[lang], baseUrl) - errorUrl.searchParams.set("error", "unable_to_verify_employee_id") + + if (result.queryParam) { + errorUrl.searchParams.set("error", result.queryParam) + } + // For 500 errors and network errors, no query param = default error message. return NextResponse.redirect(errorUrl) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts b/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts index 132e4ddb1..a87553aa6 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts @@ -165,6 +165,7 @@ const authenticatedUser: SafeUser = { name: "", phoneNumber: undefined, profileId: "", + employmentDetails: undefined, } const badAuthenticatedUser: SafeUser = { @@ -196,6 +197,7 @@ const badAuthenticatedUser: SafeUser = { name: "", phoneNumber: undefined, profileId: "", + employmentDetails: undefined, } const loggedOutGuest: Guest = { diff --git a/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/Content.tsx b/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/Content.tsx index b463317b3..21f764560 100644 --- a/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/Content.tsx +++ b/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/Content.tsx @@ -6,6 +6,7 @@ import { useIntl } from "react-intl" import { Typography } from "@scandic-hotels/design-system/Typography" import { debounce } from "@/utils/debounce" +import { getEmployeeInfo } from "@/utils/user" import styles from "./digitalTeamMemberCard.module.css" @@ -20,6 +21,9 @@ export default function DigitalTeamMemberCardContent({ }: DigitalTeamMemberCardCardProps) { const intl = useIntl() const cardRef = useRef(null) + + const employeeInfo = getEmployeeInfo(user) + const notAvailableText = "N/A" const [isHovering, setIsHovering] = useState(false) const [coords, setCoords] = useState({ x: 0, y: 0 }) const shimmerRef = useRef(null) @@ -117,17 +121,13 @@ export default function DigitalTeamMemberCardContent({ {intl.formatMessage({ defaultMessage: "Team Member" })} - {/* TODO: Should display country of employment */} - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - SWE + {employeeInfo?.country || notAvailableText}
- {/* TODO: Should display employee number */} - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} -
123 456
+
{employeeInfo?.employeeId || notAvailableText}
+ {employeeInfo?.location || notAvailableText} - {/* TODO: Should display department of employment */} - {/* eslint-disable formatjs/no-literal-string-in-jsx */} - Haymarket by Scandic - {/* eslint-enable */} + {employeeInfo?.retired + ? intl.formatMessage({ defaultMessage: "Retired" }) + : intl.formatMessage({ defaultMessage: "Employee" })} - {/* TODO: Should display current state of employment */} - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - Employee
diff --git a/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/index.tsx b/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/index.tsx index dd3d403f3..167d45aca 100644 --- a/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/index.tsx +++ b/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/index.tsx @@ -1,5 +1,7 @@ import { env } from "@/env/server" +import { isEmployeeLinked } from "@/utils/user" + import DigitalTeamMemberCardClient from "./Client" import type { User } from "@scandic-hotels/trpc/types/user" @@ -15,7 +17,9 @@ export default async function DigitalTeamMemberCard({ return null } - // TODO: Make a check whether user is eligible for benefits or not - + const hasEmploymentData = isEmployeeLinked(user) + if (!hasEmploymentData) { + return null + } return } diff --git a/apps/scandic-web/utils/user.ts b/apps/scandic-web/utils/user.ts index aaa756214..66946374c 100644 --- a/apps/scandic-web/utils/user.ts +++ b/apps/scandic-web/utils/user.ts @@ -32,3 +32,19 @@ export function getSteppedUpLevel( } return values[currentIndex + stepsUp] } + +export function isEmployeeLinked(user: User): boolean { + return !!user.employmentDetails?.employeeId +} + +export function getEmployeeInfo(user: User) { + const employment = user.employmentDetails + if (!employment) return null + + return { + employeeId: employment.employeeId, + location: employment.location, + country: employment.country, + retired: employment.retired, + } +} diff --git a/packages/trpc/lib/api/endpoints.ts b/packages/trpc/lib/api/endpoints.ts index 92224fcb1..31e0b82b8 100644 --- a/packages/trpc/lib/api/endpoints.ts +++ b/packages/trpc/lib/api/endpoints.ts @@ -221,6 +221,10 @@ export namespace endpoints { */ export namespace Profile { export const profile = `${base.path.profile}/${version}/${base.enitity.Profile}` + + export function teamMemberCard(employeeId: string) { + return `${profile}/${employeeId}/TeamMemberCard` + } } } } diff --git a/packages/trpc/lib/routers/user/output.ts b/packages/trpc/lib/routers/user/output.ts index d8920bdc0..066f42e06 100644 --- a/packages/trpc/lib/routers/user/output.ts +++ b/packages/trpc/lib/routers/user/output.ts @@ -62,6 +62,15 @@ const pointExpirationSchema = z.object({ expires: z.string(), }) +export const employmentDetailsSchema = z + .object({ + employeeId: z.string(), + location: z.string(), + country: z.string(), + retired: z.boolean(), + }) + .optional() + export const userLoyaltySchema = z.object({ memberships: z.array(membershipSchema), points: z.object({ @@ -102,6 +111,7 @@ export const getUserSchema = z .optional() .nullable(), loyalty: userLoyaltySchema.optional(), + employmentDetails: employmentDetailsSchema, }), type: z.string(), }), diff --git a/packages/trpc/lib/routers/user/utils.ts b/packages/trpc/lib/routers/user/utils.ts index 4afd34842..50882fdac 100644 --- a/packages/trpc/lib/routers/user/utils.ts +++ b/packages/trpc/lib/routers/user/utils.ts @@ -223,12 +223,13 @@ export function parsedUser(data: User, isMFA: boolean) { }, dateOfBirth: data.dateOfBirth, email: data.email, + employmentDetails: data.employmentDetails, firstName: data.firstName, language: data.language, lastName: data.lastName, + loyalty: data.loyalty, membershipNumber: data.membershipNumber, membership: data.loyalty ? getFriendsMembership(data.loyalty) : null, - loyalty: data.loyalty, name: `${data.firstName} ${data.lastName}`, phoneNumber: data.phoneNumber, profileId: data.profileId,