PSA/packages/billing/tests/contractWizardResume.test.tsx
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

924 lines
31 KiB
TypeScript

/**
* @vitest-environment jsdom
*/
import React from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { act, cleanup, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom/vitest';
// Stable t()/formatters so hooks that list `t` in effect dependencies do not
// re-run on every render (react-i18next's no-instance fallback returns an
// unstable t, which sends ContractWizard's mount effect into a render loop).
vi.mock('@alga-psa/ui/lib/i18n/client', () => {
const t = (key: string, opts?: string | { defaultValue?: string }) => {
if (typeof opts === 'string') return opts;
return typeof opts?.defaultValue === 'string' ? opts.defaultValue : key;
};
const translation = { t };
const formatters = {
formatDate: (value: unknown) => String(value),
formatCurrency: (value: number) => `$${value}`,
};
return {
useTranslation: () => translation,
useFormatters: () => formatters,
};
});
vi.mock('@alga-psa/ui/components/Dialog', () => ({
Dialog: ({ isOpen, children, footer }: { isOpen: boolean; children: React.ReactNode; footer?: React.ReactNode }) =>
isOpen ? (
<div data-testid="dialog">
{children}
{footer}
</div>
) : null,
DialogContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogFooter: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogHeader: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogDescription: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogTrigger: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('@alga-psa/ui/components/onboarding/WizardProgress', () => ({
WizardProgress: ({ currentStep }: { currentStep: number }) => (
<div data-testid="wizard-progress" data-current-step={String(currentStep)} />
),
}));
vi.mock('@alga-psa/ui/components/onboarding/WizardNavigation', () => ({
WizardNavigation: ({
onNext,
onBack,
onSaveDraft,
onFinish,
}: {
onNext: () => void;
onBack: () => void;
onSaveDraft: () => void;
onFinish: () => void;
}) => (
<div>
<button type="button" onClick={onBack}>
Back
</button>
<button type="button" onClick={onNext}>
Next
</button>
<button type="button" onClick={onSaveDraft}>
Save Draft
</button>
<button type="button" onClick={onFinish}>
Finish
</button>
</div>
),
}));
vi.mock('../src/components/billing-dashboard/contracts/wizard-steps/ContractBasicsStep', () => ({
ContractBasicsStep: ({
data,
updateData,
onTemplateSelect,
}: {
data: any;
updateData: (next: Record<string, unknown>) => void;
onTemplateSelect?: (templateId: string | null) => void;
}) => (
<div
data-testid="step-contract-basics"
data-client-id={data.client_id ?? ''}
data-contract-name={data.contract_name ?? ''}
data-start-date={data.start_date ?? ''}
data-end-date={data.end_date ?? ''}
>
<button
type="button"
onClick={() =>
updateData({
client_id: 'client-1',
contract_name: 'Seeded Contract',
start_date: '2026-01-01',
billing_frequency: 'monthly',
currency_code: 'USD',
fixed_base_rate: 10000,
fixed_services: [{ service_id: 'svc-1', quantity: 1 }],
})
}
>
Seed valid basics
</button>
<button type="button" onClick={() => onTemplateSelect?.('template-1')}>
Apply Template
</button>
</div>
),
}));
vi.mock('../src/components/billing-dashboard/contracts/wizard-steps/FixedFeeServicesStep', () => ({
FixedFeeServicesStep: ({
data,
updateData,
}: {
data: any;
updateData: (next: Record<string, unknown>) => void;
}) => (
<div
data-testid="step-fixed-fee"
data-fixed-services-count={String((data.fixed_services ?? []).length)}
data-cadence-owner={data.cadence_owner ?? 'client'}
data-billing-timing={data.billing_timing ?? 'arrears'}
data-enable-proration={String(Boolean(data.enable_proration))}
>
<button
type="button"
onClick={() => updateData({ enable_proration: !data.enable_proration })}
>
Toggle Partial-Period Adjustment
</button>
</div>
),
}));
vi.mock('../src/components/billing-dashboard/contracts/wizard-steps/ProductsStep', () => ({
ProductsStep: ({ data }: { data: any }) => (
<div data-testid="step-products" data-product-services-count={String((data.product_services ?? []).length)} />
),
}));
vi.mock('../src/components/billing-dashboard/contracts/wizard-steps/HourlyServicesStep', () => ({
HourlyServicesStep: ({ data }: { data: any }) => (
<div data-testid="step-hourly" data-hourly-services-count={String((data.hourly_services ?? []).length)} />
),
}));
vi.mock('../src/components/billing-dashboard/contracts/wizard-steps/UsageBasedServicesStep', () => ({
UsageBasedServicesStep: ({ data }: { data: any }) => (
<div data-testid="step-usage" data-usage-services-count={String((data.usage_services ?? []).length)} />
),
}));
vi.mock('../src/components/billing-dashboard/contracts/wizard-steps/ReviewContractStep', () => ({
ReviewContractStep: ({ data }: { data: any }) => (
<div
data-testid="step-review"
data-contract-name={data.contract_name ?? ''}
data-fixed-services-count={String((data.fixed_services ?? []).length)}
data-product-services-count={String((data.product_services ?? []).length)}
data-hourly-services-count={String((data.hourly_services ?? []).length)}
data-usage-services-count={String((data.usage_services ?? []).length)}
data-cadence-owner={data.cadence_owner ?? 'client'}
data-billing-timing={data.billing_timing ?? 'arrears'}
/>
),
}));
vi.mock('@alga-psa/billing/actions/contractWizardActions', () => ({
createClientContractFromWizard: vi.fn(),
listContractTemplatesForWizard: vi.fn(async () => []),
getContractTemplateSnapshotForClientWizard: vi.fn(),
}));
vi.mock('@alga-psa/billing/actions/billingSettingsActions', () => ({
getDefaultBillingSettings: vi.fn(async () => ({
defaultRenewalMode: 'manual',
defaultNoticePeriodDays: 30,
})),
}));
describe('ContractWizard resume behavior', () => {
beforeEach(() => {
document.body.removeAttribute('data-scroll-locked');
document.body.removeAttribute('style');
cleanup();
vi.clearAllMocks();
});
it('starts at Step 1 (Contract Basics) when opened (T033)', async () => {
const { ContractWizard } = await import('../src/components/billing-dashboard/contracts/ContractWizard');
render(
<ContractWizard
open={true}
onOpenChange={vi.fn()}
editingContract={{
contract_id: 'contract-1',
is_draft: true,
client_id: 'client-1',
contract_name: 'Draft Alpha',
start_date: '2026-01-01',
billing_frequency: 'monthly',
currency_code: 'USD',
enable_proration: false,
fixed_services: [],
product_services: [],
hourly_services: [],
usage_services: [],
}}
/>,
);
await waitFor(() => {
expect(screen.getByTestId('step-contract-basics')).toBeInTheDocument();
});
expect(screen.queryByTestId('step-fixed-fee')).not.toBeInTheDocument();
});
it('step 1 shows pre-populated client selection from draft (T034)', async () => {
const { ContractWizard } = await import('../src/components/billing-dashboard/contracts/ContractWizard');
render(
<ContractWizard
open={true}
onOpenChange={vi.fn()}
editingContract={{
contract_id: 'contract-1',
is_draft: true,
client_id: 'client-99',
contract_name: 'Draft Alpha',
start_date: '2026-01-01',
billing_frequency: 'monthly',
currency_code: 'USD',
enable_proration: false,
fixed_services: [],
product_services: [],
hourly_services: [],
usage_services: [],
}}
/>,
);
const step = await screen.findByTestId('step-contract-basics');
expect(step).toHaveAttribute('data-client-id', 'client-99');
});
it('step 1 shows pre-populated contract name from draft (T035)', async () => {
const { ContractWizard } = await import('../src/components/billing-dashboard/contracts/ContractWizard');
render(
<ContractWizard
open={true}
onOpenChange={vi.fn()}
editingContract={{
contract_id: 'contract-1',
is_draft: true,
client_id: 'client-1',
contract_name: 'Draft Name',
start_date: '2026-01-01',
billing_frequency: 'monthly',
currency_code: 'USD',
enable_proration: false,
fixed_services: [],
product_services: [],
hourly_services: [],
usage_services: [],
}}
/>,
);
const step = await screen.findByTestId('step-contract-basics');
expect(step).toHaveAttribute('data-contract-name', 'Draft Name');
});
it('step 1 shows pre-populated dates from draft (T036)', async () => {
const { ContractWizard } = await import('../src/components/billing-dashboard/contracts/ContractWizard');
render(
<ContractWizard
open={true}
onOpenChange={vi.fn()}
editingContract={{
contract_id: 'contract-1',
is_draft: true,
client_id: 'client-1',
contract_name: 'Draft Alpha',
start_date: '2026-01-01',
end_date: '2026-12-31',
billing_frequency: 'monthly',
currency_code: 'USD',
enable_proration: false,
fixed_services: [],
product_services: [],
hourly_services: [],
usage_services: [],
}}
/>,
);
const step = await screen.findByTestId('step-contract-basics');
expect(step).toHaveAttribute('data-start-date', '2026-01-01');
expect(step).toHaveAttribute('data-end-date', '2026-12-31');
});
it('step 2 (Fixed Fee) shows pre-populated services from draft (T037)', async () => {
const { ContractWizard } = await import('../src/components/billing-dashboard/contracts/ContractWizard');
render(
<ContractWizard
open={true}
onOpenChange={vi.fn()}
editingContract={{
contract_id: 'contract-1',
is_draft: true,
client_id: 'client-1',
contract_name: 'Draft Alpha',
start_date: '2026-01-01',
billing_frequency: 'monthly',
currency_code: 'USD',
enable_proration: false,
fixed_base_rate: 10000,
fixed_services: [
{ service_id: 'svc-1', quantity: 1 },
{ service_id: 'svc-2', quantity: 2 },
],
product_services: [],
hourly_services: [],
usage_services: [],
}}
/>,
);
await screen.findByTestId('step-contract-basics');
const user = userEvent.setup();
await act(async () => {
await user.click(screen.getByText('Next'));
});
const step = await screen.findByTestId('step-fixed-fee');
expect(step).toHaveAttribute('data-fixed-services-count', '2');
});
it('preserves cadence_owner from resumed drafts across fixed-fee step transitions (T113)', async () => {
const { ContractWizard } = await import('../src/components/billing-dashboard/contracts/ContractWizard');
render(
<ContractWizard
open={true}
onOpenChange={vi.fn()}
editingContract={{
contract_id: 'contract-1',
is_draft: true,
client_id: 'client-1',
contract_name: 'Draft Alpha',
start_date: '2026-01-01',
billing_frequency: 'monthly',
currency_code: 'USD',
cadence_owner: 'contract',
billing_timing: 'advance',
enable_proration: false,
fixed_base_rate: 10000,
fixed_services: [{ service_id: 'svc-1', quantity: 1 }],
product_services: [],
hourly_services: [],
usage_services: [],
}}
/>,
);
await screen.findByTestId('step-contract-basics');
const user = userEvent.setup();
await act(async () => {
await user.click(screen.getByText('Next'));
});
const fixedStep = await screen.findByTestId('step-fixed-fee');
expect(fixedStep).toHaveAttribute('data-cadence-owner', 'contract');
await act(async () => {
await user.click(screen.getByText('Next'));
});
await act(async () => {
await user.click(screen.getByText('Back'));
});
expect(await screen.findByTestId('step-fixed-fee')).toHaveAttribute('data-cadence-owner', 'contract');
});
it('step 3 (Products) shows pre-populated products from draft (T038)', async () => {
const { ContractWizard } = await import('../src/components/billing-dashboard/contracts/ContractWizard');
render(
<ContractWizard
open={true}
onOpenChange={vi.fn()}
editingContract={{
contract_id: 'contract-1',
is_draft: true,
client_id: 'client-1',
contract_name: 'Draft Alpha',
start_date: '2026-01-01',
billing_frequency: 'monthly',
currency_code: 'USD',
enable_proration: false,
fixed_services: [],
product_services: [
{ service_id: 'prod-1', quantity: 1 },
{ service_id: 'prod-2', quantity: 2 },
],
hourly_services: [],
usage_services: [],
}}
/>,
);
await screen.findByTestId('step-contract-basics');
const user = userEvent.setup();
await act(async () => {
await user.click(screen.getByText('Next'));
});
await act(async () => {
await user.click(screen.getByText('Next'));
});
const step = await screen.findByTestId('step-products');
expect(step).toHaveAttribute('data-product-services-count', '2');
});
it('step 4 (Hourly) shows pre-populated hourly services from draft (T039)', async () => {
const { ContractWizard } = await import('../src/components/billing-dashboard/contracts/ContractWizard');
render(
<ContractWizard
open={true}
onOpenChange={vi.fn()}
editingContract={{
contract_id: 'contract-1',
is_draft: true,
client_id: 'client-1',
contract_name: 'Draft Alpha',
start_date: '2026-01-01',
billing_frequency: 'monthly',
currency_code: 'USD',
enable_proration: false,
fixed_services: [],
product_services: [],
hourly_services: [
{ service_id: 'hr-1' },
{ service_id: 'hr-2' },
],
usage_services: [],
}}
/>,
);
await screen.findByTestId('step-contract-basics');
const user = userEvent.setup();
await act(async () => {
await user.click(screen.getByText('Next'));
});
await act(async () => {
await user.click(screen.getByText('Next'));
});
await act(async () => {
await user.click(screen.getByText('Next'));
});
const step = await screen.findByTestId('step-hourly');
expect(step).toHaveAttribute('data-hourly-services-count', '2');
});
it('step 5 (Usage) shows pre-populated usage services from draft (T040)', async () => {
const { ContractWizard } = await import('../src/components/billing-dashboard/contracts/ContractWizard');
render(
<ContractWizard
open={true}
onOpenChange={vi.fn()}
editingContract={{
contract_id: 'contract-1',
is_draft: true,
client_id: 'client-1',
contract_name: 'Draft Alpha',
start_date: '2026-01-01',
billing_frequency: 'monthly',
currency_code: 'USD',
enable_proration: false,
fixed_services: [],
product_services: [],
hourly_services: [],
usage_services: [
{ service_id: 'u-1' },
{ service_id: 'u-2' },
{ service_id: 'u-3' },
],
}}
/>,
);
await screen.findByTestId('step-contract-basics');
const user = userEvent.setup();
for (let i = 0; i < 4; i++) {
await act(async () => {
await user.click(screen.getByText('Next'));
});
}
const step = await screen.findByTestId('step-usage');
expect(step).toHaveAttribute('data-usage-services-count', '3');
});
it('step 6 (Review) shows complete draft data for review (T041)', async () => {
const { ContractWizard } = await import('../src/components/billing-dashboard/contracts/ContractWizard');
render(
<ContractWizard
open={true}
onOpenChange={vi.fn()}
editingContract={{
contract_id: 'contract-1',
is_draft: true,
client_id: 'client-1',
contract_name: 'Draft Alpha',
start_date: '2026-01-01',
billing_frequency: 'monthly',
currency_code: 'USD',
enable_proration: false,
fixed_base_rate: 10000,
fixed_services: [{ service_id: 'svc-1', quantity: 1 }],
product_services: [{ service_id: 'prod-1', quantity: 1 }],
hourly_services: [{ service_id: 'hr-1' }],
usage_services: [{ service_id: 'u-1' }],
}}
/>,
);
await screen.findByTestId('step-contract-basics');
const user = userEvent.setup();
for (let i = 0; i < 5; i++) {
await act(async () => {
await user.click(screen.getByText('Next'));
});
}
const step = await screen.findByTestId('step-review');
expect(step).toHaveAttribute('data-contract-name', 'Draft Alpha');
expect(step).toHaveAttribute('data-fixed-services-count', '1');
expect(step).toHaveAttribute('data-product-services-count', '1');
expect(step).toHaveAttribute('data-hourly-services-count', '1');
expect(step).toHaveAttribute('data-usage-services-count', '1');
});
it('clicking Save Draft in resumed wizard updates existing draft (T042)', async () => {
const { createClientContractFromWizard } = await import('@alga-psa/billing/actions/contractWizardActions');
(createClientContractFromWizard as any).mockResolvedValue({ contract_id: 'contract-1' });
const { ContractWizard } = await import('../src/components/billing-dashboard/contracts/ContractWizard');
render(
<ContractWizard
open={true}
onOpenChange={vi.fn()}
editingContract={{
contract_id: 'contract-1',
is_draft: true,
client_id: 'client-1',
contract_name: 'Draft Alpha',
start_date: '2026-01-01',
billing_frequency: 'monthly',
currency_code: 'USD',
enable_proration: false,
fixed_services: [],
product_services: [],
hourly_services: [],
usage_services: [],
}}
/>,
);
await screen.findByTestId('step-contract-basics');
const user = userEvent.setup();
await act(async () => {
await user.click(screen.getByText('Save Draft'));
});
await waitFor(() => {
expect(createClientContractFromWizard).toHaveBeenCalled();
});
const [submission, options] = (createClientContractFromWizard as any).mock.calls[0];
expect(submission.contract_id).toBe('contract-1');
expect(options).toEqual({ isDraft: true });
});
it('resaving a resumed draft preserves cadence_owner and partial-period defaults in the emitted draft payload', async () => {
const { createClientContractFromWizard } = await import('@alga-psa/billing/actions/contractWizardActions');
(createClientContractFromWizard as any).mockResolvedValue({ contract_id: 'contract-1' });
const onComplete = vi.fn();
const { ContractWizard } = await import('../src/components/billing-dashboard/contracts/ContractWizard');
render(
<ContractWizard
open={true}
onOpenChange={vi.fn()}
onComplete={onComplete}
editingContract={{
contract_id: 'contract-1',
is_draft: true,
client_id: 'client-1',
contract_name: 'Draft Alpha',
start_date: '2026-01-01',
billing_frequency: 'monthly',
currency_code: 'USD',
cadence_owner: 'contract',
billing_timing: 'advance',
enable_proration: false,
fixed_base_rate: 10000,
fixed_services: [{ service_id: 'svc-1', quantity: 1 }],
product_services: [],
hourly_services: [],
usage_services: [],
}}
/>,
);
await screen.findByTestId('step-contract-basics');
const user = userEvent.setup();
await act(async () => {
await user.click(screen.getByText('Next'));
});
const fixedStep = await screen.findByTestId('step-fixed-fee');
expect(fixedStep).toHaveAttribute('data-cadence-owner', 'contract');
expect(fixedStep).toHaveAttribute('data-billing-timing', 'advance');
expect(fixedStep).toHaveAttribute('data-enable-proration', 'false');
await act(async () => {
await user.click(screen.getByText('Toggle Partial-Period Adjustment'));
await user.click(screen.getByText('Save Draft'));
});
await waitFor(() => {
expect(onComplete).toHaveBeenCalled();
});
const [draftPayload] = onComplete.mock.calls.at(-1)!;
expect(draftPayload).toMatchObject({
contract_id: 'contract-1',
is_draft: true,
cadence_owner: 'contract',
billing_timing: 'advance',
enable_proration: true,
});
});
it('applies template-authored cadence_owner and billing_timing to the client contract submission payload (T236)', async () => {
const {
createClientContractFromWizard,
getContractTemplateSnapshotForClientWizard,
} = await import('@alga-psa/billing/actions/contractWizardActions');
(createClientContractFromWizard as any).mockResolvedValue({ contract_id: 'contract-template' });
(getContractTemplateSnapshotForClientWizard as any).mockResolvedValue({
contract_name: 'Template Contract',
billing_frequency: 'monthly',
cadence_owner: 'contract',
billing_timing: 'advance',
enable_proration: false,
fixed_base_rate: 10000,
fixed_services: [{ service_id: 'svc-template', quantity: 1 }],
});
const { ContractWizard } = await import('../src/components/billing-dashboard/contracts/ContractWizard');
render(<ContractWizard open={true} onOpenChange={vi.fn()} />);
await screen.findByTestId('step-contract-basics');
const user = userEvent.setup();
await act(async () => {
await user.click(screen.getByText('Seed valid basics'));
await user.click(screen.getByText('Apply Template'));
});
await waitFor(() => {
expect(getContractTemplateSnapshotForClientWizard).toHaveBeenCalledWith('template-1');
});
await act(async () => {
await user.click(screen.getByText('Next'));
});
const fixedStep = await screen.findByTestId('step-fixed-fee');
expect(fixedStep).toHaveAttribute('data-cadence-owner', 'contract');
expect(fixedStep).toHaveAttribute('data-billing-timing', 'advance');
await act(async () => {
await user.click(screen.getByText('Save Draft'));
});
await waitFor(() => {
expect(createClientContractFromWizard).toHaveBeenCalledTimes(1);
});
const [submission, options] = (createClientContractFromWizard as any).mock.calls[0];
expect(options).toEqual({ isDraft: true });
expect(submission).toMatchObject({
template_id: 'template-1',
cadence_owner: 'contract',
billing_timing: 'advance',
enable_proration: false,
fixed_services: [{ service_id: 'svc-template', quantity: 1 }],
});
});
it('save draft does not create a duplicate contract (T043)', async () => {
const { createClientContractFromWizard } = await import('@alga-psa/billing/actions/contractWizardActions');
(createClientContractFromWizard as any).mockResolvedValue({ contract_id: 'contract-99' });
const { ContractWizard } = await import('../src/components/billing-dashboard/contracts/ContractWizard');
render(
<ContractWizard
open={true}
onOpenChange={vi.fn()}
editingContract={{
contract_id: 'contract-99',
is_draft: true,
client_id: 'client-1',
contract_name: 'Draft Alpha',
start_date: '2026-01-01',
billing_frequency: 'monthly',
currency_code: 'USD',
enable_proration: false,
fixed_services: [],
product_services: [],
hourly_services: [],
usage_services: [],
}}
/>,
);
await screen.findByTestId('step-contract-basics');
const user = userEvent.setup();
await act(async () => {
await user.click(screen.getByText('Save Draft'));
});
await waitFor(() => {
expect(createClientContractFromWizard).toHaveBeenCalledTimes(1);
});
const [submission] = (createClientContractFromWizard as any).mock.calls[0];
expect(submission.contract_id).toBe('contract-99');
});
it('save draft preserves the original contract_id (T044)', async () => {
const { createClientContractFromWizard } = await import('@alga-psa/billing/actions/contractWizardActions');
(createClientContractFromWizard as any).mockResolvedValue({ contract_id: 'contract-1' });
const onComplete = vi.fn();
const { ContractWizard } = await import('../src/components/billing-dashboard/contracts/ContractWizard');
render(
<ContractWizard
open={true}
onOpenChange={vi.fn()}
onComplete={onComplete}
editingContract={{
contract_id: 'contract-1',
is_draft: true,
client_id: 'client-1',
contract_name: 'Draft Alpha',
start_date: '2026-01-01',
billing_frequency: 'monthly',
currency_code: 'USD',
enable_proration: false,
fixed_services: [],
product_services: [],
hourly_services: [],
usage_services: [],
}}
/>,
);
await screen.findByTestId('step-contract-basics');
const user = userEvent.setup();
await act(async () => {
await user.click(screen.getByText('Save Draft'));
});
await waitFor(() => {
expect(onComplete).toHaveBeenCalled();
});
const [data] = onComplete.mock.calls[0];
expect(data.contract_id).toBe('contract-1');
});
it("completing resumed wizard sets contract status to 'active' (T045)", async () => {
const { createClientContractFromWizard } = await import('@alga-psa/billing/actions/contractWizardActions');
(createClientContractFromWizard as any).mockResolvedValue({ contract_id: 'contract-1' });
const { ContractWizard } = await import('../src/components/billing-dashboard/contracts/ContractWizard');
render(
<ContractWizard
open={true}
onOpenChange={vi.fn()}
editingContract={{
contract_id: 'contract-1',
is_draft: true,
client_id: 'client-1',
contract_name: 'Draft Alpha',
start_date: '2026-01-01',
billing_frequency: 'monthly',
currency_code: 'USD',
enable_proration: false,
fixed_base_rate: 10000,
fixed_services: [{ service_id: 'svc-1', quantity: 1 }],
product_services: [],
hourly_services: [],
usage_services: [],
}}
/>,
);
await screen.findByTestId('step-contract-basics');
const user = userEvent.setup();
for (let i = 0; i < 5; i++) {
await act(async () => {
await user.click(screen.getByText('Next'));
});
}
await screen.findByTestId('step-review');
await act(async () => {
await user.click(screen.getByText('Finish'));
});
await waitFor(() => {
expect(createClientContractFromWizard).toHaveBeenCalled();
});
const [submission, options] = (createClientContractFromWizard as any).mock.calls[0];
expect(submission.contract_id).toBe('contract-1');
expect(options).toBeUndefined();
});
it('completing resumed wizard sets is_active to true (T046)', async () => {
const { createClientContractFromWizard } = await import('@alga-psa/billing/actions/contractWizardActions');
(createClientContractFromWizard as any).mockResolvedValue({ contract_id: 'contract-1' });
const { ContractWizard } = await import('../src/components/billing-dashboard/contracts/ContractWizard');
render(
<ContractWizard
open={true}
onOpenChange={vi.fn()}
editingContract={{
contract_id: 'contract-1',
is_draft: true,
client_id: 'client-1',
contract_name: 'Draft Alpha',
start_date: '2026-01-01',
billing_frequency: 'monthly',
currency_code: 'USD',
enable_proration: false,
fixed_base_rate: 10000,
fixed_services: [{ service_id: 'svc-1', quantity: 1 }],
product_services: [],
hourly_services: [],
usage_services: [],
}}
/>,
);
await screen.findByTestId('step-contract-basics');
const user = userEvent.setup();
for (let i = 0; i < 5; i++) {
await act(async () => {
await user.click(screen.getByText('Next'));
});
}
await screen.findByTestId('step-review');
await act(async () => {
await user.click(screen.getByText('Finish'));
});
await waitFor(() => {
expect(createClientContractFromWizard).toHaveBeenCalled();
});
const [_submission, options] = (createClientContractFromWizard as any).mock.calls[0];
expect(options).toBeUndefined();
});
it('submits the client cadence default for newly created recurring lines (T112)', async () => {
const { createClientContractFromWizard } = await import('@alga-psa/billing/actions/contractWizardActions');
(createClientContractFromWizard as any).mockResolvedValue({ contract_id: 'contract-new' });
const { ContractWizard } = await import('../src/components/billing-dashboard/contracts/ContractWizard');
render(<ContractWizard open={true} onOpenChange={vi.fn()} />);
await screen.findByTestId('step-contract-basics');
const user = userEvent.setup();
await act(async () => {
await user.click(screen.getByText('Seed valid basics'));
});
for (let i = 0; i < 5; i++) {
await act(async () => {
await user.click(screen.getByText('Next'));
});
}
expect(await screen.findByTestId('step-review')).toHaveAttribute('data-cadence-owner', 'client');
await act(async () => {
await user.click(screen.getByText('Finish'));
});
await waitFor(() => {
expect(createClientContractFromWizard).toHaveBeenCalledTimes(1);
});
const [submission] = (createClientContractFromWizard as any).mock.calls[0];
expect(submission.cadence_owner).toBe('client');
});
});