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
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
},
"dependencies": {
"@aws-sdk/client-sns": "^3.1145.0",
"@robosystems/client": "^2.17.0",
"@robosystems/client": "^2.19.0",
"@robosystems/core": "^0.15.0",
"@robosystems/report-components": "^0.9.0",
"flowbite": "^3.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const mockPreview = vi.fn()
const mockPropose = vi.fn()

vi.mock('@robosystems/core', () => ({
clients: {
ledger: {
previewConsolidationEntries: (...args: any[]) => mockPreview(...args),
proposeConsolidationEntries: (...args: any[]) => mockPropose(...args),
},
},
LoadingState: () => (
<div data-testid="loading-state" role="status">
Loading
</div>
),
}))

vi.mock('flowbite-react', () => ({
Badge: ({ children, color }: any) => (
<span data-badge-color={color}>{children}</span>
),
Button: ({ children, onClick, disabled }: any) => (
<button onClick={onClick} disabled={disabled}>
{children}
</button>
),
Card: ({ children }: any) => <div>{children}</div>,
Spinner: () => <span data-testid="spinner" />,
Table: ({ children }: any) => <table>{children}</table>,
TableBody: ({ children }: any) => <tbody>{children}</tbody>,
TableCell: ({ children }: any) => <td>{children}</td>,
TableHead: ({ children }: any) => <thead>{children}</thead>,
TableHeadCell: ({ children }: any) => <th>{children}</th>,
TableRow: ({ children }: any) => <tr>{children}</tr>,
}))

vi.mock('react-icons/hi', () => ({
HiExclamationCircle: () => <span data-testid="icon-error" />,
}))

vi.mock('react-icons/tb', () => ({
TbFileInvoice: () => <span />,
}))

import ConsolidationEntriesPanel from '../components/ConsolidationEntriesPanel'

const PROPOSAL = {
adjustment_kind: 'elimination_investment',
adjusts_entity_id: 'ent_harbor',
adjusts_entity_name: 'Harbor Café LLC',
posting_date: '2026-09-30',
memo: 'Eliminate the investment — Harbor Café LLC (2026-09)',
basis: "70% of Harbor Café LLC's capital.",
lines: [
{
element_id: 'el_cap',
account_name: 'Members Capital',
concept: 'rs-gaap:MembersEquity',
debit_amount: 700000,
credit_amount: 0,
},
{
element_id: 'el_inv',
account_name: 'Investment in Subsidiaries',
concept: 'rs-gaap:InvestmentInSubsidiaries',
debit_amount: 0,
credit_amount: 700000,
},
],
}

const PREVIEW = {
period: '2026-09',
period_start: '2026-09-01',
period_end: '2026-09-30',
consolidation_entity_id: 'ent_cons',
proposals: [PROPOSAL],
residuals: [],
reconciling_items: [],
notes: [],
up_to_date: false,
closed: false,
drafted_entry_ids: [],
}

describe('ConsolidationEntriesPanel', () => {
beforeEach(() => {
vi.clearAllMocks()
mockPreview.mockResolvedValue(PREVIEW)
})

it('lists what the month needs, by kind and member', async () => {
render(<ConsolidationEntriesPanel graphId="kg1" period="2026-09" />)

expect(
await screen.findByText('Investment elimination — Harbor Café LLC')
).toBeInTheDocument()
expect(screen.getByText('$7,000.00')).toBeInTheDocument()
expect(mockPreview).toHaveBeenCalledWith('kg1', '2026-09')
})

it('drafts the entries, then reloads the preview and the drafts', async () => {
const onDrafted = vi.fn()
mockPropose.mockResolvedValue({
period: '2026-09',
consolidation_entity_id: 'ent_cons',
entries: [{ id: 'je_1' }],
replaced: 0,
})
render(
<ConsolidationEntriesPanel
graphId="kg1"
period="2026-09"
onDrafted={onDrafted}
/>
)

mockPreview.mockResolvedValue({ ...PREVIEW, drafted_entry_ids: ['je_1'] })
fireEvent.click(await screen.findByText('Draft entries'))

// Once drafted, they are named, and a second run replaces them.
expect(
await screen.findByText(/1 draft written for this month/)
).toBeInTheDocument()
expect(screen.getByText('Draft again')).toBeInTheDocument()
expect(mockPropose).toHaveBeenCalledWith('kg1', '2026-09')
expect(onDrafted).toHaveBeenCalled()
expect(mockPreview).toHaveBeenCalledTimes(2)
})

it('shows a residual as withheld, never as an entry', async () => {
mockPreview.mockResolvedValue({
...PREVIEW,
proposals: [],
residuals: [
{
entity_id: 'ent_harbor',
entity_name: 'Harbor Café LLC',
amount: 50000,
explanation: 'The parent carries more than its share of capital.',
},
],
})
render(<ConsolidationEntriesPanel graphId="kg1" period="2026-09" />)

expect(
await screen.findByText('Withheld until explained')
).toBeInTheDocument()
expect(
screen.getByText(/The parent carries more than its share/)
).toBeInTheDocument()
// Nothing to draft: the button is there but holds back.
expect(screen.getByText('Draft entries').closest('button')).toBeDisabled()
})

it('says a closed month is as posted, with nothing to draft', async () => {
mockPreview.mockResolvedValue({
...PREVIEW,
proposals: [],
up_to_date: true,
closed: true,
})
render(<ConsolidationEntriesPanel graphId="kg1" period="2026-09" />)

expect(
await screen.findByText(/This month is closed here/)
).toBeInTheDocument()
expect(screen.queryByText('Draft entries')).not.toBeInTheDocument()
})

it('says when the entity already holds what the month needs', async () => {
mockPreview.mockResolvedValue({
...PREVIEW,
proposals: [],
up_to_date: true,
})
render(<ConsolidationEntriesPanel graphId="kg1" period="2026-09" />)

expect(await screen.findByText('Up to date')).toBeInTheDocument()
})

it('never shows the raw error envelope', async () => {
mockPreview.mockRejectedValue(
new Error(
'Preview consolidation entries failed: {"detail":"This group has no consolidation entity."}'
)
)
render(<ConsolidationEntriesPanel graphId="kg1" period="2026-09" />)

await waitFor(() =>
expect(
screen.getByText('This group has no consolidation entity.')
).toBeInTheDocument()
)
expect(screen.queryByText(/"detail"/)).not.toBeInTheDocument()
})
})
54 changes: 54 additions & 0 deletions src/app/(app)/ledger/close/__tests__/PeriodClosePanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ vi.mock('next/link', () => ({
default: ({ href, children }: any) => <a href={href}>{children}</a>,
}))

let mockScopedEntity: { entityRole: string } | null = null

vi.mock('@/lib/entity-scope', () => ({
useEntityScope: () => ({ entity: mockScopedEntity }),
}))

vi.mock('../components/ConsolidationEntriesPanel', () => ({
default: ({ period }: any) => (
<div data-testid="consolidation-entries">{period}</div>
),
}))

import PeriodClosePanel from '../components/PeriodClosePanel'

const CALENDAR = {
Expand Down Expand Up @@ -575,3 +587,45 @@ describe('PeriodClosePanel — close overrides', () => {
expect(screen.queryByText(/to QuickBooks/)).not.toBeInTheDocument()
})
})

describe('PeriodClosePanel — consolidating entries', () => {
beforeEach(() => {
vi.clearAllMocks()
mockGetFiscalCalendar.mockResolvedValue(CALENDAR)
mockGetPeriodCloseStatus.mockResolvedValue({
schedules: [],
totalDraft: 0,
totalPosted: 0,
})
mockListPeriodDrafts.mockResolvedValue({
draftCount: 0,
totalDebit: 0,
totalCredit: 0,
allBalanced: true,
qbPublishCount: 0,
localOnlyCount: 0,
drafts: [],
})
})

it("shows the engine's entries on the consolidation entity's close", async () => {
mockScopedEntity = { entityRole: 'consolidation' }
render(<PeriodClosePanel graphId="kg1" entityId="ent_cons" />)

expect(
await screen.findByTestId('consolidation-entries')
).toHaveTextContent('2026-05')
mockScopedEntity = null
})

it('leaves them off an operating company', async () => {
mockScopedEntity = { entityRole: 'operating' }
render(<PeriodClosePanel graphId="kg1" />)

await screen.findByText('Close Period')
expect(
screen.queryByTestId('consolidation-entries')
).not.toBeInTheDocument()
mockScopedEntity = null
})
})
Loading
Loading