diff --git a/example/src/Onboarding.tsx b/example/src/Onboarding.tsx index 04ebbecfb..d05dd9bbc 100644 --- a/example/src/Onboarding.tsx +++ b/example/src/Onboarding.tsx @@ -596,7 +596,12 @@ const OnboardingWithProps = ({ employmentId={employmentId} externalId={externalId} options={{ - features: ['onboarding_reserves', 'dynamic_steps', 'ea_preview'], + features: [ + 'onboarding_reserves', + 'dynamic_steps', + 'ea_preview', + 'pre_onboarding_requirements', + ], jsonSchemaVersion: { employment_basic_information: 3, }, diff --git a/example/src/ReviewOnboardingStep.tsx b/example/src/ReviewOnboardingStep.tsx index 0b247fb4b..903984f77 100644 --- a/example/src/ReviewOnboardingStep.tsx +++ b/example/src/ReviewOnboardingStep.tsx @@ -9,6 +9,7 @@ import { MetaValues, NestedMeta, PreOnboardingRequirementsBag, + PreOnboardingRequirement, } from '@remoteoss/remote-flows'; import { FullScreenDialog, @@ -31,7 +32,7 @@ import { } from '@remoteoss/remote-flows/internals'; import { AlertError } from './AlertError'; import { OnboardingAlertStatuses } from './OnboardingAlertStatuses'; -import { CheckedState } from '@radix-ui/react-checkbox'; +import { transformHtmlToComponents } from './utils/transformHtml'; export const InviteSection = ({ title, @@ -399,7 +400,7 @@ const DocumentPreviewModal = ({ ); }; -const Requirement = ({ +const DocumentRequirement = ({ requirement, onCreateDocument, onSignDocument, @@ -408,7 +409,6 @@ const Requirement = ({ isSigning, activeRequirementSlug, isLoadingDocumentPreview, - employeeCountry, }: { requirement: NonNullable< PreOnboardingRequirementsBag['requirements'] @@ -420,18 +420,10 @@ const Requirement = ({ isSigning: PreOnboardingRequirementsBag['isSigning']; activeRequirementSlug: PreOnboardingRequirementsBag['activeRequirementSlug']; isLoadingDocumentPreview: PreOnboardingRequirementsBag['isLoadingDocumentPreview']; - employeeCountry?: string; }) => { - const [constraintsAckAt, setConstraintsAckAt] = useState( - requirement.document_constraints_ack_at ?? null, - ); - const [isModalOpen, setIsModalOpen] = useState(false); const [error, setError] = useState(null); - const needsConstraintsAck = - requirement.needs_constraints_ack && !constraintsAckAt; - const isRequirementLoading = activeRequirementSlug === requirement.slug && (isCreatingDocument || isLoadingDocumentPreview); @@ -439,34 +431,32 @@ const Requirement = ({ const handleReviewDocument = async () => { try { setError(null); - await onCreateDocument(requirement.slug, constraintsAckAt || undefined); + await onCreateDocument(requirement.slug); setIsModalOpen(true); - } catch { - setError('Failed to create document'); + } catch (err) { + setError( + err instanceof Error ? err.message : 'Failed to create document', + ); } }; const renderButton = () => { if (requirement.status === 'blocked') { return ( - - + ); } return ( ); }} @@ -262,12 +258,21 @@ describe('PreOnboardingRequirements', () => { expect(renderBag?.requirements).toBeDefined(); }); - const button = screen.getByText('Generate with Ack'); + const button = screen.getByText('Generate Document'); fireEvent.click(button); await waitFor(() => { expect(createDocumentSpy).toHaveBeenCalled(); }); + + expect(createDocumentSpy).toHaveBeenCalledWith( + expect.objectContaining({ + params: expect.objectContaining({ + employmentId: TEST_EMPLOYMENT_ID, + requirementSlug: '5e39159e-96ef-40ea-82bc-b054917fc82f', + }), + }), + ); }); }); @@ -387,7 +392,7 @@ describe('PreOnboardingRequirements', () => { it('should show isCreatingDocument during document generation', async () => { server.use( http.post( - '*/v1/onboarding/employments/:employmentId/pre-onboarding-documents', + '*/v1/onboarding/employments/:employmentId/pre-onboarding-requirements/:requirementSlug/documents', () => { return new Promise(() => {}); }, @@ -570,7 +575,7 @@ describe('PreOnboardingRequirements', () => { server.use( http.post( - '*/v1/onboarding/employments/:employmentId/pre-onboarding-documents', + '*/v1/onboarding/employments/:employmentId/pre-onboarding-requirements/:requirementSlug/documents', createDocumentSpy, ), ); @@ -620,30 +625,28 @@ describe('PreOnboardingRequirements', () => { server.use( http.get( - '*/v1/onboarding/employments/:employmentId/pre-onboarding-document-requirements', + '*/v1/onboarding/employments/:employmentId/pre-onboarding-requirements', () => { return HttpResponse.json({ data: [ { name: 'Individual Labor Agreement', status: 'blocked', + type: 'document', description: 'Blocked until MSA is signed', slug: '5e39159e-96ef-40ea-82bc-b054917fc82f', depends_on_requirement: { name: 'Master Service Agreement', - status: 'awaiting_signatures', + status: 'awaiting', + type: 'document', description: 'Master Service Agreement', slug: 'dc3b954c-9d6c-4ddd-b8dc-531f9be773fb', depends_on_requirement: null, - document_constraints_ack_at: null, freeze_employment_data: false, - needs_constraints_ack: true, redlining_help_email: null, supports_redlining: false, }, - document_constraints_ack_at: null, freeze_employment_data: false, - needs_constraints_ack: true, redlining_help_email: null, supports_redlining: false, }, @@ -652,7 +655,7 @@ describe('PreOnboardingRequirements', () => { }, ), http.post( - '*/v1/onboarding/employments/:employmentId/pre-onboarding-documents', + '*/v1/onboarding/employments/:employmentId/pre-onboarding-requirements/:requirementSlug/documents', createDocumentSpy, ), ); @@ -697,30 +700,28 @@ describe('PreOnboardingRequirements', () => { it('should throw error when attempting to create document for blocked requirement', async () => { server.use( http.get( - '*/v1/onboarding/employments/:employmentId/pre-onboarding-document-requirements', + '*/v1/onboarding/employments/:employmentId/pre-onboarding-requirements', () => { return HttpResponse.json({ data: [ { name: 'Individual Labor Agreement', status: 'blocked', + type: 'document', description: 'Blocked until MSA is signed', slug: '5e39159e-96ef-40ea-82bc-b054917fc82f', depends_on_requirement: { name: 'Master Service Agreement', - status: 'awaiting_signatures', + status: 'awaiting', + type: 'document', description: 'Master Service Agreement', slug: 'dc3b954c-9d6c-4ddd-b8dc-531f9be773fb', depends_on_requirement: null, - document_constraints_ack_at: null, freeze_employment_data: false, - needs_constraints_ack: true, redlining_help_email: null, supports_redlining: false, }, - document_constraints_ack_at: null, freeze_employment_data: false, - needs_constraints_ack: true, redlining_help_email: null, supports_redlining: false, }, @@ -753,4 +754,190 @@ describe('PreOnboardingRequirements', () => { ); }); }); + + describe('requirement acknowledgement', () => { + it('should acknowledge requirement when onAcknowledgeRequirement is called', async () => { + const acknowledgeSpy = vi.fn(() => + HttpResponse.json({ data: { status: 'ok' } }), + ); + + server.use( + http.post( + '*/v1/onboarding/employments/:employmentId/pre-onboarding-requirements/:requirementSlug/acknowledge', + acknowledgeSpy, + ), + ); + + let renderBag: ReturnType; + + render( + { + renderBag = bag; + return ( + + ); + }} + />, + { wrapper: TestWrapper }, + ); + + await waitFor(() => { + expect(renderBag?.requirements).toBeDefined(); + }); + + const button = screen.getByText('Acknowledge'); + fireEvent.click(button); + + await waitFor(() => { + expect(acknowledgeSpy).toHaveBeenCalled(); + }); + + expect(acknowledgeSpy).toHaveBeenCalledWith( + expect.objectContaining({ + params: expect.objectContaining({ + employmentId: TEST_EMPLOYMENT_ID, + requirementSlug: '5e39159e-96ef-40ea-82bc-b054917fc82f', + }), + }), + ); + }); + + it('should remove acknowledgement when requirement is finished', async () => { + const removeAcknowledgeSpy = vi.fn(() => + HttpResponse.json({ data: { status: 'ok' } }), + ); + + server.use( + http.delete( + '*/v1/onboarding/employments/:employmentId/pre-onboarding-requirements/:requirementSlug/acknowledge', + removeAcknowledgeSpy, + ), + ); + + let renderBag: ReturnType; + + render( + { + renderBag = bag; + return ( + + ); + }} + />, + { wrapper: TestWrapper }, + ); + + await waitFor(() => { + expect(renderBag?.requirements).toBeDefined(); + }); + + const button = screen.getByText('Remove Acknowledgement'); + fireEvent.click(button); + + await waitFor(() => { + expect(removeAcknowledgeSpy).toHaveBeenCalled(); + }); + + expect(removeAcknowledgeSpy).toHaveBeenCalledWith( + expect.objectContaining({ + params: expect.objectContaining({ + employmentId: TEST_EMPLOYMENT_ID, + requirementSlug: 'dc3b954c-9d6c-4ddd-b8dc-531f9be773fb', + }), + }), + ); + }); + + it('should throw error when acknowledgement is locked by a dependent requirement', async () => { + server.use( + http.get( + '*/v1/onboarding/employments/:employmentId/pre-onboarding-requirements', + () => { + return HttpResponse.json({ + data: [ + { + name: 'Master Service Agreement', + status: 'finished', + type: 'acknowledgement', + description: 'Master Service Agreement', + slug: 'dc3b954c-9d6c-4ddd-b8dc-531f9be773fb', + depends_on_requirement: null, + freeze_employment_data: false, + redlining_help_email: null, + supports_redlining: false, + }, + { + name: 'Individual Labor Agreement', + status: 'finished', + type: 'document', + description: 'Individual Labor Agreement', + slug: '5e39159e-96ef-40ea-82bc-b054917fc82f', + depends_on_requirement: { + name: 'Master Service Agreement', + status: 'finished', + type: 'acknowledgement', + description: 'Master Service Agreement', + slug: 'dc3b954c-9d6c-4ddd-b8dc-531f9be773fb', + depends_on_requirement: null, + freeze_employment_data: false, + redlining_help_email: null, + supports_redlining: false, + }, + freeze_employment_data: false, + redlining_help_email: null, + supports_redlining: false, + }, + ], + }); + }, + ), + ); + + let renderBag!: ReturnType; + + render( + { + renderBag = bag; + return
Test
; + }} + />, + { wrapper: TestWrapper }, + ); + + await waitFor(() => { + expect(renderBag.requirements).toBeDefined(); + }); + + expect( + renderBag.isAckLocked('dc3b954c-9d6c-4ddd-b8dc-531f9be773fb'), + ).toBe(true); + + await expect( + renderBag.onAcknowledgeRequirement( + 'dc3b954c-9d6c-4ddd-b8dc-531f9be773fb', + ), + ).rejects.toThrow( + 'Cannot uncheck this acknowledgement because other requirements depend on it and are already in progress or completed.', + ); + }); + }); }); diff --git a/src/flows/Onboarding/index.ts b/src/flows/Onboarding/index.ts index 1e6974950..799a70b81 100644 --- a/src/flows/Onboarding/index.ts +++ b/src/flows/Onboarding/index.ts @@ -2,6 +2,7 @@ export { OnboardingFlow } from './OnboardingFlow'; export { EmploymentAgreementInfoContent } from './components/EmploymentAgreementInfoContent'; export type { OnboardingInviteProps } from './components/OnboardingInvite'; export type { PreOnboardingRequirementsBag } from './components/PreOnboardingRequirements'; +export type { PreOnboardingRequirement } from '@/src/client/types.gen'; export type { BenefitsFormPayload, BasicInformationFormPayload, diff --git a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx index 9e75949cb..9f3ecb189 100644 --- a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx @@ -89,7 +89,7 @@ describe('OnboardingInvite', () => { }), http.get( - '*/v1/onboarding/employments/:employmentId/pre-onboarding-document-requirements', + '*/v1/onboarding/employments/:employmentId/pre-onboarding-requirements', () => { return HttpResponse.json({ data: [] }); }, @@ -1215,7 +1215,7 @@ describe('OnboardingInvite', () => { server.use( http.get( - '*/v1/onboarding/employments/:employmentId/pre-onboarding-document-requirements', + '*/v1/onboarding/employments/:employmentId/pre-onboarding-requirements', () => { return HttpResponse.json(preOnboardingRequirementsMock); }, @@ -1236,19 +1236,18 @@ describe('OnboardingInvite', () => { server.use( http.get( - '*/v1/onboarding/employments/:employmentId/pre-onboarding-document-requirements', + '*/v1/onboarding/employments/:employmentId/pre-onboarding-requirements', () => { return HttpResponse.json({ data: [ { name: 'Master Service Agreement', status: 'finished', + type: 'document', description: 'Master Service Agreement required', slug: 'dc3b954c-9d6c-4ddd-b8dc-531f9be773fb', depends_on_requirement: null, - document_constraints_ack_at: '2026-05-18T12:23:24Z', freeze_employment_data: false, - needs_constraints_ack: true, redlining_help_email: null, supports_redlining: false, }, @@ -1280,31 +1279,29 @@ describe('OnboardingInvite', () => { server.use( http.get( - '*/v1/onboarding/employments/:employmentId/pre-onboarding-document-requirements', + '*/v1/onboarding/employments/:employmentId/pre-onboarding-requirements', () => { return HttpResponse.json({ data: [ { name: 'Individual Labor Agreement', - status: 'awaiting_signatures', + status: 'awaiting', + type: 'document', description: 'Individual Labor Agreement required', slug: '5e39159e-96ef-40ea-82bc-b054917fc82f', depends_on_requirement: null, - document_constraints_ack_at: null, freeze_employment_data: false, - needs_constraints_ack: true, redlining_help_email: null, supports_redlining: false, }, { name: 'Master Service Agreement', status: 'finished', + type: 'document', description: 'Master Service Agreement required', slug: 'dc3b954c-9d6c-4ddd-b8dc-531f9be773fb', depends_on_requirement: null, - document_constraints_ack_at: '2026-05-18T12:23:24Z', freeze_employment_data: false, - needs_constraints_ack: true, redlining_help_email: null, supports_redlining: false, }, @@ -1329,7 +1326,7 @@ describe('OnboardingInvite', () => { server.use( http.get( - '*/v1/onboarding/employments/:employmentId/pre-onboarding-document-requirements', + '*/v1/onboarding/employments/:employmentId/pre-onboarding-requirements', async () => { await requirementsPromise; return HttpResponse.json({ @@ -1337,12 +1334,11 @@ describe('OnboardingInvite', () => { { name: 'Master Service Agreement', status: 'finished', + type: 'document', description: 'Master Service Agreement required', slug: 'dc3b954c-9d6c-4ddd-b8dc-531f9be773fb', depends_on_requirement: null, - document_constraints_ack_at: '2026-05-18T12:23:24Z', freeze_employment_data: false, - needs_constraints_ack: true, redlining_help_email: null, supports_redlining: false, }, diff --git a/src/index.tsx b/src/index.tsx index 12184b74e..5190d2948 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -63,6 +63,7 @@ export type { CreditRiskState, OnboardingRenderProps, PreOnboardingRequirementsBag, + PreOnboardingRequirement, } from '@/src/flows/Onboarding'; export { diff --git a/src/tests/handlers.ts b/src/tests/handlers.ts index e4928e560..42c01e307 100644 --- a/src/tests/handlers.ts +++ b/src/tests/handlers.ts @@ -131,14 +131,14 @@ const benefitOffersSchemaHandler = http.get( ); const preOnboardingRequirementsHandler = http.get( - '*/v1/onboarding/employments/:employmentId/pre-onboarding-document-requirements', + '*/v1/onboarding/employments/:employmentId/pre-onboarding-requirements', () => { return HttpResponse.json(preOnboardingRequirementsMock); }, ); const createPreOnboardingDocumentHandler = http.post( - '*/v1/onboarding/employments/:employmentId/pre-onboarding-documents', + '*/v1/onboarding/employments/:employmentId/pre-onboarding-requirements/:requirementSlug/documents', () => { return HttpResponse.json(generatedDocumentMock); },