@fayz-ai/plugin-financial 0.9.0 → 0.9.2
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/README.md +3 -3
- package/dist/components/PersonStatementWidget.d.ts +1 -1
- package/dist/components/PersonStatementWidget.d.ts.map +1 -1
- package/dist/{FinancialContext.d.ts → context.d.ts} +4 -2
- package/dist/context.d.ts.map +1 -0
- package/dist/data/registries.d.ts.map +1 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1426 -1426
- package/dist/index.js.map +1 -1
- package/dist/integrations/stripe/connectorDef.d.ts +14 -0
- package/dist/integrations/stripe/connectorDef.d.ts.map +1 -0
- package/dist/integrations/stripe/data.d.ts +43 -0
- package/dist/integrations/stripe/data.d.ts.map +1 -0
- package/dist/lib/message-events.d.ts +12 -0
- package/dist/lib/message-events.d.ts.map +1 -0
- package/dist/{quick-add.d.ts → lib/quick-add.d.ts} +1 -1
- package/dist/lib/quick-add.d.ts.map +1 -0
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/{FinancialPage.d.ts → views/FinancialPage.d.ts} +3 -3
- package/dist/views/FinancialPage.d.ts.map +1 -0
- package/dist/views/InvoiceDetailView.d.ts.map +1 -1
- package/dist/views/InvoiceFormView.d.ts.map +1 -1
- package/dist/views/InvoiceListView.d.ts.map +1 -1
- package/dist/views/PayablesView.d.ts +1 -1
- package/dist/views/PayablesView.d.ts.map +1 -1
- package/dist/views/ReceivablesView.d.ts +1 -1
- package/dist/views/ReceivablesView.d.ts.map +1 -1
- package/dist/views/StatementsView.d.ts.map +1 -1
- package/dist/views/dashboardWidgets.d.ts +1 -1
- package/dist/views/dashboardWidgets.d.ts.map +1 -1
- package/package.json +12 -14
- package/dist/FinancialContext.d.ts.map +0 -1
- package/dist/FinancialPage.d.ts.map +0 -1
- package/dist/contract.test.d.ts +0 -2
- package/dist/contract.test.d.ts.map +0 -1
- package/dist/quick-add.d.ts.map +0 -1
- package/dist/registries.d.ts.map +0 -1
- package/src/FinancialContext.tsx +0 -59
- package/src/FinancialPage.tsx +0 -413
- package/src/README.md +0 -155
- package/src/components/FinancialGeneralSettings.tsx +0 -39
- package/src/components/FinancialOnboarding.tsx +0 -87
- package/src/components/FinancialSettings.tsx +0 -31
- package/src/components/PaymentModal.tsx +0 -369
- package/src/components/PersonStatementWidget.tsx +0 -133
- package/src/components/TransferModal.tsx +0 -150
- package/src/contract.test.ts +0 -58
- package/src/data/capability.test.ts +0 -135
- package/src/data/index.ts +0 -3
- package/src/data/mock.ts +0 -842
- package/src/data/supabase.ts +0 -968
- package/src/data/tables.ts +0 -13
- package/src/data/types.ts +0 -86
- package/src/index.ts +0 -487
- package/src/lib/tenant.ts +0 -3
- package/src/locales/en.ts +0 -493
- package/src/locales/index.ts +0 -7
- package/src/locales/pt-BR.ts +0 -493
- package/src/migrations/000_plg_rename.sql +0 -30
- package/src/migrations/001_financial_base.sql +0 -109
- package/src/migrations/002_chart_of_accounts.sql +0 -28
- package/src/migrations/003_card_brands.sql +0 -12
- package/src/migrations/004_order_to_cash.sql +0 -178
- package/src/migrations/005_seed_defaults.sql +0 -38
- package/src/migrations/006_rls_policies.sql +0 -37
- package/src/migrations/006b_extract_fee_amount.sql +0 -20
- package/src/migrations/007_reconciliation.sql +0 -32
- package/src/migrations/007b_movement_payment_method_type.sql +0 -6
- package/src/migrations/008_split_payment_movements.sql +0 -29
- package/src/migrations/009_agent_rpcs.sql +0 -165
- package/src/migrations/010_invoice_internal.sql +0 -133
- package/src/migrations/011_nfe.sql +0 -58
- package/src/migrations/index.ts +0 -923
- package/src/quick-add.ts +0 -58
- package/src/registries.ts +0 -308
- package/src/store.ts +0 -329
- package/src/types.ts +0 -592
- package/src/views/CardsView.tsx +0 -213
- package/src/views/CashRegistersView.tsx +0 -597
- package/src/views/CommissionsView.tsx +0 -18
- package/src/views/InvoiceDetailView.tsx +0 -616
- package/src/views/InvoiceFormView.tsx +0 -945
- package/src/views/InvoiceListView.tsx +0 -323
- package/src/views/NotasFiscaisView.tsx +0 -209
- package/src/views/PayablesView.tsx +0 -48
- package/src/views/QuickTransactionForm.tsx +0 -548
- package/src/views/ReceivablesView.tsx +0 -48
- package/src/views/ReconciliationView.tsx +0 -211
- package/src/views/StatementsView.tsx +0 -335
- package/src/views/SummaryView.tsx +0 -203
- package/src/views/dashboardWidgets.tsx +0 -437
- package/src/views/navReferrer.ts +0 -24
- /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react'
|
|
2
|
-
import { createPortal } from 'react-dom'
|
|
3
|
-
import { X, ArrowRightLeft } from 'lucide-react'
|
|
4
|
-
import { useFinancialConfig, useFinancialStore } from '../FinancialContext'
|
|
5
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
6
|
-
import { CurrencyInput, DatePicker, Button } from '@fayz-ai/ui'
|
|
7
|
-
|
|
8
|
-
export function TransferModal({ onClose, onDone, defaultFromId }: {
|
|
9
|
-
onClose: () => void
|
|
10
|
-
onDone: () => void
|
|
11
|
-
defaultFromId?: string
|
|
12
|
-
}) {
|
|
13
|
-
const t = useTranslation()
|
|
14
|
-
const { currency } = useFinancialConfig()
|
|
15
|
-
const bankAccounts = useFinancialStore((s) => s.bankAccounts)
|
|
16
|
-
const createTransfer = useFinancialStore((s) => s.createTransfer)
|
|
17
|
-
const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts)
|
|
18
|
-
|
|
19
|
-
const [fromAccountId, setFromAccountId] = useState(defaultFromId ?? '')
|
|
20
|
-
const [toAccountId, setToAccountId] = useState('')
|
|
21
|
-
const [amount, setAmount] = useState(0)
|
|
22
|
-
const [date, setDate] = useState(new Date().toISOString().slice(0, 10))
|
|
23
|
-
const [notes, setNotes] = useState('')
|
|
24
|
-
const [saving, setSaving] = useState(false)
|
|
25
|
-
const [visible, setVisible] = useState(false)
|
|
26
|
-
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
fetchBankAccounts()
|
|
29
|
-
requestAnimationFrame(() => setVisible(true))
|
|
30
|
-
}, [])
|
|
31
|
-
|
|
32
|
-
function handleClose() {
|
|
33
|
-
setVisible(false)
|
|
34
|
-
setTimeout(onClose, 200)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const sameAccount = !!fromAccountId && fromAccountId === toAccountId
|
|
38
|
-
const canSubmit = !!fromAccountId && !!toAccountId && !sameAccount && amount > 0 && !saving
|
|
39
|
-
|
|
40
|
-
// Enter anywhere in the modal submits (unless typing in a textarea / Shift+Enter).
|
|
41
|
-
function handleKeyDown(e: React.KeyboardEvent) {
|
|
42
|
-
if (e.key !== 'Enter' || e.shiftKey) return
|
|
43
|
-
if ((e.target as HTMLElement).tagName === 'TEXTAREA') return
|
|
44
|
-
e.preventDefault()
|
|
45
|
-
if (canSubmit) void handleSubmit()
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function handleSubmit() {
|
|
49
|
-
if (!canSubmit) return
|
|
50
|
-
setSaving(true)
|
|
51
|
-
try {
|
|
52
|
-
await createTransfer({ fromAccountId, toAccountId, amount, date, notes: notes || undefined })
|
|
53
|
-
setVisible(false)
|
|
54
|
-
setTimeout(onDone, 200)
|
|
55
|
-
} catch {
|
|
56
|
-
// toast handled by store
|
|
57
|
-
} finally {
|
|
58
|
-
setSaving(false)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const selectClass = 'w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm'
|
|
63
|
-
|
|
64
|
-
return createPortal(
|
|
65
|
-
<div
|
|
66
|
-
className="fixed inset-0 z-50 flex items-center justify-center transition-colors duration-200"
|
|
67
|
-
style={{ backgroundColor: visible ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0)' }}
|
|
68
|
-
onClick={handleClose}
|
|
69
|
-
>
|
|
70
|
-
<div
|
|
71
|
-
className="w-full max-w-md rounded-modal border bg-card shadow-lg mx-4 transition-all duration-200 max-h-[90vh] overflow-y-auto"
|
|
72
|
-
style={{ opacity: visible ? 1 : 0, transform: visible ? 'scale(1) translateY(0)' : 'scale(0.95) translateY(8px)' }}
|
|
73
|
-
onClick={(e) => e.stopPropagation()}
|
|
74
|
-
onKeyDown={handleKeyDown}
|
|
75
|
-
>
|
|
76
|
-
{/* Header */}
|
|
77
|
-
<div className="flex items-center justify-between px-5 py-4 border-b">
|
|
78
|
-
<div className="flex items-center gap-2">
|
|
79
|
-
<ArrowRightLeft className="h-4 w-4 text-warning" />
|
|
80
|
-
<h3 className="text-base font-semibold">{t('financial.transfer.title')}</h3>
|
|
81
|
-
</div>
|
|
82
|
-
<button onClick={handleClose} className="p-1 text-muted-foreground hover:text-foreground transition-colors">
|
|
83
|
-
<X className="h-4 w-4" />
|
|
84
|
-
</button>
|
|
85
|
-
</div>
|
|
86
|
-
|
|
87
|
-
<div className="px-5 py-4 space-y-4">
|
|
88
|
-
<div className="grid grid-cols-2 gap-3">
|
|
89
|
-
<div>
|
|
90
|
-
<label className="text-xs font-medium text-muted-foreground">{t('financial.transfer.from')}</label>
|
|
91
|
-
<select value={fromAccountId} onChange={(e) => setFromAccountId(e.target.value)} className={selectClass}>
|
|
92
|
-
<option value="">{t('financial.transfer.selectAccount')}</option>
|
|
93
|
-
{bankAccounts.map((a) => <option key={a.id} value={a.id}>{a.name}</option>)}
|
|
94
|
-
</select>
|
|
95
|
-
</div>
|
|
96
|
-
<div>
|
|
97
|
-
<label className="text-xs font-medium text-muted-foreground">{t('financial.transfer.to')}</label>
|
|
98
|
-
<select value={toAccountId} onChange={(e) => setToAccountId(e.target.value)} className={selectClass}>
|
|
99
|
-
<option value="">{t('financial.transfer.selectAccount')}</option>
|
|
100
|
-
{bankAccounts.map((a) => <option key={a.id} value={a.id}>{a.name}</option>)}
|
|
101
|
-
</select>
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
|
-
|
|
105
|
-
{sameAccount && (
|
|
106
|
-
<p className="text-xs text-destructive">{t('financial.transfer.sameAccount')}</p>
|
|
107
|
-
)}
|
|
108
|
-
|
|
109
|
-
<div className="grid grid-cols-2 gap-3">
|
|
110
|
-
<CurrencyInput
|
|
111
|
-
label={t('financial.transfer.amount')}
|
|
112
|
-
value={amount}
|
|
113
|
-
onChange={setAmount}
|
|
114
|
-
symbol={currency.symbol}
|
|
115
|
-
locale={currency.locale}
|
|
116
|
-
currencyCode={currency.code}
|
|
117
|
-
/>
|
|
118
|
-
<div>
|
|
119
|
-
<label className="text-xs font-medium text-muted-foreground">{t('financial.transfer.date')}</label>
|
|
120
|
-
<DatePicker value={date} onChange={setDate} className="mt-1" />
|
|
121
|
-
</div>
|
|
122
|
-
</div>
|
|
123
|
-
|
|
124
|
-
<div>
|
|
125
|
-
<label className="text-xs font-medium text-muted-foreground">{t('financial.transfer.notes')}</label>
|
|
126
|
-
<input
|
|
127
|
-
type="text"
|
|
128
|
-
value={notes}
|
|
129
|
-
onChange={(e) => setNotes(e.target.value)}
|
|
130
|
-
placeholder={t('financial.transfer.optionalNotes')}
|
|
131
|
-
className="w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
132
|
-
/>
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
135
|
-
|
|
136
|
-
{/* Actions */}
|
|
137
|
-
<div className="flex justify-end gap-2 px-5 py-4 border-t">
|
|
138
|
-
<Button variant="outline" size="sm" onClick={handleClose}>
|
|
139
|
-
{t('financial.transfer.cancel')}
|
|
140
|
-
</Button>
|
|
141
|
-
<Button variant="default" size="sm" onClick={handleSubmit} disabled={!canSubmit}>
|
|
142
|
-
<ArrowRightLeft className="h-3.5 w-3.5" />
|
|
143
|
-
{saving ? t('financial.transfer.processing') : t('financial.transfer.submit')}
|
|
144
|
-
</Button>
|
|
145
|
-
</div>
|
|
146
|
-
</div>
|
|
147
|
-
</div>,
|
|
148
|
-
document.body,
|
|
149
|
-
)
|
|
150
|
-
}
|
package/src/contract.test.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
// Integrity test for the shared plugin-contract assertions (@fayz-ai/core/testing).
|
|
2
|
-
// Exercises the gate's rules with inline manifests (no UI import) so the tool
|
|
3
|
-
// itself is proven: a conformant manifest passes; each malformed shape throws.
|
|
4
|
-
// Real plugins call assertPluginManifestContract(theirManifest) in an env where
|
|
5
|
-
// their UI bundle loads (app-level / jsdom). See docs/PLUGIN-PATTERNS.md → integrity.
|
|
6
|
-
import { describe, it, expect } from 'vitest'
|
|
7
|
-
import { assertPluginManifestContract, PluginContractError } from '@fayz-ai/core/testing'
|
|
8
|
-
import type { PluginManifest } from '@fayz-ai/core'
|
|
9
|
-
|
|
10
|
-
const Dummy = () => null
|
|
11
|
-
|
|
12
|
-
function manifest(overrides: Partial<PluginManifest> = {}): PluginManifest {
|
|
13
|
-
return {
|
|
14
|
-
id: 'demo', name: 'Demo', icon: 'Box', version: '1.0.0',
|
|
15
|
-
navigation: [{ section: 'main', label: 'Demo', route: '/demo', icon: 'Box' } as any],
|
|
16
|
-
routes: [{ path: '/demo', component: Dummy } as any],
|
|
17
|
-
...overrides,
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe('assertPluginManifestContract', () => {
|
|
22
|
-
it('passes a conformant page plugin', () => {
|
|
23
|
-
expect(() => assertPluginManifestContract(manifest())).not.toThrow()
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('passes a settings-only plugin (nav/routes empty, one settings tab)', () => {
|
|
27
|
-
expect(() => assertPluginManifestContract(manifest({
|
|
28
|
-
navigation: [], routes: [],
|
|
29
|
-
settings: [{ id: 'demo', label: 'Demo', icon: 'Box', component: Dummy } as any],
|
|
30
|
-
}))).not.toThrow()
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('rejects a missing id', () => {
|
|
34
|
-
expect(() => assertPluginManifestContract(manifest({ id: '' }))).toThrow(PluginContractError)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('rejects a nav entry pointing at an undeclared route', () => {
|
|
38
|
-
expect(() => assertPluginManifestContract(manifest({
|
|
39
|
-
navigation: [{ section: 'main', label: 'X', route: '/nowhere', icon: 'Box' } as any],
|
|
40
|
-
}))).toThrow(/no matching route/)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('rejects duplicate route paths', () => {
|
|
44
|
-
expect(() => assertPluginManifestContract(manifest({
|
|
45
|
-
routes: [{ path: '/demo', component: Dummy } as any, { path: '/demo', component: Dummy } as any],
|
|
46
|
-
}))).toThrow(/duplicate route/)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('rejects a settings tab without a component', () => {
|
|
50
|
-
expect(() => assertPluginManifestContract(manifest({
|
|
51
|
-
settings: [{ id: 'demo', label: 'Demo' } as any],
|
|
52
|
-
}))).toThrow(/component/)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('rejects a plugin that contributes nothing mountable', () => {
|
|
56
|
-
expect(() => assertPluginManifestContract(manifest({ navigation: [], routes: [], settings: [] }))).toThrow(/nothing/)
|
|
57
|
-
})
|
|
58
|
-
})
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
// Capability + contract test for plugin-financial (Plugin Capability Contract).
|
|
2
|
-
// Proves the data slice end-to-end against the mock provider — including the
|
|
3
|
-
// ⚖️ reconciliation (conciliação) feature: import bank lines → list unreconciled
|
|
4
|
-
// → suggest a match → reconcile → assert it leaves the pending set. Rows are
|
|
5
|
-
// created manually (mock provider), which is exactly how a capability is proven
|
|
6
|
-
// before/without a live backend. Mirrors plugins/plugin-tasks capability.test.ts.
|
|
7
|
-
import { describe, it, expect } from 'vitest'
|
|
8
|
-
import { createMockFinancialProvider } from './mock'
|
|
9
|
-
|
|
10
|
-
describe('plugin-financial · invoice/movement slice (mock provider)', () => {
|
|
11
|
-
it('persists an invoice and its installment movements', async () => {
|
|
12
|
-
const provider = createMockFinancialProvider()
|
|
13
|
-
const invoice = await provider.createInvoice({
|
|
14
|
-
direction: 'credit',
|
|
15
|
-
invoiceDate: '2026-06-01',
|
|
16
|
-
contactName: 'Acme',
|
|
17
|
-
items: [{ itemKind: 'other', description: 'Consulting', quantity: 1, unitPrice: 300 }],
|
|
18
|
-
installments: [
|
|
19
|
-
{ direction: 'credit', movementKind: 'bill', amount: 300, dueDate: '2026-06-10', installmentNumber: 1 },
|
|
20
|
-
],
|
|
21
|
-
})
|
|
22
|
-
expect(invoice.id).toBeTruthy()
|
|
23
|
-
expect(invoice.totalAmount).toBe(300)
|
|
24
|
-
|
|
25
|
-
const movs = await provider.getMovements({ direction: 'credit' })
|
|
26
|
-
expect(movs.data.some((m) => m.invoiceId === invoice.id && m.amount === 300)).toBe(true)
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('filters invoices by item account and cost center assignments', async () => {
|
|
30
|
-
const provider = createMockFinancialProvider()
|
|
31
|
-
const target = await provider.createInvoice({
|
|
32
|
-
direction: 'debit',
|
|
33
|
-
invoiceDate: '2026-06-01',
|
|
34
|
-
contactName: 'Supplier A',
|
|
35
|
-
items: [{
|
|
36
|
-
itemKind: 'other',
|
|
37
|
-
description: 'Supplies',
|
|
38
|
-
quantity: 1,
|
|
39
|
-
unitPrice: 120,
|
|
40
|
-
accountId: 'coa-expenses',
|
|
41
|
-
costCenterId: 'cc-salon',
|
|
42
|
-
}],
|
|
43
|
-
installments: [
|
|
44
|
-
{ direction: 'debit', movementKind: 'bill', amount: 120, dueDate: '2026-06-10', installmentNumber: 1 },
|
|
45
|
-
],
|
|
46
|
-
})
|
|
47
|
-
await provider.createInvoice({
|
|
48
|
-
direction: 'debit',
|
|
49
|
-
invoiceDate: '2026-06-01',
|
|
50
|
-
contactName: 'Supplier B',
|
|
51
|
-
items: [{
|
|
52
|
-
itemKind: 'other',
|
|
53
|
-
description: 'Marketing',
|
|
54
|
-
quantity: 1,
|
|
55
|
-
unitPrice: 80,
|
|
56
|
-
accountId: 'coa-marketing',
|
|
57
|
-
costCenterId: 'cc-marketing',
|
|
58
|
-
}],
|
|
59
|
-
installments: [
|
|
60
|
-
{ direction: 'debit', movementKind: 'bill', amount: 80, dueDate: '2026-06-10', installmentNumber: 1 },
|
|
61
|
-
],
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
const byAccount = await provider.getInvoices({ direction: 'debit', accountId: 'coa-expenses' })
|
|
65
|
-
expect(byAccount.data.map((invoice) => invoice.id)).toEqual([target.id])
|
|
66
|
-
|
|
67
|
-
const byCostCenter = await provider.getInvoices({ direction: 'debit', costCenterId: 'cc-salon' })
|
|
68
|
-
expect(byCostCenter.data.map((invoice) => invoice.id)).toEqual([target.id])
|
|
69
|
-
|
|
70
|
-
const byBoth = await provider.getInvoices({ direction: 'debit', accountId: 'coa-expenses', costCenterId: 'cc-marketing' })
|
|
71
|
-
expect(byBoth.data).toHaveLength(0)
|
|
72
|
-
})
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
describe('plugin-financial · ⚖️ reconciliation capability (mock provider)', () => {
|
|
76
|
-
it('imports bank lines idempotently', async () => {
|
|
77
|
-
const provider = createMockFinancialProvider()
|
|
78
|
-
const lines = [
|
|
79
|
-
{ externalId: 'tx-1', externalSource: 'plugbank', direction: 'credit' as const, amount: 150, date: '2026-06-10', description: 'PIX recebido' },
|
|
80
|
-
{ externalId: 'tx-2', externalSource: 'plugbank', direction: 'debit' as const, amount: 80, date: '2026-06-11', description: 'Tarifa' },
|
|
81
|
-
]
|
|
82
|
-
const first = await provider.importBankTransactions!({ lines })
|
|
83
|
-
expect(first.imported).toBe(2)
|
|
84
|
-
expect(first.duplicates).toBe(0)
|
|
85
|
-
|
|
86
|
-
// Re-importing the same lines must not duplicate (idempotency by external id).
|
|
87
|
-
const second = await provider.importBankTransactions!({ lines })
|
|
88
|
-
expect(second.imported).toBe(0)
|
|
89
|
-
expect(second.duplicates).toBe(2)
|
|
90
|
-
|
|
91
|
-
const pending = await provider.getUnreconciled!()
|
|
92
|
-
expect(pending).toHaveLength(2)
|
|
93
|
-
expect(pending.every((m) => m.externalSource === 'plugbank' && !m.reconciledAt)).toBe(true)
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('suggests and matches an internal movement, leaving the pending set', async () => {
|
|
97
|
-
const provider = createMockFinancialProvider()
|
|
98
|
-
// An internal receivable the bank line should reconcile against.
|
|
99
|
-
await provider.createInvoice({
|
|
100
|
-
direction: 'credit',
|
|
101
|
-
invoiceDate: '2026-06-08',
|
|
102
|
-
contactName: 'Acme',
|
|
103
|
-
items: [{ itemKind: 'other', description: 'Service', quantity: 1, unitPrice: 150 }],
|
|
104
|
-
installments: [{ direction: 'credit', movementKind: 'bill', amount: 150, dueDate: '2026-06-10', installmentNumber: 1 }],
|
|
105
|
-
})
|
|
106
|
-
// The imported bank line (same amount, same direction, near date).
|
|
107
|
-
await provider.importBankTransactions!({
|
|
108
|
-
lines: [{ externalId: 'tx-1', externalSource: 'plugbank', direction: 'credit', amount: 150, date: '2026-06-10', description: 'PIX Acme' }],
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
const [bankLine] = await provider.getUnreconciled!()
|
|
112
|
-
expect(bankLine).toBeTruthy()
|
|
113
|
-
|
|
114
|
-
const candidates = await provider.suggestReconciliation!(bankLine.id)
|
|
115
|
-
expect(candidates.length).toBeGreaterThan(0)
|
|
116
|
-
expect(candidates[0].movement.amount).toBe(150)
|
|
117
|
-
expect(candidates[0].score).toBeGreaterThan(0.5)
|
|
118
|
-
|
|
119
|
-
await provider.reconcileMovement!({ bankMovementId: bankLine.id, matchedMovementId: candidates[0].movement.id })
|
|
120
|
-
|
|
121
|
-
// The line is gone from the pending set and both sides are marked reconciled.
|
|
122
|
-
const stillPending = await provider.getUnreconciled!()
|
|
123
|
-
expect(stillPending.some((m) => m.id === bankLine.id)).toBe(false)
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
it('accepts a bank line standalone when there is no internal counterpart', async () => {
|
|
127
|
-
const provider = createMockFinancialProvider()
|
|
128
|
-
await provider.importBankTransactions!({
|
|
129
|
-
lines: [{ externalId: 'tx-9', externalSource: 'plugbank', direction: 'debit', amount: 42, date: '2026-06-12', description: 'Taxa avulsa' }],
|
|
130
|
-
})
|
|
131
|
-
const [line] = await provider.getUnreconciled!()
|
|
132
|
-
await provider.reconcileMovement!({ bankMovementId: line.id }) // no match → accept standalone
|
|
133
|
-
expect(await provider.getUnreconciled!()).toHaveLength(0)
|
|
134
|
-
})
|
|
135
|
-
})
|
package/src/data/index.ts
DELETED