Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ workflows:
branches:
only:
- dev
- hide_ba_details

- deployQa:
context: org-global
Expand Down
2 changes: 1 addition & 1 deletion src/apps/admin/src/AdminHomeRedirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { reportsRootRoute } from '~/apps/reports'
import { ProfileContextData, useProfileContext } from '~/libs/core'

import { manageChallengeRouteId } from './config/routes.config'
import { isAdministrator } from './lib/utils'
import { isAdministrator } from './lib/utils/access'

/**
* Redirects authenticated admin-app users to the first route they can access.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import _ from 'lodash'

import { TabsNavItem } from '~/libs/ui'
import { isAdministrator } from '~/apps/admin/src/lib/utils'
import { isAdministrator } from '~/apps/admin/src/lib/utils/access'
import {
billingAccountRouteId,
defaultReviewersRouteId,
Expand Down
6 changes: 6 additions & 0 deletions src/apps/admin/src/lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@ export * from './number'
export * from './string'
export * from './others'
export * from './access'
export {
administratorOnlyRoles,
adminReportsAccessRoles,
canAccessAdminReports,
isAdministrator,
} from './access'
26 changes: 24 additions & 2 deletions src/apps/copilots/src/services/copilot-opportunities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export const copilotBaseUrl = `${EnvironmentConfig.API.V6}/projects`

const PAGE_SIZE = 20

type CopilotApplicationApiResponse = Omit<CopilotApplication, 'id' | 'userId'> & {
id: number | string
userId: number | string
}

/**
* Creates a CopilotOpportunity object by merging the provided data and its nested data,
* setting specific properties, and formatting the createdAt date.
Expand All @@ -27,6 +32,23 @@ function copilotOpportunityFactory(data: any): CopilotOpportunity {
}
}

/**
* Normalizes copilot application identifiers that the API may serialize as strings.
*
* The copilot opportunity UI joins applications to member records and the signed-in profile
* using numeric ids, so we coerce the API payload into the shape expected by the UI.
*
* @param data - Raw copilot application returned by the API.
* @returns A copilot application with numeric identifiers.
*/
function copilotApplicationFactory(data: CopilotApplicationApiResponse): CopilotApplication {
return {
...data,
id: Number(data.id),
userId: Number(data.userId),
}
}

export interface CopilotOpportunitiesResponse {
isValidating: boolean;
data: CopilotOpportunity[];
Expand Down Expand Up @@ -144,8 +166,8 @@ export const useCopilotApplications = (opportunityId?: string): CopilotApplicati
? buildUrl(`${copilotBaseUrl}/copilots/opportunity/${opportunityId}/applications`)
: undefined

const fetcher = (urlp: string): Promise<CopilotApplication[]> => xhrGetAsync<CopilotApplication[]>(urlp)
.then(data => data)
const fetcher = (urlp: string): Promise<CopilotApplication[]> => xhrGetAsync<CopilotApplicationApiResponse[]>(urlp)
.then(data => data.map(copilotApplicationFactory))
.catch(() => [])

return useSWR(url, fetcher)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import {
import type { Engagement, EngagementAssignment } from '../../lib/models'
import {
formatCurrencyAmount,
formatDate,
formatLocation,
formatStandardHoursPerWeek,
truncateText,
} from '../../lib/utils'
} from '../../lib/utils/currency.utils'
import { formatDate } from '../../lib/utils/date.utils'
import { formatLocation } from '../../lib/utils/api.utils'
import { truncateText } from '../../lib/utils/application.utils'
import { StatusBadge } from '../status-badge'

import styles from './AssignmentCard.module.scss'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { BaseModal, Button } from '~/libs/ui'
import type { Engagement, EngagementAssignment } from '../../lib/models'
import {
formatCurrencyAmount,
formatDate,
formatStandardHoursPerWeek,
} from '../../lib/utils'
} from '../../lib/utils/currency.utils'
import { formatDate } from '../../lib/utils/date.utils'

import styles from './AssignmentOfferModal.module.scss'

Expand Down
8 changes: 8 additions & 0 deletions src/apps/engagements/src/lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@ export * from './application.utils'
export * from './currency.utils'
export * from './date.utils'
export * from './terms.utils'
export { formatLocation } from './api.utils'
export { truncateText } from './application.utils'
export {
formatCurrencyAmount,
formatStandardHoursPerWeek,
normalizePositiveNumericValue,
} from './currency.utils'
export { formatDate } from './date.utils'
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ jest.mock('../../utils', () => ({
getAssignmentStandardHoursPerWeek: jest.fn(() => 40),
}))

jest.mock('../../constants', () => ({
BILLING_ACCOUNT_MEMBER_PAYMENT_DETAILS_ENABLED: false,
}))

describe('PaymentFormModal', () => {
const member: Assignment = {
agreementRate: '821.20',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import {
Button,
} from '~/libs/ui'

import {
BILLING_ACCOUNT_MEMBER_PAYMENT_DETAILS_ENABLED,
} from '../../constants'
import {
Assignment,
} from '../../models'
Expand Down Expand Up @@ -156,7 +159,9 @@ const PaymentFormModal: FC<PaymentFormModalProps> = (
[hoursWorked, ratePerHour],
)
const challengeFee = useMemo(
() => calculatePaymentChallengeFee(amount, props.billingAccountMarkup),
() => (BILLING_ACCOUNT_MEMBER_PAYMENT_DETAILS_ENABLED
? calculatePaymentChallengeFee(amount, props.billingAccountMarkup)
: undefined),
[amount, props.billingAccountMarkup],
)
const paymentTitle = useMemo(
Expand Down Expand Up @@ -289,10 +294,14 @@ const PaymentFormModal: FC<PaymentFormModalProps> = (
<span className={styles.infoLabel}>Rate Per Week</span>
<span className={styles.infoValue}>{formatCurrency(props.member?.agreementRate)}</span>
</div>
<div className={styles.infoItem}>
<span className={styles.infoLabel}>Billing Account</span>
<span className={styles.infoValue}>{props.billingAccountId || 'Unavailable'}</span>
</div>
{BILLING_ACCOUNT_MEMBER_PAYMENT_DETAILS_ENABLED
? (
<div className={styles.infoItem}>
<span className={styles.infoLabel}>Billing Account</span>
<span className={styles.infoValue}>{props.billingAccountId || 'Unavailable'}</span>
</div>
)
: undefined}
</div>

<div className={styles.fieldRow}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ jest.mock('../../hooks', () => ({
useFetchAssignmentPayments: (...args: unknown[]): unknown => mockUseFetchAssignmentPayments(...args),
}))

jest.mock('../../constants', () => ({
BILLING_ACCOUNT_MEMBER_PAYMENT_DETAILS_ENABLED: false,
}))

jest.mock('~/libs/ui', () => ({
BaseModal: (props: {
buttons?: JSX.Element
Expand Down Expand Up @@ -92,9 +96,9 @@ describe('PaymentHistoryModal', () => {
.toBeTruthy()
expect(screen.getByText('payment.manager'))
.toBeTruthy()
expect(screen.getByText('Fee:'))
.toBeTruthy()
expect(screen.getByText('$18.60'))
.toBeTruthy()
expect(screen.queryByText('Fee:'))
.toBeNull()
expect(screen.queryByText('$18.60'))
.toBeNull()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {
Button,
} from '~/libs/ui'

import {
BILLING_ACCOUNT_MEMBER_PAYMENT_DETAILS_ENABLED,
} from '../../constants'
import {
useFetchAssignmentPayments,
} from '../../hooks'
Expand Down Expand Up @@ -88,7 +91,9 @@ const PaymentHistoryModal: FC<PaymentHistoryModalProps> = (
<ul className={styles.list}>
{paymentsResult.payments.map((payment, index) => {
const paymentAmount = getPaymentAmount(payment)
const paymentChallengeFee = getPaymentChallengeFee(payment)
const paymentChallengeFee = BILLING_ACCOUNT_MEMBER_PAYMENT_DETAILS_ENABLED
? getPaymentChallengeFee(payment)
: undefined
const paymentStatus = getPaymentStatus(payment)
const paymentHoursWorked = getPaymentHoursWorked(payment)
const paymentRemarks = getPaymentRemarks(payment)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* eslint-disable import/no-extraneous-dependencies, ordered-imports/ordered-imports */
import {
fireEvent,
render,
screen,
} from '@testing-library/react'
Expand Down Expand Up @@ -35,6 +34,11 @@ jest.mock('../BillingAccountLineItemsModal', () => ({
),
}))

jest.mock('../../constants', () => ({
BILLING_ACCOUNT_BUDGET_DISPLAY_ENABLED: false,
BILLING_ACCOUNT_DETAILS_MODAL_ENABLED: false,
}))

jest.mock('~/libs/ui', () => ({
IconOutline: {
InformationCircleIcon: (): JSX.Element => <span>info</span>,
Expand Down Expand Up @@ -119,6 +123,17 @@ describe('ProjectBillingAccountExpiredNotice', () => {
})
})

it('hides billing account budget and line-item details while billing details are disabled', () => {
render(
<MemoryRouter>
<ProjectBillingAccountExpiredNotice
billingAccountId={80001063}
billingAccountName='Test Project Engagement BA'
canManageProject
projectId='project-1'
/>
</MemoryRouter>,
)
it('keeps billing account details and line items available when remaining funds are insufficient', () => {
renderNotice()

Expand All @@ -128,21 +143,18 @@ describe('ProjectBillingAccountExpiredNotice', () => {
.toBeTruthy()
expect(screen.getByText(/80001063/))
.toBeTruthy()
expect(screen.getByText('$1,025 / $1,000 spent'))
.toBeTruthy()
expect(screen.getByText(/The billing account for this project has insufficient remaining funds,/))
.toBeTruthy()
expect(screen.getByRole('link', { name: 'click here to update' })
.getAttribute('href'))
.toBe('/projects/project-1/edit')

fireEvent.click(screen.getByRole('button', {
expect(screen.queryByText('$1,025 / $1,000 spent'))
.toBeNull()
expect(screen.queryByText(/The billing account for this project has insufficient remaining funds,/))
.toBeNull()
expect(screen.queryByRole('link', { name: 'click here to update' }))
.toBeNull()
expect(screen.queryByRole('button', {
name: 'View billing account details',
}))

expect(screen.getByRole('dialog')
.textContent)
.toContain('Billing account details for 80001063')
.toBeNull()
expect(screen.queryByRole('dialog'))
.toBeNull()
})

it('shows member payments remaining instead of spent and total budget for copilots', () => {
Expand Down
Loading
Loading