diff --git a/apps/api/src/background-checks/background-check-billing.service.ts b/apps/api/src/background-checks/background-check-billing.service.ts index cd1de71199..3d2c5f9ca6 100644 --- a/apps/api/src/background-checks/background-check-billing.service.ts +++ b/apps/api/src/background-checks/background-check-billing.service.ts @@ -60,7 +60,9 @@ export class BackgroundCheckBillingService { }); if (!session.url) { - throw new BadRequestException('Failed to create Stripe Checkout session.'); + throw new BadRequestException( + 'Failed to create Stripe Checkout session.', + ); } return { url: session.url }; @@ -82,8 +84,13 @@ export class BackgroundCheckBillingService { throw new BadRequestException('Checkout session is not complete.'); } - if (session.metadata?.organizationId && session.metadata.organizationId !== organizationId) { - throw new BadRequestException('Checkout session does not belong to this organization.'); + if ( + session.metadata?.organizationId && + session.metadata.organizationId !== organizationId + ) { + throw new BadRequestException( + 'Checkout session does not belong to this organization.', + ); } const stripeCustomerId = this.extractStripeId(session.customer); @@ -98,12 +105,16 @@ export class BackgroundCheckBillingService { const setupIntent = session.setup_intent; if (!setupIntent || typeof setupIntent === 'string') { - throw new BadRequestException('Checkout session is missing a setup intent.'); + throw new BadRequestException( + 'Checkout session is missing a setup intent.', + ); } const paymentMethodId = this.extractStripeId(setupIntent.payment_method); if (!paymentMethodId) { - throw new BadRequestException('Setup intent is missing a payment method.'); + throw new BadRequestException( + 'Setup intent is missing a payment method.', + ); } await stripe.customers.update(stripeCustomerId, { @@ -146,7 +157,9 @@ export class BackgroundCheckBillingService { }); if (!billing) { - throw new NotFoundException('No billing record found for this organization.'); + throw new NotFoundException( + 'No billing record found for this organization.', + ); } const portalSession = await stripe.billingPortal.sessions.create({ @@ -192,16 +205,24 @@ export class BackgroundCheckBillingService { return customer.id; } - async getBackgroundCheckPrice(): Promise<{ id: string; unitAmount: number; currency: string }> { + async getBackgroundCheckPrice(): Promise<{ + id: string; + unitAmount: number; + currency: string; + }> { const priceId = process.env.STRIPE_BACKGROUND_CHECK_PRICE_ID; if (!priceId) { - throw new BadRequestException('Background check pricing is not configured. Contact support.'); + throw new BadRequestException( + 'Background check pricing is not configured. Contact support.', + ); } const stripe = this.stripeService.getClient(); const price = await stripe.prices.retrieve(priceId); if (price.unit_amount === null || price.unit_amount === undefined) { - throw new BadRequestException('Background check pricing is not configured. Contact support.'); + throw new BadRequestException( + 'Background check pricing is not configured. Contact support.', + ); } return { @@ -213,7 +234,9 @@ export class BackgroundCheckBillingService { private validateRedirectUrl(url: string): void { const appUrl = - process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL || process.env.BETTER_AUTH_URL; + process.env.NEXT_PUBLIC_APP_URL || + process.env.APP_URL || + process.env.BETTER_AUTH_URL; if (!appUrl) { throw new BadRequestException('App URL is not configured on the server.'); } @@ -226,11 +249,15 @@ export class BackgroundCheckBillingService { } if (parsed.origin !== new URL(appUrl).origin) { - throw new BadRequestException('Redirect URL must belong to the application origin.'); + throw new BadRequestException( + 'Redirect URL must belong to the application origin.', + ); } } - private extractStripeId(value: string | { id?: string } | null): string | null { + private extractStripeId( + value: string | { id?: string } | null, + ): string | null { if (!value) return null; if (typeof value === 'string') return value; return value.id ?? null; @@ -254,8 +281,13 @@ export class BackgroundCheckBillingService { const stripe = this.stripeService.getClient(); const customer = await stripe.customers.retrieve(stripeCustomerId); - if (customer.deleted || customer.metadata?.organizationId !== organizationId) { - throw new BadRequestException('Checkout session does not belong to this organization.'); + if ( + customer.deleted || + customer.metadata?.organizationId !== organizationId + ) { + throw new BadRequestException( + 'Checkout session does not belong to this organization.', + ); } } } diff --git a/apps/api/src/background-checks/background-check-payment.service.ts b/apps/api/src/background-checks/background-check-payment.service.ts index 6e72f277d5..950fb8c3df 100644 --- a/apps/api/src/background-checks/background-check-payment.service.ts +++ b/apps/api/src/background-checks/background-check-payment.service.ts @@ -15,7 +15,12 @@ export class BackgroundCheckPaymentService { async charge(params: { organizationId: string; memberId: string; - }): Promise<{ paymentIntentId: string; status: string; amount: number; currency: string }> { + }): Promise<{ + paymentIntentId: string; + status: string; + amount: number; + currency: string; + }> { const billing = await db.organizationBilling.findUnique({ where: { organizationId: params.organizationId }, select: { diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckStatusView.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckStatusView.tsx index 8bccf52756..4b3681e726 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckStatusView.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckStatusView.tsx @@ -6,9 +6,9 @@ import { toast } from 'sonner'; import useSWR from 'swr'; import { BackgroundCheckReport } from './BackgroundCheckReport'; import { - type CustomBackgroundCheckAttachment, type BackgroundCheckRecord, type BackgroundCheckStatus, + type CustomBackgroundCheckAttachment, isCompletedBackgroundCheck, } from './backgroundCheckTypes'; @@ -48,17 +48,13 @@ export function BackgroundCheckStatusView({ CustomBackgroundCheckAttachment[], Error, readonly [string, string] | null - >( - customAttachmentsKey, - async ([endpoint, orgId]) => { - const response = await apiClient.get( - endpoint, - orgId, - ); - if (response.error) throw new Error(response.error); - return response.data ?? []; - }, - ); + >(customAttachmentsKey, async ([endpoint, orgId]) => { + const response = await apiClient.get(endpoint, orgId); + if (response.error) { + throw new Error('Failed to load custom background check attachments'); + } + return response.data ?? []; + }); const handleCopyCandidateLink = async () => { if (!backgroundCheck.candidateUrl) return; @@ -152,7 +148,7 @@ function CustomReportAttachments({ ); if (response.error || !response.data?.downloadUrl) { - toast.error(response.error ?? 'Failed to open background check'); + toast.error('Failed to open background check'); return; } @@ -177,11 +173,7 @@ function CustomReportAttachments({ Uploaded {new Date(attachment.createdAt).toLocaleString()} - @@ -192,11 +184,7 @@ function CustomReportAttachments({ ); } -function ComponentStatuses({ - backgroundCheck, -}: { - backgroundCheck: BackgroundCheckRecord; -}) { +function ComponentStatuses({ backgroundCheck }: { backgroundCheck: BackgroundCheckRecord }) { const statuses = COMPONENT_LABELS.flatMap(([key, label]) => { const value = backgroundCheck[key]; return value ? [{ label, value }] : []; diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/CustomBackgroundCheckUpload.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/CustomBackgroundCheckUpload.tsx index a120f5fb32..869f6476db 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/CustomBackgroundCheckUpload.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/CustomBackgroundCheckUpload.tsx @@ -57,7 +57,7 @@ export function CustomBackgroundCheckUpload({ ); if (response.error || !response.data) { - toast.error(response.error ?? 'Failed to upload background check'); + toast.error('Failed to upload background check'); return; } @@ -65,8 +65,8 @@ export function CustomBackgroundCheckUpload({ setSelectedFile(null); if (inputRef.current) inputRef.current.value = ''; await onUploaded(response.data); - } catch (error) { - toast.error(error instanceof Error ? error.message : 'Failed to upload background check'); + } catch { + toast.error('Failed to upload background check'); } finally { setIsUploading(false); } diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeBackgroundCheck.test.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeBackgroundCheck.test.tsx index cac86c5f26..eba693cafe 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeBackgroundCheck.test.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeBackgroundCheck.test.tsx @@ -77,6 +77,8 @@ function renderSection(props?: Partial { beforeEach(() => { vi.clearAllMocks(); + vi.mocked(apiClient.get).mockReset(); + vi.mocked(apiClient.post).mockReset(); window.sessionStorage.clear(); navigationMock.pathname = '/org_1/people/mem_1'; navigationMock.searchParams = new URLSearchParams(); @@ -188,9 +190,9 @@ describe('EmployeeBackgroundCheck', () => { }), 'org_1', ); - expect(window.sessionStorage.getItem('background-check:org_1:mem_1:pending-request')).toContain( - 'ada@example.com', - ); + expect( + window.sessionStorage.getItem('background-check:org_1:mem_1:pending-request'), + ).not.toContain('ada@example.com'); }); it('restores the pending check after Stripe setup before completing it', async () => { @@ -204,8 +206,6 @@ describe('EmployeeBackgroundCheck', () => { JSON.stringify({ organizationId: 'org_1', memberId: 'mem_1', - employeeName: 'Ada Lovelace', - employeeEmail: 'ada@example.com', requesterNotes: 'Recruiting requested an expedited check.', }), ); @@ -254,11 +254,15 @@ describe('EmployeeBackgroundCheck', () => { 'org_1', ); expect(await screen.findByText('Payment method saved')).toBeInTheDocument(); - expect(screen.getByDisplayValue('ada@example.com')).toBeInTheDocument(); + expect(screen.getByLabelText('Personal email')).toHaveValue(''); + expect( + screen.getByDisplayValue('Recruiting requested an expedited check.'), + ).toBeInTheDocument(); expect(window.sessionStorage.getItem('background-check:org_1:mem_1:pending-request')).toContain( - 'ada@example.com', + 'Recruiting requested an expedited check.', ); + await user.type(screen.getByLabelText('Personal email'), 'ada@example.com'); await user.click(screen.getByRole('button', { name: /complete/i })); await waitFor(() => { @@ -280,7 +284,7 @@ describe('EmployeeBackgroundCheck', () => { const user = userEvent.setup(); vi.mocked(apiClient.post) .mockResolvedValueOnce({ - error: 'Background check payment failed. Update billing and try again.', + error: 'Invalid API Key provided: PLACEHOLDER', status: 402, }) .mockResolvedValueOnce({ data: {}, status: 200 }); @@ -292,6 +296,10 @@ describe('EmployeeBackgroundCheck', () => { expect( await screen.findByRole('heading', { name: /update payment method/i }), ).toBeInTheDocument(); + expect(screen.queryByText(/PLACEHOLDER/)).not.toBeInTheDocument(); + expect( + screen.getByText('Payment failed. Update payment method and try again.'), + ).toBeInTheDocument(); await user.click(screen.getByRole('button', { name: /update payment method/i })); diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeBackgroundCheck.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeBackgroundCheck.tsx index db1160434e..9d0699554e 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeBackgroundCheck.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeBackgroundCheck.tsx @@ -10,18 +10,18 @@ import { useForm } from 'react-hook-form'; import { toast } from 'sonner'; import useSWR from 'swr'; import { BackgroundCheckDetailsForm } from './BackgroundCheckDetailsForm'; -import { BackgroundCheckStatusView } from './BackgroundCheckStatusView'; -import { OverviewStep } from './BackgroundCheckWizardParts'; -import { CustomBackgroundCheckUpload } from './CustomBackgroundCheckUpload'; -import { PaymentMethodUpdateDialog } from './PaymentMethodUpdateDialog'; import { + type BackgroundCheckFormValues, backgroundCheckSchema, clearPendingBackgroundCheckRequest, readPendingBackgroundCheckRequest, writePendingBackgroundCheckRequest, - type BackgroundCheckFormValues, } from './backgroundCheckForm'; +import { BackgroundCheckStatusView } from './BackgroundCheckStatusView'; import type { BackgroundCheckBillingStatus, BackgroundCheckRecord } from './backgroundCheckTypes'; +import { OverviewStep } from './BackgroundCheckWizardParts'; +import { CustomBackgroundCheckUpload } from './CustomBackgroundCheckUpload'; +import { PaymentMethodUpdateDialog } from './PaymentMethodUpdateDialog'; interface EmployeeBackgroundCheckProps { employee: Member & { user: User }; @@ -59,7 +59,7 @@ export function EmployeeBackgroundCheck({ endpoint, organizationId, ); - if (response.error) throw new Error(response.error); + if (response.error) throw new Error('Failed to load background check'); return response.data ?? null; }, { fallbackData: initialBackgroundCheck }, @@ -70,7 +70,7 @@ export function EmployeeBackgroundCheck({ async ([endpoint]) => { const response = await apiClient.get(endpoint, organizationId); if (response.error || !response.data) { - throw new Error(response.error ?? 'Failed to load billing status'); + throw new Error('Failed to load billing status'); } return response.data; }, @@ -134,12 +134,10 @@ export function EmployeeBackgroundCheck({ if (response.error || !response.data) { if (response.status === 402) { - setPaymentIssue( - response.error ?? 'Payment failed. Update billing details and try again.', - ); + setPaymentIssue('Payment failed. Update payment method and try again.'); return false; } - toast.error(response.error ?? 'Failed to request background check'); + toast.error('Failed to request background check'); return false; } @@ -168,7 +166,7 @@ export function EmployeeBackgroundCheck({ organizationId, ); if (setupResponse.error) { - toast.error(setupResponse.error); + toast.error('Failed to save payment method'); router.replace(pathname, { scroll: false }); return; } @@ -200,9 +198,9 @@ export function EmployeeBackgroundCheck({ router.replace(pathname, { scroll: false }); })(); }, [ - clearPendingRequest, form, employee.id, + employee.user.name, mutateBillingStatus, organizationId, pathname, @@ -232,7 +230,7 @@ export function EmployeeBackgroundCheck({ return; } - toast.error(response.error ?? 'Failed to open billing'); + toast.error('Failed to open billing'); setIsOpeningBilling(false); }; @@ -289,7 +287,9 @@ export function EmployeeBackgroundCheck({ employeeName={employee.user.name ?? employee.user.email} organizationId={organizationId} onUploaded={async (uploadedBackgroundCheck) => { - await mutateBackgroundCheck(uploadedBackgroundCheck, { revalidate: false }); + await mutateBackgroundCheck(uploadedBackgroundCheck, { + revalidate: false, + }); }} />