@asteby/metacore-runtime-react 47.1.0 → 49.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/dist/action-modal-dispatcher.d.ts +17 -3
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +80 -13
- package/dist/action-priority.d.ts +34 -0
- package/dist/action-priority.d.ts.map +1 -0
- package/dist/action-priority.js +60 -0
- package/dist/business/catalog-product-search.d.ts +32 -0
- package/dist/business/catalog-product-search.d.ts.map +1 -0
- package/dist/business/catalog-product-search.js +71 -0
- package/dist/business/catalog.d.ts.map +1 -1
- package/dist/business/catalog.js +2 -0
- package/dist/business/document-editor-model.d.ts +119 -0
- package/dist/business/document-editor-model.d.ts.map +1 -0
- package/dist/business/document-editor-model.js +317 -0
- package/dist/business/document-editor-panels.d.ts +64 -0
- package/dist/business/document-editor-panels.d.ts.map +1 -0
- package/dist/business/document-editor-panels.js +58 -0
- package/dist/business/document-editor.d.ts +27 -0
- package/dist/business/document-editor.d.ts.map +1 -0
- package/dist/business/document-editor.js +493 -0
- package/dist/business/document-lines.d.ts.map +1 -1
- package/dist/business/document-lines.js +5 -1
- package/dist/business/index.d.ts +5 -0
- package/dist/business/index.d.ts.map +1 -1
- package/dist/business/index.js +5 -0
- package/dist/business/line-items.d.ts +16 -1
- package/dist/business/line-items.d.ts.map +1 -1
- package/dist/business/line-items.js +14 -2
- package/dist/business/payment-allocator.d.ts +19 -0
- package/dist/business/payment-allocator.d.ts.map +1 -0
- package/dist/business/payment-allocator.js +31 -0
- package/dist/business/product-search.d.ts +4 -0
- package/dist/business/product-search.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +14 -7
- package/dist/document/document-page.d.ts.map +1 -1
- package/dist/document/document-page.js +8 -1
- package/dist/document/evaluate.d.ts.map +1 -1
- package/dist/document/evaluate.js +17 -1
- package/dist/document/index.d.ts +1 -0
- package/dist/document/index.d.ts.map +1 -1
- package/dist/document/index.js +1 -0
- package/dist/document/secondary-bar.d.ts +20 -0
- package/dist/document/secondary-bar.d.ts.map +1 -0
- package/dist/document/secondary-bar.js +44 -0
- package/dist/document/types.d.ts +8 -0
- package/dist/document/types.d.ts.map +1 -1
- package/dist/document-form-dialog.d.ts +31 -2
- package/dist/document-form-dialog.d.ts.map +1 -1
- package/dist/document-form-dialog.js +46 -11
- package/dist/dynamic-columns-helpers.d.ts +9 -0
- package/dist/dynamic-columns-helpers.d.ts.map +1 -1
- package/dist/dynamic-columns-helpers.js +19 -0
- package/dist/dynamic-columns.d.ts +9 -13
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +27 -21
- package/dist/dynamic-crud-page.d.ts +18 -0
- package/dist/dynamic-crud-page.d.ts.map +1 -1
- package/dist/dynamic-crud-page.js +15 -6
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +15 -3
- package/dist/dynamic-form.d.ts +16 -1
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +78 -20
- package/dist/dynamic-kanban.d.ts +9 -3
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +31 -12
- package/dist/dynamic-relation-helpers.d.ts.map +1 -1
- package/dist/dynamic-relation-helpers.js +8 -0
- package/dist/dynamic-relation.js +1 -1
- package/dist/dynamic-row-actions.d.ts +8 -0
- package/dist/dynamic-row-actions.d.ts.map +1 -1
- package/dist/dynamic-row-actions.js +12 -2
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +64 -63
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -2
- package/dist/installed-addons-context.d.ts +15 -0
- package/dist/installed-addons-context.d.ts.map +1 -0
- package/dist/installed-addons-context.js +39 -0
- package/dist/org-runtime-context.d.ts +8 -0
- package/dist/org-runtime-context.d.ts.map +1 -1
- package/dist/org-runtime-context.js +8 -0
- package/dist/org-runtime-provider.d.ts +3 -1
- package/dist/org-runtime-provider.d.ts.map +1 -1
- package/dist/org-runtime-provider.js +3 -3
- package/dist/primitives/aging.d.ts +34 -0
- package/dist/primitives/aging.d.ts.map +1 -0
- package/dist/primitives/aging.js +48 -0
- package/dist/primitives/allocation.d.ts +42 -0
- package/dist/primitives/allocation.d.ts.map +1 -0
- package/dist/primitives/allocation.js +59 -0
- package/dist/primitives/approvals.d.ts +71 -0
- package/dist/primitives/approvals.d.ts.map +1 -0
- package/dist/primitives/approvals.js +48 -0
- package/dist/primitives/contributions.d.ts +93 -0
- package/dist/primitives/contributions.d.ts.map +1 -0
- package/dist/primitives/contributions.js +163 -0
- package/dist/primitives/document-kinds.d.ts +25 -0
- package/dist/primitives/document-kinds.d.ts.map +1 -0
- package/dist/primitives/document-kinds.js +26 -0
- package/dist/primitives/index.d.ts +8 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +23 -0
- package/dist/primitives/panels.d.ts +68 -0
- package/dist/primitives/panels.d.ts.map +1 -0
- package/dist/primitives/panels.js +3 -0
- package/dist/primitives/use-contributions.d.ts +15 -0
- package/dist/primitives/use-contributions.d.ts.map +1 -0
- package/dist/primitives/use-contributions.js +30 -0
- package/dist/record-actions.d.ts +21 -0
- package/dist/record-actions.d.ts.map +1 -0
- package/dist/record-actions.js +76 -0
- package/dist/record-prefill-bar.d.ts +11 -0
- package/dist/record-prefill-bar.d.ts.map +1 -0
- package/dist/record-prefill-bar.js +33 -0
- package/dist/row-actions-menu.d.ts +32 -0
- package/dist/row-actions-menu.d.ts.map +1 -0
- package/dist/row-actions-menu.js +59 -0
- package/dist/slot.d.ts +4 -21
- package/dist/slot.d.ts.map +1 -1
- package/dist/slot.js +14 -36
- package/dist/types.d.ts +134 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/catalog-product-search.test.ts +61 -0
- package/src/__tests__/document-editor-model.test.ts +221 -0
- package/src/__tests__/document-editor.test.tsx +340 -0
- package/src/__tests__/document-form-dialog.test.tsx +158 -3
- package/src/__tests__/dynamic-form-select-edit.test.tsx +155 -0
- package/src/__tests__/dynamic-kanban.test.tsx +24 -3
- package/src/__tests__/dynamic-record-extension-label.test.tsx +88 -0
- package/src/__tests__/installed-addons-context.test.tsx +50 -0
- package/src/__tests__/line-item-totals.test.ts +28 -0
- package/src/__tests__/prefill-scalar-from-record.test.ts +52 -1
- package/src/__tests__/primitives-federated.test.tsx +156 -0
- package/src/__tests__/primitives.test.ts +224 -0
- package/src/__tests__/record-prefill-bar.test.tsx +50 -0
- package/src/__tests__/row-action-link-navigation.test.ts +17 -0
- package/src/__tests__/secondary-actions.test.tsx +261 -0
- package/src/action-modal-dispatcher.tsx +97 -12
- package/src/action-priority.ts +82 -0
- package/src/business/business.test.ts +1 -1
- package/src/business/catalog-product-search.ts +105 -0
- package/src/business/catalog.ts +2 -0
- package/src/business/document-editor-model.ts +367 -0
- package/src/business/document-editor-panels.tsx +164 -0
- package/src/business/document-editor.tsx +781 -0
- package/src/business/document-lines.ts +5 -1
- package/src/business/index.ts +5 -0
- package/src/business/line-items.ts +31 -2
- package/src/business/payment-allocator.tsx +110 -0
- package/src/business/product-search.ts +4 -0
- package/src/dialogs/dynamic-record.tsx +29 -6
- package/src/document/document-page.tsx +15 -0
- package/src/document/evaluate.ts +17 -1
- package/src/document/index.ts +1 -0
- package/src/document/secondary-bar.tsx +92 -0
- package/src/document/types.ts +8 -0
- package/src/document-form-dialog.tsx +96 -12
- package/src/dynamic-columns-helpers.ts +22 -0
- package/src/dynamic-columns.tsx +35 -62
- package/src/dynamic-crud-page.tsx +33 -5
- package/src/dynamic-form-schema.ts +15 -2
- package/src/dynamic-form.tsx +117 -39
- package/src/dynamic-kanban.tsx +34 -7
- package/src/dynamic-relation-helpers.ts +9 -0
- package/src/dynamic-relation.tsx +7 -2
- package/src/dynamic-row-actions.tsx +12 -2
- package/src/dynamic-table.tsx +3 -0
- package/src/index.ts +46 -1
- package/src/installed-addons-context.tsx +58 -0
- package/src/org-runtime-context.ts +10 -0
- package/src/org-runtime-provider.tsx +7 -2
- package/src/primitives/aging.ts +76 -0
- package/src/primitives/allocation.ts +104 -0
- package/src/primitives/approvals.ts +103 -0
- package/src/primitives/contributions.ts +255 -0
- package/src/primitives/document-kinds.ts +62 -0
- package/src/primitives/index.ts +23 -0
- package/src/primitives/panels.ts +59 -0
- package/src/primitives/use-contributions.ts +42 -0
- package/src/record-actions.ts +101 -0
- package/src/record-prefill-bar.tsx +62 -0
- package/src/row-action-stage-gate.test.ts +33 -0
- package/src/row-actions-menu.tsx +159 -0
- package/src/slot.tsx +18 -53
- package/src/types.ts +129 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
//
|
|
3
|
+
// Primitivos federados en React: el dispatcher pinta un modal de
|
|
4
|
+
// registerFederatedModal sin esperar al host, usa el formulario genérico de
|
|
5
|
+
// inmediato si el addon del slug no está instalado, y useDocumentContributions
|
|
6
|
+
// re-pinta al registrar/desmontar un remote.
|
|
7
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
8
|
+
import { act, cleanup, render, screen } from '@testing-library/react'
|
|
9
|
+
|
|
10
|
+
const USE_TRANSLATION = {
|
|
11
|
+
t: (_k: string, opts?: { defaultValue?: string }) => opts?.defaultValue ?? _k,
|
|
12
|
+
i18n: { language: 'es' },
|
|
13
|
+
}
|
|
14
|
+
vi.mock('react-i18next', () => ({ useTranslation: () => USE_TRANSLATION }))
|
|
15
|
+
vi.mock('@tanstack/react-router', () => ({ useNavigate: () => () => {} }))
|
|
16
|
+
|
|
17
|
+
import { ActionModalDispatcher } from '../action-modal-dispatcher'
|
|
18
|
+
import { setFederatedActionLoader } from '../federated-action-loader'
|
|
19
|
+
import { ApiProvider, type ApiClient } from '../api-context'
|
|
20
|
+
import { InstalledAddonsProvider } from '../installed-addons-context'
|
|
21
|
+
import {
|
|
22
|
+
__resetContributions,
|
|
23
|
+
registerDocumentContribution,
|
|
24
|
+
registerFederatedModal,
|
|
25
|
+
type DocumentEditorContext,
|
|
26
|
+
} from '../primitives/contributions'
|
|
27
|
+
import { useDocumentContributions } from '../primitives/use-contributions'
|
|
28
|
+
import type { ActionMetadata, ActionModalProps } from '@asteby/metacore-sdk'
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
cleanup()
|
|
32
|
+
vi.restoreAllMocks()
|
|
33
|
+
setFederatedActionLoader(null)
|
|
34
|
+
__resetContributions()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const noopApi = {
|
|
38
|
+
get: vi.fn().mockResolvedValue({ data: {} }),
|
|
39
|
+
post: vi.fn().mockResolvedValue({ data: { success: true } }),
|
|
40
|
+
delete: vi.fn().mockResolvedValue({ data: { success: true } }),
|
|
41
|
+
} as unknown as ApiClient
|
|
42
|
+
|
|
43
|
+
const importAction: ActionMetadata = {
|
|
44
|
+
key: 'import_cfdi',
|
|
45
|
+
label: 'Importar CFDI',
|
|
46
|
+
modal: 'fiscal_mexico.import_cfdi',
|
|
47
|
+
confirm: true,
|
|
48
|
+
confirmMessage: '¿Importar?',
|
|
49
|
+
executable: true,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ImportModal({ action }: ActionModalProps) {
|
|
53
|
+
return <div data-testid="import-modal">{action.modal}</div>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderDispatcher(addons?: string[]) {
|
|
57
|
+
const tree = (
|
|
58
|
+
<ActionModalDispatcher
|
|
59
|
+
open
|
|
60
|
+
onOpenChange={() => {}}
|
|
61
|
+
action={importAction}
|
|
62
|
+
model="Invoice"
|
|
63
|
+
record={{ id: '1' } as any}
|
|
64
|
+
endpoint="/data/Invoice"
|
|
65
|
+
onSuccess={() => {}}
|
|
66
|
+
/>
|
|
67
|
+
)
|
|
68
|
+
return render(
|
|
69
|
+
<ApiProvider client={noopApi}>
|
|
70
|
+
{addons ? <InstalledAddonsProvider addons={addons}>{tree}</InstalledAddonsProvider> : tree}
|
|
71
|
+
</ApiProvider>,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('ActionModalDispatcher + registerFederatedModal', () => {
|
|
76
|
+
it('pinta el modal registrado (lazy) sin pedir el remote al host', async () => {
|
|
77
|
+
const loader = vi.fn()
|
|
78
|
+
setFederatedActionLoader(loader)
|
|
79
|
+
registerFederatedModal({ key: 'fiscal_mexico.import_cfdi', addon: 'fiscal_mexico', load: async () => ({ default: ImportModal }) })
|
|
80
|
+
renderDispatcher(['fiscal_mexico'])
|
|
81
|
+
expect((await screen.findByTestId('import-modal')).textContent).toBe('fiscal_mexico.import_cfdi')
|
|
82
|
+
expect(loader).not.toHaveBeenCalled()
|
|
83
|
+
expect(screen.queryByText('¿Importar?')).toBeNull()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('addon del slug no instalado → formulario genérico de inmediato, sin esperar ni pedir el remote', () => {
|
|
87
|
+
const loader = vi.fn()
|
|
88
|
+
setFederatedActionLoader(loader)
|
|
89
|
+
registerFederatedModal({ key: 'fiscal_mexico.import_cfdi', addon: 'fiscal_mexico', load: async () => ({ default: ImportModal }) })
|
|
90
|
+
renderDispatcher(['customers'])
|
|
91
|
+
expect(screen.getByText('¿Importar?')).toBeTruthy()
|
|
92
|
+
expect(screen.queryByTestId('federated-action-loading')).toBeNull()
|
|
93
|
+
expect(loader).not.toHaveBeenCalled()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('addon instalado pero modal aún sin registrar → espera al remote (no cae al genérico)', async () => {
|
|
97
|
+
renderDispatcher(['fiscal_mexico'])
|
|
98
|
+
expect(screen.getByTestId('federated-action-loading')).toBeTruthy()
|
|
99
|
+
expect(screen.queryByText('¿Importar?')).toBeNull()
|
|
100
|
+
act(() => {
|
|
101
|
+
registerFederatedModal({ key: 'fiscal_mexico.import_cfdi', addon: 'fiscal_mexico', load: async () => ({ default: ImportModal }) })
|
|
102
|
+
})
|
|
103
|
+
expect(await screen.findByTestId('import-modal')).toBeTruthy()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('sin InstalledAddonsProvider se conserva la espera de siempre', () => {
|
|
107
|
+
renderDispatcher()
|
|
108
|
+
expect(screen.getByTestId('federated-action-loading')).toBeTruthy()
|
|
109
|
+
expect(screen.queryByText('¿Importar?')).toBeNull()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('useDocumentContributions', () => {
|
|
114
|
+
const ctx: DocumentEditorContext = { kind: 'invoice', model: 'customers.Invoice', values: {}, mode: 'create' }
|
|
115
|
+
|
|
116
|
+
function Region() {
|
|
117
|
+
const items = useDocumentContributions(ctx, 'header.fields')
|
|
118
|
+
return (
|
|
119
|
+
<div data-testid="region">
|
|
120
|
+
{items.map(({ id, component: C }) => (
|
|
121
|
+
<C key={id} ctx={ctx} setValue={() => {}} issues={{ report: () => {} }} />
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
it('re-pinta al registrar y al desmontar el remote; filtra por addon instalado', async () => {
|
|
128
|
+
render(
|
|
129
|
+
<InstalledAddonsProvider addons={['fiscal_mexico']}>
|
|
130
|
+
<Region />
|
|
131
|
+
</InstalledAddonsProvider>,
|
|
132
|
+
)
|
|
133
|
+
expect(screen.getByTestId('region').textContent).toBe('')
|
|
134
|
+
let off = () => {}
|
|
135
|
+
act(() => {
|
|
136
|
+
off = registerDocumentContribution({
|
|
137
|
+
id: 'fiscal_mexico.header.cfdi',
|
|
138
|
+
kinds: ['invoice'],
|
|
139
|
+
region: 'header.fields',
|
|
140
|
+
requiresAddon: 'fiscal_mexico',
|
|
141
|
+
load: async () => ({ default: () => <span>Uso del CFDI</span> }),
|
|
142
|
+
})
|
|
143
|
+
registerDocumentContribution({
|
|
144
|
+
id: 'inventory.header.warehouse',
|
|
145
|
+
kinds: '*',
|
|
146
|
+
region: 'header.fields',
|
|
147
|
+
requiresAddon: 'inventory',
|
|
148
|
+
component: () => <span>Almacén</span>,
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
expect(await screen.findByText('Uso del CFDI')).toBeTruthy()
|
|
152
|
+
expect(screen.queryByText('Almacén')).toBeNull()
|
|
153
|
+
act(() => off())
|
|
154
|
+
expect(screen.queryByText('Uso del CFDI')).toBeNull()
|
|
155
|
+
})
|
|
156
|
+
})
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { allocatePayment, validateAllocation, type OpenDocument } from '../primitives/allocation'
|
|
3
|
+
import { bucketFor, computeAging, daysOverdue } from '../primitives/aging'
|
|
4
|
+
import {
|
|
5
|
+
__resetContributions,
|
|
6
|
+
contributionsVersion,
|
|
7
|
+
federatedModalComponent,
|
|
8
|
+
registerDocumentContribution,
|
|
9
|
+
registerFederatedModal,
|
|
10
|
+
resolveContributions,
|
|
11
|
+
resolveFederatedModal,
|
|
12
|
+
slotIdFor,
|
|
13
|
+
subscribeContributions,
|
|
14
|
+
} from '../primitives/contributions'
|
|
15
|
+
import { approvalsClient, groupApprovals, parseJSONField, registerApprovalCategory, type ApprovalRequestDTO } from '../primitives/approvals'
|
|
16
|
+
import { CORE_DOCUMENT_KINDS, getDocumentKind, listDocumentKinds, registerDocumentKind } from '../primitives/document-kinds'
|
|
17
|
+
import { blockingChecks } from '../primitives/panels'
|
|
18
|
+
import { installedPredicate } from '../primitives/use-contributions'
|
|
19
|
+
import { slotRegistry } from '../slot'
|
|
20
|
+
import type { ApiClient } from '../api-context'
|
|
21
|
+
|
|
22
|
+
// Datos del mockup complemento-pago.html (Transportes del Bajío).
|
|
23
|
+
const docs: OpenDocument[] = [
|
|
24
|
+
{ id: 'a1060', number: 'A-1060', issued_at: '2026-09-10', due_at: '2026-10-10', total: 42270.4, balance: 42270.4, currency: 'MXN', payment_method: 'PPD' },
|
|
25
|
+
{ id: 'a1051', number: 'A-1051', issued_at: '2026-08-15', due_at: '2026-09-14', total: 18560, balance: 8500, currency: 'MXN', payment_method: 'PPD', installments_paid: 1 },
|
|
26
|
+
{ id: 'a1066', number: 'A-1066', issued_at: '2026-09-28', due_at: '2026-10-28', total: 23260, balance: 23260, currency: 'MXN', payment_method: 'PPD' },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
describe('allocatePayment', () => {
|
|
30
|
+
it('aplica primero lo más vencido y calcula parcialidad e insoluto', () => {
|
|
31
|
+
const r = allocatePayment(30000, docs)
|
|
32
|
+
expect(r.allocations).toEqual([
|
|
33
|
+
{ document_id: 'a1051', amount: 8500, balance_before: 8500, balance_after: 0, installment: 2 },
|
|
34
|
+
{ document_id: 'a1060', amount: 21500, balance_before: 42270.4, balance_after: 20770.4, installment: 1 },
|
|
35
|
+
])
|
|
36
|
+
expect(r.unapplied).toBe(0)
|
|
37
|
+
})
|
|
38
|
+
it('manual respeta montos y deja sin aplicar el resto', () => {
|
|
39
|
+
const r = allocatePayment(10000, docs, 'manual', { a1066: 4000 })
|
|
40
|
+
expect(r.applied).toBe(4000)
|
|
41
|
+
expect(r.unapplied).toBe(6000)
|
|
42
|
+
expect(validateAllocation(10000, 'MXN', docs, r.allocations).map(i => i.code)).toEqual(['unapplied'])
|
|
43
|
+
expect(validateAllocation(10000, 'MXN', docs, r.allocations, { allowUnapplied: true })[0].severity).toBe('warning')
|
|
44
|
+
})
|
|
45
|
+
it('detecta pago mayor al saldo y moneda distinta', () => {
|
|
46
|
+
const codes = validateAllocation(9000, 'USD', docs, [{ document_id: 'a1051', amount: 9000, balance_before: 8500, balance_after: -500, installment: 2 }]).map(i => i.code)
|
|
47
|
+
expect(codes).toContain('over_balance')
|
|
48
|
+
expect(codes).toContain('currency_mismatch')
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('computeAging', () => {
|
|
53
|
+
it('tramos y vencido por cliente al corte', () => {
|
|
54
|
+
expect(daysOverdue('2026-09-14', '2026-10-05')).toBe(21)
|
|
55
|
+
const { rows, totals, total } = computeAging(docs.map(d => ({ party_id: 'tba', due_at: d.due_at ?? null, issued_at: d.issued_at, balance: d.balance })), '2026-10-05')
|
|
56
|
+
expect(totals.current).toBe(65530.4)
|
|
57
|
+
expect(totals.d1_30).toBe(8500)
|
|
58
|
+
expect(total).toBe(74030.4)
|
|
59
|
+
expect(rows[0].oldest_overdue_days).toBe(21)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('contribuciones federadas', () => {
|
|
64
|
+
afterEach(() => __resetContributions())
|
|
65
|
+
const ctx = { kind: 'invoice', model: 'customers.Invoice', values: {}, mode: 'create' as const }
|
|
66
|
+
it('se ocultan si el addon no está instalado y se ordenan por prioridad', () => {
|
|
67
|
+
const Comp = () => null
|
|
68
|
+
registerDocumentContribution({ id: 'fiscal_mexico.header.cfdi', kinds: ['invoice', 'credit_note'], region: 'header.fields', requiresAddon: 'fiscal_mexico', priority: 10, component: Comp })
|
|
69
|
+
registerDocumentContribution({ id: 'inventory.header.warehouse', kinds: '*', region: 'header.fields', requiresAddon: 'inventory', component: Comp })
|
|
70
|
+
expect(resolveContributions(ctx, 'header.fields', a => a === 'inventory').map(c => c.id)).toEqual(['inventory.header.warehouse'])
|
|
71
|
+
expect(resolveContributions(ctx, 'header.fields', () => true).map(c => c.id)).toEqual(['fiscal_mexico.header.cfdi', 'inventory.header.warehouse'])
|
|
72
|
+
})
|
|
73
|
+
it('el disposer la quita (desinstalación en caliente)', () => {
|
|
74
|
+
const off = registerDocumentContribution({ id: 'x', kinds: '*', region: 'side.panels', component: () => null })
|
|
75
|
+
off()
|
|
76
|
+
expect(resolveContributions(ctx, 'side.panels', () => true)).toEqual([])
|
|
77
|
+
})
|
|
78
|
+
it('modal federado sin addon → null (el dispatcher usa el formulario genérico)', () => {
|
|
79
|
+
registerFederatedModal({ key: 'fiscal_mexico.import_cfdi', addon: 'fiscal_mexico', load: async () => ({ default: () => null }) })
|
|
80
|
+
expect(resolveFederatedModal('fiscal_mexico.import_cfdi', () => false)).toBeNull()
|
|
81
|
+
expect(resolveFederatedModal('fiscal_mexico.import_cfdi', () => true)?.addon).toBe('fiscal_mexico')
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('groupApprovals', () => {
|
|
86
|
+
it('agrupa por categoría registrada por cada addon', () => {
|
|
87
|
+
const off = registerApprovalCategory({ key: 'cfdi', label: 'Cancelación CFDI', match: r => r.action_key === 'cancel_fiscal' })
|
|
88
|
+
const base = { kind: 'action', status: 'pending' } as ApprovalRequestDTO
|
|
89
|
+
const g = groupApprovals([{ ...base, id: '1', action_key: 'cancel_fiscal' }, { ...base, id: '2', action_key: 'approve_adjustment' }])
|
|
90
|
+
expect(Object.keys(g).sort()).toEqual(['cfdi', 'other'])
|
|
91
|
+
off()
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('allocatePayment (estrategias y bordes)', () => {
|
|
96
|
+
it('oldest_issued_first ordena por emisión', () => {
|
|
97
|
+
const r = allocatePayment(50000, docs, 'oldest_issued_first')
|
|
98
|
+
expect(r.allocations.map(a => a.document_id)).toEqual(['a1051', 'a1060'])
|
|
99
|
+
})
|
|
100
|
+
it('monto negativo o cero no aplica nada y validateAllocation lo marca', () => {
|
|
101
|
+
expect(allocatePayment(-5, docs)).toEqual({ allocations: [], applied: 0, unapplied: 0 })
|
|
102
|
+
expect(validateAllocation(0, 'MXN', docs, []).map(i => i.code)).toContain('non_positive')
|
|
103
|
+
})
|
|
104
|
+
it('pago mayor a todos los saldos deja el excedente sin aplicar', () => {
|
|
105
|
+
const r = allocatePayment(100000, docs)
|
|
106
|
+
expect(r.applied).toBe(74030.4)
|
|
107
|
+
expect(r.unapplied).toBe(25969.6)
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('bucketFor', () => {
|
|
112
|
+
it('ubica los bordes de cada tramo', () => {
|
|
113
|
+
expect(bucketFor(-3)).toBe('current')
|
|
114
|
+
expect(bucketFor(0)).toBe('current')
|
|
115
|
+
expect(bucketFor(30)).toBe('d1_30')
|
|
116
|
+
expect(bucketFor(31)).toBe('d31_60')
|
|
117
|
+
expect(bucketFor(400)).toBe('d90_plus')
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('contribuciones federadas (registro)', () => {
|
|
122
|
+
afterEach(() => __resetContributions())
|
|
123
|
+
const ctx = { kind: 'invoice', model: 'customers.Invoice', values: { total: 10 }, mode: 'edit' as const }
|
|
124
|
+
|
|
125
|
+
it('publica en slotRegistry por kind/region y el disposer la retira', () => {
|
|
126
|
+
const off = registerDocumentContribution({ id: 'a.body.x', kinds: ['invoice', 'quote'], region: 'body.sections', component: () => null })
|
|
127
|
+
expect(slotRegistry.get(slotIdFor('invoice', 'body.sections'))).toHaveLength(1)
|
|
128
|
+
expect(slotRegistry.get(slotIdFor('quote', 'body.sections'))).toHaveLength(1)
|
|
129
|
+
off()
|
|
130
|
+
off() // idempotente
|
|
131
|
+
expect(slotRegistry.get(slotIdFor('invoice', 'body.sections'))).toHaveLength(0)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('re-registrar el mismo id reemplaza; el disposer viejo no borra el nuevo', () => {
|
|
135
|
+
const offOld = registerDocumentContribution({ id: 'a.side.p', kinds: '*', region: 'side.panels', priority: 1, component: () => null })
|
|
136
|
+
registerDocumentContribution({ id: 'a.side.p', kinds: '*', region: 'side.panels', priority: 5, component: () => null })
|
|
137
|
+
expect(slotRegistry.get(slotIdFor('*', 'side.panels'))).toHaveLength(1)
|
|
138
|
+
offOld()
|
|
139
|
+
expect(resolveContributions(ctx, 'side.panels', () => true).map(c => c.priority)).toEqual([5])
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('filtra por `when` y por kind; con `load` siempre expone un componente', () => {
|
|
143
|
+
registerDocumentContribution({ id: 'a.h.only_edit', kinds: ['invoice'], region: 'header.fields', when: c => c.mode === 'edit', load: async () => ({ default: () => null }) })
|
|
144
|
+
registerDocumentContribution({ id: 'a.h.quote', kinds: ['quote'], region: 'header.fields', component: () => null })
|
|
145
|
+
const [only] = resolveContributions(ctx, 'header.fields', () => true)
|
|
146
|
+
expect(only.id).toBe('a.h.only_edit')
|
|
147
|
+
expect(typeof only.component).toBe('function')
|
|
148
|
+
expect(resolveContributions({ ...ctx, mode: 'create' }, 'header.fields', () => true)).toEqual([])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('exige component o load', () => {
|
|
152
|
+
expect(() => registerDocumentContribution({ id: 'bad', kinds: '*', region: 'side.panels' })).toThrow(/component o load/)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('notifica a los suscriptores en register/dispose', () => {
|
|
156
|
+
const spy = vi.fn()
|
|
157
|
+
const unsub = subscribeContributions(spy)
|
|
158
|
+
const v0 = contributionsVersion()
|
|
159
|
+
const off = registerDocumentContribution({ id: 'n', kinds: '*', region: 'side.panels', component: () => null })
|
|
160
|
+
off()
|
|
161
|
+
expect(spy).toHaveBeenCalledTimes(2)
|
|
162
|
+
expect(contributionsVersion()).toBe(v0 + 2)
|
|
163
|
+
unsub()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('modal federado: no registrado → null; disposer lo retira; componente memoizado', () => {
|
|
167
|
+
expect(resolveFederatedModal('returns.settle_method', () => true)).toBeNull()
|
|
168
|
+
const m = { key: 'returns.settle_method', addon: 'returns', load: async () => ({ default: () => null }) }
|
|
169
|
+
const off = registerFederatedModal(m)
|
|
170
|
+
expect(resolveFederatedModal('returns.settle_method', () => true)).toBe(m)
|
|
171
|
+
expect(federatedModalComponent(m)).toBe(federatedModalComponent(m))
|
|
172
|
+
off()
|
|
173
|
+
expect(resolveFederatedModal('returns.settle_method', () => true)).toBeNull()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('installedPredicate: sin provider = instalado; con provider consulta el set', () => {
|
|
177
|
+
expect(installedPredicate(null)('lo_que_sea')).toBe(true)
|
|
178
|
+
const p = installedPredicate({ addons: new Set(['customers']), capabilities: new Set() })
|
|
179
|
+
expect(p('customers')).toBe(true)
|
|
180
|
+
expect(p('fiscal_mexico')).toBe(false)
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe('tipos de documento', () => {
|
|
185
|
+
it('registerDocumentKind con disposer y filtrado por addon instalado', () => {
|
|
186
|
+
const offs = CORE_DOCUMENT_KINDS.map(registerDocumentKind)
|
|
187
|
+
expect(getDocumentKind('invoice')?.model).toBe('customers.Invoice')
|
|
188
|
+
expect(listDocumentKinds(a => a === 'purchases').map(k => k.key)).toEqual(['purchase_order', 'supplier_invoice', 'supplier_payment'])
|
|
189
|
+
offs.forEach(off => off())
|
|
190
|
+
expect(getDocumentKind('invoice')).toBeUndefined()
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
describe('paneles', () => {
|
|
195
|
+
it('blockingChecks: solo errores sin política de autorización', () => {
|
|
196
|
+
const checks = [
|
|
197
|
+
{ key: 'a', severity: 'error' as const, message: 'Falta RFC' },
|
|
198
|
+
{ key: 'b', severity: 'error' as const, message: 'Sin crédito', approval_policy: 'credit_override' },
|
|
199
|
+
{ key: 'c', severity: 'warning' as const, message: 'Sin correo' },
|
|
200
|
+
]
|
|
201
|
+
expect(blockingChecks(checks).map(c => c.key)).toEqual(['a'])
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
describe('approvalsClient', () => {
|
|
206
|
+
it('arma la consulta y lee lista y conteo', async () => {
|
|
207
|
+
const get = vi.fn(async (url: string) =>
|
|
208
|
+
url.startsWith('/approvals/count') ? { data: { data: { count: 3 } } } : { data: { data: [{ id: '1' }], meta: { total: 1 } } },
|
|
209
|
+
)
|
|
210
|
+
const post = vi.fn(async () => ({ data: {} }))
|
|
211
|
+
const c = approvalsClient({ get, post } as unknown as ApiClient)
|
|
212
|
+
expect(await c.list({ status: 'pending', for_me: true, model: undefined })).toEqual({ items: [{ id: '1' }], total: 1 })
|
|
213
|
+
expect(get).toHaveBeenCalledWith('/approvals?status=pending&for_me=true')
|
|
214
|
+
expect(await c.count()).toBe(3)
|
|
215
|
+
await c.reject('9', 'No procede')
|
|
216
|
+
expect(post).toHaveBeenCalledWith('/approvals/9/reject', { reason: 'No procede' })
|
|
217
|
+
})
|
|
218
|
+
it('parseJSONField tolera string, objeto y basura', () => {
|
|
219
|
+
expect(parseJSONField('{"a":1}')).toEqual({ a: 1 })
|
|
220
|
+
expect(parseJSONField({ a: 2 })).toEqual({ a: 2 })
|
|
221
|
+
expect(parseJSONField('no-json')).toBeNull()
|
|
222
|
+
expect(parseJSONField('')).toBeNull()
|
|
223
|
+
})
|
|
224
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
//
|
|
3
|
+
// RecordPrefillBar: pinta los ayudantes registrados para el modelo/modo, los
|
|
4
|
+
// filtra por permiso y solo deja pasar al formulario las claves que declara.
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
|
7
|
+
import { registerRecordPrefill, unregisterRecordPrefillsByOwner, type RecordPrefillProps } from '@asteby/metacore-sdk'
|
|
8
|
+
import { RecordPrefillBar } from '../record-prefill-bar'
|
|
9
|
+
import { PermissionsProvider } from '../permissions-context'
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
cleanup()
|
|
13
|
+
unregisterRecordPrefillsByOwner('fiscal_mexico')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
function CsfButton({ applyValues }: RecordPrefillProps) {
|
|
17
|
+
return (
|
|
18
|
+
<button type="button" onClick={() => applyValues({ tax_id: 'XAXX010101000', organization_id: 'hack' })}>
|
|
19
|
+
Cargar constancia
|
|
20
|
+
</button>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('RecordPrefillBar', () => {
|
|
25
|
+
it('aplica solo claves permitidas y aparece cuando el remote registra tarde', () => {
|
|
26
|
+
const onApply = vi.fn()
|
|
27
|
+
render(
|
|
28
|
+
<RecordPrefillBar model="customers.Customer" mode="create" values={{}} allowedKeys={new Set(['tax_id'])} onApply={onApply} />,
|
|
29
|
+
)
|
|
30
|
+
expect(screen.queryByText('Cargar constancia')).toBeNull()
|
|
31
|
+
act(() => {
|
|
32
|
+
registerRecordPrefill({ id: 'fiscal_mexico.csf', models: ['Customer'], component: CsfButton }, 'fiscal_mexico')
|
|
33
|
+
})
|
|
34
|
+
fireEvent.click(screen.getByText('Cargar constancia'))
|
|
35
|
+
expect(onApply).toHaveBeenCalledWith({ tax_id: 'XAXX010101000' })
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('se oculta sin el permiso declarado', () => {
|
|
39
|
+
registerRecordPrefill(
|
|
40
|
+
{ id: 'fiscal_mexico.csf', models: ['Customer'], component: CsfButton, permission: 'customers.csf' },
|
|
41
|
+
'fiscal_mexico',
|
|
42
|
+
)
|
|
43
|
+
render(
|
|
44
|
+
<PermissionsProvider permissions={[]} isAdmin={false}>
|
|
45
|
+
<RecordPrefillBar model="Customer" mode="create" values={{}} allowedKeys={new Set(['tax_id'])} onApply={() => {}} />
|
|
46
|
+
</PermissionsProvider>,
|
|
47
|
+
)
|
|
48
|
+
expect(screen.queryByText('Cargar constancia')).toBeNull()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { linkNavigation } from '../dynamic-row-actions'
|
|
3
|
+
|
|
4
|
+
// «Crear desde»: el botón de la fila del origen es una acción link con query
|
|
5
|
+
// (`/m/invoices?create=invoice&from=sales_order&from_id={id}`). En `to` el
|
|
6
|
+
// router la tomaba como parte del path y `$model` quedaba "invoices?create=…".
|
|
7
|
+
describe('linkNavigation', () => {
|
|
8
|
+
it('separa la query como search', () => {
|
|
9
|
+
expect(linkNavigation('/m/invoices?create=invoice&from=sales_order&from_id=so-1')).toEqual({
|
|
10
|
+
to: '/m/invoices',
|
|
11
|
+
search: { create: 'invoice', from: 'sales_order', from_id: 'so-1' },
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
it('sin query deja el destino igual', () => {
|
|
15
|
+
expect(linkNavigation('/m/quotes/abc')).toEqual({ to: '/m/quotes/abc' })
|
|
16
|
+
})
|
|
17
|
+
})
|